<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

  <title><![CDATA[编程学习笔记]]></title>
  <link href="https://huanglei.work/atom.xml" rel="self"/>
  <link href="https://huanglei.work/"/>
  <updated>2025-12-17T16:20:31+08:00</updated>
  <id>https://huanglei.work/</id>
  <author>
    <name><![CDATA[]]></name>
    
  </author>
  <generator uri="http://www.mweb.im/">MWeb</generator>
  
  <entry>
    <title type="html"><![CDATA[01-File⽂件和⽬录]]></title>
    <link href="https://huanglei.work/17419191416826.html"/>
    <updated>2025-03-14T10:25:41+08:00</updated>
    <id>https://huanglei.work/17419191416826.html</id>
    <content type="html"><![CDATA[
<h4><a id="%E7%AC%AC1%E9%9B%86%E8%AE%A1%E7%AE%97%E6%9C%BA%E6%A0%B8%E5%BF%83%E5%9F%BA%E7%A1%80%E4%B9%8B%E6%96%87%E4%BB%B6%E5%92%8C%E8%B7%AF%E5%BE%84%E6%A0%B8%E5%BF%83%E7%9F%A5%E8%AF%86%E7%82%B9" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>第1集 计算机核心基础之文件和路径核心知识点</h4>
<p><strong>简介：计算机核心基础之文件和路径核心知识点</strong></p>
<ul>
<li>
<p>什么是计算机文件（File）</p>
<ul>
<li>以计算机硬盘为载体存储在计算机上的信息集合</li>
<li>可以是文本、图片、视频、程序等，文件一般有拓展名，表示文件的类型</li>
</ul>
</li>
<li>
<p>文件目录 (Directory)</p>
<ul>
<li>就是我们一般称呼的文件夹，为了便于对文件进行存取和管理</li>
<li>文件目录是由文件目录项组成的，分为一级目录、二级目录和多级目录。</li>
<li>多级目录结构也称为树形结构，在多级目录结构中，每一个磁盘有一个根目录</li>
<li>在根目录中可以包含若干子目录和文件，在子目录中不但可以包含文件，而且还可以包含下一级子目录</li>
<li>掌握了基础后的可以去了解<strong>Linux文件系统</strong>，涉及到的东西更多，推荐看小滴课堂《Linux操作系统》课程</li>
</ul>
</li>
<li>
<p>区分两个斜杠</p>
<ul>
<li>斜杠：&quot;/&quot; 与 反斜杠：&quot; \ &quot;</li>
<li>反斜杠（\）是一个特殊的字符，被称为转义字符，用来转义后面一个字符。</li>
<li>转义后的字符表示一个不可见的字符或特殊含义的字符，如 \n 则表示换行，\ ? 问号，\ &quot; 则表示双引号，\ ' 表示一个单引号等</li>
<li>在Java中的字母前面加上反斜线&quot;&quot;来表示常见的那些不能显示的ASCII字符，称之为转义字符</li>
<li>例子：需要输出双引号的一段话 <code>String title = &quot; \&quot;这个是带双引号的标题\&quot; &quot;</code>;</li>
<li>转义的详情参看 <a href="https://baike.baidu.com/item/%E8%BD%AC%E4%B9%89%E5%AD%97%E7%AC%A6/86397?fr=aladdin">https://baike.baidu.com/item/%E8%BD%AC%E4%B9%89%E5%AD%97%E7%AC%A6/86397?fr=aladdin</a></li>
</ul>
</li>
<li>
<p>相对路径</p>
<ul>
<li>相对某个基准目录或者文件的路径 ./ 表示当前路径;   ../../  表示上级目录</li>
</ul>
</li>
<li>
<p>绝对路径</p>
<ul>
<li>存储在硬盘上的真正路径</li>
</ul>
</li>
<li>
<p>window路径分割符</p>
<ul>
<li>\ 表示windows系统文件目录分割符</li>
</ul>
<img src="https://image.huanglei.work/mweb/2025/3/14/2eb8cecc-e492-47a4-8f17-776f4ab547ae.png" alt="image-20240518181751298" style="zoom:50%;" />
<ul>
<li>
<p>如果是Java代码在window下写某个文件的话需要 下面的方式</p>
<pre><code class="language-plain_text">D:\\soft\\xdclass.txt
因为单斜杠是用来转义的
</code></pre>
</li>
<li>
<p><strong>注意：开发和运行程序的路径不要有中文或者特殊字符，不然容易出现兼容性问题</strong></p>
</li>
</ul>
</li>
<li>
<p>Linux和Mac路径分割符（开发人员推荐使用）</p>
<ul>
<li>
<p>/ 表示 Linux或者Mac的路径分隔符</p>
</li>
<li>
<p>如果是Java代码在Linux或者Mac下写某个文件的话需要 下面的方式</p>
<pre><code class="language-plain_text">/usr/soft/xdcalss.txt
</code></pre>
</li>
</ul>
</li>
<li>
<p>常见的文件</p>
<ul>
<li>文本 txt</li>
<li>图片 jpg、png、jpeg</li>
<li>excel、csv等</li>
<li>tar.gzip 、zip</li>
</ul>
</li>
</ul>
<h4><a id="%E7%AC%AC2%E9%9B%86-java%E6%A0%B8%E5%BF%83%E7%9F%A5%E8%AF%86%E4%B9%8Bfile%E7%B1%BB%E5%92%8Capi%E8%AE%B2%E8%A7%A3" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>第2集 java核心知识之File类和API讲解</h4>
<p><strong>简介：介绍File类和相关API讲解</strong></p>
<ul>
<li>
<p>简单了解IO，即Input/Output</p>
<ul>
<li>把内存的中数据存储到持久化设备到上的动作称为输出，Output 操作</li>
<li>把持久化设备的数据读取到内存中的动作称为输入，Input操作</li>
<li>一般把输入和输出的动作称为IO操作，IO也分网络IO，文件IO</li>
</ul>
</li>
<li>
<p>什么是File类</p>
<ul>
<li>Java中的<code>File</code>类用于文件和目录路径名的抽象表示形式。</li>
<li>程序代码和文件目录的关系：主要就是对文件和目录进行增删改查，俗称CRUD</li>
<li>File类的包名是java.io，实现了Serializable, Comparable两大接口以便于其对象可序列化和比较</li>
<li>技巧：File.separator 目录分隔符，在不同的系统下不一样, windows和 mac /Linux</li>
<li><code>File</code>类能新建、删除、重命名文件和目录，但<code>File</code>类不能用于读取或写入文件内容。
<ul>
<li>如果需要读取文件内容，请使用<code>FileReader</code>、<code>BufferedReader</code>等类；</li>
<li>如果需要写入文件内容，请使用<code>FileWriter</code>、<code>BufferedWriter</code>或<code>PrintWriter</code>等类。</li>
</ul>
</li>
</ul>
</li>
<li>
<p><strong>File类的常用构造方法</strong></p>
<ul>
<li>
<p><code>File(String pathname)</code>：通过给定的路径名字符串（可以是相对路径或绝对路径）构造<code>File</code>对象。</p>
</li>
<li>
<p><code>File(String parent, String child)</code>：从父路径名字符串和子路径名字符串构造<code>File</code>对象。</p>
</li>
</ul>
</li>
<li>
<p><strong>File类的常用方法</strong></p>
<ul>
<li>
<p>获取文件/目录信息</p>
<ul>
<li>
<p><code>getName()</code>：返回由路径名表示的文件或目录的名称。</p>
</li>
<li>
<p><code>getPath()</code>：将此路径名转换为一个路径名字符串。</p>
</li>
<li>
<p><code>getAbsolutePath()</code>：返回路径名的绝对路径名字符串。</p>
</li>
<li>
<p><code>isDirectory()</code>：测试路径名表示的文件是否是一个目录。</p>
</li>
<li>
<p><code>isFile()</code>：测试路径名表示的文件是否是一个普通文件。</p>
</li>
<li>
<p><code>exists()</code>：测试路径名表示的文件或目录是否存在。</p>
</li>
<li>
<p><code>length()</code>：返回路径名表示的文件的长度（以字节为单位）。</p>
</li>
</ul>
</li>
<li>
<p>创建文件/目录</p>
<ul>
<li>
<p><code>createNewFile()</code>：当且仅当具有指定名称的文件尚不存在时，创建一个新的空文件。</p>
</li>
<li>
<p><code>mkdir()</code>：创建路径名指定的目录。</p>
</li>
<li>
<p><code>mkdirs()</code>：创建路径名指定的目录，包括所有必需但不存在的父目录。</p>
</li>
</ul>
</li>
<li>
<p>删除文件/目录</p>
<ul>
<li><code>delete()</code>：删除路径名表示的文件或目录。</li>
</ul>
</li>
<li>
<p>列出目录内容</p>
<ul>
<li>
<p><code>list()</code>：返回一个字符串数组，这些字符串表示此抽象路径名表示的目录中的文件和目录。</p>
</li>
<li>
<p><code>listFiles()</code>：返回一个<code>File</code>数组，这些文件和目录路径名表示此抽象路径名表示的目录中的文件和目录。</p>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>代码案例</p>
<pre><code class="language-Java">public class FileApiDemo {
    public static void main(String[] args) {
    		//Mac或者Linux
        String dir = &quot;/Users/xdclass/Desktop/coding/xdclass-account/src/chapter10&quot;;
        //windowds系统的路径
        //String dir = &quot;C:\\Desktop\\coding\\xdclass-account\\src\\chapter10&quot;;

        String name = &quot;xdclass.txt&quot;;
        //File file = new File(dir, name);
        File file = new File(dir);


        //文件的 查询和判断
        System.out.println(File.separator);
        System.out.println(&quot;基本路径 getPath()= &quot; + file.getPath());
        System.out.println(&quot;文件名 getName()= &quot; + file.getName());
        System.out.println(&quot;绝对路径 getAbsolutePath = &quot; + file.getAbsolutePath());
        System.out.println(&quot;父路径名 getParent() = &quot; + file.getParent());
        System.out.println(&quot;是否是绝对路径 isAbsolute() = &quot; + file.isAbsolute());
        System.out.println(&quot;是否是一个目录 isDirectory() = &quot; + file.isDirectory());
        System.out.println(&quot;是否是一个文件 isFile() = &quot; + file.isFile());
        System.out.println(&quot;文件或目录是否存在 exists() = &quot; + file.exists());


        System.out.println(&quot;目录中的文件和目录的名称所组成字符串数组 list() &quot;);
        String[] arr = file.list();
        for (String temp : arr) {
            System.out.println(temp);
        }


        //创建指定的目录
        File mkdirFile = new File(dir + &quot;/testdir&quot;);
        mkdirFile.mkdir();


        //创建多个层级的目录
        File mkdirsFile = new File(dir + &quot;/testdirs/test/dd&quot;);
        mkdirsFile.mkdirs();


       //创建一个新的文件
        File newFile = new File(dir + &quot;/testdir/newfile1.txt&quot;);
        try {
            newFile.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        }
        //删除文件
        newFile.delete();
    }
}
</code></pre>
</li>
<li>
<p>注意</p>
<ul>
<li>File的构造函数只是创建一个File实例，即使目录错误也不出报错，实例仅仅是一个路径名的抽象表示形式。</li>
<li><code>File</code>类的方法大多数都涉及到磁盘I/O操作，可能会抛出<code>IOException</code>或其子类。</li>
<li>在调用这些方法时，使用try-catch语句块来处理这些异常。</li>
<li>在使用<code>File</code>类时，应该避免硬编码文件路径，最好使用相对路径或配置文件来指定文件路径，以提高程序的灵活性和可移植性</li>
</ul>
</li>
</ul>
<h4><a id="%E7%AC%AC3%E9%9B%86%E8%AF%BE%E7%A8%8B%E4%BD%9C%E4%B8%9A%E6%96%87%E4%BB%B6%E5%A4%B9%E5%92%8C%E6%96%87%E4%BB%B6%E6%93%8D%E4%BD%9C%E4%BD%9C%E4%B8%9A%E5%B8%83%E7%BD%AE%E5%92%8C%E7%BB%83%E4%B9%A0" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>第3集 课程作业-文件夹和文件操作作业布置和练习</h4>
<p><strong>简介：课程作业-文件夹和文件批量作业布置和练习</strong></p>
<ul>
<li>
<p>作业需求</p>
<ul>
<li>封装一个方法，传入一个路径，则在此路径下创建test文件夹</li>
<li>然后在 test目录下创建10个文件夹，名称是1~10</li>
<li>然后再各个文件夹里面创建一个txt文本文件, 名称也是1~10命名，重复调用此方法的话结果一样。</li>
<li>效果</li>
</ul>
<pre><code class="language-plain_text">test/
      1/
        1.txt
      2/
        2.txt
      3/
      ...
</code></pre>
</li>
<li>
<p>答案代码</p>
<pre><code class="language-Java">public static void main(String[] args) throws IOException {

        //String path =&quot;C:\\Users\\79466\\Desktop;
        String path =&quot;/Users/xdclass/Desktop&quot;;

        createDir(path);
    }

    public static void createDir(String path) throws IOException {

		String root = path + File.separator + &quot;test&quot;;
        File rootDir = new File(root);
        if (!rootDir.exists()) {
            rootDir.mkdirs();
        }

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

            String dirPath = root + File.separator + (i + 1);
            File dirFile = new File(dirPath);
            if (!dirFile.exists()) {
                dirFile.mkdir();
                String txtPath = dirPath + File.separator + (i + 1) + &quot;.txt&quot;;
                File txtFile = new File(txtPath);
                if (!txtFile.exists()) {
                    txtFile.createNewFile();
                }
            }
        }

    }
</code></pre>
</li>
</ul>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[01-Java基础常见面试题总结(上)]]></title>
    <link href="https://huanglei.work/17420865517398.html"/>
    <updated>2025-03-16T08:55:51+08:00</updated>
    <id>https://huanglei.work/17420865517398.html</id>
    <content type="html"><![CDATA[
<ul>
<li><a href="#%E5%9F%BA%E7%A1%80%E6%A6%82%E5%BF%B5%E4%B8%8E%E5%B8%B8%E8%AF%86">基础概念与常识</a>
<ul>
<li><a href="#java%E8%AF%AD%E8%A8%80%E6%9C%89%E5%93%AA%E4%BA%9B%E7%89%B9%E7%82%B9">Java 语言有哪些特点?</a></li>
<li><a href="#java-se-vs-java-ee">Java SE vs Java EE</a></li>
<li><a href="#jvm-vs-jdk-vs-jre">JVM vs JDK vs JRE</a>
<ul>
<li><a href="#jvm">JVM</a></li>
<li><a href="#jdk%E5%92%8C-jre">JDK 和 JRE</a></li>
</ul>
</li>
<li><a href="#%E4%BB%80%E4%B9%88%E6%98%AF%E5%AD%97%E8%8A%82%E7%A0%81%E9%87%87%E7%94%A8%E5%AD%97%E8%8A%82%E7%A0%81%E7%9A%84%E5%A5%BD%E5%A4%84%E6%98%AF%E4%BB%80%E4%B9%88">什么是字节码?采用字节码的好处是什么?</a></li>
<li><a href="#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%AF%B4java%E8%AF%AD%E8%A8%80%E2%80%9C%E7%BC%96%E8%AF%91%E4%B8%8E%E8%A7%A3%E9%87%8A%E5%B9%B6%E5%AD%98%E2%80%9D%EF%BC%9F">为什么说 Java 语言“编译与解释并存”？</a></li>
<li><a href="#aot%E6%9C%89%E4%BB%80%E4%B9%88%E4%BC%98%E7%82%B9%EF%BC%9F%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E5%85%A8%E9%83%A8%E4%BD%BF%E7%94%A8-aot%E5%91%A2%EF%BC%9F">AOT 有什么优点？为什么不全部使用 AOT 呢？</a></li>
<li><a href="#oracle-jdk-vs-openjdk">Oracle JDK vs OpenJDK</a></li>
<li><a href="#java%E5%92%8C-c%E7%9A%84%E5%8C%BA%E5%88%AB">Java 和 C++ 的区别?</a></li>
</ul>
</li>
<li><a href="#%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95">基本语法</a>
<ul>
<li><a href="#%E6%B3%A8%E9%87%8A%E6%9C%89%E5%93%AA%E5%87%A0%E7%A7%8D%E5%BD%A2%E5%BC%8F%EF%BC%9F">注释有哪几种形式？</a></li>
<li><a href="#%E6%A0%87%E8%AF%86%E7%AC%A6%E5%92%8C%E5%85%B3%E9%94%AE%E5%AD%97%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F">标识符和关键字的区别是什么？</a></li>
<li><a href="#java%E8%AF%AD%E8%A8%80%E5%85%B3%E9%94%AE%E5%AD%97%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F">Java 语言关键字有哪些？</a></li>
<li><a href="#%E8%87%AA%E5%A2%9E%E8%87%AA%E5%87%8F%E8%BF%90%E7%AE%97%E7%AC%A6">自增自减运算符</a></li>
<li><a href="#%E7%A7%BB%E4%BD%8D%E8%BF%90%E7%AE%97%E7%AC%A6">移位运算符</a></li>
<li><a href="#continue%E3%80%81break%E5%92%8C-return%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F">continue、break 和 return 的区别是什么？</a></li>
</ul>
</li>
<li><a href="#%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B">基本数据类型</a>
<ul>
<li><a href="#java%E4%B8%AD%E7%9A%84%E5%87%A0%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E4%BA%86%E8%A7%A3%E4%B9%88%EF%BC%9F">Java 中的几种基本数据类型了解么？</a></li>
<li><a href="#%E5%9F%BA%E6%9C%AC%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%8C%85%E8%A3%85%E7%B1%BB%E5%9E%8B%E7%9A%84%E5%8C%BA%E5%88%AB%EF%BC%9F">基本类型和包装类型的区别？</a></li>
<li><a href="#%E5%8C%85%E8%A3%85%E7%B1%BB%E5%9E%8B%E7%9A%84%E7%BC%93%E5%AD%98%E6%9C%BA%E5%88%B6%E4%BA%86%E8%A7%A3%E4%B9%88%EF%BC%9F">包装类型的缓存机制了解么？</a></li>
<li><a href="#%E8%87%AA%E5%8A%A8%E8%A3%85%E7%AE%B1%E4%B8%8E%E6%8B%86%E7%AE%B1%E4%BA%86%E8%A7%A3%E5%90%97%EF%BC%9F%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F">自动装箱与拆箱了解吗？原理是什么？</a></li>
<li><a href="#%E4%B8%BA%E4%BB%80%E4%B9%88%E6%B5%AE%E7%82%B9%E6%95%B0%E8%BF%90%E7%AE%97%E7%9A%84%E6%97%B6%E5%80%99%E4%BC%9A%E6%9C%89%E7%B2%BE%E5%BA%A6%E4%B8%A2%E5%A4%B1%E7%9A%84%E9%A3%8E%E9%99%A9%EF%BC%9F">为什么浮点数运算的时候会有精度丢失的风险？</a></li>
<li><a href="#%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E6%B5%AE%E7%82%B9%E6%95%B0%E8%BF%90%E7%AE%97%E7%9A%84%E7%B2%BE%E5%BA%A6%E4%B8%A2%E5%A4%B1%E9%97%AE%E9%A2%98%EF%BC%9F">如何解决浮点数运算的精度丢失问题？</a></li>
<li><a href="#%E8%B6%85%E8%BF%87long%E6%95%B4%E5%9E%8B%E7%9A%84%E6%95%B0%E6%8D%AE%E5%BA%94%E8%AF%A5%E5%A6%82%E4%BD%95%E8%A1%A8%E7%A4%BA%EF%BC%9F">超过 long 整型的数据应该如何表示？</a></li>
</ul>
</li>
<li><a href="#%E5%8F%98%E9%87%8F">变量</a>
<ul>
<li><a href="#%E6%88%90%E5%91%98%E5%8F%98%E9%87%8F%E4%B8%8E%E5%B1%80%E9%83%A8%E5%8F%98%E9%87%8F%E7%9A%84%E5%8C%BA%E5%88%AB%EF%BC%9F">成员变量与局部变量的区别？</a></li>
<li><a href="#%E9%9D%99%E6%80%81%E5%8F%98%E9%87%8F%E6%9C%89%E4%BB%80%E4%B9%88%E4%BD%9C%E7%94%A8%EF%BC%9F">静态变量有什么作用？</a></li>
<li><a href="#%E5%AD%97%E7%AC%A6%E5%9E%8B%E5%B8%B8%E9%87%8F%E5%92%8C%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%B8%B8%E9%87%8F%E7%9A%84%E5%8C%BA%E5%88%AB">字符型常量和字符串常量的区别?</a></li>
</ul>
</li>
<li><a href="#%E6%96%B9%E6%B3%95">方法</a>
<ul>
<li><a href="#%E4%BB%80%E4%B9%88%E6%98%AF%E6%96%B9%E6%B3%95%E7%9A%84%E8%BF%94%E5%9B%9E%E5%80%BC%E6%96%B9%E6%B3%95%E6%9C%89%E5%93%AA%E5%87%A0%E7%A7%8D%E7%B1%BB%E5%9E%8B%EF%BC%9F">什么是方法的返回值?方法有哪几种类型？</a></li>
<li><a href="#%E9%9D%99%E6%80%81%E6%96%B9%E6%B3%95%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E8%83%BD%E8%B0%83%E7%94%A8%E9%9D%9E%E9%9D%99%E6%80%81%E6%88%90%E5%91%98">静态方法为什么不能调用非静态成员?</a></li>
<li><a href="#%E9%9D%99%E6%80%81%E6%96%B9%E6%B3%95%E5%92%8C%E5%AE%9E%E4%BE%8B%E6%96%B9%E6%B3%95%E6%9C%89%E4%BD%95%E4%B8%8D%E5%90%8C%EF%BC%9F">静态方法和实例方法有何不同？</a></li>
<li><a href="#%E9%87%8D%E8%BD%BD%E5%92%8C%E9%87%8D%E5%86%99%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F">重载和重写有什么区别？</a>
<ul>
<li><a href="#%E9%87%8D%E8%BD%BD">重载</a></li>
<li><a href="#%E9%87%8D%E5%86%99">重写</a></li>
<li><a href="#%E6%80%BB%E7%BB%93">总结</a></li>
</ul>
</li>
<li><a href="#%E4%BB%80%E4%B9%88%E6%98%AF%E5%8F%AF%E5%8F%98%E9%95%BF%E5%8F%82%E6%95%B0%EF%BC%9F">什么是可变长参数？</a></li>
</ul>
</li>
<li><a href="#%E5%8F%82%E8%80%83">参考</a></li>
</ul>
<h2><a id="%E5%9F%BA%E7%A1%80%E6%A6%82%E5%BF%B5%E4%B8%8E%E5%B8%B8%E8%AF%86" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>基础概念与常识</h2>
<h3><a id="java%E8%AF%AD%E8%A8%80%E6%9C%89%E5%93%AA%E4%BA%9B%E7%89%B9%E7%82%B9" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Java 语言有哪些特点?</h3>
<ol>
<li>简单易学（语法简单，上手容易）；</li>
<li>面向对象（封装，继承，多态）；</li>
<li>平台无关性（ Java 虚拟机实现平台无关性）；</li>
<li>支持多线程（ C++ 语言没有内置的多线程机制，因此必须调用操作系统的多线程功能来进行多线程程序设计，而 Java 语言却提供了多线程支持）；</li>
<li>可靠性（具备异常处理和自动内存管理机制）；</li>
<li>安全性（Java 语言本身的设计就提供了多重安全防护机制如访问权限修饰符、限制程序直接访问操作系统资源）；</li>
<li>高效性（通过 Just In Time 编译器等技术的优化，Java 语言的运行效率还是非常不错的）；</li>
<li>支持网络编程并且很方便；</li>
<li>编译与解释并存；</li>
<li>……</li>
</ol>
<blockquote>
<p><strong>🐛 修正（参见：<a href="https://github.com/Snailclimb/JavaGuide/issues/544">issue#544</a>）</strong>：C++11 开始（2011 年的时候）,C++就引入了多线程库，在 windows、linux、macos 都可以使用<code>std::thread</code>和<code>std::async</code>来创建线程。参考链接：<a href="http://www.cplusplus.com/reference/thread/thread/?kw=thread">http://www.cplusplus.com/reference/thread/thread/?kw=thread</a></p>
</blockquote>
<p>🌈 拓展一下：</p>
<p>“Write Once, Run Anywhere（一次编写，随处运行）”这句宣传口号，真心经典，流传了好多年！以至于，直到今天，依然有很多人觉得跨平台是 Java 语言最大的优势。实际上，跨平台已经不是 Java 最大的卖点了，各种 JDK 新特性也不是。目前市面上虚拟化技术已经非常成熟，比如你通过 Docker 就很容易实现跨平台了。在我看来，Java 强大的生态才是！</p>
<h3><a id="java-se-vs-java-ee" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Java SE vs Java EE</h3>
<ul>
<li>Java SE（Java Platform，Standard Edition）: Java 平台标准版，Java 编程语言的基础，它包含了支持 Java 应用程序开发和运行的核心类库以及虚拟机等核心组件。Java SE 可以用于构建桌面应用程序或简单的服务器应用程序。</li>
<li>Java EE（Java Platform, Enterprise Edition ）：Java 平台企业版，建立在 Java SE 的基础上，包含了支持企业级应用程序开发和部署的标准和规范（比如 Servlet、JSP、EJB、JDBC、JPA、JTA、JavaMail、JMS）。 Java EE 可以用于构建分布式、可移植、健壮、可伸缩和安全的服务端 Java 应用程序，例如 Web 应用程序。</li>
</ul>
<p>简单来说，Java SE 是 Java 的基础版本，Java EE 是 Java 的高级版本。Java SE 更适合开发桌面应用程序或简单的服务器应用程序，Java EE 更适合开发复杂的企业级应用程序或 Web 应用程序。</p>
<p>除了 Java SE 和 Java EE，还有一个 Java ME（Java Platform，Micro Edition）。Java ME 是 Java 的微型版本，主要用于开发嵌入式消费电子设备的应用程序，例如手机、PDA、机顶盒、冰箱、空调等。Java ME 无需重点关注，知道有这个东西就好了，现在已经用不上了。</p>
<h3><a id="jvm-vs-jdk-vs-jre" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>JVM vs JDK vs JRE</h3>
<h4><a id="jvm" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>JVM</h4>
<p>Java 虚拟机（Java Virtual Machine, JVM）是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现（Windows，Linux，macOS），目的是使用相同的字节码，它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译，随处可以运行”的关键所在。</p>
<p>如下图所示，不同编程语言（Java、Groovy、Kotlin、JRuby、Clojure ...）通过各自的编译器编译成 <code>.class</code> 文件，并最终通过 JVM 在不同平台（Windows、Mac、Linux）上运行。</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/17/1c5c63c3-bf8f-4e54-bad4-509a7649a506.png" alt="运行在 Java 虚拟机之上的编程语言" /></p>
<p><strong>JVM 并不是只有一种！只要满足 JVM 规范，每个公司、组织或者个人都可以开发自己的专属 JVM。</strong> 也就是说我们平时接触到的 HotSpot VM 仅仅是是 JVM 规范的一种实现而已。</p>
<p>除了我们平时最常用的 HotSpot VM 外，还有 J9 VM、Zing VM、JRockit VM 等 JVM 。维基百科上就有常见 JVM 的对比：<a href="https://en.wikipedia.org/wiki/Comparison_of_Java_virtual_machines">Comparison of Java virtual machines</a> ，感兴趣的可以去看看。并且，你可以在 <a href="https://docs.oracle.com/javase/specs/index.html">Java SE Specifications</a> 上找到各个版本的 JDK 对应的 JVM 规范。</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/17/193da73c-4d1f-450a-a77f-bf5c2540d2f5.jpg" alt="" /></p>
<h4><a id="jdk%E5%92%8C-jre" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>JDK 和 JRE</h4>
<p>JDK（Java Development Kit）是一个功能齐全的 Java 开发工具包，供开发者使用，用于创建和编译 Java 程序。它包含了 JRE（Java Runtime Environment），以及编译器 javac 和其他工具，如 javadoc（文档生成器）、jdb（调试器）、jconsole（监控工具）、javap（反编译工具）等。</p>
<p>JRE 是运行已编译 Java 程序所需的环境，主要包含以下两个部分：</p>
<ol>
<li><strong>JVM</strong> : 也就是我们上面提到的 Java 虚拟机。</li>
<li><strong>Java 基础类库（Class Library）</strong>：一组标准的类库，提供常用的功能和 API（如 I/O 操作、网络通信、数据结构等）。</li>
</ol>
<p>简单来说，JRE 只包含运行 Java 程序所需的环境和类库，而 JDK 不仅包含 JRE，还包括用于开发和调试 Java 程序的工具。</p>
<p>如果需要编写、编译 Java 程序或使用 Java API 文档，就需要安装 JDK。某些需要 Java 特性的应用程序（如 JSP 转换为 Servlet 或使用反射）也可能需要 JDK 来编译和运行 Java 代码。因此，即使不进行 Java 开发工作，有时也可能需要安装 JDK。</p>
<p>下图清晰展示了 JDK、JRE 和 JVM 的关系。</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/17/7ee77b7f-57b9-45b0-9095-6a6fbe56bd85.png" alt="jdk-include-jre" /></p>
<p>不过，从 JDK 9 开始，就不需要区分 JDK 和 JRE 的关系了，取而代之的是模块系统（JDK 被重新组织成 94 个模块）+ <a href="http://openjdk.java.net/jeps/282">jlink</a> 工具 (随 Java 9 一起发布的新命令行工具，用于生成自定义 Java 运行时映像，该映像仅包含给定应用程序所需的模块) 。并且，从 JDK 11 开始，Oracle 不再提供单独的 JRE 下载。</p>
<p>在 <a href="https://javaguide.cn/java/new-features/java9.html">Java 9 新特性概览</a>这篇文章中，我在介绍模块化系统的时候提到：</p>
<blockquote>
<p>在引入了模块系统之后，JDK 被重新组织成 94 个模块。Java 应用可以通过新增的 jlink 工具，创建出只包含所依赖的 JDK 模块的自定义运行时镜像。这样可以极大的减少 Java 运行时环境的大小。</p>
</blockquote>
<p>也就是说，可以用 jlink 根据自己的需求，创建一个更小的 runtime（运行时），而不是不管什么应用，都是同样的 JRE。</p>
<p>定制的、模块化的 Java 运行时映像有助于简化 Java 应用的部署和节省内存并增强安全性和可维护性。这对于满足现代应用程序架构的需求，如虚拟化、容器化、微服务和云原生开发，是非常重要的。</p>
<h3><a id="%E4%BB%80%E4%B9%88%E6%98%AF%E5%AD%97%E8%8A%82%E7%A0%81%E9%87%87%E7%94%A8%E5%AD%97%E8%8A%82%E7%A0%81%E7%9A%84%E5%A5%BD%E5%A4%84%E6%98%AF%E4%BB%80%E4%B9%88" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>什么是字节码?采用字节码的好处是什么?</h3>
<p>在 Java 中，JVM 可以理解的代码就叫做字节码（即扩展名为 <code>.class</code> 的文件），它不面向任何特定的处理器，只面向虚拟机。Java 语言通过字节码的方式，在一定程度上解决了传统解释型语言执行效率低的问题，同时又保留了解释型语言可移植的特点。所以， Java 程序运行时相对来说还是高效的（不过，和 C、 C++，Rust，Go 等语言还是有一定差距的），而且，由于字节码并不针对一种特定的机器，因此，Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。</p>
<p><strong>Java 程序从源代码到运行的过程如下图所示</strong>：</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/17/b3aa4c02-274d-4f52-85a7-d644af3079d5.png" alt="Java程序转变为机器代码的过程" /></p>
<p>我们需要格外注意的是 <code>.class-&gt;机器码</code> 这一步。在这一步 JVM 类加载器首先加载字节码文件，然后通过解释器逐行解释执行，这种方式的执行速度会相对比较慢。而且，有些方法和代码块是经常需要被调用的(也就是所谓的热点代码)，所以后面引进了 <strong>JIT（Just in Time Compilation）</strong> 编译器，而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后，其会将字节码对应的机器码保存下来，下次可以直接使用。而我们知道，机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 <strong>Java 是编译与解释共存的语言</strong> 。</p>
<blockquote>
<p>🌈 拓展阅读：</p>
<ul>
<li><a href="https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html">基本功 | Java 即时编译器原理解析及实践 - 美团技术团队</a></li>
<li><a href="https://mp.weixin.qq.com/s/4haTyXUmh8m-dBQaEzwDJw">基于静态编译构建微服务应用 - 阿里巴巴中间件</a></li>
</ul>
</blockquote>
<p><img src="https://image.huanglei.work/mweb/2025/3/17/5a5d0d0e-f2e6-4b41-a2ae-207508acd789.png" alt="Java程序转变为机器代码的过程" /></p>
<blockquote>
<p>HotSpot 采用了惰性评估(Lazy Evaluation)的做法，根据二八定律，消耗大部分系统资源的只有那一小部分的代码（热点代码），而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化，因此执行的次数越多，它的速度就越快。</p>
</blockquote>
<p>JDK、JRE、JVM、JIT 这四者的关系如下图所示。</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/17/5d49932a-6d18-4227-8187-a9907cea31d5.png" alt="JDK、JRE、JVM、JIT 这四者的关系" /></p>
<p>下面这张图是 JVM 的大致结构模型。</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/17/440a824a-cd5d-4d6e-8a34-0b9d5cb74fcc.jpg" alt="JVM 的大致结构模型" /></p>
<h3><a id="%E4%B8%BA%E4%BB%80%E4%B9%88%E8%AF%B4java%E8%AF%AD%E8%A8%80%E2%80%9C%E7%BC%96%E8%AF%91%E4%B8%8E%E8%A7%A3%E9%87%8A%E5%B9%B6%E5%AD%98%E2%80%9D%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>为什么说 Java 语言“编译与解释并存”？</h3>
<p>其实这个问题我们讲字节码的时候已经提到过，因为比较重要，所以我们这里再提一下。</p>
<p>我们可以将高级编程语言按照程序的执行方式分为两种：</p>
<ul>
<li><strong>编译型</strong>：<a href="https://zh.wikipedia.org/wiki/%E7%B7%A8%E8%AD%AF%E8%AA%9E%E8%A8%80">编译型语言</a> 会通过<a href="https://zh.wikipedia.org/wiki/%E7%B7%A8%E8%AD%AF%E5%99%A8">编译器</a>将源代码一次性翻译成可被该平台执行的机器码。一般情况下，编译语言的执行速度比较快，开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。</li>
<li><strong>解释型</strong>：<a href="https://zh.wikipedia.org/wiki/%E7%9B%B4%E8%AD%AF%E8%AA%9E%E8%A8%80">解释型语言</a>会通过<a href="https://zh.wikipedia.org/wiki/%E7%9B%B4%E8%AD%AF%E5%99%A8">解释器</a>一句一句的将代码解释（interpret）为机器代码后再执行。解释型语言开发效率比较快，执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。</li>
</ul>
<p><img src="https://image.huanglei.work/mweb/2025/3/17/093859e3-9cac-4894-b81c-3f438c5c4cdb.png" alt="编译型语言和解释型语言" /></p>
<p>根据维基百科介绍：</p>
<blockquote>
<p>为了改善解释语言的效率而发展出的<a href="https://zh.wikipedia.org/wiki/%E5%8D%B3%E6%99%82%E7%B7%A8%E8%AD%AF">即时编译</a>技术，已经缩小了这两种语言间的差距。这种技术混合了编译语言与解释型语言的优点，它像编译语言一样，先把程序源代码编译成<a href="https://zh.wikipedia.org/wiki/%E5%AD%97%E8%8A%82%E7%A0%81">字节码</a>。到执行期时，再将字节码直译，之后执行。<a href="https://zh.wikipedia.org/wiki/Java">Java</a>与<a href="https://zh.wikipedia.org/wiki/LLVM">LLVM</a>是这种技术的代表产物。</p>
<p>相关阅读：<a href="https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html">基本功 | Java 即时编译器原理解析及实践</a></p>
</blockquote>
<p><strong>为什么说 Java 语言“编译与解释并存”？</strong></p>
<p>这是因为 Java 语言既具有编译型语言的特征，也具有解释型语言的特征。因为 Java 程序要经过先编译，后解释两个步骤，由 Java 编写的程序需要先经过编译步骤，生成字节码（<code>.class</code> 文件），这种字节码必须由 Java 解释器来解释执行。</p>
<h3><a id="aot%E6%9C%89%E4%BB%80%E4%B9%88%E4%BC%98%E7%82%B9%EF%BC%9F%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E5%85%A8%E9%83%A8%E4%BD%BF%E7%94%A8-aot%E5%91%A2%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>AOT 有什么优点？为什么不全部使用 AOT 呢？</h3>
<p>JDK 9 引入了一种新的编译模式 <strong>AOT(Ahead of Time Compilation)</strong> 。和 JIT 不同的是，这种编译模式会在程序被执行前就将其编译成机器码，属于静态编译（C、 C++，Rust，Go 等语言就是静态编译）。AOT 避免了 JIT 预热等各方面的开销，可以提高 Java 程序的启动速度，避免预热时间长。并且，AOT 还能减少内存占用和增强 Java 程序的安全性（AOT 编译后的代码不容易被反编译和修改），特别适合云原生场景。</p>
<p><strong>JIT 与 AOT 两者的关键指标对比</strong>:</p>
<img src="https://image.huanglei.work/mweb/2025/3/17/b3d45c9c-a58b-4178-9418-10424942ceec.jpg" alt="JIT vs AOT" style="zoom: 25%;" />
<p>可以看出，AOT 的主要优势在于启动时间、内存占用和打包体积。JIT 的主要优势在于具备更高的极限处理能力，可以降低请求的最大延迟。</p>
<p>提到 AOT 就不得不提 <a href="https://www.graalvm.org/">GraalVM</a> 了！GraalVM 是一种高性能的 JDK（完整的 JDK 发行版本），它可以运行 Java 和其他 JVM 语言，以及 JavaScript、Python 等非 JVM 语言。 GraalVM 不仅能提供 AOT 编译，还能提供 JIT 编译。感兴趣的同学，可以去看看 GraalVM 的官方文档：<a href="https://www.graalvm.org/latest/docs/">https://www.graalvm.org/latest/docs/</a>。如果觉得官方文档看着比较难理解的话，也可以找一些文章来看看，比如：</p>
<ul>
<li><a href="https://mp.weixin.qq.com/s/4haTyXUmh8m-dBQaEzwDJw">基于静态编译构建微服务应用</a></li>
<li><a href="https://cn.dubbo.apache.org/zh-cn/blog/2023/06/28/%e8%b5%b0%e5%90%91-native-%e5%8c%96springdubbo-aot-%e6%8a%80%e6%9c%af%e7%a4%ba%e4%be%8b%e4%b8%8e%e5%8e%9f%e7%90%86%e8%ae%b2%e8%a7%a3/">走向 Native 化：Spring&amp;Dubbo AOT 技术示例与原理讲解</a></li>
</ul>
<p><strong>既然 AOT 这么多优点，那为什么不全部使用这种编译方式呢？</strong></p>
<p>我们前面也对比过 JIT 与 AOT，两者各有优点，只能说 AOT 更适合当下的云原生场景，对微服务架构的支持也比较友好。除此之外，AOT 编译无法支持 Java 的一些动态特性，如反射、动态代理、动态加载、JNI（Java Native Interface）等。然而，很多框架和库（如 Spring、CGLIB）都用到了这些特性。如果只使用 AOT 编译，那就没办法使用这些框架和库了，或者说需要针对性地去做适配和优化。举个例子，CGLIB 动态代理使用的是 ASM 技术，而这种技术大致原理是运行时直接在内存中生成并加载修改后的字节码文件也就是 <code>.class</code> 文件，如果全部使用 AOT 提前编译，也就不能使用 ASM 技术了。为了支持类似的动态特性，所以选择使用 JIT 即时编译器。</p>
<h3><a id="oracle-jdk-vs-openjdk" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Oracle JDK vs OpenJDK</h3>
<p>可能在看这个问题之前很多人和我一样并没有接触和使用过 OpenJDK 。那么 Oracle JDK 和 OpenJDK 之间是否存在重大差异？下面我通过收集到的一些资料，为你解答这个被很多人忽视的问题。</p>
<p>首先，2006 年 SUN 公司将 Java 开源，也就有了 OpenJDK。2009 年 Oracle 收购了 Sun 公司，于是自己在 OpenJDK 的基础上搞了一个 Oracle JDK。Oracle JDK 是不开源的，并且刚开始的几个版本（Java8 ~ Java11）还会相比于 OpenJDK 添加一些特有的功能和工具。</p>
<p>其次，对于 Java 7 而言，OpenJDK 和 Oracle JDK 是十分接近的。 Oracle JDK 是基于 OpenJDK 7 构建的，只添加了一些小功能，由 Oracle 工程师参与维护。</p>
<p>下面这段话摘自 Oracle 官方在 2012 年发表的一个博客：</p>
<blockquote>
<p>问：OpenJDK 存储库中的源代码与用于构建 Oracle JDK 的代码之间有什么区别？</p>
<p>答：非常接近 - 我们的 Oracle JDK 版本构建过程基于 OpenJDK 7 构建，只添加了几个部分，例如部署代码，其中包括 Oracle 的 Java 插件和 Java WebStart 的实现，以及一些闭源的第三方组件，如图形光栅化器，一些开源的第三方组件，如 Rhino，以及一些零碎的东西，如附加文档或第三方字体。展望未来，我们的目的是开源 Oracle JDK 的所有部分，除了我们考虑商业功能的部分。</p>
</blockquote>
<p>最后，简单总结一下 Oracle JDK 和 OpenJDK 的区别：</p>
<ol>
<li><strong>是否开源</strong>：OpenJDK 是一个参考模型并且是完全开源的，而 Oracle JDK 是基于 OpenJDK 实现的，并不是完全开源的（个人观点：众所周知，JDK 原来是 SUN 公司开发的，后来 SUN 公司又卖给了 Oracle 公司，Oracle 公司以 Oracle 数据库而著名，而 Oracle 数据库又是闭源的，这个时候 Oracle 公司就不想完全开源了，但是原来的 SUN 公司又把 JDK 给开源了，如果这个时候 Oracle 收购回来之后就把他给闭源，必然会引起很多 Java 开发者的不满，导致大家对 Java 失去信心，那 Oracle 公司收购回来不就把 Java 烂在手里了吗！然后，Oracle 公司就想了个骚操作，这样吧，我把一部分核心代码开源出来给你们玩，并且我要和你们自己搞的 JDK 区分下，你们叫 OpenJDK，我叫 Oracle JDK，我发布我的，你们继续玩你们的，要是你们搞出来什么好玩的东西，我后续发布 Oracle JDK 也会拿来用一下，一举两得！）OpenJDK 开源项目：<a href="https://github.com/openjdk/jdk">https://github.com/openjdk/jdk</a> 。</li>
<li><strong>是否免费</strong>：Oracle JDK 会提供免费版本，但一般有时间限制。JDK17 之后的版本可以免费分发和商用，但是仅有 3 年时间，3 年后无法免费商用。不过，JDK8u221 之前只要不升级可以无限期免费。OpenJDK 是完全免费的。</li>
<li><strong>功能性</strong>：Oracle JDK 在 OpenJDK 的基础上添加了一些特有的功能和工具，比如 Java Flight Recorder（JFR，一种监控工具）、Java Mission Control（JMC，一种监控工具）等工具。不过，在 Java 11 之后，OracleJDK 和 OpenJDK 的功能基本一致，之前 OracleJDK 中的私有组件大多数也已经被捐赠给开源组织。</li>
<li><strong>稳定性</strong>：OpenJDK 不提供 LTS 服务，而 OracleJDK 大概每三年都会推出一个 LTS 版进行长期支持。不过，很多公司都基于 OpenJDK 提供了对应的和 OracleJDK 周期相同的 LTS 版。因此，两者稳定性其实也是差不多的。</li>
<li><strong>协议</strong>：Oracle JDK 使用 BCL/OTN 协议获得许可，而 OpenJDK 根据 GPL v2 许可获得许可。</li>
</ol>
<blockquote>
<p>既然 Oracle JDK 这么好，那为什么还要有 OpenJDK？</p>
<p>答：</p>
<ol>
<li>OpenJDK 是开源的，开源意味着你可以对它根据你自己的需要进行修改、优化，比如 Alibaba 基于 OpenJDK 开发了 Dragonwell8：<a href="https://github.com/alibaba/dragonwell8">https://github.com/alibaba/dragonwell8</a></li>
<li>OpenJDK 是商业免费的（这也是为什么通过 yum 包管理器上默认安装的 JDK 是 OpenJDK 而不是 Oracle JDK）。虽然 Oracle JDK 也是商业免费（比如 JDK 8），但并不是所有版本都是免费的。</li>
<li>OpenJDK 更新频率更快。Oracle JDK 一般是每 6 个月发布一个新版本，而 OpenJDK 一般是每 3 个月发布一个新版本。（现在你知道为啥 Oracle JDK 更稳定了吧，先在 OpenJDK 试试水，把大部分问题都解决掉了才在 Oracle JDK 上发布）</li>
</ol>
<p>基于以上这些原因，OpenJDK 还是有存在的必要的！</p>
</blockquote>
<p><img src="https://image.huanglei.work/mweb/2025/3/17/f637f3d7-b358-4057-a054-2867af69dd6c.jpg" alt="oracle jdk release cadence" /></p>
<p><strong>Oracle JDK 和 OpenJDK 如何选择？</strong></p>
<p>建议选择 OpenJDK 或者基于 OpenJDK 的发行版，比如 AWS 的 Amazon Corretto，阿里巴巴的 Alibaba Dragonwell。</p>
<p>🌈 拓展一下：</p>
<ul>
<li>BCL 协议（Oracle Binary Code License Agreement）：可以使用 JDK（支持商用），但是不能进行修改。</li>
<li>OTN 协议（Oracle Technology Network License Agreement）：11 及之后新发布的 JDK 用的都是这个协议，可以自己私下用，但是商用需要付费。</li>
</ul>
<h3><a id="java%E5%92%8C-c%E7%9A%84%E5%8C%BA%E5%88%AB" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Java 和 C++ 的区别?</h3>
<p>我知道很多人没学过 C++，但是面试官就是没事喜欢拿咱们 Java 和 C++ 比呀！没办法！！！就算没学过 C++，也要记下来。</p>
<p>虽然，Java 和 C++ 都是面向对象的语言，都支持封装、继承和多态，但是，它们还是有挺多不相同的地方：</p>
<ul>
<li>Java 不提供指针来直接访问内存，程序内存更加安全</li>
<li>Java 的类是单继承的，C++ 支持多重继承；虽然 Java 的类不可以多继承，但是接口可以多继承。</li>
<li>Java 有自动内存管理垃圾回收机制(GC)，不需要程序员手动释放无用内存。</li>
<li>C ++同时支持方法重载和操作符重载，但是 Java 只支持方法重载（操作符重载增加了复杂性，这与 Java 最初的设计思想不符）。</li>
<li>……</li>
</ul>
<h2><a id="%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>基本语法</h2>
<h3><a id="%E6%B3%A8%E9%87%8A%E6%9C%89%E5%93%AA%E5%87%A0%E7%A7%8D%E5%BD%A2%E5%BC%8F%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>注释有哪几种形式？</h3>
<p>Java 中的注释有三种：</p>
<ol>
<li>
<p><strong>单行注释</strong>：通常用于解释方法内某单行代码的作用。</p>
</li>
<li>
<p><strong>多行注释</strong>：通常用于解释一段代码的作用。</p>
</li>
<li>
<p><strong>文档注释</strong>：通常用于生成 Java 开发文档。</p>
</li>
</ol>
<p>用的比较多的还是单行注释和文档注释，多行注释在实际开发中使用的相对较少。</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/17/521dff70-2d31-49f7-a467-0780b6ca37f8.png" alt="" /></p>
<p>在我们编写代码的时候，如果代码量比较少，我们自己或者团队其他成员还可以很轻易地看懂代码，但是当项目结构一旦复杂起来，我们就需要用到注释了。注释并不会执行(编译器在编译代码之前会把代码中的所有注释抹掉,字节码中不保留注释)，是我们程序员写给自己看的，注释是你的代码说明书，能够帮助看代码的人快速地理清代码之间的逻辑关系。因此，在写程序的时候随手加上注释是一个非常好的习惯。</p>
<p>《Clean Code》这本书明确指出：</p>
<blockquote>
<p><strong>代码的注释不是越详细越好。实际上好的代码本身就是注释，我们要尽量规范和美化自己的代码来减少不必要的注释。</strong></p>
<p><strong>若编程语言足够有表达力，就不需要注释，尽量通过代码来阐述。</strong></p>
<p>举个例子：</p>
<p>去掉下面复杂的注释，只需要创建一个与注释所言同一事物的函数即可</p>
<pre><code class="language-java">// check to see if the employee is eligible for full benefits
if ((employee.flags &amp; HOURLY_FLAG) &amp;&amp; (employee.age &gt; 65))
</code></pre>
<p>应替换为</p>
<pre><code class="language-java">if (employee.isEligibleForFullBenefits())
</code></pre>
</blockquote>
<h3><a id="%E6%A0%87%E8%AF%86%E7%AC%A6%E5%92%8C%E5%85%B3%E9%94%AE%E5%AD%97%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>标识符和关键字的区别是什么？</h3>
<p>在我们编写程序的时候，需要大量地为程序、类、变量、方法等取名字，于是就有了 <strong>标识符</strong> 。简单来说， <strong>标识符就是一个名字</strong> 。</p>
<p>有一些标识符，Java 语言已经赋予了其特殊的含义，只能用于特定的地方，这些特殊的标识符就是 <strong>关键字</strong> 。简单来说，<strong>关键字是被赋予特殊含义的标识符</strong> 。比如，在我们的日常生活中，如果我们想要开一家店，则要给这个店起一个名字，起的这个“名字”就叫标识符。但是我们店的名字不能叫“警察局”，因为“警察局”这个名字已经被赋予了特殊的含义，而“警察局”就是我们日常生活中的关键字。</p>
<h3><a id="java%E8%AF%AD%E8%A8%80%E5%85%B3%E9%94%AE%E5%AD%97%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Java 语言关键字有哪些？</h3>
<table>
<thead>
<tr>
<th style="text-align: left">分类</th>
<th>关键字</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left">访问控制</td>
<td>private</td>
<td>protected</td>
<td>public</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td style="text-align: left">类，方法和变量修饰符</td>
<td>abstract</td>
<td>class</td>
<td>extends</td>
<td>final</td>
<td>implements</td>
<td>interface</td>
<td>native</td>
</tr>
<tr>
<td style="text-align: left"></td>
<td>new</td>
<td>static</td>
<td>strictfp</td>
<td>synchronized</td>
<td>transient</td>
<td>volatile</td>
<td>enum</td>
</tr>
<tr>
<td style="text-align: left">程序控制</td>
<td>break</td>
<td>continue</td>
<td>return</td>
<td>do</td>
<td>while</td>
<td>if</td>
<td>else</td>
</tr>
<tr>
<td style="text-align: left"></td>
<td>for</td>
<td>instanceof</td>
<td>switch</td>
<td>case</td>
<td>default</td>
<td>assert</td>
<td></td>
</tr>
<tr>
<td style="text-align: left">错误处理</td>
<td>try</td>
<td>catch</td>
<td>throw</td>
<td>throws</td>
<td>finally</td>
<td></td>
<td></td>
</tr>
<tr>
<td style="text-align: left">包相关</td>
<td>import</td>
<td>package</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td style="text-align: left">基本类型</td>
<td>boolean</td>
<td>byte</td>
<td>char</td>
<td>double</td>
<td>float</td>
<td>int</td>
<td>long</td>
</tr>
<tr>
<td style="text-align: left"></td>
<td>short</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td style="text-align: left">变量引用</td>
<td>super</td>
<td>this</td>
<td>void</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td style="text-align: left">保留字</td>
<td>goto</td>
<td>const</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
<blockquote>
<p>Tips：所有的关键字都是小写的，在 IDE 中会以特殊颜色显示。</p>
<p><code>default</code> 这个关键字很特殊，既属于程序控制，也属于类，方法和变量修饰符，还属于访问控制。</p>
<ul>
<li>在程序控制中，当在 <code>switch</code> 中匹配不到任何情况时，可以使用 <code>default</code> 来编写默认匹配的情况。</li>
<li>在类，方法和变量修饰符中，从 JDK8 开始引入了默认方法，可以使用 <code>default</code> 关键字来定义一个方法的默认实现。</li>
<li>在访问控制中，如果一个方法前没有任何修饰符，则默认会有一个修饰符 <code>default</code>，但是这个修饰符加上了就会报错。</li>
</ul>
</blockquote>
<p>⚠️ 注意：虽然 <code>true</code>, <code>false</code>, 和 <code>null</code> 看起来像关键字但实际上他们是字面值，同时你也不可以作为标识符来使用。</p>
<p>官方文档：<a href="https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html">https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html</a></p>
<h3><a id="%E8%87%AA%E5%A2%9E%E8%87%AA%E5%87%8F%E8%BF%90%E7%AE%97%E7%AC%A6" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>自增自减运算符</h3>
<p>在写代码的过程中，常见的一种情况是需要某个整数类型变量增加 1 或减少 1。Java 提供了自增运算符 (<code>++</code>) 和自减运算符 (<code>--</code>) 来简化这种操作。</p>
<p><code>++</code> 和 <code>--</code> 运算符可以放在变量之前，也可以放在变量之后：</p>
<ul>
<li><strong>前缀形式</strong>（例如 <code>++a</code> 或 <code>--a</code>）：先自增/自减变量的值，然后再使用该变量，例如，<code>b = ++a</code> 先将 <code>a</code> 增加 1，然后把增加后的值赋给 <code>b</code>。</li>
<li><strong>后缀形式</strong>（例如 <code>a++</code> 或 <code>a--</code>）：先使用变量的当前值，然后再自增/自减变量的值。例如，<code>b = a++</code> 先将 <code>a</code> 的当前值赋给 <code>b</code>，然后再将 <code>a</code> 增加 1。</li>
</ul>
<p>为了方便记忆，可以使用下面的口诀：<strong>符号在前就先加/减，符号在后就后加/减</strong>。</p>
<p>下面来看一个考察自增自减运算符的高频笔试题：执行下面的代码后，<code>a</code> 、<code>b</code> 、 <code>c</code> 、<code>d</code>和<code>e</code>的值是？</p>
<pre><code class="language-java">int a = 9;
int b = a++;
int c = ++a;
int d = c--;
int e = --d;
</code></pre>
<p>答案：<code>a = 11</code> 、<code>b = 9</code> 、 <code>c = 10</code> 、 <code>d = 10</code> 、 <code>e = 10</code>。</p>
<hr />
<p><strong>✨个人补充</strong></p>
<p>要看清题目。</p>
<pre><code class="language-Java">int a = 9;
// a 初始为 9，b 被赋予 a 的初始值（9），然后 a 自增变为 10
int b = a++; // a = 10, b = 9

// a 已经是 10，前置自增使其先变为 11，然后这个新值赋给 c
int c = ++a; // a = 11, c = 11

// c 当前为 11，d 被赋予 c 的当前值（11），然后 c 自减变为 10
int d = c--; // c = 10, d = 11

// d 当前为 11，前置自减使其先变为 10，然后这个新值赋给 e
int e = --d; // d = 10, e = 10
</code></pre>
<hr />
<h3><a id="%E7%A7%BB%E4%BD%8D%E8%BF%90%E7%AE%97%E7%AC%A6" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>移位运算符</h3>
<p>移位运算符是最基本的运算符之一，几乎每种编程语言都包含这一运算符。移位操作中，被操作的数据被视为二进制数，移位就是将其向左或向右移动若干位的运算。</p>
<p>移位运算符在各种框架以及 JDK 自身的源码中使用还是挺广泛的，<code>HashMap</code>（JDK1.8） 中的 <code>hash</code> 方法的源码就用到了移位运算符：</p>
<pre><code class="language-java">static final int hash(Object key) {
    int h;
    // key.hashCode()：返回散列值也就是hashcode
    // ^：按位异或
    // &gt;&gt;&gt;:无符号右移，忽略符号位，空位都以0补齐
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h &gt;&gt;&gt; 16);
  }

</code></pre>
<p><strong>使用移位运算符的主要原因</strong>：</p>
<ol>
<li><strong>高效</strong>：移位运算符直接对应于处理器的移位指令。现代处理器具有专门的硬件指令来执行这些移位操作，这些指令通常在一个时钟周期内完成。相比之下，乘法和除法等算术运算在硬件层面上需要更多的时钟周期来完成。</li>
<li><strong>节省内存</strong>：通过移位操作，可以使用一个整数（如 <code>int</code> 或 <code>long</code>）来存储多个布尔值或标志位，从而节省内存。</li>
</ol>
<p>移位运算符最常用于快速乘以或除以 2 的幂次方。除此之外，它还在以下方面发挥着重要作用：</p>
<ul>
<li><strong>位字段管理</strong>：例如存储和操作多个布尔值。</li>
<li><strong>哈希算法和加密解密</strong>：通过移位和与、或等操作来混淆数据。</li>
<li><strong>数据压缩</strong>：例如霍夫曼编码通过移位运算符可以快速处理和操作二进制数据，以生成紧凑的压缩格式。</li>
<li><strong>数据校验</strong>：例如 CRC（循环冗余校验）通过移位和多项式除法生成和校验数据完整性。。</li>
<li><strong>内存对齐</strong>：通过移位操作，可以轻松计算和调整数据的对齐地址。</li>
</ul>
<p>掌握最基本的移位运算符知识还是很有必要的，这不光可以帮助我们在代码中使用，还可以帮助我们理解源码中涉及到移位运算符的代码。</p>
<p>Java 中有三种移位运算符：</p>
<ul>
<li><code>&lt;&lt;</code> :左移运算符，向左移若干位，高位丢弃，低位补零。<code>x &lt;&lt; n</code>,相当于 x 乘以 2 的 n 次方(不溢出的情况下)。</li>
<li><code>&gt;&gt;</code> :带符号右移，向右移若干位，高位补符号位，低位丢弃。正数高位补 0,负数高位补 1。<code>x &gt;&gt; n</code>,相当于 x 除以 2 的 n 次方。</li>
<li><code>&gt;&gt;&gt;</code> :无符号右移，忽略符号位，空位都以 0 补齐。</li>
</ul>
<p>虽然移位运算本质上可以分为左移和右移，但在实际应用中，右移操作需要考虑符号位的处理方式。</p>
<p>由于 <code>double</code>，<code>float</code> 在二进制中的表现比较特殊，因此不能来进行移位操作。</p>
<p>移位操作符实际上支持的类型只有<code>int</code>和<code>long</code>，编译器在对<code>short</code>、<code>byte</code>、<code>char</code>类型进行移位前，都会将其转换为<code>int</code>类型再操作。</p>
<p><strong>如果移位的位数超过数值所占有的位数会怎样？</strong></p>
<p>当 int 类型左移/右移位数大于等于 32 位操作时，会先求余（%）后再进行左移/右移操作。也就是说左移/右移 32 位相当于不进行移位操作（32%32=0），左移/右移 42 位相当于左移/右移 10 位（42%32=10）。当 long 类型进行左移/右移操作时，由于 long 对应的二进制是 64 位，因此求余操作的基数也变成了 64。</p>
<p>也就是说：<code>x&lt;&lt;42</code>等同于<code>x&lt;&lt;10</code>，<code>x&gt;&gt;42</code>等同于<code>x&gt;&gt;10</code>，<code>x &gt;&gt;&gt;42</code>等同于<code>x &gt;&gt;&gt; 10</code>。</p>
<p><strong>左移运算符代码示例</strong>：</p>
<pre><code class="language-java">int i = -1;
System.out.println(&quot;初始数据：&quot; + i);
System.out.println(&quot;初始数据对应的二进制字符串：&quot; + Integer.toBinaryString(i));
i &lt;&lt;= 10;
System.out.println(&quot;左移 10 位后的数据 &quot; + i);
System.out.println(&quot;左移 10 位后的数据对应的二进制字符 &quot; + Integer.toBinaryString(i));
</code></pre>
<p>输出：</p>
<pre><code class="language-plain">初始数据：-1
初始数据对应的二进制字符串：11111111111111111111111111111111
左移 10 位后的数据 -1024
左移 10 位后的数据对应的二进制字符 11111111111111111111110000000000
</code></pre>
<p>由于左移位数大于等于 32 位操作时，会先求余（%）后再进行左移操作，所以下面的代码左移 42 位相当于左移 10 位（42%32=10），输出结果和前面的代码一样。</p>
<pre><code class="language-java">int i = -1;
System.out.println(&quot;初始数据：&quot; + i);
System.out.println(&quot;初始数据对应的二进制字符串：&quot; + Integer.toBinaryString(i));
i &lt;&lt;= 42;
System.out.println(&quot;左移 10 位后的数据 &quot; + i);
System.out.println(&quot;左移 10 位后的数据对应的二进制字符 &quot; + Integer.toBinaryString(i));
</code></pre>
<p>右移运算符使用类似，篇幅问题，这里就不做演示了。</p>
<hr />
<p><strong>✨个人补充</strong>：<a href="17422797595577.html">彻底弄懂Java的移位操作符</a></p>
<hr />
<h3><a id="continue%E3%80%81break%E5%92%8C-return%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>continue、break 和 return 的区别是什么？</h3>
<p>在循环结构中，当循环条件不满足或者循环次数达到要求时，循环会正常结束。但是，有时候可能需要在循环的过程中，当发生了某种条件之后 ，提前终止循环，这就需要用到下面几个关键词：</p>
<ol>
<li><code>continue</code>：指跳出当前的这一次循环，继续下一次循环。</li>
<li><code>break</code>：指跳出整个循环体，继续执行循环下面的语句。</li>
</ol>
<p><code>return</code> 用于跳出所在方法，结束该方法的运行。return 一般有两种用法：</p>
<ol>
<li><code>return;</code>：直接使用 return 结束方法执行，用于没有返回值函数的方法</li>
<li><code>return value;</code>：return 一个特定值，用于有返回值函数的方法</li>
</ol>
<p>思考一下：下列语句的运行结果是什么？</p>
<pre><code class="language-java">public static void main(String[] args) {
    boolean flag = false;
    for (int i = 0; i &lt;= 3; i++) {
        if (i == 0) {
            System.out.println(&quot;0&quot;);
        } else if (i == 1) {
            System.out.println(&quot;1&quot;);
            continue;
        } else if (i == 2) {
            System.out.println(&quot;2&quot;);
            flag = true;
        } else if (i == 3) {
            System.out.println(&quot;3&quot;);
            break;
        } else if (i == 4) {
            System.out.println(&quot;4&quot;);
        }
        System.out.println(&quot;xixi&quot;);
    }
    if (flag) {
        System.out.println(&quot;haha&quot;);
        return;
    }
    System.out.println(&quot;heihei&quot;);
}
</code></pre>
<p>运行结果：</p>
<pre><code class="language-plain">0
xixi
1
2
xixi
3
haha
</code></pre>
<h2><a id="%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>基本数据类型</h2>
<h3><a id="java%E4%B8%AD%E7%9A%84%E5%87%A0%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E4%BA%86%E8%A7%A3%E4%B9%88%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Java 中的几种基本数据类型了解么？</h3>
<p>Java 中有 8 种基本数据类型，分别为：</p>
<ul>
<li>6 种数字类型：
<ul>
<li>4 种整数型：<code>byte</code>、<code>short</code>、<code>int</code>、<code>long</code></li>
<li>2 种浮点型：<code>float</code>、<code>double</code></li>
</ul>
</li>
<li>1 种字符类型：<code>char</code></li>
<li>1 种布尔型：<code>boolean</code>。</li>
</ul>
<p>这 8 种基本数据类型的默认值以及所占空间的大小如下：</p>
<table>
<thead>
<tr>
<th style="text-align: left">基本类型</th>
<th style="text-align: left">位数</th>
<th style="text-align: left">字节</th>
<th style="text-align: left">默认值</th>
<th>取值范围</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left"><code>byte</code></td>
<td style="text-align: left">8</td>
<td style="text-align: left">1</td>
<td style="text-align: left">0</td>
<td>-128 ~ 127</td>
</tr>
<tr>
<td style="text-align: left"><code>short</code></td>
<td style="text-align: left">16</td>
<td style="text-align: left">2</td>
<td style="text-align: left">0</td>
<td>-32768（-2^15） ~ 32767（2^15 - 1）</td>
</tr>
<tr>
<td style="text-align: left"><code>int</code></td>
<td style="text-align: left">32</td>
<td style="text-align: left">4</td>
<td style="text-align: left">0</td>
<td>-2147483648 ~ 2147483647</td>
</tr>
<tr>
<td style="text-align: left"><code>long</code></td>
<td style="text-align: left">64</td>
<td style="text-align: left">8</td>
<td style="text-align: left">0L</td>
<td>-9223372036854775808（-2^63） ~ 9223372036854775807（2^63 -1）</td>
</tr>
<tr>
<td style="text-align: left"><code>char</code></td>
<td style="text-align: left">16</td>
<td style="text-align: left">2</td>
<td style="text-align: left">'u0000'</td>
<td>0 ~ 65535（2^16 - 1）</td>
</tr>
<tr>
<td style="text-align: left"><code>float</code></td>
<td style="text-align: left">32</td>
<td style="text-align: left">4</td>
<td style="text-align: left">0f</td>
<td>1.4E-45 ~ 3.4028235E38</td>
</tr>
<tr>
<td style="text-align: left"><code>double</code></td>
<td style="text-align: left">64</td>
<td style="text-align: left">8</td>
<td style="text-align: left">0d</td>
<td>4.9E-324 ~ 1.7976931348623157E308</td>
</tr>
<tr>
<td style="text-align: left"><code>boolean</code></td>
<td style="text-align: left">1</td>
<td style="text-align: left"></td>
<td style="text-align: left">false</td>
<td>true、false</td>
</tr>
</tbody>
</table>
<p>可以看到，像 <code>byte</code>、<code>short</code>、<code>int</code>、<code>long</code>能表示的最大正数都减 1 了。这是为什么呢？这是因为在二进制补码表示法中，最高位是用来表示符号的（0 表示正数，1 表示负数），其余位表示数值部分。所以，如果我们要表示最大的正数，我们需要把除了最高位之外的所有位都设为 1。如果我们再加 1，就会导致溢出，变成一个负数。</p>
<p>对于 <code>boolean</code>，官方文档未明确定义，它依赖于 JVM 厂商的具体实现。逻辑上理解是占用 1 位，但是实际中会考虑计算机高效存储因素。</p>
<p>另外，Java 的每种基本类型所占存储空间的大小不会像其他大多数语言那样随机器硬件架构的变化而变化。这种所占存储空间大小的不变性是 Java 程序比用其他大多数语言编写的程序更具可移植性的原因之一（《Java 编程思想》2.2 节有提到）。</p>
<p><strong>注意：</strong></p>
<ol>
<li>Java 里使用 <code>long</code> 类型的数据一定要在数值后面加上 <strong>L</strong>，否则将作为整型解析。</li>
<li>Java 里使用 <code>float</code> 类型的数据一定要在数值后面加上 <strong>f 或 F</strong>，否则将无法通过编译。</li>
<li><code>char a = 'h'</code>char :单引号，<code>String a = &quot;hello&quot;</code> :双引号。</li>
</ol>
<p>这八种基本类型都有对应的包装类分别为：<code>Byte</code>、<code>Short</code>、<code>Integer</code>、<code>Long</code>、<code>Float</code>、<code>Double</code>、<code>Character</code>、<code>Boolean</code> 。</p>
<h3><a id="%E5%9F%BA%E6%9C%AC%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%8C%85%E8%A3%85%E7%B1%BB%E5%9E%8B%E7%9A%84%E5%8C%BA%E5%88%AB%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>基本类型和包装类型的区别？</h3>
<ul>
<li><strong>用途</strong>：除了定义一些常量和局部变量之外，我们在其他地方比如方法参数、对象属性中很少会使用基本类型来定义变量。并且，包装类型可用于泛型，而基本类型不可以。</li>
<li><strong>存储方式</strong>：基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中，基本数据类型的成员变量（未被 <code>static</code> 修饰 ）存放在 Java 虚拟机的堆中。包装类型属于对象类型，我们知道几乎所有对象实例都存在于堆中。</li>
<li><strong>占用空间</strong>：相比于包装类型（对象类型）， 基本数据类型占用的空间往往非常小。</li>
<li><strong>默认值</strong>：成员变量包装类型不赋值就是 <code>null</code> ，而基本类型有默认值且不是 <code>null</code>。</li>
<li><strong>比较方式</strong>：对于基本数据类型来说，<code>==</code> 比较的是值。对于包装数据类型来说，<code>==</code> 比较的是对象的内存地址。所有整型包装类对象之间值的比较，全部使用 <code>equals()</code> 方法。</li>
</ul>
<p><strong>为什么说是几乎所有对象实例都存在于堆中呢？</strong> 这是因为 HotSpot 虚拟机引入了 JIT 优化之后，会对对象进行逃逸分析，如果发现某一个对象并没有逃逸到方法外部，那么就可能通过标量替换来实现栈上分配，而避免堆上分配内存</p>
<p>⚠️ 注意：<strong>基本数据类型存放在栈中是一个常见的误区！</strong> 基本数据类型的存储位置取决于它们的作用域和声明方式。如果它们是局部变量，那么它们会存放在栈中；如果它们是成员变量，那么它们会存放在堆/方法区/元空间中。</p>
<pre><code class="language-java">public class Test {
    // 成员变量，存放在堆中
    int a = 10;
    // 被 static 修饰的成员变量，JDK 1.7 及之前位于方法区，1.8 后存放于元空间，均不存放于堆中。
    // 变量属于类，不属于对象。
    static int b = 20;

    public void method() {
        // 局部变量，存放在栈中
        int c = 30;
        static int d = 40; // 编译错误，不能在方法中使用 static 修饰局部变量
    }
}
</code></pre>
<h3><a id="%E5%8C%85%E8%A3%85%E7%B1%BB%E5%9E%8B%E7%9A%84%E7%BC%93%E5%AD%98%E6%9C%BA%E5%88%B6%E4%BA%86%E8%A7%A3%E4%B9%88%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>包装类型的缓存机制了解么？</h3>
<p>Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。</p>
<p><code>Byte</code>,<code>Short</code>,<code>Integer</code>,<code>Long</code> 这 4 种包装类默认创建了数值 <strong>[-128，127]</strong> 的相应类型的缓存数据，<code>Character</code> 创建了数值在 <strong>[0,127]</strong> 范围的缓存数据，<code>Boolean</code> 直接返回 <code>TRUE</code> or <code>FALSE</code>。</p>
<p>对于 <code>Integer</code>，可以通过 JVM 参数 <code>-XX:AutoBoxCacheMax=&lt;size&gt;</code> 修改缓存上限，但不能修改下限 -128。实际使用时，并不建议设置过大的值，避免浪费内存，甚至是 OOM。</p>
<p>对于<code>Byte</code>,<code>Short</code>,<code>Long</code> ,<code>Character</code> 没有类似 <code>-XX:AutoBoxCacheMax</code> 参数可以修改，因此缓存范围是固定的，无法通过 JVM 参数调整。<code>Boolean</code> 则直接返回预定义的 <code>TRUE</code> 和 <code>FALSE</code> 实例，没有缓存范围的概念。</p>
<p><strong>Integer 缓存源码：</strong></p>
<pre><code class="language-java">public static Integer valueOf(int i) {
    if (i &gt;= IntegerCache.low &amp;&amp; i &lt;= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}
private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static {
        // high value may be configured by property
        int h = 127;
    }
}
</code></pre>
<p><strong><code>Character</code> 缓存源码:</strong></p>
<pre><code class="language-java">public static Character valueOf(char c) {
    if (c &lt;= 127) { // must cache
      return CharacterCache.cache[(int)c];
    }
    return new Character(c);
}

private static class CharacterCache {
    private CharacterCache(){}
    static final Character cache[] = new Character[127 + 1];
    static {
        for (int i = 0; i &lt; cache.length; i++)
            cache[i] = new Character((char)i);
    }

}
</code></pre>
<p><strong><code>Boolean</code> 缓存源码：</strong></p>
<pre><code class="language-java">public static Boolean valueOf(boolean b) {
    return (b ? TRUE : FALSE);
}
</code></pre>
<p>如果超出对应范围仍然会去创建新的对象，缓存的范围区间的大小只是在性能和资源之间的权衡。</p>
<p>两种浮点数类型的包装类 <code>Float</code>,<code>Double</code> 并没有实现缓存机制。</p>
<pre><code class="language-java">Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 输出 true

Float i11 = 333f;
Float i22 = 333f;
System.out.println(i11 == i22);// 输出 false

Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 输出 false
</code></pre>
<p>下面我们来看一个问题：下面的代码的输出结果是 <code>true</code> 还是 <code>false</code> 呢？</p>
<pre><code class="language-java">Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);
</code></pre>
<p><code>Integer i1=40</code> 这一行代码会发生装箱，也就是说这行代码等价于 <code>Integer i1=Integer.valueOf(40)</code> 。因此，<code>i1</code> 直接使用的是缓存中的对象。而<code>Integer i2 = new Integer(40)</code> 会直接创建新的对象。</p>
<p>因此，答案是 <code>false</code> 。你答对了吗？</p>
<p>记住：<strong>所有整型包装类对象之间值的比较，全部使用 equals 方法比较</strong>。</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/17/41d8c926-10fc-4fbf-8f7c-8f554e3ab560.png" alt="" /></p>
<h3><a id="%E8%87%AA%E5%8A%A8%E8%A3%85%E7%AE%B1%E4%B8%8E%E6%8B%86%E7%AE%B1%E4%BA%86%E8%A7%A3%E5%90%97%EF%BC%9F%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>自动装箱与拆箱了解吗？原理是什么？</h3>
<p><strong>什么是自动拆装箱？</strong></p>
<ul>
<li><strong>装箱</strong>：将基本类型用它们对应的引用类型包装起来；</li>
<li><strong>拆箱</strong>：将包装类型转换为基本数据类型；</li>
</ul>
<p>举例：</p>
<pre><code class="language-java">Integer i = 10;  //装箱
int n = i;   //拆箱
</code></pre>
<p>上面这两行代码对应的字节码为：</p>
<pre><code class="language-java">   L1

    LINENUMBER 8 L1

    ALOAD 0

    BIPUSH 10

    INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;

    PUTFIELD AutoBoxTest.i : Ljava/lang/Integer;

   L2

    LINENUMBER 9 L2

    ALOAD 0

    ALOAD 0

    GETFIELD AutoBoxTest.i : Ljava/lang/Integer;

    INVOKEVIRTUAL java/lang/Integer.intValue ()I

    PUTFIELD AutoBoxTest.n : I

    RETURN
</code></pre>
<p>从字节码中，我们发现装箱其实就是调用了 包装类的<code>valueOf()</code>方法，拆箱其实就是调用了 <code>xxxValue()</code>方法。</p>
<p>因此，</p>
<ul>
<li><code>Integer i = 10</code> 等价于 <code>Integer i = Integer.valueOf(10)</code></li>
<li><code>int n = i</code> 等价于 <code>int n = i.intValue()</code>;</li>
</ul>
<p>注意：<strong>如果频繁拆装箱的话，也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。</strong></p>
<pre><code class="language-java">private static long sum() {
    // 应该使用 long 而不是 Long
    Long sum = 0L;
    for (long i = 0; i &lt;= Integer.MAX_VALUE; i++)
        sum += i;
    return sum;
}
</code></pre>
<h3><a id="%E4%B8%BA%E4%BB%80%E4%B9%88%E6%B5%AE%E7%82%B9%E6%95%B0%E8%BF%90%E7%AE%97%E7%9A%84%E6%97%B6%E5%80%99%E4%BC%9A%E6%9C%89%E7%B2%BE%E5%BA%A6%E4%B8%A2%E5%A4%B1%E7%9A%84%E9%A3%8E%E9%99%A9%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>为什么浮点数运算的时候会有精度丢失的风险？</h3>
<p>浮点数运算精度丢失代码演示：</p>
<pre><code class="language-java">float a = 2.0f - 1.9f;
float b = 1.8f - 1.7f;
System.out.printf(&quot;%.9f&quot;,a);// 0.100000024
System.out.println(b);// 0.099999905
System.out.println(a == b);// false
</code></pre>
<p>为什么会出现这个问题呢？</p>
<p>这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的，而且计算机在表示一个数字时，宽度是有限的，无限循环的小数存储在计算机时，只能被截断，所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。</p>
<p>就比如说十进制下的 0.2 就没办法精确转换成二进制小数：</p>
<pre><code class="language-java">// 0.2 转换为二进制数的过程为，不断乘以 2，直到不存在小数为止，
// 在这个计算过程中，得到的整数部分从上到下排列就是二进制的结果。
0.2 * 2 = 0.4 -&gt; 0
0.4 * 2 = 0.8 -&gt; 0
0.8 * 2 = 1.6 -&gt; 1
0.6 * 2 = 1.2 -&gt; 1
0.2 * 2 = 0.4 -&gt; 0（发生循环）
...
</code></pre>
<p>关于浮点数的更多内容，建议看一下<a href="http://kaito-kidd.com/2018/08/08/computer-system-float-point/">计算机系统基础（四）浮点数</a>这篇文章。</p>
<h3><a id="%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E6%B5%AE%E7%82%B9%E6%95%B0%E8%BF%90%E7%AE%97%E7%9A%84%E7%B2%BE%E5%BA%A6%E4%B8%A2%E5%A4%B1%E9%97%AE%E9%A2%98%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>如何解决浮点数运算的精度丢失问题？</h3>
<p><code>BigDecimal</code> 可以实现对浮点数的运算，不会造成精度丢失。通常情况下，大部分需要浮点数精确运算结果的业务场景（比如涉及到钱的场景）都是通过 <code>BigDecimal</code> 来做的。</p>
<pre><code class="language-java">BigDecimal a = new BigDecimal(&quot;1.0&quot;);
BigDecimal b = new BigDecimal(&quot;1.00&quot;);
BigDecimal c = new BigDecimal(&quot;0.8&quot;);

BigDecimal x = a.subtract(c);
BigDecimal y = b.subtract(c);

System.out.println(x); /* 0.2 */
System.out.println(y); /* 0.20 */
// 比较内容，不是比较值
System.out.println(Objects.equals(x, y)); /* false */
// 比较值相等用相等compareTo，相等返回0
System.out.println(0 == x.compareTo(y)); /* true */
</code></pre>
<p>关于 <code>BigDecimal</code> 的详细介绍，可以看看我写的这篇文章：<a href="17420865517478.html">BigDecimal 详解</a>。</p>
<h3><a id="%E8%B6%85%E8%BF%87long%E6%95%B4%E5%9E%8B%E7%9A%84%E6%95%B0%E6%8D%AE%E5%BA%94%E8%AF%A5%E5%A6%82%E4%BD%95%E8%A1%A8%E7%A4%BA%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>超过 long 整型的数据应该如何表示？</h3>
<p>基本数值类型都有一个表达范围，如果超过这个范围就会有数值溢出的风险。</p>
<p>在 Java 中，64 位 long 整型是最大的整数类型。</p>
<pre><code class="language-java">long l = Long.MAX_VALUE;
System.out.println(l + 1); // -9223372036854775808
System.out.println(l + 1 == Long.MIN_VALUE); // true
</code></pre>
<p><code>BigInteger</code> 内部使用 <code>int[]</code> 数组来存储任意大小的整形数据。</p>
<p>相对于常规整数类型的运算来说，<code>BigInteger</code> 运算的效率会相对较低。</p>
<h2><a id="%E5%8F%98%E9%87%8F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>变量</h2>
<h3><a id="%E6%88%90%E5%91%98%E5%8F%98%E9%87%8F%E4%B8%8E%E5%B1%80%E9%83%A8%E5%8F%98%E9%87%8F%E7%9A%84%E5%8C%BA%E5%88%AB%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>成员变量与局部变量的区别？</h3>
<ul>
<li><strong>语法形式</strong>：从语法形式上看，成员变量是属于类的，而局部变量是在代码块或方法中定义的变量或是方法的参数；成员变量可以被 <code>public</code>,<code>private</code>,<code>static</code> 等修饰符所修饰，而局部变量不能被访问控制修饰符及 <code>static</code> 所修饰；但是，成员变量和局部变量都能被 <code>final</code> 所修饰。</li>
<li><strong>存储方式</strong>：从变量在内存中的存储方式来看，如果成员变量是使用 <code>static</code> 修饰的，那么这个成员变量是属于类的，如果没有使用 <code>static</code> 修饰，这个成员变量是属于实例的。而对象存在于堆内存，局部变量则存在于栈内存。</li>
<li><strong>生存时间</strong>：从变量在内存中的生存时间上看，成员变量是对象的一部分，它随着对象的创建而存在，而局部变量随着方法的调用而自动生成，随着方法的调用结束而消亡。</li>
<li><strong>默认值</strong>：从变量是否有默认值来看，成员变量如果没有被赋初始值，则会自动以类型的默认值而赋值（一种情况例外:被 <code>final</code> 修饰的成员变量也必须显式地赋值），而局部变量则不会自动赋值。</li>
</ul>
<p><strong>为什么成员变量有默认值？</strong></p>
<ol>
<li>
<p>先不考虑变量类型，如果没有默认值会怎样？变量存储的是内存地址对应的任意随机值，程序读取该值运行会出现意外。</p>
</li>
<li>
<p>默认值有两种设置方式：手动和自动，根据第一点，没有手动赋值一定要自动赋值。成员变量在运行时可借助反射等方法手动赋值，而局部变量不行。</p>
</li>
<li>
<p>对于编译器（javac）来说，局部变量没赋值很好判断，可以直接报错。而成员变量可能是运行时赋值，无法判断，误报“没默认值”又会影响用户体验，所以采用自动赋默认值。</p>
</li>
</ol>
<p>成员变量与局部变量代码示例：</p>
<pre><code class="language-java">public class VariableExample {

    // 成员变量
    private String name;
    private int age;

    // 方法中的局部变量
    public void method() {
        int num1 = 10; // 栈中分配的局部变量
        String str = &quot;Hello, world!&quot;; // 栈中分配的局部变量
        System.out.println(num1);
        System.out.println(str);
    }

    // 带参数的方法中的局部变量
    public void method2(int num2) {
        int sum = num2 + 10; // 栈中分配的局部变量
        System.out.println(sum);
    }

    // 构造方法中的局部变量
    public VariableExample(String name, int age) {
        this.name = name; // 对成员变量进行赋值
        this.age = age; // 对成员变量进行赋值
        int num3 = 20; // 栈中分配的局部变量
        String str2 = &quot;Hello, &quot; + this.name + &quot;!&quot;; // 栈中分配的局部变量
        System.out.println(num3);
        System.out.println(str2);
    }
}

</code></pre>
<h3><a id="%E9%9D%99%E6%80%81%E5%8F%98%E9%87%8F%E6%9C%89%E4%BB%80%E4%B9%88%E4%BD%9C%E7%94%A8%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>静态变量有什么作用？</h3>
<p>静态变量也就是被 <code>static</code> 关键字修饰的变量。它可以被类的所有实例共享，无论一个类创建了多少个对象，它们都共享同一份静态变量。也就是说，静态变量只会被分配一次内存，即使创建多个对象，这样可以节省内存。</p>
<p>静态变量是通过类名来访问的，例如<code>StaticVariableExample.staticVar</code>（如果被 <code>private</code>关键字修饰就无法这样访问了）。</p>
<pre><code class="language-java">public class StaticVariableExample {
    // 静态变量
    public static int staticVar = 0;
}
</code></pre>
<p>通常情况下，静态变量会被 <code>final</code> 关键字修饰成为常量。</p>
<pre><code class="language-java">public class ConstantVariableExample {
    // 常量
    public static final int constantVar = 0;
}
</code></pre>
<h3><a id="%E5%AD%97%E7%AC%A6%E5%9E%8B%E5%B8%B8%E9%87%8F%E5%92%8C%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%B8%B8%E9%87%8F%E7%9A%84%E5%8C%BA%E5%88%AB" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>字符型常量和字符串常量的区别?</h3>
<ul>
<li><strong>形式</strong> : 字符常量是单引号引起的一个字符，字符串常量是双引号引起的 0 个或若干个字符。</li>
<li><strong>含义</strong> : 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)。</li>
<li><strong>占内存大小</strong>：字符常量只占 2 个字节; 字符串常量占若干个字节。</li>
</ul>
<p>⚠️ 注意 <code>char</code> 在 Java 中占两个字节。</p>
<p>字符型常量和字符串常量代码示例：</p>
<pre><code class="language-java">public class StringExample {
    // 字符型常量
    public static final char LETTER_A = 'A';

    // 字符串常量
    public static final String GREETING_MESSAGE = &quot;Hello, world!&quot;;
    public static void main(String[] args) {
        System.out.println(&quot;字符型常量占用的字节数为：&quot;+Character.BYTES);
        System.out.println(&quot;字符串常量占用的字节数为：&quot;+GREETING_MESSAGE.getBytes().length);
    }
}
</code></pre>
<p>输出：</p>
<pre><code class="language-plain">字符型常量占用的字节数为：2
字符串常量占用的字节数为：13
</code></pre>
<h2><a id="%E6%96%B9%E6%B3%95" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>方法</h2>
<h3><a id="%E4%BB%80%E4%B9%88%E6%98%AF%E6%96%B9%E6%B3%95%E7%9A%84%E8%BF%94%E5%9B%9E%E5%80%BC%E6%96%B9%E6%B3%95%E6%9C%89%E5%93%AA%E5%87%A0%E7%A7%8D%E7%B1%BB%E5%9E%8B%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>什么是方法的返回值?方法有哪几种类型？</h3>
<p><strong>方法的返回值</strong> 是指我们获取到的某个方法体中的代码执行后产生的结果！（前提是该方法可能产生结果）。返回值的作用是接收出结果，使得它可以用于其他的操作！</p>
<p>我们可以按照方法的返回值和参数类型将方法分为下面这几种：</p>
<p><strong>1、无参数无返回值的方法</strong></p>
<pre><code class="language-java">public void f1() {
    //......
}
// 下面这个方法也没有返回值，虽然用到了 return
public void f(int a) {
    if (...) {
        // 表示结束方法的执行,下方的输出语句不会执行
        return;
    }
    System.out.println(a);
}
</code></pre>
<p><strong>2、有参数无返回值的方法</strong></p>
<pre><code class="language-java">public void f2(Parameter 1, ..., Parameter n) {
    //......
}
</code></pre>
<p><strong>3、有返回值无参数的方法</strong></p>
<pre><code class="language-java">public int f3() {
    //......
    return x;
}
</code></pre>
<p><strong>4、有返回值有参数的方法</strong></p>
<pre><code class="language-java">public int f4(int a, int b) {
    return a * b;
}
</code></pre>
<h3><a id="%E9%9D%99%E6%80%81%E6%96%B9%E6%B3%95%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E8%83%BD%E8%B0%83%E7%94%A8%E9%9D%9E%E9%9D%99%E6%80%81%E6%88%90%E5%91%98" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>静态方法为什么不能调用非静态成员?</h3>
<p>这个需要结合 JVM 的相关知识，主要原因如下：</p>
<ol>
<li>静态方法是属于类的，在类加载的时候就会分配内存，可以通过类名直接访问。而非静态成员属于实例对象，只有在对象实例化之后才存在，需要通过类的实例对象去访问。</li>
<li>在类的非静态成员不存在的时候静态方法就已经存在了，此时调用在内存中还不存在的非静态成员，属于非法操作。</li>
</ol>
<pre><code class="language-java">public class Example {
    // 定义一个字符型常量
    public static final char LETTER_A = 'A';

    // 定义一个字符串常量
    public static final String GREETING_MESSAGE = &quot;Hello, world!&quot;;

    public static void main(String[] args) {
        // 输出字符型常量的值
        System.out.println(&quot;字符型常量的值为：&quot; + LETTER_A);

        // 输出字符串常量的值
        System.out.println(&quot;字符串常量的值为：&quot; + GREETING_MESSAGE);
    }
}
</code></pre>
<h3><a id="%E9%9D%99%E6%80%81%E6%96%B9%E6%B3%95%E5%92%8C%E5%AE%9E%E4%BE%8B%E6%96%B9%E6%B3%95%E6%9C%89%E4%BD%95%E4%B8%8D%E5%90%8C%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>静态方法和实例方法有何不同？</h3>
<p><strong>1、调用方式</strong></p>
<p>在外部调用静态方法时，可以使用 <code>类名.方法名</code> 的方式，也可以使用 <code>对象.方法名</code> 的方式，而实例方法只有后面这种方式。也就是说，<strong>调用静态方法可以无需创建对象</strong> 。</p>
<p>不过，需要注意的是一般不建议使用 <code>对象.方法名</code> 的方式来调用静态方法。这种方式非常容易造成混淆，静态方法不属于类的某个对象而是属于这个类。</p>
<p>因此，一般建议使用 <code>类名.方法名</code> 的方式来调用静态方法。</p>
<pre><code class="language-java">public class Person {
    public void method() {
      //......
    }

    public static void staicMethod(){
      //......
    }
    public static void main(String[] args) {
        Person person = new Person();
        // 调用实例方法
        person.method();
        // 调用静态方法
        Person.staicMethod()
    }
}
</code></pre>
<p><strong>2、访问类成员是否存在限制</strong></p>
<p>静态方法在访问本类的成员时，只允许访问静态成员（即静态成员变量和静态方法），不允许访问实例成员（即实例成员变量和实例方法），而实例方法不存在这个限制。</p>
<h3><a id="%E9%87%8D%E8%BD%BD%E5%92%8C%E9%87%8D%E5%86%99%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>重载和重写有什么区别？</h3>
<blockquote>
<p>重载就是同样的一个方法能够根据输入数据的不同，做出不同的处理</p>
<p>重写就是当子类继承自父类的相同方法，输入数据一样，但要做出有别于父类的响应时，你就要覆盖父类方法</p>
</blockquote>
<h4><a id="%E9%87%8D%E8%BD%BD" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>重载</h4>
<p>发生在同一个类中（或者父类和子类之间），方法名必须相同，参数类型不同、个数不同、顺序不同，方法返回值和访问修饰符可以不同。</p>
<p>《Java 核心技术》这本书是这样介绍重载的：</p>
<blockquote>
<p>如果多个方法(比如 <code>StringBuilder</code> 的构造方法)有相同的名字、不同的参数， 便产生了重载。</p>
<pre><code class="language-java">StringBuilder sb = new StringBuilder();
StringBuilder sb2 = new StringBuilder(&quot;HelloWorld&quot;);
</code></pre>
<p>编译器必须挑选出具体执行哪个方法，它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。 如果编译器找不到匹配的参数， 就会产生编译时错误， 因为根本不存在匹配， 或者没有一个比其他的更好(这个过程被称为重载解析(overloading resolution))。</p>
<p>Java 允许重载任何方法， 而不只是构造器方法。</p>
</blockquote>
<p>综上：重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。</p>
<h4><a id="%E9%87%8D%E5%86%99" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>重写</h4>
<p>重写发生在运行期，是子类对父类的允许访问的方法的实现过程进行重新编写。</p>
<ol>
<li>方法名、参数列表必须相同，子类方法返回值类型应比父类方法返回值类型更小或相等，抛出的异常范围小于等于父类，访问修饰符范围大于等于父类。</li>
<li>如果父类方法访问修饰符为 <code>private/final/static</code> 则子类就不能重写该方法，但是被 <code>static</code> 修饰的方法能够被再次声明。</li>
<li>构造方法无法被重写</li>
</ol>
<h4><a id="%E6%80%BB%E7%BB%93" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>总结</h4>
<p>综上：<strong>重写就是子类对父类方法的重新改造，外部样子不能改变，内部逻辑可以改变。</strong></p>
<table>
<thead>
<tr>
<th style="text-align: left">区别点</th>
<th style="text-align: left">重载方法</th>
<th style="text-align: left">重写方法</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left">发生范围</td>
<td style="text-align: left">同一个类</td>
<td style="text-align: left">子类</td>
</tr>
<tr>
<td style="text-align: left">参数列表</td>
<td style="text-align: left">必须修改</td>
<td style="text-align: left">一定不能修改</td>
</tr>
<tr>
<td style="text-align: left">返回类型</td>
<td style="text-align: left">可修改</td>
<td style="text-align: left">子类方法返回值类型应比父类方法返回值类型更小或相等</td>
</tr>
<tr>
<td style="text-align: left">异常</td>
<td style="text-align: left">可修改</td>
<td style="text-align: left">子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等；</td>
</tr>
<tr>
<td style="text-align: left">访问修饰符</td>
<td style="text-align: left">可修改</td>
<td style="text-align: left">一定不能做更严格的限制（可以降低限制）</td>
</tr>
<tr>
<td style="text-align: left">发生阶段</td>
<td style="text-align: left">编译期</td>
<td style="text-align: left">运行期</td>
</tr>
</tbody>
</table>
<p><strong>方法的重写要遵循“两同两小一大”</strong>（以下内容摘录自《疯狂 Java 讲义》，<a href="https://github.com/Snailclimb/JavaGuide/issues/892">issue#892</a> ）：</p>
<ul>
<li>“两同”即方法名相同、形参列表相同；</li>
<li>“两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等，子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等；</li>
<li>“一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。</li>
</ul>
<p>⭐️ 关于 <strong>重写的返回值类型</strong> 这里需要额外多说明一下，上面的表述不太清晰准确：如果方法的返回类型是 void 和基本数据类型，则返回值重写时不可修改。但是如果方法的返回值是引用类型，重写时是可以返回该引用类型的子类的。</p>
<pre><code class="language-java">public class Hero {
    public String name() {
        return &quot;超级英雄&quot;;
    }
}
public class SuperMan extends Hero{
    @Override
    public String name() {
        return &quot;超人&quot;;
    }
    public Hero hero() {
        return new Hero();
    }
}

public class SuperSuperMan extends SuperMan {
    @Override
    public String name() {
        return &quot;超级超级英雄&quot;;
    }

    @Override
    public SuperMan hero() {
        return new SuperMan();
    }
}
</code></pre>
<h3><a id="%E4%BB%80%E4%B9%88%E6%98%AF%E5%8F%AF%E5%8F%98%E9%95%BF%E5%8F%82%E6%95%B0%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>什么是可变长参数？</h3>
<p>从 Java5 开始，Java 支持定义可变长参数，所谓可变长参数就是允许在调用方法时传入不定长度的参数。就比如下面这个方法就可以接受 0 个或者多个参数。</p>
<pre><code class="language-java">public static void method1(String... args) {
   //......
}
</code></pre>
<p>另外，可变参数只能作为函数的最后一个参数，但其前面可以有也可以没有任何其他参数。</p>
<pre><code class="language-java">public static void method2(String arg1, String... args) {
   //......
}
</code></pre>
<p><strong>遇到方法重载的情况怎么办呢？会优先匹配固定参数还是可变参数的方法呢？</strong></p>
<p>答案是会优先匹配固定参数的方法，因为固定参数的方法匹配度更高。</p>
<p>我们通过下面这个例子来证明一下。</p>
<pre><code class="language-java">/**
 * 微信搜 JavaGuide 回复&quot;面试突击&quot;即可免费领取个人原创的 Java 面试手册
 *
 * @author Guide哥
 * @date 2021/12/13 16:52
 **/
public class VariableLengthArgument {

    public static void printVariable(String... args) {
        for (String s : args) {
            System.out.println(s);
        }
    }

    public static void printVariable(String arg1, String arg2) {
        System.out.println(arg1 + arg2);
    }

    public static void main(String[] args) {
        printVariable(&quot;a&quot;, &quot;b&quot;);
        printVariable(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;, &quot;d&quot;);
    }
}
</code></pre>
<p>输出：</p>
<pre><code class="language-plain">ab
a
b
c
d
</code></pre>
<p>另外，Java 的可变参数编译后实际会被转换成一个数组，我们看编译后生成的 <code>class</code>文件就可以看出来了。</p>
<pre><code class="language-java">public class VariableLengthArgument {

    public static void printVariable(String... args) {
        String[] var1 = args;
        int var2 = args.length;

        for(int var3 = 0; var3 &lt; var2; ++var3) {
            String s = var1[var3];
            System.out.println(s);
        }

    }
    // ......
}
</code></pre>
<h2><a id="%E5%8F%82%E8%80%83" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>参考</h2>
<ul>
<li>What is the difference between JDK and JRE?：<a href="https://stackoverflow.com/questions/1906445/what-is-the-difference-between-jdk-and-jre">https://stackoverflow.com/questions/1906445/what-is-the-difference-between-jdk-and-jre</a></li>
<li>Oracle vs OpenJDK：<a href="https://www.educba.com/oracle-vs-openjdk/">https://www.educba.com/oracle-vs-openjdk/</a></li>
<li>Differences between Oracle JDK and OpenJDK：<a href="https://stackoverflow.com/questions/22358071/differences-between-oracle-jdk-and-openjdk">https://stackoverflow.com/questions/22358071/differences-between-oracle-jdk-and-openjdk</a></li>
<li>彻底弄懂 Java 的移位操作符：<a href="https://juejin.cn/post/6844904025880526861">https://juejin.cn/post/6844904025880526861</a></li>
</ul>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[01-Java并发常见面试题总结（上）]]></title>
    <link href="https://huanglei.work/17419990613781.html"/>
    <updated>2025-03-15T08:37:41+08:00</updated>
    <id>https://huanglei.work/17419990613781.html</id>
    <content type="html"><![CDATA[
<blockquote>
<p>本文内容来自：<a href="https://javaguide.cn/java/concurrent/java-concurrent-questions-01.html">JavaGuide-Java并发常见面试题总结（上）</a></p>
</blockquote>
<ul>
<li><a href="#%E7%BA%BF%E7%A8%8B">线程</a>
<ul>
<li><a href="#%E2%AD%90%EF%B8%8F%E4%BB%80%E4%B9%88%E6%98%AF%E7%BA%BF%E7%A8%8B%E5%92%8C%E8%BF%9B%E7%A8%8B">⭐️什么是线程和进程?</a>
<ul>
<li><a href="#%E4%BD%95%E4%B8%BA%E8%BF%9B%E7%A8%8B">何为进程?</a></li>
<li><a href="#%E4%BD%95%E4%B8%BA%E7%BA%BF%E7%A8%8B">何为线程?</a></li>
</ul>
</li>
<li><a href="#java%E7%BA%BF%E7%A8%8B%E5%92%8C%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%9C%89%E5%95%A5%E5%8C%BA%E5%88%AB%EF%BC%9F">Java 线程和操作系统的线程有啥区别？</a></li>
<li><a href="#%E2%AD%90%EF%B8%8F%E8%AF%B7%E7%AE%80%E8%A6%81%E6%8F%8F%E8%BF%B0%E7%BA%BF%E7%A8%8B%E4%B8%8E%E8%BF%9B%E7%A8%8B%E7%9A%84%E5%85%B3%E7%B3%BB%EF%BC%8C%E5%8C%BA%E5%88%AB%E5%8F%8A%E4%BC%98%E7%BC%BA%E7%82%B9%EF%BC%9F">⭐️请简要描述线程与进程的关系，区别及优缺点？</a>
<ul>
<li><a href="#%E7%A8%8B%E5%BA%8F%E8%AE%A1%E6%95%B0%E5%99%A8%E4%B8%BA%E4%BB%80%E4%B9%88%E6%98%AF%E7%A7%81%E6%9C%89%E7%9A%84">程序计数器为什么是私有的?</a></li>
<li><a href="#%E8%99%9A%E6%8B%9F%E6%9C%BA%E6%A0%88%E5%92%8C%E6%9C%AC%E5%9C%B0%E6%96%B9%E6%B3%95%E6%A0%88%E4%B8%BA%E4%BB%80%E4%B9%88%E6%98%AF%E7%A7%81%E6%9C%89%E7%9A%84">虚拟机栈和本地方法栈为什么是私有的?</a></li>
<li><a href="#%E4%B8%80%E5%8F%A5%E8%AF%9D%E7%AE%80%E5%8D%95%E4%BA%86%E8%A7%A3%E5%A0%86%E5%92%8C%E6%96%B9%E6%B3%95%E5%8C%BA">一句话简单了解堆和方法区</a></li>
</ul>
</li>
<li><a href="#%E5%A6%82%E4%BD%95%E5%88%9B%E5%BB%BA%E7%BA%BF%E7%A8%8B%EF%BC%9F">如何创建线程？</a></li>
<li><a href="#%E2%AD%90%EF%B8%8F%E8%AF%B4%E8%AF%B4%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E5%92%8C%E7%8A%B6%E6%80%81">⭐️说说线程的生命周期和状态?</a></li>
<li><a href="#%E4%BB%80%E4%B9%88%E6%98%AF%E7%BA%BF%E7%A8%8B%E4%B8%8A%E4%B8%8B%E6%96%87%E5%88%87%E6%8D%A2">什么是线程上下文切换?</a></li>
<li><a href="#thread-sleep%E6%96%B9%E6%B3%95%E5%92%8C-object-wait%E6%96%B9%E6%B3%95%E5%AF%B9%E6%AF%94">Thread#sleep() 方法和 Object#wait() 方法对比</a></li>
<li><a href="#%E4%B8%BA%E4%BB%80%E4%B9%88wait%E6%96%B9%E6%B3%95%E4%B8%8D%E5%AE%9A%E4%B9%89%E5%9C%A8-thread%E4%B8%AD%EF%BC%9F">为什么 wait() 方法不定义在 Thread 中？</a></li>
<li><a href="#%E5%8F%AF%E4%BB%A5%E7%9B%B4%E6%8E%A5%E8%B0%83%E7%94%A8thread%E7%B1%BB%E7%9A%84-run%E6%96%B9%E6%B3%95%E5%90%97%EF%BC%9F">可以直接调用 Thread 类的 run 方法吗？</a></li>
</ul>
</li>
<li><a href="#%E5%A4%9A%E7%BA%BF%E7%A8%8B">多线程</a>
<ul>
<li><a href="#%E5%B9%B6%E5%8F%91%E4%B8%8E%E5%B9%B6%E8%A1%8C%E7%9A%84%E5%8C%BA%E5%88%AB">并发与并行的区别</a></li>
<li><a href="#%E5%90%8C%E6%AD%A5%E5%92%8C%E5%BC%82%E6%AD%A5%E7%9A%84%E5%8C%BA%E5%88%AB">同步和异步的区别</a></li>
<li><a href="#%E2%AD%90%EF%B8%8F%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E4%BD%BF%E7%94%A8%E5%A4%9A%E7%BA%BF%E7%A8%8B">⭐️为什么要使用多线程?</a></li>
<li><a href="#%E2%AD%90%EF%B8%8F%E5%8D%95%E6%A0%B8cpu%E6%94%AF%E6%8C%81-java%E5%A4%9A%E7%BA%BF%E7%A8%8B%E5%90%97%EF%BC%9F">⭐️单核 CPU 支持 Java 多线程吗？</a></li>
<li><a href="#%E2%AD%90%EF%B8%8F%E5%8D%95%E6%A0%B8cpu%E4%B8%8A%E8%BF%90%E8%A1%8C%E5%A4%9A%E4%B8%AA%E7%BA%BF%E7%A8%8B%E6%95%88%E7%8E%87%E4%B8%80%E5%AE%9A%E4%BC%9A%E9%AB%98%E5%90%97%EF%BC%9F">⭐️单核 CPU 上运行多个线程效率一定会高吗？</a></li>
<li><a href="#%E4%BD%BF%E7%94%A8%E5%A4%9A%E7%BA%BF%E7%A8%8B%E5%8F%AF%E8%83%BD%E5%B8%A6%E6%9D%A5%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98">使用多线程可能带来什么问题?</a></li>
<li><a href="#%E5%A6%82%E4%BD%95%E7%90%86%E8%A7%A3%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E5%92%8C%E4%B8%8D%E5%AE%89%E5%85%A8%EF%BC%9F">如何理解线程安全和不安全？</a></li>
</ul>
</li>
<li><a href="#%E2%AD%90%EF%B8%8F%E6%AD%BB%E9%94%81">⭐️死锁</a>
<ul>
<li><a href="#%E4%BB%80%E4%B9%88%E6%98%AF%E7%BA%BF%E7%A8%8B%E6%AD%BB%E9%94%81%EF%BC%9F">什么是线程死锁？</a></li>
<li><a href="#%E5%A6%82%E4%BD%95%E6%A3%80%E6%B5%8B%E6%AD%BB%E9%94%81%EF%BC%9F">如何检测死锁？</a></li>
</ul>
</li>
<li><a href="#%E4%B8%AA%E4%BA%BA%E8%A1%A5%E5%85%85">个人补充</a>
<ul>
<li><a href="#%E5%A6%82%E4%BD%95%E9%A2%84%E9%98%B2%E5%92%8C%E9%81%BF%E5%85%8D%E7%BA%BF%E7%A8%8B%E6%AD%BB%E9%94%81">如何预防和避免线程死锁?</a></li>
</ul>
</li>
<li><a href="#%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B">虚拟线程</a></li>
</ul>
<h2><a id="%E7%BA%BF%E7%A8%8B" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>线程</h2>
<h3><a id="%E2%AD%90%EF%B8%8F%E4%BB%80%E4%B9%88%E6%98%AF%E7%BA%BF%E7%A8%8B%E5%92%8C%E8%BF%9B%E7%A8%8B" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️什么是线程和进程?</h3>
<h4><a id="%E4%BD%95%E4%B8%BA%E8%BF%9B%E7%A8%8B" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>何为进程?</h4>
<p>进程是程序的一次执行过程，是系统运行程序的基本单位，因此进程是动态的。系统运行一个程序即是一个进程从创建，运行到消亡的过程。</p>
<p>在 Java 中，当我们启动 main 函数时其实就是启动了一个 JVM 的进程，而 main 函数所在的线程就是这个进程中的一个线程，也称主线程。</p>
<p>如下图所示，在 Windows 中通过查看任务管理器的方式，我们就可以清楚看到 Windows 当前运行的进程（<code>.exe</code> 文件的运行）。</p>
<p><img src="media/17419990613781/%E8%BF%9B%E7%A8%8B%E7%A4%BA%E4%BE%8B%E5%9B%BE%E7%89%87-Windows.png" alt="进程示例图片-Windows" /></p>
<h4><a id="%E4%BD%95%E4%B8%BA%E7%BA%BF%E7%A8%8B" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>何为线程?</h4>
<p>线程与进程相似，但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的<strong>堆</strong>和<strong>方法区</strong>资源，但每个线程有自己的<strong>程序计数器</strong>、<strong>虚拟机栈</strong>和<strong>本地方法栈</strong>，所以系统在产生一个线程，或是在各个线程之间做切换工作时，负担要比进程小得多，也正因为如此，线程也被称为轻量级进程。</p>
<p>Java 程序天生就是多线程程序，我们可以通过 JMX 来看看一个普通的 Java 程序有哪些线程，代码如下。</p>
<pre><code class="language-java">public class MultiThread {
    public static void main(String[] args) {
        // 获取 Java 线程管理 MXBean
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        // 不需要获取同步的 monitor 和 synchronizer 信息，仅获取线程和线程堆栈信息
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
        // 遍历线程信息，仅打印线程 ID 和线程名称信息
        for (ThreadInfo threadInfo : threadInfos) {
            System.out.println(&quot;[&quot; + threadInfo.getThreadId() + &quot;] &quot; + threadInfo.getThreadName());
        }
    }
}
</code></pre>
<p>上述程序输出如下（输出内容可能不同，不用太纠结下面每个线程的作用，只用知道 main 线程执行 main 方法即可）：</p>
<pre><code class="language-plain">[5] Attach Listener //添加事件
[4] Signal Dispatcher // 分发处理给 JVM 信号的线程
[3] Finalizer //调用对象 finalize 方法的线程
[2] Reference Handler //清除 reference 线程
[1] main //main 线程,程序入口
</code></pre>
<p>从上面的输出内容可以看出：<strong>一个 Java 程序的运行是 main 线程和多个其他线程同时运行</strong>。</p>
<h3><a id="java%E7%BA%BF%E7%A8%8B%E5%92%8C%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%9C%89%E5%95%A5%E5%8C%BA%E5%88%AB%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Java 线程和操作系统的线程有啥区别？</h3>
<p>JDK 1.2 之前，Java 线程是基于绿色线程（Green Threads）实现的，这是一种用户级线程（用户线程），也就是说 JVM 自己模拟了多线程的运行，而不依赖于操作系统。由于绿色线程和原生线程比起来在使用时有一些限制（比如绿色线程不能直接使用操作系统提供的功能如异步 I/O、只能在一个内核线程上运行无法利用多核），在 JDK 1.2 及以后，Java 线程改为基于原生线程（Native Threads）实现，也就是说 JVM 直接使用操作系统原生的内核级线程（内核线程）来实现 Java 线程，由操作系统内核进行线程的调度和管理。</p>
<p>我们上面提到了用户线程和内核线程，考虑到很多读者不太了解二者的区别，这里简单介绍一下：</p>
<ul>
<li>用户线程：由用户空间程序管理和调度的线程，运行在用户空间（专门给应用程序使用）。</li>
<li>内核线程：由操作系统内核管理和调度的线程，运行在内核空间（只有内核程序可以访问）。</li>
</ul>
<p>顺便简单总结一下用户线程和内核线程的区别和特点：用户线程创建和切换成本低，但不可以利用多核。内核态线程，创建和切换成本高，可以利用多核。</p>
<p>一句话概括 Java 线程和操作系统线程的关系：<strong>现在的 Java 线程的本质其实就是操作系统的线程</strong>。</p>
<hr />
<p><strong>✨个人补充</strong></p>
<ol>
<li>
<p><strong>Java线程与原生线程的关系</strong>：从JDK 1.2开始，Java线程改为基于操作系统原生线程（Native Threads）实现。这意味着每个Java线程在底层被映射到一个操作系统级别的线程，这些线程由操作系统内核进行调度和管理。但这并不意味着Java线程变成了内核线程，它们仍然是用户态的线程。</p>
</li>
<li>
<p><strong>用户态与内核态</strong>：尽管Java线程对应于操作系统级别的线程，它们主要运行在用户空间。只有当需要执行特定操作，如I/O操作、访问硬件资源或请求系统服务时，才会发生从用户态到内核态的转换。</p>
</li>
<li>
<p><strong>I/O操作的过程</strong>：在Java应用程序中，当某个线程发起I/O请求时，这个请求通过系统调用触发从用户态到内核态的转换。操作系统内核负责处理这个I/O请求，并在完成后将结果返回给用户态下的Java线程。这样确保了对系统资源的安全访问，并让操作系统能够有效地管理硬件资源。</p>
</li>
</ol>
<p>综上所述，虽然Java线程利用了操作系统提供的原生线程机制来增强其多线程处理能力，但它们本质上还是用户线程，在涉及到如I/O等需要操作系统介入的操作时，依然需要进行用户态和内核态之间的转换。这一机制既保证了系统的安全性也提高了程序执行效率。</p>
<hr />
<p>线程模型是用户线程和内核线程之间的关联方式，常见的线程模型有这三种：</p>
<ol>
<li>一对一（一个用户线程对应一个内核线程）</li>
<li>多对一（多个用户线程映射到一个内核线程）</li>
<li>多对多（多个用户线程映射到多个内核线程）</li>
</ol>
<p><img src="media/17419990613781/three-types-of-thread-models.png" alt="常见的三种线程模型" /></p>
<p>在 Windows 和 Linux 等主流操作系统中，Java 线程采用的是一对一的线程模型，也就是一个 Java 线程对应一个系统内核线程。Solaris 系统是一个特例（Solaris 系统本身就支持多对多的线程模型），HotSpot VM 在 Solaris 上支持多对多和一对一。具体可以参考 R 大的回答: <a href="https://www.zhihu.com/question/23096638/answer/29617153">JVM 中的线程模型是用户级的么？</a>。</p>
<h3><a id="%E2%AD%90%EF%B8%8F%E8%AF%B7%E7%AE%80%E8%A6%81%E6%8F%8F%E8%BF%B0%E7%BA%BF%E7%A8%8B%E4%B8%8E%E8%BF%9B%E7%A8%8B%E7%9A%84%E5%85%B3%E7%B3%BB%EF%BC%8C%E5%8C%BA%E5%88%AB%E5%8F%8A%E4%BC%98%E7%BC%BA%E7%82%B9%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️请简要描述线程与进程的关系，区别及优缺点？</h3>
<p>下图是 Java 内存区域，通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。</p>
<p><img src="media/17419990613781/java-runtime-data-areas-jdk1.8.png" alt="Java 运行时数据区域（JDK1.8 之后）" /></p>
<p>从上图可以看出：一个进程中可以有多个线程，多个线程共享进程的<strong>堆</strong>和<strong>方法区 (JDK1.8 之后的元空间)<strong>资源，但是每个线程有自己的</strong>程序计数器</strong>、<strong>虚拟机栈</strong> 和 <strong>本地方法栈</strong>。</p>
<p><strong>总结：</strong> <strong>线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的，而各线程则不一定，因为同一进程中的线程极有可能会相互影响。线程执行开销小，但不利于资源的管理和保护；而进程正相反。</strong></p>
<p>下面是该知识点的扩展内容！</p>
<p>下面来思考这样一个问题：为什么<strong>程序计数器</strong>、<strong>虚拟机栈</strong>和<strong>本地方法栈</strong>是线程私有的呢？为什么堆和方法区是线程共享的呢？</p>
<h4><a id="%E7%A8%8B%E5%BA%8F%E8%AE%A1%E6%95%B0%E5%99%A8%E4%B8%BA%E4%BB%80%E4%B9%88%E6%98%AF%E7%A7%81%E6%9C%89%E7%9A%84" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>程序计数器为什么是私有的?</h4>
<p>程序计数器主要有下面两个作用：</p>
<ol>
<li>字节码解释器通过改变程序计数器来依次读取指令，从而实现代码的流程控制，如：顺序执行、选择、循环、异常处理。</li>
<li>在多线程的情况下，程序计数器用于记录当前线程执行的位置，从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。</li>
</ol>
<p>需要注意的是，如果执行的是 native 方法，那么程序计数器记录的是 undefined 地址，只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。</p>
<p>所以，程序计数器私有主要是为了<strong>线程切换后能恢复到正确的执行位置</strong>。</p>
<h4><a id="%E8%99%9A%E6%8B%9F%E6%9C%BA%E6%A0%88%E5%92%8C%E6%9C%AC%E5%9C%B0%E6%96%B9%E6%B3%95%E6%A0%88%E4%B8%BA%E4%BB%80%E4%B9%88%E6%98%AF%E7%A7%81%E6%9C%89%E7%9A%84" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>虚拟机栈和本地方法栈为什么是私有的?</h4>
<ul>
<li><strong>虚拟机栈：</strong> 每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程，就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。</li>
<li><strong>本地方法栈：</strong> 和虚拟机栈所发挥的作用非常相似，区别是：<strong>虚拟机栈为虚拟机执行 Java 方法 （也就是字节码）服务，而本地方法栈则为虚拟机使用到的 Native 方法服务。</strong> 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。</li>
</ul>
<p>所以，为了<strong>保证线程中的局部变量不被别的线程访问到</strong>，虚拟机栈和本地方法栈是线程私有的。</p>
<h4><a id="%E4%B8%80%E5%8F%A5%E8%AF%9D%E7%AE%80%E5%8D%95%E4%BA%86%E8%A7%A3%E5%A0%86%E5%92%8C%E6%96%B9%E6%B3%95%E5%8C%BA" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>一句话简单了解堆和方法区</h4>
<p>堆和方法区是所有线程共享的资源，其中堆是进程中最大的一块内存，主要用于存放新创建的对象 (几乎所有对象都在这里分配内存)，方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。</p>
<h3><a id="%E5%A6%82%E4%BD%95%E5%88%9B%E5%BB%BA%E7%BA%BF%E7%A8%8B%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>如何创建线程？</h3>
<p>一般来说，创建线程有很多种方式，例如继承<code>Thread</code>类、实现<code>Runnable</code>接口、实现<code>Callable</code>接口、使用线程池、使用<code>CompletableFuture</code>类等等。</p>
<p>不过，这些方式其实并没有真正创建出线程。准确点来说，这些都属于是在 Java 代码中使用多线程的方法。</p>
<p>严格来说，Java 就只有一种方式可以创建线程，那就是通过<code>new Thread().start()</code>创建。不管是哪种方式，最终还是依赖于<code>new Thread().start()</code>。</p>
<p>关于这个问题的详细分析可以查看这篇文章：<a href="https://mp.weixin.qq.com/s/NspUsyhEmKnJ-4OprRFp9g">大家都说 Java 有三种创建线程的方式！并发编程中的惊天骗局！</a>。</p>
<h3><a id="%E2%AD%90%EF%B8%8F%E8%AF%B4%E8%AF%B4%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E5%92%8C%E7%8A%B6%E6%80%81" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️说说线程的生命周期和状态?</h3>
<p>Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态：</p>
<ul>
<li>NEW: 初始状态，线程被创建出来但没有被调用 <code>start()</code> 。</li>
<li>RUNNABLE: 运行状态，线程被调用了 <code>start()</code>等待运行的状态。</li>
<li>BLOCKED：阻塞状态，需要等待锁释放。</li>
<li>WAITING：等待状态，表示该线程需要等待其他线程做出一些特定动作（通知或中断）。</li>
<li>TIME_WAITING：超时等待状态，可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。</li>
<li>TERMINATED：终止状态，表示该线程已经运行完毕。</li>
</ul>
<p>线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。</p>
<p>Java 线程状态变迁图(图源：<a href="https://mp.weixin.qq.com/s/UOrXql_LhOD8dhTq_EPI0w">挑错 |《Java 并发编程的艺术》中关于线程状态的三处错误</a>)：</p>
<p><img src="media/17419990613781/640.png" alt="Java 线程状态变迁图" /></p>
<p>由上图可以看出：线程创建之后它将处于 <strong>NEW（新建）</strong> 状态，调用 <code>start()</code> 方法后开始运行，线程这时候处于 <strong>READY（可运行）</strong> 状态。可运行状态的线程获得了 CPU 时间片（timeslice）后就处于 <strong>RUNNING（运行）</strong> 状态。</p>
<blockquote>
<p>在操作系统层面，线程有 READY 和 RUNNING 状态；而在 JVM 层面，只能看到 RUNNABLE 状态（图源：<a href="https://howtodoinJava.com/" title="HowToDoInJava">HowToDoInJava</a>：<a href="https://howtodoinJava.com/Java/multi-threading/Java-thread-life-cycle-and-thread-states/" title="Java Thread Life Cycle and Thread States">Java Thread Life Cycle and Thread States</a>），所以 Java 系统一般将这两个状态统称为 <strong>RUNNABLE（运行中）</strong> 状态 。</p>
<p><strong>为什么 JVM 没有区分这两种状态呢？</strong> （摘自：<a href="https://www.zhihu.com/question/56494969/answer/154053599">Java 线程运行怎么有第六种状态？ - Dawell 的回答</a> ） 现在的时分（time-sharing）多任务（multi-task）操作系统架构通常都是用所谓的“时间分片（time quantum or time slice）”方式进行抢占式（preemptive）轮转调度（round-robin 式）。这个时间分片通常是很小的，一个线程一次最多只能在 CPU 上运行比如 10-20ms 的时间（此时处于 running 状态），也即大概只有 0.01 秒这一量级，时间片用后就要被切换下来放入调度队列的末尾等待再次调度。（也即回到 ready 状态）。线程切换的如此之快，区分这两种状态就没什么意义了。</p>
</blockquote>
<p><img src="media/17419990613781/RUNNABLE-VS-RUNNING.png" alt="RUNNABLE-VS-RUNNING" /></p>
<ul>
<li>当线程执行 <code>wait()</code>方法之后，线程进入 <strong>WAITING（等待）</strong> 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。</li>
<li><strong>TIMED_WAITING(超时等待)</strong> 状态相当于在等待状态的基础上增加了超时限制，比如通过 <code>sleep（long millis）</code>方法或 <code>wait（long millis）</code>方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后，线程将会返回到 RUNNABLE 状态。</li>
<li>当线程进入 <code>synchronized</code> 方法/块或者调用 <code>wait</code> 后（被 <code>notify</code>）重新进入 <code>synchronized</code> 方法/块，但是锁被其它线程占有，这个时候线程就会进入 <strong>BLOCKED（阻塞）</strong> 状态。</li>
<li>线程在执行完了 <code>run()</code>方法之后将会进入到 <strong>TERMINATED（终止）</strong> 状态。</li>
</ul>
<p>相关阅读：<a href="https://mp.weixin.qq.com/s/R5MrTsWvk9McFSQ7bS0W2w">线程的几种状态你真的了解么？</a> 。</p>
<h3><a id="%E4%BB%80%E4%B9%88%E6%98%AF%E7%BA%BF%E7%A8%8B%E4%B8%8A%E4%B8%8B%E6%96%87%E5%88%87%E6%8D%A2" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>什么是线程上下文切换?</h3>
<p>线程在执行过程中会有自己的运行条件和状态（也称上下文），比如上文所说到过的程序计数器，栈信息等。当出现如下情况的时候，线程会从占用 CPU 状态中退出。</p>
<ul>
<li>主动让出 CPU，比如调用了 <code>sleep()</code>, <code>wait()</code> 等。</li>
<li>时间片用完，因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。</li>
<li>调用了阻塞类型的系统中断，比如请求 IO，线程被阻塞。</li>
<li>被终止或结束运行</li>
</ul>
<p>这其中前三种都会发生线程切换，线程切换意味着需要保存当前线程的上下文，留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 <strong>上下文切换</strong>。</p>
<p>上下文切换是现代操作系统的基本功能，因其每次需要保存信息恢复信息，这将会占用 CPU，内存等系统资源进行处理，也就意味着效率会有一定损耗，如果频繁切换就会造成整体效率低下。</p>
<h3><a id="thread-sleep%E6%96%B9%E6%B3%95%E5%92%8C-object-wait%E6%96%B9%E6%B3%95%E5%AF%B9%E6%AF%94" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Thread#sleep() 方法和 Object#wait() 方法对比</h3>
<p><strong>共同点</strong>：两者都可以暂停线程的执行。</p>
<p><strong>区别</strong>：</p>
<ul>
<li><strong><code>sleep()</code> 方法没有释放锁，而 <code>wait()</code> 方法释放了锁</strong> 。</li>
<li><code>wait()</code> 通常被用于线程间交互/通信，<code>sleep()</code>通常被用于暂停执行。</li>
<li><code>wait()</code> 方法被调用后，线程不会自动苏醒，需要别的线程调用同一个对象上的 <code>notify()</code>或者 <code>notifyAll()</code> 方法。<code>sleep()</code>方法执行完成后，线程会自动苏醒，或者也可以使用 <code>wait(long timeout)</code> 超时后线程会自动苏醒。</li>
<li><code>sleep()</code> 是 <code>Thread</code> 类的静态本地方法，<code>wait()</code> 则是 <code>Object</code> 类的本地方法。为什么这样设计呢？下一个问题就会聊到。</li>
</ul>
<h3><a id="%E4%B8%BA%E4%BB%80%E4%B9%88wait%E6%96%B9%E6%B3%95%E4%B8%8D%E5%AE%9A%E4%B9%89%E5%9C%A8-thread%E4%B8%AD%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>为什么 wait() 方法不定义在 Thread 中？</h3>
<p><code>wait()</code> 是让获得对象锁的线程实现等待，会自动释放当前线程占有的对象锁。每个对象（<code>Object</code>）都拥有对象锁，既然要释放当前线程占有的对象锁并让其进入 WAITING 状态，自然是要操作对应的对象（<code>Object</code>）而非当前的线程（<code>Thread</code>）。</p>
<p>类似的问题：<strong>为什么 <code>sleep()</code> 方法定义在 <code>Thread</code> 中？</strong></p>
<p>因为 <code>sleep()</code> 是让当前线程暂停执行，不涉及到对象类，也不需要获得对象锁。</p>
<hr />
<p><strong>✨个人补充</strong></p>
<p>调用 <code>wait()</code> 的前提是当前线程必须拥有该对象的对象锁，否则会抛出 <code>IllegalMonitorStateException</code> 异常。当一个线程在某个对象上调用了 <code>wait()</code> 方法时，它会释放这个对象的锁，并进入等待状态，直到其他线程通过同一个对象调用 <code>notify()</code> 或 <code>notifyAll()</code> 来唤醒它。</p>
<p>下面是一个简单的代码示例，演示了如何使用 <code>wait()</code> 和 <code>notifyAll()</code> 方法：</p>
<pre><code class="language-java">public class WaitNotifyExample {

    private static final Object lock = new Object();
    private static boolean condition = false;

    public static void main(String[] args) throws InterruptedException {
        Thread waitingThread = new Thread(() -&gt; {
            synchronized (lock) {
                // Check the condition under the lock and wait if it's not met
                while (!condition) {
                    try {
                        System.out.println(&quot;Waiting thread: Condition is not met. Waiting...&quot;);
                        lock.wait(); // Releases the lock and waits
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        System.out.println(&quot;Waiting thread was interrupted.&quot;);
                    }
                }
                System.out.println(&quot;Waiting thread: Condition met. Continuing execution.&quot;);
            }
        });

        Thread notifyingThread = new Thread(() -&gt; {
            try {
                Thread.sleep(2000); // Simulate time-consuming work
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            synchronized (lock) {
                condition = true; // Change the condition
                System.out.println(&quot;Notifying thread: Condition changed. Notifying all waiting threads.&quot;);
                lock.notifyAll(); // Notify all waiting threads
            }
        });

        waitingThread.start();
        notifyingThread.start();

        waitingThread.join();
        notifyingThread.join();
    }
}
</code></pre>
<p>Output</p>
<pre><code class="language-plain">Waiting thread: Condition is not met. Waiting...
Notifying thread: Condition changed. Notifying all waiting threads.
Waiting thread: Condition met. Continuing execution.
</code></pre>
<p>在这个例子中：</p>
<ul>
<li>
<p><code>waitingThread</code> 线程尝试获取 <code>lock</code> 对象的锁，并检查 <code>condition</code> 是否为 <code>true</code>。如果条件不满足，它将调用 <code>lock.wait()</code> 方法，释放 <code>lock</code> 锁并进入等待状态。</p>
</li>
<li>
<p><code>notifyingThread</code> 线程模拟执行一些耗时的工作（通过 <code>Thread.sleep(2000)</code>），然后获取 <code>lock</code> 锁，改变 <code>condition</code> 的值，并调用 <code>lock.notifyAll()</code> 方法来唤醒所有在 <code>lock</code> 上等待的线程。</p>
</li>
</ul>
<p>这个案例展示了 <code>wait()</code> 和 <code>notifyAll()</code> 如何配合使用来实现线程间的通信和同步。注意，为了正确地使用这些方法，必须确保它们被调用时已经获取了相关对象的锁（即在同步块或同步方法内调用）。</p>
<hr />
<h3><a id="%E5%8F%AF%E4%BB%A5%E7%9B%B4%E6%8E%A5%E8%B0%83%E7%94%A8thread%E7%B1%BB%E7%9A%84-run%E6%96%B9%E6%B3%95%E5%90%97%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>可以直接调用 Thread 类的 run 方法吗？</h3>
<p>这是另一个非常经典的 Java 多线程面试问题，而且在面试中会经常被问到。很简单，但是很多人都会答不上来！</p>
<p>new 一个 <code>Thread</code>，线程进入了新建状态。调用 <code>start()</code>方法，会启动一个线程并使线程进入了就绪状态，当分配到时间片后就可以开始运行了。 <code>start()</code> 会执行线程的相应准备工作，然后自动执行 <code>run()</code> 方法的内容，这是真正的多线程工作。 但是，直接执行 <code>run()</code> 方法，会把 <code>run()</code> 方法当成一个 main 线程下的普通方法去执行，并不会在某个线程中执行它，所以这并不是多线程工作。</p>
<p><strong>总结：调用 <code>start()</code> 方法方可启动线程并使线程进入就绪状态，直接执行 <code>run()</code> 方法的话不会以多线程的方式执行。</strong></p>
<h2><a id="%E5%A4%9A%E7%BA%BF%E7%A8%8B" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>多线程</h2>
<h3><a id="%E5%B9%B6%E5%8F%91%E4%B8%8E%E5%B9%B6%E8%A1%8C%E7%9A%84%E5%8C%BA%E5%88%AB" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>并发与并行的区别</h3>
<ul>
<li><strong>并发</strong>：两个及两个以上的作业在同一 <strong>时间段</strong> 内执行。</li>
<li><strong>并行</strong>：两个及两个以上的作业在同一 <strong>时刻</strong> 执行。</li>
</ul>
<p>最关键的点是：是否是 <strong>同时</strong> 执行。</p>
<h3><a id="%E5%90%8C%E6%AD%A5%E5%92%8C%E5%BC%82%E6%AD%A5%E7%9A%84%E5%8C%BA%E5%88%AB" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>同步和异步的区别</h3>
<ul>
<li><strong>同步</strong>：发出一个调用之后，在没有得到结果之前， 该调用就不可以返回，一直等待。</li>
<li><strong>异步</strong>：调用在发出之后，不用等待返回结果，该调用直接返回。</li>
</ul>
<h3><a id="%E2%AD%90%EF%B8%8F%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E4%BD%BF%E7%94%A8%E5%A4%9A%E7%BA%BF%E7%A8%8B" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️为什么要使用多线程?</h3>
<p>先从总体上来说：</p>
<ul>
<li><strong>从计算机底层来说：</strong> 线程可以比作是轻量级的进程，是程序执行的最小单位，线程间的切换和调度的成本远远小于进程。另外，多核 CPU 时代意味着多个线程可以同时运行，这减少了线程上下文切换的开销。</li>
<li><strong>从当代互联网发展趋势来说：</strong> 现在的系统动不动就要求百万级甚至千万级的并发量，而多线程并发编程正是开发高并发系统的基础，利用好多线程机制可以大大提高系统整体的并发能力以及性能。</li>
</ul>
<p>再深入到计算机底层来探讨：</p>
<ul>
<li><strong>单核时代</strong>：在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了一个 Java 进程的情况，当我们请求 IO 的时候，如果 Java 进程中只有一个线程，此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行，那么可以简单地说系统整体效率只有 50%。当使用多线程的时候，一个线程被 IO 阻塞，其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。</li>
<li><strong>多核时代</strong>: 多核时代多线程主要是为了提高进程利用多核 CPU 的能力。举个例子：假如我们要计算一个复杂的任务，我们只用一个线程的话，不论系统有几个 CPU 核心，都只会有一个 CPU 核心被利用到。而创建多个线程，这些线程可以被映射到底层多个 CPU 核心上执行，在任务中的多个线程没有资源竞争的情况下，任务执行的效率会有显著性的提高，约等于（单核时执行时间/CPU 核心数）。</li>
</ul>
<h3><a id="%E2%AD%90%EF%B8%8F%E5%8D%95%E6%A0%B8cpu%E6%94%AF%E6%8C%81-java%E5%A4%9A%E7%BA%BF%E7%A8%8B%E5%90%97%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️单核 CPU 支持 Java 多线程吗？</h3>
<p>单核 CPU 是支持 Java 多线程的。操作系统通过时间片轮转的方式，将 CPU 的时间分配给不同的线程。尽管单核 CPU 一次只能执行一个任务，但通过快速在多个线程之间切换，可以让用户感觉多个任务是同时进行的。</p>
<p>这里顺带提一下 Java 使用的线程调度方式。</p>
<p>操作系统主要通过两种线程调度方式来管理多线程的执行：</p>
<ul>
<li><strong>抢占式调度（Preemptive Scheduling）</strong>：操作系统决定何时暂停当前正在运行的线程，并切换到另一个线程执行。这种切换通常是由系统时钟中断（时间片轮转）或其他高优先级事件（如 I/O 操作完成）触发的。这种方式存在上下文切换开销，但公平性和 CPU 资源利用率较好，不易阻塞。</li>
<li><strong>协同式调度（Cooperative Scheduling）</strong>：线程执行完毕后，主动通知系统切换到另一个线程。这种方式可以减少上下文切换带来的性能开销，但公平性较差，容易阻塞。</li>
</ul>
<p>Java 使用的线程调度是抢占式的。也就是说，JVM 本身不负责线程的调度，而是将线程的调度委托给操作系统。操作系统通常会基于线程优先级和时间片来调度线程的执行，高优先级的线程通常获得 CPU 时间片的机会更多。</p>
<h3><a id="%E2%AD%90%EF%B8%8F%E5%8D%95%E6%A0%B8cpu%E4%B8%8A%E8%BF%90%E8%A1%8C%E5%A4%9A%E4%B8%AA%E7%BA%BF%E7%A8%8B%E6%95%88%E7%8E%87%E4%B8%80%E5%AE%9A%E4%BC%9A%E9%AB%98%E5%90%97%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️单核 CPU 上运行多个线程效率一定会高吗？</h3>
<p>单核 CPU 同时运行多个线程的效率是否会高，取决于线程的类型和任务的性质。一般来说，有两种类型的线程：</p>
<ol>
<li><strong>CPU 密集型</strong>：CPU 密集型的线程主要进行计算和逻辑处理，需要占用大量的 CPU 资源。</li>
<li><strong>IO 密集型</strong>：IO 密集型的线程主要进行输入输出操作，如读写文件、网络通信等，需要等待 IO 设备的响应，而不占用太多的 CPU 资源。</li>
</ol>
<p>在单核 CPU 上，同一时刻只能有一个线程在运行，其他线程需要等待 CPU 的时间片分配。如果线程是 CPU 密集型的，那么多个线程同时运行会导致频繁的线程切换，增加了系统的开销，降低了效率。如果线程是 IO 密集型的，那么多个线程同时运行可以利用 CPU 在等待 IO 时的空闲时间，提高了效率。</p>
<p>因此，对于单核 CPU 来说，如果任务是 CPU 密集型的，那么开很多线程会影响效率；如果任务是 IO 密集型的，那么开很多线程会提高效率。当然，这里的“很多”也要适度，不能超过系统能够承受的上限。</p>
<h3><a id="%E4%BD%BF%E7%94%A8%E5%A4%9A%E7%BA%BF%E7%A8%8B%E5%8F%AF%E8%83%BD%E5%B8%A6%E6%9D%A5%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>使用多线程可能带来什么问题?</h3>
<p>并发编程的目的就是为了能提高程序的执行效率进而提高程序的运行速度，但是并发编程并不总是能提高程序运行速度的，而且并发编程可能会遇到很多问题，比如：内存泄漏、死锁、线程不安全等等。</p>
<h3><a id="%E5%A6%82%E4%BD%95%E7%90%86%E8%A7%A3%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E5%92%8C%E4%B8%8D%E5%AE%89%E5%85%A8%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>如何理解线程安全和不安全？</h3>
<p>线程安全和不安全是在多线程环境下对于同一份数据的访问是否能够保证其正确性和一致性的描述。</p>
<ul>
<li>线程安全指的是在多线程环境下，对于同一份数据，不管有多少个线程同时访问，都能保证这份数据的正确性和一致性。</li>
<li>线程不安全则表示在多线程环境下，对于同一份数据，多个线程同时访问时可能会导致数据混乱、错误或者丢失。</li>
</ul>
<h2><a id="%E2%AD%90%EF%B8%8F%E6%AD%BB%E9%94%81" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️死锁</h2>
<h3><a id="%E4%BB%80%E4%B9%88%E6%98%AF%E7%BA%BF%E7%A8%8B%E6%AD%BB%E9%94%81%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>什么是线程死锁？</h3>
<p>线程死锁描述的是这样一种情况：多个线程同时被阻塞，它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞，因此程序不可能正常终止。</p>
<p>如下图所示，线程 A 持有资源 2，线程 B 持有资源 1，他们同时都想申请对方的资源，所以这两个线程就会互相等待而进入死锁状态。</p>
<p><img src="media/17419990613781/2019-4%E6%AD%BB%E9%94%811.png" alt="线程死锁示意图 " /></p>
<p>下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》)：</p>
<pre><code class="language-java">public class DeadLockDemo {
    private static Object resource1 = new Object();//资源 1
    private static Object resource2 = new Object();//资源 2

    public static void main(String[] args) {
        new Thread(() -&gt; {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + &quot;get resource1&quot;);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + &quot;waiting get resource2&quot;);
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + &quot;get resource2&quot;);
                }
            }
        }, &quot;线程 1&quot;).start();

        new Thread(() -&gt; {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + &quot;get resource2&quot;);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + &quot;waiting get resource1&quot;);
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + &quot;get resource1&quot;);
                }
            }
        }, &quot;线程 2&quot;).start();
    }
}
</code></pre>
<p>Output</p>
<pre><code class="language-plain">Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1
</code></pre>
<p>线程 A 通过 <code>synchronized (resource1)</code> 获得 <code>resource1</code> 的监视器锁，然后通过 <code>Thread.sleep(1000);</code> 让线程 A 休眠 1s，为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源，然后这两个线程就会陷入互相等待的状态，这也就产生了死锁。</p>
<p>上面的例子符合产生死锁的四个必要条件：</p>
<ol>
<li><strong>互斥条件</strong>：该资源任意一个时刻只由一个线程占用。</li>
<li><strong>请求与保持条件</strong>：一个线程因请求资源而阻塞时，对已获得的资源保持不放。</li>
<li><strong>不剥夺条件</strong>：线程已获得的资源在未使用完之前不能被其他线程强行剥夺，只有自己使用完毕后才释放资源。</li>
<li><strong>循环等待条件</strong>：若干线程之间形成一种头尾相接的循环等待资源关系。</li>
</ol>
<h3><a id="%E5%A6%82%E4%BD%95%E6%A3%80%E6%B5%8B%E6%AD%BB%E9%94%81%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>如何检测死锁？</h3>
<ul>
<li>使用<code>jmap</code>、<code>jstack</code>等命令查看 JVM 线程栈和堆内存的情况。如果有死锁，<code>jstack</code> 的输出中通常会有 <code>Found one Java-level deadlock:</code>的字样，后面会跟着死锁相关的线程信息。另外，实际项目中还可以搭配使用<code>top</code>、<code>df</code>、<code>free</code>等命令查看操作系统的基本情况，出现死锁可能会导致 CPU、内存等资源消耗过高。</li>
<li>采用 VisualVM、JConsole 等工具进行排查。</li>
</ul>
<p>这里以 JConsole 工具为例进行演示。</p>
<p>首先，我们要找到 JDK 的 bin 目录，找到 jconsole 并双击打开。</p>
<p><img src="media/17419990613781/jdk-home-bin-jconsole.png" alt="jconsole" /></p>
<p>对于 MAC 用户来说，可以通过 <code>/usr/libexec/java_home -V</code>查看 JDK 安装目录，找到后通过 <code>open . + 文件夹地址</code>打开即可。例如，我本地的某个 JDK 的路径是：</p>
<pre><code class="language-bash"> open . /Users/guide/Library/Java/JavaVirtualMachines/corretto-1.8.0_252/Contents/Home
</code></pre>
<p>打开 jconsole 后，连接对应的程序，然后进入线程界面选择检测死锁即可！</p>
<p><img src="media/17419990613781/jconsole-check-deadlock.png" alt="jconsole 检测死锁" /></p>
<p><img src="media/17419990613781/jconsole-check-deadlock-done.png" alt="jconsole 检测到死锁" /></p>
<hr />
<h2><a id="%E4%B8%AA%E4%BA%BA%E8%A1%A5%E5%85%85" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>个人补充</h2>
<p>jstack 检测死锁<br />
<img src="media/17419990613781/17420082163763.jpg" alt="" /><br />
<img src="media/17419990613781/17420082593893.jpg" alt="" /></p>
<hr />
<h3><a id="%E5%A6%82%E4%BD%95%E9%A2%84%E9%98%B2%E5%92%8C%E9%81%BF%E5%85%8D%E7%BA%BF%E7%A8%8B%E6%AD%BB%E9%94%81" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>如何预防和避免线程死锁?</h3>
<p><strong>如何预防死锁？</strong> 破坏死锁的产生的必要条件即可：</p>
<ol>
<li><strong>破坏请求与保持条件</strong>：一次性申请所有的资源。</li>
<li><strong>破坏不剥夺条件</strong>：占用部分资源的线程进一步申请其他资源时，如果申请不到，可以主动释放它占有的资源。</li>
<li><strong>破坏循环等待条件</strong>：靠按序申请资源来预防。按某一顺序申请资源，释放资源则反序释放。破坏循环等待条件。</li>
</ol>
<p><strong>如何避免死锁？</strong></p>
<p>避免死锁就是在资源分配时，借助于算法（比如银行家算法）对资源分配进行计算评估，使其进入安全状态。</p>
<blockquote>
<p><strong>安全状态</strong> 指的是系统能够按照某种线程推进顺序（P1、P2、P3……Pn）来为每个线程分配所需资源，直到满足每个线程对资源的最大需求，使每个线程都可顺利完成。称 <code>&lt;P1、P2、P3.....Pn&gt;</code> 序列为安全序列。</p>
</blockquote>
<p>我们对线程 2 的代码修改成下面这样就不会产生死锁了。</p>
<pre><code class="language-java">new Thread(() -&gt; {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + &quot;get resource1&quot;);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + &quot;waiting get resource2&quot;);
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + &quot;get resource2&quot;);
                }
            }
        }, &quot;线程 2&quot;).start();
</code></pre>
<p>输出：</p>
<pre><code class="language-plain">Thread[线程 1,5,main]get resource1
Thread[线程 1,5,main]waiting get resource2
Thread[线程 1,5,main]get resource2
Thread[线程 2,5,main]get resource1
Thread[线程 2,5,main]waiting get resource2
Thread[线程 2,5,main]get resource2

Process finished with exit code 0
</code></pre>
<p>我们分析一下上面的代码为什么避免了死锁的发生?</p>
<p>线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁，可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用，线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件，因此避免了死锁。</p>
<h2><a id="%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>虚拟线程</h2>
<p>虚拟线程在 Java 21 正式发布，这是一项重量级的更新。我写了一篇文章来总结虚拟线程常见的问题：<a href="17419993880731.html">虚拟线程常见问题总结</a>，包含下面这些问题：</p>
<ol>
<li>什么是虚拟线程？</li>
<li>虚拟线程和平台线程有什么关系？</li>
<li>虚拟线程有什么优点和缺点？</li>
<li>如何创建虚拟线程？</li>
<li>虚拟线程的底层原理是什么？</li>
</ol>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[01-Java集合常见面试题总结(上)]]></title>
    <link href="https://huanglei.work/17420847480629.html"/>
    <updated>2025-03-16T08:25:48+08:00</updated>
    <id>https://huanglei.work/17420847480629.html</id>
    <content type="html"><![CDATA[
<ul>
<li><a href="#%E9%9B%86%E5%90%88%E6%A6%82%E8%BF%B0">集合概述</a>
<ul>
<li><a href="#java%E9%9B%86%E5%90%88%E6%A6%82%E8%A7%88">Java 集合概览</a></li>
<li><a href="#%E8%AF%B4%E8%AF%B4list-set-queue-map%E5%9B%9B%E8%80%85%E7%9A%84%E5%8C%BA%E5%88%AB%EF%BC%9F">说说 List, Set, Queue, Map 四者的区别？</a></li>
<li><a href="#%E9%9B%86%E5%90%88%E6%A1%86%E6%9E%B6%E5%BA%95%E5%B1%82%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E6%80%BB%E7%BB%93">集合框架底层数据结构总结</a>
<ul>
<li><a href="#list">List</a></li>
<li><a href="#set">Set</a></li>
<li><a href="#queue">Queue</a></li>
<li><a href="#map">Map</a></li>
</ul>
</li>
<li><a href="#%E5%A6%82%E4%BD%95%E9%80%89%E7%94%A8%E9%9B%86%E5%90%88">如何选用集合?</a></li>
<li><a href="#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E4%BD%BF%E7%94%A8%E9%9B%86%E5%90%88%EF%BC%9F">为什么要使用集合？</a></li>
</ul>
</li>
<li><a href="#list">List</a>
<ul>
<li><a href="#arraylist%E5%92%8C-array%EF%BC%88%E6%95%B0%E7%BB%84%EF%BC%89%E7%9A%84%E5%8C%BA%E5%88%AB%EF%BC%9F">ArrayList 和 Array（数组）的区别？</a></li>
<li><a href="#arraylist%E5%92%8C-vector%E7%9A%84%E5%8C%BA%E5%88%AB%EF%BC%88%E4%BA%86%E8%A7%A3%E5%8D%B3%E5%8F%AF%EF%BC%89">ArrayList 和 Vector 的区别?（了解即可）</a></li>
<li><a href="#vector%E5%92%8C-stack%E7%9A%84%E5%8C%BA%E5%88%AB%EF%BC%88%E4%BA%86%E8%A7%A3%E5%8D%B3%E5%8F%AF%EF%BC%89">Vector 和 Stack 的区别?（了解即可）</a></li>
<li><a href="#arraylist%E5%8F%AF%E4%BB%A5%E6%B7%BB%E5%8A%A0-null%E5%80%BC%E5%90%97%EF%BC%9F">ArrayList 可以添加 null 值吗？</a></li>
<li><a href="#arraylist%E6%8F%92%E5%85%A5%E5%92%8C%E5%88%A0%E9%99%A4%E5%85%83%E7%B4%A0%E7%9A%84%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6%EF%BC%9F">ArrayList 插入和删除元素的时间复杂度？</a></li>
<li><a href="#linkedlist%E6%8F%92%E5%85%A5%E5%92%8C%E5%88%A0%E9%99%A4%E5%85%83%E7%B4%A0%E7%9A%84%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6%EF%BC%9F">LinkedList 插入和删除元素的时间复杂度？</a></li>
<li><a href="#linkedlist%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E8%83%BD%E5%AE%9E%E7%8E%B0-randomaccess%E6%8E%A5%E5%8F%A3%EF%BC%9F">LinkedList 为什么不能实现 RandomAccess 接口？</a></li>
<li><a href="#arraylist%E4%B8%8E-linkedlist%E5%8C%BA%E5%88%AB">ArrayList 与 LinkedList 区别?</a>
<ul>
<li><a href="#%E8%A1%A5%E5%85%85%E5%86%85%E5%AE%B9%E5%8F%8C%E5%90%91%E9%93%BE%E8%A1%A8%E5%92%8C%E5%8F%8C%E5%90%91%E5%BE%AA%E7%8E%AF%E9%93%BE%E8%A1%A8">补充内容: 双向链表和双向循环链表</a></li>
<li><a href="#%E8%A1%A5%E5%85%85%E5%86%85%E5%AE%B9randomaccess%E6%8E%A5%E5%8F%A3">补充内容:RandomAccess 接口</a></li>
</ul>
</li>
<li><a href="#%E8%AF%B4%E4%B8%80%E8%AF%B4arraylist%E7%9A%84%E6%89%A9%E5%AE%B9%E6%9C%BA%E5%88%B6%E5%90%A7">说一说 ArrayList 的扩容机制吧</a></li>
<li><a href="#%E8%AF%B4%E8%AF%B4%E9%9B%86%E5%90%88%E4%B8%AD%E7%9A%84fail-fast%E5%92%8C-fail-safe%E6%98%AF%E4%BB%80%E4%B9%88">说说集合中的 fail-fast 和 fail-safe 是什么</a></li>
</ul>
</li>
<li><a href="#set">Set</a>
<ul>
<li><a href="#comparable%E5%92%8C-comparator%E7%9A%84%E5%8C%BA%E5%88%AB">Comparable 和 Comparator 的区别</a>
<ul>
<li><a href="#comparator%E5%AE%9A%E5%88%B6%E6%8E%92%E5%BA%8F">Comparator 定制排序</a></li>
<li><a href="#%E9%87%8D%E5%86%99compareto%E6%96%B9%E6%B3%95%E5%AE%9E%E7%8E%B0%E6%8C%89%E5%B9%B4%E9%BE%84%E6%9D%A5%E6%8E%92%E5%BA%8F">重写 compareTo 方法实现按年龄来排序</a></li>
</ul>
</li>
<li><a href="#%E6%97%A0%E5%BA%8F%E6%80%A7%E5%92%8C%E4%B8%8D%E5%8F%AF%E9%87%8D%E5%A4%8D%E6%80%A7%E7%9A%84%E5%90%AB%E4%B9%89%E6%98%AF%E4%BB%80%E4%B9%88">无序性和不可重复性的含义是什么</a></li>
<li><a href="#%E6%AF%94%E8%BE%83hashset%E3%80%81linkedhashset%E5%92%8C-treeset%E4%B8%89%E8%80%85%E7%9A%84%E5%BC%82%E5%90%8C">比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同</a></li>
</ul>
</li>
<li><a href="#queue">Queue</a>
<ul>
<li><a href="#queue%E4%B8%8E-deque%E7%9A%84%E5%8C%BA%E5%88%AB">Queue 与 Deque 的区别</a></li>
<li><a href="#arraydeque%E4%B8%8E-linkedlist%E7%9A%84%E5%8C%BA%E5%88%AB">ArrayDeque 与 LinkedList 的区别</a></li>
<li><a href="#%E8%AF%B4%E4%B8%80%E8%AF%B4priorityqueue">说一说 PriorityQueue</a></li>
<li><a href="#%E4%BB%80%E4%B9%88%E6%98%AFblockingqueue%EF%BC%9F">什么是 BlockingQueue？</a></li>
<li><a href="#blockingqueue%E7%9A%84%E5%AE%9E%E7%8E%B0%E7%B1%BB%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F">BlockingQueue 的实现类有哪些？</a></li>
<li><a href="#arrayblockingqueue%E5%92%8C-linkedblockingqueue%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F">ArrayBlockingQueue 和 LinkedBlockingQueue 有什么区别？</a></li>
</ul>
</li>
</ul>
<h2><a id="%E9%9B%86%E5%90%88%E6%A6%82%E8%BF%B0" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>集合概述</h2>
<h3><a id="java%E9%9B%86%E5%90%88%E6%A6%82%E8%A7%88" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Java 集合概览</h3>
<p>Java 集合，也叫作容器，主要是由两大接口派生而来：一个是 <code>Collection</code>接口，主要用于存放单一元素；另一个是 <code>Map</code> 接口，主要用于存放键值对。对于<code>Collection</code> 接口，下面又有三个主要的子接口：<code>List</code>、<code>Set</code> 、 <code>Queue</code>。</p>
<p>Java 集合框架如下图所示：</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/5030be79-3a08-4425-93c7-1ffa8073ad3b.png" alt="Java 集合框架概览" /></p>
<p>注：图中只列举了主要的继承派生关系，并没有列举所有关系。比方省略了<code>AbstractList</code>, <code>NavigableSet</code>等抽象类以及其他的一些辅助类，如想深入了解，可自行查看源码。</p>
<h3><a id="%E8%AF%B4%E8%AF%B4list-set-queue-map%E5%9B%9B%E8%80%85%E7%9A%84%E5%8C%BA%E5%88%AB%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>说说 List, Set, Queue, Map 四者的区别？</h3>
<ul>
<li><code>List</code>(对付顺序的好帮手): 存储的元素是有序的、可重复的。</li>
<li><code>Set</code>(注重独一无二的性质): 存储的元素不可重复的。</li>
<li><code>Queue</code>(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序，存储的元素是有序的、可重复的。</li>
<li><code>Map</code>(用 key 来搜索的专家): 使用键值对（key-value）存储，类似于数学上的函数 y=f(x)，&quot;x&quot; 代表 key，&quot;y&quot; 代表 value，key 是无序的、不可重复的，value 是无序的、可重复的，每个键最多映射到一个值。</li>
</ul>
<h3><a id="%E9%9B%86%E5%90%88%E6%A1%86%E6%9E%B6%E5%BA%95%E5%B1%82%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E6%80%BB%E7%BB%93" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>集合框架底层数据结构总结</h3>
<p>先来看一下 <code>Collection</code> 接口下面的集合。</p>
<h4><a id="list" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>List</h4>
<ul>
<li><code>ArrayList</code>：<code>Object[]</code> 数组。详细可以查看：<a href="17420847480909.html">ArrayList 源码分析</a>。</li>
<li><code>Vector</code>：<code>Object[]</code> 数组。</li>
<li><code>LinkedList</code>：双向链表(JDK1.6 之前为循环链表，JDK1.7 取消了循环)。详细可以查看：<a href="17420847480449.html">LinkedList 源码分析</a>。</li>
</ul>
<h4><a id="set" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Set</h4>
<ul>
<li><code>HashSet</code>(无序，唯一): 基于 <code>HashMap</code> 实现的，底层采用 <code>HashMap</code> 来保存元素。</li>
<li><code>LinkedHashSet</code>: <code>LinkedHashSet</code> 是 <code>HashSet</code> 的子类，并且其内部是通过 <code>LinkedHashMap</code> 来实现的。</li>
<li><code>TreeSet</code>(有序，唯一): 红黑树(自平衡的排序二叉树)。</li>
</ul>
<h4><a id="queue" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Queue</h4>
<ul>
<li><code>PriorityQueue</code>: <code>Object[]</code> 数组来实现小顶堆。详细可以查看：<a href="17420847480358.html">PriorityQueue 源码分析</a>。</li>
<li><code>DelayQueue</code>:<code>PriorityQueue</code>。详细可以查看：<a href="17420847480799.html">DelayQueue 源码分析</a>。</li>
<li><code>ArrayDeque</code>: 可扩容动态双向数组。</li>
</ul>
<p>再来看看 <code>Map</code> 接口下面的集合。</p>
<h4><a id="map" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Map</h4>
<ul>
<li><code>HashMap</code>：JDK1.8 之前 <code>HashMap</code> 由数组+链表组成的，数组是 <code>HashMap</code> 的主体，链表则是主要为了解决哈希冲突而存在的（“拉链法”解决冲突）。JDK1.8 以后在解决哈希冲突时有了较大的变化，当链表长度大于阈值（默认为 8）（将链表转换成红黑树前会判断，如果当前数组的长度小于 64，那么会选择先进行数组扩容，而不是转换为红黑树）时，将链表转化为红黑树，以减少搜索时间。详细可以查看：<a href="17420847480761.html">HashMap 源码分析</a>。</li>
<li><code>LinkedHashMap</code>：<code>LinkedHashMap</code> 继承自 <code>HashMap</code>，所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外，<code>LinkedHashMap</code> 在上面结构的基础上，增加了一条双向链表，使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作，实现了访问顺序相关逻辑。详细可以查看：<a href="17420847480526.html">LinkedHashMap 源码分析</a></li>
<li><code>Hashtable</code>：数组+链表组成的，数组是 <code>Hashtable</code> 的主体，链表则是主要为了解决哈希冲突而存在的。</li>
<li><code>TreeMap</code>：红黑树（自平衡的排序二叉树）。</li>
</ul>
<h3><a id="%E5%A6%82%E4%BD%95%E9%80%89%E7%94%A8%E9%9B%86%E5%90%88" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>如何选用集合?</h3>
<p>我们主要根据集合的特点来选择合适的集合。比如：</p>
<ul>
<li>我们需要根据键值获取到元素值时就选用 <code>Map</code> 接口下的集合，需要排序时选择 <code>TreeMap</code>,不需要排序时就选择 <code>HashMap</code>,需要保证线程安全就选用 <code>ConcurrentHashMap</code>。</li>
<li>我们只需要存放元素值时，就选择实现<code>Collection</code> 接口的集合，需要保证元素唯一时选择实现 <code>Set</code> 接口的集合比如 <code>TreeSet</code> 或 <code>HashSet</code>，不需要就选择实现 <code>List</code> 接口的比如 <code>ArrayList</code> 或 <code>LinkedList</code>，然后再根据实现这些接口的集合的特点来选用。</li>
</ul>
<h3><a id="%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E4%BD%BF%E7%94%A8%E9%9B%86%E5%90%88%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>为什么要使用集合？</h3>
<p>当我们需要存储一组类型相同的数据时，数组是最常用且最基本的容器之一。但是，使用数组存储对象存在一些不足之处，因为在实际开发中，存储的数据类型多种多样且数量不确定。这时，Java 集合就派上用场了。与数组相比，Java 集合提供了更灵活、更有效的方法来存储多个数据对象。Java 集合框架中的各种集合类和接口可以存储不同类型和数量的对象，同时还具有多样化的操作方式。相较于数组，Java 集合的优势在于它们的大小可变、支持泛型、具有内建算法等。总的来说，Java 集合提高了数据的存储和处理灵活性，可以更好地适应现代软件开发中多样化的数据需求，并支持高质量的代码编写。</p>
<h2><a id="list" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>List</h2>
<h3><a id="arraylist%E5%92%8C-array%EF%BC%88%E6%95%B0%E7%BB%84%EF%BC%89%E7%9A%84%E5%8C%BA%E5%88%AB%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>ArrayList 和 Array（数组）的区别？</h3>
<p><code>ArrayList</code> 内部基于动态数组实现，比 <code>Array</code>（静态数组） 使用起来更加灵活：</p>
<ul>
<li><code>ArrayList</code>会根据实际存储的元素动态地扩容或缩容，而 <code>Array</code> 被创建之后就不能改变它的长度了。</li>
<li><code>ArrayList</code> 允许你使用泛型来确保类型安全，<code>Array</code> 则不可以。</li>
<li><code>ArrayList</code> 中只能存储对象。对于基本类型数据，需要使用其对应的包装类（如 Integer、Double 等）。<code>Array</code> 可以直接存储基本类型数据，也可以存储对象。</li>
<li><code>ArrayList</code> 支持插入、删除、遍历等常见操作，并且提供了丰富的 API 操作方法，比如 <code>add()</code>、<code>remove()</code>等。<code>Array</code> 只是一个固定长度的数组，只能按照下标访问其中的元素，不具备动态添加、删除元素的能力。</li>
<li><code>ArrayList</code>创建时不需要指定大小，而<code>Array</code>创建时必须指定大小。</li>
</ul>
<p>下面是二者使用的简单对比：</p>
<p><code>Array</code>：</p>
<pre><code class="language-java"> // 初始化一个 String 类型的数组
 String[] stringArr = new String[]{&quot;hello&quot;, &quot;world&quot;, &quot;!&quot;};
 // 修改数组元素的值
 stringArr[0] = &quot;goodbye&quot;;
 System.out.println(Arrays.toString(stringArr));// [goodbye, world, !]
 // 删除数组中的元素，需要手动移动后面的元素
 for (int i = 0; i &lt; stringArr.length - 1; i++) {
     stringArr[i] = stringArr[i + 1];
 }
 stringArr[stringArr.length - 1] = null;
 System.out.println(Arrays.toString(stringArr));// [world, !, null]
</code></pre>
<p><code>ArrayList</code> ：</p>
<pre><code class="language-java">// 初始化一个 String 类型的 ArrayList
 ArrayList&lt;String&gt; stringList = new ArrayList&lt;&gt;(Arrays.asList(&quot;hello&quot;, &quot;world&quot;, &quot;!&quot;));
// 添加元素到 ArrayList 中
 stringList.add(&quot;goodbye&quot;);
 System.out.println(stringList);// [hello, world, !, goodbye]
 // 修改 ArrayList 中的元素
 stringList.set(0, &quot;hi&quot;);
 System.out.println(stringList);// [hi, world, !, goodbye]
 // 删除 ArrayList 中的元素
 stringList.remove(0);
 System.out.println(stringList); // [world, !, goodbye]
</code></pre>
<h3><a id="arraylist%E5%92%8C-vector%E7%9A%84%E5%8C%BA%E5%88%AB%EF%BC%88%E4%BA%86%E8%A7%A3%E5%8D%B3%E5%8F%AF%EF%BC%89" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>ArrayList 和 Vector 的区别?（了解即可）</h3>
<ul>
<li><code>ArrayList</code> 是 <code>List</code> 的主要实现类，底层使用 <code>Object[]</code>存储，适用于频繁的查找工作，线程不安全 。</li>
<li><code>Vector</code> 是 <code>List</code> 的古老实现类，底层使用<code>Object[]</code> 存储，线程安全。</li>
</ul>
<h3><a id="vector%E5%92%8C-stack%E7%9A%84%E5%8C%BA%E5%88%AB%EF%BC%88%E4%BA%86%E8%A7%A3%E5%8D%B3%E5%8F%AF%EF%BC%89" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Vector 和 Stack 的区别?（了解即可）</h3>
<ul>
<li><code>Vector</code> 和 <code>Stack</code> 两者都是线程安全的，都是使用 <code>synchronized</code> 关键字进行同步处理。</li>
<li><code>Stack</code> 继承自 <code>Vector</code>，是一个后进先出的栈，而 <code>Vector</code> 是一个列表。</li>
</ul>
<p>随着 Java 并发编程的发展，<code>Vector</code> 和 <code>Stack</code> 已经被淘汰，推荐使用并发集合类（例如 <code>ConcurrentHashMap</code>、<code>CopyOnWriteArrayList</code> 等）或者手动实现线程安全的方法来提供安全的多线程操作支持。</p>
<h3><a id="arraylist%E5%8F%AF%E4%BB%A5%E6%B7%BB%E5%8A%A0-null%E5%80%BC%E5%90%97%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>ArrayList 可以添加 null 值吗？</h3>
<p><code>ArrayList</code> 中可以存储任何类型的对象，包括 <code>null</code> 值。不过，不建议向<code>ArrayList</code> 中添加 <code>null</code> 值， <code>null</code> 值无意义，会让代码难以维护比如忘记做判空处理就会导致空指针异常。</p>
<p>示例代码：</p>
<pre><code class="language-java">ArrayList&lt;String&gt; listOfStrings = new ArrayList&lt;&gt;();
listOfStrings.add(null);
listOfStrings.add(&quot;java&quot;);
System.out.println(listOfStrings);
</code></pre>
<p>输出：</p>
<pre><code class="language-plain">[null, java]
</code></pre>
<h3><a id="arraylist%E6%8F%92%E5%85%A5%E5%92%8C%E5%88%A0%E9%99%A4%E5%85%83%E7%B4%A0%E7%9A%84%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>ArrayList 插入和删除元素的时间复杂度？</h3>
<p>对于插入：</p>
<ul>
<li>头部插入：由于需要将所有元素都依次向后移动一个位置，因此时间复杂度是 O(n)。</li>
<li>尾部插入：当 <code>ArrayList</code> 的容量未达到极限时，往列表末尾插入元素的时间复杂度是 O(1)，因为它只需要在数组末尾添加一个元素即可；当容量已达到极限并且需要扩容时，则需要执行一次 O(n) 的操作将原数组复制到新的更大的数组中，然后再执行 O(1) 的操作添加元素。</li>
<li>指定位置插入：需要将目标位置之后的所有元素都向后移动一个位置，然后再把新元素放入指定位置。这个过程需要移动平均 n/2 个元素，因此时间复杂度为 O(n)。</li>
</ul>
<p>对于删除：</p>
<ul>
<li>头部删除：由于需要将所有元素依次向前移动一个位置，因此时间复杂度是 O(n)。</li>
<li>尾部删除：当删除的元素位于列表末尾时，时间复杂度为 O(1)。</li>
<li>指定位置删除：需要将目标元素之后的所有元素向前移动一个位置以填补被删除的空白位置，因此需要移动平均 n/2 个元素，时间复杂度为 O(n)。</li>
</ul>
<p>这里简单列举一个例子：</p>
<pre><code class="language-java">// ArrayList的底层数组大小为10，此时存储了7个元素
+---+---+---+---+---+---+---+---+---+---+
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |   |   |   |
+---+---+---+---+---+---+---+---+---+---+
  0   1   2   3   4   5   6   7   8   9
// 在索引为1的位置插入一个元素8，该元素后面的所有元素都要向右移动一位
+---+---+---+---+---+---+---+---+---+---+
| 1 | 8 | 2 | 3 | 4 | 5 | 6 | 7 |   |   |
+---+---+---+---+---+---+---+---+---+---+
  0   1   2   3   4   5   6   7   8   9
// 删除索引为1的位置的元素，该元素后面的所有元素都要向左移动一位
+---+---+---+---+---+---+---+---+---+---+
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |   |   |   |
+---+---+---+---+---+---+---+---+---+---+
  0   1   2   3   4   5   6   7   8   9
</code></pre>
<h3><a id="linkedlist%E6%8F%92%E5%85%A5%E5%92%8C%E5%88%A0%E9%99%A4%E5%85%83%E7%B4%A0%E7%9A%84%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>LinkedList 插入和删除元素的时间复杂度？</h3>
<ul>
<li>头部插入/删除：只需要修改头结点的指针即可完成插入/删除操作，因此时间复杂度为 O(1)。</li>
<li>尾部插入/删除：只需要修改尾结点的指针即可完成插入/删除操作，因此时间复杂度为 O(1)。</li>
<li>指定位置插入/删除：需要先移动到指定位置，再修改指定节点的指针完成插入/删除，不过由于有头尾指针，可以从较近的指针出发，因此需要遍历平均 n/4 个元素，时间复杂度为 O(n)。</li>
</ul>
<p>这里简单列举一个例子：假如我们要删除节点 9 的话，需要先遍历链表找到该节点。然后，再执行相应节点指针指向的更改，具体的源码可以参考：<a href="17420847480449.html">LinkedList 源码分析</a> 。</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/043e10db-4bc2-46d9-9d44-28bd0acd6dff.jpg" alt="unlink 方法逻辑" /></p>
<h3><a id="linkedlist%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E8%83%BD%E5%AE%9E%E7%8E%B0-randomaccess%E6%8E%A5%E5%8F%A3%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>LinkedList 为什么不能实现 RandomAccess 接口？</h3>
<p><code>RandomAccess</code> 是一个标记接口，用来表明实现该接口的类支持随机访问（即可以通过索引快速访问元素）。由于 <code>LinkedList</code> 底层数据结构是链表，内存地址不连续，只能通过指针来定位，不支持随机快速访问，所以不能实现 <code>RandomAccess</code> 接口。</p>
<h3><a id="arraylist%E4%B8%8E-linkedlist%E5%8C%BA%E5%88%AB" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>ArrayList 与 LinkedList 区别?</h3>
<ul>
<li><strong>是否保证线程安全：</strong> <code>ArrayList</code> 和 <code>LinkedList</code> 都是不同步的，也就是不保证线程安全；</li>
<li><strong>底层数据结构：</strong> <code>ArrayList</code> 底层使用的是 <strong><code>Object</code> 数组</strong>；<code>LinkedList</code> 底层使用的是 <strong>双向链表</strong> 数据结构（JDK1.6 之前为循环链表，JDK1.7 取消了循环。注意双向链表和双向循环链表的区别，下面有介绍到！）</li>
<li><strong>插入和删除是否受元素位置的影响：</strong>
<ul>
<li><code>ArrayList</code> 采用数组存储，所以插入和删除元素的时间复杂度受元素位置的影响。 比如：执行<code>add(E e)</code>方法的时候， <code>ArrayList</code> 会默认在将指定的元素追加到此列表的末尾，这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话（<code>add(int index, E element)</code>），时间复杂度就为 O(n)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。</li>
<li><code>LinkedList</code> 采用链表存储，所以在头尾插入或者删除元素不受元素位置的影响（<code>add(E e)</code>、<code>addFirst(E e)</code>、<code>addLast(E e)</code>、<code>removeFirst()</code>、 <code>removeLast()</code>），时间复杂度为 O(1)，如果是要在指定位置 <code>i</code> 插入和删除元素的话（<code>add(int index, E element)</code>，<code>remove(Object o)</code>,<code>remove(int index)</code>）， 时间复杂度为 O(n) ，因为需要先移动到指定位置再插入和删除。</li>
</ul>
</li>
<li><strong>是否支持快速随机访问：</strong> <code>LinkedList</code> 不支持高效的随机元素访问，而 <code>ArrayList</code>（实现了 <code>RandomAccess</code> 接口） 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于<code>get(int index)</code>方法)。</li>
<li><strong>内存空间占用：</strong> <code>ArrayList</code> 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间，而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间（因为要存放直接后继和直接前驱以及数据）。</li>
</ul>
<p>我们在项目中一般是不会使用到 <code>LinkedList</code> 的，需要用到 <code>LinkedList</code> 的场景几乎都可以使用 <code>ArrayList</code> 来代替，并且，性能通常会更好！就连 <code>LinkedList</code> 的作者约书亚 · 布洛克（Josh Bloch）自己都说从来不会使用 <code>LinkedList</code> 。</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/d6a8b644-bc28-488a-ae33-c1b20200f9c2.png" alt="" /></p>
<p>另外，不要下意识地认为 <code>LinkedList</code> 作为链表就最适合元素增删的场景。我在上面也说了，<code>LinkedList</code> 仅仅在头尾插入或者删除元素的时候时间复杂度近似 O(1)，其他情况增删元素的平均时间复杂度都是 O(n) 。</p>
<h4><a id="%E8%A1%A5%E5%85%85%E5%86%85%E5%AE%B9%E5%8F%8C%E5%90%91%E9%93%BE%E8%A1%A8%E5%92%8C%E5%8F%8C%E5%90%91%E5%BE%AA%E7%8E%AF%E9%93%BE%E8%A1%A8" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>补充内容: 双向链表和双向循环链表</h4>
<p><strong>双向链表：</strong> 包含两个指针，一个 prev 指向前一个节点，一个 next 指向后一个节点。</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/05ac534f-5f83-43fd-a60c-dc63fff0fc1c.png" alt="双向链表" /></p>
<p><strong>双向循环链表：</strong> 最后一个节点的 next 指向 head，而 head 的 prev 指向最后一个节点，构成一个环。</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/7877d2fa-8e50-4538-930e-e1211948d68b.png" alt="双向循环链表" /></p>
<h4><a id="%E8%A1%A5%E5%85%85%E5%86%85%E5%AE%B9randomaccess%E6%8E%A5%E5%8F%A3" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>补充内容:RandomAccess 接口</h4>
<pre><code class="language-java">public interface RandomAccess {
}
</code></pre>
<p>查看源码我们发现实际上 <code>RandomAccess</code> 接口中什么都没有定义。所以，在我看来 <code>RandomAccess</code> 接口不过是一个标识罢了。标识什么？ 标识实现这个接口的类具有随机访问功能。</p>
<p>在 <code>binarySearch()</code> 方法中，它要判断传入的 list 是否 <code>RandomAccess</code> 的实例，如果是，调用<code>indexedBinarySearch()</code>方法，如果不是，那么调用<code>iteratorBinarySearch()</code>方法</p>
<pre><code class="language-java">    public static &lt;T&gt;
    int binarySearch(List&lt;? extends Comparable&lt;? super T&gt;&gt; list, T key) {
        if (list instanceof RandomAccess || list.size()&lt;BINARYSEARCH_THRESHOLD)
            return Collections.indexedBinarySearch(list, key);
        else
            return Collections.iteratorBinarySearch(list, key);
    }
</code></pre>
<p><code>ArrayList</code> 实现了 <code>RandomAccess</code> 接口， 而 <code>LinkedList</code> 没有实现。为什么呢？我觉得还是和底层数据结构有关！<code>ArrayList</code> 底层是数组，而 <code>LinkedList</code> 底层是链表。数组天然支持随机访问，时间复杂度为 O(1)，所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素，时间复杂度为 O(n)，所以不支持快速随机访问。<code>ArrayList</code> 实现了 <code>RandomAccess</code> 接口，就表明了他具有快速随机访问功能。 <code>RandomAccess</code> 接口只是标识，并不是说 <code>ArrayList</code> 实现 <code>RandomAccess</code> 接口才具有快速随机访问功能的！</p>
<h3><a id="%E8%AF%B4%E4%B8%80%E8%AF%B4arraylist%E7%9A%84%E6%89%A9%E5%AE%B9%E6%9C%BA%E5%88%B6%E5%90%A7" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>说一说 ArrayList 的扩容机制吧</h3>
<p>详见笔主的这篇文章: <a href="17420847480909.html">ArrayList 扩容机制分析</a>。</p>
<h3><a id="%E8%AF%B4%E8%AF%B4%E9%9B%86%E5%90%88%E4%B8%AD%E7%9A%84fail-fast%E5%92%8C-fail-safe%E6%98%AF%E4%BB%80%E4%B9%88" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>说说集合中的 fail-fast 和 fail-safe 是什么</h3>
<p>关于<code>fail-fast</code>引用<code>medium</code>中一篇文章关于<code>fail-fast</code>和<code>fail-safe</code>的说法：</p>
<blockquote>
<p>Fail-fast systems are designed to immediately stop functioning upon encountering an unexpected condition. This immediate failure helps to catch errors early, making debugging more straightforward.</p>
</blockquote>
<p>快速失败的思想即针对可能发生的异常进行提前表明故障并停止运行，通过尽早的发现和停止错误，降低故障系统级联的风险。</p>
<p>在<code>java.util</code>包下的大部分集合是不支持线程安全的，为了能够提前发现并发操作导致线程安全风险，提出通过维护一个<code>modCount</code>记录修改的次数，迭代期间通过比对预期修改次数<code>expectedModCount</code>和<code>modCount</code>是否一致来判断是否存在并发操作，从而实现快速失败，由此保证在避免在异常时执行非必要的复杂代码。</p>
<p>对应的我们给出下面这样一段在示例，我们首先插入<code>100</code>个操作元素，一个线程迭代元素，一个线程删除元素，最终输出结果如愿抛出<code>ConcurrentModificationException</code>：</p>
<pre><code class="language-java">// 使用线程安全的 CopyOnWriteArrayList 避免 ConcurrentModificationException
List&lt;Integer&gt; list = new CopyOnWriteArrayList&lt;&gt;();
CountDownLatch countDownLatch = new CountDownLatch(2);

// 添加元素
for (int i = 0; i &lt; 100; i++) {
    list.add(i);
}

Thread t1 = new Thread(() -&gt; {
    // 迭代元素 (注意：Integer 是不可变的，这里的 i++ 不会修改 list 中的值)
    for (Integer i : list) {
        i++; // 这行代码实际上没有修改list中的元素
    }
    countDownLatch.countDown();
});

Thread t2 = new Thread(() -&gt; {
    System.out.println(&quot;删除元素1&quot;);
    list.remove(Integer.valueOf(1)); // 使用 Integer.valueOf(1) 删除指定值的对象
    countDownLatch.countDown();
});

t1.start();
t2.start();
countDownLatch.await();
</code></pre>
<p>我们在初始化时插入了<code>100</code>个元素，此时对应的修改<code>modCount</code>次数为<code>100</code>，随后线程 2 在线程 1 迭代期间进行元素删除操作，此时对应的<code>modCount</code>就变为<code>101</code>。<br />
线程 1 在随后<code>foreach</code>第 2 轮循环发现<code>modCount</code> 为<code>101</code>，与预期的<code>expectedModCount(值为100因为初始化插入了元素100个)</code>不等，判定为并发操作异常，于是便快速失败，抛出<code>ConcurrentModificationException</code>：</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/224224fe-7178-47a8-b510-a4c43a0d276b.png" alt="" /></p>
<p>对此我们也给出<code>for</code>循环底层迭代器获取下一个元素时的<code>next</code>方法，可以看到其内部的<code>checkForComodification</code>具有针对修改次数比对的逻辑：</p>
<pre><code class="language-java"> public E next() {
 			//检查是否存在并发修改
            checkForComodification();
            //......
            //返回下一个元素
            return (E) elementData[lastRet = i];
        }

final void checkForComodification() {
		//当前循环遍历次数和预期修改次数不一致时，就会抛出ConcurrentModificationException
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

</code></pre>
<p>而<code>fail-safe</code>也就是安全失败的含义，它旨在即使面对意外情况也能恢复并继续运行，这使得它特别适用于不确定或者不稳定的环境：</p>
<blockquote>
<p>Fail-safe systems take a different approach, aiming to recover and continue even in the face of unexpected conditions. This makes them particularly suited for uncertain or volatile environments.</p>
</blockquote>
<p>该思想常运用于并发容器，最经典的实现就是<code>CopyOnWriteArrayList</code>的实现，通过写时复制的思想保证在进行修改操作时复制出一份快照，基于这份快照完成添加或者删除操作后，将<code>CopyOnWriteArrayList</code>底层的数组引用指向这个新的数组空间，由此避免迭代时被并发修改所干扰所导致并发操作安全问题，当然这种做法也存缺点即进行遍历操作时无法获得实时结果：</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/b8f45511-e382-4dcc-a335-98d8a3bb3baf.png" alt="" /></p>
<p>对应我们也给出<code>CopyOnWriteArrayList</code>实现<code>fail-safe</code>的核心代码，可以看到它的实现就是通过<code>getArray</code>获取数组引用然后通过<code>Arrays.copyOf</code>得到一个数组的快照，基于这个快照完成添加操作后，修改底层<code>array</code>变量指向的引用地址由此完成写时复制：</p>
<pre><code class="language-java">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;
            //array指向新的数组
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
</code></pre>
<h2><a id="set" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Set</h2>
<h3><a id="comparable%E5%92%8C-comparator%E7%9A%84%E5%8C%BA%E5%88%AB" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Comparable 和 Comparator 的区别</h3>
<p><code>Comparable</code> 接口和 <code>Comparator</code> 接口都是 Java 中用于排序的接口，它们在实现类对象之间比较大小、排序等方面发挥了重要作用：</p>
<ul>
<li><code>Comparable</code> 接口实际上是出自<code>java.lang</code>包 它有一个 <code>compareTo(Object obj)</code>方法用来排序</li>
<li><code>Comparator</code>接口实际上是出自 <code>java.util</code> 包它有一个<code>compare(Object obj1, Object obj2)</code>方法用来排序</li>
</ul>
<p>一般我们需要对一个集合使用自定义排序时，我们就要重写<code>compareTo()</code>方法或<code>compare()</code>方法，当我们需要对某一个集合实现两种排序方式，比如一个 <code>song</code> 对象中的歌名和歌手名分别采用一种排序方法的话，我们可以重写<code>compareTo()</code>方法和使用自制的<code>Comparator</code>方法或者以两个 <code>Comparator</code> 来实现歌名排序和歌星名排序，第二种代表我们只能使用两个参数版的 <code>Collections.sort()</code>.</p>
<h4><a id="comparator%E5%AE%9A%E5%88%B6%E6%8E%92%E5%BA%8F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Comparator 定制排序</h4>
<pre><code class="language-java">ArrayList&lt;Integer&gt; arrayList = new ArrayList&lt;Integer&gt;();
arrayList.add(-1);
arrayList.add(3);
arrayList.add(3);
arrayList.add(-5);
arrayList.add(7);
arrayList.add(4);
arrayList.add(-9);
arrayList.add(-7);
System.out.println(&quot;原始数组:&quot;);
System.out.println(arrayList);
// void reverse(List list)：反转
Collections.reverse(arrayList);
System.out.println(&quot;Collections.reverse(arrayList):&quot;);
System.out.println(arrayList);

// void sort(List list),按自然排序的升序排序
Collections.sort(arrayList);
System.out.println(&quot;Collections.sort(arrayList):&quot;);
System.out.println(arrayList);
// 定制排序的用法
Collections.sort(arrayList, new Comparator&lt;Integer&gt;() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2.compareTo(o1);
    }
});
System.out.println(&quot;定制排序后：&quot;);
System.out.println(arrayList);
</code></pre>
<p>Output:</p>
<pre><code class="language-plain">原始数组:
[-1, 3, 3, -5, 7, 4, -9, -7]
Collections.reverse(arrayList):
[-7, -9, 4, 7, -5, 3, 3, -1]
Collections.sort(arrayList):
[-9, -7, -5, -1, 3, 3, 4, 7]
定制排序后：
[7, 4, 3, 3, -1, -5, -7, -9]
</code></pre>
<h4><a id="%E9%87%8D%E5%86%99compareto%E6%96%B9%E6%B3%95%E5%AE%9E%E7%8E%B0%E6%8C%89%E5%B9%B4%E9%BE%84%E6%9D%A5%E6%8E%92%E5%BA%8F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>重写 compareTo 方法实现按年龄来排序</h4>
<pre><code class="language-java">// person对象没有实现Comparable接口，所以必须实现，这样才不会出错，才可以使treemap中的数据按顺序排列
// 前面一个例子的String类已经默认实现了Comparable接口，详细可以查看String类的API文档，另外其他
// 像Integer类等都已经实现了Comparable接口，所以不需要另外实现了
public  class Person implements Comparable&lt;Person&gt; {
    private String name;
    private int age;

    public Person(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    /**
     * T重写compareTo方法实现按年龄来排序
     */
    @Override
    public int compareTo(Person o) {
        if (this.age &gt; o.getAge()) {
            return 1;
        }
        if (this.age &lt; o.getAge()) {
            return -1;
        }
        return 0;
    }
}

</code></pre>
<pre><code class="language-java">    public static void main(String[] args) {
        TreeMap&lt;Person, String&gt; pdata = new TreeMap&lt;Person, String&gt;();
        pdata.put(new Person(&quot;张三&quot;, 30), &quot;zhangsan&quot;);
        pdata.put(new Person(&quot;李四&quot;, 20), &quot;lisi&quot;);
        pdata.put(new Person(&quot;王五&quot;, 10), &quot;wangwu&quot;);
        pdata.put(new Person(&quot;小红&quot;, 5), &quot;xiaohong&quot;);
        // 得到key的值的同时得到key所对应的值
        Set&lt;Person&gt; keys = pdata.keySet();
        for (Person key : keys) {
            System.out.println(key.getAge() + &quot;-&quot; + key.getName());

        }
    }
</code></pre>
<p>Output：</p>
<pre><code class="language-plain">5-小红
10-王五
20-李四
30-张三
</code></pre>
<h3><a id="%E6%97%A0%E5%BA%8F%E6%80%A7%E5%92%8C%E4%B8%8D%E5%8F%AF%E9%87%8D%E5%A4%8D%E6%80%A7%E7%9A%84%E5%90%AB%E4%B9%89%E6%98%AF%E4%BB%80%E4%B9%88" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>无序性和不可重复性的含义是什么</h3>
<ul>
<li>无序性不等于随机性 ，无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ，而是根据数据的哈希值决定的。</li>
<li>不可重复性是指添加的元素按照 <code>equals()</code> 判断时 ，返回 false，需要同时重写 <code>equals()</code> 方法和 <code>hashCode()</code> 方法。</li>
</ul>
<h3><a id="%E6%AF%94%E8%BE%83hashset%E3%80%81linkedhashset%E5%92%8C-treeset%E4%B8%89%E8%80%85%E7%9A%84%E5%BC%82%E5%90%8C" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同</h3>
<ul>
<li><code>HashSet</code>、<code>LinkedHashSet</code> 和 <code>TreeSet</code> 都是 <code>Set</code> 接口的实现类，都能保证元素唯一，并且都不是线程安全的。</li>
<li><code>HashSet</code>、<code>LinkedHashSet</code> 和 <code>TreeSet</code> 的主要区别在于底层数据结构不同。<code>HashSet</code> 的底层数据结构是哈希表（基于 <code>HashMap</code> 实现）。<code>LinkedHashSet</code> 的底层数据结构是链表和哈希表，元素的插入和取出顺序满足 FIFO。<code>TreeSet</code> 底层数据结构是红黑树，元素是有序的，排序的方式有自然排序和定制排序。</li>
<li>底层数据结构不同又导致这三者的应用场景不同。<code>HashSet</code> 用于不需要保证元素插入和取出顺序的场景，<code>LinkedHashSet</code> 用于保证元素的插入和取出顺序满足 FIFO 的场景，<code>TreeSet</code> 用于支持对元素自定义排序规则的场景。</li>
</ul>
<h2><a id="queue" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Queue</h2>
<h3><a id="queue%E4%B8%8E-deque%E7%9A%84%E5%8C%BA%E5%88%AB" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Queue 与 Deque 的区别</h3>
<p><code>Queue</code> 是单端队列，只能从一端插入元素，另一端删除元素，实现上一般遵循 <strong>先进先出（FIFO）</strong> 规则。</p>
<p><code>Queue</code> 扩展了 <code>Collection</code> 的接口，根据 <strong>因为容量问题而导致操作失败后处理方式的不同</strong> 可以分为两类方法: 一种在操作失败后会抛出异常，另一种则会返回特殊值。</p>
<table>
<thead>
<tr>
<th><code>Queue</code> 接口</th>
<th>抛出异常</th>
<th>返回特殊值</th>
</tr>
</thead>
<tbody>
<tr>
<td>插入队尾</td>
<td>add(E e)</td>
<td>offer(E e)</td>
</tr>
<tr>
<td>删除队首</td>
<td>remove()</td>
<td>poll()</td>
</tr>
<tr>
<td>查询队首元素</td>
<td>element()</td>
<td>peek()</td>
</tr>
</tbody>
</table>
<p><code>Deque</code> 是双端队列，在队列的两端均可以插入或删除元素。</p>
<p><code>Deque</code> 扩展了 <code>Queue</code> 的接口, 增加了在队首和队尾进行插入和删除的方法，同样根据失败后处理方式的不同分为两类：</p>
<table>
<thead>
<tr>
<th><code>Deque</code> 接口</th>
<th>抛出异常</th>
<th>返回特殊值</th>
</tr>
</thead>
<tbody>
<tr>
<td>插入队首</td>
<td>addFirst(E e)</td>
<td>offerFirst(E e)</td>
</tr>
<tr>
<td>插入队尾</td>
<td>addLast(E e)</td>
<td>offerLast(E e)</td>
</tr>
<tr>
<td>删除队首</td>
<td>removeFirst()</td>
<td>pollFirst()</td>
</tr>
<tr>
<td>删除队尾</td>
<td>removeLast()</td>
<td>pollLast()</td>
</tr>
<tr>
<td>查询队首元素</td>
<td>getFirst()</td>
<td>peekFirst()</td>
</tr>
<tr>
<td>查询队尾元素</td>
<td>getLast()</td>
<td>peekLast()</td>
</tr>
</tbody>
</table>
<p>事实上，<code>Deque</code> 还提供有 <code>push()</code> 和 <code>pop()</code> 等其他方法，可用于模拟栈。</p>
<h3><a id="arraydeque%E4%B8%8E-linkedlist%E7%9A%84%E5%8C%BA%E5%88%AB" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>ArrayDeque 与 LinkedList 的区别</h3>
<p><code>ArrayDeque</code> 和 <code>LinkedList</code> 都实现了 <code>Deque</code> 接口，两者都具有队列的功能，但两者有什么区别呢？</p>
<ul>
<li>
<p><code>ArrayDeque</code> 是基于可变长的数组和双指针来实现，而 <code>LinkedList</code> 则通过链表来实现。</p>
</li>
<li>
<p><code>ArrayDeque</code> 不支持存储 <code>NULL</code> 数据，但 <code>LinkedList</code> 支持。</p>
</li>
<li>
<p><code>ArrayDeque</code> 是在 JDK1.6 才被引入的，而<code>LinkedList</code> 早在 JDK1.2 时就已经存在。</p>
</li>
<li>
<p><code>ArrayDeque</code> 插入时可能存在扩容过程, 不过均摊后的插入操作依然为 O(1)。虽然 <code>LinkedList</code> 不需要扩容，但是每次插入数据时均需要申请新的堆空间，均摊性能相比更慢。</p>
</li>
</ul>
<p>从性能的角度上，选用 <code>ArrayDeque</code> 来实现队列要比 <code>LinkedList</code> 更好。此外，<code>ArrayDeque</code> 也可以用于实现栈。</p>
<h3><a id="%E8%AF%B4%E4%B8%80%E8%AF%B4priorityqueue" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>说一说 PriorityQueue</h3>
<p><code>PriorityQueue</code> 是在 JDK1.5 中被引入的, 其与 <code>Queue</code> 的区别在于元素出队顺序是与优先级相关的，即总是优先级最高的元素先出队。</p>
<p>这里列举其相关的一些要点：</p>
<ul>
<li><code>PriorityQueue</code> 利用了二叉堆的数据结构来实现的，底层使用可变长的数组来存储数据</li>
<li><code>PriorityQueue</code> 通过堆元素的上浮和下沉，实现了在 O(logn) 的时间复杂度内插入元素和删除堆顶元素。</li>
<li><code>PriorityQueue</code> 是非线程安全的，且不支持存储 <code>NULL</code> 和 <code>non-comparable</code> 的对象。</li>
<li><code>PriorityQueue</code> 默认是小顶堆，但可以接收一个 <code>Comparator</code> 作为构造参数，从而来自定义元素优先级的先后。</li>
</ul>
<p><code>PriorityQueue</code> 在面试中可能更多的会出现在手撕算法的时候，典型例题包括堆排序、求第 K 大的数、带权图的遍历等，所以需要会熟练使用才行。</p>
<h3><a id="%E4%BB%80%E4%B9%88%E6%98%AFblockingqueue%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>什么是 BlockingQueue？</h3>
<p><code>BlockingQueue</code> （阻塞队列）是一个接口，继承自 <code>Queue</code>。<code>BlockingQueue</code>阻塞的原因是其支持当队列没有元素时一直阻塞，直到有元素；还支持如果队列已满，一直等到队列可以放入新元素时再放入。</p>
<pre><code class="language-java">public interface BlockingQueue&lt;E&gt; extends Queue&lt;E&gt; {
  // ...
}
</code></pre>
<p><code>BlockingQueue</code> 常用于生产者-消费者模型中，生产者线程会向队列中添加数据，而消费者线程会从队列中取出数据进行处理。</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/7447ce03-1b1f-463b-9023-bd9350f9a190.png" alt="BlockingQueue" /></p>
<h3><a id="blockingqueue%E7%9A%84%E5%AE%9E%E7%8E%B0%E7%B1%BB%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>BlockingQueue 的实现类有哪些？</h3>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/029cfe15-4dac-45cd-977e-8c4a737d35a3.png" alt="BlockingQueue 的实现类" /></p>
<p>Java 中常用的阻塞队列实现类有以下几种：</p>
<ol>
<li><code>ArrayBlockingQueue</code>：使用数组实现的有界阻塞队列。在创建时需要指定容量大小，并支持公平和非公平两种方式的锁访问机制。</li>
<li><code>LinkedBlockingQueue</code>：使用单向链表实现的可选有界阻塞队列。在创建时可以指定容量大小，如果不指定则默认为<code>Integer.MAX_VALUE</code>。和<code>ArrayBlockingQueue</code>不同的是， 它仅支持非公平的锁访问机制。</li>
<li><code>PriorityBlockingQueue</code>：支持优先级排序的无界阻塞队列。元素必须实现<code>Comparable</code>接口或者在构造函数中传入<code>Comparator</code>对象，并且不能插入 null 元素。</li>
<li><code>SynchronousQueue</code>：同步队列，是一种不存储元素的阻塞队列。每个插入操作都必须等待对应的删除操作，反之删除操作也必须等待插入操作。因此，<code>SynchronousQueue</code>通常用于线程之间的直接传递数据。</li>
<li><code>DelayQueue</code>：延迟队列，其中的元素只有到了其指定的延迟时间，才能够从队列中出队。</li>
<li>……</li>
</ol>
<p>日常开发中，这些队列使用的其实都不多，了解即可。</p>
<h3><a id="arrayblockingqueue%E5%92%8C-linkedblockingqueue%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>ArrayBlockingQueue 和 LinkedBlockingQueue 有什么区别？</h3>
<p><code>ArrayBlockingQueue</code> 和 <code>LinkedBlockingQueue</code> 是 Java 并发包中常用的两种阻塞队列实现，它们都是线程安全的。不过，不过它们之间也存在下面这些区别：</p>
<ul>
<li>底层实现：<code>ArrayBlockingQueue</code> 基于数组实现，而 <code>LinkedBlockingQueue</code> 基于链表实现。</li>
<li>是否有界：<code>ArrayBlockingQueue</code> 是有界队列，必须在创建时指定容量大小。<code>LinkedBlockingQueue</code> 创建时可以不指定容量大小，默认是<code>Integer.MAX_VALUE</code>，也就是无界的。但也可以指定队列大小，从而成为有界的。</li>
<li>锁是否分离： <code>ArrayBlockingQueue</code>中的锁是没有分离的，即生产和消费用的是同一个锁；<code>LinkedBlockingQueue</code>中的锁是分离的，即生产用的是<code>putLock</code>，消费是<code>takeLock</code>，这样可以防止生产者和消费者线程之间的锁争夺。</li>
<li>内存占用：<code>ArrayBlockingQueue</code> 需要提前分配数组内存，而 <code>LinkedBlockingQueue</code> 则是动态分配链表节点内存。这意味着，<code>ArrayBlockingQueue</code> 在创建时就会占用一定的内存空间，且往往申请的内存比实际所用的内存更大，而<code>LinkedBlockingQueue</code> 则是根据元素的增加而逐渐占用内存空间。</li>
</ul>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[01-Mysql数据库架构演变历史]]></title>
    <link href="https://huanglei.work/17418278741915.html"/>
    <updated>2025-03-13T09:04:34+08:00</updated>
    <id>https://huanglei.work/17418278741915.html</id>
    <content type="html"><![CDATA[
<ul>
<li>数据库的演变升级
<ul>
<li>单机
<ul>
<li>请求量大查询慢</li>
<li>单机故障导致业务不可用<br />
<img src="https://image.huanglei.work/mweb/2025/3/13/81151d86-a7c2-4568-a763-be57b7181266.png" alt="image-20211119172527225" /></li>
</ul>
</li>
<li>主从
<ul>
<li>数据库主从同步，从库可以水平扩展，满足更大读需求</li>
<li>但单服务器TPS，内存，IO都是有限的<br />
<img src="https://image.huanglei.work/mweb/2025/3/13/4544b19f-a809-4992-ac6b-4f7be2206cae.png" alt="image-20211119172539370" /></li>
</ul>
</li>
<li>双主
<ul>
<li>用户量级上来后，写请求越来越多</li>
<li>一个Master是不能解决问题的，添加多了个主节点进行写入</li>
<li>多个主节点数据要保存一致性，写操作需要2个master之间同步更加复杂<br />
<img src="https://image.huanglei.work/mweb/2025/3/13/4a75867e-1264-4326-aa4b-987d96aed8db.png" alt="image-20211119174013656" /></li>
</ul>
</li>
<li>分库和分表<br />
<img src="https://image.huanglei.work/mweb/2025/3/13/d2845ad7-c6ce-49aa-9086-b9c0f1c35ede.png" alt="image-20211119172548102" /></li>
</ul>
</li>
<li>重点掌握解决方案设计思想，而不是具体框架API调用
<ul>
<li>触类旁通，会思想后，用哪种框架都类似
<ul>
<li>自研工具类、tddl、shardingsphere、mycat等</li>
</ul>
</li>
</ul>
</li>
</ul>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[01-RabbitMQ 课程介绍]]></title>
    <link href="https://huanglei.work/17421182727628.html"/>
    <updated>2025-03-16T17:44:32+08:00</updated>
    <id>https://huanglei.work/17421182727628.html</id>
    <content type="html"><![CDATA[
<ul>
<li>
<p>为什么要学习 RabbitMQ 消息队列</p>
<ul>
<li>多数互联网公司里面用的技术栈，可以承载海量消息处理</li>
<li>在多数互联网公司中，RabbitMQ 占有率很高，且全球都很流行</li>
<li>在分布式系统中存储转发消息，在易用性、扩展性、高可用性等方面表现强劲，与 SpringAMQP 完美的整合、API 丰富易用</li>
<li>可以作为公司内部培训技术分享必备知识，可靠性投递、消费、高可用集群等</li>
</ul>
</li>
<li>
<p>学后水平</p>
<ul>
<li>
<p>零基础掌握 MQ 中间件应用场景、掌握多个业界主流中间件优缺点</p>
</li>
<li>
<p>掌握 JMS、AMQP 核心概念，各个优缺点和适合场景</p>
</li>
<li>
<p>零基础急速掌握 Docker 容器知识+Linux 搭建 Docker 和部署 RabbitMQ</p>
</li>
<li>
<p>掌握 RabbitMQ 多个核心概念交换机、队列、虚拟主机和 Web 管控台使用</p>
</li>
<li>
<p>玩转 RabbitMQ 多个工作队列、发布订阅模型、主题模型通配符实战</p>
</li>
<li>
<p>掌握新版 SpringBoot+AMQP 整合 RabbitMQ 并开发多个模式实战</p>
</li>
<li>
<p>高级篇幅玩转可靠性消息投递 ConfirmCallback 和 returnCallback 编码实战</p>
</li>
<li>
<p>高级篇幅掌握消息 ACK 确认机制+多种 Reject 编码实战</p>
</li>
<li>
<p>高级篇幅掌握 RabbitMQ TTL 死信队列 + 延迟队列开发【综合案例实战】</p>
</li>
<li>
<p>高级加餐内容</p>
<ul>
<li>掌握 Docker 搭建 RabbitMQ 高可用 默认、Mirror 镜像集群搭建实战+适合场景</li>
<li>掌握多个集群模式整合 SpringBoot，模拟多种异常场景配置实战</li>
<li>掌握常见一线互联网大厂 RabbitMQ 面试题+核心原理+学习路线</li>
</ul>
</li>
</ul>
</li>
<li>
<p>适合人群</p>
<ul>
<li>高级后端工程师、高级前端/全栈工程师、运维工程师、CTO 更新必备技术栈</li>
<li>从传统软件公司过渡到互联网公司的人员</li>
</ul>
</li>
<li>
<p>课程技术技术栈和环境说明</p>
<ul>
<li>MQ 版本：RabbitMQ3.8.9+ErLang23.2.1</li>
<li>SpringBoot.2.4+Maven+IDEA 旗舰版+JDK8 或 JDK11</li>
</ul>
</li>
<li>
<p>课程开发环境准备</p>
<ul>
<li>
<p>创建新版 SpringBoot2.X 项目</p>
<ul>
<li><a href="https://spring.io/projects/spring-boot">https://spring.io/projects/spring-boot</a></li>
<li>在线构建工具 <a href="https://start.spring.io/">https://start.spring.io/</a></li>
<li>如果版本或者链接找不到，可以直接导入课程项目</li>
</ul>
</li>
<li>
<p>注意: 有些包 maven 下载慢，等待下载如果失败</p>
<ul>
<li>删除本地仓库 spring 相关的包，重新执行 mvn install</li>
<li>建议先使用默认的 maven 仓库，不用更换地址</li>
</ul>
</li>
<li>
<p>当前项目仓库地址修改</p>
<pre><code class="language-xml">&lt;!-- 代码库 --&gt;
&lt;repositories&gt;
    &lt;repository&gt;
        &lt;id&gt;maven-ali&lt;/id&gt;
        &lt;url&gt;http://maven.aliyun.com/nexus/content/groups/public//&lt;/url&gt;
        &lt;releases&gt;
            &lt;enabled&gt;true&lt;/enabled&gt;
        &lt;/releases&gt;
        &lt;snapshots&gt;
            &lt;enabled&gt;true&lt;/enabled&gt;
            &lt;updatePolicy&gt;always&lt;/updatePolicy&gt;
            &lt;checksumPolicy&gt;fail&lt;/checksumPolicy&gt;
        &lt;/snapshots&gt;
    &lt;/repository&gt;
&lt;/repositories&gt;

&lt;pluginRepositories&gt;
    &lt;pluginRepository&gt;
        &lt;id&gt;public&lt;/id&gt;
        &lt;name&gt;aliyun nexus&lt;/name&gt;
        &lt;url&gt;http://maven.aliyun.com/nexus/content/groups/public/&lt;/url&gt;
        &lt;releases&gt;
            &lt;enabled&gt;true&lt;/enabled&gt;
        &lt;/releases&gt;
        &lt;snapshots&gt;
            &lt;enabled&gt;false&lt;/enabled&gt;
        &lt;/snapshots&gt;
    &lt;/pluginRepository&gt;
&lt;/pluginRepositories&gt;
</code></pre>
</li>
</ul>
</li>
</ul>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[01-全方位 Redis6.x 之战超多案例+最佳实践专题课程介绍]]></title>
    <link href="https://huanglei.work/17421078106893.html"/>
    <updated>2025-03-16T14:50:10+08:00</updated>
    <id>https://huanglei.work/17421078106893.html</id>
    <content type="html"><![CDATA[
<ul>
<li><a href="#%E7%AC%AC1%E9%9B%86%E5%85%A8%E6%96%B9%E4%BD%8D-redis6-x%E4%B9%8B%E6%88%98%E8%B6%85%E5%A4%9A%E6%A1%88%E4%BE%8B%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%E4%B8%93%E9%A2%98%E8%AF%BE%E7%A8%8B%E4%BB%8B%E7%BB%8D">第 1 集 全方位 Redis6.x 之战超多案例+最佳实践专题课程介绍</a></li>
<li><a href="#%E7%AC%AC2%E9%9B%86%E5%85%A8%E6%96%B9%E4%BD%8D-redis6-x%E4%B9%8B%E6%88%98%E8%B6%85%E5%A4%9A%E6%A1%88%E4%BE%8B%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%E8%AF%BE%E7%A8%8B%E5%A4%A7%E7%BA%B2%E9%80%9F%E8%A7%88">第 2 集 全方位 Redis6.x 之战超多案例+最佳实践课程大纲速览</a></li>
</ul>
<h4><a id="%E7%AC%AC1%E9%9B%86%E5%85%A8%E6%96%B9%E4%BD%8D-redis6-x%E4%B9%8B%E6%88%98%E8%B6%85%E5%A4%9A%E6%A1%88%E4%BE%8B%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%E4%B8%93%E9%A2%98%E8%AF%BE%E7%A8%8B%E4%BB%8B%E7%BB%8D" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>第 1 集 全方位 Redis6.x 之战超多案例+最佳实践专题课程介绍</h4>
<p><strong>简介：讲解全方位 Redis6.x 之战专题课程介绍，课程适合人员和学后水平</strong></p>
<ul>
<li>
<p>课程介绍</p>
<ul>
<li>本套课程 是<strong>2021 年全新录制，<strong>从 0 到 1 讲解分布式缓存</strong>Redis6.x</strong>,多种核心数据结构，零基础掌握 Redis6.X 核心基础+<strong>高级知识点。</strong></li>
<li>不止讲解 Redis 核心数据结构，<strong>超多案例实战</strong>：<strong>图形验证码/商品热点数据/日销榜单/电商购物车/用户画像/社交应用/积分实时榜单/多场景分页缓存</strong>等，还整合当下新版热门框架<strong>SpringBoot2.X+SpringCache</strong>框架实战；</li>
</ul>
</li>
<li>
<p>为什么要学习分布式缓存 Redis6.X</p>
<ul>
<li>
<p>多数互联网公司里面用的技术栈，<strong>高并发项目都离不开</strong></p>
</li>
<li>
<p>在多数互联网公司中，Redis6.X 占有率很高</p>
</li>
<li>
<p>谁在使用 Redis</p>
<ul>
<li>国外: Google、Facebook、亚⻢逊</li>
<li>国内:阿里、腾讯、字节、百度 等</li>
</ul>
</li>
<li>
<p>高级工程师岗位面试都喜欢问 Redis：<strong>核心数据结构、高性能原因、key 设计、热点 key、淘汰算法</strong>等</p>
</li>
<li>
<p>可以作为公司内部培训技术分享必备知识，超多案例+高可用实战等</p>
</li>
</ul>
</li>
<li>
<p>如果你想成为下面的一种，则<strong>这个课程是必备</strong></p>
<ul>
<li><strong>后端</strong>开发工程师-<strong>中高级工程师</strong></li>
<li><strong>技术 leader</strong>或者<strong>架构师</strong></li>
<li>高级前端<strong>工程师</strong></li>
<li><strong>运维工程师（高可用搭建和运维）</strong></li>
<li><strong>高级测试工程师（白盒测试性能优化）</strong></li>
<li>从传统软件公司过渡到互联网公司的人员</li>
</ul>
</li>
<li>
<p>课程技术技术栈和测试环境说明</p>
<ul>
<li>JDK8 或者 JDK11+SpringBoot2.5+IDEA 旗舰版+Redis6.2 版本</li>
</ul>
</li>
<li>
<p>学后水平</p>
<ul>
<li>
<p>零基础掌握分布式缓存 Redis6.X 应用场景+源码安装部署+可视化工具</p>
</li>
<li>
<p>掌握核心<strong>数据结构 String/List/Hash/Set/SortedSet+命名规范</strong></p>
</li>
<li>
<p>玩转超多 Reids6.X 经典案例:</p>
<ul>
<li>图<strong>形验证码/商品热点数据/日销榜单/电商购物车/用户画像</strong></li>
<li><strong>社交应用/粉丝、好友、共同关注/积分实时榜单/多场景分页缓存等</strong></li>
</ul>
</li>
<li>
<p>玩转<strong>SpringBoot2.X</strong>整合 Redis<strong>多客户端 Jedis+Lettcuce</strong></p>
</li>
<li>
<p>轻量级缓存<strong>SpringCache</strong>+项目多场景使用+自定义配置实战+<strong>Jmeter 压测</strong></p>
</li>
<li>
<p><strong>高级篇</strong></p>
<ul>
<li>超多面试题，<strong>缓存穿透、击穿、雪崩原理分析</strong>+解决方案+热点 Key 知识</li>
<li>实战+原理:<strong>分布式锁实战</strong>+Redis+Lua 脚本最佳实践+高级面试题</li>
<li>实战+原理:Redis6.X<strong>持久化 AOF、RDB 实战</strong>+优缺点+新版混合模式</li>
<li>玩转 Redis 的监控+<strong>key 删除策略+多种内存淘汰算法</strong></li>
<li>实战+原理：新版 Redis6.X<strong>高可用实战 一主二从读写分离</strong>搭建</li>
<li>实战+原理：Redis6.X 哨兵<strong>Sentinel 集群+主从+SpringBoot 项目</strong>整合</li>
<li>实战+原理：玩转 Redis6.X<strong>集群 Cluster+数据分片+虚拟哈希槽实战</strong></li>
<li>实战+原理：Redis6.X 集群<strong>三主三从 + 故障自动转移 + SpringBoot 项目</strong>整合</li>
<li>玩转 Redis6.X 新特性<strong>多线程、ACL、客户端缓存 ClientSideCaching</strong>等</li>
</ul>
</li>
</ul>
</li>
<li>
<p>学习形式</p>
<ul>
<li>视频讲解 + 文字笔记 + 原理分析 + 交互流程图</li>
<li>配套源码 + 笔记 + 技术群答疑( 我答疑，联系客服进群)</li>
<li>只要是我的课程和项目-我会一直维护下去，大家不用担心！！！</li>
</ul>
</li>
</ul>
<h4><a id="%E7%AC%AC2%E9%9B%86%E5%85%A8%E6%96%B9%E4%BD%8D-redis6-x%E4%B9%8B%E6%88%98%E8%B6%85%E5%A4%9A%E6%A1%88%E4%BE%8B%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%E8%AF%BE%E7%A8%8B%E5%A4%A7%E7%BA%B2%E9%80%9F%E8%A7%88" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>第 2 集 全方位 Redis6.x 之战超多案例+最佳实践课程大纲速览</h4>
<p><strong>简介：讲解全方位 Redis6.x 之战课程大纲</strong></p>
<ul>
<li>
<p>课程学前基础</p>
<ul>
<li>有 Linux+SpringBoot 就行，不会的话我们有专题课程学习，联系客服小姐姐即可</li>
</ul>
</li>
<li>
<p>目录大纲浏览</p>
</li>
<li>
<p>学习要求：</p>
<ul>
<li>课程有配套资料，如果有用到就会在对应章集的资料里面，如果自己操作的情况和视频不一样，仔细对比对比验证基本就可以发现问题了</li>
<li>Redis 比较坑的点，就是配置多，然后没有比较好的编辑器，所以遇到和课程效果不一样的，建议看多几次 结合笔记里面的配置，网上的博文</li>
<li>很多错误配置，就是配置少加了 英文分号 ; 或者用了中文的； 或者防火墙问题，或者配置多了空格等特殊字符。</li>
</ul>
</li>
<li>
<p>保持谦虚好学、术业有专攻、在校的学生，有些知识掌握也比工作好几年掌握的人厉害</p>
</li>
</ul>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[01-分布式流处理平台Kafka快速认知]]></title>
    <link href="https://huanglei.work/17418276110652.html"/>
    <updated>2025-03-13T09:00:11+08:00</updated>
    <id>https://huanglei.work/17418276110652.html</id>
    <content type="html"><![CDATA[
<ul>
<li>Kafka
<ul>
<li>Kafka是最初由Linkedin公司开发，Linkedin于2010年贡献给了Apache基金会并成为顶级开源项目，也是一个开源【分布式流处理平台】，由Scala和Java编写，（也当做MQ系统，但不是纯粹的消息系统）</li>
<li>核心：一种高吞吐量的分布式流处理平台，它可以处理消费者在网站中的所有动作流数据
<ul>
<li>比如 网页浏览，搜索和其他用户的行为等，应用于大数据实时处理领域</li>
</ul>
</li>
</ul>
</li>
<li>官网：<a href="http://kafka.apache.org/">http://kafka.apache.org/</a></li>
<li>快速开始：<a href="http://kafka.apache.org/quickstart">http://kafka.apache.org/quickstart</a></li>
<li>核心概念
<ul>
<li>Broker
<ul>
<li>Kafka的服务端程序，可以认为一个mq节点就是一个broker</li>
<li>broker存储topic的数据</li>
</ul>
</li>
<li>Producer生产者
<ul>
<li>创建消息Message，然后发布到MQ中</li>
<li>该角色将消息发布到Kafka的topic中</li>
</ul>
</li>
<li>Consumer消费者
<ul>
<li>消费队列里面的消息</li>
</ul>
</li>
<li>ConsumerGroup消费者组
<ul>
<li>同个topic, 广播发送给不同的group，一个group中只有一个consumer可以消费此消息</li>
</ul>
</li>
<li>Topic
<ul>
<li>每条发布到Kafka集群的消息都有一个类别，这个类别被称为Topic，主题的意思</li>
</ul>
</li>
<li>Partition分区
<ul>
<li>kafka数据存储的基本单元，topic中的数据分割为一个或多个partition，每个topic至少有一个partition，是有序的</li>
<li>一个Topic的多个partitions, 被分布在kafka集群中的多个server上</li>
<li>消费者数量 &lt;=小于或者等于Partition数量</li>
</ul>
</li>
<li>Replication 副本（备胎）
<ul>
<li>同个Partition会有多个副本replication ，多个副本的数据是一样的，当其他broker挂掉后，系统可以主动用副本提供服务</li>
<li>默认每个topic的副本都是1（默认是没有副本，节省资源），也可以在创建topic的时候指定</li>
<li>如果当前kafka集群只有3个broker节点，则replication-factor最大就是3了，如果创建副本为4，则会报错</li>
</ul>
</li>
<li>ReplicationLeader、ReplicationFollower
<ul>
<li>Partition有多个副本，但只有一个replicationLeader负责该Partition和生产者消费者交互</li>
<li>ReplicationFollower只是做一个备份，从replicationLeader进行同步</li>
</ul>
</li>
<li>ReplicationManager
<ul>
<li>负责Broker所有分区副本信息，Replication 副本状态切换</li>
</ul>
</li>
<li>offset
<ul>
<li>每个consumer实例需要为他消费的partition维护一个记录自己消费到哪里的偏移offset</li>
<li>kafka把offset保存在消费端的消费者组里<br />
<img src="https://image.huanglei.work/mweb/2025/3/13/40d71002-9f9a-4f83-ae8e-4a9afab43baf.png" alt="Untitled Diagram" /></li>
</ul>
</li>
</ul>
</li>
<li>特点总结
<ul>
<li>多订阅者
<ul>
<li>一个topic可以有一个或者多个订阅者</li>
<li>每个订阅者都要有一个partition，所以订阅者数量要少于等于partition数量</li>
</ul>
</li>
<li>高吞吐量、低延迟: 每秒可以处理几十万条消息</li>
<li>高并发：几千个客户端同时读写</li>
<li>容错性：多副本、多分区，允许集群中节点失败，如果副本数据量为n,则可以n-1个节点失败</li>
<li>扩展性强：支持热扩展</li>
</ul>
</li>
<li>基于消费者组可以实现
<ul>
<li>基于队列的模型：所有消费者都在同一消费者组里，每条消息只会被一个消费者处理</li>
<li>基于发布订阅模型：消费者属于不同的消费者组，假如每个消费者都有自己的消费者组，这样kafka消息就能广播到所有消费者实例上</li>
</ul>
</li>
</ul>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[01-新⼀代流式处理框架Flink介绍和重要概念讲解]]></title>
    <link href="https://huanglei.work/17418277612043.html"/>
    <updated>2025-03-13T09:02:41+08:00</updated>
    <id>https://huanglei.work/17418277612043.html</id>
    <content type="html"><![CDATA[
<ul>
<li>
<p>什么是Flink</p>
<ul>
<li>Apache Flink 是一个框架和分布式处理引擎，用于在无边界和有边界数据流上进行有状态的计算</li>
<li>官网：<a href="https://flink.apache.org/zh/flink-architecture.html">https://flink.apache.org/zh/flink-architecture.html</a></li>
</ul>
</li>
<li>
<p>有谁在用呢（基本大厂都在用）</p>
<ul>
<li>用来做啥：实时数仓建设、实时数据监控、实时反作弊风控、画像系统等<br />
<img src="https://image.huanglei.work/mweb/2025/3/13/9d4fa0e6-3014-4d89-80d0-b486380ff68a.png" alt="31696751.png" /><br />
<img src="https://image.huanglei.work/mweb/2025/3/13/c0949526-e7ea-4c1a-bc16-61ffd0e01ca2.png" alt="31744063.png" /></li>
</ul>
</li>
<li>
<p>概念</p>
<ul>
<li>数据流
<ul>
<li>任何类型的数据都可以形成一种事件流，信用卡交易、传感器测量、机器日志、网站或移动应用程序上的用户交互记录，所有这些数据都形成一种流</li>
</ul>
</li>
<li>什么是有界流
<ul>
<li>有定义流的开始，也有定义流的结束。有界流可以在摄取所有数据后再进行计算。有界流所有数据可以被排序，所以并不需要有序摄取。有界流处理通常被称为批处理</li>
</ul>
</li>
<li>什么是无界流
<ul>
<li>有定义流的开始，但没有定义流的结束。它们会无休止地产生数据。无界流的数据必须持续处理，即数据被摄取后需要立刻处理。我们不能等到所有数据都到达再处理，因为输入是无限的，在任何时候输入都不会完成。处理无界数据通常要求以特定顺序摄取事件，例如事件发生的顺序，以便能够推断结果的完整性</li>
</ul>
</li>
</ul>
</li>
<li>
<p>Apache Flink 擅长处理无界和有界数据集，有出色的性能<br />
<img src="https://image.huanglei.work/mweb/2025/3/13/aa77e8fe-3bb1-4b12-8c59-79ae6fde6f45.png" alt="31835687.png" /></p>
</li>
<li>
<p>代码使用例子</p>
<ul>
<li>source、transformation、sink 都是 operator算子<br />
<img src="https://image.huanglei.work/mweb/2025/3/13/d6380a88-77ed-4de1-b5e2-91de24ddf73e.png" alt="31884554.png" /></li>
</ul>
</li>
</ul>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[02-Input、Output Stream流]]></title>
    <link href="https://huanglei.work/17419191581307.html"/>
    <updated>2025-03-14T10:25:58+08:00</updated>
    <id>https://huanglei.work/17419191581307.html</id>
    <content type="html"><![CDATA[
<h4><a id="%E7%AC%AC1%E9%9B%86-java%E6%A0%B8%E5%BF%83%E5%AD%97%E7%AC%A6%E7%BC%96%E7%A0%81%E5%92%8C%E5%AD%97%E7%AC%A6%E5%AD%97%E8%8A%82%E6%B5%81%E4%BB%8B%E7%BB%8D" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>第1集 Java核心字符编码和字符-字节流介绍</h4>
<p><strong>简介：Java核心字符编码和字符-字节流介绍</strong></p>
<ul>
<li>电报的作用是解决什么问题？</li>
</ul>
<img src="https://image.huanglei.work/mweb/2025/3/14/2deb704b-9d8f-4a1e-b486-370ea1403589.png" alt="image-20240520152822093" style="zoom:20%;" />
<ul>
<li>
<p>什么是编码</p>
<ul>
<li>
<p>将信息从一种形式或格式转换为另一种形式或格式的过程，特别是在计算机科学和电信领域。</p>
</li>
<li>
<p>在字符编码的上下文中，编码指的是将字符（如字母、数字、标点符号等）转换为计算机可以理解的二进制形式</p>
</li>
<li>
<p>常用的有</p>
<ul>
<li>
<p>ASCII 编码（American Standard Code for Information Interchange，美国信息交换标准代码）</p>
<ul>
<li>
<p>是一种基于拉丁字母的字符编码标准，它主要用于显示现代英语和其他西欧语言，包括控制符（如换行符和回车符）</p>
</li>
<li>
<p>可显示的字符（如字母、数字、标点符号、制表符等），以及一些特殊字符</p>
</li>
<li>
<p>只支持128个字符，因此它并不能表示所有语言的字符，尤其是像中文这样的复杂字符系统。</p>
</li>
<li>
<p>为解决问题，开发各种扩展ASCII编码，如ISO-8859系列（用于西欧语言）、GB2312/GBK/GB18030（简体中文）等</p>
</li>
</ul>
</li>
<li>
<p>UTF-8（8位元，Universal Character Set/Unicode Transformation Format）</p>
<ul>
<li>是针对Unicode的一种可变长度字符编码。</li>
<li>它可以用来表示Unicode标准中的任何字符，包括世界上几乎所有的书写语言的字符</li>
</ul>
</li>
<li>
<p>GBK编码</p>
<ul>
<li>是一种针对汉字的字符编码标准，也被称为GBK/GB2312。</li>
<li>它是汉字编码国家标准GB 2312-1980的扩展，包含了GB2312编码中的全部汉字</li>
<li>英文字母、数字、标点等非汉字字符仍然只占用一个字节，其编码值与ASCII码相同</li>
</ul>
</li>
<li>
<p>ISO-8859-1编码</p>
<ul>
<li>是单字节编码，向下兼容ASCII编码。,它的编码范围是0x00-0xFF，其中0x00-0x7F之间完全和ASCII编码一致</li>
<li>用于表示英语字符；0x80-0xFF之间用于表示其他语言字符，如西欧语言、希腊语等。</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>什么是java的IO流</p>
<ul>
<li>Java中的I/O（输入/输出）流是用于处理数据输入和输出的抽象类。</li>
<li>Java I/O流主要分为两大类：字节流（Byte Streams）和字符流（Character Streams）。
<ul>
<li><strong>字节流</strong>：用于处理二进制数据，包括InputStream和OutputStream两个主要类及其子类。</li>
<li><strong>字符流</strong>：用于处理文本数据，包括Reader和Writer两个主要类及其子类。</li>
</ul>
</li>
</ul>
</li>
<li>
<p><strong>字节和字符流区别</strong></p>
<ul>
<li>处理的数据类型
<ul>
<li>字节流
<ul>
<li>处理的是字节数据，即8位二进制数据，在计算机中，所有的文件都能以二进制（字节）形式存在。</li>
<li>Java的I/O中针对字节传输操作提供了一系列流，统称为字节流。</li>
<li>这些流包括两个抽象基类InputStream和OutputStream，分别处理字节流的输入和输出</li>
</ul>
</li>
<li>字符流
<ul>
<li>处理的是字符数据，即Unicode字符，通常是16位二进制数据。</li>
<li>字符流是16位unicode字符流，主要用于处理字符和文本文件。</li>
<li>由于Java中字符是采用Unicode标准，因此字符流在处理文本数据时具有更高的效率和准确性。</li>
</ul>
</li>
</ul>
</li>
<li>编码问题
<ul>
<li>字节流
<ul>
<li>因为直接操作的是字节，没有编码问题，字节流可以处理任意类型的数据，包括文本、图片、音频等。</li>
<li>当使用字节流处理文本文件时，需要自行处理编码问题，否则可能会出现乱码</li>
</ul>
</li>
<li>字符流
<ul>
<li>Java使用Unicode编码来表示字符，而外部数据源可能使用不同的编码方式。</li>
<li>字符流在读取或写入文本文件时，会自动进行字符编码的转换，使用字符流处理文本文件时通常不需要担心编码问题。</li>
</ul>
</li>
</ul>
</li>
<li>使用场景
<ul>
<li>字节流：
<ul>
<li>字节流以字节（8bit）为单位，适合处理图片、视频、音频等二进制文件，以及网络传输等场景。</li>
<li>由于字节流直接操作字节数据，因此具有更高的灵活性和效率</li>
</ul>
</li>
<li>字符流：
<ul>
<li>字符流以字符为单位，根据码表映射字符，一次可能读多个字节，适合处理文本文件、文本数据等场景。</li>
<li>字符流在处理文本数据时具有更高的效率和准确性，因为字符流会自动处理字符编码的转换</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>IO流相关类体系概览</p>
<ul>
<li>
<p>功能不同，但是具有共性内容，通过不断抽象形成4个抽象类，抽象类下面有很多子类是具体的实现</p>
<ul>
<li>字符流 Reader/Writer</li>
<li>字节流 InputStream/OutputStream</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><img src="https://image.huanglei.work/mweb/2025/3/14/af6f6527-61e9-44a1-90db-1dcbe58dea0e.png" alt="image-20191023000405651" /></p>
<h4><a id="%E7%AC%AC2%E9%9B%86-java%E8%BE%93%E5%85%A5%E6%B5%81inputstream%E6%A1%88%E4%BE%8B%E5%AE%9E%E6%88%98" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>第2集 Java输入流InputStream案例实战</h4>
<p><strong>简介：讲解InputStream相关介绍及其子类</strong></p>
<ul>
<li>
<p>InputStream</p>
<ul>
<li>是输入字节流的父类，它是一个抽象类（一般用他的子类）</li>
<li>在Java中，<code>InputStream</code>是所有字节输入流的超类。</li>
<li>它定义了字节流输入的基本操作，如读取字节、跳过字节和标记/重置流等。</li>
<li>通过<code>InputStream</code>，我们可以从文件、网络连接或其他数据源中读取字节数据。</li>
</ul>
</li>
<li>
<p>常见方法</p>
<ul>
<li>
<p><code>int read()</code></p>
<ul>
<li>从输入流中读取单个字节,返回0到255范围内的int字节值, 字节数据可直接转换为int类型</li>
<li>如果已经到达流末尾而没有可用的字节，则返回－1</li>
</ul>
</li>
<li>
<p><code>int read(byte[] b)</code>：</p>
<ul>
<li>从输入流中读取最多<code>b.length</code>个字节的数据到字节数组<code>b</code>中，并返回实际读取的字节数。</li>
<li>如果因为已经到达流末尾而没有更多的数据，则返回-1。</li>
</ul>
<p><img src="https://image.huanglei.work/mweb/2025/3/14/670d8b0f-b836-44f5-b4a4-02419a61e7f0.png" alt="image-20240520161336253" /></p>
</li>
<li>
<p><code>int read(byte[] b, int off, int len)</code>：</p>
<ul>
<li>从输入流中读取最多<code>len</code>个字节的数据到字节数组<code>b</code>中，从<code>off</code>指定的偏移量开始存储，并返回实际读取的字节数。</li>
<li>如果因为已经到达流末尾而没有更多的数据，则返回-1。</li>
</ul>
</li>
<li>
<p><code>long skip(long n)</code>：</p>
<ul>
<li>跳过输入流中的<code>n</code>个字节。如果实际跳过的字节数小于<code>n</code>，则可能是因为已经到达流的末尾。</li>
<li>此方法返回实际跳过的字节数。</li>
</ul>
</li>
<li>
<p><code>int available()</code>：返回可以从此输入流中读取的字节数的估计值。</p>
</li>
<li>
<p><code>void close()</code>：关闭此输入流并释放与该流关联的系统资源。</p>
</li>
</ul>
</li>
<li>
<p>常见子类</p>
<ul>
<li>
<p>FileInputStream</p>
<ul>
<li>抽象类InputStream用来具体实现类的创建对象,  文件字节输入流, 对文件数据以字节的形式进行读取操作</li>
</ul>
<pre><code class="language-Java">//常用构造函数,传入文件所在地址
public FileInputStream(String name) throws FileNotFoundException

//常用构造函数,传入文件对象
public FileInputStream(File file) throws FileNotFoundException
</code></pre>
</li>
<li>
<p>ByteArrayInputStream  字节数组输入流</p>
</li>
<li>
<p>ObjectInputStream 对象输入流</p>
</li>
<li>
<p>....还有很多</p>
</li>
</ul>
</li>
<li>
<p>案例实战</p>
<pre><code class="language-Java">public class InputStreamDemo {

    public static void main(String[] args) {
        String dir = &quot;/Users/xdclass/Desktop/coding/xdclass-account/src/chapter11&quot;;
        String name = &quot;a.txt&quot;;
        File file = new File(dir, name);
        try (InputStream inputStream = new FileInputStream(file)) {
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                // 处理读取到的数据（例如打印到控制台）
                System.out.println(new String(buffer, 0, bytesRead));
                //中文乱码问题，换成GBK 或者 UTF-8
                //System.out.println(new String(buffer,&quot;UTF-8&quot;));
                //System.out.println(new String(buffer,0, bytesRead,&quot;UTF-8&quot;));
                //System.out.println(new String(buffer, 0, bytesRead));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
</code></pre>
</li>
<li>
<p>注意</p>
<ul>
<li>在上面的示例中，使用了try-with-resources语句来自动关闭<code>InputStream</code>。</li>
<li>这是JDK 7及更高版本引入的一个新特性，用于确保在不再需要资源时自动关闭它们。</li>
</ul>
</li>
<li>
<p>编码小知识（节省空间）</p>
<ul>
<li>
<p>操作的中文内容多则推荐GBK：</p>
<ul>
<li>GBK中英文也是两个字节,用GBK节省了空间,</li>
<li>UTF-8 编码的中文使用了三个字节</li>
</ul>
</li>
<li>
<p>如果是英文内容多则推荐UFT-8:</p>
<ul>
<li>因为UFT-8里面英文只占一个字节</li>
<li>UTF-8编码的中文使用了三个字节</li>
</ul>
</li>
</ul>
</li>
<li>
<p>IDEA编码格式配置</p>
</li>
</ul>
<p><img src="https://image.huanglei.work/mweb/2025/3/14/8b0606fe-b35f-4f37-9290-20da3b968959.png" alt="image-20240521114040085" /></p>
<h4><a id="%E7%AC%AC3%E9%9B%86-java%E8%BE%93%E5%87%BA%E6%B5%81-outputstream%E6%A1%88%E4%BE%8B%E5%AE%9E%E6%88%98" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>第3集 Java输出流 OutputStream案例实战</h4>
<p><strong>简介：讲解OutputStream相关介绍及其子类</strong></p>
<ul>
<li>
<p>OutputStream</p>
<ul>
<li>是输出字节流的父类，它是一个抽象类，在Java中，<code>OutputStream</code>是所有字节输出流的超类。</li>
<li>它定义了字节流输出的基本操作，如写入字节、刷新输出流和关闭输出流等。</li>
<li>通过<code>OutputStream</code>，可以将数据写入文件、网络连接或其他数据接收端。</li>
</ul>
</li>
<li>
<p>OutputStream的主要方法</p>
<ul>
<li><code>void write(int b)</code>：将指定的字节写入此输出流。</li>
<li><code>void write(byte[] b)</code>：将<code>b.length</code>个字节从指定的字节数组写入此输出流。</li>
</ul>
<p><img src="https://image.huanglei.work/mweb/2025/3/14/670d8b0f-b836-44f5-b4a4-02419a61e7f0.png" alt="image-20240520161336253" /></p>
<ul>
<li><code>void write(byte[] b, int off, int len)</code>：从指定的字节数组写入<code>len</code>个字节，从偏移量<code>off</code>开始。</li>
<li><code>void flush()</code>
<ul>
<li>刷新此输出流并强制写出任何缓冲的输出字节，</li>
<li>进行输出时，为了提高效率，这些类通常会实现缓存机制。</li>
<li>当调用<code>write()</code>方法写入数据时，数据可能并不会立即被发送到目标位置，而是先被存储在内部缓冲区中。</li>
<li><strong>当缓冲区满或我们显式地调用<code>flush()</code>方法时</strong></li>
</ul>
</li>
<li><code>void close()</code>：关闭此输出流并释放与此流相关联的任何系统资源</li>
</ul>
</li>
</ul>
<ul>
<li>
<p>OutputStream的子类</p>
<ul>
<li><code>FileOutputStream</code>，抽象类用来具体实现类的创建对象,  文件字节输出流, 对文件数据以字节的形式进行输出的操作</li>
</ul>
<pre><code class="language-Java">//传入输出的文件地址
public FileOutputStream(String name)
 
//传入目标输出的文件对象
public FileOutputStream(File file) 

//传入目标输出的文件对象, 是否可以追加内容
public FileOutputStream(File file, boolean append)
</code></pre>
<ul>
<li><code>ByteArrayOutputStream</code>：在内存中创建一个缓冲区，所有写入流的数据都会置入这个缓冲区。</li>
<li>....还有很多</li>
</ul>
</li>
<li>
<p>案例实战</p>
<pre><code class="language-Java">public class OutputStreamExample {  
    public static void main(String[] args) {
    
        String dir = &quot;/Users/xdclass/Desktop/coding/xdclass-account/src/chapter11&quot;;
        String name = &quot;b.txt&quot;;
        
        try (OutputStream outputStream = new FileOutputStream(file)) {  
            String data = &quot;Hello, World!&quot;;  
            outputStream.write(data.getBytes());  
            outputStream.flush(); // 确保所有数据都写入文件  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  
}
</code></pre>
</li>
<li>
<p>注意：在Java中，通常会在以下情况下调用<code>flush()</code>方法：</p>
<ul>
<li>在完成所有写入操作并希望确保所有数据都被发送到目标位置时。</li>
<li>在需要在写入过程中立即看到数据的效果时（例如，在网络编程中）。</li>
<li>在关闭输出流之前，以确保所有缓冲的数据都被发送出去。</li>
</ul>
</li>
</ul>
<h4><a id="%E7%AC%AC4%E9%9B%86-java-io%E5%8C%85%E4%B9%8B%E7%BC%93%E5%86%B2buffer%E8%BE%93%E5%85%A5%E8%BE%93%E5%87%BA%E6%B5%81%E4%BB%8B%E7%BB%8D" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>第4集 Java IO包之缓冲Buffer输入输出流介绍</h4>
<p><strong>简介： Java IO包之缓冲Buffer输入输出流介绍</strong></p>
<ul>
<li>
<p>什么是<strong>缓冲Buffer</strong></p>
<ul>
<li>它是内存空间的一部分，在内存空间中预留了一定的存储空间</li>
<li>这些存储空间用来缓冲输入或输出的数据，这部分空间就叫做缓冲区，缓冲区是具有一定大小的</li>
<li>当使用缓冲流时，数据首先被读写到缓存区中，然后再从缓存区传输到目标位置（如文件、网络等）。</li>
<li>这种方式减少了直接对目标位置的读写操作，因此提高了性能。</li>
</ul>
</li>
<li>
<p>为啥要用缓冲</p>
<ul>
<li>缓冲，缓和冲击，例如操作磁盘比内存慢的很多，所以不用缓冲区效率很低，数据传输速度和数据处理的速度存在不平衡</li>
<li>比如
<ul>
<li>每秒要读写100次硬盘，对系统冲击很大，浪费了大量时间在忙着处理开始写和结束写这两个事件</li>
<li>所以用内存的buffer暂存起来，变成每10秒写一次硬盘，数据可以直接送往缓冲区</li>
</ul>
</li>
<li>高速设备不用再等待低速设备，对系统的冲击就很小，写入效率高了</li>
</ul>
</li>
<li>
<p>Java中的缓冲输入流与输出流（提供了四种Buffer流）</p>
<ul>
<li><strong>BufferedInputStream</strong>：缓存输入流，封装了InputStream，提供了缓存区来暂存输入数据。</li>
<li><strong>BufferedOutputStream</strong>：缓存输出流，封装了OutputStream，提供了缓存区来暂存输出数据。</li>
<li><strong>BufferedReader</strong>：缓存字符输入流，封装了Reader，提供了缓存区来暂存字符输入数据。</li>
<li><strong>BufferedWriter</strong>：缓存字符输出流，封装了Writer，提供了缓存区来暂存字符输出数据。</li>
<li>采用的<strong>装饰器设计模式</strong>（锦上添花）</li>
</ul>
</li>
<li>
<p>主要特点：<strong>由于使用了缓存区，因此减少了直接对目标位置的读写操作，从而提高了性能。</strong></p>
</li>
<li>
<p><strong>BufferedInputStream 缓冲字节输入流</strong></p>
<ul>
<li>
<p>通过预先读入一整段原始输入流数据至缓冲区中，外界对BufferedInputStream的读取操作实际上是在缓冲区上进行，</p>
</li>
<li>
<p>如果读取的数据超过了缓冲区的范围，BufferedInputStream负责重新从原始输入流中载入下一截数据，填充缓冲区</p>
</li>
<li>
<p>然后外界继续通过缓冲区进行数据读取，避免了大量的磁盘IO，原始的InputStream类实现的read是即时读取的</p>
</li>
<li>
<p>因为每一次读取都会是一次磁盘IO操作（哪怕只读取了1个字节的数据），如果数据量巨大，这样的磁盘消耗非常可怕。</p>
</li>
<li>
<p>读取可以读取缓冲区中的内容，当读取超过缓冲区的内容后再进行一次磁盘IO</p>
</li>
<li>
<p>载入一段数据填充缓冲，下一次读取一般情况就直接可以从缓冲区读取，减少了磁盘IO。</p>
</li>
<li>
<p>默认缓冲区大小是8k, <code>int DEFAULT_BUFFER_SIZE = 8192;</code></p>
</li>
<li>
<p>构造函数</p>
<pre><code class="language-Java">//对输入流进行包装，里面默认的缓冲区是8k
public BufferedInputStream(InputStream in);

//对输入流进行包装,指定创建具有指定缓冲区大小的
public BufferedInputStream(InputStream in,int size);
</code></pre>
</li>
<li>
<p>常用方法</p>
<pre><code class="language-Java">/从输入流中读取一个字节
public int read();

//从字节输入流中,给定偏移量offset处开始, 将len字节读取到指定的byte数组中。
public int read(byte[] buf,int off,int len);

//关闭释放资源，关闭的时候这个流即可，InputStream会在里面被关闭
void close();
</code></pre>
</li>
</ul>
</li>
<li>
<p><strong>BufferedOutputStream 缓冲字节输出流</strong></p>
<ul>
<li>内部使用一个缓冲区来暂存待写入的数据。</li>
<li>当缓冲区满时，或者调用<code>flush()</code>方法时，缓冲区中的数据会被一次性写入到底层输出流中。</li>
<li>这种机制提高了数据写入的效率，减少了系统I/O操作的次数。</li>
<li>构造函数</li>
</ul>
<pre><code class="language-Java">//对输出流进行包装,里面默认的缓冲区是8k
public BufferedOutputStream(OutputStream out);

//对输出流进行包装,指定创建具有指定缓冲区大小的
public BufferedOutputStream(OutputStream out,int size);
</code></pre>
<ul>
<li>常用的三个方法</li>
</ul>
<pre><code class="language-Java">  //向输出流中输出一个字节
  public void write(int b);

  //将指定 byte 数组中从偏移量 off 开始的 len 个字节写入缓冲的输出流。
  public void write(byte[] buf,int off,int len);

  //刷新此缓冲的输出流，强制使所有缓冲的输出字节被写出到底层输出流中。
  public void flush();

  //关闭释放资源，关闭的时候这个流即可，OutputStream会在里面被关闭, JDK7新特性try(在这里声明的会自动关闭){}
  void close();
</code></pre>
</li>
</ul>
<ul>
<li>
<p>注意点</p>
<ul>
<li>在使用完<code>BufferedOutputStream</code>后，一定要记得关闭它，释放系统资源，通过调用<code>close()</code>方法来实现关闭操作。</li>
<li>BufferedOutputStream在close()时会自动flush</li>
</ul>
<pre><code class="language-Java"> public void close() throws IOException {
      try (OutputStream ostream = out) {
          flush();
      }
  }
</code></pre>
<ul>
<li>在不调用close()的情况下，缓冲区不满，又需要把缓冲区的内容写入到文件或通过网络发送到别的机器时，才需要调用flush.</li>
</ul>
<ul>
<li><strong>流的关闭顺序： 后开先关, 如果A依赖B，先关闭B</strong></li>
</ul>
</li>
</ul>
<h4><a id="%E7%AC%AC5%E9%9B%86%E6%96%B0%E7%89%88%E7%BC%93%E5%86%B2-buffer%E8%BE%93%E5%85%A5%E8%BE%93%E5%87%BA%E6%B5%81%E7%BB%BC%E5%90%88%E6%A1%88%E4%BE%8B%E5%AE%9E%E6%88%98" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>第5集 新版缓冲Buffer输入输出流综合案例实战</h4>
<p><strong>简介： 新版缓冲Buffer输入输出流综合案例实战</strong></p>
<ul>
<li>
<p>新版try-with-resource语法</p>
<ul>
<li>在Java中，从JDK 7开始，可以使用try-with-resources语句</li>
<li>来自动管理实现了<code>AutoCloseable</code>或<code>Closeable</code>接口的资源如文件流、缓冲流等。</li>
<li>这种语法可以确保资源在try代码块执行完毕后被正确关闭，即使try块中的代码抛出了异常。</li>
</ul>
</li>
<li>
<p>缓冲流输入案例实战</p>
<ul>
<li>创建了一个<code>FileInputStream</code>并将其包装在<code>BufferedInputStream</code>中。</li>
<li>通过<code>bis.read(buffer)</code>方法，可以从文件中读取数据到缓冲区中，并在读取过程中处理这些数据。</li>
<li>在try代码块结束后，<code>BufferedInputStream</code>和<code>FileInputStream</code>都会被自动关闭</li>
</ul>
<pre><code class="language-Java">import java.io.*;  
  
public class BufferedInputStreamExample {  
    public static void main(String[] args) {  
        String inputFilePath = &quot;input.txt&quot;; // 假设这个文件存在并包含一些数据  
        try (FileInputStream fis = new FileInputStream(inputFilePath);  
             BufferedInputStream bis = new BufferedInputStream(fis)) {  
  
            byte[] buffer = new byte[1024];  
            int bytesRead;  
  
            // 读取数据  
            while ((bytesRead = bis.read(buffer)) != -1) {  
                // 处理读取到的数据（这里只是简单地打印出来）  
                System.out.print(new String(buffer, 0, bytesRead));  
            }  
  
            // 注意：由于使用了try-with-resources，close()方法在这里是自动调用的  
  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  
}
</code></pre>
</li>
<li>
<p>缓冲流输出案例实战</p>
<ul>
<li>在这个例子中，创建了一个<code>FileOutputStream</code>并将其包装在<code>BufferedOutputStream</code>中。</li>
<li>在try代码块结束后，<code>BufferedOutputStream</code>和<code>FileOutputStream</code>都会被自动关闭，因为它们都实现了<code>AutoCloseable</code>接口。</li>
</ul>
<pre><code class="language-Java">import java.io.*;  
  
public class BufferedOutputStreamExample {  
    public static void main(String[] args) {  
        String outputFilePath = &quot;output.txt&quot;;  
        try (FileOutputStream fos = new FileOutputStream(outputFilePath);  
             BufferedOutputStream bos = new BufferedOutputStream(fos)) {  
  
            // 写入数据到缓冲区  
            String data = &quot;Hello, World!&quot;;  
            bos.write(data.getBytes());  
  
            // 注意：由于使用了try-with-resources，flush()和close()方法在这里是自动调用的  
  
            System.out.println(&quot;数据已成功写入文件。&quot;);  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  
}
</code></pre>
</li>
<li>
<p>拓展旧版语法，<strong>流的关闭顺序： 后开先关, 如果A依赖B，先关闭B</strong></p>
<pre><code class="language-Java">public class BufferedInputStreamExample {  
    public static void main(String[] args) {  
        try {  
            // 创建一个文件输入流  
            FileInputStream fis = new FileInputStream(&quot;example.txt&quot;);  
              
            // 创建一个缓冲输入流，并封装文件输入流  
            BufferedInputStream bis = new BufferedInputStream(fis);  
              
            // 定义一个字节数组用于存储读取的数据  
            byte[] buffer = new byte[1024];  
            int bytesRead;  
              
            // 循环读取数据直到文件末尾  
            while ((bytesRead = bis.read(buffer)) != -1) {  
                // 处理读取到的数据（这里只是简单地打印出来）  
                System.out.println(new String(buffer, 0, bytesRead));  
            }  
              
            // 关闭缓冲输入流和文件输入流  
            bis.close();  
            fis.close();  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  
}


public class BufferedOutputStreamExample {  
    public static void main(String[] args) {  
        try {  
            // 创建一个文件输出流  
            FileOutputStream fos = new FileOutputStream(&quot;output.txt&quot;);  
              
            // 创建一个带缓冲区的输出流，并封装文件输出流  
            BufferedOutputStream bos = new BufferedOutputStream(fos);  
              
            // 写入数据到缓冲区  
            String data = &quot;Hello, World!&quot;;  
            bos.write(data.getBytes());  
              
            // 刷新缓冲区，确保数据被写入文件  
            bos.flush();  
              
            // 关闭输出流和底层流  
            bos.close();  
            fos.close();  
              
            System.out.println(&quot;数据已成功写入文件。&quot;);  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  
}
</code></pre>
</li>
</ul>
<h4><a id="%E7%AC%AC6%E9%9B%86%E8%AF%BE%E7%A8%8B%E4%BD%9C%E4%B8%9A%E4%B9%8B-java%E5%AE%9E%E7%8E%B0%E6%96%87%E4%BB%B6%E7%9A%84%E6%8B%B7%E8%B4%9D%E6%A1%88%E4%BE%8B%E5%AE%9E%E6%88%98%E3%80%8A%E4%B8%8A%E3%80%8B" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>第6集 课程作业之Java实现文件的拷贝案例实战《上》</h4>
<p><strong>简介： 课程作业之Java实现文件的拷贝案例实战</strong></p>
<ul>
<li>
<p>需求</p>
<ul>
<li>编写一个Java程序，实现将一个文件从源路径复制到目标路径的功能。</li>
<li>在这个过程中，你需要使用<code>BufferedInputStream</code>和<code>BufferedOutputStream</code>来提高文件传输的效率。</li>
</ul>
</li>
<li>
<p>作业思路参考</p>
<ul>
<li>定义一个<code>copyFile</code>方法，该方法接收两个参数：源文件的路径和目标文件的路径</li>
<li>在<code>copyFile</code>方法中，使用<code>BufferedInputStream</code>和<code>BufferedOutputStream</code>来读取源文件内容并写入到目标文件中。</li>
<li>确保在拷贝文件的过程中，源文件和目标文件都可以是任意大小的文件，并且拷贝过程应该能够处理大文件。</li>
<li>在拷贝完成后，输出一条消息到控制台，表示文件拷贝成功。</li>
<li>如果在拷贝过程中发生任何异常（如源文件不存在、目标文件无法创建等），请捕获异常并输出一条错误消息到控制台。</li>
<li>最后，编写一个<code>main</code>方法来测试你的<code>copyFile</code>方法。</li>
</ul>
</li>
<li>
<p>参考骨架案例</p>
<pre><code class="language-Java">import java.io.*;  
  
public class FileCopier {  
  
    public static void copyFile(String sourceFilePath, String targetFilePath) {  
        // 在这里实现文件拷贝逻辑  
    }  
  
    public static void main(String[] args) {  
        // 示例源文件和目标文件路径  
        String sourceFilePath = &quot;path/to/source/file.txt&quot;;  
        String targetFilePath = &quot;path/to/target/file.txt&quot;;  
  
        // 调用copyFile方法  
        try {  
            copyFile(sourceFilePath, targetFilePath);  
            System.out.println(&quot;文件拷贝成功！&quot;);  
        } catch (Exception e) {  
            System.err.println(&quot;文件拷贝失败：&quot; + e.getMessage());  
        }  
    }  
}
</code></pre>
</li>
<li>
<p>知识点进阶：<strong>Files和Paths类</strong></p>
<ul>
<li>
<p><code>java.nio.file</code> 包是 Java 7 引入的一个新的文件系统 API，提供了更加灵活的文件 I/O 操作。</p>
</li>
<li>
<p>这个包中的 <code>Files</code> 和 <code>Paths</code> 类是文件操作中非常常用的两个工具类</p>
</li>
<li>
<p><code>Files</code> 类提供了一系列静态方法，用于在文件系统中执行各种操作，如读取、写入、复制、移动、删除文件等</p>
<ul>
<li><code>Files.readAllBytes(Path path)</code>: 读取指定路径的文件到字节数组中。</li>
<li><code>Files.delete(Path path)</code>: 删除文件或目录。</li>
<li><code>Files.exists(Path path, LinkOption... options)</code>: 检查文件或目录是否存在。</li>
<li><code>Files.createDirectories(Path dir, FileAttribute&lt;?&gt;... attrs)</code>: 创建目录，包括所有不存在的父目录。</li>
</ul>
</li>
<li>
<p><code>Paths</code>  是一个工具类，用于创建和操作 <code>Path</code> 对象，是 <code>java.nio.file</code> 包中的一个重要类，表示文件系统中的一个路径</p>
<ul>
<li><code>Paths</code> 类中常用的方法是 <code>Paths.get(URI uri)</code> 它们用于创建 <code>Path</code> 对象；<code>Path</code> 对象提供了很多方法来操作和查询路径</li>
</ul>
<pre><code class="language-plain_text">Path path = Paths.get(&quot;/home/user/file.txt&quot;);
</code></pre>
<ul>
<li>
<p><code>Path.getFileName()</code>: 获取路径中的文件名。</p>
</li>
<li>
<p><code>Path.getParent()</code>: 获取路径的父目录。</p>
</li>
<li>
<p><code>Path.toFile()</code>: 将路径转换为 <code>File</code> 对象（注意，<code>File</code> 类是 <code>java.io</code> 包中的类）。</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h4><a id="%E7%AC%AC7%E9%9B%86%E8%AF%BE%E7%A8%8B%E4%BD%9C%E4%B8%9A%E4%B9%8B-java%E5%AE%9E%E7%8E%B0%E6%96%87%E4%BB%B6%E7%9A%84%E6%8B%B7%E8%B4%9D%E6%A1%88%E4%BE%8B%E5%AE%9E%E6%88%98%E3%80%8A%E4%B8%8B%E3%80%8B" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>第7集 课程作业之Java实现文件的拷贝案例实战《下》</h4>
<p><strong>简介： 课程作业之Java实现文件的拷贝案例实战</strong></p>
<ul>
<li>
<p>具体实现</p>
<pre><code class="language-Java">package chapter11;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class FileCopier {

    public static void copyFile(String sourceFilePath, String targetFilePath) throws IOException {
        // 检查目标文件路径中的目录是否存在
        Path targetPath = Paths.get(targetFilePath).getParent();
        if (targetPath != null &amp;&amp; !Files.exists(targetPath)) {
            // 如果目录不存在，则创建它
            Files.createDirectories(targetPath);
        }

        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(sourceFilePath));
             BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(targetFilePath))) {

            byte[] buffer = new byte[1024];
            int bytesRead;

            while ((bytesRead = bis.read(buffer)) != -1) {
                bos.write(buffer, 0, bytesRead);
            }

            // 注意：由于使用了try-with-resources，流在这里会被自动关闭
        }
    }

    public static void main(String[] args) {
        // 示例源文件和目标文件路径
        String sourceFilePath = &quot;/Users/xdclass/Desktop/coding/xdclass-account/src/chapter11/a.txt&quot;;
        String targetFilePath = &quot;/Users/xdclass/Desktop/coding/xdclass-account/src/chapter11/a/target/file.txt&quot;;

        // 调用copyFile方法
        try {
            copyFile(sourceFilePath, targetFilePath);
            System.out.println(&quot;文件拷贝成功！&quot;);
        } catch (IOException e) {
            System.err.println(&quot;文件拷贝失败：&quot; + e.getMessage());
        }
    }
}
</code></pre>
<ul>
<li>思路流程
<ul>
<li>使用<code>FileInputStream</code>和<code>FileOutputStream</code>分别创建源文件和目标文件的输入/输出流。</li>
<li>将<code>FileInputStream</code>包装在<code>BufferedInputStream</code>中，以便使用缓冲来提高读取效率。</li>
<li>将<code>FileOutputStream</code>包装在<code>BufferedOutputStream</code>中，以便使用缓冲来提高写入效率。</li>
<li>使用循环和<code>read</code>、<code>write</code>方法来从源文件读取数据并写入目标文件，直到没有更多数据可读为止。</li>
<li>在<code>finally</code>块中（或使用try-with-resources）确保关闭所有打开的流</li>
</ul>
</li>
</ul>
</li>
</ul>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[02-Java基础常见面试题总结(中)]]></title>
    <link href="https://huanglei.work/17420865517348.html"/>
    <updated>2025-03-16T08:55:51+08:00</updated>
    <id>https://huanglei.work/17420865517348.html</id>
    <content type="html"><![CDATA[
<ul>
<li><a href="#%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E5%9F%BA%E7%A1%80">面向对象基础</a>
<ul>
<li><a href="#%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E5%92%8C%E9%9D%A2%E5%90%91%E8%BF%87%E7%A8%8B%E7%9A%84%E5%8C%BA%E5%88%AB">面向对象和面向过程的区别</a></li>
<li><a href="#%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA%E5%AF%B9%E8%B1%A1%E7%94%A8%E4%BB%80%E4%B9%88%E8%BF%90%E7%AE%97%E7%AC%A6%E5%AF%B9%E8%B1%A1%E5%AE%9E%E4%BD%93%E4%B8%8E%E5%AF%B9%E8%B1%A1%E5%BC%95%E7%94%A8%E6%9C%89%E4%BD%95%E4%B8%8D%E5%90%8C">创建一个对象用什么运算符?对象实体与对象引用有何不同?</a></li>
<li><a href="#%E5%AF%B9%E8%B1%A1%E7%9A%84%E7%9B%B8%E7%AD%89%E5%92%8C%E5%BC%95%E7%94%A8%E7%9B%B8%E7%AD%89%E7%9A%84%E5%8C%BA%E5%88%AB">对象的相等和引用相等的区别</a></li>
<li><a href="#%E5%A6%82%E6%9E%9C%E4%B8%80%E4%B8%AA%E7%B1%BB%E6%B2%A1%E6%9C%89%E5%A3%B0%E6%98%8E%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95%EF%BC%8C%E8%AF%A5%E7%A8%8B%E5%BA%8F%E8%83%BD%E6%AD%A3%E7%A1%AE%E6%89%A7%E8%A1%8C%E5%90%97">如果一个类没有声明构造方法，该程序能正确执行吗?</a></li>
<li><a href="#%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95%E6%9C%89%E5%93%AA%E4%BA%9B%E7%89%B9%E7%82%B9%EF%BC%9F%E6%98%AF%E5%90%A6%E5%8F%AF%E8%A2%ABoverride">构造方法有哪些特点？是否可被 override?</a></li>
<li><a href="#%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%89%E5%A4%A7%E7%89%B9%E5%BE%81">面向对象三大特征</a>
<ul>
<li><a href="#%E5%B0%81%E8%A3%85">封装</a></li>
<li><a href="#%E7%BB%A7%E6%89%BF">继承</a></li>
<li><a href="#%E5%A4%9A%E6%80%81">多态</a></li>
</ul>
</li>
<li><a href="#%E6%8E%A5%E5%8F%A3%E5%92%8C%E6%8A%BD%E8%B1%A1%E7%B1%BB%E6%9C%89%E4%BB%80%E4%B9%88%E5%85%B1%E5%90%8C%E7%82%B9%E5%92%8C%E5%8C%BA%E5%88%AB%EF%BC%9F">接口和抽象类有什么共同点和区别？</a>
<ul>
<li><a href="#%E6%8E%A5%E5%8F%A3%E5%92%8C%E6%8A%BD%E8%B1%A1%E7%B1%BB%E7%9A%84%E5%85%B1%E5%90%8C%E7%82%B9">接口和抽象类的共同点</a></li>
<li><a href="#%E6%8E%A5%E5%8F%A3%E5%92%8C%E6%8A%BD%E8%B1%A1%E7%B1%BB%E7%9A%84%E5%8C%BA%E5%88%AB">接口和抽象类的区别</a></li>
</ul>
</li>
<li><a href="#%E6%B7%B1%E6%8B%B7%E8%B4%9D%E5%92%8C%E6%B5%85%E6%8B%B7%E8%B4%9D%E5%8C%BA%E5%88%AB%E4%BA%86%E8%A7%A3%E5%90%97%EF%BC%9F%E4%BB%80%E4%B9%88%E6%98%AF%E5%BC%95%E7%94%A8%E6%8B%B7%E8%B4%9D%EF%BC%9F">深拷贝和浅拷贝区别了解吗？什么是引用拷贝？</a>
<ul>
<li><a href="#%E6%B5%85%E6%8B%B7%E8%B4%9D">浅拷贝</a></li>
<li><a href="#%E6%B7%B1%E6%8B%B7%E8%B4%9D">深拷贝</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#object">Object</a>
<ul>
<li><a href="#object%E7%B1%BB%E7%9A%84%E5%B8%B8%E8%A7%81%E6%96%B9%E6%B3%95%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F">Object 类的常见方法有哪些？</a></li>
<li><a href="#%E5%92%8Cequals%E7%9A%84%E5%8C%BA%E5%88%AB">== 和 equals() 的区别</a></li>
<li><a href="#hashcode%E6%9C%89%E4%BB%80%E4%B9%88%E7%94%A8%EF%BC%9F">hashCode() 有什么用？</a></li>
<li><a href="#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E6%9C%89hashcode%EF%BC%9F">为什么要有 hashCode？</a></li>
<li><a href="#%E4%B8%BA%E4%BB%80%E4%B9%88%E9%87%8D%E5%86%99equals%E6%97%B6%E5%BF%85%E9%A1%BB%E9%87%8D%E5%86%99-hashcode%E6%96%B9%E6%B3%95%EF%BC%9F">为什么重写 equals() 时必须重写 hashCode() 方法？</a></li>
</ul>
</li>
<li><a href="#string">String</a>
<ul>
<li><a href="#string%E3%80%81stringbuffer%E3%80%81stringbuilder%E7%9A%84%E5%8C%BA%E5%88%AB%EF%BC%9F">String、StringBuffer、StringBuilder 的区别？</a></li>
<li><a href="#string%E4%B8%BA%E4%BB%80%E4%B9%88%E6%98%AF%E4%B8%8D%E5%8F%AF%E5%8F%98%E7%9A%84">String 为什么是不可变的?</a></li>
<li><a href="#%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%8B%BC%E6%8E%A5%E7%94%A8%E2%80%9C%E2%80%9D%E8%BF%98%E6%98%AFstringbuilder">字符串拼接用“+” 还是 StringBuilder?</a></li>
<li><a href="#string-equals%E5%92%8C-object-equals%E6%9C%89%E4%BD%95%E5%8C%BA%E5%88%AB%EF%BC%9F">String#equals() 和 Object#equals() 有何区别？</a></li>
<li><a href="#%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%B8%B8%E9%87%8F%E6%B1%A0%E7%9A%84%E4%BD%9C%E7%94%A8%E4%BA%86%E8%A7%A3%E5%90%97%EF%BC%9F">字符串常量池的作用了解吗？</a></li>
<li><a href="#string-s1-new-string-abc%E8%BF%99%E5%8F%A5%E8%AF%9D%E5%88%9B%E5%BB%BA%E4%BA%86%E5%87%A0%E4%B8%AA%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%AF%B9%E8%B1%A1%EF%BC%9F">String s1 = new String(&quot;abc&quot;);这句话创建了几个字符串对象？</a></li>
<li><a href="#string-intern%E6%96%B9%E6%B3%95%E6%9C%89%E4%BB%80%E4%B9%88%E4%BD%9C%E7%94%A8">String#intern 方法有什么作用?</a></li>
<li><a href="#string%E7%B1%BB%E5%9E%8B%E7%9A%84%E5%8F%98%E9%87%8F%E5%92%8C%E5%B8%B8%E9%87%8F%E5%81%9A%E2%80%9C%E2%80%9D%E8%BF%90%E7%AE%97%E6%97%B6%E5%8F%91%E7%94%9F%E4%BA%86%E4%BB%80%E4%B9%88%EF%BC%9F">String 类型的变量和常量做“+”运算时发生了什么？</a></li>
</ul>
</li>
<li><a href="#%E5%8F%82%E8%80%83">参考</a></li>
</ul>
<h2><a id="%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E5%9F%BA%E7%A1%80" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>面向对象基础</h2>
<h3><a id="%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E5%92%8C%E9%9D%A2%E5%90%91%E8%BF%87%E7%A8%8B%E7%9A%84%E5%8C%BA%E5%88%AB" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>面向对象和面向过程的区别</h3>
<p>面向过程编程（Procedural-Oriented Programming，POP）和面向对象编程（Object-Oriented Programming，OOP）是两种常见的编程范式，两者的主要区别在于解决问题的方式不同：</p>
<ul>
<li><strong>面向过程编程（POP）</strong>：面向过程把解决问题的过程拆成一个个方法，通过一个个方法的执行解决问题。</li>
<li><strong>面向对象编程（OOP）</strong>：面向对象会先抽象出对象，然后用对象执行方法的方式解决问题。</li>
</ul>
<p>相比较于 POP，OOP 开发的程序一般具有下面这些优点：</p>
<ul>
<li><strong>易维护</strong>：由于良好的结构和封装性，OOP 程序通常更容易维护。</li>
<li><strong>易复用</strong>：通过继承和多态，OOP 设计使得代码更具复用性，方便扩展功能。</li>
<li><strong>易扩展</strong>：模块化设计使得系统扩展变得更加容易和灵活。</li>
</ul>
<p>POP 的编程方式通常更为简单和直接，适合处理一些较简单的任务。</p>
<p>POP 和 OOP 的性能差异主要取决于它们的运行机制，而不仅仅是编程范式本身。因此，简单地比较两者的性能是一个常见的误区（相关 issue : <a href="https://github.com/Snailclimb/JavaGuide/issues/431">面向过程：面向过程性能比面向对象高？？</a> ）。</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/b2616965-1e49-4b90-bb10-da2db01c56d1.png" alt=" POP 和 OOP  性能比较不合适" /></p>
<p>在选择编程范式时，性能并不是唯一的考虑因素。代码的可维护性、可扩展性和开发效率同样重要。</p>
<p>现代编程语言基本都支持多种编程范式，既可以用来进行面向过程编程，也可以进行面向对象编程。</p>
<p>下面是一个求圆的面积和周长的示例，简单分别展示了面向对象和面向过程两种不同的解决方案。</p>
<p><strong>面向对象</strong>：</p>
<pre><code class="language-java">public class Circle {
    // 定义圆的半径
    private double radius;

    // 构造函数
    public Circle(double radius) {
        this.radius = radius;
    }

    // 计算圆的面积
    public double getArea() {
        return Math.PI * radius * radius;
    }

    // 计算圆的周长
    public double getPerimeter() {
        return 2 * Math.PI * radius;
    }

    public static void main(String[] args) {
        // 创建一个半径为3的圆
        Circle circle = new Circle(3.0);

        // 输出圆的面积和周长
        System.out.println(&quot;圆的面积为：&quot; + circle.getArea());
        System.out.println(&quot;圆的周长为：&quot; + circle.getPerimeter());
    }
}
</code></pre>
<p>我们定义了一个 <code>Circle</code> 类来表示圆，该类包含了圆的半径属性和计算面积、周长的方法。</p>
<p><strong>面向过程</strong>：</p>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) {
        // 定义圆的半径
        double radius = 3.0;

        // 计算圆的面积和周长
        double area = Math.PI * radius * radius;
        double perimeter = 2 * Math.PI * radius;

        // 输出圆的面积和周长
        System.out.println(&quot;圆的面积为：&quot; + area);
        System.out.println(&quot;圆的周长为：&quot; + perimeter);
    }
}
</code></pre>
<p>我们直接定义了圆的半径，并使用该半径直接计算出圆的面积和周长。</p>
<h3><a id="%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA%E5%AF%B9%E8%B1%A1%E7%94%A8%E4%BB%80%E4%B9%88%E8%BF%90%E7%AE%97%E7%AC%A6%E5%AF%B9%E8%B1%A1%E5%AE%9E%E4%BD%93%E4%B8%8E%E5%AF%B9%E8%B1%A1%E5%BC%95%E7%94%A8%E6%9C%89%E4%BD%95%E4%B8%8D%E5%90%8C" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>创建一个对象用什么运算符?对象实体与对象引用有何不同?</h3>
<p>new 运算符，new 创建对象实例（对象实例在堆内存中），对象引用指向对象实例（对象引用存放在栈内存中）。</p>
<ul>
<li>一个对象引用可以指向 0 个或 1 个对象（一根绳子可以不系气球，也可以系一个气球）；</li>
<li>一个对象可以有 n 个引用指向它（可以用 n 条绳子系住一个气球）。</li>
</ul>
<h3><a id="%E5%AF%B9%E8%B1%A1%E7%9A%84%E7%9B%B8%E7%AD%89%E5%92%8C%E5%BC%95%E7%94%A8%E7%9B%B8%E7%AD%89%E7%9A%84%E5%8C%BA%E5%88%AB" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>对象的相等和引用相等的区别</h3>
<ul>
<li>对象的相等一般比较的是内存中存放的内容是否相等。</li>
<li>引用相等一般比较的是他们指向的内存地址是否相等。</li>
</ul>
<p>这里举一个例子：</p>
<pre><code class="language-java">String str1 = &quot;hello&quot;;
String str2 = new String(&quot;hello&quot;);
String str3 = &quot;hello&quot;;
// 使用 == 比较字符串的引用相等
System.out.println(str1 == str2);
System.out.println(str1 == str3);
// 使用 equals 方法比较字符串的相等
System.out.println(str1.equals(str2));
System.out.println(str1.equals(str3));

</code></pre>
<p>输出结果：</p>
<pre><code class="language-plain">false
true
true
true
</code></pre>
<p>从上面的代码输出结果可以看出：</p>
<ul>
<li><code>str1</code> 和 <code>str2</code> 不相等，而 <code>str1</code> 和 <code>str3</code> 相等。这是因为 <code>==</code> 运算符比较的是字符串的引用是否相等。</li>
<li><code>str1</code>、 <code>str2</code>、<code>str3</code> 三者的内容都相等。这是因为<code>equals</code> 方法比较的是字符串的内容，即使这些字符串的对象引用不同，只要它们的内容相等，就认为它们是相等的。</li>
</ul>
<h3><a id="%E5%A6%82%E6%9E%9C%E4%B8%80%E4%B8%AA%E7%B1%BB%E6%B2%A1%E6%9C%89%E5%A3%B0%E6%98%8E%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95%EF%BC%8C%E8%AF%A5%E7%A8%8B%E5%BA%8F%E8%83%BD%E6%AD%A3%E7%A1%AE%E6%89%A7%E8%A1%8C%E5%90%97" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>如果一个类没有声明构造方法，该程序能正确执行吗?</h3>
<p>构造方法是一种特殊的方法，主要作用是完成对象的初始化工作。</p>
<p>如果一个类没有声明构造方法，也可以执行！因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法（无论是否有参），Java 就不会添加默认的无参数的构造方法了。</p>
<p>我们一直在不知不觉地使用构造方法，这也是为什么我们在创建对象的时候后面要加一个括号（因为要调用无参的构造方法）。如果我们重载了有参的构造方法，记得都要把无参的构造方法也写出来（无论是否用到），因为这可以帮助我们在创建对象的时候少踩坑。</p>
<h3><a id="%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95%E6%9C%89%E5%93%AA%E4%BA%9B%E7%89%B9%E7%82%B9%EF%BC%9F%E6%98%AF%E5%90%A6%E5%8F%AF%E8%A2%ABoverride" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>构造方法有哪些特点？是否可被 override?</h3>
<p>构造方法具有以下特点：</p>
<ul>
<li><strong>名称与类名相同</strong>：构造方法的名称必须与类名完全一致。</li>
<li><strong>没有返回值</strong>：构造方法没有返回类型，且不能使用 <code>void</code> 声明。</li>
<li><strong>自动执行</strong>：在生成类的对象时，构造方法会自动执行，无需显式调用。</li>
</ul>
<p>构造方法<strong>不能被重写（override）</strong>，但<strong>可以被重载（overload）</strong>。因此，一个类中可以有多个构造方法，这些构造方法可以具有不同的参数列表，以提供不同的对象初始化方式。</p>
<h3><a id="%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%89%E5%A4%A7%E7%89%B9%E5%BE%81" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>面向对象三大特征</h3>
<h4><a id="%E5%B0%81%E8%A3%85" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>封装</h4>
<p>封装是指把一个对象的状态信息（也就是属性）隐藏在对象内部，不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。就好像我们看不到挂在墙上的空调的内部的零件信息（也就是属性），但是可以通过遥控器（方法）来控制空调。如果属性不想被外界访问，我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法，那么这个类也没有什么意义了。就好像如果没有空调遥控器，那么我们就无法操控空凋制冷，空调本身就没有意义了（当然现在还有很多其他方法 ，这里只是为了举例子）。</p>
<pre><code class="language-java">public class Student {
    private int id;//id属性私有化
    private String name;//name属性私有化

    //获取id的方法
    public int getId() {
        return id;
    }

    //设置id的方法
    public void setId(int id) {
        this.id = id;
    }

    //获取name的方法
    public String getName() {
        return name;
    }

    //设置name的方法
    public void setName(String name) {
        this.name = name;
    }
}
</code></pre>
<h4><a id="%E7%BB%A7%E6%89%BF" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>继承</h4>
<p>不同类型的对象，相互之间经常有一定数量的共同点。例如，小明同学、小红同学、小李同学，都共享学生的特性（班级、学号等）。同时，每一个对象还定义了额外的特性使得他们与众不同。例如小明的数学比较好，小红的性格惹人喜爱；小李的力气比较大。继承是使用已存在的类的定义作为基础建立新类的技术，新类的定义可以增加新的数据或新的功能，也可以用父类的功能，但不能选择性地继承父类。通过使用继承，可以快速地创建新的类，可以提高代码的重用，程序的可维护性，节省大量创建新类的时间 ，提高我们的开发效率。</p>
<p><strong>关于继承如下 3 点请记住：</strong></p>
<ol>
<li>子类拥有父类对象所有的属性和方法（包括私有属性和私有方法），但是父类中的私有属性和方法子类是无法访问，<strong>只是拥有</strong>。</li>
<li>子类可以拥有自己属性和方法，即子类可以对父类进行扩展。</li>
<li>子类可以用自己的方式实现父类的方法。（以后介绍）。</li>
</ol>
<h4><a id="%E5%A4%9A%E6%80%81" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>多态</h4>
<p>多态，顾名思义，表示一个对象具有多种的状态，具体表现为父类的引用指向子类的实例。</p>
<p><strong>多态的特点:</strong></p>
<ul>
<li>对象类型和引用类型之间具有继承（类）/实现（接口）的关系；</li>
<li>引用类型变量发出的方法调用的到底是哪个类中的方法，必须在程序运行期间才能确定；</li>
<li>多态不能调用“只在子类存在但在父类不存在”的方法；</li>
<li>如果子类重写了父类的方法，真正执行的是子类重写的方法，如果子类没有重写父类的方法，执行的是父类的方法。</li>
</ul>
<h3><a id="%E6%8E%A5%E5%8F%A3%E5%92%8C%E6%8A%BD%E8%B1%A1%E7%B1%BB%E6%9C%89%E4%BB%80%E4%B9%88%E5%85%B1%E5%90%8C%E7%82%B9%E5%92%8C%E5%8C%BA%E5%88%AB%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>接口和抽象类有什么共同点和区别？</h3>
<h4><a id="%E6%8E%A5%E5%8F%A3%E5%92%8C%E6%8A%BD%E8%B1%A1%E7%B1%BB%E7%9A%84%E5%85%B1%E5%90%8C%E7%82%B9" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>接口和抽象类的共同点</h4>
<ul>
<li><strong>实例化</strong>：接口和抽象类都不能直接实例化，只能被实现（接口）或继承（抽象类）后才能创建具体的对象。</li>
<li><strong>抽象方法</strong>：接口和抽象类都可以包含抽象方法。抽象方法没有方法体，必须在子类或实现类中实现。</li>
</ul>
<h4><a id="%E6%8E%A5%E5%8F%A3%E5%92%8C%E6%8A%BD%E8%B1%A1%E7%B1%BB%E7%9A%84%E5%8C%BA%E5%88%AB" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>接口和抽象类的区别</h4>
<ul>
<li><strong>设计目的</strong>：接口主要用于对类的行为进行约束，你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用，强调的是所属关系。</li>
<li><strong>继承和实现</strong>：一个类只能继承一个类（包括抽象类），因为 Java 不支持多继承。但一个类可以实现多个接口，一个接口也可以继承多个其他接口。</li>
<li><strong>成员变量</strong>：接口中的成员变量只能是 <code>public static final</code> 类型的，不能被修改且必须有初始值。抽象类的成员变量可以有任何修饰符（<code>private</code>, <code>protected</code>, <code>public</code>），可以在子类中被重新定义或赋值。</li>
<li><strong>方法</strong>：
<ul>
<li>Java 8 之前，接口中的方法默认是 <code>public abstract</code> ，也就是只能有方法声明。自 Java 8 起，可以在接口中定义 <code>default</code>（默认） 方法和 <code>static</code> （静态）方法。 自 Java 9 起，接口可以包含 <code>private</code> 方法。</li>
<li>抽象类可以包含抽象方法和非抽象方法。抽象方法没有方法体，必须在子类中实现。非抽象方法有具体实现，可以直接在抽象类中使用或在子类中重写。</li>
</ul>
</li>
</ul>
<p>在 Java 8 及以上版本中，接口引入了新的方法类型：<code>default</code> 方法、<code>static</code> 方法和 <code>private</code> 方法。这些方法让接口的使用更加灵活。</p>
<p>Java 8 引入的<code>default</code> 方法用于提供接口方法的默认实现，可以在实现类中被覆盖。这样就可以在不修改实现类的情况下向现有接口添加新功能，从而增强接口的扩展性和向后兼容性。</p>
<pre><code class="language-java">public interface MyInterface {
    default void defaultMethod() {
        System.out.println(&quot;This is a default method.&quot;);
    }
}
</code></pre>
<p>Java 8 引入的<code>static</code> 方法无法在实现类中被覆盖，只能通过接口名直接调用（ <code>MyInterface.staticMethod()</code>），类似于类中的静态方法。<code>static</code> 方法通常用于定义一些通用的、与接口相关的工具方法，一般很少用。</p>
<pre><code class="language-java">public interface MyInterface {
    static void staticMethod() {
        System.out.println(&quot;This is a static method in the interface.&quot;);
    }
}
</code></pre>
<p>Java 9 允许在接口中使用 <code>private</code> 方法。<code>private</code>方法可以用于在接口内部共享代码，不对外暴露。</p>
<pre><code class="language-java">public interface MyInterface {
    // default 方法
    default void defaultMethod() {
        commonMethod();
    }

    // static 方法
    static void staticMethod() {
        commonMethod();
    }

    // 私有静态方法，可以被 static 和 default 方法调用
    private static void commonMethod() {
        System.out.println(&quot;This is a private method used internally.&quot;);
    }

      // 实例私有方法，只能被 default 方法调用。
    private void instanceCommonMethod() {
        System.out.println(&quot;This is a private instance method used internally.&quot;);
    }
}
</code></pre>
<h3><a id="%E6%B7%B1%E6%8B%B7%E8%B4%9D%E5%92%8C%E6%B5%85%E6%8B%B7%E8%B4%9D%E5%8C%BA%E5%88%AB%E4%BA%86%E8%A7%A3%E5%90%97%EF%BC%9F%E4%BB%80%E4%B9%88%E6%98%AF%E5%BC%95%E7%94%A8%E6%8B%B7%E8%B4%9D%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>深拷贝和浅拷贝区别了解吗？什么是引用拷贝？</h3>
<p>关于深拷贝和浅拷贝区别，我这里先给结论：</p>
<ul>
<li><strong>浅拷贝</strong>：浅拷贝会在堆上创建一个新的对象（区别于引用拷贝的一点），不过，如果原对象内部的属性是引用类型的话，浅拷贝会直接复制内部对象的引用地址，也就是说拷贝对象和原对象共用同一个内部对象。</li>
<li><strong>深拷贝</strong>：深拷贝会完全复制整个对象，包括这个对象所包含的内部对象。</li>
</ul>
<p>上面的结论没有完全理解的话也没关系，我们来看一个具体的案例！</p>
<h4><a id="%E6%B5%85%E6%8B%B7%E8%B4%9D" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>浅拷贝</h4>
<p>浅拷贝的示例代码如下，我们这里实现了 <code>Cloneable</code> 接口，并重写了 <code>clone()</code> 方法。</p>
<p><code>clone()</code> 方法的实现很简单，直接调用的是父类 <code>Object</code> 的 <code>clone()</code> 方法。</p>
<pre><code class="language-java">public class Address implements Cloneable{
    private String name;
    // 省略构造函数、Getter&amp;Setter方法
    @Override
    public Address clone() {
        try {
            return (Address) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

public class Person implements Cloneable {
    private Address address;
    // 省略构造函数、Getter&amp;Setter方法
    @Override
    public Person clone() {
        try {
            Person person = (Person) super.clone();
            return person;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}
</code></pre>
<p>测试：</p>
<pre><code class="language-java">Person person1 = new Person(new Address(&quot;武汉&quot;));
Person person1Copy = person1.clone();
// true
System.out.println(person1.getAddress() == person1Copy.getAddress());
</code></pre>
<p>从输出结构就可以看出， <code>person1</code> 的克隆对象和 <code>person1</code> 使用的仍然是同一个 <code>Address</code> 对象。</p>
<h4><a id="%E6%B7%B1%E6%8B%B7%E8%B4%9D" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>深拷贝</h4>
<p>这里我们简单对 <code>Person</code> 类的 <code>clone()</code> 方法进行修改，连带着要把 <code>Person</code> 对象内部的 <code>Address</code> 对象一起复制。</p>
<pre><code class="language-java">@Override
public Person clone() {
    try {
        Person person = (Person) super.clone();
        person.setAddress(person.getAddress().clone());
        return person;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}
</code></pre>
<p>测试：</p>
<pre><code class="language-java">Person person1 = new Person(new Address(&quot;武汉&quot;));
Person person1Copy = person1.clone();
// false
System.out.println(person1.getAddress() == person1Copy.getAddress());
</code></pre>
<p>从输出结构就可以看出，显然 <code>person1</code> 的克隆对象和 <code>person1</code> 包含的 <code>Address</code> 对象已经是不同的了。</p>
<p><strong>那什么是引用拷贝呢？</strong> 简单来说，引用拷贝就是两个不同的引用指向同一个对象。</p>
<p>我专门画了一张图来描述浅拷贝、深拷贝、引用拷贝：</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/fc679d28-4ccd-45b7-b9ff-4d98af4f101a.png" alt="shallow&amp;deep-copy" /></p>
<h2><a id="object" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Object</h2>
<h3><a id="object%E7%B1%BB%E7%9A%84%E5%B8%B8%E8%A7%81%E6%96%B9%E6%B3%95%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Object 类的常见方法有哪些？</h3>
<p>Object 类是一个特殊的类，是所有类的父类，主要提供了以下 11 个方法：</p>
<pre><code class="language-java">/**
 * native 方法，用于返回当前运行时对象的 Class 对象，使用了 final 关键字修饰，故不允许子类重写。
 */
public final native Class&lt;?&gt; getClass()
/**
 * native 方法，用于返回对象的哈希码，主要使用在哈希表中，比如 JDK 中的HashMap。
 */
public native int hashCode()
/**
 * 用于比较 2 个对象的内存地址是否相等，String 类对该方法进行了重写以用于比较字符串的值是否相等。
 */
public boolean equals(Object obj)
/**
 * native 方法，用于创建并返回当前对象的一份拷贝。
 */
protected native Object clone() throws CloneNotSupportedException
/**
 * 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
 */
public String toString()
/**
 * native 方法，并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
 */
public final native void notify()
/**
 * native 方法，并且不能重写。跟 notify 一样，唯一的区别就是会唤醒在此对象监视器上等待的所有线程，而不是一个线程。
 */
public final native void notifyAll()
/**
 * native方法，并且不能重写。暂停线程的执行。注意：sleep 方法没有释放锁，而 wait 方法释放了锁 ，timeout 是等待时间。
 */
public final native void wait(long timeout) throws InterruptedException
/**
 * 多了 nanos 参数，这个参数表示额外时间（以纳秒为单位，范围是 0-999999）。 所以超时的时间还需要加上 nanos 纳秒。。
 */
public final void wait(long timeout, int nanos) throws InterruptedException
/**
 * 跟之前的2个wait方法一样，只不过该方法一直等待，没有超时时间这个概念
 */
public final void wait() throws InterruptedException
/**
 * 实例被垃圾回收器回收的时候触发的操作
 */
protected void finalize() throws Throwable { }
</code></pre>
<h3><a id="%E5%92%8Cequals%E7%9A%84%E5%8C%BA%E5%88%AB" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>== 和 equals() 的区别</h3>
<p><strong><code>==</code></strong> 对于基本类型和引用类型的作用效果是不同的：</p>
<ul>
<li>对于基本数据类型来说，<code>==</code> 比较的是值。</li>
<li>对于引用数据类型来说，<code>==</code> 比较的是对象的内存地址。</li>
</ul>
<blockquote>
<p>因为 Java 只有值传递，所以，对于 == 来说，不管是比较基本数据类型，还是引用数据类型的变量，其本质比较的都是值，只是引用类型变量存的值是对象的地址。</p>
</blockquote>
<p><strong><code>equals()</code></strong> 不能用于判断基本数据类型的变量，只能用来判断两个对象是否相等。<code>equals()</code>方法存在于<code>Object</code>类中，而<code>Object</code>类是所有类的直接或间接父类，因此所有的类都有<code>equals()</code>方法。</p>
<p><code>Object</code> 类 <code>equals()</code> 方法：</p>
<pre><code class="language-java">public boolean equals(Object obj) {
     return (this == obj);
}
</code></pre>
<p><code>equals()</code> 方法存在两种使用情况：</p>
<ul>
<li><strong>类没有重写 <code>equals()</code>方法</strong>：通过<code>equals()</code>比较该类的两个对象时，等价于通过“==”比较这两个对象，使用的默认是 <code>Object</code>类<code>equals()</code>方法。</li>
<li><strong>类重写了 <code>equals()</code>方法</strong>：一般我们都重写 <code>equals()</code>方法来比较两个对象中的属性是否相等；若它们的属性相等，则返回 true(即，认为这两个对象相等)。</li>
</ul>
<p>举个例子（这里只是为了举例。实际上，你按照下面这种写法的话，像 IDEA 这种比较智能的 IDE 都会提示你将 <code>==</code> 换成 <code>equals()</code> ）：</p>
<pre><code class="language-java">String a = new String(&quot;ab&quot;); // a 为一个引用
String b = new String(&quot;ab&quot;); // b为另一个引用,对象的内容一样
String aa = &quot;ab&quot;; // 放在常量池中
String bb = &quot;ab&quot;; // 从常量池中查找
System.out.println(aa == bb);// true
System.out.println(a == b);// false
System.out.println(a.equals(b));// true
System.out.println(42 == 42.0);// true
</code></pre>
<p><code>String</code> 中的 <code>equals</code> 方法是被重写过的，因为 <code>Object</code> 的 <code>equals</code> 方法是比较的对象的内存地址，而 <code>String</code> 的 <code>equals</code> 方法比较的是对象的值。</p>
<p>当创建 <code>String</code> 类型的对象时，虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象，如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 <code>String</code> 对象。</p>
<p><code>String</code>类<code>equals()</code>方法：</p>
<pre><code class="language-java">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;
}
</code></pre>
<h3><a id="hashcode%E6%9C%89%E4%BB%80%E4%B9%88%E7%94%A8%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>hashCode() 有什么用？</h3>
<p><code>hashCode()</code> 的作用是获取哈希码（<code>int</code> 整数），也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/7eaadf0b-1dd5-4e96-888f-108ab55ca0a0.png" alt="hashCode() 方法" /></p>
<p><code>hashCode()</code> 定义在 JDK 的 <code>Object</code> 类中，这就意味着 Java 中的任何类都包含有 <code>hashCode()</code> 函数。另外需要注意的是：<code>Object</code> 的 <code>hashCode()</code> 方法是本地方法，也就是用 C 语言或 C++ 实现的。</p>
<blockquote>
<p>⚠️ 注意：该方法在 <strong>Oracle OpenJDK8</strong> 中默认是 &quot;使用线程局部状态来实现 Marsaglia's xor-shift 随机数生成&quot;, 并不是 &quot;地址&quot; 或者 &quot;地址转换而来&quot;, 不同 JDK/VM 可能不同。在 <strong>Oracle OpenJDK8</strong> 中有六种生成方式 (其中第五种是返回地址), 通过添加 VM 参数: -XX:hashCode=4 启用第五种。参考源码:</p>
<ul>
<li><a href="https://hg.openjdk.org/jdk8u/jdk8u/hotspot/file/87ee5ee27509/src/share/vm/runtime/globals.hpp">https://hg.openjdk.org/jdk8u/jdk8u/hotspot/file/87ee5ee27509/src/share/vm/runtime/globals.hpp</a>（1127 行）</li>
<li><a href="https://hg.openjdk.org/jdk8u/jdk8u/hotspot/file/87ee5ee27509/src/share/vm/runtime/synchronizer.cpp">https://hg.openjdk.org/jdk8u/jdk8u/hotspot/file/87ee5ee27509/src/share/vm/runtime/synchronizer.cpp</a>（537 行开始）</li>
</ul>
</blockquote>
<pre><code class="language-java">public native int hashCode();
</code></pre>
<p>散列表存储的是键值对(key-value)，它的特点是：<strong>能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码！（可以快速找到所需要的对象）</strong></p>
<h3><a id="%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E6%9C%89hashcode%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>为什么要有 hashCode？</h3>
<p>我们以“<code>HashSet</code> 如何检查重复”为例子来说明为什么要有 <code>hashCode</code>？</p>
<p>下面这段内容摘自我的 Java 启蒙书《Head First Java》:</p>
<blockquote>
<p>当你把对象加入 <code>HashSet</code> 时，<code>HashSet</code> 会先计算对象的 <code>hashCode</code> 值来判断对象加入的位置，同时也会与其他已经加入的对象的 <code>hashCode</code> 值作比较，如果没有相符的 <code>hashCode</code>，<code>HashSet</code> 会假设对象没有重复出现。但是如果发现有相同 <code>hashCode</code> 值的对象，这时会调用 <code>equals()</code> 方法来检查 <code>hashCode</code> 相等的对象是否真的相同。如果两者相同，<code>HashSet</code> 就不会让其加入操作成功。如果不同的话，就会重新散列到其他位置。这样我们就大大减少了 <code>equals</code> 的次数，相应就大大提高了执行速度。</p>
</blockquote>
<p>其实， <code>hashCode()</code> 和 <code>equals()</code>都是用于比较两个对象是否相等。</p>
<p><strong>那为什么 JDK 还要同时提供这两个方法呢？</strong></p>
<p>这是因为在一些容器（比如 <code>HashMap</code>、<code>HashSet</code>）中，有了 <code>hashCode()</code> 之后，判断元素是否在对应容器中的效率会更高（参考添加元素进<code>HashSet</code>的过程）！</p>
<p>我们在前面也提到了添加元素进<code>HashSet</code>的过程，如果 <code>HashSet</code> 在对比的时候，同样的 <code>hashCode</code> 有多个对象，它会继续使用 <code>equals()</code> 来判断是否真的相同。也就是说 <code>hashCode</code> 帮助我们大大缩小了查找成本。</p>
<p><strong>那为什么不只提供 <code>hashCode()</code> 方法呢？</strong></p>
<p>这是因为两个对象的<code>hashCode</code> 值相等并不代表两个对象就相等。</p>
<p><strong>那为什么两个对象有相同的 <code>hashCode</code> 值，它们也不一定是相等的？</strong></p>
<p>因为 <code>hashCode()</code> 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞，但这也与数据值域分布的特性有关（所谓哈希碰撞也就是指的是不同的对象得到相同的 <code>hashCode</code> )。</p>
<p>总结下来就是：</p>
<ul>
<li>如果两个对象的<code>hashCode</code> 值相等，那这两个对象不一定相等（哈希碰撞）。</li>
<li>如果两个对象的<code>hashCode</code> 值相等并且<code>equals()</code>方法也返回 <code>true</code>，我们才认为这两个对象相等。</li>
<li>如果两个对象的<code>hashCode</code> 值不相等，我们就可以直接认为这两个对象不相等。</li>
</ul>
<p>相信大家看了我前面对 <code>hashCode()</code> 和 <code>equals()</code> 的介绍之后，下面这个问题已经难不倒你们了。</p>
<h3><a id="%E4%B8%BA%E4%BB%80%E4%B9%88%E9%87%8D%E5%86%99equals%E6%97%B6%E5%BF%85%E9%A1%BB%E9%87%8D%E5%86%99-hashcode%E6%96%B9%E6%B3%95%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>为什么重写 equals() 时必须重写 hashCode() 方法？</h3>
<p>因为两个相等的对象的 <code>hashCode</code> 值必须是相等。也就是说如果 <code>equals</code> 方法判断两个对象是相等的，那这两个对象的 <code>hashCode</code> 值也要相等。</p>
<p>如果重写 <code>equals()</code> 时没有重写 <code>hashCode()</code> 方法的话就可能会导致 <code>equals</code> 方法判断是相等的两个对象，<code>hashCode</code> 值却不相等。</p>
<p><strong>思考</strong>：重写 <code>equals()</code> 时没有重写 <code>hashCode()</code> 方法的话，使用 <code>HashMap</code> 可能会出现什么问题。</p>
<p><strong>总结</strong>：</p>
<ul>
<li><code>equals</code> 方法判断两个对象是相等的，那这两个对象的 <code>hashCode</code> 值也要相等。</li>
<li>两个对象有相同的 <code>hashCode</code> 值，他们也不一定是相等的（哈希碰撞）。</li>
</ul>
<p>更多关于 <code>hashCode()</code> 和 <code>equals()</code> 的内容可以查看：<a href="https://www.cnblogs.com/skywang12345/p/3324958.html">Java hashCode() 和 equals()的若干问题解答</a></p>
<h2><a id="string" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>String</h2>
<h3><a id="string%E3%80%81stringbuffer%E3%80%81stringbuilder%E7%9A%84%E5%8C%BA%E5%88%AB%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>String、StringBuffer、StringBuilder 的区别？</h3>
<p><strong>可变性</strong></p>
<p><code>String</code> 是不可变的（后面会详细分析原因）。</p>
<p><code>StringBuilder</code> 与 <code>StringBuffer</code> 都继承自 <code>AbstractStringBuilder</code> 类，在 <code>AbstractStringBuilder</code> 中也是使用字符数组保存字符串，不过没有使用 <code>final</code> 和 <code>private</code> 关键字修饰，最关键的是这个 <code>AbstractStringBuilder</code> 类还提供了很多修改字符串的方法比如 <code>append</code> 方法。</p>
<pre><code class="language-java">abstract class AbstractStringBuilder implements Appendable, CharSequence {
    char[] value;
    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;
    }
    //...
}
</code></pre>
<p><strong>线程安全性</strong></p>
<p><code>String</code> 中的对象是不可变的，也就可以理解为常量，线程安全。<code>AbstractStringBuilder</code> 是 <code>StringBuilder</code> 与 <code>StringBuffer</code> 的公共父类，定义了一些字符串的基本操作，如 <code>expandCapacity</code>、<code>append</code>、<code>insert</code>、<code>indexOf</code> 等公共方法。<code>StringBuffer</code> 对方法加了同步锁或者对调用的方法加了同步锁，所以是线程安全的。<code>StringBuilder</code> 并没有对方法进行加同步锁，所以是非线程安全的。</p>
<p><strong>性能</strong></p>
<p>每次对 <code>String</code> 类型进行改变的时候，都会生成一个新的 <code>String</code> 对象，然后将指针指向新的 <code>String</code> 对象。<code>StringBuffer</code> 每次都会对 <code>StringBuffer</code> 对象本身进行操作，而不是生成新的对象并改变对象引用。相同情况下使用 <code>StringBuilder</code> 相比使用 <code>StringBuffer</code> 仅能获得 10%~15% 左右的性能提升，但却要冒多线程不安全的风险。</p>
<p><strong>对于三者使用的总结：</strong></p>
<ul>
<li>操作少量的数据: 适用 <code>String</code></li>
<li>单线程操作字符串缓冲区下操作大量数据: 适用 <code>StringBuilder</code></li>
<li>多线程操作字符串缓冲区下操作大量数据: 适用 <code>StringBuffer</code></li>
</ul>
<h3><a id="string%E4%B8%BA%E4%BB%80%E4%B9%88%E6%98%AF%E4%B8%8D%E5%8F%AF%E5%8F%98%E7%9A%84" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>String 为什么是不可变的?</h3>
<p><code>String</code> 类中使用 <code>final</code> 关键字修饰字符数组来保存字符串，<del>所以<code>String</code> 对象是不可变的。</del></p>
<pre><code class="language-java">public final class String implements java.io.Serializable, Comparable&lt;String&gt;, CharSequence {
    private final char value[];
  //...
}
</code></pre>
<blockquote>
<p>🐛 修正：我们知道被 <code>final</code> 关键字修饰的类不能被继承，修饰的方法不能被重写，修饰的变量是基本数据类型则值不能改变，修饰的变量是引用类型则不能再指向其他对象。因此，<code>final</code> 关键字修饰的数组保存字符串并不是 <code>String</code> 不可变的根本原因，因为这个数组保存的字符串是可变的（<code>final</code> 修饰引用类型变量的情况）。</p>
<p><code>String</code> 真正不可变有下面几点原因：</p>
<ol>
<li>保存字符串的数组被 <code>final</code> 修饰且为私有的，并且<code>String</code> 类没有提供/暴露修改这个字符串的方法。</li>
<li><code>String</code> 类被 <code>final</code> 修饰导致其不能被继承，进而避免了子类破坏 <code>String</code> 不可变。</li>
</ol>
<p>相关阅读：<a href="https://www.zhihu.com/question/20618891/answer/114125846">如何理解 String 类型值的不可变？ - 知乎提问</a></p>
<p>补充（来自<a href="https://github.com/Snailclimb/JavaGuide/issues/675">issue 675</a>）：在 Java 9 之后，<code>String</code>、<code>StringBuilder</code> 与 <code>StringBuffer</code> 的实现改用 <code>byte</code> 数组存储字符串。</p>
<pre><code class="language-java">public final class String implements java.io.Serializable,Comparable&lt;String&gt;, CharSequence {
    // @Stable 注解表示变量最多被修改一次，称为“稳定的”。
    @Stable
    private final byte[] value;
}

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    byte[] value;

}
</code></pre>
<p><strong>Java 9 为何要将 <code>String</code> 的底层实现由 <code>char[]</code> 改成了 <code>byte[]</code> ?</strong></p>
<p>新版的 String 其实支持两个编码方案：Latin-1 和 UTF-16。如果字符串中包含的汉字没有超过 Latin-1 可表示范围内的字符，那就会使用 Latin-1 作为编码方案。Latin-1 编码方案下，<code>byte</code> 占一个字节(8 位)，<code>char</code> 占用 2 个字节（16），<code>byte</code> 相较 <code>char</code> 节省一半的内存空间。</p>
<p>JDK 官方就说了绝大部分字符串对象只包含 Latin-1 可表示的字符。</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/7e6a70e2-0c11-427f-8f5e-0b44fa169e40.png" alt="" /></p>
<p>如果字符串中包含的汉字超过 Latin-1 可表示范围内的字符，<code>byte</code> 和 <code>char</code> 所占用的空间是一样的。</p>
<p>这是官方的介绍：<a href="https://openjdk.java.net/jeps/254">https://openjdk.java.net/jeps/254</a> 。</p>
</blockquote>
<h3><a id="%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%8B%BC%E6%8E%A5%E7%94%A8%E2%80%9C%E2%80%9D%E8%BF%98%E6%98%AFstringbuilder" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>字符串拼接用“+” 还是 StringBuilder?</h3>
<p>Java 语言本身并不支持运算符重载，“+”和“+=”是专门为 String 类重载过的运算符，也是 Java 中仅有的两个重载过的运算符。</p>
<pre><code class="language-java">String str1 = &quot;he&quot;;
String str2 = &quot;llo&quot;;
String str3 = &quot;world&quot;;
String str4 = str1 + str2 + str3;
</code></pre>
<p>上面的代码对应的字节码如下：</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/c2f38841-fd86-45f6-99a8-1023756a7e4e.png" alt="" /></p>
<p>可以看出，字符串对象通过“+”的字符串拼接方式，实际上是通过 <code>StringBuilder</code> 调用 <code>append()</code> 方法实现的，拼接完成之后调用 <code>toString()</code> 得到一个 <code>String</code> 对象 。</p>
<p>不过，在循环内使用“+”进行字符串的拼接的话，存在比较明显的缺陷：<strong>编译器不会创建单个 <code>StringBuilder</code> 以复用，会导致创建过多的 <code>StringBuilder</code> 对象</strong>。</p>
<pre><code class="language-java">String[] arr = {&quot;he&quot;, &quot;llo&quot;, &quot;world&quot;};
String s = &quot;&quot;;
for (int i = 0; i &lt; arr.length; i++) {
    s += arr[i];
}
System.out.println(s);
</code></pre>
<p><code>StringBuilder</code> 对象是在循环内部被创建的，这意味着每循环一次就会创建一个 <code>StringBuilder</code> 对象。</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/1ca83a9a-c642-4b1d-8784-b77ab9e40457.png" alt="" /></p>
<p>如果直接使用 <code>StringBuilder</code> 对象进行字符串拼接的话，就不会存在这个问题了。</p>
<pre><code class="language-java">String[] arr = {&quot;he&quot;, &quot;llo&quot;, &quot;world&quot;};
StringBuilder s = new StringBuilder();
for (String value : arr) {
    s.append(value);
}
System.out.println(s);
</code></pre>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/bd7b2837-edd6-4166-9130-7220f9c6c25f.png" alt="" /></p>
<p>如果你使用 IDEA 的话，IDEA 自带的代码检查机制也会提示你修改代码。</p>
<p>在 JDK 9 中，字符串相加“+”改为用动态方法 <code>makeConcatWithConstants()</code> 来实现，通过提前分配空间从而减少了部分临时对象的创建。然而这种优化主要针对简单的字符串拼接，如： <code>a+b+c</code> 。对于循环中的大量拼接操作，仍然会逐个动态分配内存（类似于两个两个 append 的概念），并不如手动使用 StringBuilder 来进行拼接效率高。这个改进是 JDK9 的 <a href="https://openjdk.org/jeps/280">JEP 280</a> 提出的，关于这部分改进的详细介绍，推荐阅读这篇文章：还在无脑用 <a href="https://juejin.cn/post/7182872058743750715">StringBuilder？来重温一下字符串拼接吧</a> 以及参考 <a href="https://github.com/Snailclimb/JavaGuide/issues/2442">issue#2442</a>。</p>
<h3><a id="string-equals%E5%92%8C-object-equals%E6%9C%89%E4%BD%95%E5%8C%BA%E5%88%AB%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>String#equals() 和 Object#equals() 有何区别？</h3>
<p><code>String</code> 中的 <code>equals</code> 方法是被重写过的，比较的是 String 字符串的值是否相等。 <code>Object</code> 的 <code>equals</code> 方法是比较的对象的内存地址。</p>
<h3><a id="%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%B8%B8%E9%87%8F%E6%B1%A0%E7%9A%84%E4%BD%9C%E7%94%A8%E4%BA%86%E8%A7%A3%E5%90%97%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>字符串常量池的作用了解吗？</h3>
<p><strong>字符串常量池</strong> 是 JVM 为了提升性能和减少内存消耗针对字符串（String 类）专门开辟的一块区域，主要目的是为了避免字符串的重复创建。</p>
<pre><code class="language-java">// 在字符串常量池中创建字符串对象 ”ab“
// 将字符串对象 ”ab“ 的引用赋值给 aa
String aa = &quot;ab&quot;;
// 直接返回字符串常量池中字符串对象 ”ab“，赋值给引用 bb
String bb = &quot;ab&quot;;
System.out.println(aa==bb); // true
</code></pre>
<p>更多关于字符串常量池的介绍可以看一下 <a href="17420790021839.html">Java 内存区域详解</a> 这篇文章。</p>
<h3><a id="string-s1-new-string-abc%E8%BF%99%E5%8F%A5%E8%AF%9D%E5%88%9B%E5%BB%BA%E4%BA%86%E5%87%A0%E4%B8%AA%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%AF%B9%E8%B1%A1%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>String s1 = new String(&quot;abc&quot;);这句话创建了几个字符串对象？</h3>
<p>先说答案：会创建 1 或 2 个字符串对象。</p>
<ol>
<li>字符串常量池中不存在 &quot;abc&quot;：会创建 2 个 字符串对象。一个在字符串常量池中，由 <code>ldc</code> 指令触发创建。一个在堆中，由 <code>new String()</code> 创建，并使用常量池中的 &quot;abc&quot; 进行初始化。</li>
<li>字符串常量池中已存在 &quot;abc&quot;：会创建 1 个 字符串对象。该对象在堆中，由 <code>new String()</code> 创建，并使用常量池中的 &quot;abc&quot; 进行初始化。</li>
</ol>
<p>下面开始详细分析。</p>
<p>1、如果字符串常量池中不存在字符串对象 “abc”，那么它首先会在字符串常量池中创建字符串对象 &quot;abc&quot;，然后在堆内存中再创建其中一个字符串对象 &quot;abc&quot;。</p>
<p>示例代码（JDK 1.8）：</p>
<pre><code class="language-java">String s1 = new String(&quot;abc&quot;);
</code></pre>
<p>对应的字节码：</p>
<pre><code class="language-java">// 在堆内存中分配一个尚未初始化的 String 对象。
// #2 是常量池中的一个符号引用，指向 java/lang/String 类。
// 在类加载的解析阶段，这个符号引用会被解析成直接引用，即指向实际的 java/lang/String 类。
0 new #2 &lt;java/lang/String&gt;
// 复制栈顶的 String 对象引用，为后续的构造函数调用做准备。
// 此时操作数栈中有两个相同的对象引用：一个用于传递给构造函数，另一个用于保持对新对象的引用，后续将其存储到局部变量表。
3 dup
// JVM 先检查字符串常量池中是否存在 &quot;abc&quot;。
// 如果常量池中已存在 &quot;abc&quot;，则直接返回该字符串的引用；
// 如果常量池中不存在 &quot;abc&quot;，则 JVM 会在常量池中创建该字符串字面量并返回它的引用。
// 这个引用被压入操作数栈，用作构造函数的参数。
4 ldc #3 &lt;abc&gt;
// 调用构造方法，使用从常量池中加载的 &quot;abc&quot; 初始化堆中的 String 对象
// 新的 String 对象将包含与常量池中的 &quot;abc&quot; 相同的内容，但它是一个独立的对象，存储于堆中。
6 invokespecial #4 &lt;java/lang/String.&lt;init&gt; : (Ljava/lang/String;)V&gt;
// 将堆中的 String 对象引用存储到局部变量表
9 astore_1
// 返回，结束方法
10 return
</code></pre>
<p><code>ldc (load constant)</code> 指令的确是从常量池中加载各种类型的常量，包括字符串常量、整数常量、浮点数常量，甚至类引用等。对于字符串常量，<code>ldc</code> 指令的行为如下：</p>
<ol>
<li><strong>从常量池加载字符串</strong>：<code>ldc</code> 首先检查字符串常量池中是否已经有内容相同的字符串对象。</li>
<li><strong>复用已有字符串对象</strong>：如果字符串常量池中已经存在内容相同的字符串对象，<code>ldc</code> 会将该对象的引用加载到操作数栈上。</li>
<li><strong>没有则创建新对象并加入常量池</strong>：如果字符串常量池中没有相同内容的字符串对象，JVM 会在常量池中创建一个新的字符串对象，并将其引用加载到操作数栈中。</li>
</ol>
<p>2、如果字符串常量池中已存在字符串对象“abc”，则只会在堆中创建 1 个字符串对象“abc”。</p>
<p>示例代码（JDK 1.8）：</p>
<pre><code class="language-java">// 字符串常量池中已存在字符串对象“abc”
String s1 = &quot;abc&quot;;
// 下面这段代码只会在堆中创建 1 个字符串对象“abc”
String s2 = new String(&quot;abc&quot;);
</code></pre>
<p>对应的字节码：</p>
<pre><code class="language-java">0 ldc #2 &lt;abc&gt;
2 astore_1
3 new #3 &lt;java/lang/String&gt;
6 dup
7 ldc #2 &lt;abc&gt;
9 invokespecial #4 &lt;java/lang/String.&lt;init&gt; : (Ljava/lang/String;)V&gt;
12 astore_2
13 return
</code></pre>
<p>这里就不对上面的字节码进行详细注释了，7 这个位置的 <code>ldc</code> 命令不会在堆中创建新的字符串对象“abc”，这是因为 0 这个位置已经执行了一次 <code>ldc</code> 命令，已经在堆中创建过一次字符串对象“abc”了。7 这个位置执行 <code>ldc</code> 命令会直接返回字符串常量池中字符串对象“abc”对应的引用。</p>
<h3><a id="string-intern%E6%96%B9%E6%B3%95%E6%9C%89%E4%BB%80%E4%B9%88%E4%BD%9C%E7%94%A8" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>String#intern 方法有什么作用?</h3>
<p><code>String.intern()</code> 是一个 <code>native</code> (本地) 方法，用来处理字符串常量池中的字符串对象引用。它的工作流程可以概括为以下两种情况：</p>
<ol>
<li><strong>常量池中已有相同内容的字符串对象</strong>：如果字符串常量池中已经有一个与调用 <code>intern()</code> 方法的字符串内容相同的 <code>String</code> 对象，<code>intern()</code> 方法会直接返回常量池中该对象的引用。</li>
<li><strong>常量池中没有相同内容的字符串对象</strong>：如果字符串常量池中还没有一个与调用 <code>intern()</code> 方法的字符串内容相同的对象，<code>intern()</code> 方法会将当前字符串对象的引用添加到字符串常量池中，并返回该引用。</li>
</ol>
<p>总结：</p>
<ul>
<li><code>intern()</code> 方法的主要作用是确保字符串引用在常量池中的唯一性。</li>
<li>当调用 <code>intern()</code> 时，如果常量池中已经存在相同内容的字符串，则返回常量池中已有对象的引用；否则，将该字符串添加到常量池并返回其引用。</li>
</ul>
<p>示例代码（JDK 1.8） :</p>
<pre><code class="language-java">// s1 指向字符串常量池中的 &quot;Java&quot; 对象
String s1 = &quot;Java&quot;;
// s2 也指向字符串常量池中的 &quot;Java&quot; 对象，和 s1 是同一个对象
String s2 = s1.intern();
// 在堆中创建一个新的 &quot;Java&quot; 对象，s3 指向它
String s3 = new String(&quot;Java&quot;);
// s4 指向字符串常量池中的 &quot;Java&quot; 对象，和 s1 是同一个对象
String s4 = s3.intern();
// s1 和 s2 指向的是同一个常量池中的对象
System.out.println(s1 == s2); // true
// s3 指向堆中的对象，s4 指向常量池中的对象，所以不同
System.out.println(s3 == s4); // false
// s1 和 s4 都指向常量池中的同一个对象
System.out.println(s1 == s4); // true
</code></pre>
<h3><a id="string%E7%B1%BB%E5%9E%8B%E7%9A%84%E5%8F%98%E9%87%8F%E5%92%8C%E5%B8%B8%E9%87%8F%E5%81%9A%E2%80%9C%E2%80%9D%E8%BF%90%E7%AE%97%E6%97%B6%E5%8F%91%E7%94%9F%E4%BA%86%E4%BB%80%E4%B9%88%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>String 类型的变量和常量做“+”运算时发生了什么？</h3>
<p>先来看字符串不加 <code>final</code> 关键字拼接的情况（JDK1.8）：</p>
<pre><code class="language-java">String str1 = &quot;str&quot;;
String str2 = &quot;ing&quot;;
String str3 = &quot;str&quot; + &quot;ing&quot;;
String str4 = str1 + str2;
String str5 = &quot;string&quot;;
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
</code></pre>
<blockquote>
<p><strong>注意</strong>：比较 String 字符串的值是否相等，可以使用 <code>equals()</code> 方法。 <code>String</code> 中的 <code>equals</code> 方法是被重写过的。 <code>Object</code> 的 <code>equals</code> 方法是比较的对象的内存地址，而 <code>String</code> 的 <code>equals</code> 方法比较的是字符串的值是否相等。如果你使用 <code>==</code> 比较两个字符串是否相等的话，IDEA 还是提示你使用 <code>equals()</code> 方法替换。</p>
</blockquote>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/c8373c07-e281-4467-adf6-d17ceebaea83.png" alt="" /></p>
<p><strong>对于编译期可以确定值的字符串，也就是常量字符串 ，jvm 会将其存入字符串常量池。并且，字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池，这个得益于编译器的优化。</strong></p>
<p>在编译过程中，Javac 编译器（下文中统称为编译器）会进行一个叫做 <strong>常量折叠(Constant Folding)</strong> 的代码优化。《深入理解 Java 虚拟机》中是也有介绍到：</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/2ce00030-213d-475e-9a03-a75bedb965f0.png" alt="" /></p>
<p>常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中，这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。</p>
<p>对于 <code>String str3 = &quot;str&quot; + &quot;ing&quot;;</code> 编译器会给你优化成 <code>String str3 = &quot;string&quot;;</code> 。</p>
<p>并不是所有的常量都会进行折叠，只有编译器在程序编译期就可以确定值的常量才可以：</p>
<ul>
<li>基本数据类型( <code>byte</code>、<code>boolean</code>、<code>short</code>、<code>char</code>、<code>int</code>、<code>float</code>、<code>long</code>、<code>double</code>)以及字符串常量。</li>
<li><code>final</code> 修饰的基本数据类型和字符串变量</li>
<li>字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算（加减乘除）、基本数据类型的位运算（&lt;&lt;、&gt;&gt;、&gt;&gt;&gt; ）</li>
</ul>
<p><strong>引用的值在程序编译期是无法确定的，编译器无法对其进行优化。</strong></p>
<p>对象引用和“+”的字符串拼接方式，实际上是通过 <code>StringBuilder</code> 调用 <code>append()</code> 方法实现的，拼接完成之后调用 <code>toString()</code> 得到一个 <code>String</code> 对象 。</p>
<pre><code class="language-java">String str4 = new StringBuilder().append(str1).append(str2).toString();
</code></pre>
<p>我们在平时写代码的时候，尽量避免多个字符串对象拼接，因为这样会重新创建对象。如果需要改变字符串的话，可以使用 <code>StringBuilder</code> 或者 <code>StringBuffer</code>。</p>
<p>不过，字符串使用 <code>final</code> 关键字声明之后，可以让编译器当做常量来处理。</p>
<p>示例代码：</p>
<pre><code class="language-java">final String str1 = &quot;str&quot;;
final String str2 = &quot;ing&quot;;
// 下面两个表达式其实是等价的
String c = &quot;str&quot; + &quot;ing&quot;;// 常量池中的对象
String d = str1 + str2; // 常量池中的对象
System.out.println(c == d);// true
</code></pre>
<p>被 <code>final</code> 关键字修饰之后的 <code>String</code> 会被编译器当做常量来处理，编译器在程序编译期就可以确定它的值，其效果就相当于访问常量。</p>
<p>如果 ，编译器在运行时才能知道其确切值的话，就无法对其优化。</p>
<p>示例代码（<code>str2</code> 在运行时才能确定其值）：</p>
<pre><code class="language-java">final String str1 = &quot;str&quot;;
final String str2 = getStr();
String c = &quot;str&quot; + &quot;ing&quot;;// 常量池中的对象
String d = str1 + str2; // 在堆上创建的新的对象
System.out.println(c == d);// false
public static String getStr() {
      return &quot;ing&quot;;
}
</code></pre>
<h2><a id="%E5%8F%82%E8%80%83" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>参考</h2>
<ul>
<li>深入解析 String#intern：<a href="https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html">https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html</a></li>
<li>Java String 源码解读：<a href="http://keaper.cn/2020/09/08/java-string-mian-mian-guan/">http://keaper.cn/2020/09/08/java-string-mian-mian-guan/</a></li>
<li>R 大（RednaxelaFX）关于常量折叠的回答：<a href="https://www.zhihu.com/question/55976094/answer/147302764">https://www.zhihu.com/question/55976094/answer/147302764</a></li>
</ul>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[02-Java并发常见面试题总结（中）]]></title>
    <link href="https://huanglei.work/17419985102015.html"/>
    <updated>2025-03-15T08:28:30+08:00</updated>
    <id>https://huanglei.work/17419985102015.html</id>
    <content type="html"><![CDATA[
<blockquote>
<p>本文内容来自：<a href="https://javaguide.cn/java/concurrent/java-concurrent-questions-02.html">JavaGuide-Java并发常见面试题总结（中）</a></p>
</blockquote>
<ul>
<li><a href="#%E2%AD%90%EF%B8%8Fjmm-java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B">⭐️JMM(Java 内存模型)</a></li>
<li><a href="#%E2%AD%90%EF%B8%8Fvolatile%E5%85%B3%E9%94%AE%E5%AD%97">⭐️volatile 关键字</a>
<ul>
<li><a href="#%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81%E5%8F%98%E9%87%8F%E7%9A%84%E5%8F%AF%E8%A7%81%E6%80%A7%EF%BC%9F">如何保证变量的可见性？</a></li>
<li><a href="#%E5%A6%82%E4%BD%95%E7%A6%81%E6%AD%A2%E6%8C%87%E4%BB%A4%E9%87%8D%E6%8E%92%E5%BA%8F%EF%BC%9F">如何禁止指令重排序？</a></li>
<li><a href="#volatile%E5%8F%AF%E4%BB%A5%E4%BF%9D%E8%AF%81%E5%8E%9F%E5%AD%90%E6%80%A7%E4%B9%88%EF%BC%9F">volatile 可以保证原子性么？</a></li>
</ul>
</li>
<li><a href="#%E2%AD%90%EF%B8%8F%E4%B9%90%E8%A7%82%E9%94%81%E5%92%8C%E6%82%B2%E8%A7%82%E9%94%81">⭐️乐观锁和悲观锁</a>
<ul>
<li><a href="#%E4%BB%80%E4%B9%88%E6%98%AF%E6%82%B2%E8%A7%82%E9%94%81%EF%BC%9F">什么是悲观锁？</a></li>
<li><a href="#%E4%BB%80%E4%B9%88%E6%98%AF%E4%B9%90%E8%A7%82%E9%94%81%EF%BC%9F">什么是乐观锁？</a></li>
<li><a href="#%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E4%B9%90%E8%A7%82%E9%94%81%EF%BC%9F">如何实现乐观锁？</a>
<ul>
<li><a href="#%E7%89%88%E6%9C%AC%E5%8F%B7%E6%9C%BA%E5%88%B6">版本号机制</a></li>
<li><a href="#cas%E7%AE%97%E6%B3%95">CAS 算法</a></li>
</ul>
</li>
<li><a href="#java%E4%B8%AD-cas%E6%98%AF%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E7%9A%84%EF%BC%9F">Java 中 CAS 是如何实现的？</a></li>
<li><a href="#cas%E7%AE%97%E6%B3%95%E5%AD%98%E5%9C%A8%E5%93%AA%E4%BA%9B%E9%97%AE%E9%A2%98%EF%BC%9F">CAS 算法存在哪些问题？</a>
<ul>
<li><a href="#aba%E9%97%AE%E9%A2%98">ABA 问题</a></li>
<li><a href="#%E5%BE%AA%E7%8E%AF%E6%97%B6%E9%97%B4%E9%95%BF%E5%BC%80%E9%94%80%E5%A4%A7">循环时间长开销大</a></li>
<li><a href="#%E5%8F%AA%E8%83%BD%E4%BF%9D%E8%AF%81%E4%B8%80%E4%B8%AA%E5%85%B1%E4%BA%AB%E5%8F%98%E9%87%8F%E7%9A%84%E5%8E%9F%E5%AD%90%E6%93%8D%E4%BD%9C">只能保证一个共享变量的原子操作</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#synchronized%E5%85%B3%E9%94%AE%E5%AD%97">synchronized 关键字</a>
<ul>
<li><a href="#synchronized%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F%E6%9C%89%E4%BB%80%E4%B9%88%E7%94%A8%EF%BC%9F">synchronized 是什么？有什么用？</a></li>
<li><a href="#%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8synchronized%EF%BC%9F">如何使用 synchronized？</a></li>
<li><a href="#%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95%E5%8F%AF%E4%BB%A5%E7%94%A8synchronized%E4%BF%AE%E9%A5%B0%E4%B9%88%EF%BC%9F">构造方法可以用 synchronized 修饰么？</a></li>
<li><a href="#%E2%AD%90%EF%B8%8Fsynchronized%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E4%BA%86%E8%A7%A3%E5%90%97%EF%BC%9F">⭐️synchronized 底层原理了解吗？</a>
<ul>
<li><a href="#synchronized%E5%90%8C%E6%AD%A5%E8%AF%AD%E5%8F%A5%E5%9D%97%E7%9A%84%E6%83%85%E5%86%B5">synchronized 同步语句块的情况</a></li>
<li><a href="#synchronized%E4%BF%AE%E9%A5%B0%E6%96%B9%E6%B3%95%E7%9A%84%E6%83%85%E5%86%B5">synchronized 修饰方法的情况</a></li>
<li><a href="#%E6%80%BB%E7%BB%93">总结</a></li>
</ul>
</li>
<li><a href="#jdk1-6%E4%B9%8B%E5%90%8E%E7%9A%84-synchronized%E5%BA%95%E5%B1%82%E5%81%9A%E4%BA%86%E5%93%AA%E4%BA%9B%E4%BC%98%E5%8C%96%EF%BC%9F%E9%94%81%E5%8D%87%E7%BA%A7%E5%8E%9F%E7%90%86%E4%BA%86%E8%A7%A3%E5%90%97%EF%BC%9F">JDK1.6 之后的 synchronized 底层做了哪些优化？锁升级原理了解吗？</a></li>
<li><a href="#synchronized%E7%9A%84%E5%81%8F%E5%90%91%E9%94%81%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A2%AB%E5%BA%9F%E5%BC%83%E4%BA%86%EF%BC%9F">synchronized 的偏向锁为什么被废弃了？</a></li>
<li><a href="#%E2%AD%90%EF%B8%8Fsynchronized%E5%92%8C-volatile%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F">⭐️synchronized 和 volatile 有什么区别？</a></li>
</ul>
</li>
<li><a href="#reentrantlock">ReentrantLock</a>
<ul>
<li><a href="#reentrantlock%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F">ReentrantLock 是什么？</a></li>
<li><a href="#%E5%85%AC%E5%B9%B3%E9%94%81%E5%92%8C%E9%9D%9E%E5%85%AC%E5%B9%B3%E9%94%81%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F">公平锁和非公平锁有什么区别？</a></li>
<li><a href="#%E2%AD%90%EF%B8%8Fsynchronized%E5%92%8C-reentrantlock%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F">⭐️synchronized 和 ReentrantLock 有什么区别？</a>
<ul>
<li><a href="#%E4%B8%A4%E8%80%85%E9%83%BD%E6%98%AF%E5%8F%AF%E9%87%8D%E5%85%A5%E9%94%81">两者都是可重入锁</a></li>
<li><a href="#synchronized%E4%BE%9D%E8%B5%96%E4%BA%8E-jvm%E8%80%8C-reentrantlock%E4%BE%9D%E8%B5%96%E4%BA%8E-api">synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API</a></li>
<li><a href="#reentrantlock%E6%AF%94-synchronized%E5%A2%9E%E5%8A%A0%E4%BA%86%E4%B8%80%E4%BA%9B%E9%AB%98%E7%BA%A7%E5%8A%9F%E8%83%BD">ReentrantLock 比 synchronized 增加了一些高级功能</a></li>
</ul>
</li>
<li><a href="#%E5%8F%AF%E4%B8%AD%E6%96%AD%E9%94%81%E5%92%8C%E4%B8%8D%E5%8F%AF%E4%B8%AD%E6%96%AD%E9%94%81%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F">可中断锁和不可中断锁有什么区别？</a></li>
</ul>
</li>
<li><a href="#reentrantreadwritelock">ReentrantReadWriteLock</a>
<ul>
<li><a href="#reentrantreadwritelock%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F">ReentrantReadWriteLock 是什么？</a></li>
<li><a href="#reentrantreadwritelock%E9%80%82%E5%90%88%E4%BB%80%E4%B9%88%E5%9C%BA%E6%99%AF%EF%BC%9F">ReentrantReadWriteLock 适合什么场景？</a></li>
<li><a href="#%E5%85%B1%E4%BA%AB%E9%94%81%E5%92%8C%E7%8B%AC%E5%8D%A0%E9%94%81%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F">共享锁和独占锁有什么区别？</a></li>
<li><a href="#%E7%BA%BF%E7%A8%8B%E6%8C%81%E6%9C%89%E8%AF%BB%E9%94%81%E8%BF%98%E8%83%BD%E8%8E%B7%E5%8F%96%E5%86%99%E9%94%81%E5%90%97%EF%BC%9F">线程持有读锁还能获取写锁吗？</a></li>
<li><a href="#%E8%AF%BB%E9%94%81%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E8%83%BD%E5%8D%87%E7%BA%A7%E4%B8%BA%E5%86%99%E9%94%81%EF%BC%9F">读锁为什么不能升级为写锁？</a></li>
</ul>
</li>
<li><a href="#stampedlock">StampedLock</a>
<ul>
<li><a href="#stampedlock%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F">StampedLock 是什么？</a></li>
<li><a href="#stampedlock%E7%9A%84%E6%80%A7%E8%83%BD%E4%B8%BA%E4%BB%80%E4%B9%88%E6%9B%B4%E5%A5%BD%EF%BC%9F">StampedLock 的性能为什么更好？</a></li>
<li><a href="#stampedlock%E9%80%82%E5%90%88%E4%BB%80%E4%B9%88%E5%9C%BA%E6%99%AF%EF%BC%9F">StampedLock 适合什么场景？</a></li>
<li><a href="#stampedlock%E7%9A%84%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E4%BA%86%E8%A7%A3%E5%90%97%EF%BC%9F">StampedLock 的底层原理了解吗？</a></li>
</ul>
</li>
<li><a href="#atomic%E5%8E%9F%E5%AD%90%E7%B1%BB">Atomic 原子类</a></li>
<li><a href="#%E5%8F%82%E8%80%83">参考</a></li>
</ul>
<h2><a id="%E2%AD%90%EF%B8%8Fjmm-java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️JMM(Java 内存模型)</h2>
<p>JMM（Java 内存模型）相关的问题比较多，也比较重要，于是我单独抽了一篇文章来总结 JMM 相关的知识点和问题：<a href="17419999975764.html">JMM（Java 内存模型）详解</a> 。</p>
<h2><a id="%E2%AD%90%EF%B8%8Fvolatile%E5%85%B3%E9%94%AE%E5%AD%97" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️volatile 关键字</h2>
<h3><a id="%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81%E5%8F%98%E9%87%8F%E7%9A%84%E5%8F%AF%E8%A7%81%E6%80%A7%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>如何保证变量的可见性？</h3>
<p>在 Java 中，<code>volatile</code> 关键字可以保证变量的可见性，如果我们将变量声明为 <strong><code>volatile</code></strong> ，这就指示 JVM，这个变量是共享且不稳定的，每次使用它都到主存中进行读取。</p>
<p><img src="media/17419985102015/17419996647312.png" alt="JMM(Java 内存模型)" /></p>
<p><img src="media/17419985102015/17419996647335.png" alt="JMM(Java 内存模型)强制在主存中进行读取" /></p>
<p><code>volatile</code> 关键字其实并非是 Java 语言特有的，在 C 语言里也有，它最原始的意义就是禁用 CPU 缓存。如果我们将一个变量使用 <code>volatile</code> 修饰，这就指示 编译器，这个变量是共享且不稳定的，每次使用它都到主存中进行读取。</p>
<p><code>volatile</code> 关键字能保证数据的可见性，但不能保证数据的原子性。<code>synchronized</code> 关键字两者都能保证。</p>
<h3><a id="%E5%A6%82%E4%BD%95%E7%A6%81%E6%AD%A2%E6%8C%87%E4%BB%A4%E9%87%8D%E6%8E%92%E5%BA%8F%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>如何禁止指令重排序？</h3>
<p><strong>在 Java 中，<code>volatile</code> 关键字除了可以保证变量的可见性，还有一个重要的作用就是防止 JVM 的指令重排序。</strong> 如果我们将变量声明为 <strong><code>volatile</code></strong> ，在对这个变量进行读写操作的时候，会通过插入特定的 <strong>内存屏障</strong> 的方式来禁止指令重排序。</p>
<p>在 Java 中，<code>Unsafe</code> 类提供了三个开箱即用的内存屏障相关的方法，屏蔽了操作系统底层的差异：</p>
<pre><code class="language-java">public native void loadFence();
public native void storeFence();
public native void fullFence();
</code></pre>
<p>理论上来说，你通过这个三个方法也可以实现和<code>volatile</code>禁止重排序一样的效果，只是会麻烦一些。</p>
<p>下面我以一个常见的面试题为例讲解一下 <code>volatile</code> 关键字禁止指令重排序的效果。</p>
<p>面试中面试官经常会说：“单例模式了解吗？来给我手写一下！给我解释一下双重检验锁方式实现单例模式的原理呗！”</p>
<p><strong>双重校验锁实现对象单例（线程安全）</strong>：</p>
<pre><code class="language-java">public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public  static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过，没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}
</code></pre>
<p><code>uniqueInstance</code> 采用 <code>volatile</code> 关键字修饰也是很有必要的， <code>uniqueInstance = new Singleton();</code> 这段代码其实是分为三步执行：</p>
<ol>
<li>为 <code>uniqueInstance</code> 分配内存空间</li>
<li>初始化 <code>uniqueInstance</code></li>
<li>将 <code>uniqueInstance</code> 指向分配的内存地址</li>
</ol>
<p>但是由于 JVM 具有指令重排的特性，执行顺序有可能变成 1-&gt;3-&gt;2。指令重排在单线程环境下不会出现问题，但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如，线程 T1 执行了 1 和 3，此时 T2 调用 <code>getUniqueInstance</code>() 后发现 <code>uniqueInstance</code> 不为空，因此返回 <code>uniqueInstance</code>，但此时 <code>uniqueInstance</code> 还未被初始化。</p>
<h3><a id="volatile%E5%8F%AF%E4%BB%A5%E4%BF%9D%E8%AF%81%E5%8E%9F%E5%AD%90%E6%80%A7%E4%B9%88%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>volatile 可以保证原子性么？</h3>
<p><strong><code>volatile</code> 关键字能保证变量的可见性，但不能保证对变量的操作是原子性的。</strong></p>
<p>我们通过下面的代码即可证明：</p>
<pre><code class="language-java">/**
 * 微信搜 JavaGuide 回复&quot;面试突击&quot;即可免费领取个人原创的 Java 面试手册
 *
 * @author Guide哥
 * @date 2022/08/03 13:40
 **/
public class VolatileAtomicityDemo {
    public volatile static int inc = 0;

    public void increase() {
        inc++;
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        VolatileAtomicityDemo volatileAtomicityDemo = new VolatileAtomicityDemo();
        for (int i = 0; i &lt; 5; i++) {
            threadPool.execute(() -&gt; {
                for (int j = 0; j &lt; 500; j++) {
                    volatileAtomicityDemo.increase();
                }
            });
        }
        // 等待1.5秒，保证上面程序执行完成
        Thread.sleep(1500);
        System.out.println(inc);
        threadPool.shutdown();
    }
}
</code></pre>
<p>正常情况下，运行上面的代码理应输出 <code>2500</code>。但你真正运行了上面的代码之后，你会发现每次输出结果都小于 <code>2500</code>。</p>
<p>为什么会出现这种情况呢？不是说好了，<code>volatile</code> 可以保证变量的可见性嘛！</p>
<p>也就是说，如果 <code>volatile</code> 能保证 <code>inc++</code> 操作的原子性的话。每个线程中对 <code>inc</code> 变量自增完之后，其他线程可以立即看到修改后的值。5 个线程分别进行了 500 次操作，那么最终 inc 的值应该是 5*500=2500。</p>
<p>很多人会误认为自增操作 <code>inc++</code> 是原子性的，实际上，<code>inc++</code> 其实是一个复合操作，包括三步：</p>
<ol>
<li>读取 inc 的值。</li>
<li>对 inc 加 1。</li>
<li>将 inc 的值写回内存。</li>
</ol>
<p><code>volatile</code> 是无法保证这三个操作是具有原子性的，有可能导致下面这种情况出现：</p>
<ol>
<li>线程 1 对 <code>inc</code> 进行读取操作之后，还未对其进行修改。线程 2 又读取了 <code>inc</code>的值并对其进行修改（+1），再将<code>inc</code> 的值写回内存。</li>
<li>线程 2 操作完毕后，线程 1 对 <code>inc</code>的值进行修改（+1），再将<code>inc</code> 的值写回内存。</li>
</ol>
<p>这也就导致两个线程分别对 <code>inc</code> 进行了一次自增操作后，<code>inc</code> 实际上只增加了 1。</p>
<p>其实，如果想要保证上面的代码运行正确也非常简单，利用 <code>synchronized</code>、<code>Lock</code>或者<code>AtomicInteger</code>都可以。</p>
<p>使用 <code>synchronized</code> 改进：</p>
<pre><code class="language-java">public synchronized void increase() {
    inc++;
}
</code></pre>
<p>使用 <code>AtomicInteger</code> 改进：</p>
<pre><code class="language-java">public AtomicInteger inc = new AtomicInteger();

public void increase() {
    inc.getAndIncrement();
}
</code></pre>
<p>使用 <code>ReentrantLock</code> 改进：</p>
<pre><code class="language-java">Lock lock = new ReentrantLock();
public void increase() {
    lock.lock();
    try {
        inc++;
    } finally {
        lock.unlock();
    }
}
</code></pre>
<h2><a id="%E2%AD%90%EF%B8%8F%E4%B9%90%E8%A7%82%E9%94%81%E5%92%8C%E6%82%B2%E8%A7%82%E9%94%81" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️乐观锁和悲观锁</h2>
<h3><a id="%E4%BB%80%E4%B9%88%E6%98%AF%E6%82%B2%E8%A7%82%E9%94%81%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>什么是悲观锁？</h3>
<p>悲观锁总是假设最坏的情况，认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改)，所以每次在获取资源操作的时候都会上锁，这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说，<strong>共享资源每次只给一个线程使用，其它线程阻塞，用完后再把资源转让给其它线程</strong>。</p>
<p>像 Java 中<code>synchronized</code>和<code>ReentrantLock</code>等独占锁就是悲观锁思想的实现。</p>
<pre><code class="language-java">public void performSynchronisedTask() {
    synchronized (this) {
        // 需要同步的操作
    }
}

private Lock lock = new ReentrantLock();
lock.lock();
try {
   // 需要同步的操作
} finally {
    lock.unlock();
}
</code></pre>
<p>高并发的场景下，激烈的锁竞争会造成线程阻塞，大量阻塞线程会导致系统的上下文切换，增加系统的性能开销。并且，悲观锁还可能会存在死锁问题，影响代码的正常运行。</p>
<h3><a id="%E4%BB%80%E4%B9%88%E6%98%AF%E4%B9%90%E8%A7%82%E9%94%81%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>什么是乐观锁？</h3>
<p>乐观锁总是假设最好的情况，认为共享资源每次被访问的时候不会出现问题，线程可以不停地执行，无需加锁也无需等待，只是在提交修改的时候去验证对应的资源（也就是数据）是否被其它线程修改了（具体方法可以使用版本号机制或 CAS 算法）。</p>
<p>在 Java 中<code>java.util.concurrent.atomic</code>包下面的原子变量类（比如<code>AtomicInteger</code>、<code>LongAdder</code>）就是使用了乐观锁的一种实现方式 <strong>CAS</strong> 实现的。<br />
<img src="media/17419985102015/17419996647351.png" alt="JUC原子类概览" /></p>
<pre><code class="language-java">// LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好
// 代价就是会消耗更多的内存空间（空间换时间）
LongAdder sum = new LongAdder();
sum.increment();
</code></pre>
<p>高并发的场景下，乐观锁相比悲观锁来说，不存在锁竞争造成线程阻塞，也不会有死锁的问题，在性能上往往会更胜一筹。但是，如果冲突频繁发生（写占比非常多的情况），会频繁失败和重试，这样同样会非常影响性能，导致 CPU 飙升。</p>
<p>不过，大量失败重试的问题也是可以解决的，像我们前面提到的 <code>LongAdder</code>以空间换时间的方式就解决了这个问题。</p>
<p>理论上来说：</p>
<ul>
<li>悲观锁通常多用于写比较多的情况（多写场景，竞争激烈），这样可以避免频繁失败和重试影响性能，悲观锁的开销是固定的。不过，如果乐观锁解决了频繁失败和重试这个问题的话（比如<code>LongAdder</code>），也是可以考虑使用乐观锁的，要视实际情况而定。</li>
<li>乐观锁通常多用于写比较少的情况（多读场景，竞争较少），这样可以避免频繁加锁影响性能。不过，乐观锁主要针对的对象是单个共享变量（参考<code>java.util.concurrent.atomic</code>包下面的原子变量类）。</li>
</ul>
<h3><a id="%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E4%B9%90%E8%A7%82%E9%94%81%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>如何实现乐观锁？</h3>
<p>乐观锁一般会使用版本号机制或 CAS 算法实现，CAS 算法相对来说更多一些，这里需要格外注意。</p>
<h4><a id="%E7%89%88%E6%9C%AC%E5%8F%B7%E6%9C%BA%E5%88%B6" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>版本号机制</h4>
<p>一般是在数据表中加上一个数据版本号 <code>version</code> 字段，表示数据被修改的次数。当数据被修改时，<code>version</code> 值会加一。当线程 A 要更新数据值时，在读取数据的同时也会读取 <code>version</code> 值，在提交更新时，若刚才读取到的 version 值为当前数据库中的 <code>version</code> 值相等时才更新，否则重试更新操作，直到更新成功。</p>
<p><strong>举一个简单的例子</strong>：假设数据库中帐户信息表中有一个 version 字段，当前值为 1 ；而当前帐户余额字段（ <code>balance</code> ）为 $100 。</p>
<ol>
<li>操作员 A 此时将其读出（ <code>version</code>=1 ），并从其帐户余额中扣除 $50（ $100-$50 ）。</li>
<li>在操作员 A 操作的过程中，操作员 B 也读入此用户信息（ <code>version</code>=1 ），并从其帐户余额中扣除 $20 （ $100-$20 ）。</li>
<li>操作员 A 完成了修改工作，将数据版本号（ <code>version</code>=1 ），连同帐户扣除后余额（ <code>balance</code>=$50 ），提交至数据库更新，此时由于提交数据版本等于数据库记录当前版本，数据被更新，数据库记录 <code>version</code> 更新为 2 。</li>
<li>操作员 B 完成了操作，也将版本号（ <code>version</code>=1 ）试图向数据库提交数据（ <code>balance</code>=$80 ），但此时比对数据库记录版本时发现，操作员 B 提交的数据版本号为 1 ，数据库记录当前版本也为 2 ，不满足 “ 提交版本必须等于当前版本才能执行更新 “ 的乐观锁策略，因此，操作员 B 的提交被驳回。</li>
</ol>
<p>这样就避免了操作员 B 用基于 <code>version</code>=1 的旧数据修改的结果覆盖操作员 A 的操作结果的可能。</p>
<h4><a id="cas%E7%AE%97%E6%B3%95" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>CAS 算法</h4>
<p>CAS 的全称是 <strong>Compare And Swap（比较与交换）</strong> ，用于实现乐观锁，被广泛应用于各大框架中。CAS 的思想很简单，就是用一个预期值和要更新的变量值进行比较，两值相等才会进行更新。</p>
<p>CAS 是一个原子操作，底层依赖于一条 CPU 的原子指令。</p>
<blockquote>
<p><strong>原子操作</strong> 即最小不可拆分的操作，也就是说操作一旦开始，就不能被打断，直到操作完成。</p>
</blockquote>
<p>CAS 涉及到三个操作数：</p>
<ul>
<li><strong>V</strong>：要更新的变量值(Var)</li>
<li><strong>E</strong>：预期值(Expected)</li>
<li><strong>N</strong>：拟写入的新值(New)</li>
</ul>
<p>当且仅当 V 的值等于 E 时，CAS 通过原子方式用新值 N 来更新 V 的值。如果不等，说明已经有其它线程更新了 V，则当前线程放弃更新。</p>
<p><strong>举一个简单的例子</strong>：线程 A 要修改变量 i 的值为 6，i 原值为 1（V = 1，E=1，N=6，假设不存在 ABA 问题）。</p>
<ol>
<li>i 与 1 进行比较，如果相等， 则说明没被其他线程修改，可以被设置为 6 。</li>
<li>i 与 1 进行比较，如果不相等，则说明被其他线程修改，当前线程放弃更新，CAS 操作失败。</li>
</ol>
<p>当多个线程同时使用 CAS 操作一个变量时，只有一个会胜出，并成功更新，其余均会失败，但失败的线程并不会被挂起，仅是被告知失败，并且允许再次尝试，当然也允许失败的线程放弃操作。</p>
<p>Java 语言并没有直接实现 CAS，CAS 相关的实现是通过 C++ 内联汇编的形式实现的（JNI 调用）。因此， CAS 的具体实现和操作系统以及 CPU 都有关系。</p>
<p><code>sun.misc</code>包下的<code>Unsafe</code>类提供了<code>compareAndSwapObject</code>、<code>compareAndSwapInt</code>、<code>compareAndSwapLong</code>方法来实现的对<code>Object</code>、<code>int</code>、<code>long</code>类型的 CAS 操作</p>
<pre><code class="language-java">/**
  *  CAS
  * @param o         包含要修改field的对象
  * @param offset    对象中某field的偏移量
  * @param expected  期望值
  * @param update    更新值
  * @return          true | false
  */
public final native boolean compareAndSwapObject(Object o, long offset,  Object expected, Object update);

public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);

public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);
</code></pre>
<p>关于 <code>Unsafe</code> 类的详细介绍可以看这篇文章：<a href="https://javaguide.cn/java/basis/unsafe.html">Java 魔法类 Unsafe 详解 - JavaGuide - 2022</a> 。</p>
<h3><a id="java%E4%B8%AD-cas%E6%98%AF%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E7%9A%84%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Java 中 CAS 是如何实现的？</h3>
<p>在 Java 中，实现 CAS（Compare-And-Swap, 比较并交换）操作的一个关键类是<code>Unsafe</code>。</p>
<p><code>Unsafe</code>类位于<code>sun.misc</code>包下，是一个提供低级别、不安全操作的类。由于其强大的功能和潜在的危险性，它通常用于 JVM 内部或一些需要极高性能和底层访问的库中，而不推荐普通开发者在应用程序中使用。关于 <code>Unsafe</code>类的详细介绍，可以阅读这篇文章：📌<a href="https://javaguide.cn/java/basis/unsafe.html">Java 魔法类 Unsafe 详解</a>。</p>
<p><code>sun.misc</code>包下的<code>Unsafe</code>类提供了<code>compareAndSwapObject</code>、<code>compareAndSwapInt</code>、<code>compareAndSwapLong</code>方法来实现的对<code>Object</code>、<code>int</code>、<code>long</code>类型的 CAS 操作：</p>
<pre><code class="language-java">/**
 * 以原子方式更新对象字段的值。
 *
 * @param o        要操作的对象
 * @param offset   对象字段的内存偏移量
 * @param expected 期望的旧值
 * @param x        要设置的新值
 * @return 如果值被成功更新，则返回 true；否则返回 false
 */
boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);

/**
 * 以原子方式更新 int 类型的对象字段的值。
 */
boolean compareAndSwapInt(Object o, long offset, int expected, int x);

/**
 * 以原子方式更新 long 类型的对象字段的值。
 */
boolean compareAndSwapLong(Object o, long offset, long expected, long x);
</code></pre>
<p><code>Unsafe</code>类中的 CAS 方法是<code>native</code>方法。<code>native</code>关键字表明这些方法是用本地代码（通常是 C 或 C++）实现的，而不是用 Java 实现的。这些方法直接调用底层的硬件指令来实现原子操作。也就是说，Java 语言并没有直接用 Java 实现 CAS，而是通过 C++ 内联汇编的形式实现的（通过 JNI 调用）。因此，CAS 的具体实现与操作系统以及 CPU 密切相关。</p>
<p><code>java.util.concurrent.atomic</code> 包提供了一些用于原子操作的类。这些类利用底层的原子指令，确保在多线程环境下的操作是线程安全的。</p>
<p><img src="media/17419985102015/17419996647351.png" alt="JUC原子类概览" /></p>
<p>关于这些 Atomic 原子类的介绍和使用，可以阅读这篇文章：<a href="https://javaguide.cn/java/concurrent/atomic-classes.html">Atomic 原子类总结</a>。</p>
<p><code>AtomicInteger</code>是 Java 的原子类之一，主要用于对 <code>int</code> 类型的变量进行原子操作，它利用<code>Unsafe</code>类提供的低级别原子操作方法实现无锁的线程安全性。</p>
<p>下面，我们通过解读<code>AtomicInteger</code>的核心源码（JDK1.8），来说明 Java 如何使用<code>Unsafe</code>类的方法来实现原子操作。</p>
<p><code>AtomicInteger</code>核心源码如下：</p>
<pre><code class="language-java">// 获取 Unsafe 实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
    try {
        // 获取“value”字段在AtomicInteger类中的内存偏移量
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField(&quot;value&quot;));
    } catch (Exception ex) { throw new Error(ex); }
}
// 确保“value”字段的可见性
private volatile int value;

// 如果当前值等于预期值，则原子地将值设置为newValue
// 使用 Unsafe#compareAndSwapInt 方法进行CAS操作
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

// 原子地将当前值加 delta 并返回旧值
public final int getAndAdd(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta);
}

// 原子地将当前值加 1 并返回加之前的值（旧值）
// 使用 Unsafe#getAndAddInt 方法进行CAS操作。
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

// 原子地将当前值减 1 并返回减之前的值（旧值）
public final int getAndDecrement() {
    return unsafe.getAndAddInt(this, valueOffset, -1);
}
</code></pre>
<p><code>Unsafe#getAndAddInt</code>源码：</p>
<pre><code class="language-java">// 原子地获取并增加整数值
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        // 以 volatile 方式获取对象 o 在内存偏移量 offset 处的整数值
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    // 返回旧值
    return v;
}
</code></pre>
<p>可以看到，<code>getAndAddInt</code> 使用了 <code>do-while</code> 循环：在<code>compareAndSwapInt</code>操作失败时，会不断重试直到成功。也就是说，<code>getAndAddInt</code>方法会通过 <code>compareAndSwapInt</code> 方法来尝试更新 <code>value</code> 的值，如果更新失败（当前值在此期间被其他线程修改），它会重新获取当前值并再次尝试更新，直到操作成功。</p>
<p>由于 CAS 操作可能会因为并发冲突而失败，因此通常会与<code>while</code>循环搭配使用，在失败后不断重试，直到操作成功。这就是 <strong>自旋锁机制</strong> 。</p>
<h3><a id="cas%E7%AE%97%E6%B3%95%E5%AD%98%E5%9C%A8%E5%93%AA%E4%BA%9B%E9%97%AE%E9%A2%98%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>CAS 算法存在哪些问题？</h3>
<p>ABA 问题是 CAS 算法最常见的问题。</p>
<h4><a id="aba%E9%97%AE%E9%A2%98" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>ABA 问题</h4>
<p>如果一个变量 V 初次读取的时候是 A 值，并且在准备赋值的时候检查到它仍然是 A 值，那我们就能说明它的值没有被其他线程修改过了吗？很明显是不能的，因为在这段时间它的值可能被改为其他值，然后又改回 A，那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 <strong>&quot;ABA&quot;问题。</strong></p>
<p>ABA 问题的解决思路是在变量前面追加上<strong>版本号或者时间戳</strong>。JDK 1.5 以后的 <code>AtomicStampedReference</code> 类就是用来解决 ABA 问题的，其中的 <code>compareAndSet()</code> 方法就是首先检查当前引用是否等于预期引用，并且当前标志是否等于预期标志，如果全部相等，则以原子方式将该引用和该标志的值设置为给定的更新值。</p>
<pre><code class="language-java">public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    Pair&lt;V&gt; current = pair;
    return
        expectedReference == current.reference &amp;&amp;
        expectedStamp == current.stamp &amp;&amp;
        ((newReference == current.reference &amp;&amp;
          newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));
}
</code></pre>
<h4><a id="%E5%BE%AA%E7%8E%AF%E6%97%B6%E9%97%B4%E9%95%BF%E5%BC%80%E9%94%80%E5%A4%A7" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>循环时间长开销大</h4>
<p>CAS 经常会用到自旋操作来进行重试，也就是不成功就一直循环执行直到成功。如果长时间不成功，会给 CPU 带来非常大的执行开销。</p>
<p>如果 JVM 能够支持处理器提供的<code>pause</code>指令，那么自旋操作的效率将有所提升。<code>pause</code>指令有两个重要作用：</p>
<ol>
<li><strong>延迟流水线执行指令</strong>：<code>pause</code>指令可以延迟指令的执行，从而减少 CPU 的资源消耗。具体的延迟时间取决于处理器的实现版本，在某些处理器上，延迟时间可能为零。</li>
<li><strong>避免内存顺序冲突</strong>：在退出循环时，<code>pause</code>指令可以避免由于内存顺序冲突而导致的 CPU 流水线被清空，从而提高 CPU 的执行效率。</li>
</ol>
<h4><a id="%E5%8F%AA%E8%83%BD%E4%BF%9D%E8%AF%81%E4%B8%80%E4%B8%AA%E5%85%B1%E4%BA%AB%E5%8F%98%E9%87%8F%E7%9A%84%E5%8E%9F%E5%AD%90%E6%93%8D%E4%BD%9C" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>只能保证一个共享变量的原子操作</h4>
<p>CAS 操作仅能对单个共享变量有效。当需要操作多个共享变量时，CAS 就显得无能为力。不过，从 JDK 1.5 开始，Java 提供了<code>AtomicReference</code>类，这使得我们能够保证引用对象之间的原子性。通过将多个变量封装在一个对象中，我们可以使用<code>AtomicReference</code>来执行 CAS 操作。</p>
<p>除了 <code>AtomicReference</code> 这种方式之外，还可以利用加锁来保证。</p>
<h2><a id="synchronized%E5%85%B3%E9%94%AE%E5%AD%97" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>synchronized 关键字</h2>
<h3><a id="synchronized%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F%E6%9C%89%E4%BB%80%E4%B9%88%E7%94%A8%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>synchronized 是什么？有什么用？</h3>
<p><code>synchronized</code> 是 Java 中的一个关键字，翻译成中文是同步的意思，主要解决的是多个线程之间访问资源的同步性，可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。</p>
<p>在 Java 早期版本中，<code>synchronized</code> 属于 <strong>重量级锁</strong>，效率低下。这是因为监视器锁（monitor）是依赖于底层的操作系统的 <code>Mutex Lock</code> 来实现的，Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程，都需要操作系统帮忙完成，而操作系统实现线程之间的切换时需要从用户态转换到内核态，这个状态之间的转换需要相对比较长的时间，时间成本相对较高。</p>
<p>不过，在 Java 6 之后， <code>synchronized</code> 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销，这些优化让 <code>synchronized</code> 锁的效率提升了很多。因此， <code>synchronized</code> 还是可以在实际项目中使用的，像 JDK 源码、很多开源框架都大量使用了 <code>synchronized</code> 。</p>
<p>关于偏向锁多补充一点：由于偏向锁增加了 JVM 的复杂性，同时也并没有为所有应用都带来性能提升。因此，在 JDK15 中，偏向锁被默认关闭（仍然可以使用 <code>-XX:+UseBiasedLocking</code> 启用偏向锁），在 JDK18 中，偏向锁已经被彻底废弃（无法通过命令行打开）。</p>
<h3><a id="%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8synchronized%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>如何使用 synchronized？</h3>
<p><code>synchronized</code> 关键字的使用方式主要有下面 3 种：</p>
<ol>
<li>修饰实例方法</li>
<li>修饰静态方法</li>
<li>修饰代码块</li>
</ol>
<p><strong>1、修饰实例方法</strong> （锁当前对象实例）</p>
<p>给当前对象实例加锁，进入同步代码前要获得 <strong>当前对象实例的锁</strong> 。</p>
<pre><code class="language-java">synchronized void method() {
    //业务代码
}
</code></pre>
<p><strong>2、修饰静态方法</strong> （锁当前类）</p>
<p>给当前类加锁，会作用于类的所有对象实例 ，进入同步代码前要获得 <strong>当前 class 的锁</strong>。</p>
<p>这是因为静态成员不属于任何一个实例对象，归整个类所有，不依赖于类的特定实例，被类的所有实例共享。</p>
<pre><code class="language-java">synchronized static void method() {
    //业务代码
}
</code></pre>
<p>静态 <code>synchronized</code> 方法和非静态 <code>synchronized</code> 方法之间的调用互斥么？不互斥！如果一个线程 A 调用一个实例对象的非静态 <code>synchronized</code> 方法，而线程 B 需要调用这个实例对象所属类的静态 <code>synchronized</code> 方法，是允许的，不会发生互斥现象，因为访问静态 <code>synchronized</code> 方法占用的锁是当前类的锁，而访问非静态 <code>synchronized</code> 方法占用的锁是当前实例对象锁。</p>
<p><strong>3、修饰代码块</strong> （锁指定对象/类）</p>
<p>对括号里指定的对象/类加锁：</p>
<ul>
<li><code>synchronized(object)</code> 表示进入同步代码库前要获得 <strong>给定对象的锁</strong>。</li>
<li><code>synchronized(类.class)</code> 表示进入同步代码前要获得 <strong>给定 Class 的锁</strong></li>
</ul>
<pre><code class="language-java">synchronized(this) {
    //业务代码
}
</code></pre>
<p><strong>总结：</strong></p>
<ul>
<li><code>synchronized</code> 关键字加到 <code>static</code> 静态方法和 <code>synchronized(class)</code> 代码块上都是是给 Class 类上锁；</li>
<li><code>synchronized</code> 关键字加到实例方法上是给对象实例上锁；</li>
<li>尽量不要使用 <code>synchronized(String a)</code> 因为 JVM 中，字符串常量池具有缓存功能。</li>
</ul>
<h3><a id="%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95%E5%8F%AF%E4%BB%A5%E7%94%A8synchronized%E4%BF%AE%E9%A5%B0%E4%B9%88%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>构造方法可以用 synchronized 修饰么？</h3>
<p>构造方法不能使用 synchronized 关键字修饰。不过，可以在构造方法内部使用 synchronized 代码块。</p>
<p>另外，构造方法本身是线程安全的，但如果在构造方法中涉及到共享资源的操作，就需要采取适当的同步措施来保证整个构造过程的线程安全。</p>
<h3><a id="%E2%AD%90%EF%B8%8Fsynchronized%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E4%BA%86%E8%A7%A3%E5%90%97%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️synchronized 底层原理了解吗？</h3>
<p>synchronized 关键字底层原理属于 JVM 层面的东西。</p>
<h4><a id="synchronized%E5%90%8C%E6%AD%A5%E8%AF%AD%E5%8F%A5%E5%9D%97%E7%9A%84%E6%83%85%E5%86%B5" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>synchronized 同步语句块的情况</h4>
<pre><code class="language-java">public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println(&quot;synchronized 代码块&quot;);
        }
    }
}
</code></pre>
<p>通过 JDK 自带的 <code>javap</code> 命令查看 <code>SynchronizedDemo</code> 类的相关字节码信息：首先切换到类的对应目录执行 <code>javac SynchronizedDemo.java</code> 命令生成编译后的 .class 文件，然后执行<code>javap -c -s -v -l SynchronizedDemo.class</code>。</p>
<p><img src="media/17419985102015/17419996647376.png" alt="synchronized关键字原理" /></p>
<p>从上面我们可以看出：<strong><code>synchronized</code> 同步语句块的实现使用的是 <code>monitorenter</code> 和 <code>monitorexit</code> 指令，其中 <code>monitorenter</code> 指令指向同步代码块的开始位置，<code>monitorexit</code> 指令则指明同步代码块的结束位置。</strong></p>
<p>上面的字节码中包含一个 <code>monitorenter</code> 指令以及两个 <code>monitorexit</code> 指令，这是为了保证锁在同步代码块代码正常执行以及出现异常的这两种情况下都能被正确释放。</p>
<p>当执行 <code>monitorenter</code> 指令时，线程试图获取锁也就是获取 <strong>对象监视器 <code>monitor</code></strong> 的持有权。</p>
<blockquote>
<p>在 Java 虚拟机(HotSpot)中，Monitor 是基于 C++实现的，由<a href="https://github.com/openjdk-mirror/jdk7u-hotspot/blob/50bdefc3afe944ca74c3093e7448d6b889cd20d1/src/share/vm/runtime/objectMonitor.cpp">ObjectMonitor</a>实现的。每个对象中都内置了一个 <code>ObjectMonitor</code>对象。</p>
<p>另外，<code>wait/notify</code>等方法也依赖于<code>monitor</code>对象，这就是为什么只有在同步的块或者方法中才能调用<code>wait/notify</code>等方法，否则会抛出<code>java.lang.IllegalMonitorStateException</code>的异常的原因。</p>
</blockquote>
<p>在执行<code>monitorenter</code>时，会尝试获取对象的锁，如果锁的计数器为 0 则表示锁可以被获取，获取后将锁计数器设为 1 也就是加 1。</p>
<p><img src="media/17419985102015/17419996647389.png" alt="执行 monitorenter 获取锁" /></p>
<p>对象锁的拥有者线程才可以执行 <code>monitorexit</code> 指令来释放锁。在执行 <code>monitorexit</code> 指令后，将锁计数器设为 0，表明锁被释放，其他线程可以尝试获取锁。</p>
<p><img src="media/17419985102015/17419996647402.png" alt="执行 monitorexit 释放锁" /></p>
<p>如果获取对象锁失败，那当前线程就要阻塞等待，直到锁被另外一个线程释放为止。</p>
<h4><a id="synchronized%E4%BF%AE%E9%A5%B0%E6%96%B9%E6%B3%95%E7%9A%84%E6%83%85%E5%86%B5" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>synchronized 修饰方法的情况</h4>
<pre><code class="language-java">public class SynchronizedDemo2 {
    public synchronized void method() {
        System.out.println(&quot;synchronized 方法&quot;);
    }
}

</code></pre>
<p><img src="media/17419985102015/17419996647414.png" alt="synchronized关键字原理" /></p>
<p><code>synchronized</code> 修饰的方法并没有 <code>monitorenter</code> 指令和 <code>monitorexit</code> 指令，取而代之的是 <code>ACC_SYNCHRONIZED</code> 标识，该标识指明了该方法是一个同步方法。JVM 通过该 <code>ACC_SYNCHRONIZED</code> 访问标志来辨别一个方法是否声明为同步方法，从而执行相应的同步调用。</p>
<p>如果是实例方法，JVM 会尝试获取实例对象的锁。如果是静态方法，JVM 会尝试获取当前 class 的锁。</p>
<h4><a id="%E6%80%BB%E7%BB%93" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>总结</h4>
<p><code>synchronized</code> 同步语句块的实现使用的是 <code>monitorenter</code> 和 <code>monitorexit</code> 指令，其中 <code>monitorenter</code> 指令指向同步代码块的开始位置，<code>monitorexit</code> 指令则指明同步代码块的结束位置。</p>
<p><code>synchronized</code> 修饰的方法并没有 <code>monitorenter</code> 指令和 <code>monitorexit</code> 指令，取而代之的是 <code>ACC_SYNCHRONIZED</code> 标识，该标识指明了该方法是一个同步方法。</p>
<p><strong>不过，两者的本质都是对对象监视器 monitor 的获取。</strong></p>
<p>相关推荐：<a href="https://tech.youzan.com/javasuo-yu-xian-cheng-de-na-xie-shi/">Java 锁与线程的那些事 - 有赞技术团队</a> 。</p>
<p>🧗🏻 进阶一下：学有余力的小伙伴可以抽时间详细研究一下对象监视器 <code>monitor</code>。</p>
<h3><a id="jdk1-6%E4%B9%8B%E5%90%8E%E7%9A%84-synchronized%E5%BA%95%E5%B1%82%E5%81%9A%E4%BA%86%E5%93%AA%E4%BA%9B%E4%BC%98%E5%8C%96%EF%BC%9F%E9%94%81%E5%8D%87%E7%BA%A7%E5%8E%9F%E7%90%86%E4%BA%86%E8%A7%A3%E5%90%97%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>JDK1.6 之后的 synchronized 底层做了哪些优化？锁升级原理了解吗？</h3>
<p>在 Java 6 之后， <code>synchronized</code> 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销，这些优化让 <code>synchronized</code> 锁的效率提升了很多（JDK18 中，偏向锁已经被彻底废弃，前面已经提到过了）。</p>
<p>锁主要存在四种状态，依次是：无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态，他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级，这种策略是为了提高获得锁和释放锁的效率。</p>
<p><code>synchronized</code> 锁升级是一个比较复杂的过程，面试也很少问到，如果你想要详细了解的话，可以看看这篇文章：<a href="https://www.cnblogs.com/star95/p/17542850.html">浅析 synchronized 锁升级的原理与实现</a>。</p>
<h3><a id="synchronized%E7%9A%84%E5%81%8F%E5%90%91%E9%94%81%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A2%AB%E5%BA%9F%E5%BC%83%E4%BA%86%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>synchronized 的偏向锁为什么被废弃了？</h3>
<p>Open JDK 官方声明：<a href="https://openjdk.org/jeps/374">JEP 374: Deprecate and Disable Biased Locking</a></p>
<p>在 JDK15 中，偏向锁被默认关闭（仍然可以使用 <code>-XX:+UseBiasedLocking</code> 启用偏向锁），在 JDK18 中，偏向锁已经被彻底废弃（无法通过命令行打开）。</p>
<p>在官方声明中，主要原因有两个方面：</p>
<ul>
<li><strong>性能收益不明显：</strong></li>
</ul>
<p>偏向锁是 HotSpot 虚拟机的一项优化技术，可以提升单线程对同步代码块的访问性能。</p>
<p>受益于偏向锁的应用程序通常使用了早期的 Java 集合 API，例如 HashTable、Vector，在这些集合类中通过 synchronized 来控制同步，这样在单线程频繁访问时，通过偏向锁会减少同步开销。</p>
<p>随着 JDK 的发展，出现了 ConcurrentHashMap 高性能的集合类，在集合类内部进行了许多性能优化，此时偏向锁带来的性能收益就不明显了。</p>
<p>偏向锁仅仅在单线程访问同步代码块的场景中可以获得性能收益。</p>
<p>如果存在多线程竞争，就需要 <strong>撤销偏向锁</strong> ，这个操作的性能开销是比较昂贵的。偏向锁的撤销需要等待进入到全局安全点（safe point），该状态下所有线程都是暂停的，此时去检查线程状态并进行偏向锁的撤销。</p>
<ul>
<li><strong>JVM 内部代码维护成本太高：</strong></li>
</ul>
<p>偏向锁将许多复杂代码引入到同步子系统，并且对其他的 HotSpot 组件也具有侵入性。这种复杂性为理解代码、系统重构带来了困难，因此， OpenJDK 官方希望禁用、废弃并删除偏向锁。</p>
<h3><a id="%E2%AD%90%EF%B8%8Fsynchronized%E5%92%8C-volatile%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️synchronized 和 volatile 有什么区别？</h3>
<p><code>synchronized</code> 关键字和 <code>volatile</code> 关键字是两个互补的存在，而不是对立的存在！</p>
<ul>
<li><code>volatile</code> 关键字是线程同步的轻量级实现，所以 <code>volatile</code>性能肯定比<code>synchronized</code>关键字要好 。但是 <code>volatile</code> 关键字只能用于变量而 <code>synchronized</code> 关键字可以修饰方法以及代码块 。</li>
<li><code>volatile</code> 关键字能保证数据的可见性，但不能保证数据的原子性。<code>synchronized</code> 关键字两者都能保证。</li>
<li><code>volatile</code>关键字主要用于解决变量在多个线程之间的可见性，而 <code>synchronized</code> 关键字解决的是多个线程之间访问资源的同步性。</li>
</ul>
<h2><a id="reentrantlock" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>ReentrantLock</h2>
<h3><a id="reentrantlock%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>ReentrantLock 是什么？</h3>
<p><code>ReentrantLock</code> 实现了 <code>Lock</code> 接口，是一个可重入且独占式的锁，和 <code>synchronized</code> 关键字类似。不过，<code>ReentrantLock</code> 更灵活、更强大，增加了轮询、超时、中断、公平锁和非公平锁等高级功能。</p>
<pre><code class="language-java">public class ReentrantLock implements Lock, java.io.Serializable {}
</code></pre>
<p><code>ReentrantLock</code> 里面有一个内部类 <code>Sync</code>，<code>Sync</code> 继承 AQS（<code>AbstractQueuedSynchronizer</code>），添加锁和释放锁的大部分操作实际上都是在 <code>Sync</code> 中实现的。<code>Sync</code> 有公平锁 <code>FairSync</code> 和非公平锁 <code>NonfairSync</code> 两个子类。</p>
<p><img src="media/17419985102015/17419996647430.png" alt="" /></p>
<p><code>ReentrantLock</code> 默认使用非公平锁，也可以通过构造器来显式的指定使用公平锁。</p>
<pre><code class="language-java">// 传入一个 boolean 值，true 时为公平锁，false 时为非公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
</code></pre>
<p>从上面的内容可以看出， <code>ReentrantLock</code> 的底层就是由 AQS 来实现的。关于 AQS 的相关内容推荐阅读 <a href="https://javaguide.cn/java/concurrent/aqs.html">AQS 详解</a> 这篇文章。</p>
<h3><a id="%E5%85%AC%E5%B9%B3%E9%94%81%E5%92%8C%E9%9D%9E%E5%85%AC%E5%B9%B3%E9%94%81%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>公平锁和非公平锁有什么区别？</h3>
<ul>
<li><strong>公平锁</strong> : 锁被释放之后，先申请的线程先得到锁。性能较差一些，因为公平锁为了保证时间上的绝对顺序，上下文切换更频繁。</li>
<li><strong>非公平锁</strong>：锁被释放之后，后申请的线程可能会先获取到锁，是随机或者按照其他优先级排序的。性能更好，但可能会导致某些线程永远无法获取到锁。</li>
</ul>
<h3><a id="%E2%AD%90%EF%B8%8Fsynchronized%E5%92%8C-reentrantlock%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️synchronized 和 ReentrantLock 有什么区别？</h3>
<h4><a id="%E4%B8%A4%E8%80%85%E9%83%BD%E6%98%AF%E5%8F%AF%E9%87%8D%E5%85%A5%E9%94%81" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>两者都是可重入锁</h4>
<p><strong>可重入锁</strong> 也叫递归锁，指的是线程可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁，此时这个对象锁还没有释放，当其再次想要获取这个对象的锁的时候还是可以获取的，如果是不可重入锁的话，就会造成死锁。</p>
<p>JDK 提供的所有现成的 <code>Lock</code> 实现类，包括 <code>synchronized</code> 关键字锁都是可重入的。</p>
<p>在下面的代码中，<code>method1()</code> 和 <code>method2()</code>都被 <code>synchronized</code> 关键字修饰，<code>method1()</code>调用了<code>method2()</code>。</p>
<pre><code class="language-java">public class SynchronizedDemo {
    public synchronized void method1() {
        System.out.println(&quot;方法1&quot;);
        method2();
    }

    public synchronized void method2() {
        System.out.println(&quot;方法2&quot;);
    }
}
</code></pre>
<p>由于 <code>synchronized</code>锁是可重入的，同一个线程在调用<code>method1()</code> 时可以直接获得当前对象的锁，执行 <code>method2()</code> 的时候可以再次获取这个对象的锁，不会产生死锁问题。假如<code>synchronized</code>是不可重入锁的话，由于该对象的锁已被当前线程所持有且无法释放，这就导致线程在执行 <code>method2()</code>时获取锁失败，会出现死锁问题。</p>
<h4><a id="synchronized%E4%BE%9D%E8%B5%96%E4%BA%8E-jvm%E8%80%8C-reentrantlock%E4%BE%9D%E8%B5%96%E4%BA%8E-api" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API</h4>
<p><code>synchronized</code> 是依赖于 JVM 实现的，前面我们也讲到了 虚拟机团队在 JDK1.6 为 <code>synchronized</code> 关键字进行了很多优化，但是这些优化都是在虚拟机层面实现的，并没有直接暴露给我们。</p>
<p><code>ReentrantLock</code> 是 JDK 层面实现的（也就是 API 层面，需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成），所以我们可以通过查看它的源代码，来看它是如何实现的。</p>
<h4><a id="reentrantlock%E6%AF%94-synchronized%E5%A2%9E%E5%8A%A0%E4%BA%86%E4%B8%80%E4%BA%9B%E9%AB%98%E7%BA%A7%E5%8A%9F%E8%83%BD" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>ReentrantLock 比 synchronized 增加了一些高级功能</h4>
<p>相比<code>synchronized</code>，<code>ReentrantLock</code>增加了一些高级功能。主要来说主要有三点：</p>
<ul>
<li><strong>等待可中断</strong> : <code>ReentrantLock</code>提供了一种能够中断等待锁的线程的机制，通过 <code>lock.lockInterruptibly()</code> 来实现这个机制。也就是说当前线程在等待获取锁的过程中，如果其他线程中断当前线程「 <code>interrupt()</code> 」，当前线程就会抛出 <code>InterruptedException</code> 异常，可以捕捉该异常进行相应处理。</li>
<li><strong>可实现公平锁</strong> : <code>ReentrantLock</code>可以指定是公平锁还是非公平锁。而<code>synchronized</code>只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。<code>ReentrantLock</code>默认情况是非公平的，可以通过 <code>ReentrantLock</code>类的<code>ReentrantLock(boolean fair)</code>构造方法来指定是否是公平的。</li>
<li><strong>可实现选择性通知（锁可以绑定多个条件）</strong>: <code>synchronized</code>关键字与<code>wait()</code>和<code>notify()</code>/<code>notifyAll()</code>方法相结合可以实现等待/通知机制。<code>ReentrantLock</code>类当然也可以实现，但是需要借助于<code>Condition</code>接口与<code>newCondition()</code>方法。</li>
<li><strong>支持超时</strong> ：<code>ReentrantLock</code> 提供了 <code>tryLock(timeout)</code> 的方法，可以指定等待获取锁的最长等待时间，如果超过了等待时间，就会获取锁失败，不会一直等待。</li>
</ul>
<p>如果你想使用上述功能，那么选择 <code>ReentrantLock</code> 是一个不错的选择。</p>
<p>关于 <code>Condition</code>接口的补充：</p>
<blockquote>
<p><code>Condition</code>是 JDK1.5 之后才有的，它具有很好的灵活性，比如可以实现多路通知功能也就是在一个<code>Lock</code>对象中可以创建多个<code>Condition</code>实例（即对象监视器），<strong>线程对象可以注册在指定的<code>Condition</code>中，从而可以有选择性的进行线程通知，在调度线程上更加灵活。 在使用<code>notify()/notifyAll()</code>方法进行通知时，被通知的线程是由 JVM 选择的，用<code>ReentrantLock</code>类结合<code>Condition</code>实例可以实现“选择性通知”</strong> ，这个功能非常重要，而且是 <code>Condition</code> 接口默认提供的。而<code>synchronized</code>关键字就相当于整个 <code>Lock</code> 对象中只有一个<code>Condition</code>实例，所有的线程都注册在它一个身上。如果执行<code>notifyAll()</code>方法的话就会通知所有处于等待状态的线程，这样会造成很大的效率问题。而<code>Condition</code>实例的<code>signalAll()</code>方法，只会唤醒注册在该<code>Condition</code>实例中的所有等待线程。</p>
</blockquote>
<p>关于 <strong>等待可中断</strong> 的补充：</p>
<blockquote>
<p><code>lockInterruptibly()</code> 会让获取锁的线程在阻塞等待的过程中可以响应中断，即当前线程在获取锁的时候，发现锁被其他线程持有，就会阻塞等待。</p>
<p>在阻塞等待的过程中，如果其他线程中断当前线程 <code>interrupt()</code> ，就会抛出 <code>InterruptedException</code> 异常，可以捕获该异常，做一些处理操作。</p>
<p>为了更好理解这个方法，借用 Stack Overflow 上的一个案例，可以更好地理解 <code>lockInterruptibly()</code> 可以响应中断：</p>
<pre><code class="language-JAVA">public class MyRentrantlock {
    Thread t = new Thread() {
        @Override
        public void run() {
            ReentrantLock r = new ReentrantLock();
            // 1.1、第一次尝试获取锁，可以获取成功
            r.lock();

            // 1.2、此时锁的重入次数为 1
            System.out.println(&quot;lock() : lock count :&quot; + r.getHoldCount());

            // 2、中断当前线程，通过 Thread.currentThread().isInterrupted() 可以看到当前线程的中断状态为 true
            interrupt();
            System.out.println(&quot;Current thread is intrupted&quot;);

            // 3.1、尝试获取锁，可以成功获取
            r.tryLock();
            // 3.2、此时锁的重入次数为 2
            System.out.println(&quot;tryLock() on intrupted thread lock count :&quot; + r.getHoldCount());
            try {
                // 4、打印线程的中断状态为 true，那么调用 lockInterruptibly() 方法就会抛出 InterruptedException 异常
                System.out.println(&quot;Current Thread isInterrupted:&quot; + Thread.currentThread().isInterrupted());
                r.lockInterruptibly();
                System.out.println(&quot;lockInterruptibly() --NOt executable statement&quot; + r.getHoldCount());
            } catch (InterruptedException e) {
                r.lock();
                System.out.println(&quot;Error&quot;);
            } finally {
                r.unlock();
            }

            // 5、打印锁的重入次数，可以发现 lockInterruptibly() 方法并没有成功获取到锁
            System.out.println(&quot;lockInterruptibly() not able to Acqurie lock: lock count :&quot; + r.getHoldCount());

            r.unlock();
            System.out.println(&quot;lock count :&quot; + r.getHoldCount());
            r.unlock();
            System.out.println(&quot;lock count :&quot; + r.getHoldCount());
        }
    };
    public static void main(String str[]) {
        MyRentrantlock m = new MyRentrantlock();
        m.t.start();
    }
}
</code></pre>
<p>输出：</p>
<pre><code class="language-BASH">lock() : lock count :1
Current thread is intrupted
tryLock() on intrupted thread lock count :2
Current Thread isInterrupted:true
Error
lockInterruptibly() not able to Acqurie lock: lock count :2
lock count :1
lock count :0
</code></pre>
</blockquote>
<p>关于 <strong>支持超时</strong> 的补充：</p>
<blockquote>
<p><strong>为什么需要 <code>tryLock(timeout)</code> 这个功能呢？</strong></p>
<p><code>tryLock(timeout)</code> 方法尝试在指定的超时时间内获取锁。如果成功获取锁，则返回 <code>true</code>；如果在锁可用之前超时，则返回 <code>false</code>。此功能在以下几种场景中非常有用：</p>
<ul>
<li><strong>防止死锁：</strong> 在复杂的锁场景中，<code>tryLock(timeout)</code> 可以通过允许线程在合理的时间内放弃并重试来帮助防止死锁。</li>
<li><strong>提高响应速度：</strong> 防止线程无限期阻塞。</li>
<li><strong>处理时间敏感的操作：</strong> 对于具有严格时间限制的操作，<code>tryLock(timeout)</code> 允许线程在无法及时获取锁时继续执行替代操作。</li>
</ul>
</blockquote>
<h3><a id="%E5%8F%AF%E4%B8%AD%E6%96%AD%E9%94%81%E5%92%8C%E4%B8%8D%E5%8F%AF%E4%B8%AD%E6%96%AD%E9%94%81%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>可中断锁和不可中断锁有什么区别？</h3>
<ul>
<li><strong>可中断锁</strong>：获取锁的过程中可以被中断，不需要一直等到获取锁之后 才能进行其他逻辑处理。<code>ReentrantLock</code> 就属于是可中断锁。</li>
<li><strong>不可中断锁</strong>：一旦线程申请了锁，就只能等到拿到锁以后才能进行其他的逻辑处理。 <code>synchronized</code> 就属于是不可中断锁。</li>
</ul>
<h2><a id="reentrantreadwritelock" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>ReentrantReadWriteLock</h2>
<p><code>ReentrantReadWriteLock</code> 在实际项目中使用的并不多，面试中也问的比较少，简单了解即可。JDK 1.8 引入了性能更好的读写锁 <code>StampedLock</code> 。</p>
<h3><a id="reentrantreadwritelock%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>ReentrantReadWriteLock 是什么？</h3>
<p><code>ReentrantReadWriteLock</code> 实现了 <code>ReadWriteLock</code> ，是一个可重入的读写锁，既可以保证多个线程同时读的效率，同时又可以保证有写入操作时的线程安全。</p>
<pre><code class="language-java">public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable{
}
public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}
</code></pre>
<ul>
<li>一般锁进行并发控制的规则：读读互斥、读写互斥、写写互斥。</li>
<li>读写锁进行并发控制的规则：读读不互斥、读写互斥、写写互斥（只有读读不互斥）。</li>
</ul>
<p><code>ReentrantReadWriteLock</code> 其实是两把锁，一把是 <code>WriteLock</code> (写锁)，一把是 <code>ReadLock</code>（读锁） 。读锁是共享锁，写锁是独占锁。读锁可以被同时读，可以同时被多个线程持有，而写锁最多只能同时被一个线程持有。</p>
<p>和 <code>ReentrantLock</code> 一样，<code>ReentrantReadWriteLock</code> 底层也是基于 AQS 实现的。</p>
<p><img src="media/17419985102015/17419996647445.png" alt="" /></p>
<p><code>ReentrantReadWriteLock</code> 也支持公平锁和非公平锁，默认使用非公平锁，可以通过构造器来显示的指定。</p>
<pre><code class="language-java">// 传入一个 boolean 值，true 时为公平锁，false 时为非公平锁
public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}
</code></pre>
<h3><a id="reentrantreadwritelock%E9%80%82%E5%90%88%E4%BB%80%E4%B9%88%E5%9C%BA%E6%99%AF%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>ReentrantReadWriteLock 适合什么场景？</h3>
<p>由于 <code>ReentrantReadWriteLock</code> 既可以保证多个线程同时读的效率，同时又可以保证有写入操作时的线程安全。因此，在读多写少的情况下，使用 <code>ReentrantReadWriteLock</code> 能够明显提升系统性能。</p>
<h3><a id="%E5%85%B1%E4%BA%AB%E9%94%81%E5%92%8C%E7%8B%AC%E5%8D%A0%E9%94%81%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>共享锁和独占锁有什么区别？</h3>
<ul>
<li><strong>共享锁</strong>：一把锁可以被多个线程同时获得。</li>
<li><strong>独占锁</strong>：一把锁只能被一个线程获得。</li>
</ul>
<h3><a id="%E7%BA%BF%E7%A8%8B%E6%8C%81%E6%9C%89%E8%AF%BB%E9%94%81%E8%BF%98%E8%83%BD%E8%8E%B7%E5%8F%96%E5%86%99%E9%94%81%E5%90%97%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>线程持有读锁还能获取写锁吗？</h3>
<ul>
<li>在线程持有读锁的情况下，该线程不能取得写锁(因为获取写锁的时候，如果发现当前的读锁被占用，就马上获取失败，不管读锁是不是被当前线程持有)。</li>
<li>在线程持有写锁的情况下，该线程可以继续获取读锁（获取读锁时如果发现写锁被占用，只有写锁没有被当前线程占用的情况才会获取失败）。</li>
</ul>
<p>读写锁的源码分析，推荐阅读 <a href="https://mp.weixin.qq.com/s/h3VIUyH9L0v14MrQJiiDbw">聊聊 Java 的几把 JVM 级锁 - 阿里巴巴中间件</a> 这篇文章，写的很不错。</p>
<h3><a id="%E8%AF%BB%E9%94%81%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E8%83%BD%E5%8D%87%E7%BA%A7%E4%B8%BA%E5%86%99%E9%94%81%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>读锁为什么不能升级为写锁？</h3>
<p>写锁可以降级为读锁，但是读锁却不能升级为写锁。这是因为读锁升级为写锁会引起线程的争夺，毕竟写锁属于是独占锁，这样的话，会影响性能。</p>
<p>另外，还可能会有死锁问题发生。举个例子：假设两个线程的读锁都想升级写锁，则需要对方都释放自己锁，而双方都不释放，就会产生死锁。</p>
<h2><a id="stampedlock" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>StampedLock</h2>
<p><code>StampedLock</code> 面试中问的比较少，不是很重要，简单了解即可。</p>
<h3><a id="stampedlock%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>StampedLock 是什么？</h3>
<p><code>StampedLock</code> 是 JDK 1.8 引入的性能更好的读写锁，不可重入且不支持条件变量 <code>Condition</code>。</p>
<p>不同于一般的 <code>Lock</code> 类，<code>StampedLock</code> 并不是直接实现 <code>Lock</code>或 <code>ReadWriteLock</code>接口，而是基于 <strong>CLH 锁</strong> 独立实现的（AQS 也是基于这玩意）。</p>
<pre><code class="language-java">public class StampedLock implements java.io.Serializable {
}
</code></pre>
<p><code>StampedLock</code> 提供了三种模式的读写控制模式：读锁、写锁和乐观读。</p>
<ul>
<li><strong>写锁</strong>：独占锁，一把锁只能被一个线程获得。当一个线程获取写锁后，其他请求读锁和写锁的线程必须等待。类似于 <code>ReentrantReadWriteLock</code> 的写锁，不过这里的写锁是不可重入的。</li>
<li><strong>读锁</strong> （悲观读）：共享锁，没有线程获取写锁的情况下，多个线程可以同时持有读锁。如果己经有线程持有写锁，则其他线程请求获取该读锁会被阻塞。类似于 <code>ReentrantReadWriteLock</code> 的读锁，不过这里的读锁是不可重入的。</li>
<li><strong>乐观读</strong>：允许多个线程获取乐观读以及读锁。同时允许一个写线程获取写锁。</li>
</ul>
<p>另外，<code>StampedLock</code> 还支持这三种锁在一定条件下进行相互转换 。</p>
<pre><code class="language-java">long tryConvertToWriteLock(long stamp){}
long tryConvertToReadLock(long stamp){}
long tryConvertToOptimisticRead(long stamp){}
</code></pre>
<p><code>StampedLock</code> 在获取锁的时候会返回一个 long 型的数据戳，该数据戳用于稍后的锁释放参数，如果返回的数据戳为 0 则表示锁获取失败。当前线程持有了锁再次获取锁还是会返回一个新的数据戳，这也是<code>StampedLock</code>不可重入的原因。</p>
<pre><code class="language-java">// 写锁
public long writeLock() {
    long s, next;  // bypass acquireWrite in fully unlocked case only
    return ((((s = state) &amp; ABITS) == 0L &amp;&amp;
             U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
            next : acquireWrite(false, 0L));
}
// 读锁
public long readLock() {
    long s = state, next;  // bypass acquireRead on common uncontended case
    return ((whead == wtail &amp;&amp; (s &amp; ABITS) &lt; RFULL &amp;&amp;
             U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
            next : acquireRead(false, 0L));
}
// 乐观读
public long tryOptimisticRead() {
    long s;
    return (((s = state) &amp; WBIT) == 0L) ? (s &amp; SBITS) : 0L;
}
</code></pre>
<h3><a id="stampedlock%E7%9A%84%E6%80%A7%E8%83%BD%E4%B8%BA%E4%BB%80%E4%B9%88%E6%9B%B4%E5%A5%BD%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>StampedLock 的性能为什么更好？</h3>
<p>相比于传统读写锁多出来的乐观读是<code>StampedLock</code>比 <code>ReadWriteLock</code> 性能更好的关键原因。<code>StampedLock</code> 的乐观读允许一个写线程获取写锁，所以不会导致所有写线程阻塞，也就是当读多写少的时候，写线程有机会获取写锁，减少了线程饥饿的问题，吞吐量大大提高。</p>
<h3><a id="stampedlock%E9%80%82%E5%90%88%E4%BB%80%E4%B9%88%E5%9C%BA%E6%99%AF%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>StampedLock 适合什么场景？</h3>
<p>和 <code>ReentrantReadWriteLock</code> 一样，<code>StampedLock</code> 同样适合读多写少的业务场景，可以作为 <code>ReentrantReadWriteLock</code>的替代品，性能更好。</p>
<p>不过，需要注意的是<code>StampedLock</code>不可重入，不支持条件变量 <code>Condition</code>，对中断操作支持也不友好（使用不当容易导致 CPU 飙升）。如果你需要用到 <code>ReentrantLock</code> 的一些高级性能，就不太建议使用 <code>StampedLock</code> 了。</p>
<p>另外，<code>StampedLock</code> 性能虽好，但使用起来相对比较麻烦，一旦使用不当，就会出现生产问题。强烈建议你在使用<code>StampedLock</code> 之前，看看 <a href="https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/StampedLock.html">StampedLock 官方文档中的案例</a>。</p>
<h3><a id="stampedlock%E7%9A%84%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E4%BA%86%E8%A7%A3%E5%90%97%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>StampedLock 的底层原理了解吗？</h3>
<p><code>StampedLock</code> 不是直接实现 <code>Lock</code>或 <code>ReadWriteLock</code>接口，而是基于 <strong>CLH 锁</strong> 实现的（AQS 也是基于这玩意），CLH 锁是对自旋锁的一种改良，是一种隐式的链表队列。<code>StampedLock</code> 通过 CLH 队列进行线程的管理，通过同步状态值 <code>state</code> 来表示锁的状态和类型。</p>
<p><code>StampedLock</code> 的原理和 AQS 原理比较类似，这里就不详细介绍了，感兴趣的可以看看下面这两篇文章：</p>
<ul>
<li><a href="https://javaguide.cn/java/concurrent/aqs.html">AQS 详解</a></li>
<li><a href="https://segmentfault.com/a/1190000015808032">StampedLock 底层原理分析</a></li>
</ul>
<p>如果你只是准备面试的话，建议多花点精力搞懂 AQS 原理即可，<code>StampedLock</code> 底层原理在面试中遇到的概率非常小。</p>
<h2><a id="atomic%E5%8E%9F%E5%AD%90%E7%B1%BB" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Atomic 原子类</h2>
<p>Atomic 原子类部分的内容我单独写了一篇文章来总结：<a href="17419999975836.html">Atomic 原子类总结</a>。</p>
<h2><a id="%E5%8F%82%E8%80%83" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>参考</h2>
<ul>
<li>《深入理解 Java 虚拟机》</li>
<li>《实战 Java 高并发程序设计》</li>
<li>Guide to the Volatile Keyword in Java - Baeldung：<a href="https://www.baeldung.com/java-volatile">https://www.baeldung.com/java-volatile</a></li>
<li>不可不说的 Java“锁”事 - 美团技术团队：<a href="https://tech.meituan.com/2018/11/15/java-lock.html">https://tech.meituan.com/2018/11/15/java-lock.html</a></li>
<li>在 ReadWriteLock 类中读锁为什么不能升级为写锁？：<a href="https://cloud.tencent.com/developer/article/1176230">https://cloud.tencent.com/developer/article/1176230</a></li>
<li>高性能解决线程饥饿的利器 StampedLock：<a href="https://mp.weixin.qq.com/s/2Acujjr4BHIhlFsCLGwYSg">https://mp.weixin.qq.com/s/2Acujjr4BHIhlFsCLGwYSg</a></li>
<li>理解 Java 中的 ThreadLocal - 技术小黑屋：<a href="https://droidyue.com/blog/2016/03/13/learning-threadlocal-in-java/">https://droidyue.com/blog/2016/03/13/learning-threadlocal-in-java/</a></li>
<li>ThreadLocal (Java Platform SE 8 ) - Oracle Help Center：<a href="https://docs.oracle.com/javase/8/docs/api/java/lang/ThreadLocal.html">https://docs.oracle.com/javase/8/docs/api/java/lang/ThreadLocal.html</a></li>
</ul>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[02-Java集合常见面试题总结(下)]]></title>
    <link href="https://huanglei.work/17420847480580.html"/>
    <updated>2025-03-16T08:25:48+08:00</updated>
    <id>https://huanglei.work/17420847480580.html</id>
    <content type="html"><![CDATA[
<ul>
<li><a href="#map%EF%BC%88%E9%87%8D%E8%A6%81%EF%BC%89">Map（重要）</a>
<ul>
<li><a href="#hashmap%E5%92%8C-hashtable%E7%9A%84%E5%8C%BA%E5%88%AB">HashMap 和 Hashtable 的区别</a></li>
<li><a href="#hashmap%E5%92%8C-hashset%E5%8C%BA%E5%88%AB">HashMap 和 HashSet 区别</a></li>
<li><a href="#hashmap%E5%92%8C-treemap%E5%8C%BA%E5%88%AB">HashMap 和 TreeMap 区别</a></li>
<li><a href="#hashset%E5%A6%82%E4%BD%95%E6%A3%80%E6%9F%A5%E9%87%8D%E5%A4%8D">HashSet 如何检查重复?</a></li>
<li><a href="#hashmap%E7%9A%84%E5%BA%95%E5%B1%82%E5%AE%9E%E7%8E%B0">HashMap 的底层实现</a>
<ul>
<li><a href="#jdk1-8%E4%B9%8B%E5%89%8D">JDK1.8 之前</a></li>
<li><a href="#jdk1-8%E4%B9%8B%E5%90%8E">JDK1.8 之后</a></li>
</ul>
</li>
<li><a href="#hashmap%E7%9A%84%E9%95%BF%E5%BA%A6%E4%B8%BA%E4%BB%80%E4%B9%88%E6%98%AF-2%E7%9A%84%E5%B9%82%E6%AC%A1%E6%96%B9">HashMap 的长度为什么是 2 的幂次方</a></li>
<li><a href="#hashmap%E5%A4%9A%E7%BA%BF%E7%A8%8B%E6%93%8D%E4%BD%9C%E5%AF%BC%E8%87%B4%E6%AD%BB%E5%BE%AA%E7%8E%AF%E9%97%AE%E9%A2%98">HashMap 多线程操作导致死循环问题</a></li>
<li><a href="#hashmap%E4%B8%BA%E4%BB%80%E4%B9%88%E7%BA%BF%E7%A8%8B%E4%B8%8D%E5%AE%89%E5%85%A8%EF%BC%9F">HashMap 为什么线程不安全？</a></li>
<li><a href="#hashmap%E5%B8%B8%E8%A7%81%E7%9A%84%E9%81%8D%E5%8E%86%E6%96%B9%E5%BC%8F">HashMap 常见的遍历方式?</a></li>
<li><a href="#concurrenthashmap%E5%92%8C-hashtable%E7%9A%84%E5%8C%BA%E5%88%AB">ConcurrentHashMap 和 Hashtable 的区别</a></li>
<li><a href="#concurrenthashmap%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E7%9A%84%E5%85%B7%E4%BD%93%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F%E5%BA%95%E5%B1%82%E5%85%B7%E4%BD%93%E5%AE%9E%E7%8E%B0">ConcurrentHashMap 线程安全的具体实现方式/底层具体实现</a>
<ul>
<li><a href="#jdk1-8%E4%B9%8B%E5%89%8D">JDK1.8 之前</a></li>
<li><a href="#jdk1-8%E4%B9%8B%E5%90%8E">JDK1.8 之后</a></li>
</ul>
</li>
<li><a href="#jdk-1-7%E5%92%8C-jdk-1-8%E7%9A%84-concurrenthashmap%E5%AE%9E%E7%8E%B0%E6%9C%89%E4%BB%80%E4%B9%88%E4%B8%8D%E5%90%8C%EF%BC%9F">JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同？</a></li>
<li><a href="#concurrenthashmap%E4%B8%BA%E4%BB%80%E4%B9%88-key%E5%92%8C-value%E4%B8%8D%E8%83%BD%E4%B8%BA-null%EF%BC%9F">ConcurrentHashMap 为什么 key 和 value 不能为 null？</a></li>
<li><a href="#concurrenthashmap%E8%83%BD%E4%BF%9D%E8%AF%81%E5%A4%8D%E5%90%88%E6%93%8D%E4%BD%9C%E7%9A%84%E5%8E%9F%E5%AD%90%E6%80%A7%E5%90%97%EF%BC%9F">ConcurrentHashMap 能保证复合操作的原子性吗？</a></li>
</ul>
</li>
<li><a href="#collections%E5%B7%A5%E5%85%B7%E7%B1%BB%EF%BC%88%E4%B8%8D%E9%87%8D%E8%A6%81%EF%BC%89">Collections 工具类（不重要）</a>
<ul>
<li><a href="#%E6%8E%92%E5%BA%8F%E6%93%8D%E4%BD%9C">排序操作</a></li>
<li><a href="#%E6%9F%A5%E6%89%BE%E6%9B%BF%E6%8D%A2%E6%93%8D%E4%BD%9C">查找,替换操作</a></li>
<li><a href="#%E5%90%8C%E6%AD%A5%E6%8E%A7%E5%88%B6">同步控制</a></li>
</ul>
</li>
</ul>
<h2><a id="map%EF%BC%88%E9%87%8D%E8%A6%81%EF%BC%89" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Map（重要）</h2>
<h3><a id="hashmap%E5%92%8C-hashtable%E7%9A%84%E5%8C%BA%E5%88%AB" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>HashMap 和 Hashtable 的区别</h3>
<ul>
<li><strong>线程是否安全：</strong> <code>HashMap</code> 是非线程安全的，<code>Hashtable</code> 是线程安全的,因为 <code>Hashtable</code> 内部的方法基本都经过<code>synchronized</code> 修饰。（如果你要保证线程安全的话就使用 <code>ConcurrentHashMap</code> 吧！）；</li>
<li><strong>效率：</strong> 因为线程安全的问题，<code>HashMap</code> 要比 <code>Hashtable</code> 效率高一点。另外，<code>Hashtable</code> 基本被淘汰，不要在代码中使用它；</li>
<li><strong>对 Null key 和 Null value 的支持：</strong> <code>HashMap</code> 可以存储 null 的 key 和 value，但 null 作为键只能有一个，null 作为值可以有多个；Hashtable 不允许有 null 键和 null 值，否则会抛出 <code>NullPointerException</code>。</li>
<li><strong>初始容量大小和每次扩充容量大小的不同：</strong> ① 创建时如果不指定容量初始值，<code>Hashtable</code> 默认的初始大小为 11，之后每次扩充，容量变为原来的 2n+1。<code>HashMap</code> 默认的初始化大小为 16。之后每次扩充，容量变为原来的 2 倍。② 创建时如果给定了容量初始值，那么 <code>Hashtable</code> 会直接使用你给定的大小，而 <code>HashMap</code> 会将其扩充为 2 的幂次方大小（<code>HashMap</code> 中的<code>tableSizeFor()</code>方法保证，下面给出了源代码）。也就是说 <code>HashMap</code> 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。</li>
<li><strong>底层数据结构：</strong> JDK1.8 以后的 <code>HashMap</code> 在解决哈希冲突时有了较大的变化，当链表长度大于阈值（默认为 8）时，将链表转化为红黑树（将链表转换成红黑树前会判断，如果当前数组的长度小于 64，那么会选择先进行数组扩容，而不是转换为红黑树），以减少搜索时间（后文中我会结合源码对这一过程进行分析）。<code>Hashtable</code> 没有这样的机制。</li>
<li><strong>哈希函数的实现</strong>：<code>HashMap</code> 对哈希值进行了高位和低位的混合扰动处理以减少冲突，而 <code>Hashtable</code> 直接使用键的 <code>hashCode()</code> 值。</li>
</ul>
<p><strong><code>HashMap</code> 中带有初始容量的构造函数：</strong></p>
<pre><code class="language-java">    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity &lt; 0)
            throw new IllegalArgumentException(&quot;Illegal initial capacity: &quot; +
                                               initialCapacity);
        if (initialCapacity &gt; MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor &lt;= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException(&quot;Illegal load factor: &quot; +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
     public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
</code></pre>
<p>下面这个方法保证了 <code>HashMap</code> 总是使用 2 的幂作为哈希表的大小。</p>
<pre><code class="language-java">/**
 * Returns a power of two size for the given target capacity.
 */
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n &gt;&gt;&gt; 1;
    n |= n &gt;&gt;&gt; 2;
    n |= n &gt;&gt;&gt; 4;
    n |= n &gt;&gt;&gt; 8;
    n |= n &gt;&gt;&gt; 16;
    return (n &lt; 0) ? 1 : (n &gt;= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
</code></pre>
<h3><a id="hashmap%E5%92%8C-hashset%E5%8C%BA%E5%88%AB" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>HashMap 和 HashSet 区别</h3>
<p>如果你看过 <code>HashSet</code> 源码的话就应该知道：<code>HashSet</code> 底层就是基于 <code>HashMap</code> 实现的。（<code>HashSet</code> 的源码非常非常少，因为除了 <code>clone()</code>、<code>writeObject()</code>、<code>readObject()</code>是 <code>HashSet</code> 自己不得不实现之外，其他方法都是直接调用 <code>HashMap</code> 中的方法。</p>
<table>
<thead>
<tr>
<th style="text-align: center"><code>HashMap</code></th>
<th style="text-align: center"><code>HashSet</code></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: center">实现了 <code>Map</code> 接口</td>
<td style="text-align: center">实现 <code>Set</code> 接口</td>
</tr>
<tr>
<td style="text-align: center">存储键值对</td>
<td style="text-align: center">仅存储对象</td>
</tr>
<tr>
<td style="text-align: center">调用 <code>put()</code>向 map 中添加元素</td>
<td style="text-align: center">调用 <code>add()</code>方法向 <code>Set</code> 中添加元素</td>
</tr>
<tr>
<td style="text-align: center"><code>HashMap</code> 使用键（Key）计算 <code>hashcode</code></td>
<td style="text-align: center"><code>HashSet</code> 使用成员对象来计算 <code>hashcode</code> 值，对于两个对象来说 <code>hashcode</code> 可能相同，所以<code>equals()</code>方法用来判断对象的相等性</td>
</tr>
</tbody>
</table>
<h3><a id="hashmap%E5%92%8C-treemap%E5%8C%BA%E5%88%AB" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>HashMap 和 TreeMap 区别</h3>
<p><code>TreeMap</code> 和<code>HashMap</code> 都继承自<code>AbstractMap</code> ，但是需要注意的是<code>TreeMap</code>它还实现了<code>NavigableMap</code>接口和<code>SortedMap</code> 接口。</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/71407aef-f57f-4aa2-8abc-747b53eca3da.png" alt="TreeMap 继承关系图" /></p>
<p>实现 <code>NavigableMap</code> 接口让 <code>TreeMap</code> 有了对集合内元素的搜索的能力。</p>
<p><code>NavigableMap</code> 接口提供了丰富的方法来探索和操作键值对:</p>
<ol>
<li><strong>定向搜索</strong>: <code>ceilingEntry()</code>, <code>floorEntry()</code>, <code>higherEntry()</code>和 <code>lowerEntry()</code> 等方法可以用于定位大于等于、小于等于、严格大于、严格小于给定键的最接近的键值对。</li>
<li><strong>子集操作</strong>: <code>subMap()</code>, <code>headMap()</code>和 <code>tailMap()</code> 方法可以高效地创建原集合的子集视图，而无需复制整个集合。</li>
<li><strong>逆序视图</strong>:<code>descendingMap()</code> 方法返回一个逆序的 <code>NavigableMap</code> 视图，使得可以反向迭代整个 <code>TreeMap</code>。</li>
<li><strong>边界操作</strong>: <code>firstEntry()</code>, <code>lastEntry()</code>, <code>pollFirstEntry()</code>和 <code>pollLastEntry()</code> 等方法可以方便地访问和移除元素。</li>
</ol>
<p>这些方法都是基于红黑树数据结构的属性实现的，红黑树保持平衡状态，从而保证了搜索操作的时间复杂度为 O(log n)，这让 <code>TreeMap</code> 成为了处理有序集合搜索问题的强大工具。</p>
<p>实现<code>SortedMap</code>接口让 <code>TreeMap</code> 有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序，不过我们也可以指定排序的比较器。示例代码如下：</p>
<pre><code class="language-java">/**
 * @author shuang.kou
 * @createTime 2020年06月15日 17:02:00
 */
public class Person {
    private Integer age;

    public Person(Integer age) {
        this.age = age;
    }

    public Integer getAge() {
        return age;
    }


    public static void main(String[] args) {
        TreeMap&lt;Person, String&gt; treeMap = new TreeMap&lt;&gt;(new Comparator&lt;Person&gt;() {
            @Override
            public int compare(Person person1, Person person2) {
                int num = person1.getAge() - person2.getAge();
                return Integer.compare(num, 0);
            }
        });
        treeMap.put(new Person(3), &quot;person1&quot;);
        treeMap.put(new Person(18), &quot;person2&quot;);
        treeMap.put(new Person(35), &quot;person3&quot;);
        treeMap.put(new Person(16), &quot;person4&quot;);
        treeMap.entrySet().stream().forEach(personStringEntry -&gt; {
            System.out.println(personStringEntry.getValue());
        });
    }
}
</code></pre>
<p>输出:</p>
<pre><code class="language-plain">person1
person4
person2
person3
</code></pre>
<p>可以看出，<code>TreeMap</code> 中的元素已经是按照 <code>Person</code> 的 age 字段的升序来排列了。</p>
<p>上面，我们是通过传入匿名内部类的方式实现的，你可以将代码替换成 Lambda 表达式实现的方式：</p>
<pre><code class="language-java">TreeMap&lt;Person, String&gt; treeMap = new TreeMap&lt;&gt;((person1, person2) -&gt; {
  int num = person1.getAge() - person2.getAge();
  return Integer.compare(num, 0);
});
</code></pre>
<p><strong>综上，相比于<code>HashMap</code>来说， <code>TreeMap</code> 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。</strong></p>
<h3><a id="hashset%E5%A6%82%E4%BD%95%E6%A3%80%E6%9F%A5%E9%87%8D%E5%A4%8D" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>HashSet 如何检查重复?</h3>
<p>以下内容摘自我的 Java 启蒙书《Head first java》第二版：</p>
<blockquote>
<p>当你把对象加入<code>HashSet</code>时，<code>HashSet</code> 会先计算对象的<code>hashcode</code>值来判断对象加入的位置，同时也会与其他加入的对象的 <code>hashcode</code> 值作比较，如果没有相符的 <code>hashcode</code>，<code>HashSet</code> 会假设对象没有重复出现。但是如果发现有相同 <code>hashcode</code> 值的对象，这时会调用<code>equals()</code>方法来检查 <code>hashcode</code> 相等的对象是否真的相同。如果两者相同，<code>HashSet</code> 就不会让加入操作成功。</p>
</blockquote>
<p>在 JDK1.8 中，<code>HashSet</code>的<code>add()</code>方法只是简单的调用了<code>HashMap</code>的<code>put()</code>方法，并且判断了一下返回值以确保是否有重复元素。直接看一下<code>HashSet</code>中的源码：</p>
<pre><code class="language-java">// Returns: true if this set did not already contain the specified element
// 返回值：当 set 中没有包含 add 的元素时返回真
public boolean add(E e) {
        return map.put(e, PRESENT)==null;
}
</code></pre>
<p>而在<code>HashMap</code>的<code>putVal()</code>方法中也能看到如下说明：</p>
<pre><code class="language-java">// Returns : previous value, or null if none
// 返回值：如果插入位置没有元素返回null，否则返回上一个元素
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
...
}
</code></pre>
<p>也就是说，在 JDK1.8 中，实际上无论<code>HashSet</code>中是否已经存在了某元素，<code>HashSet</code>都会直接插入，只是会在<code>add()</code>方法的返回值处告诉我们插入前是否存在相同元素。</p>
<h3><a id="hashmap%E7%9A%84%E5%BA%95%E5%B1%82%E5%AE%9E%E7%8E%B0" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>HashMap 的底层实现</h3>
<h4><a id="jdk1-8%E4%B9%8B%E5%89%8D" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>JDK1.8 之前</h4>
<p>JDK1.8 之前 <code>HashMap</code> 底层是 <strong>数组和链表</strong> 结合在一起使用也就是 <strong>链表散列</strong>。HashMap 通过 key 的 <code>hashcode</code> 经过扰动函数处理过后得到 hash 值，然后通过 <code>(n - 1) &amp; hash</code> 判断当前元素存放的位置（这里的 n 指的是数组的长度），如果当前位置存在元素的话，就判断该元素与要存入的元素的 hash 值以及 key 是否相同，如果相同的话，直接覆盖，不相同就通过拉链法解决冲突。</p>
<p><code>HashMap</code> 中的扰动函数（<code>hash</code> 方法）是用来优化哈希值的分布。通过对原始的 <code>hashCode()</code> 进行额外处理，扰动函数可以减小由于糟糕的 <code>hashCode()</code> 实现导致的碰撞，从而提高数据的分布均匀性。</p>
<p><strong>JDK 1.8 HashMap 的 hash 方法源码:</strong></p>
<p>JDK 1.8 的 hash 方法 相比于 JDK 1.7 hash 方法更加简化，但是原理不变。</p>
<pre><code class="language-java">    static final int hash(Object key) {
      int h;
      // key.hashCode()：返回散列值也就是hashcode
      // ^：按位异或
      // &gt;&gt;&gt;:无符号右移，忽略符号位，空位都以0补齐
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h &gt;&gt;&gt; 16);
  }
</code></pre>
<p>对比一下 JDK1.7 的 HashMap 的 hash 方法源码.</p>
<pre><code class="language-java">static int hash(int h) {
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).

    h ^= (h &gt;&gt;&gt; 20) ^ (h &gt;&gt;&gt; 12);
    return h ^ (h &gt;&gt;&gt; 7) ^ (h &gt;&gt;&gt; 4);
}
</code></pre>
<p>相比于 JDK1.8 的 hash 方法 ，JDK 1.7 的 hash 方法的性能会稍差一点点，因为毕竟扰动了 4 次。</p>
<p>所谓 <strong>“拉链法”</strong> 就是：将链表和数组相结合。也就是说创建一个链表数组，数组中每一格就是一个链表。若遇到哈希冲突，则将冲突的值加到链表中即可。</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/b95e0c67-71fd-4407-8c1a-4214524a225b.png" alt="jdk1.8 之前的内部结构-HashMap" /></p>
<h4><a id="jdk1-8%E4%B9%8B%E5%90%8E" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>JDK1.8 之后</h4>
<p>相比于之前的版本， JDK1.8 之后在解决哈希冲突时有了较大的变化，当链表长度大于阈值（默认为 8）（将链表转换成红黑树前会判断，如果当前数组的长度小于 64，那么会选择先进行数组扩容，而不是转换为红黑树）时，将链表转化为红黑树。</p>
<p>这样做的目的是减少搜索时间：链表的查询效率为 O(n)（n 是链表的长度），红黑树是一种自平衡二叉搜索树，其查询效率为 O(log n)。当链表较短时，O(n) 和 O(log n) 的性能差异不明显。但当链表变长时，查询性能会显著下降。</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/a50b2bd1-18ab-47e4-a934-161152fb2b65.png" alt="jdk1.8之后的内部结构-HashMap" /></p>
<p><strong>为什么优先扩容而非直接转为红黑树？</strong></p>
<p>数组扩容能减少哈希冲突的发生概率（即将元素重新分散到新的、更大的数组中），这在多数情况下比直接转换为红黑树更高效。</p>
<p>红黑树需要保持自平衡，维护成本较高。并且，过早引入红黑树反而会增加复杂度。</p>
<p><strong>为什么选择阈值 8 和 64？</strong></p>
<ol>
<li>泊松分布表明，链表长度达到 8 的概率极低（小于千万分之一）。在绝大多数情况下，链表长度都不会超过 8。阈值设置为 8，可以保证性能和空间效率的平衡。</li>
<li>数组长度阈值 64 同样是经过实践验证的经验值。在小数组中扩容成本低，优先扩容可以避免过早引入红黑树。数组大小达到 64 时，冲突概率较高，此时红黑树的性能优势开始显现。</li>
</ol>
<blockquote>
<p>TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷，因为二叉查找树在某些情况下会退化成一个线性结构。</p>
</blockquote>
<p>我们来结合源码分析一下 <code>HashMap</code> 链表到红黑树的转换。</p>
<p><strong>1、 <code>putVal</code> 方法中执行链表转红黑树的判断逻辑。</strong></p>
<p>链表的长度大于 8 的时候，就执行 <code>treeifyBin</code> （转换红黑树）的逻辑。</p>
<pre><code class="language-java">// 遍历链表
for (int binCount = 0; ; ++binCount) {
    // 遍历到链表最后一个节点
    if ((e = p.next) == null) {
        p.next = newNode(hash, key, value, null);
        // 如果链表元素个数大于TREEIFY_THRESHOLD（8）
        if (binCount &gt;= TREEIFY_THRESHOLD - 1) // -1 for 1st
            // 红黑树转换（并不会直接转换成红黑树）
            treeifyBin(tab, hash);
        break;
    }
    if (e.hash == hash &amp;&amp;
        ((k = e.key) == key || (key != null &amp;&amp; key.equals(k))))
        break;
    p = e;
}
</code></pre>
<p><strong>2、<code>treeifyBin</code> 方法中判断是否真的转换为红黑树。</strong></p>
<pre><code class="language-java">final void treeifyBin(Node&lt;K,V&gt;[] tab, int hash) {
    int n, index; Node&lt;K,V&gt; e;
    // 判断当前数组的长度是否小于 64
    if (tab == null || (n = tab.length) &lt; MIN_TREEIFY_CAPACITY)
        // 如果当前数组的长度小于 64，那么会选择先进行数组扩容
        resize();
    else if ((e = tab[index = (n - 1) &amp; hash]) != null) {
        // 否则才将列表转换为红黑树

        TreeNode&lt;K,V&gt; hd = null, tl = null;
        do {
            TreeNode&lt;K,V&gt; p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}
</code></pre>
<p>将链表转换成红黑树前会判断，如果当前数组的长度小于 64，那么会选择先进行数组扩容，而不是转换为红黑树。</p>
<h3><a id="hashmap%E7%9A%84%E9%95%BF%E5%BA%A6%E4%B8%BA%E4%BB%80%E4%B9%88%E6%98%AF-2%E7%9A%84%E5%B9%82%E6%AC%A1%E6%96%B9" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>HashMap 的长度为什么是 2 的幂次方</h3>
<p>为了让 <code>HashMap</code> 存取高效并减少碰撞，我们需要确保数据尽量均匀分布。哈希值在 Java 中通常使用 <code>int</code> 表示，其范围是 <code>-2147483648 ~ 2147483647</code>前后加起来大概 40 亿的映射空间，只要哈希函数映射得比较均匀松散，一般应用是很难出现碰撞的。但是，问题是一个 40 亿长度的数组，内存是放不下的。所以，这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算，得到的余数才能用来要存放的位置也就是对应的数组下标。</p>
<p><strong>这个算法应该如何设计呢？</strong></p>
<p>我们首先可能会想到采用 % 取余的操作来实现。但是，重点来了：“<strong>取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&amp;)操作</strong>（也就是说 <code>hash%length==hash&amp;(length-1)</code> 的前提是 length 是 2 的 n 次方）。” 并且，<strong>采用二进制位操作 &amp; 相对于 % 能够提高运算效率</strong>。</p>
<p>除了上面所说的位运算比取余效率高之外，我觉得更重要的一个原因是：<strong>长度是 2 的幂次方，可以让 <code>HashMap</code> 在扩容的时候更均匀</strong>。例如:</p>
<ul>
<li>length = 8 时，length - 1 = 7 的二进制位<code>0111</code></li>
<li>length = 16 时，length - 1 = 15 的二进制位<code>1111</code></li>
</ul>
<p>这时候原本存在 <code>HashMap</code> 中的元素计算新的数组位置时 <code>hash&amp;(length-1)</code>，取决 hash 的第四个二进制位（从右数），会出现两种情况：</p>
<ol>
<li>第四个二进制位为 0，数组位置不变，也就是说当前元素在新数组和旧数组的位置相同。</li>
<li>第四个二进制位为 1，数组位置在新数组扩容之后的那一部分。</li>
</ol>
<p>这里列举一个例子：</p>
<pre><code class="language-plain">假设有一个元素的哈希值为 10101100

旧数组元素位置计算：
hash        = 10101100
length - 1  = 00000111
&amp; -----------------
index       = 00000100  (4)

新数组元素位置计算：
hash        = 10101100
length - 1  = 00001111
&amp; -----------------
index       = 00001100  (12)

看第四位（从右数）：
1.高位为 0：位置不变。
2.高位为 1：移动到新位置（原索引位置+原容量）。
</code></pre>
<p>⚠️注意：这里列举的场景看的是第四个二进制位，更准确点来说看的是高位（从右数），例如 <code>length = 32</code> 时，<code>length - 1 = 31</code>，二进制为 <code>11111</code>，这里看的就是第五个二进制位。</p>
<p>也就是说扩容之后，在旧数组元素 hash 值比较均匀（至于 hash 值均不均匀，取决于前面讲的对象的 <code>hashcode()</code> 方法和扰动函数）的情况下，新数组元素也会被分配的比较均匀，最好的情况是会有一半在新数组的前半部分，一半在新数组后半部分。</p>
<p>这样也使得扩容机制变得简单和高效，扩容后只需检查哈希值高位的变化来决定元素的新位置，要么位置不变（高位为 0），要么就是移动到新位置（高位为 1，原索引位置+原容量）。</p>
<p>最后，简单总结一下 <code>HashMap</code> 的长度是 2 的幂次方的原因：</p>
<ol>
<li>位运算效率更高：位运算(&amp;)比取余运算(%)更高效。当长度为 2 的幂次方时，<code>hash % length</code> 等价于 <code>hash &amp; (length - 1)</code>。</li>
<li>可以更好地保证哈希值的均匀分布：扩容之后，在旧数组元素 hash 值比较均匀的情况下，新数组元素也会被分配的比较均匀，最好的情况是会有一半在新数组的前半部分，一半在新数组后半部分。</li>
<li>扩容机制变得简单和高效：扩容后只需检查哈希值高位的变化来决定元素的新位置，要么位置不变（高位为 0），要么就是移动到新位置（高位为 1，原索引位置+原容量）。</li>
</ol>
<h3><a id="hashmap%E5%A4%9A%E7%BA%BF%E7%A8%8B%E6%93%8D%E4%BD%9C%E5%AF%BC%E8%87%B4%E6%AD%BB%E5%BE%AA%E7%8E%AF%E9%97%AE%E9%A2%98" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>HashMap 多线程操作导致死循环问题</h3>
<p>JDK1.7 及之前版本的 <code>HashMap</code> 在多线程环境下扩容操作可能存在死循环问题，这是由于当一个桶位中有多个元素需要进行扩容时，多个线程同时对链表进行操作，头插法可能会导致链表中的节点指向错误的位置，从而形成一个环形链表，进而使得查询元素的操作陷入死循环无法结束。</p>
<p>为了解决这个问题，JDK1.8 版本的 HashMap 采用了尾插法而不是头插法来避免链表倒置，使得插入的节点永远都是放在链表的末尾，避免了链表中的环形结构。但是还是不建议在多线程下使用 <code>HashMap</code>，因为多线程下使用 <code>HashMap</code> 还是会存在数据覆盖的问题。并发环境下，推荐使用 <code>ConcurrentHashMap</code> 。</p>
<p>一般面试中这样介绍就差不多，不需要记各种细节，个人觉得也没必要记。如果想要详细了解 <code>HashMap</code> 扩容导致死循环问题，可以看看耗子叔的这篇文章：<a href="https://coolshell.cn/articles/9606.html">Java HashMap 的死循环</a>。</p>
<h3><a id="hashmap%E4%B8%BA%E4%BB%80%E4%B9%88%E7%BA%BF%E7%A8%8B%E4%B8%8D%E5%AE%89%E5%85%A8%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>HashMap 为什么线程不安全？</h3>
<p>JDK1.7 及之前版本，在多线程环境下，<code>HashMap</code> 扩容时会造成死循环和数据丢失的问题。</p>
<p>数据丢失这个在 JDK1.7 和 JDK 1.8 中都存在，这里以 JDK 1.8 为例进行介绍。</p>
<p>JDK 1.8 后，在 <code>HashMap</code> 中，多个键值对可能会被分配到同一个桶（bucket），并以链表或红黑树的形式存储。多个线程对 <code>HashMap</code> 的 <code>put</code> 操作会导致线程不安全，具体来说会有数据覆盖的风险。</p>
<p>举个例子：</p>
<ul>
<li>两个线程 1,2 同时进行 put 操作，并且发生了哈希冲突（hash 函数计算出的插入下标是相同的）。</li>
<li>不同的线程可能在不同的时间片获得 CPU 执行的机会，当前线程 1 执行完哈希冲突判断后，由于时间片耗尽挂起。线程 2 先完成了插入操作。</li>
<li>随后，线程 1 获得时间片，由于之前已经进行过 hash 碰撞的判断，所有此时会直接进行插入，这就导致线程 2 插入的数据被线程 1 覆盖了。</li>
</ul>
<pre><code class="language-java">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) {
    // ...
    // 判断是否出现 hash 碰撞
    // (n - 1) &amp; hash 确定元素存放在哪个桶中，桶为空，新生成结点放入桶中(此时，这个结点是放在数组中)
    if ((p = tab[i = (n - 1) &amp; hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 桶中已经存在元素（处理hash冲突）
    else {
    // ...
}
</code></pre>
<p>还有一种情况是这两个线程同时 <code>put</code> 操作导致 <code>size</code> 的值不正确，进而导致数据覆盖的问题：</p>
<ol>
<li>线程 1 执行 <code>if(++size &gt; threshold)</code> 判断时，假设获得 <code>size</code> 的值为 10，由于时间片耗尽挂起。</li>
<li>线程 2 也执行 <code>if(++size &gt; threshold)</code> 判断，获得 <code>size</code> 的值也为 10，并将元素插入到该桶位中，并将 <code>size</code> 的值更新为 11。</li>
<li>随后，线程 1 获得时间片，它也将元素放入桶位中，并将 size 的值更新为 11。</li>
<li>线程 1、2 都执行了一次 <code>put</code> 操作，但是 <code>size</code> 的值只增加了 1，也就导致实际上只有一个元素被添加到了 <code>HashMap</code> 中。</li>
</ol>
<pre><code class="language-java">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) {
    // ...
    // 实际大小大于阈值则扩容
    if (++size &gt; threshold)
        resize();
    // 插入后回调
    afterNodeInsertion(evict);
    return null;
}
</code></pre>
<h3><a id="hashmap%E5%B8%B8%E8%A7%81%E7%9A%84%E9%81%8D%E5%8E%86%E6%96%B9%E5%BC%8F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>HashMap 常见的遍历方式?</h3>
<p><a href="https://mp.weixin.qq.com/s/zQBN3UvJDhRTKP6SzcZFKw">HashMap 的 7 种遍历方式与性能分析！</a></p>
<p><strong>🐛 修正（参见：<a href="https://github.com/Snailclimb/JavaGuide/issues/1411">issue#1411</a>）</strong>：</p>
<p>这篇文章对于 parallelStream 遍历方式的性能分析有误，先说结论：<strong>存在阻塞时 parallelStream 性能最高, 非阻塞时 parallelStream 性能最低</strong> 。</p>
<p>当遍历不存在阻塞时, parallelStream 的性能是最低的：</p>
<pre><code class="language-plain">Benchmark               Mode  Cnt     Score      Error  Units
Test.entrySet           avgt    5   288.651 ±   10.536  ns/op
Test.keySet             avgt    5   584.594 ±   21.431  ns/op
Test.lambda             avgt    5   221.791 ±   10.198  ns/op
Test.parallelStream     avgt    5  6919.163 ± 1116.139  ns/op
</code></pre>
<p>加入阻塞代码<code>Thread.sleep(10)</code>后, parallelStream 的性能才是最高的:</p>
<pre><code class="language-plain">Benchmark               Mode  Cnt           Score          Error  Units
Test.entrySet           avgt    5  1554828440.000 ± 23657748.653  ns/op
Test.keySet             avgt    5  1550612500.000 ±  6474562.858  ns/op
Test.lambda             avgt    5  1551065180.000 ± 19164407.426  ns/op
Test.parallelStream     avgt    5   186345456.667 ±  3210435.590  ns/op
</code></pre>
<h3><a id="concurrenthashmap%E5%92%8C-hashtable%E7%9A%84%E5%8C%BA%E5%88%AB" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>ConcurrentHashMap 和 Hashtable 的区别</h3>
<p><code>ConcurrentHashMap</code> 和 <code>Hashtable</code> 的区别主要体现在实现线程安全的方式上不同。</p>
<ul>
<li><strong>底层数据结构：</strong> JDK1.7 的 <code>ConcurrentHashMap</code> 底层采用 <strong>分段的数组+链表</strong> 实现，JDK1.8 采用的数据结构跟 <code>HashMap1.8</code> 的结构一样，数组+链表/红黑二叉树。<code>Hashtable</code> 和 JDK1.8 之前的 <code>HashMap</code> 的底层数据结构类似都是采用 <strong>数组+链表</strong> 的形式，数组是 HashMap 的主体，链表则是主要为了解决哈希冲突而存在的；</li>
<li><strong>实现线程安全的方式（重要）：</strong>
<ul>
<li>在 JDK1.7 的时候，<code>ConcurrentHashMap</code> 对整个桶数组进行了分割分段(<code>Segment</code>，分段锁)，每一把锁只锁容器其中一部分数据（下面有示意图），多线程访问容器里不同数据段的数据，就不会存在锁竞争，提高并发访问率。</li>
<li>到了 JDK1.8 的时候，<code>ConcurrentHashMap</code> 已经摒弃了 <code>Segment</code> 的概念，而是直接用 <code>Node</code> 数组+链表+红黑树的数据结构来实现，并发控制使用 <code>synchronized</code> 和 CAS 来操作。（JDK1.6 以后 <code>synchronized</code> 锁做了很多优化） 整个看起来就像是优化过且线程安全的 <code>HashMap</code>，虽然在 JDK1.8 中还能看到 <code>Segment</code> 的数据结构，但是已经简化了属性，只是为了兼容旧版本；</li>
<li><strong><code>Hashtable</code>(同一把锁)</strong> :使用 <code>synchronized</code> 来保证线程安全，效率非常低下。当一个线程访问同步方法时，其他线程也访问同步方法，可能会进入阻塞或轮询状态，如使用 put 添加元素，另一个线程不能使用 put 添加元素，也不能使用 get，竞争会越来越激烈效率越低。</li>
</ul>
</li>
</ul>
<p>下面，我们再来看看两者底层数据结构的对比图。</p>
<p><strong>Hashtable</strong> :</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/b95e0c67-71fd-4407-8c1a-4214524a225b.png" alt="Hashtable 的内部结构" /></p>
<p style="text-align:right;font-size:13px;color:gray">https://www.cnblogs.com/chengxiao/p/6842045.html></p>
<p><strong>JDK1.7 的 ConcurrentHashMap</strong>：</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/19d63a91-63b6-4a5c-9ca6-e7560a94b68e.png" alt="Java7 ConcurrentHashMap 存储结构" /></p>
<p><code>ConcurrentHashMap</code> 是由 <code>Segment</code> 数组结构和 <code>HashEntry</code> 数组结构组成。</p>
<p><code>Segment</code> 数组中的每个元素包含一个 <code>HashEntry</code> 数组，每个 <code>HashEntry</code> 数组属于链表结构。</p>
<p><strong>JDK1.8 的 ConcurrentHashMap</strong>：</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/7a634007-edd2-497c-bf67-87256d6c8432.png" alt="Java8 ConcurrentHashMap 存储结构" /></p>
<p>JDK1.8 的 <code>ConcurrentHashMap</code> 不再是 <strong>Segment 数组 + HashEntry 数组 + 链表</strong>，而是 <strong>Node 数组 + 链表 / 红黑树</strong>。不过，Node 只能用于链表的情况，红黑树的情况需要使用 <strong><code>TreeNode</code></strong>。当冲突链表达到一定长度时，链表会转换成红黑树。</p>
<p><code>TreeNode</code>是存储红黑树节点，被<code>TreeBin</code>包装。<code>TreeBin</code>通过<code>root</code>属性维护红黑树的根结点，因为红黑树在旋转的时候，根结点可能会被它原来的子节点替换掉，在这个时间点，如果有其他线程要写这棵红黑树就会发生线程不安全问题，所以在 <code>ConcurrentHashMap</code> 中<code>TreeBin</code>通过<code>waiter</code>属性维护当前使用这棵红黑树的线程，来防止其他线程的进入。</p>
<pre><code class="language-java">static final class TreeBin&lt;K,V&gt; extends Node&lt;K,V&gt; {
        TreeNode&lt;K,V&gt; root;
        volatile TreeNode&lt;K,V&gt; first;
        volatile Thread waiter;
        volatile int lockState;
        // values for lockState
        static final int WRITER = 1; // set while holding write lock
        static final int WAITER = 2; // set when waiting for write lock
        static final int READER = 4; // increment value for setting read lock
...
}
</code></pre>
<h3><a id="concurrenthashmap%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E7%9A%84%E5%85%B7%E4%BD%93%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F%E5%BA%95%E5%B1%82%E5%85%B7%E4%BD%93%E5%AE%9E%E7%8E%B0" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>ConcurrentHashMap 线程安全的具体实现方式/底层具体实现</h3>
<h4><a id="jdk1-8%E4%B9%8B%E5%89%8D" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>JDK1.8 之前</h4>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/19d63a91-63b6-4a5c-9ca6-e7560a94b68e.png" alt="Java7 ConcurrentHashMap 存储结构" /></p>
<p>首先将数据分为一段一段（这个“段”就是 <code>Segment</code>）的存储，然后给每一段数据配一把锁，当一个线程占用锁访问其中一个段数据时，其他段的数据也能被其他线程访问。</p>
<p><strong><code>ConcurrentHashMap</code> 是由 <code>Segment</code> 数组结构和 <code>HashEntry</code> 数组结构组成</strong>。</p>
<p><code>Segment</code> 继承了 <code>ReentrantLock</code>,所以 <code>Segment</code> 是一种可重入锁，扮演锁的角色。<code>HashEntry</code> 用于存储键值对数据。</p>
<pre><code class="language-java">static class Segment&lt;K,V&gt; extends ReentrantLock implements Serializable {
}
</code></pre>
<p>一个 <code>ConcurrentHashMap</code> 里包含一个 <code>Segment</code> 数组，<code>Segment</code> 的个数一旦<strong>初始化就不能改变</strong>。 <code>Segment</code> 数组的大小默认是 16，也就是说默认可以同时支持 16 个线程并发写。</p>
<p><code>Segment</code> 的结构和 <code>HashMap</code> 类似，是一种数组和链表结构，一个 <code>Segment</code> 包含一个 <code>HashEntry</code> 数组，每个 <code>HashEntry</code> 是一个链表结构的元素，每个 <code>Segment</code> 守护着一个 <code>HashEntry</code> 数组里的元素，当对 <code>HashEntry</code> 数组的数据进行修改时，必须首先获得对应的 <code>Segment</code> 的锁。也就是说，对同一 <code>Segment</code> 的并发写入会被阻塞，不同 <code>Segment</code> 的写入是可以并发执行的。</p>
<h4><a id="jdk1-8%E4%B9%8B%E5%90%8E" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>JDK1.8 之后</h4>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/7a634007-edd2-497c-bf67-87256d6c8432.png" alt="Java8 ConcurrentHashMap 存储结构" /></p>
<p>Java 8 几乎完全重写了 <code>ConcurrentHashMap</code>，代码量从原来 Java 7 中的 1000 多行，变成了现在的 6000 多行。</p>
<p><code>ConcurrentHashMap</code> 取消了 <code>Segment</code> 分段锁，采用 <code>Node + CAS + synchronized</code> 来保证并发安全。数据结构跟 <code>HashMap</code> 1.8 的结构类似，数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值（8）时将链表（寻址时间复杂度为 O(N)）转换为红黑树（寻址时间复杂度为 O(log(N))）。</p>
<p>Java 8 中，锁粒度更细，<code>synchronized</code> 只锁定当前链表或红黑二叉树的首节点，这样只要 hash 不冲突，就不会产生并发，就不会影响其他 Node 的读写，效率大幅提升。</p>
<h3><a id="jdk-1-7%E5%92%8C-jdk-1-8%E7%9A%84-concurrenthashmap%E5%AE%9E%E7%8E%B0%E6%9C%89%E4%BB%80%E4%B9%88%E4%B8%8D%E5%90%8C%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同？</h3>
<ul>
<li><strong>线程安全实现方式</strong>：JDK 1.7 采用 <code>Segment</code> 分段锁来保证安全， <code>Segment</code> 是继承自 <code>ReentrantLock</code>。JDK1.8 放弃了 <code>Segment</code> 分段锁的设计，采用 <code>Node + CAS + synchronized</code> 保证线程安全，锁粒度更细，<code>synchronized</code> 只锁定当前链表或红黑二叉树的首节点。</li>
<li><strong>Hash 碰撞解决方法</strong> : JDK 1.7 采用拉链法，JDK1.8 采用拉链法结合红黑树（链表长度超过一定阈值时，将链表转换为红黑树）。</li>
<li><strong>并发度</strong>：JDK 1.7 最大并发度是 Segment 的个数，默认是 16。JDK 1.8 最大并发度是 Node 数组的大小，并发度更大。</li>
</ul>
<h3><a id="concurrenthashmap%E4%B8%BA%E4%BB%80%E4%B9%88-key%E5%92%8C-value%E4%B8%8D%E8%83%BD%E4%B8%BA-null%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>ConcurrentHashMap 为什么 key 和 value 不能为 null？</h3>
<p><code>ConcurrentHashMap</code> 的 key 和 value 不能为 null 主要是为了避免二义性。null 是一个特殊的值，表示没有对象或没有引用。如果你用 null 作为键，那么你就无法区分这个键是否存在于 <code>ConcurrentHashMap</code> 中，还是根本没有这个键。同样，如果你用 null 作为值，那么你就无法区分这个值是否是真正存储在 <code>ConcurrentHashMap</code> 中的，还是因为找不到对应的键而返回的。</p>
<p>拿 get 方法取值来说，返回的结果为 null 存在两种情况：</p>
<ul>
<li>值没有在集合中 ；</li>
<li>值本身就是 null。</li>
</ul>
<p>这也就是二义性的由来。</p>
<p>具体可以参考 <a href="17420847480876.html">ConcurrentHashMap 源码分析</a> 。</p>
<p>多线程环境下，存在一个线程操作该 <code>ConcurrentHashMap</code> 时，其他的线程将该 <code>ConcurrentHashMap</code> 修改的情况，所以无法通过 <code>containsKey(key)</code> 来判断否存在这个键值对，也就没办法解决二义性问题了。</p>
<p>与此形成对比的是，<code>HashMap</code> 可以存储 null 的 key 和 value，但 null 作为键只能有一个，null 作为值可以有多个。如果传入 null 作为参数，就会返回 hash 值为 0 的位置的值。单线程环境下，不存在一个线程操作该 HashMap 时，其他的线程将该 <code>HashMap</code> 修改的情况，所以可以通过 <code>contains(key)</code>来做判断是否存在这个键值对，从而做相应的处理，也就不存在二义性问题。</p>
<p>也就是说，多线程下无法正确判定键值对是否存在（存在其他线程修改的情况），单线程是可以的（不存在其他线程修改的情况）。</p>
<p>如果你确实需要在 ConcurrentHashMap 中使用 null 的话，可以使用一个特殊的静态空对象来代替 null。</p>
<pre><code class="language-java">public static final Object NULL = new Object();
</code></pre>
<p>最后，再分享一下 <code>ConcurrentHashMap</code> 作者本人 (Doug Lea)对于这个问题的回答：</p>
<blockquote>
<p>The main reason that nulls aren't allowed in ConcurrentMaps (ConcurrentHashMaps, ConcurrentSkipListMaps) is that ambiguities that may be just barely tolerable in non-concurrent maps can't be accommodated. The main one is that if <code>map.get(key)</code> returns <code>null</code>, you can't detect whether the key explicitly maps to <code>null</code> vs the key isn't mapped. In a non-concurrent map, you can check this via <code>map.contains(key)</code>, but in a concurrent one, the map might have changed between calls.</p>
</blockquote>
<p>翻译过来之后的，大致意思还是单线程下可以容忍歧义，而多线程下无法容忍。</p>
<h3><a id="concurrenthashmap%E8%83%BD%E4%BF%9D%E8%AF%81%E5%A4%8D%E5%90%88%E6%93%8D%E4%BD%9C%E7%9A%84%E5%8E%9F%E5%AD%90%E6%80%A7%E5%90%97%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>ConcurrentHashMap 能保证复合操作的原子性吗？</h3>
<p><code>ConcurrentHashMap</code> 是线程安全的，意味着它可以保证多个线程同时对它进行读写操作时，不会出现数据不一致的情况，也不会导致 JDK1.7 及之前版本的 <code>HashMap</code> 多线程操作导致死循环问题。但是，这并不意味着它可以保证所有的复合操作都是原子性的，一定不要搞混了！</p>
<p>复合操作是指由多个基本操作(如<code>put</code>、<code>get</code>、<code>remove</code>、<code>containsKey</code>等)组成的操作，例如先判断某个键是否存在<code>containsKey(key)</code>，然后根据结果进行插入或更新<code>put(key, value)</code>。这种操作在执行过程中可能会被其他线程打断，导致结果不符合预期。</p>
<p>例如，有两个线程 A 和 B 同时对 <code>ConcurrentHashMap</code> 进行复合操作，如下：</p>
<pre><code class="language-java">// 线程 A
if (!map.containsKey(key)) {
map.put(key, value);
}
// 线程 B
if (!map.containsKey(key)) {
map.put(key, anotherValue);
}
</code></pre>
<p>如果线程 A 和 B 的执行顺序是这样：</p>
<ol>
<li>线程 A 判断 map 中不存在 key</li>
<li>线程 B 判断 map 中不存在 key</li>
<li>线程 B 将 (key, anotherValue) 插入 map</li>
<li>线程 A 将 (key, value) 插入 map</li>
</ol>
<p>那么最终的结果是 (key, value)，而不是预期的 (key, anotherValue)。这就是复合操作的非原子性导致的问题。</p>
<p><strong>那如何保证 <code>ConcurrentHashMap</code> 复合操作的原子性呢？</strong></p>
<p><code>ConcurrentHashMap</code> 提供了一些原子性的复合操作，如 <code>putIfAbsent</code>、<code>compute</code>、<code>computeIfAbsent</code> 、<code>computeIfPresent</code>、<code>merge</code>等。这些方法都可以接受一个函数作为参数，根据给定的 key 和 value 来计算一个新的 value，并且将其更新到 map 中。</p>
<p>上面的代码可以改写为：</p>
<pre><code class="language-java">// 线程 A
map.putIfAbsent(key, value);
// 线程 B
map.putIfAbsent(key, anotherValue);
</code></pre>
<p>或者：</p>
<pre><code class="language-java">// 线程 A
map.computeIfAbsent(key, k -&gt; value);
// 线程 B
map.computeIfAbsent(key, k -&gt; anotherValue);
</code></pre>
<p>很多同学可能会说了，这种情况也能加锁同步呀！确实可以，但不建议使用加锁的同步机制，违背了使用 <code>ConcurrentHashMap</code> 的初衷。在使用 <code>ConcurrentHashMap</code> 的时候，尽量使用这些原子性的复合操作方法来保证原子性。</p>
<h2><a id="collections%E5%B7%A5%E5%85%B7%E7%B1%BB%EF%BC%88%E4%B8%8D%E9%87%8D%E8%A6%81%EF%BC%89" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Collections 工具类（不重要）</h2>
<p><strong><code>Collections</code> 工具类常用方法</strong>:</p>
<ul>
<li>排序</li>
<li>查找,替换操作</li>
<li>同步控制(不推荐，需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合)</li>
</ul>
<h3><a id="%E6%8E%92%E5%BA%8F%E6%93%8D%E4%BD%9C" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>排序操作</h3>
<pre><code class="language-java">void reverse(List list)//反转
void shuffle(List list)//随机排序
void sort(List list)//按自然排序的升序排序
void sort(List list, Comparator c)//定制排序，由Comparator控制排序逻辑
void swap(List list, int i , int j)//交换两个索引位置的元素
void rotate(List list, int distance)//旋转。当distance为正数时，将list后distance个元素整体移到前面。当distance为负数时，将 list的前distance个元素整体移到后面
</code></pre>
<h3><a id="%E6%9F%A5%E6%89%BE%E6%9B%BF%E6%8D%A2%E6%93%8D%E4%BD%9C" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>查找,替换操作</h3>
<pre><code class="language-java">int binarySearch(List list, Object key)//对List进行二分查找，返回索引，注意List必须是有序的
int max(Collection coll)//根据元素的自然顺序，返回最大的元素。 类比int min(Collection coll)
int max(Collection coll, Comparator c)//根据定制排序，返回最大元素，排序规则由Comparatator类控制。类比int min(Collection coll, Comparator c)
void fill(List list, Object obj)//用指定的元素代替指定list中的所有元素
int frequency(Collection c, Object o)//统计元素出现次数
int indexOfSubList(List list, List target)//统计target在list中第一次出现的索引，找不到则返回-1，类比int lastIndexOfSubList(List source, list target)
boolean replaceAll(List list, Object oldVal, Object newVal)//用新元素替换旧元素
</code></pre>
<h3><a id="%E5%90%8C%E6%AD%A5%E6%8E%A7%E5%88%B6" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>同步控制</h3>
<p><code>Collections</code> 提供了多个<code>synchronizedXxx()</code>方法·，该方法可以将指定集合包装成线程同步的集合，从而解决多线程并发访问集合时的线程安全问题。</p>
<p>我们知道 <code>HashSet</code>，<code>TreeSet</code>，<code>ArrayList</code>,<code>LinkedList</code>,<code>HashMap</code>,<code>TreeMap</code> 都是线程不安全的。<code>Collections</code> 提供了多个静态方法可以把他们包装成线程同步的集合。</p>
<p><strong>最好不要用下面这些方法，效率非常低，需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合。</strong></p>
<p>方法如下：</p>
<pre><code class="language-java">synchronizedCollection(Collection&lt;T&gt;  c) //返回指定 collection 支持的同步（线程安全的）collection。
synchronizedList(List&lt;T&gt; list)//返回指定列表支持的同步（线程安全的）List。
synchronizedMap(Map&lt;K,V&gt; m) //返回由指定映射支持的同步（线程安全的）Map。
synchronizedSet(Set&lt;T&gt; s) //返回指定 set 支持的同步（线程安全的）set。
</code></pre>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[02-Kafka相关环境准备和安装]]></title>
    <link href="https://huanglei.work/17418276110597.html"/>
    <updated>2025-03-13T09:00:11+08:00</updated>
    <id>https://huanglei.work/17418276110597.html</id>
    <content type="html"><![CDATA[
<h3><a id="%E9%9C%80%E8%A6%81%E7%9A%84%E8%BD%AF%E4%BB%B6%E5%92%8C%E7%8E%AF%E5%A2%83%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>需要的软件和环境版本说明</h3>
<ul>
<li>kafka-xx-yy
<ul>
<li>xx 是scala版本，yy是kafka版本（scala是基于jdk开发，需要安装jdk环境）</li>
<li>下载地址：<a href="http://kafka.apache.org/downloads">http://kafka.apache.org/downloads</a></li>
</ul>
</li>
<li>zookeeper
<ul>
<li>Apache 软件基金会的一个软件项目，它为大型分布式计算提供开源的分布式配置服务、同步服务和命名注册</li>
<li>下载地址：<a href="https://zookeeper.apache.org/releases.html">https://zookeeper.apache.org/releases.html</a></li>
</ul>
</li>
<li>jdk1.8</li>
</ul>
<h3><a id="%E6%AD%A5%E9%AA%A4" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>步骤</h3>
<h4><a id="%E4%B8%8A%E4%BC%A0%E5%AE%89%E8%A3%85%E5%8C%85%EF%BC%88zk%E3%80%81jdk%E3%80%81kafka%EF%BC%89" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>上传安装包（zk、jdk、kafka）</h4>
<ul>
<li>将安装包放置在 /usr/local/software 目录下，如果没有 software 目录则 mkdir 一个</li>
</ul>
<h4><a id="%E5%AE%89%E8%A3%85jdk" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>安装jdk</h4>
<ul>
<li>解压：<code>tar -zxvf jdk-8u181-linux-x64.tar.gz</code></li>
<li>重命名：<code>mv jdk1.8.0_181/ jdk1.8</code></li>
<li>配置环境变量：<code>vim /etc/profile</code></li>
</ul>
<pre><code class="language-shell">JAVA_HOME=/usr/local/software/jdk1.8
CLASSPATH=$JAVA_HOME/lib/
PATH=$PATH:$JAVA_HOME/bin
export PATH JAVA_HOME CLASSPATH
</code></pre>
<ul>
<li>使环境变量立刻生效：<code>source /etc/profile</code></li>
<li>查看安装情况：<code>java -version</code></li>
</ul>
<h4><a id="%E5%AE%89%E8%A3%85zookeeper%E9%BB%98%E8%AE%A4-2181%E7%AB%AF%E5%8F%A3" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>安装zookeeper (默认2181端口)</h4>
<ul>
<li>解压：<code>tar -zxvf apache-zookeeper-3.7.0-bin.tar.gz</code></li>
<li>重命名：<code>mv apache-zookeeper-3.7.0-bin zookeeper</code></li>
<li>修改配置文件：</li>
</ul>
<pre><code class="language-shell">cd zookeeper/
cd conf/
cp zoo_sample.cfg zoo.cfg
vim zoo.cfg

#一般不会把dataDir目录放在/tmp目录下，这里我们暂时不修改
#dataDir=/tmp/zookeeper
</code></pre>
<ul>
<li>启动zk：<code>bin/zkServer.sh start</code><br />
<img src="https://image.huanglei.work/mweb/2025/3/13/32970be5-541c-4c80-9725-fa00c0f85d0f.png" alt="d207f598-5586-4cc2-b5d4-39578b556b55.png" /></li>
<li>查看日志：<code>tail -f logs/zookeeper-root-server-iZbp17ukbamh0goeab4mgcZ.out</code></li>
<li>通过端口号查看是否启动：<code>lsof -i:2181</code> 如果没有安装lsof执行如下命令安装：<code>yum install -y lsof</code></li>
</ul>
<h4><a id="%E5%AE%89%E8%A3%85kafka%E9%BB%98%E8%AE%A4-9092%E7%AB%AF%E5%8F%A3" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>安装Kafka (默认 9092端口)</h4>
<ul>
<li>解压：<code>tar -zxvf kafka_2.13-2.8.0.tgz</code></li>
<li>重命名：<code>mv kafka_2.13-2.8.0 kafka</code></li>
<li>修改配置：<code>vim config/server.properties</code></li>
</ul>
<pre><code class="language-shell">#标识broker编号，集群中有多个broker，则每个broker的编号需要设置不同
broker.id=0

#修改下面两个配置 ( listeners 配置的ip和 advertised.listeners 相同时启动kafka会报错)
listeners(内网Ip)
advertised.listeners(公网ip)

listeners=PLAINTEXT://172.25.71.200:9092
advertised.listeners=PLAINTEXT://121.40.146.120:9092

#修改zk地址,默认地址
zookeeper.connection=localhost:2181
</code></pre>
<ul>
<li>bin目录启动</li>
</ul>
<pre><code class="language-shell">#启动
./kafka-server-start.sh  ../config/server.properties &amp;

#停止
./kafka-server-stop.sh
</code></pre>
<ul>
<li>创建topic</li>
</ul>
<pre><code class="language-shell">./kafka-topics.sh --create --zookeeper 121.40.146.120:2181 --replication-factor 1 --partitions 1 --topic xdclass-topic
</code></pre>
<ul>
<li>查看topic</li>
</ul>
<pre><code class="language-shell">./kafka-topics.sh --list --zookeeper 121.40.146.120:2181
</code></pre>
<ul>
<li>kafka如果直接启动信息会打印在控制台,如果关闭窗口，kafka随之关闭</li>
<li>守护进程方式启动</li>
</ul>
<pre><code class="language-shell">./kafka-server-start.sh -daemon ../config/server.properties &amp;
</code></pre>
<hr />
<div>
<a href="https://url07.ctfile.com/f/19056907-1334802124-4f8c67?p=1895" target="_blank">jdk-8u181-linux-x64.tar.gz</a> (访问密码: 1895)<br/>
<a href="https://url07.ctfile.com/f/19056907-1334802121-9d4bd4?p=1895" target="_blank">apache-zookeeper-3.7.0-bin.tar.gz</a> (访问密码: 1895)<br/>
<a href="https://url07.ctfile.com/f/19056907-1334802127-64f0c2?p=1895" target="_blank">kafka_2.13-2.8.0.tgz</a> (访问密码: 1895)<br/>
</div>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[02-MQ 消息中间件+JMS+AMQP 核心知识]]></title>
    <link href="https://huanglei.work/17421258021593.html"/>
    <updated>2025-03-16T19:50:02+08:00</updated>
    <id>https://huanglei.work/17421258021593.html</id>
    <content type="html"><![CDATA[
<ul>
<li><a href="#%E4%BB%80%E4%B9%88%E6%98%AFmq%E6%B6%88%E6%81%AF%E4%B8%AD%E9%97%B4%E4%BB%B6%E5%92%8C%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF">什么是 MQ 消息中间件和应用场景</a></li>
<li><a href="#jms%E6%B6%88%E6%81%AF%E6%9C%8D%E5%8A%A1%E5%92%8C%E5%92%8C%E5%B8%B8%E8%A7%81%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5%E4%BB%8B%E7%BB%8D">JMS 消息服务和和常见核心概念介绍</a></li>
<li><a href="#%E9%AB%98%E7%BA%A7%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%E5%8D%8F%E8%AE%AEamqp%E4%BB%8B%E7%BB%8D%E5%92%8C-mqtt%E6%8B%93%E5%B1%95">高级消息队列协议 AMQP 介绍和 MQTT 拓展</a></li>
</ul>
<h2><a id="%E4%BB%80%E4%B9%88%E6%98%AFmq%E6%B6%88%E6%81%AF%E4%B8%AD%E9%97%B4%E4%BB%B6%E5%92%8C%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>什么是 MQ 消息中间件和应用场景</h2>
<ul>
<li>
<p>什么是 MQ 消息中间件</p>
<ul>
<li>全称 MessageQueue，主要是用于程序和程序直接通信，异步+解耦</li>
</ul>
</li>
<li>
<p>使用场景</p>
<ul>
<li>
<p>核心应用</p>
<ul>
<li>
<p>解耦</p>
<ul>
<li>订单系统-》物流系统</li>
</ul>
</li>
<li>
<p>异步</p>
<ul>
<li>用户注册-》发送邮件，初始化信息</li>
</ul>
</li>
<li>
<p>削峰</p>
<ul>
<li>秒杀、日志处理</li>
</ul>
</li>
</ul>
</li>
<li>
<p>分布式事务、最终一致性</p>
</li>
<li>
<p>RPC 调用上下游对接，数据源变动-&gt;通知下属</p>
</li>
</ul>
<p><img src="https://image.huanglei.work/mweb/2025/3/17/4cab865b-3640-43cb-84cf-bb2e86b8e9d5.jpg" alt="" /></p>
</li>
</ul>
<h2><a id="jms%E6%B6%88%E6%81%AF%E6%9C%8D%E5%8A%A1%E5%92%8C%E5%92%8C%E5%B8%B8%E8%A7%81%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5%E4%BB%8B%E7%BB%8D" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>JMS 消息服务和和常见核心概念介绍</h2>
<ul>
<li>
<p>什么是 JMS</p>
<ul>
<li>
<p>Java 消息服务（Java Message Service),Java 平台中关于面向消息中间件的接口。</p>
<ul>
<li>JMS 是一种与厂商无关的 API，用来访问消息收发系统消息，它类似于 JDBC(Java Database Connectivity)。这里，JDBC 是可以用来访问许多不同关系数据库的 API</li>
<li>是由 Sun 公司早期提出的消息标准，旨在为 Java 应用提供统一的消息操作，包括 create、send、receive</li>
<li>JMS 是针对 Java 的，微软开发了 NMS（.NET 消息传递服务）</li>
</ul>
</li>
</ul>
<p><img src="https://image.huanglei.work/mweb/2025/3/17/20443ad7-d28d-440f-a5e8-d2cf9da61ee9.jpg" alt="" /></p>
</li>
<li>
<p>特性</p>
<ul>
<li>面向 Java 平台的标准消息传递 API</li>
<li>在 Java 或 JVM 语言比如 Scala、Groovy 中具有互用性</li>
<li>无需担心底层协议</li>
<li>有 queues 和 topics 两种消息传递模型</li>
<li>支持事务、能够定义消息格式（消息头、属性和内容）</li>
</ul>
</li>
<li>
<p>常见概念</p>
<table>
<thead>
<tr>
<th>概念</th>
<th>解释</th>
</tr>
</thead>
<tbody>
<tr>
<td>JMS 提供者</td>
<td>连接面向消息中间件的，JMS 接口的一个实现，RocketMQ,ActiveMQ,Kafka 等等</td>
</tr>
<tr>
<td>JMS 生产者(Message Producer)</td>
<td>生产消息的服务</td>
</tr>
<tr>
<td>JMS 消费者(Message Consumer)</td>
<td>消费消息的服务</td>
</tr>
<tr>
<td>JMS 消息</td>
<td>数据对象</td>
</tr>
<tr>
<td>JMS 队列</td>
<td>存储待消费消息的区域</td>
</tr>
<tr>
<td>JMS 主题</td>
<td>一种支持发送消息给多个订阅者的机制</td>
</tr>
<tr>
<td>JMS 消息通常有两种类型</td>
<td>点对点（Point-to-Point)、发布/订阅（Publish/Subscribe）</td>
</tr>
</tbody>
</table>
</li>
<li>
<p>基础编程模型</p>
<table>
<thead>
<tr>
<th>MQ 中需要用的一些类</th>
<th>解释</th>
</tr>
</thead>
<tbody>
<tr>
<td>ConnectionFactory</td>
<td>连接工厂，JMS 用它创建连接</td>
</tr>
<tr>
<td>Connection</td>
<td>JMS 客户端到 JMS Provider 的连接</td>
</tr>
<tr>
<td>Session</td>
<td>一个发送或接收消息的线程</td>
</tr>
<tr>
<td>Destination</td>
<td>消息的目的地;消息发送给谁</td>
</tr>
<tr>
<td>MessageConsumer / MessageProducer</td>
<td>消息消费者，消息生产者</td>
</tr>
</tbody>
</table>
</li>
</ul>
<h2><a id="%E9%AB%98%E7%BA%A7%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%E5%8D%8F%E8%AE%AEamqp%E4%BB%8B%E7%BB%8D%E5%92%8C-mqtt%E6%8B%93%E5%B1%95" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>高级消息队列协议 AMQP 介绍和 MQTT 拓展</h2>
<ul>
<li>
<p>背景</p>
<ul>
<li>JMS 或者 NMS 都没有标准的底层协议，它们可以在任何底层协议上运行，但是 API 是与编程语言绑定的，AMQP 解决了这个问题，它使用了一套标准的底层协议</li>
</ul>
</li>
<li>
<p>什么是 AMQP</p>
<ul>
<li>AMQP（advanced message queuing protocol）在 2003 年时被提出，最早用于解决金融领域不同平台之间的消息传递交互问题,就是一种协议，兼容 JMS</li>
<li>更准确说的链接协议 binary- wire-level-protocol 直接定义网络交换的数据格式，类似 http</li>
<li>具体的产品实现比较多，RabbitMQ 就是其中一种</li>
</ul>
<p><img src="https://image.huanglei.work/mweb/2025/3/17/30416e58-8fcf-40b0-88aa-fc3c977a08bb.jpg" alt="" /></p>
</li>
<li>
<p>特性</p>
<ul>
<li>独立于平台的底层消息传递协议</li>
<li>消费者驱动消息传递</li>
<li>跨语言和平台的互用性、属于底层协议</li>
<li>有 5 种交换类型 direct，fanout，topic，headers，system</li>
<li>面向缓存的、可实现高性能、支持经典的消息队列，循环，存储和转发</li>
<li>支持长周期消息传递、支持事务（跨消息队列）</li>
</ul>
</li>
<li>
<p>AMQP 和 JMS 的主要区别</p>
<ul>
<li>AMQP 不从 API 层进行限定，直接定义网络交换的数据格式,这使得实现了 AMQP 的 provider 天然性就是跨平台</li>
<li>比如 Java 语言产生的消息，可以用其他语言比如 python 的进行消费</li>
<li>AQMP 可以用 http 来进行类比，不关心实现接口的语言，只要都按照相应的数据格式去发送报文请求，不同语言的 client 可以和不同语言的 server 进行通讯</li>
<li>JMS 消息类型：TextMessage/ObjectMessage/StreamMessage 等</li>
<li>AMQP 消息类型：Byte[]</li>
</ul>
</li>
<li>
<p>科普：大家可能也听过 MQTT</p>
<ul>
<li>
<p>MQTT</p>
<ul>
<li>消息队列遥测传输（Message Queueing Telemetry Transport）</li>
</ul>
</li>
<li>
<p>背景</p>
<ul>
<li>我们有面向基于 Java 的企业应用的 JMS 和面向所有其他应用需求的 AMQP，那这个 MQTT 是做啥的？</li>
</ul>
</li>
<li>
<p>原因</p>
<ul>
<li>计算性能不高的设备不能适应 AMQP 上的复杂操作,MQTT 它是专门为小设备设计的</li>
<li>MQTT 主要是是物联网（IOT）中大量的使用</li>
</ul>
</li>
<li>
<p>特性</p>
<ul>
<li>内存占用低，为小型无声设备之间通过低带宽发送短消息而设计</li>
<li>不支持长周期存储和转发，不允许分段消息（很难发送长消息）</li>
<li>支持主题发布-订阅、不支持事务（仅基本确认）</li>
<li>消息实际上是短暂的（短周期）</li>
<li>简单用户名和密码、不支持安全连接、消息不透明</li>
</ul>
</li>
</ul>
</li>
</ul>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[02-Tuple数据类型+Map+FlatMap操作介绍]]></title>
    <link href="https://huanglei.work/17418277612005.html"/>
    <updated>2025-03-13T09:02:41+08:00</updated>
    <id>https://huanglei.work/17418277612005.html</id>
    <content type="html"><![CDATA[
<ul>
<li>什么是Tuple类型
<ul>
<li>元组类型, 多个语言都有的特性, flink的java版 tuple最多支持25个</li>
<li>用途
<ul>
<li>函数返回（return）多个值，多个不同类型的对象</li>
<li>List集合不是也可以吗，集合里面是单个类型</li>
<li>列表只能存储相同的数据类型，而元组Tuple可以存储不同的数据类型</li>
</ul>
</li>
</ul>
</li>
</ul>
<pre><code class="language-java">    private static void tupleTest() {
        Tuple3&lt;Integer, String, Long&gt; tuple3 = Tuple3.of(1, &quot;xdclass.net&quot;, 120L);
        System.out.println(tuple3.f0);
        System.out.println(tuple3.f1);
        System.out.println(tuple3.f2);
    }
</code></pre>
<ul>
<li>什么是java里面的Map操作
<ul>
<li>一对一 转换对象，比如DO转DTO</li>
</ul>
</li>
</ul>
<pre><code class="language-java">    private static void mapTest() {
        List&lt;String&gt; list1 = new ArrayList&lt;&gt;();
        list1.add(&quot;springboot,springcloud&quot;);
        list1.add(&quot;redis6,docker&quot;);
        list1.add(&quot;kafka,rabbitmq&quot;);

        List&lt;String&gt; result = list1.stream().map(obj -&gt; {
            obj = &quot;小滴课堂&quot; + obj;
            return obj;
        }).collect(Collectors.toList());

        System.out.println(result);
    }
// [小滴课堂springboot,springcloud, 小滴课堂redis6,docker, 小滴课堂kafka,rabbitmq]
</code></pre>
<ul>
<li>什么是java里面的FlatMap操作
<ul>
<li>一对多转换对象</li>
</ul>
</li>
</ul>
<pre><code class="language-java">    private static void flatMapTest() {
        List&lt;String&gt; list1 = new ArrayList&lt;&gt;();
        list1.add(&quot;springboot,springcloud&quot;);
        list1.add(&quot;redis6,docker&quot;);
        list1.add(&quot;kafka,rabbitmq&quot;);

        List&lt;String&gt; result = list1.stream().flatMap(obj -&gt; {
            Stream&lt;String&gt; stream = Arrays.stream(obj.split(&quot;,&quot;));
            return stream;
        }).collect(Collectors.toList());

        System.out.println(result);
    }
// [springboot, springcloud, redis6, docker, kafka, rabbitmq]
</code></pre>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[02-【面试题】业务增长-数据库性能优化思路讲解]]></title>
    <link href="https://huanglei.work/17418278741879.html"/>
    <updated>2025-03-13T09:04:34+08:00</updated>
    <id>https://huanglei.work/17418278741879.html</id>
    <content type="html"><![CDATA[
<ul>
<li>面试官：这边有个数据库-单表1千万数据，未来1年还会增长多500万，性能比较慢，说下你的优化思路</li>
<li>思路
<ul>
<li>千万不要一上来就说分库分表，这个是最忌讳的事项</li>
<li>一定要根据实际情况分析，两个角度思考
<ul>
<li>不分库分表
<ul>
<li>软优化
<ul>
<li>数据库参数调优</li>
<li>分析慢查询SQL语句，分析执行计划，进行sql改写和程序改写</li>
<li>优化数据库索引结构</li>
<li>优化数据表结构优化</li>
<li>引入NOSQL和程序架构调整</li>
</ul>
</li>
<li>硬优化
<ul>
<li>提升系统硬件（更快的IO、更多的内存）：带宽、CPU、硬盘</li>
</ul>
</li>
</ul>
</li>
<li>分库分表
<ul>
<li>根据业务情况而定，选择合适的分库分表策略（没有通用的策略）
<ul>
<li>外卖、物流、电商领域</li>
</ul>
</li>
<li>先看只分表是否满足业务的需求和未来增长
<ul>
<li>数据库分表能够解决单表数据量很大的时,数据查询的效率问题</li>
<li>无法给数据库的并发操作带来效率上的提高，分表的实质还是在一个数据库上进行的操作，受数据库IO性能的限制</li>
</ul>
</li>
<li>如果单分表满足不了需求，再分库分表一起</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>结论
<ul>
<li>在数据量及访问压力不是特别大的情况，首先考虑缓存、读写分离、索引技术等方案</li>
<li>如果数据量极大，且业务持续增长快，再考虑分库分表方案</li>
</ul>
</li>
</ul>
<hr />
<p><strong>补充</strong>：<br />
MySQL数据库参数调优是指通过调整MySQL的配置参数，以提升数据库的性能、稳定性和资源利用率。这些参数控制着MySQL的内存使用、连接管理、查询处理、存储引擎行为等方面。</p>
<h3><a id="%E8%B0%83%E4%BC%98%E7%9A%84%E4%B8%BB%E8%A6%81%E7%9B%AE%E6%A0%87%EF%BC%9A" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>调优的主要目标：</h3>
<ol>
<li><strong>提升性能</strong>：加快查询速度，减少响应时间。</li>
<li><strong>提高稳定性</strong>：防止崩溃或资源耗尽。</li>
<li><strong>优化资源利用</strong>：合理分配内存、CPU和磁盘I/O，避免浪费。</li>
</ol>
<h3><a id="%E5%B8%B8%E8%A7%81%E7%9A%84%E8%B0%83%E4%BC%98%E5%8F%82%E6%95%B0%EF%BC%9A" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>常见的调优参数：</h3>
<ol>
<li>
<p><strong>内存相关参数</strong>：</p>
<ul>
<li><code>innodb_buffer_pool_size</code>：InnoDB缓冲池的大小，缓存数据和索引，建议设置为系统内存的50%-70%。</li>
<li><code>key_buffer_size</code>：MyISAM存储引擎的键缓冲区大小。</li>
<li><code>query_cache_size</code>：查询缓存大小（MySQL 8.0已移除）。</li>
</ul>
</li>
<li>
<p><strong>连接相关参数</strong>：</p>
<ul>
<li><code>max_connections</code>：最大连接数，控制同时连接的客户端数量。</li>
<li><code>wait_timeout</code> 和 <code>interactive_timeout</code>：控制空闲连接的超时时间。</li>
</ul>
</li>
<li>
<p><strong>查询优化参数</strong>：</p>
<ul>
<li><code>query_cache_type</code>：查询缓存类型（MySQL 8.0已移除）。</li>
<li><code>tmp_table_size</code> 和 <code>max_heap_table_size</code>：控制内存中临时表的大小。</li>
</ul>
</li>
<li>
<p><strong>日志相关参数</strong>：</p>
<ul>
<li><code>slow_query_log</code>：启用慢查询日志，记录执行时间较长的查询。</li>
<li><code>log_queries_not_using_indexes</code>：记录未使用索引的查询。</li>
</ul>
</li>
<li>
<p><strong>InnoDB相关参数</strong>：</p>
<ul>
<li><code>innodb_log_file_size</code>：InnoDB日志文件大小，影响事务写入性能。</li>
<li><code>innodb_flush_log_at_trx_commit</code>：控制事务日志刷新到磁盘的频率，影响数据安全性和性能。</li>
</ul>
</li>
</ol>
<h3><a id="%E8%B0%83%E4%BC%98%E6%AD%A5%E9%AA%A4%EF%BC%9A" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>调优步骤：</h3>
<ol>
<li>
<p><strong>监控和分析</strong>：</p>
<ul>
<li>使用工具（如<code>SHOW STATUS</code>、<code>SHOW VARIABLES</code>、<code>EXPLAIN</code>、慢查询日志等）监控数据库性能，识别瓶颈。</li>
</ul>
</li>
<li>
<p><strong>调整参数</strong>：</p>
<ul>
<li>根据监控结果调整相关参数，逐步优化。</li>
</ul>
</li>
<li>
<p><strong>测试和验证</strong>：</p>
<ul>
<li>调整后测试性能，确保调优有效且无负面影响。</li>
</ul>
</li>
<li>
<p><strong>持续优化</strong>：</p>
<ul>
<li>数据库负载变化时，定期监控和调整参数。</li>
</ul>
</li>
</ol>
<h3><a id="%E7%A4%BA%E4%BE%8B%EF%BC%9A" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>示例：</h3>
<ul>
<li><strong>增大InnoDB缓冲池</strong>：
<pre><code class="language-sql">SET GLOBAL innodb_buffer_pool_size = 2G;
</code></pre>
</li>
<li><strong>调整最大连接数</strong>：
<pre><code class="language-sql">SET GLOBAL max_connections = 500;
</code></pre>
</li>
</ul>
<h3><a id="%E6%80%BB%E7%BB%93%EF%BC%9A" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>总结：</h3>
<p>MySQL参数调优是通过调整配置参数来优化数据库性能、稳定性和资源利用率的过程。需要根据实际负载和硬件条件进行监控和调整。</p>
<hr />

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[02-高并发必备技术+新版分布式缓存 Redis6 安装]]></title>
    <link href="https://huanglei.work/17421079045999.html"/>
    <updated>2025-03-16T14:51:44+08:00</updated>
    <id>https://huanglei.work/17421079045999.html</id>
    <content type="html"><![CDATA[
<ul>
<li><a href="#%E7%AC%AC1%E9%9B%86%E9%AB%98%E5%B9%B6%E5%8F%91%E7%9A%84%E5%BF%85%E5%A4%87%E4%B8%A4%E5%A4%A7%E2%80%9C%E6%A0%B8%E6%8A%80%E6%9C%AF%E2%80%9D%E9%98%9F%E5%88%97%E5%92%8C%E7%BC%93%E5%AD%98">第1集 高并发的必备两大“核技术”队列和缓存</a></li>
<li><a href="#%E7%AC%AC2%E9%9B%86%E6%9C%AC%E5%9C%B0%E7%BC%93%E5%AD%98%E5%92%8C%E5%88%86%E5%B8%83%E5%BC%8F%E7%BC%93%E5%AD%98%E4%BB%8B%E7%BB%8D%E7%83%AD%E7%82%B9-key%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88">第2集 本地缓存和分布式缓存介绍+热点key的解决方案</a></li>
<li><a href="#%E7%AC%AC3%E9%9B%86%E4%BB%80%E4%B9%88%E6%98%AF-nosql%E5%92%8Credis%E5%BF%AB%E9%80%9F%E4%BB%8B%E7%BB%8D">第3集 什么是NosQL和Redis快速介绍</a></li>
<li><a href="#%E7%AC%AC4%E9%9B%86%E9%98%BF%E9%87%8C%E4%BA%91-linux%E6%9C%8D%E5%8A%A1%E5%99%A8%E9%80%89%E6%8B%A9%E5%92%8C%E5%B8%B8%E7%94%A8%E8%BD%AF%E4%BB%B6%E4%BB%8B%E7%BB%8D">第4集 阿里云Linux服务器选择和常用软件介绍</a></li>
<li><a href="#%E7%AC%AC5%E9%9B%86-linux%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%BA%90%E7%A0%81%E5%AE%89%E8%A3%85redis6%E5%92%8C%E7%9B%B8%E5%85%B3%E4%BE%9D%E8%B5%96">第5集 Linux服务器源码安装Redis6和相关依赖</a></li>
<li><a href="#%E7%AC%AC6%E9%9B%86-linux%E6%9C%8D%E5%8A%A1%E5%99%A8docker%E5%AE%89%E8%A3%85%E5%AE%B9%E5%99%A8%E5%8C%96%E9%83%A8%E7%BD%B2-redis6">第6集 Linux服务器Docker安装+容器化部署Redis6</a></li>
</ul>
<h4><a id="%E7%AC%AC1%E9%9B%86%E9%AB%98%E5%B9%B6%E5%8F%91%E7%9A%84%E5%BF%85%E5%A4%87%E4%B8%A4%E5%A4%A7%E2%80%9C%E6%A0%B8%E6%8A%80%E6%9C%AF%E2%80%9D%E9%98%9F%E5%88%97%E5%92%8C%E7%BC%93%E5%AD%98" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>第 1 集 高并发的必备两大“核技术”队列和缓存</h4>
<p><strong>简介：高并发的必备两大“核技术”队列和缓存介绍</strong></p>
<ul>
<li>
<p>什么是队列（MQ 消息中间件）</p>
<ul>
<li>
<p>全称 MessageQueue，主要是用于程序和程序直接通信，异步+解耦</p>
</li>
<li>
<p>使用场景：</p>
<ul>
<li>
<p>核心应用</p>
<ul>
<li>解耦：订单系统-》物流系统</li>
<li>异步：用户注册-》发送邮件，初始化信息</li>
<li>削峰：秒杀、日志处理</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/576f3daa-0a31-4bb9-8ea6-ef348b0bf79c.jpg" alt="" /></p>
<ul>
<li>
<p>什么是缓存</p>
<ul>
<li>程序经常要调用的对象存在内存中,方便其使用时可以快 速调用,不必去数据库或者其他持久化设备中查询</li>
<li>主要 就是提高性能 DNS 缓存、前端缓存、代理服务器缓存 Nginx、应用程序缓存、数据库缓存</li>
</ul>
</li>
</ul>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/3a6e9d95-3f1f-494c-87be-239257a6eeae.jpg" alt="" /></p>
<h4><a id="%E7%AC%AC2%E9%9B%86%E6%9C%AC%E5%9C%B0%E7%BC%93%E5%AD%98%E5%92%8C%E5%88%86%E5%B8%83%E5%BC%8F%E7%BC%93%E5%AD%98%E4%BB%8B%E7%BB%8D%E7%83%AD%E7%82%B9-key%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>第 2 集 本地缓存和分布式缓存介绍+热点 key 的解决方案</h4>
<p><strong>简介：介绍本地缓存和分布式缓存</strong></p>
<ul>
<li>
<p>分布式缓存</p>
<ul>
<li>与应用分离的缓存组件或服务，与本地应用隔离一个独 立的应用，多个应用可直接的共享缓存</li>
<li>常⻅的分布式缓存 Redis、Memcached 等</li>
</ul>
</li>
<li>
<p>本地缓存</p>
<ul>
<li>
<p>和业务程序一起的缓存，例如 myabtis 的一级或者二级缓存，本地缓存自然是最快的，但是不能在多个节点共享</p>
</li>
<li>
<p>常⻅的本地缓存</p>
<ul>
<li>ssm 基础课程 myabtis 一级缓存、 mybatis 二级缓存;</li>
<li>框架本身的缓存;</li>
<li>redis 本地单机服 务;</li>
<li>ehchche</li>
<li>guava cache</li>
<li>Caffeine</li>
</ul>
</li>
</ul>
</li>
<li>
<p>选择本地缓存和分布式缓存</p>
<ul>
<li>
<p>和业务数据结合去选择 高并发项目里面一般都是有本地缓存和分布式缓存共同 存在的</p>
</li>
<li>
<p>热点 key 的解决方案之一：避免带宽或者传输影响，本地缓存热点 key 数据，对于每次读请求，将首先检查 key 是否存在于本地缓存中，如果存在则直接返回，如果不存在再去访问分布式缓存的机器</p>
<ul>
<li>缓存中的某些 Key 对应的 value 存储在集群中一台机器，使得所有流量涌向同一机器，成为系统的瓶颈，无法通过增加机器容量来解决</li>
<li>热卖商品、热点新闻、热点评论、大 V 明星结婚</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><img src="https://image.huanglei.work/mweb/2025/3/16/0e3d5ae0-a539-4137-876d-2d68b8e56925.jpg" alt="" /></p>
<h4><a id="%E7%AC%AC3%E9%9B%86%E4%BB%80%E4%B9%88%E6%98%AF-nosql%E5%92%8C-redis%E5%BF%AB%E9%80%9F%E4%BB%8B%E7%BB%8D" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>第 3 集 什么是 NosQL 和 Redis 快速介绍</h4>
<p><strong>简介：Nosql 介绍和 Reidis 介绍</strong></p>
<ul>
<li>
<p>什么是 Redis</p>
<ul>
<li>
<p>属于 NoSQL 的一种 ( Not Only SQL )</p>
<ul>
<li>是不同于传统的关系数据库的数据库管理系统的统称</li>
<li>其两者最重要的区别是 NoSQL 不使用 SQL 作为查询语言。</li>
<li>NoSQL 数据存储可以不需要固定的表格模式</li>
<li>键 - 值对存储，列存储，文档存储，图形数据库</li>
<li>NoSql：redis、memcached、mongodb、Hbase</li>
</ul>
</li>
<li>
<p>官网地址：<a href="https://redis.io/">https://redis.io/</a></p>
<ul>
<li>中文：<a href="http://www.redis.cn/">http://www.redis.cn/</a></li>
</ul>
</li>
<li>
<p>一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库，并提供多种语言的 API</p>
</li>
<li>
<p>高性能：Redis 能读的速度是 110000 次/s,写的速度是 81000 次/s</p>
</li>
<li>
<p>内存中的数据结构存储系统，它可以用作数据库、缓存和消息中间件。 它支持多 种类型的数据结构，如 字符串（strings）、散列（hashes）、 列表（lists）、 集合（sets）、 有序集合（sorted sets）等</p>
</li>
</ul>
</li>
<li>
<p>谁在使用 Redis</p>
<ul>
<li>
<p>国外： Google、Facebook、亚马逊</p>
</li>
<li>
<p>国内：阿里、腾讯、字节、百度</p>
<ul>
<li>大厂们都有一个习惯：基于 Redis 二次开发，比如阿里 Tair</li>
</ul>
</li>
</ul>
</li>
<li>
<p>高级工程师岗位面试都喜欢问 Redis</p>
<ul>
<li>特性：aof/rdb、高性能原因、key 设计、热点 key、淘汰算法</li>
<li>功能实现：排行榜、购物车、社交关系(粉丝、关注)、Feed 流、附近的商家、分布式锁等等</li>
</ul>
</li>
</ul>
<h4><a id="%E7%AC%AC4%E9%9B%86%E9%98%BF%E9%87%8C%E4%BA%91-linux%E6%9C%8D%E5%8A%A1%E5%99%A8%E9%80%89%E6%8B%A9%E5%92%8C%E5%B8%B8%E7%94%A8%E8%BD%AF%E4%BB%B6%E4%BB%8B%E7%BB%8D" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>第 4 集 阿里云 Linux 服务器选择和常用软件介绍</h4>
<p><strong>简介：阿里云 Linux 服务器购买和常用软件介绍</strong></p>
<ul>
<li>
<p>云厂商</p>
<ul>
<li>
<p>阿里云：<a href="https://www.aliyun.com/">https://www.aliyun.com/</a></p>
</li>
<li>
<p>腾讯云：<a href="https://cloud.tencent.com/">https://cloud.tencent.com/</a></p>
</li>
<li>
<p>亚马逊云：<a href="https://aws.amazon.com/">https://aws.amazon.com/</a></p>
</li>
<li>
<p>阿里云新用户地址（如果地址失效，联系我或者客服即可，1 折）</p>
<ul>
<li><a href="https://www.aliyun.com/minisite/goods?userCode=r5saexap&amp;share_source=copy_link">https://www.aliyun.com/minisite/goods?userCode=r5saexap&amp;share_source=copy_link</a></li>
</ul>
</li>
</ul>
</li>
<li>
<p>环境问题说明</p>
<ul>
<li>
<p>务必使用 CentOS 7 以上版本，64 位系统，不要在 Windows 系统操作！！！！</p>
</li>
<li>
<p>尽量前面先使用阿里云部署</p>
</li>
<li>
<p>大家本地用虚拟机记得也要 CentOS 7.x 系统</p>
<ul>
<li>vmware</li>
</ul>
</li>
</ul>
</li>
<li>
<p>注意：谁都不能保证每个人-硬件组成-系统版本-虚拟机软件版本都一样</p>
<ul>
<li>出现问题，大家结合报错日志搜索博文解决</li>
<li>少数同学 -Win7、Win8、Win10、Mac、虚拟机等等，可能存在兼容问题</li>
</ul>
</li>
<li>
<p>选购实操</p>
</li>
<li>
<p>windows 工具 putty，xshell, security CRT</p>
<ul>
<li>
<p>参考资料：</p>
<ul>
<li><a href="https://jingyan.baidu.com/article/e75057f210c6dcebc91a89dd.html">https://jingyan.baidu.com/article/e75057f210c6dcebc91a89dd.html</a></li>
<li><a href="https://www.jb51.net/softjc/88235.html">https://www.jb51.net/softjc/88235.html</a></li>
</ul>
</li>
</ul>
</li>
<li>
<p>苹果系统 MAC ： 通过终端登录</p>
<ul>
<li>ssh root@ip 回车后输入密码</li>
<li>ssh root@120.24.216.117</li>
</ul>
</li>
<li>
<p>linux 图形操作工具（用于远程连接上传文件）</p>
<ul>
<li>
<p>mac: filezilla</p>
<ul>
<li>sftp://120.24.216.117</li>
</ul>
</li>
<li>
<p>windows: winscp</p>
</li>
<li>
<p>参考资料：<a href="https://jingyan.baidu.com/article/ed2a5d1f346fd409f6be179a.html">https://jingyan.baidu.com/article/ed2a5d1f346fd409f6be179a.html</a></p>
</li>
</ul>
</li>
<li>
<p>可以尝试自己通过百度进行找文档， 安装 mysql jdk nginx maven git redis 等，也可以看我们的课程 xdclass.net</p>
</li>
</ul>
<h4><a id="%E7%AC%AC5%E9%9B%86-linux%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%BA%90%E7%A0%81%E5%AE%89%E8%A3%85-redis6%E5%92%8C%E7%9B%B8%E5%85%B3%E4%BE%9D%E8%B5%96" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>第 5 集 Linux 服务器源码安装 Redis6 和相关依赖</h4>
<p><strong>简介：Linux 服务器源码安装 Redis6 和相关依赖</strong></p>
<ul>
<li>
<p>源码安装 Redis-上传到 Linux 服务（安装包在本章本集资料里面, 先安装升级 gcc 再编译，不然会有问题）</p>
<pre><code class="language-shell">#安装gcc
yum install -y gcc-c++ autoconf automake
​
#centos7 默认的 gcc 默认是4.8.5,版本小于 5.3 无法编译,需要先安装gcc新版才能编译
gcc -v
​
#升级新版gcc，配置永久生效
yum -y install centos-release-scl
yum -y install devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils
​
scl enable devtoolset-9 bash  
echo &quot;source /opt/rh/devtoolset-9/enable&quot; &gt;&gt;/etc/profile 
​
#编译redis
cd redis
make
​
#安装到指定目录
mkdir -p /usr/local/redis
​
make PREFIX=/usr/local/redis install
</code></pre>
</li>
<li>
<p>安装编译 redis6 需要升级 gcc，默认自带的 gcc 版本比较老</p>
</li>
<li>
<p>目录介绍</p>
<ul>
<li>配置文件</li>
<li>redis-server</li>
<li>redis-cli</li>
<li>指定配置文件</li>
</ul>
</li>
</ul>
<h4><a id="%E7%AC%AC6%E9%9B%86-linux%E6%9C%8D%E5%8A%A1%E5%99%A8-docker%E5%AE%89%E8%A3%85%E5%AE%B9%E5%99%A8%E5%8C%96%E9%83%A8%E7%BD%B2-redis6" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>第 6 集 Linux 服务器 Docker 安装+容器化部署 Redis6</h4>
<p><strong>简介：Linux 服务器 Docker 安装+容器化部署 Redis6</strong></p>
<p>云计算+容器化是当下的主流，也是未来的趋势，docker就是可以快速部署启动应用，实现虚拟化，完整资源隔离，一次编写，四处运行。</p>
<p>但有一定的限制，比如Docker是基于Linux 64bit的，无法在32bit的linux/Windows/unix环境下使用</p>
<ul>
<li>
<p>Docker 安装</p>
<pre><code class="language-shell">安装并运行Docker。
yum install docker-io -y
systemctl start docker
​
检查安装结果。
docker info
​
启动使用Docker
systemctl start docker     #运行Docker守护进程
systemctl stop docker      #停止Docker守护进程
systemctl restart docker   #重启Docker守护进程
​
docker ps查看容器
docker stop 容器id
​
修改镜像仓库
vim /etc/docker/daemon.json
#改为下面内容，然后重启docker
{
&quot;debug&quot;:true,&quot;experimental&quot;:true,
&quot;registry-mirrors&quot;:[&quot;https://pb5bklzr.mirror.aliyuncs.com&quot;,&quot;https://hub-mirror.c.163.com&quot;,&quot;https://docker.mirrors.ustc.edu.cn&quot;]
}
​
#查看信息
docker info
</code></pre>
</li>
<li>
<p>docker 部署 redis 并配置密码</p>
<pre><code class="language-plain_text">如果访问不了，记得看防火墙/网络安全组端口是否开放
源码安装redis的话默认不能远程访问 docker安装redis可以远程访问
 
docker run -itd --name xdclass-redis -p 6379:6379 redis --requirepass 123456
​
​
-i 以交互模式运行容器，通常与 
-t 同时使用;
-d 后台运行容器，并返回容器ID;
</code></pre>
</li>
</ul>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[03-Java基础常见面试题总结(下)]]></title>
    <link href="https://huanglei.work/17420865516984.html"/>
    <updated>2025-03-16T08:55:51+08:00</updated>
    <id>https://huanglei.work/17420865516984.html</id>
    <content type="html"><![CDATA[
<ul>
<li><a href="#%E5%BC%82%E5%B8%B8">异常</a>
<ul>
<li><a href="#exception%E5%92%8C-error%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F">Exception 和 Error 有什么区别？</a></li>
<li><a href="#checked-exception%E5%92%8C-unchecked-exception%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F">Checked Exception 和 Unchecked Exception 有什么区别？</a></li>
<li><a href="#throwable%E7%B1%BB%E5%B8%B8%E7%94%A8%E6%96%B9%E6%B3%95%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F">Throwable 类常用方法有哪些？</a></li>
<li><a href="#try-catch-finally%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8%EF%BC%9F">try-catch-finally 如何使用？</a></li>
<li><a href="#finally%E4%B8%AD%E7%9A%84%E4%BB%A3%E7%A0%81%E4%B8%80%E5%AE%9A%E4%BC%9A%E6%89%A7%E8%A1%8C%E5%90%97%EF%BC%9F">finally 中的代码一定会执行吗？</a></li>
<li><a href="#%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8try-with-resources%E4%BB%A3%E6%9B%BF-try-catch-finally%EF%BC%9F">如何使用 <code>try-with-resources</code> 代替<code>try-catch-finally</code>？</a></li>
<li><a href="#%E5%BC%82%E5%B8%B8%E4%BD%BF%E7%94%A8%E6%9C%89%E5%93%AA%E4%BA%9B%E9%9C%80%E8%A6%81%E6%B3%A8%E6%84%8F%E7%9A%84%E5%9C%B0%E6%96%B9%EF%BC%9F">异常使用有哪些需要注意的地方？</a></li>
</ul>
</li>
<li><a href="#%E6%B3%9B%E5%9E%8B">泛型</a>
<ul>
<li><a href="#%E4%BB%80%E4%B9%88%E6%98%AF%E6%B3%9B%E5%9E%8B%EF%BC%9F%E6%9C%89%E4%BB%80%E4%B9%88%E4%BD%9C%E7%94%A8%EF%BC%9F">什么是泛型？有什么作用？</a></li>
<li><a href="#%E6%B3%9B%E5%9E%8B%E7%9A%84%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F%E6%9C%89%E5%93%AA%E5%87%A0%E7%A7%8D%EF%BC%9F">泛型的使用方式有哪几种？</a></li>
<li><a href="#%E9%A1%B9%E7%9B%AE%E4%B8%AD%E5%93%AA%E9%87%8C%E7%94%A8%E5%88%B0%E4%BA%86%E6%B3%9B%E5%9E%8B%EF%BC%9F">项目中哪里用到了泛型？</a></li>
</ul>
</li>
<li><a href="#%E5%8F%8D%E5%B0%84">反射</a>
<ul>
<li><a href="#%E4%BD%95%E8%B0%93%E5%8F%8D%E5%B0%84%EF%BC%9F">何谓反射？</a></li>
<li><a href="#%E5%8F%8D%E5%B0%84%E7%9A%84%E4%BC%98%E7%BC%BA%E7%82%B9%EF%BC%9F">反射的优缺点？</a></li>
<li><a href="#%E5%8F%8D%E5%B0%84%E7%9A%84%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF%EF%BC%9F">反射的应用场景？</a></li>
</ul>
</li>
<li><a href="#%E6%B3%A8%E8%A7%A3">注解</a>
<ul>
<li><a href="#%E4%BD%95%E8%B0%93%E6%B3%A8%E8%A7%A3%EF%BC%9F">何谓注解？</a></li>
<li><a href="#%E6%B3%A8%E8%A7%A3%E7%9A%84%E8%A7%A3%E6%9E%90%E6%96%B9%E6%B3%95%E6%9C%89%E5%93%AA%E5%87%A0%E7%A7%8D%EF%BC%9F">注解的解析方法有哪几种？</a></li>
</ul>
</li>
<li><a href="#spi">SPI</a>
<ul>
<li><a href="#%E4%BD%95%E8%B0%93spi">何谓 SPI?</a></li>
<li><a href="#spi%E5%92%8C-api%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F">SPI 和 API 有什么区别？</a></li>
<li><a href="#spi%E7%9A%84%E4%BC%98%E7%BC%BA%E7%82%B9%EF%BC%9F">SPI 的优缺点？</a></li>
</ul>
</li>
<li><a href="#%E5%BA%8F%E5%88%97%E5%8C%96%E5%92%8C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96">序列化和反序列化</a>
<ul>
<li><a href="#%E4%BB%80%E4%B9%88%E6%98%AF%E5%BA%8F%E5%88%97%E5%8C%96%E4%BB%80%E4%B9%88%E6%98%AF%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96">什么是序列化?什么是反序列化?</a></li>
<li><a href="#%E5%A6%82%E6%9E%9C%E6%9C%89%E4%BA%9B%E5%AD%97%E6%AE%B5%E4%B8%8D%E6%83%B3%E8%BF%9B%E8%A1%8C%E5%BA%8F%E5%88%97%E5%8C%96%E6%80%8E%E4%B9%88%E5%8A%9E%EF%BC%9F">如果有些字段不想进行序列化怎么办？</a></li>
<li><a href="#%E5%B8%B8%E8%A7%81%E5%BA%8F%E5%88%97%E5%8C%96%E5%8D%8F%E8%AE%AE%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F">常见序列化协议有哪些？</a></li>
<li><a href="#%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E6%8E%A8%E8%8D%90%E4%BD%BF%E7%94%A8jdk%E8%87%AA%E5%B8%A6%E7%9A%84%E5%BA%8F%E5%88%97%E5%8C%96%EF%BC%9F">为什么不推荐使用 JDK 自带的序列化？</a></li>
</ul>
</li>
<li><a href="#io">I/O</a>
<ul>
<li><a href="#java-io%E6%B5%81%E4%BA%86%E8%A7%A3%E5%90%97%EF%BC%9F">Java IO 流了解吗？</a></li>
<li><a href="#io%E6%B5%81%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E5%88%86%E4%B8%BA%E5%AD%97%E8%8A%82%E6%B5%81%E5%92%8C%E5%AD%97%E7%AC%A6%E6%B5%81%E5%91%A2">I/O 流为什么要分为字节流和字符流呢?</a></li>
<li><a href="#java-io%E4%B8%AD%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F">Java IO 中的设计模式有哪些？</a></li>
<li><a href="#bio%E3%80%81nio%E5%92%8C-aio%E7%9A%84%E5%8C%BA%E5%88%AB%EF%BC%9F">BIO、NIO 和 AIO 的区别？</a></li>
</ul>
</li>
<li><a href="#%E8%AF%AD%E6%B3%95%E7%B3%96">语法糖</a>
<ul>
<li><a href="#%E4%BB%80%E4%B9%88%E6%98%AF%E8%AF%AD%E6%B3%95%E7%B3%96%EF%BC%9F">什么是语法糖？</a></li>
<li><a href="#java%E4%B8%AD%E6%9C%89%E5%93%AA%E4%BA%9B%E5%B8%B8%E8%A7%81%E7%9A%84%E8%AF%AD%E6%B3%95%E7%B3%96%EF%BC%9F">Java 中有哪些常见的语法糖？</a></li>
</ul>
</li>
</ul>
<h2><a id="%E5%BC%82%E5%B8%B8" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>异常</h2>
<p><strong>Java 异常类层次结构图概览</strong>：</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/17/b1a02356-7f30-4ede-a7db-a6ab49cb9e0b.png" alt="Java 异常类层次结构图" /></p>
<h3><a id="exception%E5%92%8C-error%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Exception 和 Error 有什么区别？</h3>
<p>在 Java 中，所有的异常都有一个共同的祖先 <code>java.lang</code> 包中的 <code>Throwable</code> 类。<code>Throwable</code> 类有两个重要的子类:</p>
<ul>
<li><strong><code>Exception</code></strong> :程序本身可以处理的异常，可以通过 <code>catch</code> 来进行捕获。<code>Exception</code> 又可以分为 Checked Exception (受检查异常，必须处理) 和 Unchecked Exception (不受检查异常，可以不处理)。</li>
<li><strong><code>Error</code></strong>：<code>Error</code> 属于程序无法处理的错误 ，<del>我们没办法通过 <code>catch</code> 来进行捕获</del>不建议通过<code>catch</code>捕获 。例如 Java 虚拟机运行错误（<code>Virtual MachineError</code>）、虚拟机内存不够错误(<code>OutOfMemoryError</code>)、类定义错误（<code>NoClassDefFoundError</code>）等 。这些异常发生时，Java 虚拟机（JVM）一般会选择线程终止。</li>
</ul>
<h3><a id="checked-exception%E5%92%8C-unchecked-exception%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Checked Exception 和 Unchecked Exception 有什么区别？</h3>
<p><strong>Checked Exception</strong> 即 受检查异常 ，Java 代码在编译过程中，如果受检查异常没有被 <code>catch</code>或者<code>throws</code> 关键字处理的话，就没办法通过编译。</p>
<p>比如下面这段 IO 操作的代码：</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/17/05f753f8-2afb-4524-a1d1-4b5f37e357f1.png" alt="" /></p>
<p>除了<code>RuntimeException</code>及其子类以外，其他的<code>Exception</code>类及其子类都属于受检查异常 。常见的受检查异常有：IO 相关的异常、<code>ClassNotFoundException</code>、<code>SQLException</code>...。</p>
<p><strong>Unchecked Exception</strong> 即 <strong>不受检查异常</strong> ，Java 代码在编译过程中 ，我们即使不处理不受检查异常也可以正常通过编译。</p>
<p><code>RuntimeException</code> 及其子类都统称为非受检查异常，常见的有（建议记下来，日常开发中会经常用到）：</p>
<ul>
<li><code>NullPointerException</code>(空指针错误)</li>
<li><code>IllegalArgumentException</code>(参数错误比如方法入参类型错误)</li>
<li><code>NumberFormatException</code>（字符串转换为数字格式错误，<code>IllegalArgumentException</code>的子类）</li>
<li><code>ArrayIndexOutOfBoundsException</code>（数组越界错误）</li>
<li><code>ClassCastException</code>（类型转换错误）</li>
<li><code>ArithmeticException</code>（算术错误）</li>
<li><code>SecurityException</code> （安全错误比如权限不够）</li>
<li><code>UnsupportedOperationException</code>(不支持的操作错误比如重复创建同一用户)</li>
<li>……</li>
</ul>
<p><img src="https://image.huanglei.work/mweb/2025/3/17/1eea0fb9-201e-4421-9c0b-dac158c2a5d4.png" alt="" /></p>
<h3><a id="throwable%E7%B1%BB%E5%B8%B8%E7%94%A8%E6%96%B9%E6%B3%95%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Throwable 类常用方法有哪些？</h3>
<ul>
<li><code>String getMessage()</code>: 返回异常发生时的详细信息</li>
<li><code>String toString()</code>: 返回异常发生时的简要描述</li>
<li><code>String getLocalizedMessage()</code>: 返回异常对象的本地化信息。使用 <code>Throwable</code> 的子类覆盖这个方法，可以生成本地化信息。如果子类没有覆盖该方法，则该方法返回的信息与 <code>getMessage()</code>返回的结果相同</li>
<li><code>void printStackTrace()</code>: 在控制台上打印 <code>Throwable</code> 对象封装的异常信息</li>
</ul>
<h3><a id="try-catch-finally%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>try-catch-finally 如何使用？</h3>
<ul>
<li><code>try</code>块：用于捕获异常。其后可接零个或多个 <code>catch</code> 块，如果没有 <code>catch</code> 块，则必须跟一个 <code>finally</code> 块。</li>
<li><code>catch</code>块：用于处理 try 捕获到的异常。</li>
<li><code>finally</code> 块：无论是否捕获或处理异常，<code>finally</code> 块里的语句都会被执行。当在 <code>try</code> 块或 <code>catch</code> 块中遇到 <code>return</code> 语句时，<code>finally</code> 语句块将在方法返回之前被执行。</li>
</ul>
<p>代码示例：</p>
<pre><code class="language-java">try {
    System.out.println(&quot;Try to do something&quot;);
    throw new RuntimeException(&quot;RuntimeException&quot;);
} catch (Exception e) {
    System.out.println(&quot;Catch Exception -&gt; &quot; + e.getMessage());
} finally {
    System.out.println(&quot;Finally&quot;);
}
</code></pre>
<p>输出：</p>
<pre><code class="language-plain">Try to do something
Catch Exception -&gt; RuntimeException
Finally
</code></pre>
<p><strong>注意：不要在 finally 语句块中使用 return!</strong> 当 try 语句和 finally 语句中都有 return 语句时，try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中，当执行到 finally 语句中的 return 之后，这个本地变量的值就变为了 finally 语句中的 return 返回值。</p>
<p>代码示例：</p>
<pre><code class="language-java">public static void main(String[] args) {
    System.out.println(f(2));
}

public static int f(int value) {
    try {
        return value * value;
    } finally {
        if (value == 2) {
            return 0;
        }
    }
}
</code></pre>
<p>输出：</p>
<pre><code class="language-plain">0
</code></pre>
<h3><a id="finally%E4%B8%AD%E7%9A%84%E4%BB%A3%E7%A0%81%E4%B8%80%E5%AE%9A%E4%BC%9A%E6%89%A7%E8%A1%8C%E5%90%97%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>finally 中的代码一定会执行吗？</h3>
<p>不一定的！在某些情况下，finally 中的代码不会被执行。</p>
<p>就比如说 finally 之前虚拟机被终止运行的话，finally 中的代码就不会被执行。</p>
<pre><code class="language-java">try {
    System.out.println(&quot;Try to do something&quot;);
    throw new RuntimeException(&quot;RuntimeException&quot;);
} catch (Exception e) {
    System.out.println(&quot;Catch Exception -&gt; &quot; + e.getMessage());
    // 终止当前正在运行的Java虚拟机
    System.exit(1);
} finally {
    System.out.println(&quot;Finally&quot;);
}
</code></pre>
<p>输出：</p>
<pre><code class="language-plain">Try to do something
Catch Exception -&gt; RuntimeException
</code></pre>
<p>另外，在以下 2 种特殊情况下，<code>finally</code> 块的代码也不会被执行：</p>
<ol>
<li>程序所在的线程死亡。</li>
<li>关闭 CPU。</li>
</ol>
<p>相关 issue：<a href="https://github.com/Snailclimb/JavaGuide/issues/190">https://github.com/Snailclimb/JavaGuide/issues/190</a>。</p>
<p>🧗🏻 进阶一下：从字节码角度分析<code>try catch finally</code>这个语法糖背后的实现原理。</p>
<h3><a id="%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8try-with-resources%E4%BB%A3%E6%9B%BF-try-catch-finally%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>如何使用 <code>try-with-resources</code> 代替<code>try-catch-finally</code>？</h3>
<ol>
<li><strong>适用范围（资源的定义）：</strong> 任何实现 <code>java.lang.AutoCloseable</code>或者 <code>java.io.Closeable</code> 的对象</li>
<li><strong>关闭资源和 finally 块的执行顺序：</strong> 在 <code>try-with-resources</code> 语句中，任何 catch 或 finally 块在声明的资源关闭后运行</li>
</ol>
<p>《Effective Java》中明确指出：</p>
<blockquote>
<p>面对必须要关闭的资源，我们总是应该优先使用 <code>try-with-resources</code> 而不是<code>try-finally</code>。随之产生的代码更简短，更清晰，产生的异常对我们也更有用。<code>try-with-resources</code>语句让我们更容易编写必须要关闭的资源的代码，若采用<code>try-finally</code>则几乎做不到这点。</p>
</blockquote>
<p>Java 中类似于<code>InputStream</code>、<code>OutputStream</code>、<code>Scanner</code>、<code>PrintWriter</code>等的资源都需要我们调用<code>close()</code>方法来手动关闭，一般情况下我们都是通过<code>try-catch-finally</code>语句来实现这个需求，如下：</p>
<pre><code class="language-java">//读取文本文件的内容
Scanner scanner = null;
try {
    scanner = new Scanner(new File(&quot;D://read.txt&quot;));
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException e) {
    e.printStackTrace();
} finally {
    if (scanner != null) {
        scanner.close();
    }
}
</code></pre>
<p>使用 Java 7 之后的 <code>try-with-resources</code> 语句改造上面的代码:</p>
<pre><code class="language-java">try (Scanner scanner = new Scanner(new File(&quot;test.txt&quot;))) {
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException fnfe) {
    fnfe.printStackTrace();
}
</code></pre>
<p>当然多个资源需要关闭的时候，使用 <code>try-with-resources</code> 实现起来也非常简单，如果你还是用<code>try-catch-finally</code>可能会带来很多问题。</p>
<p>通过使用分号分隔，可以在<code>try-with-resources</code>块中声明多个资源。</p>
<pre><code class="language-java">try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File(&quot;test.txt&quot;)));
     BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File(&quot;out.txt&quot;)))) {
    int b;
    while ((b = bin.read()) != -1) {
        bout.write(b);
    }
}
catch (IOException e) {
    e.printStackTrace();
}
</code></pre>
<h3><a id="%E5%BC%82%E5%B8%B8%E4%BD%BF%E7%94%A8%E6%9C%89%E5%93%AA%E4%BA%9B%E9%9C%80%E8%A6%81%E6%B3%A8%E6%84%8F%E7%9A%84%E5%9C%B0%E6%96%B9%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>异常使用有哪些需要注意的地方？</h3>
<ul>
<li>不要把异常定义为静态变量，因为这样会导致异常栈信息错乱。每次手动抛出异常，我们都需要手动 new 一个异常对象抛出。</li>
<li>抛出的异常信息一定要有意义。</li>
<li>建议抛出更加具体的异常，比如字符串转换为数字格式错误的时候应该抛出<code>NumberFormatException</code>而不是其父类<code>IllegalArgumentException</code>。</li>
<li>避免重复记录日志：如果在捕获异常的地方已经记录了足够的信息（包括异常类型、错误信息和堆栈跟踪等），那么在业务代码中再次抛出这个异常时，就不应该再次记录相同的错误信息。重复记录日志会使得日志文件膨胀，并且可能会掩盖问题的实际原因，使得问题更难以追踪和解决。</li>
<li>……</li>
</ul>
<h2><a id="%E6%B3%9B%E5%9E%8B" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>泛型</h2>
<h3><a id="%E4%BB%80%E4%B9%88%E6%98%AF%E6%B3%9B%E5%9E%8B%EF%BC%9F%E6%9C%89%E4%BB%80%E4%B9%88%E4%BD%9C%E7%94%A8%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>什么是泛型？有什么作用？</h3>
<p><strong>Java 泛型（Generics）</strong> 是 JDK 5 中引入的一个新特性。使用泛型参数，可以增强代码的可读性以及稳定性。</p>
<p>编译器可以对泛型参数进行检测，并且通过泛型参数可以指定传入的对象类型。比如 <code>ArrayList&lt;Person&gt; persons = new ArrayList&lt;Person&gt;()</code> 这行代码就指明了该 <code>ArrayList</code> 对象只能传入 <code>Person</code> 对象，如果传入其他类型的对象就会报错。</p>
<pre><code class="language-java">ArrayList&lt;E&gt; extends AbstractList&lt;E&gt;
</code></pre>
<p>并且，原生 <code>List</code> 返回类型是 <code>Object</code> ，需要手动转换类型才能使用，使用泛型后编译器自动转换。</p>
<h3><a id="%E6%B3%9B%E5%9E%8B%E7%9A%84%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F%E6%9C%89%E5%93%AA%E5%87%A0%E7%A7%8D%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>泛型的使用方式有哪几种？</h3>
<p>泛型一般有三种使用方式:<strong>泛型类</strong>、<strong>泛型接口</strong>、<strong>泛型方法</strong>。</p>
<p><strong>1.泛型类</strong>：</p>
<pre><code class="language-java">//此处T可以随便写为任意标识，常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时，必须指定T的具体类型
public class Generic&lt;T&gt;{

    private T key;

    public Generic(T key) {
        this.key = key;
    }

    public T getKey(){
        return key;
    }
}
</code></pre>
<p>如何实例化泛型类：</p>
<pre><code class="language-java">Generic&lt;Integer&gt; genericInteger = new Generic&lt;Integer&gt;(123456);
</code></pre>
<p><strong>2.泛型接口</strong>：</p>
<pre><code class="language-java">public interface Generator&lt;T&gt; {
    public T method();
}
</code></pre>
<p>实现泛型接口，不指定类型：</p>
<pre><code class="language-java">class GeneratorImpl&lt;T&gt; implements Generator&lt;T&gt;{
    @Override
    public T method() {
        return null;
    }
}
</code></pre>
<p>实现泛型接口，指定类型：</p>
<pre><code class="language-java">class GeneratorImpl implements Generator&lt;String&gt; {
    @Override
    public String method() {
        return &quot;hello&quot;;
    }
}
</code></pre>
<p><strong>3.泛型方法</strong>：</p>
<pre><code class="language-java">   public static &lt; E &gt; void printArray( E[] inputArray )
   {
         for ( E element : inputArray ){
            System.out.printf( &quot;%s &quot;, element );
         }
         System.out.println();
    }
</code></pre>
<p>使用：</p>
<pre><code class="language-java">// 创建不同类型数组：Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { &quot;Hello&quot;, &quot;World&quot; };
printArray( intArray  );
printArray( stringArray  );
</code></pre>
<blockquote>
<p>注意: <code>public static &lt; E &gt; void printArray( E[] inputArray )</code> 一般被称为静态泛型方法;在 java 中泛型只是一个占位符，必须在传递类型后才能使用。类在实例化时才能真正的传递类型参数，由于静态方法的加载先于类的实例化，也就是说类中的泛型还没有传递真正的类型参数，静态的方法的加载就已经完成了，所以静态泛型方法是没有办法使用类上声明的泛型的。只能使用自己声明的 <code>&lt;E&gt;</code></p>
</blockquote>
<h3><a id="%E9%A1%B9%E7%9B%AE%E4%B8%AD%E5%93%AA%E9%87%8C%E7%94%A8%E5%88%B0%E4%BA%86%E6%B3%9B%E5%9E%8B%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>项目中哪里用到了泛型？</h3>
<ul>
<li>自定义接口通用返回结果 <code>CommonResult&lt;T&gt;</code> 通过参数 <code>T</code> 可根据具体的返回类型动态指定结果的数据类型</li>
<li>定义 <code>Excel</code> 处理类 <code>ExcelUtil&lt;T&gt;</code> 用于动态指定 <code>Excel</code> 导出的数据类型</li>
<li>构建集合工具类（参考 <code>Collections</code> 中的 <code>sort</code>, <code>binarySearch</code> 方法）。</li>
<li>……</li>
</ul>
<h2><a id="%E5%8F%8D%E5%B0%84" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>反射</h2>
<p>关于反射的详细解读，请看这篇文章 <a href="17420865516849.html">Java 反射机制详解</a> 。</p>
<h3><a id="%E4%BD%95%E8%B0%93%E5%8F%8D%E5%B0%84%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>何谓反射？</h3>
<p>如果说大家研究过框架的底层原理或者咱们自己写过框架的话，一定对反射这个概念不陌生。反射之所以被称为框架的灵魂，主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。通过反射你可以获取任意一个类的所有属性和方法，你还可以调用这些方法和属性。</p>
<h3><a id="%E5%8F%8D%E5%B0%84%E7%9A%84%E4%BC%98%E7%BC%BA%E7%82%B9%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>反射的优缺点？</h3>
<p>反射可以让我们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利。</p>
<p>不过，反射让我们在运行时有了分析操作类的能力的同时，也增加了安全问题，比如可以无视泛型参数的安全检查（泛型参数的安全检查发生在编译时）。另外，反射的性能也要稍差点，不过，对于框架来说实际是影响不大的。</p>
<p>相关阅读：<a href="https://stackoverflow.com/questions/1392351/java-reflection-why-is-it-so-slow">Java Reflection: Why is it so slow?</a> 。</p>
<h3><a id="%E5%8F%8D%E5%B0%84%E7%9A%84%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>反射的应用场景？</h3>
<p>像咱们平时大部分时候都是在写业务代码，很少会接触到直接使用反射机制的场景。但是！这并不代表反射没有用。相反，正是因为反射，你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。</p>
<p><strong>这些框架中也大量使用了动态代理，而动态代理的实现也依赖反射。</strong></p>
<p>比如下面是通过 JDK 实现动态代理的示例代码，其中就使用了反射类 <code>Method</code> 来调用指定的方法。</p>
<pre><code class="language-java">public class DebugInvocationHandler implements InvocationHandler {
    /**
     * 代理类中的真实对象
     */
    private final Object target;

    public DebugInvocationHandler(Object target) {
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
        System.out.println(&quot;before method &quot; + method.getName());
        Object result = method.invoke(target, args);
        System.out.println(&quot;after method &quot; + method.getName());
        return result;
    }
}

</code></pre>
<p>另外，像 Java 中的一大利器 <strong>注解</strong> 的实现也用到了反射。</p>
<p>为什么你使用 Spring 的时候 ，一个<code>@Component</code>注解就声明了一个类为 Spring Bean 呢？为什么你通过一个 <code>@Value</code>注解就读取到配置文件中的值呢？究竟是怎么起作用的呢？</p>
<p>这些都是因为你可以基于反射分析类，然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后，就可以做进一步的处理。</p>
<h2><a id="%E6%B3%A8%E8%A7%A3" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>注解</h2>
<h3><a id="%E4%BD%95%E8%B0%93%E6%B3%A8%E8%A7%A3%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>何谓注解？</h3>
<p><code>Annotation</code> （注解） 是 Java5 开始引入的新特性，可以看作是一种特殊的注释，主要用于修饰类、方法或者变量，提供某些信息供程序在编译或者运行时使用。</p>
<p>注解本质是一个继承了<code>Annotation</code> 的特殊接口：</p>
<pre><code class="language-java">@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {

}

public interface Override extends Annotation{

}
</code></pre>
<p>JDK 提供了很多内置的注解（比如 <code>@Override</code>、<code>@Deprecated</code>），同时，我们还可以自定义注解。</p>
<h3><a id="%E6%B3%A8%E8%A7%A3%E7%9A%84%E8%A7%A3%E6%9E%90%E6%96%B9%E6%B3%95%E6%9C%89%E5%93%AA%E5%87%A0%E7%A7%8D%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>注解的解析方法有哪几种？</h3>
<p>注解只有被解析之后才会生效，常见的解析方法有两种：</p>
<ul>
<li><strong>编译期直接扫描</strong>：编译器在编译 Java 代码的时候扫描对应的注解并处理，比如某个方法使用<code>@Override</code> 注解，编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。</li>
<li><strong>运行期通过反射处理</strong>：像框架中自带的注解(比如 Spring 框架的 <code>@Value</code>、<code>@Component</code>)都是通过反射来进行处理的。</li>
</ul>
<h2><a id="spi" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>SPI</h2>
<p>关于 SPI 的详细解读，请看这篇文章 <a href="17420865516757.html">Java SPI 机制详解</a> 。</p>
<h3><a id="%E4%BD%95%E8%B0%93spi" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>何谓 SPI?</h3>
<p>SPI 即 Service Provider Interface ，字面意思就是：“服务提供者的接口”，我的理解是：专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。</p>
<p>SPI 将服务接口和具体的服务实现分离开来，将服务调用方和服务实现者解耦，能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。</p>
<p>很多框架都使用了 Java 的 SPI 机制，比如：Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。</p>
<img src="https://image.huanglei.work/mweb/2025/3/17/9c45f733-aeb2-4e0d-9e97-e8adf06f1335.jpg" style="zoom:50%;" />
<h3><a id="spi%E5%92%8C-api%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>SPI 和 API 有什么区别？</h3>
<p><strong>那 SPI 和 API 有啥区别？</strong></p>
<p>说到 SPI 就不得不说一下 API（Application Programming Interface） 了，从广义上来说它们都属于接口，而且很容易混淆。下面先用一张图说明一下：</p>
<p><img src="https://image.huanglei.work/mweb/2025/3/17/b95441b8-51a5-4132-8c5d-9880e514c0ca.png" alt="SPI VS API" /></p>
<p>一般模块之间都是通过接口进行通讯，因此我们在服务调用方和服务实现方（也称服务提供者）之间引入一个“接口”。</p>
<ul>
<li>当实现方提供了接口和实现，我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力，这就是 <strong>API</strong>。这种情况下，接口和实现都是放在实现方的包中。调用方通过接口调用实现方的功能，而不需要关心具体的实现细节。</li>
<li>当接口存在于调用方这边时，这就是 <strong>SPI</strong> 。由接口调用方确定接口规则，然后由不同的厂商根据这个规则对这个接口进行实现，从而提供服务。</li>
</ul>
<p>举个通俗易懂的例子：公司 H 是一家科技公司，新设计了一款芯片，然后现在需要量产了，而市面上有好几家芯片制造业公司，这个时候，只要 H 公司指定好了这芯片生产的标准（定义好了接口标准），那么这些合作的芯片公司（服务提供者）就按照标准交付自家特色的芯片（提供不同方案的实现，但是给出来的结果是一样的）。</p>
<h3><a id="spi%E7%9A%84%E4%BC%98%E7%BC%BA%E7%82%B9%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>SPI 的优缺点？</h3>
<p>通过 SPI 机制能够大大地提高接口设计的灵活性，但是 SPI 机制也存在一些缺点，比如：</p>
<ul>
<li>需要遍历加载所有的实现类，不能做到按需加载，这样效率还是相对较低的。</li>
<li>当多个 <code>ServiceLoader</code> 同时 <code>load</code> 时，会有并发问题。</li>
</ul>
<h2><a id="%E5%BA%8F%E5%88%97%E5%8C%96%E5%92%8C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>序列化和反序列化</h2>
<p>关于序列化和反序列化的详细解读，请看这篇文章 <a href="17420865516806.html">Java 序列化详解</a> ，里面涉及到的知识点和面试题更全面。</p>
<h3><a id="%E4%BB%80%E4%B9%88%E6%98%AF%E5%BA%8F%E5%88%97%E5%8C%96%E4%BB%80%E4%B9%88%E6%98%AF%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>什么是序列化?什么是反序列化?</h3>
<p>如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中，或者在网络传输 Java 对象，这些场景都需要用到序列化。</p>
<p>简单来说：</p>
<ul>
<li><strong>序列化</strong>：将数据结构或对象转换成可以存储或传输的形式，通常是二进制字节流，也可以是 JSON, XML 等文本格式</li>
<li><strong>反序列化</strong>：将在序列化过程中所生成的数据转换为原始数据结构或者对象的过程</li>
</ul>
<p>对于 Java 这种面向对象编程语言来说，我们序列化的都是对象（Object）也就是实例化后的类(Class)，但是在 C++这种半面向对象的语言中，struct(结构体)定义的是数据结构类型，而 class 对应的是对象类型。</p>
<p>下面是序列化和反序列化常见应用场景：</p>
<ul>
<li>对象在进行网络传输（比如远程方法调用 RPC 的时候）之前需要先被序列化，接收到序列化的对象之后需要再进行反序列化；</li>
<li>将对象存储到文件之前需要进行序列化，将对象从文件中读取出来需要进行反序列化；</li>
<li>将对象存储到数据库（如 Redis）之前需要用到序列化，将对象从缓存数据库中读取出来需要反序列化；</li>
<li>将对象存储到内存之前需要进行序列化，从内存中读取出来之后需要进行反序列化。</li>
</ul>
<p>维基百科是如是介绍序列化的：</p>
<blockquote>
<p><strong>序列化</strong>（serialization）在计算机科学的数据处理中，是指将数据结构或对象状态转换成可取用格式（例如存成文件，存于缓冲，或经由网络中发送），以留待后续在相同或另一台计算机环境中，能恢复原先状态的过程。依照序列化格式重新获取字节的结果时，可以利用它来产生与原始对象相同语义的副本。对于许多对象，像是使用大量引用的复杂对象，这种序列化重建的过程并不容易。面向对象中的对象序列化，并不概括之前原始对象所关系的函数。这种过程也称为对象编组（marshalling）。从一系列字节提取数据结构的反向操作，是反序列化（也称为解编组、deserialization、unmarshalling）。</p>
</blockquote>
<p>综上：<strong>序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。</strong></p>
<p><img src="https://image.huanglei.work/mweb/2025/3/17/0a973030-2020-43b3-97d6-c78478b26877.png" alt="" /></p>
<p style="text-align:right;font-size:13px;color:gray">https://www.corejavaguru.com/java/serialization/interview-questions-1</p>
<p><strong>序列化协议对应于 TCP/IP 4 层模型的哪一层？</strong></p>
<p>我们知道网络通信的双方必须要采用和遵守相同的协议。TCP/IP 四层模型是下面这样的，序列化协议属于哪一层呢？</p>
<ol>
<li>应用层</li>
<li>传输层</li>
<li>网络层</li>
<li>网络接口层</li>
</ol>
<p><img src="https://image.huanglei.work/mweb/2025/3/17/61d1afd9-8c18-499d-b4a4-2730c196ee0a.png" alt="TCP/IP 四层模型" /></p>
<p>如上图所示，OSI 七层协议模型中，表示层做的事情主要就是对应用层的用户数据进行处理转换为二进制流。反过来的话，就是将二进制流转换成应用层的用户数据。这不就对应的是序列化和反序列化么？</p>
<p>因为，OSI 七层协议模型中的应用层、表示层和会话层对应的都是 TCP/IP 四层模型中的应用层，所以序列化协议属于 TCP/IP 协议应用层的一部分。</p>
<h3><a id="%E5%A6%82%E6%9E%9C%E6%9C%89%E4%BA%9B%E5%AD%97%E6%AE%B5%E4%B8%8D%E6%83%B3%E8%BF%9B%E8%A1%8C%E5%BA%8F%E5%88%97%E5%8C%96%E6%80%8E%E4%B9%88%E5%8A%9E%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>如果有些字段不想进行序列化怎么办？</h3>
<p>对于不想进行序列化的变量，使用 <code>transient</code> 关键字修饰。</p>
<p><code>transient</code> 关键字的作用是：阻止实例中那些用此关键字修饰的的变量序列化；当对象被反序列化时，被 <code>transient</code> 修饰的变量值不会被持久化和恢复。</p>
<p>关于 <code>transient</code> 还有几点注意：</p>
<ul>
<li><code>transient</code> 只能修饰变量，不能修饰类和方法。</li>
<li><code>transient</code> 修饰的变量，在反序列化后变量值将会被置成类型的默认值。例如，如果是修饰 <code>int</code> 类型，那么反序列后结果就是 <code>0</code>。</li>
<li><code>static</code> 变量因为不属于任何对象(Object)，所以无论有没有 <code>transient</code> 关键字修饰，均不会被序列化。</li>
</ul>
<h3><a id="%E5%B8%B8%E8%A7%81%E5%BA%8F%E5%88%97%E5%8C%96%E5%8D%8F%E8%AE%AE%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>常见序列化协议有哪些？</h3>
<p>JDK 自带的序列化方式一般不会用 ，因为序列化效率低并且存在安全问题。比较常用的序列化协议有 Hessian、Kryo、Protobuf、ProtoStuff，这些都是基于二进制的序列化协议。</p>
<p>像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好，但是性能较差，一般不会选择。</p>
<h3><a id="%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E6%8E%A8%E8%8D%90%E4%BD%BF%E7%94%A8jdk%E8%87%AA%E5%B8%A6%E7%9A%84%E5%BA%8F%E5%88%97%E5%8C%96%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>为什么不推荐使用 JDK 自带的序列化？</h3>
<p>我们很少或者说几乎不会直接使用 JDK 自带的序列化方式，主要原因有下面这些原因：</p>
<ul>
<li><strong>不支持跨语言调用</strong> : 如果调用的是其他语言开发的服务的时候就不支持了。</li>
<li><strong>性能差</strong>：相比于其他序列化框架性能更低，主要原因是序列化之后的字节数组体积较大，导致传输成本加大。</li>
<li><strong>存在安全问题</strong>：序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制，那么攻击者即可通过构造恶意输入，让反序列化产生非预期的对象，在此过程中执行构造的任意代码。相关阅读：<a href="https://cryin.github.io/blog/secure-development-java-deserialization-vulnerability/">应用安全：JAVA 反序列化漏洞之殇</a> 。</li>
</ul>
<h2><a id="io" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>I/O</h2>
<p>关于 I/O 的详细解读，请看下面这几篇文章，里面涉及到的知识点和面试题更全面。</p>
<ul>
<li><a href="17420876681975.html">Java IO 基础知识总结</a></li>
<li><a href="17420876681916.html">Java IO 设计模式总结</a></li>
<li><a href="17420876681854.html">Java IO 模型详解</a></li>
</ul>
<h3><a id="java-io%E6%B5%81%E4%BA%86%E8%A7%A3%E5%90%97%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Java IO 流了解吗？</h3>
<p>IO 即 <code>Input/Output</code>，输入和输出。数据输入到计算机内存的过程即输入，反之输出到外部存储（比如数据库，文件，远程主机）的过程即输出。数据传输过程类似于水流，因此称为 IO 流。IO 流在 Java 中分为输入流和输出流，而根据数据的处理方式又分为字节流和字符流。</p>
<p>Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。</p>
<ul>
<li><code>InputStream</code>/<code>Reader</code>: 所有的输入流的基类，前者是字节输入流，后者是字符输入流。</li>
<li><code>OutputStream</code>/<code>Writer</code>: 所有输出流的基类，前者是字节输出流，后者是字符输出流。</li>
</ul>
<h3><a id="io%E6%B5%81%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E5%88%86%E4%B8%BA%E5%AD%97%E8%8A%82%E6%B5%81%E5%92%8C%E5%AD%97%E7%AC%A6%E6%B5%81%E5%91%A2" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>I/O 流为什么要分为字节流和字符流呢?</h3>
<p>问题本质想问：<strong>不管是文件读写还是网络发送接收，信息的最小存储单元都是字节，那为什么 I/O 流操作要分为字节流操作和字符流操作呢？</strong></p>
<p>个人认为主要有两点原因：</p>
<ul>
<li>字符流是由 Java 虚拟机将字节转换得到的，这个过程还算是比较耗时；</li>
<li>如果我们不知道编码类型的话，使用字节流的过程中很容易出现乱码问题。</li>
</ul>
<h3><a id="java-io%E4%B8%AD%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Java IO 中的设计模式有哪些？</h3>
<p>参考答案：<a href="17420876681916.html">Java IO 设计模式总结</a></p>
<h3><a id="bio%E3%80%81nio%E5%92%8C-aio%E7%9A%84%E5%8C%BA%E5%88%AB%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>BIO、NIO 和 AIO 的区别？</h3>
<p>参考答案：<a href="17420876681854.html">Java IO 模型详解</a></p>
<h2><a id="%E8%AF%AD%E6%B3%95%E7%B3%96" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>语法糖</h2>
<h3><a id="%E4%BB%80%E4%B9%88%E6%98%AF%E8%AF%AD%E6%B3%95%E7%B3%96%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>什么是语法糖？</h3>
<p><strong>语法糖（Syntactic sugar）</strong> 代指的是编程语言为了方便程序员开发程序而设计的一种特殊语法，这种语法对编程语言的功能并没有影响。实现相同的功能，基于语法糖写出来的代码往往更简单简洁且更易阅读。</p>
<p>举个例子，Java 中的 <code>for-each</code> 就是一个常用的语法糖，其原理其实就是基于普通的 for 循环和迭代器。</p>
<pre><code class="language-java">String[] strs = {&quot;JavaGuide&quot;, &quot;公众号：JavaGuide&quot;, &quot;博客：https://javaguide.cn/&quot;};
for (String s : strs) {
    System.out.println(s);
}
</code></pre>
<p>不过，JVM 其实并不能识别语法糖，Java 语法糖要想被正确执行，需要先通过编译器进行解糖，也就是在程序编译阶段将其转换成 JVM 认识的基本语法。这也侧面说明，Java 中真正支持语法糖的是 Java 编译器而不是 JVM。如果你去看<code>com.sun.tools.javac.main.JavaCompiler</code>的源码，你会发现在<code>compile()</code>中有一个步骤就是调用<code>desugar()</code>，这个方法就是负责解语法糖的实现的。</p>
<h3><a id="java%E4%B8%AD%E6%9C%89%E5%93%AA%E4%BA%9B%E5%B8%B8%E8%A7%81%E7%9A%84%E8%AF%AD%E6%B3%95%E7%B3%96%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Java 中有哪些常见的语法糖？</h3>
<p>Java 中最常用的语法糖主要有泛型、自动拆装箱、变长参数、枚举、内部类、增强 for 循环、try-with-resources 语法、lambda 表达式等。</p>
<p>关于这些语法糖的详细解读，请看这篇文章 <a href="17420865516706.html">Java 语法糖详解</a> 。</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[03-Java并发常见面试题总结（下）]]></title>
    <link href="https://huanglei.work/17419985101934.html"/>
    <updated>2025-03-15T08:28:30+08:00</updated>
    <id>https://huanglei.work/17419985101934.html</id>
    <content type="html"><![CDATA[
<blockquote>
<p>本文内容来自：<a href="https://javaguide.cn/java/concurrent/java-concurrent-questions-03.html">JavaGuide-Java并发常见面试题总结（下）</a></p>
</blockquote>
<ul>
<li><a href="#threadlocal">ThreadLocal</a>
<ul>
<li><a href="#threadlocal%E6%9C%89%E4%BB%80%E4%B9%88%E7%94%A8%EF%BC%9F">ThreadLocal 有什么用？</a></li>
<li><a href="#%E2%AD%90%EF%B8%8Fthreadlocal%E5%8E%9F%E7%90%86%E4%BA%86%E8%A7%A3%E5%90%97%EF%BC%9F">⭐️ThreadLocal 原理了解吗？</a></li>
<li><a href="#%E2%AD%90%EF%B8%8Fthreadlocal%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E9%97%AE%E9%A2%98%E6%98%AF%E6%80%8E%E4%B9%88%E5%AF%BC%E8%87%B4%E7%9A%84%EF%BC%9F">⭐️ThreadLocal 内存泄露问题是怎么导致的？</a></li>
<li><a href="#%E2%AD%90%EF%B8%8F%E5%A6%82%E4%BD%95%E8%B7%A8%E7%BA%BF%E7%A8%8B%E4%BC%A0%E9%80%92threadlocal%E7%9A%84%E5%80%BC%EF%BC%9F">⭐️如何跨线程传递 ThreadLocal 的值？</a>
<ul>
<li><a href="#inheritablethreadlocal%E5%8E%9F%E7%90%86">InheritableThreadLocal 原理</a></li>
<li><a href="#transmittablethreadlocal%E5%8E%9F%E7%90%86">TransmittableThreadLocal 原理</a></li>
<li><a href="#%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF">应用场景</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#%E7%BA%BF%E7%A8%8B%E6%B1%A0">线程池</a>
<ul>
<li><a href="#%E4%BB%80%E4%B9%88%E6%98%AF%E7%BA%BF%E7%A8%8B%E6%B1%A0">什么是线程池?</a></li>
<li><a href="#%E2%AD%90%EF%B8%8F%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E7%94%A8%E7%BA%BF%E7%A8%8B%E6%B1%A0%EF%BC%9F">⭐️为什么要用线程池？</a></li>
<li><a href="#%E5%A6%82%E4%BD%95%E5%88%9B%E5%BB%BA%E7%BA%BF%E7%A8%8B%E6%B1%A0%EF%BC%9F">如何创建线程池？</a></li>
<li><a href="#%E2%AD%90%EF%B8%8F%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E6%8E%A8%E8%8D%90%E4%BD%BF%E7%94%A8%E5%86%85%E7%BD%AE%E7%BA%BF%E7%A8%8B%E6%B1%A0%EF%BC%9F">⭐️为什么不推荐使用内置线程池？</a></li>
<li><a href="#%E2%AD%90%EF%B8%8F%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%B8%B8%E8%A7%81%E5%8F%82%E6%95%B0%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F%E5%A6%82%E4%BD%95%E8%A7%A3%E9%87%8A%EF%BC%9F">⭐️线程池常见参数有哪些？如何解释？</a></li>
<li><a href="#%E7%BA%BF%E7%A8%8B%E6%B1%A0%E7%9A%84%E6%A0%B8%E5%BF%83%E7%BA%BF%E7%A8%8B%E4%BC%9A%E8%A2%AB%E5%9B%9E%E6%94%B6%E5%90%97%EF%BC%9F">线程池的核心线程会被回收吗？</a></li>
<li><a href="#%E6%A0%B8%E5%BF%83%E7%BA%BF%E7%A8%8B%E7%A9%BA%E9%97%B2%E6%97%B6%E5%A4%84%E4%BA%8E%E4%BB%80%E4%B9%88%E7%8A%B6%E6%80%81%EF%BC%9F">核心线程空闲时处于什么状态？</a></li>
<li><a href="#%E2%AD%90%EF%B8%8F%E7%BA%BF%E7%A8%8B%E6%B1%A0%E7%9A%84%E6%8B%92%E7%BB%9D%E7%AD%96%E7%95%A5%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F">⭐️线程池的拒绝策略有哪些？</a></li>
<li><a href="#%E5%A6%82%E6%9E%9C%E4%B8%8D%E5%85%81%E8%AE%B8%E4%B8%A2%E5%BC%83%E4%BB%BB%E5%8A%A1%EF%BC%8C%E5%BA%94%E8%AF%A5%E9%80%89%E6%8B%A9%E5%93%AA%E4%B8%AA%E6%8B%92%E7%BB%9D%E7%AD%96%E7%95%A5%EF%BC%9F">如果不允许丢弃任务，应该选择哪个拒绝策略？</a></li>
<li><a href="#callerrunspolicy%E6%8B%92%E7%BB%9D%E7%AD%96%E7%95%A5%E6%9C%89%E4%BB%80%E4%B9%88%E9%A3%8E%E9%99%A9%EF%BC%9F%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%EF%BC%9F">CallerRunsPolicy 拒绝策略有什么风险？如何解决？</a></li>
<li><a href="#%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%B8%B8%E7%94%A8%E7%9A%84%E9%98%BB%E5%A1%9E%E9%98%9F%E5%88%97%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F">线程池常用的阻塞队列有哪些？</a></li>
<li><a href="#%E2%AD%90%EF%B8%8F%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%A4%84%E7%90%86%E4%BB%BB%E5%8A%A1%E7%9A%84%E6%B5%81%E7%A8%8B%E4%BA%86%E8%A7%A3%E5%90%97%EF%BC%9F">⭐️线程池处理任务的流程了解吗？</a></li>
<li><a href="#%E2%AD%90%EF%B8%8F%E7%BA%BF%E7%A8%8B%E6%B1%A0%E4%B8%AD%E7%BA%BF%E7%A8%8B%E5%BC%82%E5%B8%B8%E5%90%8E%EF%BC%8C%E9%94%80%E6%AF%81%E8%BF%98%E6%98%AF%E5%A4%8D%E7%94%A8%EF%BC%9F">⭐️线程池中线程异常后，销毁还是复用？</a></li>
<li><a href="#%E2%AD%90%EF%B8%8F%E5%A6%82%E4%BD%95%E7%BB%99%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%91%BD%E5%90%8D%EF%BC%9F">⭐️如何给线程池命名？</a></li>
<li><a href="#%E5%A6%82%E4%BD%95%E8%AE%BE%E5%AE%9A%E7%BA%BF%E7%A8%8B%E6%B1%A0%E7%9A%84%E5%A4%A7%E5%B0%8F%EF%BC%9F">如何设定线程池的大小？</a></li>
<li><a href="#%E2%AD%90%EF%B8%8F%E5%A6%82%E4%BD%95%E5%8A%A8%E6%80%81%E4%BF%AE%E6%94%B9%E7%BA%BF%E7%A8%8B%E6%B1%A0%E7%9A%84%E5%8F%82%E6%95%B0%EF%BC%9F">⭐️如何动态修改线程池的参数？</a></li>
<li><a href="#%E2%AD%90%EF%B8%8F%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%E4%B8%80%E4%B8%AA%E8%83%BD%E5%A4%9F%E6%A0%B9%E6%8D%AE%E4%BB%BB%E5%8A%A1%E7%9A%84%E4%BC%98%E5%85%88%E7%BA%A7%E6%9D%A5%E6%89%A7%E8%A1%8C%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%B1%A0%EF%BC%9F">⭐️如何设计一个能够根据任务的优先级来执行的线程池？</a></li>
</ul>
</li>
<li><a href="#future">Future</a>
<ul>
<li><a href="#future%E7%B1%BB%E6%9C%89%E4%BB%80%E4%B9%88%E7%94%A8%EF%BC%9F">Future 类有什么用？</a></li>
<li><a href="#callable%E5%92%8C-future%E6%9C%89%E4%BB%80%E4%B9%88%E5%85%B3%E7%B3%BB%EF%BC%9F">Callable 和 Future 有什么关系？</a></li>
<li><a href="#completablefuture%E7%B1%BB%E6%9C%89%E4%BB%80%E4%B9%88%E7%94%A8%EF%BC%9F">CompletableFuture 类有什么用？</a></li>
<li><a href="#%E2%AD%90%EF%B8%8F%E4%B8%80%E4%B8%AA%E4%BB%BB%E5%8A%A1%E9%9C%80%E8%A6%81%E4%BE%9D%E8%B5%96%E5%8F%A6%E5%A4%96%E4%B8%A4%E4%B8%AA%E4%BB%BB%E5%8A%A1%E6%89%A7%E8%A1%8C%E5%AE%8C%E4%B9%8B%E5%90%8E%E5%86%8D%E6%89%A7%E8%A1%8C%EF%BC%8C%E6%80%8E%E4%B9%88%E8%AE%BE%E8%AE%A1%EF%BC%9F">⭐️一个任务需要依赖另外两个任务执行完之后再执行，怎么设计？</a></li>
<li><a href="#%E2%AD%90%EF%B8%8F%E4%BD%BF%E7%94%A8completablefuture%EF%BC%8C%E6%9C%89%E4%B8%80%E4%B8%AA%E4%BB%BB%E5%8A%A1%E5%A4%B1%E8%B4%A5%EF%BC%8C%E5%A6%82%E4%BD%95%E5%A4%84%E7%90%86%E5%BC%82%E5%B8%B8%EF%BC%9F">⭐️使用 CompletableFuture，有一个任务失败，如何处理异常？</a></li>
<li><a href="#%E2%AD%90%EF%B8%8F%E5%9C%A8%E4%BD%BF%E7%94%A8completablefuture%E7%9A%84%E6%97%B6%E5%80%99%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BA%BF%E7%A8%8B%E6%B1%A0%EF%BC%9F">⭐️在使用 CompletableFuture 的时候为什么要自定义线程池？</a></li>
</ul>
</li>
<li><a href="#aqs">AQS</a>
<ul>
<li><a href="#aqs%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F">AQS 是什么？</a></li>
<li><a href="#%E2%AD%90%EF%B8%8Faqs%E7%9A%84%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F">⭐️AQS 的原理是什么？</a></li>
<li><a href="#semaphore%E6%9C%89%E4%BB%80%E4%B9%88%E7%94%A8%EF%BC%9F">Semaphore 有什么用？</a></li>
<li><a href="#semaphore%E7%9A%84%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F">Semaphore 的原理是什么？</a></li>
<li><a href="#countdownlatch%E6%9C%89%E4%BB%80%E4%B9%88%E7%94%A8%EF%BC%9F">CountDownLatch 有什么用？</a></li>
<li><a href="#countdownlatch%E7%9A%84%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F">CountDownLatch 的原理是什么？</a></li>
<li><a href="#%E7%94%A8%E8%BF%87countdownlatch%E4%B9%88%EF%BC%9F%E4%BB%80%E4%B9%88%E5%9C%BA%E6%99%AF%E4%B8%8B%E7%94%A8%E7%9A%84%EF%BC%9F">用过 CountDownLatch 么？什么场景下用的？</a></li>
<li><a href="#cyclicbarrier%E6%9C%89%E4%BB%80%E4%B9%88%E7%94%A8%EF%BC%9F">CyclicBarrier 有什么用？</a></li>
<li><a href="#cyclicbarrier%E7%9A%84%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F">CyclicBarrier 的原理是什么？</a></li>
</ul>
</li>
<li><a href="#%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B">虚拟线程</a></li>
<li><a href="#%E5%8F%82%E8%80%83">参考</a></li>
</ul>
<h2><a id="threadlocal" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>ThreadLocal</h2>
<h3><a id="threadlocal%E6%9C%89%E4%BB%80%E4%B9%88%E7%94%A8%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>ThreadLocal 有什么用？</h3>
<p>通常情况下，我们创建的变量可以被任何一个线程访问和修改。这在多线程环境中可能导致数据竞争和线程安全问题。那么，<strong>如果想让每个线程都有自己的专属本地变量，该如何实现呢？</strong></p>
<p>JDK 中提供的 <code>ThreadLocal</code> 类正是为了解决这个问题。<strong><code>ThreadLocal</code> 类允许每个线程绑定自己的值</strong>，可以将其形象地比喻为一个“存放数据的盒子”。每个线程都有自己独立的盒子，用于存储私有数据，确保不同线程之间的数据互不干扰。</p>
<p>当你创建一个 <code>ThreadLocal</code> 变量时，每个访问该变量的线程都会拥有一个独立的副本。这也是 <code>ThreadLocal</code> 名称的由来。线程可以通过 <code>get()</code> 方法获取自己线程的本地副本，或通过 <code>set()</code> 方法修改该副本的值，从而避免了线程安全问题。</p>
<p>举个简单的例子：假设有两个人去宝屋收集宝物。如果他们共用一个袋子，必然会产生争执；但如果每个人都有一个独立的袋子，就不会有这个问题。如果将这两个人比作线程，那么 <code>ThreadLocal</code> 就是用来避免这两个线程竞争同一个资源的方法。</p>
<pre><code class="language-java">public class ThreadLocalExample {
    private static ThreadLocal&lt;Integer&gt; threadLocal = ThreadLocal.withInitial(() -&gt; 0);

    public static void main(String[] args) {
        Runnable task = () -&gt; {
            int value = threadLocal.get();
            value += 1;
            threadLocal.set(value);
            System.out.println(Thread.currentThread().getName() + &quot; Value: &quot; + threadLocal.get());
        };

        Thread thread1 = new Thread(task, &quot;Thread-1&quot;);
        Thread thread2 = new Thread(task, &quot;Thread-2&quot;);

        thread1.start(); // 输出: Thread-1 Value: 1
        thread2.start(); // 输出: Thread-2 Value: 1
    }
}
</code></pre>
<h3><a id="%E2%AD%90%EF%B8%8Fthreadlocal%E5%8E%9F%E7%90%86%E4%BA%86%E8%A7%A3%E5%90%97%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️ThreadLocal 原理了解吗？</h3>
<p>从 <code>Thread</code>类源代码入手。</p>
<pre><code class="language-java">public class Thread implements Runnable {
    //......
    //与此线程有关的ThreadLocal值。由ThreadLocal类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;

    //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    //......
}
</code></pre>
<p>从上面<code>Thread</code>类 源代码可以看出<code>Thread</code> 类中有一个 <code>threadLocals</code> 和 一个 <code>inheritableThreadLocals</code> 变量，它们都是 <code>ThreadLocalMap</code> 类型的变量,我们可以把 <code>ThreadLocalMap</code> 理解为<code>ThreadLocal</code> 类实现的定制化的 <code>HashMap</code>。默认情况下这两个变量都是 null，只有当前线程调用 <code>ThreadLocal</code> 类的 <code>set</code>或<code>get</code>方法时才创建它们，实际上调用这两个方法的时候，我们调用的是<code>ThreadLocalMap</code>类对应的 <code>get()</code>、<code>set()</code>方法。</p>
<p><code>ThreadLocal</code>类的<code>set()</code>方法</p>
<pre><code class="language-java">public void set(T value) {
    //获取当前请求的线程
    Thread t = Thread.currentThread();
    //取出 Thread 类内部的 threadLocals 变量(哈希表结构)
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 将需要存储的值放入到这个哈希表中
        map.set(this, value);
    else
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
</code></pre>
<p>通过上面这些内容，我们足以通过猜测得出结论：<strong>最终的变量是放在了当前线程的 <code>ThreadLocalMap</code> 中，并不是存在 <code>ThreadLocal</code> 上，<code>ThreadLocal</code> 可以理解为只是<code>ThreadLocalMap</code>的封装，传递了变量值。</strong> <code>ThrealLocal</code> 类中可以通过<code>Thread.currentThread()</code>获取到当前线程对象后，直接通过<code>getMap(Thread t)</code>可以访问到该线程的<code>ThreadLocalMap</code>对象。</p>
<p><strong>每个<code>Thread</code>中都具备一个<code>ThreadLocalMap</code>，而<code>ThreadLocalMap</code>可以存储以<code>ThreadLocal</code>为 key ，Object 对象为 value 的键值对。</strong></p>
<pre><code class="language-java">ThreadLocalMap(ThreadLocal&lt;?&gt; firstKey, Object firstValue) {
    //......
}
</code></pre>
<p>比如我们在同一个线程中声明了两个 <code>ThreadLocal</code> 对象的话， <code>Thread</code>内部都是使用仅有的那个<code>ThreadLocalMap</code> 存放数据的，<code>ThreadLocalMap</code>的 key 就是 <code>ThreadLocal</code>对象，value 就是 <code>ThreadLocal</code> 对象调用<code>set</code>方法设置的值。</p>
<p><code>ThreadLocal</code> 数据结构如下图所示：</p>
<p><img src="media/17419985101934/17419997043305.png" alt="ThreadLocal 数据结构" /></p>
<p><code>ThreadLocalMap</code>是<code>ThreadLocal</code>的静态内部类。</p>
<p><img src="media/17419985101934/17419997043331.png" alt="ThreadLocal内部类" /></p>
<h3><a id="%E2%AD%90%EF%B8%8Fthreadlocal%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E9%97%AE%E9%A2%98%E6%98%AF%E6%80%8E%E4%B9%88%E5%AF%BC%E8%87%B4%E7%9A%84%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️ThreadLocal 内存泄露问题是怎么导致的？</h3>
<p><code>ThreadLocal</code> 内存泄漏的根本原因在于其内部实现机制。</p>
<p>通过上面的内容我们已经知道：每个线程维护一个名为 <code>ThreadLocalMap</code> 的 map。 当你使用 <code>ThreadLocal</code> 存储值时，实际上是将值存储在当前线程的 <code>ThreadLocalMap</code> 中，其中 <code>ThreadLocal</code> 实例本身作为 key，而你要存储的值作为 value。</p>
<p><code>ThreadLocal</code> 的 <code>set()</code> 方法源码如下：</p>
<pre><code class="language-java">public void set(T value) {
    Thread t = Thread.currentThread(); // 获取当前线程
    ThreadLocalMap map = getMap(t);   // 获取当前线程的 ThreadLocalMap
    if (map != null) {
        map.set(this, value);         // 设置值
    } else {
        createMap(t, value);          // 创建新的 ThreadLocalMap
    }
}
</code></pre>
<p><code>ThreadLocalMap</code> 的 <code>set()</code> 和 <code>createMap()</code> 方法中，并没有直接存储 <code>ThreadLocal</code> 对象本身，而是使用 <code>ThreadLocal</code> 的哈希值计算数组索引，最终存储于类型为<code>static class Entry extends WeakReference&lt;ThreadLocal&lt;?&gt;&gt;</code>的数组中。</p>
<pre><code class="language-java">int i = key.threadLocalHashCode &amp; (len-1);
</code></pre>
<p><code>ThreadLocalMap</code> 的 <code>Entry</code> 定义如下：</p>
<pre><code class="language-java">static class Entry extends WeakReference&lt;ThreadLocal&lt;?&gt;&gt; {
    Object value;

    Entry(ThreadLocal&lt;?&gt; k, Object v) {
        super(k);
        value = v;
    }
}
</code></pre>
<p><code>ThreadLocalMap</code> 的 <code>key</code> 和 <code>value</code> 引用机制：</p>
<ul>
<li><strong>key 是弱引用</strong>：<code>ThreadLocalMap</code> 中的 key 是 <code>ThreadLocal</code> 的弱引用 (<code>WeakReference&lt;ThreadLocal&lt;?&gt;&gt;</code>)。 这意味着，如果 <code>ThreadLocal</code> 实例不再被任何强引用指向，垃圾回收器会在下次 GC 时回收该实例，导致 <code>ThreadLocalMap</code> 中对应的 key 变为 <code>null</code>。</li>
<li><strong>value 是强引用</strong>：即使 <code>key</code> 被 GC 回收，<code>value</code> 仍然被 <code>ThreadLocalMap.Entry</code> 强引用存在，无法被 GC 回收。</li>
</ul>
<p>当 <code>ThreadLocal</code> 实例失去强引用后，其对应的 value 仍然存在于 <code>ThreadLocalMap</code> 中，因为 <code>Entry</code> 对象强引用了它。如果线程持续存活（例如线程池中的线程），<code>ThreadLocalMap</code> 也会一直存在，导致 key 为 <code>null</code> 的 entry 无法被垃圾回收，即会造成内存泄漏。</p>
<p>也就是说，内存泄漏的发生需要同时满足两个条件：</p>
<ol>
<li><code>ThreadLocal</code> 实例不再被强引用；</li>
<li>线程持续存活，导致 <code>ThreadLocalMap</code> 长期存在。</li>
</ol>
<p>虽然 <code>ThreadLocalMap</code> 在 <code>get()</code>, <code>set()</code> 和 <code>remove()</code> 操作时会尝试清理 key 为 null 的 entry，但这种清理机制是被动的，并不完全可靠。</p>
<p><strong>如何避免内存泄漏的发生？</strong></p>
<ol>
<li>在使用完 <code>ThreadLocal</code> 后，务必调用 <code>remove()</code> 方法。 这是最安全和最推荐的做法。 <code>remove()</code> 方法会从 <code>ThreadLocalMap</code> 中显式地移除对应的 entry，彻底解决内存泄漏的风险。 即使将 <code>ThreadLocal</code> 定义为 <code>static final</code>，也强烈建议在每次使用后调用 <code>remove()</code>。</li>
<li>在线程池等线程复用的场景下，使用 <code>try-finally</code> 块可以确保即使发生异常，<code>remove()</code> 方法也一定会被执行。</li>
</ol>
<h3><a id="%E2%AD%90%EF%B8%8F%E5%A6%82%E4%BD%95%E8%B7%A8%E7%BA%BF%E7%A8%8B%E4%BC%A0%E9%80%92threadlocal%E7%9A%84%E5%80%BC%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️如何跨线程传递 ThreadLocal 的值？</h3>
<p>由于 <code>ThreadLocal</code> 的变量值存放在 <code>Thread</code> 里，而父子线程属于不同的 <code>Thread</code> 的。因此在异步场景下，父子线程的 <code>ThreadLocal</code> 值无法进行传递。</p>
<p>如果想要在异步场景下传递 <code>ThreadLocal</code> 值，有两种解决方案：</p>
<ul>
<li><code>InheritableThreadLocal</code> ：<code>InheritableThreadLocal</code> 是 JDK1.2 提供的工具，继承自 <code>ThreadLocal</code> 。使用 <code>InheritableThreadLocal</code> 时，会在创建子线程时，令子线程继承父线程中的 <code>ThreadLocal</code> 值，但是无法支持线程池场景下的 <code>ThreadLocal</code> 值传递。</li>
<li><code>TransmittableThreadLocal</code> ： <code>TransmittableThreadLocal</code> （简称 TTL） 是阿里巴巴开源的工具类，继承并加强了<code>InheritableThreadLocal</code>类，可以在线程池的场景下支持 <code>ThreadLocal</code> 值传递。项目地址：<a href="https://github.com/alibaba/transmittable-thread-local">https://github.com/alibaba/transmittable-thread-local</a>。</li>
</ul>
<h4><a id="inheritablethreadlocal%E5%8E%9F%E7%90%86" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>InheritableThreadLocal 原理</h4>
<p><code>InheritableThreadLocal</code> 实现了创建异步线程时，继承父线程 <code>ThreadLocal</code> 值的功能。该类是 JDK 团队提供的，通过改造 JDK 源码包中的 <code>Thread</code> 类来实现创建线程时，<code>ThreadLocal</code> 值的传递。</p>
<p><strong><code>InheritableThreadLocal</code> 的值存储在哪里？</strong></p>
<p>在 <code>Thread</code> 类中添加了一个新的 <code>ThreadLocalMap</code> ，命名为 <code>inheritableThreadLocals</code> ，该变量用于存储需要跨线程传递的 <code>ThreadLocal</code> 值。如下：</p>
<pre><code class="language-JAVA">class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
</code></pre>
<p><strong>如何完成 <code>ThreadLocal</code> 值的传递？</strong></p>
<p>通过改造 <code>Thread</code> 类的构造方法来实现，在创建 <code>Thread</code> 线程时，拿到父线程的 <code>inheritableThreadLocals</code> 变量赋值给子线程即可。相关代码如下：</p>
<pre><code class="language-JAVA">// Thread 的构造方法会调用 init() 方法
private void init(/* ... */) {
	// 1、获取父线程
    Thread parent = currentThread();
    // 2、将父线程的 inheritableThreadLocals 赋值给子线程
    if (inheritThreadLocals &amp;&amp; parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
        	ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
</code></pre>
<h4><a id="transmittablethreadlocal%E5%8E%9F%E7%90%86" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>TransmittableThreadLocal 原理</h4>
<p>JDK 默认没有支持线程池场景下 <code>ThreadLocal</code> 值传递的功能，因此阿里巴巴开源了一套工具 <code>TransmittableThreadLocal</code> 来实现该功能。</p>
<p>阿里巴巴无法改动 JDK 的源码，因此他内部通过 <strong>装饰器模式</strong> 在原有的功能上做增强，以此来实现线程池场景下的 <code>ThreadLocal</code> 值传递。</p>
<p>TTL 改造的地方有两处：</p>
<ul>
<li>
<p>实现自定义的 <code>Thread</code> ，在 <code>run()</code> 方法内部做 <code>ThreadLocal</code> 变量的赋值操作。</p>
</li>
<li>
<p>基于 <strong>线程池</strong> 进行装饰，在 <code>execute()</code> 方法中，不提交 JDK 内部的 <code>Thread</code> ，而是提交自定义的 <code>Thread</code> 。</p>
</li>
</ul>
<p>如果想要查看相关源码，可以引入 Maven 依赖进行下载。</p>
<pre><code class="language-XML">&lt;dependency&gt;
    &lt;groupId&gt;com.alibaba&lt;/groupId&gt;
    &lt;artifactId&gt;transmittable-thread-local&lt;/artifactId&gt;
    &lt;version&gt;2.12.0&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<h4><a id="%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>应用场景</h4>
<ol>
<li><strong>压测流量标记</strong>： 在压测场景中，使用 <code>ThreadLocal</code> 存储压测标记，用于区分压测流量和真实流量。如果标记丢失，可能导致压测流量被错误地当成线上流量处理。</li>
<li><strong>上下文传递</strong>：在分布式系统中，传递链路追踪信息（如 Trace ID）或用户上下文信息。</li>
</ol>
<h2><a id="%E7%BA%BF%E7%A8%8B%E6%B1%A0" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>线程池</h2>
<h3><a id="%E4%BB%80%E4%B9%88%E6%98%AF%E7%BA%BF%E7%A8%8B%E6%B1%A0" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>什么是线程池?</h3>
<p>顾名思义，线程池就是管理一系列线程的资源池。当有任务要处理时，直接从线程池中获取线程来处理，处理完之后线程并不会立即被销毁，而是等待下一个任务。</p>
<h3><a id="%E2%AD%90%EF%B8%8F%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E7%94%A8%E7%BA%BF%E7%A8%8B%E6%B1%A0%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️为什么要用线程池？</h3>
<p>池化技术想必大家已经屡见不鲜了，线程池、数据库连接池、HTTP 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗，提高对资源的利用率。</p>
<p><strong>线程池</strong>提供了一种限制和管理资源（包括执行一个任务）的方式。 每个<strong>线程池</strong>还维护一些基本统计信息，例如已完成任务的数量。</p>
<p>这里借用《Java 并发编程的艺术》提到的来说一下<strong>使用线程池的好处</strong>：</p>
<ul>
<li><strong>降低资源消耗</strong>。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。</li>
<li><strong>提高响应速度</strong>。当任务到达时，任务可以不需要等到线程创建就能立即执行。</li>
<li><strong>提高线程的可管理性</strong>。线程是稀缺资源，如果无限制的创建，不仅会消耗系统资源，还会降低系统的稳定性，使用线程池可以进行统一的分配，调优和监控。</li>
</ul>
<h3><a id="%E5%A6%82%E4%BD%95%E5%88%9B%E5%BB%BA%E7%BA%BF%E7%A8%8B%E6%B1%A0%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>如何创建线程池？</h3>
<p><strong>方式一：通过<code>ThreadPoolExecutor</code>构造函数来创建（推荐）。</strong></p>
<p><img src="media/17419985101934/threadpoolexecutor%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0.png" alt="通过构造方法实现" /></p>
<p><strong>方式二：通过 <code>Executor</code> 框架的工具类 <code>Executors</code> 来创建。</strong></p>
<p><code>Executors</code>工具类提供的创建线程池的方法如下图所示：</p>
<p><img src="media/17419985101934/17419997043349.png" alt="" /></p>
<p>可以看出，通过<code>Executors</code>工具类可以创建多种类型的线程池，包括：</p>
<ul>
<li><code>FixedThreadPool</code>：固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时，线程池中若有空闲线程，则立即执行。若没有，则新的任务会被暂存在一个任务队列中，待有线程空闲时，便处理在任务队列中的任务。</li>
<li><code>SingleThreadExecutor</code>： 只有一个线程的线程池。若多余一个任务被提交到该线程池，任务会被保存在一个任务队列中，待线程空闲，按先入先出的顺序执行队列中的任务。</li>
<li><code>CachedThreadPool</code>： 可根据实际情况调整线程数量的线程池。线程池的线程数量不确定，但若有空闲线程可以复用，则会优先使用可复用的线程。若所有线程均在工作，又有新的任务提交，则会创建新的线程处理任务。所有线程在当前任务执行完毕后，将返回线程池进行复用。</li>
<li><code>ScheduledThreadPool</code>：给定的延迟后运行任务或者定期执行任务的线程池。</li>
</ul>
<h3><a id="%E2%AD%90%EF%B8%8F%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E6%8E%A8%E8%8D%90%E4%BD%BF%E7%94%A8%E5%86%85%E7%BD%AE%E7%BA%BF%E7%A8%8B%E6%B1%A0%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️为什么不推荐使用内置线程池？</h3>
<p>在《阿里巴巴 Java 开发手册》“并发处理”这一章节，明确指出线程资源必须通过线程池提供，不允许在应用中自行显式创建线程。</p>
<p><strong>为什么呢？</strong></p>
<blockquote>
<p>使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销，解决资源不足的问题。如果不使用线程池，有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。</p>
</blockquote>
<p>另外，《阿里巴巴 Java 开发手册》中强制线程池不允许使用 <code>Executors</code> 去创建，而是通过 <code>ThreadPoolExecutor</code> 构造函数的方式，这样的处理方式让写的同学更加明确线程池的运行规则，规避资源耗尽的风险</p>
<p><code>Executors</code> 返回线程池对象的弊端如下(后文会详细介绍到)：</p>
<ul>
<li><code>FixedThreadPool</code> 和 <code>SingleThreadExecutor</code>:使用的是阻塞队列 <code>LinkedBlockingQueue</code>，任务队列最大长度为 <code>Integer.MAX_VALUE</code>，可以看作是无界的，可能堆积大量的请求，从而导致 OOM。</li>
<li><code>CachedThreadPool</code>:使用的是同步队列 <code>SynchronousQueue</code>, 允许创建的线程数量为 <code>Integer.MAX_VALUE</code> ，如果任务数量过多且执行速度较慢，可能会创建大量的线程，从而导致 OOM。</li>
<li><code>ScheduledThreadPool</code> 和 <code>SingleThreadScheduledExecutor</code>:使用的无界的延迟阻塞队列<code>DelayedWorkQueue</code>，任务队列最大长度为 <code>Integer.MAX_VALUE</code>,可能堆积大量的请求，从而导致 OOM。</li>
</ul>
<pre><code class="language-java">public static ExecutorService newFixedThreadPool(int nThreads) {
    // LinkedBlockingQueue 的默认长度为 Integer.MAX_VALUE，可以看作是无界的
    return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue&lt;Runnable&gt;());

}

public static ExecutorService newSingleThreadExecutor() {
    // LinkedBlockingQueue 的默认长度为 Integer.MAX_VALUE，可以看作是无界的
    return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue&lt;Runnable&gt;()));

}

// 同步队列 SynchronousQueue，没有容量，最大线程数是 Integer.MAX_VALUE`
public static ExecutorService newCachedThreadPool() {

    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue&lt;Runnable&gt;());

}

// DelayedWorkQueue（延迟阻塞队列）
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}
</code></pre>
<h3><a id="%E2%AD%90%EF%B8%8F%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%B8%B8%E8%A7%81%E5%8F%82%E6%95%B0%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F%E5%A6%82%E4%BD%95%E8%A7%A3%E9%87%8A%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️线程池常见参数有哪些？如何解释？</h3>
<pre><code class="language-java">    /**
     * 用给定的初始参数创建一个新的ThreadPoolExecutor。
     */
    public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                              int maximumPoolSize,//线程池的最大线程数
                              long keepAliveTime,//当线程数大于核心线程数时，多余的空闲线程存活的最长时间
                              TimeUnit unit,//时间单位
                              BlockingQueue&lt;Runnable&gt; workQueue,//任务队列，用来储存等待执行任务的队列
                              ThreadFactory threadFactory,//线程工厂，用来创建线程，一般默认即可
                              RejectedExecutionHandler handler//拒绝策略，当提交的任务过多而不能及时处理时，我们可以定制策略来处理任务
                               ) {
        if (corePoolSize &lt; 0 ||
            maximumPoolSize &lt;= 0 ||
            maximumPoolSize &lt; corePoolSize ||
            keepAliveTime &lt; 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
</code></pre>
<p><code>ThreadPoolExecutor</code> 3 个最重要的参数：</p>
<ul>
<li><code>corePoolSize</code> : 任务队列未达到队列容量时，最大可以同时运行的线程数量。</li>
<li><code>maximumPoolSize</code> : 任务队列中存放的任务达到队列容量的时候，当前可以同时运行的线程数量变为最大线程数。</li>
<li><code>workQueue</code>: 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数，如果达到的话，新任务就会被存放在队列中。</li>
</ul>
<p><code>ThreadPoolExecutor</code>其他常见参数 :</p>
<ul>
<li><code>keepAliveTime</code>:当线程池中的线程数量大于 <code>corePoolSize</code> ，即有非核心线程（线程池中核心线程以外的线程）时，这些非核心线程空闲后不会立即销毁，而是会等待，直到等待的时间超过了 <code>keepAliveTime</code>才会被回收销毁。</li>
<li><code>unit</code> : <code>keepAliveTime</code> 参数的时间单位。</li>
<li><code>threadFactory</code> :executor 创建新线程的时候会用到。</li>
<li><code>handler</code> :拒绝策略（后面会单独详细介绍一下）。</li>
</ul>
<p>下面这张图可以加深你对线程池中各个参数的相互关系的理解（图片来源：《Java 性能调优实战》）：</p>
<p><img src="media/17419985101934/17419997043374.jpg" alt="线程池各个参数的关系" /></p>
<h3><a id="%E7%BA%BF%E7%A8%8B%E6%B1%A0%E7%9A%84%E6%A0%B8%E5%BF%83%E7%BA%BF%E7%A8%8B%E4%BC%9A%E8%A2%AB%E5%9B%9E%E6%94%B6%E5%90%97%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>线程池的核心线程会被回收吗？</h3>
<p><code>ThreadPoolExecutor</code> 默认不会回收核心线程，即使它们已经空闲了。这是为了减少创建线程的开销，因为核心线程通常是要长期保持活跃的。但是，如果线程池是被用于周期性使用的场景，且频率不高（周期之间有明显的空闲时间），可以考虑将 <code>allowCoreThreadTimeOut(boolean value)</code> 方法的参数设置为 <code>true</code>，这样就会回收空闲（时间间隔由 <code>keepAliveTime</code> 指定）的核心线程了。</p>
<pre><code class="language-java">public void allowCoreThreadTimeOut(boolean value) {
    // 核心线程的 keepAliveTime 必须大于 0 才能启用超时机制
    if (value &amp;&amp; keepAliveTime &lt;= 0) {
        throw new IllegalArgumentException(&quot;Core threads must have nonzero keep alive times&quot;);
    }
    // 设置 allowCoreThreadTimeOut 的值
    if (value != allowCoreThreadTimeOut) {
        allowCoreThreadTimeOut = value;
        // 如果启用了超时机制，清理所有空闲的线程，包括核心线程
        if (value) {
            interruptIdleWorkers();
        }
    }
}
</code></pre>
<h3><a id="%E6%A0%B8%E5%BF%83%E7%BA%BF%E7%A8%8B%E7%A9%BA%E9%97%B2%E6%97%B6%E5%A4%84%E4%BA%8E%E4%BB%80%E4%B9%88%E7%8A%B6%E6%80%81%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>核心线程空闲时处于什么状态？</h3>
<p>核心线程空闲时，其状态分为以下两种情况：</p>
<ul>
<li><strong>设置了核心线程的存活时间</strong> ：核心线程在空闲时，会处于 <code>WAITING</code> 状态，等待获取任务。如果阻塞等待的时间超过了核心线程存活时间，则该线程会退出工作，将该线程从线程池的工作线程集合中移除，线程状态变为 <code>TERMINATED</code> 状态。</li>
<li><strong>没有设置核心线程的存活时间</strong> ：核心线程在空闲时，会一直处于 <code>WAITING</code> 状态，等待获取任务，核心线程会一直存活在线程池中。</li>
</ul>
<p>当队列中有可用任务时，会唤醒被阻塞的线程，线程的状态会由 <code>WAITING</code> 状态变为 <code>RUNNABLE</code> 状态，之后去执行对应任务。</p>
<p>接下来通过相关源码，了解一下线程池内部是如何做的。</p>
<p>线程在线程池内部被抽象为了 <code>Worker</code> ，当 <code>Worker</code> 被启动之后，会不断去任务队列中获取任务。</p>
<p>在获取任务的时候，会根据 <code>timed</code> 值来决定从任务队列（ <code>BlockingQueue</code> ）获取任务的行为。</p>
<p>如果「设置了核心线程的存活时间」或者「线程数量超过了核心线程数量」，则将 <code>timed</code> 标记为 <code>true</code> ，表明获取任务时需要使用 <code>poll()</code> 指定超时时间。</p>
<ul>
<li><code>timed == true</code> ：使用 <code>poll()</code> 来获取任务。使用 <code>poll()</code> 方法获取任务超时的话，则当前线程会退出执行（ <code>TERMINATED</code> ），该线程从线程池中被移除。</li>
<li><code>timed == false</code> ：使用 <code>take()</code> 来获取任务。使用 <code>take()</code> 方法获取任务会让当前线程一直阻塞等待（<code>WAITING</code>）。</li>
</ul>
<p>源码如下：</p>
<pre><code class="language-JAVA">// ThreadPoolExecutor
private Runnable getTask() {
    boolean timedOut = false;
    for (;;) {
        // ...

        // 1、如果「设置了核心线程的存活时间」或者是「线程数量超过了核心线程数量」，则 timed 为 true。
        boolean timed = allowCoreThreadTimeOut || wc &gt; corePoolSize;
        // 2、扣减线程数量。
        // wc &gt; maximuimPoolSize：线程池中的线程数量超过最大线程数量。其中 wc 为线程池中的线程数量。
        // timed &amp;&amp; timeOut：timeOut 表示获取任务超时。
        // 分为两种情况：核心线程设置了存活时间 &amp;&amp; 获取任务超时，则扣减线程数量；线程数量超过了核心线程数量 &amp;&amp; 获取任务超时，则扣减线程数量。
        if ((wc &gt; maximumPoolSize || (timed &amp;&amp; timedOut))
            &amp;&amp; (wc &gt; 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }
        try {
            // 3、如果 timed 为 true，则使用 poll() 获取任务；否则，使用 take() 获取任务。
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            // 4、获取任务之后返回。
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}
</code></pre>
<h3><a id="%E2%AD%90%EF%B8%8F%E7%BA%BF%E7%A8%8B%E6%B1%A0%E7%9A%84%E6%8B%92%E7%BB%9D%E7%AD%96%E7%95%A5%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️线程池的拒绝策略有哪些？</h3>
<p>如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时，<code>ThreadPoolExecutor</code> 定义一些策略:</p>
<ul>
<li><code>ThreadPoolExecutor.AbortPolicy</code>：抛出 <code>RejectedExecutionException</code>来拒绝新任务的处理。</li>
<li><code>ThreadPoolExecutor.CallerRunsPolicy</code>：调用执行者自己的线程运行任务，也就是直接在调用<code>execute</code>方法的线程中运行(<code>run</code>)被拒绝的任务，如果执行程序已关闭，则会丢弃该任务。因此这种策略会降低对于新任务提交速度，影响程序的整体性能。如果你的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话，你可以选择这个策略。</li>
<li><code>ThreadPoolExecutor.DiscardPolicy</code>：不处理新任务，直接丢弃掉。</li>
<li><code>ThreadPoolExecutor.DiscardOldestPolicy</code>：此策略将丢弃最早的未处理的任务请求。</li>
</ul>
<p>举个例子：Spring 通过 <code>ThreadPoolTaskExecutor</code> 或者我们直接通过 <code>ThreadPoolExecutor</code> 的构造函数创建线程池的时候，当我们不指定 <code>RejectedExecutionHandler</code> 拒绝策略来配置线程池的时候，默认使用的是 <code>AbortPolicy</code>。在这种拒绝策略下，如果队列满了，<code>ThreadPoolExecutor</code> 将抛出 <code>RejectedExecutionException</code> 异常来拒绝新来的任务 ，这代表你将丢失对这个任务的处理。如果不想丢弃任务的话，可以使用<code>CallerRunsPolicy</code>。<code>CallerRunsPolicy</code> 和其他的几个策略不同，它既不会抛弃任务，也不会抛出异常，而是将任务回退给调用者，使用调用者的线程来执行任务。</p>
<pre><code class="language-java">public static class CallerRunsPolicy implements RejectedExecutionHandler {

        public CallerRunsPolicy() { }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                // 直接主线程执行，而不是线程池中的线程执行
                r.run();
            }
        }
    }
</code></pre>
<h3><a id="%E5%A6%82%E6%9E%9C%E4%B8%8D%E5%85%81%E8%AE%B8%E4%B8%A2%E5%BC%83%E4%BB%BB%E5%8A%A1%EF%BC%8C%E5%BA%94%E8%AF%A5%E9%80%89%E6%8B%A9%E5%93%AA%E4%B8%AA%E6%8B%92%E7%BB%9D%E7%AD%96%E7%95%A5%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>如果不允许丢弃任务，应该选择哪个拒绝策略？</h3>
<p>根据上面对线程池拒绝策略的介绍，相信大家很容易能够得出答案是：<code>CallerRunsPolicy</code> 。</p>
<p>这里我们再来结合<code>CallerRunsPolicy</code> 的源码来看看：</p>
<pre><code class="language-java">public static class CallerRunsPolicy implements RejectedExecutionHandler {

        public CallerRunsPolicy() { }


        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            //只要当前程序没有关闭，就用执行execute方法的线程执行该任务
            if (!e.isShutdown()) {

                r.run();
            }
        }
    }
</code></pre>
<p>从源码可以看出，只要当前程序不关闭就会使用执行<code>execute</code>方法的线程执行该任务。</p>
<h3><a id="callerrunspolicy%E6%8B%92%E7%BB%9D%E7%AD%96%E7%95%A5%E6%9C%89%E4%BB%80%E4%B9%88%E9%A3%8E%E9%99%A9%EF%BC%9F%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>CallerRunsPolicy 拒绝策略有什么风险？如何解决？</h3>
<p>我们上面也提到了：如果想要保证任何一个任务请求都要被执行的话，那选择 <code>CallerRunsPolicy</code> 拒绝策略更合适一些。</p>
<p>不过，如果走到<code>CallerRunsPolicy</code>的任务是个非常耗时的任务，且处理提交任务的线程是主线程，可能会导致主线程阻塞，影响程序的正常运行。</p>
<p>这里简单举一个例子，该线程池限定了最大线程数为 2，阻塞队列大小为 1(这意味着第 4 个任务就会走到拒绝策略)，<code>ThreadUtil</code>为 Hutool 提供的工具类：</p>
<pre><code class="language-java">public class ThreadPoolTest {

    private static final Logger log = LoggerFactory.getLogger(ThreadPoolTest.class);

    public static void main(String[] args) {
        // 创建一个线程池，核心线程数为1，最大线程数为2
        // 当线程数大于核心线程数时，多余的空闲线程存活的最长时间为60秒，
        // 任务队列为容量为1的ArrayBlockingQueue，饱和策略为CallerRunsPolicy。
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1,
                2,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue&lt;&gt;(1),
                new ThreadPoolExecutor.CallerRunsPolicy());

        // 提交第一个任务，由核心线程执行
        threadPoolExecutor.execute(() -&gt; {
            log.info(&quot;核心线程执行第一个任务&quot;);
            ThreadUtil.sleep(1, TimeUnit.MINUTES);
        });

        // 提交第二个任务，由于核心线程被占用，任务将进入队列等待
        threadPoolExecutor.execute(() -&gt; {
            log.info(&quot;非核心线程处理入队的第二个任务&quot;);
            ThreadUtil.sleep(1, TimeUnit.MINUTES);
        });

        // 提交第三个任务，由于核心线程被占用且队列已满，创建非核心线程处理
        threadPoolExecutor.execute(() -&gt; {
            log.info(&quot;非核心线程处理第三个任务&quot;);
            ThreadUtil.sleep(1, TimeUnit.MINUTES);
        });

        // 提交第四个任务，由于核心线程和非核心线程都被占用，队列也满了，根据CallerRunsPolicy策略，任务将由提交任务的线程（即主线程）来执行
        threadPoolExecutor.execute(() -&gt; {
            log.info(&quot;主线程处理第四个任务&quot;);
            ThreadUtil.sleep(2, TimeUnit.MINUTES);
        });

        // 提交第五个任务，主线程被第四个任务卡住，该任务必须等到主线程执行完才能提交
        threadPoolExecutor.execute(() -&gt; {
            log.info(&quot;核心线程执行第五个任务&quot;);
        });

        // 关闭线程池
        threadPoolExecutor.shutdown();
    }
}

</code></pre>
<p>输出：</p>
<pre><code class="language-bash">18:19:48.203 INFO  [pool-1-thread-1] c.j.concurrent.ThreadPoolTest - 核心线程执行第一个任务
18:19:48.203 INFO  [pool-1-thread-2] c.j.concurrent.ThreadPoolTest - 非核心线程处理第三个任务
18:19:48.203 INFO  [main] c.j.concurrent.ThreadPoolTest - 主线程处理第四个任务
18:20:48.212 INFO  [pool-1-thread-2] c.j.concurrent.ThreadPoolTest - 非核心线程处理入队的第二个任务
18:21:48.219 INFO  [pool-1-thread-2] c.j.concurrent.ThreadPoolTest - 核心线程执行第五个任务
</code></pre>
<p>从输出结果可以看出，因为<code>CallerRunsPolicy</code>这个拒绝策略，导致耗时的任务用了主线程执行，导致线程池阻塞，进而导致后续任务无法及时执行，严重的情况下很可能导致 OOM。</p>
<p>我们从问题的本质入手，调用者采用<code>CallerRunsPolicy</code>是希望所有的任务都能够被执行，暂时无法处理的任务又被保存在阻塞队列<code>BlockingQueue</code>中。这样的话，在内存允许的情况下，我们可以增加阻塞队列<code>BlockingQueue</code>的大小并调整堆内存以容纳更多的任务，确保任务能够被准确执行。</p>
<p>为了充分利用 CPU，我们还可以调整线程池的<code>maximumPoolSize</code> （最大线程数）参数，这样可以提高任务处理速度，避免累计在 <code>BlockingQueue</code>的任务过多导致内存用完。</p>
<p><img src="media/17419985101934/17419997043386.png" alt="调整阻塞队列大小和最大线程数" /></p>
<p>如果服务器资源以达到可利用的极限，这就意味我们要在设计策略上改变线程池的调度了，我们都知道，导致主线程卡死的本质就是因为我们不希望任何一个任务被丢弃。换个思路，有没有办法既能保证任务不被丢弃且在服务器有余力时及时处理呢？</p>
<p>这里提供的一种<strong>任务持久化</strong>的思路，这里所谓的任务持久化，包括但不限于:</p>
<ol>
<li>设计一张任务表将任务存储到 MySQL 数据库中。</li>
<li>Redis 缓存任务。</li>
<li>将任务提交到消息队列中。</li>
</ol>
<p>这里以方案一为例，简单介绍一下实现逻辑：</p>
<ol>
<li>实现<code>RejectedExecutionHandler</code>接口自定义拒绝策略，自定义拒绝策略负责将线程池暂时无法处理（此时阻塞队列已满）的任务入库（保存到 MySQL 中）。注意：线程池暂时无法处理的任务会先被放在阻塞队列中，阻塞队列满了才会触发拒绝策略。</li>
<li>继承<code>BlockingQueue</code>实现一个混合式阻塞队列，该队列包含 JDK 自带的<code>ArrayBlockingQueue</code>。另外，该混合式阻塞队列需要修改取任务处理的逻辑，也就是重写<code>take()</code>方法，取任务时优先从数据库中读取最早的任务，数据库中无任务时再从 <code>ArrayBlockingQueue</code>中去取任务。</li>
</ol>
<p><img src="media/17419985101934/17419997043400.png" alt="将一部分任务保存到MySQL中" /></p>
<p>整个实现逻辑还是比较简单的，核心在于自定义拒绝策略和阻塞队列。如此一来，一旦我们的线程池中线程以达到满载时，我们就可以通过拒绝策略将最新任务持久化到 MySQL 数据库中，等到线程池有了有余力处理所有任务时，让其优先处理数据库中的任务以避免&quot;饥饿&quot;问题。</p>
<p>当然，对于这个问题，我们也可以参考其他主流框架的做法，以 Netty 为例，它的拒绝策略则是直接创建一个线程池以外的线程处理这些任务，为了保证任务的实时处理，这种做法可能需要良好的硬件设备且临时创建的线程无法做到准确的监控：</p>
<pre><code class="language-java">private static final class NewThreadRunsPolicy implements RejectedExecutionHandler {
    NewThreadRunsPolicy() {
        super();
    }
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        try {
            //创建一个临时线程处理任务
            final Thread t = new Thread(r, &quot;Temporary task executor&quot;);
            t.start();
        } catch (Throwable e) {
            throw new RejectedExecutionException(
                    &quot;Failed to start a new thread&quot;, e);
        }
    }
}
</code></pre>
<p>ActiveMQ 则是尝试在指定的时效内尽可能的争取将任务入队，以保证最大交付：</p>
<pre><code class="language-java">new RejectedExecutionHandler() {
                @Override
                public void rejectedExecution(final Runnable r, final ThreadPoolExecutor executor) {
                    try {
                        //限时阻塞等待，实现尽可能交付
                        executor.getQueue().offer(r, 60, TimeUnit.SECONDS);
                    } catch (InterruptedException e) {
                        throw new RejectedExecutionException(&quot;Interrupted waiting for BrokerService.worker&quot;);
                    }
                    throw new RejectedExecutionException(&quot;Timed Out while attempting to enqueue Task.&quot;);
                }
            });
</code></pre>
<h3><a id="%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%B8%B8%E7%94%A8%E7%9A%84%E9%98%BB%E5%A1%9E%E9%98%9F%E5%88%97%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>线程池常用的阻塞队列有哪些？</h3>
<p>新任务来的时候会先判断当前运行的线程数量是否达到核心线程数，如果达到的话，新任务就会被存放在队列中。</p>
<p>不同的线程池会选用不同的阻塞队列，我们可以结合内置线程池来分析。</p>
<ul>
<li>容量为 <code>Integer.MAX_VALUE</code> 的 <code>LinkedBlockingQueue</code>（有界阻塞队列）：<code>FixedThreadPool</code> 和 <code>SingleThreadExecutor</code> 。<code>FixedThreadPool</code>最多只能创建核心线程数的线程（核心线程数和最大线程数相等），<code>SingleThreadExecutor</code>只能创建一个线程（核心线程数和最大线程数都是 1），二者的任务队列永远不会被放满。</li>
<li><code>SynchronousQueue</code>（同步队列）：<code>CachedThreadPool</code> 。<code>SynchronousQueue</code> 没有容量，不存储元素，目的是保证对于提交的任务，如果有空闲线程，则使用空闲线程来处理；否则新建一个线程来处理任务。也就是说，<code>CachedThreadPool</code> 的最大线程数是 <code>Integer.MAX_VALUE</code> ，可以理解为线程数是可以无限扩展的，可能会创建大量线程，从而导致 OOM。</li>
<li><code>DelayedWorkQueue</code>（延迟队列）：<code>ScheduledThreadPool</code> 和 <code>SingleThreadScheduledExecutor</code> 。<code>DelayedWorkQueue</code> 的内部元素并不是按照放入的时间排序，而是会按照延迟的时间长短对任务进行排序，内部采用的是“堆”的数据结构，可以保证每次出队的任务都是当前队列中执行时间最靠前的。<code>DelayedWorkQueue</code> 添加元素满了之后会自动扩容，增加原来容量的 50%，即永远不会阻塞，最大扩容可达 <code>Integer.MAX_VALUE</code>，所以最多只能创建核心线程数的线程。</li>
<li><code>ArrayBlockingQueue</code>（有界阻塞队列）：底层由数组实现，容量一旦创建，就不能修改。</li>
</ul>
<h3><a id="%E2%AD%90%EF%B8%8F%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%A4%84%E7%90%86%E4%BB%BB%E5%8A%A1%E7%9A%84%E6%B5%81%E7%A8%8B%E4%BA%86%E8%A7%A3%E5%90%97%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️线程池处理任务的流程了解吗？</h3>
<p><img src="media/17419985101934/17419997043415.png" alt="图解线程池实现原理" /></p>
<ol>
<li>如果当前运行的线程数小于核心线程数，那么就会新建一个线程来执行任务。</li>
<li>如果当前运行的线程数等于或大于核心线程数，但是小于最大线程数，那么就把该任务放入到任务队列里等待执行。</li>
<li>如果向任务队列投放任务失败（任务队列已经满了），但是当前运行的线程数是小于最大线程数的，就新建一个线程来执行任务。</li>
<li>如果当前运行的线程数已经等同于最大线程数了，新建线程将会使当前运行的线程超出最大线程数，那么当前任务会被拒绝，拒绝策略会调用<code>RejectedExecutionHandler.rejectedExecution()</code>方法。</li>
</ol>
<p>再提一个有意思的小问题：<strong>线程池在提交任务前，可以提前创建线程吗？</strong></p>
<p>答案是可以的！<code>ThreadPoolExecutor</code> 提供了两个方法帮助我们在提交任务之前，完成核心线程的创建，从而实现线程池预热的效果：</p>
<ul>
<li><code>prestartCoreThread()</code>:启动一个线程，等待任务，如果已达到核心线程数，这个方法返回 false，否则返回 true；</li>
<li><code>prestartAllCoreThreads()</code>:启动所有的核心线程，并返回启动成功的核心线程数。</li>
</ul>
<h3><a id="%E2%AD%90%EF%B8%8F%E7%BA%BF%E7%A8%8B%E6%B1%A0%E4%B8%AD%E7%BA%BF%E7%A8%8B%E5%BC%82%E5%B8%B8%E5%90%8E%EF%BC%8C%E9%94%80%E6%AF%81%E8%BF%98%E6%98%AF%E5%A4%8D%E7%94%A8%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️线程池中线程异常后，销毁还是复用？</h3>
<p>直接说结论，需要分两种情况：</p>
<ul>
<li><strong>使用<code>execute()</code>提交任务</strong>：当任务通过<code>execute()</code>提交到线程池并在执行过程中抛出异常时，如果这个异常没有在任务内被捕获，那么该异常会导致当前线程终止，并且异常会被打印到控制台或日志文件中。线程池会检测到这种线程终止，并创建一个新线程来替换它，从而保持配置的线程数不变。</li>
<li><strong>使用<code>submit()</code>提交任务</strong>：对于通过<code>submit()</code>提交的任务，如果在任务执行中发生异常，这个异常不会直接打印出来。相反，异常会被封装在由<code>submit()</code>返回的<code>Future</code>对象中。当调用<code>Future.get()</code>方法时，可以捕获到一个<code>ExecutionException</code>。在这种情况下，线程不会因为异常而终止，它会继续存在于线程池中，准备执行后续的任务。</li>
</ul>
<p>简单来说：使用<code>execute()</code>时，未捕获异常导致线程终止，线程池创建新线程替代；使用<code>submit()</code>时，异常被封装在<code>Future</code>中，线程继续复用。</p>
<p>这种设计允许<code>submit()</code>提供更灵活的错误处理机制，因为它允许调用者决定如何处理异常，而<code>execute()</code>则适用于那些不需要关注执行结果的场景。</p>
<p>具体的源码分析可以参考这篇：<a href="https://mp.weixin.qq.com/s/9ODjdUU-EwQFF5PrnzOGfw">线程池中线程异常后：销毁还是复用？ - 京东技术</a>。</p>
<h3><a id="%E2%AD%90%EF%B8%8F%E5%A6%82%E4%BD%95%E7%BB%99%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%91%BD%E5%90%8D%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️如何给线程池命名？</h3>
<p>初始化线程池的时候需要显示命名（设置线程池名称前缀），有利于定位问题。</p>
<p>默认情况下创建的线程名字类似 <code>pool-1-thread-n</code> 这样的，没有业务含义，不利于我们定位问题。</p>
<p>给线程池里的线程命名通常有下面两种方式：</p>
<p><strong>1、利用 guava 的 <code>ThreadFactoryBuilder</code></strong></p>
<pre><code class="language-java">ThreadFactory threadFactory = new ThreadFactoryBuilder()
                        .setNameFormat(threadNamePrefix + &quot;-%d&quot;)
                        .setDaemon(true).build();
ExecutorService threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory);
</code></pre>
<p><strong>2、自己实现 <code>ThreadFactory</code>。</strong></p>
<pre><code class="language-java">import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 线程工厂，它设置线程名称，有利于我们定位问题。
 */
public final class NamingThreadFactory implements ThreadFactory {

    private final AtomicInteger threadNum = new AtomicInteger();
    private final String name;

    /**
     * 创建一个带名字的线程池生产工厂
     */
    public NamingThreadFactory(String name) {
        this.name = name;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        t.setName(name + &quot; [#&quot; + threadNum.incrementAndGet() + &quot;]&quot;);
        return t;
    }
}
</code></pre>
<h3><a id="%E5%A6%82%E4%BD%95%E8%AE%BE%E5%AE%9A%E7%BA%BF%E7%A8%8B%E6%B1%A0%E7%9A%84%E5%A4%A7%E5%B0%8F%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>如何设定线程池的大小？</h3>
<p>很多人甚至可能都会觉得把线程池配置过大一点比较好！我觉得这明显是有问题的。就拿我们生活中非常常见的一例子来说：<strong>并不是人多就能把事情做好，增加了沟通交流成本。你本来一件事情只需要 3 个人做，你硬是拉来了 6 个人，会提升做事效率嘛？我想并不会。</strong> 线程数量过多的影响也是和我们分配多少人做事情一样，对于多线程这个场景来说主要是增加了<strong>上下文切换</strong>成本。不清楚什么是上下文切换的话，可以看我下面的介绍。</p>
<blockquote>
<p>上下文切换：</p>
<p>多线程编程中一般线程的个数都大于 CPU 核心的个数，而一个 CPU 核心在任意时刻只能被一个线程使用，为了让这些线程都能得到有效执行，CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用，这个过程就属于一次上下文切换。概括来说就是：当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态，以便下次再切换回这个任务时，可以再加载这个任务的状态。<strong>任务从保存到再加载的过程就是一次上下文切换</strong>。</p>
<p>上下文切换通常是计算密集型的。也就是说，它需要相当可观的处理器时间，在每秒几十上百次的切换中，每次切换都需要纳秒量级的时间。所以，上下文切换对系统来说意味着消耗大量的 CPU 时间，事实上，可能是操作系统中时间消耗最大的操作。</p>
<p>Linux 相比与其他操作系统（包括其他类 Unix 系统）有很多的优点，其中有一项就是，其上下文切换和模式切换的时间消耗非常少。</p>
</blockquote>
<p>类比于现实世界中的人类通过合作做某件事情，我们可以肯定的一点是线程池大小设置过大或者过小都会有问题，合适的才是最好。</p>
<ul>
<li>如果我们设置的线程池数量太小的话，如果同一时间有大量任务/请求需要处理，可能会导致大量的请求/任务在任务队列中排队等待执行，甚至会出现任务队列满了之后任务/请求无法处理的情况，或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的，CPU 根本没有得到充分利用。</li>
<li>如果我们设置线程数量太大，大量线程可能会同时在争取 CPU 资源，这样会导致大量的上下文切换，从而增加线程的执行时间，影响了整体执行效率。</li>
</ul>
<p>有一个简单并且适用面比较广的公式：</p>
<ul>
<li><strong>CPU 密集型任务(N+1)：</strong> 这种任务消耗的主要是 CPU 资源，可以将线程数设置为 N（CPU 核心数）+1。比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断，或者其它原因导致的任务暂停而带来的影响。一旦任务暂停，CPU 就会处于空闲状态，而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。</li>
<li><strong>I/O 密集型任务(2N)：</strong> 这种任务应用起来，系统会用大部分的时间来处理 I/O 交互，而线程在处理 I/O 的时间段内不会占用 CPU 来处理，这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中，我们可以多配置一些线程，具体的计算方法是 2N。</li>
</ul>
<p><strong>如何判断是 CPU 密集任务还是 IO 密集任务？</strong></p>
<p>CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取，文件读取这类都是 IO 密集型，这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少，大部分时间都花在了等待 IO 操作完成上。</p>
<blockquote>
<p>🌈 拓展一下（参见：<a href="https://github.com/Snailclimb/JavaGuide/issues/1737">issue#1737</a>）：</p>
<p>线程数更严谨的计算的方法应该是：<code>最佳线程数 = N（CPU 核心数）∗（1+WT（线程等待时间）/ST（线程计算时间））</code>，其中 <code>WT（线程等待时间）=线程运行总时间 - ST（线程计算时间）</code>。</p>
<p>线程等待时间所占比例越高，需要越多线程。线程计算时间所占比例越高，需要越少线程。</p>
<p>我们可以通过 JDK 自带的工具 VisualVM 来查看 <code>WT/ST</code> 比例。</p>
<p>CPU 密集型任务的 <code>WT/ST</code> 接近或者等于 0，因此， 线程数可以设置为 N（CPU 核心数）∗（1+0）= N，和我们上面说的 N（CPU 核心数）+1 差不多。</p>
<p>IO 密集型任务下，几乎全是线程等待时间，从理论上来说，你就可以将线程数设置为 2N（按道理来说，WT/ST 的结果应该比较大，这里选择 2N 的原因应该是为了避免创建过多线程吧）。</p>
</blockquote>
<p>公式也只是参考，具体还是要根据项目实际线上运行情况来动态调整。我在后面介绍的美团的线程池参数动态配置这种方案就非常不错，很实用！</p>
<h3><a id="%E2%AD%90%EF%B8%8F%E5%A6%82%E4%BD%95%E5%8A%A8%E6%80%81%E4%BF%AE%E6%94%B9%E7%BA%BF%E7%A8%8B%E6%B1%A0%E7%9A%84%E5%8F%82%E6%95%B0%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️如何动态修改线程池的参数？</h3>
<p>美团技术团队在<a href="https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html">《Java 线程池实现原理及其在美团业务中的实践》</a>这篇文章中介绍到对线程池参数实现可自定义配置的思路和方法。</p>
<p>美团技术团队的思路是主要对线程池的核心参数实现自定义可配置。这三个核心参数是：</p>
<ul>
<li><strong><code>corePoolSize</code> :</strong> 核心线程数线程数定义了最小可以同时运行的线程数量。</li>
<li><strong><code>maximumPoolSize</code> :</strong> 当队列中存放的任务达到队列容量的时候，当前可以同时运行的线程数量变为最大线程数。</li>
<li><strong><code>workQueue</code>:</strong> 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数，如果达到的话，新任务就会被存放在队列中。</li>
</ul>
<p><strong>为什么是这三个参数？</strong></p>
<p>我在<a href="https://javaguide.cn/java/concurrent/java-thread-pool-summary.html">Java 线程池详解</a> 这篇文章中就说过这三个参数是 <code>ThreadPoolExecutor</code> 最重要的参数，它们基本决定了线程池对于任务的处理策略。</p>
<p><strong>如何支持参数动态配置？</strong> 且看 <code>ThreadPoolExecutor</code> 提供的下面这些方法。</p>
<p><img src="media/17419985101934/17419997043432.png" alt="" /></p>
<p>格外需要注意的是<code>corePoolSize</code>， 程序运行期间的时候，我们调用 <code>setCorePoolSize()</code>这个方法的话，线程池会首先判断当前工作线程数是否大于<code>corePoolSize</code>，如果大于的话就会回收工作线程。</p>
<p>另外，你也看到了上面并没有动态指定队列长度的方法，美团的方式是自定义了一个叫做 <code>ResizableCapacityLinkedBlockIngQueue</code> 的队列（主要就是把<code>LinkedBlockingQueue</code>的 capacity 字段的 final 关键字修饰给去掉了，让它变为可变的）。</p>
<p>最终实现的可动态修改线程池参数效果如下。👏👏👏</p>
<p><img src="media/17419985101934/17419997043447.png" alt="动态配置线程池参数最终效果" /></p>
<p>还没看够？我在<a href="https://javaguide.cn/zhuanlan/back-end-interview-high-frequency-system-design-and-scenario-questions.html#%E4%BB%8B%E7%BB%8D">《后端面试高频系统设计&amp;场景题》</a>中详细介绍了如何设计一个动态线程池，这也是面试中常问的一道系统设计题。</p>
<p><img src="media/17419985101934/17419997043467.png" alt="《后端面试高频系统设计&amp;场景题》" /></p>
<p>如果我们的项目也想要实现这种效果的话，可以借助现成的开源项目：</p>
<ul>
<li><strong><a href="https://github.com/opengoofy/hippo4j">Hippo4j</a></strong>：异步线程池框架，支持线程池动态变更&amp;监控&amp;报警，无需修改代码轻松引入。支持多种使用模式，轻松引入，致力于提高系统运行保障能力。</li>
<li><strong><a href="https://github.com/dromara/dynamic-tp">Dynamic TP</a></strong>：轻量级动态线程池，内置监控告警功能，集成三方中间件线程池管理，基于主流配置中心（已支持 Nacos、Apollo，Zookeeper、Consul、Etcd，可通过 SPI 自定义实现）。</li>
</ul>
<h3><a id="%E2%AD%90%EF%B8%8F%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%E4%B8%80%E4%B8%AA%E8%83%BD%E5%A4%9F%E6%A0%B9%E6%8D%AE%E4%BB%BB%E5%8A%A1%E7%9A%84%E4%BC%98%E5%85%88%E7%BA%A7%E6%9D%A5%E6%89%A7%E8%A1%8C%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%B1%A0%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️如何设计一个能够根据任务的优先级来执行的线程池？</h3>
<p>这是一个常见的面试问题，本质其实还是在考察求职者对于线程池以及阻塞队列的掌握。</p>
<p>我们上面也提到了，不同的线程池会选用不同的阻塞队列作为任务队列，比如<code>FixedThreadPool</code> 使用的是<code>LinkedBlockingQueue</code>（有界队列），默认构造器初始的队列长度为 <code>Integer.MAX_VALUE</code> ，由于队列永远不会被放满，因此<code>FixedThreadPool</code>最多只能创建核心线程数的线程。</p>
<p>假如我们需要实现一个优先级任务线程池的话，那可以考虑使用 <code>PriorityBlockingQueue</code> （优先级阻塞队列）作为任务队列（<code>ThreadPoolExecutor</code> 的构造函数有一个 <code>workQueue</code> 参数可以传入任务队列）。</p>
<p><img src="media/17419985101934/17419997043487.jpg" alt="ThreadPoolExecutor构造函数" /></p>
<p><code>PriorityBlockingQueue</code> 是一个支持优先级的无界阻塞队列，可以看作是线程安全的 <code>PriorityQueue</code>，两者底层都是使用小顶堆形式的二叉堆，即值最小的元素优先出队。不过，<code>PriorityQueue</code> 不支持阻塞操作。</p>
<p>要想让 <code>PriorityBlockingQueue</code> 实现对任务的排序，传入其中的任务必须是具备排序能力的，方式有两种：</p>
<ol>
<li>提交到线程池的任务实现 <code>Comparable</code> 接口，并重写 <code>compareTo</code> 方法来指定任务之间的优先级比较规则。</li>
<li>创建 <code>PriorityBlockingQueue</code> 时传入一个 <code>Comparator</code> 对象来指定任务之间的排序规则(推荐)。</li>
</ol>
<p>不过，这存在一些风险和问题，比如：</p>
<ul>
<li><code>PriorityBlockingQueue</code> 是无界的，可能堆积大量的请求，从而导致 OOM。</li>
<li>可能会导致饥饿问题，即低优先级的任务长时间得不到执行。</li>
<li>由于需要对队列中的元素进行排序操作以及保证线程安全（并发控制采用的是可重入锁 <code>ReentrantLock</code>），因此会降低性能。</li>
</ul>
<p>对于 OOM 这个问题的解决比较简单粗暴，就是继承<code>PriorityBlockingQueue</code> 并重写一下 <code>offer</code> 方法(入队)的逻辑，当插入的元素数量超过指定值就返回 false 。</p>
<p>饥饿问题这个可以通过优化设计来解决（比较麻烦），比如等待时间过长的任务会被移除并重新添加到队列中，但是优先级会被提升。</p>
<p>对于性能方面的影响，是没办法避免的，毕竟需要对任务进行排序操作。并且，对于大部分业务场景来说，这点性能影响是可以接受的。</p>
<h2><a id="future" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Future</h2>
<p>重点是要掌握 <code>CompletableFuture</code> 的使用以及常见面试题。</p>
<p>除了下面的面试题之外，还推荐你看看我写的这篇文章： <a href="17420003237169.html">CompletableFuture 详解</a>。</p>
<h3><a id="future%E7%B1%BB%E6%9C%89%E4%BB%80%E4%B9%88%E7%94%A8%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Future 类有什么用？</h3>
<p><code>Future</code> 类是异步思想的典型运用，主要用在一些需要执行耗时任务的场景，避免程序一直原地等待耗时任务执行完成，执行效率太低。具体来说是这样的：当我们执行某一耗时的任务时，可以将这个耗时任务交给一个子线程去异步执行，同时我们可以干点其他事情，不用傻傻等待耗时任务执行完成。等我们的事情干完后，我们再通过 <code>Future</code> 类获取到耗时任务的执行结果。这样一来，程序的执行效率就明显提高了。</p>
<p>这其实就是多线程中经典的 <strong>Future 模式</strong>，你可以将其看作是一种设计模式，核心思想是异步调用，主要用在多线程领域，并非 Java 语言独有。</p>
<p>在 Java 中，<code>Future</code> 类只是一个泛型接口，位于 <code>java.util.concurrent</code> 包下，其中定义了 5 个方法，主要包括下面这 4 个功能：</p>
<ul>
<li>取消任务；</li>
<li>判断任务是否被取消;</li>
<li>判断任务是否已经执行完成;</li>
<li>获取任务执行结果。</li>
</ul>
<pre><code class="language-java">// V 代表了Future执行的任务返回值的类型
public interface Future&lt;V&gt; {
    // 取消任务执行
    // 成功取消返回 true，否则返回 false
    boolean cancel(boolean mayInterruptIfRunning);
    // 判断任务是否被取消
    boolean isCancelled();
    // 判断任务是否已经执行完成
    boolean isDone();
    // 获取任务执行结果
    V get() throws InterruptedException, ExecutionException;
    // 指定时间内没有返回计算结果就抛出 TimeOutException 异常
    V get(long timeout, TimeUnit unit)

        throws InterruptedException, ExecutionException, TimeoutExceptio

}
</code></pre>
<p>简单理解就是：我有一个任务，提交给了 <code>Future</code> 来处理。任务执行期间我自己可以去做任何想做的事情。并且，在这期间我还可以取消任务以及获取任务的执行状态。一段时间之后，我就可以 <code>Future</code> 那里直接取出任务执行结果。</p>
<h3><a id="callable%E5%92%8C-future%E6%9C%89%E4%BB%80%E4%B9%88%E5%85%B3%E7%B3%BB%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Callable 和 Future 有什么关系？</h3>
<p>我们可以通过 <code>FutureTask</code> 来理解 <code>Callable</code> 和 <code>Future</code> 之间的关系。</p>
<p><code>FutureTask</code> 提供了 <code>Future</code> 接口的基本实现，常用来封装 <code>Callable</code> 和 <code>Runnable</code>，具有取消任务、查看任务是否执行完成以及获取任务执行结果的方法。<code>ExecutorService.submit()</code> 方法返回的其实就是 <code>Future</code> 的实现类 <code>FutureTask</code> 。</p>
<pre><code class="language-java">&lt;T&gt; Future&lt;T&gt; submit(Callable&lt;T&gt; task);
Future&lt;?&gt; submit(Runnable task);
</code></pre>
<p><code>FutureTask</code> 不光实现了 <code>Future</code>接口，还实现了<code>Runnable</code> 接口，因此可以作为任务直接被线程执行。</p>
<p><img src="media/17419985101934/17419997043498.jpg" alt="" /></p>
<p><code>FutureTask</code> 有两个构造函数，可传入 <code>Callable</code> 或者 <code>Runnable</code> 对象。实际上，传入 <code>Runnable</code> 对象也会在方法内部转换为<code>Callable</code> 对象。</p>
<pre><code class="language-java">public FutureTask(Callable&lt;V&gt; callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;
}
public FutureTask(Runnable runnable, V result) {
    // 通过适配器RunnableAdapter来将Runnable对象runnable转换成Callable对象
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;
}
</code></pre>
<p><code>FutureTask</code>相当于对<code>Callable</code> 进行了封装，管理着任务执行的情况，存储了 <code>Callable</code> 的 <code>call</code> 方法的任务执行结果。</p>
<p>关于更多 <code>Future</code> 的源码细节，可以肝这篇万字解析，写的很清楚：<a href="https://juejin.cn/post/6844904199625375757">Java是如何实现Future模式的？万字详解！</a>。</p>
<h3><a id="completablefuture%E7%B1%BB%E6%9C%89%E4%BB%80%E4%B9%88%E7%94%A8%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>CompletableFuture 类有什么用？</h3>
<p><code>Future</code> 在实际使用过程中存在一些局限性，比如不支持异步任务的编排组合、获取计算结果的 <code>get()</code> 方法为阻塞调用。</p>
<p>Java 8 才被引入<code>CompletableFuture</code> 类可以解决<code>Future</code> 的这些缺陷。<code>CompletableFuture</code> 除了提供了更为好用和强大的 <code>Future</code> 特性之外，还提供了函数式编程、异步任务编排组合（可以将多个异步任务串联起来，组成一个完整的链式调用）等能力。</p>
<p>下面我们来简单看看 <code>CompletableFuture</code> 类的定义。</p>
<pre><code class="language-java">public class CompletableFuture&lt;T&gt; implements Future&lt;T&gt;, CompletionStage&lt;T&gt; {
}
</code></pre>
<p>可以看到，<code>CompletableFuture</code> 同时实现了 <code>Future</code> 和 <code>CompletionStage</code> 接口。</p>
<p><img src="media/17419985101934/17419997043498.jpg" alt="" /></p>
<p><code>CompletionStage</code> 接口描述了一个异步计算的阶段。很多计算可以分成多个阶段或步骤，此时可以通过它将所有步骤组合起来，形成异步计算的流水线。</p>
<p><code>CompletionStage</code> 接口中的方法比较多，<code>CompletableFuture</code> 的函数式能力就是这个接口赋予的。从这个接口的方法参数你就可以发现其大量使用了 Java8 引入的函数式编程。</p>
<p><img src="media/17419985101934/17419997043509.png" alt="" /></p>
<h3><a id="%E2%AD%90%EF%B8%8F%E4%B8%80%E4%B8%AA%E4%BB%BB%E5%8A%A1%E9%9C%80%E8%A6%81%E4%BE%9D%E8%B5%96%E5%8F%A6%E5%A4%96%E4%B8%A4%E4%B8%AA%E4%BB%BB%E5%8A%A1%E6%89%A7%E8%A1%8C%E5%AE%8C%E4%B9%8B%E5%90%8E%E5%86%8D%E6%89%A7%E8%A1%8C%EF%BC%8C%E6%80%8E%E4%B9%88%E8%AE%BE%E8%AE%A1%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️一个任务需要依赖另外两个任务执行完之后再执行，怎么设计？</h3>
<p>这种任务编排场景非常适合通过<code>CompletableFuture</code>实现。这里假设要实现 T3 在 T2 和 T1 执行完后执行。</p>
<p>代码如下（这里为了简化代码，用到了 Hutool 的线程工具类 <code>ThreadUtil</code> 和日期时间工具类 <code>DateUtil</code>）：</p>
<pre><code class="language-java">// T1
CompletableFuture&lt;Void&gt; futureT1 = CompletableFuture.runAsync(() -&gt; {
    System.out.println(&quot;T1 is executing. Current time：&quot; + DateUtil.now());
    // 模拟耗时操作
    ThreadUtil.sleep(1000);
});
// T2
CompletableFuture&lt;Void&gt; futureT2 = CompletableFuture.runAsync(() -&gt; {
    System.out.println(&quot;T2 is executing. Current time：&quot; + DateUtil.now());
    ThreadUtil.sleep(1000);
});

// 使用allOf()方法合并T1和T2的CompletableFuture，等待它们都完成
CompletableFuture&lt;Void&gt; bothCompleted = CompletableFuture.allOf(futureT1, futureT2);
// 当T1和T2都完成后，执行T3
bothCompleted.thenRunAsync(() -&gt; System.out.println(&quot;T3 is executing after T1 and T2 have completed.Current time：&quot; + DateUtil.now()));
// 等待所有任务完成，验证效果
ThreadUtil.sleep(3000);
</code></pre>
<p>通过 <code>CompletableFuture</code> 的 <code>allOf()</code> 这个静态方法来并行运行 T1 和 T2，当 T1 和 T2 都完成后，再执行 T3。</p>
<h3><a id="%E2%AD%90%EF%B8%8F%E4%BD%BF%E7%94%A8completablefuture%EF%BC%8C%E6%9C%89%E4%B8%80%E4%B8%AA%E4%BB%BB%E5%8A%A1%E5%A4%B1%E8%B4%A5%EF%BC%8C%E5%A6%82%E4%BD%95%E5%A4%84%E7%90%86%E5%BC%82%E5%B8%B8%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️使用 CompletableFuture，有一个任务失败，如何处理异常？</h3>
<p>使用 <code>CompletableFuture</code>的时候一定要以正确的方式进行异常处理，避免异常丢失或者出现不可控问题。</p>
<p>下面是一些建议：</p>
<ul>
<li>使用 <code>whenComplete</code> 方法可以在任务完成时触发回调函数，并正确地处理异常，而不是让异常被吞噬或丢失。</li>
<li>使用 <code>exceptionally</code> 方法可以处理异常并重新抛出，以便异常能够传播到后续阶段，而不是让异常被忽略或终止。</li>
<li>使用 <code>handle</code> 方法可以处理正常的返回结果和异常，并返回一个新的结果，而不是让异常影响正常的业务逻辑。</li>
<li>使用 <code>CompletableFuture.allOf</code> 方法可以组合多个 <code>CompletableFuture</code>，并统一处理所有任务的异常，而不是让异常处理过于冗长或重复。</li>
<li>……</li>
</ul>
<h3><a id="%E2%AD%90%EF%B8%8F%E5%9C%A8%E4%BD%BF%E7%94%A8completablefuture%E7%9A%84%E6%97%B6%E5%80%99%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BA%BF%E7%A8%8B%E6%B1%A0%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️在使用 CompletableFuture 的时候为什么要自定义线程池？</h3>
<p><code>CompletableFuture</code> 默认使用全局共享的 <code>ForkJoinPool.commonPool()</code> 作为执行器，所有未指定执行器的异步任务都会使用该线程池。这意味着应用程序、多个库或框架（如 Spring、第三方库）若都依赖 <code>CompletableFuture</code>，默认情况下它们都会共享同一个线程池。</p>
<p>虽然 <code>ForkJoinPool</code> 效率很高，但当同时提交大量任务时，可能会导致资源竞争和线程饥饿，进而影响系统性能。</p>
<p>为避免这些问题，建议为 <code>CompletableFuture</code> 提供自定义线程池，带来以下优势：</p>
<ul>
<li>隔离性：为不同任务分配独立的线程池，避免全局线程池资源争夺。</li>
<li>资源控制：根据任务特性调整线程池大小和队列类型，优化性能表现。</li>
<li>异常处理：通过自定义 <code>ThreadFactory</code> 更好地处理线程中的异常情况。</li>
</ul>
<pre><code class="language-java">private ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue&lt;Runnable&gt;());

CompletableFuture.runAsync(() -&gt; {
     //...
}, executor);
</code></pre>
<h2><a id="aqs" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>AQS</h2>
<p>关于 AQS 源码的详细分析，可以看看这一篇文章：<a href="17420003237074.html">AQS 详解</a>。</p>
<h3><a id="aqs%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>AQS 是什么？</h3>
<p>AQS （<code>AbstractQueuedSynchronizer</code> ，抽象队列同步器）是从 JDK1.5 开始提供的 Java 并发核心组件。</p>
<p>AQS 解决了开发者在实现同步器时的复杂性问题。它提供了一个通用框架，用于实现各种同步器，例如 <strong>可重入锁</strong>（<code>ReentrantLock</code>）、<strong>信号量</strong>（<code>Semaphore</code>）和 <strong>倒计时器</strong>（<code>CountDownLatch</code>）。通过封装底层的线程同步机制，AQS 将复杂的线程管理逻辑隐藏起来，使开发者只需专注于具体的同步逻辑。</p>
<p>简单来说，AQS 是一个抽象类，为同步器提供了通用的 <strong>执行框架</strong>。它定义了 <strong>资源获取和释放的通用流程</strong>，而具体的资源获取逻辑则由具体同步器通过重写模板方法来实现。 因此，可以将 AQS 看作是同步器的 <strong>基础“底座”</strong>，而同步器则是基于 AQS 实现的 <strong>具体“应用”</strong>。</p>
<h3><a id="%E2%AD%90%EF%B8%8Faqs%E7%9A%84%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>⭐️AQS 的原理是什么？</h3>
<p>AQS 核心思想是，如果被请求的共享资源空闲，则将当前请求资源的线程设置为有效的工作线程，并且将共享资源设置为锁定状态。如果被请求的共享资源被占用，那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制，这个机制 AQS 是基于 <strong>CLH 锁</strong> （Craig, Landin, and Hagersten locks） 进一步优化实现的。</p>
<p><strong>CLH 锁</strong> 对自旋锁进行了改进，是基于单链表的自旋锁。在多线程场景下，会将请求获取锁的线程组织成一个单向队列，每个等待的线程会通过自旋访问前一个线程节点的状态，前一个节点释放锁之后，当前节点才可以获取锁。<strong>CLH 锁</strong> 的队列结构如下图所示。</p>
<p><img src="media/17419985101934/17419997043527.png" alt="CLH 锁的队列结构" /></p>
<p>AQS 中使用的 <strong>等待队列</strong> 是 CLH 锁队列的变体（接下来简称为 CLH 变体队列）。</p>
<p>AQS 的 CLH 变体队列是一个双向队列，会暂时获取不到锁的线程将被加入到该队列中，CLH 变体队列和原本的 CLH 锁队列的区别主要有两点：</p>
<ul>
<li>由 <strong>自旋</strong> 优化为 <strong>自旋 + 阻塞</strong> ：自旋操作的性能很高，但大量的自旋操作比较占用 CPU 资源，因此在 CLH 变体队列中会先通过自旋尝试获取锁，如果失败再进行阻塞等待。</li>
<li>由 <strong>单向队列</strong> 优化为 <strong>双向队列</strong> ：在 CLH 变体队列中，会对等待的线程进行阻塞操作，当队列前边的线程释放锁之后，需要对后边的线程进行唤醒，因此增加了 <code>next</code> 指针，成为了双向队列。</li>
</ul>
<p>AQS 将每条请求共享资源的线程封装成一个 CLH 变体队列的一个结点（Node）来实现锁的分配。在 CLH 变体队列中，一个节点表示一个线程，它保存着线程的引用（thread）、 当前节点在队列中的状态（waitStatus）、前驱节点（prev）、后继节点（next）。</p>
<p>AQS 中的 CLH 变体队列结构如下图所示：</p>
<p><img src="media/17419985101934/17419997043545.png" alt="CLH 变体队列结构" /></p>
<p>AQS(<code>AbstractQueuedSynchronizer</code>)的核心原理图：</p>
<p><img src="media/17419985101934/17419997043563.png" alt="CLH 变体队列" /></p>
<p>AQS 使用 <strong>int 成员变量 <code>state</code> 表示同步状态</strong>，通过内置的 <strong>线程等待队列</strong> 来完成获取资源线程的排队工作。</p>
<p><code>state</code> 变量由 <code>volatile</code> 修饰，用于展示当前临界资源的获锁情况。</p>
<pre><code class="language-java">// 共享变量，使用volatile修饰保证线程可见性
private volatile int state;
</code></pre>
<p>另外，状态信息 <code>state</code> 可以通过 <code>protected</code> 类型的<code>getState()</code>、<code>setState()</code>和<code>compareAndSetState()</code> 进行操作。并且，这几个方法都是 <code>final</code> 修饰的，在子类中无法被重写。</p>
<pre><code class="language-java">//返回同步状态的当前值
protected final int getState() {
     return state;
}
 // 设置同步状态的值
protected final void setState(int newState) {
     state = newState;
}
//原子地（CAS操作）将同步状态值设置为给定值update如果当前同步状态的值等于expect（期望值）
protected final boolean compareAndSetState(int expect, int update) {
      return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
</code></pre>
<p>以 <code>ReentrantLock</code> 为例，<code>state</code> 初始值为 0，表示未锁定状态。A 线程 <code>lock()</code> 时，会调用 <code>tryAcquire()</code> 独占该锁并将 <code>state+1</code> 。此后，其他线程再 <code>tryAcquire()</code> 时就会失败，直到 A 线程 <code>unlock()</code> 到 <code>state=</code>0（即释放锁）为止，其它线程才有机会获取该锁。当然，释放锁之前，A 线程自己是可以重复获取此锁的（<code>state</code> 会累加），这就是可重入的概念。但要注意，获取多少次就要释放多少次，这样才能保证 state 是能回到零态的。</p>
<p>再以 <code>CountDownLatch</code> 以例，任务分为 N 个子线程去执行，<code>state</code> 也初始化为 N（注意 N 要与线程个数一致）。这 N 个子线程是并行执行的，每个子线程执行完后<code>countDown()</code> 一次，state 会 CAS(Compare and Swap) 减 1。等到所有子线程都执行完后(即 <code>state=0</code> )，会 <code>unpark()</code> 主调用线程，然后主调用线程就会从 <code>await()</code> 函数返回，继续后续动作。</p>
<h3><a id="semaphore%E6%9C%89%E4%BB%80%E4%B9%88%E7%94%A8%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Semaphore 有什么用？</h3>
<p><code>synchronized</code> 和 <code>ReentrantLock</code> 都是一次只允许一个线程访问某个资源，而<code>Semaphore</code>(信号量)可以用来控制同时访问特定资源的线程数量。</p>
<p>Semaphore 的使用简单，我们这里假设有 N(N&gt;5) 个线程来获取 <code>Semaphore</code> 中的共享资源，下面的代码表示同一时刻 N 个线程中只有 5 个线程能获取到共享资源，其他线程都会阻塞，只有获取到共享资源的线程才能执行。等到有线程释放了共享资源，其他阻塞的线程才能获取到。</p>
<pre><code class="language-java">// 初始共享资源数量
final Semaphore semaphore = new Semaphore(5);
// 获取1个许可
semaphore.acquire();
// 释放1个许可
semaphore.release();
</code></pre>
<p>当初始的资源个数为 1 的时候，<code>Semaphore</code> 退化为排他锁。</p>
<p><code>Semaphore</code> 有两种模式：。</p>
<ul>
<li><strong>公平模式：</strong> 调用 <code>acquire()</code> 方法的顺序就是获取许可证的顺序，遵循 FIFO；</li>
<li><strong>非公平模式：</strong> 抢占式的。</li>
</ul>
<p><code>Semaphore</code> 对应的两个构造方法如下：</p>
<pre><code class="language-java">public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}

public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
</code></pre>
<p><strong>这两个构造方法，都必须提供许可的数量，第二个构造方法可以指定是公平模式还是非公平模式，默认非公平模式。</strong></p>
<p><code>Semaphore</code> 通常用于那些资源有明确访问数量限制的场景比如限流（仅限于单机模式，实际项目中推荐使用 Redis +Lua 来做限流）。</p>
<h3><a id="semaphore%E7%9A%84%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>Semaphore 的原理是什么？</h3>
<p><code>Semaphore</code> 是共享锁的一种实现，它默认构造 AQS 的 <code>state</code> 值为 <code>permits</code>，你可以将 <code>permits</code> 的值理解为许可证的数量，只有拿到许可证的线程才能执行。</p>
<p>调用<code>semaphore.acquire()</code> ，线程尝试获取许可证，如果 <code>state &gt;= 0</code> 的话，则表示可以获取成功。如果获取成功的话，使用 CAS 操作去修改 <code>state</code> 的值 <code>state=state-1</code>。如果 <code>state&lt;0</code> 的话，则表示许可证数量不足。此时会创建一个 Node 节点加入阻塞队列，挂起当前线程。</p>
<pre><code class="language-java">/**
 *  获取1个许可证
 */
public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
/**
 * 共享模式下获取许可证，获取成功则返回，失败则加入阻塞队列，挂起线程
 */
public final void acquireSharedInterruptibly(int arg)
    throws InterruptedException {
    if (Thread.interrupted())
      throw new InterruptedException();
        // 尝试获取许可证，arg为获取许可证个数，当可用许可证数减当前获取的许可证数结果小于0,则创建一个节点加入阻塞队列，挂起当前线程。
    if (tryAcquireShared(arg) &lt; 0)
      doAcquireSharedInterruptibly(arg);
}
</code></pre>
<p>调用<code>semaphore.release();</code> ，线程尝试释放许可证，并使用 CAS 操作去修改 <code>state</code> 的值 <code>state=state+1</code>。释放许可证成功之后，同时会唤醒同步队列中的一个线程。被唤醒的线程会重新尝试去修改 <code>state</code> 的值 <code>state=state-1</code> ，如果 <code>state&gt;=0</code> 则获取令牌成功，否则重新进入阻塞队列，挂起线程。</p>
<pre><code class="language-java">// 释放一个许可证
public void release() {
    sync.releaseShared(1);
}

// 释放共享锁，同时会唤醒同步队列中的一个线程。
public final boolean releaseShared(int arg) {
    //释放共享锁
    if (tryReleaseShared(arg)) {
      //唤醒同步队列中的一个线程
      doReleaseShared();
      return true;
    }
    return false;
}
</code></pre>
<h3><a id="countdownlatch%E6%9C%89%E4%BB%80%E4%B9%88%E7%94%A8%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>CountDownLatch 有什么用？</h3>
<p><code>CountDownLatch</code> 允许 <code>count</code> 个线程阻塞在一个地方，直至所有线程的任务都执行完毕。</p>
<p><code>CountDownLatch</code> 是一次性的，计数器的值只能在构造方法中初始化一次，之后没有任何机制再次对其设置值，当 <code>CountDownLatch</code> 使用完毕后，它不能再次被使用。</p>
<h3><a id="countdownlatch%E7%9A%84%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>CountDownLatch 的原理是什么？</h3>
<p><code>CountDownLatch</code> 是共享锁的一种实现,它默认构造 AQS 的 <code>state</code> 值为 <code>count</code>。当线程使用 <code>countDown()</code> 方法时,其实使用了<code>tryReleaseShared</code>方法以 CAS 的操作来减少 <code>state</code>,直至 <code>state</code> 为 0 。当调用 <code>await()</code> 方法的时候，如果 <code>state</code> 不为 0，那就证明任务还没有执行完毕，<code>await()</code> 方法就会一直阻塞，也就是说 <code>await()</code> 方法之后的语句不会被执行。直到<code>count</code> 个线程调用了<code>countDown()</code>使 state 值被减为 0，或者调用<code>await()</code>的线程被中断，该线程才会从阻塞中被唤醒，<code>await()</code> 方法之后的语句得到执行。</p>
<h3><a id="%E7%94%A8%E8%BF%87countdownlatch%E4%B9%88%EF%BC%9F%E4%BB%80%E4%B9%88%E5%9C%BA%E6%99%AF%E4%B8%8B%E7%94%A8%E7%9A%84%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>用过 CountDownLatch 么？什么场景下用的？</h3>
<p><code>CountDownLatch</code> 的作用就是 允许 count 个线程阻塞在一个地方，直至所有线程的任务都执行完毕。之前在项目中，有一个使用多线程读取多个文件处理的场景，我用到了 <code>CountDownLatch</code> 。具体场景是下面这样的：</p>
<p>我们要读取处理 6 个文件，这 6 个任务都是没有执行顺序依赖的任务，但是我们需要返回给用户的时候将这几个文件的处理的结果进行统计整理。</p>
<p>为此我们定义了一个线程池和 count 为 6 的<code>CountDownLatch</code>对象 。使用线程池处理读取任务，每一个线程处理完之后就将 count-1，调用<code>CountDownLatch</code>对象的 <code>await()</code>方法，直到所有文件读取完之后，才会接着执行后面的逻辑。</p>
<p>伪代码是下面这样的：</p>
<pre><code class="language-java">public class CountDownLatchExample1 {
    // 处理文件的数量
    private static final int threadCount = 6;

    public static void main(String[] args) throws InterruptedException {
        // 创建一个具有固定线程数量的线程池对象（推荐使用构造方法创建）
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        for (int i = 0; i &lt; threadCount; i++) {
            final int threadnum = i;
            threadPool.execute(() -&gt; {
                try {
                    //处理文件的业务操作
                    //......
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //表示一个文件已经被完成
                    countDownLatch.countDown();
                }

            });
        }
        countDownLatch.await();
        threadPool.shutdown();
        System.out.println(&quot;finish&quot;);
    }
}
</code></pre>
<p><strong>有没有可以改进的地方呢？</strong></p>
<p>可以使用 <code>CompletableFuture</code> 类来改进！Java8 的 <code>CompletableFuture</code> 提供了很多对多线程友好的方法，使用它可以很方便地为我们编写多线程程序，什么异步、串行、并行或者等待所有线程执行完任务什么的都非常方便。</p>
<pre><code class="language-java">CompletableFuture&lt;Void&gt; task1 =
    CompletableFuture.supplyAsync(()-&gt;{
        //自定义业务操作
    });
......
CompletableFuture&lt;Void&gt; task6 =
    CompletableFuture.supplyAsync(()-&gt;{
    //自定义业务操作
    });
......
CompletableFuture&lt;Void&gt; headerFuture=CompletableFuture.allOf(task1,.....,task6);

try {
    headerFuture.join();
} catch (Exception ex) {
    //......
}
System.out.println(&quot;all done. &quot;);
</code></pre>
<p>上面的代码还可以继续优化，当任务过多的时候，把每一个 task 都列出来不太现实，可以考虑通过循环来添加任务。</p>
<pre><code class="language-java">//文件夹位置
List&lt;String&gt; filePaths = Arrays.asList(...)
// 异步处理所有文件
List&lt;CompletableFuture&lt;String&gt;&gt; fileFutures = filePaths.stream()
    .map(filePath -&gt; doSomeThing(filePath))
    .collect(Collectors.toList());
// 将他们合并起来
CompletableFuture&lt;Void&gt; allFutures = CompletableFuture.allOf(
    fileFutures.toArray(new CompletableFuture[fileFutures.size()])
);
</code></pre>
<h3><a id="cyclicbarrier%E6%9C%89%E4%BB%80%E4%B9%88%E7%94%A8%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>CyclicBarrier 有什么用？</h3>
<p><code>CyclicBarrier</code> 和 <code>CountDownLatch</code> 非常类似，它也可以实现线程间的技术等待，但是它的功能比 <code>CountDownLatch</code> 更加复杂和强大。主要应用场景和 <code>CountDownLatch</code> 类似。</p>
<blockquote>
<p><code>CountDownLatch</code> 的实现是基于 AQS 的，而 <code>CyclicBarrier</code> 是基于 <code>ReentrantLock</code>(<code>ReentrantLock</code> 也属于 AQS 同步器)和 <code>Condition</code> 的。</p>
</blockquote>
<p><code>CyclicBarrier</code> 的字面意思是可循环使用（Cyclic）的屏障（Barrier）。它要做的事情是：让一组线程到达一个屏障（也可以叫同步点）时被阻塞，直到最后一个线程到达屏障时，屏障才会开门，所有被屏障拦截的线程才会继续干活。</p>
<h3><a id="cyclicbarrier%E7%9A%84%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>CyclicBarrier 的原理是什么？</h3>
<p><code>CyclicBarrier</code> 内部通过一个 <code>count</code> 变量作为计数器，<code>count</code> 的初始值为 <code>parties</code> 属性的初始化值，每当一个线程到了栅栏这里了，那么就将计数器减 1。如果 count 值为 0 了，表示这是这一代最后一个线程到达栅栏，就尝试执行我们构造方法中输入的任务。</p>
<pre><code class="language-java">//每次拦截的线程数
private final int parties;
//计数器
private int count;
</code></pre>
<p>下面我们结合源码来简单看看。</p>
<p>1、<code>CyclicBarrier</code> 默认的构造方法是 <code>CyclicBarrier(int parties)</code>，其参数表示屏障拦截的线程数量，每个线程调用 <code>await()</code> 方法告诉 <code>CyclicBarrier</code> 我已经到达了屏障，然后当前线程被阻塞。</p>
<pre><code class="language-java">public CyclicBarrier(int parties) {
    this(parties, null);
}

public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties &lt;= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}
</code></pre>
<p>其中，<code>parties</code> 就代表了有拦截的线程的数量，当拦截的线程数量达到这个值的时候就打开栅栏，让所有线程通过。</p>
<p>2、当调用 <code>CyclicBarrier</code> 对象调用 <code>await()</code> 方法时，实际上调用的是 <code>dowait(false, 0L)</code>方法。 <code>await()</code> 方法就像树立起一个栅栏的行为一样，将线程挡住了，当拦住的线程数量达到 <code>parties</code> 的值时，栅栏才会打开，线程才得以通过执行。</p>
<pre><code class="language-java">public int await() throws InterruptedException, BrokenBarrierException {
  try {
      return dowait(false, 0L);
  } catch (TimeoutException toe) {
      throw new Error(toe); // cannot happen
  }
}
</code></pre>
<p><code>dowait(false, 0L)</code>方法源码分析如下：</p>
<pre><code class="language-java">    // 当线程数量或者请求数量达到 count 时 await 之后的方法才会被执行。上面的示例中 count 的值就为 5。
    private int count;
    /**
     * Main barrier code, covering the various policies.
     */
    private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        final ReentrantLock lock = this.lock;
        // 锁住
        lock.lock();
        try {
            final Generation g = generation;

            if (g.broken)
                throw new BrokenBarrierException();

            // 如果线程中断了，抛出异常
            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }
            // cout减1
            int index = --count;
            // 当 count 数量减为 0 之后说明最后一个线程已经到达栅栏了，也就是达到了可以执行await 方法之后的条件
            if (index == 0) {  // tripped
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    // 将 count 重置为 parties 属性的初始化值
                    // 唤醒之前等待的线程
                    // 下一波执行开始
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction)
                        breakBarrier();
                }
            }

            // loop until tripped, broken, interrupted, or timed out
            for (;;) {
                try {
                    if (!timed)
                        trip.await();
                    else if (nanos &gt; 0L)
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    if (g == generation &amp;&amp; ! g.broken) {
                        breakBarrier();
                        throw ie;
                    } else {
                        // We're about to finish waiting even if we had not
                        // been interrupted, so this interrupt is deemed to
                        // &quot;belong&quot; to subsequent execution.
                        Thread.currentThread().interrupt();
                    }
                }

                if (g.broken)
                    throw new BrokenBarrierException();

                if (g != generation)
                    return index;

                if (timed &amp;&amp; nanos &lt;= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
    }
</code></pre>
<h2><a id="%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>虚拟线程</h2>
<p>虚拟线程在 Java 21 正式发布，这是一项重量级的更新。</p>
<p>虽然目前面试中问的不多，但还是建议大家去简单了解一下，具体可以阅读这篇文章：<a href="17419993880731.html">虚拟线程极简入门</a> 。重点搞清楚虚拟线程和平台线程的关系以及虚拟线程的优势即可。</p>
<h2><a id="%E5%8F%82%E8%80%83" class="anchor" aria-hidden="true"><span class="octicon octicon-link"></span></a>参考</h2>
<ul>
<li>《深入理解 Java 虚拟机》</li>
<li>《实战 Java 高并发程序设计》</li>
<li>Java 线程池的实现原理及其在业务中的最佳实践:阿里云开发者：<a href="https://mp.weixin.qq.com/s/icrrxEsbABBvEU0Gym7D5Q">https://mp.weixin.qq.com/s/icrrxEsbABBvEU0Gym7D5Q</a></li>
<li>带你了解下 SynchronousQueue（并发队列专题）：<a href="https://juejin.cn/post/7031196740128768037">https://juejin.cn/post/7031196740128768037</a></li>
<li>阻塞队列 — DelayedWorkQueue 源码分析：<a href="https://zhuanlan.zhihu.com/p/310621485">https://zhuanlan.zhihu.com/p/310621485</a></li>
<li>Java 多线程（三）——FutureTask/CompletableFuture：<a href="https://www.cnblogs.com/iwehdio/p/14285282.html">https://www.cnblogs.com/iwehdio/p/14285282.html</a></li>
<li>Java 并发之 AQS 详解：<a href="https://www.cnblogs.com/waterystone/p/4920797.html">https://www.cnblogs.com/waterystone/p/4920797.html</a></li>
<li>Java 并发包基石-AQS 详解：<a href="https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html">https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html</a></li>
</ul>

]]></content>
  </entry>
  
</feed>
