<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
<channel>
<atom:link href="https://www.taitres.com/feed" rel="self" type="application/rss+xml"/>
<title>一打木</title>
<link>https://www.taitres.com</link>
<description>分支不限界</description>
<language>zh-CN</language>
<copyright>© 一打木 </copyright>
<pubDate>Tue, 07 Apr 2026 10:27:34 GMT</pubDate>
<generator>Mix Space CMS (https://github.com/mx-space)</generator>
<docs>https://mx-space.js.org</docs>
<image>
    <url>https://api.taitres.com/api/v2/objects/avatar/o5sxl6l2152ezsr6cs.jpg</url>
    <title>一打木</title>
    <link>https://www.taitres.com</link>
</image>
<item>
    <title>Vue入门</title>
    <link>https://www.taitres.com/posts/vue/vueinit</link>
    <pubDate>Fri, 13 Feb 2026 03:00:00 GMT</pubDate>
    <description>Vue入门

1. Vue 是什么？为什么后端也要学？

Vue 是一个渐进式前端框架，专门用来构建</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://www.taitres.com/posts/vue/vueinit'>https://www.taitres.com/posts/vue/vueinit</a></blockquote>
      <h1>Vue入门</h1>
<h3>1. Vue 是什么？为什么后端也要学？</h3>
<ul>
<li>Vue 是一个<strong>渐进式前端框架</strong>，专门用来构建用户界面（View层）。</li>
<li>核心特点：<strong>声明式渲染</strong> + <strong>组件化</strong> + <strong>响应式系统</strong>。</li>
<li>和你熟悉的后端不同，前端关注“数据变化 → 页面自动更新”，Vue 把这部分做得非常简单。</li>
<li>学习Vue后，你能快速做出交互丰富的管理后台、单页应用（SPA）。</li>
</ul>
<p>推荐直接看官方文档（中文极好）：
<a href="https://cn.vuejs.org/">Vue 3 官方文档</a></p>
<h3>2. 快速起步（10分钟搭建第一个项目）</h3>
<p>使用 Vite（官方推荐，最快最现代的构建工具）：</p>
<pre><code class="language-undefined">[object Object]</code></pre><p>打开 <a href="http://localhost:5173%EF%BC%8C%E5%B0%B1%E8%83%BD%E7%9C%8B%E5%88%B0%E2%80%9CHello">http://localhost:5173，就能看到“Hello</a> Vue”页面。</p>
<p>项目结构重点看：</p>
<ul>
<li><code>src/App.vue</code>：根组件（单文件组件，.vue文件集成了HTML、JS、CSS）</li>
<li><code>src/main.js</code>：入口文件，创建Vue应用实例</li>
</ul>
<h3>3. 单文件组件（.vue文件）结构</h3>
<p>每个.vue文件分为三部分：</p>
<pre><code class="language-undefined">[object Object]</code></pre><p>这就是Vue最常见的写法，后面所有例子都基于<code>&lt;script setup&gt;</code>。</p>
<h3>4. 核心概念</h3>
<h4>4.1 响应式数据（ref / reactive）</h4>
<p>Vue 最牛的地方：数据变了，页面自动更新。</p>
<pre><code class="language-undefined">[object Object]</code></pre><h4>4.2 模板语法</h4>
<ul>
<li>插值：<code>{{ expression }}</code></li>
<li>指令（以v-开头）：<ul>
<li><code>v-bind</code>（缩写 <code>:</code>）绑定属性：<code>&lt;img :src=&quot;imgUrl&quot;&gt;</code></li>
<li><code>v-on</code>（缩写 <code>@</code>）绑定事件：<code>&lt;button @click=&quot;handleClick&quot;&gt;</code></li>
<li><code>v-model</code> 双向绑定表单：<code>&lt;input v-model=&quot;searchText&quot;&gt;</code></li>
<li><code>v-if / v-else</code> 条件渲染</li>
<li><code>v-for</code> 列表渲染（必须加 <code>:key</code>）</li>
</ul>
</li>
</ul>
<pre><code class="language-undefined">[object Object]</code></pre><h4>4.3 计算属性（computed）</h4>
<p>当你需要基于响应式数据做计算时用，避免在模板里写复杂逻辑。</p>
<pre><code class="language-undefined">[object Object]</code></pre><h4>4.4 事件处理</h4>
<pre><code class="language-undefined">[object Object]</code></pre><h4>4.5 生命周期</h4>
<p>组件的生命周期大致流程：</p>
<pre><code class="language-undefined">[object Object]</code></pre><ul>
<li><strong>创建阶段</strong>：实例被创建，data/props 初始化（适合初始化数据）</li>
<li><strong>挂载阶段</strong>：模板编译、DOM 插入（适合发请求、操作 DOM）</li>
<li><strong>更新阶段</strong>：数据变化触发（适合处理副作用）</li>
<li><strong>销毁阶段</strong>：组件移除（适合清理定时器、事件监听）</li>
</ul>
<h5>4.5.2. 完整钩子对比表（Vue 3）</h5>
<table>
<thead>
<tr>
<th>阶段</th>
<th>Options API（旧写法）</th>
<th>Composition API（新写法，<script setup>）</th>
<th>常见用途</th>
</tr>
</thead>
<tbody><tr>
<td>创建前</td>
<td>beforeCreate</td>
<td>setup() 开始</td>
<td>几乎不用（实例还没创建）</td>
</tr>
<tr>
<td>创建后</td>
<td>created</td>
<td>setup() 结束</td>
<td>初始化数据、发初始请求（不能操作 DOM）</td>
</tr>
<tr>
<td>挂载前</td>
<td>beforeMount</td>
<td>onBeforeMount</td>
<td>模板已编译，但还没插入 DOM</td>
</tr>
<tr>
<td>挂载后</td>
<td>mounted</td>
<td>onMounted</td>
<td><strong>最常用</strong>：发请求、操作 DOM、启动定时器</td>
</tr>
<tr>
<td>更新前</td>
<td>beforeUpdate</td>
<td>onBeforeUpdate</td>
<td>数据变了，即将重新渲染（可访问旧 DOM）</td>
</tr>
<tr>
<td>更新后</td>
<td>updated</td>
<td>onUpdated</td>
<td>渲染完成（慎用，容易无限循环）</td>
</tr>
<tr>
<td>销毁前</td>
<td>beforeUnmount</td>
<td>onBeforeUnmount</td>
<td>即将移除，清理资源</td>
</tr>
<tr>
<td>销毁后</td>
<td>unmounted</td>
<td>onUnmounted</td>
<td><strong>常用</strong>：清理定时器、事件监听、订阅</td>
</tr>
<tr>
<td>错误捕获</td>
<td>errorCaptured</td>
<td>onErrorCaptured</td>
<td>子组件报错时捕获</td>
</tr>
<tr>
<td>keep-alive 激活</td>
<td>activated</td>
<td>onActivated</td>
<td>缓存组件被激活时</td>
</tr>
<tr>
<td>keep-alive 失活</td>
<td>deactivated</td>
<td>onDeactivated</td>
<td>缓存组件被停用时</td>
</tr>
</tbody></table>
<h5>4.5.3. 代码示例对比</h5>
<p>Options API 写法（传统对象式，很多老教程用这个）</p>
<pre><code class="language-undefined">[object Object]</code></pre><p>Composition API 写法（Vue 3 推荐，<script setup> 最简洁）</p>
<pre><code class="language-undefined">[object Object]</code></pre><h3>5. 组件化（Vue的灵魂）</h3>
<ul>
<li>所有页面都是组件嵌套而成。</li>
<li>创建组件：新建一个.vue文件即可。</li>
</ul>
<p><strong>父组件 → 子组件传值（props）</strong></p>
<p>子组件 <code>Child.vue</code>：</p>
<pre><code class="language-undefined">[object Object]</code></pre><p>父组件使用：</p>
<pre><code class="language-undefined">[object Object]</code></pre><p><strong>子组件 → 父组件通信（emit）</strong></p>
<p>子组件：</p>
<pre><code class="language-undefined">[object Object]</code></pre><p>父组件：</p>
<pre><code class="language-undefined">[object Object]</code></pre><h3>6. 常用进阶</h3>
<h4>6.1 Vue Router（路由）</h4>
<p>安装：<code>npm install vue-router@4</code></p>
<p>基本配置（src/router/index.js）：</p>
<pre><code class="language-undefined">[object Object]</code></pre><p>main.js 中使用：</p>
<pre><code class="language-undefined">[object Object]</code></pre><p>模板中使用：</p>
<pre><code class="language-undefined">[object Object]</code></pre><h4>6.2 Pinia（状态管理，Vue官方推荐，替代Vuex）</h4>
<p>安装：<code>npm install pinia</code></p>
<p>创建 store（src/stores/counter.js）：</p>
<pre><code class="language-undefined">[object Object]</code></pre><p>任意组件中使用：</p>
<pre><code class="language-undefined">[object Object]</code></pre><p>推荐UI组件库（省大量CSS时间）：</p>
<ul>
<li><a href="https://element-plus.org/">Element Plus</a>（最流行）</li>
<li><a href="https://www.naiveui.com/">Naive UI</a></li>
</ul>

      <p style='text-align: right'>
      <a href='https://www.taitres.com/posts/vue/vueinit#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">698e93b0516ad4ebc3699b84</guid>
  <category>posts</category>
<category>Vue</category>
 </item>
  <item>
    <title>一些破解工具</title>
    <link>https://www.taitres.com/posts/tool/pojie</link>
    <pubDate>Fri, 10 Oct 2025 08:27:39 GMT</pubDate>
    <description>idea

https://blog.idejihuo.com/

navicat 12

http</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://www.taitres.com/posts/tool/pojie'>https://www.taitres.com/posts/tool/pojie</a></blockquote>
      <h2>idea</h2>
<p><a href="https://blog.idejihuo.com/">https://blog.idejihuo.com/</a></p>
<h2>navicat 12</h2>
<p><a href="https://www.jb51.net/article/202450.htm">https://www.jb51.net/article/202450.htm</a></p>

      <p style='text-align: right'>
      <a href='https://www.taitres.com/posts/tool/pojie#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">68e8c37b1aa1cd53f32b0da8</guid>
  <category>posts</category>
<category>工具</category>
 </item>
  <item>
    <title>写在毕业后</title>
    <link>https://www.taitres.com/notes/4</link>
    <pubDate>Fri, 29 Aug 2025 17:14:11 GMT</pubDate>
    <description>工作已经一个半月了，本来早想写点什么去总结秋招，去缅怀这段记忆，但是不得闲。闲不是像要毕业时有大把时</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://www.taitres.com/notes/4'>https://www.taitres.com/notes/4</a></blockquote>
      <p>工作已经一个半月了，本来早想写点什么去总结秋招，去缅怀这段记忆，但是不得闲。闲不是像要毕业时有大把时间挥霍，那会让我觉得玩排满了档期，不能从玩里偷闲，闲就像现在这个时刻，我刚写完一个工作的文档，我突然就感觉闲下来了，没有人玩游戏，没有人发消息，也没人，只有我自己。</p>
<p>秋招是痛苦的，没啥好总结的，拼实力也拼运。结果在现在看来是不那么好的，学了大半年，自认八股娴熟，最后去了一家国企，没问任何的技术知识。我常常想那我之前还学干嘛，我以后是不是会废了，现在说的话对于技术而言，应该是没啥上升空间了，虽然理智告诉我技术不值钱，但作为一个学习了二十多年的学生来说，在学校说知识，在社会说技术的东西感性上是底气和资本，不免有些泄气。</p>
<p>每当发工资的时候都很后悔，我不是一个节约的人，但过了学生这段小资阶段，我会觉得30的外卖有些贵了，聚餐频繁有些奢侈了，有落差有压力，更有点想家。</p>
<p>刚租房的时候，我一个人把屋子收拾好，坐在沙发上，在B站看到GAI哥的故湘·风，我不是湖南的，不知道是不是人寂寞无助的时候就会思乡，唱的每一句都扎在我心里，会想亲人，会想朋友，会想那儿时的味道，会想那山和水，高中上学时的公交，和窗外的城。会想一切北京不会有的，真的忍不住落泪，哭完还得点个乡村基。</p>
<p>工作了，开始干事了就没那么多想法了，刚开始熟悉了会儿系统，改了改代码，我还是很喜欢改代码的，那一周很快乐很规律。上班半个多月，事情确实忙不过来了，我开始出差。我明明还什么都不知道呀，没办法，做中学，社会不会给人时间准备好，干活不是考试不是学了才考，分数没有八九十，只有干完和没干完。</p>
<p>第一次出差很新奇，虽然工作前不知道原来干的是外包的活，本着中国人骨子里的既来之则安之，我很想把事干好。与出差单位的交流是困难的，我不懂很多东西，我得猜，我猜不出来了，我得问，问了没人回，我得等，一边等一边自己琢磨，每个人都有自己的事。不同的城市确实有不一样的风情，但下班后的我没心力去逛太多。</p>
<p>工作中最常见的就是昨天的计划，早上一上班就被打乱了，意想不到的事永远意想不到。下周出差得去四川了，算是一个小的回家，至少能找点小馆子一个人吃饭。</p>
<p>生活往前走，也许我真的做了很错的决定，但就这样吧，我以前最擅长的就是乐观，但离开父母太久，已经快忘了。没有过不去的坎，但有失去的心气。</p>

      <p style='text-align: right'>
      <a href='https://www.taitres.com/notes/4#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">68b1dfe3a2a91ef7ae02fa4b</guid>
  <category>notes</category>
false
 </item>
  <item>
    <title>Linux命令</title>
    <link>https://www.taitres.com/posts/computer/linuxcommand</link>
    <pubDate>Thu, 25 Jul 2024 12:45:45 GMT</pubDate>
    <description>系统命令

Linux怎么看进程号/进程占用的 CPU?

ps 和 top 命令都能查看进程号和进</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://www.taitres.com/posts/computer/linuxcommand'>https://www.taitres.com/posts/computer/linuxcommand</a></blockquote>
      <h1>系统命令</h1>
<h2>Linux怎么看进程号/进程占用的 CPU?</h2>
<p>ps 和 top 命令都能查看进程号和进程占用的的 CPU。</p>
<p>TOP命令：进程按照CPU使用率的降序排列，最占用CPU的进程会显示在列表的顶部。</p>
<p></p>
<p></p>
<h2>Linux怎么看进程中的线程？</h2>
<p>先通过 ps 命令找到进程的 pid 号之后，通过 ps -T -p <pid>，就可以显示进程中的线程了。或者 top -H -p <pid> 命令也可以。</p>
<p></p>
<p></p>
<h2>Linux怎么看端口被哪个进程占用了？</h2>
<p>通过 netstat 或者 lsof 命令都可以知道端口被哪个进程占用了。</p>
<p></p>
<h2>怎么查看一个进程占用的端口号？</h2>
<p>通过 netstat 或者 lsof 命令输出的内容之后，通过 grep 指定的进程号，然后就可以过滤出进程占用的端口号。</p>
<p></p>
<h2>Linux怎么看tcp状态？</h2>
<p>netstat 或者 ss 命令可以查看到 tcp 连接的状态</p>
<p></p>
<p></p>
<h2>如何判断远端端口是否开启？</h2>
<p>可以用 nc 或者 telnet 来判断远程端的端口是否开启</p>
<p></p>
<h2>Linux查看TCP连接数</h2>
<pre><code class="language-undefined">[object Object]</code></pre><ul>
<li>-nat：显示所有tcp连接状态，并以数字形式显示端口号和IP地址，而不进行主机名解析。 </li>
<li>grep ESTABLISHED：通过 grep 命令筛选出处于 ESTABLISHED 状态的连接。</li>
<li>wc -l：wc 命令用于统计输出的行数，-l 参数表示只统计行数。</li>
</ul>
<p>先通过 netstat -nat 命令显示 tcp 连接信息，然后用 grep 过滤出处于 ESTABLISHED 状态的 tcp 连接，最后用 wc -l 统计连接个数，就可以查看当前系统上处于 ESTABLISHED 状态的连接数了</p>
<p></p>
<h2>Linux top 命令有哪些信息？</h2>
<p>主要有系统的负载均衡情况、 CPU使用情况、内存使用情况、运行进程数量，还有进程列表，进程列表主要会显示当前运行的进程的详细信息，包括进程ID（PID）、CPU使用率、内存使用率、进程优先级等。</p>
<h2>CPU使用率达到100%呢？怎么排查？</h2>
<p>首先先通过 top 命令找到占用CPU最高的进程，然后通过 top -Hp (top -H -p)命令找到进程中占用CPU最高的线程，记录这个线程的 id 号，接着通过 jstack 打印这个线程的堆栈信息，通过这些信息定位到具体的代码位置去排查问题。</p>
<h2>Linux top结果CPU占用会超过100%吗？</h2>
<p>top命令显示的是所有 cpu 占用的总数，如果 cpu 是多核心的，那么是会观察到 cpu 显示超过 100%的，可以通过按键盘数字 1，来显示每个 cpu 的 cpu 占用率。</p>
<h2>Linux如何查看内存使用情况？</h2>
<p>可以用 free 命令来看</p>
<p></p>
<h2>Linux怎么查看磁盘剩余多少</h2>
<p>df -h 就能查看磁盘的空间大小了</p>
<p></p>
<h1>文件相关的命令</h1>
<h2>Linux查看文件的命令有哪些？</h2>
<p>查看文件内容常用的命令是： </p>
<ul>
<li>cat，可以显示文件的所有内容 </li>
<li>head，显示文件的开头部分，默认显示头部 10 行。</li>
<li>tail：显示文件的末尾部分，默认显示末尾 10 行。 </li>
<li>more：逐页显示文件内容，只能向前翻页，无法向后滚动 </li>
<li>less ：与 more 类似，提供更多功能，如向前/向后翻页、搜索等。</li>
</ul>
<h2>Linux查看文件大小命令</h2>
<p>可以用 ls -l 或者 du -h 命令查看文件的大小。</p>
<p>ls -l：显示文件的详细信息，包括文件大小（以字节为单位）。</p>
<p>du -h：显示目录或文件的大小，以人类可读的方式（例如 KB、MB）显示。</p>
<p></p>
<h2>Linux查询当前所在目录的语句</h2>
<p>pwd</p>
<h2>Linux创建文件夹和文件的语句是什么？</h2>
<p>创建文件夹用 mkdir 命令， 创建文件用 touch 命令</p>
<h2>Linux如何删除一个文件？</h2>
<p>可以使用 rm 命令，可以使用以下语句来删除一个文件：rm your_file_name</p>
<p><strong>如何删除一个目录（文件夹）？</strong></p>
<p>可以使用 <code>rm</code> 命令结合 <code>-r</code> 参数来递归地删除目录：rm -r directory_name</p>
<h2>Linux cp 命令怎么复制整个文件夹？</h2>
<p>使用 -r 选项来进行递归复制。</p>
<pre><code class="language-undefined">[object Object]</code></pre><h2>Linux如何文件重命名</h2>
<p>可以使用 mv 命令。可以使用以下语句来重命名文件：mv old_file_name new_file_name</p>
<h2>Linux 文件夹中如何查看最近被修改的文件？</h2>
<p>可以用 ls 命令结合 -lt 参数，按照文件修改时间排序显示，这样最近修改的文件将会显示在最上面。</p>
<p>-l 是以列表方式显示文件和目录的详细信息，-t 是按照文件修改时间排序，最近修改的文件将会显示在最上面。</p>
<h2>Linux怎么修改文件的权限？</h2>
<p>chmod命令的基本语法：<code>chmod [选项] 权限模式 文件名</code></p>
<p>其中，选项可以是：</p>
<ul>
<li>-c：显示修改的详细信息。</li>
<li>-R：递归地修改目录及其子目录下的文件权限。</li>
</ul>
<p>权限模式可以使用数字或符号两种方式表示。 </p>
<ul>
<li>数字方式：每个权限用一个数字表示，分别对应读（r）、写（w）和执行（x）权限。数字1表示执行权限，数字2表示写权限，数字4表示读权限。将这三个数字相加，即可得到对应的权限模式。例如，权限模式为rwxr-xr--可以用数字表示为754。</li>
<li>符号方式：使用u（所有者）、g（所属组）和o（其他人）表示权限的对象，加上+、-、=表示添加、删除或设置权限。例如，将文件的所有者权限设置为读写，可以使用命令chmod u+rw 文件名。</li>
</ul>
<p>例：通过 chmod 命令来修改文件权限，如果这个文件需要执行权限，可以通过 chmod u+x 文件名命令来实现。</p>
<h2>Linux中如何查找一个文件</h2>
<p>find命令：<code>find /path/to/search -name &quot;filename&quot;</code></p>
<h2>Linux 怎么查看实时滚动日志？</h2>
<p>可以用 tail 命令，再加一个 -f 的参数，来查看实时滚动日志</p>
<h2>现在有一个txt文件，如何查看后三行</h2>
<p>tail -n 3 your_file.txt</p>
<h2>查找一个字符串是否在文件中</h2>
<p>可以使用grep命令来查找一个字符串是否在文件中出现。grep命令用于在文件中搜索指定的字符串模式，并输出匹配的行。命令的基本语法：<code>grep [选项] 字符串 文件名</code></p>
<p>选项可以是：</p>
<ul>
<li>-i：忽略大小写。</li>
<li>-r：递归地搜索指定目录及其子目录下的文件。 </li>
<li>-n：显示匹配行的行号。 </li>
<li>-l：仅显示包含匹配字符串的文件名。</li>
</ul>
<h3>Linux怎么查找一个文件里的某一个字符串的位置</h3>
<p><code>grep -n &quot;your_string&quot; your_file</code>，这将显示包含指定字符串的行数。</p>
<h3>在一个目录下寻找含有字符串“admin”的文件</h3>
<p><code>grep -r &quot;admin&quot; 目录名</code></p>
<h2>Linux查看文件行数命令</h2>
<p>wc -l your_file</p>
<h2>统计一个文件中某一个字段的次数</h2>
<p>可以通过 grep -o 过滤出字段之后，然后用 wc -l 统计出现的次数。<code>grep -o &#39;字段A&#39; filename | wc -l</code></p>
<p>-o 或 --only-matching : 只显示匹配PATTERN 部分。</p>

      <p style='text-align: right'>
      <a href='https://www.taitres.com/posts/computer/linuxcommand#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">66a248f9daa01fc3ed869db9</guid>
  <category>posts</category>
<category>计算机基础</category>
 </item>
  <item>
    <title>操作系统（五）网络 I/O</title>
    <link>https://www.taitres.com/posts/computer/systemio</link>
    <pubDate>Tue, 23 Jul 2024 11:22:07 GMT</pubDate>
    <description>Linux五种IO模型



阻塞I/O： 这是最常见的I/O模型。在此模式中，当应用程序执行I/O</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://www.taitres.com/posts/computer/systemio'>https://www.taitres.com/posts/computer/systemio</a></blockquote>
      <h2>Linux五种IO模型</h2>
<p></p>
<ul>
<li><strong>阻塞I/O</strong>： 这是最常见的I/O模型。在此模式中，当应用程序执行I/O操作时，如果数据还没有准备好，应用程序就会被阻塞（挂起），直到数据准备好为止。这期间，应用程序不能做其他事情。 </li>
<li><strong>非阻塞I/O</strong>：在此模式中，如果I/O操作的数据还没有准备好，操作会立即返回一个错误，而不是阻塞应用程序。应用程序可以继续执行其他操作，也可以反复尝试该I/O操作。 </li>
<li><strong>I/O多路复用</strong>：也常称为事件驱动I/O。在此模式中，应用程序可以同时监控多个I/O描述符（比如，socket），当任何一个I/O描述符准备好数据时，应用程序就可以对其进行处理。这可以在一个单独的进程或线程中同时处理多个I/O操作，并且不需要阻塞或轮询。select、poll、epoll都是这种模型的实现。 </li>
<li><strong>信号驱动</strong>：在此模型中，应用程序可以向操作系统注册一个信号处理函数，当数据准备好时，操作系统会发送一个信号，应用程序可以在接收到信号时读取数据。这种模式避免了阻塞和轮询，但是编程复杂性较高。</li>
<li><strong>异步I/O</strong>：在此模型中，应用程序发起I/O操作后，可以立即开始做其他事情，当数据准备好时，操作系统会将数据复制到应用程序的缓冲区，并通知应用程序。这种模型的优点是应用程序不需要等待I/O操作的完成，缺点是编程复杂性较高。</li>
</ul>
<blockquote>
<p>5 种 I/O 模型主要是阻塞 I/O、非阻塞 I/O、信号驱动式 I/O、I/O 多路复用、异步 I/O，其中前四个都属于同步I/O模型。 </p>
<p>阻塞同步I/O模型和非阻塞同步I/O模型的区别在于：进程发起系统调用后，是会被挂起直到收到数据后在返回、还是立即返回成功或错误。</p>
<p>同步I/O 和异步I/O 的区别在于：将数据从内核复制到用户空间时，用户进程是否会阻塞，如果用户进程会阻塞，则是同步I/O，如果不会阻塞就是异步  I/O。</p>
</blockquote>
<h2>阻塞IO和非阻塞IO的应用场景问题，有一个计算密集型的场景，和一个给用户传视频的场景，分别应该用什么io？</h2>
<p>计算密集型的场景，需要消耗的是 CPU 资源，用阻塞 IO 会比较好，如果用非阻塞 IO，在非阻塞IO模型中，用户线程需要不断地询问内核数据是否就绪，也就说非阻塞IO不会交出CPU，而会一直占用CPU。 </p>
<p>用户传视频的场景，瓶颈不是 CPU 资源，用非阻塞 IO 比较好，使用非阻塞IO可以避免阻塞在传输函数上，提高程序的并发性和响应时间。</p>
<h2>谈谈你对 I/O 多路复用的理解</h2>
<p>如果不使用 I/O 多路复用，服务端要并发处理多个客户端的 I/O 事件的话，需要通过创建子进程或者线程的方式来实现，也就是针对每一个连接的  I/O 事件要需要一个子进程或者线程来处理，但是随着客户端越来越多，意味着服务端需要创建更多的子进程或者线程，这样对系统的开销太大了。 </p>
<p>那么有了 I/O 多路复用就可以解决这个问题，I/O 多路复用可以实现是多个I/O复用一个进程，也就是只需要一个进程就能并发处理多个客户端的 I/O 事件， 进程可以通过select、poll、epoll这类 I/O 多路复用系统调用接口从内核中获取有事件发生的 socket 集合，然后应用程序就可以遍历这个集合，对每一个 socket 事件进行处理。 </p>
<p>Redis 单线程也能做到高性能的原因，也跟 I/O 多路复用有关系。</p>
<h2>select、poll、epoll 有什么区别？</h2>
<p>select 和 poll 内部都是使用「线性结构」来存储进程关注的 Socket 集合，在使用的时候，首先需要把关注的 Socket 集合通过 select/poll 系统调用从用户态拷贝到内核态，然后由内核检测事件，当有网络事件产生时，内核需要遍历进程关注 Socket 集合，找到对应的 Socket，并设置其状态为可读/可写，然后把整个 Socket 集合从内核态拷贝到用户态，用户态还要继续遍历整个 Socket 集合找到可读/可写的 Socket，然后对其处理。 很明显发现，select 和 poll 的缺陷在于，当客户端越多，也就是 Socket 集合越大，Socket 集合的遍历和拷贝会带来很大的开销，epoll 通过两个方面解决了 select/poll 的问题。 </p>
<p>epoll 在内核里使用「红黑树」来关注进程所有待检测的 Socket，通过 <code>epoll_ctl()</code> 函数加入内核中的红黑树里，红黑树是个高效的数据结构，增删改一般时间复杂度是 O(logn)，通过对这棵黑红树的管理，不需要像 select/poll 在每次操作时都传入整个 Socket 集合，只需要传入一个待检测的 socket，减少了内核和用户空间大量的数据拷贝和内存分配。</p>
<p>epoll 使用事件驱动的机制，内核里维护了一个链表来记录就绪事件，当某个 socket 有事件发生时，通过回调函数内核会将其加入到这个就绪事件列表中，当用户调用 <code>epoll_wait() </code>函数时，只会返回有事件发生的文件描述符链表表，不需要像 select/poll 那样轮询扫描整个 socket 集合，大大提高了检测的效率。</p>
<h2>select、poll、epoll 适合哪些应用场景？</h2>
<p>在连接数较少并且都十分活跃的情况下，选择 select 或者 poll 会比 epoll 性能好，因为  epoll 中的所有描述符都存储在内核中，必须使用epoll_ctl来添加到内核的红黑树中，这就意味着每一个新的连接都需要两次系统调用（epoll_ctl+epoll_wait），而在select 和poll中只需要一次，频繁系统调用降低效率。</p>
<p> 在连接数较多并且有很多的不活跃连接时，epoll 会比 select 和 poll 性能好，因为 epoll 在内核用红黑树来关注所有待检测的  socket，不需要像 select/poll 在每次操作时都传入整个 Socket 集合，减少了内核和用户空间大量的数据拷贝和内存分配，再加上 epoll 有就绪队列，也不需要像 select/poll 那样轮询扫描整个集合（包含有和无事件的 Socket ），大大提高了检测的效率。</p>
<h2>epoll ET 模式和 LT 模式有什么区别？哪一个更高效？</h2>
<p>ET 模式和 LT 模式 区别如下：</p>
<ul>
<li>边缘触发（ET，Edge Trigger）：当描述符从未就绪变为就绪时，只会通知一次，之后不会再通知，因此我们程序要保证一次性将事件处理完。 </li>
<li>水平触发（LT，Level Trigger）：当文件描述符就绪时，会触发通知，如果用户程序没有一次性把数据读/写完，下次还会发出可读/可写信号进行通知。</li>
</ul>
<p> 边缘触发的效率比水平触发的效率要高，因为边缘触发可以减少 epoll_wait 的系统调用次数。</p>
<h2>零拷贝技术了解过吗？说一下原理</h2>
<p><strong>传统文件传输</strong>：</p>
<ul>
<li>共发生了 <strong>4 次用户态与内核态的上下文切换</strong>，因为发生了两次系统调用，一次是 read() ，一次是 write()，每次系统调用都得先从用户态切换到内核态，等内核完成任务后，再从内核态切换回用户态。 </li>
<li>其次，还发生了 <strong>4 次数据拷贝</strong>，其中两次是 DMA 的拷贝，另外两次则是通过 CPU 拷贝的。</li>
</ul>
<p><strong>sendfile</strong>：</p>
<pre><code class="language-undefined">[object Object]</code></pre><p>它的前两个参数分别是目的端和源端的文件描述符，后面两个参数是源端的偏移量和复制数据的长度，返回值是实际复制数据的长度。 </p>
<ul>
<li>首先，它可以替代前面的 read() 和 write() 这两个系统调用，这样就可以减少一次系统调用，也就减少了 2 次上下文切换的开销。 </li>
<li>其次，该系统调用，可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里，不再拷贝到用户态，这样就只有 <strong>2 次上下文切换</strong>，和 <strong>3 次数据拷贝</strong>。</li>
</ul>
<p><strong>零拷贝</strong>：</p>
<p>对于支持网卡支持 SG-DMA 技术的情况下， <code>sendfile() </code>系统调用的过程发生了点变化，具体过程如下： </p>
<ul>
<li>第一步，通过 DMA 将磁盘上的数据拷贝到内核缓冲区里； </li>
<li>第二步，缓冲区描述符和数据长度传到 socket 缓冲区，这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里，此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中，这样就减少了一次数据拷贝，这个过程之中，只进行了 2 次数据拷贝；</li>
</ul>
<p>这就是所谓的<strong>零拷贝（Zero-copy）技术</strong>，因为我们没有在内存层面去拷贝数据，也就是说全程没有通过 CPU 来搬运数据，所有的数据都是通过 DMA 来进行传输的。</p>
<p>零拷贝技术的文件传输方式相比传统文件传输的方式，减少了 2 次上下文切换和数据拷贝次数，<strong>只需要 2 次上下文切换和数据拷贝次数</strong>，就可以完成文件的传输，而且 2 次的数据拷贝过程，都不需要通过 CPU，2 次都是由 DMA 来搬运。</p>
<p>所以零拷贝技术主要是为了提升文件传输的效率，Kafka 消息队列 I/O 的吞吐量高的原因，也是因为使用了零拷贝技术。</p>
<p></p>
<p></p>
<p></p>
<h2>reactor 模式有哪些方案？</h2>
<p>常见的 Reactor 实现方案有三种。</p>
<p>第一种方案单 Reactor 单进程 / 线程，不用考虑进程间通信以及数据同步的问题，因此实现起来比较简单，这种方案的缺陷在于无法充分利用多核 CPU，而且处理业务逻辑的时间不能太长，否则会延迟响应，所以不适用于计算机密集型的场景，适用于业务处理快速的场景，比如 Redis（6.0之前 ） 采用的是单 Reactor 单进程的方案。</p>
<p>第二种方案单 Reactor 多线程，通过多线程的方式解决了方案一的缺陷，但它离高并发还差一点距离，差在只有一个 Reactor 对象来承担所有事件的监听和响应，而且只在主线程中运行，在面对瞬间高并发的场景时，容易成为性能的瓶颈的地方。</p>
<p>第三种方案多 Reactor 多进程 / 线程，通过多个 Reactor 来解决了方案二的缺陷，主 Reactor 只负责监听事件，响应事件的工作交给了从 Reactor，Netty 和 Memcache 都采用了「多 Reactor 多线程」的方案，Nginx 则采用了类似于 「多 Reactor 多进程」的方案。</p>
<h2>proactor 和 reactor 模式有什么区别？</h2>
<ul>
<li><strong>Reactor 是非阻塞同步网络模式，感知的是就绪可读写事件</strong>。在每次感知到有事件发生（比如可读就绪事件）后，就需要应用进程主动调用 read 方法来完成数据的读取，也就是要应用进程主动将 socket 接收缓存中的数据读到应用进程内存中，这个过程是同步的，读取完数据后应用进程才能处理数据。</li>
<li><strong>Proactor 是异步网络模式， 感知的是已完成的读写事件</strong>。在发起异步读写请求时，需要传入数据缓冲区的地址（用来存放结果数据）等信息，这样系统内核才可以自动帮我们把数据的读写工作完成，这里的读写工作全程由操作系统来做，并不需要像 Reactor 那样还需要应用进程主动发起 read/write 来读写数据，操作系统完成读写工作后，就会通知应用进程直接处理数据。</li>
</ul>
<p>因此，<strong>Reactor 可以理解为「来了事件操作系统通知应用进程，让应用进程来处理」</strong>，而 <strong>Proactor 可以理解为「来了事件操作系统来处理，处理完再通知应用进程」</strong>。</p>
<p>参考：</p>
<p><a href="https://xiaolincoding.com/os/">https://xiaolincoding.com/os/</a></p>

      <p style='text-align: right'>
      <a href='https://www.taitres.com/posts/computer/systemio#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">66a2358edaa01fc3ed869d0f</guid>
  <category>posts</category>
<category>计算机基础</category>
 </item>
  <item>
    <title>操作系统（四）文件系统</title>
    <link>https://www.taitres.com/posts/computer/systemfile</link>
    <pubDate>Sun, 21 Jul 2024 07:52:29 GMT</pubDate>
    <description>读取一个文件的时候，操作系统会发生什么？



读取磁盘文件的时候，CPU 不会负责从磁盘读取数据的</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://www.taitres.com/posts/computer/systemfile'>https://www.taitres.com/posts/computer/systemfile</a></blockquote>
      <h2>读取一个文件的时候，操作系统会发生什么？</h2>
<p>读取磁盘文件的时候，CPU 不会负责从磁盘读取数据的过程，而是将磁盘数据搬运到内存的工作全部交给 DMA （直接内存访问）控制器，CPU 不再参与任何与数据搬运相关的事情，这样 CPU 就可以去处理别的工作，不会阻塞住。 具体的过程是这样的： </p>
<ul>
<li>用户进程调用 read 方法，向操作系统发出 I/O 请求，然后进程进入阻塞状态； </li>
<li>操作系统收到请求后，进一步将 I/O 请求发送 DMA，然后 CPU 会执行其他任务；</li>
<li>DMA 进一步将 I/O 请求发送给磁盘，磁盘收到 DMA 的 I/O 请求，把数据从磁盘读取到磁盘控制器的缓冲区中，当磁盘控制器的缓冲区被读满后，向 DMA 发起中断信号，告知自己缓冲区已满；</li>
<li>DMA 收到磁盘的信号，将磁盘控制器缓冲区中的数据拷贝到内核缓冲区中，数据准备好后，然后就会发送中断信号给 CPU，CPU 收到 DMA 的信号，于是将数据从内核拷贝到用户空间，read 系统调用返回；</li>
</ul>
<h2>操作系统复制一个文件的流程是怎么样的？</h2>
<p>复制文件的流程：</p>
<ul>
<li>打开源文件，调用 read 系统调用读取源文件的数据，这时候会通过 DMA 将磁盘数据拷贝到内核缓冲区，然后 CPU 再将内核缓冲区的数据拷贝到用户缓冲区，然后 read 系统调用返回。</li>
<li>打开目标文件，调用 write 系统调用将从源文件读到的数据，写入到目标文件，这时候 CPU 会将用户缓冲区的数据拷贝到内核缓冲区， 然后write函数返回，后续操作系统会将内核缓冲区的数据刷入磁盘。</li>
</ul>
<h2>软链接和硬链接有什么区别？</h2>
<p>软链接和硬链接都是给文件取一个别名，区别主要有：</p>
<ul>
<li>软链接文件有独立的 inode 节点，文件的内容是目标文件的路径，不存储实际的文件数据，支持跨文件系统建立，而且目标文件删除了，软链接还是存在的，只不过指向的文件找不到了（类似快捷方式）</li>
<li>硬链接文件和目标文件都共用一个 inode 节点，说明他们是用一个文件，不支持跨文件系统建立，删除文件的时候，要除文件的所有硬链接以及源文件时，系统才会彻底删除该文件。</li>
</ul>

      <p style='text-align: right'>
      <a href='https://www.taitres.com/posts/computer/systemfile#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">66a20440daa01fc3ed869aef</guid>
  <category>posts</category>
<category>计算机基础</category>
 </item>
  <item>
    <title>操作系统（三）内存管理</title>
    <link>https://www.taitres.com/posts/computer/systemmemory</link>
    <pubDate>Sat, 20 Jul 2024 07:40:23 GMT</pubDate>
    <description>为什么操作系统会有虚拟内存？

虚拟内存可以使得进程对运行内存超过物理内存大小，因为程序运行符合局部</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://www.taitres.com/posts/computer/systemmemory'>https://www.taitres.com/posts/computer/systemmemory</a></blockquote>
      <h2>为什么操作系统会有虚拟内存？</h2>
<ul>
<li>虚拟内存可以使得进程对运行内存超过物理内存大小，因为程序运行符合局部性原理，CPU 访问内存会有很明显的重复访问的倾向性，对于那些没有被经常使用到的内存，我们可以把它换出到物理内存之外，比如硬盘上的 swap 区域。 </li>
<li>由于每个进程都有自己的页表，所以每个进程的虚拟内存空间就是相互独立的。进程也没有办法访问其他进程的页表，所以这些页表是私有的，这就解决了多进程之间地址冲突的问题。</li>
<li>页表里的页表项中除了物理地址之外，还有一些标记属性的比特，比如控制一个页的读写权限，标记该页是否存在等。在内存访问方面，操作系统提供了更好的安全性。（如写时复制的权限）</li>
</ul>
<h2>什么是内存分段？</h2>
<p>分段是为了满足程序员在编写代码的时候的一些逻辑需求(比如数据共享，数据保护，动态链接等)。 </p>
<p>分段内存管理当中，地址是二维的，一维是段号，二维是段内地址；其中每个段的长度是不一样的，而且每个段内部都是从0开始编址的。</p>
<p>由于分段管理中，每个段内部是连续内存分配，但是段和段之间是离散分配的，因此也存在一个逻辑地址到物理地址的映射关系，相应的就是段表机制。</p>
<h2>什么是内存分页？</h2>
<p>把内存空间划分为大小相等且固定的块，作为主存的基本单位。</p>
<p>因为程序数据存储在不同的页面中，而页面又离散的分布在内存中，因此需要一个页表来记录映射关系，以实现从页号到物理块号的映射。</p>
<p>访问分页系统中内存数据需要两次的内存访问 (一次是从内存中访问页表，从中找到指定的物理块号，加上页内偏移得到实际物理地址；第二次就是根据第一次得到的物理地址访问内存取出数据)。</p>
<h2>段式管理和页式管理会出现内存碎片吗？</h2>
<p>段式管理可以做到根据段根据实际需求分配空间，所以有多少需求就分配多大的段，那么在段的内部就不会产生空间浪费，也就是没有碎片，但是由于每个段的长度不是固定的，所以段与段之间会产生碎片，这种称为<strong>外部碎片</strong>。 </p>
<p>而页则是固定大小的，通常是 4k，假设我们要为代码准备空间，即使这一段机器码不足4k，我们也要为它分配 4k，因为一个页的大小就是 4k，我们最少只能分配一个页，所以页式管理，页与页之间紧密排列，但是页内出现内存浪费，也就是内存碎片，这种称为<strong>内部碎片</strong>。</p>
<h2>页面置换算法</h2>
<p><strong>最佳置换算法（OPT）</strong>：置换掉未来永远不会被访问，理论上这种算法效果最好，但是在实际中无法得知未来进程的行为，所以无法实现。</p>
<p><strong>先进先出算法（FIFO）</strong>：置换掉最早进入内存的页面。这个算法简单，易于实现，但可能导致&quot;先进入&quot;的页面是常用页面而被频繁置换，造成性能下降。</p>
<p><strong>最近最久未使用算法（LRU）</strong>：置换掉最近一段时间内最少被访问的页面，算法的实现是给每个页面设置一个时间戳，记录最近一次访问的时间，如果发生缺页错误，则从所有页面中淘汰时间戳最久远的一个。</p>
<p><strong>最少使用算法（LFU）</strong>：置换掉访问频率最少的内存页面，实现方式是，对每个页面设置一个「访问计数器」，每当一个页面被访问时，该页面的访问计数器就累加 1。在发生缺页中断时，淘汰计数器值最小的那个页面。然而，当一个页面在进程的初始阶段大量使用但是随后不再使用时，会出现问题。由于被大量使用，它有一个大的计数，即使不再需要却仍保留在内存中。一种解决方案是，定期地将计数右移 1 位，以形成指数衰减的平均使用计数。</p>
<p><strong>时钟算法（Clock）</strong>：每页设置一个访问位，再将内存中的所有页面都通过链接指针链接成一个循环队列。当某个页面被访问时，其访问位置1。淘汰时，检查其访问位，如果是0，就换出；若为1，则重新将它置0，然后检查下一个页面，如果到队列中的最后一个页面时，若其访问位仍为1，则再返回到队首再去检查第一个页面。</p>
<h2>数组的物理空间连续吗？</h2>
<p>不是的，数组的虚拟地址是连续的，但是虚拟地址映射到物理地址是由操作系统负责的，操作系统并不会保证映射的物理地址也是连续的。</p>
<h2>栈的增长趋势是什么？</h2>
<p>栈的增长趋势是向内存地址减小的方向增长。也就是说，栈的底部在较高的内存地址，而栈的顶部在较低的内存地址。当新的元素被添加到栈中时，栈的顶部会向较低的内存地址方向移动，栈的大小会增加。当元素从栈中被移除时，栈的顶部会向较高的内存地址方向移动，栈的大小会减小。因此，栈的增长趋势是由内存布局决定的，向内存地址减小的方向增长。</p>
<h2>堆区和栈区有什么区别？</h2>
<ul>
<li>栈区的内存用于函数调用栈，主要保存函数的入参、局部变量、返回值等，栈区内存不可被其他线程共享，栈区的内存管理是由编译器自动完成的，编译时就确定了变量的生命周期，不需程序员来分配内存和释放内存，当函数执行结束时，栈区中的数据会自动被释放。</li>
<li>堆区内存需要通过动态内存分配函数来申请，堆区的内存可被其他线程共享，堆区内存的生命周期是由程序员控制的，需要进行显示分配和释放内存，如果反复向操作系统申请堆内存而不释放，会导致内存泄露。在 C / C++ 中，必须由程序员手动释放堆内存。而 Java / Golang 中有垃圾回收器，会定期主动回收内存。但是即使有垃圾回收器，也有内存泄漏的风险，比如长期持有某个大对象的引用。</li>
<li>堆区可分配的大小会比栈大很多，因为栈的内存是限的，通常是 8MB 大小，而堆的空间较大，受限于系统中有效的虚拟内存，比如 32 位系统，虚拟内存最大 4GB，64 位操作系统就 128TB。</li>
<li>栈区的内存分配效率会比堆区分配内存效率高，因为分配栈区内存，只需要移动栈指针即可，而分配堆区内存需要在堆中找到可用的内存块。</li>
</ul>
<h2>在栈上的数据操作比堆上快很多的原因？</h2>
<ul>
<li><p>访问栈上的数据是可以直接访问，因为 CPU 有栈相关的寄存器直接对栈进行访问，而堆上的数据需要通过指针进行访问，是一个间接寻址的过程，访问速度相对较慢。 </p>
</li>
<li><p>还有栈上的数据是连续存在，这样可以更好的利用 CPU 的 CPU Cache，访问栈上的数据缓存命中率比较高，而堆上的数据分布是散乱的，无法充分利用 CPU 缓存，导致缓存命中率较低，访问速度相对较慢。</p>
</li>
</ul>
<h2>32 位操作系统，4G物理内存，程序可以申请 8GB内存吗？</h2>
<p>32 位系统的内核空间占用 1G，位于最高处，剩下的 3G 是用户空间； </p>
<p>64 位系统的内核空间和用户空间都是 128T，分别占据整个内存空间的最高和最低处，剩下的中间部分是未定义的。</p>
<p>程序申请的内存实际上是虚拟内存，32 位操作系统中用户空间的虚拟内存大小是 3 GB，进程最多只能申请 3 GB 大小的虚拟内存空间，所以进程申请 8GB 内存的话，在申请虚拟内存阶段就会失败。</p>
<h2>64 位操作系统，4G物理内存，程序可以申请 8GB内存吗？</h2>
<p>在 64位 位操作系统，因为进程理论上最大能申请 128 TB 大小的虚拟内存，即使物理内存只有 4GB，申请 8G 内存也是没问题，因为申请的内存是虚拟内存。如果这块虚拟内存被访问了，要看系统有没有 Swap 分区： </p>
<ul>
<li>如果没有 Swap 分区，因为物理空间不够，进程会被操作系统杀掉，原因是 OOM（内存溢出）； </li>
<li>如果有 Swap 分区，即使物理内存只有 4GB，程序也能正常使用 8GB 的内存，进程可以正常运行；</li>
</ul>
<h2>fork 的写时复制是如何实现的？</h2>
<p>fork 是创建进程的系统调用， 执行 fork 的时候，子进程会复制父进程的 PCB 复制一份，这里包含页表、文件资源等，并且将页表的权限设置为只读，但是并不会复制物理内存，所以这时候父子进程的页表指向的<strong>物理内存的是同一片区域</strong>，此时父子进程的内存数据都是共享的。 </p>
<p>当父进程或者子进程对这片共享的内存数据进行了写操作，发现页表是只读的属性，这时候就会发生<strong>写保护中断</strong>，内核就会为发生中断的进程<strong>分配新的物理内存</strong>，把老的物理页的数据拷贝进这个新的物理页，然后将最新的数据写入到这个新的物理页，最后把发生中断的虚拟地址映射到新的物理页，同时该进程的页表的权限设置为可读写，这就完成了一次写时复制。</p>
<h2>malloc会陷入内核态吗？malloc的底层原理是什么？</h2>
<p>malloc 不是系统调用函数，不会陷入到内核态，malloc 申请内存的时候，会通过 brk 或者 mmap 这两个系统调用来申请内存。为了避免频繁向操作系统申请内存，malloc 还实现了内存池化技术，先申请一大块内存，然后将内存分成不同大小的内存块，然后用户申请内存时，直接从内存池中选择一块相近的内存块即可，这样就减少系统调用的开销。</p>

      <p style='text-align: right'>
      <a href='https://www.taitres.com/posts/computer/systemmemory#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">66a20167daa01fc3ed869a7e</guid>
  <category>posts</category>
<category>计算机基础</category>
 </item>
  <item>
    <title>操作系统（二）进程管理</title>
    <link>https://www.taitres.com/posts/computer/systemprocess</link>
    <pubDate>Wed, 17 Jul 2024 12:16:19 GMT</pubDate>
    <description>进程与线程

为什么创建进程比创建线程慢？

Linux 中创建一个进程自然会创建一个线程，也就是主</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://www.taitres.com/posts/computer/systemprocess'>https://www.taitres.com/posts/computer/systemprocess</a></blockquote>
      <h1>进程与线程</h1>
<h2>为什么创建进程比创建线程慢？</h2>
<p>Linux 中创建一个进程自然会创建一个线程，也就是主线程。创建进程需要为进程划分出一块完整的内存空间，包含代码区、堆区、栈区、数据区等内存资源 </p>
<p>创建线程则简单得多，只需要确定 PC 指针和寄存器的值，并且给线程分配一个栈用于执行程序，同一个进程的多个线程间可以复用进程的虚拟内存空间，减少了创建虚拟内存的开销。</p>
<p>因此，创建进程比创建线程慢，而且进程的内存开销更大。</p>
<h2>为什么进程的切换比线程开销大？</h2>
<p>进程间的切换的时候，除了需要切换 CPU 上下文，还需要切换页表，切换了页表会影响TLB的命中率，那么虚拟地址转换为物理地址就会变慢，表现出来的就是程序运行会变慢，而线程切换只需要切换 CPU 上下文，不会改变虚拟地址空间，不会影响TLB命中率。</p>
<h2>线程的上下文切换过程</h2>
<p>线程发生上下文切换的时候，正在运行的线程会将寄存器的状态保存到内核中的 TCB（线程控制块） 里，然后恢复另一个线程的上下文。和进程的区别是，线程只需要切换CPU的上下文，不会改变地址空间.</p>
<h2>进程有哪些状态？</h2>
<p><em><strong>操作系统理论中的进程状态</strong></em></p>
<p>主要有 5 种，分别是创建状态、就绪状态、运行状态、阻塞状态、结束状态。</p>
<p> 一个进程刚开始创建的时候，是处于创建状态，随后就会进入就绪状态，等待被操作系统调度，当进程被调度器选中后，就会进入运行状态，此时进程就持有 CPU 执行权，当进程发生 I/O 事件的时候，就会进入到阻塞状态，等待 I/O 事件完成，I/O 事件完后进程就会恢复为就绪状态，当进程退出后，就会变为结束状态。</p>
<p><em><strong>Linux系统的进程状态</strong></em></p>
<ul>
<li><p><strong>可运行状态</strong>：该状态表示进程正在运行或者等待被调度（就绪和运行）；</p>
</li>
<li><p><strong>睡眠状态</strong>：在运行中的进程，一旦要进行一些 I/O 操作，需要等待 I/O 完毕，这个时候会释放 CPU，进入睡眠状态；</p>
<ul>
<li><strong>可中断的睡眠状态</strong>：等待 I/O 完毕期间，如果收到了其他信号，还是可以被唤醒；</li>
<li><strong>不可中断的睡眠状态</strong>：只有等待 I/O 完毕才有可能返回运行状态，任何信号都无法打断它。如果这种状态的进程出错，无法杀死，只能重启。</li>
</ul>
</li>
<li><p><strong>暂停状态</strong>：当进程接收到SIGSTOP、SIGTTIN、SIGTSTP 或者 SIGTTOU 信号之后进入该状态；</p>
</li>
<li><p><strong>跟踪状态</strong>：当对进程进行 gdb 调试的时候，进程会进入跟踪状态；</p>
</li>
<li><p><strong>僵尸状态</strong>：当子进程先于父进程退出后，父进程没有执行 wait 或者 waitpid 函数来回收子进程的时候，子进程的状态就会变为僵尸状态，表示已经死亡的进程，但是进程ID还保留着；</p>
</li>
<li><p><strong>结束状态</strong>：当进程正确退出后，就进入结束状态，是进程的最终状态。</p>
</li>
</ul>
<h2>僵尸进程，孤儿进程，守护进程的区别？</h2>
<ul>
<li><p>僵尸进程是指子进程已经终止，但其父进程尚未调用wait()或waitpid()函数来获取子进程的终止状态，导致子进程的进程描述符仍然保留在系统进程表中，成为僵死进程，僵死进程不占用系统资源，但会占用一个进程ID。 </p>
</li>
<li><p>孤儿进程是指父进程先于子进程退出或异常终止，导致子进程成为孤儿进程。孤儿进程会被init进程（进程ID为1）接管，init进程会成为孤儿进程的新的父进程。 </p>
</li>
<li><p>守护进程是在后台运行的一种特殊进程，不与任何终端关联，关闭终端并不会影响守护进程的生命周期，守护进程的生命周期通常是伴随系统的启动和关闭，会一直在后台运行。</p>
</li>
</ul>
<h2>怎么杀死僵尸进程？</h2>
<p>僵尸进程是已经死了的，不能直接使用 kill 命令杀掉僵尸进程，只能通过 ps 命令找到僵尸进程的父进程的 pid 号，然后通过 kill 命令杀掉父进程的方式来达到僵尸进程的效果，因为当父进程被杀掉后，操作系统会将僵尸进程的父进程会变为 1 号 init 进程，接着 init 进程会自动接管僵尸进程的回收工作。</p>
<h2>多进程和多线程的区别？</h2>
<p>多线程由于可以共享进程资源，而多进程不共享地址空间和资源，多进程需要通过进程间通信技术来实现数据传输，开发起来会比较麻烦。 </p>
<p>但多进程安全性较好，在某一个进程出问题时，其他进程一般不受影响；而在多线程的情况下，一个线程执行了非法操作会导致整个进程退出。</p>
<h2>一个进程fork出一个子进程，那么他们占用的内存是之前的2倍吗？</h2>
<p>不是的。</p>
<p> fork 的时候，创建的子进程是复父进程的虚拟内存，并不是物理内存，这时候父子的虚拟内存指向的是同一个物理内存空间，这样能够节约物理内存资源，页表对应的页表项的属性会标记该物理内存的权限为<strong>只读</strong>。</p>
<p>不过，当父进程或者子进程在向这个内存发起写操作时，CPU 就会触发写保护中断，这个写保护中断是由于违反权限导致的，然后操作系统会在「写保护中断处理函数」里进行物理内存的复制，并重新设置其内存映射关系，新页面的访问权限设置为可读写。此时，写入操作的进程将拥有该页面的独立副本，而另一个进程仍然共享原始的只读页面，这个过程被称为「<strong>写时复制</strong>」，发生了这个过程，内存占用才会增多。</p>
<h1>进程间通信</h1>
<h2>进程间有哪些通信方式？</h2>
<p>进程间通信方式主要有管道、消息队列、共享内存、信号、信号量和 socket 通信。</p>
<ul>
<li><p><strong>管道</strong>通信的数据是无格式的字节流，并且通信方向是单向的，只能在一个方向上流动，管道分为匿名管道和有名管道；</p>
<ul>
<li>匿名管道是没有文件实体，只能用于存在父子关系的进程间通信，匿名管道的生命周期随着进程创建而建立，随着进程终止而消失。</li>
<li>有名管道有文件实体，可以用于任何进程间的通信，并且数据可以持久化保存，即使创建它的进程退出，其他进程仍然可以使用该管道。</li>
</ul>
</li>
<li><p><strong>消息队列</strong>在内核中是通过链表来组织消息的，克服了管道通信的数据是无格式的问题；</p>
</li>
<li><p>管道和消息队列在读写数据的时候，都需要经过用户态与内核态之间的拷贝过程，<strong>共享内存</strong>就解决了这个问题，多个进程可以将共享内存映射到各自的虚拟地址空间，实现共享数据的读写，不会涉及用户态和内核态之间的数据拷贝，所以共享内存方式是进程间通信方式里最高效的，不过共享内存带来新的问题，多进程竞争同个共享资源会造成数据的错乱，因为需要同步机制来保证多进程下的读写数据的安全。</p>
</li>
<li><p><strong>信号量</strong>可以实现进程间的同步和互斥访问共享资源，信号量其实是一个计数器，表示的是资源个数，当一个进程想要访问资源时，它会尝试递减信号量（称为P操作）。如果信号量的值大于零，这个操作会成功，否则进程就会阻塞，直到信号量变为正数。当进程完成对资源的访问后，它会递增信号量（称为V操作），允许其他阻塞的进程访问资源。</p>
</li>
<li><p><strong>信号</strong>是一种异步的通知机制，用于通知进程某个事件已经发生。当一个信号发送给一个进程时，操作系统会中断进程的正常流程来处理信号，只适用于简单的通知和事件处理。</p>
</li>
<li><p>前面提到的这些通信方式都只能在本地上进行进程间通信，如果要实现跨主机的进程间通信，就需要通过 <strong>socket 通信</strong>了，可以实现基于 TCP 或者 UDP 协议的通信方式。</p>
</li>
</ul>
<h3>哪个进程间通信效率最高的</h3>
<p>共享内存的通信效率最高，因为共享内存不涉及内核态和用户态之间的数据拷贝。</p>
<p>共享内存的机制，就是拿出一块虚拟地址空间来，映射到相同的物理内存中。这样这个进程写入的东西，另外一个进程马上就能看到了，都不需要拷贝、传递，大大提高了进程间通信的速度。</p>
<h3>信号和信号量的区别</h3>
<p><strong>信号用于异步</strong>处理异常或特殊事件，而<strong>信号量用于同步</strong>进程以安全地访问共享资源。 </p>
<ul>
<li>信号是一种异步的通知机制，用于通知进程某个事件已经发生。当一个信号发送给一个进程时，操作系统会中断进程的正常流程来处理信号。</li>
<li>信号量是一种用于解决多进程同步和互斥问题的工具，主要用于保护共享资源，防止多个进程同时访问。信号量有一个整数值，该值的含义是可以同时访问某个资源的进程数量。当一个进程想要访问资源时，它会尝试递减信号量（称为P操作）。如果信号量的值大于零，这个操作会成功，否则进程就会阻塞，直到信号量变为正数。当进程完成对资源的访问后，它会递增信号量（称为V操作），允许其他阻塞的进程访问资源。</li>
</ul>
<h1>调度</h1>
<h2>进程的调度算法有哪些？</h2>
<ul>
<li><p><strong>先来先服务</strong>：按照进程到达的顺序进行调度，先到达的进程先执行。优势是<strong>简单、公平</strong>，但可能导致<strong>长作业等待时间过长，无法适应实时性要求高</strong>的场景。</p>
</li>
<li><p><strong>最短作业优先</strong>：选择估计运行时间最短的进程优先执行。优势是能够最大程度地<strong>减少平均等待时间</strong>，但需要准确预测进程的运行时间，这个<strong>实现起来会比较困难</strong>。</p>
</li>
<li><p><strong>时间片轮转（RR）</strong>：将CPU时间分成多个时间片，每个进程轮流占用一个时间片，如果一个进程在该时间片结束时还没有完成，则将其移到队列的末尾，等待下一次调度。优势是<strong>公平</strong>，但对于<strong>长作业和实时性要求高的场景可能不够高效</strong>。</p>
</li>
<li><p><strong>优先级调度</strong>：为每个进程分配一个优先级，优先级高的进程先执行。可根据不同的调度策略确定优先级，如静态优先级、动态优先级等。优势是<strong>能够根据进程的重要性和紧急程度进行调度</strong>，但<strong>可能导致优先级低的进程长时间等待</strong>。</p>
</li>
<li><p><strong>多级反馈队列调度（MFQS）</strong>：「多级」表示有多个队列，每个队列优先级从高到低，同时优先级越高时间片越短。「反馈」表示如果有新的进程加入优先级高的队列时，立刻停止当前正在运行的进程，转而去运行优先级高的队列。优点：<strong>动态优先级调整、短作业优先处理、提高系统吞吐量和利用率、支持实时任务</strong>。多级反馈队列调度存在的问题，优先级较低的队列可能会被优先级较高的队列长时间占用，导致优先级较低的进程无法得到执行，从而产生<strong>饥饿</strong>现象。</p>
<ul>
<li><p>设置了多个队列，赋予每个队列不同的优先级，每个队列优先级从高到低，同时优先级越高时间片越短；</p>
</li>
<li><p>新的进程会被放入到第一级队列的末尾，按先来先服务的原则排队等待被调度，如果在第一级队列规定的时间片没运行完成，则将其转入到第二级队列的末尾，以此类推，直至完成；</p>
</li>
<li><p>当较高优先级的队列为空，才调度较低优先级的队列中的进程运行。如果进程运行时，有新进程进入较高优先级的队列，则停止当前运行的进程并将其移入到原队列末尾，接着让较高优先级的进程运行；</p>
</li>
</ul>
</li>
</ul>
<h1>锁</h1>
<h2>线程间同步方式有哪些？</h2>
<p>Linux 系统提供了五种用于线程间同步的方式：互斥量、读写锁、信号量、自旋锁、条件变量。 </p>
<ul>
<li>互斥锁：用于保护共享资源，确保同一时间只有一个线程可以访问该资源。只有获得互斥锁的线程才能进入临界区，其他线程需要等待锁的释放。 </li>
<li>读写锁：也称为共享-独占锁，允许多个线程同时读取共享资源，但在写操作时需要独占访问。读写锁在读多写少的场景中可以提供更好的并发性能。 </li>
<li>信号量：用于控制对一组资源的访问。信号量可以允许多个线程同时访问资源，但是需要在访问前进行P操作（申请资源）和在访问结束后进行V操作（释放资源），以确保资源的正确使用。 </li>
<li>自旋锁：是一种忙等待锁，在获取锁之前，线程会一直尝试获取锁，而不会进入睡眠状态。自旋锁适用于保护临界区较小、锁占用时间短暂的情况。 </li>
<li>条件变量：用于在线程之间进行条件同步。一个线程可以等待某个条件满足，而另一个线程在满足条件时可以通知等待的线程继续执行。（wait-notify）</li>
</ul>
<h3>信号量和互斥锁应用场景有什么区别？</h3>
<ul>
<li>信号量一般以<strong>同步</strong>的方式对共享资源进行控制，而互斥锁通过<strong>互斥</strong>的方式对共享资源对其进行控制；</li>
<li>互斥量的加锁和解锁必须由同一线程分别对应使用，信号量可以由一个线程释放，另一个线程得到。</li>
</ul>
<h3>自旋锁和互斥锁有什么区别？分别适合哪些应用场景？</h3>
<ul>
<li>互斥锁加锁失败的时候，线程会放弃CPU，陷入到内核态，执行线程切换和CPU上下文切换的过程，切换到其他线程；</li>
<li>而自旋锁加锁失败后，线程不会放弃 CPU，而是选择忙等待，直到它拿到锁。</li>
</ul>
<p>自旋锁适用于并发竞争时间短暂的情况，可以减少线程切换和CPU上下文切换的开销。互斥锁适用于并发竞争时间较长或资源争用较激烈的情况，可以避免线程忙等待，但会带来线程切换和上下文切换的开销。</p>
<h2>悲观锁和乐观锁有什么区别？</h2>
<p>悲观锁做事比较悲观，它认为多线程同时修改共享资源的概率比较高，很容易出现冲突，造成数据错乱，所以会每次读写共享资源之前，先要加锁，确保任意时刻只有一个线程才能对数据进行读写操作。 </p>
<p>乐观锁做事比较乐观，它假定冲突的概率很低，先修改完共享资源，再验证这段时间内有没有发生冲突，如果没有其他线程在修改资源，那么操作完成，如果发现有其他线程已经修改过这个资源，就放弃本次操作，并选择报错、重试等策略。 </p>
<p>悲观锁适合<strong>并发写入多和竞争激烈</strong>的场景，这种场景下悲观锁可以避免大量的无用的反复尝试等消耗。乐观锁适合<strong>读多写少和并发不激烈</strong>的场景，在这些场景下乐观锁不加锁的特点能让性能大幅提高。</p>

      <p style='text-align: right'>
      <a href='https://www.taitres.com/posts/computer/systemprocess#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">66a0f097daa01fc3ed86940b</guid>
  <category>posts</category>
<category>计算机基础</category>
 </item>
  <item>
    <title>操作系统（一）系统结构</title>
    <link>https://www.taitres.com/posts/computer/systemstructure</link>
    <pubDate>Tue, 16 Jul 2024 13:36:34 GMT</pubDate>
    <description>存储结构 

为什么计算机要给储存结构分级？

不同级别的储存结构具有不同的访问速度，CPU 访问 </description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://www.taitres.com/posts/computer/systemstructure'>https://www.taitres.com/posts/computer/systemstructure</a></blockquote>
      <h1>存储结构</h1>
<h2>为什么计算机要给储存结构分级？</h2>
<p>不同级别的储存结构具有不同的访问速度，CPU 访问 CPU Cache 的延时只需要几个时钟周期，访问物理内存的延时则是几百个，速度相差 100 倍，访问磁盘延时就更高了，已经到毫秒级别了（百万时钟周期），但是访问速度越快的存储器，造价成本会高很多，容量也比较小。 通过分级储存结构，可以将数据按照不同的访问速度、容量和成本要求进行管理，CPU Cache用于存储近期频繁访问的数据，内存用于存储当前执行的程序和数据，而硬盘则用于存储大量数据和长期存储。（一个时钟周期小于10纳秒）</p>
<h2>CPU cache L1 L2 L3 读取数据的时间量级差距有多大？</h2>
<p>CPU 访问 L1 Cache 只需要 2<del>4 个时钟周期，访问 L2 Cache 大约 10</del>20 个时钟周期，访问 L3 Cache 大约 20<del>60 个时钟周期，而访问内存速度大概在 200</del>300 个 时钟周期之间。</p>
<h2>CPU 缓存对程序性能的影响都体现在哪些方面？</h2>
<p>程序具有<strong>局部性原理</strong>：如果一个数据被访问，那么附近的数据很可能也会被访问到，</p>
<p>CPU Cache就是基于这个局部性原理，在访问数据的时候，如果访问的数据不在 CPU Cache中，不会只从内存读这一个数据，而是会从内存连续加载数据到 CPU Cache，这样相邻的数据都会被缓存在  CPU Cache，那么程序在访问相邻数据的时候，相邻数据就可以直接在缓存中命中，无需访问内存，可以提高访问的效率。</p>
<h1>内核态</h1>
<h2>什么是内核？</h2>
<p>内核是系统中第一个加载的程序，并拥有最高的特权级别，控制整个系统的运行和资源分配。</p>
<p>负责<strong>管理进程</strong>、线程：决定哪个进程、线程使用 CPU，也就是进程调度的能力；</p>
<p><strong>管理内存</strong>：决定内存的分配和回收，也就是内存管理的能力；</p>
<p><strong>管理硬件设备</strong>：为进程与硬件设备之间提供通信能力；</p>
<p><strong>提供系统调用</strong>：它与用户空间程序进行交互，提供系统调用接口，实现了操作系统的基本功能。</p>
<h2>内核态和用户态区别？为什么要区分内核态和用户态？</h2>
<p>CPU 将指令分为了特权指令和非特权指令，然后操作系统根据 CPU 的特权分级机制，把进程的运行空间分为内核空间和用户空间。</p>
<p><strong>内核态</strong>具有最高权限，可以执行<strong>特权指令</strong>，直接访问所有系统和硬件资源，而<strong>用户态</strong>不能执行特权指令，只能执行<strong>非特权指令</strong>，所以只能访问受限的资源，比如不可以直接访问内存、硬盘、网卡等硬件设备，如果要访问这些硬件资源，需要通过系统调用的方式，让进程陷入到内核态才能访问这些特权资源。 </p>
<p>之所以要区分内核态和用户态，目的就是为了保证系统的稳定性和安全性，因为通过限制应用程序对特权指令的访问，可以防止恶意程序直接操作危险的指令导致系统崩溃的问题。</p>
<h2>什么时候会由用户态陷入内核态？</h2>
<p>主要有三种情况，会导致应用程序从用户态陷入到内核态： </p>
<ul>
<li><p>应用程序执行<strong>系统调用</strong>的时候，会触发一个<strong>软中断</strong>，会进入内核态执行相应的内核代码，完成后再返回用户态。 </p>
</li>
<li><p>当应用程序发生<strong>异常</strong>情况，如访问非法内存、除零错误等，CPU会触发一个异常，将控制权转移到内核态的异常处理程序中。 </p>
</li>
<li><p>当系统接收到<strong>外部设备的中断</strong>信号，如硬件设备的输入/输出请求、定时器中断等，处理器会中断当前的执行，进入内核态执行相应的中断处理程序。</p>
</li>
</ul>
<p>第一种属于软件中断，后两种属于硬件中断</p>
<h2>系统调用的过程？</h2>
<p>当应用程序发生系统调用的时候，会发生用户态和内核态的切换，过程是这样的： </p>
<ul>
<li>执行系统调用的时候，先将系统调用名称转换为系统调用号，接着将<strong>系统调用号</strong>和请求的参数放到寄存器中，然后执行软件中断命令，CPU会从<strong>用户态切换到内核态</strong>。 </li>
<li>CPU 跳转到中断处理程序，将当前<strong>用户态的现场信息保存到内核栈中</strong>，接着根据系统调用号从系统调用表中找到对应的系统调用函数，并将寄存器中保存的参数取出来，作为函数参数，然后在<strong>内核中执行系统调用函数</strong>。 </li>
<li>执行完系统调用后，执行<strong>中断返回指令</strong>，<strong>内核栈</strong>会弹出之前保存的用户态的现场信息，将原来用户态保存的<strong>现场恢复</strong>回来，这时候 <strong>CPU 恢复到用户态</strong>，用户态进程恢复执行。</li>
</ul>
<h2>用户态和内核态是如何切换的？</h2>
<p>简单就参考<a href="#%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8%E7%9A%84%E8%BF%87%E7%A8%8B%EF%BC%9F">系统调用的过程？</a></p>
<h2>为什么用户态和内核态的相互切换过程开销比较大</h2>
<ul>
<li>上下文切换：在用户态和内核态之间切换时，需要保存当前进程或线程的上下文信息，包括程序计数器、寄存器状态、栈指针等。这些上下文信息保存在内存中或者内核数据结构中，在切换完成后需要再次加载回来。这个过程会消耗一定的时间和资源。 </li>
<li>权限切换：用户态和内核态具有不同的权限级别，内核态拥有更高的特权级别，可以执行一些用户态不可访问的指令和操作。因此，从用户态切换到内核态需要进行特权级别的变更，这个过程也会增加开销。</li>
<li>地址空间切换：用户态和内核态通常有不同的地址空间，为了保护操作系统内核不受用户程序的直接影响，当切换到内核态时需要进行地址空间的切换和映射，这也会增加开销。</li>
<li>TLB刷新：TLB是CPU中的一种缓存，用于存储虚拟地址到物理地址的映射关系。当发生用户态和内核态的切换时，可能导致TLB中的缓存无效，需要进行TLB刷新，这会引起一定的性能损失。</li>
</ul>
<h1>中断</h1>
<h2>什么是中断？为什么要有中断？</h2>
<p>中断是系统用来响应硬件设备请求的一种机制，操作系统收到硬件的中断请求，会打断正在执行的进程，然后调用内核中的中断处理程序来响应请求。</p>
<p>中断机制的好处是化主动为被动，避免 CPU 轮询等待某条件成立。如果没有中断机制，那么“某个条件成立”就需要 CPU 轮询判断，这样就会增加系统的开销。而使用中断机制，就可以在条件成立之后，向 CPU 发送中断事件，强制中断 CPU 执行程序，转而去执行中断处理程序。</p>
<p>可以理解为中断是一种异步的事件处理机制，可以提高系统的并发处理能力。</p>
<h2>什么是硬件中断和软件中断？</h2>
<ul>
<li>中断（interrupt）：由<strong>硬件设备</strong>触发，比如点击一下鼠标、敲一下键盘，这时候外部设备会给 CPU 发送一个中断号，属于异步事件； </li>
<li>异常（exception）：<strong>CPU</strong> 在执行指令时检测到的反常条件，比如除法异常、错误指令异常，缺页异常等，然后CPU 自己给自己一个中断号，无需外界给，属于同步事件； </li>
<li>INT 指令：INT 指令后面跟一个数字，就相当于直接用指令的形式，告诉 CPU 一个中断号。比如 INT 0x80，就是告诉 CPU 中断号是 0x80。Linux 内核提供的系统调用，就是用了 INT 0x80 这种指令。</li>
</ul>
<p>硬件中断：由硬件自动触发的中断就属于硬件中断，比如中断和异常都属于硬件中断，中断是由外部设备触发的，异常是由 CPU 触发的。 </p>
<p>软件中断：由软件程序主动触发的中断属于软件中断，比如 INT 指令，Linux 内核提供的系统调用，就是用了 INT 0x80 这种指令。</p>
<h2>什么是硬中断和软中断？</h2>
<p>根据实现的方式的不同，分为了硬中断和软中断： </p>
<ul>
<li>由 CPU <strong>硬件实现</strong>的中断机制，就属于硬中断，比如中断、异常、INT指令都是由 CPU 实现的中断机制。 （ CPU 在每一个指令周期的最后，都会留一个 CPU 周期去查看是否有中断，如果有，就把中断号取出，去「中断向量表」中寻找中断处理程序，然后跳过去执行中断处理程序。）</li>
<li>由<strong>软件实现</strong>的中断机制，就属于软中断。比如 Linux 实现的软中断守护进程。 （一个单独的守护进程，不断轮询一组标志位，如果哪个标志位有值了，那去这个标志位对应的「软中断向量表数组」的相应位置，找到软中断处理函数，然后跳过去执行软中断处理函数。）</li>
</ul>
<p>硬中断和软中断的中断效果是一样的，都是打断当前正在运行的程序，转而去执行中断处理程序，执行完之后再返回原程序。</p>
<h2>为什么要有软中断？</h2>
<p>软中断可以承接原本硬中断处理程序比较复杂且耗时的工作，让硬中断的中断处理函数的逻辑尽可能的简单，从而提高系统的中断响应速度。 </p>
<p>比方说，在网卡收到数据包的时候，会把数据包的事件分为上半部分和下半部分： </p>
<ul>
<li>上部分由硬中断处理程序处理，要做的事情很少，做完后就会发起了一次软中断，然后就结束了。这里发起的软中断，并不是向 CPU 发送中断信号，而是将软中断标记数组中的某一个位置标记一下。 </li>
<li>下半部由软中断处理程序处理，内核守护进程会不断轮询软中断标记数组，看哪个位置被标记为 1 了，接着就去软中断向量表里，寻找这个标志位对应的处理程序，然后执行软中断处理程序处理，其主要是需要从内存中找到网络数据，再按照网络协议栈，对网络数据进行逐层解析和处理，最后把数据送给应用程序。</li>
</ul>

      <p style='text-align: right'>
      <a href='https://www.taitres.com/posts/computer/systemstructure#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">6696776294239403a4bd29b2</guid>
  <category>posts</category>
<category>计算机基础</category>
 </item>
  <item>
    <title>SSM框架</title>
    <link>https://www.taitres.com/posts/java/ssm</link>
    <pubDate>Mon, 24 Jun 2024 08:23:55 GMT</pubDate>
    <description>Spring概述

什么是Spring

Spring是一个生态，可以构建Java应用所需的一切基础</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://www.taitres.com/posts/java/ssm'>https://www.taitres.com/posts/java/ssm</a></blockquote>
      <h1>Spring概述</h1>
<h2>什么是Spring</h2>
<p>Spring是一个生态，可以构建Java应用所需的一切基础设施，通常Spring是指Spring Framework</p>
<p>Spring是一个<strong>轻量级</strong>、非入侵式的<strong>控制反转</strong>(IoC)和<strong>面向切面</strong>(AOP)的开源<strong>容器</strong>框架</p>
<p>容器：包含并管理应用对象的生命周期</p>
<h2>Spring框架的好处</h2>
<ul>
<li>轻量：Spring是轻量的，基本的版本大约2MB</li>
<li>控制反转：Spring通过控制反转实现了松散耦合，集中管理对象，方便维护</li>
<li>面向切面的编程(AOP)：Spring支持面向切面的编程，并且把应用业务逻辑和系统服务分开。</li>
<li>容器：Spring包含并管理应用中对象的生命周期和配置</li>
<li>MVC框架：Spring的WEB框架是个精心设计的框架，是Wb框架的一个很好的替代品</li>
<li>事务管理：Spring提供一个持续的事务管理接口，可以扩展到上至本地事务下至全局事务(JTA)</li>
<li>异常处理：Spring提供方便的API把具体技术相关的异常（比如由JDBC,Hibernate or JDO抛出的)转化为一致的unchecked异常</li>
</ul>
<h2>Spring有哪些模块</h2>
<ul>
<li>Spring Core：提供了框架的基本组成部分，包括控制反转(Inversionof Control,IOC)和依赖注入(Dependency Injection,DI)功能</li>
<li>Spring Beans：提供了BeanFactory,是工厂模式的一个经典实现，Spring将管理对象称为Bean</li>
<li>Spring Context：构建于core封装包基础上的context封装包，提供了一种框架式的对象访问方法</li>
<li>Spring JDBC：提供了一个JDBC的抽象层，消除了烦琐的]DBC编码和数据库厂商特有的错误代码解析，用于简化JDBC。</li>
<li>Spring AOP：提供了面向切面的编程实现，让你可以自定义拦截器、切点等</li>
<li>Spring Web：提供了针对Web开发的集成特性，例如文件上传，利用servlet listeners进行ioc容器初始化和对Web的ApplicationContext</li>
<li>Spring Test：主要为测试提供支特的，支持使用JUnit或TestNG对Spring细件进行单元试和集成测试</li>
</ul>
<h2>Spring中的设计模式有哪些？</h2>
<ul>
<li>工厂模式：Spring容器本质是一个大工厂，使用工厂模式通过BeanFactory、ApplicationContext创建bean对象</li>
<li>代理模式：Spring AOP功能功能就是通过代理模式来实现的，分为动态代理和静态代理</li>
<li>单例模式：Spring中的Bean默认都是单例的，这样有利于容器对Bean的管理</li>
<li>模板模式：Spring中JdbcTemplate、RestTemplate等以Template结尾的对数据库、网络等等进行操作的板类，就使用到了模板模式</li>
<li>观察者模式：Spring事件区动模型就是观察者模式很经典的一个应用</li>
<li>适配器模式：Spring AOP的增强或通知(Advice)使用到了适配器模式、Spring MVC中也是用到了适配器模式适配Controller</li>
<li>策略模式：Spring中有一个Resource接口，它的不同实现类，会根据不同的策略去访问资源</li>
</ul>
<h1>Spring IOC</h1>
<h2>什么是IOC?</h2>
<p>是什么：IoC即控制反转(Inversion of Control,缩写为IoC)它是一种思想不是一个技术实现。描述的是：Java 开发领域对象的创建以及管理的问题。</p>
<p>干什么：</p>
<ul>
<li><strong>传统的开发方式</strong> ：往往是在类 A 中手动通过 new 关键字来 new 一个 B 的对象出来</li>
<li><strong>使用 IoC 思想的开发方式</strong> ：不通过 new 关键字来创建对象，而是通过 IoC 容器(Spring 框架) 来帮助我们实例化对象。我们需要哪个对象，直接从 IoC 容器里面去取即可。</li>
</ul>
<p>从以上两种开发方式的对比来看：我们 “丧失了一个权力” (创建、管理对象的权力)，从而也得到了一个好处（不用再考虑对象的创建、管理等一系列的事情）</p>
<p>UserService service = new UserService();  //耦合度太高，维护不方便，想要修改UserService，每一个依赖了UserService的类都得做更改</p>
<p><strong>为什么叫控制反转?</strong></p>
<p>IOC将创建对象的控制权从程序员交给Spring的IOC，程序员使用需要通过依赖注入（DI）</p>
<ul>
<li><strong>控制</strong> ：指的是对象创建（实例化、管理）的权力</li>
<li><strong>反转</strong> ：控制权交给外部环境（IoC 容器）</li>
</ul>
<p><strong>实现机制</strong>： 工厂 +  反射</p>
<ul>
<li><p>实现是利用工厂模式，通过反射创建bean</p>
</li>
<li><p>扫描和解析配置文件或注解信息，将其转换为内部的对象定义和依赖关系；</p>
<ul>
<li>在Spring中，可以使用XML文件或注解来配置对象和依赖关系。
Sping通过解析配置文件或注解信息，将其转换为内部的对象定义和依赖关系(BeanDefinition)放到容器(BeanFactory)中。对象定义包括对象的类型、属性、构造函数等信息，依赖关系包括对象之间的依赖关系、依赖注入方式等信息</li>
</ul>
</li>
<li><p>根据对象定义和依赖关系，使用反射机制动态创建和初始化对象，并将对象注入到需要使用它们的地方。</p>
</li>
</ul>
<p>总的来说，Spring IOC的实现原理是通过反射机制动态创建对象，依赖注入，对象初始化。通过解耦对象之间的依赖关系，使得应用程序更加灵活、可维护、可扩展。</p>
<h2>什么是依赖注入（DI）</h2>
<p>是什么：</p>
<p>依赖注入是指组件之间的依赖关系由容器在运行期决定，即由容器动态的将某个依赖关系注入到组件之中。</p>
<p>干什么：</p>
<ul>
<li>依赖注入的目的并非为软件系统带来更多功能，而是为了提升组件重用的频率，并为系统搭建一个灵活、可扩展的平台</li>
<li>通过依赖注入机制，我们只需要通过简单的配置，而无需任何代码就可指定目标需要的资源，完成自身的业务逻辑，而不需要关心具体的资源来自何处，由谁实现</li>
</ul>
<h2>IoC和DI有什么关系？</h2>
<p>IoC和DI都是Spring框架中的核心概念，它们的区别在于：</p>
<ul>
<li>IoC(Inverse of Control,控制反转)：它是一种思想，主要解决程序设计中的对象依赖关系管理问题。在IoC思想中，对象的创建权反转给第三方容器，由容器进行对象的创建及依赖关系的管理。</li>
<li>DI(Dependency Injection,依赖注入)：它是IoC思想的具体实现方式之一，用于实现IoC。在Spring中，依赖注入是指：在对象创建时，由容器自动将依赖对象注入到需要依赖的对象中。</li>
</ul>
<h1>Spring Bean</h1>
<h2>什么是Bean</h2>
<p>Bean 代指的就是那些被 IoC 容器所管理的对象。</p>
<h2>Bean的生命周期</h2>
<ol>
<li><p><strong>创建 Bean 的实例</strong>：Bean 容器首先会找到配置文件中的 Bean 定义，然后使用 Java 反射 API 来创建 Bean 的实例。</p>
</li>
<li><p><strong>Bean 属性赋值/填充</strong>：为 Bean 设置相关属性和依赖，例如<code>@Autowired</code> 等注解注入的对象、<code>@Value</code> 注入的值、<code>setter</code>方法或构造函数注入依赖和值、<code>@Resource</code>注入的各种资源。</p>
</li>
<li><p><strong>Bean 初始化</strong>：</p>
</li>
</ol>
<ul>
<li><p>Aware 接口的依赖注入</p>
<ul>
<li>如果 Bean 实现了 <code>BeanNameAware</code> 接口，调用 <code>setBeanName()</code>方法，传入 Bean 的名字</li>
<li>如果 Bean 实现了 <code>BeanClassLoaderAware</code> 接口，调用 <code>setBeanClassLoader()</code>方法，传入 <code>ClassLoader</code>对象的实例</li>
<li>如果 Bean 实现了 <code>BeanFactoryAware</code> 接口，调用 <code>setBeanFactory()</code>方法，传入 <code>BeanFactory</code>对象的实例</li>
<li>与上面的类似，如果实现了其他 <code>*.Aware</code>接口，就调用相应的方法</li>
</ul>
</li>
<li><p>BeanPostProcessor 在初始化前的处理</p>
<ul>
<li>如果有和加载这个 Bean 的 Spring 容器相关的 <code>BeanPostProcessor</code> 对象，执行<code>postProcessBeforeInitialization()</code> 方法</li>
</ul>
</li>
<li><p>在 afterPropertiesSet() 方法写初始化逻辑，指定 init-method 方法，指定初始化方法</p>
</li>
<li><p>BeanPostProcessor 在初始化后的处理</p>
<ul>
<li>如果有和加载这个 Bean 的 Spring 容器相关的 <code>BeanPostProcessor</code> 对象，执行<code>postProcessAfterInitialization()</code> 方法（打印日志）</li>
</ul>
</li>
</ul>
<ol start="4">
<li><strong>销毁 Bean</strong>：注册相关销毁回调接口，最后通过<code>DisposableBean</code> 和 <code>destory-method</code> 进行销毁<ul>
<li>如果 Bean 实现了 <code>DisposableBean</code> 接口，执行 <code>destroy()</code> 方法</li>
<li>如果 Bean 在配置文件中的定义包含 <code>destroy-method</code> 属性，执行指定的 Bean 销毁方法。或者，也可以直接通过<code>@PreDestroy</code> 注解标记 Bean 销毁之前执行的方法</li>
</ul>
</li>
</ol>
<h2>什么是FactoryBean</h2>
<p>FactoryBean是Spring所提供的一种较灵活的创建Bean的方式，可以通过实现FactoryBean接口中的<code>getObject()</code>方法来返回一个对象，这个对象就是最终的Bean对象。</p>
<p>如果一个对象实现了这接口，那它就成为一种特殊的Bean，注册到IOC容器之后，如果调用getBean，Spring会调用FactoryBean的<code>getObject()</code>方法来获取实际的Bean实例,而不是返回FactoryBean本身。</p>
<h2>BeanFactory和FactoryBean区别是什么</h2>
<p>BeanFactory和FactoryBean是Spring框架中的两个关键概念，用于创建和管理Bean实例。</p>
<ul>
<li>BeanFactory是Spring的基本容器，负责创建和管理Bean实例的；</li>
<li>而FactoryBean是一个特殊的Bean，它实现了FactoryBean接口，负责创建其他Bean实例，并提供一些初始化Bean的设置。</li>
</ul>
<p>下面是BeanFactory和FactoryBean之间的一些关键区别：</p>
<ol>
<li><strong>功能</strong>：BeanFactory是一个<strong>容器</strong>，负责管理Bean的完整生命周期,包括初始化、属性设置、依赖注入和销毁等。FactoryBean是一个接口，定义了创建Bean的规范和逻辑，它负责创建其他Bean实例</li>
<li><strong>创建Bean的方式</strong>：<ul>
<li>BeanFactory通过配置信息(XML或注解)来创建和管理Bean</li>
<li>FactoryBean通过实现<code>getObject()</code>方法来自定义Bean的创建逻辑</li>
</ul>
</li>
<li><strong>灵活性</strong>：FactoryBean具有更高的灵活性，因为它允许自定义的逻辑来创建和配置Bean实例。FactoryBean的实现类可以根据特定的条件选择性地创建不同的Bean实例，或者在创建Bean之前进行一些初始化操作。</li>
<li><strong>返回类型</strong>：<ul>
<li>BeanFactory返回的是Bean实例本身</li>
<li>而FactoryBean返回的是由FactoryBean创建的Bean实例</li>
</ul>
</li>
</ol>
<h2>BeanFactoryi和ApplicationContext的关系是什么</h2>
<p>总的来说，</p>
<ul>
<li>BeanFactory是Spring框架中最基本的容器，提供最基础的IOC和DI的支持；</li>
<li>而ApplicationContext是在BeanFactoryl的基础上扩展而来的，提供了更多的功能和特性ApplicationContext是Spring框架中使用较为广泛的容器。</li>
</ul>
<h2>Bean 的作用域有哪些</h2>
<p>Spring 中 Bean 的作用域通常有下面几种：</p>
<ul>
<li><strong>singleton</strong> : IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的，是对单例设计模式的应用。</li>
<li><strong>prototype</strong> : 每次获取都会创建一个新的 bean 实例。也就是说，连续 <code>getBean()</code> 两次，得到的是不同的 Bean 实例。</li>
<li><strong>request</strong> （仅 Web 应用可用）: 每一次 HTTP 请求都会产生一个新的 bean（请求 bean），该 bean 仅在当前 HTTP request 内有效。</li>
<li><strong>session</strong> （仅 Web 应用可用） : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean（会话 bean），该 bean 仅在当前 HTTP session 内有效。</li>
<li><strong>application/global-session</strong> （仅 Web 应用可用）：每个 Web 应用在启动时创建一个 Bean（应用 Bean），该 bean 仅在当前应用启动时间内有效。</li>
<li><strong>websocket</strong> （仅 Web 应用可用）：每一次 WebSocket 会话产生一个新的 bean。</li>
</ul>
<h2>Bean是线程安全的吗</h2>
<p>Spring 框架中的 Bean 是否线程安全，取决于其作用域和状态。默认的是singleton</p>
<p>prototype 作用域下，每次获取都会创建一个新的 bean 实例，不存在资源竞争问题，所以不存在线程安全问题。singleton 作用域下，IoC 容器中只有唯一的 bean 实例，可能会存在资源竞争问题（取决于 Bean 是否有状态）。如果这个 bean 是有状态的话，那就存在线程安全问题（有状态 Bean 是指包含可变的成员变量的对象）。</p>
<p>不过，大部分 Bean 实际都是无状态（没有定义可变的成员变量）的（比如 Dao、Service），这种情况下， Bean 是线程安全的。</p>
<p>对于有状态单例 Bean 的线程安全问题，常见的有两种解决办法：</p>
<ol>
<li>在 Bean 中尽量避免定义可变的成员变量；</li>
<li>在类中定义一个 <code>ThreadLocal</code> 成员变量，将需要的可变成员变量保存在 <code>ThreadLocal</code> 中（推荐的一种方式）。</li>
</ol>
<h2>将一个类声明为 Bean 的注解有哪些</h2>
<ul>
<li><code>@Component</code>：通用的注解，可标注任意类为 <code>Spring</code> 组件。如果一个 Bean 不知道属于哪个层，可以使用<code>@Component</code> 注解标注。</li>
<li><code>@Repository</code> : 对应持久层即 Dao 层，主要用于数据库相关操作。</li>
<li><code>@Service</code> : 对应服务层，主要涉及一些复杂的逻辑，需要用到 Dao 层。</li>
<li><code>@Controller</code> : 对应 Spring MVC 控制层，主要用于接受用户请求并调用 <code>Service</code> 层返回数据给前端页面</li>
</ul>
<h2>@Component 和 @Bean 的区别是什么</h2>
<p><code>@Component</code> 注解作用于类，而<code>@Bean</code>注解作用于方法。</p>
<ul>
<li><code>@Component</code>使用 <code>@ComponentScan</code> 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中。<code>@Bean</code> 注解通常是我们在标有该注解的方法中定义产生这个 bean，<code>@Bean</code>告诉了 Spring 这是某个类的实例，当我需要用它的时候还给我。</li>
<li><code>@Bean</code> 注解比 <code>@Component</code> 注解的自定义性更强，而且很多地方我们只能通过 <code>@Bean</code> 注解来注册 bean。比如当我们引用第三方库中的类需要装配到 <code>Spring</code>容器时，则只能通过 <code>@Bean</code>来实现</li>
</ul>
<h2>注入 Bean 的注解有哪些</h2>
<p>Spring 内置的 <code>@Autowired</code> 以及 JDK 内置的 <code>@Resource</code> 和 <code>@Inject</code> 都可以用于注入 Bean。</p>
<table>
<thead>
<tr>
<th>Annotation</th>
<th>Package</th>
<th>Source</th>
</tr>
</thead>
<tbody><tr>
<td><code>@Autowired</code></td>
<td><code>org.springframework.bean.factory</code></td>
<td>Spring 2.5+</td>
</tr>
<tr>
<td><code>@Resource</code></td>
<td><code>javax.annotation</code></td>
<td>Java JSR-250</td>
</tr>
<tr>
<td><code>@Inject</code></td>
<td><code>javax.inject</code></td>
<td>Java JSR-330</td>
</tr>
</tbody></table>
<h2>@Autowired 和 @Resource 的区别是什么</h2>
<ul>
<li><code>@Autowired</code> 是 Spring 提供的注解，<code>@Resource</code> 是 JDK 提供的注解。</li>
<li><code>Autowired</code> 默认的注入方式为<code>byType</code>（根据类型进行匹配），<code>@Resource</code>默认注入方式为 <code>byName</code>（根据名称进行匹配）。</li>
<li>当一个接口存在多个实现类的情况下，<code>@Autowired</code> 和<code>@Resource</code>都需要通过名称才能正确匹配到对应的 Bean。<code>Autowired</code> 可以通过 <code>@Qualifier</code> 注解来显式指定名称，<code>@Resource</code>可以通过 <code>name</code> 属性来显式指定名称。</li>
<li><code>@Autowired</code> 支持在构造函数、方法、字段和参数上使用。<code>@Resource</code> 主要用于字段和方法上的注入，不支持在构造函数或参数上使用。</li>
</ul>
<h2>@Autowired底层的实现原理是什么</h2>
<p>Spring中的@Autowired注解是通过依赖注入(DI)实现的，依赖注入是一种设计模式，它将对象的创建和依赖关系的管理从应用程序代码中分离出来，使得应用程序更加灵活和可维护。</p>
<ul>
<li>具体来说，当Spring容器启动时，它会扫描应用程序中的所有Bean,并将它们存储在一个BeanFactory中。当应用程序需要使用某个Bean时，Spring容器会自动将该Bean注入到应用程序中。</li>
<li>但再往底层说，DI是通过Java反射机制实现的。当Spring容器需要注入某个Bean时，它会使用Java反射机制来查找符合条件的Bean,并将其注入到应用程序中。</li>
</ul>
<p>所以说，@Autowired注解是通过DI的方式，底层通过Java的反射机制来实现的。</p>
<h2>Spring如何解决循环依赖问题</h2>
<p>Spring循环依赖问题指的是在Spring容器中出现相互依赖的情况，即两个或多个Bean之间相互依赖，形成了一个循环依赖链。例如，Bean A依赖Bean B，Bean B又依赖Bean A,这就构成了一个循环依赖。</p>
<p>Spring是通过三级缓存解决循环依赖问题的，基本思路是：在Bean创建过程中，将正在创建的Bean对象放入一个专门用于缓存正在创建中的Bean对象的缓存池中，当后续创建其他Bean对象时，若需要依赖于该缓存池中正在创建的Bean,则直接使用缓存池中的Bean对象，而不是重新创建一个新的Bean对象。</p>
<p>简单来说，Spring 的三级缓存包括：</p>
<ol>
<li><strong>一级缓存（singletonObjects）</strong>：存放最终形态的 Bean（已经实例化、属性填充、初始化），单例池，为“Spring 的单例属性”⽽⽣。一般情况我们获取 Bean 都是从这里获取的，但是并不是所有的 Bean 都在单例池里面，例如原型 Bean 就不在里面。</li>
<li><strong>二级缓存（earlySingletonObjects）</strong>：存放过渡 Bean（半成品，尚未属性填充），也就是三级缓存中<code>ObjectFactory</code>产生的对象，与三级缓存配合使用的，可以防止 AOP 的情况下，每次调用<code>ObjectFactory#getObject()</code>都是会产生新的代理对象的（如果B,C都依赖A，没有二级缓存会创建两个代理对象）。</li>
<li><strong>三级缓存（singletonFactories）</strong>：存放<code>ObjectFactory</code>，<code>ObjectFactory</code>的<code>getObject()</code>方法（最终调用的是<code>getEarlyBeanReference()</code>方法）可以生成原始 Bean 对象或者代理对象（如果 Bean 被 AOP 切面代理）。<ul>
<li>不会立即调：（如果在实例化后立即调用的话：所有的aop不管bean是否循环依赖都会在实例化后创建proxy,正常Bean其实spring还是希望遵循生命周期在初始化创建动态代理，只能循环依赖才创建)</li>
<li>会在ABA(第二次getBean(A)才会去调用三级缓存（如果实现了aop才会创建动态代理，如果没有实现依然返回的Bean的实例）)</li>
<li>放入二级缓存（避免重复创建）</li>
<li>三级缓存只会对单例 Bean 生效。</li>
</ul>
</li>
</ol>
<p>具体而言，Spring通过三级缓存解决循环依赖问题的步骤如下：</p>
<ul>
<li>当 Spring 创建 A 之后，发现 A 依赖了 B ，又去创建 B，B 依赖了 A ，又去创建 A；</li>
<li>在 B 创建 A 的时候，那么此时 A 就发生了循环依赖，由于 A 此时还没有初始化完成，因此在 <strong>一二级缓存</strong> 中肯定没有 A；</li>
<li>那么此时就去三级缓存中调用 <code>getObject()</code> 方法去获取 A 的 <strong>前期暴露的对象</strong> ，也就是调用上边加入的 <code>getEarlyBeanReference()</code> 方法，生成一个 A 的 <strong>前期暴露对象</strong>(原始 Bean 对象或者代理对象）；</li>
<li>然后就将这个 <code>ObjectFactory</code> 从三级缓存中移除，并且将前期暴露对象放入到二级缓存中，那么 B 就将这个前期暴露对象注入到依赖，来支持循环依赖。</li>
</ul>
<p>需要注意的是，三级缓存并不是无限制地缓存Bean对象，而是限定在Bean对象创建过程中使用，Bean对象创建完成后将会从三级缓存中移除。此外，如果Bean对象的依赖关系存在循环依赖，则在创建过程中将会抛出异常，因为无法通过缓存解决循环依赖的问题</p>
<p></p>
<p><em><strong>只用两级缓存够吗？</strong></em></p>
<ul>
<li>如果只是死循环的问题：一级缓存就可以解决：<a href="#Spring:%E6%98%AF%E5%A6%82%E4%BD%95%E5%B8%AE%E6%88%91%E4%BB%AC%E5%9C%A8%E5%B9%B6%E5%8F%91%E4%B8%8B%E9%81%BF%E5%85%8D%E8%8E%B7%E5%8F%96%E4%B8%8D%E5%AE%8C%E6%95%B4%E7%9A%84Bean?">无法避免在并发下获取不完整的Bean?</a></li>
<li>只使用一级和三级缓存也可以解决循环依赖：只不过如果出现重复循环依赖会多次创建AOP的动态代理</li>
</ul>
<p><em><strong>Spring有没有解决多例Bean的循环依赖？</strong></em></p>
<ul>
<li>多例不会使用缓存进行存储（多例Bean每次使用都需要重新创建)</li>
<li>不缓存早期对象就无法解决循环</li>
</ul>
<p><em><strong>Spring有没有解决构造函数参数Bean的循环依赖？</strong></em></p>
<p>没有，可以通过人工进行解决：@Lazy</p>
<ul>
<li>不会立即创建依赖的bean</li>
<li>而是等到用到才通过动态代理进行创建</li>
</ul>
<h2>Spring:是如何帮我们在并发下避免获取不完整的Bean?</h2>
<p>不完整Bean：只是实例化，没有属性填充，初始化</p>
<p>如果不加锁，线程1实例化加入三级缓存，线程2直接拿三级缓存，就不完整</p>
<p>使用双重检查锁，检查完一级缓存后加锁，放锁后再检查一次，因为放锁后已经创建好的bean放在一级缓存但是第一次已经检查过，所以得双重检查</p>
<p></p>
<h2>Spring中可以出现两个ID相同的bean吗，如果不行会在什么时候报错</h2>
<p>分情况，同一个spring配置文件里不能存在id相同的bean，会在解析xml文件转换为BeanDefinition阶段报错。不同的spring配置文件里可以存在id相同的两个bean，默认会把多个id相同的bean进行覆盖，spring3.X版本后使用@Configuration进行配置的时候，同一个配置类中使用@Bean声明多个相同名字的bean默认只会注册第一个，使用@Autowired可能会提示找不到未注册的类，使用@Resource注解会在bean初始化之后依赖注入的时候可能会提示类型不匹配错误</p>
<h2>Spring提供了哪些配置方式</h2>
<p>将Spring配置到应用开发中有以下三种方式：</p>
<ul>
<li><p>基于XML的配置</p>
<ul>
<li><p>bean所需的依赖项和服务在XML格式的配置文件中指定。这些配置文件通常包含许多bean定义和特定于应用程序的配置选项。它们通常以bean标签开头。例如：</p>
</li>
<li><pre><code class="language-undefined">[object Object]</code></pre></li>
</ul>
</li>
<li><p>基于注解的配置</p>
<ul>
<li>可以通过在相关的类，方法或字段声明上使用注解，将bean配置为组件类本身</li>
</ul>
</li>
<li><p>基于Java API配置</p>
<ul>
<li><p>Spring的Java配置是通过使用@Bean和@Configuration来实现</p>
</li>
<li><p>@Bean注解扮演与<bean/>元素相同的角色。</p>
</li>
<li><p>@Configuration类允许通过简单地调用同一个类中的其他@Bean方法来定义bean间依赖关系。
例如：</p>
</li>
<li><pre><code class="language-undefined">[object Object]</code></pre></li>
</ul>
</li>
</ul>
<blockquote>
<ol>
<li><strong>作用范围</strong>:<ul>
<li><code>@Bean</code> 注解用于标记方法,表示该方法将返回一个对象,这个对象要注册为 Spring 容器中的 Bean。</li>
<li><code>@Configuration</code> 注解用于标记类,表示该类是一个配置类,里面可以包含多个使用 <code>@Bean</code> 注解的方法。</li>
</ul>
</li>
<li><strong>代理机制</strong>:<ul>
<li><code>@Configuration</code> 类会被 Spring 特殊处理,生成一个CGLIB代理对象,当在 <code>@Configuration</code> 类中调用 <code>@Bean</code> 方法时,Spring 会用代理对象来拦截方法调用,确保返回的是单例 Bean。</li>
<li><code>@Bean</code> 方法如果不在 <code>@Configuration</code> 类中使用,则不会被 CGLIB 代理,也就没有这种单例 Bean 的保证。</li>
</ul>
</li>
<li><strong>注入依赖</strong>:<ul>
<li>在 <code>@Configuration</code> 类中,<code>@Bean</code> 方法可以接受参数,Spring 会自动根据参数类型注入依赖的 Bean。</li>
<li>在普通类中使用 <code>@Bean</code> 注解,由于没有 CGLIB 代理,方法参数需要手动注入依赖 Bean。</li>
</ul>
</li>
<li><strong>使用场景</strong>:<ul>
<li><code>@Configuration</code> 类通常用于替代 XML 配置文件,集中管理应用程序的 Bean 定义。</li>
<li><code>@Bean</code> 注解则可以单独使用,在任何类中定义 Bean,比如在 service 层或 controller 层。</li>
</ul>
</li>
</ol>
<p>总的来说,<code>@Configuration</code> 是一种特殊的配置类,它能够确保 <code>@Bean</code> 方法返回的 Bean 是单例的,并且可以方便地管理 Bean 之间的依赖关系。而单独使用 <code>@Bean</code> 注解则更灵活,可以在任意位置定义 Bean,但需要自己管理依赖关系。</p>
</blockquote>
<h2>什么是Spring的内部bean</h2>
<p>Spring 中的内部 bean (Inner Bean) 是指在另一个 bean 的内部定义的 bean。它通常用于属性注入中,作为某个属性的值。</p>
<p>内部 bean 有以下特点:</p>
<ol>
<li><strong>定义位置</strong>: 内部 bean 是定义在另一个 bean 的内部,通常是作为某个属性的值。</li>
<li><strong>作用域限制</strong>: 内部 bean 的作用域仅限于包含它的外部 bean,它没有独立的 bean 标识。</li>
<li><strong>匿名</strong>: 内部 bean 通常是匿名的,没有显式的 bean 标识。</li>
<li><strong>生命周期</strong>: 内部 bean 的生命周期与包含它的外部 bean 实例的生命周期相同。</li>
</ol>
<h2>什么是Spring装配</h2>
<p>Spring 装配(Wiring)是指在 Spring 容器中,定义 bean 之间的依赖关系和注入方式的过程。它是 Spring IoC 容器的核心功能之一。</p>
<p>Spring 装配主要包括以下几个方面:</p>
<ol>
<li><strong>Bean 定义</strong>: 通过 XML、Java 注解或 Java 配置类等方式,定义 bean 及其属性。</li>
<li><strong>依赖注入</strong>: 确定 bean 与 bean 之间的依赖关系,并通过构造器注入、setter 注入或字段注入等方式将依赖注入到 bean 中。</li>
<li><strong>自动装配</strong>: 容器可以自动识别 bean 之间的依赖关系,并自动完成注入,无需显式配置。</li>
<li><strong>Bean 作用域</strong>: 确定 bean 的作用域,如 singleton、prototype 等。</li>
<li><strong>生命周期回调</strong>: 定义 bean 的初始化和销毁方法。</li>
</ol>
<h3>自动装配有哪些方式？</h3>
<p>Spring容器能够自动装配bean。也就是说，可以通过检查BeanFactory的内容让Spring自动解析bean的协作者。
自动装配的不同模式：</p>
<ul>
<li><p>no，这是默认设置，表示没有自动装配。应使用显式beān引用进行装配。</p>
</li>
<li><p>byName，它根据bean的名称注入对象依赖项。它匹配并装配其属性与XML文件中由相同名称定义的bean</p>
</li>
<li><p>byType，它根据类型注入对象依赖项。如果属性的类型与XML文件中的一个bean名称匹配，则匹配并装配属
性。</p>
</li>
<li><p>构造器，它通过调用类的构造器来注入依赖项。它有大量的参数。</p>
</li>
<li><p>autodetect首先容器尝试通过构造器使用autowire装配，如果不能，则尝试通过byType自动装配。</p>
</li>
</ul>
<h3>自动装配有什么局限？</h3>
<ul>
<li>覆盖的可能性-您始终可以使用<constructor-arg>和<property>设置指定依赖项，这将覆盖自动装配。</li>
<li>基本元数据类型简单属性（如原数据类型，字符串和类)无法自动装配。</li>
<li>令人困惑的性质，总是喜欢使用明确的装配，因为自动装配不太精确。</li>
</ul>
<h1>Spring AOP</h1>
<h2>什么是AOP</h2>
<p>是什么：AOP(Aspect-Oriented Programming),即面向切面编程，它与OOP(Object-Oriented Programming,面向对象编程)相辅相成，提供了与OOP不同的抽象软件结构的视角。</p>
<p>在OOP中，我们以类(class)作为我们的基本单元，而AOP中的基本单元是Aspect(切面)。</p>
<p>干什么：AOP(Aspect-Oriented Programming面向切面编程)通过代理的方式，在调用想要的对象方法时候，进行拦截处理，执行切入的逻辑，能够将那些与业务无关，却为业务模块所共同调用的逻辑或责任（例如事务处理、日志管理、权限控制等）封装起来，便于减少系统的重复代码，降低模块间的耦合度，并有利于未来的可拓展性和可维护性。</p>
<p>怎么做：AOP采用代理模式实现，动态切入。
实现上代理大体上可以分为：动态代理和静态代理。</p>
<ul>
<li><p>静态代理，代理类在编译阶段生成，在编译阶段将通知织入Java字节码中，也称编译时增强。典型的AspectJ就是静态代理。</p>
<ul>
<li>缺点：对于每一个目标接口或类，都需要定义一个对应的代理类，会有冗余代码。同时，一旦接口增加方法，目标对象与代理对象都要维护。</li>
</ul>
</li>
<li><p>动态代理，代理类在程序运行时创建，AOP框架不会去修改字节码，而是在内存中临时生成一个代理对象，在运行期间对业务方法进行增强，不会生成新类。动态代理：动态代理主要有两种实现方式：</p>
<ul>
<li>JDK动态代理：JDK动态代理要求被代理的类必须实现一个接口，它通过反射来接收被代理的类，并使用
InvocationHandler接口和Proxy类实现代理</li>
<li>CGLIB动态代理：CGLIB则是一个代码生成的类库，它可以在运行时动态地生成某个类的子类，通过继承
的方式实现代理。如果目标类没有实现接口，Spring AOP会选择使用CGLIB来动态代理目标类</li>
</ul>
</li>
</ul>
<p><em><strong>Spring AOP</strong></em></p>
<p>Spring AOP 就是基于动态代理的，如果要代理的对象，实现了某个接口，那么 Spring AOP 会使用 <strong>JDK Proxy</strong>，去创建代理对象，而对于没有实现接口的对象，就无法使用 JDK Proxy 去进行代理了，这时候 Spring AOP 会使用 <strong>Cglib</strong> 生成一个被代理对象的子类来作为代理。</p>
<blockquote>
<p>当然也可以使用 <strong>AspectJ</strong> ！Spring AOP 已经集成了 AspectJ ，AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。</p>
</blockquote>
<p>Spring AOP主要由以下概念组成：</p>
<table>
<thead>
<tr>
<th align="left">术语</th>
<th align="center">含义</th>
</tr>
</thead>
<tbody><tr>
<td align="left">目标(Target)</td>
<td align="center">被通知的对象</td>
</tr>
<tr>
<td align="left">代理(Proxy)</td>
<td align="center">向目标对象应用通知之后创建的代理对象</td>
</tr>
<tr>
<td align="left">连接点(JoinPoint)</td>
<td align="center">目标对象的所属类中，定义的所有方法均为连接点</td>
</tr>
<tr>
<td align="left">切入点(Pointcut)</td>
<td align="center">被切面拦截 / 增强的连接点（切入点一定是连接点，连接点不一定是切入点）</td>
</tr>
<tr>
<td align="left">通知(Advice)</td>
<td align="center">增强的逻辑 / 代码，也即拦截到目标对象的连接点之后要做的事情</td>
</tr>
<tr>
<td align="left">切面(Aspect)</td>
<td align="center">切入点(Pointcut)+通知(Advice)</td>
</tr>
<tr>
<td align="left">Weaving(织入)</td>
<td align="center">将通知应用到目标对象，进而生成代理对象的过程动作</td>
</tr>
</tbody></table>
<h3>Spring AOP 和 AspectJ AOP 有什么区别</h3>
<p><strong>Spring AOP 属于运行时增强，而 AspectJ 是编译时增强。</strong> Spring AOP 基于代理(Proxying)，而 AspectJ 基于字节码操作(Bytecode Manipulation)。</p>
<p>Spring AOP 已经集成了 AspectJ 。AspectJ 相比于 Spring AOP 功能更加强大，但是 Spring AOP 相对来说更简单，</p>
<p>如果我们的切面比较少，那么两者性能差异不大。但是，当切面太多的话，最好选择 AspectJ ，它比 Spring AOP 快很多。</p>
<h3>AspectJ 定义的通知类型有哪些？</h3>
<ul>
<li><strong>Before</strong>（前置通知）：目标对象的方法调用之前触发</li>
<li><strong>After</strong> （后置通知）：目标对象的方法调用之后触发</li>
<li><strong>AfterReturning</strong>（返回通知）：目标对象的方法调用完成，在返回结果值之后触发</li>
<li><strong>AfterThrowing</strong>（异常通知）：目标对象的方法运行中抛出 / 触发异常后触发。AfterReturning 和 AfterThrowing 两者互斥。如果方法调用成功无异常，则会有返回值；如果方法抛出了异常，则不会有返回值</li>
<li><strong>Around</strong> （环绕通知）：编程式控制目标对象的方法调用。环绕通知是所有通知类型中可操作范围最大的一种，因为它可以直接拿到目标对象，以及要执行的方法，所以环绕通知可以任意的在目标对象的方法调用前后搞事，甚至不调用目标对象的方法</li>
</ul>
<h1>Spring MVC</h1>
<h2>MVC是什么？MVC设计模式的好处有哪些</h2>
<p>Spring MVC是一个基于Java实现了MVC设计模式的请求驱动类型的轻量级Web框架，通过把模型(model)-视图(view)-控制器(controller)分离，将web层进行职责解耦，把复杂的web应用分成逻辑清晰的几部分，简化开发，减少出错，方便组内开发人员之间的配合。</p>
<p></p>
<p>流程：</p>
<ol>
<li>用户通过View页面向服务端提出请求，可以是表单请求、超链接请求、AJAX请求等；</li>
<li>服务端Controller控制器接收到请求后对请求进行解析，找到相应的Model，对用户请求进行处理Mode!处理；</li>
<li>将处理结果再交给Controller(控制器其实只是起到了承上启下的作用)；</li>
<li>根据处理结果找到要作为向客户端发回的响应View页面，页面经渲染后发送给客户端。</li>
</ol>
<p><strong>MVC设计模式的好处</strong>：</p>
<ul>
<li>分层设计，实现了业务系统各个组件之间的解耦，有利于业务系统的可扩展性，可维护性；</li>
<li>有利于系统的并行开发，提升开发效率。</li>
</ul>
<h2>Spring MVC常用的注解有哪些</h2>
<ul>
<li><p>@RequestMapping：用于处理请求url映射的注解，可用于类或方法上。用于类上，则表示类中的所有响应请求的方法都是以该地址作为父路径</p>
</li>
<li><p>@RequestBody：注解实现接收http请求的json数据，将json转换为java对象</p>
</li>
<li><p>@ResponseBody：注解实现将conreoller方法返回对象转化为json对象响应给客户</p>
</li>
<li><p>@Controller：控制器的注解，不能用用别的注解代替</p>
</li>
<li><p>@RestController：@ResponseBody + @Controller</p>
</li>
</ul>
<h2>Spring MVC 的核心组件有哪些，执行流程了解吗</h2>
<p>记住了下面这些组件，也就记住了 SpringMVC 的工作原理。</p>
<ul>
<li><strong><code>DispatcherServlet</code></strong>：<strong>核心的中央处理器</strong>，负责接收请求、分发，并给予客户端响应。</li>
<li><strong><code>Handler</code></strong>：<strong>请求处理器</strong>，完成具体业务逻辑。</li>
<li><strong><code>HandlerMapping</code></strong>：<strong>处理器映射器</strong>，根据 URL 去匹配查找能处理的 <code>Handler</code> ，并会将请求涉及到的拦截器和 <code>Handler</code> 一起封装。</li>
<li><strong><code>HandlerAdapter</code></strong>：<strong>处理器适配器</strong>，根据 <code>HandlerMapping</code> 找到的 <code>Handler</code> ，适配执行对应的 <code>Handler</code>；</li>
<li><strong><code>HandlerInterceptor</code></strong>：<strong>处理器拦截器</strong>，是一个接口，如果需要完成一些拦截处理，可以实现该接口。</li>
<li><strong><code>HandlerExecutionChain</code></strong>：<strong>处理器执行链</strong>，包括两部分内容：<code>Handler</code>和<code>HandlerInterceptor</code>(系统会有一个默认的<code>HandlerInterceptor</code>，如果需要额外设置拦截，可以添加拦截器)。</li>
<li><strong><code>ModelAndView</code></strong>：装载了模型数据和视图信息，作为<code>Handler</code>的处理结果，返回给<code>DispatcherServlet</code></li>
<li><strong><code>ViewResolver</code></strong>：<strong>视图解析器</strong>，根据 <code>Handler</code> 返回的逻辑视图 / 视图，解析并渲染真正的视图，并传递给 <code>DispatcherServlet</code> 响应客户端</li>
</ul>
<p></p>
<p>SpringMVC是基于MVC设计模式实现的Web框架，其工作流程如下：</p>
<ol>
<li><strong>客户端</strong>发送HTTP请求至前端控制器<code>DispatcherServlet</code></li>
<li><code>DispatcherServlet</code>根据请求信息调用<code>HandlerMapping</code>，解析请求对应的<code>Handler</code>即处理器(Controller)</li>
<li><code>HandlerMapping</code>根据请求URL查找对应的<strong>Controller</strong>，同时生成用于执行该请求的<code>HandlerExecutionChain</code>对象</li>
<li><code>DispatcherServlet</code>调用<code>HandlerAdapter</code>执行<code>Handler</code></li>
<li><code>Handler</code>执行完成后，返回一个<code>ModelAndView</code>对象给<code>HandlerAdapter</code></li>
<li><code>HandlerAdapter</code>将<code>ModelAndView</code>对象传递给<code>DispatcherServlet</code></li>
<li><code>DispatcherServlet</code>调用<code>ViewResolver</code>解析<strong>视图</strong>(View)</li>
<li><code>ViewResolver</code>解析出<code>View</code>对象后，将其返回给<code>DispatcherServlet</code></li>
<li><code>DispatcherServlet</code>调用<code>View</code>对象的<strong>render()<strong>方法进行</strong>视图渲染</strong></li>
<li><code>DispatcherServlet</code>将<strong>渲染后的视图</strong>（生成好的HTML内容）返回给<strong>客户端</strong></li>
</ol>
<p>在这个过程中，<code>DispatcherServlet</code>是整个SpringMVC的核心，它负责协调各个组件的工作。<code>HandlerMapping</code>负责将请求映射到对应的Controller，而<code>HandlerAdapter</code>负责执行Controller。<code>ViewResolver</code>则根据逻辑视图名（如JSP文件名)解析出View对象，最后由View渲染出实际的页面内容。通过这种分工协作的方式，SpringMVC可以实现灵活、高效、可扩展的Web应用程序开发。</p>
<h2>Spring MVC拦截器是什么</h2>
<p>Spring的处理程序映射机制包括处理程序拦截器，当你希望将特定功能应用于某些请求时，例如，检查用户主题时，这些拦截器非常有用。
拦截器必须实现org.springframework.web.servlet包的HandlerInterceptor。此接口定义了三种方法：</p>
<ul>
<li>preHandle:在执行实际处理程序之前调用。</li>
<li>postHandle:在执行完实际程序，之后调用。</li>
<li>afterCompletion:在完成请求后调用。</li>
</ul>
<p><strong>使用场景：</strong></p>
<ul>
<li>日志记录：可用于记录请求日志，便于信息监控和信息统计；</li>
<li>权限检查：可用于用户登录状态的检查；</li>
<li>统一安全处理：可用于统一的安全效验或参数的加密/解密等。</li>
</ul>
<h2>拦截器和过滤器区别是什么</h2>
<p>拦截器和过滤器的区别主要体现在以下5点：</p>
<ol>
<li>出身不同：过滤器来自于Servlet，而拦截器来自于Spring框架；</li>
<li>触发时机不同：请求的执行顺序是：请求进入容器&gt;进入过滤器&gt;进入Servlet&gt;进入拦截器&gt;执行控制器(Controller)，所以过滤器和拦截器的执行时机，是过滤器会先执行，然后才会执行拦截器，最后才会进入真正的要调用的方法；</li>
<li>底层实现不同：过滤器是基于方法回调实现的，拦截器是基于动态代理（底层是反射）实现的；</li>
<li>支持的项目类型不同：过滤器是Servlet规范中定义的，所以过滤器要依赖Servlet容器，它只能用在Web项目中；而拦截器是Spring中的一个组件，因此拦截器既可以用在Web项目中，同时还可以用在Application或Swing程序中；</li>
<li>使用的场景不同：因为拦截器更接近业务系统，所以拦截器主要用来实现项目中的业务判断的，比如：登录判断、权限判断、日志记录等业务；而过滤器通常是用来实现通用功能过滤的，比如：敏感词过滤、字符集编码设置、响应数据压缩等功能。</li>
</ol>
<h2>Spring MVC统一异常处理</h2>
<p><code>@ControllerAdvice</code> + <code>@ExceptionHandler</code>，会给所有或者指定的 <code>Controller</code> 织入异常处理的逻辑（AOP），当 <code>Controller</code> 中的方法抛出异常的时候，由被<code>@ExceptionHandler</code> 注解修饰的方法进行处理。</p>
<h1>Spring事务</h1>
<h2>Spring事务实现方式有哪些？</h2>
<ul>
<li>编程式事务：在代码中硬编码（在分布式系统中推荐使用）：通过TransactionTemplate或者TransactionManager手动管理事务，事务范围小。</li>
<li>声明式事务：在XML配置文件中配置或者直接基于注解（单体应用或者简单业务系统推荐使用）：实际是通过AOP实现（基于@Transactional的全注解方式使用最多）</li>
</ul>
<h2>事务的传播级别有哪些？</h2>
<p>事务的传播机制定义了在方法被另一个事务方法调用时，这个方法的事务行为应该如何。</p>
<p>Spig提供了一系列事务传播行为，这些传播行为定义了事务的边界和事务上下文如何在方法调用链中传播。</p>
<p>Spring事务定义了7种传播机制：</p>
<ul>
<li>PROPAGATION_REQUIRED：表示如果当前存在事务，则在当前事务中执行；如果当前没有事务，则创建一个新的事务并在其中执行。即，方法被调用时会尝试加入当前的事务，如果不存在事务，则创建一个新的事务。如果外部事务回滚，那么内部事务也会被回滚（一句话：<strong>方法调用将加入当前事务，或者创建一个新事务</strong>）</li>
<li>PROPAGATION_NESTED：表示如果当前存在事务，则在嵌套事务中执行；如果当前没有事务，则创建一个新的事务并在其中执行。嵌套事务是独立于外部事务的子事务，它具有自己的保存点，并且可以独立于外部事务进行回滚。如果嵌套事务发生异常并回滚，它将会回滚到自己的保存点，而不影响外部事务（一句话：<strong>是嵌套的传播行为，方法调用将在独立的子事务中执行，具有自己的保存点，可以独立于外部事务进行回滚，而不影响外部事务</strong>）</li>
<li>如果希望内部方法能够独立于外部事务进行回滚，可以选择PROPAGATION_NESTED，如果你希望内部方法与外部事务一同回滚或提交，可以选择PROPAGATION_REQUIRED。</li>
</ul>
<h2>Spring事务中的隔离级别有哪几种？</h2>
<p>在TransactionDefinition接口中定义了五个表示隔离级别的常量：</p>
<ul>
<li>ISOLATION_DEFAULT：使用后端数据库默认的隔离界别，MySQL默认可重复读，Oracle默认读已提交</li>
<li>ISOLATION_READ_UNCOMMITTED：<strong>读未提交</strong>，最低的隔离级别，允许读取尚未提交的数据变更，可能会导致脏读、幻读或不可重复读</li>
<li>ISOLATION_READ_COMMITTED：读已提交，允许读取并发事务已经提交的数据，可以阻止脏读，但是幻读或不可重复读仍有可能发生</li>
<li>ISOLATION_REPEATABLE_READ：<strong>可重复读</strong>，对同一字段的多次读取结果都是一致的，除非数据是被本身事务
自己所修改，可以阻止脏读和不可重复读，但幻读仍有可能发生</li>
<li>ISOLATION_SERIALIZABLE：<strong>串行化</strong>，最高的隔离级别，完全服从ACID的隔离级别。所有的事务依次逐个执行，这样事务之间就完全不可能产生干扰，也就是说，该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。</li>
</ul>
<h2>声明式事务实现原理了解吗？</h2>
<p>Spring的声明式事务管理是通过AOP(面向切面编程)和代理机制实现的</p>
<p><strong>第一步，在Bean初始化阶段创建代理对象：</strong>
Spring容器在初始化单例Bean的时候，会遍历所有的BeanPostProcessor实现类，并执行其postProcessAfterInitialization方法。</p>
<p>在执行postProcessAfterInitialization方法时会遍历容器中所有的切面，查找与当前Bean匹配的切面，这里会获取事务的属性切面，也就是@Transactional注解及其属性值。</p>
<p>然后根据得到的切面创建一个代理对象，默认使用JDK动态代理创建代理，如果目标类是接口，则使用JDK动态代理，否则使用Cglib。</p>
<p><strong>第二步，在执行目标方法时进行事务增强操作：</strong>
当通过代理对象调用Bean方法的时候，会触发对应的AOP增强拦截器，声明式事务是一种环绕增强，对应接口为MethodInterceptor，事务增强对该接口的实现为TransactionInterceptor,</p>
<p>事务拦截器TransactionInterceptor在invoke方法中，通过调用父类TransactionAspectSupport的invokeWithinTransaction方法进行事务处理，包括开启事务、事务提交、异常回滚等。</p>
<h2>Spring声明式事务无效可能的原因有哪些？</h2>
<p>在开发过程中，可能会遇到使用@Transactional进行事务管理时出现失效的情况。这里是基于事务的默认传播行为是REQUIRED</p>
<p>常见失效场景：</p>
<ul>
<li>如果使用MySQL且引擎是MyISAM，则事务会不起作用，原因是MyISAM不支持事务，改成InnoDB引擎则支持事务。</li>
<li>注解@Trasactional只能加在public修饰的方法上事务才起效。如果加在protect、private等非public修饰的方法上，事务将失效。<ul>
<li>原因：Spring默认使用基于JDK的动态代理（当接口存在时）或基于CGLIB的代理(当只有类时)来实现事务。这两种代理机制都只能代理公开的方法。</li>
</ul>
</li>
<li>如果在开启了事务的方法内，使用了try-catch语句块对异常进行了捕获，而没有将异常抛到外层，事务将不起效（事务管理依赖于异常传播机制，无法感知异常）。</li>
<li>不同类之间方法调用时，只有异常发生在开启事务的方法内，事务才有效。</li>
<li>在同一个类的方法之间调用中，如果A方法调用了B方法，不管A方法有没有开启事务，由于Sping的代理机制（同一个类的两个方法之间互相调用时，AOP代理就失效了）B方法的事务是无效的</li>
</ul>
<h1>Spring注解</h1>
<h2>用过哪些重要的Spring注解？</h2>
<ul>
<li>@Controller用于Spring MVC项目中的控制器类。</li>
<li>@Service用于服务类。</li>
<li>@RequestMapping用于在控制器处理程序方法中配置URI映射。</li>
<li>@ResponseBody用于发送Object作为响应，通常用于发送XML或JSON数据作为响应。</li>
<li>@PathVariable用于将动态值从URI映射到处理程序方法参数。</li>
<li>@Autowired用于在spring bean中自动装配依赖项。</li>
<li>@Qualifier使用@Autowired注解，以避免在存在多个bean类型实例时出现混淆。</li>
<li>@Scope用于配置spring bean的范围。</li>
<li>@Configuration，@ComponentScan和@Bean用于基于java的配置.</li>
<li>@Aspect，@Before，@Ater，@Around，@Pointcut用于切面编程(AOP)</li>
</ul>
<h2>@Component，@Controller，@Repository，@Service有何区别？</h2>
<ul>
<li>@Component：这将java类标记为bean。它是任何Spring管理组件的通用构造型。spring的组件扫描机制现在可以将其拾取并将其拉入应用程序环境中。</li>
<li>@Controller：这将一个类标记为Spring Web MVC控制器。标有它的Bean会自动导入到IoC容器中。</li>
<li>@Service：此注解是组件注解的特化。它不会对@Component注解提供任何其他行为。您可以在服务层类中使用@Service而不是@Component,因为它以更好的方式指定了意图。</li>
<li>@Repository：这个注解是具有类似用途和功能的@Component注解的特化。它为DAO提供了额外的好处。它将DAO导入IoC容器，并使未经检查的异常有资格转换为Spring DataAccessException。</li>
</ul>
<h2>@Qualifier注解有什么用？</h2>
<p><strong>多个相同类型的bean注入冲突</strong>，使用@Qualifier注解和@Autowired通过指定应该装配哪个确切的bean来消除歧义。</p>
<p>相同类型的bean指的是多个bean实例实现了同一个接口或继承自同一个抽象类</p>
<h2>@RequestMapping和@GetMapping注解的不同之处在哪里？</h2>
<ul>
<li>@RequestMapping：可注解在类和方法上；@GetMapping仅可注册在方法上</li>
<li>@RequestMapping:可进行GET、POST、PUT、DELETE等请求方法；@GetMapping是@RequestMapping的GET请求方法的特例，目的是为了提高清晰度</li>
</ul>
<h2>@Controller注解有什么用？</h2>
<p>@Controller注解标记一个类为Spring Web MVC控制器Controller。Spring MVC会将扫描到该注解的类，然后扫描这个类下面带有@RequestMapping注解的方法，根据注解信息，为这个方法生成一个对应的处理器对象。</p>
<h2>@RestController和@Controller有什么区别？</h2>
<p>@RestController注解，在@Controller基础上，增加了@ResponseBody注解，更加适合目前前后端分离的架构下，提供Restful API，返回</p>
<p>例如JSON数据格式。当然，返回什么样铂的数据格式，根据客户端的ACCEPT请求头来决定。</p>
<h2>@RequestParam和@PathVariable两个注解的区别？</h2>
<p>两个注解都用于方法参数，获取参数值的方式不同，@RequestParam注解的参数从请求携带的参数中获取，而@PathVariable注解从请求的URI中获取</p>
<h1>SpringBoot</h1>
<h1>MyBatis</h1>
<p><a href="https://javaguide.cn/system-design/framework/mybatis/mybatis-interview.html">MyBatis常见面试题总结 | JavaGuide</a></p>
<p>参考：
<a href="https://javaguide.cn/system-design/framework/spring">https://javaguide.cn/system-design/framework/spring</a></p>

      <p style='text-align: right'>
      <a href='https://www.taitres.com/posts/java/ssm#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">66792d1b9233a1d47fb1114c</guid>
  <category>posts</category>
<category>Java</category>
 </item>
  
</channel>
</rss>