From 12250859d5096f840e3db6ebbb962f13c17373c9 Mon Sep 17 00:00:00 2001 From: AdmireAn <854818303@qq.com> Date: Fri, 20 May 2022 10:52:16 +0800 Subject: [PATCH 01/15] Create .gitattributes --- .gitattributes | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..f34960fcc --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +*.js linguist-language=java +*.css linguist-language=java +*.html linguist-language=java +*.sh linguist-language=java From 9b37983aa1a50bd61f9121720523dc4228790e23 Mon Sep 17 00:00:00 2001 From: admirean Date: Fri, 20 May 2022 12:06:44 +0800 Subject: [PATCH 02/15] fix --- README.MD | 87 ++----------------- .../01.\345\205\263\344\272\216.md" | 26 ++---- 2 files changed, 13 insertions(+), 100 deletions(-) diff --git a/README.MD b/README.MD index 93bc98e06..9d7de1afc 100644 --- a/README.MD +++ b/README.MD @@ -1,101 +1,26 @@ -

logo

- -

- npm - npm - GitHub stars - CI - baiduPush - - - -

+

logo

JavaDriver

-* [文档 (国内源)](https://doc.xugaoyi.com/) -* [效果:Evan's blog](https://xugaoyi.com/) - - -## 介绍 -1. 这个主题的初衷是打造一个好用的、面向程序员的`知识管理工具` -2. 轻松构建一个`结构化`的知识库,让你的知识海洋像一本本书一样清晰易读。 -3. 博客功能提供一种知识的`碎片化`形态,并支持个性化博客配置。 -4. `简洁高效`,以 Markdown 为中心的项目结构。内置自动化工具,以更少的配置完成更多的事。配合多维索引快速定位每个知识点。 - -[**更新日志**](https://github.com/xugaoyi/vuepress-theme-vdoing/releases) - -## 这个主题可以做什么? -* 案例1:[知识库兼博客站](https://xugaoyi.com/) -* 案例2:[仅博客站](https://xugaoyi.github.io/vdoing-demo-blog/) -* 案例3:[仅知识库](https://xugaoyi.github.io/vdoing-demo-repository/) -* 案例4:[文档站](https://doc.xugaoyi.com/) - - -## 快速上手 +* [文档 (国内源)](https://admirean.github.io/JavaDriver/) -```bash -# clone the project -git clone https://github.com/xugaoyi/vuepress-theme-vdoing.git -# enter the project directory -cd vuepress-theme-vdoing -# install dependency -npm install # or yarn install +## 介绍 +1. Java基础相关的面试题 -# develop -npm run dev # or yarn dev -``` ## ⚡️未来... -期待 [VuePress v2.0](https://github.com/vuepress/vuepress-next) 以及 [VitePress](https://github.com/vuejs/vitepress) 的正式发布... - -届时,VuePress 1.x 编译慢的缺点将得到极大的改善。我将会视情况把主题升级至 VuePress v2.0 或 VitePress,也可能两个都升级。目前(2020.10.29)来看还需要一段时间才能让大家使用上基于它们的新版本,还希望大家多多 [:sparkling_heart:支持](https://doc.xugaoyi.com/pages/1b12ed/) 哟,持续关注吧~ +期待 [计算机基础](https://admirean.github.io/JavaDriver/basic/) 以及 [中间件](https://admirean.github.io/JavaDriver/middleware/) 的正式发布... ## :sparkling_heart:支持这个项目 如果你正在使用这个项目并感觉良好,或只是想要支持我继续开发,你可以通过如下*任意* 方式支持我: 1. *Star* 并 分享这个项目 :rocket: -2. 保留主题 footer(页脚) 的主题链接 :D -3. 关注公众号`yqyjs666`,回复`前端资源`,可获取 [前端学习资源](https://github.com/xugaoyi/blog-gitalk-comment/wiki/Front-end-Study) - -4. 通过以下二维码 一次性捐款。 我多半会买一杯 ~~咖啡~~ 茶。:tea: - -谢谢! :heart: - -| 微信赞赏 | 微信 | 支付宝 | -| :---: | :---: | :---: | -| 赞赏码 | Wechat QRcode| Alipay QRcode | - -二维码没有正常显示?点 [这里😎](https://doc.xugaoyi.com/pages/1b12ed/) ## 致谢 :heart:感谢支持这个项目的朋友 -:heart:感谢为这个项目贡献代码的朋友 → [Contributors](https://github.com/xugaoyi/vuepress-theme-vdoing/graphs/contributors) - -## 交流群 - - - - - - - - -
- -

vdoing微信群(添加我的微信进群)

-
- 群号: 694387113 -

vdoing QQ群: 694387113

-
- - -## 许可证 -[MIT](https://github.com/xugaoyi/vuepress-theme-vdoing/blob/master/LICENSE) - -Copyright (c) 2019-present Evan Xu +:heart:感谢为这个项目贡献代码的朋友 diff --git "a/docs/15.\345\205\263\344\272\216/01.\345\205\263\344\272\216.md" "b/docs/15.\345\205\263\344\272\216/01.\345\205\263\344\272\216.md" index 5fcf55687..dfd3c11c8 100644 --- "a/docs/15.\345\205\263\344\272\216/01.\345\205\263\344\272\216.md" +++ "b/docs/15.\345\205\263\344\272\216/01.\345\205\263\344\272\216.md" @@ -7,7 +7,7 @@ article: false --- ## 📚Blog -这是一个兼具博客文章、知识管理、文档查找的个人网站,主要内容是Web前端技术。如果你喜欢这个博客&主题欢迎到[GitHub](https://github.com/xugaoyi/vuepress-theme-vdoing)点个Star,或者交换[友链](/friends/) ( •̀ ω •́ )✧ +这是一个Java学习网站 :::tip 文章内容仅是我个人的小总结,资历尚浅,如有误还请指正。 @@ -23,22 +23,12 @@ article: false -## 🎨Theme - -[ReadMe Card](https://github.com/xugaoyi/vuepress-theme-vdoing) -[ReadMe Card](https://doc.xugaoyi.com/) - -本站主题是 [`Vdoing`](https://github.com/xugaoyi/vuepress-theme-vdoing),这是一款简洁高效的VuePress 知识管理&博客 主题。旨在轻松打造一个`结构化`与`碎片化`并存的个人在线知识库&博客,让你的知识海洋像一本本书一样清晰易读。配合多维索引,让每一个知识点都可以快速定位! [Github地址](https://github.com/xugaoyi/vuepress-theme-vdoing) | [在线vscode预览源码](https://github1s.com/xugaoyi/vuepress-theme-vdoing) - - - ## 🐼Me -web前端小学生 +西二旗Java老司机 ### 技能 -* 熟悉 JavaScript、HTML、CSS、Vue、React 的拼写 +* 熟悉 SpringBoot、Mybatis、Netty、Kafka 的拼写 * 了解 Linux、windows、macOS 的开关机方式 * 精通 Git 的 pull 和 push,并注册了 GitHub 帐号刷了一些 star @@ -46,19 +36,17 @@ web前端小学生 本人照片 --> -## 前端学习 +## Java学习
-关注公众号,回复`前端资源`,即可获取这些 [前端学习资源](https://github.com/xugaoyi/blog-gitalk-comment/wiki/Front-end-Study)。 +关注公众号,回复`Java资源`,即可获取这些 [Java学习资源](https://github.com/xugaoyi/blog-gitalk-comment/wiki/Front-end-Study)。 ## :email: 联系 - WeChat or QQ: {{ QQ }} -- Email: 894072666@qq.com -- GitHub: -- Vdoing主题文档: -- Vdoing交流QQ群:694387113 +- Email: 854818303@qq.com +- GitHub: \n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n57\n58\n59\n60\n61\n62\n63\n64\n65\n66\n67\n68\n69\n70\n71\n72\n73\n74\n75\n76\n77\n78\n79\n80\n81\n82\n83\n84\n85\n86\n87\n88\n89\n90\n91\n92\n93\n94\n95\n96\n97\n98\n99\n100\n101\n102\n103\n104\n105\n106\n107\n108\n109\n110\n111\n112\n113\n114\n115\n116\n117\n118\n119\n120\n121\n122\n123\n124\n125\n126\n127\n128\n129\n130\n131\n132\n133\n134\n135\n136\n137\n138\n139\n140\n141\n142\n143\n144\n145\n146\n147\n148\n149\n150\n151\n152\n153\n154\n155\n156\n157\n158\n159\n160\n161\n162\n163\n164\n165\n166\n167\n168\n169\n170\n171\n172\n173\n174\n175\n176\n177\n178\n179\n180\n181\n182\n183\n184\n185\n186\n187\n188\n189\n190\n191\n192\n193\n194\n195\n196\n197\n198\n199\n200\n201\n202\n203\n204\n205\n206\n207\n208\n209\n210\n211\n212\n213\n214\n215\n216\n217\n218\n219\n220\n221\n222\n223\n224\n225\n226\n227\n228\n229\n230\n231\n232\n233\n234\n235\n236\n237\n238\n239\n240\n241\n242\n243\n244\n245\n246\n247\n248\n249\n250\n251\n252\n253\n254\n255\n256\n257\n258\n259\n260\n261\n262\n263\n264\n265\n266\n267\n268\n269\n270\n271\n272\n273\n274\n275\n276\n277\n278\n279\n280\n281\n282\n283\n284\n285\n286\n287\n288\n289\n290\n291\n292\n293\n294\n295\n296\n297\n298\n299\n300\n301\n302\n303\n304\n305\n306\n307\n308\n309\n310\n311\n312\n313\n314\n315\n316\n317\n318\n319\n320\n321\n322\n323\n324\n325\n326\n327\n328\n329\n330\n331\n332\n333\n334\n335\n336\n337\n338\n339\n340\n341\n342\n343\n344\n345\n346\n347\n348\n349\n350\n351\n352\n353\n354\n355\n356\n357\n358\n359\n360\n361\n362\n363\n364\n365\n366\n367\n368\n369\n370\n371\n372\n373\n374\n375\n376\n377\n378\n379\n380\n381\n382\n383\n384\n385\n386\n387\n388\n389\n390\n391\n392\n393\n394\n395\n396\n397\n398\n399\n400\n401\n402\n403\n404\n405\n406\n407\n408\n409\n410\n411\n412\n413\n414\n415\n416\n417\n418\n419\n420\n421\n422\n423\n424\n425\n426\n427\n428\n429\n430\n431\n432\n433\n434\n435\n436\n437\n438\n439\n440\n441\n442\n443\n444\n445\n446\n447\n448\n449\n450\n451\n452\n453\n454\n455\n456\n457\n458\n459\n460\n461\n462\n463\n464\n465\n466\n467\n468\n469\n470\n471\n472\n473\n474\n475\n476\n477\n478\n479\n480\n481\n482\n483\n484\n485\n486\n487\n488\n489\n490\n491\n492\n493\n494\n495\n496\n497\n498\n499\n500\n501\n502\n503\n504\n505\n506\n507\n508\n509\n510\n511\n512\n513\n514\n515\n516\n517\n518\n519\n520\n521\n522\n523\n524\n525\n526\n527\n528\n529\n530\n531\n532\n533\n534\n535\n536\n537\n538\n539\n540\n541\n542\n543\n544\n545\n546\n547\n548\n549\n550\n551\n552\n553\n554\n555\n556\n557\n558\n559\n560\n561\n562\n563\n564\n565\n566\n567\n568\n569\n570\n571\n572\n573\n574\n575\n576\n577\n578\n579\n580\n581\n582\n583\n584\n585\n586\n587\n588\n589\n590\n591\n592\n593\n594\n595\n596\n597\n598\n599\n600\n601\n602\n603\n604\n605\n606\n607\n608\n609\n610\n611\n612\n613\n614\n615\n616\n617\n618\n619\n620\n621\n622\n623\n624\n625\n626\n627\n628\n629\n630\n631\n632\n633\n634\n635\n636\n637\n638\n639\n640\n641\n642\n643\n644\n645\n646\n647\n648\n649\n650\n651\n652\n653\n654\n655\n656\n657\n658\n659\n660\n661\n662\n663\n664\n665\n666\n667\n668\n669\n670\n671\n672\n673\n674\n675\n676\n677\n678\n679\n680\n681\n682\n683\n684\n685\n686\n687\n688\n689\n690\n691\n692\n693\n694\n695\n696\n697\n698\n699\n700\n701\n702\n703\n704\n705\n706\n707\n708\n709\n710\n711\n712\n713\n714\n715\n716\n717\n718\n719\n720\n721\n722\n723\n724\n725\n726\n727\n728\n729\n730\n731\n732\n733\n734\n735\n736\n737\n\n\n更多有趣的小网页欢迎关注公众号有趣研究社:\n\n> 手写春联\n> FC在线模拟器\n> 爱国头像生成器\n> 到账语音生成器", + "normalizedContent": "手写春联:https://cl.xugaoyi.com/\n\n\n# 前言\n\n虎年春节快到了,首先祝大家新年快乐,轻松暴富。 最近在网上经常看到生成春联的文章,不过这些小demo要么功能简陋,要么ui特别‘程序员’,满足不了我挑剔的眼光。干脆我自己做一个吧,顺便简单体验一下vite+vue3。(因为页面相对简单,vue组件风格还是使用选项式api,重点还是想把产品快速做出来。)\n\n\n\n\n# 产品构思\n\n包含手写春节和生成春联两大功能:\n\n * 手写春联\n \n * 模拟用笔写字的字迹\n * 选择画笔颜色\n * 调整画笔大小\n * 清空画布\n * 撤回笔画\n * 切换上、下联、横批、福字\n * 随机切换对联提示\n * 预览图片和下载\n * 贴春联海报和下载\n\n * 生成模式\n \n * 选择画笔颜色\n * 挑选生成的对联\n * 输入对联\n * 随机切换对联\n * 贴春联海报和下载\n\n * 其他\n \n * 快速切换模式按钮\n * 可控制的背景音乐\n * 微信分享网页\n\n\n# 设计\n\n\n\n\n# 开发\n\n * 技术栈\n * vite (打包&构建)\n * vue3 (页面开发)\n * vant(ui)\n * sass (css)\n * smooth-signature.js (带笔锋手写库)\n\n\n\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n57\n58\n59\n60\n61\n62\n63\n64\n65\n66\n67\n68\n69\n70\n71\n72\n73\n74\n75\n76\n77\n78\n79\n80\n81\n82\n83\n84\n85\n86\n87\n88\n89\n90\n91\n92\n93\n94\n95\n96\n97\n98\n99\n100\n101\n102\n103\n104\n105\n106\n107\n108\n109\n110\n111\n112\n113\n114\n115\n116\n117\n118\n119\n120\n121\n122\n123\n124\n125\n126\n127\n128\n129\n130\n131\n132\n133\n134\n135\n136\n137\n138\n139\n140\n141\n142\n143\n144\n145\n146\n147\n148\n149\n150\n151\n152\n153\n154\n155\n156\n157\n158\n159\n160\n161\n162\n163\n164\n165\n166\n167\n168\n169\n170\n171\n172\n173\n174\n175\n176\n177\n178\n179\n180\n181\n182\n183\n184\n185\n186\n187\n188\n189\n190\n191\n192\n193\n194\n195\n196\n197\n198\n199\n200\n201\n202\n203\n204\n205\n206\n207\n208\n209\n210\n211\n212\n213\n214\n215\n216\n217\n218\n219\n220\n221\n222\n223\n224\n225\n226\n227\n228\n229\n230\n231\n232\n233\n234\n235\n236\n237\n238\n239\n240\n241\n242\n243\n244\n245\n246\n247\n248\n249\n250\n251\n252\n253\n254\n255\n256\n257\n258\n259\n260\n261\n262\n263\n264\n265\n266\n267\n268\n269\n270\n271\n272\n273\n274\n275\n276\n277\n278\n279\n280\n281\n282\n283\n284\n285\n286\n287\n288\n289\n290\n291\n292\n293\n294\n295\n296\n297\n298\n299\n300\n301\n302\n303\n304\n305\n306\n307\n308\n309\n310\n311\n312\n313\n314\n315\n316\n317\n318\n319\n320\n321\n322\n323\n324\n325\n326\n327\n328\n329\n330\n331\n332\n333\n334\n335\n336\n337\n338\n339\n340\n341\n342\n343\n344\n345\n346\n347\n348\n349\n350\n351\n352\n353\n354\n355\n356\n357\n358\n359\n360\n361\n362\n363\n364\n365\n366\n367\n368\n369\n370\n371\n372\n373\n374\n375\n376\n377\n378\n379\n380\n381\n382\n383\n384\n385\n386\n387\n388\n389\n390\n391\n392\n393\n394\n395\n396\n397\n398\n399\n400\n401\n402\n403\n404\n405\n406\n407\n408\n409\n410\n411\n412\n413\n414\n415\n416\n417\n418\n419\n420\n421\n422\n423\n424\n425\n426\n427\n428\n429\n430\n431\n432\n433\n434\n435\n436\n437\n438\n439\n440\n441\n442\n443\n444\n445\n446\n447\n448\n449\n450\n451\n452\n453\n454\n455\n456\n457\n458\n459\n460\n461\n462\n463\n464\n465\n466\n467\n468\n469\n470\n471\n472\n473\n474\n475\n476\n477\n478\n479\n480\n481\n482\n483\n484\n485\n486\n487\n488\n489\n490\n491\n492\n493\n494\n495\n496\n497\n498\n499\n500\n501\n502\n503\n504\n505\n506\n507\n508\n509\n510\n511\n512\n513\n514\n515\n516\n517\n518\n519\n520\n521\n522\n523\n524\n525\n526\n527\n528\n529\n530\n531\n532\n533\n534\n535\n536\n537\n538\n539\n540\n541\n542\n543\n544\n545\n546\n547\n548\n549\n550\n551\n552\n553\n554\n555\n556\n557\n558\n559\n560\n561\n562\n563\n564\n565\n566\n567\n568\n569\n570\n571\n572\n573\n574\n575\n576\n577\n578\n579\n580\n581\n582\n583\n584\n585\n586\n587\n588\n589\n590\n591\n592\n593\n594\n595\n596\n597\n598\n599\n600\n601\n602\n603\n604\n605\n606\n607\n608\n609\n610\n611\n612\n613\n614\n615\n616\n617\n618\n619\n620\n621\n622\n623\n624\n625\n626\n627\n628\n629\n630\n631\n632\n633\n634\n635\n636\n637\n638\n639\n640\n641\n642\n643\n644\n645\n646\n647\n648\n649\n650\n651\n652\n653\n654\n655\n656\n657\n658\n659\n660\n661\n662\n663\n664\n665\n666\n667\n668\n669\n670\n671\n672\n673\n674\n675\n676\n677\n678\n679\n680\n681\n682\n683\n684\n685\n686\n687\n688\n689\n690\n691\n692\n693\n694\n695\n696\n697\n698\n699\n700\n701\n702\n703\n704\n705\n706\n707\n708\n709\n710\n711\n712\n713\n714\n715\n716\n717\n718\n719\n720\n721\n722\n723\n724\n725\n726\n727\n728\n729\n730\n731\n732\n733\n734\n735\n736\n737\n\n\n更多有趣的小网页欢迎关注公众号有趣研究社:\n\n> 手写春联\n> fc在线模拟器\n> 爱国头像生成器\n> 到账语音生成器", "charsets": { "cjk": true }, - "lastUpdated": "2022/05/20, 22:11:32", - "lastUpdatedTimestamp": 1653055892000 + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 }, { "title": "网站", @@ -5759,9 +5790,9 @@ export const siteData = { "date": "2020-04-19T11:33:04.000Z", "article": false }, - "regularPath": "/20.%E6%94%B6%E8%97%8F%E5%A4%B9/01.%E7%BD%91%E7%AB%99.html", - "relativePath": "20.收藏夹/01.网站.md", - "key": "v-89a39f02", + "regularPath": "/@pages/20.%E6%94%B6%E8%97%8F%E5%A4%B9/01.%E7%BD%91%E7%AB%99.html", + "relativePath": "@pages/20.收藏夹/01.网站.md", + "key": "v-7ab50b9f", "path": "/pages/beb6c0bd8a66cea6/", "headers": [ { @@ -6017,198 +6048,8 @@ export const siteData = { "charsets": { "cjk": true }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, - { - "title": "常用的前端轮子", - "frontmatter": { - "title": "常用的前端轮子", - "date": "2021-10-09T19:42:02.000Z", - "permalink": "/pages/47cf96/", - "article": false - }, - "regularPath": "/20.%E6%94%B6%E8%97%8F%E5%A4%B9/02.%E5%B8%B8%E7%94%A8%E7%9A%84%E5%89%8D%E7%AB%AF%E8%BD%AE%E5%AD%90.html", - "relativePath": "20.收藏夹/02.常用的前端轮子.md", - "key": "v-40502781", - "path": "/pages/47cf96/", - "headers": [ - { - "level": 2, - "title": "React UI 组件库", - "slug": "react-ui-组件库", - "normalizedTitle": "react ui 组件库", - "charIndex": 2 - }, - { - "level": 2, - "title": "Vue UI组件库", - "slug": "vue-ui组件库", - "normalizedTitle": "vue ui组件库", - "charIndex": 68 - }, - { - "level": 2, - "title": "常用效果组件", - "slug": "常用效果组件", - "normalizedTitle": "常用效果组件", - "charIndex": 124 - }, - { - "level": 2, - "title": "工具类", - "slug": "工具类", - "normalizedTitle": "工具类", - "charIndex": 201 - }, - { - "level": 2, - "title": "Vue工具类", - "slug": "vue工具类", - "normalizedTitle": "vue工具类", - "charIndex": 331 - }, - { - "level": 2, - "title": "其他", - "slug": "其他", - "normalizedTitle": "其他", - "charIndex": 469 - } - ], - "headersStr": "React UI 组件库 Vue UI组件库 常用效果组件 工具类 Vue工具类 其他", - "content": "# React UI 组件库\n\n * Ant Design\n * React Bootstrap\n * MATERIAL-UI\n\n\n# Vue UI组件库\n\n * Element UI PC端\n * Vant 移动端\n * View UI\n\n\n# 常用效果组件\n\n * Animate.css 动画库\n * Swiper 轮播组件\n * mescroll 下拉刷新和上拉加载框架-基于原生JS\n\n\n# 工具类\n\n * Lodash.js\n * Day.js 处理日期\n * Timeago.js 相对时间,如N小时前\n * Echarts 百度图表\n * Meditor.md 开源在线 Markdown 编辑器\n * validator.js 验证库\n\n\n# Vue工具类\n\n * vue-draggable 基于Sortable.js实现的vue拖拽插件\n * vue-qr 文本转二维码\n * vue-cropper 图片裁剪插件\n * vue-lazyload 懒加载\n * vue-simple-upload 上传组件\n\n\n# 其他\n\n * H5带笔锋手写签名,支持PC端和移动端", - "normalizedContent": "# react ui 组件库\n\n * ant design\n * react bootstrap\n * material-ui\n\n\n# vue ui组件库\n\n * element ui pc端\n * vant 移动端\n * view ui\n\n\n# 常用效果组件\n\n * animate.css 动画库\n * swiper 轮播组件\n * mescroll 下拉刷新和上拉加载框架-基于原生js\n\n\n# 工具类\n\n * lodash.js\n * day.js 处理日期\n * timeago.js 相对时间,如n小时前\n * echarts 百度图表\n * meditor.md 开源在线 markdown 编辑器\n * validator.js 验证库\n\n\n# vue工具类\n\n * vue-draggable 基于sortable.js实现的vue拖拽插件\n * vue-qr 文本转二维码\n * vue-cropper 图片裁剪插件\n * vue-lazyload 懒加载\n * vue-simple-upload 上传组件\n\n\n# 其他\n\n * h5带笔锋手写签名,支持pc端和移动端", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, - { - "title": "你知道的越多,不知道的也就越多", - "frontmatter": { - "title": "你知道的越多,不知道的也就越多", - "date": "2020-05-06T15:52:40.000Z", - "permalink": "/pages/f2e63f", - "sidebar": "auto", - "categories": [ - "随笔" - ], - "tags": [ - "学习", - "知识", - "鸡汤" - ], - "author": { - "name": "xugaoyi", - "link": "https://github.com/xugaoyi" - } - }, - "regularPath": "/_posts/%E9%9A%8F%E7%AC%94/%E4%BD%A0%E7%9F%A5%E9%81%93%E7%9A%84%E8%B6%8A%E5%A4%9A%EF%BC%8C%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84%E4%B9%9F%E5%B0%B1%E8%B6%8A%E5%A4%9A.html", - "relativePath": "_posts/随笔/你知道的越多,不知道的也就越多.md", - "key": "v-2db11cb2", - "path": "/pages/f2e63f/", - "excerpt": "

不知道大家有没有发现,我们身边经常有这样的人,他们越是有能力的,越是有知识的,越是低调,越是谦逊,因为他们深知,知道的越多,不知道的也就越多。

\n", - "headersStr": null, - "content": "不知道大家有没有发现,我们身边经常有这样的人,他们越是有能力的,越是有知识的,越是低调,越是谦逊,因为他们深知,知道的越多,不知道的也就越多。\n\n你知道的越多,你不知道的也就越多,这是一句非常有哲理的话。\n\n每个人的知识面都是有限的,你有可能在某个领域会有较深的研究,成为这个领域里的专家,等到你站在高处的时候,才会发现,自己是多么的渺小,才知道自己有多少没有涉及的领域。知道的越多,疑惑、问题就会越来越多,对已知的质疑、疑虑、困惑就会越来越多。\n\n即使如此,我们也应该努力,至少可以成为某个领域的佼佼者。\n\n鸡汤1\n\n弱小的人,才习惯嘲讽与否定,而内心强大的人,从不吝啬赞美与鼓励。\n\n鸡汤2\n\n当代青年人都应该摆脱冷气,只管向上走,不必听从自暴自弃者的流言。能做事的做事,能发声的发声。有一份热,发一份光,就像萤火一般,也可以在黑暗里发一点光,不必等候炬火。", - "normalizedContent": "不知道大家有没有发现,我们身边经常有这样的人,他们越是有能力的,越是有知识的,越是低调,越是谦逊,因为他们深知,知道的越多,不知道的也就越多。\n\n你知道的越多,你不知道的也就越多,这是一句非常有哲理的话。\n\n每个人的知识面都是有限的,你有可能在某个领域会有较深的研究,成为这个领域里的专家,等到你站在高处的时候,才会发现,自己是多么的渺小,才知道自己有多少没有涉及的领域。知道的越多,疑惑、问题就会越来越多,对已知的质疑、疑虑、困惑就会越来越多。\n\n即使如此,我们也应该努力,至少可以成为某个领域的佼佼者。\n\n鸡汤1\n\n弱小的人,才习惯嘲讽与否定,而内心强大的人,从不吝啬赞美与鼓励。\n\n鸡汤2\n\n当代青年人都应该摆脱冷气,只管向上走,不必听从自暴自弃者的流言。能做事的做事,能发声的发声。有一份热,发一份光,就像萤火一般,也可以在黑暗里发一点光,不必等候炬火。", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, - { - "title": "拥抱生活,拥抱快乐", - "frontmatter": { - "title": "拥抱生活,拥抱快乐", - "date": "2020-06-26T20:40:38.000Z", - "permalink": "/pages/cd8bde/", - "sidebar": "auto", - "categories": [ - "随笔" - ], - "tags": [ - "鸡汤" - ], - "author": { - "name": "xugaoyi", - "link": "https://github.com/xugaoyi" - } - }, - "regularPath": "/_posts/%E9%9A%8F%E7%AC%94/%E6%8B%A5%E6%8A%B1%E7%94%9F%E6%B4%BB%EF%BC%8C%E6%8B%A5%E6%8A%B1%E5%BF%AB%E4%B9%90.html", - "relativePath": "_posts/随笔/拥抱生活,拥抱快乐.md", - "key": "v-7d77d69c", - "path": "/pages/cd8bde/", - "excerpt": "

生活在后现代的今天,很多人都有一种虚无感,认为人生没有意义。但是,人生不可能没有意义,因为当你认为没有意义的时候,一定有一个与之相对应的概念叫有意义。

\n", - "headersStr": null, - "content": "生活在后现代的今天,很多人都有一种虚无感,认为人生没有意义。但是,人生不可能没有意义,因为当你认为没有意义的时候,一定有一个与之相对应的概念叫有意义。\n\n当你怀疑人生没有意义时,难道怀疑本身不值得怀疑吗?\n\n不要任由你内心的虚无感蔓延,我们需要去拥抱真实的生活。\n\n所有真实的快乐,都离不开艰辛的努力,无论是金榜题名的快乐,还是事业成功的喜悦,甚至包括洞房花烛的激动。所有真实的快乐,都需要长久的铺垫与努力,没有辛勤的汗水,快乐也就不再真实。\n\n如果快乐触手可及,这种廉价的快乐也就不值得珍惜,随时都可能抛弃。因此,对于年轻人而言,一个重要的功课就是学会去节制欲望。\n\n所有通过捷径所带来的快乐,都是廉价的,以至于所有追求都变得毫无意义,人生就了无生趣。我们需要在每天真实的努力中去拥抱生活,追寻真实的快乐。\n\n\n\n> 文章摘录自:B站视频《罗翔说刑法》,链接https://b23.tv/K8ulrE", - "normalizedContent": "生活在后现代的今天,很多人都有一种虚无感,认为人生没有意义。但是,人生不可能没有意义,因为当你认为没有意义的时候,一定有一个与之相对应的概念叫有意义。\n\n当你怀疑人生没有意义时,难道怀疑本身不值得怀疑吗?\n\n不要任由你内心的虚无感蔓延,我们需要去拥抱真实的生活。\n\n所有真实的快乐,都离不开艰辛的努力,无论是金榜题名的快乐,还是事业成功的喜悦,甚至包括洞房花烛的激动。所有真实的快乐,都需要长久的铺垫与努力,没有辛勤的汗水,快乐也就不再真实。\n\n如果快乐触手可及,这种廉价的快乐也就不值得珍惜,随时都可能抛弃。因此,对于年轻人而言,一个重要的功课就是学会去节制欲望。\n\n所有通过捷径所带来的快乐,都是廉价的,以至于所有追求都变得毫无意义,人生就了无生趣。我们需要在每天真实的努力中去拥抱生活,追寻真实的快乐。\n\n\n\n> 文章摘录自:b站视频《罗翔说刑法》,链接https://b23.tv/k8ulre", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, - { - "title": "我做了一个手写春联小网页,祝大家虎年暴富", - "frontmatter": { - "title": "我做了一个手写春联小网页,祝大家虎年暴富", - "date": "2022-01-28T14:59:51.000Z", - "permalink": "/pages/829589/", - "titleTag": "原创", - "sidebar": "auto", - "categories": [ - "随笔" - ], - "tags": [ - null - ] - }, - "regularPath": "/_posts/%E9%9A%8F%E7%AC%94/%E6%88%91%E5%81%9A%E4%BA%86%E4%B8%80%E4%B8%AA%E6%89%8B%E5%86%99%E6%98%A5%E8%81%94%E5%B0%8F%E7%BD%91%E9%A1%B5%EF%BC%8C%E7%A5%9D%E5%A4%A7%E5%AE%B6%E8%99%8E%E5%B9%B4%E6%9A%B4%E5%AF%8C.html", - "relativePath": "_posts/随笔/我做了一个手写春联小网页,祝大家虎年暴富.md", - "key": "v-a5c5f278", - "path": "/pages/829589/", - "headers": [ - { - "level": 3, - "title": "前言", - "slug": "前言", - "normalizedTitle": "前言", - "charIndex": 33 - }, - { - "level": 3, - "title": "产品构思", - "slug": "产品构思", - "normalizedTitle": "产品构思", - "charIndex": 195 - }, - { - "level": 3, - "title": "设计", - "slug": "设计", - "normalizedTitle": "设计", - "charIndex": 478 - }, - { - "level": 3, - "title": "开发", - "slug": "开发", - "normalizedTitle": "开发", - "charIndex": 487 - } - ], - "excerpt": "

手写春联:https://cl.xugaoyi.com/

\n

# 前言

\n

虎年春节快到了,首先祝大家新年快乐,轻松暴富。\n最近在网上经常看到生成春联的文章,不过这些小demo要么功能简陋,要么UI特别‘程序员’,满足不了我挑剔的眼光。干脆我自己做一个吧,顺便简单体验一下vite+vue3。(因为页面相对简单,vue组件风格还是使用选项式api,重点还是想把产品快速做出来。)

\n", - "headersStr": "前言 产品构思 设计 开发", - "content": "手写春联:https://cl.xugaoyi.com/\n\n\n# 前言\n\n虎年春节快到了,首先祝大家新年快乐,轻松暴富。 最近在网上经常看到生成春联的文章,不过这些小demo要么功能简陋,要么UI特别‘程序员’,满足不了我挑剔的眼光。干脆我自己做一个吧,顺便简单体验一下vite+vue3。(因为页面相对简单,vue组件风格还是使用选项式api,重点还是想把产品快速做出来。)\n\n\n\n\n# 产品构思\n\n包含手写春节和生成春联两大功能:\n\n * 手写春联\n \n * 模拟用笔写字的字迹\n * 选择画笔颜色\n * 调整画笔大小\n * 清空画布\n * 撤回笔画\n * 切换上、下联、横批、福字\n * 随机切换对联提示\n * 预览图片和下载\n * 贴春联海报和下载\n\n * 生成模式\n \n * 选择画笔颜色\n * 挑选生成的对联\n * 输入对联\n * 随机切换对联\n * 贴春联海报和下载\n\n * 其他\n \n * 快速切换模式按钮\n * 可控制的背景音乐\n * 微信分享网页\n\n\n# 设计\n\n\n\n\n# 开发\n\n * 技术栈\n * vite (打包&构建)\n * vue3 (页面开发)\n * vant(ui)\n * sass (css)\n * smooth-signature.js (带笔锋手写库)\n\n\n\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n57\n58\n59\n60\n61\n62\n63\n64\n65\n66\n67\n68\n69\n70\n71\n72\n73\n74\n75\n76\n77\n78\n79\n80\n81\n82\n83\n84\n85\n86\n87\n88\n89\n90\n91\n92\n93\n94\n95\n96\n97\n98\n99\n100\n101\n102\n103\n104\n105\n106\n107\n108\n109\n110\n111\n112\n113\n114\n115\n116\n117\n118\n119\n120\n121\n122\n123\n124\n125\n126\n127\n128\n129\n130\n131\n132\n133\n134\n135\n136\n137\n138\n139\n140\n141\n142\n143\n144\n145\n146\n147\n148\n149\n150\n151\n152\n153\n154\n155\n156\n157\n158\n159\n160\n161\n162\n163\n164\n165\n166\n167\n168\n169\n170\n171\n172\n173\n174\n175\n176\n177\n178\n179\n180\n181\n182\n183\n184\n185\n186\n187\n188\n189\n190\n191\n192\n193\n194\n195\n196\n197\n198\n199\n200\n201\n202\n203\n204\n205\n206\n207\n208\n209\n210\n211\n212\n213\n214\n215\n216\n217\n218\n219\n220\n221\n222\n223\n224\n225\n226\n227\n228\n229\n230\n231\n232\n233\n234\n235\n236\n237\n238\n239\n240\n241\n242\n243\n244\n245\n246\n247\n248\n249\n250\n251\n252\n253\n254\n255\n256\n257\n258\n259\n260\n261\n262\n263\n264\n265\n266\n267\n268\n269\n270\n271\n272\n273\n274\n275\n276\n277\n278\n279\n280\n281\n282\n283\n284\n285\n286\n287\n288\n289\n290\n291\n292\n293\n294\n295\n296\n297\n298\n299\n300\n301\n302\n303\n304\n305\n306\n307\n308\n309\n310\n311\n312\n313\n314\n315\n316\n317\n318\n319\n320\n321\n322\n323\n324\n325\n326\n327\n328\n329\n330\n331\n332\n333\n334\n335\n336\n337\n338\n339\n340\n341\n342\n343\n344\n345\n346\n347\n348\n349\n350\n351\n352\n353\n354\n355\n356\n357\n358\n359\n360\n361\n362\n363\n364\n365\n366\n367\n368\n369\n370\n371\n372\n373\n374\n375\n376\n377\n378\n379\n380\n381\n382\n383\n384\n385\n386\n387\n388\n389\n390\n391\n392\n393\n394\n395\n396\n397\n398\n399\n400\n401\n402\n403\n404\n405\n406\n407\n408\n409\n410\n411\n412\n413\n414\n415\n416\n417\n418\n419\n420\n421\n422\n423\n424\n425\n426\n427\n428\n429\n430\n431\n432\n433\n434\n435\n436\n437\n438\n439\n440\n441\n442\n443\n444\n445\n446\n447\n448\n449\n450\n451\n452\n453\n454\n455\n456\n457\n458\n459\n460\n461\n462\n463\n464\n465\n466\n467\n468\n469\n470\n471\n472\n473\n474\n475\n476\n477\n478\n479\n480\n481\n482\n483\n484\n485\n486\n487\n488\n489\n490\n491\n492\n493\n494\n495\n496\n497\n498\n499\n500\n501\n502\n503\n504\n505\n506\n507\n508\n509\n510\n511\n512\n513\n514\n515\n516\n517\n518\n519\n520\n521\n522\n523\n524\n525\n526\n527\n528\n529\n530\n531\n532\n533\n534\n535\n536\n537\n538\n539\n540\n541\n542\n543\n544\n545\n546\n547\n548\n549\n550\n551\n552\n553\n554\n555\n556\n557\n558\n559\n560\n561\n562\n563\n564\n565\n566\n567\n568\n569\n570\n571\n572\n573\n574\n575\n576\n577\n578\n579\n580\n581\n582\n583\n584\n585\n586\n587\n588\n589\n590\n591\n592\n593\n594\n595\n596\n597\n598\n599\n600\n601\n602\n603\n604\n605\n606\n607\n608\n609\n610\n611\n612\n613\n614\n615\n616\n617\n618\n619\n620\n621\n622\n623\n624\n625\n626\n627\n628\n629\n630\n631\n632\n633\n634\n635\n636\n637\n638\n639\n640\n641\n642\n643\n644\n645\n646\n647\n648\n649\n650\n651\n652\n653\n654\n655\n656\n657\n658\n659\n660\n661\n662\n663\n664\n665\n666\n667\n668\n669\n670\n671\n672\n673\n674\n675\n676\n677\n678\n679\n680\n681\n682\n683\n684\n685\n686\n687\n688\n689\n690\n691\n692\n693\n694\n695\n696\n697\n698\n699\n700\n701\n702\n703\n704\n705\n706\n707\n708\n709\n710\n711\n712\n713\n714\n715\n716\n717\n718\n719\n720\n721\n722\n723\n724\n725\n726\n727\n728\n729\n730\n731\n732\n733\n734\n735\n736\n737\n\n\n更多有趣的小网页欢迎关注公众号有趣研究社:\n\n> 手写春联\n> FC在线模拟器\n> 爱国头像生成器\n> 到账语音生成器", - "normalizedContent": "手写春联:https://cl.xugaoyi.com/\n\n\n# 前言\n\n虎年春节快到了,首先祝大家新年快乐,轻松暴富。 最近在网上经常看到生成春联的文章,不过这些小demo要么功能简陋,要么ui特别‘程序员’,满足不了我挑剔的眼光。干脆我自己做一个吧,顺便简单体验一下vite+vue3。(因为页面相对简单,vue组件风格还是使用选项式api,重点还是想把产品快速做出来。)\n\n\n\n\n# 产品构思\n\n包含手写春节和生成春联两大功能:\n\n * 手写春联\n \n * 模拟用笔写字的字迹\n * 选择画笔颜色\n * 调整画笔大小\n * 清空画布\n * 撤回笔画\n * 切换上、下联、横批、福字\n * 随机切换对联提示\n * 预览图片和下载\n * 贴春联海报和下载\n\n * 生成模式\n \n * 选择画笔颜色\n * 挑选生成的对联\n * 输入对联\n * 随机切换对联\n * 贴春联海报和下载\n\n * 其他\n \n * 快速切换模式按钮\n * 可控制的背景音乐\n * 微信分享网页\n\n\n# 设计\n\n\n\n\n# 开发\n\n * 技术栈\n * vite (打包&构建)\n * vue3 (页面开发)\n * vant(ui)\n * sass (css)\n * smooth-signature.js (带笔锋手写库)\n\n\n\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n57\n58\n59\n60\n61\n62\n63\n64\n65\n66\n67\n68\n69\n70\n71\n72\n73\n74\n75\n76\n77\n78\n79\n80\n81\n82\n83\n84\n85\n86\n87\n88\n89\n90\n91\n92\n93\n94\n95\n96\n97\n98\n99\n100\n101\n102\n103\n104\n105\n106\n107\n108\n109\n110\n111\n112\n113\n114\n115\n116\n117\n118\n119\n120\n121\n122\n123\n124\n125\n126\n127\n128\n129\n130\n131\n132\n133\n134\n135\n136\n137\n138\n139\n140\n141\n142\n143\n144\n145\n146\n147\n148\n149\n150\n151\n152\n153\n154\n155\n156\n157\n158\n159\n160\n161\n162\n163\n164\n165\n166\n167\n168\n169\n170\n171\n172\n173\n174\n175\n176\n177\n178\n179\n180\n181\n182\n183\n184\n185\n186\n187\n188\n189\n190\n191\n192\n193\n194\n195\n196\n197\n198\n199\n200\n201\n202\n203\n204\n205\n206\n207\n208\n209\n210\n211\n212\n213\n214\n215\n216\n217\n218\n219\n220\n221\n222\n223\n224\n225\n226\n227\n228\n229\n230\n231\n232\n233\n234\n235\n236\n237\n238\n239\n240\n241\n242\n243\n244\n245\n246\n247\n248\n249\n250\n251\n252\n253\n254\n255\n256\n257\n258\n259\n260\n261\n262\n263\n264\n265\n266\n267\n268\n269\n270\n271\n272\n273\n274\n275\n276\n277\n278\n279\n280\n281\n282\n283\n284\n285\n286\n287\n288\n289\n290\n291\n292\n293\n294\n295\n296\n297\n298\n299\n300\n301\n302\n303\n304\n305\n306\n307\n308\n309\n310\n311\n312\n313\n314\n315\n316\n317\n318\n319\n320\n321\n322\n323\n324\n325\n326\n327\n328\n329\n330\n331\n332\n333\n334\n335\n336\n337\n338\n339\n340\n341\n342\n343\n344\n345\n346\n347\n348\n349\n350\n351\n352\n353\n354\n355\n356\n357\n358\n359\n360\n361\n362\n363\n364\n365\n366\n367\n368\n369\n370\n371\n372\n373\n374\n375\n376\n377\n378\n379\n380\n381\n382\n383\n384\n385\n386\n387\n388\n389\n390\n391\n392\n393\n394\n395\n396\n397\n398\n399\n400\n401\n402\n403\n404\n405\n406\n407\n408\n409\n410\n411\n412\n413\n414\n415\n416\n417\n418\n419\n420\n421\n422\n423\n424\n425\n426\n427\n428\n429\n430\n431\n432\n433\n434\n435\n436\n437\n438\n439\n440\n441\n442\n443\n444\n445\n446\n447\n448\n449\n450\n451\n452\n453\n454\n455\n456\n457\n458\n459\n460\n461\n462\n463\n464\n465\n466\n467\n468\n469\n470\n471\n472\n473\n474\n475\n476\n477\n478\n479\n480\n481\n482\n483\n484\n485\n486\n487\n488\n489\n490\n491\n492\n493\n494\n495\n496\n497\n498\n499\n500\n501\n502\n503\n504\n505\n506\n507\n508\n509\n510\n511\n512\n513\n514\n515\n516\n517\n518\n519\n520\n521\n522\n523\n524\n525\n526\n527\n528\n529\n530\n531\n532\n533\n534\n535\n536\n537\n538\n539\n540\n541\n542\n543\n544\n545\n546\n547\n548\n549\n550\n551\n552\n553\n554\n555\n556\n557\n558\n559\n560\n561\n562\n563\n564\n565\n566\n567\n568\n569\n570\n571\n572\n573\n574\n575\n576\n577\n578\n579\n580\n581\n582\n583\n584\n585\n586\n587\n588\n589\n590\n591\n592\n593\n594\n595\n596\n597\n598\n599\n600\n601\n602\n603\n604\n605\n606\n607\n608\n609\n610\n611\n612\n613\n614\n615\n616\n617\n618\n619\n620\n621\n622\n623\n624\n625\n626\n627\n628\n629\n630\n631\n632\n633\n634\n635\n636\n637\n638\n639\n640\n641\n642\n643\n644\n645\n646\n647\n648\n649\n650\n651\n652\n653\n654\n655\n656\n657\n658\n659\n660\n661\n662\n663\n664\n665\n666\n667\n668\n669\n670\n671\n672\n673\n674\n675\n676\n677\n678\n679\n680\n681\n682\n683\n684\n685\n686\n687\n688\n689\n690\n691\n692\n693\n694\n695\n696\n697\n698\n699\n700\n701\n702\n703\n704\n705\n706\n707\n708\n709\n710\n711\n712\n713\n714\n715\n716\n717\n718\n719\n720\n721\n722\n723\n724\n725\n726\n727\n728\n729\n730\n731\n732\n733\n734\n735\n736\n737\n\n\n更多有趣的小网页欢迎关注公众号有趣研究社:\n\n> 手写春联\n> fc在线模拟器\n> 爱国头像生成器\n> 到账语音生成器", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 + "lastUpdated": "2022/06/20, 22:47:41", + "lastUpdatedTimestamp": 1655736461000 }, { "title": "分类", @@ -6229,25 +6070,6 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, - { - "title": "归档", - "frontmatter": { - "archivesPage": true, - "title": "归档", - "permalink": "/archives/", - "article": false - }, - "regularPath": "/@pages/archivesPage.html", - "relativePath": "@pages/archivesPage.md", - "key": "v-48f7eec0", - "path": "/archives/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, { "title": "标签", "frontmatter": { @@ -6310,31 +6132,31 @@ export const siteData = { "title": "🎉上新推荐", "slug": "🎉上新推荐", "normalizedTitle": "🎉上新推荐", - "charIndex": 696 + "charIndex": 723 }, { "level": 2, "title": "⚡️未来...", "slug": "⚡️未来", "normalizedTitle": "⚡️未来...", - "charIndex": 748 + "charIndex": 775 }, { "level": 2, "title": "⚡ 反馈与交流", "slug": "⚡-反馈与交流", "normalizedTitle": "⚡ 反馈与交流", - "charIndex": 812 + "charIndex": 839 } ], "headersStr": "🎖友情链接 🎉上新推荐 ⚡️未来... ⚡ 反馈与交流", - "content": "支持这个项目\n\n\n# 🎖友情链接\n\nOpenHarmony\n\n开放原子开源基金会\n\nMyBatis-Plus官网\n\n🚀为简化开发而生\n\nDeepin 社区\n\nDeepin 应用开发技术分享、DTK开发经验等\n\nVForm官网\n\n低代码表单优选方案,拖拽式设计,一键生成源码\n\n- name: OpenHarmony\n desc: 开放原子开源基金会\n link: https://docs.openharmony.cn/pages/000000/\n bgColor: '#f1f1f1'\n textColor: '#2A3344'\n- name: MyBatis-Plus官网\n desc: 🚀为简化开发而生\n link: https://baomidou.com/\n bgColor: '#f1f1f1'\n textColor: '#2A3344'\n- name: Deepin 社区\n desc: Deepin 应用开发技术分享、DTK开发经验等\n link: https://docs.deepin.org\n bgColor: '#f1f1f1'\n textColor: '#2A3344'\n- name: VForm官网\n desc: 低代码表单优选方案,拖拽式设计,一键生成源码\n link: http://www.vform666.com\n bgColor: '#f1f1f1'\n textColor: '#2A3344'\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\n\n\n# 🎉上新推荐\n\n * v1.1.x:第一个版本,快去 看看 吧~\n\n更多上新请查阅:更新日志\n\n\n\n# ⚡️未来...\n\n提示\n\n期待 更多Java领域的面试题正式发布...\n\n还希望大家多多 💖支持 哟,持续关注吧~\n\n\n\n# ⚡ 反馈与交流\n\n在使用过程中有任何问题和想法,你可以:\n\n * 给我提 Issue。\n\n你也可以在Issue查看别人提的问题和给出解决方案。\n\n * 戳JavaDriver社区\n\n", - "normalizedContent": "支持这个项目\n\n\n# 🎖友情链接\n\nopenharmony\n\n开放原子开源基金会\n\nmybatis-plus官网\n\n🚀为简化开发而生\n\ndeepin 社区\n\ndeepin 应用开发技术分享、dtk开发经验等\n\nvform官网\n\n低代码表单优选方案,拖拽式设计,一键生成源码\n\n- name: openharmony\n desc: 开放原子开源基金会\n link: https://docs.openharmony.cn/pages/000000/\n bgcolor: '#f1f1f1'\n textcolor: '#2a3344'\n- name: mybatis-plus官网\n desc: 🚀为简化开发而生\n link: https://baomidou.com/\n bgcolor: '#f1f1f1'\n textcolor: '#2a3344'\n- name: deepin 社区\n desc: deepin 应用开发技术分享、dtk开发经验等\n link: https://docs.deepin.org\n bgcolor: '#f1f1f1'\n textcolor: '#2a3344'\n- name: vform官网\n desc: 低代码表单优选方案,拖拽式设计,一键生成源码\n link: http://www.vform666.com\n bgcolor: '#f1f1f1'\n textcolor: '#2a3344'\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\n\n\n# 🎉上新推荐\n\n * v1.1.x:第一个版本,快去 看看 吧~\n\n更多上新请查阅:更新日志\n\n\n\n# ⚡️未来...\n\n提示\n\n期待 更多java领域的面试题正式发布...\n\n还希望大家多多 💖支持 哟,持续关注吧~\n\n\n\n# ⚡ 反馈与交流\n\n在使用过程中有任何问题和想法,你可以:\n\n * 给我提 issue。\n\n你也可以在issue查看别人提的问题和给出解决方案。\n\n * 戳javadriver社区\n\n", + "content": "支持这个项目\n\n\n# 🎖友情链接\n\nOpenHarmony\n\n开放原子开源基金会\n\nMyBatis-Plus官网\n\n🚀为简化开发而生\n\nvuepress-theme-vdoing\n\n优秀的vuepress主题\n\nVForm官网\n\n低代码表单优选方案,拖拽式设计,一键生成源码\n\n- name: OpenHarmony\n desc: 开放原子开源基金会\n link: https://docs.openharmony.cn/pages/000000/\n bgColor: '#f1f1f1'\n textColor: '#2A3344'\n- name: MyBatis-Plus官网\n desc: 🚀为简化开发而生\n link: https://baomidou.com/\n bgColor: '#f1f1f1'\n textColor: '#2A3344'\n- name: vuepress-theme-vdoing\n desc: 优秀的vuepress主题\n link: https://github.com/xugaoyi/vuepress-theme-vdoing\n bgColor: '#f1f1f1'\n textColor: '#2A3344'\n- name: VForm官网\n desc: 低代码表单优选方案,拖拽式设计,一键生成源码\n link: http://www.vform666.com\n bgColor: '#f1f1f1'\n textColor: '#2A3344'\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\n\n\n# 🎉上新推荐\n\n * v1.1.x:第一个版本,快去 看看 吧~\n\n更多上新请查阅:更新日志\n\n\n\n# ⚡️未来...\n\n提示\n\n期待 更多Java领域的面试题正式发布...\n\n还希望大家多多 💖支持 哟,持续关注吧~\n\n\n\n# ⚡ 反馈与交流\n\n在使用过程中有任何问题和想法,你可以:\n\n * 给我提 Issue。\n\n你也可以在Issue查看别人提的问题和给出解决方案。\n\n * 戳JavaDriver社区\n\n", + "normalizedContent": "支持这个项目\n\n\n# 🎖友情链接\n\nopenharmony\n\n开放原子开源基金会\n\nmybatis-plus官网\n\n🚀为简化开发而生\n\nvuepress-theme-vdoing\n\n优秀的vuepress主题\n\nvform官网\n\n低代码表单优选方案,拖拽式设计,一键生成源码\n\n- name: openharmony\n desc: 开放原子开源基金会\n link: https://docs.openharmony.cn/pages/000000/\n bgcolor: '#f1f1f1'\n textcolor: '#2a3344'\n- name: mybatis-plus官网\n desc: 🚀为简化开发而生\n link: https://baomidou.com/\n bgcolor: '#f1f1f1'\n textcolor: '#2a3344'\n- name: vuepress-theme-vdoing\n desc: 优秀的vuepress主题\n link: https://github.com/xugaoyi/vuepress-theme-vdoing\n bgcolor: '#f1f1f1'\n textcolor: '#2a3344'\n- name: vform官网\n desc: 低代码表单优选方案,拖拽式设计,一键生成源码\n link: http://www.vform666.com\n bgcolor: '#f1f1f1'\n textcolor: '#2a3344'\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\n\n\n# 🎉上新推荐\n\n * v1.1.x:第一个版本,快去 看看 吧~\n\n更多上新请查阅:更新日志\n\n\n\n# ⚡️未来...\n\n提示\n\n期待 更多java领域的面试题正式发布...\n\n还希望大家多多 💖支持 哟,持续关注吧~\n\n\n\n# ⚡ 反馈与交流\n\n在使用过程中有任何问题和想法,你可以:\n\n * 给我提 issue。\n\n你也可以在issue查看别人提的问题和给出解决方案。\n\n * 戳javadriver社区\n\n", "charsets": { "cjk": true }, - "lastUpdated": "2022/05/20, 20:19:56", - "lastUpdatedTimestamp": 1653049196000 + "lastUpdated": "2022/06/20, 22:47:41", + "lastUpdatedTimestamp": 1655736461000 } ], "themeConfig": { @@ -6797,44 +6619,44 @@ export const siteData = { ], "/02.计算机基础/": [ { - "title": "计算机网络.HTML", + "title": "计算机网络", "collapsable": true, "children": [ [ - "10.计算机网络.HTML/01.计算机网络.md", + "10.计算机网络/01.计算机网络.md", "计算机网络", "/pages/e00207/" ] ] }, { - "title": "操作系统.CSS", + "title": "操作系统", "collapsable": true, "children": [ [ - "20.操作系统.CSS/01.操作系统.md", + "20.操作系统/01.操作系统.md", "操作系统", "/pages/e5f2d7/" ] ] }, { - "title": "数据结构.CSS", + "title": "数据结构", "collapsable": true, "children": [ [ - "30.数据结构.CSS/01.数据结构.md", + "30.数据结构/01.数据结构.md", "数据结构", "/pages/a25c3a/" ] ] }, { - "title": "算法.CSS", + "title": "算法", "collapsable": true, "children": [ [ - "40.算法.CSS/01.算法.md", + "40.算法/01.算法.md", "算法", "/pages/92288a/" ] @@ -7077,28 +6899,6 @@ export const siteData = { ] ] }, - { - "title": "Spring", - "collapsable": true, - "children": [ - [ - "20.Spring/01.Spring.md", - "Spring", - "/pages/434235/" - ] - ] - }, - { - "title": "Mybatis", - "collapsable": true, - "children": [ - [ - "30.Mybatis/01.Mybatis.md", - "Mybatis", - "/pages/3793a5/" - ] - ] - }, { "title": "Kafka", "collapsable": true, @@ -7129,17 +6929,6 @@ export const siteData = { "/pages/76a94c/" ] ] - }, - { - "title": "Dubbo", - "collapsable": true, - "children": [ - [ - "50.Dubbo/01.Dubbo.md", - "Dubbo", - "/pages/78517c/" - ] - ] } ], "/08.系统设计/": [ @@ -7172,18 +6961,6 @@ export const siteData = { "关于", "/about" ] - ], - "/20.收藏夹/": [ - [ - "01.网站.md", - "网站", - "/pages/beb6c0bd8a66cea6" - ], - [ - "02.常用的前端轮子.md", - "常用的前端轮子", - "/pages/47cf96/" - ] ] }, "author": { From 4e473939dafb6930489a19ddbb63b317c5d93537 Mon Sep 17 00:00:00 2001 From: admirean <854818303@qq.com> Date: Sun, 18 Sep 2022 21:28:02 +0800 Subject: [PATCH 11/15] =?UTF-8?q?add=20=E7=AB=A0=E8=8A=82=20java=E7=9B=B8?= =?UTF-8?q?=E5=85=B3-JVM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...01\345\233\236\346\224\266\357\274\237.md" | 107 + ...11\345\223\252\344\272\233\357\274\237.md" | 10 - ...11\345\223\252\344\272\233\357\274\237.md" | 141 + ...C \350\260\203\344\274\230\357\274\237.md" | 84 + ...15\346\216\222\345\272\217\357\274\237.md" | 74 + ...05\345\255\230\347\232\204\357\274\237.md" | 53 + ...14\346\214\207\344\273\244\357\274\237.md" | 33 + ...30\346\263\204\346\274\217\357\274\237.md" | 169 + ...66\347\232\204\345\214\272\345\210\253.md" | 91 + ...27\346\234\272\347\275\221\347\273\234.md" | 2 +- ...01\347\232\204\345\220\253\344\271\211.md" | 32 + ...04\345\205\250\350\277\207\347\250\213.md" | 83 + .../core/.temp/internal/page-components.js | 73 +- .../@vuepress/core/.temp/internal/routes.js | 822 ++--- .../@vuepress/core/.temp/internal/siteData.js | 3087 ++++++++++------- 15 files changed, 3200 insertions(+), 1661 deletions(-) create mode 100644 "docs/01.Java\347\233\270\345\205\263/30.JVM/001.Java \344\270\255\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266\344\270\255\345\246\202\344\275\225\345\210\244\346\226\255\345\257\271\350\261\241\351\234\200\350\246\201\345\233\236\346\224\266\357\274\237.md" delete mode 100644 "docs/01.Java\347\233\270\345\205\263/30.JVM/001.Java \344\270\255\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266\344\270\255\345\246\202\344\275\225\345\210\244\346\226\255\345\257\271\350\261\241\351\234\200\350\246\201\345\233\236\346\224\266\357\274\237\345\270\270\350\247\201\347\232\204 GC \345\233\236\346\224\266\347\256\227\346\263\225\346\234\211\345\223\252\344\272\233\357\274\237.md" create mode 100644 "docs/01.Java\347\233\270\345\205\263/30.JVM/010.\345\270\270\350\247\201\347\232\204 GC \345\233\236\346\224\266\347\256\227\346\263\225\346\234\211\345\223\252\344\272\233\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/010.\347\256\200\350\277\260\345\270\270\350\247\201\347\232\204 HTTP \347\212\266\346\200\201\347\240\201\347\232\204\345\220\253\344\271\211.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/020.\344\273\216\350\276\223\345\205\245 URL \345\210\260\345\261\225\347\216\260\351\241\265\351\235\242\347\232\204\345\205\250\350\277\207\347\250\213.md" diff --git "a/docs/01.Java\347\233\270\345\205\263/30.JVM/001.Java \344\270\255\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266\344\270\255\345\246\202\344\275\225\345\210\244\346\226\255\345\257\271\350\261\241\351\234\200\350\246\201\345\233\236\346\224\266\357\274\237.md" "b/docs/01.Java\347\233\270\345\205\263/30.JVM/001.Java \344\270\255\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266\344\270\255\345\246\202\344\275\225\345\210\244\346\226\255\345\257\271\350\261\241\351\234\200\350\246\201\345\233\236\346\224\266\357\274\237.md" new file mode 100644 index 000000000..70ba53364 --- /dev/null +++ "b/docs/01.Java\347\233\270\345\205\263/30.JVM/001.Java \344\270\255\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266\344\270\255\345\246\202\344\275\225\345\210\244\346\226\255\345\257\271\350\261\241\351\234\200\350\246\201\345\233\236\346\224\266\357\274\237.md" @@ -0,0 +1,107 @@ +--- +title: Java 中垃圾回收机制中如何判断对象需要回收? +date: 2022-05-21 17:31:41 +permalink: /pages/948850/ +categories: + - Java相关 + - JVM +tags: + - +--- +Java 中垃圾回收机制中如何判断对象需要回收? +即死亡对象判断方法 +堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。 + +## 引用计数法 +给对象中添加一个引用计数器: + +每当有一个地方引用它,计数器就加 1; +当引用失效,计数器就减 1; +任何时候计数器为 0 的对象就是不可能再被使用的。 +这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。 + +所谓对象之间的相互引用问题,如下面代码所示:除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。 +```java +public class ReferenceCountingGc { + Object instance = null; + public static void main(String[] args) { + ReferenceCountingGc objA = new ReferenceCountingGc(); + ReferenceCountingGc objB = new ReferenceCountingGc(); + objA.instance = objB; + objB.instance = objA; + objA = null; + objB = null; + } +} +``` + +## 可达性分析算法 +这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。 + +下图中的 Object 6 ~ Object 10 之间虽有引用关系,但它们到 GC Roots 不可达,因此为需要被回收的对象。 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6aygyumajj218s0u0mzp.jpg) + +哪些对象可以作为 GC Roots 呢? + +虚拟机栈(栈帧中的本地变量表)中引用的对象 +本地方法栈(Native 方法)中引用的对象 +方法区中类静态属性引用的对象 +方法区中常量引用的对象 +所有被同步锁持有的对象 +对象可以被回收,就代表一定会被回收吗? + +即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 'finalize' 方法。当对象没有覆盖 'finalize' 方法,或 'finalize' 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。 + +被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。 + +>Object 类中的 finalize 方法一直被认为是一个糟糕的设计,成为了 Java 语言的负担,影响了 Java 语言的安全和 GC 的性能。JDK9 版本及后续版本中各个类中的 finalize 方法会被逐渐弃用移除。忘掉它的存在吧! + + +## 引用类型总结 +无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。 + +JDK1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。 + +JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱) + +1.强引用(StrongReference) + +以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。 + +2.软引用(SoftReference) + +如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 + +软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。 + +3.弱引用(WeakReference) + +如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 + +弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。 + +4.虚引用(PhantomReference) + +"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。 + +虚引用主要用来跟踪对象被垃圾回收的活动。 + +虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 + +特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。 + + +## 如何判断一个常量是废弃常量? +运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢? + +假如在字符串常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被系统清理出常量池了。 + +## 如何判断一个类是无用的类 +方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢? + +判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” : + +* 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。 +* 加载该类的 ClassLoader 已经被回收。 +* 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 +虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。 \ No newline at end of file diff --git "a/docs/01.Java\347\233\270\345\205\263/30.JVM/001.Java \344\270\255\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266\344\270\255\345\246\202\344\275\225\345\210\244\346\226\255\345\257\271\350\261\241\351\234\200\350\246\201\345\233\236\346\224\266\357\274\237\345\270\270\350\247\201\347\232\204 GC \345\233\236\346\224\266\347\256\227\346\263\225\346\234\211\345\223\252\344\272\233\357\274\237.md" "b/docs/01.Java\347\233\270\345\205\263/30.JVM/001.Java \344\270\255\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266\344\270\255\345\246\202\344\275\225\345\210\244\346\226\255\345\257\271\350\261\241\351\234\200\350\246\201\345\233\236\346\224\266\357\274\237\345\270\270\350\247\201\347\232\204 GC \345\233\236\346\224\266\347\256\227\346\263\225\346\234\211\345\223\252\344\272\233\357\274\237.md" deleted file mode 100644 index db473f03e..000000000 --- "a/docs/01.Java\347\233\270\345\205\263/30.JVM/001.Java \344\270\255\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266\344\270\255\345\246\202\344\275\225\345\210\244\346\226\255\345\257\271\350\261\241\351\234\200\350\246\201\345\233\236\346\224\266\357\274\237\345\270\270\350\247\201\347\232\204 GC \345\233\236\346\224\266\347\256\227\346\263\225\346\234\211\345\223\252\344\272\233\357\274\237.md" +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: Java 中垃圾回收机制中如何判断对象需要回收?常见的 GC 回收算法有哪些? -date: 2022-05-21 17:31:41 -permalink: /pages/948850/ -categories: - - Java相关 - - JVM -tags: - - ---- diff --git "a/docs/01.Java\347\233\270\345\205\263/30.JVM/010.\345\270\270\350\247\201\347\232\204 GC \345\233\236\346\224\266\347\256\227\346\263\225\346\234\211\345\223\252\344\272\233\357\274\237.md" "b/docs/01.Java\347\233\270\345\205\263/30.JVM/010.\345\270\270\350\247\201\347\232\204 GC \345\233\236\346\224\266\347\256\227\346\263\225\346\234\211\345\223\252\344\272\233\357\274\237.md" new file mode 100644 index 000000000..f5c74f19b --- /dev/null +++ "b/docs/01.Java\347\233\270\345\205\263/30.JVM/010.\345\270\270\350\247\201\347\232\204 GC \345\233\236\346\224\266\347\256\227\346\263\225\346\234\211\345\223\252\344\272\233\357\274\237.md" @@ -0,0 +1,141 @@ +--- +title: 常见的 GC 回收算法有哪些? +date: 2022-09-18 19:01:29 +permalink: /pages/9b26e8/ +categories: + - Java相关 + - JVM +tags: + - +--- +## 标记-清除算法 +该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题: + +效率问题 +空间问题(标记清除后会产生大量不连续的碎片) +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6aysggm1gj20ny0jpmxv.jpg) + +## 标记-复制算法 +为了解决效率问题,“标记-复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6ayt1s4lbj20ny0jp74f.jpg) + +## 标记-整理算法 +根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6aytfwnjbj20hs09hdg0.jpg) + +## 分代收集算法 +当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。 + +比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。 + +延伸面试问题: HotSpot 为什么要分为新生代和老年代? + +根据上面的对分代收集算法的介绍回答。 + +## 垃圾收集器 +如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。 + +虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的 HotSpot 虚拟机就不会实现那么多不同的垃圾收集器了。 + +## Serial 收集器 +Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。 + +新生代采用标记-复制算法,老年代采用标记-整理算法。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6aytsqyaxj20a205rt8o.jpg) + +虚拟机的设计者们当然知道 Stop The World 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。 + +但是 Serial 收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。 + +## ParNew 收集器 +ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。 + +新生代采用标记-复制算法,老年代采用标记-整理算法。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6ayu4tt15j20bx07974b.jpg) + +它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。 + +并行和并发概念补充: + +并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。 + +并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。 + +## Parallel Scavenge 收集器 +Parallel Scavenge 收集器也是使用标记-复制算法的多线程收集器,它看上去几乎和 ParNew 都一样。 那么它有什么特别之处呢? + +```xml +-XX:+UseParallelGC + + 使用 Parallel 收集器+ 老年代串行 + +-XX:+UseParallelOldGC + + 使用 Parallel 收集器+ 老年代并行 +``` + +Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。 Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。 + +新生代采用标记-复制算法,老年代采用标记-整理算法。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6ayuvquk9j20bx07974b.jpg) + +这是 JDK1.8 默认收集器 + +使用 java -XX:+PrintCommandLineFlags -version 命令查看 + +```xml +-XX:InitialHeapSize=262921408 -XX:MaxHeapSize=4206742528 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC +java version "1.8.0_211" +Java(TM) SE Runtime Environment (build 1.8.0_211-b12) +Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode) +JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC 参数,则默认指定了-XX:+UseParallelOldGC,可以使用-XX:-UseParallelOldGC 来禁用该功能 +``` +## Serial Old 收集器 +Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。 + +## Parallel Old 收集器 +Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。 + +## CMS 收集器 +CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。 + +CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。 + +从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤: + +初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ; +并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。 +重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短 +并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6ayvgixf4j20gl07tmxh.jpg) + +从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点: + +对 CPU 资源敏感; +无法处理浮动垃圾; +它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。 +## G1 收集器 +G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征. + +被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备以下特点: + +并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。 +分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。 +空间整合:与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。 +可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。 +G1 收集器的运作大致分为以下几个步骤: + +初始标记 +并发标记 +最终标记 +筛选回收 +G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。 + +## ZGC 收集器 +与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。 + +在 ZGC 中出现 Stop The World 的情况会更少! +详情可以看 : [《新一代垃圾回收器 ZGC 的探索与实践》](https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html) \ No newline at end of file diff --git "a/docs/01.Java\347\233\270\345\205\263/30.JVM/020.Java \344\270\255\345\246\202\344\275\225\350\277\233\350\241\214 GC \350\260\203\344\274\230\357\274\237.md" "b/docs/01.Java\347\233\270\345\205\263/30.JVM/020.Java \344\270\255\345\246\202\344\275\225\350\277\233\350\241\214 GC \350\260\203\344\274\230\357\274\237.md" index e0243e675..b1485c139 100644 --- "a/docs/01.Java\347\233\270\345\205\263/30.JVM/020.Java \344\270\255\345\246\202\344\275\225\350\277\233\350\241\214 GC \350\260\203\344\274\230\357\274\237.md" +++ "b/docs/01.Java\347\233\270\345\205\263/30.JVM/020.Java \344\270\255\345\246\202\344\275\225\350\277\233\350\241\214 GC \350\260\203\344\274\230\357\274\237.md" @@ -8,3 +8,87 @@ categories: tags: - --- +当Java程序性能达不到既定目标,且其他优化手段都已经穷尽时,通常需要调整垃圾回收器来进一步提高性能,称为GC优化。 +但GC算法复杂,影响GC性能的参数众多,且参数调整又依赖于应用各自的特点,这些因素很大程度上增加了GC优化的难度。 + +即便如此,GC调优也不是无章可循,仍然有一些通用的思考方法。 + +本篇会介绍这些通用的GC优化策略和相关实践案例,主要包括如下内容: + +>优化前准备: 简单回顾JVM相关知识、介绍GC优化的一些通用策略。
+> 优化方法: 介绍调优的一般流程:明确优化目标→优化→跟踪优化结果。
+> 优化案例: 简述笔者所在团队遇到的GC问题以及优化方案。
+ +# 一、优化前的准备 +## GC优化需知 +为了更好地理解本篇所介绍的内容,你需要了解如下内容。 1. GC相关基础知识,包括但不限于: a) GC工作原理。 b) 理解新生代、老年代、晋升等术语含义。 c) 可以看懂GC日志。 + +GC优化不能解决一切性能问题,它是最后的调优手段。 +如果对第一点中提及的知识点不是很熟悉,可以先阅读小结-JVM基础回顾;如果已经很熟悉,可以跳过该节直接往下阅读。 + +# JVM基础回顾 +## JVM内存结构 +简单介绍一下JVM内存结构和常见的垃圾回收器。 + +当代主流虚拟机(Hotspot VM)的垃圾回收都采用“分代回收”的算法。“分代回收”是基于这样一个事实:对象的生命周期不同,所以针对不同生命周期的对象可以采取不同的回收方式,以便提高回收效率。 + +Hotspot VM将内存划分为不同的物理区,就是“分代”思想的体现。如图所示,JVM内存主要由新生代、老年代、永久代构成。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6az33lnh9j20fy091mx9.jpg) + +① 新生代(Young Generation):大多数对象在新生代中被创建,其中很多对象的生命周期很短。每次新生代的垃圾回收(又称Minor GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。 + +新生代内又分三个区:一个Eden区,两个Survivor区(一般而言),大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到两个Survivor区(中的一个)。当这个Survivor区满时,此区的存活且不满足“晋升”条件的对象将被复制到另外一个Survivor区。对象每经历一次Minor GC,年龄加1,达到“晋升年龄阈值”后,被放到老年代,这个过程也称为“晋升”。显然,“晋升年龄阈值”的大小直接影响着对象在新生代中的停留时间,在Serial和ParNew GC两种回收器中,“晋升年龄阈值”通过参数MaxTenuringThreshold设定,默认值为15。 + +② 老年代(Old Generation):在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代,该区域中对象存活率高。老年代的垃圾回收(又称Major GC)通常使用“标记-清理”或“标记-整理”算法。整堆包括新生代和老年代的垃圾回收称为Full GC(HotSpot VM里,除了CMS之外,其它能收集老年代的GC都会同时收集整个GC堆,包括新生代)。 + +③ 永久代(Perm Generation):主要存放元数据,例如Class、Method的元信息,与垃圾回收要回收的Java对象关系不大。相对于新生代和年老代来说,该区域的划分对垃圾回收影响比较小。 + +## 常见垃圾回收器 +不同的垃圾回收器,适用于不同的场景。常用的垃圾回收器: + +串行(Serial)回收器是单线程的一个回收器,简单、易实现、效率高。 +并行(ParNew)回收器是Serial的多线程版,可以充分的利用CPU资源,减少回收的时间。 +吞吐量优先(Parallel Scavenge)回收器,侧重于吞吐量的控制。 +并发标记清除(CMS,Concurrent Mark Sweep)回收器是一种以获取最短回收停顿时间为目标的回收器,该回收器是基于“标记-清除”算法实现的。 +GC日志 +每一种回收器的日志格式都是由其自身的实现决定的,换而言之,每种回收器的日志格式都可以不一样。但虚拟机设计者为了方便用户阅读,将各个回收器的日志都维持一定的共性。JavaGC日志 中简单介绍了这些共性。 + +## 参数基本策略 +各分区的大小对GC的性能影响很大。如何将各分区调整到合适的大小,分析活跃数据的大小是很好的切入点。 + +活跃数据的大小是指,应用程序稳定运行时长期存活对象在堆中占用的空间大小,也就是Full GC后堆中老年代占用空间的大小。可以通过GC日志中Full GC之后老年代数据大小得出,比较准确的方法是在程序稳定后,多次获取GC数据,通过取平均值的方式计算活跃数据的大小。活跃数据和各分区之间的比例关系如下(见参考文献1): +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6az3mfu0xj20ha09udgq.jpg) + +例如,根据GC日志获得老年代的活跃数据大小为300M,那么各分区大小可以设为: + +总堆:1200MB = 300MB × 4* 新生代:450MB = 300MB × 1.5* 老年代: 750MB = 1200MB - 450MB* + +这部分设置仅仅是堆大小的初始值,后面的优化中,可能会调整这些值,具体情况取决于应用程序的特性和需求。 + +# 二、优化步骤 +GC优化一般步骤可以概括为:确定目标、优化参数、验收结果。 + +## 确定目标 +明确应用程序的系统需求是性能优化的基础,系统的需求是指应用程序运行时某方面的要求,譬如: - 高可用,可用性达到几个9。 - 低延迟,请求必须多少毫秒内完成响应。 - 高吞吐,每秒完成多少次事务。 + +明确系统需求之所以重要,是因为上述性能指标间可能冲突。比如通常情况下,缩小延迟的代价是降低吞吐量或者消耗更多的内存或者两者同时发生。 + +由于笔者所在团队主要关注高可用和低延迟两项指标,所以接下来分析,如何量化GC时间和频率对于响应时间和可用性的影响。通过这个量化指标,可以计算出当前GC情况对服务的影响,也能评估出GC优化后对响应时间的收益,这两点对于低延迟服务很重要。 + +举例:假设单位时间T内发生一次持续25ms的GC,接口平均响应时间为50ms,且请求均匀到达,根据下图所示: + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6az42xfzzj22320fsq5h.jpg) + +那么有(50ms+25ms)/T比例的请求会受GC影响,其中GC前的50ms内到达的请求都会增加25ms,GC期间的25ms内到达的请求,会增加0-25ms不等,如果时间T内发生N次GC,受GC影响请求占比=(接口响应时间+GC时间)×N/T 。可见无论降低单次GC时间还是降低GC次数N都可以有效减少GC对响应时间的影响。 + +## 优化 +通过收集GC信息,结合系统需求,确定优化方案,例如选用合适的GC回收器、重新设置内存比例、调整JVM参数等。 + +进行调整后,将不同的优化方案分别应用到多台机器上,然后比较这些机器上GC的性能差异,有针对性的做出选择,再通过不断的试验和观察,找到最合适的参数。 + +## 验收优化结果 +将修改应用到所有服务器,判断优化结果是否符合预期,总结相关经验。 + + +参考:[美团技术团队-从实际案例聊聊Java应用的GC优化](https://tech.meituan.com/2017/12/29/jvm-optimize.html) \ No newline at end of file diff --git "a/docs/01.Java\347\233\270\345\205\263/30.JVM/030.JMM \344\270\255\345\206\205\345\255\230\346\250\241\345\236\213\346\230\257\346\200\216\346\240\267\347\232\204\357\274\237\344\273\200\344\271\210\346\230\257\346\214\207\344\273\244\345\272\217\345\210\227\351\207\215\346\216\222\345\272\217\357\274\237.md" "b/docs/01.Java\347\233\270\345\205\263/30.JVM/030.JMM \344\270\255\345\206\205\345\255\230\346\250\241\345\236\213\346\230\257\346\200\216\346\240\267\347\232\204\357\274\237\344\273\200\344\271\210\346\230\257\346\214\207\344\273\244\345\272\217\345\210\227\351\207\215\346\216\222\345\272\217\357\274\237.md" index cf5adfb50..67ef47f0d 100644 --- "a/docs/01.Java\347\233\270\345\205\263/30.JVM/030.JMM \344\270\255\345\206\205\345\255\230\346\250\241\345\236\213\346\230\257\346\200\216\346\240\267\347\232\204\357\274\237\344\273\200\344\271\210\346\230\257\346\214\207\344\273\244\345\272\217\345\210\227\351\207\215\346\216\222\345\272\217\357\274\237.md" +++ "b/docs/01.Java\347\233\270\345\205\263/30.JVM/030.JMM \344\270\255\345\206\205\345\255\230\346\250\241\345\236\213\346\230\257\346\200\216\346\240\267\347\232\204\357\274\237\344\273\200\344\271\210\346\230\257\346\214\207\344\273\244\345\272\217\345\210\227\351\207\215\346\216\222\345\272\217\357\274\237.md" @@ -8,3 +8,77 @@ categories: tags: - --- +Java内存模型(JMM)和 Java 运行时内存区域是不一样的概念。 +## 为什么要有内存模型? + +要想回答这个问题,我们需要先弄懂传统计算机硬件内存架构。好了,我要开始画图了。 + +## 硬件内存架构 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6azbc9p50j20hs088q3e.jpg) + +(1)CPU + +去过机房的同学都知道,一般在大型服务器上会配置多个CPU,每个CPU还会有多个 + +核 + +,这就意味着多个CPU或者多个核可以同时(并发)工作。如果使用Java 起了一个多线程的任务,很有可能每个 CPU 都会跑一个线程,那么你的任务在某一刻就是真正并发执行了。 + +(2)CPU Register + +CPU Register也就是 CPU 寄存器。CPU 寄存器是 CPU 内部集成的,在寄存器上执行操作的效率要比在主存上高出几个数量级。 + +(3)CPU Cache Memory + +CPU Cache Memory也就是 CPU 高速缓存,相对于寄存器来说,通常也可以成为 L2 二级缓存。相对于硬盘读取速度来说内存读取的效率非常高,但是与 CPU 还是相差数量级,所以在 CPU 和主存间引入了多级缓存,目的是为了做一下缓冲。 + +(4)Main Memory + +Main Memory 就是主存,主存比 L1、L2 缓存要大很多。 + +注意:部分高端机器还有 L3 三级缓存。 + +## 缓存一致性问题 + +由于主存与 CPU 处理器的运算能力之间有数量级的差距,所以在传统计算机内存架构中会引入高速缓存来作为主存和处理器之间的缓冲,CPU 将常用的数据放在高速缓存中,运算结束后 CPU 再讲运算结果同步到主存中。 + +使用高速缓存解决了 CPU 和主存速率不匹配的问题,但同时又引入另外一个新问题:缓存一致性问题。 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6azdkmebrj20hs0jzq3r.jpg) + +在多CPU的系统中(或者单CPU多核的系统),每个CPU内核都有自己的高速缓存,它们共享同一主内存(Main Memory)。当多个CPU的运算任务都涉及同一块主内存区域时,CPU 会将数据读取到缓存中进行运算,这可能会导致各自的缓存数据不一致。 + +因此需要每个 CPU 访问缓存时遵循一定的协议,在读写数据时根据协议进行操作,共同来维护缓存的一致性。这类协议有 MSI、MESI、MOSI、和 Dragon Protocol 等。 + +## 处理器优化和指令重排序 + +为了提升性能在 CPU 和主内存之间增加了高速缓存,但在多线程并发场景可能会遇到 + +。那还有没有办法进一步提升 CPU 的执行效率呢?答案是:处理器优化。 + +为了使处理器内部的运算单元能够最大化被充分利用,处理器会对输入代码进行乱序执行处理,这就是处理器优化。 + +除了处理器会对代码进行优化处理,很多现代编程语言的编译器也会做类似的优化,比如像 Java 的即时编译器(JIT)会做指令重排序。 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6azdsyv3dj20hs02sjrd.jpg) + +处理器优化其实也是重排序的一种类型,这里总结一下,重排序可以分为三种类型: +** 编译器优化的重排序。编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序。 +** 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 +** 内存系统的重排序。由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。 + +## 并发编程的问题 + +上面讲了一堆硬件相关的东西,有些同学可能会有点懵,绕了这么大圈,这些东西跟 Java 内存模型有啥关系吗?不要急咱们慢慢往下看。 + +熟悉 Java 并发的同学肯定对这三个问题很熟悉:『可见性问题』、『原子性问题』、『有序性问题』。如果从更深层次看这三个问题,其实就是上面讲的『缓存一致性』、『处理器优化』、『指令重排序』造成的。 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6azesaxhzj20hs0cmglu.jpg) + +缓存一致性问题其实就是可见性问题,处理器优化可能会造成原子性问题,指令重排序会造成有序性问题,你看是不是都联系上了。 + +出了问题总是要解决的,那有什么办法呢?首先想到简单粗暴的办法,干掉缓存让 CPU 直接与主内存交互就解决了可见性问题,禁止处理器优化和指令重排序就解决了原子性和有序性问题,但这样一夜回到解放前了,显然不可取。 + +所以技术前辈们想到了在物理机器上定义出一套内存模型, 规范内存的读写操作。内存模型解决并发问题主要采用两种方式: + +限制处理器优化和使用内存屏障。 + +参考: +[全面学习掌握Java内存模型](https://baijiahao.baidu.com/s?id=1700599301374287320&wfr=spider&for=pc) \ No newline at end of file diff --git "a/docs/01.Java\347\233\270\345\205\263/30.JVM/040.JVM \345\206\205\345\255\230\346\230\257\345\246\202\344\275\225\345\257\271\345\272\224\345\210\260\346\223\215\344\275\234\347\263\273\347\273\237\345\206\205\345\255\230\347\232\204\357\274\237.md" "b/docs/01.Java\347\233\270\345\205\263/30.JVM/040.JVM \345\206\205\345\255\230\346\230\257\345\246\202\344\275\225\345\257\271\345\272\224\345\210\260\346\223\215\344\275\234\347\263\273\347\273\237\345\206\205\345\255\230\347\232\204\357\274\237.md" index 067fff993..9bc6f0c2f 100644 --- "a/docs/01.Java\347\233\270\345\205\263/30.JVM/040.JVM \345\206\205\345\255\230\346\230\257\345\246\202\344\275\225\345\257\271\345\272\224\345\210\260\346\223\215\344\275\234\347\263\273\347\273\237\345\206\205\345\255\230\347\232\204\357\274\237.md" +++ "b/docs/01.Java\347\233\270\345\205\263/30.JVM/040.JVM \345\206\205\345\255\230\346\230\257\345\246\202\344\275\225\345\257\271\345\272\224\345\210\260\346\223\215\344\275\234\347\263\273\347\273\237\345\206\205\345\255\230\347\232\204\357\274\237.md" @@ -8,3 +8,56 @@ categories: tags: - --- +## Java 内存模型 + +同一套内存模型规范,不同语言在实现上可能会有些差别。接下来着重讲一下 Java 内存模型实现原理。 + +Java 运行时内存区域与硬件内存的关系 + +了解过 JVM 的同学都知道,JVM 运行时内存区域是分片的,分为栈、堆等,其实这些都是 JVM 定义的逻辑概念。在传统的硬件内存架构中是没有栈和堆这种概念。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6azfyvt0oj20hs081aaf.jpg) + +从图中可以看出栈和堆既存在于高速缓存中又存在于主内存中,所以两者并没有很直接的关系。 + +## Java 线程与主内存的关系 + +Java 内存模型是一种规范,定义了很多东西: + +所有的变量都存储在主内存(Main Memory)中。 +每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。 +线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。 +不同的线程之间无法直接访问对方本地内存中的变量。 +看文字太枯燥了,我又画了一张图: + +## 线程间通信 + +如果两个线程都对一个共享变量进行操作,共享变量初始值为 1,每个线程都变量进行加 1,预期共享变量的值为 3。在 JMM 规范下会有一系列的操作。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6azg831isj20hs0di3z5.jpg) + +为了更好的控制主内存和本地内存的交互,Java 内存模型定义了八种操作来实现: +* lock:锁定。作用于主内存的变量,把一个变量标识为一条线程独占状态。 +* unlock:解锁。作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。 +* read:读取。作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用 +* load:载入。作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。 +* use:使用。作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。 +* assign:赋值。作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。 +* store:存储。作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。 +* write:写入。作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。 +注意:工作内存也就是本地内存的意思。 + +## 有态度的总结 + +由于CPU 和主内存间存在数量级的速率差,想到了引入了多级高速缓存的传统硬件内存架构来解决,多级高速缓存作为 CPU 和主内间的缓冲提升了整体性能。解决了速率差的问题,却又带来了缓存一致性问题。 + +数据同时存在于高速缓存和主内存中,如果不加以规范势必造成灾难,因此在传统机器上又抽象出了内存模型。 + +Java 语言在遵循内存模型的基础上推出了 JMM 规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。 + +为了更精准控制工作内存和主内存间的交互,JMM 还定义了八种操作:lock, unlock, read, load,use,assign, store, write。 + +好了,今天就给大家介绍到这里,简单总结下Java内存模型的定义:Java内存模型并不是一件容易的事情,这个模型必须定义得足够严谨,才能让Java的并发操作不会产生歧义;但是,也必须得足够宽松,使得虚拟机的实现能有足够的自由空间去利用硬件的各种特性(寄存器、高速缓存等)来获取更好的执行速度。经过长时间的验证和修补,在JDK1.5发布后,Java内存模型就已经成熟和完善起来了。 + +参考: +[全面学习掌握Java内存模型](https://baijiahao.baidu.com/s?id=1700599301374287320&wfr=spider&for=pc) \ No newline at end of file diff --git "a/docs/01.Java\347\233\270\345\205\263/30.JVM/050.JVM \346\230\257\346\200\216\344\271\210\345\216\273\350\260\203\344\274\230\347\232\204\357\274\237\344\272\206\350\247\243\345\223\252\344\272\233\345\217\202\346\225\260\345\222\214\346\214\207\344\273\244\357\274\237.md" "b/docs/01.Java\347\233\270\345\205\263/30.JVM/050.JVM \346\230\257\346\200\216\344\271\210\345\216\273\350\260\203\344\274\230\347\232\204\357\274\237\344\272\206\350\247\243\345\223\252\344\272\233\345\217\202\346\225\260\345\222\214\346\214\207\344\273\244\357\274\237.md" index 8c7c43466..e9144aa07 100644 --- "a/docs/01.Java\347\233\270\345\205\263/30.JVM/050.JVM \346\230\257\346\200\216\344\271\210\345\216\273\350\260\203\344\274\230\347\232\204\357\274\237\344\272\206\350\247\243\345\223\252\344\272\233\345\217\202\346\225\260\345\222\214\346\214\207\344\273\244\357\274\237.md" +++ "b/docs/01.Java\347\233\270\345\205\263/30.JVM/050.JVM \346\230\257\346\200\216\344\271\210\345\216\273\350\260\203\344\274\230\347\232\204\357\274\237\344\272\206\350\247\243\345\223\252\344\272\233\345\217\202\346\225\260\345\222\214\346\214\207\344\273\244\357\274\237.md" @@ -8,3 +8,36 @@ categories: tags: - --- + +## 查看进程 +由于JVM大多数命令都是需要用到进程id的,所以我们需要先查看一下我们系统的进程id,通过使用jps -l,输出所有java相关进程。 + + +这里我们可以根据类名,大致确认出我们的系统进程是什么 + +注:-l代表的是输出应用程序main class的完整package名或者应用程序的jar文件完整路径名 + +## 查看JVM参数 jinfo +通过运行jinfo -flags <端口号>查看jvm的参数。 + +通过运行jinfo -sysprops <端口号>查看java系统参数。 + +## 查看各项指标数据 jstat +通过运行 -class <端口号>完成类加载统计 +通过运行jstat -gc <端口号>完成垃圾回收统计 +通过运行jstat -gccapacity <端口号>完成堆内存统计 +通过运行jstat -gcnew <端口号>完成新生代垃圾回收统计 +通过运行jstat -gcnewcapacity <端口号>完成新生代内存统计 +通过运行jstat -gcold <端口号>完成老年代垃圾回收统计 +通过运行jstat -gcoldcapacity <端口号>完成老年代内存统计 +通过运行jstat -gcmetacapacity <端口号>完成老年代内存统计 +通过运行jstat -gcutil <端口号>数据总览 +查看对象及内存使用情况 +通过运行jmap -histo <端口号> > ./log.txt类及其对象所占内存数据。 + +## jmap +通过运行jmap -heap <端口号>堆信息。 + +参考: +[深度好文:JVM调优](https://baijiahao.baidu.com/s?id=1708710107377134976&wfr=spider&for=pc) +[Java核心篇-故事里的JVM性能调优](https://zhuanlan.zhihu.com/p/445492854) \ No newline at end of file diff --git "a/docs/01.Java\347\233\270\345\205\263/30.JVM/060.\344\273\200\344\271\210\346\230\257\345\206\205\345\255\230\346\263\204\346\274\217\357\274\214\346\200\216\344\271\210\347\241\256\345\256\232\345\206\205\345\255\230\346\263\204\346\274\217\357\274\237.md" "b/docs/01.Java\347\233\270\345\205\263/30.JVM/060.\344\273\200\344\271\210\346\230\257\345\206\205\345\255\230\346\263\204\346\274\217\357\274\214\346\200\216\344\271\210\347\241\256\345\256\232\345\206\205\345\255\230\346\263\204\346\274\217\357\274\237.md" index 0f0b0c1a8..bfab9f4c1 100644 --- "a/docs/01.Java\347\233\270\345\205\263/30.JVM/060.\344\273\200\344\271\210\346\230\257\345\206\205\345\255\230\346\263\204\346\274\217\357\274\214\346\200\216\344\271\210\347\241\256\345\256\232\345\206\205\345\255\230\346\263\204\346\274\217\357\274\237.md" +++ "b/docs/01.Java\347\233\270\345\205\263/30.JVM/060.\344\273\200\344\271\210\346\230\257\345\206\205\345\255\230\346\263\204\346\274\217\357\274\214\346\200\216\344\271\210\347\241\256\345\256\232\345\206\205\345\255\230\346\263\204\346\274\217\357\274\237.md" @@ -8,3 +8,172 @@ categories: tags: - --- + +## 前言 + +这个问题是我之前翻看面经的时候见到的。那位小姐姐把内存泄漏当成了内存溢出问题去解答的,结果当场挂掉了。为此总结一下,之前和一位老哥也讨论过这个问题。可见不管是面试还是工作这都是一个极为重要的点。 + +我也曾在面阿里的时候也遇到过原题,题目是写出俩内存泄漏案例,然后问如何排查?如何解决? + +本篇文章大体结构来自外国大佬baeldung; + +## 一、介绍 + +### 1、什么是内存泄漏 + +java的优势之一就是内置了垃圾回收器GC,它帮助我们实现了自动化内存管理。但是GC再好,也有老马失前蹄的时候,它不能保证提供一个解决内存泄漏的万无一失的解决方案。什么是内存泄漏?可以看看下面这张图, +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6azxlwdwmj20hs09rq34.jpg) + +也就是一部分内存空间我明明已经使用了,却没有引用指向这部分空间。造成这片已经使用的空间无法处理的情况。 + +正规点的理解:动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。 + +### 2、内存泄漏的危害 + +长时间运行,程序变卡,性能严重下降 +程序莫名其妙挂掉 +OutOfMemoryError错误 +乱七八糟的错误,还不易排查 +反正内存泄漏不是好事。 + +## 二、内存泄漏原因 + +内存泄漏原因太多了。说不定就是某一行代码不对就会出现这种情况,因此这里给出最常见的几种。关键的还是如何找出哪个地方出现了内存泄漏,代码好修改,错误不易查。 + +### 1、大量使用静态变量 + +静态变量的生命周期与程序一致。因此常驻内存。 + +```java +public class Static Test{ + public static List list = new ArrayList<>(); + public void populateList(){ + for (int i = 0; i < 10000000; i++) { + list.add((int)Math.random()); + } + System.out.println("running......"); + } + public static void main(String[] args){ + System.out.println("before......"); + new StaticTest().populateList(); + System.out.println("after......"); + } +} +``` + +现在可以使用jvisualvm运行一边,看看内存效果。 + +* 带static关键字(使用静态变量) +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6b00y6a0xj20hs08v74q.jpg) + +从上图可以看到,堆内存从一开始的135M左右飙升了到了200M。直接占据了65M的内存。 + +* 不使用static关键字(不使用静态变量) +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6b01ibz3aj20hs08wgm3.jpg) + +由于全局变量与程序周期不一致,因此不使用时,就会进行回收。此时内存最高150M。 + +**总结:由于静态变量与程序生命周期一致,因此对象常驻内存,造成内存泄漏** + +### 2、连接资源未关闭 + +每当建立一个连接,jvm就会为这么资源分配内存。比如数据库连接、文件输入输出流、网络连接等等。 + +```java +public class File Test{ + public static void main(String[] args)throws IOException { + File f=new File("G:\\nginx配套资料\\笔记资料.zip"); + System.out.println(f.exists()); + System.out.println(f.isDirectory()); + } +} +``` +依然使用jvisualvm运行一边,看看内存效果。 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6b02hvjugj20hs08ut97.jpg) + +可以看出,在连接文件资源时,jvm会为本资源分配内存。 + +3、equals()和hashCode()方法使用不当 + +定义新类时,如果没有重新equals()和hashCode()方法,也有可能会造成内存泄漏。主要原因是没有这两个方法时,很容易造成重复的数据添加。看例子: +```java +public class User{ + public String name; + publicint age; + public User(String name, int age){ + this.name = name; + this.age = age;} + } + public class EqualTest{ + public static void main(String[] args){ + Map map = new HashMap<>(); + for(int i=0; i<100; i++) { + map.put(new User("", 1), 1); + } + System.out.println(map.size() == 1);//输出为false + } +} +``` +然后运行一下,看看内存情况: +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6b03ovma3j20hs08vmxm.jpg) + +内存从150M一下子飙升到225M,可见飙升的厉害。输出为false,说明user对象被重复添加了。我们知道像HashMap在添加新的对象时,会对其hashcode进行比较,如果一样,那就不插入。如果一样那就插入。此时说明这100个User其hashcode不同。 + + +### 4、内部类持有外部类 + +这个场景和上面类似。 + +### 5、finalize方法 + +这个方法之前曾经专门花过文章写过,这个问题很简单。看一张图 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6b065hm3ij20hs0ajjs5.jpg) + +这就是整个过程。不过在这里我们主要看的是finalize方法对垃圾回收的影响,其实就是在第三步,也就是这个对象含有finalize,进入了队列但一直没有被调用的这段时间,会一直占用内存。造成内存泄漏。 + +### 6、ThreadLocal的错误使用 + +ThreadLocal主要用于创建本地线程变量,不合理的使用也有可能会造成内存泄漏。 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6b06e0irzj20hs0a00t8.jpg) + +上面这张图详细的揭示了ThreadLocal和Thread以及ThreadLocalMap三者的关系。 + +1、Thread中有一个map,就是ThreadLocalMap + +2、ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。 + +3、ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收 + +4、重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。 + +解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。 + +现在介绍了几种常见的内存泄漏情况,上面的知识点比较常见,最主要的是如何检测出来。 + +## 三、检测内存泄漏 + +检测的目的是定位内存泄漏出现的位置,常见的有以下几种方法: + +### 1、工具分析 + +这个工具比较多,比如说JProfiler、YourKit、Java VisualVM和Netbeans Profiler。他可以帮助我们分析是哪一个对象或者是类内存的飙升。也可以看到内存CPU的等等各种情况。上面多次演示到了。 + +### 2、垃圾回收分析 + +这个其实也可以用工具进行分析。上面的VisualVM中,可以打印堆。也可以从外部导入dump文件进行分析。 + +如果不用工具的话,我们可以通过IDE看到。JVM配置添加-verbose:gc。然后就会打印出相关信息。下面这张图非原创,来自Baeldung。 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6b06ljen1j20hs0853z4.jpg) + +### 3、基准测试 + +也就是使用科学的方式进行分析java代码的性能。进而判断分析。 + +## 四、结论 + +内存泄漏是个很严重的问题,也比较常见。 + +最主要的原因是动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。因此良好的代码规范,可以有效地避免这些错误。 + +参考: +[什么是内存泄漏?该如何检测?又该如何解决?](https://baijiahao.baidu.com/s?id=1693292113788131574&wfr=spider&for=pc) \ No newline at end of file diff --git "a/docs/01.Java\347\233\270\345\205\263/30.JVM/070.\347\256\200\350\277\260 CMS \344\270\216 G1 \346\234\272\345\210\266\347\232\204\345\214\272\345\210\253.md" "b/docs/01.Java\347\233\270\345\205\263/30.JVM/070.\347\256\200\350\277\260 CMS \344\270\216 G1 \346\234\272\345\210\266\347\232\204\345\214\272\345\210\253.md" index 1c5c5a737..c67666fea 100644 --- "a/docs/01.Java\347\233\270\345\205\263/30.JVM/070.\347\256\200\350\277\260 CMS \344\270\216 G1 \346\234\272\345\210\266\347\232\204\345\214\272\345\210\253.md" +++ "b/docs/01.Java\347\233\270\345\205\263/30.JVM/070.\347\256\200\350\277\260 CMS \344\270\216 G1 \346\234\272\345\210\266\347\232\204\345\214\272\345\210\253.md" @@ -8,3 +8,94 @@ categories: tags: - --- +## G1特有数据结构和算法 +### Region +堆仍然有新生代(eden、survivor)、老年代的划分,但是不再要求它们是内存连续的。每个区都由多个Region组成。 +部分老年代Region存储Humongous对象(即下图的H),这种对象大小大于等于Region的一半。 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6b2i7la2aj20iu0akdgh.jpg) + +(图片来源-Java Hotspot G1 GC的一些关键技术) + +### SATB算法 +全称Snapshot-At-The-Beginning,起始时活对象的快照。在理解SATB前需要先了解以下知识。 + +### 三色标记法 +CMS和G1的算法都是通过对gc root 进行遍历,并进行三色标记。标记规则为 + +黑色(black): 节点被遍历完成,而且子节点都遍历完成。 +灰色(gray): 当前正在遍历的节点,而且子节点(即对象的域)还没有遍历。遍历完所有子节点后,将成为黑色 +白色(white): 还没有遍历到的节点,即灰色节点的子节点。扫描结束仍是白色时会被回收。 +并发扫描时,对于白色有两种情况同时发生时,可能会漏标导致被误回收: + +增加了被黑色引用的关系。 +被灰色下应用,删除了到它的引用 +具体执行过程:https://www.cnblogs.com/javaadu/p/10713956.html + +按照R大的说法:CMS的incremental update设计使得它在remark阶段必须重新扫描所有线程栈和整个young gen作为root;G1的SATB设计在remark阶段则只需要扫描剩下的satb_mark_queue。 + +### RSet +全称Remember Set,记录一个Region里的对象被哪些其他Region引用。 +相对应地,有另一种辅助数据结构Collection Set(CSet),它记录了GC要收集的Region集合。GC时只需扫描CSet中各个Rset即可。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6b2ihn8mfj20gf0a1aad.jpg) +(Tips for Tuning the Garbage First Garbage Collector) + +更详细的访问机制和回收过程这里不再展开,有兴趣可以参考后文引用文献。 + +Pause Prediction Model +暂停预测模型,G1根据它计算出的历史数据来预测本次收集需要选择的Region数量,从而尽量满足用户设定的目标停顿时间。 +具体算法和公式略,可见Java Hotspot G1 GC的一些关键技术 + +### 垃圾回收过程 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6b2isd0khj20u406kmxx.jpg) +分为以下几步: + +初始标记(Initial Mark)—— 标记GC root能直接关联的对象(短暂STW) +并发标记(Concurrent mark)—— GCRootsTracing,从并发标记中的root遍历,对不可达的对象进行标记,耗时长但可并行 +最终标记(Final Remark)—— 收集并发标记期间产生的新垃圾(短暂STW),采用了SATB算法比CMS更快 +筛选回收(Live Data Counting and Evacuation)—— 对各个Region的回收性价比排序,在保证时间可控的情况下清除失活对象,清除Remember Sets +作为对比,CMS的回收过程 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6b2iyik1ij20ug06g750.jpg) + +初始标记(CMS Initial Mark)—— 标记GC root能直接关联的对象(短暂STW) +并发标记(CMS Concurrent Mark)—— GCRootsTracing,从并发标记中的root遍历,对不可达的对象进行标记 +重新标记(CMS Remark)—— 修正并发标记期间因为用户操作导致标记发生变更的对象,有STW +并发清除(CMS Concurrent Sweep) + +### 与CMS相比的优势 +并发度更高,充分利用CPU多线程 —— CMS对CPU资源敏感,需要占用25%的线程,如果核数小于4更会占用一半的资源。 +整体上是标记-整理(分代),局部是复制(分Region),运行期不产生碎片 —— CMS是标记-清除,会产生空间碎片和本次回收期间产生导致本次无法回收的浮动垃圾 +可预测的停顿(基于Region) + +## CMS +### 什么是CMS? + +Concurrent Mark Sweep。 + +看名字就知道,CMS是一款并发、使用标记-清除算法的gc。 + +CMS是针对老年代进行回收的GC。 + +### CMS有什么用? + +CMS以获取最小停顿时间为目的。 + +在一些对响应时间有很高要求的应用或网站中,用户程序不能有长时间的停顿,CMS 可以用于此场景。 + +### CMS如何执行? +    总体来说CMS的执行过程可以分为以下几个阶段: + + 3.1 初始标记(STW) + + 3.2 并发标记 + + 3.3 并发预清理 + + 3.4 重标记(STW) + + 3.5 并发清理 + + 3.6 重置 +参考: +[关于GC(下):CMS和G1GC的比较](https://www.bbsmax.com/A/QV5Z37vydy/) +[详解CMS垃圾回收机制](https://www.cnblogs.com/lzmrex/articles/12887198.html) \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/01.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/01.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" index 67e4d1a2e..b12197895 100644 --- "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/01.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/01.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" @@ -9,7 +9,7 @@ tags: - --- ## 简述 TCP 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手? -## 简述常见的 HTTP 状态码的含义(301,304,401,403) + ## 从输入 URL 到展现页面的全过程 ## HTTP 中 GET 和 POST 区别 ## TCP 怎么保证可靠传输 diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/010.\347\256\200\350\277\260\345\270\270\350\247\201\347\232\204 HTTP \347\212\266\346\200\201\347\240\201\347\232\204\345\220\253\344\271\211.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/010.\347\256\200\350\277\260\345\270\270\350\247\201\347\232\204 HTTP \347\212\266\346\200\201\347\240\201\347\232\204\345\220\253\344\271\211.md" new file mode 100644 index 000000000..5ffd48600 --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/010.\347\256\200\350\277\260\345\270\270\350\247\201\347\232\204 HTTP \347\212\266\346\200\201\347\240\201\347\232\204\345\220\253\344\271\211.md" @@ -0,0 +1,32 @@ +--- +title: 简述常见的 HTTP 状态码的含义 +date: 2022-06-23 21:09:29 +permalink: /pages/f33310/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- +## 2XX 成功 + +200 OK,表示从客户端发来的请求在服务器端被正确处理 +204 No content,表示请求成功,但响应报文不含实体的主体部分 +206 Partial Content,进行范围请求 +## 3XX 重定向 + +301 moved permanently,永久性重定向,表示资源已被分配了新的 URL +302 found,临时性重定向,表示资源临时被分配了新的 URL +303 see other,表示资源存在着另一个 URL,应使用 GET 方法定向获取资源 +304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况 +307 temporary redirect,临时重定向,和302含义相同 +## 4XX 客户端错误 + +400 bad request,请求报文存在语法错误 +401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息 +403 forbidden,表示对请求资源的访问被服务器拒绝 +404 not found,表示在服务器上没有找到请求的资源 +## 5XX 服务器错误 + +500 internal sever error,表示服务器端在执行请求时发生了错误 +503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求 \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/020.\344\273\216\350\276\223\345\205\245 URL \345\210\260\345\261\225\347\216\260\351\241\265\351\235\242\347\232\204\345\205\250\350\277\207\347\250\213.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/020.\344\273\216\350\276\223\345\205\245 URL \345\210\260\345\261\225\347\216\260\351\241\265\351\235\242\347\232\204\345\205\250\350\277\207\347\250\213.md" new file mode 100644 index 000000000..8b6c22e30 --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/020.\344\273\216\350\276\223\345\205\245 URL \345\210\260\345\261\225\347\216\260\351\241\265\351\235\242\347\232\204\345\205\250\350\277\207\347\250\213.md" @@ -0,0 +1,83 @@ +--- +title: 从输入 URL 到展现页面的全过程 +date: 2022-06-23 21:10:44 +permalink: /pages/ab48ba/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- +从我们输入一个网址到页面展现的过程到底发生了什么呢? + +# 总体来说分为以下几个过程: +* 输入网址 +* DNS解析 +* 建立TCP/IP链接 +* 发送HTTP请求 +* 服务器处理请求 +* 服务器返回HTTP响应 +* 浏览器渲染页面并展现 +* 断开连接 + +# 输入网址 +当我们想要打开一个页面时,很简单的会想到去输入一个网址 +我们以本网址为例,解释一下它各部分的组成: +```html +https://www.baidu.com +``` +* https :https是协议(Protocol),它表明了浏览器使用何种协议,有http、https、ftp、mailto等,其中https是加密认证版的http(详见《图解HTTP》) + +* www.baidu.com :这是域名(Domain name),它表明正在请求哪个Web服务器,其中www是主机名(host) + +# DNS解析 +输入网址(URL)这一步我们很好理解,但是接下来所谓的域名解析是什么呢?又是为什么要进行域名解析呢? + +## 什么是IP地址 + +IP 地址(Internet Protocol Address)是互联网协议特有的一种地址,它是 IP 协议提供的一种统一的地址格式。IP 地址为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。 +像192.168.59.0就是一个主机IP地址 + +## 什么是DNS +DNS服务器上存储着映射<域名-IP>,DNS解析就是去DNS服务器查询IP地址。 + +## DNS解析 +DNS解析按照下面顺序逐个查找。 +* 本地(hosts文件) +* DNS服务器(本地DNS | 根DNS) +* 域服务器 +* 解析服务器的地址 + +![](https://tva1.sinaimg.cn/large/e6c9d24ely1h3jr49albxj20sl0jt40n.jpg) + +## DNS查询的两种方式:递归查询和迭代查询 +递归解析 +![](https://tva1.sinaimg.cn/large/e6c9d24ely1h3jrcpsp8tj20fn02r3yh.jpg) +迭代解析 +![](https://tva1.sinaimg.cn/large/e6c9d24ely1h3jrd9k881j20bx0983yo.jpg) +## DNS域名称空间的组织方式 +![](https://tva1.sinaimg.cn/large/e6c9d24ely1h3jrafkgijj20fi06ogm7.jpg) + +## DNS负载均衡 +当一个网站有足够多的用户的时候,假如每次请求的资源都位于同一台机器上面,那么这台机器随时可能会蹦掉。 + +处理办法就是用DNS负载均衡技术,它的原理是在DNS服务器中为同一个主机名配置多个IP地址 +在应答DNS查询时,DNS服务器对每个查询将以DNS文件中主机记录的IP地址按顺序返回不同的解析结果,将客户端的访问引导到不同的机器上去,使得不同的客户端访问不同的服务器,从而达到负载均衡的目的。 + +# 建立TCP/IP链接 +## ”三次握手”的详解 +![](https://tva1.sinaimg.cn/large/e6c9d24ely1h3jrlodi0dj20ei09daaf.jpg) +## 为什么要三次握手 +* clien端 +防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。 +client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致连接释放以后的某个时间才到达server。 +本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。 +于是就向client发出确认报文段,同意建立连接。 + +* server端 +假设不采用“三次握手”。 +如果只要server发出确认,新的连接就建立了。并一直等待client发来数据。 +事实是client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。 +但server端就会有许多这样的等待,server的很多资源就白白浪费掉了。 + +采用“三次握手”的办法可以防止上述现象发生。主要目的防止server端一直等待,浪费资源。 diff --git a/node_modules/@vuepress/core/.temp/internal/page-components.js b/node_modules/@vuepress/core/.temp/internal/page-components.js index 5987efa43..213ea8820 100644 --- a/node_modules/@vuepress/core/.temp/internal/page-components.js +++ b/node_modules/@vuepress/core/.temp/internal/page-components.js @@ -2,128 +2,131 @@ * Generated by "@vuepress/internal-page-components" */ export default { - "v-a612ff08": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/00.目录页/03.数据库.md"), + "v-36edf5bb": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/00.目录页/01.Java相关.md"), "v-10f5d0e5": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/00.目录页/02.计算机基础.md"), + "v-a612ff08": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/00.目录页/03.数据库.md"), "v-7abe58c4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/00.目录页/04.中间件.md"), - "v-36edf5bb": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/00.目录页/01.Java相关.md"), "v-8bf153e4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/001.Java 异常有哪些类型?.md"), "v-0734374b": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/010.HashMap 与 ConcurrentHashMap 的实现原理是怎样的?.md"), "v-4e1fd39c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/020.Java 中接口和抽象类的区别.md"), "v-57012c44": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/030.为什么重写 equals 必须重写 hashcode ?.md"), "v-d8a2f15c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/040.== 和 equals() 的区别?.md"), - "v-7f48b402": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/050.String,StringBuffer,StringBuilder 之间有什么区别?.md"), "v-57b2551a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/060.简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度.md"), + "v-7f48b402": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/050.String,StringBuffer,StringBuilder 之间有什么区别?.md"), "v-d4e20148": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/070.简述 BIO, NIO, AIO 的区别.md"), - "v-85b06162": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/090.简述 Java 的反射机制及其应用场景.md"), - "v-4c273a66": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/100.Java 类的加载流程是怎样的?.md"), "v-d32e07ea": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/080.在一个静态方法内调用一个非静态成员为什么是非法的?.md"), + "v-85b06162": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/090.简述 Java 的反射机制及其应用场景.md"), "v-7665e30e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/110.什么是双亲委派机制?.md"), + "v-4c273a66": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/100.Java 类的加载流程是怎样的?.md"), "v-6ff6e2f0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/120.Java 中 sleep() 与 wait() 的区别 .md"), "v-620ebadc": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/130.简述 Java 中 final 关键字的作用.md"), "v-5290507e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/140.String 类能不能被继承?为什么?.md"), "v-6f5c6512": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/150.Java 缓冲流 buffer 的用途和原理是什么?.md"), "v-75844e97": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/160.简述动态代理与静态代理 .md"), - "v-1218fa85": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/170.阻塞队列都有哪几种,有什么区别?.md"), "v-b21bbc4c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/180.简述 HashSet 实现原理.md"), + "v-1218fa85": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/170.阻塞队列都有哪几种,有什么区别?.md"), "v-45fc1217": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/001.线程池是如何实现的?.md"), "v-35324a50": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/030.简述 Synchronized,Volatile,可重入锁的不同使用场景及优缺点.md"), - "v-352f36cc": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/040.Synchronized 关键字底层是如何实现的?.md"), "v-5390e3d4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/020.简述 CAS 原理,什么是 ABA 问题,怎么解决?.md"), - "v-4927c049": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/050.重入锁是如何实现的?.md"), + "v-352f36cc": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/040.Synchronized 关键字底层是如何实现的?.md"), "v-f5bcd180": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/060.volatile 关键字解决了什么问题,它的实现原理是什么?.md"), + "v-4927c049": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/050.重入锁是如何实现的?.md"), "v-1065f7be": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/070.简述 Java 锁升级的机制.md"), "v-2dbdb772": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/080.简述 Java AQS 的原理以及使用场景.md"), "v-134f28dc": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/090.什么是公平锁?什么是非公平锁?.md"), "v-03e420c4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/100.Java 的线程有哪些状态,转换关系是怎么样的?.md"), "v-625fbeda": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/110.Java 是如何实现线程安全的,哪些数据结构是线程安全的?.md"), - "v-7b2a2ed4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/115.手写死锁.md"), "v-9baaad8e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/120.为什么我们不能直接调用 run() 方法?.md"), - "v-75c00225": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/130.Java 线程有哪些常用方法?.md"), + "v-7b2a2ed4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/115.手写死锁.md"), "v-083aef82": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/140.手写生产者消费者模型.md"), + "v-75c00225": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/130.Java 线程有哪些常用方法?.md"), "v-f406b4e6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/150.ThreadLocal 实现原理是什么?为什么要使用弱引用?.md"), - "v-64652416": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/001.Java 中垃圾回收机制中如何判断对象需要回收?常见的 GC 回收算法有哪些?.md"), + "v-7cd97c82": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/001.Java 中垃圾回收机制中如何判断对象需要回收?.md"), + "v-af821fc8": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/010.常见的 GC 回收算法有哪些?.md"), "v-5474bec0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/020.Java 中如何进行 GC 调优?.md"), "v-0970cd54": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/030.JMM 中内存模型是怎样的?什么是指令序列重排序?.md"), "v-2de19204": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/040.JVM 内存是如何对应到操作系统内存的?.md"), - "v-37fcb440": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/050.JVM 是怎么去调优的?了解哪些参数和指令?.md"), "v-15fba8f1": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/060.什么是内存泄漏,怎么确定内存泄漏?.md"), - "v-ba94c306": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/002.实现单例设计模式(懒汉,饿汉).md"), + "v-37fcb440": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/050.JVM 是怎么去调优的?了解哪些参数和指令?.md"), "v-28c1d6d9": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/070.简述 CMS 与 G1 机制的区别.md"), + "v-ba94c306": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/002.实现单例设计模式(懒汉,饿汉).md"), "v-504a5213": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/003.什么是设计模式,描述几个常用的设计模式 .md"), "v-2d6ed001": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/004.工厂模式使用场景 能举个例子吗.md"), "v-22a9eaef": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/005.装饰者模式.md"), "v-31509c80": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/010.适配器模式.md"), "v-0564ba27": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/020.代理模式.md"), "v-34f8aaa2": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/030.责任链模式.md"), - "v-28deb9da": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/040.模板方法模式.md"), - "v-5b0157cf": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/050.策略模式.md"), "v-64f1031c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/060.观察模式.md"), + "v-28deb9da": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/040.模板方法模式.md"), "v-2279e49d": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/070.代理模式.md"), - "v-dd5f4440": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/080.建造者模式.md"), + "v-5b0157cf": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/050.策略模式.md"), "v-1e653940": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/01.计算机网络.md"), + "v-dd5f4440": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/080.建造者模式.md"), + "v-0a1d1fb4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/010.简述常见的 HTTP 状态码的含义.md"), + "v-6602e3da": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/020.从输入 URL 到展现页面的全过程.md"), "v-6f178cbc": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/01.操作系统.md"), "v-49688930": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/30.数据结构/01.数据结构.md"), "v-2e6d42b4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/40.算法/01.算法.md"), - "v-674454ac": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/001.MySQL中 InnoDB 和 MylSAM 的区别是什么?.md"), "v-f0f711c4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/006.聚簇索引和非聚簇索引有什么区别?.md"), + "v-674454ac": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/001.MySQL中 InnoDB 和 MylSAM 的区别是什么?.md"), "v-824b776c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/007.简述 MySQL 常见索引类型,介绍一下覆盖索引.md"), - "v-7e02c4a1": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/010.简述Mysql的事务隔离级别.md"), "v-5b0e09c6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/015.说一下Mysql锁.md"), + "v-7e02c4a1": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/010.简述Mysql的事务隔离级别.md"), "v-6c681639": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/020.MySQL 为什么使用 B+ 树来作索引,对比 B 树它的优点和缺点是什么.md"), "v-c39c4fe2": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/030.简述乐观锁以及悲观锁的区别以及使用场景.md"), "v-689e142a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/040.简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?.md"), - "v-4ecddbec": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/050.简述什么是最左匹配原则.md"), "v-505fbe2e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/060.MySQL 联合索引底层原理是什么?.md"), - "v-0dd47f09": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/070.产生死锁的必要条件有哪些?如何解决死锁?.md"), "v-53bcbb7a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/080.简述常见的负载均衡算法.md"), + "v-0dd47f09": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/070.产生死锁的必要条件有哪些?如何解决死锁?.md"), + "v-4ecddbec": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/050.简述什么是最左匹配原则.md"), "v-c383b96e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/090.简述 MySQL 的主从同步机制,如果同步失败会怎么样?.md"), - "v-4cef944a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/100.如何设计数据库压测方案?.md"), "v-ed0c93b8": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/110.简述数据库中什么情况下进行分库,什么情况下进行分表?.md"), "v-d6bc240a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/120.数据库的读写分离的作用是什么?如何实现?.md"), "v-f3acaa1a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/130.简述 undo log 和 redo log 的作用.md"), - "v-77790024": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/150.MySQL 中 join 与 left join 的区别是什么?.md"), + "v-4cef944a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/100.如何设计数据库压测方案?.md"), "v-ceed6ba6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/140.什么是 SQL 注入攻击?如何防止这类攻击?.md"), - "v-76f5e79a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/160.简述 MySQL MVCC 的实现原理 .md"), + "v-77790024": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/150.MySQL 中 join 与 left join 的区别是什么?.md"), "v-0a7e6416": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/170.MySQL 中 varchar 和 char 的区别是什么?.md"), + "v-76f5e79a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/160.简述 MySQL MVCC 的实现原理 .md"), "v-6b967a6e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/180.模糊查询是如何实现的?.md"), "v-d804fba0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/001.Redis 有几种数据结构?Zset 是如何实现的?.md"), "v-cb742458": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/002.为什么 Redis 在单线程下能如此快?.md"), "v-3e917471": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/005.简述 Redis 字符串的底层结构.md"), "v-c0bc7b7a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/010.Redis的缓存淘汰策略有哪些?.md"), "v-7c214d00": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/020.简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点.md"), - "v-2c6b511c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/030.Redis 如何实现分布式锁?.md"), "v-3d2fdd6a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/040.简述 Redis 集群配置以及基础原理.md"), + "v-2c6b511c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/030.Redis 如何实现分布式锁?.md"), "v-18b427a0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/050.简述 Redis 中跳表的应用以及优缺点.md"), "v-6a45f68d": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/060.Redis 中,sentinel 和 cluster 的区别和适用场景是什么?.md"), - "v-c2fe5e1a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/070.简述 Redis 中如何防止缓存雪崩和缓存击穿.md"), "v-35718f4e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/080.简述 Redis 的线程模型以及底层架构设计.md"), + "v-c2fe5e1a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/070.简述 Redis 中如何防止缓存雪崩和缓存击穿.md"), "v-57957421": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/100.简述 Redis 的哨兵机制.md"), "v-37f158ba": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/110.简述 Redis 如何处理热点 key 访问.md"), - "v-02ea33c3": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/120.Redis 序列化有哪些方式?.md"), "v-be02b3c0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/001.你熟悉哪些IO模型?.md"), - "v-073f22cd": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/015.Netty中有哪些解码器?.md"), + "v-02ea33c3": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/120.Redis 序列化有哪些方式?.md"), "v-5acdab4d": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/010.TCP粘包拆包问题的解决之道.md"), + "v-073f22cd": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/015.Netty中有哪些解码器?.md"), "v-bbcf6e5a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/020.为什么Netty要自己写一个ByteBuf.md"), "v-5157927c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/030.ChannelPipeline和ChannelHandler.md"), "v-62f3fa3e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/040.EventLoop和EventLoopGroup.md"), "v-6b32ada0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/050.Java NIO了解吗?.md"), - "v-0df39df8": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/001.Kafka包含那几部分.md"), "v-7c0136cf": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/010.生产者.md"), + "v-0df39df8": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/001.Kafka包含那几部分.md"), "v-04af9e42": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/020.消费者.md"), "v-0c440466": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/030.集群下的Kafka要考虑哪些事情.md"), "v-684c4815": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/040.如何保证Kafka的可靠性.md"), "v-a9558ec0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/08.系统设计/001.系统设计.md"), - "v-f88544f4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/08.系统设计/020.简述什么是两阶段提交?.md"), "v-5675b06b": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/08.系统设计/010.什么是 CAP ?什么是最终一致性?什么是幂等操作?.md"), - "v-2a52a762": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/10.非技术/01.非技术问题.md"), + "v-f88544f4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/08.系统设计/020.简述什么是两阶段提交?.md"), "v-918dfaf8": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/15.关于/01.关于.md"), + "v-2a52a762": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/10.非技术/01.非技术问题.md"), "v-48f7eec0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/archivesPage.md"), - "v-09c150e6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/20.收藏夹/02.常用的前端轮子.md"), + "v-7ab50b9f": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/20.收藏夹/01.网站.md"), + "v-a5c5f278": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/_posts/随笔/我做了一个手写春联小网页,祝大家虎年暴富.md"), "v-2db11cb2": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/_posts/随笔/你知道的越多,不知道的也就越多.md"), "v-7d77d69c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/_posts/随笔/拥抱生活,拥抱快乐.md"), - "v-a5c5f278": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/_posts/随笔/我做了一个手写春联小网页,祝大家虎年暴富.md"), - "v-7ab50b9f": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/20.收藏夹/01.网站.md"), - "v-5f8dfe80": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/categoriesPage.md"), + "v-2cf94a0e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/index.md"), + "v-09c150e6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/20.收藏夹/02.常用的前端轮子.md"), "v-64f9b920": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/tagsPage.md"), - "v-2cf94a0e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/index.md") + "v-5f8dfe80": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/categoriesPage.md") } \ No newline at end of file diff --git a/node_modules/@vuepress/core/.temp/internal/routes.js b/node_modules/@vuepress/core/.temp/internal/routes.js index fe0437b67..3d4dc5abe 100644 --- a/node_modules/@vuepress/core/.temp/internal/routes.js +++ b/node_modules/@vuepress/core/.temp/internal/routes.js @@ -9,20 +9,20 @@ import GlobalLayout from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/n injectComponentOption(GlobalLayout, 'mixins', rootMixins) export const routes = [ { - name: "v-a612ff08", - path: "/database/", + name: "v-36edf5bb", + path: "/java/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-a612ff08").then(next) + ensureAsyncComponentsLoaded("Layout", "v-36edf5bb").then(next) }, }, { - path: "/database/index.html", - redirect: "/database/" + path: "/java/index.html", + redirect: "/java/" }, { - path: "/00.目录页/03.数据库.html", - redirect: "/database/" + path: "/00.目录页/01.Java相关.html", + redirect: "/java/" }, { name: "v-10f5d0e5", @@ -41,36 +41,36 @@ export const routes = [ redirect: "/basic/" }, { - name: "v-7abe58c4", - path: "/middleware/", + name: "v-a612ff08", + path: "/database/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-7abe58c4").then(next) + ensureAsyncComponentsLoaded("Layout", "v-a612ff08").then(next) }, }, { - path: "/middleware/index.html", - redirect: "/middleware/" + path: "/database/index.html", + redirect: "/database/" }, { - path: "/00.目录页/04.中间件.html", - redirect: "/middleware/" + path: "/00.目录页/03.数据库.html", + redirect: "/database/" }, { - name: "v-36edf5bb", - path: "/java/", + name: "v-7abe58c4", + path: "/middleware/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-36edf5bb").then(next) + ensureAsyncComponentsLoaded("Layout", "v-7abe58c4").then(next) }, }, { - path: "/java/index.html", - redirect: "/java/" + path: "/middleware/index.html", + redirect: "/middleware/" }, { - path: "/00.目录页/01.Java相关.html", - redirect: "/java/" + path: "/00.目录页/04.中间件.html", + redirect: "/middleware/" }, { name: "v-8bf153e4", @@ -153,36 +153,36 @@ export const routes = [ redirect: "/pages/06b28a/" }, { - name: "v-7f48b402", - path: "/pages/211d8c/", + name: "v-57b2551a", + path: "/pages/305919/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-7f48b402").then(next) + ensureAsyncComponentsLoaded("Layout", "v-57b2551a").then(next) }, }, { - path: "/pages/211d8c/index.html", - redirect: "/pages/211d8c/" + path: "/pages/305919/index.html", + redirect: "/pages/305919/" }, { - path: "/01.Java相关/10.基础/050.String,StringBuffer,StringBuilder 之间有什么区别?.html", - redirect: "/pages/211d8c/" + path: "/01.Java相关/10.基础/060.简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度.html", + redirect: "/pages/305919/" }, { - name: "v-57b2551a", - path: "/pages/305919/", + name: "v-7f48b402", + path: "/pages/211d8c/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-57b2551a").then(next) + ensureAsyncComponentsLoaded("Layout", "v-7f48b402").then(next) }, }, { - path: "/pages/305919/index.html", - redirect: "/pages/305919/" + path: "/pages/211d8c/index.html", + redirect: "/pages/211d8c/" }, { - path: "/01.Java相关/10.基础/060.简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度.html", - redirect: "/pages/305919/" + path: "/01.Java相关/10.基础/050.String,StringBuffer,StringBuilder 之间有什么区别?.html", + redirect: "/pages/211d8c/" }, { name: "v-d4e20148", @@ -201,68 +201,68 @@ export const routes = [ redirect: "/pages/276740/" }, { - name: "v-85b06162", - path: "/pages/fae5d5/", + name: "v-d32e07ea", + path: "/pages/56314c/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-85b06162").then(next) + ensureAsyncComponentsLoaded("Layout", "v-d32e07ea").then(next) }, }, { - path: "/pages/fae5d5/index.html", - redirect: "/pages/fae5d5/" + path: "/pages/56314c/index.html", + redirect: "/pages/56314c/" }, { - path: "/01.Java相关/10.基础/090.简述 Java 的反射机制及其应用场景.html", - redirect: "/pages/fae5d5/" + path: "/01.Java相关/10.基础/080.在一个静态方法内调用一个非静态成员为什么是非法的?.html", + redirect: "/pages/56314c/" }, { - name: "v-4c273a66", - path: "/pages/33c5d6/", + name: "v-85b06162", + path: "/pages/fae5d5/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-4c273a66").then(next) + ensureAsyncComponentsLoaded("Layout", "v-85b06162").then(next) }, }, { - path: "/pages/33c5d6/index.html", - redirect: "/pages/33c5d6/" + path: "/pages/fae5d5/index.html", + redirect: "/pages/fae5d5/" }, { - path: "/01.Java相关/10.基础/100.Java 类的加载流程是怎样的?.html", - redirect: "/pages/33c5d6/" + path: "/01.Java相关/10.基础/090.简述 Java 的反射机制及其应用场景.html", + redirect: "/pages/fae5d5/" }, { - name: "v-d32e07ea", - path: "/pages/56314c/", + name: "v-7665e30e", + path: "/pages/38bcaf/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-d32e07ea").then(next) + ensureAsyncComponentsLoaded("Layout", "v-7665e30e").then(next) }, }, { - path: "/pages/56314c/index.html", - redirect: "/pages/56314c/" + path: "/pages/38bcaf/index.html", + redirect: "/pages/38bcaf/" }, { - path: "/01.Java相关/10.基础/080.在一个静态方法内调用一个非静态成员为什么是非法的?.html", - redirect: "/pages/56314c/" + path: "/01.Java相关/10.基础/110.什么是双亲委派机制?.html", + redirect: "/pages/38bcaf/" }, { - name: "v-7665e30e", - path: "/pages/38bcaf/", + name: "v-4c273a66", + path: "/pages/33c5d6/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-7665e30e").then(next) + ensureAsyncComponentsLoaded("Layout", "v-4c273a66").then(next) }, }, { - path: "/pages/38bcaf/index.html", - redirect: "/pages/38bcaf/" + path: "/pages/33c5d6/index.html", + redirect: "/pages/33c5d6/" }, { - path: "/01.Java相关/10.基础/110.什么是双亲委派机制?.html", - redirect: "/pages/38bcaf/" + path: "/01.Java相关/10.基础/100.Java 类的加载流程是怎样的?.html", + redirect: "/pages/33c5d6/" }, { name: "v-6ff6e2f0", @@ -345,36 +345,36 @@ export const routes = [ redirect: "/pages/37dba4/" }, { - name: "v-1218fa85", - path: "/pages/e9ce4e/", + name: "v-b21bbc4c", + path: "/pages/75552c/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-1218fa85").then(next) + ensureAsyncComponentsLoaded("Layout", "v-b21bbc4c").then(next) }, }, { - path: "/pages/e9ce4e/index.html", - redirect: "/pages/e9ce4e/" + path: "/pages/75552c/index.html", + redirect: "/pages/75552c/" }, { - path: "/01.Java相关/10.基础/170.阻塞队列都有哪几种,有什么区别?.html", - redirect: "/pages/e9ce4e/" + path: "/01.Java相关/10.基础/180.简述 HashSet 实现原理.html", + redirect: "/pages/75552c/" }, { - name: "v-b21bbc4c", - path: "/pages/75552c/", + name: "v-1218fa85", + path: "/pages/e9ce4e/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-b21bbc4c").then(next) + ensureAsyncComponentsLoaded("Layout", "v-1218fa85").then(next) }, }, { - path: "/pages/75552c/index.html", - redirect: "/pages/75552c/" + path: "/pages/e9ce4e/index.html", + redirect: "/pages/e9ce4e/" }, { - path: "/01.Java相关/10.基础/180.简述 HashSet 实现原理.html", - redirect: "/pages/75552c/" + path: "/01.Java相关/10.基础/170.阻塞队列都有哪几种,有什么区别?.html", + redirect: "/pages/e9ce4e/" }, { name: "v-45fc1217", @@ -408,22 +408,6 @@ export const routes = [ path: "/01.Java相关/20.并发/030.简述 Synchronized,Volatile,可重入锁的不同使用场景及优缺点.html", redirect: "/pages/97245f/" }, - { - name: "v-352f36cc", - path: "/pages/d5d161/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-352f36cc").then(next) - }, - }, - { - path: "/pages/d5d161/index.html", - redirect: "/pages/d5d161/" - }, - { - path: "/01.Java相关/20.并发/040.Synchronized 关键字底层是如何实现的?.html", - redirect: "/pages/d5d161/" - }, { name: "v-5390e3d4", path: "/pages/3356c6/", @@ -441,20 +425,20 @@ export const routes = [ redirect: "/pages/3356c6/" }, { - name: "v-4927c049", - path: "/pages/9b59d1/", + name: "v-352f36cc", + path: "/pages/d5d161/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-4927c049").then(next) + ensureAsyncComponentsLoaded("Layout", "v-352f36cc").then(next) }, }, { - path: "/pages/9b59d1/index.html", - redirect: "/pages/9b59d1/" + path: "/pages/d5d161/index.html", + redirect: "/pages/d5d161/" }, { - path: "/01.Java相关/20.并发/050.重入锁是如何实现的?.html", - redirect: "/pages/9b59d1/" + path: "/01.Java相关/20.并发/040.Synchronized 关键字底层是如何实现的?.html", + redirect: "/pages/d5d161/" }, { name: "v-f5bcd180", @@ -472,6 +456,22 @@ export const routes = [ path: "/01.Java相关/20.并发/060.volatile 关键字解决了什么问题,它的实现原理是什么?.html", redirect: "/pages/383264/" }, + { + name: "v-4927c049", + path: "/pages/9b59d1/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-4927c049").then(next) + }, + }, + { + path: "/pages/9b59d1/index.html", + redirect: "/pages/9b59d1/" + }, + { + path: "/01.Java相关/20.并发/050.重入锁是如何实现的?.html", + redirect: "/pages/9b59d1/" + }, { name: "v-1065f7be", path: "/pages/ff0616/", @@ -552,22 +552,6 @@ export const routes = [ path: "/01.Java相关/20.并发/110.Java 是如何实现线程安全的,哪些数据结构是线程安全的?.html", redirect: "/pages/c1f760/" }, - { - name: "v-7b2a2ed4", - path: "/pages/ce292c/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-7b2a2ed4").then(next) - }, - }, - { - path: "/pages/ce292c/index.html", - redirect: "/pages/ce292c/" - }, - { - path: "/01.Java相关/20.并发/115.手写死锁.html", - redirect: "/pages/ce292c/" - }, { name: "v-9baaad8e", path: "/pages/13856f/", @@ -585,20 +569,20 @@ export const routes = [ redirect: "/pages/13856f/" }, { - name: "v-75c00225", - path: "/pages/a6c84e/", + name: "v-7b2a2ed4", + path: "/pages/ce292c/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-75c00225").then(next) + ensureAsyncComponentsLoaded("Layout", "v-7b2a2ed4").then(next) }, }, { - path: "/pages/a6c84e/index.html", - redirect: "/pages/a6c84e/" + path: "/pages/ce292c/index.html", + redirect: "/pages/ce292c/" }, { - path: "/01.Java相关/20.并发/130.Java 线程有哪些常用方法?.html", - redirect: "/pages/a6c84e/" + path: "/01.Java相关/20.并发/115.手写死锁.html", + redirect: "/pages/ce292c/" }, { name: "v-083aef82", @@ -616,6 +600,22 @@ export const routes = [ path: "/01.Java相关/20.并发/140.手写生产者消费者模型.html", redirect: "/pages/2b8c6e/" }, + { + name: "v-75c00225", + path: "/pages/a6c84e/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-75c00225").then(next) + }, + }, + { + path: "/pages/a6c84e/index.html", + redirect: "/pages/a6c84e/" + }, + { + path: "/01.Java相关/20.并发/130.Java 线程有哪些常用方法?.html", + redirect: "/pages/a6c84e/" + }, { name: "v-f406b4e6", path: "/pages/b3e720/", @@ -633,11 +633,11 @@ export const routes = [ redirect: "/pages/b3e720/" }, { - name: "v-64652416", + name: "v-7cd97c82", path: "/pages/948850/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-64652416").then(next) + ensureAsyncComponentsLoaded("Layout", "v-7cd97c82").then(next) }, }, { @@ -645,9 +645,25 @@ export const routes = [ redirect: "/pages/948850/" }, { - path: "/01.Java相关/30.JVM/001.Java 中垃圾回收机制中如何判断对象需要回收?常见的 GC 回收算法有哪些?.html", + path: "/01.Java相关/30.JVM/001.Java 中垃圾回收机制中如何判断对象需要回收?.html", redirect: "/pages/948850/" }, + { + name: "v-af821fc8", + path: "/pages/9b26e8/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-af821fc8").then(next) + }, + }, + { + path: "/pages/9b26e8/index.html", + redirect: "/pages/9b26e8/" + }, + { + path: "/01.Java相关/30.JVM/010.常见的 GC 回收算法有哪些?.html", + redirect: "/pages/9b26e8/" + }, { name: "v-5474bec0", path: "/pages/07a030/", @@ -696,22 +712,6 @@ export const routes = [ path: "/01.Java相关/30.JVM/040.JVM 内存是如何对应到操作系统内存的?.html", redirect: "/pages/da65e7/" }, - { - name: "v-37fcb440", - path: "/pages/d730dd/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-37fcb440").then(next) - }, - }, - { - path: "/pages/d730dd/index.html", - redirect: "/pages/d730dd/" - }, - { - path: "/01.Java相关/30.JVM/050.JVM 是怎么去调优的?了解哪些参数和指令?.html", - redirect: "/pages/d730dd/" - }, { name: "v-15fba8f1", path: "/pages/3ab5f5/", @@ -729,20 +729,20 @@ export const routes = [ redirect: "/pages/3ab5f5/" }, { - name: "v-ba94c306", - path: "/pages/b37ad7/", + name: "v-37fcb440", + path: "/pages/d730dd/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-ba94c306").then(next) + ensureAsyncComponentsLoaded("Layout", "v-37fcb440").then(next) }, }, { - path: "/pages/b37ad7/index.html", - redirect: "/pages/b37ad7/" + path: "/pages/d730dd/index.html", + redirect: "/pages/d730dd/" }, { - path: "/01.Java相关/40.设计模式/002.实现单例设计模式(懒汉,饿汉).html", - redirect: "/pages/b37ad7/" + path: "/01.Java相关/30.JVM/050.JVM 是怎么去调优的?了解哪些参数和指令?.html", + redirect: "/pages/d730dd/" }, { name: "v-28c1d6d9", @@ -760,6 +760,22 @@ export const routes = [ path: "/01.Java相关/30.JVM/070.简述 CMS 与 G1 机制的区别.html", redirect: "/pages/be69af/" }, + { + name: "v-ba94c306", + path: "/pages/b37ad7/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-ba94c306").then(next) + }, + }, + { + path: "/pages/b37ad7/index.html", + redirect: "/pages/b37ad7/" + }, + { + path: "/01.Java相关/40.设计模式/002.实现单例设计模式(懒汉,饿汉).html", + redirect: "/pages/b37ad7/" + }, { name: "v-504a5213", path: "/pages/3d768c/", @@ -856,6 +872,22 @@ export const routes = [ path: "/01.Java相关/40.设计模式/030.责任链模式.html", redirect: "/pages/d3ff35/" }, + { + name: "v-64f1031c", + path: "/pages/cbff3c/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-64f1031c").then(next) + }, + }, + { + path: "/pages/cbff3c/index.html", + redirect: "/pages/cbff3c/" + }, + { + path: "/01.Java相关/40.设计模式/060.观察模式.html", + redirect: "/pages/cbff3c/" + }, { name: "v-28deb9da", path: "/pages/43b656/", @@ -873,52 +905,52 @@ export const routes = [ redirect: "/pages/43b656/" }, { - name: "v-5b0157cf", - path: "/pages/78c7aa/", + name: "v-2279e49d", + path: "/pages/a3c735/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-5b0157cf").then(next) + ensureAsyncComponentsLoaded("Layout", "v-2279e49d").then(next) }, }, { - path: "/pages/78c7aa/index.html", - redirect: "/pages/78c7aa/" + path: "/pages/a3c735/index.html", + redirect: "/pages/a3c735/" }, { - path: "/01.Java相关/40.设计模式/050.策略模式.html", - redirect: "/pages/78c7aa/" + path: "/01.Java相关/40.设计模式/070.代理模式.html", + redirect: "/pages/a3c735/" }, { - name: "v-64f1031c", - path: "/pages/cbff3c/", + name: "v-5b0157cf", + path: "/pages/78c7aa/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-64f1031c").then(next) + ensureAsyncComponentsLoaded("Layout", "v-5b0157cf").then(next) }, }, { - path: "/pages/cbff3c/index.html", - redirect: "/pages/cbff3c/" + path: "/pages/78c7aa/index.html", + redirect: "/pages/78c7aa/" }, { - path: "/01.Java相关/40.设计模式/060.观察模式.html", - redirect: "/pages/cbff3c/" + path: "/01.Java相关/40.设计模式/050.策略模式.html", + redirect: "/pages/78c7aa/" }, { - name: "v-2279e49d", - path: "/pages/a3c735/", + name: "v-1e653940", + path: "/pages/e00207/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-2279e49d").then(next) + ensureAsyncComponentsLoaded("Layout", "v-1e653940").then(next) }, }, { - path: "/pages/a3c735/index.html", - redirect: "/pages/a3c735/" + path: "/pages/e00207/index.html", + redirect: "/pages/e00207/" }, { - path: "/01.Java相关/40.设计模式/070.代理模式.html", - redirect: "/pages/a3c735/" + path: "/02.计算机基础/10.计算机网络/01.计算机网络.html", + redirect: "/pages/e00207/" }, { name: "v-dd5f4440", @@ -937,20 +969,36 @@ export const routes = [ redirect: "/pages/c332ae/" }, { - name: "v-1e653940", - path: "/pages/e00207/", + name: "v-0a1d1fb4", + path: "/pages/f33310/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-1e653940").then(next) + ensureAsyncComponentsLoaded("Layout", "v-0a1d1fb4").then(next) }, }, { - path: "/pages/e00207/index.html", - redirect: "/pages/e00207/" + path: "/pages/f33310/index.html", + redirect: "/pages/f33310/" }, { - path: "/02.计算机基础/10.计算机网络/01.计算机网络.html", - redirect: "/pages/e00207/" + path: "/02.计算机基础/10.计算机网络/010.简述常见的 HTTP 状态码的含义.html", + redirect: "/pages/f33310/" + }, + { + name: "v-6602e3da", + path: "/pages/ab48ba/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-6602e3da").then(next) + }, + }, + { + path: "/pages/ab48ba/index.html", + redirect: "/pages/ab48ba/" + }, + { + path: "/02.计算机基础/10.计算机网络/020.从输入 URL 到展现页面的全过程.html", + redirect: "/pages/ab48ba/" }, { name: "v-6f178cbc", @@ -1001,36 +1049,36 @@ export const routes = [ redirect: "/pages/92288a/" }, { - name: "v-674454ac", - path: "/pages/3a8659/", + name: "v-f0f711c4", + path: "/pages/a5adb0/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-674454ac").then(next) + ensureAsyncComponentsLoaded("Layout", "v-f0f711c4").then(next) }, }, { - path: "/pages/3a8659/index.html", - redirect: "/pages/3a8659/" + path: "/pages/a5adb0/index.html", + redirect: "/pages/a5adb0/" }, { - path: "/03.数据库/01.MYSQL相关/001.MySQL中 InnoDB 和 MylSAM 的区别是什么?.html", - redirect: "/pages/3a8659/" + path: "/03.数据库/01.MYSQL相关/006.聚簇索引和非聚簇索引有什么区别?.html", + redirect: "/pages/a5adb0/" }, { - name: "v-f0f711c4", - path: "/pages/a5adb0/", + name: "v-674454ac", + path: "/pages/3a8659/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-f0f711c4").then(next) + ensureAsyncComponentsLoaded("Layout", "v-674454ac").then(next) }, }, { - path: "/pages/a5adb0/index.html", - redirect: "/pages/a5adb0/" + path: "/pages/3a8659/index.html", + redirect: "/pages/3a8659/" }, { - path: "/03.数据库/01.MYSQL相关/006.聚簇索引和非聚簇索引有什么区别?.html", - redirect: "/pages/a5adb0/" + path: "/03.数据库/01.MYSQL相关/001.MySQL中 InnoDB 和 MylSAM 的区别是什么?.html", + redirect: "/pages/3a8659/" }, { name: "v-824b776c", @@ -1049,36 +1097,36 @@ export const routes = [ redirect: "/pages/17b014/" }, { - name: "v-7e02c4a1", - path: "/pages/9f2936/", + name: "v-5b0e09c6", + path: "/pages/b17e88/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-7e02c4a1").then(next) + ensureAsyncComponentsLoaded("Layout", "v-5b0e09c6").then(next) }, }, { - path: "/pages/9f2936/index.html", - redirect: "/pages/9f2936/" + path: "/pages/b17e88/index.html", + redirect: "/pages/b17e88/" }, { - path: "/03.数据库/01.MYSQL相关/010.简述Mysql的事务隔离级别.html", - redirect: "/pages/9f2936/" + path: "/03.数据库/01.MYSQL相关/015.说一下Mysql锁.html", + redirect: "/pages/b17e88/" }, { - name: "v-5b0e09c6", - path: "/pages/b17e88/", + name: "v-7e02c4a1", + path: "/pages/9f2936/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-5b0e09c6").then(next) + ensureAsyncComponentsLoaded("Layout", "v-7e02c4a1").then(next) }, }, { - path: "/pages/b17e88/index.html", - redirect: "/pages/b17e88/" + path: "/pages/9f2936/index.html", + redirect: "/pages/9f2936/" }, { - path: "/03.数据库/01.MYSQL相关/015.说一下Mysql锁.html", - redirect: "/pages/b17e88/" + path: "/03.数据库/01.MYSQL相关/010.简述Mysql的事务隔离级别.html", + redirect: "/pages/9f2936/" }, { name: "v-6c681639", @@ -1129,36 +1177,36 @@ export const routes = [ redirect: "/pages/36672c/" }, { - name: "v-4ecddbec", - path: "/pages/7a1f1e/", + name: "v-505fbe2e", + path: "/pages/52773a/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-4ecddbec").then(next) + ensureAsyncComponentsLoaded("Layout", "v-505fbe2e").then(next) }, }, { - path: "/pages/7a1f1e/index.html", - redirect: "/pages/7a1f1e/" + path: "/pages/52773a/index.html", + redirect: "/pages/52773a/" }, { - path: "/03.数据库/01.MYSQL相关/050.简述什么是最左匹配原则.html", - redirect: "/pages/7a1f1e/" + path: "/03.数据库/01.MYSQL相关/060.MySQL 联合索引底层原理是什么?.html", + redirect: "/pages/52773a/" }, { - name: "v-505fbe2e", - path: "/pages/52773a/", + name: "v-53bcbb7a", + path: "/pages/f2dbbe/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-505fbe2e").then(next) + ensureAsyncComponentsLoaded("Layout", "v-53bcbb7a").then(next) }, }, { - path: "/pages/52773a/index.html", - redirect: "/pages/52773a/" + path: "/pages/f2dbbe/index.html", + redirect: "/pages/f2dbbe/" }, { - path: "/03.数据库/01.MYSQL相关/060.MySQL 联合索引底层原理是什么?.html", - redirect: "/pages/52773a/" + path: "/03.数据库/01.MYSQL相关/080.简述常见的负载均衡算法.html", + redirect: "/pages/f2dbbe/" }, { name: "v-0dd47f09", @@ -1177,20 +1225,20 @@ export const routes = [ redirect: "/pages/f4cf50/" }, { - name: "v-53bcbb7a", - path: "/pages/f2dbbe/", + name: "v-4ecddbec", + path: "/pages/7a1f1e/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-53bcbb7a").then(next) + ensureAsyncComponentsLoaded("Layout", "v-4ecddbec").then(next) }, }, { - path: "/pages/f2dbbe/index.html", - redirect: "/pages/f2dbbe/" + path: "/pages/7a1f1e/index.html", + redirect: "/pages/7a1f1e/" }, { - path: "/03.数据库/01.MYSQL相关/080.简述常见的负载均衡算法.html", - redirect: "/pages/f2dbbe/" + path: "/03.数据库/01.MYSQL相关/050.简述什么是最左匹配原则.html", + redirect: "/pages/7a1f1e/" }, { name: "v-c383b96e", @@ -1208,22 +1256,6 @@ export const routes = [ path: "/03.数据库/01.MYSQL相关/090.简述 MySQL 的主从同步机制,如果同步失败会怎么样?.html", redirect: "/pages/1ac30e/" }, - { - name: "v-4cef944a", - path: "/pages/a8eb47/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-4cef944a").then(next) - }, - }, - { - path: "/pages/a8eb47/index.html", - redirect: "/pages/a8eb47/" - }, - { - path: "/03.数据库/01.MYSQL相关/100.如何设计数据库压测方案?.html", - redirect: "/pages/a8eb47/" - }, { name: "v-ed0c93b8", path: "/pages/ac0bb8/", @@ -1273,20 +1305,20 @@ export const routes = [ redirect: "/pages/7bbbf6/" }, { - name: "v-77790024", - path: "/pages/429100/", + name: "v-4cef944a", + path: "/pages/a8eb47/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-77790024").then(next) + ensureAsyncComponentsLoaded("Layout", "v-4cef944a").then(next) }, }, { - path: "/pages/429100/index.html", - redirect: "/pages/429100/" + path: "/pages/a8eb47/index.html", + redirect: "/pages/a8eb47/" }, { - path: "/03.数据库/01.MYSQL相关/150.MySQL 中 join 与 left join 的区别是什么?.html", - redirect: "/pages/429100/" + path: "/03.数据库/01.MYSQL相关/100.如何设计数据库压测方案?.html", + redirect: "/pages/a8eb47/" }, { name: "v-ceed6ba6", @@ -1305,20 +1337,20 @@ export const routes = [ redirect: "/pages/b4338e/" }, { - name: "v-76f5e79a", - path: "/pages/1656d8/", + name: "v-77790024", + path: "/pages/429100/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-76f5e79a").then(next) + ensureAsyncComponentsLoaded("Layout", "v-77790024").then(next) }, }, { - path: "/pages/1656d8/index.html", - redirect: "/pages/1656d8/" - }, + path: "/pages/429100/index.html", + redirect: "/pages/429100/" + }, { - path: "/03.数据库/01.MYSQL相关/160.简述 MySQL MVCC 的实现原理 .html", - redirect: "/pages/1656d8/" + path: "/03.数据库/01.MYSQL相关/150.MySQL 中 join 与 left join 的区别是什么?.html", + redirect: "/pages/429100/" }, { name: "v-0a7e6416", @@ -1336,6 +1368,22 @@ export const routes = [ path: "/03.数据库/01.MYSQL相关/170.MySQL 中 varchar 和 char 的区别是什么?.html", redirect: "/pages/d97948/" }, + { + name: "v-76f5e79a", + path: "/pages/1656d8/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-76f5e79a").then(next) + }, + }, + { + path: "/pages/1656d8/index.html", + redirect: "/pages/1656d8/" + }, + { + path: "/03.数据库/01.MYSQL相关/160.简述 MySQL MVCC 的实现原理 .html", + redirect: "/pages/1656d8/" + }, { name: "v-6b967a6e", path: "/pages/32696d/", @@ -1433,36 +1481,36 @@ export const routes = [ redirect: "/pages/7e708d/" }, { - name: "v-2c6b511c", - path: "/pages/656f46/", + name: "v-3d2fdd6a", + path: "/pages/338e30/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-2c6b511c").then(next) + ensureAsyncComponentsLoaded("Layout", "v-3d2fdd6a").then(next) }, }, { - path: "/pages/656f46/index.html", - redirect: "/pages/656f46/" + path: "/pages/338e30/index.html", + redirect: "/pages/338e30/" }, { - path: "/03.数据库/02.REDIS相关/030.Redis 如何实现分布式锁?.html", - redirect: "/pages/656f46/" + path: "/03.数据库/02.REDIS相关/040.简述 Redis 集群配置以及基础原理.html", + redirect: "/pages/338e30/" }, { - name: "v-3d2fdd6a", - path: "/pages/338e30/", + name: "v-2c6b511c", + path: "/pages/656f46/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-3d2fdd6a").then(next) + ensureAsyncComponentsLoaded("Layout", "v-2c6b511c").then(next) }, }, { - path: "/pages/338e30/index.html", - redirect: "/pages/338e30/" + path: "/pages/656f46/index.html", + redirect: "/pages/656f46/" }, { - path: "/03.数据库/02.REDIS相关/040.简述 Redis 集群配置以及基础原理.html", - redirect: "/pages/338e30/" + path: "/03.数据库/02.REDIS相关/030.Redis 如何实现分布式锁?.html", + redirect: "/pages/656f46/" }, { name: "v-18b427a0", @@ -1497,36 +1545,36 @@ export const routes = [ redirect: "/pages/e2e910/" }, { - name: "v-c2fe5e1a", - path: "/pages/03cd25/", + name: "v-35718f4e", + path: "/pages/9c28c9/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-c2fe5e1a").then(next) + ensureAsyncComponentsLoaded("Layout", "v-35718f4e").then(next) }, }, { - path: "/pages/03cd25/index.html", - redirect: "/pages/03cd25/" + path: "/pages/9c28c9/index.html", + redirect: "/pages/9c28c9/" }, { - path: "/03.数据库/02.REDIS相关/070.简述 Redis 中如何防止缓存雪崩和缓存击穿.html", - redirect: "/pages/03cd25/" + path: "/03.数据库/02.REDIS相关/080.简述 Redis 的线程模型以及底层架构设计.html", + redirect: "/pages/9c28c9/" }, { - name: "v-35718f4e", - path: "/pages/9c28c9/", + name: "v-c2fe5e1a", + path: "/pages/03cd25/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-35718f4e").then(next) + ensureAsyncComponentsLoaded("Layout", "v-c2fe5e1a").then(next) }, }, { - path: "/pages/9c28c9/index.html", - redirect: "/pages/9c28c9/" + path: "/pages/03cd25/index.html", + redirect: "/pages/03cd25/" }, { - path: "/03.数据库/02.REDIS相关/080.简述 Redis 的线程模型以及底层架构设计.html", - redirect: "/pages/9c28c9/" + path: "/03.数据库/02.REDIS相关/070.简述 Redis 中如何防止缓存雪崩和缓存击穿.html", + redirect: "/pages/03cd25/" }, { name: "v-57957421", @@ -1560,22 +1608,6 @@ export const routes = [ path: "/03.数据库/02.REDIS相关/110.简述 Redis 如何处理热点 key 访问.html", redirect: "/pages/795c09/" }, - { - name: "v-02ea33c3", - path: "/pages/0d3c6b/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-02ea33c3").then(next) - }, - }, - { - path: "/pages/0d3c6b/index.html", - redirect: "/pages/0d3c6b/" - }, - { - path: "/03.数据库/02.REDIS相关/120.Redis 序列化有哪些方式?.html", - redirect: "/pages/0d3c6b/" - }, { name: "v-be02b3c0", path: "/pages/40f3c9/", @@ -1593,20 +1625,20 @@ export const routes = [ redirect: "/pages/40f3c9/" }, { - name: "v-073f22cd", - path: "/pages/59089d/", + name: "v-02ea33c3", + path: "/pages/0d3c6b/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-073f22cd").then(next) + ensureAsyncComponentsLoaded("Layout", "v-02ea33c3").then(next) }, }, { - path: "/pages/59089d/index.html", - redirect: "/pages/59089d/" + path: "/pages/0d3c6b/index.html", + redirect: "/pages/0d3c6b/" }, { - path: "/04.中间件/10.Netty/015.Netty中有哪些解码器?.html", - redirect: "/pages/59089d/" + path: "/03.数据库/02.REDIS相关/120.Redis 序列化有哪些方式?.html", + redirect: "/pages/0d3c6b/" }, { name: "v-5acdab4d", @@ -1624,6 +1656,22 @@ export const routes = [ path: "/04.中间件/10.Netty/010.TCP粘包拆包问题的解决之道.html", redirect: "/pages/3db019/" }, + { + name: "v-073f22cd", + path: "/pages/59089d/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-073f22cd").then(next) + }, + }, + { + path: "/pages/59089d/index.html", + redirect: "/pages/59089d/" + }, + { + path: "/04.中间件/10.Netty/015.Netty中有哪些解码器?.html", + redirect: "/pages/59089d/" + }, { name: "v-bbcf6e5a", path: "/pages/ef17ee/", @@ -1689,36 +1737,36 @@ export const routes = [ redirect: "/pages/bfdcc8/" }, { - name: "v-0df39df8", - path: "/pages/561f34/", + name: "v-7c0136cf", + path: "/pages/fe7ca2/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-0df39df8").then(next) + ensureAsyncComponentsLoaded("Layout", "v-7c0136cf").then(next) }, }, { - path: "/pages/561f34/index.html", - redirect: "/pages/561f34/" + path: "/pages/fe7ca2/index.html", + redirect: "/pages/fe7ca2/" }, { - path: "/04.中间件/40.Kafka/001.Kafka包含那几部分.html", - redirect: "/pages/561f34/" + path: "/04.中间件/40.Kafka/010.生产者.html", + redirect: "/pages/fe7ca2/" }, { - name: "v-7c0136cf", - path: "/pages/fe7ca2/", + name: "v-0df39df8", + path: "/pages/561f34/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-7c0136cf").then(next) + ensureAsyncComponentsLoaded("Layout", "v-0df39df8").then(next) }, }, { - path: "/pages/fe7ca2/index.html", - redirect: "/pages/fe7ca2/" + path: "/pages/561f34/index.html", + redirect: "/pages/561f34/" }, { - path: "/04.中间件/40.Kafka/010.生产者.html", - redirect: "/pages/fe7ca2/" + path: "/04.中间件/40.Kafka/001.Kafka包含那几部分.html", + redirect: "/pages/561f34/" }, { name: "v-04af9e42", @@ -1784,6 +1832,22 @@ export const routes = [ path: "/08.系统设计/001.系统设计.html", redirect: "/pages/01b9b2/" }, + { + name: "v-5675b06b", + path: "/pages/20c647/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-5675b06b").then(next) + }, + }, + { + path: "/pages/20c647/index.html", + redirect: "/pages/20c647/" + }, + { + path: "/08.系统设计/010.什么是 CAP ?什么是最终一致性?什么是幂等操作?.html", + redirect: "/pages/20c647/" + }, { name: "v-f88544f4", path: "/pages/4eae5b/", @@ -1801,20 +1865,20 @@ export const routes = [ redirect: "/pages/4eae5b/" }, { - name: "v-5675b06b", - path: "/pages/20c647/", + name: "v-918dfaf8", + path: "/about/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-5675b06b").then(next) + ensureAsyncComponentsLoaded("Layout", "v-918dfaf8").then(next) }, }, { - path: "/pages/20c647/index.html", - redirect: "/pages/20c647/" + path: "/about/index.html", + redirect: "/about/" }, { - path: "/08.系统设计/010.什么是 CAP ?什么是最终一致性?什么是幂等操作?.html", - redirect: "/pages/20c647/" + path: "/15.关于/01.关于.html", + redirect: "/about/" }, { name: "v-2a52a762", @@ -1833,52 +1897,52 @@ export const routes = [ redirect: "/pages/5e507a/" }, { - name: "v-918dfaf8", - path: "/about/", + name: "v-48f7eec0", + path: "/archives/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-918dfaf8").then(next) + ensureAsyncComponentsLoaded("Layout", "v-48f7eec0").then(next) }, }, { - path: "/about/index.html", - redirect: "/about/" + path: "/archives/index.html", + redirect: "/archives/" }, { - path: "/15.关于/01.关于.html", - redirect: "/about/" + path: "/@pages/archivesPage.html", + redirect: "/archives/" }, { - name: "v-48f7eec0", - path: "/archives/", + name: "v-7ab50b9f", + path: "/pages/beb6c0bd8a66cea6/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-48f7eec0").then(next) + ensureAsyncComponentsLoaded("Layout", "v-7ab50b9f").then(next) }, }, { - path: "/archives/index.html", - redirect: "/archives/" + path: "/pages/beb6c0bd8a66cea6/index.html", + redirect: "/pages/beb6c0bd8a66cea6/" }, { - path: "/@pages/archivesPage.html", - redirect: "/archives/" + path: "/@pages/20.收藏夹/01.网站.html", + redirect: "/pages/beb6c0bd8a66cea6/" }, { - name: "v-09c150e6", - path: "/pages/47cf96/", + name: "v-a5c5f278", + path: "/pages/829589/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-09c150e6").then(next) + ensureAsyncComponentsLoaded("Layout", "v-a5c5f278").then(next) }, }, { - path: "/pages/47cf96/index.html", - redirect: "/pages/47cf96/" + path: "/pages/829589/index.html", + redirect: "/pages/829589/" }, { - path: "/@pages/20.收藏夹/02.常用的前端轮子.html", - redirect: "/pages/47cf96/" + path: "/_posts/随笔/我做了一个手写春联小网页,祝大家虎年暴富.html", + redirect: "/pages/829589/" }, { name: "v-2db11cb2", @@ -1913,52 +1977,32 @@ export const routes = [ redirect: "/pages/cd8bde/" }, { - name: "v-a5c5f278", - path: "/pages/829589/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-a5c5f278").then(next) - }, - }, - { - path: "/pages/829589/index.html", - redirect: "/pages/829589/" - }, - { - path: "/_posts/随笔/我做了一个手写春联小网页,祝大家虎年暴富.html", - redirect: "/pages/829589/" - }, - { - name: "v-7ab50b9f", - path: "/pages/beb6c0bd8a66cea6/", + name: "v-2cf94a0e", + path: "/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-7ab50b9f").then(next) + ensureAsyncComponentsLoaded("Layout", "v-2cf94a0e").then(next) }, }, { - path: "/pages/beb6c0bd8a66cea6/index.html", - redirect: "/pages/beb6c0bd8a66cea6/" - }, - { - path: "/@pages/20.收藏夹/01.网站.html", - redirect: "/pages/beb6c0bd8a66cea6/" + path: "/index.html", + redirect: "/" }, { - name: "v-5f8dfe80", - path: "/categories/", + name: "v-09c150e6", + path: "/pages/47cf96/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-5f8dfe80").then(next) + ensureAsyncComponentsLoaded("Layout", "v-09c150e6").then(next) }, }, { - path: "/categories/index.html", - redirect: "/categories/" + path: "/pages/47cf96/index.html", + redirect: "/pages/47cf96/" }, { - path: "/@pages/categoriesPage.html", - redirect: "/categories/" + path: "/@pages/20.收藏夹/02.常用的前端轮子.html", + redirect: "/pages/47cf96/" }, { name: "v-64f9b920", @@ -1977,16 +2021,20 @@ export const routes = [ redirect: "/tags/" }, { - name: "v-2cf94a0e", - path: "/", + name: "v-5f8dfe80", + path: "/categories/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-2cf94a0e").then(next) + ensureAsyncComponentsLoaded("Layout", "v-5f8dfe80").then(next) }, }, { - path: "/index.html", - redirect: "/" + path: "/categories/index.html", + redirect: "/categories/" + }, + { + path: "/@pages/categoriesPage.html", + redirect: "/categories/" }, { path: '*', diff --git a/node_modules/@vuepress/core/.temp/internal/siteData.js b/node_modules/@vuepress/core/.temp/internal/siteData.js index 77fb95d15..dd709efd3 100644 --- a/node_modules/@vuepress/core/.temp/internal/siteData.js +++ b/node_modules/@vuepress/core/.temp/internal/siteData.js @@ -37,28 +37,28 @@ export const siteData = { ], "pages": [ { - "title": "数据库", + "title": "Java相关", "frontmatter": { "pageComponent": { "name": "Catalogue", "data": { - "key": "03.数据库", - "imgUrl": "/img/other.png", - "description": "Mysql、Redis" + "key": "01.Java相关", + "imgUrl": "/img/web.png", + "description": "Java基础、JVM、并发、设计模式" } }, - "title": "数据库", - "date": "2020-03-11T21:50:55.000Z", - "permalink": "/database", + "title": "Java相关", + "date": "2020-03-11T21:50:53.000Z", + "permalink": "/java", "sidebar": false, "article": false, "comment": false, "editLink": false }, - "regularPath": "/00.%E7%9B%AE%E5%BD%95%E9%A1%B5/03.%E6%95%B0%E6%8D%AE%E5%BA%93.html", - "relativePath": "00.目录页/03.数据库.md", - "key": "v-a612ff08", - "path": "/database/", + "regularPath": "/00.%E7%9B%AE%E5%BD%95%E9%A1%B5/01.Java%E7%9B%B8%E5%85%B3.html", + "relativePath": "00.目录页/01.Java相关.md", + "key": "v-36edf5bb", + "path": "/java/", "headersStr": null, "content": "", "normalizedContent": "", @@ -97,28 +97,28 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "中间件", + "title": "数据库", "frontmatter": { "pageComponent": { "name": "Catalogue", "data": { - "key": "04.中间件", - "imgUrl": "/img/more.png", - "description": "Dubbo、Mybatis" + "key": "03.数据库", + "imgUrl": "/img/other.png", + "description": "Mysql、Redis" } }, - "title": "中间件", - "date": "2020-03-11T21:50:56.000Z", - "permalink": "/middleware", + "title": "数据库", + "date": "2020-03-11T21:50:55.000Z", + "permalink": "/database", "sidebar": false, "article": false, "comment": false, "editLink": false }, - "regularPath": "/00.%E7%9B%AE%E5%BD%95%E9%A1%B5/04.%E4%B8%AD%E9%97%B4%E4%BB%B6.html", - "relativePath": "00.目录页/04.中间件.md", - "key": "v-7abe58c4", - "path": "/middleware/", + "regularPath": "/00.%E7%9B%AE%E5%BD%95%E9%A1%B5/03.%E6%95%B0%E6%8D%AE%E5%BA%93.html", + "relativePath": "00.目录页/03.数据库.md", + "key": "v-a612ff08", + "path": "/database/", "headersStr": null, "content": "", "normalizedContent": "", @@ -127,28 +127,28 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "Java相关", + "title": "中间件", "frontmatter": { "pageComponent": { "name": "Catalogue", "data": { - "key": "01.Java相关", - "imgUrl": "/img/web.png", - "description": "Java基础、JVM、并发、设计模式" + "key": "04.中间件", + "imgUrl": "/img/more.png", + "description": "Dubbo、Mybatis" } }, - "title": "Java相关", - "date": "2020-03-11T21:50:53.000Z", - "permalink": "/java", + "title": "中间件", + "date": "2020-03-11T21:50:56.000Z", + "permalink": "/middleware", "sidebar": false, "article": false, "comment": false, "editLink": false }, - "regularPath": "/00.%E7%9B%AE%E5%BD%95%E9%A1%B5/01.Java%E7%9B%B8%E5%85%B3.html", - "relativePath": "00.目录页/01.Java相关.md", - "key": "v-36edf5bb", - "path": "/java/", + "regularPath": "/00.%E7%9B%AE%E5%BD%95%E9%A1%B5/04.%E4%B8%AD%E9%97%B4%E4%BB%B6.html", + "relativePath": "00.目录页/04.中间件.md", + "key": "v-7abe58c4", + "path": "/middleware/", "headersStr": null, "content": "", "normalizedContent": "", @@ -337,11 +337,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "String,StringBuffer,StringBuilder 之间有什么区别?", + "title": "简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度", "frontmatter": { - "title": "String,StringBuffer,StringBuilder 之间有什么区别?", - "date": "2022-04-04T14:24:07.000Z", - "permalink": "/pages/211d8c/", + "title": "简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度", + "date": "2022-04-04T14:24:34.000Z", + "permalink": "/pages/305919/", "categories": [ "Java相关", "基础" @@ -350,22 +350,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/050.String%EF%BC%8CStringBuffer%EF%BC%8CStringBuilder%20%E4%B9%8B%E9%97%B4%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F.html", - "relativePath": "01.Java相关/10.基础/050.String,StringBuffer,StringBuilder 之间有什么区别?.md", - "key": "v-7f48b402", - "path": "/pages/211d8c/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/060.%E7%AE%80%E8%BF%B0%20ArrayList%20%E4%B8%8E%20LinkedList%20%E7%9A%84%E5%BA%95%E5%B1%82%E5%AE%9E%E7%8E%B0%E4%BB%A5%E5%8F%8A%E5%B8%B8%E8%A7%81%E6%93%8D%E4%BD%9C%E7%9A%84%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6.html", + "relativePath": "01.Java相关/10.基础/060.简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度.md", + "key": "v-57b2551a", + "path": "/pages/305919/", "headers": [ { "level": 2, - "title": "String,StringBuffer,StringBuilder 之间有什么区别?", - "slug": "string-stringbuffer-stringbuilder-之间有什么区别", - "normalizedTitle": "string,stringbuffer,stringbuilder 之间有什么区别?", + "title": "简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度", + "slug": "简述-arraylist-与-linkedlist-的底层实现以及常见操作的时间复杂度", + "normalizedTitle": "简述 arraylist 与 linkedlist 的底层实现以及常见操作的时间复杂度", "charIndex": 2 } ], - "headersStr": "String,StringBuffer,StringBuilder 之间有什么区别?", - "content": "# String,StringBuffer,StringBuilder 之间有什么区别?\n\n * String 对象是不可变的。String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[]。\n * StringBuffer,StringBuilder是可变的\n * StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 是线程不安全的。\n\n STRING STRINGBUFFER STRINGBUILDER\n可变性 不可变 可变 可变\n线程安全性 安全 安全 不安全\n使用场景 操作少量数据 多线程操作字符串缓冲区下操作大量数据 单线程操作字符串缓冲区下操作大量数据", - "normalizedContent": "# string,stringbuffer,stringbuilder 之间有什么区别?\n\n * string 对象是不可变的。string 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[]。\n * stringbuffer,stringbuilder是可变的\n * stringbuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。stringbuilder 是线程不安全的。\n\n string stringbuffer stringbuilder\n可变性 不可变 可变 可变\n线程安全性 安全 安全 不安全\n使用场景 操作少量数据 多线程操作字符串缓冲区下操作大量数据 单线程操作字符串缓冲区下操作大量数据", + "headersStr": "简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度", + "content": "# 简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度\n\n ARRAYLIST LINKEDLIST\n线程安全 不安全 不安全\n底层结构 Object 数组 双向链表\n适用场景 按照数组下表快速访问元素 时间复杂度就是 O(1) 顺序访问元素\n不适用场景 在指定位置n插入、删除, 会导致位置n后面的元素移动 时间复杂度就是 O(n-1) 随机访问元素", + "normalizedContent": "# 简述 arraylist 与 linkedlist 的底层实现以及常见操作的时间复杂度\n\n arraylist linkedlist\n线程安全 不安全 不安全\n底层结构 object 数组 双向链表\n适用场景 按照数组下表快速访问元素 时间复杂度就是 o(1) 顺序访问元素\n不适用场景 在指定位置n插入、删除, 会导致位置n后面的元素移动 时间复杂度就是 o(n-1) 随机访问元素", "charsets": { "cjk": true }, @@ -373,11 +373,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度", + "title": "String,StringBuffer,StringBuilder 之间有什么区别?", "frontmatter": { - "title": "简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度", - "date": "2022-04-04T14:24:34.000Z", - "permalink": "/pages/305919/", + "title": "String,StringBuffer,StringBuilder 之间有什么区别?", + "date": "2022-04-04T14:24:07.000Z", + "permalink": "/pages/211d8c/", "categories": [ "Java相关", "基础" @@ -386,22 +386,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/060.%E7%AE%80%E8%BF%B0%20ArrayList%20%E4%B8%8E%20LinkedList%20%E7%9A%84%E5%BA%95%E5%B1%82%E5%AE%9E%E7%8E%B0%E4%BB%A5%E5%8F%8A%E5%B8%B8%E8%A7%81%E6%93%8D%E4%BD%9C%E7%9A%84%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6.html", - "relativePath": "01.Java相关/10.基础/060.简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度.md", - "key": "v-57b2551a", - "path": "/pages/305919/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/050.String%EF%BC%8CStringBuffer%EF%BC%8CStringBuilder%20%E4%B9%8B%E9%97%B4%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F.html", + "relativePath": "01.Java相关/10.基础/050.String,StringBuffer,StringBuilder 之间有什么区别?.md", + "key": "v-7f48b402", + "path": "/pages/211d8c/", "headers": [ { "level": 2, - "title": "简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度", - "slug": "简述-arraylist-与-linkedlist-的底层实现以及常见操作的时间复杂度", - "normalizedTitle": "简述 arraylist 与 linkedlist 的底层实现以及常见操作的时间复杂度", + "title": "String,StringBuffer,StringBuilder 之间有什么区别?", + "slug": "string-stringbuffer-stringbuilder-之间有什么区别", + "normalizedTitle": "string,stringbuffer,stringbuilder 之间有什么区别?", "charIndex": 2 } ], - "headersStr": "简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度", - "content": "# 简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度\n\n ARRAYLIST LINKEDLIST\n线程安全 不安全 不安全\n底层结构 Object 数组 双向链表\n适用场景 按照数组下表快速访问元素 时间复杂度就是 O(1) 顺序访问元素\n不适用场景 在指定位置n插入、删除, 会导致位置n后面的元素移动 时间复杂度就是 O(n-1) 随机访问元素", - "normalizedContent": "# 简述 arraylist 与 linkedlist 的底层实现以及常见操作的时间复杂度\n\n arraylist linkedlist\n线程安全 不安全 不安全\n底层结构 object 数组 双向链表\n适用场景 按照数组下表快速访问元素 时间复杂度就是 o(1) 顺序访问元素\n不适用场景 在指定位置n插入、删除, 会导致位置n后面的元素移动 时间复杂度就是 o(n-1) 随机访问元素", + "headersStr": "String,StringBuffer,StringBuilder 之间有什么区别?", + "content": "# String,StringBuffer,StringBuilder 之间有什么区别?\n\n * String 对象是不可变的。String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[]。\n * StringBuffer,StringBuilder是可变的\n * StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 是线程不安全的。\n\n STRING STRINGBUFFER STRINGBUILDER\n可变性 不可变 可变 可变\n线程安全性 安全 安全 不安全\n使用场景 操作少量数据 多线程操作字符串缓冲区下操作大量数据 单线程操作字符串缓冲区下操作大量数据", + "normalizedContent": "# string,stringbuffer,stringbuilder 之间有什么区别?\n\n * string 对象是不可变的。string 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[]。\n * stringbuffer,stringbuilder是可变的\n * stringbuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。stringbuilder 是线程不安全的。\n\n string stringbuffer stringbuilder\n可变性 不可变 可变 可变\n线程安全性 安全 安全 不安全\n使用场景 操作少量数据 多线程操作字符串缓冲区下操作大量数据 单线程操作字符串缓冲区下操作大量数据", "charsets": { "cjk": true }, @@ -445,11 +445,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "简述 Java 的反射机制及其应用场景", + "title": "在一个静态方法内调用一个非静态成员为什么是非法的?", "frontmatter": { - "title": "简述 Java 的反射机制及其应用场景", - "date": "2022-04-04T14:25:59.000Z", - "permalink": "/pages/fae5d5/", + "title": "在一个静态方法内调用一个非静态成员为什么是非法的?", + "date": "2022-04-04T14:25:35.000Z", + "permalink": "/pages/56314c/", "categories": [ "Java相关", "基础" @@ -458,22 +458,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/090.%E7%AE%80%E8%BF%B0%20Java%20%E7%9A%84%E5%8F%8D%E5%B0%84%E6%9C%BA%E5%88%B6%E5%8F%8A%E5%85%B6%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF.html", - "relativePath": "01.Java相关/10.基础/090.简述 Java 的反射机制及其应用场景.md", - "key": "v-85b06162", - "path": "/pages/fae5d5/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/080.%E5%9C%A8%E4%B8%80%E4%B8%AA%E9%9D%99%E6%80%81%E6%96%B9%E6%B3%95%E5%86%85%E8%B0%83%E7%94%A8%E4%B8%80%E4%B8%AA%E9%9D%9E%E9%9D%99%E6%80%81%E6%88%90%E5%91%98%E4%B8%BA%E4%BB%80%E4%B9%88%E6%98%AF%E9%9D%9E%E6%B3%95%E7%9A%84%EF%BC%9F.html", + "relativePath": "01.Java相关/10.基础/080.在一个静态方法内调用一个非静态成员为什么是非法的?.md", + "key": "v-d32e07ea", + "path": "/pages/56314c/", "headers": [ { "level": 2, - "title": "简述 Java 的反射机制及其应用场景", - "slug": "简述-java-的反射机制及其应用场景", - "normalizedTitle": "简述 java 的反射机制及其应用场景", + "title": "在一个静态方法内调用一个非静态成员为什么是非法的?", + "slug": "在一个静态方法内调用一个非静态成员为什么是非法的", + "normalizedTitle": "在一个静态方法内调用一个非静态成员为什么是非法的?", "charIndex": 2 } ], - "headersStr": "简述 Java 的反射机制及其应用场景", - "content": "# 简述 Java 的反射机制及其应用场景\n\n*** Java 的反射机制 ***\n这个机制允许程序在运行时透过Reflection APIs取得任何一个已知名称的class的内部信息\n包括其modifiers(诸如public, static 等)、superclass(例如Object)、实现之interfaces(例如Cloneable),也包括fields和methods的所有信息,并可于运行时改变fields内容或唤起methods。\nJava反射机制容许程序在运行时加载、探知、使用编译期间完全未知的classes。\n换言之,Java可以加载一个运行时才得知名称的class,获得其完整结构。\n\n\n*** 应用场景 ***\nArthas\nRPC框架中的 动态代理\nSpring 加载 XML\nJava 动态代理\nJDBC 驱动加载\n", - "normalizedContent": "# 简述 java 的反射机制及其应用场景\n\n*** java 的反射机制 ***\n这个机制允许程序在运行时透过reflection apis取得任何一个已知名称的class的内部信息\n包括其modifiers(诸如public, static 等)、superclass(例如object)、实现之interfaces(例如cloneable),也包括fields和methods的所有信息,并可于运行时改变fields内容或唤起methods。\njava反射机制容许程序在运行时加载、探知、使用编译期间完全未知的classes。\n换言之,java可以加载一个运行时才得知名称的class,获得其完整结构。\n\n\n*** 应用场景 ***\narthas\nrpc框架中的 动态代理\nspring 加载 xml\njava 动态代理\njdbc 驱动加载\n", + "headersStr": "在一个静态方法内调用一个非静态成员为什么是非法的?", + "content": "# 在一个静态方法内调用一个非静态成员为什么是非法的?\n\n从java的生命周期角度来说,静态方法的初始化早于非静态方法。\n\n 静态方法 非静态成员\n初始化阶段 准备 使用", + "normalizedContent": "# 在一个静态方法内调用一个非静态成员为什么是非法的?\n\n从java的生命周期角度来说,静态方法的初始化早于非静态方法。\n\n 静态方法 非静态成员\n初始化阶段 准备 使用", "charsets": { "cjk": true }, @@ -481,11 +481,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "Java 类的加载流程是怎样的?", + "title": "简述 Java 的反射机制及其应用场景", "frontmatter": { - "title": "Java 类的加载流程是怎样的?", - "date": "2022-04-04T14:26:24.000Z", - "permalink": "/pages/33c5d6/", + "title": "简述 Java 的反射机制及其应用场景", + "date": "2022-04-04T14:25:59.000Z", + "permalink": "/pages/fae5d5/", "categories": [ "Java相关", "基础" @@ -494,22 +494,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/100.Java%20%E7%B1%BB%E7%9A%84%E5%8A%A0%E8%BD%BD%E6%B5%81%E7%A8%8B%E6%98%AF%E6%80%8E%E6%A0%B7%E7%9A%84%EF%BC%9F.html", - "relativePath": "01.Java相关/10.基础/100.Java 类的加载流程是怎样的?.md", - "key": "v-4c273a66", - "path": "/pages/33c5d6/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/090.%E7%AE%80%E8%BF%B0%20Java%20%E7%9A%84%E5%8F%8D%E5%B0%84%E6%9C%BA%E5%88%B6%E5%8F%8A%E5%85%B6%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF.html", + "relativePath": "01.Java相关/10.基础/090.简述 Java 的反射机制及其应用场景.md", + "key": "v-85b06162", + "path": "/pages/fae5d5/", "headers": [ { "level": 2, - "title": "Java 类的加载流程是怎样的?", - "slug": "java-类的加载流程是怎样的", - "normalizedTitle": "java 类的加载流程是怎样的?", + "title": "简述 Java 的反射机制及其应用场景", + "slug": "简述-java-的反射机制及其应用场景", + "normalizedTitle": "简述 java 的反射机制及其应用场景", "charIndex": 2 } ], - "headersStr": "Java 类的加载流程是怎样的?", - "content": "# Java 类的加载流程是怎样的?\n\n类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。 其中类加载过程包括加载、验证、准备、解析和初始化五个阶段。\n\n 做什么\n加载 类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例\n验证 验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等\n准备 为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内)\n 被final修饰的静态变量,会直接赋予原值;类字段的字段属性表中存在ConstantValue属性,则在准备阶段,其值就是ConstantValue的值\n解析 可以认为是一些静态绑定的会被解析,动态绑定则只会在运行是进行解析;静态绑定包括一些final方法(不可以重写),static方法(只会属于当前类),构造器(不会被重写)\n初始化 初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。", - "normalizedContent": "# java 类的加载流程是怎样的?\n\n类从被加载到jvm中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。 其中类加载过程包括加载、验证、准备、解析和初始化五个阶段。\n\n 做什么\n加载 类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到jvm内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.class对象实例\n验证 验证类数据信息是否符合jvm规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等\n准备 为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内)\n 被final修饰的静态变量,会直接赋予原值;类字段的字段属性表中存在constantvalue属性,则在准备阶段,其值就是constantvalue的值\n解析 可以认为是一些静态绑定的会被解析,动态绑定则只会在运行是进行解析;静态绑定包括一些final方法(不可以重写),static方法(只会属于当前类),构造器(不会被重写)\n初始化 初始化,为类的静态变量赋予正确的初始值,jvm负责对类进行初始化,主要对类变量进行初始化。", + "headersStr": "简述 Java 的反射机制及其应用场景", + "content": "# 简述 Java 的反射机制及其应用场景\n\n*** Java 的反射机制 ***\n这个机制允许程序在运行时透过Reflection APIs取得任何一个已知名称的class的内部信息\n包括其modifiers(诸如public, static 等)、superclass(例如Object)、实现之interfaces(例如Cloneable),也包括fields和methods的所有信息,并可于运行时改变fields内容或唤起methods。\nJava反射机制容许程序在运行时加载、探知、使用编译期间完全未知的classes。\n换言之,Java可以加载一个运行时才得知名称的class,获得其完整结构。\n\n\n*** 应用场景 ***\nArthas\nRPC框架中的 动态代理\nSpring 加载 XML\nJava 动态代理\nJDBC 驱动加载\n", + "normalizedContent": "# 简述 java 的反射机制及其应用场景\n\n*** java 的反射机制 ***\n这个机制允许程序在运行时透过reflection apis取得任何一个已知名称的class的内部信息\n包括其modifiers(诸如public, static 等)、superclass(例如object)、实现之interfaces(例如cloneable),也包括fields和methods的所有信息,并可于运行时改变fields内容或唤起methods。\njava反射机制容许程序在运行时加载、探知、使用编译期间完全未知的classes。\n换言之,java可以加载一个运行时才得知名称的class,获得其完整结构。\n\n\n*** 应用场景 ***\narthas\nrpc框架中的 动态代理\nspring 加载 xml\njava 动态代理\njdbc 驱动加载\n", "charsets": { "cjk": true }, @@ -517,11 +517,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "在一个静态方法内调用一个非静态成员为什么是非法的?", + "title": "什么是双亲委派机制?", "frontmatter": { - "title": "在一个静态方法内调用一个非静态成员为什么是非法的?", - "date": "2022-04-04T14:25:35.000Z", - "permalink": "/pages/56314c/", + "title": "什么是双亲委派机制?", + "date": "2022-04-04T14:26:57.000Z", + "permalink": "/pages/38bcaf/", "categories": [ "Java相关", "基础" @@ -530,22 +530,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/080.%E5%9C%A8%E4%B8%80%E4%B8%AA%E9%9D%99%E6%80%81%E6%96%B9%E6%B3%95%E5%86%85%E8%B0%83%E7%94%A8%E4%B8%80%E4%B8%AA%E9%9D%9E%E9%9D%99%E6%80%81%E6%88%90%E5%91%98%E4%B8%BA%E4%BB%80%E4%B9%88%E6%98%AF%E9%9D%9E%E6%B3%95%E7%9A%84%EF%BC%9F.html", - "relativePath": "01.Java相关/10.基础/080.在一个静态方法内调用一个非静态成员为什么是非法的?.md", - "key": "v-d32e07ea", - "path": "/pages/56314c/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/110.%E4%BB%80%E4%B9%88%E6%98%AF%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE%E6%9C%BA%E5%88%B6%EF%BC%9F.html", + "relativePath": "01.Java相关/10.基础/110.什么是双亲委派机制?.md", + "key": "v-7665e30e", + "path": "/pages/38bcaf/", "headers": [ { "level": 2, - "title": "在一个静态方法内调用一个非静态成员为什么是非法的?", - "slug": "在一个静态方法内调用一个非静态成员为什么是非法的", - "normalizedTitle": "在一个静态方法内调用一个非静态成员为什么是非法的?", + "title": "什么是双亲委派机制?", + "slug": "什么是双亲委派机制", + "normalizedTitle": "什么是双亲委派机制?", "charIndex": 2 } ], - "headersStr": "在一个静态方法内调用一个非静态成员为什么是非法的?", - "content": "# 在一个静态方法内调用一个非静态成员为什么是非法的?\n\n从java的生命周期角度来说,静态方法的初始化早于非静态方法。\n\n 静态方法 非静态成员\n初始化阶段 准备 使用", - "normalizedContent": "# 在一个静态方法内调用一个非静态成员为什么是非法的?\n\n从java的生命周期角度来说,静态方法的初始化早于非静态方法。\n\n 静态方法 非静态成员\n初始化阶段 准备 使用", + "headersStr": "什么是双亲委派机制?", + "content": "# 什么是双亲委派机制?\n\n双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。\n\n\n\n加载器 加载哪些类\nbootstrap classloader 加载jre/lib/rt.jar\nextension classloader jre/lib/ext/*.jar\napplication classloader 加载应用程序目录\n自定义ClassLoader 定制化加载\n\n再来读一下java.lang.ClassLoader这段代码 是不是通透了许多?\n\n public Class loadClass(String name) throws ClassNotFoundException {\n return loadClass(name, false);\n }\n // -----??-----\n protected Class loadClass(String name, boolean resolve)\n throws ClassNotFoundException\n {\n // 首先,检查是否已经被类加载器加载过\n Class c = findLoadedClass(name);\n if (c == null) {\n try {\n // 存在父加载器,递归的交由父加载器\n if (parent != null) {\n c = parent.loadClass(name, false);\n } else {\n // 直到最上面的Bootstrap类加载器\n c = findBootstrapClassOrNull(name);\n }\n } catch (ClassNotFoundException e) {\n // ClassNotFoundException thrown if class not found\n // from the non-null parent class loader\n }\n \n if (c == null) {\n // If still not found, then invoke findClass in order\n // to find the class.\n c = findClass(name);\n }\n }\n return c;\n }\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n\n\n> Q1:使用双亲委派有什么好处?\n> A:双亲委派机制能保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同。\n> 考虑到安全因素。\n> 假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。\n\n> Q2:什么场景需要破坏双亲委派?\n> 双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载)\n> 基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API 但没有绝对,如果基础类调用会用户的代码怎么办呢? 比如JDBC驱动加载。 这种场景就需要用到SPI机制,是不符合双亲委派的。\n\n> Q3:JDBC、Tomcat中为什么要破坏双亲委派模型\n> A:JDBC\n> 先来回顾一下JDBC的用法\n\nString url = \"jdbc:mysql:///consult?serverTimezone=UTC\";\nString user = \"root\";\nString password = \"root\";\n\nClass.forName(\"com.mysql.jdbc.Driver\");\nConnection connection = DriverManager.getConnection(url, user, password);\n\n\n1\n2\n3\n4\n5\n6\n\n\n> DriverManager 类中要加载各个实现了Driver接口的类,然后进行管理,DriverManager位于 JAVA_HOME中jre/lib/rt.jar 包,由BootStrap类加载器加载.\n> JDBC的Driver接口定义在JDK中,其实现由各个数据库的服务商来提供,比如MySQL驱动包位于服务商MySQL提供的 Jar 中包含Driver的实现,按照双亲委派模型应该由application classloader加载\n> 实际上却是由BootStrap类加载器加载。\n> 这是因为:根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类。\n> 这就破坏了双亲委派模型。\n\nTomcat\n每个webappClassLoader加载自己目录下的class文件\n\n> Tomcat支持部署多个Web应用,多个Web应用可能使用不同版本的JDK、也可能在应用内部定义全限定名一样的类,所以要做到应用隔离。", + "normalizedContent": "# 什么是双亲委派机制?\n\n双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。\n\n\n\n加载器 加载哪些类\nbootstrap classloader 加载jre/lib/rt.jar\nextension classloader jre/lib/ext/*.jar\napplication classloader 加载应用程序目录\n自定义classloader 定制化加载\n\n再来读一下java.lang.classloader这段代码 是不是通透了许多?\n\n public class loadclass(string name) throws classnotfoundexception {\n return loadclass(name, false);\n }\n // -----??-----\n protected class loadclass(string name, boolean resolve)\n throws classnotfoundexception\n {\n // 首先,检查是否已经被类加载器加载过\n class c = findloadedclass(name);\n if (c == null) {\n try {\n // 存在父加载器,递归的交由父加载器\n if (parent != null) {\n c = parent.loadclass(name, false);\n } else {\n // 直到最上面的bootstrap类加载器\n c = findbootstrapclassornull(name);\n }\n } catch (classnotfoundexception e) {\n // classnotfoundexception thrown if class not found\n // from the non-null parent class loader\n }\n \n if (c == null) {\n // if still not found, then invoke findclass in order\n // to find the class.\n c = findclass(name);\n }\n }\n return c;\n }\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n\n\n> q1:使用双亲委派有什么好处?\n> a:双亲委派机制能保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同。\n> 考虑到安全因素。\n> 假设通过网络传递一个名为java.lang.integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心java api发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.integer,而直接返回已加载过的integer.class,这样便可以防止核心api库被随意篡改。\n\n> q2:什么场景需要破坏双亲委派?\n> 双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载)\n> 基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的api 但没有绝对,如果基础类调用会用户的代码怎么办呢? 比如jdbc驱动加载。 这种场景就需要用到spi机制,是不符合双亲委派的。\n\n> q3:jdbc、tomcat中为什么要破坏双亲委派模型\n> a:jdbc\n> 先来回顾一下jdbc的用法\n\nstring url = \"jdbc:mysql:///consult?servertimezone=utc\";\nstring user = \"root\";\nstring password = \"root\";\n\nclass.forname(\"com.mysql.jdbc.driver\");\nconnection connection = drivermanager.getconnection(url, user, password);\n\n\n1\n2\n3\n4\n5\n6\n\n\n> drivermanager 类中要加载各个实现了driver接口的类,然后进行管理,drivermanager位于 java_home中jre/lib/rt.jar 包,由bootstrap类加载器加载.\n> jdbc的driver接口定义在jdk中,其实现由各个数据库的服务商来提供,比如mysql驱动包位于服务商mysql提供的 jar 中包含driver的实现,按照双亲委派模型应该由application classloader加载\n> 实际上却是由bootstrap类加载器加载。\n> 这是因为:根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类。\n> 这就破坏了双亲委派模型。\n\ntomcat\n每个webappclassloader加载自己目录下的class文件\n\n> tomcat支持部署多个web应用,多个web应用可能使用不同版本的jdk、也可能在应用内部定义全限定名一样的类,所以要做到应用隔离。", "charsets": { "cjk": true }, @@ -553,11 +553,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "什么是双亲委派机制?", + "title": "Java 类的加载流程是怎样的?", "frontmatter": { - "title": "什么是双亲委派机制?", - "date": "2022-04-04T14:26:57.000Z", - "permalink": "/pages/38bcaf/", + "title": "Java 类的加载流程是怎样的?", + "date": "2022-04-04T14:26:24.000Z", + "permalink": "/pages/33c5d6/", "categories": [ "Java相关", "基础" @@ -566,22 +566,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/110.%E4%BB%80%E4%B9%88%E6%98%AF%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE%E6%9C%BA%E5%88%B6%EF%BC%9F.html", - "relativePath": "01.Java相关/10.基础/110.什么是双亲委派机制?.md", - "key": "v-7665e30e", - "path": "/pages/38bcaf/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/100.Java%20%E7%B1%BB%E7%9A%84%E5%8A%A0%E8%BD%BD%E6%B5%81%E7%A8%8B%E6%98%AF%E6%80%8E%E6%A0%B7%E7%9A%84%EF%BC%9F.html", + "relativePath": "01.Java相关/10.基础/100.Java 类的加载流程是怎样的?.md", + "key": "v-4c273a66", + "path": "/pages/33c5d6/", "headers": [ { "level": 2, - "title": "什么是双亲委派机制?", - "slug": "什么是双亲委派机制", - "normalizedTitle": "什么是双亲委派机制?", + "title": "Java 类的加载流程是怎样的?", + "slug": "java-类的加载流程是怎样的", + "normalizedTitle": "java 类的加载流程是怎样的?", "charIndex": 2 } ], - "headersStr": "什么是双亲委派机制?", - "content": "# 什么是双亲委派机制?\n\n双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。\n\n\n\n加载器 加载哪些类\nbootstrap classloader 加载jre/lib/rt.jar\nextension classloader jre/lib/ext/*.jar\napplication classloader 加载应用程序目录\n自定义ClassLoader 定制化加载\n\n再来读一下java.lang.ClassLoader这段代码 是不是通透了许多?\n\n public Class loadClass(String name) throws ClassNotFoundException {\n return loadClass(name, false);\n }\n // -----??-----\n protected Class loadClass(String name, boolean resolve)\n throws ClassNotFoundException\n {\n // 首先,检查是否已经被类加载器加载过\n Class c = findLoadedClass(name);\n if (c == null) {\n try {\n // 存在父加载器,递归的交由父加载器\n if (parent != null) {\n c = parent.loadClass(name, false);\n } else {\n // 直到最上面的Bootstrap类加载器\n c = findBootstrapClassOrNull(name);\n }\n } catch (ClassNotFoundException e) {\n // ClassNotFoundException thrown if class not found\n // from the non-null parent class loader\n }\n \n if (c == null) {\n // If still not found, then invoke findClass in order\n // to find the class.\n c = findClass(name);\n }\n }\n return c;\n }\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n\n\n> Q1:使用双亲委派有什么好处?\n> A:双亲委派机制能保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同。\n> 考虑到安全因素。\n> 假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。\n\n> Q2:什么场景需要破坏双亲委派?\n> 双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载)\n> 基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API 但没有绝对,如果基础类调用会用户的代码怎么办呢? 比如JDBC驱动加载。 这种场景就需要用到SPI机制,是不符合双亲委派的。\n\n> Q3:JDBC、Tomcat中为什么要破坏双亲委派模型\n> A:JDBC\n> 先来回顾一下JDBC的用法\n\nString url = \"jdbc:mysql:///consult?serverTimezone=UTC\";\nString user = \"root\";\nString password = \"root\";\n\nClass.forName(\"com.mysql.jdbc.Driver\");\nConnection connection = DriverManager.getConnection(url, user, password);\n\n\n1\n2\n3\n4\n5\n6\n\n\n> DriverManager 类中要加载各个实现了Driver接口的类,然后进行管理,DriverManager位于 JAVA_HOME中jre/lib/rt.jar 包,由BootStrap类加载器加载.\n> JDBC的Driver接口定义在JDK中,其实现由各个数据库的服务商来提供,比如MySQL驱动包位于服务商MySQL提供的 Jar 中包含Driver的实现,按照双亲委派模型应该由application classloader加载\n> 实际上却是由BootStrap类加载器加载。\n> 这是因为:根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类。\n> 这就破坏了双亲委派模型。\n\nTomcat\n每个webappClassLoader加载自己目录下的class文件\n\n> Tomcat支持部署多个Web应用,多个Web应用可能使用不同版本的JDK、也可能在应用内部定义全限定名一样的类,所以要做到应用隔离。", - "normalizedContent": "# 什么是双亲委派机制?\n\n双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。\n\n\n\n加载器 加载哪些类\nbootstrap classloader 加载jre/lib/rt.jar\nextension classloader jre/lib/ext/*.jar\napplication classloader 加载应用程序目录\n自定义classloader 定制化加载\n\n再来读一下java.lang.classloader这段代码 是不是通透了许多?\n\n public class loadclass(string name) throws classnotfoundexception {\n return loadclass(name, false);\n }\n // -----??-----\n protected class loadclass(string name, boolean resolve)\n throws classnotfoundexception\n {\n // 首先,检查是否已经被类加载器加载过\n class c = findloadedclass(name);\n if (c == null) {\n try {\n // 存在父加载器,递归的交由父加载器\n if (parent != null) {\n c = parent.loadclass(name, false);\n } else {\n // 直到最上面的bootstrap类加载器\n c = findbootstrapclassornull(name);\n }\n } catch (classnotfoundexception e) {\n // classnotfoundexception thrown if class not found\n // from the non-null parent class loader\n }\n \n if (c == null) {\n // if still not found, then invoke findclass in order\n // to find the class.\n c = findclass(name);\n }\n }\n return c;\n }\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n\n\n> q1:使用双亲委派有什么好处?\n> a:双亲委派机制能保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同。\n> 考虑到安全因素。\n> 假设通过网络传递一个名为java.lang.integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心java api发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.integer,而直接返回已加载过的integer.class,这样便可以防止核心api库被随意篡改。\n\n> q2:什么场景需要破坏双亲委派?\n> 双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载)\n> 基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的api 但没有绝对,如果基础类调用会用户的代码怎么办呢? 比如jdbc驱动加载。 这种场景就需要用到spi机制,是不符合双亲委派的。\n\n> q3:jdbc、tomcat中为什么要破坏双亲委派模型\n> a:jdbc\n> 先来回顾一下jdbc的用法\n\nstring url = \"jdbc:mysql:///consult?servertimezone=utc\";\nstring user = \"root\";\nstring password = \"root\";\n\nclass.forname(\"com.mysql.jdbc.driver\");\nconnection connection = drivermanager.getconnection(url, user, password);\n\n\n1\n2\n3\n4\n5\n6\n\n\n> drivermanager 类中要加载各个实现了driver接口的类,然后进行管理,drivermanager位于 java_home中jre/lib/rt.jar 包,由bootstrap类加载器加载.\n> jdbc的driver接口定义在jdk中,其实现由各个数据库的服务商来提供,比如mysql驱动包位于服务商mysql提供的 jar 中包含driver的实现,按照双亲委派模型应该由application classloader加载\n> 实际上却是由bootstrap类加载器加载。\n> 这是因为:根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类。\n> 这就破坏了双亲委派模型。\n\ntomcat\n每个webappclassloader加载自己目录下的class文件\n\n> tomcat支持部署多个web应用,多个web应用可能使用不同版本的jdk、也可能在应用内部定义全限定名一样的类,所以要做到应用隔离。", + "headersStr": "Java 类的加载流程是怎样的?", + "content": "# Java 类的加载流程是怎样的?\n\n类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。 其中类加载过程包括加载、验证、准备、解析和初始化五个阶段。\n\n 做什么\n加载 类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例\n验证 验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等\n准备 为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内)\n 被final修饰的静态变量,会直接赋予原值;类字段的字段属性表中存在ConstantValue属性,则在准备阶段,其值就是ConstantValue的值\n解析 可以认为是一些静态绑定的会被解析,动态绑定则只会在运行是进行解析;静态绑定包括一些final方法(不可以重写),static方法(只会属于当前类),构造器(不会被重写)\n初始化 初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。", + "normalizedContent": "# java 类的加载流程是怎样的?\n\n类从被加载到jvm中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。 其中类加载过程包括加载、验证、准备、解析和初始化五个阶段。\n\n 做什么\n加载 类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到jvm内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.class对象实例\n验证 验证类数据信息是否符合jvm规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等\n准备 为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内)\n 被final修饰的静态变量,会直接赋予原值;类字段的字段属性表中存在constantvalue属性,则在准备阶段,其值就是constantvalue的值\n解析 可以认为是一些静态绑定的会被解析,动态绑定则只会在运行是进行解析;静态绑定包括一些final方法(不可以重写),static方法(只会属于当前类),构造器(不会被重写)\n初始化 初始化,为类的静态变量赋予正确的初始值,jvm负责对类进行初始化,主要对类变量进行初始化。", "charsets": { "cjk": true }, @@ -769,11 +769,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "阻塞队列都有哪几种,有什么区别?", + "title": "简述 HashSet 实现原理", "frontmatter": { - "title": "阻塞队列都有哪几种,有什么区别?", - "date": "2022-04-04T14:30:18.000Z", - "permalink": "/pages/e9ce4e/", + "title": "简述 HashSet 实现原理", + "date": "2022-04-04T14:30:47.000Z", + "permalink": "/pages/75552c/", "categories": [ "Java相关", "基础" @@ -782,22 +782,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/170.%E9%98%BB%E5%A1%9E%E9%98%9F%E5%88%97%E9%83%BD%E6%9C%89%E5%93%AA%E5%87%A0%E7%A7%8D%EF%BC%8C%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F.html", - "relativePath": "01.Java相关/10.基础/170.阻塞队列都有哪几种,有什么区别?.md", - "key": "v-1218fa85", - "path": "/pages/e9ce4e/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/180.%E7%AE%80%E8%BF%B0%20HashSet%20%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86.html", + "relativePath": "01.Java相关/10.基础/180.简述 HashSet 实现原理.md", + "key": "v-b21bbc4c", + "path": "/pages/75552c/", "headers": [ { "level": 2, - "title": "阻塞队列都有哪几种,有什么区别?", - "slug": "阻塞队列都有哪几种-有什么区别", - "normalizedTitle": "阻塞队列都有哪几种,有什么区别?", + "title": "简述 HashSet 实现原理", + "slug": "简述-hashset-实现原理", + "normalizedTitle": "简述 hashset 实现原理", "charIndex": 2 } ], - "headersStr": "阻塞队列都有哪几种,有什么区别?", - "content": "# 阻塞队列都有哪几种,有什么区别?\n\njava.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:\n\n\nFIFO 队列 :LinkedBlockingQueue、ArrayBlockingQueue(固定长度)\n优先级队列 :PriorityBlockingQueue\n延时队列 : DelayQueue\n同步队列 : SynchronousQueue\n\n\njava.util.concurrent.BlockingQueue提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,直到队列有空闲位置。\n\n\n \nLinkedBlockingQueue 1.内部是用链表实现的\n 2.默认容量是 Integer.MAX_VALUE\n 3.队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能\n 4.会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响\n 5.在无界的情况下,可能会造成内存溢出等问题。\nArrayBlockingQueue 1.ArrayBlockingQueue 是最典型的有界队列\n 2.其内部是用数组存储元素的\n 3.利用 ReentrantLock 实现线程安全,添加操作和移除操作采用的同一个ReenterLock锁\n 4.在创建它的时候就需要指定它的容量,之后也不可以再扩容了\n 5.构造函数中我们同样可以指定是否是公平的\nPriorityBlockingQueue 1.无界阻塞队列\n 2. 支持优先级,可以通过自定义类实现 compareTo() 方法来指定元素排序规则\nDelayQueue 1.无界队列\n 2.放入的元素必须实现 Delayed 接口\n 3.内部使用了 PriorityQueue 的能力来进行排序\nSynchronousQueue 容量为\n 0,所以没有一个地方来暂存元素,导致每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取", - "normalizedContent": "# 阻塞队列都有哪几种,有什么区别?\n\njava.util.concurrent.blockingqueue 接口有以下阻塞队列的实现:\n\n\nfifo 队列 :linkedblockingqueue、arrayblockingqueue(固定长度)\n优先级队列 :priorityblockingqueue\n延时队列 : delayqueue\n同步队列 : synchronousqueue\n\n\njava.util.concurrent.blockingqueue提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,直到队列有空闲位置。\n\n\n \nlinkedblockingqueue 1.内部是用链表实现的\n 2.默认容量是 integer.max_value\n 3.队列中的锁是分离的,其添加采用的是putlock,移除采用的则是takelock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能\n 4.会生成一个额外的node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于gc可能存在较大影响\n 5.在无界的情况下,可能会造成内存溢出等问题。\narrayblockingqueue 1.arrayblockingqueue 是最典型的有界队列\n 2.其内部是用数组存储元素的\n 3.利用 reentrantlock 实现线程安全,添加操作和移除操作采用的同一个reenterlock锁\n 4.在创建它的时候就需要指定它的容量,之后也不可以再扩容了\n 5.构造函数中我们同样可以指定是否是公平的\npriorityblockingqueue 1.无界阻塞队列\n 2. 支持优先级,可以通过自定义类实现 compareto() 方法来指定元素排序规则\ndelayqueue 1.无界队列\n 2.放入的元素必须实现 delayed 接口\n 3.内部使用了 priorityqueue 的能力来进行排序\nsynchronousqueue 容量为\n 0,所以没有一个地方来暂存元素,导致每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取", + "headersStr": "简述 HashSet 实现原理", + "content": "# 简述 HashSet 实现原理\n\n底层结构是 HashMap 。value都是存放同一个虚拟对象。\n\npublic class HashSet extends AbstractSet implements Set, Cloneable, java.io.Serializable {\n static final long serialVersionUID = -5024744406713321676L;\n \n private transient HashMap map;\n \n // 虚拟对象\n private static final Object PRESENT = new Object();\n \n // 无参构造\n public HashSet() {\n map = new HashMap<>();\n }\n \n // 集合构造\n public HashSet(Collection c) {\n map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));\n addAll(c);\n }\n \n // 初始化容量及负载因子构造\n public HashSet(int initialCapacity, float loadFactor) {\n map = new HashMap<>(initialCapacity, loadFactor);\n }\n \n // 初始化容量构造\n public HashSet(int initialCapacity) {\n map = new HashMap<>(initialCapacity);\n }\n \n // 由于默认修饰符,同包可访问,我们用不到,在LinkedLinkedHashMap中有具体应用,后期会讲解\n HashSet(int initialCapacity, float loadFactor, boolean dummy) {\n map = new LinkedHashMap<>(initialCapacity, loadFactor);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n", + "normalizedContent": "# 简述 hashset 实现原理\n\n底层结构是 hashmap 。value都是存放同一个虚拟对象。\n\npublic class hashset extends abstractset implements set, cloneable, java.io.serializable {\n static final long serialversionuid = -5024744406713321676l;\n \n private transient hashmap map;\n \n // 虚拟对象\n private static final object present = new object();\n \n // 无参构造\n public hashset() {\n map = new hashmap<>();\n }\n \n // 集合构造\n public hashset(collection c) {\n map = new hashmap<>(math.max((int) (c.size()/.75f) + 1, 16));\n addall(c);\n }\n \n // 初始化容量及负载因子构造\n public hashset(int initialcapacity, float loadfactor) {\n map = new hashmap<>(initialcapacity, loadfactor);\n }\n \n // 初始化容量构造\n public hashset(int initialcapacity) {\n map = new hashmap<>(initialcapacity);\n }\n \n // 由于默认修饰符,同包可访问,我们用不到,在linkedlinkedhashmap中有具体应用,后期会讲解\n hashset(int initialcapacity, float loadfactor, boolean dummy) {\n map = new linkedhashmap<>(initialcapacity, loadfactor);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n", "charsets": { "cjk": true }, @@ -805,11 +805,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "简述 HashSet 实现原理", + "title": "阻塞队列都有哪几种,有什么区别?", "frontmatter": { - "title": "简述 HashSet 实现原理", - "date": "2022-04-04T14:30:47.000Z", - "permalink": "/pages/75552c/", + "title": "阻塞队列都有哪几种,有什么区别?", + "date": "2022-04-04T14:30:18.000Z", + "permalink": "/pages/e9ce4e/", "categories": [ "Java相关", "基础" @@ -818,22 +818,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/180.%E7%AE%80%E8%BF%B0%20HashSet%20%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86.html", - "relativePath": "01.Java相关/10.基础/180.简述 HashSet 实现原理.md", - "key": "v-b21bbc4c", - "path": "/pages/75552c/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/170.%E9%98%BB%E5%A1%9E%E9%98%9F%E5%88%97%E9%83%BD%E6%9C%89%E5%93%AA%E5%87%A0%E7%A7%8D%EF%BC%8C%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F.html", + "relativePath": "01.Java相关/10.基础/170.阻塞队列都有哪几种,有什么区别?.md", + "key": "v-1218fa85", + "path": "/pages/e9ce4e/", "headers": [ { "level": 2, - "title": "简述 HashSet 实现原理", - "slug": "简述-hashset-实现原理", - "normalizedTitle": "简述 hashset 实现原理", + "title": "阻塞队列都有哪几种,有什么区别?", + "slug": "阻塞队列都有哪几种-有什么区别", + "normalizedTitle": "阻塞队列都有哪几种,有什么区别?", "charIndex": 2 } ], - "headersStr": "简述 HashSet 实现原理", - "content": "# 简述 HashSet 实现原理\n\n底层结构是 HashMap 。value都是存放同一个虚拟对象。\n\npublic class HashSet extends AbstractSet implements Set, Cloneable, java.io.Serializable {\n static final long serialVersionUID = -5024744406713321676L;\n \n private transient HashMap map;\n \n // 虚拟对象\n private static final Object PRESENT = new Object();\n \n // 无参构造\n public HashSet() {\n map = new HashMap<>();\n }\n \n // 集合构造\n public HashSet(Collection c) {\n map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));\n addAll(c);\n }\n \n // 初始化容量及负载因子构造\n public HashSet(int initialCapacity, float loadFactor) {\n map = new HashMap<>(initialCapacity, loadFactor);\n }\n \n // 初始化容量构造\n public HashSet(int initialCapacity) {\n map = new HashMap<>(initialCapacity);\n }\n \n // 由于默认修饰符,同包可访问,我们用不到,在LinkedLinkedHashMap中有具体应用,后期会讲解\n HashSet(int initialCapacity, float loadFactor, boolean dummy) {\n map = new LinkedHashMap<>(initialCapacity, loadFactor);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n", - "normalizedContent": "# 简述 hashset 实现原理\n\n底层结构是 hashmap 。value都是存放同一个虚拟对象。\n\npublic class hashset extends abstractset implements set, cloneable, java.io.serializable {\n static final long serialversionuid = -5024744406713321676l;\n \n private transient hashmap map;\n \n // 虚拟对象\n private static final object present = new object();\n \n // 无参构造\n public hashset() {\n map = new hashmap<>();\n }\n \n // 集合构造\n public hashset(collection c) {\n map = new hashmap<>(math.max((int) (c.size()/.75f) + 1, 16));\n addall(c);\n }\n \n // 初始化容量及负载因子构造\n public hashset(int initialcapacity, float loadfactor) {\n map = new hashmap<>(initialcapacity, loadfactor);\n }\n \n // 初始化容量构造\n public hashset(int initialcapacity) {\n map = new hashmap<>(initialcapacity);\n }\n \n // 由于默认修饰符,同包可访问,我们用不到,在linkedlinkedhashmap中有具体应用,后期会讲解\n hashset(int initialcapacity, float loadfactor, boolean dummy) {\n map = new linkedhashmap<>(initialcapacity, loadfactor);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n", + "headersStr": "阻塞队列都有哪几种,有什么区别?", + "content": "# 阻塞队列都有哪几种,有什么区别?\n\njava.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:\n\n\nFIFO 队列 :LinkedBlockingQueue、ArrayBlockingQueue(固定长度)\n优先级队列 :PriorityBlockingQueue\n延时队列 : DelayQueue\n同步队列 : SynchronousQueue\n\n\njava.util.concurrent.BlockingQueue提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,直到队列有空闲位置。\n\n\n \nLinkedBlockingQueue 1.内部是用链表实现的\n 2.默认容量是 Integer.MAX_VALUE\n 3.队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能\n 4.会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响\n 5.在无界的情况下,可能会造成内存溢出等问题。\nArrayBlockingQueue 1.ArrayBlockingQueue 是最典型的有界队列\n 2.其内部是用数组存储元素的\n 3.利用 ReentrantLock 实现线程安全,添加操作和移除操作采用的同一个ReenterLock锁\n 4.在创建它的时候就需要指定它的容量,之后也不可以再扩容了\n 5.构造函数中我们同样可以指定是否是公平的\nPriorityBlockingQueue 1.无界阻塞队列\n 2. 支持优先级,可以通过自定义类实现 compareTo() 方法来指定元素排序规则\nDelayQueue 1.无界队列\n 2.放入的元素必须实现 Delayed 接口\n 3.内部使用了 PriorityQueue 的能力来进行排序\nSynchronousQueue 容量为\n 0,所以没有一个地方来暂存元素,导致每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取", + "normalizedContent": "# 阻塞队列都有哪几种,有什么区别?\n\njava.util.concurrent.blockingqueue 接口有以下阻塞队列的实现:\n\n\nfifo 队列 :linkedblockingqueue、arrayblockingqueue(固定长度)\n优先级队列 :priorityblockingqueue\n延时队列 : delayqueue\n同步队列 : synchronousqueue\n\n\njava.util.concurrent.blockingqueue提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,直到队列有空闲位置。\n\n\n \nlinkedblockingqueue 1.内部是用链表实现的\n 2.默认容量是 integer.max_value\n 3.队列中的锁是分离的,其添加采用的是putlock,移除采用的则是takelock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能\n 4.会生成一个额外的node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于gc可能存在较大影响\n 5.在无界的情况下,可能会造成内存溢出等问题。\narrayblockingqueue 1.arrayblockingqueue 是最典型的有界队列\n 2.其内部是用数组存储元素的\n 3.利用 reentrantlock 实现线程安全,添加操作和移除操作采用的同一个reenterlock锁\n 4.在创建它的时候就需要指定它的容量,之后也不可以再扩容了\n 5.构造函数中我们同样可以指定是否是公平的\npriorityblockingqueue 1.无界阻塞队列\n 2. 支持优先级,可以通过自定义类实现 compareto() 方法来指定元素排序规则\ndelayqueue 1.无界队列\n 2.放入的元素必须实现 delayed 接口\n 3.内部使用了 priorityqueue 的能力来进行排序\nsynchronousqueue 容量为\n 0,所以没有一个地方来暂存元素,导致每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取", "charsets": { "cjk": true }, @@ -1052,6 +1052,56 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, + { + "title": "简述 CAS 原理,什么是 ABA 问题,怎么解决?", + "frontmatter": { + "title": "简述 CAS 原理,什么是 ABA 问题,怎么解决?", + "date": "2022-04-04T14:32:48.000Z", + "permalink": "/pages/3356c6/", + "categories": [ + "Java相关", + "并发" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/020.%E7%AE%80%E8%BF%B0%20CAS%20%E5%8E%9F%E7%90%86%EF%BC%8C%E4%BB%80%E4%B9%88%E6%98%AF%20ABA%20%E9%97%AE%E9%A2%98%EF%BC%8C%E6%80%8E%E4%B9%88%E8%A7%A3%E5%86%B3%EF%BC%9F.html", + "relativePath": "01.Java相关/20.并发/020.简述 CAS 原理,什么是 ABA 问题,怎么解决?.md", + "key": "v-5390e3d4", + "path": "/pages/3356c6/", + "headers": [ + { + "level": 2, + "title": "简述 CAS 原理,什么是 ABA 问题,怎么解决?", + "slug": "简述-cas-原理-什么是-aba-问题-怎么解决", + "normalizedTitle": "简述 cas 原理,什么是 aba 问题,怎么解决?", + "charIndex": 2 + }, + { + "level": 3, + "title": "CAS实现原子操作", + "slug": "cas实现原子操作", + "normalizedTitle": "cas实现原子操作", + "charIndex": 33 + }, + { + "level": 3, + "title": "ABA问题", + "slug": "aba问题", + "normalizedTitle": "aba问题", + "charIndex": 256 + } + ], + "headersStr": "简述 CAS 原理,什么是 ABA 问题,怎么解决? CAS实现原子操作 ABA问题", + "content": "# 简述 CAS 原理,什么是 ABA 问题,怎么解决?\n\n\n# CAS实现原子操作\n\nCAS理论是 juc 包实现的基石,在intel的CPU中,CAS 通过调用本地方法(JNI)使用cmpxchg指令来实现的非阻塞算法。对比于synchronized阻塞算法,基于 CAS 实现的 juc 在性能上有了很大的提升。\n\nCAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。\n\n\n# ABA问题\n\n * 因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。\n\n * ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。\n\n * 从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。", + "normalizedContent": "# 简述 cas 原理,什么是 aba 问题,怎么解决?\n\n\n# cas实现原子操作\n\ncas理论是 juc 包实现的基石,在intel的cpu中,cas 通过调用本地方法(jni)使用cmpxchg指令来实现的非阻塞算法。对比于synchronized阻塞算法,基于 cas 实现的 juc 在性能上有了很大的提升。\n\ncas 操作包含三个操作数 —— 内存位置(v)、预期原值(a)和新值(b)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。\n\n\n# aba问题\n\n * 因为cas需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是a,变成了b,又变成了a,那么使用cas进行检查时会发现它的值没有发生变化,但是实际上却变化了。\n\n * aba问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么a-b-a 就会变成1a-2b-3a。\n\n * 从java1.5开始jdk的atomic包里提供了一个类atomicstampedreference来解决aba问题。这个类的compareandset方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, { "title": "Synchronized 与 Lock 相比优缺点分别是什么?", "frontmatter": { @@ -1103,11 +1153,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "简述 CAS 原理,什么是 ABA 问题,怎么解决?", + "title": "volatile 关键字解决了什么问题,它的实现原理是什么?", "frontmatter": { - "title": "简述 CAS 原理,什么是 ABA 问题,怎么解决?", - "date": "2022-04-04T14:32:48.000Z", - "permalink": "/pages/3356c6/", + "title": "volatile 关键字解决了什么问题,它的实现原理是什么?", + "date": "2022-04-04T14:34:07.000Z", + "permalink": "/pages/383264/", "categories": [ "Java相关", "并发" @@ -1116,36 +1166,29 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/020.%E7%AE%80%E8%BF%B0%20CAS%20%E5%8E%9F%E7%90%86%EF%BC%8C%E4%BB%80%E4%B9%88%E6%98%AF%20ABA%20%E9%97%AE%E9%A2%98%EF%BC%8C%E6%80%8E%E4%B9%88%E8%A7%A3%E5%86%B3%EF%BC%9F.html", - "relativePath": "01.Java相关/20.并发/020.简述 CAS 原理,什么是 ABA 问题,怎么解决?.md", - "key": "v-5390e3d4", - "path": "/pages/3356c6/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/060.volatile%20%E5%85%B3%E9%94%AE%E5%AD%97%E8%A7%A3%E5%86%B3%E4%BA%86%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98%EF%BC%8C%E5%AE%83%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", + "relativePath": "01.Java相关/20.并发/060.volatile 关键字解决了什么问题,它的实现原理是什么?.md", + "key": "v-f5bcd180", + "path": "/pages/383264/", "headers": [ { "level": 2, - "title": "简述 CAS 原理,什么是 ABA 问题,怎么解决?", - "slug": "简述-cas-原理-什么是-aba-问题-怎么解决", - "normalizedTitle": "简述 cas 原理,什么是 aba 问题,怎么解决?", - "charIndex": 2 - }, - { - "level": 3, - "title": "CAS实现原子操作", - "slug": "cas实现原子操作", - "normalizedTitle": "cas实现原子操作", - "charIndex": 33 + "title": "Java中的内存可见性", + "slug": "java中的内存可见性", + "normalizedTitle": "java中的内存可见性", + "charIndex": 129 }, { - "level": 3, - "title": "ABA问题", - "slug": "aba问题", - "normalizedTitle": "aba问题", - "charIndex": 256 + "level": 2, + "title": "Volatile 的实现原理", + "slug": "volatile-的实现原理", + "normalizedTitle": "volatile 的实现原理", + "charIndex": 335 } ], - "headersStr": "简述 CAS 原理,什么是 ABA 问题,怎么解决? CAS实现原子操作 ABA问题", - "content": "# 简述 CAS 原理,什么是 ABA 问题,怎么解决?\n\n\n# CAS实现原子操作\n\nCAS理论是 juc 包实现的基石,在intel的CPU中,CAS 通过调用本地方法(JNI)使用cmpxchg指令来实现的非阻塞算法。对比于synchronized阻塞算法,基于 CAS 实现的 juc 在性能上有了很大的提升。\n\nCAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。\n\n\n# ABA问题\n\n * 因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。\n\n * ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。\n\n * 从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。", - "normalizedContent": "# 简述 cas 原理,什么是 aba 问题,怎么解决?\n\n\n# cas实现原子操作\n\ncas理论是 juc 包实现的基石,在intel的cpu中,cas 通过调用本地方法(jni)使用cmpxchg指令来实现的非阻塞算法。对比于synchronized阻塞算法,基于 cas 实现的 juc 在性能上有了很大的提升。\n\ncas 操作包含三个操作数 —— 内存位置(v)、预期原值(a)和新值(b)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。\n\n\n# aba问题\n\n * 因为cas需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是a,变成了b,又变成了a,那么使用cas进行检查时会发现它的值没有发生变化,但是实际上却变化了。\n\n * aba问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么a-b-a 就会变成1a-2b-3a。\n\n * 从java1.5开始jdk的atomic包里提供了一个类atomicstampedreference来解决aba问题。这个类的compareandset方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。", + "headersStr": "Java中的内存可见性 Volatile 的实现原理", + "content": "volatile 能保证 Java 中的内存可见性。 可见性的意思是:当线程A修改共享变量的值后,线程B能立刻读到这个修改后的值。\n\n\nVolatile 不会引起线程上下文的切换和调度,如果使用的恰当,会比 Synchronized 执行成本更低。\n\n\n# Java中的内存可见性\n\n * 可见性:一个线程对共享变量值的修改,能够及时被其他线程看到。\n\n * 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那这个变量就是这几个线程的共享变量。\n\n * Java内存的规定:\n\n-线程对共享变量的所有操作都必须在自己的工作内存中进行,不可直接从主内存中读写; -不同线程之间无法直接访问其他线程工作内存中的变量,线程间的变量值的传递需要通过主内存。\n\n\n\n\n# Volatile 的实现原理\n\n如果对用 Volatile 修饰的变量写操作,JVM 会向处理器发出一条 Lock 前缀的指令,Lock 前缀的指令在多核处理器下会引发两件事情:\n\n 1. 将当前处理器缓存行的数据写会到系统内存\n\n 2. 这个写会内存的操作会使其他缓存中的该内存地址的数据无效", + "normalizedContent": "volatile 能保证 java 中的内存可见性。 可见性的意思是:当线程a修改共享变量的值后,线程b能立刻读到这个修改后的值。\n\n\nvolatile 不会引起线程上下文的切换和调度,如果使用的恰当,会比 synchronized 执行成本更低。\n\n\n# java中的内存可见性\n\n * 可见性:一个线程对共享变量值的修改,能够及时被其他线程看到。\n\n * 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那这个变量就是这几个线程的共享变量。\n\n * java内存的规定:\n\n-线程对共享变量的所有操作都必须在自己的工作内存中进行,不可直接从主内存中读写; -不同线程之间无法直接访问其他线程工作内存中的变量,线程间的变量值的传递需要通过主内存。\n\n\n\n\n# volatile 的实现原理\n\n如果对用 volatile 修饰的变量写操作,jvm 会向处理器发出一条 lock 前缀的指令,lock 前缀的指令在多核处理器下会引发两件事情:\n\n 1. 将当前处理器缓存行的数据写会到系统内存\n\n 2. 这个写会内存的操作会使其他缓存中的该内存地址的数据无效", "charsets": { "cjk": true }, @@ -1202,49 +1245,6 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, - { - "title": "volatile 关键字解决了什么问题,它的实现原理是什么?", - "frontmatter": { - "title": "volatile 关键字解决了什么问题,它的实现原理是什么?", - "date": "2022-04-04T14:34:07.000Z", - "permalink": "/pages/383264/", - "categories": [ - "Java相关", - "并发" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/060.volatile%20%E5%85%B3%E9%94%AE%E5%AD%97%E8%A7%A3%E5%86%B3%E4%BA%86%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98%EF%BC%8C%E5%AE%83%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", - "relativePath": "01.Java相关/20.并发/060.volatile 关键字解决了什么问题,它的实现原理是什么?.md", - "key": "v-f5bcd180", - "path": "/pages/383264/", - "headers": [ - { - "level": 2, - "title": "Java中的内存可见性", - "slug": "java中的内存可见性", - "normalizedTitle": "java中的内存可见性", - "charIndex": 129 - }, - { - "level": 2, - "title": "Volatile 的实现原理", - "slug": "volatile-的实现原理", - "normalizedTitle": "volatile 的实现原理", - "charIndex": 335 - } - ], - "headersStr": "Java中的内存可见性 Volatile 的实现原理", - "content": "volatile 能保证 Java 中的内存可见性。 可见性的意思是:当线程A修改共享变量的值后,线程B能立刻读到这个修改后的值。\n\n\nVolatile 不会引起线程上下文的切换和调度,如果使用的恰当,会比 Synchronized 执行成本更低。\n\n\n# Java中的内存可见性\n\n * 可见性:一个线程对共享变量值的修改,能够及时被其他线程看到。\n\n * 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那这个变量就是这几个线程的共享变量。\n\n * Java内存的规定:\n\n-线程对共享变量的所有操作都必须在自己的工作内存中进行,不可直接从主内存中读写; -不同线程之间无法直接访问其他线程工作内存中的变量,线程间的变量值的传递需要通过主内存。\n\n\n\n\n# Volatile 的实现原理\n\n如果对用 Volatile 修饰的变量写操作,JVM 会向处理器发出一条 Lock 前缀的指令,Lock 前缀的指令在多核处理器下会引发两件事情:\n\n 1. 将当前处理器缓存行的数据写会到系统内存\n\n 2. 这个写会内存的操作会使其他缓存中的该内存地址的数据无效", - "normalizedContent": "volatile 能保证 java 中的内存可见性。 可见性的意思是:当线程a修改共享变量的值后,线程b能立刻读到这个修改后的值。\n\n\nvolatile 不会引起线程上下文的切换和调度,如果使用的恰当,会比 synchronized 执行成本更低。\n\n\n# java中的内存可见性\n\n * 可见性:一个线程对共享变量值的修改,能够及时被其他线程看到。\n\n * 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那这个变量就是这几个线程的共享变量。\n\n * java内存的规定:\n\n-线程对共享变量的所有操作都必须在自己的工作内存中进行,不可直接从主内存中读写; -不同线程之间无法直接访问其他线程工作内存中的变量,线程间的变量值的传递需要通过主内存。\n\n\n\n\n# volatile 的实现原理\n\n如果对用 volatile 修饰的变量写操作,jvm 会向处理器发出一条 lock 前缀的指令,lock 前缀的指令在多核处理器下会引发两件事情:\n\n 1. 将当前处理器缓存行的数据写会到系统内存\n\n 2. 这个写会内存的操作会使其他缓存中的该内存地址的数据无效", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, { "title": "简述 Java 锁升级的机制", "frontmatter": { @@ -1572,6 +1572,42 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, + { + "title": "为什么我们不能直接调用 run() 方法?", + "frontmatter": { + "title": "为什么我们不能直接调用 run() 方法?", + "date": "2022-04-04T14:36:29.000Z", + "permalink": "/pages/13856f/", + "categories": [ + "Java相关", + "并发" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/120.%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E4%BB%AC%E4%B8%8D%E8%83%BD%E7%9B%B4%E6%8E%A5%E8%B0%83%E7%94%A8%20run()%20%E6%96%B9%E6%B3%95%EF%BC%9F.html", + "relativePath": "01.Java相关/20.并发/120.为什么我们不能直接调用 run() 方法?.md", + "key": "v-9baaad8e", + "path": "/pages/13856f/", + "headers": [ + { + "level": 2, + "title": "为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?", + "slug": "为什么我们调用-start-方法时会执行-run-方法-为什么我们不能直接调用-run-方法", + "normalizedTitle": "为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?", + "charIndex": null + } + ], + "headersStr": "为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?", + "content": "# 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run()方法?\n\n如果我们直接调用 run() 方法 是不会创建新线程的。\nstart() 源代码:\n\n/**\n * Causes this thread to begin execution; the Java Virtual Machine\n * calls the run method of this thread.\n *

线程被执行,JVM调用run方法\n * The result is that two threads are running concurrently: the\n * current thread (which returns from the call to the\n * start method) and the other thread (which executes its\n * run method).\n *

\n * It is never legal to start a thread more than once.多次调用start方法启动一个线程是非法的\n * In particular, a thread may not be restarted once it has completed\n * execution.\n *\n * @exception IllegalThreadStateException if the thread was already已经启动的线程再次start,异常\n * started.\n * @see #run()\n * @see #stop()\n */\n public synchronized void start() {\n /**\n * This method is not invoked for the main method thread or \"system\"\n * group threads created/set up by the VM. Any new functionality added\n * to this method in the future may have to also be added to the VM.\n *\n * A zero status value corresponds to state \"NEW\".\n */\n if (threadStatus != 0)//状态校验 0:NEW 新建状态\n throw new IllegalThreadStateException();\n\n /* Notify the group that this thread is about to be started\n * so that it can be added to the group's list of threads\n * and the group's unstarted count can be decremented. */\n group.add(this);//添加进线程组\n\n boolean started = false;\n try {\n start0();//调用native方法执行线程run方法\n started = true;\n } finally {\n try {\n if (!started) {\n group.threadStartFailed(this);//启动失败,从线程组中移除当前前程。\n }\n } catch (Throwable ignore) {\n /* do nothing. If start0 threw a Throwable then\n it will be passed up the call stack */\n }\n }\n }\n\n private native void start0();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n\n\n通过源码我们可以看到,在start()中调用了native方法 start0(),在start0()中会创建一个新的线程调用run()。 所以,如果我们直接调用 run() 方法 是不会创建新线程的。", + "normalizedContent": "# 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run()方法?\n\n如果我们直接调用 run() 方法 是不会创建新线程的。\nstart() 源代码:\n\n/**\n * causes this thread to begin execution; the java virtual machine\n * calls the run method of this thread.\n *

线程被执行,jvm调用run方法\n * the result is that two threads are running concurrently: the\n * current thread (which returns from the call to the\n * start method) and the other thread (which executes its\n * run method).\n *

\n * it is never legal to start a thread more than once.多次调用start方法启动一个线程是非法的\n * in particular, a thread may not be restarted once it has completed\n * execution.\n *\n * @exception illegalthreadstateexception if the thread was already已经启动的线程再次start,异常\n * started.\n * @see #run()\n * @see #stop()\n */\n public synchronized void start() {\n /**\n * this method is not invoked for the main method thread or \"system\"\n * group threads created/set up by the vm. any new functionality added\n * to this method in the future may have to also be added to the vm.\n *\n * a zero status value corresponds to state \"new\".\n */\n if (threadstatus != 0)//状态校验 0:new 新建状态\n throw new illegalthreadstateexception();\n\n /* notify the group that this thread is about to be started\n * so that it can be added to the group's list of threads\n * and the group's unstarted count can be decremented. */\n group.add(this);//添加进线程组\n\n boolean started = false;\n try {\n start0();//调用native方法执行线程run方法\n started = true;\n } finally {\n try {\n if (!started) {\n group.threadstartfailed(this);//启动失败,从线程组中移除当前前程。\n }\n } catch (throwable ignore) {\n /* do nothing. if start0 threw a throwable then\n it will be passed up the call stack */\n }\n }\n }\n\n private native void start0();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n\n\n通过源码我们可以看到,在start()中调用了native方法 start0(),在start0()中会创建一个新的线程调用run()。 所以,如果我们直接调用 run() 方法 是不会创建新线程的。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, { "title": "手写死锁", "frontmatter": { @@ -1616,11 +1652,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "为什么我们不能直接调用 run() 方法?", + "title": "手写生产者消费者模型", "frontmatter": { - "title": "为什么我们不能直接调用 run() 方法?", - "date": "2022-04-04T14:36:29.000Z", - "permalink": "/pages/13856f/", + "title": "手写生产者消费者模型", + "date": "2022-04-04T14:37:07.000Z", + "permalink": "/pages/2b8c6e/", "categories": [ "Java相关", "并发" @@ -1629,117 +1665,10 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/120.%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E4%BB%AC%E4%B8%8D%E8%83%BD%E7%9B%B4%E6%8E%A5%E8%B0%83%E7%94%A8%20run()%20%E6%96%B9%E6%B3%95%EF%BC%9F.html", - "relativePath": "01.Java相关/20.并发/120.为什么我们不能直接调用 run() 方法?.md", - "key": "v-9baaad8e", - "path": "/pages/13856f/", - "headers": [ - { - "level": 2, - "title": "为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?", - "slug": "为什么我们调用-start-方法时会执行-run-方法-为什么我们不能直接调用-run-方法", - "normalizedTitle": "为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?", - "charIndex": null - } - ], - "headersStr": "为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?", - "content": "# 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run()方法?\n\n如果我们直接调用 run() 方法 是不会创建新线程的。\nstart() 源代码:\n\n/**\n * Causes this thread to begin execution; the Java Virtual Machine\n * calls the run method of this thread.\n *

线程被执行,JVM调用run方法\n * The result is that two threads are running concurrently: the\n * current thread (which returns from the call to the\n * start method) and the other thread (which executes its\n * run method).\n *

\n * It is never legal to start a thread more than once.多次调用start方法启动一个线程是非法的\n * In particular, a thread may not be restarted once it has completed\n * execution.\n *\n * @exception IllegalThreadStateException if the thread was already已经启动的线程再次start,异常\n * started.\n * @see #run()\n * @see #stop()\n */\n public synchronized void start() {\n /**\n * This method is not invoked for the main method thread or \"system\"\n * group threads created/set up by the VM. Any new functionality added\n * to this method in the future may have to also be added to the VM.\n *\n * A zero status value corresponds to state \"NEW\".\n */\n if (threadStatus != 0)//状态校验 0:NEW 新建状态\n throw new IllegalThreadStateException();\n\n /* Notify the group that this thread is about to be started\n * so that it can be added to the group's list of threads\n * and the group's unstarted count can be decremented. */\n group.add(this);//添加进线程组\n\n boolean started = false;\n try {\n start0();//调用native方法执行线程run方法\n started = true;\n } finally {\n try {\n if (!started) {\n group.threadStartFailed(this);//启动失败,从线程组中移除当前前程。\n }\n } catch (Throwable ignore) {\n /* do nothing. If start0 threw a Throwable then\n it will be passed up the call stack */\n }\n }\n }\n\n private native void start0();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n\n\n通过源码我们可以看到,在start()中调用了native方法 start0(),在start0()中会创建一个新的线程调用run()。 所以,如果我们直接调用 run() 方法 是不会创建新线程的。", - "normalizedContent": "# 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run()方法?\n\n如果我们直接调用 run() 方法 是不会创建新线程的。\nstart() 源代码:\n\n/**\n * causes this thread to begin execution; the java virtual machine\n * calls the run method of this thread.\n *

线程被执行,jvm调用run方法\n * the result is that two threads are running concurrently: the\n * current thread (which returns from the call to the\n * start method) and the other thread (which executes its\n * run method).\n *

\n * it is never legal to start a thread more than once.多次调用start方法启动一个线程是非法的\n * in particular, a thread may not be restarted once it has completed\n * execution.\n *\n * @exception illegalthreadstateexception if the thread was already已经启动的线程再次start,异常\n * started.\n * @see #run()\n * @see #stop()\n */\n public synchronized void start() {\n /**\n * this method is not invoked for the main method thread or \"system\"\n * group threads created/set up by the vm. any new functionality added\n * to this method in the future may have to also be added to the vm.\n *\n * a zero status value corresponds to state \"new\".\n */\n if (threadstatus != 0)//状态校验 0:new 新建状态\n throw new illegalthreadstateexception();\n\n /* notify the group that this thread is about to be started\n * so that it can be added to the group's list of threads\n * and the group's unstarted count can be decremented. */\n group.add(this);//添加进线程组\n\n boolean started = false;\n try {\n start0();//调用native方法执行线程run方法\n started = true;\n } finally {\n try {\n if (!started) {\n group.threadstartfailed(this);//启动失败,从线程组中移除当前前程。\n }\n } catch (throwable ignore) {\n /* do nothing. if start0 threw a throwable then\n it will be passed up the call stack */\n }\n }\n }\n\n private native void start0();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n\n\n通过源码我们可以看到,在start()中调用了native方法 start0(),在start0()中会创建一个新的线程调用run()。 所以,如果我们直接调用 run() 方法 是不会创建新线程的。", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, - { - "title": "Java 线程有哪些常用方法?", - "frontmatter": { - "title": "Java 线程有哪些常用方法?", - "date": "2022-04-04T14:36:48.000Z", - "permalink": "/pages/a6c84e/", - "categories": [ - "Java相关", - "并发" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/130.Java%20%E7%BA%BF%E7%A8%8B%E6%9C%89%E5%93%AA%E4%BA%9B%E5%B8%B8%E7%94%A8%E6%96%B9%E6%B3%95%EF%BC%9F.html", - "relativePath": "01.Java相关/20.并发/130.Java 线程有哪些常用方法?.md", - "key": "v-75c00225", - "path": "/pages/a6c84e/", - "headers": [ - { - "level": 2, - "title": "Java 线程如何启动?如何终止?", - "slug": "java-线程如何启动-如何终止", - "normalizedTitle": "java 线程如何启动?如何终止?", - "charIndex": 2 - }, - { - "level": 2, - "title": "构造线程", - "slug": "构造线程", - "normalizedTitle": "构造线程", - "charIndex": 97 - }, - { - "level": 2, - "title": "启动线程", - "slug": "启动线程", - "normalizedTitle": "启动线程", - "charIndex": 637 - }, - { - "level": 2, - "title": "理解中断", - "slug": "理解中断", - "normalizedTitle": "理解中断", - "charIndex": 1259 - }, - { - "level": 2, - "title": "已不建议使用的方法:suspend(),resume(),stop()", - "slug": "已不建议使用的方法-suspend-resume-stop", - "normalizedTitle": "已不建议使用的方法:suspend(),resume(),stop()", - "charIndex": 1581 - }, - { - "level": 2, - "title": "安全的终止线程", - "slug": "安全的终止线程", - "normalizedTitle": "安全的终止线程", - "charIndex": 1813 - } - ], - "headersStr": "Java 线程如何启动?如何终止? 构造线程 启动线程 理解中断 已不建议使用的方法:suspend(),resume(),stop() 安全的终止线程", - "content": "# Java 线程如何启动?如何终止?\n\n线程随着start()方法的调用而启动,随着run()方法调用结束而终止。 但是这其中还会涉及到线程的构造、中断、暂停、恢复,我们来一一看下。\n\n\n# 构造线程\n\n在运行一个线程之前 肯定要先构造一个线程对象,构造线程对象需要提供所需属性:如线程所属组,线程优先级,是否deamon等。\n\nprivate void init(ThreadGroup g, Runnable target, String name,\n long stackSize, AccessControlContext acc) {\n //1.设置线程名\n if (name == null) {\n throw new NullPointerException(\"name cannot be null\");\n }\n\n this.name = name;\n //2.设置线程的线程组,如果未指定,则此线程的线程组为当前初始化线程的线程组\n Thread parent = currentThread();\n ...\n if (g == null) {\n //这里有一段注释说,安卓不支持SecurityManager,所以删除了相关代码\n g = parent.getThreadGroup();\n }\n\n //所属线程组未启动线程计数+1\n g.addUnstarted();\n\n this.group = g;\n //3.设置线程的其他属性\n //线程是否是守护线程以及优先级都继承自当前线程\n this.daemon = parent.isDaemon();\n this.priority = parent.getPriority();\n this.target = target;\n //4.调用init2 执行剩余初始化操作\n init2(parent);\n //5.设置线程的堆栈大小。但是否生效要看虚拟机。那么在art虚拟机上是否生效呢,我们后续再说\n /* Stash the specified stack size in case the VM cares */\n this.stackSize = stackSize;\n\n //6.设置当前线程id\n tid = nextThreadID();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n\n\n到这里为止,我们的thread就已经初始化完毕了。在堆内存中等待这被执行start()方法开始运行。\n\n\n# 启动线程\n\n调用start()方法开始启动线程。\n\n\n# 理解中断\n\n如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。\n\n中断可以理解为线程的一个标志位属性,调用interrupt()方法中断线程仅仅是把中断标志位置为true,线程可以通过检查自身的标志位来判断是否被中断了。\n\n另外还有两点需要注意的:\n\n * 如果线程处于终结状态,即使线程被中断过,其中断标识位依然是false\n * 许多声明抛出InterruptException的方法(如Thread.sleep(long millis)),在抛出InterruptException之前会将中断标识位置为false。\n\n\n# 已不建议使用的方法:suspend(),resume(),stop()\n\nsuspend(),resume(),stop()完成了线程的暂停、恢复、停止。但是这些方法过期了 不建议继续使用,原因是:\n\n 1. suspend()暂停线程是不会释放资源(比如锁),而是占着资源入睡 ,这样容易引发死锁\n 2. stop() 终止线程时没有给线程清理资源的机会,导致程序运行在不确定状态\n\n取而代之的是wait()和notify()/notifyAll()。\n\n\n# 安全的终止线程\n\n可以通过interrupt()或者标识位来优雅的停止线程。\n\npublic class MyRunner implements Runnable {\n private volatile boolean on = true;\n public void run() {\n while(on){\n System.out.println(\"Running...\");\n }\n System.out.println(\"Stoped!\");\n }\n\n public void cancel(){\n on = false;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n通过标识位来停止线程\n\npublic class ShutdownThread {\n\n public static void main(String[] args) {\n Thread t1 = new Thread(new MyRunner().start();\n t1.cancel();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n通过interrupt()来停止线程\n\npublic class ShutdownThread {\n\n public static void main(String[] args) {\n Thread t1 = new Thread(new MyRunner().start();\n t1.interrupt();\n }\n}\n\n1\n2\n3\n4\n5\n6\n", - "normalizedContent": "# java 线程如何启动?如何终止?\n\n线程随着start()方法的调用而启动,随着run()方法调用结束而终止。 但是这其中还会涉及到线程的构造、中断、暂停、恢复,我们来一一看下。\n\n\n# 构造线程\n\n在运行一个线程之前 肯定要先构造一个线程对象,构造线程对象需要提供所需属性:如线程所属组,线程优先级,是否deamon等。\n\nprivate void init(threadgroup g, runnable target, string name,\n long stacksize, accesscontrolcontext acc) {\n //1.设置线程名\n if (name == null) {\n throw new nullpointerexception(\"name cannot be null\");\n }\n\n this.name = name;\n //2.设置线程的线程组,如果未指定,则此线程的线程组为当前初始化线程的线程组\n thread parent = currentthread();\n ...\n if (g == null) {\n //这里有一段注释说,安卓不支持securitymanager,所以删除了相关代码\n g = parent.getthreadgroup();\n }\n\n //所属线程组未启动线程计数+1\n g.addunstarted();\n\n this.group = g;\n //3.设置线程的其他属性\n //线程是否是守护线程以及优先级都继承自当前线程\n this.daemon = parent.isdaemon();\n this.priority = parent.getpriority();\n this.target = target;\n //4.调用init2 执行剩余初始化操作\n init2(parent);\n //5.设置线程的堆栈大小。但是否生效要看虚拟机。那么在art虚拟机上是否生效呢,我们后续再说\n /* stash the specified stack size in case the vm cares */\n this.stacksize = stacksize;\n\n //6.设置当前线程id\n tid = nextthreadid();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n\n\n到这里为止,我们的thread就已经初始化完毕了。在堆内存中等待这被执行start()方法开始运行。\n\n\n# 启动线程\n\n调用start()方法开始启动线程。\n\n\n# 理解中断\n\n如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。\n\n中断可以理解为线程的一个标志位属性,调用interrupt()方法中断线程仅仅是把中断标志位置为true,线程可以通过检查自身的标志位来判断是否被中断了。\n\n另外还有两点需要注意的:\n\n * 如果线程处于终结状态,即使线程被中断过,其中断标识位依然是false\n * 许多声明抛出interruptexception的方法(如thread.sleep(long millis)),在抛出interruptexception之前会将中断标识位置为false。\n\n\n# 已不建议使用的方法:suspend(),resume(),stop()\n\nsuspend(),resume(),stop()完成了线程的暂停、恢复、停止。但是这些方法过期了 不建议继续使用,原因是:\n\n 1. suspend()暂停线程是不会释放资源(比如锁),而是占着资源入睡 ,这样容易引发死锁\n 2. stop() 终止线程时没有给线程清理资源的机会,导致程序运行在不确定状态\n\n取而代之的是wait()和notify()/notifyall()。\n\n\n# 安全的终止线程\n\n可以通过interrupt()或者标识位来优雅的停止线程。\n\npublic class myrunner implements runnable {\n private volatile boolean on = true;\n public void run() {\n while(on){\n system.out.println(\"running...\");\n }\n system.out.println(\"stoped!\");\n }\n\n public void cancel(){\n on = false;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n通过标识位来停止线程\n\npublic class shutdownthread {\n\n public static void main(string[] args) {\n thread t1 = new thread(new myrunner().start();\n t1.cancel();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n通过interrupt()来停止线程\n\npublic class shutdownthread {\n\n public static void main(string[] args) {\n thread t1 = new thread(new myrunner().start();\n t1.interrupt();\n }\n}\n\n1\n2\n3\n4\n5\n6\n", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, - { - "title": "手写生产者消费者模型", - "frontmatter": { - "title": "手写生产者消费者模型", - "date": "2022-04-04T14:37:07.000Z", - "permalink": "/pages/2b8c6e/", - "categories": [ - "Java相关", - "并发" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/140.%E6%89%8B%E5%86%99%E7%94%9F%E4%BA%A7%E8%80%85%E6%B6%88%E8%B4%B9%E8%80%85%E6%A8%A1%E5%9E%8B.html", - "relativePath": "01.Java相关/20.并发/140.手写生产者消费者模型.md", - "key": "v-083aef82", - "path": "/pages/2b8c6e/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/140.%E6%89%8B%E5%86%99%E7%94%9F%E4%BA%A7%E8%80%85%E6%B6%88%E8%B4%B9%E8%80%85%E6%A8%A1%E5%9E%8B.html", + "relativePath": "01.Java相关/20.并发/140.手写生产者消费者模型.md", + "key": "v-083aef82", + "path": "/pages/2b8c6e/", "headers": [ { "level": 2, @@ -1849,6 +1778,77 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, + { + "title": "Java 线程有哪些常用方法?", + "frontmatter": { + "title": "Java 线程有哪些常用方法?", + "date": "2022-04-04T14:36:48.000Z", + "permalink": "/pages/a6c84e/", + "categories": [ + "Java相关", + "并发" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/130.Java%20%E7%BA%BF%E7%A8%8B%E6%9C%89%E5%93%AA%E4%BA%9B%E5%B8%B8%E7%94%A8%E6%96%B9%E6%B3%95%EF%BC%9F.html", + "relativePath": "01.Java相关/20.并发/130.Java 线程有哪些常用方法?.md", + "key": "v-75c00225", + "path": "/pages/a6c84e/", + "headers": [ + { + "level": 2, + "title": "Java 线程如何启动?如何终止?", + "slug": "java-线程如何启动-如何终止", + "normalizedTitle": "java 线程如何启动?如何终止?", + "charIndex": 2 + }, + { + "level": 2, + "title": "构造线程", + "slug": "构造线程", + "normalizedTitle": "构造线程", + "charIndex": 97 + }, + { + "level": 2, + "title": "启动线程", + "slug": "启动线程", + "normalizedTitle": "启动线程", + "charIndex": 637 + }, + { + "level": 2, + "title": "理解中断", + "slug": "理解中断", + "normalizedTitle": "理解中断", + "charIndex": 1259 + }, + { + "level": 2, + "title": "已不建议使用的方法:suspend(),resume(),stop()", + "slug": "已不建议使用的方法-suspend-resume-stop", + "normalizedTitle": "已不建议使用的方法:suspend(),resume(),stop()", + "charIndex": 1581 + }, + { + "level": 2, + "title": "安全的终止线程", + "slug": "安全的终止线程", + "normalizedTitle": "安全的终止线程", + "charIndex": 1813 + } + ], + "headersStr": "Java 线程如何启动?如何终止? 构造线程 启动线程 理解中断 已不建议使用的方法:suspend(),resume(),stop() 安全的终止线程", + "content": "# Java 线程如何启动?如何终止?\n\n线程随着start()方法的调用而启动,随着run()方法调用结束而终止。 但是这其中还会涉及到线程的构造、中断、暂停、恢复,我们来一一看下。\n\n\n# 构造线程\n\n在运行一个线程之前 肯定要先构造一个线程对象,构造线程对象需要提供所需属性:如线程所属组,线程优先级,是否deamon等。\n\nprivate void init(ThreadGroup g, Runnable target, String name,\n long stackSize, AccessControlContext acc) {\n //1.设置线程名\n if (name == null) {\n throw new NullPointerException(\"name cannot be null\");\n }\n\n this.name = name;\n //2.设置线程的线程组,如果未指定,则此线程的线程组为当前初始化线程的线程组\n Thread parent = currentThread();\n ...\n if (g == null) {\n //这里有一段注释说,安卓不支持SecurityManager,所以删除了相关代码\n g = parent.getThreadGroup();\n }\n\n //所属线程组未启动线程计数+1\n g.addUnstarted();\n\n this.group = g;\n //3.设置线程的其他属性\n //线程是否是守护线程以及优先级都继承自当前线程\n this.daemon = parent.isDaemon();\n this.priority = parent.getPriority();\n this.target = target;\n //4.调用init2 执行剩余初始化操作\n init2(parent);\n //5.设置线程的堆栈大小。但是否生效要看虚拟机。那么在art虚拟机上是否生效呢,我们后续再说\n /* Stash the specified stack size in case the VM cares */\n this.stackSize = stackSize;\n\n //6.设置当前线程id\n tid = nextThreadID();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n\n\n到这里为止,我们的thread就已经初始化完毕了。在堆内存中等待这被执行start()方法开始运行。\n\n\n# 启动线程\n\n调用start()方法开始启动线程。\n\n\n# 理解中断\n\n如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。\n\n中断可以理解为线程的一个标志位属性,调用interrupt()方法中断线程仅仅是把中断标志位置为true,线程可以通过检查自身的标志位来判断是否被中断了。\n\n另外还有两点需要注意的:\n\n * 如果线程处于终结状态,即使线程被中断过,其中断标识位依然是false\n * 许多声明抛出InterruptException的方法(如Thread.sleep(long millis)),在抛出InterruptException之前会将中断标识位置为false。\n\n\n# 已不建议使用的方法:suspend(),resume(),stop()\n\nsuspend(),resume(),stop()完成了线程的暂停、恢复、停止。但是这些方法过期了 不建议继续使用,原因是:\n\n 1. suspend()暂停线程是不会释放资源(比如锁),而是占着资源入睡 ,这样容易引发死锁\n 2. stop() 终止线程时没有给线程清理资源的机会,导致程序运行在不确定状态\n\n取而代之的是wait()和notify()/notifyAll()。\n\n\n# 安全的终止线程\n\n可以通过interrupt()或者标识位来优雅的停止线程。\n\npublic class MyRunner implements Runnable {\n private volatile boolean on = true;\n public void run() {\n while(on){\n System.out.println(\"Running...\");\n }\n System.out.println(\"Stoped!\");\n }\n\n public void cancel(){\n on = false;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n通过标识位来停止线程\n\npublic class ShutdownThread {\n\n public static void main(String[] args) {\n Thread t1 = new Thread(new MyRunner().start();\n t1.cancel();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n通过interrupt()来停止线程\n\npublic class ShutdownThread {\n\n public static void main(String[] args) {\n Thread t1 = new Thread(new MyRunner().start();\n t1.interrupt();\n }\n}\n\n1\n2\n3\n4\n5\n6\n", + "normalizedContent": "# java 线程如何启动?如何终止?\n\n线程随着start()方法的调用而启动,随着run()方法调用结束而终止。 但是这其中还会涉及到线程的构造、中断、暂停、恢复,我们来一一看下。\n\n\n# 构造线程\n\n在运行一个线程之前 肯定要先构造一个线程对象,构造线程对象需要提供所需属性:如线程所属组,线程优先级,是否deamon等。\n\nprivate void init(threadgroup g, runnable target, string name,\n long stacksize, accesscontrolcontext acc) {\n //1.设置线程名\n if (name == null) {\n throw new nullpointerexception(\"name cannot be null\");\n }\n\n this.name = name;\n //2.设置线程的线程组,如果未指定,则此线程的线程组为当前初始化线程的线程组\n thread parent = currentthread();\n ...\n if (g == null) {\n //这里有一段注释说,安卓不支持securitymanager,所以删除了相关代码\n g = parent.getthreadgroup();\n }\n\n //所属线程组未启动线程计数+1\n g.addunstarted();\n\n this.group = g;\n //3.设置线程的其他属性\n //线程是否是守护线程以及优先级都继承自当前线程\n this.daemon = parent.isdaemon();\n this.priority = parent.getpriority();\n this.target = target;\n //4.调用init2 执行剩余初始化操作\n init2(parent);\n //5.设置线程的堆栈大小。但是否生效要看虚拟机。那么在art虚拟机上是否生效呢,我们后续再说\n /* stash the specified stack size in case the vm cares */\n this.stacksize = stacksize;\n\n //6.设置当前线程id\n tid = nextthreadid();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n\n\n到这里为止,我们的thread就已经初始化完毕了。在堆内存中等待这被执行start()方法开始运行。\n\n\n# 启动线程\n\n调用start()方法开始启动线程。\n\n\n# 理解中断\n\n如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。\n\n中断可以理解为线程的一个标志位属性,调用interrupt()方法中断线程仅仅是把中断标志位置为true,线程可以通过检查自身的标志位来判断是否被中断了。\n\n另外还有两点需要注意的:\n\n * 如果线程处于终结状态,即使线程被中断过,其中断标识位依然是false\n * 许多声明抛出interruptexception的方法(如thread.sleep(long millis)),在抛出interruptexception之前会将中断标识位置为false。\n\n\n# 已不建议使用的方法:suspend(),resume(),stop()\n\nsuspend(),resume(),stop()完成了线程的暂停、恢复、停止。但是这些方法过期了 不建议继续使用,原因是:\n\n 1. suspend()暂停线程是不会释放资源(比如锁),而是占着资源入睡 ,这样容易引发死锁\n 2. stop() 终止线程时没有给线程清理资源的机会,导致程序运行在不确定状态\n\n取而代之的是wait()和notify()/notifyall()。\n\n\n# 安全的终止线程\n\n可以通过interrupt()或者标识位来优雅的停止线程。\n\npublic class myrunner implements runnable {\n private volatile boolean on = true;\n public void run() {\n while(on){\n system.out.println(\"running...\");\n }\n system.out.println(\"stoped!\");\n }\n\n public void cancel(){\n on = false;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n通过标识位来停止线程\n\npublic class shutdownthread {\n\n public static void main(string[] args) {\n thread t1 = new thread(new myrunner().start();\n t1.cancel();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n通过interrupt()来停止线程\n\npublic class shutdownthread {\n\n public static void main(string[] args) {\n thread t1 = new thread(new myrunner().start();\n t1.interrupt();\n }\n}\n\n1\n2\n3\n4\n5\n6\n", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, { "title": "ThreadLocal 实现原理是什么?为什么要使用弱引用?", "frontmatter": { @@ -1949,9 +1949,9 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "Java 中垃圾回收机制中如何判断对象需要回收?常见的 GC 回收算法有哪些?", + "title": "Java 中垃圾回收机制中如何判断对象需要回收?", "frontmatter": { - "title": "Java 中垃圾回收机制中如何判断对象需要回收?常见的 GC 回收算法有哪些?", + "title": "Java 中垃圾回收机制中如何判断对象需要回收?", "date": "2022-05-21T17:31:41.000Z", "permalink": "/pages/948850/", "categories": [ @@ -1962,23 +1962,60 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/001.Java%20%E4%B8%AD%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6%E4%B8%AD%E5%A6%82%E4%BD%95%E5%88%A4%E6%96%AD%E5%AF%B9%E8%B1%A1%E9%9C%80%E8%A6%81%E5%9B%9E%E6%94%B6%EF%BC%9F%E5%B8%B8%E8%A7%81%E7%9A%84%20GC%20%E5%9B%9E%E6%94%B6%E7%AE%97%E6%B3%95%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F.html", - "relativePath": "01.Java相关/30.JVM/001.Java 中垃圾回收机制中如何判断对象需要回收?常见的 GC 回收算法有哪些?.md", - "key": "v-64652416", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/001.Java%20%E4%B8%AD%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6%E4%B8%AD%E5%A6%82%E4%BD%95%E5%88%A4%E6%96%AD%E5%AF%B9%E8%B1%A1%E9%9C%80%E8%A6%81%E5%9B%9E%E6%94%B6%EF%BC%9F.html", + "relativePath": "01.Java相关/30.JVM/001.Java 中垃圾回收机制中如何判断对象需要回收?.md", + "key": "v-7cd97c82", "path": "/pages/948850/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 + "headers": [ + { + "level": 2, + "title": "引用计数法", + "slug": "引用计数法", + "normalizedTitle": "引用计数法", + "charIndex": 95 + }, + { + "level": 2, + "title": "可达性分析算法", + "slug": "可达性分析算法", + "normalizedTitle": "可达性分析算法", + "charIndex": 738 + }, + { + "level": 2, + "title": "引用类型总结", + "slug": "引用类型总结", + "normalizedTitle": "引用类型总结", + "charIndex": 1442 + }, + { + "level": 2, + "title": "如何判断一个常量是废弃常量?", + "slug": "如何判断一个常量是废弃常量", + "normalizedTitle": "如何判断一个常量是废弃常量?", + "charIndex": 2760 + }, + { + "level": 2, + "title": "如何判断一个类是无用的类", + "slug": "如何判断一个类是无用的类", + "normalizedTitle": "如何判断一个类是无用的类", + "charIndex": 2930 + } + ], + "headersStr": "引用计数法 可达性分析算法 引用类型总结 如何判断一个常量是废弃常量? 如何判断一个类是无用的类", + "content": "Java 中垃圾回收机制中如何判断对象需要回收? 即死亡对象判断方法 堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。\n\n\n# 引用计数法\n\n给对象中添加一个引用计数器:\n\n每当有一个地方引用它,计数器就加 1; 当引用失效,计数器就减 1; 任何时候计数器为 0 的对象就是不可能再被使用的。 这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。\n\n所谓对象之间的相互引用问题,如下面代码所示:除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。\n\npublic class ReferenceCountingGc {\n Object instance = null;\n public static void main(String[] args) {\n ReferenceCountingGc objA = new ReferenceCountingGc();\n ReferenceCountingGc objB = new ReferenceCountingGc();\n objA.instance = objB;\n objB.instance = objA;\n objA = null;\n objB = null;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n\n# 可达性分析算法\n\n这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。\n\n下图中的 Object 6 ~ Object 10 之间虽有引用关系,但它们到 GC Roots 不可达,因此为需要被回收的对象。\n\n哪些对象可以作为 GC Roots 呢?\n\n虚拟机栈(栈帧中的本地变量表)中引用的对象 本地方法栈(Native 方法)中引用的对象 方法区中类静态属性引用的对象 方法区中常量引用的对象 所有被同步锁持有的对象 对象可以被回收,就代表一定会被回收吗?\n\n即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 'finalize' 方法。当对象没有覆盖 'finalize' 方法,或 'finalize' 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。\n\n被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。\n\n> Object 类中的 finalize 方法一直被认为是一个糟糕的设计,成为了 Java 语言的负担,影响了 Java 语言的安全和 GC 的性能。JDK9 版本及后续版本中各个类中的 finalize 方法会被逐渐弃用移除。忘掉它的存在吧!\n\n\n# 引用类型总结\n\n无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。\n\nJDK1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。\n\nJDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)\n\n1.强引用(StrongReference)\n\n以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。\n\n2.软引用(SoftReference)\n\n如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。\n\n软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。\n\n3.弱引用(WeakReference)\n\n如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。\n\n弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。\n\n4.虚引用(PhantomReference)\n\n\"虚引用\"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。\n\n虚引用主要用来跟踪对象被垃圾回收的活动。\n\n虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。\n\n特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。\n\n\n# 如何判断一个常量是废弃常量?\n\n运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?\n\n假如在字符串常量池中存在字符串 \"abc\",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 \"abc\" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,\"abc\" 就会被系统清理出常量池了。\n\n\n# 如何判断一个类是无用的类\n\n方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?\n\n判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :\n\n * 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。\n * 加载该类的 ClassLoader 已经被回收。\n * 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。", + "normalizedContent": "java 中垃圾回收机制中如何判断对象需要回收? 即死亡对象判断方法 堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。\n\n\n# 引用计数法\n\n给对象中添加一个引用计数器:\n\n每当有一个地方引用它,计数器就加 1; 当引用失效,计数器就减 1; 任何时候计数器为 0 的对象就是不可能再被使用的。 这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。\n\n所谓对象之间的相互引用问题,如下面代码所示:除了对象 obja 和 objb 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 gc 回收器回收他们。\n\npublic class referencecountinggc {\n object instance = null;\n public static void main(string[] args) {\n referencecountinggc obja = new referencecountinggc();\n referencecountinggc objb = new referencecountinggc();\n obja.instance = objb;\n objb.instance = obja;\n obja = null;\n objb = null;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n\n# 可达性分析算法\n\n这个算法的基本思想就是通过一系列的称为 “gc roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 gc roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。\n\n下图中的 object 6 ~ object 10 之间虽有引用关系,但它们到 gc roots 不可达,因此为需要被回收的对象。\n\n哪些对象可以作为 gc roots 呢?\n\n虚拟机栈(栈帧中的本地变量表)中引用的对象 本地方法栈(native 方法)中引用的对象 方法区中类静态属性引用的对象 方法区中常量引用的对象 所有被同步锁持有的对象 对象可以被回收,就代表一定会被回收吗?\n\n即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 'finalize' 方法。当对象没有覆盖 'finalize' 方法,或 'finalize' 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。\n\n被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。\n\n> object 类中的 finalize 方法一直被认为是一个糟糕的设计,成为了 java 语言的负担,影响了 java 语言的安全和 gc 的性能。jdk9 版本及后续版本中各个类中的 finalize 方法会被逐渐弃用移除。忘掉它的存在吧!\n\n\n# 引用类型总结\n\n无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。\n\njdk1.2 之前,java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。\n\njdk1.2 以后,java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)\n\n1.强引用(strongreference)\n\n以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,java 虚拟机宁愿抛出 outofmemoryerror 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。\n\n2.软引用(softreference)\n\n如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。\n\n软引用可以和一个引用队列(referencequeue)联合使用,如果软引用所引用的对象被垃圾回收,java 虚拟机就会把这个软引用加入到与之关联的引用队列中。\n\n3.弱引用(weakreference)\n\n如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。\n\n弱引用可以和一个引用队列(referencequeue)联合使用,如果弱引用所引用的对象被垃圾回收,java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。\n\n4.虚引用(phantomreference)\n\n\"虚引用\"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。\n\n虚引用主要用来跟踪对象被垃圾回收的活动。\n\n虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(referencequeue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。\n\n特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 jvm 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(outofmemory)等问题的产生。\n\n\n# 如何判断一个常量是废弃常量?\n\n运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?\n\n假如在字符串常量池中存在字符串 \"abc\",如果当前没有任何 string 对象引用该字符串常量的话,就说明常量 \"abc\" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,\"abc\" 就会被系统清理出常量池了。\n\n\n# 如何判断一个类是无用的类\n\n方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?\n\n判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :\n\n * 该类所有的实例都已经被回收,也就是 java 堆中不存在该类的任何实例。\n * 加载该类的 classloader 已经被回收。\n * 该类对应的 java.lang.class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。", + "charsets": { + "cjk": true + } }, { - "title": "Java 中如何进行 GC 调优?", + "title": "常见的 GC 回收算法有哪些?", "frontmatter": { - "title": "Java 中如何进行 GC 调优?", - "date": "2022-05-21T17:31:53.000Z", - "permalink": "/pages/07a030/", + "title": "常见的 GC 回收算法有哪些?", + "date": "2022-09-18T19:01:29.000Z", + "permalink": "/pages/9b26e8/", "categories": [ "Java相关", "JVM" @@ -1987,23 +2024,116 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/020.Java%20%E4%B8%AD%E5%A6%82%E4%BD%95%E8%BF%9B%E8%A1%8C%20GC%20%E8%B0%83%E4%BC%98%EF%BC%9F.html", - "relativePath": "01.Java相关/30.JVM/020.Java 中如何进行 GC 调优?.md", - "key": "v-5474bec0", - "path": "/pages/07a030/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/010.%E5%B8%B8%E8%A7%81%E7%9A%84%20GC%20%E5%9B%9E%E6%94%B6%E7%AE%97%E6%B3%95%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F.html", + "relativePath": "01.Java相关/30.JVM/010.常见的 GC 回收算法有哪些?.md", + "key": "v-af821fc8", + "path": "/pages/9b26e8/", + "headers": [ + { + "level": 2, + "title": "标记-清除算法", + "slug": "标记-清除算法", + "normalizedTitle": "标记-清除算法", + "charIndex": 2 + }, + { + "level": 2, + "title": "标记-复制算法", + "slug": "标记-复制算法", + "normalizedTitle": "标记-复制算法", + "charIndex": 148 + }, + { + "level": 2, + "title": "标记-整理算法", + "slug": "标记-整理算法", + "normalizedTitle": "标记-整理算法", + "charIndex": 281 + }, + { + "level": 2, + "title": "分代收集算法", + "slug": "分代收集算法", + "normalizedTitle": "分代收集算法", + "charIndex": 382 + }, + { + "level": 2, + "title": "垃圾收集器", + "slug": "垃圾收集器", + "normalizedTitle": "垃圾收集器", + "charIndex": 682 + }, + { + "level": 2, + "title": "Serial 收集器", + "slug": "serial-收集器", + "normalizedTitle": "serial 收集器", + "charIndex": 893 + }, + { + "level": 2, + "title": "ParNew 收集器", + "slug": "parnew-收集器", + "normalizedTitle": "parnew 收集器", + "charIndex": 1324 + }, + { + "level": 2, + "title": "Parallel Scavenge 收集器", + "slug": "parallel-scavenge-收集器", + "normalizedTitle": "parallel scavenge 收集器", + "charIndex": 1678 + }, + { + "level": 2, + "title": "Serial Old 收集器", + "slug": "serial-old-收集器", + "normalizedTitle": "serial old 收集器", + "charIndex": 2694 + }, + { + "level": 2, + "title": "Parallel Old 收集器", + "slug": "parallel-old-收集器", + "normalizedTitle": "parallel old 收集器", + "charIndex": 2826 + }, + { + "level": 2, + "title": "CMS 收集器", + "slug": "cms-收集器", + "normalizedTitle": "cms 收集器", + "charIndex": 1503 + }, + { + "level": 2, + "title": "G1 收集器", + "slug": "g1-收集器", + "normalizedTitle": "g1 收集器", + "charIndex": 3638 + }, + { + "level": 2, + "title": "ZGC 收集器", + "slug": "zgc-收集器", + "normalizedTitle": "zgc 收集器", + "charIndex": 4351 + } + ], + "headersStr": "标记-清除算法 标记-复制算法 标记-整理算法 分代收集算法 垃圾收集器 Serial 收集器 ParNew 收集器 Parallel Scavenge 收集器 Serial Old 收集器 Parallel Old 收集器 CMS 收集器 G1 收集器 ZGC 收集器", + "content": "# 标记-清除算法\n\n该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:\n\n效率问题 空间问题(标记清除后会产生大量不连续的碎片)\n\n\n# 标记-复制算法\n\n为了解决效率问题,“标记-复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。\n\n\n# 标记-整理算法\n\n根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。\n\n\n# 分代收集算法\n\n当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。\n\n比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。\n\n延伸面试问题: HotSpot 为什么要分为新生代和老年代?\n\n根据上面的对分代收集算法的介绍回答。\n\n\n# 垃圾收集器\n\n如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。\n\n虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的 HotSpot 虚拟机就不会实现那么多不同的垃圾收集器了。\n\n\n# Serial 收集器\n\nSerial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( \"Stop The World\" ),直到它收集结束。\n\n新生代采用标记-复制算法,老年代采用标记-整理算法。\n\n\n\n虚拟机的设计者们当然知道 Stop The World 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。\n\n但是 Serial 收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。\n\n\n# ParNew 收集器\n\nParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。\n\n新生代采用标记-复制算法,老年代采用标记-整理算法。\n\n\n\n它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。\n\n并行和并发概念补充:\n\n并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。\n\n并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。\n\n\n# Parallel Scavenge 收集器\n\nParallel Scavenge 收集器也是使用标记-复制算法的多线程收集器,它看上去几乎和 ParNew 都一样。 那么它有什么特别之处呢?\n\n-XX:+UseParallelGC\n\n 使用 Parallel 收集器+ 老年代串行\n\n-XX:+UseParallelOldGC\n\n 使用 Parallel 收集器+ 老年代并行\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nParallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。 Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。\n\n新生代采用标记-复制算法,老年代采用标记-整理算法。\n\n\n\n这是 JDK1.8 默认收集器\n\n使用 java -XX:+PrintCommandLineFlags -version 命令查看\n\n-XX:InitialHeapSize=262921408 -XX:MaxHeapSize=4206742528 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC\njava version \"1.8.0_211\"\nJava(TM) SE Runtime Environment (build 1.8.0_211-b12)\nJava HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)\nJDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC 参数,则默认指定了-XX:+UseParallelOldGC,可以使用-XX:-UseParallelOldGC 来禁用该功能\n\n\n1\n2\n3\n4\n5\n\n\n\n# Serial Old 收集器\n\nSerial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。\n\n\n# Parallel Old 收集器\n\nParallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。\n\n\n# CMS 收集器\n\nCMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。\n\nCMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。\n\n从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:\n\n初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ; 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。\n\n从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:\n\n对 CPU 资源敏感; 无法处理浮动垃圾; 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。\n\n\n# G1 收集器\n\nG1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.\n\n被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备以下特点:\n\n并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。 空间整合:与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。 G1 收集器的运作大致分为以下几个步骤:\n\n初始标记 并发标记 最终标记 筛选回收 G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。\n\n\n# ZGC 收集器\n\n与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。\n\n在 ZGC 中出现 Stop The World 的情况会更少! 详情可以看 : 《新一代垃圾回收器 ZGC 的探索与实践》", + "normalizedContent": "# 标记-清除算法\n\n该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:\n\n效率问题 空间问题(标记清除后会产生大量不连续的碎片)\n\n\n# 标记-复制算法\n\n为了解决效率问题,“标记-复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。\n\n\n# 标记-整理算法\n\n根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。\n\n\n# 分代收集算法\n\n当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。\n\n比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。\n\n延伸面试问题: hotspot 为什么要分为新生代和老年代?\n\n根据上面的对分代收集算法的介绍回答。\n\n\n# 垃圾收集器\n\n如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。\n\n虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的 hotspot 虚拟机就不会实现那么多不同的垃圾收集器了。\n\n\n# serial 收集器\n\nserial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( \"stop the world\" ),直到它收集结束。\n\n新生代采用标记-复制算法,老年代采用标记-整理算法。\n\n\n\n虚拟机的设计者们当然知道 stop the world 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。\n\n但是 serial 收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。serial 收集器对于运行在 client 模式下的虚拟机来说是个不错的选择。\n\n\n# parnew 收集器\n\nparnew 收集器其实就是 serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 serial 收集器完全一样。\n\n新生代采用标记-复制算法,老年代采用标记-整理算法。\n\n\n\n它是许多运行在 server 模式下的虚拟机的首要选择,除了 serial 收集器外,只有它能与 cms 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。\n\n并行和并发概念补充:\n\n并行(parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。\n\n并发(concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 cpu 上。\n\n\n# parallel scavenge 收集器\n\nparallel scavenge 收集器也是使用标记-复制算法的多线程收集器,它看上去几乎和 parnew 都一样。 那么它有什么特别之处呢?\n\n-xx:+useparallelgc\n\n 使用 parallel 收集器+ 老年代串行\n\n-xx:+useparalleloldgc\n\n 使用 parallel 收集器+ 老年代并行\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nparallel scavenge 收集器关注点是吞吐量(高效率的利用 cpu)。cms 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 cpu 中用于运行用户代码的时间与 cpu 总消耗时间的比值。 parallel scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 parallel scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。\n\n新生代采用标记-复制算法,老年代采用标记-整理算法。\n\n\n\n这是 jdk1.8 默认收集器\n\n使用 java -xx:+printcommandlineflags -version 命令查看\n\n-xx:initialheapsize=262921408 -xx:maxheapsize=4206742528 -xx:+printcommandlineflags -xx:+usecompressedclasspointers -xx:+usecompressedoops -xx:+useparallelgc\njava version \"1.8.0_211\"\njava(tm) se runtime environment (build 1.8.0_211-b12)\njava hotspot(tm) 64-bit server vm (build 25.211-b12, mixed mode)\njdk1.8 默认使用的是 parallel scavenge + parallel old,如果指定了-xx:+useparallelgc 参数,则默认指定了-xx:+useparalleloldgc,可以使用-xx:-useparalleloldgc 来禁用该功能\n\n\n1\n2\n3\n4\n5\n\n\n\n# serial old 收集器\n\nserial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 jdk1.5 以及以前的版本中与 parallel scavenge 收集器搭配使用,另一种用途是作为 cms 收集器的后备方案。\n\n\n# parallel old 收集器\n\nparallel scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 cpu 资源的场合,都可以优先考虑 parallel scavenge 收集器和 parallel old 收集器。\n\n\n# cms 收集器\n\ncms(concurrent mark sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。\n\ncms(concurrent mark sweep)收集器是 hotspot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。\n\n从名字中的mark sweep这两个词可以看出,cms 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:\n\n初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ; 并发标记: 同时开启 gc 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 gc 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短 并发清除: 开启用户线程,同时 gc 线程开始对未标记的区域做清扫。\n\n从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:\n\n对 cpu 资源敏感; 无法处理浮动垃圾; 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。\n\n\n# g1 收集器\n\ng1 (garbage-first) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 gc 停顿时间要求的同时,还具备高吞吐量性能特征.\n\n被视为 jdk1.7 中 hotspot 虚拟机的一个重要进化特征。它具备以下特点:\n\n并行与并发:g1 能充分利用 cpu、多核环境下的硬件优势,使用多个 cpu(cpu 或者 cpu 核心)来缩短 stop-the-world 停顿时间。部分其他收集器原本需要停顿 java 线程执行的 gc 动作,g1 收集器仍然可以通过并发的方式让 java 程序继续执行。 分代收集:虽然 g1 可以不需要其他收集器配合就能独立管理整个 gc 堆,但是还是保留了分代的概念。 空间整合:与 cms 的“标记-清理”算法不同,g1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。 可预测的停顿:这是 g1 相对于 cms 的另一个大优势,降低停顿时间是 g1 和 cms 共同的关注点,但 g1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 m 毫秒的时间片段内。 g1 收集器的运作大致分为以下几个步骤:\n\n初始标记 并发标记 最终标记 筛选回收 g1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 region(这也就是它的名字 garbage-first 的由来) 。这种使用 region 划分内存空间以及有优先级的区域回收方式,保证了 g1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。\n\n\n# zgc 收集器\n\n与 cms 中的 parnew 和 g1 类似,zgc 也采用标记-复制算法,不过 zgc 对该算法做了重大改进。\n\n在 zgc 中出现 stop the world 的情况会更少! 详情可以看 : 《新一代垃圾回收器 zgc 的探索与实践》", + "charsets": { + "cjk": true + } }, { - "title": "JMM 中内存模型是怎样的?什么是指令序列重排序?", + "title": "Java 中如何进行 GC 调优?", "frontmatter": { - "title": "JMM 中内存模型是怎样的?什么是指令序列重排序?", - "date": "2022-05-21T17:32:49.000Z", - "permalink": "/pages/c8e6cb/", + "title": "Java 中如何进行 GC 调优?", + "date": "2022-05-21T17:31:53.000Z", + "permalink": "/pages/07a030/", "categories": [ "Java相关", "JVM" @@ -2012,23 +2142,76 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/030.JMM%20%E4%B8%AD%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B%E6%98%AF%E6%80%8E%E6%A0%B7%E7%9A%84%EF%BC%9F%E4%BB%80%E4%B9%88%E6%98%AF%E6%8C%87%E4%BB%A4%E5%BA%8F%E5%88%97%E9%87%8D%E6%8E%92%E5%BA%8F%EF%BC%9F.html", - "relativePath": "01.Java相关/30.JVM/030.JMM 中内存模型是怎样的?什么是指令序列重排序?.md", - "key": "v-0970cd54", - "path": "/pages/c8e6cb/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "JVM 内存是如何对应到操作系统内存的?", - "frontmatter": { - "title": "JVM 内存是如何对应到操作系统内存的?", - "date": "2022-05-21T17:33:05.000Z", - "permalink": "/pages/da65e7/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/020.Java%20%E4%B8%AD%E5%A6%82%E4%BD%95%E8%BF%9B%E8%A1%8C%20GC%20%E8%B0%83%E4%BC%98%EF%BC%9F.html", + "relativePath": "01.Java相关/30.JVM/020.Java 中如何进行 GC 调优?.md", + "key": "v-5474bec0", + "path": "/pages/07a030/", + "headers": [ + { + "level": 2, + "title": "GC优化需知", + "slug": "gc优化需知", + "normalizedTitle": "gc优化需知", + "charIndex": 305 + }, + { + "level": 2, + "title": "JVM内存结构", + "slug": "jvm内存结构", + "normalizedTitle": "jvm内存结构", + "charIndex": 506 + }, + { + "level": 2, + "title": "常见垃圾回收器", + "slug": "常见垃圾回收器", + "normalizedTitle": "常见垃圾回收器", + "charIndex": 1401 + }, + { + "level": 2, + "title": "参数基本策略", + "slug": "参数基本策略", + "normalizedTitle": "参数基本策略", + "charIndex": 1751 + }, + { + "level": 2, + "title": "确定目标", + "slug": "确定目标", + "normalizedTitle": "确定目标", + "charIndex": 2165 + }, + { + "level": 2, + "title": "优化", + "slug": "优化", + "normalizedTitle": "优化", + "charIndex": 20 + }, + { + "level": 2, + "title": "验收优化结果", + "slug": "验收优化结果", + "normalizedTitle": "验收优化结果", + "charIndex": 2841 + } + ], + "headersStr": "GC优化需知 JVM内存结构 常见垃圾回收器 参数基本策略 确定目标 优化 验收优化结果", + "content": "当Java程序性能达不到既定目标,且其他优化手段都已经穷尽时,通常需要调整垃圾回收器来进一步提高性能,称为GC优化。 但GC算法复杂,影响GC性能的参数众多,且参数调整又依赖于应用各自的特点,这些因素很大程度上增加了GC优化的难度。\n\n即便如此,GC调优也不是无章可循,仍然有一些通用的思考方法。\n\n本篇会介绍这些通用的GC优化策略和相关实践案例,主要包括如下内容:\n\n> 优化前准备: 简单回顾JVM相关知识、介绍GC优化的一些通用策略。\n> 优化方法: 介绍调优的一般流程:明确优化目标→优化→跟踪优化结果。\n> 优化案例: 简述笔者所在团队遇到的GC问题以及优化方案。\n\n\n# 一、优化前的准备\n\n\n# GC优化需知\n\n为了更好地理解本篇所介绍的内容,你需要了解如下内容。 1. GC相关基础知识,包括但不限于: a) GC工作原理。 b) 理解新生代、老年代、晋升等术语含义。 c) 可以看懂GC日志。\n\nGC优化不能解决一切性能问题,它是最后的调优手段。 如果对第一点中提及的知识点不是很熟悉,可以先阅读小结-JVM基础回顾;如果已经很熟悉,可以跳过该节直接往下阅读。\n\n\n# JVM基础回顾\n\n\n# JVM内存结构\n\n简单介绍一下JVM内存结构和常见的垃圾回收器。\n\n当代主流虚拟机(Hotspot VM)的垃圾回收都采用“分代回收”的算法。“分代回收”是基于这样一个事实:对象的生命周期不同,所以针对不同生命周期的对象可以采取不同的回收方式,以便提高回收效率。\n\nHotspot VM将内存划分为不同的物理区,就是“分代”思想的体现。如图所示,JVM内存主要由新生代、老年代、永久代构成。\n\n\n\n① 新生代(Young Generation):大多数对象在新生代中被创建,其中很多对象的生命周期很短。每次新生代的垃圾回收(又称Minor GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。\n\n新生代内又分三个区:一个Eden区,两个Survivor区(一般而言),大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到两个Survivor区(中的一个)。当这个Survivor区满时,此区的存活且不满足“晋升”条件的对象将被复制到另外一个Survivor区。对象每经历一次Minor GC,年龄加1,达到“晋升年龄阈值”后,被放到老年代,这个过程也称为“晋升”。显然,“晋升年龄阈值”的大小直接影响着对象在新生代中的停留时间,在Serial和ParNew GC两种回收器中,“晋升年龄阈值”通过参数MaxTenuringThreshold设定,默认值为15。\n\n② 老年代(Old Generation):在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代,该区域中对象存活率高。老年代的垃圾回收(又称Major GC)通常使用“标记-清理”或“标记-整理”算法。整堆包括新生代和老年代的垃圾回收称为Full GC(HotSpot VM里,除了CMS之外,其它能收集老年代的GC都会同时收集整个GC堆,包括新生代)。\n\n③ 永久代(Perm Generation):主要存放元数据,例如Class、Method的元信息,与垃圾回收要回收的Java对象关系不大。相对于新生代和年老代来说,该区域的划分对垃圾回收影响比较小。\n\n\n# 常见垃圾回收器\n\n不同的垃圾回收器,适用于不同的场景。常用的垃圾回收器:\n\n串行(Serial)回收器是单线程的一个回收器,简单、易实现、效率高。 并行(ParNew)回收器是Serial的多线程版,可以充分的利用CPU资源,减少回收的时间。 吞吐量优先(Parallel Scavenge)回收器,侧重于吞吐量的控制。 并发标记清除(CMS,Concurrent Mark Sweep)回收器是一种以获取最短回收停顿时间为目标的回收器,该回收器是基于“标记-清除”算法实现的。 GC日志 每一种回收器的日志格式都是由其自身的实现决定的,换而言之,每种回收器的日志格式都可以不一样。但虚拟机设计者为了方便用户阅读,将各个回收器的日志都维持一定的共性。JavaGC日志 中简单介绍了这些共性。\n\n\n# 参数基本策略\n\n各分区的大小对GC的性能影响很大。如何将各分区调整到合适的大小,分析活跃数据的大小是很好的切入点。\n\n活跃数据的大小是指,应用程序稳定运行时长期存活对象在堆中占用的空间大小,也就是Full GC后堆中老年代占用空间的大小。可以通过GC日志中Full GC之后老年代数据大小得出,比较准确的方法是在程序稳定后,多次获取GC数据,通过取平均值的方式计算活跃数据的大小。活跃数据和各分区之间的比例关系如下(见参考文献1):\n\n例如,根据GC日志获得老年代的活跃数据大小为300M,那么各分区大小可以设为:\n\n总堆:1200MB = 300MB × 4* 新生代:450MB = 300MB × 1.5* 老年代: 750MB = 1200MB - 450MB*\n\n这部分设置仅仅是堆大小的初始值,后面的优化中,可能会调整这些值,具体情况取决于应用程序的特性和需求。\n\n\n# 二、优化步骤\n\nGC优化一般步骤可以概括为:确定目标、优化参数、验收结果。\n\n\n# 确定目标\n\n明确应用程序的系统需求是性能优化的基础,系统的需求是指应用程序运行时某方面的要求,譬如: - 高可用,可用性达到几个9。 - 低延迟,请求必须多少毫秒内完成响应。 - 高吞吐,每秒完成多少次事务。\n\n明确系统需求之所以重要,是因为上述性能指标间可能冲突。比如通常情况下,缩小延迟的代价是降低吞吐量或者消耗更多的内存或者两者同时发生。\n\n由于笔者所在团队主要关注高可用和低延迟两项指标,所以接下来分析,如何量化GC时间和频率对于响应时间和可用性的影响。通过这个量化指标,可以计算出当前GC情况对服务的影响,也能评估出GC优化后对响应时间的收益,这两点对于低延迟服务很重要。\n\n举例:假设单位时间T内发生一次持续25ms的GC,接口平均响应时间为50ms,且请求均匀到达,根据下图所示:\n\n\n\n那么有(50ms+25ms)/T比例的请求会受GC影响,其中GC前的50ms内到达的请求都会增加25ms,GC期间的25ms内到达的请求,会增加0-25ms不等,如果时间T内发生N次GC,受GC影响请求占比=(接口响应时间+GC时间)×N/T 。可见无论降低单次GC时间还是降低GC次数N都可以有效减少GC对响应时间的影响。\n\n\n# 优化\n\n通过收集GC信息,结合系统需求,确定优化方案,例如选用合适的GC回收器、重新设置内存比例、调整JVM参数等。\n\n进行调整后,将不同的优化方案分别应用到多台机器上,然后比较这些机器上GC的性能差异,有针对性的做出选择,再通过不断的试验和观察,找到最合适的参数。\n\n\n# 验收优化结果\n\n将修改应用到所有服务器,判断优化结果是否符合预期,总结相关经验。\n\n参考:美团技术团队-从实际案例聊聊Java应用的GC优化", + "normalizedContent": "当java程序性能达不到既定目标,且其他优化手段都已经穷尽时,通常需要调整垃圾回收器来进一步提高性能,称为gc优化。 但gc算法复杂,影响gc性能的参数众多,且参数调整又依赖于应用各自的特点,这些因素很大程度上增加了gc优化的难度。\n\n即便如此,gc调优也不是无章可循,仍然有一些通用的思考方法。\n\n本篇会介绍这些通用的gc优化策略和相关实践案例,主要包括如下内容:\n\n> 优化前准备: 简单回顾jvm相关知识、介绍gc优化的一些通用策略。\n> 优化方法: 介绍调优的一般流程:明确优化目标→优化→跟踪优化结果。\n> 优化案例: 简述笔者所在团队遇到的gc问题以及优化方案。\n\n\n# 一、优化前的准备\n\n\n# gc优化需知\n\n为了更好地理解本篇所介绍的内容,你需要了解如下内容。 1. gc相关基础知识,包括但不限于: a) gc工作原理。 b) 理解新生代、老年代、晋升等术语含义。 c) 可以看懂gc日志。\n\ngc优化不能解决一切性能问题,它是最后的调优手段。 如果对第一点中提及的知识点不是很熟悉,可以先阅读小结-jvm基础回顾;如果已经很熟悉,可以跳过该节直接往下阅读。\n\n\n# jvm基础回顾\n\n\n# jvm内存结构\n\n简单介绍一下jvm内存结构和常见的垃圾回收器。\n\n当代主流虚拟机(hotspot vm)的垃圾回收都采用“分代回收”的算法。“分代回收”是基于这样一个事实:对象的生命周期不同,所以针对不同生命周期的对象可以采取不同的回收方式,以便提高回收效率。\n\nhotspot vm将内存划分为不同的物理区,就是“分代”思想的体现。如图所示,jvm内存主要由新生代、老年代、永久代构成。\n\n\n\n① 新生代(young generation):大多数对象在新生代中被创建,其中很多对象的生命周期很短。每次新生代的垃圾回收(又称minor gc)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。\n\n新生代内又分三个区:一个eden区,两个survivor区(一般而言),大部分对象在eden区中生成。当eden区满时,还存活的对象将被复制到两个survivor区(中的一个)。当这个survivor区满时,此区的存活且不满足“晋升”条件的对象将被复制到另外一个survivor区。对象每经历一次minor gc,年龄加1,达到“晋升年龄阈值”后,被放到老年代,这个过程也称为“晋升”。显然,“晋升年龄阈值”的大小直接影响着对象在新生代中的停留时间,在serial和parnew gc两种回收器中,“晋升年龄阈值”通过参数maxtenuringthreshold设定,默认值为15。\n\n② 老年代(old generation):在新生代中经历了n次垃圾回收后仍然存活的对象,就会被放到年老代,该区域中对象存活率高。老年代的垃圾回收(又称major gc)通常使用“标记-清理”或“标记-整理”算法。整堆包括新生代和老年代的垃圾回收称为full gc(hotspot vm里,除了cms之外,其它能收集老年代的gc都会同时收集整个gc堆,包括新生代)。\n\n③ 永久代(perm generation):主要存放元数据,例如class、method的元信息,与垃圾回收要回收的java对象关系不大。相对于新生代和年老代来说,该区域的划分对垃圾回收影响比较小。\n\n\n# 常见垃圾回收器\n\n不同的垃圾回收器,适用于不同的场景。常用的垃圾回收器:\n\n串行(serial)回收器是单线程的一个回收器,简单、易实现、效率高。 并行(parnew)回收器是serial的多线程版,可以充分的利用cpu资源,减少回收的时间。 吞吐量优先(parallel scavenge)回收器,侧重于吞吐量的控制。 并发标记清除(cms,concurrent mark sweep)回收器是一种以获取最短回收停顿时间为目标的回收器,该回收器是基于“标记-清除”算法实现的。 gc日志 每一种回收器的日志格式都是由其自身的实现决定的,换而言之,每种回收器的日志格式都可以不一样。但虚拟机设计者为了方便用户阅读,将各个回收器的日志都维持一定的共性。javagc日志 中简单介绍了这些共性。\n\n\n# 参数基本策略\n\n各分区的大小对gc的性能影响很大。如何将各分区调整到合适的大小,分析活跃数据的大小是很好的切入点。\n\n活跃数据的大小是指,应用程序稳定运行时长期存活对象在堆中占用的空间大小,也就是full gc后堆中老年代占用空间的大小。可以通过gc日志中full gc之后老年代数据大小得出,比较准确的方法是在程序稳定后,多次获取gc数据,通过取平均值的方式计算活跃数据的大小。活跃数据和各分区之间的比例关系如下(见参考文献1):\n\n例如,根据gc日志获得老年代的活跃数据大小为300m,那么各分区大小可以设为:\n\n总堆:1200mb = 300mb × 4* 新生代:450mb = 300mb × 1.5* 老年代: 750mb = 1200mb - 450mb*\n\n这部分设置仅仅是堆大小的初始值,后面的优化中,可能会调整这些值,具体情况取决于应用程序的特性和需求。\n\n\n# 二、优化步骤\n\ngc优化一般步骤可以概括为:确定目标、优化参数、验收结果。\n\n\n# 确定目标\n\n明确应用程序的系统需求是性能优化的基础,系统的需求是指应用程序运行时某方面的要求,譬如: - 高可用,可用性达到几个9。 - 低延迟,请求必须多少毫秒内完成响应。 - 高吞吐,每秒完成多少次事务。\n\n明确系统需求之所以重要,是因为上述性能指标间可能冲突。比如通常情况下,缩小延迟的代价是降低吞吐量或者消耗更多的内存或者两者同时发生。\n\n由于笔者所在团队主要关注高可用和低延迟两项指标,所以接下来分析,如何量化gc时间和频率对于响应时间和可用性的影响。通过这个量化指标,可以计算出当前gc情况对服务的影响,也能评估出gc优化后对响应时间的收益,这两点对于低延迟服务很重要。\n\n举例:假设单位时间t内发生一次持续25ms的gc,接口平均响应时间为50ms,且请求均匀到达,根据下图所示:\n\n\n\n那么有(50ms+25ms)/t比例的请求会受gc影响,其中gc前的50ms内到达的请求都会增加25ms,gc期间的25ms内到达的请求,会增加0-25ms不等,如果时间t内发生n次gc,受gc影响请求占比=(接口响应时间+gc时间)×n/t 。可见无论降低单次gc时间还是降低gc次数n都可以有效减少gc对响应时间的影响。\n\n\n# 优化\n\n通过收集gc信息,结合系统需求,确定优化方案,例如选用合适的gc回收器、重新设置内存比例、调整jvm参数等。\n\n进行调整后,将不同的优化方案分别应用到多台机器上,然后比较这些机器上gc的性能差异,有针对性的做出选择,再通过不断的试验和观察,找到最合适的参数。\n\n\n# 验收优化结果\n\n将修改应用到所有服务器,判断优化结果是否符合预期,总结相关经验。\n\n参考:美团技术团队-从实际案例聊聊java应用的gc优化", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "JMM 中内存模型是怎样的?什么是指令序列重排序?", + "frontmatter": { + "title": "JMM 中内存模型是怎样的?什么是指令序列重排序?", + "date": "2022-05-21T17:32:49.000Z", + "permalink": "/pages/c8e6cb/", "categories": [ "Java相关", "JVM" @@ -2037,23 +2220,62 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/040.JVM%20%E5%86%85%E5%AD%98%E6%98%AF%E5%A6%82%E4%BD%95%E5%AF%B9%E5%BA%94%E5%88%B0%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%86%85%E5%AD%98%E7%9A%84%EF%BC%9F.html", - "relativePath": "01.Java相关/30.JVM/040.JVM 内存是如何对应到操作系统内存的?.md", - "key": "v-2de19204", - "path": "/pages/da65e7/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/030.JMM%20%E4%B8%AD%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B%E6%98%AF%E6%80%8E%E6%A0%B7%E7%9A%84%EF%BC%9F%E4%BB%80%E4%B9%88%E6%98%AF%E6%8C%87%E4%BB%A4%E5%BA%8F%E5%88%97%E9%87%8D%E6%8E%92%E5%BA%8F%EF%BC%9F.html", + "relativePath": "01.Java相关/30.JVM/030.JMM 中内存模型是怎样的?什么是指令序列重排序?.md", + "key": "v-0970cd54", + "path": "/pages/c8e6cb/", + "headers": [ + { + "level": 2, + "title": "为什么要有内存模型?", + "slug": "为什么要有内存模型", + "normalizedTitle": "为什么要有内存模型?", + "charIndex": 40 + }, + { + "level": 2, + "title": "硬件内存架构", + "slug": "硬件内存架构", + "normalizedTitle": "硬件内存架构", + "charIndex": 73 + }, + { + "level": 2, + "title": "缓存一致性问题", + "slug": "缓存一致性问题", + "normalizedTitle": "缓存一致性问题", + "charIndex": 557 + }, + { + "level": 2, + "title": "处理器优化和指令重排序", + "slug": "处理器优化和指令重排序", + "normalizedTitle": "处理器优化和指令重排序", + "charIndex": 943 + }, + { + "level": 2, + "title": "并发编程的问题", + "slug": "并发编程的问题", + "normalizedTitle": "并发编程的问题", + "charIndex": 1368 + } + ], + "headersStr": "为什么要有内存模型? 硬件内存架构 缓存一致性问题 处理器优化和指令重排序 并发编程的问题", + "content": "Java内存模型(JMM)和 Java 运行时内存区域是不一样的概念。\n\n\n# 为什么要有内存模型?\n\n要想回答这个问题,我们需要先弄懂传统计算机硬件内存架构。好了,我要开始画图了。\n\n\n# 硬件内存架构\n\n\n\n(1)CPU\n\n去过机房的同学都知道,一般在大型服务器上会配置多个CPU,每个CPU还会有多个\n\n核\n\n,这就意味着多个CPU或者多个核可以同时(并发)工作。如果使用Java 起了一个多线程的任务,很有可能每个 CPU 都会跑一个线程,那么你的任务在某一刻就是真正并发执行了。\n\n(2)CPU Register\n\nCPU Register也就是 CPU 寄存器。CPU 寄存器是 CPU 内部集成的,在寄存器上执行操作的效率要比在主存上高出几个数量级。\n\n(3)CPU Cache Memory\n\nCPU Cache Memory也就是 CPU 高速缓存,相对于寄存器来说,通常也可以成为 L2 二级缓存。相对于硬盘读取速度来说内存读取的效率非常高,但是与 CPU 还是相差数量级,所以在 CPU 和主存间引入了多级缓存,目的是为了做一下缓冲。\n\n(4)Main Memory\n\nMain Memory 就是主存,主存比 L1、L2 缓存要大很多。\n\n注意:部分高端机器还有 L3 三级缓存。\n\n\n# 缓存一致性问题\n\n由于主存与 CPU 处理器的运算能力之间有数量级的差距,所以在传统计算机内存架构中会引入高速缓存来作为主存和处理器之间的缓冲,CPU 将常用的数据放在高速缓存中,运算结束后 CPU 再讲运算结果同步到主存中。\n\n使用高速缓存解决了 CPU 和主存速率不匹配的问题,但同时又引入另外一个新问题:缓存一致性问题。\n\n在多CPU的系统中(或者单CPU多核的系统),每个CPU内核都有自己的高速缓存,它们共享同一主内存(Main Memory)。当多个CPU的运算任务都涉及同一块主内存区域时,CPU 会将数据读取到缓存中进行运算,这可能会导致各自的缓存数据不一致。\n\n因此需要每个 CPU 访问缓存时遵循一定的协议,在读写数据时根据协议进行操作,共同来维护缓存的一致性。这类协议有 MSI、MESI、MOSI、和 Dragon Protocol 等。\n\n\n# 处理器优化和指令重排序\n\n为了提升性能在 CPU 和主内存之间增加了高速缓存,但在多线程并发场景可能会遇到\n\n。那还有没有办法进一步提升 CPU 的执行效率呢?答案是:处理器优化。\n\n为了使处理器内部的运算单元能够最大化被充分利用,处理器会对输入代码进行乱序执行处理,这就是处理器优化。\n\n除了处理器会对代码进行优化处理,很多现代编程语言的编译器也会做类似的优化,比如像 Java 的即时编译器(JIT)会做指令重排序。\n\n处理器优化其实也是重排序的一种类型,这里总结一下,重排序可以分为三种类型: ** 编译器优化的重排序。编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序。 ** 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 ** 内存系统的重排序。由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。\n\n\n# 并发编程的问题\n\n上面讲了一堆硬件相关的东西,有些同学可能会有点懵,绕了这么大圈,这些东西跟 Java 内存模型有啥关系吗?不要急咱们慢慢往下看。\n\n熟悉 Java 并发的同学肯定对这三个问题很熟悉:『可见性问题』、『原子性问题』、『有序性问题』。如果从更深层次看这三个问题,其实就是上面讲的『缓存一致性』、『处理器优化』、『指令重排序』造成的。\n\n缓存一致性问题其实就是可见性问题,处理器优化可能会造成原子性问题,指令重排序会造成有序性问题,你看是不是都联系上了。\n\n出了问题总是要解决的,那有什么办法呢?首先想到简单粗暴的办法,干掉缓存让 CPU 直接与主内存交互就解决了可见性问题,禁止处理器优化和指令重排序就解决了原子性和有序性问题,但这样一夜回到解放前了,显然不可取。\n\n所以技术前辈们想到了在物理机器上定义出一套内存模型, 规范内存的读写操作。内存模型解决并发问题主要采用两种方式:\n\n限制处理器优化和使用内存屏障。\n\n参考: 全面学习掌握Java内存模型", + "normalizedContent": "java内存模型(jmm)和 java 运行时内存区域是不一样的概念。\n\n\n# 为什么要有内存模型?\n\n要想回答这个问题,我们需要先弄懂传统计算机硬件内存架构。好了,我要开始画图了。\n\n\n# 硬件内存架构\n\n\n\n(1)cpu\n\n去过机房的同学都知道,一般在大型服务器上会配置多个cpu,每个cpu还会有多个\n\n核\n\n,这就意味着多个cpu或者多个核可以同时(并发)工作。如果使用java 起了一个多线程的任务,很有可能每个 cpu 都会跑一个线程,那么你的任务在某一刻就是真正并发执行了。\n\n(2)cpu register\n\ncpu register也就是 cpu 寄存器。cpu 寄存器是 cpu 内部集成的,在寄存器上执行操作的效率要比在主存上高出几个数量级。\n\n(3)cpu cache memory\n\ncpu cache memory也就是 cpu 高速缓存,相对于寄存器来说,通常也可以成为 l2 二级缓存。相对于硬盘读取速度来说内存读取的效率非常高,但是与 cpu 还是相差数量级,所以在 cpu 和主存间引入了多级缓存,目的是为了做一下缓冲。\n\n(4)main memory\n\nmain memory 就是主存,主存比 l1、l2 缓存要大很多。\n\n注意:部分高端机器还有 l3 三级缓存。\n\n\n# 缓存一致性问题\n\n由于主存与 cpu 处理器的运算能力之间有数量级的差距,所以在传统计算机内存架构中会引入高速缓存来作为主存和处理器之间的缓冲,cpu 将常用的数据放在高速缓存中,运算结束后 cpu 再讲运算结果同步到主存中。\n\n使用高速缓存解决了 cpu 和主存速率不匹配的问题,但同时又引入另外一个新问题:缓存一致性问题。\n\n在多cpu的系统中(或者单cpu多核的系统),每个cpu内核都有自己的高速缓存,它们共享同一主内存(main memory)。当多个cpu的运算任务都涉及同一块主内存区域时,cpu 会将数据读取到缓存中进行运算,这可能会导致各自的缓存数据不一致。\n\n因此需要每个 cpu 访问缓存时遵循一定的协议,在读写数据时根据协议进行操作,共同来维护缓存的一致性。这类协议有 msi、mesi、mosi、和 dragon protocol 等。\n\n\n# 处理器优化和指令重排序\n\n为了提升性能在 cpu 和主内存之间增加了高速缓存,但在多线程并发场景可能会遇到\n\n。那还有没有办法进一步提升 cpu 的执行效率呢?答案是:处理器优化。\n\n为了使处理器内部的运算单元能够最大化被充分利用,处理器会对输入代码进行乱序执行处理,这就是处理器优化。\n\n除了处理器会对代码进行优化处理,很多现代编程语言的编译器也会做类似的优化,比如像 java 的即时编译器(jit)会做指令重排序。\n\n处理器优化其实也是重排序的一种类型,这里总结一下,重排序可以分为三种类型: ** 编译器优化的重排序。编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序。 ** 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 ** 内存系统的重排序。由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。\n\n\n# 并发编程的问题\n\n上面讲了一堆硬件相关的东西,有些同学可能会有点懵,绕了这么大圈,这些东西跟 java 内存模型有啥关系吗?不要急咱们慢慢往下看。\n\n熟悉 java 并发的同学肯定对这三个问题很熟悉:『可见性问题』、『原子性问题』、『有序性问题』。如果从更深层次看这三个问题,其实就是上面讲的『缓存一致性』、『处理器优化』、『指令重排序』造成的。\n\n缓存一致性问题其实就是可见性问题,处理器优化可能会造成原子性问题,指令重排序会造成有序性问题,你看是不是都联系上了。\n\n出了问题总是要解决的,那有什么办法呢?首先想到简单粗暴的办法,干掉缓存让 cpu 直接与主内存交互就解决了可见性问题,禁止处理器优化和指令重排序就解决了原子性和有序性问题,但这样一夜回到解放前了,显然不可取。\n\n所以技术前辈们想到了在物理机器上定义出一套内存模型, 规范内存的读写操作。内存模型解决并发问题主要采用两种方式:\n\n限制处理器优化和使用内存屏障。\n\n参考: 全面学习掌握java内存模型", + "charsets": { + "cjk": true + }, "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, { - "title": "JVM 是怎么去调优的?了解哪些参数和指令?", + "title": "JVM 内存是如何对应到操作系统内存的?", "frontmatter": { - "title": "JVM 是怎么去调优的?了解哪些参数和指令?", - "date": "2022-05-21T17:33:16.000Z", - "permalink": "/pages/d730dd/", + "title": "JVM 内存是如何对应到操作系统内存的?", + "date": "2022-05-21T17:33:05.000Z", + "permalink": "/pages/da65e7/", "categories": [ "Java相关", "JVM" @@ -2062,14 +2284,46 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/050.JVM%20%E6%98%AF%E6%80%8E%E4%B9%88%E5%8E%BB%E8%B0%83%E4%BC%98%E7%9A%84%EF%BC%9F%E4%BA%86%E8%A7%A3%E5%93%AA%E4%BA%9B%E5%8F%82%E6%95%B0%E5%92%8C%E6%8C%87%E4%BB%A4%EF%BC%9F.html", - "relativePath": "01.Java相关/30.JVM/050.JVM 是怎么去调优的?了解哪些参数和指令?.md", - "key": "v-37fcb440", - "path": "/pages/d730dd/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/040.JVM%20%E5%86%85%E5%AD%98%E6%98%AF%E5%A6%82%E4%BD%95%E5%AF%B9%E5%BA%94%E5%88%B0%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%86%85%E5%AD%98%E7%9A%84%EF%BC%9F.html", + "relativePath": "01.Java相关/30.JVM/040.JVM 内存是如何对应到操作系统内存的?.md", + "key": "v-2de19204", + "path": "/pages/da65e7/", + "headers": [ + { + "level": 2, + "title": "Java 内存模型", + "slug": "java-内存模型", + "normalizedTitle": "java 内存模型", + "charIndex": 2 + }, + { + "level": 2, + "title": "Java 线程与主内存的关系", + "slug": "java-线程与主内存的关系", + "normalizedTitle": "java 线程与主内存的关系", + "charIndex": 216 + }, + { + "level": 2, + "title": "线程间通信", + "slug": "线程间通信", + "normalizedTitle": "线程间通信", + "charIndex": 421 + }, + { + "level": 2, + "title": "有态度的总结", + "slug": "有态度的总结", + "normalizedTitle": "有态度的总结", + "charIndex": 1043 + } + ], + "headersStr": "Java 内存模型 Java 线程与主内存的关系 线程间通信 有态度的总结", + "content": "# Java 内存模型\n\n同一套内存模型规范,不同语言在实现上可能会有些差别。接下来着重讲一下 Java 内存模型实现原理。\n\nJava 运行时内存区域与硬件内存的关系\n\n了解过 JVM 的同学都知道,JVM 运行时内存区域是分片的,分为栈、堆等,其实这些都是 JVM 定义的逻辑概念。在传统的硬件内存架构中是没有栈和堆这种概念。\n\n\n\n从图中可以看出栈和堆既存在于高速缓存中又存在于主内存中,所以两者并没有很直接的关系。\n\n\n# Java 线程与主内存的关系\n\nJava 内存模型是一种规范,定义了很多东西:\n\n所有的变量都存储在主内存(Main Memory)中。 每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。 不同的线程之间无法直接访问对方本地内存中的变量。 看文字太枯燥了,我又画了一张图:\n\n\n# 线程间通信\n\n如果两个线程都对一个共享变量进行操作,共享变量初始值为 1,每个线程都变量进行加 1,预期共享变量的值为 3。在 JMM 规范下会有一系列的操作。\n\n\n\n为了更好的控制主内存和本地内存的交互,Java 内存模型定义了八种操作来实现:\n\n * lock:锁定。作用于主内存的变量,把一个变量标识为一条线程独占状态。\n * unlock:解锁。作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。\n * read:读取。作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用\n * load:载入。作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。\n * use:使用。作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。\n * assign:赋值。作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。\n * store:存储。作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。\n * write:写入。作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。 注意:工作内存也就是本地内存的意思。\n\n\n# 有态度的总结\n\n由于CPU 和主内存间存在数量级的速率差,想到了引入了多级高速缓存的传统硬件内存架构来解决,多级高速缓存作为 CPU 和主内间的缓冲提升了整体性能。解决了速率差的问题,却又带来了缓存一致性问题。\n\n数据同时存在于高速缓存和主内存中,如果不加以规范势必造成灾难,因此在传统机器上又抽象出了内存模型。\n\nJava 语言在遵循内存模型的基础上推出了 JMM 规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。\n\n为了更精准控制工作内存和主内存间的交互,JMM 还定义了八种操作:lock, unlock, read, load,use,assign, store, write。\n\n好了,今天就给大家介绍到这里,简单总结下Java内存模型的定义:Java内存模型并不是一件容易的事情,这个模型必须定义得足够严谨,才能让Java的并发操作不会产生歧义;但是,也必须得足够宽松,使得虚拟机的实现能有足够的自由空间去利用硬件的各种特性(寄存器、高速缓存等)来获取更好的执行速度。经过长时间的验证和修补,在JDK1.5发布后,Java内存模型就已经成熟和完善起来了。\n\n参考: 全面学习掌握Java内存模型", + "normalizedContent": "# java 内存模型\n\n同一套内存模型规范,不同语言在实现上可能会有些差别。接下来着重讲一下 java 内存模型实现原理。\n\njava 运行时内存区域与硬件内存的关系\n\n了解过 jvm 的同学都知道,jvm 运行时内存区域是分片的,分为栈、堆等,其实这些都是 jvm 定义的逻辑概念。在传统的硬件内存架构中是没有栈和堆这种概念。\n\n\n\n从图中可以看出栈和堆既存在于高速缓存中又存在于主内存中,所以两者并没有很直接的关系。\n\n\n# java 线程与主内存的关系\n\njava 内存模型是一种规范,定义了很多东西:\n\n所有的变量都存储在主内存(main memory)中。 每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。 不同的线程之间无法直接访问对方本地内存中的变量。 看文字太枯燥了,我又画了一张图:\n\n\n# 线程间通信\n\n如果两个线程都对一个共享变量进行操作,共享变量初始值为 1,每个线程都变量进行加 1,预期共享变量的值为 3。在 jmm 规范下会有一系列的操作。\n\n\n\n为了更好的控制主内存和本地内存的交互,java 内存模型定义了八种操作来实现:\n\n * lock:锁定。作用于主内存的变量,把一个变量标识为一条线程独占状态。\n * unlock:解锁。作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。\n * read:读取。作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用\n * load:载入。作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。\n * use:使用。作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。\n * assign:赋值。作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。\n * store:存储。作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。\n * write:写入。作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。 注意:工作内存也就是本地内存的意思。\n\n\n# 有态度的总结\n\n由于cpu 和主内存间存在数量级的速率差,想到了引入了多级高速缓存的传统硬件内存架构来解决,多级高速缓存作为 cpu 和主内间的缓冲提升了整体性能。解决了速率差的问题,却又带来了缓存一致性问题。\n\n数据同时存在于高速缓存和主内存中,如果不加以规范势必造成灾难,因此在传统机器上又抽象出了内存模型。\n\njava 语言在遵循内存模型的基础上推出了 jmm 规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。\n\n为了更精准控制工作内存和主内存间的交互,jmm 还定义了八种操作:lock, unlock, read, load,use,assign, store, write。\n\n好了,今天就给大家介绍到这里,简单总结下java内存模型的定义:java内存模型并不是一件容易的事情,这个模型必须定义得足够严谨,才能让java的并发操作不会产生歧义;但是,也必须得足够宽松,使得虚拟机的实现能有足够的自由空间去利用硬件的各种特性(寄存器、高速缓存等)来获取更好的执行速度。经过长时间的验证和修补,在jdk1.5发布后,java内存模型就已经成熟和完善起来了。\n\n参考: 全面学习掌握java内存模型", + "charsets": { + "cjk": true + }, "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, @@ -2091,48 +2345,178 @@ export const siteData = { "relativePath": "01.Java相关/30.JVM/060.什么是内存泄漏,怎么确定内存泄漏?.md", "key": "v-15fba8f1", "path": "/pages/3ab5f5/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, + "headers": [ + { + "level": 2, + "title": "前言", + "slug": "前言", + "normalizedTitle": "前言", + "charIndex": 2 + }, + { + "level": 2, + "title": "一、介绍", + "slug": "一、介绍", + "normalizedTitle": "一、介绍", + "charIndex": 178 + }, + { + "level": 3, + "title": "1、什么是内存泄漏", + "slug": "_1、什么是内存泄漏", + "normalizedTitle": "1、什么是内存泄漏", + "charIndex": 187 + }, + { + "level": 3, + "title": "2、内存泄漏的危害", + "slug": "_2、内存泄漏的危害", + "normalizedTitle": "2、内存泄漏的危害", + "charIndex": 404 + }, + { + "level": 2, + "title": "二、内存泄漏原因", + "slug": "二、内存泄漏原因", + "normalizedTitle": "二、内存泄漏原因", + "charIndex": 491 + }, + { + "level": 3, + "title": "1、大量使用静态变量", + "slug": "_1、大量使用静态变量", + "normalizedTitle": "1、大量使用静态变量", + "charIndex": 583 + }, + { + "level": 3, + "title": "2、连接资源未关闭", + "slug": "_2、连接资源未关闭", + "normalizedTitle": "2、连接资源未关闭", + "charIndex": 1310 + }, + { + "level": 3, + "title": "4、内部类持有外部类", + "slug": "_4、内部类持有外部类", + "normalizedTitle": "4、内部类持有外部类", + "charIndex": 2418 + }, + { + "level": 3, + "title": "5、finalize方法", + "slug": "_5、finalize方法", + "normalizedTitle": "5、finalize方法", + "charIndex": 2445 + }, + { + "level": 3, + "title": "6、ThreadLocal的错误使用", + "slug": "_6、threadlocal的错误使用", + "normalizedTitle": "6、threadlocal的错误使用", + "charIndex": 2595 + }, + { + "level": 2, + "title": "三、检测内存泄漏", + "slug": "三、检测内存泄漏", + "normalizedTitle": "三、检测内存泄漏", + "charIndex": 3062 + }, + { + "level": 3, + "title": "1、工具分析", + "slug": "_1、工具分析", + "normalizedTitle": "1、工具分析", + "charIndex": 3106 + }, + { + "level": 3, + "title": "2、垃圾回收分析", + "slug": "_2、垃圾回收分析", + "normalizedTitle": "2、垃圾回收分析", + "charIndex": 3232 + }, + { + "level": 3, + "title": "3、基准测试", + "slug": "_3、基准测试", + "normalizedTitle": "3、基准测试", + "charIndex": 3374 + }, + { + "level": 2, + "title": "四、结论", + "slug": "四、结论", + "normalizedTitle": "四、结论", + "charIndex": 3418 + } + ], + "headersStr": "前言 一、介绍 1、什么是内存泄漏 2、内存泄漏的危害 二、内存泄漏原因 1、大量使用静态变量 2、连接资源未关闭 4、内部类持有外部类 5、finalize方法 6、ThreadLocal的错误使用 三、检测内存泄漏 1、工具分析 2、垃圾回收分析 3、基准测试 四、结论", + "content": "# 前言\n\n这个问题是我之前翻看面经的时候见到的。那位小姐姐把内存泄漏当成了内存溢出问题去解答的,结果当场挂掉了。为此总结一下,之前和一位老哥也讨论过这个问题。可见不管是面试还是工作这都是一个极为重要的点。\n\n我也曾在面阿里的时候也遇到过原题,题目是写出俩内存泄漏案例,然后问如何排查?如何解决?\n\n本篇文章大体结构来自外国大佬baeldung;\n\n\n# 一、介绍\n\n\n# 1、什么是内存泄漏\n\njava的优势之一就是内置了垃圾回收器GC,它帮助我们实现了自动化内存管理。但是GC再好,也有老马失前蹄的时候,它不能保证提供一个解决内存泄漏的万无一失的解决方案。什么是内存泄漏?可以看看下面这张图,\n\n也就是一部分内存空间我明明已经使用了,却没有引用指向这部分空间。造成这片已经使用的空间无法处理的情况。\n\n正规点的理解:动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。\n\n\n# 2、内存泄漏的危害\n\n长时间运行,程序变卡,性能严重下降 程序莫名其妙挂掉 OutOfMemoryError错误 乱七八糟的错误,还不易排查 反正内存泄漏不是好事。\n\n\n# 二、内存泄漏原因\n\n内存泄漏原因太多了。说不定就是某一行代码不对就会出现这种情况,因此这里给出最常见的几种。关键的还是如何找出哪个地方出现了内存泄漏,代码好修改,错误不易查。\n\n\n# 1、大量使用静态变量\n\n静态变量的生命周期与程序一致。因此常驻内存。\n\npublic class Static Test{\n public static List list = new ArrayList<>();\n public void populateList(){\n for (int i = 0; i < 10000000; i++) {\n list.add((int)Math.random()); \n } \n System.out.println(\"running......\"); \n }\n public static void main(String[] args){ \n System.out.println(\"before......\");\n new StaticTest().populateList(); \n System.out.println(\"after......\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n\n\n现在可以使用jvisualvm运行一边,看看内存效果。\n\n * 带static关键字(使用静态变量)\n\n从上图可以看到,堆内存从一开始的135M左右飙升了到了200M。直接占据了65M的内存。\n\n * 不使用static关键字(不使用静态变量)\n\n由于全局变量与程序周期不一致,因此不使用时,就会进行回收。此时内存最高150M。\n\n总结:由于静态变量与程序生命周期一致,因此对象常驻内存,造成内存泄漏\n\n\n# 2、连接资源未关闭\n\n每当建立一个连接,jvm就会为这么资源分配内存。比如数据库连接、文件输入输出流、网络连接等等。\n\npublic class File Test{\n public static void main(String[] args)throws IOException {\n File f=new File(\"G:\\\\nginx配套资料\\\\笔记资料.zip\"); \n System.out.println(f.exists());\n System.out.println(f.isDirectory());\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n依然使用jvisualvm运行一边,看看内存效果。\n\n可以看出,在连接文件资源时,jvm会为本资源分配内存。\n\n3、equals()和hashCode()方法使用不当\n\n定义新类时,如果没有重新equals()和hashCode()方法,也有可能会造成内存泄漏。主要原因是没有这两个方法时,很容易造成重复的数据添加。看例子:\n\npublic class User{\n public String name;\n publicint age;\n public User(String name, int age){\n this.name = name;\n this.age = age;}\n }\n public class EqualTest{\n public static void main(String[] args){ \n Map map = new HashMap<>();\n for(int i=0; i<100; i++) { \n map.put(new User(\"\", 1), 1);\n } \n System.out.println(map.size() == 1);//输出为false \n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\n然后运行一下,看看内存情况:\n\n内存从150M一下子飙升到225M,可见飙升的厉害。输出为false,说明user对象被重复添加了。我们知道像HashMap在添加新的对象时,会对其hashcode进行比较,如果一样,那就不插入。如果一样那就插入。此时说明这100个User其hashcode不同。\n\n\n# 4、内部类持有外部类\n\n这个场景和上面类似。\n\n\n# 5、finalize方法\n\n这个方法之前曾经专门花过文章写过,这个问题很简单。看一张图\n\n这就是整个过程。不过在这里我们主要看的是finalize方法对垃圾回收的影响,其实就是在第三步,也就是这个对象含有finalize,进入了队列但一直没有被调用的这段时间,会一直占用内存。造成内存泄漏。\n\n\n# 6、ThreadLocal的错误使用\n\nThreadLocal主要用于创建本地线程变量,不合理的使用也有可能会造成内存泄漏。\n\n上面这张图详细的揭示了ThreadLocal和Thread以及ThreadLocalMap三者的关系。\n\n1、Thread中有一个map,就是ThreadLocalMap\n\n2、ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。\n\n3、ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收\n\n4、重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。\n\n解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。\n\n现在介绍了几种常见的内存泄漏情况,上面的知识点比较常见,最主要的是如何检测出来。\n\n\n# 三、检测内存泄漏\n\n检测的目的是定位内存泄漏出现的位置,常见的有以下几种方法:\n\n\n# 1、工具分析\n\n这个工具比较多,比如说JProfiler、YourKit、Java VisualVM和Netbeans Profiler。他可以帮助我们分析是哪一个对象或者是类内存的飙升。也可以看到内存CPU的等等各种情况。上面多次演示到了。\n\n\n# 2、垃圾回收分析\n\n这个其实也可以用工具进行分析。上面的VisualVM中,可以打印堆。也可以从外部导入dump文件进行分析。\n\n如果不用工具的话,我们可以通过IDE看到。JVM配置添加-verbose:gc。然后就会打印出相关信息。下面这张图非原创,来自Baeldung。\n\n\n# 3、基准测试\n\n也就是使用科学的方式进行分析java代码的性能。进而判断分析。\n\n\n# 四、结论\n\n内存泄漏是个很严重的问题,也比较常见。\n\n最主要的原因是动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。因此良好的代码规范,可以有效地避免这些错误。\n\n参考: 什么是内存泄漏?该如何检测?又该如何解决?", + "normalizedContent": "# 前言\n\n这个问题是我之前翻看面经的时候见到的。那位小姐姐把内存泄漏当成了内存溢出问题去解答的,结果当场挂掉了。为此总结一下,之前和一位老哥也讨论过这个问题。可见不管是面试还是工作这都是一个极为重要的点。\n\n我也曾在面阿里的时候也遇到过原题,题目是写出俩内存泄漏案例,然后问如何排查?如何解决?\n\n本篇文章大体结构来自外国大佬baeldung;\n\n\n# 一、介绍\n\n\n# 1、什么是内存泄漏\n\njava的优势之一就是内置了垃圾回收器gc,它帮助我们实现了自动化内存管理。但是gc再好,也有老马失前蹄的时候,它不能保证提供一个解决内存泄漏的万无一失的解决方案。什么是内存泄漏?可以看看下面这张图,\n\n也就是一部分内存空间我明明已经使用了,却没有引用指向这部分空间。造成这片已经使用的空间无法处理的情况。\n\n正规点的理解:动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。\n\n\n# 2、内存泄漏的危害\n\n长时间运行,程序变卡,性能严重下降 程序莫名其妙挂掉 outofmemoryerror错误 乱七八糟的错误,还不易排查 反正内存泄漏不是好事。\n\n\n# 二、内存泄漏原因\n\n内存泄漏原因太多了。说不定就是某一行代码不对就会出现这种情况,因此这里给出最常见的几种。关键的还是如何找出哪个地方出现了内存泄漏,代码好修改,错误不易查。\n\n\n# 1、大量使用静态变量\n\n静态变量的生命周期与程序一致。因此常驻内存。\n\npublic class static test{\n public static list list = new arraylist<>();\n public void populatelist(){\n for (int i = 0; i < 10000000; i++) {\n list.add((int)math.random()); \n } \n system.out.println(\"running......\"); \n }\n public static void main(string[] args){ \n system.out.println(\"before......\");\n new statictest().populatelist(); \n system.out.println(\"after......\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n\n\n现在可以使用jvisualvm运行一边,看看内存效果。\n\n * 带static关键字(使用静态变量)\n\n从上图可以看到,堆内存从一开始的135m左右飙升了到了200m。直接占据了65m的内存。\n\n * 不使用static关键字(不使用静态变量)\n\n由于全局变量与程序周期不一致,因此不使用时,就会进行回收。此时内存最高150m。\n\n总结:由于静态变量与程序生命周期一致,因此对象常驻内存,造成内存泄漏\n\n\n# 2、连接资源未关闭\n\n每当建立一个连接,jvm就会为这么资源分配内存。比如数据库连接、文件输入输出流、网络连接等等。\n\npublic class file test{\n public static void main(string[] args)throws ioexception {\n file f=new file(\"g:\\\\nginx配套资料\\\\笔记资料.zip\"); \n system.out.println(f.exists());\n system.out.println(f.isdirectory());\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n依然使用jvisualvm运行一边,看看内存效果。\n\n可以看出,在连接文件资源时,jvm会为本资源分配内存。\n\n3、equals()和hashcode()方法使用不当\n\n定义新类时,如果没有重新equals()和hashcode()方法,也有可能会造成内存泄漏。主要原因是没有这两个方法时,很容易造成重复的数据添加。看例子:\n\npublic class user{\n public string name;\n publicint age;\n public user(string name, int age){\n this.name = name;\n this.age = age;}\n }\n public class equaltest{\n public static void main(string[] args){ \n map map = new hashmap<>();\n for(int i=0; i<100; i++) { \n map.put(new user(\"\", 1), 1);\n } \n system.out.println(map.size() == 1);//输出为false \n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\n然后运行一下,看看内存情况:\n\n内存从150m一下子飙升到225m,可见飙升的厉害。输出为false,说明user对象被重复添加了。我们知道像hashmap在添加新的对象时,会对其hashcode进行比较,如果一样,那就不插入。如果一样那就插入。此时说明这100个user其hashcode不同。\n\n\n# 4、内部类持有外部类\n\n这个场景和上面类似。\n\n\n# 5、finalize方法\n\n这个方法之前曾经专门花过文章写过,这个问题很简单。看一张图\n\n这就是整个过程。不过在这里我们主要看的是finalize方法对垃圾回收的影响,其实就是在第三步,也就是这个对象含有finalize,进入了队列但一直没有被调用的这段时间,会一直占用内存。造成内存泄漏。\n\n\n# 6、threadlocal的错误使用\n\nthreadlocal主要用于创建本地线程变量,不合理的使用也有可能会造成内存泄漏。\n\n上面这张图详细的揭示了threadlocal和thread以及threadlocalmap三者的关系。\n\n1、thread中有一个map,就是threadlocalmap\n\n2、threadlocalmap的key是threadlocal,值是我们自己设定的。\n\n3、threadlocal是一个弱引用,当为null时,会被当成垃圾回收\n\n4、重点来了,突然我们threadlocal是null了,也就是要被垃圾回收器回收了,但是此时我们的threadlocalmap生命周期和thread的一样,它不会回收,这时候就出现了一个现象。那就是threadlocalmap的key没了,但是value还在,这就造成了内存泄漏。\n\n解决办法:使用完threadlocal后,执行remove操作,避免出现内存溢出情况。\n\n现在介绍了几种常见的内存泄漏情况,上面的知识点比较常见,最主要的是如何检测出来。\n\n\n# 三、检测内存泄漏\n\n检测的目的是定位内存泄漏出现的位置,常见的有以下几种方法:\n\n\n# 1、工具分析\n\n这个工具比较多,比如说jprofiler、yourkit、java visualvm和netbeans profiler。他可以帮助我们分析是哪一个对象或者是类内存的飙升。也可以看到内存cpu的等等各种情况。上面多次演示到了。\n\n\n# 2、垃圾回收分析\n\n这个其实也可以用工具进行分析。上面的visualvm中,可以打印堆。也可以从外部导入dump文件进行分析。\n\n如果不用工具的话,我们可以通过ide看到。jvm配置添加-verbose:gc。然后就会打印出相关信息。下面这张图非原创,来自baeldung。\n\n\n# 3、基准测试\n\n也就是使用科学的方式进行分析java代码的性能。进而判断分析。\n\n\n# 四、结论\n\n内存泄漏是个很严重的问题,也比较常见。\n\n最主要的原因是动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。因此良好的代码规范,可以有效地避免这些错误。\n\n参考: 什么是内存泄漏?该如何检测?又该如何解决?", + "charsets": { + "cjk": true + }, "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, { - "title": "实现单例设计模式(懒汉,饿汉)", + "title": "JVM 是怎么去调优的?了解哪些参数和指令?", "frontmatter": { - "title": "实现单例设计模式(懒汉,饿汉)", - "date": "2022-05-21T17:26:00.000Z", - "permalink": "/pages/b37ad7/", + "title": "JVM 是怎么去调优的?了解哪些参数和指令?", + "date": "2022-05-21T17:33:16.000Z", + "permalink": "/pages/d730dd/", "categories": [ "Java相关", - "设计模式" + "JVM" ], "tags": [ null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/002.%E5%AE%9E%E7%8E%B0%E5%8D%95%E4%BE%8B%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%EF%BC%88%E6%87%92%E6%B1%89%EF%BC%8C%E9%A5%BF%E6%B1%89%EF%BC%89.html", - "relativePath": "01.Java相关/40.设计模式/002.实现单例设计模式(懒汉,饿汉).md", - "key": "v-ba94c306", - "path": "/pages/b37ad7/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/050.JVM%20%E6%98%AF%E6%80%8E%E4%B9%88%E5%8E%BB%E8%B0%83%E4%BC%98%E7%9A%84%EF%BC%9F%E4%BA%86%E8%A7%A3%E5%93%AA%E4%BA%9B%E5%8F%82%E6%95%B0%E5%92%8C%E6%8C%87%E4%BB%A4%EF%BC%9F.html", + "relativePath": "01.Java相关/30.JVM/050.JVM 是怎么去调优的?了解哪些参数和指令?.md", + "key": "v-37fcb440", + "path": "/pages/d730dd/", "headers": [ { "level": 2, - "title": "饿汉式", - "slug": "饿汉式", - "normalizedTitle": "饿汉式", - "charIndex": 311 + "title": "查看进程", + "slug": "查看进程", + "normalizedTitle": "查看进程", + "charIndex": 2 + }, + { + "level": 2, + "title": "查看JVM参数 jinfo", + "slug": "查看jvm参数-jinfo", + "normalizedTitle": "查看jvm参数 jinfo", + "charIndex": 162 + }, + { + "level": 2, + "title": "查看各项指标数据 jstat", + "slug": "查看各项指标数据-jstat", + "normalizedTitle": "查看各项指标数据 jstat", + "charIndex": 251 + }, + { + "level": 2, + "title": "jmap", + "slug": "jmap", + "normalizedTitle": "jmap", + "charIndex": 588 } ], - "headersStr": "饿汉式", - "content": "> 面试官:带笔了吧,那写两种单例模式的实现方法吧 沙沙沙刷刷刷~~~ 写好了 面试官:怎样防止new 对象出来?\n\n有一些对象我们确实只需要一个,比如,线程池、数据库连接、缓存、日志对象等,如果有多个的话,会造成程序的行为异常,资源使用过量或者不一致的问题。你也许会说,这种我用全局变量不也能实现吗,还整个单例模式,好像你很流弊的样子,如果将对象赋值给一个全局变量,那程序启动就会创建好对象,万一这个对象很耗资源,我们还可能在某些时候用不到,这就造成了资源的浪费,不合理,所以就有了单例模式。\n\n#单例模式的定义 单例模式确保一个类只有一个实例,并提供一个全局唯一访问点\n\n#单例模式的类图\n\n#单例模式的实现\n\n\n# 饿汉式\n\nstatic 变量在类装载的时候进行初始化 多个实例的 static 变量会共享同一块内存区域 用这两个知识点写出的单例类就是饿汉式了,初始化类的时候就创建,饥不择食,饿汉\n\npublic class Singleton {\n\n //构造私有化,防止直接new\n private Singleton(){}\n\n //静态初始化器(static initializer)中创建实例,保证线程安全\n private static Singleton instance = new Singleton();\n\n public static Singleton getInstance(){\n return instance;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n饿汉式是线程安全的,JVM在加载类时马上创建唯一的实例对象,且只会装载一次。\n\nJava 实现的单例是一个虚拟机的范围,因为装载类的功能是虚拟机的,所以一个虚拟机通过自己的ClassLoader 装载饿汉式实现单例类的时候就会创建一个类实例。(如果一个虚拟机里有多个ClassLoader的话,就会有多个实例)\n\n##懒汉式 懒汉式,就是实例在用到的时候才去创建,比较“懒”\n\n单例模式的懒汉式实现方式体现了延迟加载的思想(延迟加载也称懒加载Lazy Load,就是一开始不要加载资源或数据,等到要使用的时候才加载)\n\n同步方法\n\npublic class Singleton {\n private static Singleton singleton;\n\n private Singleton(){}\n\n \t//解决了线程不安全问题,但是效率太低了,每个线程想获得类的实例的时候,都需要同步方法,不推荐\n public static synchronized Singleton getInstance(){\n if(singleton == null){\n singleton = new Singleton();\n }\n return singleton;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n双重检查加锁\n\npublic class Singleton {\n\n \t//volatitle关键词确保,多线程正确处理singleton\n private static volatile Singleton singleton;\n \n private Singleton(){}\n \n public static Singleton getInstance(){\n if(singleton ==null){\n synchronized (Singleton.class){\n if(singleton == null){\n singleton = new Singleton();\n }\n }\n }\n return singleton;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n\n\nDouble-Check 概念(进行两次检查)是多线程开发中经常使用的,为什么需要双重检查锁呢?因为第一次检查是确保之前是一个空对象,而非空对象就不需要同步了,空对象的线程然后进入同步代码块,如果不加第二次空对象检查,两个线程同时获取同步代码块,一个线程进入同步代码块,另一个线程就会等待,而这两个线程就会创建两个实例化对象,所以需要在线程进入同步代码块后再次进行空对象检查,才能确保只创建一个实例化对象。\n\n双重检查加锁(double checked locking)线程安全、延迟加载、效率比较高\n\nvolatile:volatile一般用于多线程的可见性,这里用来防止指令重排(防止new Singleton时指令重排序导致其他线程获取到未初始化完的对象)。被volatile 修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。\n\n指令重排\n\n指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序。\n\nJava中创建一个对象,往往包含三个过程。对于singleton = new Singleton(),这不是一个原子操作,在 JVM 中包含如下三个过程。\n\n给 singleton 分配内存\n\n调用 Singleton 的构造函数来初始化成员变量,形成实例\n\n将 singleton 对象指向分配的内存空间(执行完这步 singleton才是非 null 了)\n\n但是,由于JVM会进行指令重排序,所以上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3,也可能是 1-3-2。\n\n如果是 1-3-2,则在 3 执行完毕,2 未执行之前,被另一个线程抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以这个线程会直接返回 instance,然后使用,那肯定就会报错了,所以要加入 volatile关键字。\n\n##静态内部类\n\npublic class Singleton {\n\n private Singleton(){}\n\n private static class SingletonInstance{\n private static final Singleton INSTANCE = new Singleton();\n }\n \n public static Singleton getInstance(){\n return SingletonInstance.INSTANCE;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n采用类加载的机制来保证初始化实例时只有一个线程;\n\n静态内部类方式在Singleton 类被装载的时候并不会立即实例化,而是在调用getInstance的时候,才去装载内部类SingletonInstance ,从而完成Singleton的实例化\n\n类的静态属性只会在第一次加载类的时候初始化,所以,JVM帮我们保证了线程的安全性,在类初始化时,其他线程无法进入\n\n优点:线程安全,利用静态内部类实现延迟加载,效率较高,推荐使用\n\n##枚举\n\nenum Singleton{\n INSTANCE;\n public void method(){}\n}\n\n\n1\n2\n3\n4\n\n\n借助JDK5 添加的枚举实现单例,不仅可以避免多线程同步问题,还能防止反序列化重新创建新的对象,但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。不过,由于Java1.5中才加入enum特性,所以使用的人并不多。\n\n这种方式是《Effective Java》 作者Josh Bloch 提倡的方式。\n\n#单例模式在JDK 中的源码分析 JDK 中,java.lang.Runtime 就是经典的单例模式(饿汉式)\n\n#单例模式注意事项和细节 单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使 用new 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或 耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数 据库或文件的对象(比如数据源、session工厂等)", - "normalizedContent": "> 面试官:带笔了吧,那写两种单例模式的实现方法吧 沙沙沙刷刷刷~~~ 写好了 面试官:怎样防止new 对象出来?\n\n有一些对象我们确实只需要一个,比如,线程池、数据库连接、缓存、日志对象等,如果有多个的话,会造成程序的行为异常,资源使用过量或者不一致的问题。你也许会说,这种我用全局变量不也能实现吗,还整个单例模式,好像你很流弊的样子,如果将对象赋值给一个全局变量,那程序启动就会创建好对象,万一这个对象很耗资源,我们还可能在某些时候用不到,这就造成了资源的浪费,不合理,所以就有了单例模式。\n\n#单例模式的定义 单例模式确保一个类只有一个实例,并提供一个全局唯一访问点\n\n#单例模式的类图\n\n#单例模式的实现\n\n\n# 饿汉式\n\nstatic 变量在类装载的时候进行初始化 多个实例的 static 变量会共享同一块内存区域 用这两个知识点写出的单例类就是饿汉式了,初始化类的时候就创建,饥不择食,饿汉\n\npublic class singleton {\n\n //构造私有化,防止直接new\n private singleton(){}\n\n //静态初始化器(static initializer)中创建实例,保证线程安全\n private static singleton instance = new singleton();\n\n public static singleton getinstance(){\n return instance;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n饿汉式是线程安全的,jvm在加载类时马上创建唯一的实例对象,且只会装载一次。\n\njava 实现的单例是一个虚拟机的范围,因为装载类的功能是虚拟机的,所以一个虚拟机通过自己的classloader 装载饿汉式实现单例类的时候就会创建一个类实例。(如果一个虚拟机里有多个classloader的话,就会有多个实例)\n\n##懒汉式 懒汉式,就是实例在用到的时候才去创建,比较“懒”\n\n单例模式的懒汉式实现方式体现了延迟加载的思想(延迟加载也称懒加载lazy load,就是一开始不要加载资源或数据,等到要使用的时候才加载)\n\n同步方法\n\npublic class singleton {\n private static singleton singleton;\n\n private singleton(){}\n\n \t//解决了线程不安全问题,但是效率太低了,每个线程想获得类的实例的时候,都需要同步方法,不推荐\n public static synchronized singleton getinstance(){\n if(singleton == null){\n singleton = new singleton();\n }\n return singleton;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n双重检查加锁\n\npublic class singleton {\n\n \t//volatitle关键词确保,多线程正确处理singleton\n private static volatile singleton singleton;\n \n private singleton(){}\n \n public static singleton getinstance(){\n if(singleton ==null){\n synchronized (singleton.class){\n if(singleton == null){\n singleton = new singleton();\n }\n }\n }\n return singleton;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n\n\ndouble-check 概念(进行两次检查)是多线程开发中经常使用的,为什么需要双重检查锁呢?因为第一次检查是确保之前是一个空对象,而非空对象就不需要同步了,空对象的线程然后进入同步代码块,如果不加第二次空对象检查,两个线程同时获取同步代码块,一个线程进入同步代码块,另一个线程就会等待,而这两个线程就会创建两个实例化对象,所以需要在线程进入同步代码块后再次进行空对象检查,才能确保只创建一个实例化对象。\n\n双重检查加锁(double checked locking)线程安全、延迟加载、效率比较高\n\nvolatile:volatile一般用于多线程的可见性,这里用来防止指令重排(防止new singleton时指令重排序导致其他线程获取到未初始化完的对象)。被volatile 修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。\n\n指令重排\n\n指令重排是指在程序执行过程中, 为了性能考虑, 编译器和cpu可能会对指令重新排序。\n\njava中创建一个对象,往往包含三个过程。对于singleton = new singleton(),这不是一个原子操作,在 jvm 中包含如下三个过程。\n\n给 singleton 分配内存\n\n调用 singleton 的构造函数来初始化成员变量,形成实例\n\n将 singleton 对象指向分配的内存空间(执行完这步 singleton才是非 null 了)\n\n但是,由于jvm会进行指令重排序,所以上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3,也可能是 1-3-2。\n\n如果是 1-3-2,则在 3 执行完毕,2 未执行之前,被另一个线程抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以这个线程会直接返回 instance,然后使用,那肯定就会报错了,所以要加入 volatile关键字。\n\n##静态内部类\n\npublic class singleton {\n\n private singleton(){}\n\n private static class singletoninstance{\n private static final singleton instance = new singleton();\n }\n \n public static singleton getinstance(){\n return singletoninstance.instance;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n采用类加载的机制来保证初始化实例时只有一个线程;\n\n静态内部类方式在singleton 类被装载的时候并不会立即实例化,而是在调用getinstance的时候,才去装载内部类singletoninstance ,从而完成singleton的实例化\n\n类的静态属性只会在第一次加载类的时候初始化,所以,jvm帮我们保证了线程的安全性,在类初始化时,其他线程无法进入\n\n优点:线程安全,利用静态内部类实现延迟加载,效率较高,推荐使用\n\n##枚举\n\nenum singleton{\n instance;\n public void method(){}\n}\n\n\n1\n2\n3\n4\n\n\n借助jdk5 添加的枚举实现单例,不仅可以避免多线程同步问题,还能防止反序列化重新创建新的对象,但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。不过,由于java1.5中才加入enum特性,所以使用的人并不多。\n\n这种方式是《effective java》 作者josh bloch 提倡的方式。\n\n#单例模式在jdk 中的源码分析 jdk 中,java.lang.runtime 就是经典的单例模式(饿汉式)\n\n#单例模式注意事项和细节 单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使 用new 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或 耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数 据库或文件的对象(比如数据源、session工厂等)", + "headersStr": "查看进程 查看JVM参数 jinfo 查看各项指标数据 jstat jmap", + "content": "# 查看进程\n\n由于JVM大多数命令都是需要用到进程id的,所以我们需要先查看一下我们系统的进程id,通过使用jps -l,输出所有java相关进程。\n\n这里我们可以根据类名,大致确认出我们的系统进程是什么\n\n注:-l代表的是输出应用程序main class的完整package名或者应用程序的jar文件完整路径名\n\n\n# 查看JVM参数 jinfo\n\n通过运行jinfo -flags <端口号>查看jvm的参数。\n\n通过运行jinfo -sysprops <端口号>查看java系统参数。\n\n\n# 查看各项指标数据 jstat\n\n通过运行 -class <端口号>完成类加载统计 通过运行jstat -gc <端口号>完成垃圾回收统计 通过运行jstat -gccapacity <端口号>完成堆内存统计 通过运行jstat -gcnew <端口号>完成新生代垃圾回收统计 通过运行jstat -gcnewcapacity <端口号>完成新生代内存统计 通过运行jstat -gcold <端口号>完成老年代垃圾回收统计 通过运行jstat -gcoldcapacity <端口号>完成老年代内存统计 通过运行jstat -gcmetacapacity <端口号>完成老年代内存统计 通过运行jstat -gcutil <端口号>数据总览 查看对象及内存使用情况 通过运行jmap -histo <端口号> > ./log.txt类及其对象所占内存数据。\n\n\n# jmap\n\n通过运行jmap -heap <端口号>堆信息。\n\n参考: 深度好文:JVM调优 Java核心篇-故事里的JVM性能调优", + "normalizedContent": "# 查看进程\n\n由于jvm大多数命令都是需要用到进程id的,所以我们需要先查看一下我们系统的进程id,通过使用jps -l,输出所有java相关进程。\n\n这里我们可以根据类名,大致确认出我们的系统进程是什么\n\n注:-l代表的是输出应用程序main class的完整package名或者应用程序的jar文件完整路径名\n\n\n# 查看jvm参数 jinfo\n\n通过运行jinfo -flags <端口号>查看jvm的参数。\n\n通过运行jinfo -sysprops <端口号>查看java系统参数。\n\n\n# 查看各项指标数据 jstat\n\n通过运行 -class <端口号>完成类加载统计 通过运行jstat -gc <端口号>完成垃圾回收统计 通过运行jstat -gccapacity <端口号>完成堆内存统计 通过运行jstat -gcnew <端口号>完成新生代垃圾回收统计 通过运行jstat -gcnewcapacity <端口号>完成新生代内存统计 通过运行jstat -gcold <端口号>完成老年代垃圾回收统计 通过运行jstat -gcoldcapacity <端口号>完成老年代内存统计 通过运行jstat -gcmetacapacity <端口号>完成老年代内存统计 通过运行jstat -gcutil <端口号>数据总览 查看对象及内存使用情况 通过运行jmap -histo <端口号> > ./log.txt类及其对象所占内存数据。\n\n\n# jmap\n\n通过运行jmap -heap <端口号>堆信息。\n\n参考: 深度好文:jvm调优 java核心篇-故事里的jvm性能调优", "charsets": { "cjk": true }, - "lastUpdated": "2022/06/20, 22:47:41", - "lastUpdatedTimestamp": 1655736461000 + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 }, { "title": "简述 CMS 与 G1 机制的区别", @@ -2152,49 +2536,166 @@ export const siteData = { "relativePath": "01.Java相关/30.JVM/070.简述 CMS 与 G1 机制的区别.md", "key": "v-28c1d6d9", "path": "/pages/be69af/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "什么是设计模式,描述几个常用的设计模式", - "frontmatter": { - "title": "什么是设计模式,描述几个常用的设计模式", - "date": "2022-05-21T17:26:19.000Z", - "permalink": "/pages/3d768c/", - "categories": [ - "Java相关", - "设计模式" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/003.%E4%BB%80%E4%B9%88%E6%98%AF%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%EF%BC%8C%E6%8F%8F%E8%BF%B0%E5%87%A0%E4%B8%AA%E5%B8%B8%E7%94%A8%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20.html", - "relativePath": "01.Java相关/40.设计模式/003.什么是设计模式,描述几个常用的设计模式 .md", - "key": "v-504a5213", - "path": "/pages/3d768c/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "工厂模式使用场景 能举个例子吗", - "frontmatter": { - "title": "工厂模式使用场景 能举个例子吗", - "date": "2022-05-21T17:26:32.000Z", - "permalink": "/pages/f15489/", - "categories": [ - "Java相关", - "设计模式" - ], - "tags": [ + "headers": [ + { + "level": 2, + "title": "G1特有数据结构和算法", + "slug": "g1特有数据结构和算法", + "normalizedTitle": "g1特有数据结构和算法", + "charIndex": 2 + }, + { + "level": 3, + "title": "Region", + "slug": "region", + "normalizedTitle": "region", + "charIndex": 18 + }, + { + "level": 3, + "title": "SATB算法", + "slug": "satb算法", + "normalizedTitle": "satb算法", + "charIndex": 179 + }, + { + "level": 3, + "title": "三色标记法", + "slug": "三色标记法", + "normalizedTitle": "三色标记法", + "charIndex": 248 + }, + { + "level": 3, + "title": "RSet", + "slug": "rset", + "normalizedTitle": "rset", + "charIndex": 670 + }, + { + "level": 3, + "title": "垃圾回收过程", + "slug": "垃圾回收过程", + "normalizedTitle": "垃圾回收过程", + "charIndex": 1013 + }, + { + "level": 3, + "title": "与CMS相比的优势", + "slug": "与cms相比的优势", + "normalizedTitle": "与cms相比的优势", + "charIndex": 1517 + }, + { + "level": 2, + "title": "CMS", + "slug": "cms", + "normalizedTitle": "cms", + "charIndex": 255 + }, + { + "level": 3, + "title": "什么是CMS?", + "slug": "什么是cms", + "normalizedTitle": "什么是cms?", + "charIndex": 1699 + }, + { + "level": 3, + "title": "CMS有什么用?", + "slug": "cms有什么用", + "normalizedTitle": "cms有什么用?", + "charIndex": 1785 + }, + { + "level": 3, + "title": "CMS如何执行?", + "slug": "cms如何执行", + "normalizedTitle": "cms如何执行?", + "charIndex": 1865 + } + ], + "headersStr": "G1特有数据结构和算法 Region SATB算法 三色标记法 RSet 垃圾回收过程 与CMS相比的优势 CMS 什么是CMS? CMS有什么用? CMS如何执行?", + "content": "# G1特有数据结构和算法\n\n\n# Region\n\n堆仍然有新生代(eden、survivor)、老年代的划分,但是不再要求它们是内存连续的。每个区都由多个Region组成。 部分老年代Region存储Humongous对象(即下图的H),这种对象大小大于等于Region的一半。\n\n(图片来源-Java Hotspot G1 GC的一些关键技术)\n\n\n# SATB算法\n\n全称Snapshot-At-The-Beginning,起始时活对象的快照。在理解SATB前需要先了解以下知识。\n\n\n# 三色标记法\n\nCMS和G1的算法都是通过对gc root 进行遍历,并进行三色标记。标记规则为\n\n黑色(black): 节点被遍历完成,而且子节点都遍历完成。 灰色(gray): 当前正在遍历的节点,而且子节点(即对象的域)还没有遍历。遍历完所有子节点后,将成为黑色 白色(white): 还没有遍历到的节点,即灰色节点的子节点。扫描结束仍是白色时会被回收。 并发扫描时,对于白色有两种情况同时发生时,可能会漏标导致被误回收:\n\n增加了被黑色引用的关系。 被灰色下应用,删除了到它的引用 具体执行过程:https://www.cnblogs.com/javaadu/p/10713956.html\n\n按照R大的说法:CMS的incremental update设计使得它在remark阶段必须重新扫描所有线程栈和整个young gen作为root;G1的SATB设计在remark阶段则只需要扫描剩下的satb_mark_queue。\n\n\n# RSet\n\n全称Remember Set,记录一个Region里的对象被哪些其他Region引用。 相对应地,有另一种辅助数据结构Collection Set(CSet),它记录了GC要收集的Region集合。GC时只需扫描CSet中各个Rset即可。\n\n(Tips for Tuning the Garbage First Garbage Collector)\n\n更详细的访问机制和回收过程这里不再展开,有兴趣可以参考后文引用文献。\n\nPause Prediction Model 暂停预测模型,G1根据它计算出的历史数据来预测本次收集需要选择的Region数量,从而尽量满足用户设定的目标停顿时间。 具体算法和公式略,可见Java Hotspot G1 GC的一些关键技术\n\n\n# 垃圾回收过程\n\n分为以下几步:\n\n初始标记(Initial Mark)—— 标记GC root能直接关联的对象(短暂STW) 并发标记(Concurrent mark)—— GCRootsTracing,从并发标记中的root遍历,对不可达的对象进行标记,耗时长但可并行 最终标记(Final Remark)—— 收集并发标记期间产生的新垃圾(短暂STW),采用了SATB算法比CMS更快 筛选回收(Live Data Counting and Evacuation)—— 对各个Region的回收性价比排序,在保证时间可控的情况下清除失活对象,清除Remember Sets 作为对比,CMS的回收过程\n\n初始标记(CMS Initial Mark)—— 标记GC root能直接关联的对象(短暂STW) 并发标记(CMS Concurrent Mark)—— GCRootsTracing,从并发标记中的root遍历,对不可达的对象进行标记 重新标记(CMS Remark)—— 修正并发标记期间因为用户操作导致标记发生变更的对象,有STW 并发清除(CMS Concurrent Sweep)\n\n\n# 与CMS相比的优势\n\n并发度更高,充分利用CPU多线程 —— CMS对CPU资源敏感,需要占用25%的线程,如果核数小于4更会占用一半的资源。 整体上是标记-整理(分代),局部是复制(分Region),运行期不产生碎片 —— CMS是标记-清除,会产生空间碎片和本次回收期间产生导致本次无法回收的浮动垃圾 可预测的停顿(基于Region)\n\n\n# CMS\n\n\n# 什么是CMS?\n\nConcurrent Mark Sweep。\n\n看名字就知道,CMS是一款并发、使用标记-清除算法的gc。\n\nCMS是针对老年代进行回收的GC。\n\n\n# CMS有什么用?\n\nCMS以获取最小停顿时间为目的。\n\n在一些对响应时间有很高要求的应用或网站中,用户程序不能有长时间的停顿,CMS 可以用于此场景。\n\n\n# CMS如何执行?\n\n总体来说CMS的执行过程可以分为以下几个阶段:\n\n 3.1 初始标记(STW)\n\n 3.2 并发标记\n\n 3.3 并发预清理\n\n 3.4 重标记(STW)\n\n 3.5 并发清理\n\n 3.6 重置\n\n\n参考: 关于GC(下):CMS和G1GC的比较 详解CMS垃圾回收机制", + "normalizedContent": "# g1特有数据结构和算法\n\n\n# region\n\n堆仍然有新生代(eden、survivor)、老年代的划分,但是不再要求它们是内存连续的。每个区都由多个region组成。 部分老年代region存储humongous对象(即下图的h),这种对象大小大于等于region的一半。\n\n(图片来源-java hotspot g1 gc的一些关键技术)\n\n\n# satb算法\n\n全称snapshot-at-the-beginning,起始时活对象的快照。在理解satb前需要先了解以下知识。\n\n\n# 三色标记法\n\ncms和g1的算法都是通过对gc root 进行遍历,并进行三色标记。标记规则为\n\n黑色(black): 节点被遍历完成,而且子节点都遍历完成。 灰色(gray): 当前正在遍历的节点,而且子节点(即对象的域)还没有遍历。遍历完所有子节点后,将成为黑色 白色(white): 还没有遍历到的节点,即灰色节点的子节点。扫描结束仍是白色时会被回收。 并发扫描时,对于白色有两种情况同时发生时,可能会漏标导致被误回收:\n\n增加了被黑色引用的关系。 被灰色下应用,删除了到它的引用 具体执行过程:https://www.cnblogs.com/javaadu/p/10713956.html\n\n按照r大的说法:cms的incremental update设计使得它在remark阶段必须重新扫描所有线程栈和整个young gen作为root;g1的satb设计在remark阶段则只需要扫描剩下的satb_mark_queue。\n\n\n# rset\n\n全称remember set,记录一个region里的对象被哪些其他region引用。 相对应地,有另一种辅助数据结构collection set(cset),它记录了gc要收集的region集合。gc时只需扫描cset中各个rset即可。\n\n(tips for tuning the garbage first garbage collector)\n\n更详细的访问机制和回收过程这里不再展开,有兴趣可以参考后文引用文献。\n\npause prediction model 暂停预测模型,g1根据它计算出的历史数据来预测本次收集需要选择的region数量,从而尽量满足用户设定的目标停顿时间。 具体算法和公式略,可见java hotspot g1 gc的一些关键技术\n\n\n# 垃圾回收过程\n\n分为以下几步:\n\n初始标记(initial mark)—— 标记gc root能直接关联的对象(短暂stw) 并发标记(concurrent mark)—— gcrootstracing,从并发标记中的root遍历,对不可达的对象进行标记,耗时长但可并行 最终标记(final remark)—— 收集并发标记期间产生的新垃圾(短暂stw),采用了satb算法比cms更快 筛选回收(live data counting and evacuation)—— 对各个region的回收性价比排序,在保证时间可控的情况下清除失活对象,清除remember sets 作为对比,cms的回收过程\n\n初始标记(cms initial mark)—— 标记gc root能直接关联的对象(短暂stw) 并发标记(cms concurrent mark)—— gcrootstracing,从并发标记中的root遍历,对不可达的对象进行标记 重新标记(cms remark)—— 修正并发标记期间因为用户操作导致标记发生变更的对象,有stw 并发清除(cms concurrent sweep)\n\n\n# 与cms相比的优势\n\n并发度更高,充分利用cpu多线程 —— cms对cpu资源敏感,需要占用25%的线程,如果核数小于4更会占用一半的资源。 整体上是标记-整理(分代),局部是复制(分region),运行期不产生碎片 —— cms是标记-清除,会产生空间碎片和本次回收期间产生导致本次无法回收的浮动垃圾 可预测的停顿(基于region)\n\n\n# cms\n\n\n# 什么是cms?\n\nconcurrent mark sweep。\n\n看名字就知道,cms是一款并发、使用标记-清除算法的gc。\n\ncms是针对老年代进行回收的gc。\n\n\n# cms有什么用?\n\ncms以获取最小停顿时间为目的。\n\n在一些对响应时间有很高要求的应用或网站中,用户程序不能有长时间的停顿,cms 可以用于此场景。\n\n\n# cms如何执行?\n\n总体来说cms的执行过程可以分为以下几个阶段:\n\n 3.1 初始标记(stw)\n\n 3.2 并发标记\n\n 3.3 并发预清理\n\n 3.4 重标记(stw)\n\n 3.5 并发清理\n\n 3.6 重置\n\n\n参考: 关于gc(下):cms和g1gc的比较 详解cms垃圾回收机制", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "实现单例设计模式(懒汉,饿汉)", + "frontmatter": { + "title": "实现单例设计模式(懒汉,饿汉)", + "date": "2022-05-21T17:26:00.000Z", + "permalink": "/pages/b37ad7/", + "categories": [ + "Java相关", + "设计模式" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/002.%E5%AE%9E%E7%8E%B0%E5%8D%95%E4%BE%8B%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%EF%BC%88%E6%87%92%E6%B1%89%EF%BC%8C%E9%A5%BF%E6%B1%89%EF%BC%89.html", + "relativePath": "01.Java相关/40.设计模式/002.实现单例设计模式(懒汉,饿汉).md", + "key": "v-ba94c306", + "path": "/pages/b37ad7/", + "headers": [ + { + "level": 2, + "title": "饿汉式", + "slug": "饿汉式", + "normalizedTitle": "饿汉式", + "charIndex": 311 + } + ], + "headersStr": "饿汉式", + "content": "> 面试官:带笔了吧,那写两种单例模式的实现方法吧 沙沙沙刷刷刷~~~ 写好了 面试官:怎样防止new 对象出来?\n\n有一些对象我们确实只需要一个,比如,线程池、数据库连接、缓存、日志对象等,如果有多个的话,会造成程序的行为异常,资源使用过量或者不一致的问题。你也许会说,这种我用全局变量不也能实现吗,还整个单例模式,好像你很流弊的样子,如果将对象赋值给一个全局变量,那程序启动就会创建好对象,万一这个对象很耗资源,我们还可能在某些时候用不到,这就造成了资源的浪费,不合理,所以就有了单例模式。\n\n#单例模式的定义 单例模式确保一个类只有一个实例,并提供一个全局唯一访问点\n\n#单例模式的类图\n\n#单例模式的实现\n\n\n# 饿汉式\n\nstatic 变量在类装载的时候进行初始化 多个实例的 static 变量会共享同一块内存区域 用这两个知识点写出的单例类就是饿汉式了,初始化类的时候就创建,饥不择食,饿汉\n\npublic class Singleton {\n\n //构造私有化,防止直接new\n private Singleton(){}\n\n //静态初始化器(static initializer)中创建实例,保证线程安全\n private static Singleton instance = new Singleton();\n\n public static Singleton getInstance(){\n return instance;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n饿汉式是线程安全的,JVM在加载类时马上创建唯一的实例对象,且只会装载一次。\n\nJava 实现的单例是一个虚拟机的范围,因为装载类的功能是虚拟机的,所以一个虚拟机通过自己的ClassLoader 装载饿汉式实现单例类的时候就会创建一个类实例。(如果一个虚拟机里有多个ClassLoader的话,就会有多个实例)\n\n##懒汉式 懒汉式,就是实例在用到的时候才去创建,比较“懒”\n\n单例模式的懒汉式实现方式体现了延迟加载的思想(延迟加载也称懒加载Lazy Load,就是一开始不要加载资源或数据,等到要使用的时候才加载)\n\n同步方法\n\npublic class Singleton {\n private static Singleton singleton;\n\n private Singleton(){}\n\n \t//解决了线程不安全问题,但是效率太低了,每个线程想获得类的实例的时候,都需要同步方法,不推荐\n public static synchronized Singleton getInstance(){\n if(singleton == null){\n singleton = new Singleton();\n }\n return singleton;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n双重检查加锁\n\npublic class Singleton {\n\n \t//volatitle关键词确保,多线程正确处理singleton\n private static volatile Singleton singleton;\n \n private Singleton(){}\n \n public static Singleton getInstance(){\n if(singleton ==null){\n synchronized (Singleton.class){\n if(singleton == null){\n singleton = new Singleton();\n }\n }\n }\n return singleton;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n\n\nDouble-Check 概念(进行两次检查)是多线程开发中经常使用的,为什么需要双重检查锁呢?因为第一次检查是确保之前是一个空对象,而非空对象就不需要同步了,空对象的线程然后进入同步代码块,如果不加第二次空对象检查,两个线程同时获取同步代码块,一个线程进入同步代码块,另一个线程就会等待,而这两个线程就会创建两个实例化对象,所以需要在线程进入同步代码块后再次进行空对象检查,才能确保只创建一个实例化对象。\n\n双重检查加锁(double checked locking)线程安全、延迟加载、效率比较高\n\nvolatile:volatile一般用于多线程的可见性,这里用来防止指令重排(防止new Singleton时指令重排序导致其他线程获取到未初始化完的对象)。被volatile 修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。\n\n指令重排\n\n指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序。\n\nJava中创建一个对象,往往包含三个过程。对于singleton = new Singleton(),这不是一个原子操作,在 JVM 中包含如下三个过程。\n\n给 singleton 分配内存\n\n调用 Singleton 的构造函数来初始化成员变量,形成实例\n\n将 singleton 对象指向分配的内存空间(执行完这步 singleton才是非 null 了)\n\n但是,由于JVM会进行指令重排序,所以上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3,也可能是 1-3-2。\n\n如果是 1-3-2,则在 3 执行完毕,2 未执行之前,被另一个线程抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以这个线程会直接返回 instance,然后使用,那肯定就会报错了,所以要加入 volatile关键字。\n\n##静态内部类\n\npublic class Singleton {\n\n private Singleton(){}\n\n private static class SingletonInstance{\n private static final Singleton INSTANCE = new Singleton();\n }\n \n public static Singleton getInstance(){\n return SingletonInstance.INSTANCE;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n采用类加载的机制来保证初始化实例时只有一个线程;\n\n静态内部类方式在Singleton 类被装载的时候并不会立即实例化,而是在调用getInstance的时候,才去装载内部类SingletonInstance ,从而完成Singleton的实例化\n\n类的静态属性只会在第一次加载类的时候初始化,所以,JVM帮我们保证了线程的安全性,在类初始化时,其他线程无法进入\n\n优点:线程安全,利用静态内部类实现延迟加载,效率较高,推荐使用\n\n##枚举\n\nenum Singleton{\n INSTANCE;\n public void method(){}\n}\n\n\n1\n2\n3\n4\n\n\n借助JDK5 添加的枚举实现单例,不仅可以避免多线程同步问题,还能防止反序列化重新创建新的对象,但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。不过,由于Java1.5中才加入enum特性,所以使用的人并不多。\n\n这种方式是《Effective Java》 作者Josh Bloch 提倡的方式。\n\n#单例模式在JDK 中的源码分析 JDK 中,java.lang.Runtime 就是经典的单例模式(饿汉式)\n\n#单例模式注意事项和细节 单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使 用new 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或 耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数 据库或文件的对象(比如数据源、session工厂等)", + "normalizedContent": "> 面试官:带笔了吧,那写两种单例模式的实现方法吧 沙沙沙刷刷刷~~~ 写好了 面试官:怎样防止new 对象出来?\n\n有一些对象我们确实只需要一个,比如,线程池、数据库连接、缓存、日志对象等,如果有多个的话,会造成程序的行为异常,资源使用过量或者不一致的问题。你也许会说,这种我用全局变量不也能实现吗,还整个单例模式,好像你很流弊的样子,如果将对象赋值给一个全局变量,那程序启动就会创建好对象,万一这个对象很耗资源,我们还可能在某些时候用不到,这就造成了资源的浪费,不合理,所以就有了单例模式。\n\n#单例模式的定义 单例模式确保一个类只有一个实例,并提供一个全局唯一访问点\n\n#单例模式的类图\n\n#单例模式的实现\n\n\n# 饿汉式\n\nstatic 变量在类装载的时候进行初始化 多个实例的 static 变量会共享同一块内存区域 用这两个知识点写出的单例类就是饿汉式了,初始化类的时候就创建,饥不择食,饿汉\n\npublic class singleton {\n\n //构造私有化,防止直接new\n private singleton(){}\n\n //静态初始化器(static initializer)中创建实例,保证线程安全\n private static singleton instance = new singleton();\n\n public static singleton getinstance(){\n return instance;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n饿汉式是线程安全的,jvm在加载类时马上创建唯一的实例对象,且只会装载一次。\n\njava 实现的单例是一个虚拟机的范围,因为装载类的功能是虚拟机的,所以一个虚拟机通过自己的classloader 装载饿汉式实现单例类的时候就会创建一个类实例。(如果一个虚拟机里有多个classloader的话,就会有多个实例)\n\n##懒汉式 懒汉式,就是实例在用到的时候才去创建,比较“懒”\n\n单例模式的懒汉式实现方式体现了延迟加载的思想(延迟加载也称懒加载lazy load,就是一开始不要加载资源或数据,等到要使用的时候才加载)\n\n同步方法\n\npublic class singleton {\n private static singleton singleton;\n\n private singleton(){}\n\n \t//解决了线程不安全问题,但是效率太低了,每个线程想获得类的实例的时候,都需要同步方法,不推荐\n public static synchronized singleton getinstance(){\n if(singleton == null){\n singleton = new singleton();\n }\n return singleton;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n双重检查加锁\n\npublic class singleton {\n\n \t//volatitle关键词确保,多线程正确处理singleton\n private static volatile singleton singleton;\n \n private singleton(){}\n \n public static singleton getinstance(){\n if(singleton ==null){\n synchronized (singleton.class){\n if(singleton == null){\n singleton = new singleton();\n }\n }\n }\n return singleton;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n\n\ndouble-check 概念(进行两次检查)是多线程开发中经常使用的,为什么需要双重检查锁呢?因为第一次检查是确保之前是一个空对象,而非空对象就不需要同步了,空对象的线程然后进入同步代码块,如果不加第二次空对象检查,两个线程同时获取同步代码块,一个线程进入同步代码块,另一个线程就会等待,而这两个线程就会创建两个实例化对象,所以需要在线程进入同步代码块后再次进行空对象检查,才能确保只创建一个实例化对象。\n\n双重检查加锁(double checked locking)线程安全、延迟加载、效率比较高\n\nvolatile:volatile一般用于多线程的可见性,这里用来防止指令重排(防止new singleton时指令重排序导致其他线程获取到未初始化完的对象)。被volatile 修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。\n\n指令重排\n\n指令重排是指在程序执行过程中, 为了性能考虑, 编译器和cpu可能会对指令重新排序。\n\njava中创建一个对象,往往包含三个过程。对于singleton = new singleton(),这不是一个原子操作,在 jvm 中包含如下三个过程。\n\n给 singleton 分配内存\n\n调用 singleton 的构造函数来初始化成员变量,形成实例\n\n将 singleton 对象指向分配的内存空间(执行完这步 singleton才是非 null 了)\n\n但是,由于jvm会进行指令重排序,所以上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3,也可能是 1-3-2。\n\n如果是 1-3-2,则在 3 执行完毕,2 未执行之前,被另一个线程抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以这个线程会直接返回 instance,然后使用,那肯定就会报错了,所以要加入 volatile关键字。\n\n##静态内部类\n\npublic class singleton {\n\n private singleton(){}\n\n private static class singletoninstance{\n private static final singleton instance = new singleton();\n }\n \n public static singleton getinstance(){\n return singletoninstance.instance;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n采用类加载的机制来保证初始化实例时只有一个线程;\n\n静态内部类方式在singleton 类被装载的时候并不会立即实例化,而是在调用getinstance的时候,才去装载内部类singletoninstance ,从而完成singleton的实例化\n\n类的静态属性只会在第一次加载类的时候初始化,所以,jvm帮我们保证了线程的安全性,在类初始化时,其他线程无法进入\n\n优点:线程安全,利用静态内部类实现延迟加载,效率较高,推荐使用\n\n##枚举\n\nenum singleton{\n instance;\n public void method(){}\n}\n\n\n1\n2\n3\n4\n\n\n借助jdk5 添加的枚举实现单例,不仅可以避免多线程同步问题,还能防止反序列化重新创建新的对象,但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。不过,由于java1.5中才加入enum特性,所以使用的人并不多。\n\n这种方式是《effective java》 作者josh bloch 提倡的方式。\n\n#单例模式在jdk 中的源码分析 jdk 中,java.lang.runtime 就是经典的单例模式(饿汉式)\n\n#单例模式注意事项和细节 单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使 用new 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或 耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数 据库或文件的对象(比如数据源、session工厂等)", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/06/20, 22:47:41", + "lastUpdatedTimestamp": 1655736461000 + }, + { + "title": "什么是设计模式,描述几个常用的设计模式", + "frontmatter": { + "title": "什么是设计模式,描述几个常用的设计模式", + "date": "2022-05-21T17:26:19.000Z", + "permalink": "/pages/3d768c/", + "categories": [ + "Java相关", + "设计模式" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/003.%E4%BB%80%E4%B9%88%E6%98%AF%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%EF%BC%8C%E6%8F%8F%E8%BF%B0%E5%87%A0%E4%B8%AA%E5%B8%B8%E7%94%A8%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20.html", + "relativePath": "01.Java相关/40.设计模式/003.什么是设计模式,描述几个常用的设计模式 .md", + "key": "v-504a5213", + "path": "/pages/3d768c/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "工厂模式使用场景 能举个例子吗", + "frontmatter": { + "title": "工厂模式使用场景 能举个例子吗", + "date": "2022-05-21T17:26:32.000Z", + "permalink": "/pages/f15489/", + "categories": [ + "Java相关", + "设计模式" + ], + "tags": [ null ] }, @@ -2377,36 +2878,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "模板方法模式", - "frontmatter": { - "title": "模板方法模式", - "date": "2022-05-21T17:29:10.000Z", - "permalink": "/pages/43b656/", - "categories": [ - "Java相关", - "设计模式" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/040.%E6%A8%A1%E6%9D%BF%E6%96%B9%E6%B3%95%E6%A8%A1%E5%BC%8F.html", - "relativePath": "01.Java相关/40.设计模式/040.模板方法模式.md", - "key": "v-28deb9da", - "path": "/pages/43b656/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "策略模式", + "title": "观察模式", "frontmatter": { - "title": "策略模式", - "date": "2022-05-21T17:29:26.000Z", - "permalink": "/pages/78c7aa/", + "title": "观察模式", + "date": "2022-05-21T17:29:45.000Z", + "permalink": "/pages/cbff3c/", "categories": [ "Java相关", "设计模式" @@ -2415,10 +2891,10 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/050.%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F.html", - "relativePath": "01.Java相关/40.设计模式/050.策略模式.md", - "key": "v-5b0157cf", - "path": "/pages/78c7aa/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/060.%E8%A7%82%E5%AF%9F%E6%A8%A1%E5%BC%8F.html", + "relativePath": "01.Java相关/40.设计模式/060.观察模式.md", + "key": "v-64f1031c", + "path": "/pages/cbff3c/", "headersStr": null, "content": "", "normalizedContent": "", @@ -2427,11 +2903,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "观察模式", + "title": "模板方法模式", "frontmatter": { - "title": "观察模式", - "date": "2022-05-21T17:29:45.000Z", - "permalink": "/pages/cbff3c/", + "title": "模板方法模式", + "date": "2022-05-21T17:29:10.000Z", + "permalink": "/pages/43b656/", "categories": [ "Java相关", "设计模式" @@ -2440,10 +2916,10 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/060.%E8%A7%82%E5%AF%9F%E6%A8%A1%E5%BC%8F.html", - "relativePath": "01.Java相关/40.设计模式/060.观察模式.md", - "key": "v-64f1031c", - "path": "/pages/cbff3c/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/040.%E6%A8%A1%E6%9D%BF%E6%96%B9%E6%B3%95%E6%A8%A1%E5%BC%8F.html", + "relativePath": "01.Java相关/40.设计模式/040.模板方法模式.md", + "key": "v-28deb9da", + "path": "/pages/43b656/", "headersStr": null, "content": "", "normalizedContent": "", @@ -2477,11 +2953,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "建造者模式", + "title": "策略模式", "frontmatter": { - "title": "建造者模式", - "date": "2022-05-21T17:30:20.000Z", - "permalink": "/pages/c332ae/", + "title": "策略模式", + "date": "2022-05-21T17:29:26.000Z", + "permalink": "/pages/78c7aa/", "categories": [ "Java相关", "设计模式" @@ -2490,10 +2966,10 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/080.%E5%BB%BA%E9%80%A0%E8%80%85%E6%A8%A1%E5%BC%8F.html", - "relativePath": "01.Java相关/40.设计模式/080.建造者模式.md", - "key": "v-dd5f4440", - "path": "/pages/c332ae/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/050.%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F.html", + "relativePath": "01.Java相关/40.设计模式/050.策略模式.md", + "key": "v-5b0157cf", + "path": "/pages/78c7aa/", "headersStr": null, "content": "", "normalizedContent": "", @@ -2527,110 +3003,103 @@ export const siteData = { "normalizedTitle": "简述 tcp 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手?", "charIndex": 2 }, - { - "level": 2, - "title": "简述常见的 HTTP 状态码的含义(301,304,401,403)", - "slug": "简述常见的-http-状态码的含义-301-304-401-403", - "normalizedTitle": "简述常见的 http 状态码的含义(301,304,401,403)", - "charIndex": 44 - }, { "level": 2, "title": "从输入 URL 到展现页面的全过程", "slug": "从输入-url-到展现页面的全过程", "normalizedTitle": "从输入 url 到展现页面的全过程", - "charIndex": 83 + "charIndex": 44 }, { "level": 2, "title": "HTTP 中 GET 和 POST 区别", "slug": "http-中-get-和-post-区别", "normalizedTitle": "http 中 get 和 post 区别", - "charIndex": 105 + "charIndex": 66 }, { "level": 2, "title": "TCP 怎么保证可靠传输", "slug": "tcp-怎么保证可靠传输", "normalizedTitle": "tcp 怎么保证可靠传输", - "charIndex": 130 + "charIndex": 91 }, { "level": 2, "title": "什么是 TCP 粘包和拆包?", "slug": "什么是-tcp-粘包和拆包", "normalizedTitle": "什么是 tcp 粘包和拆包?", - "charIndex": 147 + "charIndex": 108 }, { "level": 2, "title": "简述 HTTPS 的加密与认证过程", "slug": "简述-https-的加密与认证过程", "normalizedTitle": "简述 https 的加密与认证过程", - "charIndex": 166 + "charIndex": 127 }, { "level": 2, "title": "简述对称与非对称加密的概念", "slug": "简述对称与非对称加密的概念", "normalizedTitle": "简述对称与非对称加密的概念", - "charIndex": 188 + "charIndex": 149 }, { "level": 2, "title": "TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别?", "slug": "tcp-与-udp-在网络协议中的哪一层-他们之间有什么区别", "normalizedTitle": "tcp 与 udp 在网络协议中的哪一层,他们之间有什么区别?", - "charIndex": 206 + "charIndex": 167 }, { "level": 2, "title": "如何解决 TCP 传输丢包问题?", "slug": "如何解决-tcp-传输丢包问题", "normalizedTitle": "如何解决 tcp 传输丢包问题?", - "charIndex": 242 + "charIndex": 203 }, { "level": 2, "title": "TCP 长连接和短连接有那么不同的使用场景?", "slug": "tcp-长连接和短连接有那么不同的使用场景", "normalizedTitle": "tcp 长连接和短连接有那么不同的使用场景?", - "charIndex": 263 + "charIndex": 224 }, { "level": 2, "title": "TCP 的 keepalive 了解吗?说一说它和 HTTP 的 keepalive 的区别?", "slug": "tcp-的-keepalive-了解吗-说一说它和-http-的-keepalive-的区别", "normalizedTitle": "tcp 的 keepalive 了解吗?说一说它和 http 的 keepalive 的区别?", - "charIndex": 290 + "charIndex": 251 }, { "level": 2, "title": "HTTP 与 HTTPS 有哪些区别?", "slug": "http-与-https-有哪些区别", "normalizedTitle": "http 与 https 有哪些区别?", - "charIndex": 342 + "charIndex": 303 }, { "level": 2, "title": "TCP 中常见的拥塞控制算法有哪些?", "slug": "tcp-中常见的拥塞控制算法有哪些", "normalizedTitle": "tcp 中常见的拥塞控制算法有哪些?", - "charIndex": 366 + "charIndex": 327 }, { "level": 2, "title": "简述 HTTP 1.0,1.1,2.0 的主要区别", "slug": "简述-http-1-0-1-1-2-0-的主要区别", "normalizedTitle": "简述 http 1.0,1.1,2.0 的主要区别", - "charIndex": 389 + "charIndex": 350 }, { "level": 2, "title": "从系统层面上,UDP如何保证尽量可靠?", "slug": "从系统层面上-udp如何保证尽量可靠", "normalizedTitle": "从系统层面上,udp如何保证尽量可靠?", - "charIndex": 419 + "charIndex": 380 }, { "level": 2, @@ -2644,103 +3113,103 @@ export const siteData = { "title": "简述 TCP 滑动窗口以及重传机制", "slug": "简述-tcp-滑动窗口以及重传机制", "normalizedTitle": "简述 tcp 滑动窗口以及重传机制", - "charIndex": 488 + "charIndex": 449 }, { "level": 2, "title": "简述 JWT 的原理和校验机制", "slug": "简述-jwt-的原理和校验机制", "normalizedTitle": "简述 jwt 的原理和校验机制", - "charIndex": 510 + "charIndex": 471 }, { "level": 2, "title": "为什么需要序列化?有什么序列化的方式?", "slug": "为什么需要序列化-有什么序列化的方式", "normalizedTitle": "为什么需要序列化?有什么序列化的方式?", - "charIndex": 530 + "charIndex": 491 }, { "level": 2, "title": "简述 iPv4 和 iPv6 的区别", "slug": "简述-ipv4-和-ipv6-的区别", "normalizedTitle": "简述 ipv4 和 ipv6 的区别", - "charIndex": 554 + "charIndex": 515 }, { "level": 2, "title": "TCP 中 SYN 攻击是什么?如何防止?", "slug": "tcp-中-syn-攻击是什么-如何防止", "normalizedTitle": "tcp 中 syn 攻击是什么?如何防止?", - "charIndex": 577 + "charIndex": 538 }, { "level": 2, "title": "简述 DDOS 攻击原理,如何防范它?", "slug": "简述-ddos-攻击原理-如何防范它", "normalizedTitle": "简述 ddos 攻击原理,如何防范它?", - "charIndex": 603 + "charIndex": 564 }, { "level": 2, "title": "什么是 ARP 协议?简述其使用场景", "slug": "什么是-arp-协议-简述其使用场景", "normalizedTitle": "什么是 arp 协议?简述其使用场景", - "charIndex": 627 + "charIndex": 588 }, { "level": 2, "title": "什么是跨域,什么情况下会发生跨域请求?", "slug": "什么是跨域-什么情况下会发生跨域请求", "normalizedTitle": "什么是跨域,什么情况下会发生跨域请求?", - "charIndex": 650 + "charIndex": 611 }, { "level": 2, "title": "Cookie 和 Session 的关系和区别是什么?", "slug": "cookie-和-session-的关系和区别是什么", "normalizedTitle": "cookie 和 session 的关系和区别是什么?", - "charIndex": 674 + "charIndex": 635 }, { "level": 2, "title": "简述 WebSocket 是如何进行传输的", "slug": "简述-websocket-是如何进行传输的", "normalizedTitle": "简述 websocket 是如何进行传输的", - "charIndex": 706 + "charIndex": 667 }, { "level": 2, "title": "DNS 查询服务器的基本流程是什么?DNS 劫持是什么?", "slug": "dns-查询服务器的基本流程是什么-dns-劫持是什么", "normalizedTitle": "dns 查询服务器的基本流程是什么?dns 劫持是什么?", - "charIndex": 732 + "charIndex": 693 }, { "level": 2, "title": "RestFul 是什么?RestFul 请求的 URL 有什么特点?", "slug": "restful-是什么-restful-请求的-url-有什么特点", "normalizedTitle": "restful 是什么?restful 请求的 url 有什么特点?", - "charIndex": 765 + "charIndex": 726 }, { "level": 2, "title": "简述 HTTP 报文头部的组成结构", "slug": "简述-http-报文头部的组成结构", "normalizedTitle": "简述 http 报文头部的组成结构", - "charIndex": 804 + "charIndex": 765 }, { "level": 2, "title": "什么是 SYN flood,如何防止这类攻击?", "slug": "什么是-syn-flood-如何防止这类攻击", "normalizedTitle": "什么是 syn flood,如何防止这类攻击?", - "charIndex": 826 + "charIndex": 787 } ], - "headersStr": "简述 TCP 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手? 简述常见的 HTTP 状态码的含义(301,304,401,403) 从输入 URL 到展现页面的全过程 HTTP 中 GET 和 POST 区别 TCP 怎么保证可靠传输 什么是 TCP 粘包和拆包? 简述 HTTPS 的加密与认证过程 简述对称与非对称加密的概念 TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别? 如何解决 TCP 传输丢包问题? TCP 长连接和短连接有那么不同的使用场景? TCP 的 keepalive 了解吗?说一说它和 HTTP 的 keepalive 的区别? HTTP 与 HTTPS 有哪些区别? TCP 中常见的拥塞控制算法有哪些? 简述 HTTP 1.0,1.1,2.0 的主要区别 从系统层面上,UDP如何保证尽量可靠? TCP 挥手时出现大量 CLOSEWAIT 或 TIMEWAIT 怎么解决? 简述 TCP 滑动窗口以及重传机制 简述 JWT 的原理和校验机制 为什么需要序列化?有什么序列化的方式? 简述 iPv4 和 iPv6 的区别 TCP 中 SYN 攻击是什么?如何防止? 简述 DDOS 攻击原理,如何防范它? 什么是 ARP 协议?简述其使用场景 什么是跨域,什么情况下会发生跨域请求? Cookie 和 Session 的关系和区别是什么? 简述 WebSocket 是如何进行传输的 DNS 查询服务器的基本流程是什么?DNS 劫持是什么? RestFul 是什么?RestFul 请求的 URL 有什么特点? 简述 HTTP 报文头部的组成结构 什么是 SYN flood,如何防止这类攻击?", - "content": "# 简述 TCP 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手?\n\n\n# 简述常见的 HTTP 状态码的含义(301,304,401,403)\n\n\n# 从输入 URL 到展现页面的全过程\n\n\n# HTTP 中 GET 和 POST 区别\n\n\n# TCP 怎么保证可靠传输\n\n\n# 什么是 TCP 粘包和拆包?\n\n\n# 简述 HTTPS 的加密与认证过程\n\n\n# 简述对称与非对称加密的概念\n\n\n# TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别?\n\n\n# 如何解决 TCP 传输丢包问题?\n\n\n# TCP 长连接和短连接有那么不同的使用场景?\n\n\n# TCP 的 keepalive 了解吗?说一说它和 HTTP 的 keepalive 的区别?\n\n\n# HTTP 与 HTTPS 有哪些区别?\n\n\n# TCP 中常见的拥塞控制算法有哪些?\n\n\n# 简述 HTTP 1.0,1.1,2.0 的主要区别\n\n\n# 从系统层面上,UDP如何保证尽量可靠?\n\n\n# TCP 挥手时出现大量 CLOSE_WAIT 或 TIME_WAIT 怎么解决?\n\n\n# 简述 TCP 滑动窗口以及重传机制\n\n\n# 简述 JWT 的原理和校验机制\n\n\n# 为什么需要序列化?有什么序列化的方式?\n\n\n# 简述 iPv4 和 iPv6 的区别\n\n\n# TCP 中 SYN 攻击是什么?如何防止?\n\n\n# 简述 DDOS 攻击原理,如何防范它?\n\n\n# 什么是 ARP 协议?简述其使用场景\n\n\n# 什么是跨域,什么情况下会发生跨域请求?\n\n\n# Cookie 和 Session 的关系和区别是什么?\n\n\n# 简述 WebSocket 是如何进行传输的\n\n\n# DNS 查询服务器的基本流程是什么?DNS 劫持是什么?\n\n\n# RestFul 是什么?RestFul 请求的 URL 有什么特点?\n\n\n# 简述 HTTP 报文头部的组成结构\n\n\n# 什么是 SYN flood,如何防止这类攻击?", - "normalizedContent": "# 简述 tcp 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手?\n\n\n# 简述常见的 http 状态码的含义(301,304,401,403)\n\n\n# 从输入 url 到展现页面的全过程\n\n\n# http 中 get 和 post 区别\n\n\n# tcp 怎么保证可靠传输\n\n\n# 什么是 tcp 粘包和拆包?\n\n\n# 简述 https 的加密与认证过程\n\n\n# 简述对称与非对称加密的概念\n\n\n# tcp 与 udp 在网络协议中的哪一层,他们之间有什么区别?\n\n\n# 如何解决 tcp 传输丢包问题?\n\n\n# tcp 长连接和短连接有那么不同的使用场景?\n\n\n# tcp 的 keepalive 了解吗?说一说它和 http 的 keepalive 的区别?\n\n\n# http 与 https 有哪些区别?\n\n\n# tcp 中常见的拥塞控制算法有哪些?\n\n\n# 简述 http 1.0,1.1,2.0 的主要区别\n\n\n# 从系统层面上,udp如何保证尽量可靠?\n\n\n# tcp 挥手时出现大量 close_wait 或 time_wait 怎么解决?\n\n\n# 简述 tcp 滑动窗口以及重传机制\n\n\n# 简述 jwt 的原理和校验机制\n\n\n# 为什么需要序列化?有什么序列化的方式?\n\n\n# 简述 ipv4 和 ipv6 的区别\n\n\n# tcp 中 syn 攻击是什么?如何防止?\n\n\n# 简述 ddos 攻击原理,如何防范它?\n\n\n# 什么是 arp 协议?简述其使用场景\n\n\n# 什么是跨域,什么情况下会发生跨域请求?\n\n\n# cookie 和 session 的关系和区别是什么?\n\n\n# 简述 websocket 是如何进行传输的\n\n\n# dns 查询服务器的基本流程是什么?dns 劫持是什么?\n\n\n# restful 是什么?restful 请求的 url 有什么特点?\n\n\n# 简述 http 报文头部的组成结构\n\n\n# 什么是 syn flood,如何防止这类攻击?", + "headersStr": "简述 TCP 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手? 从输入 URL 到展现页面的全过程 HTTP 中 GET 和 POST 区别 TCP 怎么保证可靠传输 什么是 TCP 粘包和拆包? 简述 HTTPS 的加密与认证过程 简述对称与非对称加密的概念 TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别? 如何解决 TCP 传输丢包问题? TCP 长连接和短连接有那么不同的使用场景? TCP 的 keepalive 了解吗?说一说它和 HTTP 的 keepalive 的区别? HTTP 与 HTTPS 有哪些区别? TCP 中常见的拥塞控制算法有哪些? 简述 HTTP 1.0,1.1,2.0 的主要区别 从系统层面上,UDP如何保证尽量可靠? TCP 挥手时出现大量 CLOSEWAIT 或 TIMEWAIT 怎么解决? 简述 TCP 滑动窗口以及重传机制 简述 JWT 的原理和校验机制 为什么需要序列化?有什么序列化的方式? 简述 iPv4 和 iPv6 的区别 TCP 中 SYN 攻击是什么?如何防止? 简述 DDOS 攻击原理,如何防范它? 什么是 ARP 协议?简述其使用场景 什么是跨域,什么情况下会发生跨域请求? Cookie 和 Session 的关系和区别是什么? 简述 WebSocket 是如何进行传输的 DNS 查询服务器的基本流程是什么?DNS 劫持是什么? RestFul 是什么?RestFul 请求的 URL 有什么特点? 简述 HTTP 报文头部的组成结构 什么是 SYN flood,如何防止这类攻击?", + "content": "# 简述 TCP 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手?\n\n\n# 从输入 URL 到展现页面的全过程\n\n\n# HTTP 中 GET 和 POST 区别\n\n\n# TCP 怎么保证可靠传输\n\n\n# 什么是 TCP 粘包和拆包?\n\n\n# 简述 HTTPS 的加密与认证过程\n\n\n# 简述对称与非对称加密的概念\n\n\n# TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别?\n\n\n# 如何解决 TCP 传输丢包问题?\n\n\n# TCP 长连接和短连接有那么不同的使用场景?\n\n\n# TCP 的 keepalive 了解吗?说一说它和 HTTP 的 keepalive 的区别?\n\n\n# HTTP 与 HTTPS 有哪些区别?\n\n\n# TCP 中常见的拥塞控制算法有哪些?\n\n\n# 简述 HTTP 1.0,1.1,2.0 的主要区别\n\n\n# 从系统层面上,UDP如何保证尽量可靠?\n\n\n# TCP 挥手时出现大量 CLOSE_WAIT 或 TIME_WAIT 怎么解决?\n\n\n# 简述 TCP 滑动窗口以及重传机制\n\n\n# 简述 JWT 的原理和校验机制\n\n\n# 为什么需要序列化?有什么序列化的方式?\n\n\n# 简述 iPv4 和 iPv6 的区别\n\n\n# TCP 中 SYN 攻击是什么?如何防止?\n\n\n# 简述 DDOS 攻击原理,如何防范它?\n\n\n# 什么是 ARP 协议?简述其使用场景\n\n\n# 什么是跨域,什么情况下会发生跨域请求?\n\n\n# Cookie 和 Session 的关系和区别是什么?\n\n\n# 简述 WebSocket 是如何进行传输的\n\n\n# DNS 查询服务器的基本流程是什么?DNS 劫持是什么?\n\n\n# RestFul 是什么?RestFul 请求的 URL 有什么特点?\n\n\n# 简述 HTTP 报文头部的组成结构\n\n\n# 什么是 SYN flood,如何防止这类攻击?", + "normalizedContent": "# 简述 tcp 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手?\n\n\n# 从输入 url 到展现页面的全过程\n\n\n# http 中 get 和 post 区别\n\n\n# tcp 怎么保证可靠传输\n\n\n# 什么是 tcp 粘包和拆包?\n\n\n# 简述 https 的加密与认证过程\n\n\n# 简述对称与非对称加密的概念\n\n\n# tcp 与 udp 在网络协议中的哪一层,他们之间有什么区别?\n\n\n# 如何解决 tcp 传输丢包问题?\n\n\n# tcp 长连接和短连接有那么不同的使用场景?\n\n\n# tcp 的 keepalive 了解吗?说一说它和 http 的 keepalive 的区别?\n\n\n# http 与 https 有哪些区别?\n\n\n# tcp 中常见的拥塞控制算法有哪些?\n\n\n# 简述 http 1.0,1.1,2.0 的主要区别\n\n\n# 从系统层面上,udp如何保证尽量可靠?\n\n\n# tcp 挥手时出现大量 close_wait 或 time_wait 怎么解决?\n\n\n# 简述 tcp 滑动窗口以及重传机制\n\n\n# 简述 jwt 的原理和校验机制\n\n\n# 为什么需要序列化?有什么序列化的方式?\n\n\n# 简述 ipv4 和 ipv6 的区别\n\n\n# tcp 中 syn 攻击是什么?如何防止?\n\n\n# 简述 ddos 攻击原理,如何防范它?\n\n\n# 什么是 arp 协议?简述其使用场景\n\n\n# 什么是跨域,什么情况下会发生跨域请求?\n\n\n# cookie 和 session 的关系和区别是什么?\n\n\n# 简述 websocket 是如何进行传输的\n\n\n# dns 查询服务器的基本流程是什么?dns 劫持是什么?\n\n\n# restful 是什么?restful 请求的 url 有什么特点?\n\n\n# 简述 http 报文头部的组成结构\n\n\n# 什么是 syn flood,如何防止这类攻击?", "charsets": { "cjk": true }, @@ -2748,53 +3217,216 @@ export const siteData = { "lastUpdatedTimestamp": 1655736461000 }, { - "title": "操作系统", + "title": "建造者模式", "frontmatter": { - "title": "操作系统", - "date": "2022-03-11T00:51:48.000Z", - "permalink": "/pages/e5f2d7/", + "title": "建造者模式", + "date": "2022-05-21T17:30:20.000Z", + "permalink": "/pages/c332ae/", + "categories": [ + "Java相关", + "设计模式" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/080.%E5%BB%BA%E9%80%A0%E8%80%85%E6%A8%A1%E5%BC%8F.html", + "relativePath": "01.Java相关/40.设计模式/080.建造者模式.md", + "key": "v-dd5f4440", + "path": "/pages/c332ae/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "简述常见的 HTTP 状态码的含义", + "frontmatter": { + "title": "简述常见的 HTTP 状态码的含义", + "date": "2022-06-23T21:09:29.000Z", + "permalink": "/pages/f33310/", "categories": [ "计算机基础", - "CSS" + "计算机网络" ], "tags": [ null ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/01.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F.html", - "relativePath": "02.计算机基础/20.操作系统/01.操作系统.md", - "key": "v-6f178cbc", - "path": "/pages/e5f2d7/", + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/010.%E7%AE%80%E8%BF%B0%E5%B8%B8%E8%A7%81%E7%9A%84%20HTTP%20%E7%8A%B6%E6%80%81%E7%A0%81%E7%9A%84%E5%90%AB%E4%B9%89.html", + "relativePath": "02.计算机基础/10.计算机网络/010.简述常见的 HTTP 状态码的含义.md", + "key": "v-0a1d1fb4", + "path": "/pages/f33310/", "headers": [ { "level": 2, - "title": "进程间有哪些通信方式?", - "slug": "进程间有哪些通信方式", - "normalizedTitle": "进程间有哪些通信方式?", + "title": "2XX 成功", + "slug": "_2xx-成功", + "normalizedTitle": "2xx 成功", "charIndex": 2 }, { "level": 2, - "title": "简述自旋锁与互斥锁的使用场景", - "slug": "简述自旋锁与互斥锁的使用场景", - "normalizedTitle": "简述自旋锁与互斥锁的使用场景", - "charIndex": 18 + "title": "3XX 重定向", + "slug": "_3xx-重定向", + "normalizedTitle": "3xx 重定向", + "charIndex": 107 }, { "level": 2, - "title": "进程和线程之间有什么区别?", - "slug": "进程和线程之间有什么区别", - "normalizedTitle": "进程和线程之间有什么区别?", - "charIndex": 37 + "title": "4XX 客户端错误", + "slug": "_4xx-客户端错误", + "normalizedTitle": "4xx 客户端错误", + "charIndex": 327 }, { "level": 2, - "title": "简述几个常用的 Linux 命令以及他们的功能", - "slug": "简述几个常用的-linux-命令以及他们的功能", - "normalizedTitle": "简述几个常用的 linux 命令以及他们的功能", - "charIndex": 55 - }, - { + "title": "5XX 服务器错误", + "slug": "_5xx-服务器错误", + "normalizedTitle": "5xx 服务器错误", + "charIndex": 474 + } + ], + "headersStr": "2XX 成功 3XX 重定向 4XX 客户端错误 5XX 服务器错误", + "content": "# 2XX 成功\n\n200 OK,表示从客户端发来的请求在服务器端被正确处理 204 No content,表示请求成功,但响应报文不含实体的主体部分 206 Partial Content,进行范围请求\n\n\n# 3XX 重定向\n\n301 moved permanently,永久性重定向,表示资源已被分配了新的 URL 302 found,临时性重定向,表示资源临时被分配了新的 URL 303 see other,表示资源存在着另一个 URL,应使用 GET 方法定向获取资源 304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况 307 temporary redirect,临时重定向,和302含义相同\n\n\n# 4XX 客户端错误\n\n400 bad request,请求报文存在语法错误 401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息 403 forbidden,表示对请求资源的访问被服务器拒绝 404 not found,表示在服务器上没有找到请求的资源\n\n\n# 5XX 服务器错误\n\n500 internal sever error,表示服务器端在执行请求时发生了错误 503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求", + "normalizedContent": "# 2xx 成功\n\n200 ok,表示从客户端发来的请求在服务器端被正确处理 204 no content,表示请求成功,但响应报文不含实体的主体部分 206 partial content,进行范围请求\n\n\n# 3xx 重定向\n\n301 moved permanently,永久性重定向,表示资源已被分配了新的 url 302 found,临时性重定向,表示资源临时被分配了新的 url 303 see other,表示资源存在着另一个 url,应使用 get 方法定向获取资源 304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况 307 temporary redirect,临时重定向,和302含义相同\n\n\n# 4xx 客户端错误\n\n400 bad request,请求报文存在语法错误 401 unauthorized,表示发送的请求需要有通过 http 认证的认证信息 403 forbidden,表示对请求资源的访问被服务器拒绝 404 not found,表示在服务器上没有找到请求的资源\n\n\n# 5xx 服务器错误\n\n500 internal sever error,表示服务器端在执行请求时发生了错误 503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求", + "charsets": { + "cjk": true + } + }, + { + "title": "从输入 URL 到展现页面的全过程", + "frontmatter": { + "title": "从输入 URL 到展现页面的全过程", + "date": "2022-06-23T21:10:44.000Z", + "permalink": "/pages/ab48ba/", + "categories": [ + "计算机基础", + "计算机网络" + ], + "tags": [ + null + ] + }, + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/020.%E4%BB%8E%E8%BE%93%E5%85%A5%20URL%20%E5%88%B0%E5%B1%95%E7%8E%B0%E9%A1%B5%E9%9D%A2%E7%9A%84%E5%85%A8%E8%BF%87%E7%A8%8B.html", + "relativePath": "02.计算机基础/10.计算机网络/020.从输入 URL 到展现页面的全过程.md", + "key": "v-6602e3da", + "path": "/pages/ab48ba/", + "headers": [ + { + "level": 2, + "title": "什么是IP地址", + "slug": "什么是ip地址", + "normalizedTitle": "什么是ip地址", + "charIndex": 459 + }, + { + "level": 2, + "title": "什么是DNS", + "slug": "什么是dns", + "normalizedTitle": "什么是dns", + "charIndex": 608 + }, + { + "level": 2, + "title": "DNS解析", + "slug": "dns解析-2", + "normalizedTitle": "dns解析", + "charIndex": 57 + }, + { + "level": 2, + "title": "DNS查询的两种方式:递归查询和迭代查询", + "slug": "dns查询的两种方式-递归查询和迭代查询", + "normalizedTitle": "dns查询的两种方式:递归查询和迭代查询", + "charIndex": 752 + }, + { + "level": 2, + "title": "DNS域名称空间的组织方式", + "slug": "dns域名称空间的组织方式", + "normalizedTitle": "dns域名称空间的组织方式", + "charIndex": 788 + }, + { + "level": 2, + "title": "DNS负载均衡", + "slug": "dns负载均衡", + "normalizedTitle": "dns负载均衡", + "charIndex": 808 + }, + { + "level": 2, + "title": "”三次握手”的详解", + "slug": "三次握手-的详解", + "normalizedTitle": "”三次握手”的详解", + "charIndex": 1033 + }, + { + "level": 2, + "title": "为什么要三次握手", + "slug": "为什么要三次握手", + "normalizedTitle": "为什么要三次握手", + "charIndex": 1049 + } + ], + "headersStr": "什么是IP地址 什么是DNS DNS解析 DNS查询的两种方式:递归查询和迭代查询 DNS域名称空间的组织方式 DNS负载均衡 ”三次握手”的详解 为什么要三次握手", + "content": "从我们输入一个网址到页面展现的过程到底发生了什么呢?\n\n\n# 总体来说分为以下几个过程:\n\n * 输入网址\n * DNS解析\n * 建立TCP/IP链接\n * 发送HTTP请求\n * 服务器处理请求\n * 服务器返回HTTP响应\n * 浏览器渲染页面并展现\n * 断开连接\n\n\n# 输入网址\n\n当我们想要打开一个页面时,很简单的会想到去输入一个网址 我们以本网址为例,解释一下它各部分的组成:\n\nhttps://www.baidu.com\n\n\n1\n\n\n * https :https是协议(Protocol),它表明了浏览器使用何种协议,有http、https、ftp、mailto等,其中https是加密认证版的http(详见《图解HTTP》)\n\n * www.baidu.com :这是域名(Domain name),它表明正在请求哪个Web服务器,其中www是主机名(host)\n\n\n# DNS解析\n\n输入网址(URL)这一步我们很好理解,但是接下来所谓的域名解析是什么呢?又是为什么要进行域名解析呢?\n\n\n# 什么是IP地址\n\nIP 地址(Internet Protocol Address)是互联网协议特有的一种地址,它是 IP 协议提供的一种统一的地址格式。IP 地址为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。 像192.168.59.0就是一个主机IP地址\n\n\n# 什么是DNS\n\nDNS服务器上存储着映射<域名-IP>,DNS解析就是去DNS服务器查询IP地址。\n\n\n# DNS解析\n\nDNS解析按照下面顺序逐个查找。\n\n * 本地(hosts文件)\n * DNS服务器(本地DNS | 根DNS)\n * 域服务器\n * 解析服务器的地址\n\n\n\n\n# DNS查询的两种方式:递归查询和迭代查询\n\n递归解析 迭代解析\n\n\n# DNS域名称空间的组织方式\n\n\n\n\n# DNS负载均衡\n\n当一个网站有足够多的用户的时候,假如每次请求的资源都位于同一台机器上面,那么这台机器随时可能会蹦掉。\n\n处理办法就是用DNS负载均衡技术,它的原理是在DNS服务器中为同一个主机名配置多个IP地址 在应答DNS查询时,DNS服务器对每个查询将以DNS文件中主机记录的IP地址按顺序返回不同的解析结果,将客户端的访问引导到不同的机器上去,使得不同的客户端访问不同的服务器,从而达到负载均衡的目的。\n\n\n# 建立TCP/IP链接\n\n\n# ”三次握手”的详解\n\n\n\n\n# 为什么要三次握手\n\n * clien端 防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。 client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致连接释放以后的某个时间才到达server。 本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。 于是就向client发出确认报文段,同意建立连接。\n\n * server端 假设不采用“三次握手”。 如果只要server发出确认,新的连接就建立了。并一直等待client发来数据。 事实是client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。 但server端就会有许多这样的等待,server的很多资源就白白浪费掉了。\n\n采用“三次握手”的办法可以防止上述现象发生。主要目的防止server端一直等待,浪费资源。", + "normalizedContent": "从我们输入一个网址到页面展现的过程到底发生了什么呢?\n\n\n# 总体来说分为以下几个过程:\n\n * 输入网址\n * dns解析\n * 建立tcp/ip链接\n * 发送http请求\n * 服务器处理请求\n * 服务器返回http响应\n * 浏览器渲染页面并展现\n * 断开连接\n\n\n# 输入网址\n\n当我们想要打开一个页面时,很简单的会想到去输入一个网址 我们以本网址为例,解释一下它各部分的组成:\n\nhttps://www.baidu.com\n\n\n1\n\n\n * https :https是协议(protocol),它表明了浏览器使用何种协议,有http、https、ftp、mailto等,其中https是加密认证版的http(详见《图解http》)\n\n * www.baidu.com :这是域名(domain name),它表明正在请求哪个web服务器,其中www是主机名(host)\n\n\n# dns解析\n\n输入网址(url)这一步我们很好理解,但是接下来所谓的域名解析是什么呢?又是为什么要进行域名解析呢?\n\n\n# 什么是ip地址\n\nip 地址(internet protocol address)是互联网协议特有的一种地址,它是 ip 协议提供的一种统一的地址格式。ip 地址为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。 像192.168.59.0就是一个主机ip地址\n\n\n# 什么是dns\n\ndns服务器上存储着映射<域名-ip>,dns解析就是去dns服务器查询ip地址。\n\n\n# dns解析\n\ndns解析按照下面顺序逐个查找。\n\n * 本地(hosts文件)\n * dns服务器(本地dns | 根dns)\n * 域服务器\n * 解析服务器的地址\n\n\n\n\n# dns查询的两种方式:递归查询和迭代查询\n\n递归解析 迭代解析\n\n\n# dns域名称空间的组织方式\n\n\n\n\n# dns负载均衡\n\n当一个网站有足够多的用户的时候,假如每次请求的资源都位于同一台机器上面,那么这台机器随时可能会蹦掉。\n\n处理办法就是用dns负载均衡技术,它的原理是在dns服务器中为同一个主机名配置多个ip地址 在应答dns查询时,dns服务器对每个查询将以dns文件中主机记录的ip地址按顺序返回不同的解析结果,将客户端的访问引导到不同的机器上去,使得不同的客户端访问不同的服务器,从而达到负载均衡的目的。\n\n\n# 建立tcp/ip链接\n\n\n# ”三次握手”的详解\n\n\n\n\n# 为什么要三次握手\n\n * clien端 防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。 client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致连接释放以后的某个时间才到达server。 本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。 于是就向client发出确认报文段,同意建立连接。\n\n * server端 假设不采用“三次握手”。 如果只要server发出确认,新的连接就建立了。并一直等待client发来数据。 事实是client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。 但server端就会有许多这样的等待,server的很多资源就白白浪费掉了。\n\n采用“三次握手”的办法可以防止上述现象发生。主要目的防止server端一直等待,浪费资源。", + "charsets": { + "cjk": true + } + }, + { + "title": "操作系统", + "frontmatter": { + "title": "操作系统", + "date": "2022-03-11T00:51:48.000Z", + "permalink": "/pages/e5f2d7/", + "categories": [ + "计算机基础", + "CSS" + ], + "tags": [ + null + ] + }, + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/01.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F.html", + "relativePath": "02.计算机基础/20.操作系统/01.操作系统.md", + "key": "v-6f178cbc", + "path": "/pages/e5f2d7/", + "headers": [ + { + "level": 2, + "title": "进程间有哪些通信方式?", + "slug": "进程间有哪些通信方式", + "normalizedTitle": "进程间有哪些通信方式?", + "charIndex": 2 + }, + { + "level": 2, + "title": "简述自旋锁与互斥锁的使用场景", + "slug": "简述自旋锁与互斥锁的使用场景", + "normalizedTitle": "简述自旋锁与互斥锁的使用场景", + "charIndex": 18 + }, + { + "level": 2, + "title": "进程和线程之间有什么区别?", + "slug": "进程和线程之间有什么区别", + "normalizedTitle": "进程和线程之间有什么区别?", + "charIndex": 37 + }, + { + "level": 2, + "title": "简述几个常用的 Linux 命令以及他们的功能", + "slug": "简述几个常用的-linux-命令以及他们的功能", + "normalizedTitle": "简述几个常用的 linux 命令以及他们的功能", + "charIndex": 55 + }, + { "level": 2, "title": "创建线程有多少种方式?", "slug": "创建线程有多少种方式", @@ -3087,11 +3719,11 @@ export const siteData = { "lastUpdatedTimestamp": 1655736461000 }, { - "title": "MySQL中 InnoDB 和 MylSAM 的区别是什么?", + "title": "聚簇索引和非聚簇索引有什么区别?", "frontmatter": { - "title": "MySQL中 InnoDB 和 MylSAM 的区别是什么?", - "date": "2022-05-21T17:07:47.000Z", - "permalink": "/pages/3a8659/", + "title": "聚簇索引和非聚簇索引有什么区别?", + "date": "2022-05-21T17:08:04.000Z", + "permalink": "/pages/a5adb0/", "categories": [ "数据库", "MYSQL相关" @@ -3100,29 +3732,29 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/001.MySQL%E4%B8%AD%20InnoDB%20%E5%92%8C%20MylSAM%20%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/001.MySQL中 InnoDB 和 MylSAM 的区别是什么?.md", - "key": "v-674454ac", - "path": "/pages/3a8659/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/006.%E8%81%9A%E7%B0%87%E7%B4%A2%E5%BC%95%E5%92%8C%E9%9D%9E%E8%81%9A%E7%B0%87%E7%B4%A2%E5%BC%95%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/006.聚簇索引和非聚簇索引有什么区别?.md", + "key": "v-f0f711c4", + "path": "/pages/a5adb0/", "headers": [ { "level": 2, - "title": "区别", - "slug": "区别", - "normalizedTitle": "区别", - "charIndex": 37 + "title": "聚簇索引", + "slug": "聚簇索引", + "normalizedTitle": "聚簇索引", + "charIndex": 2 }, { "level": 2, - "title": "场景选择", - "slug": "场景选择", - "normalizedTitle": "场景选择", - "charIndex": 253 + "title": "非聚簇索引", + "slug": "非聚簇索引", + "normalizedTitle": "非聚簇索引", + "charIndex": 119 } ], - "headersStr": "区别 场景选择", - "content": "MySQL5.5版本开始Innodb已经成为Mysql的默认引擎\n\n\n# 区别\n\n * InnoDB 支持事务,MyISAM 不支持事务。 这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一\n * MyISAM锁的粒度是表级的,而InnoDB支持行级锁\n * MyISAM支持全文类型索引,而InnoDB不支持全文索引\n * MyISAM相对简单,效率上要优于InnoDB,小型应用可以考虑使用MyISAM\n * MyISAM表保存成文件形式,跨平台使用更加方便\n\n\n# 场景选择\n\nMyISAM管理非事务表,提供高速存储和检索以及全文搜索能力,如果再应用中执行大量select操作,应该选择MyISAM InnoDB用于事务处理,具有ACID事务支持等特性,如果在应用中执行大量insert和update操作,应该选择InnoDB", - "normalizedContent": "mysql5.5版本开始innodb已经成为mysql的默认引擎\n\n\n# 区别\n\n * innodb 支持事务,myisam 不支持事务。 这是 mysql 将默认存储引擎从 myisam 变成 innodb 的重要原因之一\n * myisam锁的粒度是表级的,而innodb支持行级锁\n * myisam支持全文类型索引,而innodb不支持全文索引\n * myisam相对简单,效率上要优于innodb,小型应用可以考虑使用myisam\n * myisam表保存成文件形式,跨平台使用更加方便\n\n\n# 场景选择\n\nmyisam管理非事务表,提供高速存储和检索以及全文搜索能力,如果再应用中执行大量select操作,应该选择myisam innodb用于事务处理,具有acid事务支持等特性,如果在应用中执行大量insert和update操作,应该选择innodb", + "headersStr": "聚簇索引 非聚簇索引", + "content": "# 聚簇索引\n\n * 对应Innodb引擎中的主键索引\n * 索引的叶节点就是数据节点 -正式聚簇索引的顺序就是数据的物理存储顺序,所以一个表最多只能有一个聚簇索引,因为物理存储只能有一个顺序。正因为一个表最多只能有一个聚簇索引\n\n\n# 非聚簇索引\n\n * 对应MyISAM\n * 非聚簇索引的叶节点仍然是索引节点,只不过有一个指针指向对应的数据块。\n\n", + "normalizedContent": "# 聚簇索引\n\n * 对应innodb引擎中的主键索引\n * 索引的叶节点就是数据节点 -正式聚簇索引的顺序就是数据的物理存储顺序,所以一个表最多只能有一个聚簇索引,因为物理存储只能有一个顺序。正因为一个表最多只能有一个聚簇索引\n\n\n# 非聚簇索引\n\n * 对应myisam\n * 非聚簇索引的叶节点仍然是索引节点,只不过有一个指针指向对应的数据块。\n\n", "charsets": { "cjk": true }, @@ -3130,11 +3762,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "聚簇索引和非聚簇索引有什么区别?", + "title": "MySQL中 InnoDB 和 MylSAM 的区别是什么?", "frontmatter": { - "title": "聚簇索引和非聚簇索引有什么区别?", - "date": "2022-05-21T17:08:04.000Z", - "permalink": "/pages/a5adb0/", + "title": "MySQL中 InnoDB 和 MylSAM 的区别是什么?", + "date": "2022-05-21T17:07:47.000Z", + "permalink": "/pages/3a8659/", "categories": [ "数据库", "MYSQL相关" @@ -3143,29 +3775,29 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/006.%E8%81%9A%E7%B0%87%E7%B4%A2%E5%BC%95%E5%92%8C%E9%9D%9E%E8%81%9A%E7%B0%87%E7%B4%A2%E5%BC%95%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/006.聚簇索引和非聚簇索引有什么区别?.md", - "key": "v-f0f711c4", - "path": "/pages/a5adb0/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/001.MySQL%E4%B8%AD%20InnoDB%20%E5%92%8C%20MylSAM%20%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/001.MySQL中 InnoDB 和 MylSAM 的区别是什么?.md", + "key": "v-674454ac", + "path": "/pages/3a8659/", "headers": [ { "level": 2, - "title": "聚簇索引", - "slug": "聚簇索引", - "normalizedTitle": "聚簇索引", - "charIndex": 2 + "title": "区别", + "slug": "区别", + "normalizedTitle": "区别", + "charIndex": 37 }, { "level": 2, - "title": "非聚簇索引", - "slug": "非聚簇索引", - "normalizedTitle": "非聚簇索引", - "charIndex": 119 + "title": "场景选择", + "slug": "场景选择", + "normalizedTitle": "场景选择", + "charIndex": 253 } ], - "headersStr": "聚簇索引 非聚簇索引", - "content": "# 聚簇索引\n\n * 对应Innodb引擎中的主键索引\n * 索引的叶节点就是数据节点 -正式聚簇索引的顺序就是数据的物理存储顺序,所以一个表最多只能有一个聚簇索引,因为物理存储只能有一个顺序。正因为一个表最多只能有一个聚簇索引\n\n\n# 非聚簇索引\n\n * 对应MyISAM\n * 非聚簇索引的叶节点仍然是索引节点,只不过有一个指针指向对应的数据块。\n\n", - "normalizedContent": "# 聚簇索引\n\n * 对应innodb引擎中的主键索引\n * 索引的叶节点就是数据节点 -正式聚簇索引的顺序就是数据的物理存储顺序,所以一个表最多只能有一个聚簇索引,因为物理存储只能有一个顺序。正因为一个表最多只能有一个聚簇索引\n\n\n# 非聚簇索引\n\n * 对应myisam\n * 非聚簇索引的叶节点仍然是索引节点,只不过有一个指针指向对应的数据块。\n\n", + "headersStr": "区别 场景选择", + "content": "MySQL5.5版本开始Innodb已经成为Mysql的默认引擎\n\n\n# 区别\n\n * InnoDB 支持事务,MyISAM 不支持事务。 这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一\n * MyISAM锁的粒度是表级的,而InnoDB支持行级锁\n * MyISAM支持全文类型索引,而InnoDB不支持全文索引\n * MyISAM相对简单,效率上要优于InnoDB,小型应用可以考虑使用MyISAM\n * MyISAM表保存成文件形式,跨平台使用更加方便\n\n\n# 场景选择\n\nMyISAM管理非事务表,提供高速存储和检索以及全文搜索能力,如果再应用中执行大量select操作,应该选择MyISAM InnoDB用于事务处理,具有ACID事务支持等特性,如果在应用中执行大量insert和update操作,应该选择InnoDB", + "normalizedContent": "mysql5.5版本开始innodb已经成为mysql的默认引擎\n\n\n# 区别\n\n * innodb 支持事务,myisam 不支持事务。 这是 mysql 将默认存储引擎从 myisam 变成 innodb 的重要原因之一\n * myisam锁的粒度是表级的,而innodb支持行级锁\n * myisam支持全文类型索引,而innodb不支持全文索引\n * myisam相对简单,效率上要优于innodb,小型应用可以考虑使用myisam\n * myisam表保存成文件形式,跨平台使用更加方便\n\n\n# 场景选择\n\nmyisam管理非事务表,提供高速存储和检索以及全文搜索能力,如果再应用中执行大量select操作,应该选择myisam innodb用于事务处理,具有acid事务支持等特性,如果在应用中执行大量insert和update操作,应该选择innodb", "charsets": { "cjk": true }, @@ -3197,63 +3829,6 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, - { - "title": "简述Mysql的事务隔离级别", - "frontmatter": { - "title": "简述Mysql的事务隔离级别", - "date": "2022-05-20T11:35:30.000Z", - "permalink": "/pages/9f2936/", - "categories": [ - "数据库", - "MYSQL相关" - ], - "tags": [ - null - ] - }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/010.%E7%AE%80%E8%BF%B0Mysql%E7%9A%84%E4%BA%8B%E5%8A%A1%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB.html", - "relativePath": "03.数据库/01.MYSQL相关/010.简述Mysql的事务隔离级别.md", - "key": "v-7e02c4a1", - "path": "/pages/9f2936/", - "headers": [ - { - "level": 2, - "title": "READ UNCOMMITTED (未提交读)", - "slug": "read-uncommitted-未提交读", - "normalizedTitle": "read uncommitted (未提交读)", - "charIndex": 2 - }, - { - "level": 2, - "title": "READ COMMITTED (提交读)", - "slug": "read-committed-提交读", - "normalizedTitle": "read committed (提交读)", - "charIndex": 55 - }, - { - "level": 2, - "title": "REPEATABLE READ (可重复读)", - "slug": "repeatable-read-可重复读", - "normalizedTitle": "repeatable read (可重复读)", - "charIndex": 117 - }, - { - "level": 2, - "title": "SERIALIZABLE (可串行化)", - "slug": "serializable-可串行化", - "normalizedTitle": "serializable (可串行化)", - "charIndex": 168 - } - ], - "headersStr": "READ UNCOMMITTED (未提交读) READ COMMITTED (提交读) REPEATABLE READ (可重复读) SERIALIZABLE (可串行化)", - "content": "# READ UNCOMMITTED (未提交读)\n\n存在脏读问题 存在不可重复读问题 存在幻读问题\n\n\n# READ COMMITTED (提交读)\n\n只读已经事物提交的数据,不存在脏读。 存在不可重复读问题 存在幻读问题\n\n\n# REPEATABLE READ (可重复读)\n\n是mysql默认的隔离级别 mvcc解决幻读\n\n\n# SERIALIZABLE (可串行化)\n\n读写串行化,效率太低,没人使用。", - "normalizedContent": "# read uncommitted (未提交读)\n\n存在脏读问题 存在不可重复读问题 存在幻读问题\n\n\n# read committed (提交读)\n\n只读已经事物提交的数据,不存在脏读。 存在不可重复读问题 存在幻读问题\n\n\n# repeatable read (可重复读)\n\n是mysql默认的隔离级别 mvcc解决幻读\n\n\n# serializable (可串行化)\n\n读写串行化,效率太低,没人使用。", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, { "title": "说一下Mysql锁", "frontmatter": { @@ -3312,11 +3887,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "MySQL 为什么使用 B+ 树来作索引,对比 B 树它的优点和缺点是什么", + "title": "简述Mysql的事务隔离级别", "frontmatter": { - "title": "MySQL 为什么使用 B+ 树来作索引,对比 B 树它的优点和缺点是什么", - "date": "2022-05-21T17:05:11.000Z", - "permalink": "/pages/bc5538/", + "title": "简述Mysql的事务隔离级别", + "date": "2022-05-20T11:35:30.000Z", + "permalink": "/pages/9f2936/", "categories": [ "数据库", "MYSQL相关" @@ -3325,24 +3900,81 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/020.MySQL%20%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BD%BF%E7%94%A8%20B+%20%E6%A0%91%E6%9D%A5%E4%BD%9C%E7%B4%A2%E5%BC%95%EF%BC%8C%E5%AF%B9%E6%AF%94%20B%20%E6%A0%91%E5%AE%83%E7%9A%84%E4%BC%98%E7%82%B9%E5%92%8C%E7%BC%BA%E7%82%B9%E6%98%AF%E4%BB%80%E4%B9%88.html", - "relativePath": "03.数据库/01.MYSQL相关/020.MySQL 为什么使用 B+ 树来作索引,对比 B 树它的优点和缺点是什么.md", - "key": "v-6c681639", - "path": "/pages/bc5538/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/010.%E7%AE%80%E8%BF%B0Mysql%E7%9A%84%E4%BA%8B%E5%8A%A1%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB.html", + "relativePath": "03.数据库/01.MYSQL相关/010.简述Mysql的事务隔离级别.md", + "key": "v-7e02c4a1", + "path": "/pages/9f2936/", "headers": [ { "level": 2, - "title": "磁盘存取原理", - "slug": "磁盘存取原理", - "normalizedTitle": "磁盘存取原理", - "charIndex": 59 + "title": "READ UNCOMMITTED (未提交读)", + "slug": "read-uncommitted-未提交读", + "normalizedTitle": "read uncommitted (未提交读)", + "charIndex": 2 }, { "level": 2, - "title": "B-/+Tree索引的性能分析", - "slug": "b-tree索引的性能分析", - "normalizedTitle": "b-/+tree索引的性能分析", - "charIndex": 246 + "title": "READ COMMITTED (提交读)", + "slug": "read-committed-提交读", + "normalizedTitle": "read committed (提交读)", + "charIndex": 55 + }, + { + "level": 2, + "title": "REPEATABLE READ (可重复读)", + "slug": "repeatable-read-可重复读", + "normalizedTitle": "repeatable read (可重复读)", + "charIndex": 117 + }, + { + "level": 2, + "title": "SERIALIZABLE (可串行化)", + "slug": "serializable-可串行化", + "normalizedTitle": "serializable (可串行化)", + "charIndex": 168 + } + ], + "headersStr": "READ UNCOMMITTED (未提交读) READ COMMITTED (提交读) REPEATABLE READ (可重复读) SERIALIZABLE (可串行化)", + "content": "# READ UNCOMMITTED (未提交读)\n\n存在脏读问题 存在不可重复读问题 存在幻读问题\n\n\n# READ COMMITTED (提交读)\n\n只读已经事物提交的数据,不存在脏读。 存在不可重复读问题 存在幻读问题\n\n\n# REPEATABLE READ (可重复读)\n\n是mysql默认的隔离级别 mvcc解决幻读\n\n\n# SERIALIZABLE (可串行化)\n\n读写串行化,效率太低,没人使用。", + "normalizedContent": "# read uncommitted (未提交读)\n\n存在脏读问题 存在不可重复读问题 存在幻读问题\n\n\n# read committed (提交读)\n\n只读已经事物提交的数据,不存在脏读。 存在不可重复读问题 存在幻读问题\n\n\n# repeatable read (可重复读)\n\n是mysql默认的隔离级别 mvcc解决幻读\n\n\n# serializable (可串行化)\n\n读写串行化,效率太低,没人使用。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "MySQL 为什么使用 B+ 树来作索引,对比 B 树它的优点和缺点是什么", + "frontmatter": { + "title": "MySQL 为什么使用 B+ 树来作索引,对比 B 树它的优点和缺点是什么", + "date": "2022-05-21T17:05:11.000Z", + "permalink": "/pages/bc5538/", + "categories": [ + "数据库", + "MYSQL相关" + ], + "tags": [ + null + ] + }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/020.MySQL%20%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BD%BF%E7%94%A8%20B+%20%E6%A0%91%E6%9D%A5%E4%BD%9C%E7%B4%A2%E5%BC%95%EF%BC%8C%E5%AF%B9%E6%AF%94%20B%20%E6%A0%91%E5%AE%83%E7%9A%84%E4%BC%98%E7%82%B9%E5%92%8C%E7%BC%BA%E7%82%B9%E6%98%AF%E4%BB%80%E4%B9%88.html", + "relativePath": "03.数据库/01.MYSQL相关/020.MySQL 为什么使用 B+ 树来作索引,对比 B 树它的优点和缺点是什么.md", + "key": "v-6c681639", + "path": "/pages/bc5538/", + "headers": [ + { + "level": 2, + "title": "磁盘存取原理", + "slug": "磁盘存取原理", + "normalizedTitle": "磁盘存取原理", + "charIndex": 59 + }, + { + "level": 2, + "title": "B-/+Tree索引的性能分析", + "slug": "b-tree索引的性能分析", + "normalizedTitle": "b-/+tree索引的性能分析", + "charIndex": 246 } ], "headersStr": "磁盘存取原理 B-/+Tree索引的性能分析", @@ -3405,11 +4037,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述什么是最左匹配原则", + "title": "MySQL 联合索引底层原理是什么?", "frontmatter": { - "title": "简述什么是最左匹配原则", - "date": "2022-05-21T17:06:10.000Z", - "permalink": "/pages/7a1f1e/", + "title": "MySQL 联合索引底层原理是什么?", + "date": "2022-05-21T17:06:26.000Z", + "permalink": "/pages/52773a/", "categories": [ "数据库", "MYSQL相关" @@ -3418,10 +4050,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/050.%E7%AE%80%E8%BF%B0%E4%BB%80%E4%B9%88%E6%98%AF%E6%9C%80%E5%B7%A6%E5%8C%B9%E9%85%8D%E5%8E%9F%E5%88%99.html", - "relativePath": "03.数据库/01.MYSQL相关/050.简述什么是最左匹配原则.md", - "key": "v-4ecddbec", - "path": "/pages/7a1f1e/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/060.MySQL%20%E8%81%94%E5%90%88%E7%B4%A2%E5%BC%95%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/060.MySQL 联合索引底层原理是什么?.md", + "key": "v-505fbe2e", + "path": "/pages/52773a/", "headersStr": null, "content": "", "normalizedContent": "", @@ -3430,11 +4062,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "MySQL 联合索引底层原理是什么?", + "title": "简述常见的负载均衡算法", "frontmatter": { - "title": "MySQL 联合索引底层原理是什么?", - "date": "2022-05-21T17:06:26.000Z", - "permalink": "/pages/52773a/", + "title": "简述常见的负载均衡算法", + "date": "2022-05-21T17:07:07.000Z", + "permalink": "/pages/f2dbbe/", "categories": [ "数据库", "MYSQL相关" @@ -3443,10 +4075,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/060.MySQL%20%E8%81%94%E5%90%88%E7%B4%A2%E5%BC%95%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/060.MySQL 联合索引底层原理是什么?.md", - "key": "v-505fbe2e", - "path": "/pages/52773a/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/080.%E7%AE%80%E8%BF%B0%E5%B8%B8%E8%A7%81%E7%9A%84%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1%E7%AE%97%E6%B3%95.html", + "relativePath": "03.数据库/01.MYSQL相关/080.简述常见的负载均衡算法.md", + "key": "v-53bcbb7a", + "path": "/pages/f2dbbe/", "headersStr": null, "content": "", "normalizedContent": "", @@ -3480,11 +4112,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述常见的负载均衡算法", + "title": "简述什么是最左匹配原则", "frontmatter": { - "title": "简述常见的负载均衡算法", - "date": "2022-05-21T17:07:07.000Z", - "permalink": "/pages/f2dbbe/", + "title": "简述什么是最左匹配原则", + "date": "2022-05-21T17:06:10.000Z", + "permalink": "/pages/7a1f1e/", "categories": [ "数据库", "MYSQL相关" @@ -3493,10 +4125,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/080.%E7%AE%80%E8%BF%B0%E5%B8%B8%E8%A7%81%E7%9A%84%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1%E7%AE%97%E6%B3%95.html", - "relativePath": "03.数据库/01.MYSQL相关/080.简述常见的负载均衡算法.md", - "key": "v-53bcbb7a", - "path": "/pages/f2dbbe/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/050.%E7%AE%80%E8%BF%B0%E4%BB%80%E4%B9%88%E6%98%AF%E6%9C%80%E5%B7%A6%E5%8C%B9%E9%85%8D%E5%8E%9F%E5%88%99.html", + "relativePath": "03.数据库/01.MYSQL相关/050.简述什么是最左匹配原则.md", + "key": "v-4ecddbec", + "path": "/pages/7a1f1e/", "headersStr": null, "content": "", "normalizedContent": "", @@ -3529,31 +4161,6 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, - { - "title": "如何设计数据库压测方案?", - "frontmatter": { - "title": "如何设计数据库压测方案?", - "date": "2022-05-21T17:08:40.000Z", - "permalink": "/pages/a8eb47/", - "categories": [ - "数据库", - "MYSQL相关" - ], - "tags": [ - null - ] - }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/100.%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%8B%E6%B5%8B%E6%96%B9%E6%A1%88%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/100.如何设计数据库压测方案?.md", - "key": "v-4cef944a", - "path": "/pages/a8eb47/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, { "title": "简述数据库中什么情况下进行分库,什么情况下进行分表?", "frontmatter": { @@ -3656,11 +4263,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "MySQL 中 join 与 left join 的区别是什么?", + "title": "如何设计数据库压测方案?", "frontmatter": { - "title": "MySQL 中 join 与 left join 的区别是什么?", - "date": "2022-05-21T17:09:52.000Z", - "permalink": "/pages/429100/", + "title": "如何设计数据库压测方案?", + "date": "2022-05-21T17:08:40.000Z", + "permalink": "/pages/a8eb47/", "categories": [ "数据库", "MYSQL相关" @@ -3669,10 +4276,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/150.MySQL%20%E4%B8%AD%20join%20%E4%B8%8E%20left%20join%20%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/150.MySQL 中 join 与 left join 的区别是什么?.md", - "key": "v-77790024", - "path": "/pages/429100/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/100.%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%8B%E6%B5%8B%E6%96%B9%E6%A1%88%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/100.如何设计数据库压测方案?.md", + "key": "v-4cef944a", + "path": "/pages/a8eb47/", "headersStr": null, "content": "", "normalizedContent": "", @@ -3705,6 +4312,56 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, + { + "title": "MySQL 中 join 与 left join 的区别是什么?", + "frontmatter": { + "title": "MySQL 中 join 与 left join 的区别是什么?", + "date": "2022-05-21T17:09:52.000Z", + "permalink": "/pages/429100/", + "categories": [ + "数据库", + "MYSQL相关" + ], + "tags": [ + null + ] + }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/150.MySQL%20%E4%B8%AD%20join%20%E4%B8%8E%20left%20join%20%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/150.MySQL 中 join 与 left join 的区别是什么?.md", + "key": "v-77790024", + "path": "/pages/429100/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "MySQL 中 varchar 和 char 的区别是什么?", + "frontmatter": { + "title": "MySQL 中 varchar 和 char 的区别是什么?", + "date": "2022-05-21T17:10:21.000Z", + "permalink": "/pages/d97948/", + "categories": [ + "数据库", + "MYSQL相关" + ], + "tags": [ + null + ] + }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/170.MySQL%20%E4%B8%AD%20varchar%20%E5%92%8C%20char%20%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/170.MySQL 中 varchar 和 char 的区别是什么?.md", + "key": "v-0a7e6416", + "path": "/pages/d97948/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, { "title": "简述 MySQL MVCC 的实现原理", "frontmatter": { @@ -3818,31 +4475,6 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, - { - "title": "MySQL 中 varchar 和 char 的区别是什么?", - "frontmatter": { - "title": "MySQL 中 varchar 和 char 的区别是什么?", - "date": "2022-05-21T17:10:21.000Z", - "permalink": "/pages/d97948/", - "categories": [ - "数据库", - "MYSQL相关" - ], - "tags": [ - null - ] - }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/170.MySQL%20%E4%B8%AD%20varchar%20%E5%92%8C%20char%20%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/170.MySQL 中 varchar 和 char 的区别是什么?.md", - "key": "v-0a7e6416", - "path": "/pages/d97948/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, { "title": "模糊查询是如何实现的?", "frontmatter": { @@ -4082,31 +4714,6 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, - { - "title": "Redis 如何实现分布式锁?", - "frontmatter": { - "title": "Redis 如何实现分布式锁?", - "date": "2022-05-21T17:21:32.000Z", - "permalink": "/pages/656f46/", - "categories": [ - "数据库", - "REDIS相关" - ], - "tags": [ - null - ] - }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/030.Redis%20%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%EF%BC%9F.html", - "relativePath": "03.数据库/02.REDIS相关/030.Redis 如何实现分布式锁?.md", - "key": "v-2c6b511c", - "path": "/pages/656f46/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, { "title": "简述 Redis 集群配置以及基础原理", "frontmatter": { @@ -4144,12 +4751,37 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述 Redis 中跳表的应用以及优缺点", + "title": "Redis 如何实现分布式锁?", "frontmatter": { - "title": "简述 Redis 中跳表的应用以及优缺点", - "date": "2022-05-21T17:22:09.000Z", - "permalink": "/pages/a42826/", - "categories": [ + "title": "Redis 如何实现分布式锁?", + "date": "2022-05-21T17:21:32.000Z", + "permalink": "/pages/656f46/", + "categories": [ + "数据库", + "REDIS相关" + ], + "tags": [ + null + ] + }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/030.Redis%20%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%EF%BC%9F.html", + "relativePath": "03.数据库/02.REDIS相关/030.Redis 如何实现分布式锁?.md", + "key": "v-2c6b511c", + "path": "/pages/656f46/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "简述 Redis 中跳表的应用以及优缺点", + "frontmatter": { + "title": "简述 Redis 中跳表的应用以及优缺点", + "date": "2022-05-21T17:22:09.000Z", + "permalink": "/pages/a42826/", + "categories": [ "数据库", "REDIS相关" ], @@ -4194,11 +4826,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述 Redis 中如何防止缓存雪崩和缓存击穿", + "title": "简述 Redis 的线程模型以及底层架构设计", "frontmatter": { - "title": "简述 Redis 中如何防止缓存雪崩和缓存击穿", - "date": "2022-05-21T17:22:52.000Z", - "permalink": "/pages/03cd25/", + "title": "简述 Redis 的线程模型以及底层架构设计", + "date": "2022-05-21T17:23:08.000Z", + "permalink": "/pages/9c28c9/", "categories": [ "数据库", "REDIS相关" @@ -4207,10 +4839,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/070.%E7%AE%80%E8%BF%B0%20Redis%20%E4%B8%AD%E5%A6%82%E4%BD%95%E9%98%B2%E6%AD%A2%E7%BC%93%E5%AD%98%E9%9B%AA%E5%B4%A9%E5%92%8C%E7%BC%93%E5%AD%98%E5%87%BB%E7%A9%BF.html", - "relativePath": "03.数据库/02.REDIS相关/070.简述 Redis 中如何防止缓存雪崩和缓存击穿.md", - "key": "v-c2fe5e1a", - "path": "/pages/03cd25/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/080.%E7%AE%80%E8%BF%B0%20Redis%20%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B%E4%BB%A5%E5%8F%8A%E5%BA%95%E5%B1%82%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1.html", + "relativePath": "03.数据库/02.REDIS相关/080.简述 Redis 的线程模型以及底层架构设计.md", + "key": "v-35718f4e", + "path": "/pages/9c28c9/", "headersStr": null, "content": "", "normalizedContent": "", @@ -4219,11 +4851,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述 Redis 的线程模型以及底层架构设计", + "title": "简述 Redis 中如何防止缓存雪崩和缓存击穿", "frontmatter": { - "title": "简述 Redis 的线程模型以及底层架构设计", - "date": "2022-05-21T17:23:08.000Z", - "permalink": "/pages/9c28c9/", + "title": "简述 Redis 中如何防止缓存雪崩和缓存击穿", + "date": "2022-05-21T17:22:52.000Z", + "permalink": "/pages/03cd25/", "categories": [ "数据库", "REDIS相关" @@ -4232,10 +4864,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/080.%E7%AE%80%E8%BF%B0%20Redis%20%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B%E4%BB%A5%E5%8F%8A%E5%BA%95%E5%B1%82%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1.html", - "relativePath": "03.数据库/02.REDIS相关/080.简述 Redis 的线程模型以及底层架构设计.md", - "key": "v-35718f4e", - "path": "/pages/9c28c9/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/070.%E7%AE%80%E8%BF%B0%20Redis%20%E4%B8%AD%E5%A6%82%E4%BD%95%E9%98%B2%E6%AD%A2%E7%BC%93%E5%AD%98%E9%9B%AA%E5%B4%A9%E5%92%8C%E7%BC%93%E5%AD%98%E5%87%BB%E7%A9%BF.html", + "relativePath": "03.数据库/02.REDIS相关/070.简述 Redis 中如何防止缓存雪崩和缓存击穿.md", + "key": "v-c2fe5e1a", + "path": "/pages/03cd25/", "headersStr": null, "content": "", "normalizedContent": "", @@ -4304,31 +4936,6 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, - { - "title": "Redis 序列化有哪些方式?", - "frontmatter": { - "title": "Redis 序列化有哪些方式?", - "date": "2022-05-21T17:23:58.000Z", - "permalink": "/pages/0d3c6b/", - "categories": [ - "数据库", - "REDIS相关" - ], - "tags": [ - null - ] - }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/120.Redis%20%E5%BA%8F%E5%88%97%E5%8C%96%E6%9C%89%E5%93%AA%E4%BA%9B%E6%96%B9%E5%BC%8F%EF%BC%9F.html", - "relativePath": "03.数据库/02.REDIS相关/120.Redis 序列化有哪些方式?.md", - "key": "v-02ea33c3", - "path": "/pages/0d3c6b/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, { "title": "Netty", "frontmatter": { @@ -4436,45 +5043,27 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "Netty中有哪些解码器?", + "title": "Redis 序列化有哪些方式?", "frontmatter": { - "title": "Netty中有哪些解码器?", - "date": "2022-05-21T23:33:34.000Z", - "permalink": "/pages/59089d/", + "title": "Redis 序列化有哪些方式?", + "date": "2022-05-21T17:23:58.000Z", + "permalink": "/pages/0d3c6b/", "categories": [ - "中间件", - "Netty" + "数据库", + "REDIS相关" ], "tags": [ null ] }, - "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/10.Netty/015.Netty%E4%B8%AD%E6%9C%89%E5%93%AA%E4%BA%9B%E8%A7%A3%E7%A0%81%E5%99%A8%EF%BC%9F.html", - "relativePath": "04.中间件/10.Netty/015.Netty中有哪些解码器?.md", - "key": "v-073f22cd", - "path": "/pages/59089d/", - "headers": [ - { - "level": 2, - "title": "5.1 DelimiterBasedFrameDecoder", - "slug": "_5-1-delimiterbasedframedecoder", - "normalizedTitle": "5.1 delimiterbasedframedecoder", - "charIndex": 2 - }, - { - "level": 2, - "title": "5.2 FixedLengthFrameDecoder", - "slug": "_5-2-fixedlengthframedecoder", - "normalizedTitle": "5.2 fixedlengthframedecoder", - "charIndex": 83 - } - ], - "headersStr": "5.1 DelimiterBasedFrameDecoder 5.2 FixedLengthFrameDecoder", - "content": "# 5.1 DelimiterBasedFrameDecoder\n\n支持任意字符为分隔符 支持设置单条消息最大长度,如果找了最大长度还没找到分隔符就抛出异常\n\n\n# 5.2 FixedLengthFrameDecoder\n\n使用简单,指定包长渡就ok。", - "normalizedContent": "# 5.1 delimiterbasedframedecoder\n\n支持任意字符为分隔符 支持设置单条消息最大长度,如果找了最大长度还没找到分隔符就抛出异常\n\n\n# 5.2 fixedlengthframedecoder\n\n使用简单,指定包长渡就ok。", - "charsets": { - "cjk": true - }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/120.Redis%20%E5%BA%8F%E5%88%97%E5%8C%96%E6%9C%89%E5%93%AA%E4%BA%9B%E6%96%B9%E5%BC%8F%EF%BC%9F.html", + "relativePath": "03.数据库/02.REDIS相关/120.Redis 序列化有哪些方式?.md", + "key": "v-02ea33c3", + "path": "/pages/0d3c6b/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, @@ -4542,6 +5131,49 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, + { + "title": "Netty中有哪些解码器?", + "frontmatter": { + "title": "Netty中有哪些解码器?", + "date": "2022-05-21T23:33:34.000Z", + "permalink": "/pages/59089d/", + "categories": [ + "中间件", + "Netty" + ], + "tags": [ + null + ] + }, + "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/10.Netty/015.Netty%E4%B8%AD%E6%9C%89%E5%93%AA%E4%BA%9B%E8%A7%A3%E7%A0%81%E5%99%A8%EF%BC%9F.html", + "relativePath": "04.中间件/10.Netty/015.Netty中有哪些解码器?.md", + "key": "v-073f22cd", + "path": "/pages/59089d/", + "headers": [ + { + "level": 2, + "title": "5.1 DelimiterBasedFrameDecoder", + "slug": "_5-1-delimiterbasedframedecoder", + "normalizedTitle": "5.1 delimiterbasedframedecoder", + "charIndex": 2 + }, + { + "level": 2, + "title": "5.2 FixedLengthFrameDecoder", + "slug": "_5-2-fixedlengthframedecoder", + "normalizedTitle": "5.2 fixedlengthframedecoder", + "charIndex": 83 + } + ], + "headersStr": "5.1 DelimiterBasedFrameDecoder 5.2 FixedLengthFrameDecoder", + "content": "# 5.1 DelimiterBasedFrameDecoder\n\n支持任意字符为分隔符 支持设置单条消息最大长度,如果找了最大长度还没找到分隔符就抛出异常\n\n\n# 5.2 FixedLengthFrameDecoder\n\n使用简单,指定包长渡就ok。", + "normalizedContent": "# 5.1 delimiterbasedframedecoder\n\n支持任意字符为分隔符 支持设置单条消息最大长度,如果找了最大长度还没找到分隔符就抛出异常\n\n\n# 5.2 fixedlengthframedecoder\n\n使用简单,指定包长渡就ok。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, { "title": "为什么Netty要自己写一个ByteBuf", "frontmatter": { @@ -4981,11 +5613,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "Kafka", + "title": "生产者", "frontmatter": { - "title": "Kafka", - "date": "2020-05-21T21:50:26.000Z", - "permalink": "/pages/561f34/", + "title": "生产者", + "date": "2022-05-21T23:43:06.000Z", + "permalink": "/pages/fe7ca2/", "categories": [ "中间件", "Kafka" @@ -4994,13 +5626,13 @@ export const siteData = { null ] }, - "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/40.Kafka/001.Kafka%E5%8C%85%E5%90%AB%E9%82%A3%E5%87%A0%E9%83%A8%E5%88%86.html", - "relativePath": "04.中间件/40.Kafka/001.Kafka包含那几部分.md", - "key": "v-0df39df8", - "path": "/pages/561f34/", + "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/40.Kafka/010.%E7%94%9F%E4%BA%A7%E8%80%85.html", + "relativePath": "04.中间件/40.Kafka/010.生产者.md", + "key": "v-7c0136cf", + "path": "/pages/fe7ca2/", "headersStr": null, - "content": "# 主题和分区\n\nkafka通过主题对消息分类。 kafka通过分区实现数据冗余和伸缩性。 一个主题包含多个分区,所以在topic内消息可能是无需的,但是在单个partition内部是有序的。\n\n\n# 生产者和消费者\n\n生产者创建消息,发布到一个特定的主题上,消息会均衡在所有分区上。 消费者订阅一个或者多个主题,按照生产顺序消费消息。 偏移量是一种元数据,是一个不断递增的整数值,消费者通过偏移量来区分已经消费的消息。每个分区有唯一的偏移量,消费者把最后消费的消息的偏移量发送到broker或者zookeeper保存。 消费者群组多个消费者共同读取一个主题,有唯一的groupId,群组保证,每个分区只能被一个消费者使用。\n\n\n# broker和集群\n\nbroker是一个独立的kafka 服务器。负责接收生产者的消息,为消息设置偏移量,提交消息到磁盘保存。是集群的组成部分。 集群控制器是集群中的一个broker。负责分区分配和broker监控。 分区首领。一个分区会分配给多个broker,这种复制机制是为了提供消息冗余。在这多个broker中需要有一个分区首领,消费者只消费分区首领的消息,分区首领负责把消费的offset同步到冗余分区中。", - "normalizedContent": "# 主题和分区\n\nkafka通过主题对消息分类。 kafka通过分区实现数据冗余和伸缩性。 一个主题包含多个分区,所以在topic内消息可能是无需的,但是在单个partition内部是有序的。\n\n\n# 生产者和消费者\n\n生产者创建消息,发布到一个特定的主题上,消息会均衡在所有分区上。 消费者订阅一个或者多个主题,按照生产顺序消费消息。 偏移量是一种元数据,是一个不断递增的整数值,消费者通过偏移量来区分已经消费的消息。每个分区有唯一的偏移量,消费者把最后消费的消息的偏移量发送到broker或者zookeeper保存。 消费者群组多个消费者共同读取一个主题,有唯一的groupid,群组保证,每个分区只能被一个消费者使用。\n\n\n# broker和集群\n\nbroker是一个独立的kafka 服务器。负责接收生产者的消息,为消息设置偏移量,提交消息到磁盘保存。是集群的组成部分。 集群控制器是集群中的一个broker。负责分区分配和broker监控。 分区首领。一个分区会分配给多个broker,这种复制机制是为了提供消息冗余。在这多个broker中需要有一个分区首领,消费者只消费分区首领的消息,分区首领负责把消费的offset同步到冗余分区中。", + "content": "# 创建生产者\n\n创建生产者有三个属性是必选的 bootstrap.servers 指定broker集群的地址,格式为host:port key.serializer kafka broker希望收到消息的键和值都是字节数组,但是为了代码的可读性,允许producer把java对象发送给broker,但是需要指定一个实现了org.apache.kafka.common.serialization.Serializer接口的序列化器。 value.serializer 与key.serializer一样的道理。\n\n\t\tProperties props = new Properties();\n props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, \"172.16.1.1,172.16.1.2\");\n props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, \"org.apache.kafka.common.serialization.StringSerializer\");\n props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, \"org.apache.kafka.common.serialization.StringSerializer\");\n producer = new KafkaProducer<>(props);\n\n\n1\n2\n3\n4\n5\n\n\n\n# 发送消息\n\n发送消息有三种方式:\n\n * 发送并忘记(fire-and-forget)\n * 同步发送\n * 异步发送\n\n发送并忘记 不关心消息是否到达\n\nProducerRecord record = new ProducerRecord<>(topicName, key,value);\nproducer.send(record);\n\n\n1\n2\n\n\n同步发送\n\nProducerRecord record = new ProducerRecord<>(topicName, key,value);\ntry{\n\tproducer.send(record).get();//producer.send(record)返回一个Future对象,调用get()阻塞等待返回结果\n} catch (Exception e){\n\te.printStackTrace();\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\nkafkaProducer会发送两类错误\n\n * 可重试的:比如连接错误、无主(no leader)错误\n * 不可重试的:比如消息太大 异步发送 异步发送的异常需要记录下拉,producer支持回调。\n\nProducerRecord record = new ProducerRecord<>(topicNameTrade, key,value);\n producer.send(record, new Callback() {\n @Override\n public void onCompletion(RecordMetadata metadata, Exception exception) {\n exception.printStackTrace();\n }\n });\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 生产者的配置\n\nacks 指定要多少个分区副本收到消息,生产者才认为消息写入是成功的。其值有0、1、all.\n\n * 0:不等待任何服务器的相应\n * 1: 只要集群的首领节点收到\n * all: 等待所有副本都收到\n\nbuffer.memory 设置生产者内存缓冲区大小。 程序发送消息的速度 > 发送到broker的速度会导致这个缓冲区空间不足。 空间不足时程序阻塞还是抛异常取决于配置max.block.ms compression.type 默认,消息是不会被压缩的。 producer向broker发送消息不是逐条发送的,是一批一批发送的。 发送批次有可选的压缩方式,有三种:\n\n * snappy\n * gzip\n * lz4\n\nretries 重试次数 默认重试之间会等待100ms retry.backoff.ms可设置这个等待时间 batch.size 有多个消息要发送到一个分区的时候,producer会把他们放到一个批次。 该配置指定是批次大小(字节)。 批次装满的时候会被发送出去,\n\n\n# 序列化器\n\n\n# 分区\n\nkafka消息的recode包含topic、key、value。 broker用key分配分区。key相同的一定会分配到同一个分区。 如果key为null,那么将使用默认分区,默认的分区器是使用Round Robin算法。 也可以实现partitioner实现自定义分区器。", + "normalizedContent": "# 创建生产者\n\n创建生产者有三个属性是必选的 bootstrap.servers 指定broker集群的地址,格式为host:port key.serializer kafka broker希望收到消息的键和值都是字节数组,但是为了代码的可读性,允许producer把java对象发送给broker,但是需要指定一个实现了org.apache.kafka.common.serialization.serializer接口的序列化器。 value.serializer 与key.serializer一样的道理。\n\n\t\tproperties props = new properties();\n props.put(producerconfig.bootstrap_servers_config, \"172.16.1.1,172.16.1.2\");\n props.put(producerconfig.key_serializer_class_config, \"org.apache.kafka.common.serialization.stringserializer\");\n props.put(producerconfig.value_serializer_class_config, \"org.apache.kafka.common.serialization.stringserializer\");\n producer = new kafkaproducer<>(props);\n\n\n1\n2\n3\n4\n5\n\n\n\n# 发送消息\n\n发送消息有三种方式:\n\n * 发送并忘记(fire-and-forget)\n * 同步发送\n * 异步发送\n\n发送并忘记 不关心消息是否到达\n\nproducerrecord record = new producerrecord<>(topicname, key,value);\nproducer.send(record);\n\n\n1\n2\n\n\n同步发送\n\nproducerrecord record = new producerrecord<>(topicname, key,value);\ntry{\n\tproducer.send(record).get();//producer.send(record)返回一个future对象,调用get()阻塞等待返回结果\n} catch (exception e){\n\te.printstacktrace();\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\nkafkaproducer会发送两类错误\n\n * 可重试的:比如连接错误、无主(no leader)错误\n * 不可重试的:比如消息太大 异步发送 异步发送的异常需要记录下拉,producer支持回调。\n\nproducerrecord record = new producerrecord<>(topicnametrade, key,value);\n producer.send(record, new callback() {\n @override\n public void oncompletion(recordmetadata metadata, exception exception) {\n exception.printstacktrace();\n }\n });\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 生产者的配置\n\nacks 指定要多少个分区副本收到消息,生产者才认为消息写入是成功的。其值有0、1、all.\n\n * 0:不等待任何服务器的相应\n * 1: 只要集群的首领节点收到\n * all: 等待所有副本都收到\n\nbuffer.memory 设置生产者内存缓冲区大小。 程序发送消息的速度 > 发送到broker的速度会导致这个缓冲区空间不足。 空间不足时程序阻塞还是抛异常取决于配置max.block.ms compression.type 默认,消息是不会被压缩的。 producer向broker发送消息不是逐条发送的,是一批一批发送的。 发送批次有可选的压缩方式,有三种:\n\n * snappy\n * gzip\n * lz4\n\nretries 重试次数 默认重试之间会等待100ms retry.backoff.ms可设置这个等待时间 batch.size 有多个消息要发送到一个分区的时候,producer会把他们放到一个批次。 该配置指定是批次大小(字节)。 批次装满的时候会被发送出去,\n\n\n# 序列化器\n\n\n# 分区\n\nkafka消息的recode包含topic、key、value。 broker用key分配分区。key相同的一定会分配到同一个分区。 如果key为null,那么将使用默认分区,默认的分区器是使用round robin算法。 也可以实现partitioner实现自定义分区器。", "charsets": { "cjk": true }, @@ -5008,11 +5640,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "生产者", + "title": "Kafka", "frontmatter": { - "title": "生产者", - "date": "2022-05-21T23:43:06.000Z", - "permalink": "/pages/fe7ca2/", + "title": "Kafka", + "date": "2020-05-21T21:50:26.000Z", + "permalink": "/pages/561f34/", "categories": [ "中间件", "Kafka" @@ -5021,13 +5653,13 @@ export const siteData = { null ] }, - "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/40.Kafka/010.%E7%94%9F%E4%BA%A7%E8%80%85.html", - "relativePath": "04.中间件/40.Kafka/010.生产者.md", - "key": "v-7c0136cf", - "path": "/pages/fe7ca2/", + "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/40.Kafka/001.Kafka%E5%8C%85%E5%90%AB%E9%82%A3%E5%87%A0%E9%83%A8%E5%88%86.html", + "relativePath": "04.中间件/40.Kafka/001.Kafka包含那几部分.md", + "key": "v-0df39df8", + "path": "/pages/561f34/", "headersStr": null, - "content": "# 创建生产者\n\n创建生产者有三个属性是必选的 bootstrap.servers 指定broker集群的地址,格式为host:port key.serializer kafka broker希望收到消息的键和值都是字节数组,但是为了代码的可读性,允许producer把java对象发送给broker,但是需要指定一个实现了org.apache.kafka.common.serialization.Serializer接口的序列化器。 value.serializer 与key.serializer一样的道理。\n\n\t\tProperties props = new Properties();\n props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, \"172.16.1.1,172.16.1.2\");\n props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, \"org.apache.kafka.common.serialization.StringSerializer\");\n props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, \"org.apache.kafka.common.serialization.StringSerializer\");\n producer = new KafkaProducer<>(props);\n\n\n1\n2\n3\n4\n5\n\n\n\n# 发送消息\n\n发送消息有三种方式:\n\n * 发送并忘记(fire-and-forget)\n * 同步发送\n * 异步发送\n\n发送并忘记 不关心消息是否到达\n\nProducerRecord record = new ProducerRecord<>(topicName, key,value);\nproducer.send(record);\n\n\n1\n2\n\n\n同步发送\n\nProducerRecord record = new ProducerRecord<>(topicName, key,value);\ntry{\n\tproducer.send(record).get();//producer.send(record)返回一个Future对象,调用get()阻塞等待返回结果\n} catch (Exception e){\n\te.printStackTrace();\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\nkafkaProducer会发送两类错误\n\n * 可重试的:比如连接错误、无主(no leader)错误\n * 不可重试的:比如消息太大 异步发送 异步发送的异常需要记录下拉,producer支持回调。\n\nProducerRecord record = new ProducerRecord<>(topicNameTrade, key,value);\n producer.send(record, new Callback() {\n @Override\n public void onCompletion(RecordMetadata metadata, Exception exception) {\n exception.printStackTrace();\n }\n });\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 生产者的配置\n\nacks 指定要多少个分区副本收到消息,生产者才认为消息写入是成功的。其值有0、1、all.\n\n * 0:不等待任何服务器的相应\n * 1: 只要集群的首领节点收到\n * all: 等待所有副本都收到\n\nbuffer.memory 设置生产者内存缓冲区大小。 程序发送消息的速度 > 发送到broker的速度会导致这个缓冲区空间不足。 空间不足时程序阻塞还是抛异常取决于配置max.block.ms compression.type 默认,消息是不会被压缩的。 producer向broker发送消息不是逐条发送的,是一批一批发送的。 发送批次有可选的压缩方式,有三种:\n\n * snappy\n * gzip\n * lz4\n\nretries 重试次数 默认重试之间会等待100ms retry.backoff.ms可设置这个等待时间 batch.size 有多个消息要发送到一个分区的时候,producer会把他们放到一个批次。 该配置指定是批次大小(字节)。 批次装满的时候会被发送出去,\n\n\n# 序列化器\n\n\n# 分区\n\nkafka消息的recode包含topic、key、value。 broker用key分配分区。key相同的一定会分配到同一个分区。 如果key为null,那么将使用默认分区,默认的分区器是使用Round Robin算法。 也可以实现partitioner实现自定义分区器。", - "normalizedContent": "# 创建生产者\n\n创建生产者有三个属性是必选的 bootstrap.servers 指定broker集群的地址,格式为host:port key.serializer kafka broker希望收到消息的键和值都是字节数组,但是为了代码的可读性,允许producer把java对象发送给broker,但是需要指定一个实现了org.apache.kafka.common.serialization.serializer接口的序列化器。 value.serializer 与key.serializer一样的道理。\n\n\t\tproperties props = new properties();\n props.put(producerconfig.bootstrap_servers_config, \"172.16.1.1,172.16.1.2\");\n props.put(producerconfig.key_serializer_class_config, \"org.apache.kafka.common.serialization.stringserializer\");\n props.put(producerconfig.value_serializer_class_config, \"org.apache.kafka.common.serialization.stringserializer\");\n producer = new kafkaproducer<>(props);\n\n\n1\n2\n3\n4\n5\n\n\n\n# 发送消息\n\n发送消息有三种方式:\n\n * 发送并忘记(fire-and-forget)\n * 同步发送\n * 异步发送\n\n发送并忘记 不关心消息是否到达\n\nproducerrecord record = new producerrecord<>(topicname, key,value);\nproducer.send(record);\n\n\n1\n2\n\n\n同步发送\n\nproducerrecord record = new producerrecord<>(topicname, key,value);\ntry{\n\tproducer.send(record).get();//producer.send(record)返回一个future对象,调用get()阻塞等待返回结果\n} catch (exception e){\n\te.printstacktrace();\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\nkafkaproducer会发送两类错误\n\n * 可重试的:比如连接错误、无主(no leader)错误\n * 不可重试的:比如消息太大 异步发送 异步发送的异常需要记录下拉,producer支持回调。\n\nproducerrecord record = new producerrecord<>(topicnametrade, key,value);\n producer.send(record, new callback() {\n @override\n public void oncompletion(recordmetadata metadata, exception exception) {\n exception.printstacktrace();\n }\n });\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 生产者的配置\n\nacks 指定要多少个分区副本收到消息,生产者才认为消息写入是成功的。其值有0、1、all.\n\n * 0:不等待任何服务器的相应\n * 1: 只要集群的首领节点收到\n * all: 等待所有副本都收到\n\nbuffer.memory 设置生产者内存缓冲区大小。 程序发送消息的速度 > 发送到broker的速度会导致这个缓冲区空间不足。 空间不足时程序阻塞还是抛异常取决于配置max.block.ms compression.type 默认,消息是不会被压缩的。 producer向broker发送消息不是逐条发送的,是一批一批发送的。 发送批次有可选的压缩方式,有三种:\n\n * snappy\n * gzip\n * lz4\n\nretries 重试次数 默认重试之间会等待100ms retry.backoff.ms可设置这个等待时间 batch.size 有多个消息要发送到一个分区的时候,producer会把他们放到一个批次。 该配置指定是批次大小(字节)。 批次装满的时候会被发送出去,\n\n\n# 序列化器\n\n\n# 分区\n\nkafka消息的recode包含topic、key、value。 broker用key分配分区。key相同的一定会分配到同一个分区。 如果key为null,那么将使用默认分区,默认的分区器是使用round robin算法。 也可以实现partitioner实现自定义分区器。", + "content": "# 主题和分区\n\nkafka通过主题对消息分类。 kafka通过分区实现数据冗余和伸缩性。 一个主题包含多个分区,所以在topic内消息可能是无需的,但是在单个partition内部是有序的。\n\n\n# 生产者和消费者\n\n生产者创建消息,发布到一个特定的主题上,消息会均衡在所有分区上。 消费者订阅一个或者多个主题,按照生产顺序消费消息。 偏移量是一种元数据,是一个不断递增的整数值,消费者通过偏移量来区分已经消费的消息。每个分区有唯一的偏移量,消费者把最后消费的消息的偏移量发送到broker或者zookeeper保存。 消费者群组多个消费者共同读取一个主题,有唯一的groupId,群组保证,每个分区只能被一个消费者使用。\n\n\n# broker和集群\n\nbroker是一个独立的kafka 服务器。负责接收生产者的消息,为消息设置偏移量,提交消息到磁盘保存。是集群的组成部分。 集群控制器是集群中的一个broker。负责分区分配和broker监控。 分区首领。一个分区会分配给多个broker,这种复制机制是为了提供消息冗余。在这多个broker中需要有一个分区首领,消费者只消费分区首领的消息,分区首领负责把消费的offset同步到冗余分区中。", + "normalizedContent": "# 主题和分区\n\nkafka通过主题对消息分类。 kafka通过分区实现数据冗余和伸缩性。 一个主题包含多个分区,所以在topic内消息可能是无需的,但是在单个partition内部是有序的。\n\n\n# 生产者和消费者\n\n生产者创建消息,发布到一个特定的主题上,消息会均衡在所有分区上。 消费者订阅一个或者多个主题,按照生产顺序消费消息。 偏移量是一种元数据,是一个不断递增的整数值,消费者通过偏移量来区分已经消费的消息。每个分区有唯一的偏移量,消费者把最后消费的消息的偏移量发送到broker或者zookeeper保存。 消费者群组多个消费者共同读取一个主题,有唯一的groupid,群组保证,每个分区只能被一个消费者使用。\n\n\n# broker和集群\n\nbroker是一个独立的kafka 服务器。负责接收生产者的消息,为消息设置偏移量,提交消息到磁盘保存。是集群的组成部分。 集群控制器是集群中的一个broker。负责分区分配和broker监控。 分区首领。一个分区会分配给多个broker,这种复制机制是为了提供消息冗余。在这多个broker中需要有一个分区首领,消费者只消费分区首领的消息,分区首领负责把消费的offset同步到冗余分区中。", "charsets": { "cjk": true }, @@ -5285,32 +5917,6 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, - { - "title": "简述什么是两阶段提交?", - "frontmatter": { - "title": "简述什么是两阶段提交?", - "date": "2022-05-21T17:12:23.000Z", - "permalink": "/pages/4eae5b/", - "categories": [ - "系统设计" - ], - "tags": [ - null - ] - }, - "regularPath": "/08.%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/020.%E7%AE%80%E8%BF%B0%E4%BB%80%E4%B9%88%E6%98%AF%E4%B8%A4%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4%EF%BC%9F.html", - "relativePath": "08.系统设计/020.简述什么是两阶段提交?.md", - "key": "v-f88544f4", - "path": "/pages/4eae5b/", - "headersStr": null, - "content": "TCC/Try Confirm Cancel模式TCC模式只有在所有的服务的第一阶段(try)都成功的时候才进行第二阶段确认(Confirm)操作,否则进行补偿(Cancel)操作,而在try阶段是不会进行真正的业务处理的。\n\n\nTCC模式TCC模式的具体流程为两个阶段:Try,业务服务完成所有的业务检查,预留必需的业务资源如果Try在所有服务中都成功,那么执行Confirm操作,Confirm操作不做任何的业务检查(因为try中已经做过),只是用Try阶段预留的业务资源进行业务处理;否则进行Cancel操作,Cancel操作释放Try阶段预留的业务资源。\n\n\n这么说可能比较模糊,下面我举一个具体的例子,小明在线从招商银行转账100元到广发银行。\n\n\n这个操作可看作两个服务,服务a从小明的招行账户转出100元,服务b从小明的广发银行帐户汇入100元。\n\n\n服务a(小明从招行转出100元):try: update cmb_account set balance=balance-100, freeze=freeze+100 where\nacc_id=1 and balance>100;\n\nconfirm: update cmb_account set freeze=freeze-100 where acc_id=1;\n\ncancel: update cmb_account set balance=balance+100, freeze=freeze-100 where\nacc_id=1;\n\n\n服务b(小明往广发银行汇入100元):try: update cgb_account set freeze=freeze+100 where acc_id=1;\n\nconfirm: update cgb_account set balance=balance+100, freeze=freeze-100 where acc_id=1;\n\ncancel: update cgb_account set freeze=freeze-100 where acc_id=1;\n\n\n具体说明:a的try阶段,服务做了两件事,1:业务检查,这里是检查小明的帐户里的钱是否多余100元;2:预留资源,将100元从余额中划入冻结资金。\na的confirm阶段,这里不再进行业务检查,因为try阶段已经做过了,同时由于转账已经成功,将冻结资金扣除。a的cancel阶段,释放预留资源,既100元冻结资金,并恢复到余额。\n\n\nb的try阶段进行,预留资源,将100元冻结。b的confirm阶段,使用try阶段预留的资源,将100元冻结资金划入余额。\nb的cancel阶段,释放try阶段的预留资源,将100元从冻结资金中减去。从上面的简单例子可以看出,TCC模式比纯业务补偿模式更加复杂,所以在实现上每个服务都需要实现Cofirm和Cancel两个接口。", - "normalizedContent": "tcc/try confirm cancel模式tcc模式只有在所有的服务的第一阶段(try)都成功的时候才进行第二阶段确认(confirm)操作,否则进行补偿(cancel)操作,而在try阶段是不会进行真正的业务处理的。\n\n\ntcc模式tcc模式的具体流程为两个阶段:try,业务服务完成所有的业务检查,预留必需的业务资源如果try在所有服务中都成功,那么执行confirm操作,confirm操作不做任何的业务检查(因为try中已经做过),只是用try阶段预留的业务资源进行业务处理;否则进行cancel操作,cancel操作释放try阶段预留的业务资源。\n\n\n这么说可能比较模糊,下面我举一个具体的例子,小明在线从招商银行转账100元到广发银行。\n\n\n这个操作可看作两个服务,服务a从小明的招行账户转出100元,服务b从小明的广发银行帐户汇入100元。\n\n\n服务a(小明从招行转出100元):try: update cmb_account set balance=balance-100, freeze=freeze+100 where\nacc_id=1 and balance>100;\n\nconfirm: update cmb_account set freeze=freeze-100 where acc_id=1;\n\ncancel: update cmb_account set balance=balance+100, freeze=freeze-100 where\nacc_id=1;\n\n\n服务b(小明往广发银行汇入100元):try: update cgb_account set freeze=freeze+100 where acc_id=1;\n\nconfirm: update cgb_account set balance=balance+100, freeze=freeze-100 where acc_id=1;\n\ncancel: update cgb_account set freeze=freeze-100 where acc_id=1;\n\n\n具体说明:a的try阶段,服务做了两件事,1:业务检查,这里是检查小明的帐户里的钱是否多余100元;2:预留资源,将100元从余额中划入冻结资金。\na的confirm阶段,这里不再进行业务检查,因为try阶段已经做过了,同时由于转账已经成功,将冻结资金扣除。a的cancel阶段,释放预留资源,既100元冻结资金,并恢复到余额。\n\n\nb的try阶段进行,预留资源,将100元冻结。b的confirm阶段,使用try阶段预留的资源,将100元冻结资金划入余额。\nb的cancel阶段,释放try阶段的预留资源,将100元从冻结资金中减去。从上面的简单例子可以看出,tcc模式比纯业务补偿模式更加复杂,所以在实现上每个服务都需要实现cofirm和cancel两个接口。", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, { "title": "什么是 CAP ?什么是最终一致性?什么是幂等操作?", "frontmatter": { @@ -5389,23 +5995,115 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "非技术问题", + "title": "简述什么是两阶段提交?", "frontmatter": { - "title": "非技术问题", - "date": "2022-03-27T13:53:27.000Z", - "permalink": "/pages/5e507a/", + "title": "简述什么是两阶段提交?", + "date": "2022-05-21T17:12:23.000Z", + "permalink": "/pages/4eae5b/", "categories": [ - "非技术" + "系统设计" ], "tags": [ null ] }, - "regularPath": "/10.%E9%9D%9E%E6%8A%80%E6%9C%AF/01.%E9%9D%9E%E6%8A%80%E6%9C%AF%E9%97%AE%E9%A2%98.html", - "relativePath": "10.非技术/01.非技术问题.md", - "key": "v-2a52a762", - "path": "/pages/5e507a/", - "headers": [ + "regularPath": "/08.%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/020.%E7%AE%80%E8%BF%B0%E4%BB%80%E4%B9%88%E6%98%AF%E4%B8%A4%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4%EF%BC%9F.html", + "relativePath": "08.系统设计/020.简述什么是两阶段提交?.md", + "key": "v-f88544f4", + "path": "/pages/4eae5b/", + "headersStr": null, + "content": "TCC/Try Confirm Cancel模式TCC模式只有在所有的服务的第一阶段(try)都成功的时候才进行第二阶段确认(Confirm)操作,否则进行补偿(Cancel)操作,而在try阶段是不会进行真正的业务处理的。\n\n\nTCC模式TCC模式的具体流程为两个阶段:Try,业务服务完成所有的业务检查,预留必需的业务资源如果Try在所有服务中都成功,那么执行Confirm操作,Confirm操作不做任何的业务检查(因为try中已经做过),只是用Try阶段预留的业务资源进行业务处理;否则进行Cancel操作,Cancel操作释放Try阶段预留的业务资源。\n\n\n这么说可能比较模糊,下面我举一个具体的例子,小明在线从招商银行转账100元到广发银行。\n\n\n这个操作可看作两个服务,服务a从小明的招行账户转出100元,服务b从小明的广发银行帐户汇入100元。\n\n\n服务a(小明从招行转出100元):try: update cmb_account set balance=balance-100, freeze=freeze+100 where\nacc_id=1 and balance>100;\n\nconfirm: update cmb_account set freeze=freeze-100 where acc_id=1;\n\ncancel: update cmb_account set balance=balance+100, freeze=freeze-100 where\nacc_id=1;\n\n\n服务b(小明往广发银行汇入100元):try: update cgb_account set freeze=freeze+100 where acc_id=1;\n\nconfirm: update cgb_account set balance=balance+100, freeze=freeze-100 where acc_id=1;\n\ncancel: update cgb_account set freeze=freeze-100 where acc_id=1;\n\n\n具体说明:a的try阶段,服务做了两件事,1:业务检查,这里是检查小明的帐户里的钱是否多余100元;2:预留资源,将100元从余额中划入冻结资金。\na的confirm阶段,这里不再进行业务检查,因为try阶段已经做过了,同时由于转账已经成功,将冻结资金扣除。a的cancel阶段,释放预留资源,既100元冻结资金,并恢复到余额。\n\n\nb的try阶段进行,预留资源,将100元冻结。b的confirm阶段,使用try阶段预留的资源,将100元冻结资金划入余额。\nb的cancel阶段,释放try阶段的预留资源,将100元从冻结资金中减去。从上面的简单例子可以看出,TCC模式比纯业务补偿模式更加复杂,所以在实现上每个服务都需要实现Cofirm和Cancel两个接口。", + "normalizedContent": "tcc/try confirm cancel模式tcc模式只有在所有的服务的第一阶段(try)都成功的时候才进行第二阶段确认(confirm)操作,否则进行补偿(cancel)操作,而在try阶段是不会进行真正的业务处理的。\n\n\ntcc模式tcc模式的具体流程为两个阶段:try,业务服务完成所有的业务检查,预留必需的业务资源如果try在所有服务中都成功,那么执行confirm操作,confirm操作不做任何的业务检查(因为try中已经做过),只是用try阶段预留的业务资源进行业务处理;否则进行cancel操作,cancel操作释放try阶段预留的业务资源。\n\n\n这么说可能比较模糊,下面我举一个具体的例子,小明在线从招商银行转账100元到广发银行。\n\n\n这个操作可看作两个服务,服务a从小明的招行账户转出100元,服务b从小明的广发银行帐户汇入100元。\n\n\n服务a(小明从招行转出100元):try: update cmb_account set balance=balance-100, freeze=freeze+100 where\nacc_id=1 and balance>100;\n\nconfirm: update cmb_account set freeze=freeze-100 where acc_id=1;\n\ncancel: update cmb_account set balance=balance+100, freeze=freeze-100 where\nacc_id=1;\n\n\n服务b(小明往广发银行汇入100元):try: update cgb_account set freeze=freeze+100 where acc_id=1;\n\nconfirm: update cgb_account set balance=balance+100, freeze=freeze-100 where acc_id=1;\n\ncancel: update cgb_account set freeze=freeze-100 where acc_id=1;\n\n\n具体说明:a的try阶段,服务做了两件事,1:业务检查,这里是检查小明的帐户里的钱是否多余100元;2:预留资源,将100元从余额中划入冻结资金。\na的confirm阶段,这里不再进行业务检查,因为try阶段已经做过了,同时由于转账已经成功,将冻结资金扣除。a的cancel阶段,释放预留资源,既100元冻结资金,并恢复到余额。\n\n\nb的try阶段进行,预留资源,将100元冻结。b的confirm阶段,使用try阶段预留的资源,将100元冻结资金划入余额。\nb的cancel阶段,释放try阶段的预留资源,将100元从冻结资金中减去。从上面的简单例子可以看出,tcc模式比纯业务补偿模式更加复杂,所以在实现上每个服务都需要实现cofirm和cancel两个接口。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "关于", + "frontmatter": { + "title": "关于", + "date": "2019-12-25T14:27:01.000Z", + "permalink": "/about", + "sidebar": false, + "article": false + }, + "regularPath": "/15.%E5%85%B3%E4%BA%8E/01.%E5%85%B3%E4%BA%8E.html", + "relativePath": "15.关于/01.关于.md", + "key": "v-918dfaf8", + "path": "/about/", + "headers": [ + { + "level": 2, + "title": "📚Blog", + "slug": "📚blog", + "normalizedTitle": "📚blog", + "charIndex": 2 + }, + { + "level": 2, + "title": "⚡ 反馈与交流", + "slug": "⚡-反馈与交流", + "normalizedTitle": "⚡ 反馈与交流", + "charIndex": 27 + }, + { + "level": 2, + "title": "🐼Me", + "slug": "🐼me", + "normalizedTitle": "🐼me", + "charIndex": 156 + }, + { + "level": 3, + "title": "技能", + "slug": "技能", + "normalizedTitle": "技能", + "charIndex": 177 + }, + { + "level": 2, + "title": "Java学习", + "slug": "java学习", + "normalizedTitle": "java学习", + "charIndex": 14 + }, + { + "level": 2, + "title": "✉️ 联系", + "slug": "联系", + "normalizedTitle": "✉️ 联系", + "charIndex": 345 + } + ], + "headersStr": "📚Blog ⚡ 反馈与交流 🐼Me 技能 Java学习 ✉️ 联系", + "content": "# 📚Blog\n\n这是一个Java学习网站\n\n\n# ⚡ 反馈与交流\n\n文章内容仅是我个人的小总结,资历尚浅,如有误还请指正。\n\n在使用过程中有任何问题和想法,你可以:\n\n * 给我提 Issue。\n\n你也可以在Issue查看别人提的问题和给出解决方案。\n\n * 戳JavaDriver社区\n\n更新日志\n\n\n# 🐼Me\n\n西二旗Java老司机\n\n\n# 技能\n\n * 熟悉 SpringBoot、Mybatis、Netty、Kafka 的拼写\n * 了解 Linux、windows、macOS 的开关机方式\n * 精通 Git 的 pull 和 push,并注册了 GitHub 帐号刷了一些 star\n\n\n# Java学习\n\n\n关注公众号,回复后端资源,即可获取学习资源一份。\n\n\n# ✉️ 联系\n\n * WeChat or QQ: {{ QQ }}\n * Email: 854818303@qq.com\n * GitHub: https://github.com/AdmireAn/JavaDriver", + "normalizedContent": "# 📚blog\n\n这是一个java学习网站\n\n\n# ⚡ 反馈与交流\n\n文章内容仅是我个人的小总结,资历尚浅,如有误还请指正。\n\n在使用过程中有任何问题和想法,你可以:\n\n * 给我提 issue。\n\n你也可以在issue查看别人提的问题和给出解决方案。\n\n * 戳javadriver社区\n\n更新日志\n\n\n# 🐼me\n\n西二旗java老司机\n\n\n# 技能\n\n * 熟悉 springboot、mybatis、netty、kafka 的拼写\n * 了解 linux、windows、macos 的开关机方式\n * 精通 git 的 pull 和 push,并注册了 github 帐号刷了一些 star\n\n\n# java学习\n\n\n关注公众号,回复后端资源,即可获取学习资源一份。\n\n\n# ✉️ 联系\n\n * wechat or qq: {{ qq }}\n * email: 854818303@qq.com\n * github: https://github.com/admirean/javadriver", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/06/20, 22:47:41", + "lastUpdatedTimestamp": 1655736461000 + }, + { + "title": "非技术问题", + "frontmatter": { + "title": "非技术问题", + "date": "2022-03-27T13:53:27.000Z", + "permalink": "/pages/5e507a/", + "categories": [ + "非技术" + ], + "tags": [ + null + ] + }, + "regularPath": "/10.%E9%9D%9E%E6%8A%80%E6%9C%AF/01.%E9%9D%9E%E6%8A%80%E6%9C%AF%E9%97%AE%E9%A2%98.html", + "relativePath": "10.非技术/01.非技术问题.md", + "key": "v-2a52a762", + "path": "/pages/5e507a/", + "headers": [ { "level": 2, "title": "最有技术难度的项目是什么?遇到技术难点怎么解决?", @@ -5507,72 +6205,6 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, - { - "title": "关于", - "frontmatter": { - "title": "关于", - "date": "2019-12-25T14:27:01.000Z", - "permalink": "/about", - "sidebar": false, - "article": false - }, - "regularPath": "/15.%E5%85%B3%E4%BA%8E/01.%E5%85%B3%E4%BA%8E.html", - "relativePath": "15.关于/01.关于.md", - "key": "v-918dfaf8", - "path": "/about/", - "headers": [ - { - "level": 2, - "title": "📚Blog", - "slug": "📚blog", - "normalizedTitle": "📚blog", - "charIndex": 2 - }, - { - "level": 2, - "title": "⚡ 反馈与交流", - "slug": "⚡-反馈与交流", - "normalizedTitle": "⚡ 反馈与交流", - "charIndex": 27 - }, - { - "level": 2, - "title": "🐼Me", - "slug": "🐼me", - "normalizedTitle": "🐼me", - "charIndex": 156 - }, - { - "level": 3, - "title": "技能", - "slug": "技能", - "normalizedTitle": "技能", - "charIndex": 177 - }, - { - "level": 2, - "title": "Java学习", - "slug": "java学习", - "normalizedTitle": "java学习", - "charIndex": 14 - }, - { - "level": 2, - "title": "✉️ 联系", - "slug": "联系", - "normalizedTitle": "✉️ 联系", - "charIndex": 345 - } - ], - "headersStr": "📚Blog ⚡ 反馈与交流 🐼Me 技能 Java学习 ✉️ 联系", - "content": "# 📚Blog\n\n这是一个Java学习网站\n\n\n# ⚡ 反馈与交流\n\n文章内容仅是我个人的小总结,资历尚浅,如有误还请指正。\n\n在使用过程中有任何问题和想法,你可以:\n\n * 给我提 Issue。\n\n你也可以在Issue查看别人提的问题和给出解决方案。\n\n * 戳JavaDriver社区\n\n更新日志\n\n\n# 🐼Me\n\n西二旗Java老司机\n\n\n# 技能\n\n * 熟悉 SpringBoot、Mybatis、Netty、Kafka 的拼写\n * 了解 Linux、windows、macOS 的开关机方式\n * 精通 Git 的 pull 和 push,并注册了 GitHub 帐号刷了一些 star\n\n\n# Java学习\n\n\n关注公众号,回复后端资源,即可获取学习资源一份。\n\n\n# ✉️ 联系\n\n * WeChat or QQ: {{ QQ }}\n * Email: 854818303@qq.com\n * GitHub: https://github.com/AdmireAn/JavaDriver", - "normalizedContent": "# 📚blog\n\n这是一个java学习网站\n\n\n# ⚡ 反馈与交流\n\n文章内容仅是我个人的小总结,资历尚浅,如有误还请指正。\n\n在使用过程中有任何问题和想法,你可以:\n\n * 给我提 issue。\n\n你也可以在issue查看别人提的问题和给出解决方案。\n\n * 戳javadriver社区\n\n更新日志\n\n\n# 🐼me\n\n西二旗java老司机\n\n\n# 技能\n\n * 熟悉 springboot、mybatis、netty、kafka 的拼写\n * 了解 linux、windows、macos 的开关机方式\n * 精通 git 的 pull 和 push,并注册了 github 帐号刷了一些 star\n\n\n# java学习\n\n\n关注公众号,回复后端资源,即可获取学习资源一份。\n\n\n# ✉️ 联系\n\n * wechat or qq: {{ qq }}\n * email: 854818303@qq.com\n * github: https://github.com/admirean/javadriver", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/06/20, 22:47:41", - "lastUpdatedTimestamp": 1655736461000 - }, { "title": "归档", "frontmatter": { @@ -5585,200 +6217,10 @@ export const siteData = { "relativePath": "@pages/archivesPage.md", "key": "v-48f7eec0", "path": "/archives/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, - { - "title": "常用的前端轮子", - "frontmatter": { - "title": "常用的前端轮子", - "date": "2021-10-09T19:42:02.000Z", - "permalink": "/pages/47cf96/", - "article": false - }, - "regularPath": "/@pages/20.%E6%94%B6%E8%97%8F%E5%A4%B9/02.%E5%B8%B8%E7%94%A8%E7%9A%84%E5%89%8D%E7%AB%AF%E8%BD%AE%E5%AD%90.html", - "relativePath": "@pages/20.收藏夹/02.常用的前端轮子.md", - "key": "v-09c150e6", - "path": "/pages/47cf96/", - "headers": [ - { - "level": 2, - "title": "React UI 组件库", - "slug": "react-ui-组件库", - "normalizedTitle": "react ui 组件库", - "charIndex": 2 - }, - { - "level": 2, - "title": "Vue UI组件库", - "slug": "vue-ui组件库", - "normalizedTitle": "vue ui组件库", - "charIndex": 68 - }, - { - "level": 2, - "title": "常用效果组件", - "slug": "常用效果组件", - "normalizedTitle": "常用效果组件", - "charIndex": 124 - }, - { - "level": 2, - "title": "工具类", - "slug": "工具类", - "normalizedTitle": "工具类", - "charIndex": 201 - }, - { - "level": 2, - "title": "Vue工具类", - "slug": "vue工具类", - "normalizedTitle": "vue工具类", - "charIndex": 331 - }, - { - "level": 2, - "title": "其他", - "slug": "其他", - "normalizedTitle": "其他", - "charIndex": 469 - } - ], - "headersStr": "React UI 组件库 Vue UI组件库 常用效果组件 工具类 Vue工具类 其他", - "content": "# React UI 组件库\n\n * Ant Design\n * React Bootstrap\n * MATERIAL-UI\n\n\n# Vue UI组件库\n\n * Element UI PC端\n * Vant 移动端\n * View UI\n\n\n# 常用效果组件\n\n * Animate.css 动画库\n * Swiper 轮播组件\n * mescroll 下拉刷新和上拉加载框架-基于原生JS\n\n\n# 工具类\n\n * Lodash.js\n * Day.js 处理日期\n * Timeago.js 相对时间,如N小时前\n * Echarts 百度图表\n * Meditor.md 开源在线 Markdown 编辑器\n * validator.js 验证库\n\n\n# Vue工具类\n\n * vue-draggable 基于Sortable.js实现的vue拖拽插件\n * vue-qr 文本转二维码\n * vue-cropper 图片裁剪插件\n * vue-lazyload 懒加载\n * vue-simple-upload 上传组件\n\n\n# 其他\n\n * H5带笔锋手写签名,支持PC端和移动端", - "normalizedContent": "# react ui 组件库\n\n * ant design\n * react bootstrap\n * material-ui\n\n\n# vue ui组件库\n\n * element ui pc端\n * vant 移动端\n * view ui\n\n\n# 常用效果组件\n\n * animate.css 动画库\n * swiper 轮播组件\n * mescroll 下拉刷新和上拉加载框架-基于原生js\n\n\n# 工具类\n\n * lodash.js\n * day.js 处理日期\n * timeago.js 相对时间,如n小时前\n * echarts 百度图表\n * meditor.md 开源在线 markdown 编辑器\n * validator.js 验证库\n\n\n# vue工具类\n\n * vue-draggable 基于sortable.js实现的vue拖拽插件\n * vue-qr 文本转二维码\n * vue-cropper 图片裁剪插件\n * vue-lazyload 懒加载\n * vue-simple-upload 上传组件\n\n\n# 其他\n\n * h5带笔锋手写签名,支持pc端和移动端", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/06/20, 22:47:41", - "lastUpdatedTimestamp": 1655736461000 - }, - { - "title": "你知道的越多,不知道的也就越多", - "frontmatter": { - "title": "你知道的越多,不知道的也就越多", - "date": "2020-05-06T15:52:40.000Z", - "permalink": "/pages/f2e63f", - "sidebar": "auto", - "categories": [ - "随笔" - ], - "tags": [ - "学习", - "知识", - "鸡汤" - ], - "author": { - "name": "xugaoyi", - "link": "https://github.com/xugaoyi" - } - }, - "regularPath": "/_posts/%E9%9A%8F%E7%AC%94/%E4%BD%A0%E7%9F%A5%E9%81%93%E7%9A%84%E8%B6%8A%E5%A4%9A%EF%BC%8C%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84%E4%B9%9F%E5%B0%B1%E8%B6%8A%E5%A4%9A.html", - "relativePath": "_posts/随笔/你知道的越多,不知道的也就越多.md", - "key": "v-2db11cb2", - "path": "/pages/f2e63f/", - "excerpt": "

不知道大家有没有发现,我们身边经常有这样的人,他们越是有能力的,越是有知识的,越是低调,越是谦逊,因为他们深知,知道的越多,不知道的也就越多。

\n", - "headersStr": null, - "content": "不知道大家有没有发现,我们身边经常有这样的人,他们越是有能力的,越是有知识的,越是低调,越是谦逊,因为他们深知,知道的越多,不知道的也就越多。\n\n你知道的越多,你不知道的也就越多,这是一句非常有哲理的话。\n\n每个人的知识面都是有限的,你有可能在某个领域会有较深的研究,成为这个领域里的专家,等到你站在高处的时候,才会发现,自己是多么的渺小,才知道自己有多少没有涉及的领域。知道的越多,疑惑、问题就会越来越多,对已知的质疑、疑虑、困惑就会越来越多。\n\n即使如此,我们也应该努力,至少可以成为某个领域的佼佼者。\n\n鸡汤1\n\n弱小的人,才习惯嘲讽与否定,而内心强大的人,从不吝啬赞美与鼓励。\n\n鸡汤2\n\n当代青年人都应该摆脱冷气,只管向上走,不必听从自暴自弃者的流言。能做事的做事,能发声的发声。有一份热,发一份光,就像萤火一般,也可以在黑暗里发一点光,不必等候炬火。", - "normalizedContent": "不知道大家有没有发现,我们身边经常有这样的人,他们越是有能力的,越是有知识的,越是低调,越是谦逊,因为他们深知,知道的越多,不知道的也就越多。\n\n你知道的越多,你不知道的也就越多,这是一句非常有哲理的话。\n\n每个人的知识面都是有限的,你有可能在某个领域会有较深的研究,成为这个领域里的专家,等到你站在高处的时候,才会发现,自己是多么的渺小,才知道自己有多少没有涉及的领域。知道的越多,疑惑、问题就会越来越多,对已知的质疑、疑虑、困惑就会越来越多。\n\n即使如此,我们也应该努力,至少可以成为某个领域的佼佼者。\n\n鸡汤1\n\n弱小的人,才习惯嘲讽与否定,而内心强大的人,从不吝啬赞美与鼓励。\n\n鸡汤2\n\n当代青年人都应该摆脱冷气,只管向上走,不必听从自暴自弃者的流言。能做事的做事,能发声的发声。有一份热,发一份光,就像萤火一般,也可以在黑暗里发一点光,不必等候炬火。", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, - { - "title": "拥抱生活,拥抱快乐", - "frontmatter": { - "title": "拥抱生活,拥抱快乐", - "date": "2020-06-26T20:40:38.000Z", - "permalink": "/pages/cd8bde/", - "sidebar": "auto", - "categories": [ - "随笔" - ], - "tags": [ - "鸡汤" - ], - "author": { - "name": "xugaoyi", - "link": "https://github.com/xugaoyi" - } - }, - "regularPath": "/_posts/%E9%9A%8F%E7%AC%94/%E6%8B%A5%E6%8A%B1%E7%94%9F%E6%B4%BB%EF%BC%8C%E6%8B%A5%E6%8A%B1%E5%BF%AB%E4%B9%90.html", - "relativePath": "_posts/随笔/拥抱生活,拥抱快乐.md", - "key": "v-7d77d69c", - "path": "/pages/cd8bde/", - "excerpt": "

生活在后现代的今天,很多人都有一种虚无感,认为人生没有意义。但是,人生不可能没有意义,因为当你认为没有意义的时候,一定有一个与之相对应的概念叫有意义。

\n", - "headersStr": null, - "content": "生活在后现代的今天,很多人都有一种虚无感,认为人生没有意义。但是,人生不可能没有意义,因为当你认为没有意义的时候,一定有一个与之相对应的概念叫有意义。\n\n当你怀疑人生没有意义时,难道怀疑本身不值得怀疑吗?\n\n不要任由你内心的虚无感蔓延,我们需要去拥抱真实的生活。\n\n所有真实的快乐,都离不开艰辛的努力,无论是金榜题名的快乐,还是事业成功的喜悦,甚至包括洞房花烛的激动。所有真实的快乐,都需要长久的铺垫与努力,没有辛勤的汗水,快乐也就不再真实。\n\n如果快乐触手可及,这种廉价的快乐也就不值得珍惜,随时都可能抛弃。因此,对于年轻人而言,一个重要的功课就是学会去节制欲望。\n\n所有通过捷径所带来的快乐,都是廉价的,以至于所有追求都变得毫无意义,人生就了无生趣。我们需要在每天真实的努力中去拥抱生活,追寻真实的快乐。\n\n\n\n> 文章摘录自:B站视频《罗翔说刑法》,链接https://b23.tv/K8ulrE", - "normalizedContent": "生活在后现代的今天,很多人都有一种虚无感,认为人生没有意义。但是,人生不可能没有意义,因为当你认为没有意义的时候,一定有一个与之相对应的概念叫有意义。\n\n当你怀疑人生没有意义时,难道怀疑本身不值得怀疑吗?\n\n不要任由你内心的虚无感蔓延,我们需要去拥抱真实的生活。\n\n所有真实的快乐,都离不开艰辛的努力,无论是金榜题名的快乐,还是事业成功的喜悦,甚至包括洞房花烛的激动。所有真实的快乐,都需要长久的铺垫与努力,没有辛勤的汗水,快乐也就不再真实。\n\n如果快乐触手可及,这种廉价的快乐也就不值得珍惜,随时都可能抛弃。因此,对于年轻人而言,一个重要的功课就是学会去节制欲望。\n\n所有通过捷径所带来的快乐,都是廉价的,以至于所有追求都变得毫无意义,人生就了无生趣。我们需要在每天真实的努力中去拥抱生活,追寻真实的快乐。\n\n\n\n> 文章摘录自:b站视频《罗翔说刑法》,链接https://b23.tv/k8ulre", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, - { - "title": "我做了一个手写春联小网页,祝大家虎年暴富", - "frontmatter": { - "title": "我做了一个手写春联小网页,祝大家虎年暴富", - "date": "2022-01-28T14:59:51.000Z", - "permalink": "/pages/829589/", - "titleTag": "原创", - "sidebar": "auto", - "categories": [ - "随笔" - ], - "tags": [ - null - ] - }, - "regularPath": "/_posts/%E9%9A%8F%E7%AC%94/%E6%88%91%E5%81%9A%E4%BA%86%E4%B8%80%E4%B8%AA%E6%89%8B%E5%86%99%E6%98%A5%E8%81%94%E5%B0%8F%E7%BD%91%E9%A1%B5%EF%BC%8C%E7%A5%9D%E5%A4%A7%E5%AE%B6%E8%99%8E%E5%B9%B4%E6%9A%B4%E5%AF%8C.html", - "relativePath": "_posts/随笔/我做了一个手写春联小网页,祝大家虎年暴富.md", - "key": "v-a5c5f278", - "path": "/pages/829589/", - "headers": [ - { - "level": 3, - "title": "前言", - "slug": "前言", - "normalizedTitle": "前言", - "charIndex": 33 - }, - { - "level": 3, - "title": "产品构思", - "slug": "产品构思", - "normalizedTitle": "产品构思", - "charIndex": 195 - }, - { - "level": 3, - "title": "设计", - "slug": "设计", - "normalizedTitle": "设计", - "charIndex": 478 - }, - { - "level": 3, - "title": "开发", - "slug": "开发", - "normalizedTitle": "开发", - "charIndex": 487 - } - ], - "excerpt": "

手写春联:https://cl.xugaoyi.com/

\n

# 前言

\n

虎年春节快到了,首先祝大家新年快乐,轻松暴富。\n最近在网上经常看到生成春联的文章,不过这些小demo要么功能简陋,要么UI特别‘程序员’,满足不了我挑剔的眼光。干脆我自己做一个吧,顺便简单体验一下vite+vue3。(因为页面相对简单,vue组件风格还是使用选项式api,重点还是想把产品快速做出来。)

\n", - "headersStr": "前言 产品构思 设计 开发", - "content": "手写春联:https://cl.xugaoyi.com/\n\n\n# 前言\n\n虎年春节快到了,首先祝大家新年快乐,轻松暴富。 最近在网上经常看到生成春联的文章,不过这些小demo要么功能简陋,要么UI特别‘程序员’,满足不了我挑剔的眼光。干脆我自己做一个吧,顺便简单体验一下vite+vue3。(因为页面相对简单,vue组件风格还是使用选项式api,重点还是想把产品快速做出来。)\n\n\n\n\n# 产品构思\n\n包含手写春节和生成春联两大功能:\n\n * 手写春联\n \n * 模拟用笔写字的字迹\n * 选择画笔颜色\n * 调整画笔大小\n * 清空画布\n * 撤回笔画\n * 切换上、下联、横批、福字\n * 随机切换对联提示\n * 预览图片和下载\n * 贴春联海报和下载\n\n * 生成模式\n \n * 选择画笔颜色\n * 挑选生成的对联\n * 输入对联\n * 随机切换对联\n * 贴春联海报和下载\n\n * 其他\n \n * 快速切换模式按钮\n * 可控制的背景音乐\n * 微信分享网页\n\n\n# 设计\n\n\n\n\n# 开发\n\n * 技术栈\n * vite (打包&构建)\n * vue3 (页面开发)\n * vant(ui)\n * sass (css)\n * smooth-signature.js (带笔锋手写库)\n\n\n\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n57\n58\n59\n60\n61\n62\n63\n64\n65\n66\n67\n68\n69\n70\n71\n72\n73\n74\n75\n76\n77\n78\n79\n80\n81\n82\n83\n84\n85\n86\n87\n88\n89\n90\n91\n92\n93\n94\n95\n96\n97\n98\n99\n100\n101\n102\n103\n104\n105\n106\n107\n108\n109\n110\n111\n112\n113\n114\n115\n116\n117\n118\n119\n120\n121\n122\n123\n124\n125\n126\n127\n128\n129\n130\n131\n132\n133\n134\n135\n136\n137\n138\n139\n140\n141\n142\n143\n144\n145\n146\n147\n148\n149\n150\n151\n152\n153\n154\n155\n156\n157\n158\n159\n160\n161\n162\n163\n164\n165\n166\n167\n168\n169\n170\n171\n172\n173\n174\n175\n176\n177\n178\n179\n180\n181\n182\n183\n184\n185\n186\n187\n188\n189\n190\n191\n192\n193\n194\n195\n196\n197\n198\n199\n200\n201\n202\n203\n204\n205\n206\n207\n208\n209\n210\n211\n212\n213\n214\n215\n216\n217\n218\n219\n220\n221\n222\n223\n224\n225\n226\n227\n228\n229\n230\n231\n232\n233\n234\n235\n236\n237\n238\n239\n240\n241\n242\n243\n244\n245\n246\n247\n248\n249\n250\n251\n252\n253\n254\n255\n256\n257\n258\n259\n260\n261\n262\n263\n264\n265\n266\n267\n268\n269\n270\n271\n272\n273\n274\n275\n276\n277\n278\n279\n280\n281\n282\n283\n284\n285\n286\n287\n288\n289\n290\n291\n292\n293\n294\n295\n296\n297\n298\n299\n300\n301\n302\n303\n304\n305\n306\n307\n308\n309\n310\n311\n312\n313\n314\n315\n316\n317\n318\n319\n320\n321\n322\n323\n324\n325\n326\n327\n328\n329\n330\n331\n332\n333\n334\n335\n336\n337\n338\n339\n340\n341\n342\n343\n344\n345\n346\n347\n348\n349\n350\n351\n352\n353\n354\n355\n356\n357\n358\n359\n360\n361\n362\n363\n364\n365\n366\n367\n368\n369\n370\n371\n372\n373\n374\n375\n376\n377\n378\n379\n380\n381\n382\n383\n384\n385\n386\n387\n388\n389\n390\n391\n392\n393\n394\n395\n396\n397\n398\n399\n400\n401\n402\n403\n404\n405\n406\n407\n408\n409\n410\n411\n412\n413\n414\n415\n416\n417\n418\n419\n420\n421\n422\n423\n424\n425\n426\n427\n428\n429\n430\n431\n432\n433\n434\n435\n436\n437\n438\n439\n440\n441\n442\n443\n444\n445\n446\n447\n448\n449\n450\n451\n452\n453\n454\n455\n456\n457\n458\n459\n460\n461\n462\n463\n464\n465\n466\n467\n468\n469\n470\n471\n472\n473\n474\n475\n476\n477\n478\n479\n480\n481\n482\n483\n484\n485\n486\n487\n488\n489\n490\n491\n492\n493\n494\n495\n496\n497\n498\n499\n500\n501\n502\n503\n504\n505\n506\n507\n508\n509\n510\n511\n512\n513\n514\n515\n516\n517\n518\n519\n520\n521\n522\n523\n524\n525\n526\n527\n528\n529\n530\n531\n532\n533\n534\n535\n536\n537\n538\n539\n540\n541\n542\n543\n544\n545\n546\n547\n548\n549\n550\n551\n552\n553\n554\n555\n556\n557\n558\n559\n560\n561\n562\n563\n564\n565\n566\n567\n568\n569\n570\n571\n572\n573\n574\n575\n576\n577\n578\n579\n580\n581\n582\n583\n584\n585\n586\n587\n588\n589\n590\n591\n592\n593\n594\n595\n596\n597\n598\n599\n600\n601\n602\n603\n604\n605\n606\n607\n608\n609\n610\n611\n612\n613\n614\n615\n616\n617\n618\n619\n620\n621\n622\n623\n624\n625\n626\n627\n628\n629\n630\n631\n632\n633\n634\n635\n636\n637\n638\n639\n640\n641\n642\n643\n644\n645\n646\n647\n648\n649\n650\n651\n652\n653\n654\n655\n656\n657\n658\n659\n660\n661\n662\n663\n664\n665\n666\n667\n668\n669\n670\n671\n672\n673\n674\n675\n676\n677\n678\n679\n680\n681\n682\n683\n684\n685\n686\n687\n688\n689\n690\n691\n692\n693\n694\n695\n696\n697\n698\n699\n700\n701\n702\n703\n704\n705\n706\n707\n708\n709\n710\n711\n712\n713\n714\n715\n716\n717\n718\n719\n720\n721\n722\n723\n724\n725\n726\n727\n728\n729\n730\n731\n732\n733\n734\n735\n736\n737\n\n\n更多有趣的小网页欢迎关注公众号有趣研究社:\n\n> 手写春联\n> FC在线模拟器\n> 爱国头像生成器\n> 到账语音生成器", - "normalizedContent": "手写春联:https://cl.xugaoyi.com/\n\n\n# 前言\n\n虎年春节快到了,首先祝大家新年快乐,轻松暴富。 最近在网上经常看到生成春联的文章,不过这些小demo要么功能简陋,要么ui特别‘程序员’,满足不了我挑剔的眼光。干脆我自己做一个吧,顺便简单体验一下vite+vue3。(因为页面相对简单,vue组件风格还是使用选项式api,重点还是想把产品快速做出来。)\n\n\n\n\n# 产品构思\n\n包含手写春节和生成春联两大功能:\n\n * 手写春联\n \n * 模拟用笔写字的字迹\n * 选择画笔颜色\n * 调整画笔大小\n * 清空画布\n * 撤回笔画\n * 切换上、下联、横批、福字\n * 随机切换对联提示\n * 预览图片和下载\n * 贴春联海报和下载\n\n * 生成模式\n \n * 选择画笔颜色\n * 挑选生成的对联\n * 输入对联\n * 随机切换对联\n * 贴春联海报和下载\n\n * 其他\n \n * 快速切换模式按钮\n * 可控制的背景音乐\n * 微信分享网页\n\n\n# 设计\n\n\n\n\n# 开发\n\n * 技术栈\n * vite (打包&构建)\n * vue3 (页面开发)\n * vant(ui)\n * sass (css)\n * smooth-signature.js (带笔锋手写库)\n\n\n\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n57\n58\n59\n60\n61\n62\n63\n64\n65\n66\n67\n68\n69\n70\n71\n72\n73\n74\n75\n76\n77\n78\n79\n80\n81\n82\n83\n84\n85\n86\n87\n88\n89\n90\n91\n92\n93\n94\n95\n96\n97\n98\n99\n100\n101\n102\n103\n104\n105\n106\n107\n108\n109\n110\n111\n112\n113\n114\n115\n116\n117\n118\n119\n120\n121\n122\n123\n124\n125\n126\n127\n128\n129\n130\n131\n132\n133\n134\n135\n136\n137\n138\n139\n140\n141\n142\n143\n144\n145\n146\n147\n148\n149\n150\n151\n152\n153\n154\n155\n156\n157\n158\n159\n160\n161\n162\n163\n164\n165\n166\n167\n168\n169\n170\n171\n172\n173\n174\n175\n176\n177\n178\n179\n180\n181\n182\n183\n184\n185\n186\n187\n188\n189\n190\n191\n192\n193\n194\n195\n196\n197\n198\n199\n200\n201\n202\n203\n204\n205\n206\n207\n208\n209\n210\n211\n212\n213\n214\n215\n216\n217\n218\n219\n220\n221\n222\n223\n224\n225\n226\n227\n228\n229\n230\n231\n232\n233\n234\n235\n236\n237\n238\n239\n240\n241\n242\n243\n244\n245\n246\n247\n248\n249\n250\n251\n252\n253\n254\n255\n256\n257\n258\n259\n260\n261\n262\n263\n264\n265\n266\n267\n268\n269\n270\n271\n272\n273\n274\n275\n276\n277\n278\n279\n280\n281\n282\n283\n284\n285\n286\n287\n288\n289\n290\n291\n292\n293\n294\n295\n296\n297\n298\n299\n300\n301\n302\n303\n304\n305\n306\n307\n308\n309\n310\n311\n312\n313\n314\n315\n316\n317\n318\n319\n320\n321\n322\n323\n324\n325\n326\n327\n328\n329\n330\n331\n332\n333\n334\n335\n336\n337\n338\n339\n340\n341\n342\n343\n344\n345\n346\n347\n348\n349\n350\n351\n352\n353\n354\n355\n356\n357\n358\n359\n360\n361\n362\n363\n364\n365\n366\n367\n368\n369\n370\n371\n372\n373\n374\n375\n376\n377\n378\n379\n380\n381\n382\n383\n384\n385\n386\n387\n388\n389\n390\n391\n392\n393\n394\n395\n396\n397\n398\n399\n400\n401\n402\n403\n404\n405\n406\n407\n408\n409\n410\n411\n412\n413\n414\n415\n416\n417\n418\n419\n420\n421\n422\n423\n424\n425\n426\n427\n428\n429\n430\n431\n432\n433\n434\n435\n436\n437\n438\n439\n440\n441\n442\n443\n444\n445\n446\n447\n448\n449\n450\n451\n452\n453\n454\n455\n456\n457\n458\n459\n460\n461\n462\n463\n464\n465\n466\n467\n468\n469\n470\n471\n472\n473\n474\n475\n476\n477\n478\n479\n480\n481\n482\n483\n484\n485\n486\n487\n488\n489\n490\n491\n492\n493\n494\n495\n496\n497\n498\n499\n500\n501\n502\n503\n504\n505\n506\n507\n508\n509\n510\n511\n512\n513\n514\n515\n516\n517\n518\n519\n520\n521\n522\n523\n524\n525\n526\n527\n528\n529\n530\n531\n532\n533\n534\n535\n536\n537\n538\n539\n540\n541\n542\n543\n544\n545\n546\n547\n548\n549\n550\n551\n552\n553\n554\n555\n556\n557\n558\n559\n560\n561\n562\n563\n564\n565\n566\n567\n568\n569\n570\n571\n572\n573\n574\n575\n576\n577\n578\n579\n580\n581\n582\n583\n584\n585\n586\n587\n588\n589\n590\n591\n592\n593\n594\n595\n596\n597\n598\n599\n600\n601\n602\n603\n604\n605\n606\n607\n608\n609\n610\n611\n612\n613\n614\n615\n616\n617\n618\n619\n620\n621\n622\n623\n624\n625\n626\n627\n628\n629\n630\n631\n632\n633\n634\n635\n636\n637\n638\n639\n640\n641\n642\n643\n644\n645\n646\n647\n648\n649\n650\n651\n652\n653\n654\n655\n656\n657\n658\n659\n660\n661\n662\n663\n664\n665\n666\n667\n668\n669\n670\n671\n672\n673\n674\n675\n676\n677\n678\n679\n680\n681\n682\n683\n684\n685\n686\n687\n688\n689\n690\n691\n692\n693\n694\n695\n696\n697\n698\n699\n700\n701\n702\n703\n704\n705\n706\n707\n708\n709\n710\n711\n712\n713\n714\n715\n716\n717\n718\n719\n720\n721\n722\n723\n724\n725\n726\n727\n728\n729\n730\n731\n732\n733\n734\n735\n736\n737\n\n\n更多有趣的小网页欢迎关注公众号有趣研究社:\n\n> 手写春联\n> fc在线模拟器\n> 爱国头像生成器\n> 到账语音生成器", - "charsets": { - "cjk": true - }, + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, @@ -6052,40 +6494,127 @@ export const siteData = { "lastUpdatedTimestamp": 1655736461000 }, { - "title": "分类", + "title": "我做了一个手写春联小网页,祝大家虎年暴富", "frontmatter": { - "categoriesPage": true, - "title": "分类", - "permalink": "/categories/", - "article": false + "title": "我做了一个手写春联小网页,祝大家虎年暴富", + "date": "2022-01-28T14:59:51.000Z", + "permalink": "/pages/829589/", + "titleTag": "原创", + "sidebar": "auto", + "categories": [ + "随笔" + ], + "tags": [ + null + ] }, - "regularPath": "/@pages/categoriesPage.html", - "relativePath": "@pages/categoriesPage.md", - "key": "v-5f8dfe80", - "path": "/categories/", + "regularPath": "/_posts/%E9%9A%8F%E7%AC%94/%E6%88%91%E5%81%9A%E4%BA%86%E4%B8%80%E4%B8%AA%E6%89%8B%E5%86%99%E6%98%A5%E8%81%94%E5%B0%8F%E7%BD%91%E9%A1%B5%EF%BC%8C%E7%A5%9D%E5%A4%A7%E5%AE%B6%E8%99%8E%E5%B9%B4%E6%9A%B4%E5%AF%8C.html", + "relativePath": "_posts/随笔/我做了一个手写春联小网页,祝大家虎年暴富.md", + "key": "v-a5c5f278", + "path": "/pages/829589/", + "headers": [ + { + "level": 3, + "title": "前言", + "slug": "前言", + "normalizedTitle": "前言", + "charIndex": 33 + }, + { + "level": 3, + "title": "产品构思", + "slug": "产品构思", + "normalizedTitle": "产品构思", + "charIndex": 195 + }, + { + "level": 3, + "title": "设计", + "slug": "设计", + "normalizedTitle": "设计", + "charIndex": 478 + }, + { + "level": 3, + "title": "开发", + "slug": "开发", + "normalizedTitle": "开发", + "charIndex": 487 + } + ], + "excerpt": "

手写春联:https://cl.xugaoyi.com/

\n

# 前言

\n

虎年春节快到了,首先祝大家新年快乐,轻松暴富。\n最近在网上经常看到生成春联的文章,不过这些小demo要么功能简陋,要么UI特别‘程序员’,满足不了我挑剔的眼光。干脆我自己做一个吧,顺便简单体验一下vite+vue3。(因为页面相对简单,vue组件风格还是使用选项式api,重点还是想把产品快速做出来。)

\n", + "headersStr": "前言 产品构思 设计 开发", + "content": "手写春联:https://cl.xugaoyi.com/\n\n\n# 前言\n\n虎年春节快到了,首先祝大家新年快乐,轻松暴富。 最近在网上经常看到生成春联的文章,不过这些小demo要么功能简陋,要么UI特别‘程序员’,满足不了我挑剔的眼光。干脆我自己做一个吧,顺便简单体验一下vite+vue3。(因为页面相对简单,vue组件风格还是使用选项式api,重点还是想把产品快速做出来。)\n\n\n\n\n# 产品构思\n\n包含手写春节和生成春联两大功能:\n\n * 手写春联\n \n * 模拟用笔写字的字迹\n * 选择画笔颜色\n * 调整画笔大小\n * 清空画布\n * 撤回笔画\n * 切换上、下联、横批、福字\n * 随机切换对联提示\n * 预览图片和下载\n * 贴春联海报和下载\n\n * 生成模式\n \n * 选择画笔颜色\n * 挑选生成的对联\n * 输入对联\n * 随机切换对联\n * 贴春联海报和下载\n\n * 其他\n \n * 快速切换模式按钮\n * 可控制的背景音乐\n * 微信分享网页\n\n\n# 设计\n\n\n\n\n# 开发\n\n * 技术栈\n * vite (打包&构建)\n * vue3 (页面开发)\n * vant(ui)\n * sass (css)\n * smooth-signature.js (带笔锋手写库)\n\n\n\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n57\n58\n59\n60\n61\n62\n63\n64\n65\n66\n67\n68\n69\n70\n71\n72\n73\n74\n75\n76\n77\n78\n79\n80\n81\n82\n83\n84\n85\n86\n87\n88\n89\n90\n91\n92\n93\n94\n95\n96\n97\n98\n99\n100\n101\n102\n103\n104\n105\n106\n107\n108\n109\n110\n111\n112\n113\n114\n115\n116\n117\n118\n119\n120\n121\n122\n123\n124\n125\n126\n127\n128\n129\n130\n131\n132\n133\n134\n135\n136\n137\n138\n139\n140\n141\n142\n143\n144\n145\n146\n147\n148\n149\n150\n151\n152\n153\n154\n155\n156\n157\n158\n159\n160\n161\n162\n163\n164\n165\n166\n167\n168\n169\n170\n171\n172\n173\n174\n175\n176\n177\n178\n179\n180\n181\n182\n183\n184\n185\n186\n187\n188\n189\n190\n191\n192\n193\n194\n195\n196\n197\n198\n199\n200\n201\n202\n203\n204\n205\n206\n207\n208\n209\n210\n211\n212\n213\n214\n215\n216\n217\n218\n219\n220\n221\n222\n223\n224\n225\n226\n227\n228\n229\n230\n231\n232\n233\n234\n235\n236\n237\n238\n239\n240\n241\n242\n243\n244\n245\n246\n247\n248\n249\n250\n251\n252\n253\n254\n255\n256\n257\n258\n259\n260\n261\n262\n263\n264\n265\n266\n267\n268\n269\n270\n271\n272\n273\n274\n275\n276\n277\n278\n279\n280\n281\n282\n283\n284\n285\n286\n287\n288\n289\n290\n291\n292\n293\n294\n295\n296\n297\n298\n299\n300\n301\n302\n303\n304\n305\n306\n307\n308\n309\n310\n311\n312\n313\n314\n315\n316\n317\n318\n319\n320\n321\n322\n323\n324\n325\n326\n327\n328\n329\n330\n331\n332\n333\n334\n335\n336\n337\n338\n339\n340\n341\n342\n343\n344\n345\n346\n347\n348\n349\n350\n351\n352\n353\n354\n355\n356\n357\n358\n359\n360\n361\n362\n363\n364\n365\n366\n367\n368\n369\n370\n371\n372\n373\n374\n375\n376\n377\n378\n379\n380\n381\n382\n383\n384\n385\n386\n387\n388\n389\n390\n391\n392\n393\n394\n395\n396\n397\n398\n399\n400\n401\n402\n403\n404\n405\n406\n407\n408\n409\n410\n411\n412\n413\n414\n415\n416\n417\n418\n419\n420\n421\n422\n423\n424\n425\n426\n427\n428\n429\n430\n431\n432\n433\n434\n435\n436\n437\n438\n439\n440\n441\n442\n443\n444\n445\n446\n447\n448\n449\n450\n451\n452\n453\n454\n455\n456\n457\n458\n459\n460\n461\n462\n463\n464\n465\n466\n467\n468\n469\n470\n471\n472\n473\n474\n475\n476\n477\n478\n479\n480\n481\n482\n483\n484\n485\n486\n487\n488\n489\n490\n491\n492\n493\n494\n495\n496\n497\n498\n499\n500\n501\n502\n503\n504\n505\n506\n507\n508\n509\n510\n511\n512\n513\n514\n515\n516\n517\n518\n519\n520\n521\n522\n523\n524\n525\n526\n527\n528\n529\n530\n531\n532\n533\n534\n535\n536\n537\n538\n539\n540\n541\n542\n543\n544\n545\n546\n547\n548\n549\n550\n551\n552\n553\n554\n555\n556\n557\n558\n559\n560\n561\n562\n563\n564\n565\n566\n567\n568\n569\n570\n571\n572\n573\n574\n575\n576\n577\n578\n579\n580\n581\n582\n583\n584\n585\n586\n587\n588\n589\n590\n591\n592\n593\n594\n595\n596\n597\n598\n599\n600\n601\n602\n603\n604\n605\n606\n607\n608\n609\n610\n611\n612\n613\n614\n615\n616\n617\n618\n619\n620\n621\n622\n623\n624\n625\n626\n627\n628\n629\n630\n631\n632\n633\n634\n635\n636\n637\n638\n639\n640\n641\n642\n643\n644\n645\n646\n647\n648\n649\n650\n651\n652\n653\n654\n655\n656\n657\n658\n659\n660\n661\n662\n663\n664\n665\n666\n667\n668\n669\n670\n671\n672\n673\n674\n675\n676\n677\n678\n679\n680\n681\n682\n683\n684\n685\n686\n687\n688\n689\n690\n691\n692\n693\n694\n695\n696\n697\n698\n699\n700\n701\n702\n703\n704\n705\n706\n707\n708\n709\n710\n711\n712\n713\n714\n715\n716\n717\n718\n719\n720\n721\n722\n723\n724\n725\n726\n727\n728\n729\n730\n731\n732\n733\n734\n735\n736\n737\n\n\n更多有趣的小网页欢迎关注公众号有趣研究社:\n\n> 手写春联\n> FC在线模拟器\n> 爱国头像生成器\n> 到账语音生成器", + "normalizedContent": "手写春联:https://cl.xugaoyi.com/\n\n\n# 前言\n\n虎年春节快到了,首先祝大家新年快乐,轻松暴富。 最近在网上经常看到生成春联的文章,不过这些小demo要么功能简陋,要么ui特别‘程序员’,满足不了我挑剔的眼光。干脆我自己做一个吧,顺便简单体验一下vite+vue3。(因为页面相对简单,vue组件风格还是使用选项式api,重点还是想把产品快速做出来。)\n\n\n\n\n# 产品构思\n\n包含手写春节和生成春联两大功能:\n\n * 手写春联\n \n * 模拟用笔写字的字迹\n * 选择画笔颜色\n * 调整画笔大小\n * 清空画布\n * 撤回笔画\n * 切换上、下联、横批、福字\n * 随机切换对联提示\n * 预览图片和下载\n * 贴春联海报和下载\n\n * 生成模式\n \n * 选择画笔颜色\n * 挑选生成的对联\n * 输入对联\n * 随机切换对联\n * 贴春联海报和下载\n\n * 其他\n \n * 快速切换模式按钮\n * 可控制的背景音乐\n * 微信分享网页\n\n\n# 设计\n\n\n\n\n# 开发\n\n * 技术栈\n * vite (打包&构建)\n * vue3 (页面开发)\n * vant(ui)\n * sass (css)\n * smooth-signature.js (带笔锋手写库)\n\n\n\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n57\n58\n59\n60\n61\n62\n63\n64\n65\n66\n67\n68\n69\n70\n71\n72\n73\n74\n75\n76\n77\n78\n79\n80\n81\n82\n83\n84\n85\n86\n87\n88\n89\n90\n91\n92\n93\n94\n95\n96\n97\n98\n99\n100\n101\n102\n103\n104\n105\n106\n107\n108\n109\n110\n111\n112\n113\n114\n115\n116\n117\n118\n119\n120\n121\n122\n123\n124\n125\n126\n127\n128\n129\n130\n131\n132\n133\n134\n135\n136\n137\n138\n139\n140\n141\n142\n143\n144\n145\n146\n147\n148\n149\n150\n151\n152\n153\n154\n155\n156\n157\n158\n159\n160\n161\n162\n163\n164\n165\n166\n167\n168\n169\n170\n171\n172\n173\n174\n175\n176\n177\n178\n179\n180\n181\n182\n183\n184\n185\n186\n187\n188\n189\n190\n191\n192\n193\n194\n195\n196\n197\n198\n199\n200\n201\n202\n203\n204\n205\n206\n207\n208\n209\n210\n211\n212\n213\n214\n215\n216\n217\n218\n219\n220\n221\n222\n223\n224\n225\n226\n227\n228\n229\n230\n231\n232\n233\n234\n235\n236\n237\n238\n239\n240\n241\n242\n243\n244\n245\n246\n247\n248\n249\n250\n251\n252\n253\n254\n255\n256\n257\n258\n259\n260\n261\n262\n263\n264\n265\n266\n267\n268\n269\n270\n271\n272\n273\n274\n275\n276\n277\n278\n279\n280\n281\n282\n283\n284\n285\n286\n287\n288\n289\n290\n291\n292\n293\n294\n295\n296\n297\n298\n299\n300\n301\n302\n303\n304\n305\n306\n307\n308\n309\n310\n311\n312\n313\n314\n315\n316\n317\n318\n319\n320\n321\n322\n323\n324\n325\n326\n327\n328\n329\n330\n331\n332\n333\n334\n335\n336\n337\n338\n339\n340\n341\n342\n343\n344\n345\n346\n347\n348\n349\n350\n351\n352\n353\n354\n355\n356\n357\n358\n359\n360\n361\n362\n363\n364\n365\n366\n367\n368\n369\n370\n371\n372\n373\n374\n375\n376\n377\n378\n379\n380\n381\n382\n383\n384\n385\n386\n387\n388\n389\n390\n391\n392\n393\n394\n395\n396\n397\n398\n399\n400\n401\n402\n403\n404\n405\n406\n407\n408\n409\n410\n411\n412\n413\n414\n415\n416\n417\n418\n419\n420\n421\n422\n423\n424\n425\n426\n427\n428\n429\n430\n431\n432\n433\n434\n435\n436\n437\n438\n439\n440\n441\n442\n443\n444\n445\n446\n447\n448\n449\n450\n451\n452\n453\n454\n455\n456\n457\n458\n459\n460\n461\n462\n463\n464\n465\n466\n467\n468\n469\n470\n471\n472\n473\n474\n475\n476\n477\n478\n479\n480\n481\n482\n483\n484\n485\n486\n487\n488\n489\n490\n491\n492\n493\n494\n495\n496\n497\n498\n499\n500\n501\n502\n503\n504\n505\n506\n507\n508\n509\n510\n511\n512\n513\n514\n515\n516\n517\n518\n519\n520\n521\n522\n523\n524\n525\n526\n527\n528\n529\n530\n531\n532\n533\n534\n535\n536\n537\n538\n539\n540\n541\n542\n543\n544\n545\n546\n547\n548\n549\n550\n551\n552\n553\n554\n555\n556\n557\n558\n559\n560\n561\n562\n563\n564\n565\n566\n567\n568\n569\n570\n571\n572\n573\n574\n575\n576\n577\n578\n579\n580\n581\n582\n583\n584\n585\n586\n587\n588\n589\n590\n591\n592\n593\n594\n595\n596\n597\n598\n599\n600\n601\n602\n603\n604\n605\n606\n607\n608\n609\n610\n611\n612\n613\n614\n615\n616\n617\n618\n619\n620\n621\n622\n623\n624\n625\n626\n627\n628\n629\n630\n631\n632\n633\n634\n635\n636\n637\n638\n639\n640\n641\n642\n643\n644\n645\n646\n647\n648\n649\n650\n651\n652\n653\n654\n655\n656\n657\n658\n659\n660\n661\n662\n663\n664\n665\n666\n667\n668\n669\n670\n671\n672\n673\n674\n675\n676\n677\n678\n679\n680\n681\n682\n683\n684\n685\n686\n687\n688\n689\n690\n691\n692\n693\n694\n695\n696\n697\n698\n699\n700\n701\n702\n703\n704\n705\n706\n707\n708\n709\n710\n711\n712\n713\n714\n715\n716\n717\n718\n719\n720\n721\n722\n723\n724\n725\n726\n727\n728\n729\n730\n731\n732\n733\n734\n735\n736\n737\n\n\n更多有趣的小网页欢迎关注公众号有趣研究社:\n\n> 手写春联\n> fc在线模拟器\n> 爱国头像生成器\n> 到账语音生成器", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, + { + "title": "你知道的越多,不知道的也就越多", + "frontmatter": { + "title": "你知道的越多,不知道的也就越多", + "date": "2020-05-06T15:52:40.000Z", + "permalink": "/pages/f2e63f", + "sidebar": "auto", + "categories": [ + "随笔" + ], + "tags": [ + "学习", + "知识", + "鸡汤" + ], + "author": { + "name": "xugaoyi", + "link": "https://github.com/xugaoyi" + } + }, + "regularPath": "/_posts/%E9%9A%8F%E7%AC%94/%E4%BD%A0%E7%9F%A5%E9%81%93%E7%9A%84%E8%B6%8A%E5%A4%9A%EF%BC%8C%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84%E4%B9%9F%E5%B0%B1%E8%B6%8A%E5%A4%9A.html", + "relativePath": "_posts/随笔/你知道的越多,不知道的也就越多.md", + "key": "v-2db11cb2", + "path": "/pages/f2e63f/", + "excerpt": "

不知道大家有没有发现,我们身边经常有这样的人,他们越是有能力的,越是有知识的,越是低调,越是谦逊,因为他们深知,知道的越多,不知道的也就越多。

\n", "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, + "content": "不知道大家有没有发现,我们身边经常有这样的人,他们越是有能力的,越是有知识的,越是低调,越是谦逊,因为他们深知,知道的越多,不知道的也就越多。\n\n你知道的越多,你不知道的也就越多,这是一句非常有哲理的话。\n\n每个人的知识面都是有限的,你有可能在某个领域会有较深的研究,成为这个领域里的专家,等到你站在高处的时候,才会发现,自己是多么的渺小,才知道自己有多少没有涉及的领域。知道的越多,疑惑、问题就会越来越多,对已知的质疑、疑虑、困惑就会越来越多。\n\n即使如此,我们也应该努力,至少可以成为某个领域的佼佼者。\n\n鸡汤1\n\n弱小的人,才习惯嘲讽与否定,而内心强大的人,从不吝啬赞美与鼓励。\n\n鸡汤2\n\n当代青年人都应该摆脱冷气,只管向上走,不必听从自暴自弃者的流言。能做事的做事,能发声的发声。有一份热,发一份光,就像萤火一般,也可以在黑暗里发一点光,不必等候炬火。", + "normalizedContent": "不知道大家有没有发现,我们身边经常有这样的人,他们越是有能力的,越是有知识的,越是低调,越是谦逊,因为他们深知,知道的越多,不知道的也就越多。\n\n你知道的越多,你不知道的也就越多,这是一句非常有哲理的话。\n\n每个人的知识面都是有限的,你有可能在某个领域会有较深的研究,成为这个领域里的专家,等到你站在高处的时候,才会发现,自己是多么的渺小,才知道自己有多少没有涉及的领域。知道的越多,疑惑、问题就会越来越多,对已知的质疑、疑虑、困惑就会越来越多。\n\n即使如此,我们也应该努力,至少可以成为某个领域的佼佼者。\n\n鸡汤1\n\n弱小的人,才习惯嘲讽与否定,而内心强大的人,从不吝啬赞美与鼓励。\n\n鸡汤2\n\n当代青年人都应该摆脱冷气,只管向上走,不必听从自暴自弃者的流言。能做事的做事,能发声的发声。有一份热,发一份光,就像萤火一般,也可以在黑暗里发一点光,不必等候炬火。", + "charsets": { + "cjk": true + }, "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, { - "title": "标签", + "title": "拥抱生活,拥抱快乐", "frontmatter": { - "tagsPage": true, - "title": "标签", - "permalink": "/tags/", - "article": false + "title": "拥抱生活,拥抱快乐", + "date": "2020-06-26T20:40:38.000Z", + "permalink": "/pages/cd8bde/", + "sidebar": "auto", + "categories": [ + "随笔" + ], + "tags": [ + "鸡汤" + ], + "author": { + "name": "xugaoyi", + "link": "https://github.com/xugaoyi" + } }, - "regularPath": "/@pages/tagsPage.html", - "relativePath": "@pages/tagsPage.md", - "key": "v-64f9b920", - "path": "/tags/", + "regularPath": "/_posts/%E9%9A%8F%E7%AC%94/%E6%8B%A5%E6%8A%B1%E7%94%9F%E6%B4%BB%EF%BC%8C%E6%8B%A5%E6%8A%B1%E5%BF%AB%E4%B9%90.html", + "relativePath": "_posts/随笔/拥抱生活,拥抱快乐.md", + "key": "v-7d77d69c", + "path": "/pages/cd8bde/", + "excerpt": "

生活在后现代的今天,很多人都有一种虚无感,认为人生没有意义。但是,人生不可能没有意义,因为当你认为没有意义的时候,一定有一个与之相对应的概念叫有意义。

\n", "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, + "content": "生活在后现代的今天,很多人都有一种虚无感,认为人生没有意义。但是,人生不可能没有意义,因为当你认为没有意义的时候,一定有一个与之相对应的概念叫有意义。\n\n当你怀疑人生没有意义时,难道怀疑本身不值得怀疑吗?\n\n不要任由你内心的虚无感蔓延,我们需要去拥抱真实的生活。\n\n所有真实的快乐,都离不开艰辛的努力,无论是金榜题名的快乐,还是事业成功的喜悦,甚至包括洞房花烛的激动。所有真实的快乐,都需要长久的铺垫与努力,没有辛勤的汗水,快乐也就不再真实。\n\n如果快乐触手可及,这种廉价的快乐也就不值得珍惜,随时都可能抛弃。因此,对于年轻人而言,一个重要的功课就是学会去节制欲望。\n\n所有通过捷径所带来的快乐,都是廉价的,以至于所有追求都变得毫无意义,人生就了无生趣。我们需要在每天真实的努力中去拥抱生活,追寻真实的快乐。\n\n\n\n> 文章摘录自:B站视频《罗翔说刑法》,链接https://b23.tv/K8ulrE", + "normalizedContent": "生活在后现代的今天,很多人都有一种虚无感,认为人生没有意义。但是,人生不可能没有意义,因为当你认为没有意义的时候,一定有一个与之相对应的概念叫有意义。\n\n当你怀疑人生没有意义时,难道怀疑本身不值得怀疑吗?\n\n不要任由你内心的虚无感蔓延,我们需要去拥抱真实的生活。\n\n所有真实的快乐,都离不开艰辛的努力,无论是金榜题名的快乐,还是事业成功的喜悦,甚至包括洞房花烛的激动。所有真实的快乐,都需要长久的铺垫与努力,没有辛勤的汗水,快乐也就不再真实。\n\n如果快乐触手可及,这种廉价的快乐也就不值得珍惜,随时都可能抛弃。因此,对于年轻人而言,一个重要的功课就是学会去节制欲望。\n\n所有通过捷径所带来的快乐,都是廉价的,以至于所有追求都变得毫无意义,人生就了无生趣。我们需要在每天真实的努力中去拥抱生活,追寻真实的快乐。\n\n\n\n> 文章摘录自:b站视频《罗翔说刑法》,链接https://b23.tv/k8ulre", + "charsets": { + "cjk": true + }, "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, @@ -6157,6 +6686,109 @@ export const siteData = { }, "lastUpdated": "2022/06/20, 22:47:41", "lastUpdatedTimestamp": 1655736461000 + }, + { + "title": "常用的前端轮子", + "frontmatter": { + "title": "常用的前端轮子", + "date": "2021-10-09T19:42:02.000Z", + "permalink": "/pages/47cf96/", + "article": false + }, + "regularPath": "/@pages/20.%E6%94%B6%E8%97%8F%E5%A4%B9/02.%E5%B8%B8%E7%94%A8%E7%9A%84%E5%89%8D%E7%AB%AF%E8%BD%AE%E5%AD%90.html", + "relativePath": "@pages/20.收藏夹/02.常用的前端轮子.md", + "key": "v-09c150e6", + "path": "/pages/47cf96/", + "headers": [ + { + "level": 2, + "title": "React UI 组件库", + "slug": "react-ui-组件库", + "normalizedTitle": "react ui 组件库", + "charIndex": 2 + }, + { + "level": 2, + "title": "Vue UI组件库", + "slug": "vue-ui组件库", + "normalizedTitle": "vue ui组件库", + "charIndex": 68 + }, + { + "level": 2, + "title": "常用效果组件", + "slug": "常用效果组件", + "normalizedTitle": "常用效果组件", + "charIndex": 124 + }, + { + "level": 2, + "title": "工具类", + "slug": "工具类", + "normalizedTitle": "工具类", + "charIndex": 201 + }, + { + "level": 2, + "title": "Vue工具类", + "slug": "vue工具类", + "normalizedTitle": "vue工具类", + "charIndex": 331 + }, + { + "level": 2, + "title": "其他", + "slug": "其他", + "normalizedTitle": "其他", + "charIndex": 469 + } + ], + "headersStr": "React UI 组件库 Vue UI组件库 常用效果组件 工具类 Vue工具类 其他", + "content": "# React UI 组件库\n\n * Ant Design\n * React Bootstrap\n * MATERIAL-UI\n\n\n# Vue UI组件库\n\n * Element UI PC端\n * Vant 移动端\n * View UI\n\n\n# 常用效果组件\n\n * Animate.css 动画库\n * Swiper 轮播组件\n * mescroll 下拉刷新和上拉加载框架-基于原生JS\n\n\n# 工具类\n\n * Lodash.js\n * Day.js 处理日期\n * Timeago.js 相对时间,如N小时前\n * Echarts 百度图表\n * Meditor.md 开源在线 Markdown 编辑器\n * validator.js 验证库\n\n\n# Vue工具类\n\n * vue-draggable 基于Sortable.js实现的vue拖拽插件\n * vue-qr 文本转二维码\n * vue-cropper 图片裁剪插件\n * vue-lazyload 懒加载\n * vue-simple-upload 上传组件\n\n\n# 其他\n\n * H5带笔锋手写签名,支持PC端和移动端", + "normalizedContent": "# react ui 组件库\n\n * ant design\n * react bootstrap\n * material-ui\n\n\n# vue ui组件库\n\n * element ui pc端\n * vant 移动端\n * view ui\n\n\n# 常用效果组件\n\n * animate.css 动画库\n * swiper 轮播组件\n * mescroll 下拉刷新和上拉加载框架-基于原生js\n\n\n# 工具类\n\n * lodash.js\n * day.js 处理日期\n * timeago.js 相对时间,如n小时前\n * echarts 百度图表\n * meditor.md 开源在线 markdown 编辑器\n * validator.js 验证库\n\n\n# vue工具类\n\n * vue-draggable 基于sortable.js实现的vue拖拽插件\n * vue-qr 文本转二维码\n * vue-cropper 图片裁剪插件\n * vue-lazyload 懒加载\n * vue-simple-upload 上传组件\n\n\n# 其他\n\n * h5带笔锋手写签名,支持pc端和移动端", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/06/20, 22:47:41", + "lastUpdatedTimestamp": 1655736461000 + }, + { + "title": "标签", + "frontmatter": { + "tagsPage": true, + "title": "标签", + "permalink": "/tags/", + "article": false + }, + "regularPath": "/@pages/tagsPage.html", + "relativePath": "@pages/tagsPage.md", + "key": "v-64f9b920", + "path": "/tags/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, + { + "title": "分类", + "frontmatter": { + "categoriesPage": true, + "title": "分类", + "permalink": "/categories/", + "article": false + }, + "regularPath": "/@pages/categoriesPage.html", + "relativePath": "@pages/categoriesPage.md", + "key": "v-5f8dfe80", + "path": "/categories/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 } ], "themeConfig": { @@ -6231,21 +6863,9 @@ export const siteData = { "text": "Netty", "link": "/pages/40f3c9/" }, - { - "text": "Spring", - "link": "/pages/434235/" - }, - { - "text": "Mybatis", - "link": "/pages/3793a5/" - }, { "text": "Kafka", "link": "/pages/561f34/" - }, - { - "text": "Dubbo", - "link": "/pages/78517c/" } ] }, @@ -6261,10 +6881,6 @@ export const siteData = { "text": "关于", "link": "/about/" }, - { - "text": "收藏", - "link": "/pages/beb6c0bd8a66cea6/" - }, { "text": "索引", "link": "/archives/", @@ -6514,10 +7130,15 @@ export const siteData = { "collapsable": true, "children": [ [ - "30.JVM/001.Java 中垃圾回收机制中如何判断对象需要回收?常见的 GC 回收算法有哪些?.md", - "Java 中垃圾回收机制中如何判断对象需要回收?常见的 GC 回收算法有哪些?", + "30.JVM/001.Java 中垃圾回收机制中如何判断对象需要回收?.md", + "Java 中垃圾回收机制中如何判断对象需要回收?", "/pages/948850/" ], + [ + "30.JVM/010.常见的 GC 回收算法有哪些?.md", + "常见的 GC 回收算法有哪些?", + "/pages/9b26e8/" + ], [ "30.JVM/020.Java 中如何进行 GC 调优?.md", "Java 中如何进行 GC 调优?", @@ -6626,6 +7247,16 @@ export const siteData = { "10.计算机网络/01.计算机网络.md", "计算机网络", "/pages/e00207/" + ], + [ + "10.计算机网络/010.简述常见的 HTTP 状态码的含义.md", + "简述常见的 HTTP 状态码的含义", + "/pages/f33310/" + ], + [ + "10.计算机网络/020.从输入 URL 到展现页面的全过程.md", + "从输入 URL 到展现页面的全过程", + "/pages/ab48ba/" ] ] }, From 3dabbd72454ac666c76d7706d6c444ef71d18e5a Mon Sep 17 00:00:00 2001 From: admirean <854818303@qq.com> Date: Tue, 20 Sep 2022 22:28:30 +0800 Subject: [PATCH 12/15] =?UTF-8?q?=E5=AE=8C=E5=96=84=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E7=AB=A0=E8=8A=82=20gitTalk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/.vuepress/config.ts | 4 +- ...14\351\245\277\346\261\211\357\274\211.md" | 18 +- ...6\350\256\241\346\250\241\345\274\217 .md" | 68 + ...6\350\256\241\346\250\241\345\274\217 .md" | 10 - ...52\344\276\213\345\255\220\345\220\227.md" | 84 +- ...60\350\200\205\346\250\241\345\274\217.md" | 249 +- ...15\345\231\250\346\250\241\345\274\217.md" | 347 ++ .../core/.temp/internal/app-enhancers.js | 2 +- .../core/.temp/internal/page-components.js | 98 +- .../@vuepress/core/.temp/internal/routes.js | 1026 ++-- .../@vuepress/core/.temp/internal/siteData.js | 4607 +++++++++-------- 11 files changed, 3822 insertions(+), 2691 deletions(-) create mode 100644 "docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/003.\344\273\200\344\271\210\346\230\257\350\256\276\350\256\241\346\250\241\345\274\217 .md" delete mode 100644 "docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/003.\344\273\200\344\271\210\346\230\257\350\256\276\350\256\241\346\250\241\345\274\217\357\274\214\346\217\217\350\277\260\345\207\240\344\270\252\345\270\270\347\224\250\347\232\204\350\256\276\350\256\241\346\250\241\345\274\217 .md" diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts index 6b1952864..ab2b26f17 100644 --- a/docs/.vuepress/config.ts +++ b/docs/.vuepress/config.ts @@ -263,8 +263,8 @@ export default defineConfig4CustomTheme({ { choosen: 'gitalk', options: { - clientID: 'a6e1355287947096b88b', - clientSecret: 'f0e77d070fabfcd5af95bebb82b2d574d7248d71', + clientID: '0da91d0586b0f2ec6868', + clientSecret: 'c8e2f12b870b53708500bd8c368b55c07f915034', repo: 'blog-gitalk-comment', // GitHub 仓库 owner: 'YoungAnnn', // GitHub仓库所有者 admin: ['YoungAnnn'], // 对仓库有写权限的人 diff --git "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/002.\345\256\236\347\216\260\345\215\225\344\276\213\350\256\276\350\256\241\346\250\241\345\274\217\357\274\210\346\207\222\346\261\211\357\274\214\351\245\277\346\261\211\357\274\211.md" "b/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/002.\345\256\236\347\216\260\345\215\225\344\276\213\350\256\276\350\256\241\346\250\241\345\274\217\357\274\210\346\207\222\346\261\211\357\274\214\351\245\277\346\261\211\357\274\211.md" index 5d52caa31..5cda46dc3 100644 --- "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/002.\345\256\236\347\216\260\345\215\225\344\276\213\350\256\276\350\256\241\346\250\241\345\274\217\357\274\210\346\207\222\346\261\211\357\274\214\351\245\277\346\261\211\357\274\211.md" +++ "b/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/002.\345\256\236\347\216\260\345\215\225\344\276\213\350\256\276\350\256\241\346\250\241\345\274\217\357\274\210\346\207\222\346\261\211\357\274\214\351\245\277\346\261\211\357\274\211.md" @@ -16,14 +16,14 @@ tags: 有一些对象我们确实只需要一个,比如,线程池、数据库连接、缓存、日志对象等,如果有多个的话,会造成程序的行为异常,资源使用过量或者不一致的问题。你也许会说,这种我用全局变量不也能实现吗,还整个单例模式,好像你很流弊的样子,如果将对象赋值给一个全局变量,那程序启动就会创建好对象,万一这个对象很耗资源,我们还可能在某些时候用不到,这就造成了资源的浪费,不合理,所以就有了单例模式。 -#单例模式的定义 +## 单例模式的定义 单例模式确保一个类只有一个实例,并提供一个全局唯一访问点 -#单例模式的类图 +## 单例模式的类图 ![](https://tva1.sinaimg.cn/large/e6c9d24egy1h2gwxw15ybj209u064q2y.jpg) -#单例模式的实现 -## 饿汉式 +## 单例模式的实现 +### 饿汉式 static 变量在类装载的时候进行初始化 多个实例的 static 变量会共享同一块内存区域 用这两个知识点写出的单例类就是饿汉式了,初始化类的时候就创建,饥不择食,饿汉 @@ -46,7 +46,7 @@ public class Singleton { Java 实现的单例是一个虚拟机的范围,因为装载类的功能是虚拟机的,所以一个虚拟机通过自己的ClassLoader 装载饿汉式实现单例类的时候就会创建一个类实例。(如果一个虚拟机里有多个ClassLoader的话,就会有多个实例) -##懒汉式 +### 懒汉式 懒汉式,就是实例在用到的时候才去创建,比较“懒” 单例模式的懒汉式实现方式体现了延迟加载的思想(延迟加载也称懒加载Lazy Load,就是一开始不要加载资源或数据,等到要使用的时候才加载) @@ -110,7 +110,7 @@ Java中创建一个对象,往往包含三个过程。对于singleton = new Sin 如果是 1-3-2,则在 3 执行完毕,2 未执行之前,被另一个线程抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以这个线程会直接返回 instance,然后使用,那肯定就会报错了,所以要加入 volatile关键字。 -##静态内部类 +### 静态内部类 ```java public class Singleton { @@ -133,7 +133,7 @@ public class Singleton { 优点:线程安全,利用静态内部类实现延迟加载,效率较高,推荐使用 -##枚举 +### 枚举 ```java enum Singleton{ INSTANCE; @@ -144,12 +144,12 @@ enum Singleton{ 这种方式是《Effective Java》 作者Josh Bloch 提倡的方式。 -#单例模式在JDK 中的源码分析 +## 单例模式在JDK 中的源码分析 JDK 中,java.lang.Runtime 就是经典的单例模式(饿汉式) ![](https://tva1.sinaimg.cn/large/e6c9d24egy1h2gwyaevuxj21al0u044k.jpg) -#单例模式注意事项和细节 +## 单例模式注意事项和细节 单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使 用new 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或 耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数 据库或文件的对象(比如数据源、session工厂等) \ No newline at end of file diff --git "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/003.\344\273\200\344\271\210\346\230\257\350\256\276\350\256\241\346\250\241\345\274\217 .md" "b/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/003.\344\273\200\344\271\210\346\230\257\350\256\276\350\256\241\346\250\241\345\274\217 .md" new file mode 100644 index 000000000..8967a5c45 --- /dev/null +++ "b/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/003.\344\273\200\344\271\210\346\230\257\350\256\276\350\256\241\346\250\241\345\274\217 .md" @@ -0,0 +1,68 @@ +--- +title: 什么是设计模式 +date: 2022-05-21 17:26:19 +permalink: /pages/3d768c/ +categories: + - Java相关 + - 设计模式 +tags: + - +--- +## 什么是设计模式? +设计模式是一套代码设计「经验的总结」。项目中「合理的」运用设计模式可以「巧妙的解决很多问题」。 +* 经验的总结:抱着「代码虐我千百遍,我待代码如初恋」的心态,最终得出来的「套路」。 + +* 合理的:要对设计模式的使用场景有一定的认识后才使用,「不要滥用」。如:输出一句“hello world”,非要强行给加上各种模式。 +问:“为什么”,答:“总感觉少了模式!”。 + +* 巧妙的解决了很多问题:被广泛应用的原因。 + +>为什么要提倡“Design Pattern呢?根本原因是为了代码复用,增加可维护性。那么怎么才能实现代码复用呢? + +## 设计模式之六大原则 +### 开闭原则(Open Close Principle) +1988年,勃兰特·梅耶(Bertrand Meyer)在他的著作《面向对象软件构造(Object Oriented Software Construction)》中提出了开闭原则,它的原文是这样:“Software entities should be open for extension,but closed for modification”。 +意思:软件模块应该对扩展开放,对修改关闭。 +举例:在程序需要进行新增功能的时候,不能去修改原有的代码,而是新增代码,实现一个热插拔的效果(热插拔:灵活的去除或添加功能,不影响到原有的功能)。 +目的:为了使程序的扩展性好,易于维护和升级。 +### 里氏代换原则(Liskov Substitution Principle) +意思:里氏代换原则是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。 +举例:球类,原本是一种体育用品,它的衍生类有篮球、足球、排球、羽毛球等等,如果衍生类替换了基类的原本方法,如把体育用品改成了食用品(那么软件单位的功能受到影响),就不符合里氏代换原则。 +目的:对实现抽象化的具体步骤的规范。 +### 依赖倒转原则(Dependence Inversion Principle) +意思:针对接口编程,而不是针对实现编程。 +举例:以计算机系统为例,无论主板、CPU、内存、硬件都是在针对接口设计的,如果针对实现来设计,内存就要对应到针对某个品牌的主板,那么会出现换内存需要把主板也换掉的尴尬。 +目的:降低模块间的耦合。 +### 接口隔离原则(Interface Segregation Principle) +使用多个隔离的接口,比使用单个接口要好。 +举例:比如:登录,注册时属于用户模块的两个接口,比写成一个接口好。 +目的:提高程序设计灵活性。 +### 迪米特法则(最少知道原则)(Demeter Principle) +1987年秋天由美国Northeastern University的Ian Holland提出,被UML的创始者之一[Booch]等普及。后来,因为在经典著作《 The Pragmatic Programmer》而广为人知。 +意思:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。 +举例:一个类公开的public属性或方法越多,修改时涉及的面也就越大,变更引起的风险扩散也就越大。 +目的:降低类之间的耦合,减少对其他类的依赖。 +### 单一职责原则( Single responsibility principle ) +该原则由罗伯特·C·马丁(Robert C. Martin)于《敏捷软件开发:原则、模式和实践》一书中给出的。马丁表示此原则是基于汤姆·狄马克(Tom DeMarco)和Meilir Page-Jones的著作中的内聚性原则发展出的。 +意思:一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。 +举例:该原则意思简单到不需要举例! +目的:类的复杂性降低,可读性提高,可维护性提高。 + + + + + + + +刚入行的时候,在想什么样的代码是好代码?看到很多前辈的文字都说好的代码要符合「高内聚,低耦合」,但是我听到这样的解释,是这样的 + + + +而现在对设计模式有了一定程度上的学习,感觉懂了一些,小伙伴们你们学会了吗? +## 高内聚,低耦合? +内聚是从功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事。它描述的是模块内的功能联系; +耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及通过接口的数据。 + +参考: +[图解九种常见的设计模式](https://segmentfault.com/a/1190000030850326) +[什么是「设计模式」?](https://zhuanlan.zhihu.com/p/28901918) \ No newline at end of file diff --git "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/003.\344\273\200\344\271\210\346\230\257\350\256\276\350\256\241\346\250\241\345\274\217\357\274\214\346\217\217\350\277\260\345\207\240\344\270\252\345\270\270\347\224\250\347\232\204\350\256\276\350\256\241\346\250\241\345\274\217 .md" "b/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/003.\344\273\200\344\271\210\346\230\257\350\256\276\350\256\241\346\250\241\345\274\217\357\274\214\346\217\217\350\277\260\345\207\240\344\270\252\345\270\270\347\224\250\347\232\204\350\256\276\350\256\241\346\250\241\345\274\217 .md" deleted file mode 100644 index 1745ac3ea..000000000 --- "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/003.\344\273\200\344\271\210\346\230\257\350\256\276\350\256\241\346\250\241\345\274\217\357\274\214\346\217\217\350\277\260\345\207\240\344\270\252\345\270\270\347\224\250\347\232\204\350\256\276\350\256\241\346\250\241\345\274\217 .md" +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: 什么是设计模式,描述几个常用的设计模式 -date: 2022-05-21 17:26:19 -permalink: /pages/3d768c/ -categories: - - Java相关 - - 设计模式 -tags: - - ---- diff --git "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/004.\345\267\245\345\216\202\346\250\241\345\274\217\344\275\277\347\224\250\345\234\272\346\231\257 \350\203\275\344\270\276\344\270\252\344\276\213\345\255\220\345\220\227.md" "b/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/004.\345\267\245\345\216\202\346\250\241\345\274\217\344\275\277\347\224\250\345\234\272\346\231\257 \350\203\275\344\270\276\344\270\252\344\276\213\345\255\220\345\220\227.md" index 01c59fe23..cb6be881a 100644 --- "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/004.\345\267\245\345\216\202\346\250\241\345\274\217\344\275\277\347\224\250\345\234\272\346\231\257 \350\203\275\344\270\276\344\270\252\344\276\213\345\255\220\345\220\227.md" +++ "b/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/004.\345\267\245\345\216\202\346\250\241\345\274\217\344\275\277\347\224\250\345\234\272\346\231\257 \350\203\275\344\270\276\344\270\252\344\276\213\345\255\220\345\220\227.md" @@ -12,19 +12,19 @@ tags: 你知道工厂模式分为几类吗?能举例子吗? 那你说说你们项目中是怎么使用工厂模式的? -#工厂模式 +## 工厂模式 工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。 -## 模式动机 +### 模式动机 考虑一个简单的软件应用场景,一个软件系统可以提供多个外观不同的按钮(如圆形按钮、矩形按钮、菱形按钮等), 这些按钮都源自同一个基类,不过在继承基类后不同的子类修改了部分属性从而使得它们可以呈现不同的外观,如果我们希望在使用这些按钮时,不需要知道这些具体按钮类的名字,只需要知道表示该按钮类的一个参数,并提供一个调用方便的方法,把该参数传入方法即可返回一个相应的按钮对象,此时,就可以使用简单工厂模式。 -## 模式定义 +### 模式定义 简单工厂模式(Simple Factory Pattern):又称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。 -## 模式结构 +### 模式结构 简单工厂模式包含如下角色: Factory:工厂角色 @@ -37,7 +37,7 @@ ConcreteProduct:具体产品角色 1.4. 时序图 ![](https://tva1.sinaimg.cn/large/e6c9d24egy1h2gxepi0hbj20ha079aa9.jpg) -## 模式分析 +### 模式分析 将对象的创建和对象本身业务处理分离可以降低系统的耦合度,使得两者修改起来都相对容易。 在调用工厂类的工厂方法时,由于工厂方法是静态方法,使用起来很方便,可通过类名直接调用,而且只需要传入一个简单的参数即可. @@ -50,7 +50,7 @@ ConcreteProduct:具体产品角色 简单工厂模式的要点在于:当你需要什么,只需要传入一个正确的参数,就可以获取你所需要的对象,而无须知道其创建细节。 -#工厂模式可以分为三类: +## 工厂模式可以分为三类: 简单工厂模式(Simple Factory) 工厂方法模式(Factory Method) 抽象工厂模式(Abstract Factory) @@ -80,11 +80,12 @@ ConcreteProduct:具体产品角色 再次,由于工厂模式是依靠抽象架构的,它把实例化产品的任务交由实现类完成,扩展性比较好。也就是说,当需要系统有比较好的扩展性时,可以考虑工厂模式,不同的产品用不同的实现工厂来组装。 -#一、简单工厂模式 +## 一、简单工厂模式 在介绍简单工厂模式之前,我们尝试解决以下问题: 现在我们要使用面向对象的形式定义计算器,为了实现各算法之间的解耦。我们一般会这么写: +```java // 计算类的基类 @Setter @Getter @@ -125,8 +126,10 @@ public class OperationDiv extends Operation { throw new IllegalArgumentException("除数不能为零"); } } -当我们要使用这个计算器的时候,又会这么写: +``` +当我们要使用这个计算器的时候,又会这么写: +```java public static void main(String[] args) { //计算两数之和 OperationAdd operationAdd = new OperationAdd(); @@ -140,14 +143,15 @@ public static void main(String[] args) { System.out.println("multiply:"+operationMul.getResule()); //计算两数之差。。。 } +``` 想要使用不同的运算的时候就要创建不同的类,并且要明确知道该类的名字。那么这种重复的创建类的工作其实可以放到一个统一的类中去管理。这样的方法我们就叫做「简单工厂模式」,在简单工厂模式中用于创建实例的方法是静态(static)方法,因此简单工厂模式又被称为「静态工厂方法」模式。。简单工厂模式有以下优点: 一个调用者想创建一个对象,只要知道其名称就可以了。 屏蔽产品的具体实现,调用者只关心产品的接口。 -## 定义 +### 定义 提供一个创建对象实例的功能,而无需关心其具体实现。被创建实例的类型可以是接口、抽象类,也可以是具体的类。 -## 简单工厂模式实现方式 +### 简单工厂模式实现方式 没骗你,简单工厂模式,真是因为简单才被叫做简单工厂模式的。 简单工厂模式包含 3 个角色(要素): @@ -155,10 +159,10 @@ public static void main(String[] args) { Factory:即工厂类, 简单工厂模式的核心部分,负责实现创建所有产品的内部逻辑;工厂类可以被外界直接调用,创建所需对象 Product:抽象类产品, 它是工厂类所创建的所有对象的父类,封装了各种产品对象的公有方法,它的引入将提高系统的灵活性,使得在工厂类中只需定义一个通用的工厂方法,因为所有创建的具体产品对象都是其子类对象 ConcreteProduct:具体产品, 它是简单工厂模式的创建目标,所有被创建的对象都充当这个角色的某个具体类的实例。它要实现抽象产品中声明的抽象方法 -#UML类图 +## UML类图 ![](https://tva1.sinaimg.cn/large/e6c9d24egy1h2gxibp4q2j20li0fpq3h.jpg) -#实例 +## 实例 现在我们定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。 ```java //工厂类 @@ -199,12 +203,12 @@ public static void main(String[] args) { ``` 通过简单工厂模式,该计算器的使用者不需要关系实现加法逻辑的那个类的具体名字,只要知道该类对应的参数"add"就可以了。这就体现了之前提到的工厂模式的优点。 -## 简单工厂模式存在的问题 +### 简单工厂模式存在的问题 当我们需要增加一种计算时,例如开平方。这个时候我们需要先定义一个类继承Operation类,其中实现平方的代码。除此之外我们还要修改 OperationFactory 类的代码,增加一个 case。这显然是违背开闭原则的。可想而知对于新产品的加入,工厂类是很被动的。 我们举的例子是最简单的情况。而在实际应用中,很可能产品是一个多层次的树状结构。 简单工厂可能就不太适用了。 -## 简单工厂模式总结 +### 简单工厂模式总结 工厂类是整个简单工厂模式的关键。包含了必要的逻辑判断,根据外界给定的信息,决定究竟应该创建哪个具体类的对象。通过使用工厂类,外界可以从直接创建具体产品对象的尴尬局面摆脱出来,仅仅需要负责“消费”对象就可以了。而不必管这些对象究竟如何创建及如何组织的。明确了各自的职责和权利,有利于整个软件体系结构的优化。 但是由于工厂类集中了所有实例的创建逻辑,违反了高内聚责任分配原则,将全部创建逻辑集中到了一个工厂类中;它所能创建的类只能是事先考虑到的,如果需要添加新的类,则就需要改变工厂类了。 @@ -213,23 +217,23 @@ public static void main(String[] args) { 为了解决这些缺点,就有了工厂方法模式。 -#二、工厂方法模式 +## 二、工厂方法模式 我们常说的工厂模式,就是指「工厂方法模式」,也叫「虚拟构造器模式」或「多态工厂模式」。 -## 2.1 定义 +### 2.1 定义 定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类。工厂方法让类的实例化推迟到子类中进行。 -##2.2 工厂方法模式实现方式 +### 2.2 工厂方法模式实现方式 工厂方法模式包含 4 个角色(要素): Product:抽象产品,定义工厂方法所创建的对象的接口,也就是实际需要使用的对象的接口 ConcreteProduct:具体产品,具体的Product接口的实现对象 Factory:工厂接口,也可以叫 Creator(创建器),申明工厂方法,通常返回一个 Product 类型的实例对象 ConcreteFactory:工厂实现,或者叫 ConcreteCreator(创建器对象),覆盖 Factory 定义的工厂方法,返回具体的 Product 实例 -#UML类图 +## UML类图 ![](https://tva1.sinaimg.cn/large/e6c9d24egy1h2gxjel3b8j20za0haab3.jpg) -#实例 +## 实例 从 UML 类图可以看出,每种产品实现,我们都要增加一个继承于工厂接口 IFactory 的工厂类 Factory ,修改简单工厂模式代码中的工厂类如下: ```java //工厂接口 @@ -286,7 +290,7 @@ public class Client { ``` 纳尼,这不是更复杂了吗,每个产品对应一个工厂,我又不是按代码量赚钱的。。。 -##2.3 工厂方法模式适用场景 +### 2.3 工厂方法模式适用场景 工厂方法模式和简单工厂模式虽然都是通过工厂来创建对象,他们之间最大的不同是——工厂方法模式在设计上完全完全符合“开闭原则”。 在以下情况下可以使用工厂方法模式: @@ -299,7 +303,7 @@ public class Client { 数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。 设计一个连接服务器的框架,需要三个协议,"POP3"、"IMAP"、"HTTP",可以把这三个作为产品类,共同实现一个接口。 比如 Hibernate 换数据库只需换方言和驱动就可以 -##2.4 工厂方法模式总结 +### 2.4 工厂方法模式总结 工厂方法模式是简单工厂模式的进一步抽象和推广。 由于使用了面向对象的多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。 @@ -315,15 +319,15 @@ public class Client { 每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。 -#三、抽象工厂模式 +## 三、抽象工厂模式 工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。此时,我们可以考虑将一些相关的产品组成一个“产品族”,由同一个工厂来统一生产,这就是抽象工厂模式的基本思想。 -##3.1 定义 +### 3.1 定义 为创建一组相关或相互依赖的对象提供一个接口,而且无需指定他们的具体类。 抽象工厂(Abstract Factory)模式,又称工具箱(Kit 或Toolkit)模式。 -##3.2 抽象工厂模式实现方式 +### 3.2 抽象工厂模式实现方式 抽象工厂模式是工厂方法模式的升级版本,他用来创建一组相关或者相互依赖的对象。他与工厂方法模式的区别就在于,工厂方法模式针对的是一个产品等级结构;而抽象工厂模式则是针对的多个产品等级结构。在编程中,通常一个产品结构,表现为一个接口或者抽象类,也就是说,工厂方法模式提供的所有产品都是衍生自同一个接口或抽象类,而抽象工厂模式所提供的产品则是衍生自不同的接口或抽象类。 在抽象工厂模式中,有一个产品族的概念:所谓的产品族,是指位于不同产品等级结构中功能相关联的产品组成的家族。抽象工厂模式所提供的一系列产品就组成一个产品族;而工厂方法提供的一系列产品称为一个等级结构。 @@ -337,60 +341,72 @@ ConcreteFactory:具体工厂,实现抽象工厂定义的方法,具体实 AbstractProduct:抽象产品,定义一类产品对象的接口 ConcreteProduct:具体产品,通常在具体工厂里,会选择具体的产品实现,来创建符合抽象工厂定义的方法返回的产品类型的对象。 Client:客户端,使用抽象工厂来获取一系列所需要的产品对象 -#UML类图 +## UML类图 -#实例 +## 实例 我把维基百科的例子改下用于理解,假设我们要生产两种产品,键盘(Keyboard)和鼠标(Mouse) ,每一种产品都支持多种系列,比如 Mac 系列和 Windows 系列。这样每个系列的产品分别是 MacKeyboard WinKeyboard, MacMouse, WinMouse 。为了可以在运行时刻创建一个系列的产品族,我们可以为每个系列的产品族创建一个工厂 MacFactory 和 WinFactory 。每个工厂都有两个方法 CreateMouse 和 CreateKeyboard 并返回对应的产品,可以将这两个方法抽象成一个接口 HardWare 。这样在运行时刻我们可以选择创建需要的产品系列。 抽象产品 - +```java public interface Keyboard { void input(); } public interface Mouse { void click(); } +``` 具体产品 //具体产品 +```java public class MacKeyboard implements Keyboard { @Override public void input() { System.out.println("Mac 专用键盘"); } } +``` +```java public class MacMouse implements Mouse { @Override public void click() { System.out.println("Mac 专用鼠标"); } } +``` +```java public class WinKeyboard implements Keyboard { @Override public void input() { System.out.println("Win 专用键盘"); } } +``` +```java public class WinMouse implements Mouse { @Override public void click() { System.out.println("win 专用鼠标"); } } +``` 抽象工厂 +```java public interface Hardware { Keyboard createKyeBoard(); Mouse createMouse(); } +``` 具体的工厂类 +```java public class MacFactory implements Hardware{ @Override public Keyboard createKyeBoard() { @@ -402,7 +418,9 @@ public class MacFactory implements Hardware{ return new MacMouse(); } } +``` +```java public class WinFactory implements Hardware{ @Override public Keyboard createKyeBoard() { @@ -414,8 +432,9 @@ public class WinFactory implements Hardware{ return new WinMouse(); } } +``` 使用 - +```java public class Client { public static void main(String[] args) { Hardware macFactory = new MacFactory(); @@ -427,7 +446,8 @@ public class Client { mouse.click(); //win 专用鼠标 } } -#3.3 抽象工厂模式适用场景 +``` +## 3.3 抽象工厂模式适用场景 抽象工厂模式和工厂方法模式一样,都符合开闭原则。但是不同的是,工厂方法模式在增加一个具体产品的时候,都要增加对应的工厂。但是抽象工厂模式只有在新增一个类型的具体产品时才需要新增工厂。也就是说,工厂方法模式的一个工厂只能创建一个具体产品。而抽象工厂模式的一个工厂可以创建属于一类类型的多种具体产品。工厂创建产品的个数介于简单工厂模式和工厂方法模式之间。 在以下情况下可以使用抽象工厂模式: @@ -444,7 +464,7 @@ public class Client { 增加新的产品等级结构:对于增加新的产品等级结构,需要修改所有的工厂角色,包括抽象工厂类,在所有的工厂类中都需要增加生产新产品的方法,违背了“开闭原则”。 正因为抽象工厂模式存在“开闭原则”的倾斜性,它以一种倾斜的方式来满足“开闭原则”,为增加新产品族提供方便,但不能为增加新产品结构提供这样的方便,因此要求设计人员在设计之初就能够全面考虑,不会在设计完成之后向系统中增加新的产品等级结构,也不会删除已有的产品等级结构,否则将会导致系统出现较大的修改,为后续维护工作带来诸多麻烦。 -#3.4 抽象工厂模式总结 +## 3.4 抽象工厂模式总结 抽象工厂模式是工厂方法模式的进一步延伸,由于它提供了功能更为强大的工厂类并且具备较好的可扩展性,在软件开发中得以广泛应用,尤其是在一些框架和API类库的设计中,例如在Java语言的AWT(抽象窗口工具包)中就使用了抽象工厂模式,它使用抽象工厂模式来实现在不同的操作系统中应用程序呈现与所在操作系统一致的外观界面。抽象工厂模式也是在软件开发中最常用的设计模式之一。 优点: @@ -456,10 +476,10 @@ public class Client { 增加新的产品等级结构麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,违背了“开闭原则”。 -#工厂模式的退化 +## 工厂模式的退化 当抽象工厂模式中每一个具体工厂类只创建一个产品对象,也就是只存在一个产品等级结构时,抽象工厂模式退化成工厂方法模式;当工厂方法模式中抽象工厂与具体工厂合并,提供一个统一的工厂来创建产品对象,并将创建对象的工厂方法设计为静态方法时,工厂方法模式退化成简单工厂模式。 -#四、我们身边的工厂模式 +## 四、我们身边的工厂模式 工厂模式在Java码农身边真是无处不在,不信打开你的项目,搜索 Factory 我们最常用的 Spring 就是一个最大的 Bean 工厂,IOC 通过BeanFactory对Bean 进行管理。 diff --git "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/005.\350\243\205\351\245\260\350\200\205\346\250\241\345\274\217.md" "b/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/005.\350\243\205\351\245\260\350\200\205\346\250\241\345\274\217.md" index 0e81d8257..fab01cbed 100644 --- "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/005.\350\243\205\351\245\260\350\200\205\346\250\241\345\274\217.md" +++ "b/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/005.\350\243\205\351\245\260\350\200\205\346\250\241\345\274\217.md" @@ -1,5 +1,5 @@ --- -title: 简述装饰者模式以及适配器模式 +title: 简述装饰者模式 date: 2022-05-21 17:26:42 permalink: /pages/47a4e3/ categories: @@ -8,3 +8,250 @@ categories: tags: - --- +《Head First 设计模式》中是这么形容装饰者模式的——“给爱用继承的人一个全新的设计眼界”,拒绝继承滥用,从装饰者模式开始。 + +装饰者模式允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。 + +这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。 + +## 模式动机 +一般有两种方式可以实现给一个类或对象增加行为: + +* 继承机制,使用继承机制是给现有类添加功能的一种有效途径,通过继承一个现有类可以使得子类在拥有自身方法的同时还拥有父类的方法。但是这种方法是静态的,用户不能控制增加行为的方式和时机。 + +* 关联机制,即将一个类的对象嵌入另一个对象中,由另一个对象来决定是否调用嵌入对象的行为以便扩展自己的行为,我们称这个嵌入的对象为装饰器(Decorator) + +装饰模式以对客户透明的方式动态地给一个对象附加上更多的责任,换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。 + +## 定义 +装饰模式(Decorator Pattern) :动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类(继承)实现更为灵活。其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。 + +## 角色 +* Component: 抽象组件,装饰者和被装饰者共同的父类,是一个接口或者抽象类,用来定义基本行为,可以给这些对象动态添加职责 + +* ConcreteComponent: 具体的组件对象,实现类 ,即被装饰者,通常就是被装饰器装饰的原始对象,也就是可以给这个对象添加职责 + +* Decorator: 所有装饰器的抽象父类,一般是抽象类,实现接口;它的属性必然有个指向 Conponent 抽象组件的对象 ,其实就是持有一个被装饰的对象 + +* ConcreteDecorator: 具体的装饰对象,实现具体要被装饰对象添加的功能。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为。 + +装饰者和被装饰者对象有相同的父类,因为装饰者和被装饰者必须是一样的类型,这里利用继承是为了达到类型匹配,而不是利用继承获得行为。 + +利用继承设计子类,只能在编译时静态决定,并且所有子类都会继承相同的行为;利用组合的做法扩展对象,就可以在运行时动态的进行扩展。装饰者模式遵循开放-关闭原则:**类应该对扩展开放,对修改关闭。**利用装饰者,我们可以实现新的装饰者增加新的行为而不用修改现有代码,而如果单纯依赖继承,每当需要新行为时,还得修改现有的代码。 + +## 类图 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6cb19pa0ij21750tp0v3.jpg) + +## 实例 +看了好多资料的例子,比如 + +* 公司发放奖金,不同的员工类型对应不同的奖金计算规则,用各种计算规则去装饰统一的奖金计算类 +* 星巴克售卖用咖啡,用摩卡、奶泡去装饰咖啡,实现不同的计费 +* 变形金刚在变形之前是一辆汽车,它可以在陆地上移动。当它变成机器人之后除了能够在陆地上移动之外,还可以说话;如果需要,它还可以变成飞机,除了在陆地上移动还可以在天空中飞翔 + +我还是比较喜欢卖煎饼的例子 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6cb1yvnvdj20740740ss.jpg) + + +1、定义抽象组件 +```java +public abstract class Pancake { + + String description = "普通煎饼"; + + public String getDescription(){ + return description; + } + + public abstract double cost(); +} +``` +2、定义具体的被装饰者,这里是煎饼果子,当然还可以有鸡蛋灌饼、手抓饼等其他被装饰者 +```java +public class Battercake extends Pancake { + @Override + public double cost() { + return 8; + } + + public Battercake(){ + description = "煎饼果子"; + } +} +``` +3、抽象的装饰器对象,定义一个调料抽象类 +```java +public abstract class CondimentDecorator extends Pancake { + + // 持有组件对象 + protected Pancake pancake; + public CondimentDecorator(Pancake pancake){ + this.pancake = pancake; + } + + public abstract String getDescription(); +} +``` +4、具体的装饰者,我们定义一个鸡蛋装饰器,一个火腿装饰器 +```java +public class Egg extends CondimentDecorator { + public Egg(Pancake pancake){ + super(pancake); + } + + @Override + public String getDescription() { + return pancake.getDescription() + "加鸡蛋"; + } + + @Override + public double cost() { + return pancake.cost() + 1; + } +} +public class Sausage extends CondimentDecorator{ + public Sausage(Pancake pancake){ + super(pancake); + } + @Override + public String getDescription() { + return pancake.getDescription() + "加火腿"; + } + + @Override + public double cost() { + return pancake.cost() + 2; + } +} +``` +5、测试煎饼交易,over +```java +public class Client { + + public static void main(String[] args) { + //买一个普通的煎饼果子 + Pancake battercake = new Battercake(); + System.out.println(battercake.getDescription() + "花费:"+battercake.cost() + "元"); + + //买一个加双蛋的煎饼果子 + Pancake doubleEgg = new Battercake(); + doubleEgg = new Egg(doubleEgg); + doubleEgg = new Egg(doubleEgg); + System.out.println(doubleEgg.getDescription() + "花费" + doubleEgg.cost() + "元"); + + //加火腿和鸡蛋 + Pancake battercakePlus = new Battercake(); + battercakePlus = new Egg(battercakePlus); + battercakePlus = new Sausage(battercakePlus); + System.out.println(battercakePlus.getDescription() + "花费" + battercakePlus.cost() + "元"); + } +} +``` +输出: +``` +煎饼果子花费:8.0元 +煎饼果子加鸡蛋加鸡蛋花费10.0元 +煎饼果子加鸡蛋加火腿花费11.0元 +``` + +顺便看下通过 IDEA 生成的 UML 类图(和我们画的类图一样哈) +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6cb3r8mp4j218c0pmdi0.jpg) + + +## 应用 +### Java I/O 中的装饰者模式 +我们使用 `java.io` 包下的各种输入流、输出流、字节流、字符流、缓冲流等各种各样的流,他们中的许多类都是装饰者,下面是一个典型的对象集合,用装饰者将功能结合起来,以读取文件数据 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6cb3xjz90j20ls0auwf8.jpg) + +`BufferedInputStream` 和 `LinerNumberInputStream` 都是扩展自 `FilterInputStream`,而 `FilterInputStream` 是一个抽象的装饰类。 + +在 `idea` 中选中一些常见 `InputStream` 类,生成 UML 图如下: +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6cb4jjz64j222e0regqv.jpg) + + +我们平时读取一个文件中的内容其实就使用到了装饰模式的思想,简化《Head First 设计模式》的例子,我们自定义一个装饰者,把输入流中的所有大写字符转换为小写 +```java +public class LowerCaseInputStream extends FilterInputStream { + + protected LowerCaseInputStream(InputStream in) { + super(in); + } + + public int read() throws IOException { + int c = super.read(); + return (c == -1 ? c:Character.toLowerCase(c)); + } +} +``` +```java +public class InputTest { + + public static void main(String[] args) throws IOException { + int c; + //装饰器的组装过程 + InputStream in = new LowerCaseInputStream(new BufferedInputStream(new FileInputStream("JavaKeeper.txt"))); + + while ((c = in.read()) >= 0){ + System.out.print((char) c); + } + in.close(); + } +} +``` +采用装饰者模式在实例化组件时,将增加代码的复杂度,一旦使用装饰者模式,不只需要实例化组件,还把把此组件包装进装饰者中,天晓得有几个,所以在某些复杂情况下,我们还会结合工厂模式和生成器模式。比如Spring中的装饰者模式。 + +### Servlet 中的装饰者模式 +Servlet API 源自于 4 个实现类,它很少被使用,但是十分强大:`ServletRequestWrapper`、`ServletResponseWrapper`以及 `HttpServletRequestWrapper`、`HttpServletResponseWrapper`。 + +比如 `ServletRequestWrapper` 是 `ServletRequest` 接口的简单实现,开发者可以继承 `ServletRequestWrapper` 去扩展原来的 `request` +```java +public class ServletRequestWrapper implements ServletRequest { + private ServletRequest request; + + public ServletRequestWrapper(ServletRequest request) { + if (request == null) { + throw new IllegalArgumentException("Request cannot be null"); + } else { + this.request = request; + } + } + //....... +} +``` +### spring 中的装饰者模式 +`Spring` 的 `ApplicationContext` 中配置所有的 `DataSource`。 这些 `DataSource` 可能是各种不同类型的, 比如不同的数据库: `Oracle`、 `SQL Server`、 `MySQL` 等, 也可能是不同的数据源。 然后 `SessionFactory` 根据客户的每次请求, 将 `DataSource` 属性设置成不同的数据源, 以达到切换数据源的目的。 + +在 `Spring` 的命名体现:`Spring` 中用到的包装器模式在类名上有两种表现: 一种是类名中含有 `Wrapper`, 另一种是类名中含有 `Decorator`。 基本上都是动态地给一个对象添加一些额外的职责,比如 + +`org.springframework.cache.transaction` 包下的 `TransactionAwareCacheDecorator` 类 +`org.springframework.session.web.http` 包下的 `SessionRepositoryFilter` 内部类 `SessionRepositoryRequestWrapper` +### Mybatis 缓存中的装饰者模式 +`Mybatis` 的缓存模块中,使用了装饰器模式的变体,其中将 `Decorator` 接口和 `Componet` 接口合并为一个 `Component` 接口。`org.apache.ibatis.cache` 包下的结构 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6cb89i5iwj20kw0r6gmo.jpg) + +## 总结 +装饰模式的本质:动态组合 + +动态组合是手段,组合才是目的。这里的组合有两个意思,一个是动态功能的组合,也就是动态进行装饰器的组合;另外一个是指对象组合,通过对象组合来实现为被装饰对象透明的增加功能。 + +## 优缺点 +装饰模式的优点: + +* 装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。 +* 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的装饰器,从而实现不同的行为。 +* 通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更为强大的对象。 +* 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开闭原则” + +装饰模式的缺点: + +* 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度。 +* 这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。 +## 何时选用 +* 如果需要在不影响其他对象的情况下,以动态、透明的方式给对象添加职责,可以使用装饰模式 +* 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类已定义为不能被继承(如 Java 语言中的 final 类) + +参考: +[装饰模式——看看 JDK 和 Spring 是如何杜绝继承滥用的](https://javakeeper.starfish.ink/design-pattern/Decorator-Pattern.html) \ No newline at end of file diff --git "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/010.\351\200\202\351\205\215\345\231\250\346\250\241\345\274\217.md" "b/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/010.\351\200\202\351\205\215\345\231\250\346\250\241\345\274\217.md" index fe3b7c189..d91e6e69e 100644 --- "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/010.\351\200\202\351\205\215\345\231\250\346\250\241\345\274\217.md" +++ "b/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/010.\351\200\202\351\205\215\345\231\250\346\250\241\345\274\217.md" @@ -8,3 +8,350 @@ categories: tags: - --- +## 问题 +假设我们在做一套股票看盘系统,数据提供方给我们提供 XML 格式数据,我们获取数据用来显示,随着系统的迭代,我们要整合一些第三方系统的对外数据,但是他们只提供获取 JSON 格式的数据接口。 + +在不想改变原有代码逻辑的情况下,如何解决呢? + +这时候我们就可以创建一个「适配器」。这是一个特殊的对象, 能够转换对象接口, 使其能与其他对象进行交互。 + +适配器模式通过封装对象将复杂的转换过程隐藏于幕后。 被封装的对象甚至察觉不到适配器的存在。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6cam85konj20w60de0ti.jpg) + +## 真实世界类比 +适配器是什么,不难理解,生活中也随处可见。比如,笔记本电脑的电源适配器、万能充(曾经的它真有一个这么牛逼的名字)、一拖十数据线等等。 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6camnwis8j21bo0b440r.jpg) + + +## 基本介绍 +* 适配器模式将一个类的接口,转换成客户期望的另外一个接口。适配器让原本接口不兼容的类可以合作无间。也可以叫包装器(Wrapper) + +* 适配器模式是一种结构型设计模式, 它能使接口不兼容的对象能够相互合作 + +* 主要分为两类:类适配器模式、对象适配器模式 + +## 工作原理 +* 适配器模式:将一个类的接口转换成另一种接口,让原本接口不兼容的类可以兼容 +* 从用户的角度看不到被适配者,是解耦的 +* 用户调用适配器转化出来的目标接口方法,适配器再调用被适配者的相关接口方法 +* 用户收到反馈结果,感觉只是和目标接口交互 +## 适配器模式结构 +### 对象适配器 +实现时使用了构成原则: 适配器实现了其中一个对象的接口, 并对另一个对象进行封装。 所有流行的编程语言都可以实现适配器。 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6canxj50gj212q0mktaj.jpg) + +适配器设计模式的结构(对象适配器) + +* 客户端 (Client) 是包含当前程序业务逻辑的类。 +* 客户端接口 (Target) 描述了其他类与客户端代码合作时必须遵循的协议。 +* 服务 (Service) 中有一些功能类 (通常来自第三方或遗留系统)。 客户端与其接口不兼容, 因此无法直接调用其功能,也可以叫适配者类(Adaptee)。 +* 适配器 (Adapter) 是一个可以同时与客户端和服务交互的类: 它在实现客户端接口的同时封装了服务对象。 适配器接受客户端通过适配器接口发起的调用, 并将其转换为适用于被封装服务对象的调用。 +* 客户端代码只需通过接口与适配器交互即可, 无需与具体的适配器类耦合。 因此, 你可以向程序中添加新类型的适配器而无需修改已有代码。 这在服务类的接口被更改或替换时很有用: 你无需修改客户端代码就可以创建新的适配器类。 +### Coding +定义客户端使用的接口,与业务相关 + +```java +public interface Target { + + /* + * 客户端请求处理的方法 + */ + void request(); +} +``` +已经存在的接口,这个接口需要配置 +```java +public class Adaptee { + + /* + * 原本存在的方法 + */ + public void specificRequest(){ + //业务代码 + } +} +``` +适配器类 +```java +public class Adapter implements Target { + + /* + * 持有需要被适配的接口对象 + */ + private Adaptee adaptee; + + /* + * 构造方法,传入需要被适配的对象 + * @param adaptee 需要被适配的对象 + */ + public Adapter(Adaptee adaptee) { + this.adaptee = adaptee; + } + + @Override + public void request() { + // TODO Auto-generated method stub + adaptee.specificRequest(); + } + +} +``` +使用适配器的客户端 +```java +public class Client { + + public static void main(String[] args) { + //创建需要被适配的对象 + Adaptee adaptee = new Adaptee(); + //创建客户端需要调用的接口对象 + Target target = new Adapter(adaptee); + //请求处理 + target.request(); + } +} +``` +## 类适配器 +这一实现使用了继承机制: 适配器同时继承两个对象的接口。 请注意, 这种方式仅能在支持多重继承的编程语言中实现,例如 C++, Java 不支持多重继承,也就没有这种适配器了。 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6capjm071j214e0kwwg9.jpg) + +适配器设计模式(类适配器) + +类适配器不需要封装任何对象, 因为它同时继承了客户端和服务的行为。 适配功能在重写的方法中完成。 最后生成的适配器可替代已有的客户端类进行使用。 + +## Coding +Java 虽然不能实现标准的类适配器,但是有一种变通的方式,也能够使用继承来实现接口的适配,那就是让适配器去实现 Target 的接口,然后继承 Adaptee 的实现,虽然不是十分标准,但意思差不多。 + +首先有一个已存在的将被适配的类 +```java +public class Adaptee { + public void adapteeRequest() { + System.out.println("被适配者的方法"); + } +} +``` +定义客户端使用的接口,与业务相关 +```java +public interface Target { + + void request(); +} +``` +怎么才可以在目标接口中的 request() 调用 Adaptee 的 adapteeRequest() 方法呢?直接实现 Target 肯定是不行的,所以我们通过一个适配器类,实现 Target 接口,同时继承了 Adaptee 类,然后在实现的 request() 方法中调用父类的 adapteeRequest() 即可 +```java +public class Adapter extends Adaptee implements Target{ + @Override + public void request() { + //...一些操作... + super.adapteeRequest(); + //...一些操作... + } +} +``` +使用适配器的客户端 +```java +public class Client { + public static void main(String[] args) { + + Target adapterTarget = new Adapter(); + adapterTarget.request(); + } +} +``` +## 适配器模式适合应用场景 +当你希望使用某个类, 但是其接口与其他代码不兼容时, 可以使用适配器类。 + +适配器模式允许你创建一个中间层类, 其可作为代码与遗留类、 第三方类或提供怪异接口的类之间的转换器。 + +如果您需要复用这样一些类, 他们处于同一个继承体系, 并且他们又有了额外的一些共同的方法, 但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。 + +你可以扩展每个子类, 将缺少的功能添加到新的子类中。 但是, 你必须在所有新子类中重复添加这些代码, 这样会使得代码有坏味道。 + +将缺失功能添加到一个适配器类中是一种优雅得多的解决方案。 然后你可以将缺少功能的对象封装在适配器中, 从而动态地获取所需功能。 如要这一点正常运作, 目标类必须要有通用接口, 适配器的成员变量应当遵循该通用接口。 这种方式同装饰模式非常相似。 + +## demo +用一个生活中的充电器的例子来讲解下适配器,我国民用电都是 220V,而我们的手机充电一般需要 5V。 + +220V 的交流电相当于被适配者 Adaptee,我们的目标 Target 是 5V 直流电,充电器本身相当于一个 Adapter,将220V 的输入电压变换为 5V 输出。 + +首先是我们的民用电(我国是 220V,当然还可以有其他国家的其他准备,可随时扩展) + +```java +public class Volatage220V { + + public final int output = 220; + + public int output220v() { + System.out.println("输出电压 " + output); + return output; + } +} +``` +适配接口 +```java +public interface IVoltage5V { + int output5V(); +} +``` +我们的手机充电,只支持 5V 电压 +```java +public class Phone { + + public void charging(IVoltage5V v) { + if (v.output5V() == 5) { + System.out.println("电压 5V ,符合充电标准,开始充电"); + } else { + System.out.println("电压不符合标准,无法充电"); + } + } +} +``` +适配器 +```java +public class VoltageAdapter implements IVoltage5V { + + private Volatage220V volatage220V; //聚合 + + public VoltageAdapter(Volatage220V v) { + this.volatage220V = v; + } + + @Override + public int output5V() { + int dst = 0; + if (null != volatage220V) { + int src = volatage220V.output220v(); + System.out.println("适配器工作~~~~~"); + dst = src / 44; + System.out.println("适配器工作完成,输出电压" + dst); + } + return dst; + } +} +``` +工作,如果去国外旅游,有不同的电压,只需要扩展适配器即可。 +```java +public class Client { + public static void main(String[] args) { + Phone phone = new Phone(); + phone.charging(new VoltageAdapter(new Volatage220V())); + } +} +``` +## 适配器模式优缺点 +单一职责原则,你可以将接口或数据转换代码从程序主要业务逻辑中分离。 + +开闭原则。 只要客户端代码通过客户端接口与适配器进行交互, 你就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。 + +代码整体复杂度增加, 因为你需要新增一系列接口和类。 有时直接更改服务类使其与其他代码兼容会更简单。 + +## Spring 中的适配器 +Spring 源码中搜关键字Adapter 会出现很多实现类,SpringMVC 中的 HandlerAdapter ,就是适配器的应用。 + +我们先回顾下 SpringMVC 处理流程: +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6carvcn2oj20vq0ij0v8.jpg) + +Spring MVC 中的适配器模式主要用于执行目标 `Controller` 中的请求处理方法。 + +在`Spring MVC`中,`DispatcherServlet` 作为用户,`HandlerAdapter` 作为期望接口,具体的适配器实现类用于对目标类进行适配,`Controller` 作为需要适配的类。 + +为什么要在 `Spring MVC` 中使用适配器模式?`Spring MVC` 中的 `Controller` 种类众多,不同类型的 `Controller` 通过不同的方法来对请求进行处理。如果不利用适配器模式的话,DispatcherServlet 直接获取对应类型的 Controller,需要的自行来判断,像下面这段代码一样: + +```java +if(mappedHandler.getHandler() instanceof MultiActionController){ + ((MultiActionController)mappedHandler.getHandler()).xxx +}else if(mappedHandler.getHandler() instanceof XXX){ + ... +}else if(...){ + ... +} +``` +这样假设如果我们增加一个 `Controller`,就要在代码中加入一行 `if` 语句,这种形式就使得程序难以维护,也违反了设计模式中的开闭原则 – 对扩展开放,对修改关闭。 + +我们通过源码看看 `SpringMVC` 是如何实现的,首先看下核心类 `DispatcherServlet`: +```java +public class DispatcherServlet extends FrameworkServlet { + //...... + //维护所有HandlerAdapter类的集合 + @Nullable + private List handlerAdapters; + + //初始化handlerAdapters + private void initHandlerAdapters(ApplicationContext context) { + this.handlerAdapters = null; + if (this.detectAllHandlerAdapters) { + Map matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false); + if (!matchingBeans.isEmpty()) { + this.handlerAdapters = new ArrayList(matchingBeans.values()); + AnnotationAwareOrderComparator.sort(this.handlerAdapters); + } + } else { + try { + HandlerAdapter ha = (HandlerAdapter)context.getBean("handlerAdapter", HandlerAdapter.class); + this.handlerAdapters = Collections.singletonList(ha); + } catch (NoSuchBeanDefinitionException var3) { + } + } + + if (this.handlerAdapters == null) { + this.handlerAdapters = this.getDefaultStrategies(context, HandlerAdapter.class); + if (this.logger.isTraceEnabled()) { + this.logger.trace("No HandlerAdapters declared for servlet '" + this.getServletName() + "': using default strategies from DispatcherServlet.properties"); + } + } + } + + //dispatch 方法中会获取 HandlerAdapter + protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + //... + + //获得controller对应的适配器 + HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler()); + + //调用适配器的handler方法处理请求,并返回ModelAndView + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + //... + } + + //返回对应的controller的处理器 + protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException { + if (this.handlerAdapters != null) { + Iterator var2 = this.handlerAdapters.iterator(); + + while(var2.hasNext()) { + HandlerAdapter adapter = (HandlerAdapter)var2.next(); + if (adapter.supports(handler)) { + return adapter; + } + } + } + } +``` +接着看下 `HandlerAdapter` 的源码,也就是适配器接口: +```java +public interface HandlerAdapter { + boolean supports(Object var1); + + @Nullable + ModelAndView handle(HttpServletRequest var1, HttpServletResponse var2, Object var3) throws Exception; + + long getLastModified(HttpServletRequest var1, Object var2); +} +``` +再来屡一下这个流程: + +首先是适配器接口 `DispatchServlet` 中有一个集合维护所有的 `HandlerAdapter`,如果配置文件中没有对适配器进行配置,那么 `DispatchServlet` 会在创建时对该变量进行初始化,注册所有默认的 `HandlerAdapter`。 + +当一个请求过来时,`DispatchServlet` 会根据传过来的 handler 类型从该集合中寻找对应的 `HandlerAdapter`子类进行处理,并且调用它的 `handler()` 方法 +对应的 `HandlerAdapter` 中的 `handler()` 方法又会执行对应 `Controller` 的 `handleRequest()` 方法 + +适配器与 `handler` 有对应关系,而各个适配器又都是适配器接口的实现类,因此,它们都遵循相同的适配器标准,所以用户可以按照相同的方式,通过不同的 `handler` 去处理请求。 + +当然了,`Spring` 框架中也为我们定义了一些默认的 `Handler` 对应的适配器。 + + +通过适配器模式我们将所有的 `controller` 统一交给 `HandlerAdapter` 处理,免去了写大量的 `if-else` 语句对 `Controller` 进行判断,也更利于扩展新的 `Controller` 类型。 + + +参考: +[随遇而安的适配器模式 | Spring 中的适配器](https://javakeeper.starfish.ink/design-pattern/Adapter-Pattern.html#%E9%97%AE%E9%A2%98) \ No newline at end of file diff --git a/node_modules/@vuepress/core/.temp/internal/app-enhancers.js b/node_modules/@vuepress/core/.temp/internal/app-enhancers.js index 368895c79..256fdff06 100644 --- a/node_modules/@vuepress/core/.temp/internal/app-enhancers.js +++ b/node_modules/@vuepress/core/.temp/internal/app-enhancers.js @@ -2,7 +2,7 @@ import m0 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_module import m1 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_modules/@vuepress/core/.temp/app-enhancers/1.js" import m2 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_modules/@vuepress/core/.temp/app-enhancers/2.js" import m3 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_modules/@vuepress/core/.temp/app-enhancers/data-block.js" -import m4 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_modules/@vuepress/core/.temp/app-enhancers/global-components-1.js" +import m4 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_modules/@vuepress/core/.temp/app-enhancers/global-components-23.js" import m5 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_modules/@vuepress/core/.temp/app-enhancers/3.js" import m6 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_modules/@vuepress/core/.temp/app-enhancers/4.js" import m7 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_modules/@vuepress/core/.temp/app-enhancers/5.js" diff --git a/node_modules/@vuepress/core/.temp/internal/page-components.js b/node_modules/@vuepress/core/.temp/internal/page-components.js index 213ea8820..3beb00e19 100644 --- a/node_modules/@vuepress/core/.temp/internal/page-components.js +++ b/node_modules/@vuepress/core/.temp/internal/page-components.js @@ -3,130 +3,130 @@ */ export default { "v-36edf5bb": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/00.目录页/01.Java相关.md"), - "v-10f5d0e5": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/00.目录页/02.计算机基础.md"), "v-a612ff08": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/00.目录页/03.数据库.md"), - "v-7abe58c4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/00.目录页/04.中间件.md"), + "v-10f5d0e5": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/00.目录页/02.计算机基础.md"), "v-8bf153e4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/001.Java 异常有哪些类型?.md"), - "v-0734374b": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/010.HashMap 与 ConcurrentHashMap 的实现原理是怎样的?.md"), + "v-7abe58c4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/00.目录页/04.中间件.md"), "v-4e1fd39c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/020.Java 中接口和抽象类的区别.md"), - "v-57012c44": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/030.为什么重写 equals 必须重写 hashcode ?.md"), + "v-7f48b402": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/050.String,StringBuffer,StringBuilder 之间有什么区别?.md"), "v-d8a2f15c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/040.== 和 equals() 的区别?.md"), + "v-57012c44": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/030.为什么重写 equals 必须重写 hashcode ?.md"), "v-57b2551a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/060.简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度.md"), - "v-7f48b402": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/050.String,StringBuffer,StringBuilder 之间有什么区别?.md"), "v-d4e20148": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/070.简述 BIO, NIO, AIO 的区别.md"), + "v-0734374b": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/010.HashMap 与 ConcurrentHashMap 的实现原理是怎样的?.md"), "v-d32e07ea": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/080.在一个静态方法内调用一个非静态成员为什么是非法的?.md"), "v-85b06162": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/090.简述 Java 的反射机制及其应用场景.md"), - "v-7665e30e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/110.什么是双亲委派机制?.md"), "v-4c273a66": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/100.Java 类的加载流程是怎样的?.md"), "v-6ff6e2f0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/120.Java 中 sleep() 与 wait() 的区别 .md"), - "v-620ebadc": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/130.简述 Java 中 final 关键字的作用.md"), "v-5290507e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/140.String 类能不能被继承?为什么?.md"), + "v-620ebadc": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/130.简述 Java 中 final 关键字的作用.md"), + "v-7665e30e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/110.什么是双亲委派机制?.md"), "v-6f5c6512": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/150.Java 缓冲流 buffer 的用途和原理是什么?.md"), "v-75844e97": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/160.简述动态代理与静态代理 .md"), - "v-b21bbc4c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/180.简述 HashSet 实现原理.md"), "v-1218fa85": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/170.阻塞队列都有哪几种,有什么区别?.md"), - "v-45fc1217": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/001.线程池是如何实现的?.md"), - "v-35324a50": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/030.简述 Synchronized,Volatile,可重入锁的不同使用场景及优缺点.md"), + "v-b21bbc4c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/180.简述 HashSet 实现原理.md"), "v-5390e3d4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/020.简述 CAS 原理,什么是 ABA 问题,怎么解决?.md"), "v-352f36cc": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/040.Synchronized 关键字底层是如何实现的?.md"), + "v-35324a50": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/030.简述 Synchronized,Volatile,可重入锁的不同使用场景及优缺点.md"), "v-f5bcd180": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/060.volatile 关键字解决了什么问题,它的实现原理是什么?.md"), + "v-45fc1217": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/001.线程池是如何实现的?.md"), "v-4927c049": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/050.重入锁是如何实现的?.md"), - "v-1065f7be": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/070.简述 Java 锁升级的机制.md"), - "v-2dbdb772": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/080.简述 Java AQS 的原理以及使用场景.md"), "v-134f28dc": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/090.什么是公平锁?什么是非公平锁?.md"), + "v-2dbdb772": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/080.简述 Java AQS 的原理以及使用场景.md"), + "v-1065f7be": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/070.简述 Java 锁升级的机制.md"), "v-03e420c4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/100.Java 的线程有哪些状态,转换关系是怎么样的?.md"), "v-625fbeda": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/110.Java 是如何实现线程安全的,哪些数据结构是线程安全的?.md"), - "v-9baaad8e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/120.为什么我们不能直接调用 run() 方法?.md"), "v-7b2a2ed4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/115.手写死锁.md"), - "v-083aef82": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/140.手写生产者消费者模型.md"), + "v-9baaad8e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/120.为什么我们不能直接调用 run() 方法?.md"), "v-75c00225": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/130.Java 线程有哪些常用方法?.md"), + "v-083aef82": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/140.手写生产者消费者模型.md"), "v-f406b4e6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/150.ThreadLocal 实现原理是什么?为什么要使用弱引用?.md"), "v-7cd97c82": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/001.Java 中垃圾回收机制中如何判断对象需要回收?.md"), "v-af821fc8": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/010.常见的 GC 回收算法有哪些?.md"), + "v-2de19204": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/040.JVM 内存是如何对应到操作系统内存的?.md"), "v-5474bec0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/020.Java 中如何进行 GC 调优?.md"), "v-0970cd54": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/030.JMM 中内存模型是怎样的?什么是指令序列重排序?.md"), - "v-2de19204": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/040.JVM 内存是如何对应到操作系统内存的?.md"), - "v-15fba8f1": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/060.什么是内存泄漏,怎么确定内存泄漏?.md"), - "v-37fcb440": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/050.JVM 是怎么去调优的?了解哪些参数和指令?.md"), - "v-28c1d6d9": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/070.简述 CMS 与 G1 机制的区别.md"), "v-ba94c306": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/002.实现单例设计模式(懒汉,饿汉).md"), - "v-504a5213": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/003.什么是设计模式,描述几个常用的设计模式 .md"), - "v-2d6ed001": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/004.工厂模式使用场景 能举个例子吗.md"), - "v-22a9eaef": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/005.装饰者模式.md"), - "v-31509c80": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/010.适配器模式.md"), + "v-28c1d6d9": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/070.简述 CMS 与 G1 机制的区别.md"), + "v-37fcb440": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/050.JVM 是怎么去调优的?了解哪些参数和指令?.md"), "v-0564ba27": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/020.代理模式.md"), + "v-88b087a6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/003.什么是设计模式 .md"), + "v-15fba8f1": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/060.什么是内存泄漏,怎么确定内存泄漏?.md"), "v-34f8aaa2": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/030.责任链模式.md"), - "v-64f1031c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/060.观察模式.md"), + "v-22a9eaef": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/005.装饰者模式.md"), + "v-31509c80": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/010.适配器模式.md"), + "v-2d6ed001": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/004.工厂模式使用场景 能举个例子吗.md"), "v-28deb9da": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/040.模板方法模式.md"), "v-2279e49d": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/070.代理模式.md"), + "v-64f1031c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/060.观察模式.md"), "v-5b0157cf": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/050.策略模式.md"), - "v-1e653940": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/01.计算机网络.md"), "v-dd5f4440": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/080.建造者模式.md"), - "v-0a1d1fb4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/010.简述常见的 HTTP 状态码的含义.md"), - "v-6602e3da": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/020.从输入 URL 到展现页面的全过程.md"), "v-6f178cbc": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/01.操作系统.md"), - "v-49688930": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/30.数据结构/01.数据结构.md"), + "v-0a1d1fb4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/010.简述常见的 HTTP 状态码的含义.md"), "v-2e6d42b4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/40.算法/01.算法.md"), - "v-f0f711c4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/006.聚簇索引和非聚簇索引有什么区别?.md"), + "v-49688930": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/30.数据结构/01.数据结构.md"), "v-674454ac": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/001.MySQL中 InnoDB 和 MylSAM 的区别是什么?.md"), "v-824b776c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/007.简述 MySQL 常见索引类型,介绍一下覆盖索引.md"), + "v-1e653940": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/01.计算机网络.md"), + "v-f0f711c4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/006.聚簇索引和非聚簇索引有什么区别?.md"), "v-5b0e09c6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/015.说一下Mysql锁.md"), - "v-7e02c4a1": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/010.简述Mysql的事务隔离级别.md"), - "v-6c681639": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/020.MySQL 为什么使用 B+ 树来作索引,对比 B 树它的优点和缺点是什么.md"), "v-c39c4fe2": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/030.简述乐观锁以及悲观锁的区别以及使用场景.md"), + "v-7e02c4a1": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/010.简述Mysql的事务隔离级别.md"), "v-689e142a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/040.简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?.md"), + "v-6c681639": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/020.MySQL 为什么使用 B+ 树来作索引,对比 B 树它的优点和缺点是什么.md"), + "v-4ecddbec": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/050.简述什么是最左匹配原则.md"), + "v-0dd47f09": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/070.产生死锁的必要条件有哪些?如何解决死锁?.md"), "v-505fbe2e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/060.MySQL 联合索引底层原理是什么?.md"), "v-53bcbb7a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/080.简述常见的负载均衡算法.md"), - "v-0dd47f09": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/070.产生死锁的必要条件有哪些?如何解决死锁?.md"), - "v-4ecddbec": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/050.简述什么是最左匹配原则.md"), "v-c383b96e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/090.简述 MySQL 的主从同步机制,如果同步失败会怎么样?.md"), "v-ed0c93b8": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/110.简述数据库中什么情况下进行分库,什么情况下进行分表?.md"), "v-d6bc240a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/120.数据库的读写分离的作用是什么?如何实现?.md"), - "v-f3acaa1a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/130.简述 undo log 和 redo log 的作用.md"), - "v-4cef944a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/100.如何设计数据库压测方案?.md"), "v-ceed6ba6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/140.什么是 SQL 注入攻击?如何防止这类攻击?.md"), "v-77790024": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/150.MySQL 中 join 与 left join 的区别是什么?.md"), - "v-0a7e6416": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/170.MySQL 中 varchar 和 char 的区别是什么?.md"), + "v-f3acaa1a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/130.简述 undo log 和 redo log 的作用.md"), "v-76f5e79a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/160.简述 MySQL MVCC 的实现原理 .md"), + "v-4cef944a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/100.如何设计数据库压测方案?.md"), + "v-0a7e6416": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/170.MySQL 中 varchar 和 char 的区别是什么?.md"), "v-6b967a6e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/180.模糊查询是如何实现的?.md"), - "v-d804fba0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/001.Redis 有几种数据结构?Zset 是如何实现的?.md"), "v-cb742458": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/002.为什么 Redis 在单线程下能如此快?.md"), - "v-3e917471": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/005.简述 Redis 字符串的底层结构.md"), + "v-d804fba0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/001.Redis 有几种数据结构?Zset 是如何实现的?.md"), "v-c0bc7b7a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/010.Redis的缓存淘汰策略有哪些?.md"), - "v-7c214d00": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/020.简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点.md"), - "v-3d2fdd6a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/040.简述 Redis 集群配置以及基础原理.md"), + "v-3e917471": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/005.简述 Redis 字符串的底层结构.md"), "v-2c6b511c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/030.Redis 如何实现分布式锁?.md"), + "v-3d2fdd6a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/040.简述 Redis 集群配置以及基础原理.md"), "v-18b427a0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/050.简述 Redis 中跳表的应用以及优缺点.md"), "v-6a45f68d": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/060.Redis 中,sentinel 和 cluster 的区别和适用场景是什么?.md"), - "v-35718f4e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/080.简述 Redis 的线程模型以及底层架构设计.md"), "v-c2fe5e1a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/070.简述 Redis 中如何防止缓存雪崩和缓存击穿.md"), - "v-57957421": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/100.简述 Redis 的哨兵机制.md"), + "v-35718f4e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/080.简述 Redis 的线程模型以及底层架构设计.md"), "v-37f158ba": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/110.简述 Redis 如何处理热点 key 访问.md"), + "v-57957421": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/100.简述 Redis 的哨兵机制.md"), + "v-7c214d00": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/020.简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点.md"), + "v-5acdab4d": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/010.TCP粘包拆包问题的解决之道.md"), "v-be02b3c0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/001.你熟悉哪些IO模型?.md"), "v-02ea33c3": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/120.Redis 序列化有哪些方式?.md"), - "v-5acdab4d": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/010.TCP粘包拆包问题的解决之道.md"), "v-073f22cd": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/015.Netty中有哪些解码器?.md"), - "v-bbcf6e5a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/020.为什么Netty要自己写一个ByteBuf.md"), "v-5157927c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/030.ChannelPipeline和ChannelHandler.md"), - "v-62f3fa3e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/040.EventLoop和EventLoopGroup.md"), "v-6b32ada0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/050.Java NIO了解吗?.md"), + "v-62f3fa3e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/040.EventLoop和EventLoopGroup.md"), + "v-bbcf6e5a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/020.为什么Netty要自己写一个ByteBuf.md"), "v-7c0136cf": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/010.生产者.md"), "v-0df39df8": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/001.Kafka包含那几部分.md"), "v-04af9e42": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/020.消费者.md"), "v-0c440466": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/030.集群下的Kafka要考虑哪些事情.md"), - "v-684c4815": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/040.如何保证Kafka的可靠性.md"), - "v-a9558ec0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/08.系统设计/001.系统设计.md"), "v-5675b06b": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/08.系统设计/010.什么是 CAP ?什么是最终一致性?什么是幂等操作?.md"), + "v-a9558ec0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/08.系统设计/001.系统设计.md"), + "v-684c4815": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/040.如何保证Kafka的可靠性.md"), "v-f88544f4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/08.系统设计/020.简述什么是两阶段提交?.md"), "v-918dfaf8": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/15.关于/01.关于.md"), "v-2a52a762": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/10.非技术/01.非技术问题.md"), + "v-09c150e6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/20.收藏夹/02.常用的前端轮子.md"), "v-48f7eec0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/archivesPage.md"), "v-7ab50b9f": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/20.收藏夹/01.网站.md"), - "v-a5c5f278": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/_posts/随笔/我做了一个手写春联小网页,祝大家虎年暴富.md"), "v-2db11cb2": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/_posts/随笔/你知道的越多,不知道的也就越多.md"), + "v-a5c5f278": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/_posts/随笔/我做了一个手写春联小网页,祝大家虎年暴富.md"), "v-7d77d69c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/_posts/随笔/拥抱生活,拥抱快乐.md"), "v-2cf94a0e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/index.md"), - "v-09c150e6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/20.收藏夹/02.常用的前端轮子.md"), "v-64f9b920": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/tagsPage.md"), - "v-5f8dfe80": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/categoriesPage.md") + "v-5f8dfe80": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/categoriesPage.md"), + "v-6602e3da": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/020.从输入 URL 到展现页面的全过程.md") } \ No newline at end of file diff --git a/node_modules/@vuepress/core/.temp/internal/routes.js b/node_modules/@vuepress/core/.temp/internal/routes.js index 3d4dc5abe..1d59a8b45 100644 --- a/node_modules/@vuepress/core/.temp/internal/routes.js +++ b/node_modules/@vuepress/core/.temp/internal/routes.js @@ -24,22 +24,6 @@ export const routes = [ path: "/00.目录页/01.Java相关.html", redirect: "/java/" }, - { - name: "v-10f5d0e5", - path: "/basic/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-10f5d0e5").then(next) - }, - }, - { - path: "/basic/index.html", - redirect: "/basic/" - }, - { - path: "/00.目录页/02.计算机基础.html", - redirect: "/basic/" - }, { name: "v-a612ff08", path: "/database/", @@ -57,20 +41,20 @@ export const routes = [ redirect: "/database/" }, { - name: "v-7abe58c4", - path: "/middleware/", + name: "v-10f5d0e5", + path: "/basic/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-7abe58c4").then(next) + ensureAsyncComponentsLoaded("Layout", "v-10f5d0e5").then(next) }, }, { - path: "/middleware/index.html", - redirect: "/middleware/" + path: "/basic/index.html", + redirect: "/basic/" }, { - path: "/00.目录页/04.中间件.html", - redirect: "/middleware/" + path: "/00.目录页/02.计算机基础.html", + redirect: "/basic/" }, { name: "v-8bf153e4", @@ -89,20 +73,20 @@ export const routes = [ redirect: "/pages/a030ff/" }, { - name: "v-0734374b", - path: "/pages/d1c564/", + name: "v-7abe58c4", + path: "/middleware/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-0734374b").then(next) + ensureAsyncComponentsLoaded("Layout", "v-7abe58c4").then(next) }, }, { - path: "/pages/d1c564/index.html", - redirect: "/pages/d1c564/" + path: "/middleware/index.html", + redirect: "/middleware/" }, { - path: "/01.Java相关/10.基础/010.HashMap 与 ConcurrentHashMap 的实现原理是怎样的?.html", - redirect: "/pages/d1c564/" + path: "/00.目录页/04.中间件.html", + redirect: "/middleware/" }, { name: "v-4e1fd39c", @@ -121,20 +105,20 @@ export const routes = [ redirect: "/pages/826c8b/" }, { - name: "v-57012c44", - path: "/pages/0d22ad/", + name: "v-7f48b402", + path: "/pages/211d8c/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-57012c44").then(next) + ensureAsyncComponentsLoaded("Layout", "v-7f48b402").then(next) }, }, { - path: "/pages/0d22ad/index.html", - redirect: "/pages/0d22ad/" + path: "/pages/211d8c/index.html", + redirect: "/pages/211d8c/" }, { - path: "/01.Java相关/10.基础/030.为什么重写 equals 必须重写 hashcode ?.html", - redirect: "/pages/0d22ad/" + path: "/01.Java相关/10.基础/050.String,StringBuffer,StringBuilder 之间有什么区别?.html", + redirect: "/pages/211d8c/" }, { name: "v-d8a2f15c", @@ -153,36 +137,36 @@ export const routes = [ redirect: "/pages/06b28a/" }, { - name: "v-57b2551a", - path: "/pages/305919/", + name: "v-57012c44", + path: "/pages/0d22ad/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-57b2551a").then(next) + ensureAsyncComponentsLoaded("Layout", "v-57012c44").then(next) }, }, { - path: "/pages/305919/index.html", - redirect: "/pages/305919/" + path: "/pages/0d22ad/index.html", + redirect: "/pages/0d22ad/" }, { - path: "/01.Java相关/10.基础/060.简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度.html", - redirect: "/pages/305919/" + path: "/01.Java相关/10.基础/030.为什么重写 equals 必须重写 hashcode ?.html", + redirect: "/pages/0d22ad/" }, { - name: "v-7f48b402", - path: "/pages/211d8c/", + name: "v-57b2551a", + path: "/pages/305919/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-7f48b402").then(next) + ensureAsyncComponentsLoaded("Layout", "v-57b2551a").then(next) }, }, { - path: "/pages/211d8c/index.html", - redirect: "/pages/211d8c/" + path: "/pages/305919/index.html", + redirect: "/pages/305919/" }, { - path: "/01.Java相关/10.基础/050.String,StringBuffer,StringBuilder 之间有什么区别?.html", - redirect: "/pages/211d8c/" + path: "/01.Java相关/10.基础/060.简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度.html", + redirect: "/pages/305919/" }, { name: "v-d4e20148", @@ -200,6 +184,22 @@ export const routes = [ path: "/01.Java相关/10.基础/070.简述 BIO, NIO, AIO 的区别.html", redirect: "/pages/276740/" }, + { + name: "v-0734374b", + path: "/pages/d1c564/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-0734374b").then(next) + }, + }, + { + path: "/pages/d1c564/index.html", + redirect: "/pages/d1c564/" + }, + { + path: "/01.Java相关/10.基础/010.HashMap 与 ConcurrentHashMap 的实现原理是怎样的?.html", + redirect: "/pages/d1c564/" + }, { name: "v-d32e07ea", path: "/pages/56314c/", @@ -232,22 +232,6 @@ export const routes = [ path: "/01.Java相关/10.基础/090.简述 Java 的反射机制及其应用场景.html", redirect: "/pages/fae5d5/" }, - { - name: "v-7665e30e", - path: "/pages/38bcaf/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-7665e30e").then(next) - }, - }, - { - path: "/pages/38bcaf/index.html", - redirect: "/pages/38bcaf/" - }, - { - path: "/01.Java相关/10.基础/110.什么是双亲委派机制?.html", - redirect: "/pages/38bcaf/" - }, { name: "v-4c273a66", path: "/pages/33c5d6/", @@ -280,6 +264,22 @@ export const routes = [ path: "/01.Java相关/10.基础/120.Java 中 sleep() 与 wait() 的区别 .html", redirect: "/pages/d69890/" }, + { + name: "v-5290507e", + path: "/pages/73113f/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-5290507e").then(next) + }, + }, + { + path: "/pages/73113f/index.html", + redirect: "/pages/73113f/" + }, + { + path: "/01.Java相关/10.基础/140.String 类能不能被继承?为什么?.html", + redirect: "/pages/73113f/" + }, { name: "v-620ebadc", path: "/pages/2b00a3/", @@ -297,20 +297,20 @@ export const routes = [ redirect: "/pages/2b00a3/" }, { - name: "v-5290507e", - path: "/pages/73113f/", + name: "v-7665e30e", + path: "/pages/38bcaf/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-5290507e").then(next) + ensureAsyncComponentsLoaded("Layout", "v-7665e30e").then(next) }, }, { - path: "/pages/73113f/index.html", - redirect: "/pages/73113f/" + path: "/pages/38bcaf/index.html", + redirect: "/pages/38bcaf/" }, { - path: "/01.Java相关/10.基础/140.String 类能不能被继承?为什么?.html", - redirect: "/pages/73113f/" + path: "/01.Java相关/10.基础/110.什么是双亲委派机制?.html", + redirect: "/pages/38bcaf/" }, { name: "v-6f5c6512", @@ -344,22 +344,6 @@ export const routes = [ path: "/01.Java相关/10.基础/160.简述动态代理与静态代理 .html", redirect: "/pages/37dba4/" }, - { - name: "v-b21bbc4c", - path: "/pages/75552c/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-b21bbc4c").then(next) - }, - }, - { - path: "/pages/75552c/index.html", - redirect: "/pages/75552c/" - }, - { - path: "/01.Java相关/10.基础/180.简述 HashSet 实现原理.html", - redirect: "/pages/75552c/" - }, { name: "v-1218fa85", path: "/pages/e9ce4e/", @@ -377,36 +361,20 @@ export const routes = [ redirect: "/pages/e9ce4e/" }, { - name: "v-45fc1217", - path: "/pages/0da4b0/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-45fc1217").then(next) - }, - }, - { - path: "/pages/0da4b0/index.html", - redirect: "/pages/0da4b0/" - }, - { - path: "/01.Java相关/20.并发/001.线程池是如何实现的?.html", - redirect: "/pages/0da4b0/" - }, - { - name: "v-35324a50", - path: "/pages/97245f/", + name: "v-b21bbc4c", + path: "/pages/75552c/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-35324a50").then(next) + ensureAsyncComponentsLoaded("Layout", "v-b21bbc4c").then(next) }, }, { - path: "/pages/97245f/index.html", - redirect: "/pages/97245f/" + path: "/pages/75552c/index.html", + redirect: "/pages/75552c/" }, { - path: "/01.Java相关/20.并发/030.简述 Synchronized,Volatile,可重入锁的不同使用场景及优缺点.html", - redirect: "/pages/97245f/" + path: "/01.Java相关/10.基础/180.简述 HashSet 实现原理.html", + redirect: "/pages/75552c/" }, { name: "v-5390e3d4", @@ -440,6 +408,22 @@ export const routes = [ path: "/01.Java相关/20.并发/040.Synchronized 关键字底层是如何实现的?.html", redirect: "/pages/d5d161/" }, + { + name: "v-35324a50", + path: "/pages/97245f/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-35324a50").then(next) + }, + }, + { + path: "/pages/97245f/index.html", + redirect: "/pages/97245f/" + }, + { + path: "/01.Java相关/20.并发/030.简述 Synchronized,Volatile,可重入锁的不同使用场景及优缺点.html", + redirect: "/pages/97245f/" + }, { name: "v-f5bcd180", path: "/pages/383264/", @@ -456,6 +440,22 @@ export const routes = [ path: "/01.Java相关/20.并发/060.volatile 关键字解决了什么问题,它的实现原理是什么?.html", redirect: "/pages/383264/" }, + { + name: "v-45fc1217", + path: "/pages/0da4b0/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-45fc1217").then(next) + }, + }, + { + path: "/pages/0da4b0/index.html", + redirect: "/pages/0da4b0/" + }, + { + path: "/01.Java相关/20.并发/001.线程池是如何实现的?.html", + redirect: "/pages/0da4b0/" + }, { name: "v-4927c049", path: "/pages/9b59d1/", @@ -473,20 +473,20 @@ export const routes = [ redirect: "/pages/9b59d1/" }, { - name: "v-1065f7be", - path: "/pages/ff0616/", + name: "v-134f28dc", + path: "/pages/fd6a59/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-1065f7be").then(next) + ensureAsyncComponentsLoaded("Layout", "v-134f28dc").then(next) }, }, { - path: "/pages/ff0616/index.html", - redirect: "/pages/ff0616/" + path: "/pages/fd6a59/index.html", + redirect: "/pages/fd6a59/" }, { - path: "/01.Java相关/20.并发/070.简述 Java 锁升级的机制.html", - redirect: "/pages/ff0616/" + path: "/01.Java相关/20.并发/090.什么是公平锁?什么是非公平锁?.html", + redirect: "/pages/fd6a59/" }, { name: "v-2dbdb772", @@ -505,20 +505,20 @@ export const routes = [ redirect: "/pages/52a687/" }, { - name: "v-134f28dc", - path: "/pages/fd6a59/", + name: "v-1065f7be", + path: "/pages/ff0616/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-134f28dc").then(next) + ensureAsyncComponentsLoaded("Layout", "v-1065f7be").then(next) }, }, { - path: "/pages/fd6a59/index.html", - redirect: "/pages/fd6a59/" + path: "/pages/ff0616/index.html", + redirect: "/pages/ff0616/" }, { - path: "/01.Java相关/20.并发/090.什么是公平锁?什么是非公平锁?.html", - redirect: "/pages/fd6a59/" + path: "/01.Java相关/20.并发/070.简述 Java 锁升级的机制.html", + redirect: "/pages/ff0616/" }, { name: "v-03e420c4", @@ -552,22 +552,6 @@ export const routes = [ path: "/01.Java相关/20.并发/110.Java 是如何实现线程安全的,哪些数据结构是线程安全的?.html", redirect: "/pages/c1f760/" }, - { - name: "v-9baaad8e", - path: "/pages/13856f/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-9baaad8e").then(next) - }, - }, - { - path: "/pages/13856f/index.html", - redirect: "/pages/13856f/" - }, - { - path: "/01.Java相关/20.并发/120.为什么我们不能直接调用 run() 方法?.html", - redirect: "/pages/13856f/" - }, { name: "v-7b2a2ed4", path: "/pages/ce292c/", @@ -585,20 +569,20 @@ export const routes = [ redirect: "/pages/ce292c/" }, { - name: "v-083aef82", - path: "/pages/2b8c6e/", + name: "v-9baaad8e", + path: "/pages/13856f/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-083aef82").then(next) + ensureAsyncComponentsLoaded("Layout", "v-9baaad8e").then(next) }, }, { - path: "/pages/2b8c6e/index.html", - redirect: "/pages/2b8c6e/" + path: "/pages/13856f/index.html", + redirect: "/pages/13856f/" }, { - path: "/01.Java相关/20.并发/140.手写生产者消费者模型.html", - redirect: "/pages/2b8c6e/" + path: "/01.Java相关/20.并发/120.为什么我们不能直接调用 run() 方法?.html", + redirect: "/pages/13856f/" }, { name: "v-75c00225", @@ -616,6 +600,22 @@ export const routes = [ path: "/01.Java相关/20.并发/130.Java 线程有哪些常用方法?.html", redirect: "/pages/a6c84e/" }, + { + name: "v-083aef82", + path: "/pages/2b8c6e/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-083aef82").then(next) + }, + }, + { + path: "/pages/2b8c6e/index.html", + redirect: "/pages/2b8c6e/" + }, + { + path: "/01.Java相关/20.并发/140.手写生产者消费者模型.html", + redirect: "/pages/2b8c6e/" + }, { name: "v-f406b4e6", path: "/pages/b3e720/", @@ -664,6 +664,22 @@ export const routes = [ path: "/01.Java相关/30.JVM/010.常见的 GC 回收算法有哪些?.html", redirect: "/pages/9b26e8/" }, + { + name: "v-2de19204", + path: "/pages/da65e7/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-2de19204").then(next) + }, + }, + { + path: "/pages/da65e7/index.html", + redirect: "/pages/da65e7/" + }, + { + path: "/01.Java相关/30.JVM/040.JVM 内存是如何对应到操作系统内存的?.html", + redirect: "/pages/da65e7/" + }, { name: "v-5474bec0", path: "/pages/07a030/", @@ -697,36 +713,36 @@ export const routes = [ redirect: "/pages/c8e6cb/" }, { - name: "v-2de19204", - path: "/pages/da65e7/", + name: "v-ba94c306", + path: "/pages/b37ad7/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-2de19204").then(next) + ensureAsyncComponentsLoaded("Layout", "v-ba94c306").then(next) }, }, { - path: "/pages/da65e7/index.html", - redirect: "/pages/da65e7/" + path: "/pages/b37ad7/index.html", + redirect: "/pages/b37ad7/" }, { - path: "/01.Java相关/30.JVM/040.JVM 内存是如何对应到操作系统内存的?.html", - redirect: "/pages/da65e7/" + path: "/01.Java相关/40.设计模式/002.实现单例设计模式(懒汉,饿汉).html", + redirect: "/pages/b37ad7/" }, { - name: "v-15fba8f1", - path: "/pages/3ab5f5/", + name: "v-28c1d6d9", + path: "/pages/be69af/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-15fba8f1").then(next) + ensureAsyncComponentsLoaded("Layout", "v-28c1d6d9").then(next) }, }, { - path: "/pages/3ab5f5/index.html", - redirect: "/pages/3ab5f5/" + path: "/pages/be69af/index.html", + redirect: "/pages/be69af/" }, { - path: "/01.Java相关/30.JVM/060.什么是内存泄漏,怎么确定内存泄漏?.html", - redirect: "/pages/3ab5f5/" + path: "/01.Java相关/30.JVM/070.简述 CMS 与 G1 机制的区别.html", + redirect: "/pages/be69af/" }, { name: "v-37fcb440", @@ -745,68 +761,68 @@ export const routes = [ redirect: "/pages/d730dd/" }, { - name: "v-28c1d6d9", - path: "/pages/be69af/", + name: "v-0564ba27", + path: "/pages/dd3a6f/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-28c1d6d9").then(next) + ensureAsyncComponentsLoaded("Layout", "v-0564ba27").then(next) }, }, { - path: "/pages/be69af/index.html", - redirect: "/pages/be69af/" + path: "/pages/dd3a6f/index.html", + redirect: "/pages/dd3a6f/" }, { - path: "/01.Java相关/30.JVM/070.简述 CMS 与 G1 机制的区别.html", - redirect: "/pages/be69af/" + path: "/01.Java相关/40.设计模式/020.代理模式.html", + redirect: "/pages/dd3a6f/" }, { - name: "v-ba94c306", - path: "/pages/b37ad7/", + name: "v-88b087a6", + path: "/pages/3d768c/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-ba94c306").then(next) + ensureAsyncComponentsLoaded("Layout", "v-88b087a6").then(next) }, }, { - path: "/pages/b37ad7/index.html", - redirect: "/pages/b37ad7/" + path: "/pages/3d768c/index.html", + redirect: "/pages/3d768c/" }, { - path: "/01.Java相关/40.设计模式/002.实现单例设计模式(懒汉,饿汉).html", - redirect: "/pages/b37ad7/" + path: "/01.Java相关/40.设计模式/003.什么是设计模式 .html", + redirect: "/pages/3d768c/" }, { - name: "v-504a5213", - path: "/pages/3d768c/", + name: "v-15fba8f1", + path: "/pages/3ab5f5/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-504a5213").then(next) + ensureAsyncComponentsLoaded("Layout", "v-15fba8f1").then(next) }, }, { - path: "/pages/3d768c/index.html", - redirect: "/pages/3d768c/" + path: "/pages/3ab5f5/index.html", + redirect: "/pages/3ab5f5/" }, { - path: "/01.Java相关/40.设计模式/003.什么是设计模式,描述几个常用的设计模式 .html", - redirect: "/pages/3d768c/" + path: "/01.Java相关/30.JVM/060.什么是内存泄漏,怎么确定内存泄漏?.html", + redirect: "/pages/3ab5f5/" }, { - name: "v-2d6ed001", - path: "/pages/f15489/", + name: "v-34f8aaa2", + path: "/pages/d3ff35/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-2d6ed001").then(next) + ensureAsyncComponentsLoaded("Layout", "v-34f8aaa2").then(next) }, }, { - path: "/pages/f15489/index.html", - redirect: "/pages/f15489/" + path: "/pages/d3ff35/index.html", + redirect: "/pages/d3ff35/" }, { - path: "/01.Java相关/40.设计模式/004.工厂模式使用场景 能举个例子吗.html", - redirect: "/pages/f15489/" + path: "/01.Java相关/40.设计模式/030.责任链模式.html", + redirect: "/pages/d3ff35/" }, { name: "v-22a9eaef", @@ -841,52 +857,20 @@ export const routes = [ redirect: "/pages/827fc4/" }, { - name: "v-0564ba27", - path: "/pages/dd3a6f/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-0564ba27").then(next) - }, - }, - { - path: "/pages/dd3a6f/index.html", - redirect: "/pages/dd3a6f/" - }, - { - path: "/01.Java相关/40.设计模式/020.代理模式.html", - redirect: "/pages/dd3a6f/" - }, - { - name: "v-34f8aaa2", - path: "/pages/d3ff35/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-34f8aaa2").then(next) - }, - }, - { - path: "/pages/d3ff35/index.html", - redirect: "/pages/d3ff35/" - }, - { - path: "/01.Java相关/40.设计模式/030.责任链模式.html", - redirect: "/pages/d3ff35/" - }, - { - name: "v-64f1031c", - path: "/pages/cbff3c/", + name: "v-2d6ed001", + path: "/pages/f15489/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-64f1031c").then(next) + ensureAsyncComponentsLoaded("Layout", "v-2d6ed001").then(next) }, }, { - path: "/pages/cbff3c/index.html", - redirect: "/pages/cbff3c/" + path: "/pages/f15489/index.html", + redirect: "/pages/f15489/" }, { - path: "/01.Java相关/40.设计模式/060.观察模式.html", - redirect: "/pages/cbff3c/" + path: "/01.Java相关/40.设计模式/004.工厂模式使用场景 能举个例子吗.html", + redirect: "/pages/f15489/" }, { name: "v-28deb9da", @@ -921,36 +905,36 @@ export const routes = [ redirect: "/pages/a3c735/" }, { - name: "v-5b0157cf", - path: "/pages/78c7aa/", + name: "v-64f1031c", + path: "/pages/cbff3c/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-5b0157cf").then(next) + ensureAsyncComponentsLoaded("Layout", "v-64f1031c").then(next) }, }, { - path: "/pages/78c7aa/index.html", - redirect: "/pages/78c7aa/" + path: "/pages/cbff3c/index.html", + redirect: "/pages/cbff3c/" }, { - path: "/01.Java相关/40.设计模式/050.策略模式.html", - redirect: "/pages/78c7aa/" + path: "/01.Java相关/40.设计模式/060.观察模式.html", + redirect: "/pages/cbff3c/" }, { - name: "v-1e653940", - path: "/pages/e00207/", + name: "v-5b0157cf", + path: "/pages/78c7aa/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-1e653940").then(next) + ensureAsyncComponentsLoaded("Layout", "v-5b0157cf").then(next) }, }, { - path: "/pages/e00207/index.html", - redirect: "/pages/e00207/" + path: "/pages/78c7aa/index.html", + redirect: "/pages/78c7aa/" }, { - path: "/02.计算机基础/10.计算机网络/01.计算机网络.html", - redirect: "/pages/e00207/" + path: "/01.Java相关/40.设计模式/050.策略模式.html", + redirect: "/pages/78c7aa/" }, { name: "v-dd5f4440", @@ -968,38 +952,6 @@ export const routes = [ path: "/01.Java相关/40.设计模式/080.建造者模式.html", redirect: "/pages/c332ae/" }, - { - name: "v-0a1d1fb4", - path: "/pages/f33310/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-0a1d1fb4").then(next) - }, - }, - { - path: "/pages/f33310/index.html", - redirect: "/pages/f33310/" - }, - { - path: "/02.计算机基础/10.计算机网络/010.简述常见的 HTTP 状态码的含义.html", - redirect: "/pages/f33310/" - }, - { - name: "v-6602e3da", - path: "/pages/ab48ba/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-6602e3da").then(next) - }, - }, - { - path: "/pages/ab48ba/index.html", - redirect: "/pages/ab48ba/" - }, - { - path: "/02.计算机基础/10.计算机网络/020.从输入 URL 到展现页面的全过程.html", - redirect: "/pages/ab48ba/" - }, { name: "v-6f178cbc", path: "/pages/e5f2d7/", @@ -1017,20 +969,20 @@ export const routes = [ redirect: "/pages/e5f2d7/" }, { - name: "v-49688930", - path: "/pages/a25c3a/", + name: "v-0a1d1fb4", + path: "/pages/f33310/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-49688930").then(next) + ensureAsyncComponentsLoaded("Layout", "v-0a1d1fb4").then(next) }, }, { - path: "/pages/a25c3a/index.html", - redirect: "/pages/a25c3a/" + path: "/pages/f33310/index.html", + redirect: "/pages/f33310/" }, { - path: "/02.计算机基础/30.数据结构/01.数据结构.html", - redirect: "/pages/a25c3a/" + path: "/02.计算机基础/10.计算机网络/010.简述常见的 HTTP 状态码的含义.html", + redirect: "/pages/f33310/" }, { name: "v-2e6d42b4", @@ -1049,20 +1001,20 @@ export const routes = [ redirect: "/pages/92288a/" }, { - name: "v-f0f711c4", - path: "/pages/a5adb0/", + name: "v-49688930", + path: "/pages/a25c3a/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-f0f711c4").then(next) + ensureAsyncComponentsLoaded("Layout", "v-49688930").then(next) }, }, { - path: "/pages/a5adb0/index.html", - redirect: "/pages/a5adb0/" + path: "/pages/a25c3a/index.html", + redirect: "/pages/a25c3a/" }, { - path: "/03.数据库/01.MYSQL相关/006.聚簇索引和非聚簇索引有什么区别?.html", - redirect: "/pages/a5adb0/" + path: "/02.计算机基础/30.数据结构/01.数据结构.html", + redirect: "/pages/a25c3a/" }, { name: "v-674454ac", @@ -1097,52 +1049,52 @@ export const routes = [ redirect: "/pages/17b014/" }, { - name: "v-5b0e09c6", - path: "/pages/b17e88/", + name: "v-1e653940", + path: "/pages/e00207/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-5b0e09c6").then(next) + ensureAsyncComponentsLoaded("Layout", "v-1e653940").then(next) }, }, { - path: "/pages/b17e88/index.html", - redirect: "/pages/b17e88/" + path: "/pages/e00207/index.html", + redirect: "/pages/e00207/" }, { - path: "/03.数据库/01.MYSQL相关/015.说一下Mysql锁.html", - redirect: "/pages/b17e88/" + path: "/02.计算机基础/10.计算机网络/01.计算机网络.html", + redirect: "/pages/e00207/" }, { - name: "v-7e02c4a1", - path: "/pages/9f2936/", + name: "v-f0f711c4", + path: "/pages/a5adb0/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-7e02c4a1").then(next) + ensureAsyncComponentsLoaded("Layout", "v-f0f711c4").then(next) }, }, { - path: "/pages/9f2936/index.html", - redirect: "/pages/9f2936/" + path: "/pages/a5adb0/index.html", + redirect: "/pages/a5adb0/" }, { - path: "/03.数据库/01.MYSQL相关/010.简述Mysql的事务隔离级别.html", - redirect: "/pages/9f2936/" + path: "/03.数据库/01.MYSQL相关/006.聚簇索引和非聚簇索引有什么区别?.html", + redirect: "/pages/a5adb0/" }, { - name: "v-6c681639", - path: "/pages/bc5538/", + name: "v-5b0e09c6", + path: "/pages/b17e88/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-6c681639").then(next) + ensureAsyncComponentsLoaded("Layout", "v-5b0e09c6").then(next) }, }, { - path: "/pages/bc5538/index.html", - redirect: "/pages/bc5538/" + path: "/pages/b17e88/index.html", + redirect: "/pages/b17e88/" }, { - path: "/03.数据库/01.MYSQL相关/020.MySQL 为什么使用 B+ 树来作索引,对比 B 树它的优点和缺点是什么.html", - redirect: "/pages/bc5538/" + path: "/03.数据库/01.MYSQL相关/015.说一下Mysql锁.html", + redirect: "/pages/b17e88/" }, { name: "v-c39c4fe2", @@ -1160,6 +1112,22 @@ export const routes = [ path: "/03.数据库/01.MYSQL相关/030.简述乐观锁以及悲观锁的区别以及使用场景.html", redirect: "/pages/57561d/" }, + { + name: "v-7e02c4a1", + path: "/pages/9f2936/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-7e02c4a1").then(next) + }, + }, + { + path: "/pages/9f2936/index.html", + redirect: "/pages/9f2936/" + }, + { + path: "/03.数据库/01.MYSQL相关/010.简述Mysql的事务隔离级别.html", + redirect: "/pages/9f2936/" + }, { name: "v-689e142a", path: "/pages/36672c/", @@ -1177,36 +1145,36 @@ export const routes = [ redirect: "/pages/36672c/" }, { - name: "v-505fbe2e", - path: "/pages/52773a/", + name: "v-6c681639", + path: "/pages/bc5538/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-505fbe2e").then(next) + ensureAsyncComponentsLoaded("Layout", "v-6c681639").then(next) }, }, { - path: "/pages/52773a/index.html", - redirect: "/pages/52773a/" + path: "/pages/bc5538/index.html", + redirect: "/pages/bc5538/" }, { - path: "/03.数据库/01.MYSQL相关/060.MySQL 联合索引底层原理是什么?.html", - redirect: "/pages/52773a/" + path: "/03.数据库/01.MYSQL相关/020.MySQL 为什么使用 B+ 树来作索引,对比 B 树它的优点和缺点是什么.html", + redirect: "/pages/bc5538/" }, { - name: "v-53bcbb7a", - path: "/pages/f2dbbe/", + name: "v-4ecddbec", + path: "/pages/7a1f1e/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-53bcbb7a").then(next) + ensureAsyncComponentsLoaded("Layout", "v-4ecddbec").then(next) }, }, { - path: "/pages/f2dbbe/index.html", - redirect: "/pages/f2dbbe/" + path: "/pages/7a1f1e/index.html", + redirect: "/pages/7a1f1e/" }, { - path: "/03.数据库/01.MYSQL相关/080.简述常见的负载均衡算法.html", - redirect: "/pages/f2dbbe/" + path: "/03.数据库/01.MYSQL相关/050.简述什么是最左匹配原则.html", + redirect: "/pages/7a1f1e/" }, { name: "v-0dd47f09", @@ -1225,20 +1193,36 @@ export const routes = [ redirect: "/pages/f4cf50/" }, { - name: "v-4ecddbec", - path: "/pages/7a1f1e/", + name: "v-505fbe2e", + path: "/pages/52773a/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-4ecddbec").then(next) + ensureAsyncComponentsLoaded("Layout", "v-505fbe2e").then(next) }, }, { - path: "/pages/7a1f1e/index.html", - redirect: "/pages/7a1f1e/" + path: "/pages/52773a/index.html", + redirect: "/pages/52773a/" }, { - path: "/03.数据库/01.MYSQL相关/050.简述什么是最左匹配原则.html", - redirect: "/pages/7a1f1e/" + path: "/03.数据库/01.MYSQL相关/060.MySQL 联合索引底层原理是什么?.html", + redirect: "/pages/52773a/" + }, + { + name: "v-53bcbb7a", + path: "/pages/f2dbbe/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-53bcbb7a").then(next) + }, + }, + { + path: "/pages/f2dbbe/index.html", + redirect: "/pages/f2dbbe/" + }, + { + path: "/03.数据库/01.MYSQL相关/080.简述常见的负载均衡算法.html", + redirect: "/pages/f2dbbe/" }, { name: "v-c383b96e", @@ -1288,38 +1272,6 @@ export const routes = [ path: "/03.数据库/01.MYSQL相关/120.数据库的读写分离的作用是什么?如何实现?.html", redirect: "/pages/f62359/" }, - { - name: "v-f3acaa1a", - path: "/pages/7bbbf6/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-f3acaa1a").then(next) - }, - }, - { - path: "/pages/7bbbf6/index.html", - redirect: "/pages/7bbbf6/" - }, - { - path: "/03.数据库/01.MYSQL相关/130.简述 undo log 和 redo log 的作用.html", - redirect: "/pages/7bbbf6/" - }, - { - name: "v-4cef944a", - path: "/pages/a8eb47/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-4cef944a").then(next) - }, - }, - { - path: "/pages/a8eb47/index.html", - redirect: "/pages/a8eb47/" - }, - { - path: "/03.数据库/01.MYSQL相关/100.如何设计数据库压测方案?.html", - redirect: "/pages/a8eb47/" - }, { name: "v-ceed6ba6", path: "/pages/b4338e/", @@ -1353,20 +1305,20 @@ export const routes = [ redirect: "/pages/429100/" }, { - name: "v-0a7e6416", - path: "/pages/d97948/", + name: "v-f3acaa1a", + path: "/pages/7bbbf6/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-0a7e6416").then(next) + ensureAsyncComponentsLoaded("Layout", "v-f3acaa1a").then(next) }, }, { - path: "/pages/d97948/index.html", - redirect: "/pages/d97948/" + path: "/pages/7bbbf6/index.html", + redirect: "/pages/7bbbf6/" }, { - path: "/03.数据库/01.MYSQL相关/170.MySQL 中 varchar 和 char 的区别是什么?.html", - redirect: "/pages/d97948/" + path: "/03.数据库/01.MYSQL相关/130.简述 undo log 和 redo log 的作用.html", + redirect: "/pages/7bbbf6/" }, { name: "v-76f5e79a", @@ -1385,36 +1337,52 @@ export const routes = [ redirect: "/pages/1656d8/" }, { - name: "v-6b967a6e", - path: "/pages/32696d/", + name: "v-4cef944a", + path: "/pages/a8eb47/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-6b967a6e").then(next) + ensureAsyncComponentsLoaded("Layout", "v-4cef944a").then(next) }, }, { - path: "/pages/32696d/index.html", - redirect: "/pages/32696d/" + path: "/pages/a8eb47/index.html", + redirect: "/pages/a8eb47/" }, { - path: "/03.数据库/01.MYSQL相关/180.模糊查询是如何实现的?.html", - redirect: "/pages/32696d/" + path: "/03.数据库/01.MYSQL相关/100.如何设计数据库压测方案?.html", + redirect: "/pages/a8eb47/" }, { - name: "v-d804fba0", - path: "/pages/d70c1f/", + name: "v-0a7e6416", + path: "/pages/d97948/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-d804fba0").then(next) + ensureAsyncComponentsLoaded("Layout", "v-0a7e6416").then(next) }, }, { - path: "/pages/d70c1f/index.html", - redirect: "/pages/d70c1f/" + path: "/pages/d97948/index.html", + redirect: "/pages/d97948/" }, { - path: "/03.数据库/02.REDIS相关/001.Redis 有几种数据结构?Zset 是如何实现的?.html", - redirect: "/pages/d70c1f/" + path: "/03.数据库/01.MYSQL相关/170.MySQL 中 varchar 和 char 的区别是什么?.html", + redirect: "/pages/d97948/" + }, + { + name: "v-6b967a6e", + path: "/pages/32696d/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-6b967a6e").then(next) + }, + }, + { + path: "/pages/32696d/index.html", + redirect: "/pages/32696d/" + }, + { + path: "/03.数据库/01.MYSQL相关/180.模糊查询是如何实现的?.html", + redirect: "/pages/32696d/" }, { name: "v-cb742458", @@ -1433,20 +1401,20 @@ export const routes = [ redirect: "/pages/d66211/" }, { - name: "v-3e917471", - path: "/pages/34aec2/", + name: "v-d804fba0", + path: "/pages/d70c1f/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-3e917471").then(next) + ensureAsyncComponentsLoaded("Layout", "v-d804fba0").then(next) }, }, { - path: "/pages/34aec2/index.html", - redirect: "/pages/34aec2/" + path: "/pages/d70c1f/index.html", + redirect: "/pages/d70c1f/" }, { - path: "/03.数据库/02.REDIS相关/005.简述 Redis 字符串的底层结构.html", - redirect: "/pages/34aec2/" + path: "/03.数据库/02.REDIS相关/001.Redis 有几种数据结构?Zset 是如何实现的?.html", + redirect: "/pages/d70c1f/" }, { name: "v-c0bc7b7a", @@ -1461,40 +1429,24 @@ export const routes = [ redirect: "/pages/9326cc/" }, { - path: "/03.数据库/02.REDIS相关/010.Redis的缓存淘汰策略有哪些?.html", - redirect: "/pages/9326cc/" - }, - { - name: "v-7c214d00", - path: "/pages/7e708d/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-7c214d00").then(next) - }, - }, - { - path: "/pages/7e708d/index.html", - redirect: "/pages/7e708d/" - }, - { - path: "/03.数据库/02.REDIS相关/020.简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点.html", - redirect: "/pages/7e708d/" + path: "/03.数据库/02.REDIS相关/010.Redis的缓存淘汰策略有哪些?.html", + redirect: "/pages/9326cc/" }, { - name: "v-3d2fdd6a", - path: "/pages/338e30/", + name: "v-3e917471", + path: "/pages/34aec2/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-3d2fdd6a").then(next) + ensureAsyncComponentsLoaded("Layout", "v-3e917471").then(next) }, }, { - path: "/pages/338e30/index.html", - redirect: "/pages/338e30/" + path: "/pages/34aec2/index.html", + redirect: "/pages/34aec2/" }, { - path: "/03.数据库/02.REDIS相关/040.简述 Redis 集群配置以及基础原理.html", - redirect: "/pages/338e30/" + path: "/03.数据库/02.REDIS相关/005.简述 Redis 字符串的底层结构.html", + redirect: "/pages/34aec2/" }, { name: "v-2c6b511c", @@ -1512,6 +1464,22 @@ export const routes = [ path: "/03.数据库/02.REDIS相关/030.Redis 如何实现分布式锁?.html", redirect: "/pages/656f46/" }, + { + name: "v-3d2fdd6a", + path: "/pages/338e30/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-3d2fdd6a").then(next) + }, + }, + { + path: "/pages/338e30/index.html", + redirect: "/pages/338e30/" + }, + { + path: "/03.数据库/02.REDIS相关/040.简述 Redis 集群配置以及基础原理.html", + redirect: "/pages/338e30/" + }, { name: "v-18b427a0", path: "/pages/a42826/", @@ -1544,6 +1512,22 @@ export const routes = [ path: "/03.数据库/02.REDIS相关/060.Redis 中,sentinel 和 cluster 的区别和适用场景是什么?.html", redirect: "/pages/e2e910/" }, + { + name: "v-c2fe5e1a", + path: "/pages/03cd25/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-c2fe5e1a").then(next) + }, + }, + { + path: "/pages/03cd25/index.html", + redirect: "/pages/03cd25/" + }, + { + path: "/03.数据库/02.REDIS相关/070.简述 Redis 中如何防止缓存雪崩和缓存击穿.html", + redirect: "/pages/03cd25/" + }, { name: "v-35718f4e", path: "/pages/9c28c9/", @@ -1561,20 +1545,20 @@ export const routes = [ redirect: "/pages/9c28c9/" }, { - name: "v-c2fe5e1a", - path: "/pages/03cd25/", + name: "v-37f158ba", + path: "/pages/795c09/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-c2fe5e1a").then(next) + ensureAsyncComponentsLoaded("Layout", "v-37f158ba").then(next) }, }, { - path: "/pages/03cd25/index.html", - redirect: "/pages/03cd25/" + path: "/pages/795c09/index.html", + redirect: "/pages/795c09/" }, { - path: "/03.数据库/02.REDIS相关/070.简述 Redis 中如何防止缓存雪崩和缓存击穿.html", - redirect: "/pages/03cd25/" + path: "/03.数据库/02.REDIS相关/110.简述 Redis 如何处理热点 key 访问.html", + redirect: "/pages/795c09/" }, { name: "v-57957421", @@ -1593,20 +1577,36 @@ export const routes = [ redirect: "/pages/cc8507/" }, { - name: "v-37f158ba", - path: "/pages/795c09/", + name: "v-7c214d00", + path: "/pages/7e708d/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-37f158ba").then(next) + ensureAsyncComponentsLoaded("Layout", "v-7c214d00").then(next) }, }, { - path: "/pages/795c09/index.html", - redirect: "/pages/795c09/" + path: "/pages/7e708d/index.html", + redirect: "/pages/7e708d/" }, { - path: "/03.数据库/02.REDIS相关/110.简述 Redis 如何处理热点 key 访问.html", - redirect: "/pages/795c09/" + path: "/03.数据库/02.REDIS相关/020.简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点.html", + redirect: "/pages/7e708d/" + }, + { + name: "v-5acdab4d", + path: "/pages/3db019/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-5acdab4d").then(next) + }, + }, + { + path: "/pages/3db019/index.html", + redirect: "/pages/3db019/" + }, + { + path: "/04.中间件/10.Netty/010.TCP粘包拆包问题的解决之道.html", + redirect: "/pages/3db019/" }, { name: "v-be02b3c0", @@ -1640,22 +1640,6 @@ export const routes = [ path: "/03.数据库/02.REDIS相关/120.Redis 序列化有哪些方式?.html", redirect: "/pages/0d3c6b/" }, - { - name: "v-5acdab4d", - path: "/pages/3db019/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-5acdab4d").then(next) - }, - }, - { - path: "/pages/3db019/index.html", - redirect: "/pages/3db019/" - }, - { - path: "/04.中间件/10.Netty/010.TCP粘包拆包问题的解决之道.html", - redirect: "/pages/3db019/" - }, { name: "v-073f22cd", path: "/pages/59089d/", @@ -1673,36 +1657,36 @@ export const routes = [ redirect: "/pages/59089d/" }, { - name: "v-bbcf6e5a", - path: "/pages/ef17ee/", + name: "v-5157927c", + path: "/pages/f851b6/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-bbcf6e5a").then(next) + ensureAsyncComponentsLoaded("Layout", "v-5157927c").then(next) }, }, { - path: "/pages/ef17ee/index.html", - redirect: "/pages/ef17ee/" + path: "/pages/f851b6/index.html", + redirect: "/pages/f851b6/" }, { - path: "/04.中间件/10.Netty/020.为什么Netty要自己写一个ByteBuf.html", - redirect: "/pages/ef17ee/" + path: "/04.中间件/10.Netty/030.ChannelPipeline和ChannelHandler.html", + redirect: "/pages/f851b6/" }, { - name: "v-5157927c", - path: "/pages/f851b6/", + name: "v-6b32ada0", + path: "/pages/bfdcc8/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-5157927c").then(next) + ensureAsyncComponentsLoaded("Layout", "v-6b32ada0").then(next) }, }, { - path: "/pages/f851b6/index.html", - redirect: "/pages/f851b6/" + path: "/pages/bfdcc8/index.html", + redirect: "/pages/bfdcc8/" }, { - path: "/04.中间件/10.Netty/030.ChannelPipeline和ChannelHandler.html", - redirect: "/pages/f851b6/" + path: "/04.中间件/10.Netty/050.Java NIO了解吗?.html", + redirect: "/pages/bfdcc8/" }, { name: "v-62f3fa3e", @@ -1721,20 +1705,20 @@ export const routes = [ redirect: "/pages/e7aa60/" }, { - name: "v-6b32ada0", - path: "/pages/bfdcc8/", + name: "v-bbcf6e5a", + path: "/pages/ef17ee/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-6b32ada0").then(next) + ensureAsyncComponentsLoaded("Layout", "v-bbcf6e5a").then(next) }, }, { - path: "/pages/bfdcc8/index.html", - redirect: "/pages/bfdcc8/" + path: "/pages/ef17ee/index.html", + redirect: "/pages/ef17ee/" }, { - path: "/04.中间件/10.Netty/050.Java NIO了解吗?.html", - redirect: "/pages/bfdcc8/" + path: "/04.中间件/10.Netty/020.为什么Netty要自己写一个ByteBuf.html", + redirect: "/pages/ef17ee/" }, { name: "v-7c0136cf", @@ -1801,20 +1785,20 @@ export const routes = [ redirect: "/pages/8d1d04/" }, { - name: "v-684c4815", - path: "/pages/76a94c/", + name: "v-5675b06b", + path: "/pages/20c647/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-684c4815").then(next) + ensureAsyncComponentsLoaded("Layout", "v-5675b06b").then(next) }, }, { - path: "/pages/76a94c/index.html", - redirect: "/pages/76a94c/" + path: "/pages/20c647/index.html", + redirect: "/pages/20c647/" }, { - path: "/04.中间件/40.Kafka/040.如何保证Kafka的可靠性.html", - redirect: "/pages/76a94c/" + path: "/08.系统设计/010.什么是 CAP ?什么是最终一致性?什么是幂等操作?.html", + redirect: "/pages/20c647/" }, { name: "v-a9558ec0", @@ -1833,20 +1817,20 @@ export const routes = [ redirect: "/pages/01b9b2/" }, { - name: "v-5675b06b", - path: "/pages/20c647/", + name: "v-684c4815", + path: "/pages/76a94c/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-5675b06b").then(next) + ensureAsyncComponentsLoaded("Layout", "v-684c4815").then(next) }, }, { - path: "/pages/20c647/index.html", - redirect: "/pages/20c647/" + path: "/pages/76a94c/index.html", + redirect: "/pages/76a94c/" }, { - path: "/08.系统设计/010.什么是 CAP ?什么是最终一致性?什么是幂等操作?.html", - redirect: "/pages/20c647/" + path: "/04.中间件/40.Kafka/040.如何保证Kafka的可靠性.html", + redirect: "/pages/76a94c/" }, { name: "v-f88544f4", @@ -1896,6 +1880,22 @@ export const routes = [ path: "/10.非技术/01.非技术问题.html", redirect: "/pages/5e507a/" }, + { + name: "v-09c150e6", + path: "/pages/47cf96/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-09c150e6").then(next) + }, + }, + { + path: "/pages/47cf96/index.html", + redirect: "/pages/47cf96/" + }, + { + path: "/@pages/20.收藏夹/02.常用的前端轮子.html", + redirect: "/pages/47cf96/" + }, { name: "v-48f7eec0", path: "/archives/", @@ -1929,36 +1929,36 @@ export const routes = [ redirect: "/pages/beb6c0bd8a66cea6/" }, { - name: "v-a5c5f278", - path: "/pages/829589/", + name: "v-2db11cb2", + path: "/pages/f2e63f/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-a5c5f278").then(next) + ensureAsyncComponentsLoaded("Layout", "v-2db11cb2").then(next) }, }, { - path: "/pages/829589/index.html", - redirect: "/pages/829589/" + path: "/pages/f2e63f/index.html", + redirect: "/pages/f2e63f/" }, { - path: "/_posts/随笔/我做了一个手写春联小网页,祝大家虎年暴富.html", - redirect: "/pages/829589/" + path: "/_posts/随笔/你知道的越多,不知道的也就越多.html", + redirect: "/pages/f2e63f/" }, { - name: "v-2db11cb2", - path: "/pages/f2e63f/", + name: "v-a5c5f278", + path: "/pages/829589/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-2db11cb2").then(next) + ensureAsyncComponentsLoaded("Layout", "v-a5c5f278").then(next) }, }, { - path: "/pages/f2e63f/index.html", - redirect: "/pages/f2e63f/" + path: "/pages/829589/index.html", + redirect: "/pages/829589/" }, { - path: "/_posts/随笔/你知道的越多,不知道的也就越多.html", - redirect: "/pages/f2e63f/" + path: "/_posts/随笔/我做了一个手写春联小网页,祝大家虎年暴富.html", + redirect: "/pages/829589/" }, { name: "v-7d77d69c", @@ -1988,22 +1988,6 @@ export const routes = [ path: "/index.html", redirect: "/" }, - { - name: "v-09c150e6", - path: "/pages/47cf96/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-09c150e6").then(next) - }, - }, - { - path: "/pages/47cf96/index.html", - redirect: "/pages/47cf96/" - }, - { - path: "/@pages/20.收藏夹/02.常用的前端轮子.html", - redirect: "/pages/47cf96/" - }, { name: "v-64f9b920", path: "/tags/", @@ -2036,6 +2020,22 @@ export const routes = [ path: "/@pages/categoriesPage.html", redirect: "/categories/" }, + { + name: "v-6602e3da", + path: "/pages/ab48ba/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-6602e3da").then(next) + }, + }, + { + path: "/pages/ab48ba/index.html", + redirect: "/pages/ab48ba/" + }, + { + path: "/02.计算机基础/10.计算机网络/020.从输入 URL 到展现页面的全过程.html", + redirect: "/pages/ab48ba/" + }, { path: '*', component: GlobalLayout diff --git a/node_modules/@vuepress/core/.temp/internal/siteData.js b/node_modules/@vuepress/core/.temp/internal/siteData.js index dd709efd3..147418574 100644 --- a/node_modules/@vuepress/core/.temp/internal/siteData.js +++ b/node_modules/@vuepress/core/.temp/internal/siteData.js @@ -66,36 +66,6 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, - { - "title": "计算机基础", - "frontmatter": { - "pageComponent": { - "name": "Catalogue", - "data": { - "key": "02.计算机基础", - "imgUrl": "/img/ui.png", - "description": "计算机网络、数据结构、" - } - }, - "title": "计算机基础", - "date": "2020-03-11T21:50:54.000Z", - "permalink": "/basic", - "sidebar": false, - "article": false, - "comment": false, - "editLink": false - }, - "regularPath": "/00.%E7%9B%AE%E5%BD%95%E9%A1%B5/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80.html", - "relativePath": "00.目录页/02.计算机基础.md", - "key": "v-10f5d0e5", - "path": "/basic/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, { "title": "数据库", "frontmatter": { @@ -127,28 +97,28 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "中间件", + "title": "计算机基础", "frontmatter": { "pageComponent": { "name": "Catalogue", "data": { - "key": "04.中间件", - "imgUrl": "/img/more.png", - "description": "Dubbo、Mybatis" + "key": "02.计算机基础", + "imgUrl": "/img/ui.png", + "description": "计算机网络、数据结构、" } }, - "title": "中间件", - "date": "2020-03-11T21:50:56.000Z", - "permalink": "/middleware", + "title": "计算机基础", + "date": "2020-03-11T21:50:54.000Z", + "permalink": "/basic", "sidebar": false, "article": false, "comment": false, "editLink": false }, - "regularPath": "/00.%E7%9B%AE%E5%BD%95%E9%A1%B5/04.%E4%B8%AD%E9%97%B4%E4%BB%B6.html", - "relativePath": "00.目录页/04.中间件.md", - "key": "v-7abe58c4", - "path": "/middleware/", + "regularPath": "/00.%E7%9B%AE%E5%BD%95%E9%A1%B5/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80.html", + "relativePath": "00.目录页/02.计算机基础.md", + "key": "v-10f5d0e5", + "path": "/basic/", "headersStr": null, "content": "", "normalizedContent": "", @@ -193,38 +163,32 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "HashMap 与 ConcurrentHashMap 的实现原理是怎样的?", + "title": "中间件", "frontmatter": { - "title": "HashMap 与 ConcurrentHashMap 的实现原理是怎样的?", - "date": "2022-04-04T14:21:18.000Z", - "permalink": "/pages/d1c564/", - "categories": [ - "Java相关", - "基础" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/010.HashMap%20%E4%B8%8E%20ConcurrentHashMap%20%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E6%98%AF%E6%80%8E%E6%A0%B7%E7%9A%84%EF%BC%9F.html", - "relativePath": "01.Java相关/10.基础/010.HashMap 与 ConcurrentHashMap 的实现原理是怎样的?.md", - "key": "v-0734374b", - "path": "/pages/d1c564/", - "headers": [ - { - "level": 2, - "title": "HashMap 与 ConcurrentHashMap 的实现原理是怎样的?ConcurrentHashMap 是如何保证线程安全的?", - "slug": "hashmap-与-concurrenthashmap-的实现原理是怎样的-concurrenthashmap-是如何保证线程安全的", - "normalizedTitle": "hashmap 与 concurrenthashmap 的实现原理是怎样的?concurrenthashmap 是如何保证线程安全的?", - "charIndex": 2 - } - ], - "headersStr": "HashMap 与 ConcurrentHashMap 的实现原理是怎样的?ConcurrentHashMap 是如何保证线程安全的?", - "content": "# HashMap 与 ConcurrentHashMap 的实现原理是怎样的?ConcurrentHashMap 是如何保证线程安全的?\n\n HASHMAP CONCURRENTHASHMAP\n是否线程安全 不安全 安全\nJDK1.7-数据结构 数组+链表 数组+链表+Segment\nJDK1.7-数据结构-图示 \n锁粒度 --- 对segment加锁,最大并发修改线程数=segment数量\n--- --- ---\nJDK1.8-数据结构 数组+链表+红黑树 数组+链表+红黑树\nJDK1.8-数据结构-图示 \n锁粒度 --- HashEntry(首节点),最大并发修改线程数=数组.size\n\n> Q1:ConcurrentHashMap 中变量使用 final 和 volatile 修饰有什么用呢?\n> A:Final域使得确保初始化安全性(initialization safety)成为可能,初始化安全性让不可变形对象不需要同步就能自由地被访问和共享。\n> Node的元素val和指针next是用 volatile 修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。\n\n> Q2:ConcurrentHashMap在JDK 7和8之间的区别\n> A:去掉了Segment这种数据结构\n> 增加了红黑树结构来优化链表,解决了长链表遍历慢的问题", - "normalizedContent": "# hashmap 与 concurrenthashmap 的实现原理是怎样的?concurrenthashmap 是如何保证线程安全的?\n\n hashmap concurrenthashmap\n是否线程安全 不安全 安全\njdk1.7-数据结构 数组+链表 数组+链表+segment\njdk1.7-数据结构-图示 \n锁粒度 --- 对segment加锁,最大并发修改线程数=segment数量\n--- --- ---\njdk1.8-数据结构 数组+链表+红黑树 数组+链表+红黑树\njdk1.8-数据结构-图示 \n锁粒度 --- hashentry(首节点),最大并发修改线程数=数组.size\n\n> q1:concurrenthashmap 中变量使用 final 和 volatile 修饰有什么用呢?\n> a:final域使得确保初始化安全性(initialization safety)成为可能,初始化安全性让不可变形对象不需要同步就能自由地被访问和共享。\n> node的元素val和指针next是用 volatile 修饰的,在多线程环境下线程a修改结点的val或者新增节点的时候是对线程b可见的。\n\n> q2:concurrenthashmap在jdk 7和8之间的区别\n> a:去掉了segment这种数据结构\n> 增加了红黑树结构来优化链表,解决了长链表遍历慢的问题", - "charsets": { - "cjk": true + "pageComponent": { + "name": "Catalogue", + "data": { + "key": "04.中间件", + "imgUrl": "/img/more.png", + "description": "Dubbo、Mybatis" + } + }, + "title": "中间件", + "date": "2020-03-11T21:50:56.000Z", + "permalink": "/middleware", + "sidebar": false, + "article": false, + "comment": false, + "editLink": false }, + "regularPath": "/00.%E7%9B%AE%E5%BD%95%E9%A1%B5/04.%E4%B8%AD%E9%97%B4%E4%BB%B6.html", + "relativePath": "00.目录页/04.中间件.md", + "key": "v-7abe58c4", + "path": "/middleware/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, @@ -265,11 +229,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "为什么重写 equals 必须重写 hashcode ?", + "title": "String,StringBuffer,StringBuilder 之间有什么区别?", "frontmatter": { - "title": "为什么重写 equals 必须重写 hashcode ?", - "date": "2022-04-04T14:23:08.000Z", - "permalink": "/pages/0d22ad/", + "title": "String,StringBuffer,StringBuilder 之间有什么区别?", + "date": "2022-04-04T14:24:07.000Z", + "permalink": "/pages/211d8c/", "categories": [ "Java相关", "基础" @@ -278,22 +242,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/030.%E4%B8%BA%E4%BB%80%E4%B9%88%E9%87%8D%E5%86%99%20equals%20%E5%BF%85%E9%A1%BB%E9%87%8D%E5%86%99%20hashcode%20%EF%BC%9F.html", - "relativePath": "01.Java相关/10.基础/030.为什么重写 equals 必须重写 hashcode ?.md", - "key": "v-57012c44", - "path": "/pages/0d22ad/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/050.String%EF%BC%8CStringBuffer%EF%BC%8CStringBuilder%20%E4%B9%8B%E9%97%B4%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F.html", + "relativePath": "01.Java相关/10.基础/050.String,StringBuffer,StringBuilder 之间有什么区别?.md", + "key": "v-7f48b402", + "path": "/pages/211d8c/", "headers": [ { "level": 2, - "title": "为什么重写 equals 必须重写 hashcode ?", - "slug": "为什么重写-equals-必须重写-hashcode", - "normalizedTitle": "为什么重写 equals 必须重写 hashcode ?", + "title": "String,StringBuffer,StringBuilder 之间有什么区别?", + "slug": "string-stringbuffer-stringbuilder-之间有什么区别", + "normalizedTitle": "string,stringbuffer,stringbuilder 之间有什么区别?", "charIndex": 2 } ], - "headersStr": "为什么重写 equals 必须重写 hashcode ?", - "content": "# 为什么重写 equals 必须重写 hashcode ?\n\n如果 我们重写了 equals 没有重写 hashcode 会导致HashSet 里面可以存储重复的元素。\n\n\n以 HashSet 为例来说\nHashSet 底层是数组结构 存储的元素不重复( O1和O2重复的意思是O1.equals(O2)==true )\nadd 一个元素进来放在哪里呢?\n是根据元素的hash值(即 hashcode )来确定数组下标 将元素放在里面。\n如果 我们重写了 equals 没有重写 hashcode\n那么 可能会产生这样的数据: O1.equals(O2)==true 但是O1.hashcode()!=O2.hashcode()\n导致 HashSet 里面可以存储重复的元素。", - "normalizedContent": "# 为什么重写 equals 必须重写 hashcode ?\n\n如果 我们重写了 equals 没有重写 hashcode 会导致hashset 里面可以存储重复的元素。\n\n\n以 hashset 为例来说\nhashset 底层是数组结构 存储的元素不重复( o1和o2重复的意思是o1.equals(o2)==true )\nadd 一个元素进来放在哪里呢?\n是根据元素的hash值(即 hashcode )来确定数组下标 将元素放在里面。\n如果 我们重写了 equals 没有重写 hashcode\n那么 可能会产生这样的数据: o1.equals(o2)==true 但是o1.hashcode()!=o2.hashcode()\n导致 hashset 里面可以存储重复的元素。", + "headersStr": "String,StringBuffer,StringBuilder 之间有什么区别?", + "content": "# String,StringBuffer,StringBuilder 之间有什么区别?\n\n * String 对象是不可变的。String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[]。\n * StringBuffer,StringBuilder是可变的\n * StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 是线程不安全的。\n\n STRING STRINGBUFFER STRINGBUILDER\n可变性 不可变 可变 可变\n线程安全性 安全 安全 不安全\n使用场景 操作少量数据 多线程操作字符串缓冲区下操作大量数据 单线程操作字符串缓冲区下操作大量数据", + "normalizedContent": "# string,stringbuffer,stringbuilder 之间有什么区别?\n\n * string 对象是不可变的。string 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[]。\n * stringbuffer,stringbuilder是可变的\n * stringbuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。stringbuilder 是线程不安全的。\n\n string stringbuffer stringbuilder\n可变性 不可变 可变 可变\n线程安全性 安全 安全 不安全\n使用场景 操作少量数据 多线程操作字符串缓冲区下操作大量数据 单线程操作字符串缓冲区下操作大量数据", "charsets": { "cjk": true }, @@ -337,11 +301,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度", + "title": "为什么重写 equals 必须重写 hashcode ?", "frontmatter": { - "title": "简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度", - "date": "2022-04-04T14:24:34.000Z", - "permalink": "/pages/305919/", + "title": "为什么重写 equals 必须重写 hashcode ?", + "date": "2022-04-04T14:23:08.000Z", + "permalink": "/pages/0d22ad/", "categories": [ "Java相关", "基础" @@ -350,22 +314,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/060.%E7%AE%80%E8%BF%B0%20ArrayList%20%E4%B8%8E%20LinkedList%20%E7%9A%84%E5%BA%95%E5%B1%82%E5%AE%9E%E7%8E%B0%E4%BB%A5%E5%8F%8A%E5%B8%B8%E8%A7%81%E6%93%8D%E4%BD%9C%E7%9A%84%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6.html", - "relativePath": "01.Java相关/10.基础/060.简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度.md", - "key": "v-57b2551a", - "path": "/pages/305919/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/030.%E4%B8%BA%E4%BB%80%E4%B9%88%E9%87%8D%E5%86%99%20equals%20%E5%BF%85%E9%A1%BB%E9%87%8D%E5%86%99%20hashcode%20%EF%BC%9F.html", + "relativePath": "01.Java相关/10.基础/030.为什么重写 equals 必须重写 hashcode ?.md", + "key": "v-57012c44", + "path": "/pages/0d22ad/", "headers": [ { "level": 2, - "title": "简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度", - "slug": "简述-arraylist-与-linkedlist-的底层实现以及常见操作的时间复杂度", - "normalizedTitle": "简述 arraylist 与 linkedlist 的底层实现以及常见操作的时间复杂度", + "title": "为什么重写 equals 必须重写 hashcode ?", + "slug": "为什么重写-equals-必须重写-hashcode", + "normalizedTitle": "为什么重写 equals 必须重写 hashcode ?", "charIndex": 2 } ], - "headersStr": "简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度", - "content": "# 简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度\n\n ARRAYLIST LINKEDLIST\n线程安全 不安全 不安全\n底层结构 Object 数组 双向链表\n适用场景 按照数组下表快速访问元素 时间复杂度就是 O(1) 顺序访问元素\n不适用场景 在指定位置n插入、删除, 会导致位置n后面的元素移动 时间复杂度就是 O(n-1) 随机访问元素", - "normalizedContent": "# 简述 arraylist 与 linkedlist 的底层实现以及常见操作的时间复杂度\n\n arraylist linkedlist\n线程安全 不安全 不安全\n底层结构 object 数组 双向链表\n适用场景 按照数组下表快速访问元素 时间复杂度就是 o(1) 顺序访问元素\n不适用场景 在指定位置n插入、删除, 会导致位置n后面的元素移动 时间复杂度就是 o(n-1) 随机访问元素", + "headersStr": "为什么重写 equals 必须重写 hashcode ?", + "content": "# 为什么重写 equals 必须重写 hashcode ?\n\n如果 我们重写了 equals 没有重写 hashcode 会导致HashSet 里面可以存储重复的元素。\n\n\n以 HashSet 为例来说\nHashSet 底层是数组结构 存储的元素不重复( O1和O2重复的意思是O1.equals(O2)==true )\nadd 一个元素进来放在哪里呢?\n是根据元素的hash值(即 hashcode )来确定数组下标 将元素放在里面。\n如果 我们重写了 equals 没有重写 hashcode\n那么 可能会产生这样的数据: O1.equals(O2)==true 但是O1.hashcode()!=O2.hashcode()\n导致 HashSet 里面可以存储重复的元素。", + "normalizedContent": "# 为什么重写 equals 必须重写 hashcode ?\n\n如果 我们重写了 equals 没有重写 hashcode 会导致hashset 里面可以存储重复的元素。\n\n\n以 hashset 为例来说\nhashset 底层是数组结构 存储的元素不重复( o1和o2重复的意思是o1.equals(o2)==true )\nadd 一个元素进来放在哪里呢?\n是根据元素的hash值(即 hashcode )来确定数组下标 将元素放在里面。\n如果 我们重写了 equals 没有重写 hashcode\n那么 可能会产生这样的数据: o1.equals(o2)==true 但是o1.hashcode()!=o2.hashcode()\n导致 hashset 里面可以存储重复的元素。", "charsets": { "cjk": true }, @@ -373,11 +337,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "String,StringBuffer,StringBuilder 之间有什么区别?", + "title": "简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度", "frontmatter": { - "title": "String,StringBuffer,StringBuilder 之间有什么区别?", - "date": "2022-04-04T14:24:07.000Z", - "permalink": "/pages/211d8c/", + "title": "简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度", + "date": "2022-04-04T14:24:34.000Z", + "permalink": "/pages/305919/", "categories": [ "Java相关", "基础" @@ -386,22 +350,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/050.String%EF%BC%8CStringBuffer%EF%BC%8CStringBuilder%20%E4%B9%8B%E9%97%B4%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F.html", - "relativePath": "01.Java相关/10.基础/050.String,StringBuffer,StringBuilder 之间有什么区别?.md", - "key": "v-7f48b402", - "path": "/pages/211d8c/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/060.%E7%AE%80%E8%BF%B0%20ArrayList%20%E4%B8%8E%20LinkedList%20%E7%9A%84%E5%BA%95%E5%B1%82%E5%AE%9E%E7%8E%B0%E4%BB%A5%E5%8F%8A%E5%B8%B8%E8%A7%81%E6%93%8D%E4%BD%9C%E7%9A%84%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6.html", + "relativePath": "01.Java相关/10.基础/060.简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度.md", + "key": "v-57b2551a", + "path": "/pages/305919/", "headers": [ { "level": 2, - "title": "String,StringBuffer,StringBuilder 之间有什么区别?", - "slug": "string-stringbuffer-stringbuilder-之间有什么区别", - "normalizedTitle": "string,stringbuffer,stringbuilder 之间有什么区别?", + "title": "简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度", + "slug": "简述-arraylist-与-linkedlist-的底层实现以及常见操作的时间复杂度", + "normalizedTitle": "简述 arraylist 与 linkedlist 的底层实现以及常见操作的时间复杂度", "charIndex": 2 } ], - "headersStr": "String,StringBuffer,StringBuilder 之间有什么区别?", - "content": "# String,StringBuffer,StringBuilder 之间有什么区别?\n\n * String 对象是不可变的。String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[]。\n * StringBuffer,StringBuilder是可变的\n * StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 是线程不安全的。\n\n STRING STRINGBUFFER STRINGBUILDER\n可变性 不可变 可变 可变\n线程安全性 安全 安全 不安全\n使用场景 操作少量数据 多线程操作字符串缓冲区下操作大量数据 单线程操作字符串缓冲区下操作大量数据", - "normalizedContent": "# string,stringbuffer,stringbuilder 之间有什么区别?\n\n * string 对象是不可变的。string 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[]。\n * stringbuffer,stringbuilder是可变的\n * stringbuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。stringbuilder 是线程不安全的。\n\n string stringbuffer stringbuilder\n可变性 不可变 可变 可变\n线程安全性 安全 安全 不安全\n使用场景 操作少量数据 多线程操作字符串缓冲区下操作大量数据 单线程操作字符串缓冲区下操作大量数据", + "headersStr": "简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度", + "content": "# 简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度\n\n ARRAYLIST LINKEDLIST\n线程安全 不安全 不安全\n底层结构 Object 数组 双向链表\n适用场景 按照数组下表快速访问元素 时间复杂度就是 O(1) 顺序访问元素\n不适用场景 在指定位置n插入、删除, 会导致位置n后面的元素移动 时间复杂度就是 O(n-1) 随机访问元素", + "normalizedContent": "# 简述 arraylist 与 linkedlist 的底层实现以及常见操作的时间复杂度\n\n arraylist linkedlist\n线程安全 不安全 不安全\n底层结构 object 数组 双向链表\n适用场景 按照数组下表快速访问元素 时间复杂度就是 o(1) 顺序访问元素\n不适用场景 在指定位置n插入、删除, 会导致位置n后面的元素移动 时间复杂度就是 o(n-1) 随机访问元素", "charsets": { "cjk": true }, @@ -444,6 +408,42 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, + { + "title": "HashMap 与 ConcurrentHashMap 的实现原理是怎样的?", + "frontmatter": { + "title": "HashMap 与 ConcurrentHashMap 的实现原理是怎样的?", + "date": "2022-04-04T14:21:18.000Z", + "permalink": "/pages/d1c564/", + "categories": [ + "Java相关", + "基础" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/010.HashMap%20%E4%B8%8E%20ConcurrentHashMap%20%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E6%98%AF%E6%80%8E%E6%A0%B7%E7%9A%84%EF%BC%9F.html", + "relativePath": "01.Java相关/10.基础/010.HashMap 与 ConcurrentHashMap 的实现原理是怎样的?.md", + "key": "v-0734374b", + "path": "/pages/d1c564/", + "headers": [ + { + "level": 2, + "title": "HashMap 与 ConcurrentHashMap 的实现原理是怎样的?ConcurrentHashMap 是如何保证线程安全的?", + "slug": "hashmap-与-concurrenthashmap-的实现原理是怎样的-concurrenthashmap-是如何保证线程安全的", + "normalizedTitle": "hashmap 与 concurrenthashmap 的实现原理是怎样的?concurrenthashmap 是如何保证线程安全的?", + "charIndex": 2 + } + ], + "headersStr": "HashMap 与 ConcurrentHashMap 的实现原理是怎样的?ConcurrentHashMap 是如何保证线程安全的?", + "content": "# HashMap 与 ConcurrentHashMap 的实现原理是怎样的?ConcurrentHashMap 是如何保证线程安全的?\n\n HASHMAP CONCURRENTHASHMAP\n是否线程安全 不安全 安全\nJDK1.7-数据结构 数组+链表 数组+链表+Segment\nJDK1.7-数据结构-图示 \n锁粒度 --- 对segment加锁,最大并发修改线程数=segment数量\n--- --- ---\nJDK1.8-数据结构 数组+链表+红黑树 数组+链表+红黑树\nJDK1.8-数据结构-图示 \n锁粒度 --- HashEntry(首节点),最大并发修改线程数=数组.size\n\n> Q1:ConcurrentHashMap 中变量使用 final 和 volatile 修饰有什么用呢?\n> A:Final域使得确保初始化安全性(initialization safety)成为可能,初始化安全性让不可变形对象不需要同步就能自由地被访问和共享。\n> Node的元素val和指针next是用 volatile 修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。\n\n> Q2:ConcurrentHashMap在JDK 7和8之间的区别\n> A:去掉了Segment这种数据结构\n> 增加了红黑树结构来优化链表,解决了长链表遍历慢的问题", + "normalizedContent": "# hashmap 与 concurrenthashmap 的实现原理是怎样的?concurrenthashmap 是如何保证线程安全的?\n\n hashmap concurrenthashmap\n是否线程安全 不安全 安全\njdk1.7-数据结构 数组+链表 数组+链表+segment\njdk1.7-数据结构-图示 \n锁粒度 --- 对segment加锁,最大并发修改线程数=segment数量\n--- --- ---\njdk1.8-数据结构 数组+链表+红黑树 数组+链表+红黑树\njdk1.8-数据结构-图示 \n锁粒度 --- hashentry(首节点),最大并发修改线程数=数组.size\n\n> q1:concurrenthashmap 中变量使用 final 和 volatile 修饰有什么用呢?\n> a:final域使得确保初始化安全性(initialization safety)成为可能,初始化安全性让不可变形对象不需要同步就能自由地被访问和共享。\n> node的元素val和指针next是用 volatile 修饰的,在多线程环境下线程a修改结点的val或者新增节点的时候是对线程b可见的。\n\n> q2:concurrenthashmap在jdk 7和8之间的区别\n> a:去掉了segment这种数据结构\n> 增加了红黑树结构来优化链表,解决了长链表遍历慢的问题", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, { "title": "在一个静态方法内调用一个非静态成员为什么是非法的?", "frontmatter": { @@ -517,11 +517,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "什么是双亲委派机制?", + "title": "Java 类的加载流程是怎样的?", "frontmatter": { - "title": "什么是双亲委派机制?", - "date": "2022-04-04T14:26:57.000Z", - "permalink": "/pages/38bcaf/", + "title": "Java 类的加载流程是怎样的?", + "date": "2022-04-04T14:26:24.000Z", + "permalink": "/pages/33c5d6/", "categories": [ "Java相关", "基础" @@ -530,22 +530,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/110.%E4%BB%80%E4%B9%88%E6%98%AF%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE%E6%9C%BA%E5%88%B6%EF%BC%9F.html", - "relativePath": "01.Java相关/10.基础/110.什么是双亲委派机制?.md", - "key": "v-7665e30e", - "path": "/pages/38bcaf/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/100.Java%20%E7%B1%BB%E7%9A%84%E5%8A%A0%E8%BD%BD%E6%B5%81%E7%A8%8B%E6%98%AF%E6%80%8E%E6%A0%B7%E7%9A%84%EF%BC%9F.html", + "relativePath": "01.Java相关/10.基础/100.Java 类的加载流程是怎样的?.md", + "key": "v-4c273a66", + "path": "/pages/33c5d6/", "headers": [ { "level": 2, - "title": "什么是双亲委派机制?", - "slug": "什么是双亲委派机制", - "normalizedTitle": "什么是双亲委派机制?", + "title": "Java 类的加载流程是怎样的?", + "slug": "java-类的加载流程是怎样的", + "normalizedTitle": "java 类的加载流程是怎样的?", "charIndex": 2 } ], - "headersStr": "什么是双亲委派机制?", - "content": "# 什么是双亲委派机制?\n\n双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。\n\n\n\n加载器 加载哪些类\nbootstrap classloader 加载jre/lib/rt.jar\nextension classloader jre/lib/ext/*.jar\napplication classloader 加载应用程序目录\n自定义ClassLoader 定制化加载\n\n再来读一下java.lang.ClassLoader这段代码 是不是通透了许多?\n\n public Class loadClass(String name) throws ClassNotFoundException {\n return loadClass(name, false);\n }\n // -----??-----\n protected Class loadClass(String name, boolean resolve)\n throws ClassNotFoundException\n {\n // 首先,检查是否已经被类加载器加载过\n Class c = findLoadedClass(name);\n if (c == null) {\n try {\n // 存在父加载器,递归的交由父加载器\n if (parent != null) {\n c = parent.loadClass(name, false);\n } else {\n // 直到最上面的Bootstrap类加载器\n c = findBootstrapClassOrNull(name);\n }\n } catch (ClassNotFoundException e) {\n // ClassNotFoundException thrown if class not found\n // from the non-null parent class loader\n }\n \n if (c == null) {\n // If still not found, then invoke findClass in order\n // to find the class.\n c = findClass(name);\n }\n }\n return c;\n }\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n\n\n> Q1:使用双亲委派有什么好处?\n> A:双亲委派机制能保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同。\n> 考虑到安全因素。\n> 假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。\n\n> Q2:什么场景需要破坏双亲委派?\n> 双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载)\n> 基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API 但没有绝对,如果基础类调用会用户的代码怎么办呢? 比如JDBC驱动加载。 这种场景就需要用到SPI机制,是不符合双亲委派的。\n\n> Q3:JDBC、Tomcat中为什么要破坏双亲委派模型\n> A:JDBC\n> 先来回顾一下JDBC的用法\n\nString url = \"jdbc:mysql:///consult?serverTimezone=UTC\";\nString user = \"root\";\nString password = \"root\";\n\nClass.forName(\"com.mysql.jdbc.Driver\");\nConnection connection = DriverManager.getConnection(url, user, password);\n\n\n1\n2\n3\n4\n5\n6\n\n\n> DriverManager 类中要加载各个实现了Driver接口的类,然后进行管理,DriverManager位于 JAVA_HOME中jre/lib/rt.jar 包,由BootStrap类加载器加载.\n> JDBC的Driver接口定义在JDK中,其实现由各个数据库的服务商来提供,比如MySQL驱动包位于服务商MySQL提供的 Jar 中包含Driver的实现,按照双亲委派模型应该由application classloader加载\n> 实际上却是由BootStrap类加载器加载。\n> 这是因为:根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类。\n> 这就破坏了双亲委派模型。\n\nTomcat\n每个webappClassLoader加载自己目录下的class文件\n\n> Tomcat支持部署多个Web应用,多个Web应用可能使用不同版本的JDK、也可能在应用内部定义全限定名一样的类,所以要做到应用隔离。", - "normalizedContent": "# 什么是双亲委派机制?\n\n双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。\n\n\n\n加载器 加载哪些类\nbootstrap classloader 加载jre/lib/rt.jar\nextension classloader jre/lib/ext/*.jar\napplication classloader 加载应用程序目录\n自定义classloader 定制化加载\n\n再来读一下java.lang.classloader这段代码 是不是通透了许多?\n\n public class loadclass(string name) throws classnotfoundexception {\n return loadclass(name, false);\n }\n // -----??-----\n protected class loadclass(string name, boolean resolve)\n throws classnotfoundexception\n {\n // 首先,检查是否已经被类加载器加载过\n class c = findloadedclass(name);\n if (c == null) {\n try {\n // 存在父加载器,递归的交由父加载器\n if (parent != null) {\n c = parent.loadclass(name, false);\n } else {\n // 直到最上面的bootstrap类加载器\n c = findbootstrapclassornull(name);\n }\n } catch (classnotfoundexception e) {\n // classnotfoundexception thrown if class not found\n // from the non-null parent class loader\n }\n \n if (c == null) {\n // if still not found, then invoke findclass in order\n // to find the class.\n c = findclass(name);\n }\n }\n return c;\n }\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n\n\n> q1:使用双亲委派有什么好处?\n> a:双亲委派机制能保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同。\n> 考虑到安全因素。\n> 假设通过网络传递一个名为java.lang.integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心java api发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.integer,而直接返回已加载过的integer.class,这样便可以防止核心api库被随意篡改。\n\n> q2:什么场景需要破坏双亲委派?\n> 双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载)\n> 基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的api 但没有绝对,如果基础类调用会用户的代码怎么办呢? 比如jdbc驱动加载。 这种场景就需要用到spi机制,是不符合双亲委派的。\n\n> q3:jdbc、tomcat中为什么要破坏双亲委派模型\n> a:jdbc\n> 先来回顾一下jdbc的用法\n\nstring url = \"jdbc:mysql:///consult?servertimezone=utc\";\nstring user = \"root\";\nstring password = \"root\";\n\nclass.forname(\"com.mysql.jdbc.driver\");\nconnection connection = drivermanager.getconnection(url, user, password);\n\n\n1\n2\n3\n4\n5\n6\n\n\n> drivermanager 类中要加载各个实现了driver接口的类,然后进行管理,drivermanager位于 java_home中jre/lib/rt.jar 包,由bootstrap类加载器加载.\n> jdbc的driver接口定义在jdk中,其实现由各个数据库的服务商来提供,比如mysql驱动包位于服务商mysql提供的 jar 中包含driver的实现,按照双亲委派模型应该由application classloader加载\n> 实际上却是由bootstrap类加载器加载。\n> 这是因为:根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类。\n> 这就破坏了双亲委派模型。\n\ntomcat\n每个webappclassloader加载自己目录下的class文件\n\n> tomcat支持部署多个web应用,多个web应用可能使用不同版本的jdk、也可能在应用内部定义全限定名一样的类,所以要做到应用隔离。", + "headersStr": "Java 类的加载流程是怎样的?", + "content": "# Java 类的加载流程是怎样的?\n\n类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。 其中类加载过程包括加载、验证、准备、解析和初始化五个阶段。\n\n 做什么\n加载 类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例\n验证 验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等\n准备 为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内)\n 被final修饰的静态变量,会直接赋予原值;类字段的字段属性表中存在ConstantValue属性,则在准备阶段,其值就是ConstantValue的值\n解析 可以认为是一些静态绑定的会被解析,动态绑定则只会在运行是进行解析;静态绑定包括一些final方法(不可以重写),static方法(只会属于当前类),构造器(不会被重写)\n初始化 初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。", + "normalizedContent": "# java 类的加载流程是怎样的?\n\n类从被加载到jvm中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。 其中类加载过程包括加载、验证、准备、解析和初始化五个阶段。\n\n 做什么\n加载 类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到jvm内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.class对象实例\n验证 验证类数据信息是否符合jvm规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等\n准备 为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内)\n 被final修饰的静态变量,会直接赋予原值;类字段的字段属性表中存在constantvalue属性,则在准备阶段,其值就是constantvalue的值\n解析 可以认为是一些静态绑定的会被解析,动态绑定则只会在运行是进行解析;静态绑定包括一些final方法(不可以重写),static方法(只会属于当前类),构造器(不会被重写)\n初始化 初始化,为类的静态变量赋予正确的初始值,jvm负责对类进行初始化,主要对类变量进行初始化。", "charsets": { "cjk": true }, @@ -553,11 +553,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "Java 类的加载流程是怎样的?", + "title": "Java 中 sleep() 与 wait() 的区别", "frontmatter": { - "title": "Java 类的加载流程是怎样的?", - "date": "2022-04-04T14:26:24.000Z", - "permalink": "/pages/33c5d6/", + "title": "Java 中 sleep() 与 wait() 的区别", + "date": "2022-04-04T14:27:16.000Z", + "permalink": "/pages/d69890/", "categories": [ "Java相关", "基础" @@ -566,22 +566,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/100.Java%20%E7%B1%BB%E7%9A%84%E5%8A%A0%E8%BD%BD%E6%B5%81%E7%A8%8B%E6%98%AF%E6%80%8E%E6%A0%B7%E7%9A%84%EF%BC%9F.html", - "relativePath": "01.Java相关/10.基础/100.Java 类的加载流程是怎样的?.md", - "key": "v-4c273a66", - "path": "/pages/33c5d6/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/120.Java%20%E4%B8%AD%20sleep()%20%E4%B8%8E%20wait()%20%E7%9A%84%E5%8C%BA%E5%88%AB%20.html", + "relativePath": "01.Java相关/10.基础/120.Java 中 sleep() 与 wait() 的区别 .md", + "key": "v-6ff6e2f0", + "path": "/pages/d69890/", "headers": [ { "level": 2, - "title": "Java 类的加载流程是怎样的?", - "slug": "java-类的加载流程是怎样的", - "normalizedTitle": "java 类的加载流程是怎样的?", + "title": "Java 中 sleep() 与 wait() 的区别", + "slug": "java-中-sleep-与-wait-的区别", + "normalizedTitle": "java 中 sleep() 与 wait() 的区别", "charIndex": 2 } ], - "headersStr": "Java 类的加载流程是怎样的?", - "content": "# Java 类的加载流程是怎样的?\n\n类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。 其中类加载过程包括加载、验证、准备、解析和初始化五个阶段。\n\n 做什么\n加载 类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例\n验证 验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等\n准备 为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内)\n 被final修饰的静态变量,会直接赋予原值;类字段的字段属性表中存在ConstantValue属性,则在准备阶段,其值就是ConstantValue的值\n解析 可以认为是一些静态绑定的会被解析,动态绑定则只会在运行是进行解析;静态绑定包括一些final方法(不可以重写),static方法(只会属于当前类),构造器(不会被重写)\n初始化 初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。", - "normalizedContent": "# java 类的加载流程是怎样的?\n\n类从被加载到jvm中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。 其中类加载过程包括加载、验证、准备、解析和初始化五个阶段。\n\n 做什么\n加载 类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到jvm内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.class对象实例\n验证 验证类数据信息是否符合jvm规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等\n准备 为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内)\n 被final修饰的静态变量,会直接赋予原值;类字段的字段属性表中存在constantvalue属性,则在准备阶段,其值就是constantvalue的值\n解析 可以认为是一些静态绑定的会被解析,动态绑定则只会在运行是进行解析;静态绑定包括一些final方法(不可以重写),static方法(只会属于当前类),构造器(不会被重写)\n初始化 初始化,为类的静态变量赋予正确的初始值,jvm负责对类进行初始化,主要对类变量进行初始化。", + "headersStr": "Java 中 sleep() 与 wait() 的区别", + "content": "# Java 中 sleep() 与 wait() 的区别\n\n两者都可以暂停线程的执行。区别如下:\n\n\n SLEEP() WAIT()\n 不让出锁 让出锁\n使用场景 用于暂停执行 用于线程间交互/通信\n唤醒方式 线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒 需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法\n\nyield() 和 join() 你了解吗?\n\n YIELD() JOIN()\n描述 调用yield方法,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度器可能会忽略这个暗示。 在线程A中调用线程B.join(),那么线程A会等待,cpu时间片给线程B执行,让线程B先执行,然后A再执行。\n使用场景 很少使用 使异步执行的线程变成同步执行,底层是wait方法\n\npublic class JoinTest {\n\tpublic static void main(String[] args) {\n\t\tThread t = new Thread(new ThreadImp());\n\t\tt.start();\n\t\ttry {\n\t\t\tt.join(1000);//主程序等待t结束,只等1s\n\t\t\tif(t.isAlive()){\n\t\t\t\tSystem.out.println(\"t has not finished\");\n\t\t\t}else{\n\t\t\t\tSystem.out.println(\"t has finished\");\n\t\t\t}\n\t\t\tSystem.out.println(\"Joinfinished\");\n\t\t} catch (Exception e) {\n\t\t\tSystem.out.println(e);\n\t\t}\n\t}\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n", + "normalizedContent": "# java 中 sleep() 与 wait() 的区别\n\n两者都可以暂停线程的执行。区别如下:\n\n\n sleep() wait()\n 不让出锁 让出锁\n使用场景 用于暂停执行 用于线程间交互/通信\n唤醒方式 线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒 需要别的线程调用同一个对象上的 notify() 或者 notifyall() 方法\n\nyield() 和 join() 你了解吗?\n\n yield() join()\n描述 调用yield方法,会给线程调度器一个当前线程愿意让出cpu使用的暗示,但是线程调度器可能会忽略这个暗示。 在线程a中调用线程b.join(),那么线程a会等待,cpu时间片给线程b执行,让线程b先执行,然后a再执行。\n使用场景 很少使用 使异步执行的线程变成同步执行,底层是wait方法\n\npublic class jointest {\n\tpublic static void main(string[] args) {\n\t\tthread t = new thread(new threadimp());\n\t\tt.start();\n\t\ttry {\n\t\t\tt.join(1000);//主程序等待t结束,只等1s\n\t\t\tif(t.isalive()){\n\t\t\t\tsystem.out.println(\"t has not finished\");\n\t\t\t}else{\n\t\t\t\tsystem.out.println(\"t has finished\");\n\t\t\t}\n\t\t\tsystem.out.println(\"joinfinished\");\n\t\t} catch (exception e) {\n\t\t\tsystem.out.println(e);\n\t\t}\n\t}\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n", "charsets": { "cjk": true }, @@ -589,11 +589,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "Java 中 sleep() 与 wait() 的区别", + "title": "String 类能不能被继承?为什么?", "frontmatter": { - "title": "Java 中 sleep() 与 wait() 的区别", - "date": "2022-04-04T14:27:16.000Z", - "permalink": "/pages/d69890/", + "title": "String 类能不能被继承?为什么?", + "date": "2022-04-04T14:28:11.000Z", + "permalink": "/pages/73113f/", "categories": [ "Java相关", "基础" @@ -602,22 +602,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/120.Java%20%E4%B8%AD%20sleep()%20%E4%B8%8E%20wait()%20%E7%9A%84%E5%8C%BA%E5%88%AB%20.html", - "relativePath": "01.Java相关/10.基础/120.Java 中 sleep() 与 wait() 的区别 .md", - "key": "v-6ff6e2f0", - "path": "/pages/d69890/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/140.String%20%E7%B1%BB%E8%83%BD%E4%B8%8D%E8%83%BD%E8%A2%AB%E7%BB%A7%E6%89%BF%EF%BC%9F%E4%B8%BA%E4%BB%80%E4%B9%88%EF%BC%9F.html", + "relativePath": "01.Java相关/10.基础/140.String 类能不能被继承?为什么?.md", + "key": "v-5290507e", + "path": "/pages/73113f/", "headers": [ { "level": 2, - "title": "Java 中 sleep() 与 wait() 的区别", - "slug": "java-中-sleep-与-wait-的区别", - "normalizedTitle": "java 中 sleep() 与 wait() 的区别", + "title": "String 类能不能被继承?为什么?", + "slug": "string-类能不能被继承-为什么", + "normalizedTitle": "string 类能不能被继承?为什么?", "charIndex": 2 } ], - "headersStr": "Java 中 sleep() 与 wait() 的区别", - "content": "# Java 中 sleep() 与 wait() 的区别\n\n两者都可以暂停线程的执行。区别如下:\n\n\n SLEEP() WAIT()\n 不让出锁 让出锁\n使用场景 用于暂停执行 用于线程间交互/通信\n唤醒方式 线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒 需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法\n\nyield() 和 join() 你了解吗?\n\n YIELD() JOIN()\n描述 调用yield方法,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度器可能会忽略这个暗示。 在线程A中调用线程B.join(),那么线程A会等待,cpu时间片给线程B执行,让线程B先执行,然后A再执行。\n使用场景 很少使用 使异步执行的线程变成同步执行,底层是wait方法\n\npublic class JoinTest {\n\tpublic static void main(String[] args) {\n\t\tThread t = new Thread(new ThreadImp());\n\t\tt.start();\n\t\ttry {\n\t\t\tt.join(1000);//主程序等待t结束,只等1s\n\t\t\tif(t.isAlive()){\n\t\t\t\tSystem.out.println(\"t has not finished\");\n\t\t\t}else{\n\t\t\t\tSystem.out.println(\"t has finished\");\n\t\t\t}\n\t\t\tSystem.out.println(\"Joinfinished\");\n\t\t} catch (Exception e) {\n\t\t\tSystem.out.println(e);\n\t\t}\n\t}\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n", - "normalizedContent": "# java 中 sleep() 与 wait() 的区别\n\n两者都可以暂停线程的执行。区别如下:\n\n\n sleep() wait()\n 不让出锁 让出锁\n使用场景 用于暂停执行 用于线程间交互/通信\n唤醒方式 线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒 需要别的线程调用同一个对象上的 notify() 或者 notifyall() 方法\n\nyield() 和 join() 你了解吗?\n\n yield() join()\n描述 调用yield方法,会给线程调度器一个当前线程愿意让出cpu使用的暗示,但是线程调度器可能会忽略这个暗示。 在线程a中调用线程b.join(),那么线程a会等待,cpu时间片给线程b执行,让线程b先执行,然后a再执行。\n使用场景 很少使用 使异步执行的线程变成同步执行,底层是wait方法\n\npublic class jointest {\n\tpublic static void main(string[] args) {\n\t\tthread t = new thread(new threadimp());\n\t\tt.start();\n\t\ttry {\n\t\t\tt.join(1000);//主程序等待t结束,只等1s\n\t\t\tif(t.isalive()){\n\t\t\t\tsystem.out.println(\"t has not finished\");\n\t\t\t}else{\n\t\t\t\tsystem.out.println(\"t has finished\");\n\t\t\t}\n\t\t\tsystem.out.println(\"joinfinished\");\n\t\t} catch (exception e) {\n\t\t\tsystem.out.println(e);\n\t\t}\n\t}\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n", + "headersStr": "String 类能不能被继承?为什么?", + "content": "# String 类能不能被继承?为什么?\n\n不可以,String 是被final修饰的。\n\n\npublic final class String implements java.io.Serializable, Comparable, CharSequence {\n // 省略... \n}\n\n\n1\n2\n3\n\n\n> Q:String 为什么是 final 类型?\n> A:被final修饰的类,是不可以被继承的,这样做的目的可以保证该类不被修改", + "normalizedContent": "# string 类能不能被继承?为什么?\n\n不可以,string 是被final修饰的。\n\n\npublic final class string implements java.io.serializable, comparable, charsequence {\n // 省略... \n}\n\n\n1\n2\n3\n\n\n> q:string 为什么是 final 类型?\n> a:被final修饰的类,是不可以被继承的,这样做的目的可以保证该类不被修改", "charsets": { "cjk": true }, @@ -661,11 +661,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "String 类能不能被继承?为什么?", + "title": "什么是双亲委派机制?", "frontmatter": { - "title": "String 类能不能被继承?为什么?", - "date": "2022-04-04T14:28:11.000Z", - "permalink": "/pages/73113f/", + "title": "什么是双亲委派机制?", + "date": "2022-04-04T14:26:57.000Z", + "permalink": "/pages/38bcaf/", "categories": [ "Java相关", "基础" @@ -674,22 +674,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/140.String%20%E7%B1%BB%E8%83%BD%E4%B8%8D%E8%83%BD%E8%A2%AB%E7%BB%A7%E6%89%BF%EF%BC%9F%E4%B8%BA%E4%BB%80%E4%B9%88%EF%BC%9F.html", - "relativePath": "01.Java相关/10.基础/140.String 类能不能被继承?为什么?.md", - "key": "v-5290507e", - "path": "/pages/73113f/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/110.%E4%BB%80%E4%B9%88%E6%98%AF%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE%E6%9C%BA%E5%88%B6%EF%BC%9F.html", + "relativePath": "01.Java相关/10.基础/110.什么是双亲委派机制?.md", + "key": "v-7665e30e", + "path": "/pages/38bcaf/", "headers": [ { "level": 2, - "title": "String 类能不能被继承?为什么?", - "slug": "string-类能不能被继承-为什么", - "normalizedTitle": "string 类能不能被继承?为什么?", + "title": "什么是双亲委派机制?", + "slug": "什么是双亲委派机制", + "normalizedTitle": "什么是双亲委派机制?", "charIndex": 2 } ], - "headersStr": "String 类能不能被继承?为什么?", - "content": "# String 类能不能被继承?为什么?\n\n不可以,String 是被final修饰的。\n\n\npublic final class String implements java.io.Serializable, Comparable, CharSequence {\n // 省略... \n}\n\n\n1\n2\n3\n\n\n> Q:String 为什么是 final 类型?\n> A:被final修饰的类,是不可以被继承的,这样做的目的可以保证该类不被修改", - "normalizedContent": "# string 类能不能被继承?为什么?\n\n不可以,string 是被final修饰的。\n\n\npublic final class string implements java.io.serializable, comparable, charsequence {\n // 省略... \n}\n\n\n1\n2\n3\n\n\n> q:string 为什么是 final 类型?\n> a:被final修饰的类,是不可以被继承的,这样做的目的可以保证该类不被修改", + "headersStr": "什么是双亲委派机制?", + "content": "# 什么是双亲委派机制?\n\n双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。\n\n\n\n加载器 加载哪些类\nbootstrap classloader 加载jre/lib/rt.jar\nextension classloader jre/lib/ext/*.jar\napplication classloader 加载应用程序目录\n自定义ClassLoader 定制化加载\n\n再来读一下java.lang.ClassLoader这段代码 是不是通透了许多?\n\n public Class loadClass(String name) throws ClassNotFoundException {\n return loadClass(name, false);\n }\n // -----??-----\n protected Class loadClass(String name, boolean resolve)\n throws ClassNotFoundException\n {\n // 首先,检查是否已经被类加载器加载过\n Class c = findLoadedClass(name);\n if (c == null) {\n try {\n // 存在父加载器,递归的交由父加载器\n if (parent != null) {\n c = parent.loadClass(name, false);\n } else {\n // 直到最上面的Bootstrap类加载器\n c = findBootstrapClassOrNull(name);\n }\n } catch (ClassNotFoundException e) {\n // ClassNotFoundException thrown if class not found\n // from the non-null parent class loader\n }\n \n if (c == null) {\n // If still not found, then invoke findClass in order\n // to find the class.\n c = findClass(name);\n }\n }\n return c;\n }\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n\n\n> Q1:使用双亲委派有什么好处?\n> A:双亲委派机制能保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同。\n> 考虑到安全因素。\n> 假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。\n\n> Q2:什么场景需要破坏双亲委派?\n> 双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载)\n> 基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API 但没有绝对,如果基础类调用会用户的代码怎么办呢? 比如JDBC驱动加载。 这种场景就需要用到SPI机制,是不符合双亲委派的。\n\n> Q3:JDBC、Tomcat中为什么要破坏双亲委派模型\n> A:JDBC\n> 先来回顾一下JDBC的用法\n\nString url = \"jdbc:mysql:///consult?serverTimezone=UTC\";\nString user = \"root\";\nString password = \"root\";\n\nClass.forName(\"com.mysql.jdbc.Driver\");\nConnection connection = DriverManager.getConnection(url, user, password);\n\n\n1\n2\n3\n4\n5\n6\n\n\n> DriverManager 类中要加载各个实现了Driver接口的类,然后进行管理,DriverManager位于 JAVA_HOME中jre/lib/rt.jar 包,由BootStrap类加载器加载.\n> JDBC的Driver接口定义在JDK中,其实现由各个数据库的服务商来提供,比如MySQL驱动包位于服务商MySQL提供的 Jar 中包含Driver的实现,按照双亲委派模型应该由application classloader加载\n> 实际上却是由BootStrap类加载器加载。\n> 这是因为:根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类。\n> 这就破坏了双亲委派模型。\n\nTomcat\n每个webappClassLoader加载自己目录下的class文件\n\n> Tomcat支持部署多个Web应用,多个Web应用可能使用不同版本的JDK、也可能在应用内部定义全限定名一样的类,所以要做到应用隔离。", + "normalizedContent": "# 什么是双亲委派机制?\n\n双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。\n\n\n\n加载器 加载哪些类\nbootstrap classloader 加载jre/lib/rt.jar\nextension classloader jre/lib/ext/*.jar\napplication classloader 加载应用程序目录\n自定义classloader 定制化加载\n\n再来读一下java.lang.classloader这段代码 是不是通透了许多?\n\n public class loadclass(string name) throws classnotfoundexception {\n return loadclass(name, false);\n }\n // -----??-----\n protected class loadclass(string name, boolean resolve)\n throws classnotfoundexception\n {\n // 首先,检查是否已经被类加载器加载过\n class c = findloadedclass(name);\n if (c == null) {\n try {\n // 存在父加载器,递归的交由父加载器\n if (parent != null) {\n c = parent.loadclass(name, false);\n } else {\n // 直到最上面的bootstrap类加载器\n c = findbootstrapclassornull(name);\n }\n } catch (classnotfoundexception e) {\n // classnotfoundexception thrown if class not found\n // from the non-null parent class loader\n }\n \n if (c == null) {\n // if still not found, then invoke findclass in order\n // to find the class.\n c = findclass(name);\n }\n }\n return c;\n }\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n\n\n> q1:使用双亲委派有什么好处?\n> a:双亲委派机制能保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同。\n> 考虑到安全因素。\n> 假设通过网络传递一个名为java.lang.integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心java api发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.integer,而直接返回已加载过的integer.class,这样便可以防止核心api库被随意篡改。\n\n> q2:什么场景需要破坏双亲委派?\n> 双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载)\n> 基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的api 但没有绝对,如果基础类调用会用户的代码怎么办呢? 比如jdbc驱动加载。 这种场景就需要用到spi机制,是不符合双亲委派的。\n\n> q3:jdbc、tomcat中为什么要破坏双亲委派模型\n> a:jdbc\n> 先来回顾一下jdbc的用法\n\nstring url = \"jdbc:mysql:///consult?servertimezone=utc\";\nstring user = \"root\";\nstring password = \"root\";\n\nclass.forname(\"com.mysql.jdbc.driver\");\nconnection connection = drivermanager.getconnection(url, user, password);\n\n\n1\n2\n3\n4\n5\n6\n\n\n> drivermanager 类中要加载各个实现了driver接口的类,然后进行管理,drivermanager位于 java_home中jre/lib/rt.jar 包,由bootstrap类加载器加载.\n> jdbc的driver接口定义在jdk中,其实现由各个数据库的服务商来提供,比如mysql驱动包位于服务商mysql提供的 jar 中包含driver的实现,按照双亲委派模型应该由application classloader加载\n> 实际上却是由bootstrap类加载器加载。\n> 这是因为:根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类。\n> 这就破坏了双亲委派模型。\n\ntomcat\n每个webappclassloader加载自己目录下的class文件\n\n> tomcat支持部署多个web应用,多个web应用可能使用不同版本的jdk、也可能在应用内部定义全限定名一样的类,所以要做到应用隔离。", "charsets": { "cjk": true }, @@ -768,6 +768,42 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, + { + "title": "阻塞队列都有哪几种,有什么区别?", + "frontmatter": { + "title": "阻塞队列都有哪几种,有什么区别?", + "date": "2022-04-04T14:30:18.000Z", + "permalink": "/pages/e9ce4e/", + "categories": [ + "Java相关", + "基础" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/170.%E9%98%BB%E5%A1%9E%E9%98%9F%E5%88%97%E9%83%BD%E6%9C%89%E5%93%AA%E5%87%A0%E7%A7%8D%EF%BC%8C%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F.html", + "relativePath": "01.Java相关/10.基础/170.阻塞队列都有哪几种,有什么区别?.md", + "key": "v-1218fa85", + "path": "/pages/e9ce4e/", + "headers": [ + { + "level": 2, + "title": "阻塞队列都有哪几种,有什么区别?", + "slug": "阻塞队列都有哪几种-有什么区别", + "normalizedTitle": "阻塞队列都有哪几种,有什么区别?", + "charIndex": 2 + } + ], + "headersStr": "阻塞队列都有哪几种,有什么区别?", + "content": "# 阻塞队列都有哪几种,有什么区别?\n\njava.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:\n\n\nFIFO 队列 :LinkedBlockingQueue、ArrayBlockingQueue(固定长度)\n优先级队列 :PriorityBlockingQueue\n延时队列 : DelayQueue\n同步队列 : SynchronousQueue\n\n\njava.util.concurrent.BlockingQueue提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,直到队列有空闲位置。\n\n\n \nLinkedBlockingQueue 1.内部是用链表实现的\n 2.默认容量是 Integer.MAX_VALUE\n 3.队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能\n 4.会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响\n 5.在无界的情况下,可能会造成内存溢出等问题。\nArrayBlockingQueue 1.ArrayBlockingQueue 是最典型的有界队列\n 2.其内部是用数组存储元素的\n 3.利用 ReentrantLock 实现线程安全,添加操作和移除操作采用的同一个ReenterLock锁\n 4.在创建它的时候就需要指定它的容量,之后也不可以再扩容了\n 5.构造函数中我们同样可以指定是否是公平的\nPriorityBlockingQueue 1.无界阻塞队列\n 2. 支持优先级,可以通过自定义类实现 compareTo() 方法来指定元素排序规则\nDelayQueue 1.无界队列\n 2.放入的元素必须实现 Delayed 接口\n 3.内部使用了 PriorityQueue 的能力来进行排序\nSynchronousQueue 容量为\n 0,所以没有一个地方来暂存元素,导致每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取", + "normalizedContent": "# 阻塞队列都有哪几种,有什么区别?\n\njava.util.concurrent.blockingqueue 接口有以下阻塞队列的实现:\n\n\nfifo 队列 :linkedblockingqueue、arrayblockingqueue(固定长度)\n优先级队列 :priorityblockingqueue\n延时队列 : delayqueue\n同步队列 : synchronousqueue\n\n\njava.util.concurrent.blockingqueue提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,直到队列有空闲位置。\n\n\n \nlinkedblockingqueue 1.内部是用链表实现的\n 2.默认容量是 integer.max_value\n 3.队列中的锁是分离的,其添加采用的是putlock,移除采用的则是takelock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能\n 4.会生成一个额外的node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于gc可能存在较大影响\n 5.在无界的情况下,可能会造成内存溢出等问题。\narrayblockingqueue 1.arrayblockingqueue 是最典型的有界队列\n 2.其内部是用数组存储元素的\n 3.利用 reentrantlock 实现线程安全,添加操作和移除操作采用的同一个reenterlock锁\n 4.在创建它的时候就需要指定它的容量,之后也不可以再扩容了\n 5.构造函数中我们同样可以指定是否是公平的\npriorityblockingqueue 1.无界阻塞队列\n 2. 支持优先级,可以通过自定义类实现 compareto() 方法来指定元素排序规则\ndelayqueue 1.无界队列\n 2.放入的元素必须实现 delayed 接口\n 3.内部使用了 priorityqueue 的能力来进行排序\nsynchronousqueue 容量为\n 0,所以没有一个地方来暂存元素,导致每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, { "title": "简述 HashSet 实现原理", "frontmatter": { @@ -805,35 +841,49 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "阻塞队列都有哪几种,有什么区别?", + "title": "简述 CAS 原理,什么是 ABA 问题,怎么解决?", "frontmatter": { - "title": "阻塞队列都有哪几种,有什么区别?", - "date": "2022-04-04T14:30:18.000Z", - "permalink": "/pages/e9ce4e/", + "title": "简述 CAS 原理,什么是 ABA 问题,怎么解决?", + "date": "2022-04-04T14:32:48.000Z", + "permalink": "/pages/3356c6/", "categories": [ "Java相关", - "基础" + "并发" ], "tags": [ null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/170.%E9%98%BB%E5%A1%9E%E9%98%9F%E5%88%97%E9%83%BD%E6%9C%89%E5%93%AA%E5%87%A0%E7%A7%8D%EF%BC%8C%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F.html", - "relativePath": "01.Java相关/10.基础/170.阻塞队列都有哪几种,有什么区别?.md", - "key": "v-1218fa85", - "path": "/pages/e9ce4e/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/020.%E7%AE%80%E8%BF%B0%20CAS%20%E5%8E%9F%E7%90%86%EF%BC%8C%E4%BB%80%E4%B9%88%E6%98%AF%20ABA%20%E9%97%AE%E9%A2%98%EF%BC%8C%E6%80%8E%E4%B9%88%E8%A7%A3%E5%86%B3%EF%BC%9F.html", + "relativePath": "01.Java相关/20.并发/020.简述 CAS 原理,什么是 ABA 问题,怎么解决?.md", + "key": "v-5390e3d4", + "path": "/pages/3356c6/", "headers": [ { "level": 2, - "title": "阻塞队列都有哪几种,有什么区别?", - "slug": "阻塞队列都有哪几种-有什么区别", - "normalizedTitle": "阻塞队列都有哪几种,有什么区别?", + "title": "简述 CAS 原理,什么是 ABA 问题,怎么解决?", + "slug": "简述-cas-原理-什么是-aba-问题-怎么解决", + "normalizedTitle": "简述 cas 原理,什么是 aba 问题,怎么解决?", "charIndex": 2 + }, + { + "level": 3, + "title": "CAS实现原子操作", + "slug": "cas实现原子操作", + "normalizedTitle": "cas实现原子操作", + "charIndex": 33 + }, + { + "level": 3, + "title": "ABA问题", + "slug": "aba问题", + "normalizedTitle": "aba问题", + "charIndex": 256 } ], - "headersStr": "阻塞队列都有哪几种,有什么区别?", - "content": "# 阻塞队列都有哪几种,有什么区别?\n\njava.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:\n\n\nFIFO 队列 :LinkedBlockingQueue、ArrayBlockingQueue(固定长度)\n优先级队列 :PriorityBlockingQueue\n延时队列 : DelayQueue\n同步队列 : SynchronousQueue\n\n\njava.util.concurrent.BlockingQueue提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,直到队列有空闲位置。\n\n\n \nLinkedBlockingQueue 1.内部是用链表实现的\n 2.默认容量是 Integer.MAX_VALUE\n 3.队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能\n 4.会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响\n 5.在无界的情况下,可能会造成内存溢出等问题。\nArrayBlockingQueue 1.ArrayBlockingQueue 是最典型的有界队列\n 2.其内部是用数组存储元素的\n 3.利用 ReentrantLock 实现线程安全,添加操作和移除操作采用的同一个ReenterLock锁\n 4.在创建它的时候就需要指定它的容量,之后也不可以再扩容了\n 5.构造函数中我们同样可以指定是否是公平的\nPriorityBlockingQueue 1.无界阻塞队列\n 2. 支持优先级,可以通过自定义类实现 compareTo() 方法来指定元素排序规则\nDelayQueue 1.无界队列\n 2.放入的元素必须实现 Delayed 接口\n 3.内部使用了 PriorityQueue 的能力来进行排序\nSynchronousQueue 容量为\n 0,所以没有一个地方来暂存元素,导致每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取", - "normalizedContent": "# 阻塞队列都有哪几种,有什么区别?\n\njava.util.concurrent.blockingqueue 接口有以下阻塞队列的实现:\n\n\nfifo 队列 :linkedblockingqueue、arrayblockingqueue(固定长度)\n优先级队列 :priorityblockingqueue\n延时队列 : delayqueue\n同步队列 : synchronousqueue\n\n\njava.util.concurrent.blockingqueue提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,直到队列有空闲位置。\n\n\n \nlinkedblockingqueue 1.内部是用链表实现的\n 2.默认容量是 integer.max_value\n 3.队列中的锁是分离的,其添加采用的是putlock,移除采用的则是takelock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能\n 4.会生成一个额外的node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于gc可能存在较大影响\n 5.在无界的情况下,可能会造成内存溢出等问题。\narrayblockingqueue 1.arrayblockingqueue 是最典型的有界队列\n 2.其内部是用数组存储元素的\n 3.利用 reentrantlock 实现线程安全,添加操作和移除操作采用的同一个reenterlock锁\n 4.在创建它的时候就需要指定它的容量,之后也不可以再扩容了\n 5.构造函数中我们同样可以指定是否是公平的\npriorityblockingqueue 1.无界阻塞队列\n 2. 支持优先级,可以通过自定义类实现 compareto() 方法来指定元素排序规则\ndelayqueue 1.无界队列\n 2.放入的元素必须实现 delayed 接口\n 3.内部使用了 priorityqueue 的能力来进行排序\nsynchronousqueue 容量为\n 0,所以没有一个地方来暂存元素,导致每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取", + "headersStr": "简述 CAS 原理,什么是 ABA 问题,怎么解决? CAS实现原子操作 ABA问题", + "content": "# 简述 CAS 原理,什么是 ABA 问题,怎么解决?\n\n\n# CAS实现原子操作\n\nCAS理论是 juc 包实现的基石,在intel的CPU中,CAS 通过调用本地方法(JNI)使用cmpxchg指令来实现的非阻塞算法。对比于synchronized阻塞算法,基于 CAS 实现的 juc 在性能上有了很大的提升。\n\nCAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。\n\n\n# ABA问题\n\n * 因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。\n\n * ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。\n\n * 从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。", + "normalizedContent": "# 简述 cas 原理,什么是 aba 问题,怎么解决?\n\n\n# cas实现原子操作\n\ncas理论是 juc 包实现的基石,在intel的cpu中,cas 通过调用本地方法(jni)使用cmpxchg指令来实现的非阻塞算法。对比于synchronized阻塞算法,基于 cas 实现的 juc 在性能上有了很大的提升。\n\ncas 操作包含三个操作数 —— 内存位置(v)、预期原值(a)和新值(b)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。\n\n\n# aba问题\n\n * 因为cas需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是a,变成了b,又变成了a,那么使用cas进行检查时会发现它的值没有发生变化,但是实际上却变化了。\n\n * aba问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么a-b-a 就会变成1a-2b-3a。\n\n * 从java1.5开始jdk的atomic包里提供了一个类atomicstampedreference来解决aba问题。这个类的compareandset方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。", "charsets": { "cjk": true }, @@ -841,11 +891,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "线程池是如何实现的?", + "title": "Synchronized 与 Lock 相比优缺点分别是什么?", "frontmatter": { - "title": "线程池是如何实现的?", - "date": "2022-04-04T14:38:19.000Z", - "permalink": "/pages/0da4b0/", + "title": "Synchronized 与 Lock 相比优缺点分别是什么?", + "date": "2022-04-04T14:33:27.000Z", + "permalink": "/pages/d5d161/", "categories": [ "Java相关", "并发" @@ -854,64 +904,36 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/001.%E7%BA%BF%E7%A8%8B%E6%B1%A0%E6%98%AF%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E7%9A%84%EF%BC%9F.html", - "relativePath": "01.Java相关/20.并发/001.线程池是如何实现的?.md", - "key": "v-45fc1217", - "path": "/pages/0da4b0/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/040.Synchronized%20%E5%85%B3%E9%94%AE%E5%AD%97%E5%BA%95%E5%B1%82%E6%98%AF%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E7%9A%84%EF%BC%9F.html", + "relativePath": "01.Java相关/20.并发/040.Synchronized 关键字底层是如何实现的?.md", + "key": "v-352f36cc", + "path": "/pages/d5d161/", "headers": [ { - "level": 3, - "title": "使用线程池有哪些好处", - "slug": "使用线程池有哪些好处", - "normalizedTitle": "使用线程池有哪些好处", - "charIndex": 2 - }, - { - "level": 3, - "title": "线程池的创建", - "slug": "线程池的创建", - "normalizedTitle": "线程池的创建", - "charIndex": 163 - }, - { - "level": 3, - "title": "向线程池提交一个任务后,线程池是如何处理这个任务的呢?", - "slug": "向线程池提交一个任务后-线程池是如何处理这个任务的呢", - "normalizedTitle": "向线程池提交一个任务后,线程池是如何处理这个任务的呢?", - "charIndex": 1987 - }, - { - "level": 3, - "title": "线程池是如何工作的呢?", - "slug": "线程池是如何工作的呢", - "normalizedTitle": "线程池是如何工作的呢?", - "charIndex": 3296 - }, - { - "level": 3, - "title": "向线程池提交任务", - "slug": "向线程池提交任务", - "normalizedTitle": "向线程池提交任务", - "charIndex": 5167 + "level": 2, + "title": "Lock 接口 VS Synchronized", + "slug": "lock-接口-vs-synchronized", + "normalizedTitle": "lock 接口 vs synchronized", + "charIndex": 286 }, { - "level": 3, - "title": "关闭线程池", - "slug": "关闭线程池", - "normalizedTitle": "关闭线程池", - "charIndex": 6580 + "level": 2, + "title": "Lock 锁的常规用法", + "slug": "lock-锁的常规用法", + "normalizedTitle": "lock 锁的常规用法", + "charIndex": 751 }, { - "level": 3, - "title": "合理的配置线程池", - "slug": "合理的配置线程池", - "normalizedTitle": "合理的配置线程池", - "charIndex": 6963 + "level": 2, + "title": "Lock 锁的API", + "slug": "lock-锁的api", + "normalizedTitle": "lock 锁的api", + "charIndex": 928 } ], - "headersStr": "使用线程池有哪些好处 线程池的创建 向线程池提交一个任务后,线程池是如何处理这个任务的呢? 线程池是如何工作的呢? 向线程池提交任务 关闭线程池 合理的配置线程池", - "content": "# 使用线程池有哪些好处\n\n 1. 降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁的消耗。\n 2. 提高相应速度。当任务到达时,任务可以不需要线程创建就可以执行。\n 3. 提高线程的可管理性。线程是稀缺资源、如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性。使用线程池可以统一分配、调优和监控。\n\n\n# 线程池的创建\n\n我们可以通过 ThreadPoolExecutor 来创建一个线程池\n\npublic ThreadPoolExecutor(int corePoolSize,\n int maximumPoolSize,\n long keepAliveTime,\n TimeUnit unit,\n BlockingQueue workQueue,\n ThreadFactory threadFactory,\n RejectedExecutionHandler handler) {//...}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n创建一个线程池需要传入如下几个参数:\n\n * corePoolSize: 线程池中的常驻核心线程数。\n 当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的核心线程能够执行新任务也会创建线程。当线程池中的线程数量到达 corePoolSize 时就不再创建。如果调用了线程池的 prestartAllCoreThread()方法,线程池会在启动后就创建所有核心线程。\n\n * workQueue: 存放任务的阻塞队列,被提交但尚未被执行的任务。\n 可以选择如下几个阻塞队列: 1.ArrayBlockingQueue 2.LinkedBlockingQueue:吞吐量高于LinkedBlockingQueue,是 Executors.newFixedThreadPool()创建的线程池的默认队列。 3.SynchronousQueue:一个不存储元素的队列。每个提交的任务都必须等到线程来执行,否则阻塞提交。是 Executors.newCachedThreadPool()创建的线程池的默认队列。 4.PriorityBlockingQueue:一个具有优先级的无限阻塞队列。\n\n * maximumPoolSize: 线程池最大线程数量。\n 如果队列满了并且已经创建的线程数量小于最大线程数量,则线程池会再继续创建新的线程执行任务。值得注意的是如果使用无界队列 (比如LinkedBlockingQueue)这个参数就没有意义了。\n\n * threadFactory: 用于设置创建线程的工厂\n 可以给创建的线程设置有意义的名字,可方便排查问题\n\n * handler: 拒绝策略,表示当队列满了且工作线程都满了如何来拒绝请求执行的线程的策略,默认是AbortPolicy策略,主要有四种类型。\n\n 1. AbortPolicy 直接抛出RegectedExcutionException 异常阻止系统正常进行,默认策略\n\n 2. DiscardPolicy 直接丢弃任务,不予任何处理也不抛出异常,如果允许任务丢失,这是最好的一种方案\n\n 3. DiscardOldestPolicy 抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务\n\n 4. CallerRunsPolicy 交给线程池调用所在的线程进行处理,“调用者运行”的一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量\n\n以上内置拒绝策略均实现了 RejectExcutionHandler 接口,我们也可以实现接口RejectExcutionHandler来自定义策略,如记录日志或者持久化不能拒绝的任务。\n\n * keepAliveTime: 非核心线程存活时间\n 线程池中非核心线程空闲的存活时间 当前线程池数量超过 corePoolSize 时,当空闲时间达到 keepAliveTime 值时,非核心线程会被销毁直到只剩下 corePoolSize 个线程为止。\n \n\n> tips:如果任务很多并且任务执行时间比较短,可以调大 keepAliveTime ,提高线程利用率。\n\n * unit: keepAliveTime 的时间单位\n \n\n\n# 向线程池提交一个任务后,线程池是如何处理这个任务的呢?\n\n\n\n 1. 如果当前运行的线程数=corePoolSize,则将任务加入阻塞队列。\n 3. 如果队列已满或者不能加入,接下来的处理分两种情况:\n 1. 线程池中的线程是否都处于工作状态。创建新的线程来执行任务(这一步骤需要获取全局锁)。\n 2. 线程池中有空闲的线程,则用其来执行任务。\n 4. 如果创建新线程会导致线程数量>maximumPoolSize,则执行拒绝策略。\n\n我们再从源码的角度来理解下这个过程:\nThreadPoolExecutor#execute(Runnble command) 方法\n\npublic void execute(Runnable command) {\n // 参数校验\n if (command == null)\n throw new NullPointerException();\n \n // 从 c 的值可以判断出线程池的状态, 以及线程池中线程的数量\n int c = ctl.get();\n // 1. 如果线程池的线程数量 小于 核心线程数\n if (workerCountOf(c) < corePoolSize) {\n // 添加一个核心线程 command 表示一个具体的任务, true 表示为核心线程\n if (addWorker(command, true))\n return;\n c = ctl.get();\n }\n // 2. 如果线程池处于 RUNNING 状态(只有处于此状态,才能接受新的任务)\n // 并且线程池的数量大于核心线程数, 就把任务添加到阻塞队列\n if (isRunning(c) && workQueue.offer(command)) {\n int recheck = ctl.get();\n if (! isRunning(recheck) && remove(command))\n reject(command);\n else if (workerCountOf(recheck) == 0)\n addWorker(null, false);\n }\n // 3. 如果队列也满了,就创建一个非核心线程(core==false)\n else if (!addWorker(command, false))\n // 如果创建失败,就执行拒绝策略\n reject(command);\n}\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n\n\n\n# 线程池是如何工作的呢?\n\n线程池中有一个 workers 集合,里面记录了所有的工作线程,只有拿到 mainLock 锁的线程才能访问。 ThreadPoolExecutor#workers 属性\n\nprivate final HashSet workers = new HashSet();\n\n\n1\n\n\n线程池创建线程时,会将线程包装成 Worker ,Worker 在执行任务后还会循环获取队里里的任务。我们可以从 Worker 类的 run() 方法看到这一点。\n\nfinal void runWorker(Worker w) {\n Thread wt = Thread.currentThread();\n Runnable task = w.firstTask;\n w.firstTask = null;\n w.unlock(); // allow interrupts\n boolean completedAbruptly = true;\n try {\n while (task != null || (task = getTask()) != null) {\n w.lock();\n if ((runStateAtLeast(ctl.get(), STOP) ||\n (Thread.interrupted() &&\n runStateAtLeast(ctl.get(), STOP))) &&\n !wt.isInterrupted())\n wt.interrupt();\n try {\n beforeExecute(wt, task);\n Throwable thrown = null;\n try {\n task.run();\n } catch (RuntimeException x) {\n thrown = x; throw x;\n } catch (Error x) {\n thrown = x; throw x;\n } catch (Throwable x) {\n thrown = x; throw new Error(x);\n } finally {\n afterExecute(task, thrown);\n }\n } finally {\n task = null;\n w.completedTasks++;\n w.unlock();\n }\n }\n completedAbruptly = false;\n } finally {\n processWorkerExit(w, completedAbruptly);\n }\n }\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n\n\n上面代码关键点是while循环和getTask()方法,通过循环不断的调用getTask()从阻塞队列中获取任务,通过这个方法,它与阻塞队列建立桥梁。\n\n\n# 向线程池提交任务\n\n有两个方法提交任务,分别是execute(),submit()\n\n\n * execute():execute只能提交Runnable类型的任务,用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。如果遇到异常会直接抛出。使用方法如下:\n\npublic static void main(String[] args) throws Exception{\n ExecutorService es = Executors.newSingleThreadExecutor();\n Runnable runnable = new Runnable() {\n @Override\n public void run() {\n System.out.println(\"Runnable线程处理开始...\");\n }\n };\n es.execute(runnable);\n es.shutdown();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n * submit():submit既可以提交Runnable类型的任务,也可以提交Callable类型的任务,会有一个类型为Future的返回值,但当任务类型为Runnable时,返回值为null。如果遇到异常不会直接抛出,只有在使用Future的get方法获取返回值时,才会抛出异常。使用方法:\n\npublic static void main(String[] args) throws Exception{\n ExecutorService es = Executors.newSingleThreadExecutor();\n Callable callable = new Callable() {\n @Override\n public String call() throws Exception {\n System.out.println(\"线程处理开始...\");\n return \"hello world\";\n }\n };\n Future future = es.submit(callable);\n while(true) {\n //idDone:如果任务已完成,则返回 true。 可能由于正常终止、异常或取消而完成,在所有这些情况中,此方法都将返回 true。\n if(future.isDone()) {\n System.out.println(\"任务执行完成:\" + future.get());\n break;\n }\n }\n es.shutdown();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n\n\n\n# 关闭线程池\n\n可以通过shutdown和shutdownNow方法来关闭线程池。他们的原理是遍历线程池的中工作线程,调用其interrupt()方法来中断线程,所以,无法相应中断的任务永远无法被终止。但是他们也存在一定的区别:\n\n * shutdown:将线程的状态设置为 SHUTDOWN 状态,然后中断没有执行任务的线程。\n * shutdownNow:首先将线程的状态设置为 STOP ,然后尝试停止所有正在执行或者暂停任务的线程,并返回队列中的待执行任务。\n\n只要调用了这两个方法中的任意一个,isShutdown()方法就会返回true,当所有任务都关闭后,才表示线程池关闭成功,这是调用isTerminaed()方法才会返回 true 。\n\n\n通常调用 shutdown 来关闭线程池,如果不需要等任务执行完可以调用 shutdownNow 。\n\n\n# 合理的配置线程池\n\n 1. 最大线程数 maximumPoolSize 的是指可参考如下规则:\n * CPU 密集型任务应该配置尽可能小的线程,如cpu数量+1。\n * IO密集型任务线程并不是一直占用cpu,则应该适度更多的配置线程,如2*cpu数量\n * 可以通过 Runtime.getRuntime().availableProcessors() 获得当前设备的 cpu 个数。\n 2. 建议使用有界队列。如果任务执行速度变慢,线程池不断向队列中 add 元素,会有内存溢出的风险。", - "normalizedContent": "# 使用线程池有哪些好处\n\n 1. 降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁的消耗。\n 2. 提高相应速度。当任务到达时,任务可以不需要线程创建就可以执行。\n 3. 提高线程的可管理性。线程是稀缺资源、如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性。使用线程池可以统一分配、调优和监控。\n\n\n# 线程池的创建\n\n我们可以通过 threadpoolexecutor 来创建一个线程池\n\npublic threadpoolexecutor(int corepoolsize,\n int maximumpoolsize,\n long keepalivetime,\n timeunit unit,\n blockingqueue workqueue,\n threadfactory threadfactory,\n rejectedexecutionhandler handler) {//...}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n创建一个线程池需要传入如下几个参数:\n\n * corepoolsize: 线程池中的常驻核心线程数。\n 当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的核心线程能够执行新任务也会创建线程。当线程池中的线程数量到达 corepoolsize 时就不再创建。如果调用了线程池的 prestartallcorethread()方法,线程池会在启动后就创建所有核心线程。\n\n * workqueue: 存放任务的阻塞队列,被提交但尚未被执行的任务。\n 可以选择如下几个阻塞队列: 1.arrayblockingqueue 2.linkedblockingqueue:吞吐量高于linkedblockingqueue,是 executors.newfixedthreadpool()创建的线程池的默认队列。 3.synchronousqueue:一个不存储元素的队列。每个提交的任务都必须等到线程来执行,否则阻塞提交。是 executors.newcachedthreadpool()创建的线程池的默认队列。 4.priorityblockingqueue:一个具有优先级的无限阻塞队列。\n\n * maximumpoolsize: 线程池最大线程数量。\n 如果队列满了并且已经创建的线程数量小于最大线程数量,则线程池会再继续创建新的线程执行任务。值得注意的是如果使用无界队列 (比如linkedblockingqueue)这个参数就没有意义了。\n\n * threadfactory: 用于设置创建线程的工厂\n 可以给创建的线程设置有意义的名字,可方便排查问题\n\n * handler: 拒绝策略,表示当队列满了且工作线程都满了如何来拒绝请求执行的线程的策略,默认是abortpolicy策略,主要有四种类型。\n\n 1. abortpolicy 直接抛出regectedexcutionexception 异常阻止系统正常进行,默认策略\n\n 2. discardpolicy 直接丢弃任务,不予任何处理也不抛出异常,如果允许任务丢失,这是最好的一种方案\n\n 3. discardoldestpolicy 抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务\n\n 4. callerrunspolicy 交给线程池调用所在的线程进行处理,“调用者运行”的一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量\n\n以上内置拒绝策略均实现了 rejectexcutionhandler 接口,我们也可以实现接口rejectexcutionhandler来自定义策略,如记录日志或者持久化不能拒绝的任务。\n\n * keepalivetime: 非核心线程存活时间\n 线程池中非核心线程空闲的存活时间 当前线程池数量超过 corepoolsize 时,当空闲时间达到 keepalivetime 值时,非核心线程会被销毁直到只剩下 corepoolsize 个线程为止。\n \n\n> tips:如果任务很多并且任务执行时间比较短,可以调大 keepalivetime ,提高线程利用率。\n\n * unit: keepalivetime 的时间单位\n \n\n\n# 向线程池提交一个任务后,线程池是如何处理这个任务的呢?\n\n\n\n 1. 如果当前运行的线程数=corepoolsize,则将任务加入阻塞队列。\n 3. 如果队列已满或者不能加入,接下来的处理分两种情况:\n 1. 线程池中的线程是否都处于工作状态。创建新的线程来执行任务(这一步骤需要获取全局锁)。\n 2. 线程池中有空闲的线程,则用其来执行任务。\n 4. 如果创建新线程会导致线程数量>maximumpoolsize,则执行拒绝策略。\n\n我们再从源码的角度来理解下这个过程:\nthreadpoolexecutor#execute(runnble command) 方法\n\npublic void execute(runnable command) {\n // 参数校验\n if (command == null)\n throw new nullpointerexception();\n \n // 从 c 的值可以判断出线程池的状态, 以及线程池中线程的数量\n int c = ctl.get();\n // 1. 如果线程池的线程数量 小于 核心线程数\n if (workercountof(c) < corepoolsize) {\n // 添加一个核心线程 command 表示一个具体的任务, true 表示为核心线程\n if (addworker(command, true))\n return;\n c = ctl.get();\n }\n // 2. 如果线程池处于 running 状态(只有处于此状态,才能接受新的任务)\n // 并且线程池的数量大于核心线程数, 就把任务添加到阻塞队列\n if (isrunning(c) && workqueue.offer(command)) {\n int recheck = ctl.get();\n if (! isrunning(recheck) && remove(command))\n reject(command);\n else if (workercountof(recheck) == 0)\n addworker(null, false);\n }\n // 3. 如果队列也满了,就创建一个非核心线程(core==false)\n else if (!addworker(command, false))\n // 如果创建失败,就执行拒绝策略\n reject(command);\n}\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n\n\n\n# 线程池是如何工作的呢?\n\n线程池中有一个 workers 集合,里面记录了所有的工作线程,只有拿到 mainlock 锁的线程才能访问。 threadpoolexecutor#workers 属性\n\nprivate final hashset workers = new hashset();\n\n\n1\n\n\n线程池创建线程时,会将线程包装成 worker ,worker 在执行任务后还会循环获取队里里的任务。我们可以从 worker 类的 run() 方法看到这一点。\n\nfinal void runworker(worker w) {\n thread wt = thread.currentthread();\n runnable task = w.firsttask;\n w.firsttask = null;\n w.unlock(); // allow interrupts\n boolean completedabruptly = true;\n try {\n while (task != null || (task = gettask()) != null) {\n w.lock();\n if ((runstateatleast(ctl.get(), stop) ||\n (thread.interrupted() &&\n runstateatleast(ctl.get(), stop))) &&\n !wt.isinterrupted())\n wt.interrupt();\n try {\n beforeexecute(wt, task);\n throwable thrown = null;\n try {\n task.run();\n } catch (runtimeexception x) {\n thrown = x; throw x;\n } catch (error x) {\n thrown = x; throw x;\n } catch (throwable x) {\n thrown = x; throw new error(x);\n } finally {\n afterexecute(task, thrown);\n }\n } finally {\n task = null;\n w.completedtasks++;\n w.unlock();\n }\n }\n completedabruptly = false;\n } finally {\n processworkerexit(w, completedabruptly);\n }\n }\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n\n\n上面代码关键点是while循环和gettask()方法,通过循环不断的调用gettask()从阻塞队列中获取任务,通过这个方法,它与阻塞队列建立桥梁。\n\n\n# 向线程池提交任务\n\n有两个方法提交任务,分别是execute(),submit()\n\n\n * execute():execute只能提交runnable类型的任务,用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。如果遇到异常会直接抛出。使用方法如下:\n\npublic static void main(string[] args) throws exception{\n executorservice es = executors.newsinglethreadexecutor();\n runnable runnable = new runnable() {\n @override\n public void run() {\n system.out.println(\"runnable线程处理开始...\");\n }\n };\n es.execute(runnable);\n es.shutdown();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n * submit():submit既可以提交runnable类型的任务,也可以提交callable类型的任务,会有一个类型为future的返回值,但当任务类型为runnable时,返回值为null。如果遇到异常不会直接抛出,只有在使用future的get方法获取返回值时,才会抛出异常。使用方法:\n\npublic static void main(string[] args) throws exception{\n executorservice es = executors.newsinglethreadexecutor();\n callable callable = new callable() {\n @override\n public string call() throws exception {\n system.out.println(\"线程处理开始...\");\n return \"hello world\";\n }\n };\n future future = es.submit(callable);\n while(true) {\n //iddone:如果任务已完成,则返回 true。 可能由于正常终止、异常或取消而完成,在所有这些情况中,此方法都将返回 true。\n if(future.isdone()) {\n system.out.println(\"任务执行完成:\" + future.get());\n break;\n }\n }\n es.shutdown();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n\n\n\n# 关闭线程池\n\n可以通过shutdown和shutdownnow方法来关闭线程池。他们的原理是遍历线程池的中工作线程,调用其interrupt()方法来中断线程,所以,无法相应中断的任务永远无法被终止。但是他们也存在一定的区别:\n\n * shutdown:将线程的状态设置为 shutdown 状态,然后中断没有执行任务的线程。\n * shutdownnow:首先将线程的状态设置为 stop ,然后尝试停止所有正在执行或者暂停任务的线程,并返回队列中的待执行任务。\n\n只要调用了这两个方法中的任意一个,isshutdown()方法就会返回true,当所有任务都关闭后,才表示线程池关闭成功,这是调用isterminaed()方法才会返回 true 。\n\n\n通常调用 shutdown 来关闭线程池,如果不需要等任务执行完可以调用 shutdownnow 。\n\n\n# 合理的配置线程池\n\n 1. 最大线程数 maximumpoolsize 的是指可参考如下规则:\n * cpu 密集型任务应该配置尽可能小的线程,如cpu数量+1。\n * io密集型任务线程并不是一直占用cpu,则应该适度更多的配置线程,如2*cpu数量\n * 可以通过 runtime.getruntime().availableprocessors() 获得当前设备的 cpu 个数。\n 2. 建议使用有界队列。如果任务执行速度变慢,线程池不断向队列中 add 元素,会有内存溢出的风险。", + "headersStr": "Lock 接口 VS Synchronized Lock 锁的常规用法 Lock 锁的API", + "content": "Lock 实现提供比使用 synchronized 方法和语句可以获得的更广泛的锁定操作。 它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象Condition 。\n\n虽然 synchronized 方法和语句的范围机制使得使用监视器锁更容易编程,并且有助于避免涉及锁的许多常见编程错误,但是有时您需要以更灵活的方式处理锁。\n\n例如,您获取节点A的锁定,然后获取节点B,然后释放A并获取C,然后释放B并获得D等。\n\n所述的实施方式中Lock接口通过允许获得并在不同的范围释放的锁,并允许获得并以任何顺序释放多个锁使得能够使用这样的技术。\n\n\n# Lock 接口 VS Synchronized\n\nJava SE 5之后,juc 包中增加了 Lock 接口,在此之前 Java 是靠 Synchronized 关键字来实现锁功能的,二者具有相同的内存同步语义,区别如下:\n\n SYNCHRONIZED LOCK接口\n加锁、解锁 隐式的加锁、解锁 显式的加锁、解锁\n是否异常自动释放锁 是 不是,必须手动unlock来释放锁\n是否支持相应中断 只能等待锁的释放,不能响应中断 等待锁过程中可以用interrupt来中断等待\n适用场景 比如这种场景:先获得A锁,再获取B锁,获取到B锁释放A锁同时获取C锁,获得C锁后释放B锁在获取D锁。这种场景用Synchronized就不好实现了。\n\nLock 相比于 Synchronized 还有如下特性:\n\n * 尝试非阻塞获取锁\n * 能被中断的获取锁\n * 超时获取锁\n\n\n# Lock 锁的常规用法\n\n Lock l = ...; \n l.lock(); \n try {\n // access the resource protected by this lock }\n finally { \n l.unlock(); \n } \n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# Lock 锁的API\n\nLock 接口定义了所得获取和释放的基本操作, Lock的API如下:\n\n方法 描述\nvoid lock() 获得锁\nvoid lockInterruptibly() 获取锁定,除非当前线程是 interrupted 。\nCondition newCondition() 返回一个新Condition绑定到该实例Lock实例。\nboolean tryLock() 只有在调用时才可以获得锁。\nboolean tryLock(long time, TimeUnit unit) 如果在给定的等待时间内是空闲的,并且当前的线程尚未得到 interrupted,则获取该锁\nvoid unlock() 释放锁。", + "normalizedContent": "lock 实现提供比使用 synchronized 方法和语句可以获得的更广泛的锁定操作。 它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象condition 。\n\n虽然 synchronized 方法和语句的范围机制使得使用监视器锁更容易编程,并且有助于避免涉及锁的许多常见编程错误,但是有时您需要以更灵活的方式处理锁。\n\n例如,您获取节点a的锁定,然后获取节点b,然后释放a并获取c,然后释放b并获得d等。\n\n所述的实施方式中lock接口通过允许获得并在不同的范围释放的锁,并允许获得并以任何顺序释放多个锁使得能够使用这样的技术。\n\n\n# lock 接口 vs synchronized\n\njava se 5之后,juc 包中增加了 lock 接口,在此之前 java 是靠 synchronized 关键字来实现锁功能的,二者具有相同的内存同步语义,区别如下:\n\n synchronized lock接口\n加锁、解锁 隐式的加锁、解锁 显式的加锁、解锁\n是否异常自动释放锁 是 不是,必须手动unlock来释放锁\n是否支持相应中断 只能等待锁的释放,不能响应中断 等待锁过程中可以用interrupt来中断等待\n适用场景 比如这种场景:先获得a锁,再获取b锁,获取到b锁释放a锁同时获取c锁,获得c锁后释放b锁在获取d锁。这种场景用synchronized就不好实现了。\n\nlock 相比于 synchronized 还有如下特性:\n\n * 尝试非阻塞获取锁\n * 能被中断的获取锁\n * 超时获取锁\n\n\n# lock 锁的常规用法\n\n lock l = ...; \n l.lock(); \n try {\n // access the resource protected by this lock }\n finally { \n l.unlock(); \n } \n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# lock 锁的api\n\nlock 接口定义了所得获取和释放的基本操作, lock的api如下:\n\n方法 描述\nvoid lock() 获得锁\nvoid lockinterruptibly() 获取锁定,除非当前线程是 interrupted 。\ncondition newcondition() 返回一个新condition绑定到该实例lock实例。\nboolean trylock() 只有在调用时才可以获得锁。\nboolean trylock(long time, timeunit unit) 如果在给定的等待时间内是空闲的,并且当前的线程尚未得到 interrupted,则获取该锁\nvoid unlock() 释放锁。", "charsets": { "cjk": true }, @@ -1053,11 +1075,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "简述 CAS 原理,什么是 ABA 问题,怎么解决?", + "title": "volatile 关键字解决了什么问题,它的实现原理是什么?", "frontmatter": { - "title": "简述 CAS 原理,什么是 ABA 问题,怎么解决?", - "date": "2022-04-04T14:32:48.000Z", - "permalink": "/pages/3356c6/", + "title": "volatile 关键字解决了什么问题,它的实现原理是什么?", + "date": "2022-04-04T14:34:07.000Z", + "permalink": "/pages/383264/", "categories": [ "Java相关", "并发" @@ -1066,36 +1088,29 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/020.%E7%AE%80%E8%BF%B0%20CAS%20%E5%8E%9F%E7%90%86%EF%BC%8C%E4%BB%80%E4%B9%88%E6%98%AF%20ABA%20%E9%97%AE%E9%A2%98%EF%BC%8C%E6%80%8E%E4%B9%88%E8%A7%A3%E5%86%B3%EF%BC%9F.html", - "relativePath": "01.Java相关/20.并发/020.简述 CAS 原理,什么是 ABA 问题,怎么解决?.md", - "key": "v-5390e3d4", - "path": "/pages/3356c6/", - "headers": [ - { - "level": 2, - "title": "简述 CAS 原理,什么是 ABA 问题,怎么解决?", - "slug": "简述-cas-原理-什么是-aba-问题-怎么解决", - "normalizedTitle": "简述 cas 原理,什么是 aba 问题,怎么解决?", - "charIndex": 2 - }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/060.volatile%20%E5%85%B3%E9%94%AE%E5%AD%97%E8%A7%A3%E5%86%B3%E4%BA%86%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98%EF%BC%8C%E5%AE%83%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", + "relativePath": "01.Java相关/20.并发/060.volatile 关键字解决了什么问题,它的实现原理是什么?.md", + "key": "v-f5bcd180", + "path": "/pages/383264/", + "headers": [ { - "level": 3, - "title": "CAS实现原子操作", - "slug": "cas实现原子操作", - "normalizedTitle": "cas实现原子操作", - "charIndex": 33 + "level": 2, + "title": "Java中的内存可见性", + "slug": "java中的内存可见性", + "normalizedTitle": "java中的内存可见性", + "charIndex": 129 }, { - "level": 3, - "title": "ABA问题", - "slug": "aba问题", - "normalizedTitle": "aba问题", - "charIndex": 256 + "level": 2, + "title": "Volatile 的实现原理", + "slug": "volatile-的实现原理", + "normalizedTitle": "volatile 的实现原理", + "charIndex": 335 } ], - "headersStr": "简述 CAS 原理,什么是 ABA 问题,怎么解决? CAS实现原子操作 ABA问题", - "content": "# 简述 CAS 原理,什么是 ABA 问题,怎么解决?\n\n\n# CAS实现原子操作\n\nCAS理论是 juc 包实现的基石,在intel的CPU中,CAS 通过调用本地方法(JNI)使用cmpxchg指令来实现的非阻塞算法。对比于synchronized阻塞算法,基于 CAS 实现的 juc 在性能上有了很大的提升。\n\nCAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。\n\n\n# ABA问题\n\n * 因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。\n\n * ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。\n\n * 从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。", - "normalizedContent": "# 简述 cas 原理,什么是 aba 问题,怎么解决?\n\n\n# cas实现原子操作\n\ncas理论是 juc 包实现的基石,在intel的cpu中,cas 通过调用本地方法(jni)使用cmpxchg指令来实现的非阻塞算法。对比于synchronized阻塞算法,基于 cas 实现的 juc 在性能上有了很大的提升。\n\ncas 操作包含三个操作数 —— 内存位置(v)、预期原值(a)和新值(b)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。\n\n\n# aba问题\n\n * 因为cas需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是a,变成了b,又变成了a,那么使用cas进行检查时会发现它的值没有发生变化,但是实际上却变化了。\n\n * aba问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么a-b-a 就会变成1a-2b-3a。\n\n * 从java1.5开始jdk的atomic包里提供了一个类atomicstampedreference来解决aba问题。这个类的compareandset方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。", + "headersStr": "Java中的内存可见性 Volatile 的实现原理", + "content": "volatile 能保证 Java 中的内存可见性。 可见性的意思是:当线程A修改共享变量的值后,线程B能立刻读到这个修改后的值。\n\n\nVolatile 不会引起线程上下文的切换和调度,如果使用的恰当,会比 Synchronized 执行成本更低。\n\n\n# Java中的内存可见性\n\n * 可见性:一个线程对共享变量值的修改,能够及时被其他线程看到。\n\n * 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那这个变量就是这几个线程的共享变量。\n\n * Java内存的规定:\n\n-线程对共享变量的所有操作都必须在自己的工作内存中进行,不可直接从主内存中读写; -不同线程之间无法直接访问其他线程工作内存中的变量,线程间的变量值的传递需要通过主内存。\n\n\n\n\n# Volatile 的实现原理\n\n如果对用 Volatile 修饰的变量写操作,JVM 会向处理器发出一条 Lock 前缀的指令,Lock 前缀的指令在多核处理器下会引发两件事情:\n\n 1. 将当前处理器缓存行的数据写会到系统内存\n\n 2. 这个写会内存的操作会使其他缓存中的该内存地址的数据无效", + "normalizedContent": "volatile 能保证 java 中的内存可见性。 可见性的意思是:当线程a修改共享变量的值后,线程b能立刻读到这个修改后的值。\n\n\nvolatile 不会引起线程上下文的切换和调度,如果使用的恰当,会比 synchronized 执行成本更低。\n\n\n# java中的内存可见性\n\n * 可见性:一个线程对共享变量值的修改,能够及时被其他线程看到。\n\n * 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那这个变量就是这几个线程的共享变量。\n\n * java内存的规定:\n\n-线程对共享变量的所有操作都必须在自己的工作内存中进行,不可直接从主内存中读写; -不同线程之间无法直接访问其他线程工作内存中的变量,线程间的变量值的传递需要通过主内存。\n\n\n\n\n# volatile 的实现原理\n\n如果对用 volatile 修饰的变量写操作,jvm 会向处理器发出一条 lock 前缀的指令,lock 前缀的指令在多核处理器下会引发两件事情:\n\n 1. 将当前处理器缓存行的数据写会到系统内存\n\n 2. 这个写会内存的操作会使其他缓存中的该内存地址的数据无效", "charsets": { "cjk": true }, @@ -1103,11 +1118,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "Synchronized 与 Lock 相比优缺点分别是什么?", + "title": "线程池是如何实现的?", "frontmatter": { - "title": "Synchronized 与 Lock 相比优缺点分别是什么?", - "date": "2022-04-04T14:33:27.000Z", - "permalink": "/pages/d5d161/", + "title": "线程池是如何实现的?", + "date": "2022-04-04T14:38:19.000Z", + "permalink": "/pages/0da4b0/", "categories": [ "Java相关", "并发" @@ -1116,79 +1131,64 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/040.Synchronized%20%E5%85%B3%E9%94%AE%E5%AD%97%E5%BA%95%E5%B1%82%E6%98%AF%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E7%9A%84%EF%BC%9F.html", - "relativePath": "01.Java相关/20.并发/040.Synchronized 关键字底层是如何实现的?.md", - "key": "v-352f36cc", - "path": "/pages/d5d161/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/001.%E7%BA%BF%E7%A8%8B%E6%B1%A0%E6%98%AF%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E7%9A%84%EF%BC%9F.html", + "relativePath": "01.Java相关/20.并发/001.线程池是如何实现的?.md", + "key": "v-45fc1217", + "path": "/pages/0da4b0/", "headers": [ { - "level": 2, - "title": "Lock 接口 VS Synchronized", - "slug": "lock-接口-vs-synchronized", - "normalizedTitle": "lock 接口 vs synchronized", - "charIndex": 286 + "level": 3, + "title": "使用线程池有哪些好处", + "slug": "使用线程池有哪些好处", + "normalizedTitle": "使用线程池有哪些好处", + "charIndex": 2 }, { - "level": 2, - "title": "Lock 锁的常规用法", - "slug": "lock-锁的常规用法", - "normalizedTitle": "lock 锁的常规用法", - "charIndex": 751 + "level": 3, + "title": "线程池的创建", + "slug": "线程池的创建", + "normalizedTitle": "线程池的创建", + "charIndex": 163 }, { - "level": 2, - "title": "Lock 锁的API", - "slug": "lock-锁的api", - "normalizedTitle": "lock 锁的api", - "charIndex": 928 - } - ], - "headersStr": "Lock 接口 VS Synchronized Lock 锁的常规用法 Lock 锁的API", - "content": "Lock 实现提供比使用 synchronized 方法和语句可以获得的更广泛的锁定操作。 它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象Condition 。\n\n虽然 synchronized 方法和语句的范围机制使得使用监视器锁更容易编程,并且有助于避免涉及锁的许多常见编程错误,但是有时您需要以更灵活的方式处理锁。\n\n例如,您获取节点A的锁定,然后获取节点B,然后释放A并获取C,然后释放B并获得D等。\n\n所述的实施方式中Lock接口通过允许获得并在不同的范围释放的锁,并允许获得并以任何顺序释放多个锁使得能够使用这样的技术。\n\n\n# Lock 接口 VS Synchronized\n\nJava SE 5之后,juc 包中增加了 Lock 接口,在此之前 Java 是靠 Synchronized 关键字来实现锁功能的,二者具有相同的内存同步语义,区别如下:\n\n SYNCHRONIZED LOCK接口\n加锁、解锁 隐式的加锁、解锁 显式的加锁、解锁\n是否异常自动释放锁 是 不是,必须手动unlock来释放锁\n是否支持相应中断 只能等待锁的释放,不能响应中断 等待锁过程中可以用interrupt来中断等待\n适用场景 比如这种场景:先获得A锁,再获取B锁,获取到B锁释放A锁同时获取C锁,获得C锁后释放B锁在获取D锁。这种场景用Synchronized就不好实现了。\n\nLock 相比于 Synchronized 还有如下特性:\n\n * 尝试非阻塞获取锁\n * 能被中断的获取锁\n * 超时获取锁\n\n\n# Lock 锁的常规用法\n\n Lock l = ...; \n l.lock(); \n try {\n // access the resource protected by this lock }\n finally { \n l.unlock(); \n } \n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# Lock 锁的API\n\nLock 接口定义了所得获取和释放的基本操作, Lock的API如下:\n\n方法 描述\nvoid lock() 获得锁\nvoid lockInterruptibly() 获取锁定,除非当前线程是 interrupted 。\nCondition newCondition() 返回一个新Condition绑定到该实例Lock实例。\nboolean tryLock() 只有在调用时才可以获得锁。\nboolean tryLock(long time, TimeUnit unit) 如果在给定的等待时间内是空闲的,并且当前的线程尚未得到 interrupted,则获取该锁\nvoid unlock() 释放锁。", - "normalizedContent": "lock 实现提供比使用 synchronized 方法和语句可以获得的更广泛的锁定操作。 它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象condition 。\n\n虽然 synchronized 方法和语句的范围机制使得使用监视器锁更容易编程,并且有助于避免涉及锁的许多常见编程错误,但是有时您需要以更灵活的方式处理锁。\n\n例如,您获取节点a的锁定,然后获取节点b,然后释放a并获取c,然后释放b并获得d等。\n\n所述的实施方式中lock接口通过允许获得并在不同的范围释放的锁,并允许获得并以任何顺序释放多个锁使得能够使用这样的技术。\n\n\n# lock 接口 vs synchronized\n\njava se 5之后,juc 包中增加了 lock 接口,在此之前 java 是靠 synchronized 关键字来实现锁功能的,二者具有相同的内存同步语义,区别如下:\n\n synchronized lock接口\n加锁、解锁 隐式的加锁、解锁 显式的加锁、解锁\n是否异常自动释放锁 是 不是,必须手动unlock来释放锁\n是否支持相应中断 只能等待锁的释放,不能响应中断 等待锁过程中可以用interrupt来中断等待\n适用场景 比如这种场景:先获得a锁,再获取b锁,获取到b锁释放a锁同时获取c锁,获得c锁后释放b锁在获取d锁。这种场景用synchronized就不好实现了。\n\nlock 相比于 synchronized 还有如下特性:\n\n * 尝试非阻塞获取锁\n * 能被中断的获取锁\n * 超时获取锁\n\n\n# lock 锁的常规用法\n\n lock l = ...; \n l.lock(); \n try {\n // access the resource protected by this lock }\n finally { \n l.unlock(); \n } \n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# lock 锁的api\n\nlock 接口定义了所得获取和释放的基本操作, lock的api如下:\n\n方法 描述\nvoid lock() 获得锁\nvoid lockinterruptibly() 获取锁定,除非当前线程是 interrupted 。\ncondition newcondition() 返回一个新condition绑定到该实例lock实例。\nboolean trylock() 只有在调用时才可以获得锁。\nboolean trylock(long time, timeunit unit) 如果在给定的等待时间内是空闲的,并且当前的线程尚未得到 interrupted,则获取该锁\nvoid unlock() 释放锁。", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, - { - "title": "volatile 关键字解决了什么问题,它的实现原理是什么?", - "frontmatter": { - "title": "volatile 关键字解决了什么问题,它的实现原理是什么?", - "date": "2022-04-04T14:34:07.000Z", - "permalink": "/pages/383264/", - "categories": [ - "Java相关", - "并发" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/060.volatile%20%E5%85%B3%E9%94%AE%E5%AD%97%E8%A7%A3%E5%86%B3%E4%BA%86%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98%EF%BC%8C%E5%AE%83%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", - "relativePath": "01.Java相关/20.并发/060.volatile 关键字解决了什么问题,它的实现原理是什么?.md", - "key": "v-f5bcd180", - "path": "/pages/383264/", - "headers": [ + "level": 3, + "title": "向线程池提交一个任务后,线程池是如何处理这个任务的呢?", + "slug": "向线程池提交一个任务后-线程池是如何处理这个任务的呢", + "normalizedTitle": "向线程池提交一个任务后,线程池是如何处理这个任务的呢?", + "charIndex": 1987 + }, { - "level": 2, - "title": "Java中的内存可见性", - "slug": "java中的内存可见性", - "normalizedTitle": "java中的内存可见性", - "charIndex": 129 + "level": 3, + "title": "线程池是如何工作的呢?", + "slug": "线程池是如何工作的呢", + "normalizedTitle": "线程池是如何工作的呢?", + "charIndex": 3296 }, { - "level": 2, - "title": "Volatile 的实现原理", - "slug": "volatile-的实现原理", - "normalizedTitle": "volatile 的实现原理", - "charIndex": 335 + "level": 3, + "title": "向线程池提交任务", + "slug": "向线程池提交任务", + "normalizedTitle": "向线程池提交任务", + "charIndex": 5167 + }, + { + "level": 3, + "title": "关闭线程池", + "slug": "关闭线程池", + "normalizedTitle": "关闭线程池", + "charIndex": 6580 + }, + { + "level": 3, + "title": "合理的配置线程池", + "slug": "合理的配置线程池", + "normalizedTitle": "合理的配置线程池", + "charIndex": 6963 } ], - "headersStr": "Java中的内存可见性 Volatile 的实现原理", - "content": "volatile 能保证 Java 中的内存可见性。 可见性的意思是:当线程A修改共享变量的值后,线程B能立刻读到这个修改后的值。\n\n\nVolatile 不会引起线程上下文的切换和调度,如果使用的恰当,会比 Synchronized 执行成本更低。\n\n\n# Java中的内存可见性\n\n * 可见性:一个线程对共享变量值的修改,能够及时被其他线程看到。\n\n * 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那这个变量就是这几个线程的共享变量。\n\n * Java内存的规定:\n\n-线程对共享变量的所有操作都必须在自己的工作内存中进行,不可直接从主内存中读写; -不同线程之间无法直接访问其他线程工作内存中的变量,线程间的变量值的传递需要通过主内存。\n\n\n\n\n# Volatile 的实现原理\n\n如果对用 Volatile 修饰的变量写操作,JVM 会向处理器发出一条 Lock 前缀的指令,Lock 前缀的指令在多核处理器下会引发两件事情:\n\n 1. 将当前处理器缓存行的数据写会到系统内存\n\n 2. 这个写会内存的操作会使其他缓存中的该内存地址的数据无效", - "normalizedContent": "volatile 能保证 java 中的内存可见性。 可见性的意思是:当线程a修改共享变量的值后,线程b能立刻读到这个修改后的值。\n\n\nvolatile 不会引起线程上下文的切换和调度,如果使用的恰当,会比 synchronized 执行成本更低。\n\n\n# java中的内存可见性\n\n * 可见性:一个线程对共享变量值的修改,能够及时被其他线程看到。\n\n * 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那这个变量就是这几个线程的共享变量。\n\n * java内存的规定:\n\n-线程对共享变量的所有操作都必须在自己的工作内存中进行,不可直接从主内存中读写; -不同线程之间无法直接访问其他线程工作内存中的变量,线程间的变量值的传递需要通过主内存。\n\n\n\n\n# volatile 的实现原理\n\n如果对用 volatile 修饰的变量写操作,jvm 会向处理器发出一条 lock 前缀的指令,lock 前缀的指令在多核处理器下会引发两件事情:\n\n 1. 将当前处理器缓存行的数据写会到系统内存\n\n 2. 这个写会内存的操作会使其他缓存中的该内存地址的数据无效", + "headersStr": "使用线程池有哪些好处 线程池的创建 向线程池提交一个任务后,线程池是如何处理这个任务的呢? 线程池是如何工作的呢? 向线程池提交任务 关闭线程池 合理的配置线程池", + "content": "# 使用线程池有哪些好处\n\n 1. 降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁的消耗。\n 2. 提高相应速度。当任务到达时,任务可以不需要线程创建就可以执行。\n 3. 提高线程的可管理性。线程是稀缺资源、如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性。使用线程池可以统一分配、调优和监控。\n\n\n# 线程池的创建\n\n我们可以通过 ThreadPoolExecutor 来创建一个线程池\n\npublic ThreadPoolExecutor(int corePoolSize,\n int maximumPoolSize,\n long keepAliveTime,\n TimeUnit unit,\n BlockingQueue workQueue,\n ThreadFactory threadFactory,\n RejectedExecutionHandler handler) {//...}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n创建一个线程池需要传入如下几个参数:\n\n * corePoolSize: 线程池中的常驻核心线程数。\n 当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的核心线程能够执行新任务也会创建线程。当线程池中的线程数量到达 corePoolSize 时就不再创建。如果调用了线程池的 prestartAllCoreThread()方法,线程池会在启动后就创建所有核心线程。\n\n * workQueue: 存放任务的阻塞队列,被提交但尚未被执行的任务。\n 可以选择如下几个阻塞队列: 1.ArrayBlockingQueue 2.LinkedBlockingQueue:吞吐量高于LinkedBlockingQueue,是 Executors.newFixedThreadPool()创建的线程池的默认队列。 3.SynchronousQueue:一个不存储元素的队列。每个提交的任务都必须等到线程来执行,否则阻塞提交。是 Executors.newCachedThreadPool()创建的线程池的默认队列。 4.PriorityBlockingQueue:一个具有优先级的无限阻塞队列。\n\n * maximumPoolSize: 线程池最大线程数量。\n 如果队列满了并且已经创建的线程数量小于最大线程数量,则线程池会再继续创建新的线程执行任务。值得注意的是如果使用无界队列 (比如LinkedBlockingQueue)这个参数就没有意义了。\n\n * threadFactory: 用于设置创建线程的工厂\n 可以给创建的线程设置有意义的名字,可方便排查问题\n\n * handler: 拒绝策略,表示当队列满了且工作线程都满了如何来拒绝请求执行的线程的策略,默认是AbortPolicy策略,主要有四种类型。\n\n 1. AbortPolicy 直接抛出RegectedExcutionException 异常阻止系统正常进行,默认策略\n\n 2. DiscardPolicy 直接丢弃任务,不予任何处理也不抛出异常,如果允许任务丢失,这是最好的一种方案\n\n 3. DiscardOldestPolicy 抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务\n\n 4. CallerRunsPolicy 交给线程池调用所在的线程进行处理,“调用者运行”的一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量\n\n以上内置拒绝策略均实现了 RejectExcutionHandler 接口,我们也可以实现接口RejectExcutionHandler来自定义策略,如记录日志或者持久化不能拒绝的任务。\n\n * keepAliveTime: 非核心线程存活时间\n 线程池中非核心线程空闲的存活时间 当前线程池数量超过 corePoolSize 时,当空闲时间达到 keepAliveTime 值时,非核心线程会被销毁直到只剩下 corePoolSize 个线程为止。\n \n\n> tips:如果任务很多并且任务执行时间比较短,可以调大 keepAliveTime ,提高线程利用率。\n\n * unit: keepAliveTime 的时间单位\n \n\n\n# 向线程池提交一个任务后,线程池是如何处理这个任务的呢?\n\n\n\n 1. 如果当前运行的线程数=corePoolSize,则将任务加入阻塞队列。\n 3. 如果队列已满或者不能加入,接下来的处理分两种情况:\n 1. 线程池中的线程是否都处于工作状态。创建新的线程来执行任务(这一步骤需要获取全局锁)。\n 2. 线程池中有空闲的线程,则用其来执行任务。\n 4. 如果创建新线程会导致线程数量>maximumPoolSize,则执行拒绝策略。\n\n我们再从源码的角度来理解下这个过程:\nThreadPoolExecutor#execute(Runnble command) 方法\n\npublic void execute(Runnable command) {\n // 参数校验\n if (command == null)\n throw new NullPointerException();\n \n // 从 c 的值可以判断出线程池的状态, 以及线程池中线程的数量\n int c = ctl.get();\n // 1. 如果线程池的线程数量 小于 核心线程数\n if (workerCountOf(c) < corePoolSize) {\n // 添加一个核心线程 command 表示一个具体的任务, true 表示为核心线程\n if (addWorker(command, true))\n return;\n c = ctl.get();\n }\n // 2. 如果线程池处于 RUNNING 状态(只有处于此状态,才能接受新的任务)\n // 并且线程池的数量大于核心线程数, 就把任务添加到阻塞队列\n if (isRunning(c) && workQueue.offer(command)) {\n int recheck = ctl.get();\n if (! isRunning(recheck) && remove(command))\n reject(command);\n else if (workerCountOf(recheck) == 0)\n addWorker(null, false);\n }\n // 3. 如果队列也满了,就创建一个非核心线程(core==false)\n else if (!addWorker(command, false))\n // 如果创建失败,就执行拒绝策略\n reject(command);\n}\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n\n\n\n# 线程池是如何工作的呢?\n\n线程池中有一个 workers 集合,里面记录了所有的工作线程,只有拿到 mainLock 锁的线程才能访问。 ThreadPoolExecutor#workers 属性\n\nprivate final HashSet workers = new HashSet();\n\n\n1\n\n\n线程池创建线程时,会将线程包装成 Worker ,Worker 在执行任务后还会循环获取队里里的任务。我们可以从 Worker 类的 run() 方法看到这一点。\n\nfinal void runWorker(Worker w) {\n Thread wt = Thread.currentThread();\n Runnable task = w.firstTask;\n w.firstTask = null;\n w.unlock(); // allow interrupts\n boolean completedAbruptly = true;\n try {\n while (task != null || (task = getTask()) != null) {\n w.lock();\n if ((runStateAtLeast(ctl.get(), STOP) ||\n (Thread.interrupted() &&\n runStateAtLeast(ctl.get(), STOP))) &&\n !wt.isInterrupted())\n wt.interrupt();\n try {\n beforeExecute(wt, task);\n Throwable thrown = null;\n try {\n task.run();\n } catch (RuntimeException x) {\n thrown = x; throw x;\n } catch (Error x) {\n thrown = x; throw x;\n } catch (Throwable x) {\n thrown = x; throw new Error(x);\n } finally {\n afterExecute(task, thrown);\n }\n } finally {\n task = null;\n w.completedTasks++;\n w.unlock();\n }\n }\n completedAbruptly = false;\n } finally {\n processWorkerExit(w, completedAbruptly);\n }\n }\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n\n\n上面代码关键点是while循环和getTask()方法,通过循环不断的调用getTask()从阻塞队列中获取任务,通过这个方法,它与阻塞队列建立桥梁。\n\n\n# 向线程池提交任务\n\n有两个方法提交任务,分别是execute(),submit()\n\n\n * execute():execute只能提交Runnable类型的任务,用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。如果遇到异常会直接抛出。使用方法如下:\n\npublic static void main(String[] args) throws Exception{\n ExecutorService es = Executors.newSingleThreadExecutor();\n Runnable runnable = new Runnable() {\n @Override\n public void run() {\n System.out.println(\"Runnable线程处理开始...\");\n }\n };\n es.execute(runnable);\n es.shutdown();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n * submit():submit既可以提交Runnable类型的任务,也可以提交Callable类型的任务,会有一个类型为Future的返回值,但当任务类型为Runnable时,返回值为null。如果遇到异常不会直接抛出,只有在使用Future的get方法获取返回值时,才会抛出异常。使用方法:\n\npublic static void main(String[] args) throws Exception{\n ExecutorService es = Executors.newSingleThreadExecutor();\n Callable callable = new Callable() {\n @Override\n public String call() throws Exception {\n System.out.println(\"线程处理开始...\");\n return \"hello world\";\n }\n };\n Future future = es.submit(callable);\n while(true) {\n //idDone:如果任务已完成,则返回 true。 可能由于正常终止、异常或取消而完成,在所有这些情况中,此方法都将返回 true。\n if(future.isDone()) {\n System.out.println(\"任务执行完成:\" + future.get());\n break;\n }\n }\n es.shutdown();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n\n\n\n# 关闭线程池\n\n可以通过shutdown和shutdownNow方法来关闭线程池。他们的原理是遍历线程池的中工作线程,调用其interrupt()方法来中断线程,所以,无法相应中断的任务永远无法被终止。但是他们也存在一定的区别:\n\n * shutdown:将线程的状态设置为 SHUTDOWN 状态,然后中断没有执行任务的线程。\n * shutdownNow:首先将线程的状态设置为 STOP ,然后尝试停止所有正在执行或者暂停任务的线程,并返回队列中的待执行任务。\n\n只要调用了这两个方法中的任意一个,isShutdown()方法就会返回true,当所有任务都关闭后,才表示线程池关闭成功,这是调用isTerminaed()方法才会返回 true 。\n\n\n通常调用 shutdown 来关闭线程池,如果不需要等任务执行完可以调用 shutdownNow 。\n\n\n# 合理的配置线程池\n\n 1. 最大线程数 maximumPoolSize 的是指可参考如下规则:\n * CPU 密集型任务应该配置尽可能小的线程,如cpu数量+1。\n * IO密集型任务线程并不是一直占用cpu,则应该适度更多的配置线程,如2*cpu数量\n * 可以通过 Runtime.getRuntime().availableProcessors() 获得当前设备的 cpu 个数。\n 2. 建议使用有界队列。如果任务执行速度变慢,线程池不断向队列中 add 元素,会有内存溢出的风险。", + "normalizedContent": "# 使用线程池有哪些好处\n\n 1. 降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁的消耗。\n 2. 提高相应速度。当任务到达时,任务可以不需要线程创建就可以执行。\n 3. 提高线程的可管理性。线程是稀缺资源、如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性。使用线程池可以统一分配、调优和监控。\n\n\n# 线程池的创建\n\n我们可以通过 threadpoolexecutor 来创建一个线程池\n\npublic threadpoolexecutor(int corepoolsize,\n int maximumpoolsize,\n long keepalivetime,\n timeunit unit,\n blockingqueue workqueue,\n threadfactory threadfactory,\n rejectedexecutionhandler handler) {//...}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n创建一个线程池需要传入如下几个参数:\n\n * corepoolsize: 线程池中的常驻核心线程数。\n 当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的核心线程能够执行新任务也会创建线程。当线程池中的线程数量到达 corepoolsize 时就不再创建。如果调用了线程池的 prestartallcorethread()方法,线程池会在启动后就创建所有核心线程。\n\n * workqueue: 存放任务的阻塞队列,被提交但尚未被执行的任务。\n 可以选择如下几个阻塞队列: 1.arrayblockingqueue 2.linkedblockingqueue:吞吐量高于linkedblockingqueue,是 executors.newfixedthreadpool()创建的线程池的默认队列。 3.synchronousqueue:一个不存储元素的队列。每个提交的任务都必须等到线程来执行,否则阻塞提交。是 executors.newcachedthreadpool()创建的线程池的默认队列。 4.priorityblockingqueue:一个具有优先级的无限阻塞队列。\n\n * maximumpoolsize: 线程池最大线程数量。\n 如果队列满了并且已经创建的线程数量小于最大线程数量,则线程池会再继续创建新的线程执行任务。值得注意的是如果使用无界队列 (比如linkedblockingqueue)这个参数就没有意义了。\n\n * threadfactory: 用于设置创建线程的工厂\n 可以给创建的线程设置有意义的名字,可方便排查问题\n\n * handler: 拒绝策略,表示当队列满了且工作线程都满了如何来拒绝请求执行的线程的策略,默认是abortpolicy策略,主要有四种类型。\n\n 1. abortpolicy 直接抛出regectedexcutionexception 异常阻止系统正常进行,默认策略\n\n 2. discardpolicy 直接丢弃任务,不予任何处理也不抛出异常,如果允许任务丢失,这是最好的一种方案\n\n 3. discardoldestpolicy 抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务\n\n 4. callerrunspolicy 交给线程池调用所在的线程进行处理,“调用者运行”的一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量\n\n以上内置拒绝策略均实现了 rejectexcutionhandler 接口,我们也可以实现接口rejectexcutionhandler来自定义策略,如记录日志或者持久化不能拒绝的任务。\n\n * keepalivetime: 非核心线程存活时间\n 线程池中非核心线程空闲的存活时间 当前线程池数量超过 corepoolsize 时,当空闲时间达到 keepalivetime 值时,非核心线程会被销毁直到只剩下 corepoolsize 个线程为止。\n \n\n> tips:如果任务很多并且任务执行时间比较短,可以调大 keepalivetime ,提高线程利用率。\n\n * unit: keepalivetime 的时间单位\n \n\n\n# 向线程池提交一个任务后,线程池是如何处理这个任务的呢?\n\n\n\n 1. 如果当前运行的线程数=corepoolsize,则将任务加入阻塞队列。\n 3. 如果队列已满或者不能加入,接下来的处理分两种情况:\n 1. 线程池中的线程是否都处于工作状态。创建新的线程来执行任务(这一步骤需要获取全局锁)。\n 2. 线程池中有空闲的线程,则用其来执行任务。\n 4. 如果创建新线程会导致线程数量>maximumpoolsize,则执行拒绝策略。\n\n我们再从源码的角度来理解下这个过程:\nthreadpoolexecutor#execute(runnble command) 方法\n\npublic void execute(runnable command) {\n // 参数校验\n if (command == null)\n throw new nullpointerexception();\n \n // 从 c 的值可以判断出线程池的状态, 以及线程池中线程的数量\n int c = ctl.get();\n // 1. 如果线程池的线程数量 小于 核心线程数\n if (workercountof(c) < corepoolsize) {\n // 添加一个核心线程 command 表示一个具体的任务, true 表示为核心线程\n if (addworker(command, true))\n return;\n c = ctl.get();\n }\n // 2. 如果线程池处于 running 状态(只有处于此状态,才能接受新的任务)\n // 并且线程池的数量大于核心线程数, 就把任务添加到阻塞队列\n if (isrunning(c) && workqueue.offer(command)) {\n int recheck = ctl.get();\n if (! isrunning(recheck) && remove(command))\n reject(command);\n else if (workercountof(recheck) == 0)\n addworker(null, false);\n }\n // 3. 如果队列也满了,就创建一个非核心线程(core==false)\n else if (!addworker(command, false))\n // 如果创建失败,就执行拒绝策略\n reject(command);\n}\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n\n\n\n# 线程池是如何工作的呢?\n\n线程池中有一个 workers 集合,里面记录了所有的工作线程,只有拿到 mainlock 锁的线程才能访问。 threadpoolexecutor#workers 属性\n\nprivate final hashset workers = new hashset();\n\n\n1\n\n\n线程池创建线程时,会将线程包装成 worker ,worker 在执行任务后还会循环获取队里里的任务。我们可以从 worker 类的 run() 方法看到这一点。\n\nfinal void runworker(worker w) {\n thread wt = thread.currentthread();\n runnable task = w.firsttask;\n w.firsttask = null;\n w.unlock(); // allow interrupts\n boolean completedabruptly = true;\n try {\n while (task != null || (task = gettask()) != null) {\n w.lock();\n if ((runstateatleast(ctl.get(), stop) ||\n (thread.interrupted() &&\n runstateatleast(ctl.get(), stop))) &&\n !wt.isinterrupted())\n wt.interrupt();\n try {\n beforeexecute(wt, task);\n throwable thrown = null;\n try {\n task.run();\n } catch (runtimeexception x) {\n thrown = x; throw x;\n } catch (error x) {\n thrown = x; throw x;\n } catch (throwable x) {\n thrown = x; throw new error(x);\n } finally {\n afterexecute(task, thrown);\n }\n } finally {\n task = null;\n w.completedtasks++;\n w.unlock();\n }\n }\n completedabruptly = false;\n } finally {\n processworkerexit(w, completedabruptly);\n }\n }\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n\n\n上面代码关键点是while循环和gettask()方法,通过循环不断的调用gettask()从阻塞队列中获取任务,通过这个方法,它与阻塞队列建立桥梁。\n\n\n# 向线程池提交任务\n\n有两个方法提交任务,分别是execute(),submit()\n\n\n * execute():execute只能提交runnable类型的任务,用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。如果遇到异常会直接抛出。使用方法如下:\n\npublic static void main(string[] args) throws exception{\n executorservice es = executors.newsinglethreadexecutor();\n runnable runnable = new runnable() {\n @override\n public void run() {\n system.out.println(\"runnable线程处理开始...\");\n }\n };\n es.execute(runnable);\n es.shutdown();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n * submit():submit既可以提交runnable类型的任务,也可以提交callable类型的任务,会有一个类型为future的返回值,但当任务类型为runnable时,返回值为null。如果遇到异常不会直接抛出,只有在使用future的get方法获取返回值时,才会抛出异常。使用方法:\n\npublic static void main(string[] args) throws exception{\n executorservice es = executors.newsinglethreadexecutor();\n callable callable = new callable() {\n @override\n public string call() throws exception {\n system.out.println(\"线程处理开始...\");\n return \"hello world\";\n }\n };\n future future = es.submit(callable);\n while(true) {\n //iddone:如果任务已完成,则返回 true。 可能由于正常终止、异常或取消而完成,在所有这些情况中,此方法都将返回 true。\n if(future.isdone()) {\n system.out.println(\"任务执行完成:\" + future.get());\n break;\n }\n }\n es.shutdown();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n\n\n\n# 关闭线程池\n\n可以通过shutdown和shutdownnow方法来关闭线程池。他们的原理是遍历线程池的中工作线程,调用其interrupt()方法来中断线程,所以,无法相应中断的任务永远无法被终止。但是他们也存在一定的区别:\n\n * shutdown:将线程的状态设置为 shutdown 状态,然后中断没有执行任务的线程。\n * shutdownnow:首先将线程的状态设置为 stop ,然后尝试停止所有正在执行或者暂停任务的线程,并返回队列中的待执行任务。\n\n只要调用了这两个方法中的任意一个,isshutdown()方法就会返回true,当所有任务都关闭后,才表示线程池关闭成功,这是调用isterminaed()方法才会返回 true 。\n\n\n通常调用 shutdown 来关闭线程池,如果不需要等任务执行完可以调用 shutdownnow 。\n\n\n# 合理的配置线程池\n\n 1. 最大线程数 maximumpoolsize 的是指可参考如下规则:\n * cpu 密集型任务应该配置尽可能小的线程,如cpu数量+1。\n * io密集型任务线程并不是一直占用cpu,则应该适度更多的配置线程,如2*cpu数量\n * 可以通过 runtime.getruntime().availableprocessors() 获得当前设备的 cpu 个数。\n 2. 建议使用有界队列。如果任务执行速度变慢,线程池不断向队列中 add 元素,会有内存溢出的风险。", "charsets": { "cjk": true }, @@ -1246,11 +1246,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "简述 Java 锁升级的机制", + "title": "什么是公平锁?什么是非公平锁?", "frontmatter": { - "title": "简述 Java 锁升级的机制", - "date": "2022-04-04T14:34:27.000Z", - "permalink": "/pages/ff0616/", + "title": "什么是公平锁?什么是非公平锁?", + "date": "2022-04-04T14:35:15.000Z", + "permalink": "/pages/fd6a59/", "categories": [ "Java相关", "并发" @@ -1259,50 +1259,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/070.%E7%AE%80%E8%BF%B0%20Java%20%E9%94%81%E5%8D%87%E7%BA%A7%E7%9A%84%E6%9C%BA%E5%88%B6.html", - "relativePath": "01.Java相关/20.并发/070.简述 Java 锁升级的机制.md", - "key": "v-1065f7be", - "path": "/pages/ff0616/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/090.%E4%BB%80%E4%B9%88%E6%98%AF%E5%85%AC%E5%B9%B3%E9%94%81%EF%BC%9F%E4%BB%80%E4%B9%88%E6%98%AF%E9%9D%9E%E5%85%AC%E5%B9%B3%E9%94%81%EF%BC%9F.html", + "relativePath": "01.Java相关/20.并发/090.什么是公平锁?什么是非公平锁?.md", + "key": "v-134f28dc", + "path": "/pages/fd6a59/", "headers": [ { - "level": 3, - "title": "偏向锁", - "slug": "偏向锁", - "normalizedTitle": "偏向锁", - "charIndex": 83 - }, - { - "level": 3, - "title": "轻量级锁", - "slug": "轻量级锁", - "normalizedTitle": "轻量级锁", - "charIndex": 94 - }, - { - "level": 3, - "title": "重量级锁", - "slug": "重量级锁", - "normalizedTitle": "重量级锁", - "charIndex": 18 - }, - { - "level": 3, - "title": "锁升级", - "slug": "锁升级", - "normalizedTitle": "锁升级", - "charIndex": 659 - }, - { - "level": 3, - "title": "锁的优缺点对比", - "slug": "锁的优缺点对比", - "normalizedTitle": "锁的优缺点对比", - "charIndex": 910 + "level": 2, + "title": "什么是公平锁?什么是非公平锁?", + "slug": "什么是公平锁-什么是非公平锁", + "normalizedTitle": "什么是公平锁?什么是非公平锁?", + "charIndex": 2 } ], - "headersStr": "偏向锁 轻量级锁 重量级锁 锁升级 锁的优缺点对比", - "content": "Synchronized 一直被称为重量级锁。但是在JDK 1.6之后它已经变得不那么重了。JDK 1.6 对Synchronized 的优化点在于:\n\n * 引入了偏向锁\n\n * 引入了轻量级锁\n\n在JDK 1.6 中,Synchronized 锁有四种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁、重量级锁。\n\n\n这几个状态会随着锁竞争升级,但是不可以降级。\n\n\n# 偏向锁\n\n为什么引入偏向锁?\n\n\n不存锁竞争,或者总是由同一线程多次获得锁的场景,偏向锁的代价更低。\n\n当一个线程访问同步块并获取到锁时,在锁对象头记录该线程的id,以后该线程进入和退出该同步块时不需要CAS来加锁和解锁。\n\n偏向锁何时释放?\n\n\n偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。\n\n\n\n偏向锁一定起到正面作用吗?\n\n\n不是的。\n\n\n偏向锁的适用场景是:不存锁竞争,或者总是由同一线程多次获得锁的场景。\n\n如果你确定你的程序中 锁通常处于竞争状态,可以通过JVM参数关闭偏向锁。关闭后,程序回魔人进入轻量级锁状态。\n\n\n-XX:UseBiasedLocking=false\n\n\n\n1\n2\n3\n\n\n\n# 轻量级锁\n\n轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋(关于自旋的介绍见文末)的形式尝试获取锁,线程不会阻塞,从而提高性能。\n\n轻量级锁的获取主要由两种情况:① 当关闭偏向锁功能时;② 由于多个线程竞争偏向锁导致偏向锁升级为轻量级锁。\n\n轻量级锁何时升级为重量级锁?\n\n\n若当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁(锁膨胀)。\n\n另外,当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁(锁膨胀)。\n\n\n# 重量级锁\n\n重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。\n\n重量级锁的获取是释放一般会有线程上下文切换,代价是比较大的,所以说是重量级锁。\n\n\n# 锁升级\n\n\n\n\n# 锁的优缺点对比\n\n", - "normalizedContent": "synchronized 一直被称为重量级锁。但是在jdk 1.6之后它已经变得不那么重了。jdk 1.6 对synchronized 的优化点在于:\n\n * 引入了偏向锁\n\n * 引入了轻量级锁\n\n在jdk 1.6 中,synchronized 锁有四种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁、重量级锁。\n\n\n这几个状态会随着锁竞争升级,但是不可以降级。\n\n\n# 偏向锁\n\n为什么引入偏向锁?\n\n\n不存锁竞争,或者总是由同一线程多次获得锁的场景,偏向锁的代价更低。\n\n当一个线程访问同步块并获取到锁时,在锁对象头记录该线程的id,以后该线程进入和退出该同步块时不需要cas来加锁和解锁。\n\n偏向锁何时释放?\n\n\n偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。\n\n\n\n偏向锁一定起到正面作用吗?\n\n\n不是的。\n\n\n偏向锁的适用场景是:不存锁竞争,或者总是由同一线程多次获得锁的场景。\n\n如果你确定你的程序中 锁通常处于竞争状态,可以通过jvm参数关闭偏向锁。关闭后,程序回魔人进入轻量级锁状态。\n\n\n-xx:usebiasedlocking=false\n\n\n\n1\n2\n3\n\n\n\n# 轻量级锁\n\n轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋(关于自旋的介绍见文末)的形式尝试获取锁,线程不会阻塞,从而提高性能。\n\n轻量级锁的获取主要由两种情况:① 当关闭偏向锁功能时;② 由于多个线程竞争偏向锁导致偏向锁升级为轻量级锁。\n\n轻量级锁何时升级为重量级锁?\n\n\n若当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁(锁膨胀)。\n\n另外,当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁(锁膨胀)。\n\n\n# 重量级锁\n\n重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。\n\n重量级锁的获取是释放一般会有线程上下文切换,代价是比较大的,所以说是重量级锁。\n\n\n# 锁升级\n\n\n\n\n# 锁的优缺点对比\n\n", + "headersStr": "什么是公平锁?什么是非公平锁?", + "content": "# 什么是公平锁?什么是非公平锁?\n\n其中sync是AbstractQueuedSynchronizer的实现,我们知道,ReentrantLock支持公平锁和非公平锁,其实现类分别是FairSync和NonfairSync,我们看看公平锁和非公平锁分别是怎么实现的\n\n// FairSync 公平锁的实现\nfinal void lock() {\n acquire(1);\n}\n\n// NonfairSync 非公平锁的实现\nfinal void lock() {\n if (compareAndSetState(0, 1))\n setExclusiveOwnerThread(Thread.currentThread());\n else\n acquire(1);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n可以看到,非公平锁的实现仅仅是多了一个步骤:通过CAS的方式(compareAndSetState)尝试改变state的状态,修改成功后设置当前线程以独占的方式获取了锁,修改失败执行的逻辑和公平锁一样。\n\n从这段代码中可以看到,独占锁加锁的核心逻辑就是acquire方法,接下来就看看这个方法\n\npublic final void acquire(int arg) {\n if (!tryAcquire(arg) &&\n acquireQueued(addWaiter(Node.EXCLUSIVE), arg))\n selfInterrupt();\n}\n\n\n1\n2\n3\n4\n5\n\n\n这个方法是 AQS 的核心代码。", + "normalizedContent": "# 什么是公平锁?什么是非公平锁?\n\n其中sync是abstractqueuedsynchronizer的实现,我们知道,reentrantlock支持公平锁和非公平锁,其实现类分别是fairsync和nonfairsync,我们看看公平锁和非公平锁分别是怎么实现的\n\n// fairsync 公平锁的实现\nfinal void lock() {\n acquire(1);\n}\n\n// nonfairsync 非公平锁的实现\nfinal void lock() {\n if (compareandsetstate(0, 1))\n setexclusiveownerthread(thread.currentthread());\n else\n acquire(1);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n可以看到,非公平锁的实现仅仅是多了一个步骤:通过cas的方式(compareandsetstate)尝试改变state的状态,修改成功后设置当前线程以独占的方式获取了锁,修改失败执行的逻辑和公平锁一样。\n\n从这段代码中可以看到,独占锁加锁的核心逻辑就是acquire方法,接下来就看看这个方法\n\npublic final void acquire(int arg) {\n if (!tryacquire(arg) &&\n acquirequeued(addwaiter(node.exclusive), arg))\n selfinterrupt();\n}\n\n\n1\n2\n3\n4\n5\n\n\n这个方法是 aqs 的核心代码。", "charsets": { "cjk": true }, @@ -1423,11 +1395,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "什么是公平锁?什么是非公平锁?", + "title": "简述 Java 锁升级的机制", "frontmatter": { - "title": "什么是公平锁?什么是非公平锁?", - "date": "2022-04-04T14:35:15.000Z", - "permalink": "/pages/fd6a59/", + "title": "简述 Java 锁升级的机制", + "date": "2022-04-04T14:34:27.000Z", + "permalink": "/pages/ff0616/", "categories": [ "Java相关", "并发" @@ -1436,38 +1408,66 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/090.%E4%BB%80%E4%B9%88%E6%98%AF%E5%85%AC%E5%B9%B3%E9%94%81%EF%BC%9F%E4%BB%80%E4%B9%88%E6%98%AF%E9%9D%9E%E5%85%AC%E5%B9%B3%E9%94%81%EF%BC%9F.html", - "relativePath": "01.Java相关/20.并发/090.什么是公平锁?什么是非公平锁?.md", - "key": "v-134f28dc", - "path": "/pages/fd6a59/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/070.%E7%AE%80%E8%BF%B0%20Java%20%E9%94%81%E5%8D%87%E7%BA%A7%E7%9A%84%E6%9C%BA%E5%88%B6.html", + "relativePath": "01.Java相关/20.并发/070.简述 Java 锁升级的机制.md", + "key": "v-1065f7be", + "path": "/pages/ff0616/", "headers": [ { - "level": 2, - "title": "什么是公平锁?什么是非公平锁?", - "slug": "什么是公平锁-什么是非公平锁", - "normalizedTitle": "什么是公平锁?什么是非公平锁?", - "charIndex": 2 - } - ], - "headersStr": "什么是公平锁?什么是非公平锁?", - "content": "# 什么是公平锁?什么是非公平锁?\n\n其中sync是AbstractQueuedSynchronizer的实现,我们知道,ReentrantLock支持公平锁和非公平锁,其实现类分别是FairSync和NonfairSync,我们看看公平锁和非公平锁分别是怎么实现的\n\n// FairSync 公平锁的实现\nfinal void lock() {\n acquire(1);\n}\n\n// NonfairSync 非公平锁的实现\nfinal void lock() {\n if (compareAndSetState(0, 1))\n setExclusiveOwnerThread(Thread.currentThread());\n else\n acquire(1);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n可以看到,非公平锁的实现仅仅是多了一个步骤:通过CAS的方式(compareAndSetState)尝试改变state的状态,修改成功后设置当前线程以独占的方式获取了锁,修改失败执行的逻辑和公平锁一样。\n\n从这段代码中可以看到,独占锁加锁的核心逻辑就是acquire方法,接下来就看看这个方法\n\npublic final void acquire(int arg) {\n if (!tryAcquire(arg) &&\n acquireQueued(addWaiter(Node.EXCLUSIVE), arg))\n selfInterrupt();\n}\n\n\n1\n2\n3\n4\n5\n\n\n这个方法是 AQS 的核心代码。", - "normalizedContent": "# 什么是公平锁?什么是非公平锁?\n\n其中sync是abstractqueuedsynchronizer的实现,我们知道,reentrantlock支持公平锁和非公平锁,其实现类分别是fairsync和nonfairsync,我们看看公平锁和非公平锁分别是怎么实现的\n\n// fairsync 公平锁的实现\nfinal void lock() {\n acquire(1);\n}\n\n// nonfairsync 非公平锁的实现\nfinal void lock() {\n if (compareandsetstate(0, 1))\n setexclusiveownerthread(thread.currentthread());\n else\n acquire(1);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n可以看到,非公平锁的实现仅仅是多了一个步骤:通过cas的方式(compareandsetstate)尝试改变state的状态,修改成功后设置当前线程以独占的方式获取了锁,修改失败执行的逻辑和公平锁一样。\n\n从这段代码中可以看到,独占锁加锁的核心逻辑就是acquire方法,接下来就看看这个方法\n\npublic final void acquire(int arg) {\n if (!tryacquire(arg) &&\n acquirequeued(addwaiter(node.exclusive), arg))\n selfinterrupt();\n}\n\n\n1\n2\n3\n4\n5\n\n\n这个方法是 aqs 的核心代码。", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, - { - "title": "Java 的线程有哪些状态,转换关系是怎么样的?", - "frontmatter": { - "title": "Java 的线程有哪些状态,转换关系是怎么样的?", - "date": "2022-04-04T14:35:37.000Z", - "permalink": "/pages/52483b/", - "categories": [ - "Java相关", - "并发" - ], + "level": 3, + "title": "偏向锁", + "slug": "偏向锁", + "normalizedTitle": "偏向锁", + "charIndex": 83 + }, + { + "level": 3, + "title": "轻量级锁", + "slug": "轻量级锁", + "normalizedTitle": "轻量级锁", + "charIndex": 94 + }, + { + "level": 3, + "title": "重量级锁", + "slug": "重量级锁", + "normalizedTitle": "重量级锁", + "charIndex": 18 + }, + { + "level": 3, + "title": "锁升级", + "slug": "锁升级", + "normalizedTitle": "锁升级", + "charIndex": 659 + }, + { + "level": 3, + "title": "锁的优缺点对比", + "slug": "锁的优缺点对比", + "normalizedTitle": "锁的优缺点对比", + "charIndex": 910 + } + ], + "headersStr": "偏向锁 轻量级锁 重量级锁 锁升级 锁的优缺点对比", + "content": "Synchronized 一直被称为重量级锁。但是在JDK 1.6之后它已经变得不那么重了。JDK 1.6 对Synchronized 的优化点在于:\n\n * 引入了偏向锁\n\n * 引入了轻量级锁\n\n在JDK 1.6 中,Synchronized 锁有四种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁、重量级锁。\n\n\n这几个状态会随着锁竞争升级,但是不可以降级。\n\n\n# 偏向锁\n\n为什么引入偏向锁?\n\n\n不存锁竞争,或者总是由同一线程多次获得锁的场景,偏向锁的代价更低。\n\n当一个线程访问同步块并获取到锁时,在锁对象头记录该线程的id,以后该线程进入和退出该同步块时不需要CAS来加锁和解锁。\n\n偏向锁何时释放?\n\n\n偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。\n\n\n\n偏向锁一定起到正面作用吗?\n\n\n不是的。\n\n\n偏向锁的适用场景是:不存锁竞争,或者总是由同一线程多次获得锁的场景。\n\n如果你确定你的程序中 锁通常处于竞争状态,可以通过JVM参数关闭偏向锁。关闭后,程序回魔人进入轻量级锁状态。\n\n\n-XX:UseBiasedLocking=false\n\n\n\n1\n2\n3\n\n\n\n# 轻量级锁\n\n轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋(关于自旋的介绍见文末)的形式尝试获取锁,线程不会阻塞,从而提高性能。\n\n轻量级锁的获取主要由两种情况:① 当关闭偏向锁功能时;② 由于多个线程竞争偏向锁导致偏向锁升级为轻量级锁。\n\n轻量级锁何时升级为重量级锁?\n\n\n若当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁(锁膨胀)。\n\n另外,当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁(锁膨胀)。\n\n\n# 重量级锁\n\n重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。\n\n重量级锁的获取是释放一般会有线程上下文切换,代价是比较大的,所以说是重量级锁。\n\n\n# 锁升级\n\n\n\n\n# 锁的优缺点对比\n\n", + "normalizedContent": "synchronized 一直被称为重量级锁。但是在jdk 1.6之后它已经变得不那么重了。jdk 1.6 对synchronized 的优化点在于:\n\n * 引入了偏向锁\n\n * 引入了轻量级锁\n\n在jdk 1.6 中,synchronized 锁有四种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁、重量级锁。\n\n\n这几个状态会随着锁竞争升级,但是不可以降级。\n\n\n# 偏向锁\n\n为什么引入偏向锁?\n\n\n不存锁竞争,或者总是由同一线程多次获得锁的场景,偏向锁的代价更低。\n\n当一个线程访问同步块并获取到锁时,在锁对象头记录该线程的id,以后该线程进入和退出该同步块时不需要cas来加锁和解锁。\n\n偏向锁何时释放?\n\n\n偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。\n\n\n\n偏向锁一定起到正面作用吗?\n\n\n不是的。\n\n\n偏向锁的适用场景是:不存锁竞争,或者总是由同一线程多次获得锁的场景。\n\n如果你确定你的程序中 锁通常处于竞争状态,可以通过jvm参数关闭偏向锁。关闭后,程序回魔人进入轻量级锁状态。\n\n\n-xx:usebiasedlocking=false\n\n\n\n1\n2\n3\n\n\n\n# 轻量级锁\n\n轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋(关于自旋的介绍见文末)的形式尝试获取锁,线程不会阻塞,从而提高性能。\n\n轻量级锁的获取主要由两种情况:① 当关闭偏向锁功能时;② 由于多个线程竞争偏向锁导致偏向锁升级为轻量级锁。\n\n轻量级锁何时升级为重量级锁?\n\n\n若当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁(锁膨胀)。\n\n另外,当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁(锁膨胀)。\n\n\n# 重量级锁\n\n重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。\n\n重量级锁的获取是释放一般会有线程上下文切换,代价是比较大的,所以说是重量级锁。\n\n\n# 锁升级\n\n\n\n\n# 锁的优缺点对比\n\n", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, + { + "title": "Java 的线程有哪些状态,转换关系是怎么样的?", + "frontmatter": { + "title": "Java 的线程有哪些状态,转换关系是怎么样的?", + "date": "2022-04-04T14:35:37.000Z", + "permalink": "/pages/52483b/", + "categories": [ + "Java相关", + "并发" + ], "tags": [ null ] @@ -1572,6 +1572,49 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, + { + "title": "手写死锁", + "frontmatter": { + "title": "手写死锁", + "date": "2022-04-09T13:51:43.000Z", + "permalink": "/pages/ce292c/", + "categories": [ + "Java相关", + "并发" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/115.%E6%89%8B%E5%86%99%E6%AD%BB%E9%94%81.html", + "relativePath": "01.Java相关/20.并发/115.手写死锁.md", + "key": "v-7b2a2ed4", + "path": "/pages/ce292c/", + "headers": [ + { + "level": 2, + "title": "产生死锁的条件", + "slug": "产生死锁的条件", + "normalizedTitle": "产生死锁的条件", + "charIndex": 2 + }, + { + "level": 2, + "title": "手写死锁例子", + "slug": "手写死锁例子", + "normalizedTitle": "手写死锁例子", + "charIndex": 170 + } + ], + "headersStr": "产生死锁的条件 手写死锁例子", + "content": "# 产生死锁的条件\n\n一般来说,要出现死锁问题需要满足以下条件:\n\n * 互斥条件:一个资源每次只能被一个线程使用。\n * 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。\n * 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。\n * 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。\n\n\n# 手写死锁例子\n\n//可能发生静态锁顺序死锁的代码\nclass StaticLockOrderDeadLock {\n private final Object lockA = new Object();\n private final Object lockB = new Object();\n public void a() {\n synchronized (lockA) {\n synchronized (lockB) {\n System.out.println(\"function a\");\n }\n }\n }\n \n public void b() {\n synchronized (lockB) {\n synchronized (lockA) {\n System.out.println(\"function b\");\n }\n }\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n * a和b两个方法都需要获得A锁和B锁。一个线程执行a方法且已经获得了A锁,在等待B锁;\n * 另一个线程执行了b方法且已经获得了B锁,在等待A锁。\n\n这种状态,就是发生了静态的锁顺序死锁。\n解决办法: 所有需要多个锁的线程,都要以相同的顺序来获得锁。\n\n\n//正确的代码\nclass StaticLockOrderDeadLock {\n private final Object lockA = new Object();\n private final Object lockB = new Object();\n public void a() {\n synchronized (lockA) {\n synchronized (lockB) {\n System.out.println(\"function a\");\n }\n }\n }\n \n public void b() {\n synchronized (lockA) {\n synchronized (lockB) {\n System.out.println(\"function b\");\n }\n }\n }\n}\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n", + "normalizedContent": "# 产生死锁的条件\n\n一般来说,要出现死锁问题需要满足以下条件:\n\n * 互斥条件:一个资源每次只能被一个线程使用。\n * 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。\n * 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。\n * 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。\n\n\n# 手写死锁例子\n\n//可能发生静态锁顺序死锁的代码\nclass staticlockorderdeadlock {\n private final object locka = new object();\n private final object lockb = new object();\n public void a() {\n synchronized (locka) {\n synchronized (lockb) {\n system.out.println(\"function a\");\n }\n }\n }\n \n public void b() {\n synchronized (lockb) {\n synchronized (locka) {\n system.out.println(\"function b\");\n }\n }\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n * a和b两个方法都需要获得a锁和b锁。一个线程执行a方法且已经获得了a锁,在等待b锁;\n * 另一个线程执行了b方法且已经获得了b锁,在等待a锁。\n\n这种状态,就是发生了静态的锁顺序死锁。\n解决办法: 所有需要多个锁的线程,都要以相同的顺序来获得锁。\n\n\n//正确的代码\nclass staticlockorderdeadlock {\n private final object locka = new object();\n private final object lockb = new object();\n public void a() {\n synchronized (locka) {\n synchronized (lockb) {\n system.out.println(\"function a\");\n }\n }\n }\n \n public void b() {\n synchronized (locka) {\n synchronized (lockb) {\n system.out.println(\"function b\");\n }\n }\n }\n}\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, { "title": "为什么我们不能直接调用 run() 方法?", "frontmatter": { @@ -1609,11 +1652,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "手写死锁", + "title": "Java 线程有哪些常用方法?", "frontmatter": { - "title": "手写死锁", - "date": "2022-04-09T13:51:43.000Z", - "permalink": "/pages/ce292c/", + "title": "Java 线程有哪些常用方法?", + "date": "2022-04-04T14:36:48.000Z", + "permalink": "/pages/a6c84e/", "categories": [ "Java相关", "并发" @@ -1622,29 +1665,57 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/115.%E6%89%8B%E5%86%99%E6%AD%BB%E9%94%81.html", - "relativePath": "01.Java相关/20.并发/115.手写死锁.md", - "key": "v-7b2a2ed4", - "path": "/pages/ce292c/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/130.Java%20%E7%BA%BF%E7%A8%8B%E6%9C%89%E5%93%AA%E4%BA%9B%E5%B8%B8%E7%94%A8%E6%96%B9%E6%B3%95%EF%BC%9F.html", + "relativePath": "01.Java相关/20.并发/130.Java 线程有哪些常用方法?.md", + "key": "v-75c00225", + "path": "/pages/a6c84e/", "headers": [ { "level": 2, - "title": "产生死锁的条件", - "slug": "产生死锁的条件", - "normalizedTitle": "产生死锁的条件", + "title": "Java 线程如何启动?如何终止?", + "slug": "java-线程如何启动-如何终止", + "normalizedTitle": "java 线程如何启动?如何终止?", "charIndex": 2 }, { "level": 2, - "title": "手写死锁例子", - "slug": "手写死锁例子", - "normalizedTitle": "手写死锁例子", - "charIndex": 170 + "title": "构造线程", + "slug": "构造线程", + "normalizedTitle": "构造线程", + "charIndex": 97 + }, + { + "level": 2, + "title": "启动线程", + "slug": "启动线程", + "normalizedTitle": "启动线程", + "charIndex": 637 + }, + { + "level": 2, + "title": "理解中断", + "slug": "理解中断", + "normalizedTitle": "理解中断", + "charIndex": 1259 + }, + { + "level": 2, + "title": "已不建议使用的方法:suspend(),resume(),stop()", + "slug": "已不建议使用的方法-suspend-resume-stop", + "normalizedTitle": "已不建议使用的方法:suspend(),resume(),stop()", + "charIndex": 1581 + }, + { + "level": 2, + "title": "安全的终止线程", + "slug": "安全的终止线程", + "normalizedTitle": "安全的终止线程", + "charIndex": 1813 } ], - "headersStr": "产生死锁的条件 手写死锁例子", - "content": "# 产生死锁的条件\n\n一般来说,要出现死锁问题需要满足以下条件:\n\n * 互斥条件:一个资源每次只能被一个线程使用。\n * 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。\n * 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。\n * 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。\n\n\n# 手写死锁例子\n\n//可能发生静态锁顺序死锁的代码\nclass StaticLockOrderDeadLock {\n private final Object lockA = new Object();\n private final Object lockB = new Object();\n public void a() {\n synchronized (lockA) {\n synchronized (lockB) {\n System.out.println(\"function a\");\n }\n }\n }\n \n public void b() {\n synchronized (lockB) {\n synchronized (lockA) {\n System.out.println(\"function b\");\n }\n }\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n * a和b两个方法都需要获得A锁和B锁。一个线程执行a方法且已经获得了A锁,在等待B锁;\n * 另一个线程执行了b方法且已经获得了B锁,在等待A锁。\n\n这种状态,就是发生了静态的锁顺序死锁。\n解决办法: 所有需要多个锁的线程,都要以相同的顺序来获得锁。\n\n\n//正确的代码\nclass StaticLockOrderDeadLock {\n private final Object lockA = new Object();\n private final Object lockB = new Object();\n public void a() {\n synchronized (lockA) {\n synchronized (lockB) {\n System.out.println(\"function a\");\n }\n }\n }\n \n public void b() {\n synchronized (lockA) {\n synchronized (lockB) {\n System.out.println(\"function b\");\n }\n }\n }\n}\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n", - "normalizedContent": "# 产生死锁的条件\n\n一般来说,要出现死锁问题需要满足以下条件:\n\n * 互斥条件:一个资源每次只能被一个线程使用。\n * 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。\n * 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。\n * 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。\n\n\n# 手写死锁例子\n\n//可能发生静态锁顺序死锁的代码\nclass staticlockorderdeadlock {\n private final object locka = new object();\n private final object lockb = new object();\n public void a() {\n synchronized (locka) {\n synchronized (lockb) {\n system.out.println(\"function a\");\n }\n }\n }\n \n public void b() {\n synchronized (lockb) {\n synchronized (locka) {\n system.out.println(\"function b\");\n }\n }\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n * a和b两个方法都需要获得a锁和b锁。一个线程执行a方法且已经获得了a锁,在等待b锁;\n * 另一个线程执行了b方法且已经获得了b锁,在等待a锁。\n\n这种状态,就是发生了静态的锁顺序死锁。\n解决办法: 所有需要多个锁的线程,都要以相同的顺序来获得锁。\n\n\n//正确的代码\nclass staticlockorderdeadlock {\n private final object locka = new object();\n private final object lockb = new object();\n public void a() {\n synchronized (locka) {\n synchronized (lockb) {\n system.out.println(\"function a\");\n }\n }\n }\n \n public void b() {\n synchronized (locka) {\n synchronized (lockb) {\n system.out.println(\"function b\");\n }\n }\n }\n}\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n", + "headersStr": "Java 线程如何启动?如何终止? 构造线程 启动线程 理解中断 已不建议使用的方法:suspend(),resume(),stop() 安全的终止线程", + "content": "# Java 线程如何启动?如何终止?\n\n线程随着start()方法的调用而启动,随着run()方法调用结束而终止。 但是这其中还会涉及到线程的构造、中断、暂停、恢复,我们来一一看下。\n\n\n# 构造线程\n\n在运行一个线程之前 肯定要先构造一个线程对象,构造线程对象需要提供所需属性:如线程所属组,线程优先级,是否deamon等。\n\nprivate void init(ThreadGroup g, Runnable target, String name,\n long stackSize, AccessControlContext acc) {\n //1.设置线程名\n if (name == null) {\n throw new NullPointerException(\"name cannot be null\");\n }\n\n this.name = name;\n //2.设置线程的线程组,如果未指定,则此线程的线程组为当前初始化线程的线程组\n Thread parent = currentThread();\n ...\n if (g == null) {\n //这里有一段注释说,安卓不支持SecurityManager,所以删除了相关代码\n g = parent.getThreadGroup();\n }\n\n //所属线程组未启动线程计数+1\n g.addUnstarted();\n\n this.group = g;\n //3.设置线程的其他属性\n //线程是否是守护线程以及优先级都继承自当前线程\n this.daemon = parent.isDaemon();\n this.priority = parent.getPriority();\n this.target = target;\n //4.调用init2 执行剩余初始化操作\n init2(parent);\n //5.设置线程的堆栈大小。但是否生效要看虚拟机。那么在art虚拟机上是否生效呢,我们后续再说\n /* Stash the specified stack size in case the VM cares */\n this.stackSize = stackSize;\n\n //6.设置当前线程id\n tid = nextThreadID();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n\n\n到这里为止,我们的thread就已经初始化完毕了。在堆内存中等待这被执行start()方法开始运行。\n\n\n# 启动线程\n\n调用start()方法开始启动线程。\n\n\n# 理解中断\n\n如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。\n\n中断可以理解为线程的一个标志位属性,调用interrupt()方法中断线程仅仅是把中断标志位置为true,线程可以通过检查自身的标志位来判断是否被中断了。\n\n另外还有两点需要注意的:\n\n * 如果线程处于终结状态,即使线程被中断过,其中断标识位依然是false\n * 许多声明抛出InterruptException的方法(如Thread.sleep(long millis)),在抛出InterruptException之前会将中断标识位置为false。\n\n\n# 已不建议使用的方法:suspend(),resume(),stop()\n\nsuspend(),resume(),stop()完成了线程的暂停、恢复、停止。但是这些方法过期了 不建议继续使用,原因是:\n\n 1. suspend()暂停线程是不会释放资源(比如锁),而是占着资源入睡 ,这样容易引发死锁\n 2. stop() 终止线程时没有给线程清理资源的机会,导致程序运行在不确定状态\n\n取而代之的是wait()和notify()/notifyAll()。\n\n\n# 安全的终止线程\n\n可以通过interrupt()或者标识位来优雅的停止线程。\n\npublic class MyRunner implements Runnable {\n private volatile boolean on = true;\n public void run() {\n while(on){\n System.out.println(\"Running...\");\n }\n System.out.println(\"Stoped!\");\n }\n\n public void cancel(){\n on = false;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n通过标识位来停止线程\n\npublic class ShutdownThread {\n\n public static void main(String[] args) {\n Thread t1 = new Thread(new MyRunner().start();\n t1.cancel();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n通过interrupt()来停止线程\n\npublic class ShutdownThread {\n\n public static void main(String[] args) {\n Thread t1 = new Thread(new MyRunner().start();\n t1.interrupt();\n }\n}\n\n1\n2\n3\n4\n5\n6\n", + "normalizedContent": "# java 线程如何启动?如何终止?\n\n线程随着start()方法的调用而启动,随着run()方法调用结束而终止。 但是这其中还会涉及到线程的构造、中断、暂停、恢复,我们来一一看下。\n\n\n# 构造线程\n\n在运行一个线程之前 肯定要先构造一个线程对象,构造线程对象需要提供所需属性:如线程所属组,线程优先级,是否deamon等。\n\nprivate void init(threadgroup g, runnable target, string name,\n long stacksize, accesscontrolcontext acc) {\n //1.设置线程名\n if (name == null) {\n throw new nullpointerexception(\"name cannot be null\");\n }\n\n this.name = name;\n //2.设置线程的线程组,如果未指定,则此线程的线程组为当前初始化线程的线程组\n thread parent = currentthread();\n ...\n if (g == null) {\n //这里有一段注释说,安卓不支持securitymanager,所以删除了相关代码\n g = parent.getthreadgroup();\n }\n\n //所属线程组未启动线程计数+1\n g.addunstarted();\n\n this.group = g;\n //3.设置线程的其他属性\n //线程是否是守护线程以及优先级都继承自当前线程\n this.daemon = parent.isdaemon();\n this.priority = parent.getpriority();\n this.target = target;\n //4.调用init2 执行剩余初始化操作\n init2(parent);\n //5.设置线程的堆栈大小。但是否生效要看虚拟机。那么在art虚拟机上是否生效呢,我们后续再说\n /* stash the specified stack size in case the vm cares */\n this.stacksize = stacksize;\n\n //6.设置当前线程id\n tid = nextthreadid();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n\n\n到这里为止,我们的thread就已经初始化完毕了。在堆内存中等待这被执行start()方法开始运行。\n\n\n# 启动线程\n\n调用start()方法开始启动线程。\n\n\n# 理解中断\n\n如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。\n\n中断可以理解为线程的一个标志位属性,调用interrupt()方法中断线程仅仅是把中断标志位置为true,线程可以通过检查自身的标志位来判断是否被中断了。\n\n另外还有两点需要注意的:\n\n * 如果线程处于终结状态,即使线程被中断过,其中断标识位依然是false\n * 许多声明抛出interruptexception的方法(如thread.sleep(long millis)),在抛出interruptexception之前会将中断标识位置为false。\n\n\n# 已不建议使用的方法:suspend(),resume(),stop()\n\nsuspend(),resume(),stop()完成了线程的暂停、恢复、停止。但是这些方法过期了 不建议继续使用,原因是:\n\n 1. suspend()暂停线程是不会释放资源(比如锁),而是占着资源入睡 ,这样容易引发死锁\n 2. stop() 终止线程时没有给线程清理资源的机会,导致程序运行在不确定状态\n\n取而代之的是wait()和notify()/notifyall()。\n\n\n# 安全的终止线程\n\n可以通过interrupt()或者标识位来优雅的停止线程。\n\npublic class myrunner implements runnable {\n private volatile boolean on = true;\n public void run() {\n while(on){\n system.out.println(\"running...\");\n }\n system.out.println(\"stoped!\");\n }\n\n public void cancel(){\n on = false;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n通过标识位来停止线程\n\npublic class shutdownthread {\n\n public static void main(string[] args) {\n thread t1 = new thread(new myrunner().start();\n t1.cancel();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n通过interrupt()来停止线程\n\npublic class shutdownthread {\n\n public static void main(string[] args) {\n thread t1 = new thread(new myrunner().start();\n t1.interrupt();\n }\n}\n\n1\n2\n3\n4\n5\n6\n", "charsets": { "cjk": true }, @@ -1778,77 +1849,6 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, - { - "title": "Java 线程有哪些常用方法?", - "frontmatter": { - "title": "Java 线程有哪些常用方法?", - "date": "2022-04-04T14:36:48.000Z", - "permalink": "/pages/a6c84e/", - "categories": [ - "Java相关", - "并发" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/130.Java%20%E7%BA%BF%E7%A8%8B%E6%9C%89%E5%93%AA%E4%BA%9B%E5%B8%B8%E7%94%A8%E6%96%B9%E6%B3%95%EF%BC%9F.html", - "relativePath": "01.Java相关/20.并发/130.Java 线程有哪些常用方法?.md", - "key": "v-75c00225", - "path": "/pages/a6c84e/", - "headers": [ - { - "level": 2, - "title": "Java 线程如何启动?如何终止?", - "slug": "java-线程如何启动-如何终止", - "normalizedTitle": "java 线程如何启动?如何终止?", - "charIndex": 2 - }, - { - "level": 2, - "title": "构造线程", - "slug": "构造线程", - "normalizedTitle": "构造线程", - "charIndex": 97 - }, - { - "level": 2, - "title": "启动线程", - "slug": "启动线程", - "normalizedTitle": "启动线程", - "charIndex": 637 - }, - { - "level": 2, - "title": "理解中断", - "slug": "理解中断", - "normalizedTitle": "理解中断", - "charIndex": 1259 - }, - { - "level": 2, - "title": "已不建议使用的方法:suspend(),resume(),stop()", - "slug": "已不建议使用的方法-suspend-resume-stop", - "normalizedTitle": "已不建议使用的方法:suspend(),resume(),stop()", - "charIndex": 1581 - }, - { - "level": 2, - "title": "安全的终止线程", - "slug": "安全的终止线程", - "normalizedTitle": "安全的终止线程", - "charIndex": 1813 - } - ], - "headersStr": "Java 线程如何启动?如何终止? 构造线程 启动线程 理解中断 已不建议使用的方法:suspend(),resume(),stop() 安全的终止线程", - "content": "# Java 线程如何启动?如何终止?\n\n线程随着start()方法的调用而启动,随着run()方法调用结束而终止。 但是这其中还会涉及到线程的构造、中断、暂停、恢复,我们来一一看下。\n\n\n# 构造线程\n\n在运行一个线程之前 肯定要先构造一个线程对象,构造线程对象需要提供所需属性:如线程所属组,线程优先级,是否deamon等。\n\nprivate void init(ThreadGroup g, Runnable target, String name,\n long stackSize, AccessControlContext acc) {\n //1.设置线程名\n if (name == null) {\n throw new NullPointerException(\"name cannot be null\");\n }\n\n this.name = name;\n //2.设置线程的线程组,如果未指定,则此线程的线程组为当前初始化线程的线程组\n Thread parent = currentThread();\n ...\n if (g == null) {\n //这里有一段注释说,安卓不支持SecurityManager,所以删除了相关代码\n g = parent.getThreadGroup();\n }\n\n //所属线程组未启动线程计数+1\n g.addUnstarted();\n\n this.group = g;\n //3.设置线程的其他属性\n //线程是否是守护线程以及优先级都继承自当前线程\n this.daemon = parent.isDaemon();\n this.priority = parent.getPriority();\n this.target = target;\n //4.调用init2 执行剩余初始化操作\n init2(parent);\n //5.设置线程的堆栈大小。但是否生效要看虚拟机。那么在art虚拟机上是否生效呢,我们后续再说\n /* Stash the specified stack size in case the VM cares */\n this.stackSize = stackSize;\n\n //6.设置当前线程id\n tid = nextThreadID();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n\n\n到这里为止,我们的thread就已经初始化完毕了。在堆内存中等待这被执行start()方法开始运行。\n\n\n# 启动线程\n\n调用start()方法开始启动线程。\n\n\n# 理解中断\n\n如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。\n\n中断可以理解为线程的一个标志位属性,调用interrupt()方法中断线程仅仅是把中断标志位置为true,线程可以通过检查自身的标志位来判断是否被中断了。\n\n另外还有两点需要注意的:\n\n * 如果线程处于终结状态,即使线程被中断过,其中断标识位依然是false\n * 许多声明抛出InterruptException的方法(如Thread.sleep(long millis)),在抛出InterruptException之前会将中断标识位置为false。\n\n\n# 已不建议使用的方法:suspend(),resume(),stop()\n\nsuspend(),resume(),stop()完成了线程的暂停、恢复、停止。但是这些方法过期了 不建议继续使用,原因是:\n\n 1. suspend()暂停线程是不会释放资源(比如锁),而是占着资源入睡 ,这样容易引发死锁\n 2. stop() 终止线程时没有给线程清理资源的机会,导致程序运行在不确定状态\n\n取而代之的是wait()和notify()/notifyAll()。\n\n\n# 安全的终止线程\n\n可以通过interrupt()或者标识位来优雅的停止线程。\n\npublic class MyRunner implements Runnable {\n private volatile boolean on = true;\n public void run() {\n while(on){\n System.out.println(\"Running...\");\n }\n System.out.println(\"Stoped!\");\n }\n\n public void cancel(){\n on = false;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n通过标识位来停止线程\n\npublic class ShutdownThread {\n\n public static void main(String[] args) {\n Thread t1 = new Thread(new MyRunner().start();\n t1.cancel();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n通过interrupt()来停止线程\n\npublic class ShutdownThread {\n\n public static void main(String[] args) {\n Thread t1 = new Thread(new MyRunner().start();\n t1.interrupt();\n }\n}\n\n1\n2\n3\n4\n5\n6\n", - "normalizedContent": "# java 线程如何启动?如何终止?\n\n线程随着start()方法的调用而启动,随着run()方法调用结束而终止。 但是这其中还会涉及到线程的构造、中断、暂停、恢复,我们来一一看下。\n\n\n# 构造线程\n\n在运行一个线程之前 肯定要先构造一个线程对象,构造线程对象需要提供所需属性:如线程所属组,线程优先级,是否deamon等。\n\nprivate void init(threadgroup g, runnable target, string name,\n long stacksize, accesscontrolcontext acc) {\n //1.设置线程名\n if (name == null) {\n throw new nullpointerexception(\"name cannot be null\");\n }\n\n this.name = name;\n //2.设置线程的线程组,如果未指定,则此线程的线程组为当前初始化线程的线程组\n thread parent = currentthread();\n ...\n if (g == null) {\n //这里有一段注释说,安卓不支持securitymanager,所以删除了相关代码\n g = parent.getthreadgroup();\n }\n\n //所属线程组未启动线程计数+1\n g.addunstarted();\n\n this.group = g;\n //3.设置线程的其他属性\n //线程是否是守护线程以及优先级都继承自当前线程\n this.daemon = parent.isdaemon();\n this.priority = parent.getpriority();\n this.target = target;\n //4.调用init2 执行剩余初始化操作\n init2(parent);\n //5.设置线程的堆栈大小。但是否生效要看虚拟机。那么在art虚拟机上是否生效呢,我们后续再说\n /* stash the specified stack size in case the vm cares */\n this.stacksize = stacksize;\n\n //6.设置当前线程id\n tid = nextthreadid();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n\n\n到这里为止,我们的thread就已经初始化完毕了。在堆内存中等待这被执行start()方法开始运行。\n\n\n# 启动线程\n\n调用start()方法开始启动线程。\n\n\n# 理解中断\n\n如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。\n\n中断可以理解为线程的一个标志位属性,调用interrupt()方法中断线程仅仅是把中断标志位置为true,线程可以通过检查自身的标志位来判断是否被中断了。\n\n另外还有两点需要注意的:\n\n * 如果线程处于终结状态,即使线程被中断过,其中断标识位依然是false\n * 许多声明抛出interruptexception的方法(如thread.sleep(long millis)),在抛出interruptexception之前会将中断标识位置为false。\n\n\n# 已不建议使用的方法:suspend(),resume(),stop()\n\nsuspend(),resume(),stop()完成了线程的暂停、恢复、停止。但是这些方法过期了 不建议继续使用,原因是:\n\n 1. suspend()暂停线程是不会释放资源(比如锁),而是占着资源入睡 ,这样容易引发死锁\n 2. stop() 终止线程时没有给线程清理资源的机会,导致程序运行在不确定状态\n\n取而代之的是wait()和notify()/notifyall()。\n\n\n# 安全的终止线程\n\n可以通过interrupt()或者标识位来优雅的停止线程。\n\npublic class myrunner implements runnable {\n private volatile boolean on = true;\n public void run() {\n while(on){\n system.out.println(\"running...\");\n }\n system.out.println(\"stoped!\");\n }\n\n public void cancel(){\n on = false;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n通过标识位来停止线程\n\npublic class shutdownthread {\n\n public static void main(string[] args) {\n thread t1 = new thread(new myrunner().start();\n t1.cancel();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n通过interrupt()来停止线程\n\npublic class shutdownthread {\n\n public static void main(string[] args) {\n thread t1 = new thread(new myrunner().start();\n t1.interrupt();\n }\n}\n\n1\n2\n3\n4\n5\n6\n", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, { "title": "ThreadLocal 实现原理是什么?为什么要使用弱引用?", "frontmatter": { @@ -2008,7 +2008,9 @@ export const siteData = { "normalizedContent": "java 中垃圾回收机制中如何判断对象需要回收? 即死亡对象判断方法 堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。\n\n\n# 引用计数法\n\n给对象中添加一个引用计数器:\n\n每当有一个地方引用它,计数器就加 1; 当引用失效,计数器就减 1; 任何时候计数器为 0 的对象就是不可能再被使用的。 这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。\n\n所谓对象之间的相互引用问题,如下面代码所示:除了对象 obja 和 objb 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 gc 回收器回收他们。\n\npublic class referencecountinggc {\n object instance = null;\n public static void main(string[] args) {\n referencecountinggc obja = new referencecountinggc();\n referencecountinggc objb = new referencecountinggc();\n obja.instance = objb;\n objb.instance = obja;\n obja = null;\n objb = null;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n\n# 可达性分析算法\n\n这个算法的基本思想就是通过一系列的称为 “gc roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 gc roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。\n\n下图中的 object 6 ~ object 10 之间虽有引用关系,但它们到 gc roots 不可达,因此为需要被回收的对象。\n\n哪些对象可以作为 gc roots 呢?\n\n虚拟机栈(栈帧中的本地变量表)中引用的对象 本地方法栈(native 方法)中引用的对象 方法区中类静态属性引用的对象 方法区中常量引用的对象 所有被同步锁持有的对象 对象可以被回收,就代表一定会被回收吗?\n\n即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 'finalize' 方法。当对象没有覆盖 'finalize' 方法,或 'finalize' 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。\n\n被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。\n\n> object 类中的 finalize 方法一直被认为是一个糟糕的设计,成为了 java 语言的负担,影响了 java 语言的安全和 gc 的性能。jdk9 版本及后续版本中各个类中的 finalize 方法会被逐渐弃用移除。忘掉它的存在吧!\n\n\n# 引用类型总结\n\n无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。\n\njdk1.2 之前,java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。\n\njdk1.2 以后,java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)\n\n1.强引用(strongreference)\n\n以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,java 虚拟机宁愿抛出 outofmemoryerror 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。\n\n2.软引用(softreference)\n\n如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。\n\n软引用可以和一个引用队列(referencequeue)联合使用,如果软引用所引用的对象被垃圾回收,java 虚拟机就会把这个软引用加入到与之关联的引用队列中。\n\n3.弱引用(weakreference)\n\n如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。\n\n弱引用可以和一个引用队列(referencequeue)联合使用,如果弱引用所引用的对象被垃圾回收,java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。\n\n4.虚引用(phantomreference)\n\n\"虚引用\"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。\n\n虚引用主要用来跟踪对象被垃圾回收的活动。\n\n虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(referencequeue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。\n\n特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 jvm 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(outofmemory)等问题的产生。\n\n\n# 如何判断一个常量是废弃常量?\n\n运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?\n\n假如在字符串常量池中存在字符串 \"abc\",如果当前没有任何 string 对象引用该字符串常量的话,就说明常量 \"abc\" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,\"abc\" 就会被系统清理出常量池了。\n\n\n# 如何判断一个类是无用的类\n\n方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?\n\n判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :\n\n * 该类所有的实例都已经被回收,也就是 java 堆中不存在该类的任何实例。\n * 加载该类的 classloader 已经被回收。\n * 该类对应的 java.lang.class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。", "charsets": { "cjk": true - } + }, + "lastUpdated": "2022/09/18, 21:28:02", + "lastUpdatedTimestamp": 1663507682000 }, { "title": "常见的 GC 回收算法有哪些?", @@ -2126,14 +2128,16 @@ export const siteData = { "normalizedContent": "# 标记-清除算法\n\n该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:\n\n效率问题 空间问题(标记清除后会产生大量不连续的碎片)\n\n\n# 标记-复制算法\n\n为了解决效率问题,“标记-复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。\n\n\n# 标记-整理算法\n\n根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。\n\n\n# 分代收集算法\n\n当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。\n\n比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。\n\n延伸面试问题: hotspot 为什么要分为新生代和老年代?\n\n根据上面的对分代收集算法的介绍回答。\n\n\n# 垃圾收集器\n\n如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。\n\n虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的 hotspot 虚拟机就不会实现那么多不同的垃圾收集器了。\n\n\n# serial 收集器\n\nserial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( \"stop the world\" ),直到它收集结束。\n\n新生代采用标记-复制算法,老年代采用标记-整理算法。\n\n\n\n虚拟机的设计者们当然知道 stop the world 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。\n\n但是 serial 收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。serial 收集器对于运行在 client 模式下的虚拟机来说是个不错的选择。\n\n\n# parnew 收集器\n\nparnew 收集器其实就是 serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 serial 收集器完全一样。\n\n新生代采用标记-复制算法,老年代采用标记-整理算法。\n\n\n\n它是许多运行在 server 模式下的虚拟机的首要选择,除了 serial 收集器外,只有它能与 cms 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。\n\n并行和并发概念补充:\n\n并行(parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。\n\n并发(concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 cpu 上。\n\n\n# parallel scavenge 收集器\n\nparallel scavenge 收集器也是使用标记-复制算法的多线程收集器,它看上去几乎和 parnew 都一样。 那么它有什么特别之处呢?\n\n-xx:+useparallelgc\n\n 使用 parallel 收集器+ 老年代串行\n\n-xx:+useparalleloldgc\n\n 使用 parallel 收集器+ 老年代并行\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nparallel scavenge 收集器关注点是吞吐量(高效率的利用 cpu)。cms 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 cpu 中用于运行用户代码的时间与 cpu 总消耗时间的比值。 parallel scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 parallel scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。\n\n新生代采用标记-复制算法,老年代采用标记-整理算法。\n\n\n\n这是 jdk1.8 默认收集器\n\n使用 java -xx:+printcommandlineflags -version 命令查看\n\n-xx:initialheapsize=262921408 -xx:maxheapsize=4206742528 -xx:+printcommandlineflags -xx:+usecompressedclasspointers -xx:+usecompressedoops -xx:+useparallelgc\njava version \"1.8.0_211\"\njava(tm) se runtime environment (build 1.8.0_211-b12)\njava hotspot(tm) 64-bit server vm (build 25.211-b12, mixed mode)\njdk1.8 默认使用的是 parallel scavenge + parallel old,如果指定了-xx:+useparallelgc 参数,则默认指定了-xx:+useparalleloldgc,可以使用-xx:-useparalleloldgc 来禁用该功能\n\n\n1\n2\n3\n4\n5\n\n\n\n# serial old 收集器\n\nserial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 jdk1.5 以及以前的版本中与 parallel scavenge 收集器搭配使用,另一种用途是作为 cms 收集器的后备方案。\n\n\n# parallel old 收集器\n\nparallel scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 cpu 资源的场合,都可以优先考虑 parallel scavenge 收集器和 parallel old 收集器。\n\n\n# cms 收集器\n\ncms(concurrent mark sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。\n\ncms(concurrent mark sweep)收集器是 hotspot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。\n\n从名字中的mark sweep这两个词可以看出,cms 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:\n\n初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ; 并发标记: 同时开启 gc 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 gc 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短 并发清除: 开启用户线程,同时 gc 线程开始对未标记的区域做清扫。\n\n从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:\n\n对 cpu 资源敏感; 无法处理浮动垃圾; 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。\n\n\n# g1 收集器\n\ng1 (garbage-first) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 gc 停顿时间要求的同时,还具备高吞吐量性能特征.\n\n被视为 jdk1.7 中 hotspot 虚拟机的一个重要进化特征。它具备以下特点:\n\n并行与并发:g1 能充分利用 cpu、多核环境下的硬件优势,使用多个 cpu(cpu 或者 cpu 核心)来缩短 stop-the-world 停顿时间。部分其他收集器原本需要停顿 java 线程执行的 gc 动作,g1 收集器仍然可以通过并发的方式让 java 程序继续执行。 分代收集:虽然 g1 可以不需要其他收集器配合就能独立管理整个 gc 堆,但是还是保留了分代的概念。 空间整合:与 cms 的“标记-清理”算法不同,g1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。 可预测的停顿:这是 g1 相对于 cms 的另一个大优势,降低停顿时间是 g1 和 cms 共同的关注点,但 g1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 m 毫秒的时间片段内。 g1 收集器的运作大致分为以下几个步骤:\n\n初始标记 并发标记 最终标记 筛选回收 g1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 region(这也就是它的名字 garbage-first 的由来) 。这种使用 region 划分内存空间以及有优先级的区域回收方式,保证了 g1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。\n\n\n# zgc 收集器\n\n与 cms 中的 parnew 和 g1 类似,zgc 也采用标记-复制算法,不过 zgc 对该算法做了重大改进。\n\n在 zgc 中出现 stop the world 的情况会更少! 详情可以看 : 《新一代垃圾回收器 zgc 的探索与实践》", "charsets": { "cjk": true - } + }, + "lastUpdated": "2022/09/18, 21:28:02", + "lastUpdatedTimestamp": 1663507682000 }, { - "title": "Java 中如何进行 GC 调优?", + "title": "JVM 内存是如何对应到操作系统内存的?", "frontmatter": { - "title": "Java 中如何进行 GC 调优?", - "date": "2022-05-21T17:31:53.000Z", - "permalink": "/pages/07a030/", + "title": "JVM 内存是如何对应到操作系统内存的?", + "date": "2022-05-21T17:33:05.000Z", + "permalink": "/pages/da65e7/", "categories": [ "Java相关", "JVM" @@ -2142,17 +2146,74 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/020.Java%20%E4%B8%AD%E5%A6%82%E4%BD%95%E8%BF%9B%E8%A1%8C%20GC%20%E8%B0%83%E4%BC%98%EF%BC%9F.html", - "relativePath": "01.Java相关/30.JVM/020.Java 中如何进行 GC 调优?.md", - "key": "v-5474bec0", - "path": "/pages/07a030/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/040.JVM%20%E5%86%85%E5%AD%98%E6%98%AF%E5%A6%82%E4%BD%95%E5%AF%B9%E5%BA%94%E5%88%B0%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%86%85%E5%AD%98%E7%9A%84%EF%BC%9F.html", + "relativePath": "01.Java相关/30.JVM/040.JVM 内存是如何对应到操作系统内存的?.md", + "key": "v-2de19204", + "path": "/pages/da65e7/", "headers": [ { "level": 2, - "title": "GC优化需知", - "slug": "gc优化需知", - "normalizedTitle": "gc优化需知", - "charIndex": 305 + "title": "Java 内存模型", + "slug": "java-内存模型", + "normalizedTitle": "java 内存模型", + "charIndex": 2 + }, + { + "level": 2, + "title": "Java 线程与主内存的关系", + "slug": "java-线程与主内存的关系", + "normalizedTitle": "java 线程与主内存的关系", + "charIndex": 216 + }, + { + "level": 2, + "title": "线程间通信", + "slug": "线程间通信", + "normalizedTitle": "线程间通信", + "charIndex": 421 + }, + { + "level": 2, + "title": "有态度的总结", + "slug": "有态度的总结", + "normalizedTitle": "有态度的总结", + "charIndex": 1043 + } + ], + "headersStr": "Java 内存模型 Java 线程与主内存的关系 线程间通信 有态度的总结", + "content": "# Java 内存模型\n\n同一套内存模型规范,不同语言在实现上可能会有些差别。接下来着重讲一下 Java 内存模型实现原理。\n\nJava 运行时内存区域与硬件内存的关系\n\n了解过 JVM 的同学都知道,JVM 运行时内存区域是分片的,分为栈、堆等,其实这些都是 JVM 定义的逻辑概念。在传统的硬件内存架构中是没有栈和堆这种概念。\n\n\n\n从图中可以看出栈和堆既存在于高速缓存中又存在于主内存中,所以两者并没有很直接的关系。\n\n\n# Java 线程与主内存的关系\n\nJava 内存模型是一种规范,定义了很多东西:\n\n所有的变量都存储在主内存(Main Memory)中。 每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。 不同的线程之间无法直接访问对方本地内存中的变量。 看文字太枯燥了,我又画了一张图:\n\n\n# 线程间通信\n\n如果两个线程都对一个共享变量进行操作,共享变量初始值为 1,每个线程都变量进行加 1,预期共享变量的值为 3。在 JMM 规范下会有一系列的操作。\n\n\n\n为了更好的控制主内存和本地内存的交互,Java 内存模型定义了八种操作来实现:\n\n * lock:锁定。作用于主内存的变量,把一个变量标识为一条线程独占状态。\n * unlock:解锁。作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。\n * read:读取。作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用\n * load:载入。作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。\n * use:使用。作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。\n * assign:赋值。作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。\n * store:存储。作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。\n * write:写入。作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。 注意:工作内存也就是本地内存的意思。\n\n\n# 有态度的总结\n\n由于CPU 和主内存间存在数量级的速率差,想到了引入了多级高速缓存的传统硬件内存架构来解决,多级高速缓存作为 CPU 和主内间的缓冲提升了整体性能。解决了速率差的问题,却又带来了缓存一致性问题。\n\n数据同时存在于高速缓存和主内存中,如果不加以规范势必造成灾难,因此在传统机器上又抽象出了内存模型。\n\nJava 语言在遵循内存模型的基础上推出了 JMM 规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。\n\n为了更精准控制工作内存和主内存间的交互,JMM 还定义了八种操作:lock, unlock, read, load,use,assign, store, write。\n\n好了,今天就给大家介绍到这里,简单总结下Java内存模型的定义:Java内存模型并不是一件容易的事情,这个模型必须定义得足够严谨,才能让Java的并发操作不会产生歧义;但是,也必须得足够宽松,使得虚拟机的实现能有足够的自由空间去利用硬件的各种特性(寄存器、高速缓存等)来获取更好的执行速度。经过长时间的验证和修补,在JDK1.5发布后,Java内存模型就已经成熟和完善起来了。\n\n参考: 全面学习掌握Java内存模型", + "normalizedContent": "# java 内存模型\n\n同一套内存模型规范,不同语言在实现上可能会有些差别。接下来着重讲一下 java 内存模型实现原理。\n\njava 运行时内存区域与硬件内存的关系\n\n了解过 jvm 的同学都知道,jvm 运行时内存区域是分片的,分为栈、堆等,其实这些都是 jvm 定义的逻辑概念。在传统的硬件内存架构中是没有栈和堆这种概念。\n\n\n\n从图中可以看出栈和堆既存在于高速缓存中又存在于主内存中,所以两者并没有很直接的关系。\n\n\n# java 线程与主内存的关系\n\njava 内存模型是一种规范,定义了很多东西:\n\n所有的变量都存储在主内存(main memory)中。 每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。 不同的线程之间无法直接访问对方本地内存中的变量。 看文字太枯燥了,我又画了一张图:\n\n\n# 线程间通信\n\n如果两个线程都对一个共享变量进行操作,共享变量初始值为 1,每个线程都变量进行加 1,预期共享变量的值为 3。在 jmm 规范下会有一系列的操作。\n\n\n\n为了更好的控制主内存和本地内存的交互,java 内存模型定义了八种操作来实现:\n\n * lock:锁定。作用于主内存的变量,把一个变量标识为一条线程独占状态。\n * unlock:解锁。作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。\n * read:读取。作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用\n * load:载入。作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。\n * use:使用。作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。\n * assign:赋值。作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。\n * store:存储。作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。\n * write:写入。作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。 注意:工作内存也就是本地内存的意思。\n\n\n# 有态度的总结\n\n由于cpu 和主内存间存在数量级的速率差,想到了引入了多级高速缓存的传统硬件内存架构来解决,多级高速缓存作为 cpu 和主内间的缓冲提升了整体性能。解决了速率差的问题,却又带来了缓存一致性问题。\n\n数据同时存在于高速缓存和主内存中,如果不加以规范势必造成灾难,因此在传统机器上又抽象出了内存模型。\n\njava 语言在遵循内存模型的基础上推出了 jmm 规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。\n\n为了更精准控制工作内存和主内存间的交互,jmm 还定义了八种操作:lock, unlock, read, load,use,assign, store, write。\n\n好了,今天就给大家介绍到这里,简单总结下java内存模型的定义:java内存模型并不是一件容易的事情,这个模型必须定义得足够严谨,才能让java的并发操作不会产生歧义;但是,也必须得足够宽松,使得虚拟机的实现能有足够的自由空间去利用硬件的各种特性(寄存器、高速缓存等)来获取更好的执行速度。经过长时间的验证和修补,在jdk1.5发布后,java内存模型就已经成熟和完善起来了。\n\n参考: 全面学习掌握java内存模型", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/18, 21:28:02", + "lastUpdatedTimestamp": 1663507682000 + }, + { + "title": "Java 中如何进行 GC 调优?", + "frontmatter": { + "title": "Java 中如何进行 GC 调优?", + "date": "2022-05-21T17:31:53.000Z", + "permalink": "/pages/07a030/", + "categories": [ + "Java相关", + "JVM" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/020.Java%20%E4%B8%AD%E5%A6%82%E4%BD%95%E8%BF%9B%E8%A1%8C%20GC%20%E8%B0%83%E4%BC%98%EF%BC%9F.html", + "relativePath": "01.Java相关/30.JVM/020.Java 中如何进行 GC 调优?.md", + "key": "v-5474bec0", + "path": "/pages/07a030/", + "headers": [ + { + "level": 2, + "title": "GC优化需知", + "slug": "gc优化需知", + "normalizedTitle": "gc优化需知", + "charIndex": 305 }, { "level": 2, @@ -2203,8 +2264,8 @@ export const siteData = { "charsets": { "cjk": true }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 + "lastUpdated": "2022/09/18, 21:28:02", + "lastUpdatedTimestamp": 1663507682000 }, { "title": "JMM 中内存模型是怎样的?什么是指令序列重排序?", @@ -2267,72 +2328,107 @@ export const siteData = { "charsets": { "cjk": true }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 + "lastUpdated": "2022/09/18, 21:28:02", + "lastUpdatedTimestamp": 1663507682000 }, { - "title": "JVM 内存是如何对应到操作系统内存的?", + "title": "实现单例设计模式(懒汉,饿汉)", "frontmatter": { - "title": "JVM 内存是如何对应到操作系统内存的?", - "date": "2022-05-21T17:33:05.000Z", - "permalink": "/pages/da65e7/", + "title": "实现单例设计模式(懒汉,饿汉)", + "date": "2022-05-21T17:26:00.000Z", + "permalink": "/pages/b37ad7/", "categories": [ "Java相关", - "JVM" + "设计模式" ], "tags": [ null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/040.JVM%20%E5%86%85%E5%AD%98%E6%98%AF%E5%A6%82%E4%BD%95%E5%AF%B9%E5%BA%94%E5%88%B0%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%86%85%E5%AD%98%E7%9A%84%EF%BC%9F.html", - "relativePath": "01.Java相关/30.JVM/040.JVM 内存是如何对应到操作系统内存的?.md", - "key": "v-2de19204", - "path": "/pages/da65e7/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/002.%E5%AE%9E%E7%8E%B0%E5%8D%95%E4%BE%8B%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%EF%BC%88%E6%87%92%E6%B1%89%EF%BC%8C%E9%A5%BF%E6%B1%89%EF%BC%89.html", + "relativePath": "01.Java相关/40.设计模式/002.实现单例设计模式(懒汉,饿汉).md", + "key": "v-ba94c306", + "path": "/pages/b37ad7/", "headers": [ { "level": 2, - "title": "Java 内存模型", - "slug": "java-内存模型", - "normalizedTitle": "java 内存模型", - "charIndex": 2 + "title": "单例模式的定义", + "slug": "单例模式的定义", + "normalizedTitle": "单例模式的定义", + "charIndex": 252 }, { "level": 2, - "title": "Java 线程与主内存的关系", - "slug": "java-线程与主内存的关系", - "normalizedTitle": "java 线程与主内存的关系", - "charIndex": 216 + "title": "单例模式的类图", + "slug": "单例模式的类图", + "normalizedTitle": "单例模式的类图", + "charIndex": 294 }, { "level": 2, - "title": "线程间通信", - "slug": "线程间通信", - "normalizedTitle": "线程间通信", - "charIndex": 421 + "title": "单例模式的实现", + "slug": "单例模式的实现", + "normalizedTitle": "单例模式的实现", + "charIndex": 15 + }, + { + "level": 3, + "title": "饿汉式", + "slug": "饿汉式", + "normalizedTitle": "饿汉式", + "charIndex": 320 + }, + { + "level": 3, + "title": "懒汉式", + "slug": "懒汉式", + "normalizedTitle": "懒汉式", + "charIndex": 856 + }, + { + "level": 3, + "title": "静态内部类", + "slug": "静态内部类", + "normalizedTitle": "静态内部类", + "charIndex": 2627 + }, + { + "level": 3, + "title": "枚举", + "slug": "枚举", + "normalizedTitle": "枚举", + "charIndex": 3151 }, { "level": 2, - "title": "有态度的总结", - "slug": "有态度的总结", - "normalizedTitle": "有态度的总结", - "charIndex": 1043 + "title": "单例模式在JDK 中的源码分析", + "slug": "单例模式在jdk-中的源码分析", + "normalizedTitle": "单例模式在jdk 中的源码分析", + "charIndex": 3399 + }, + { + "level": 2, + "title": "单例模式注意事项和细节", + "slug": "单例模式注意事项和细节", + "normalizedTitle": "单例模式注意事项和细节", + "charIndex": 3459 } ], - "headersStr": "Java 内存模型 Java 线程与主内存的关系 线程间通信 有态度的总结", - "content": "# Java 内存模型\n\n同一套内存模型规范,不同语言在实现上可能会有些差别。接下来着重讲一下 Java 内存模型实现原理。\n\nJava 运行时内存区域与硬件内存的关系\n\n了解过 JVM 的同学都知道,JVM 运行时内存区域是分片的,分为栈、堆等,其实这些都是 JVM 定义的逻辑概念。在传统的硬件内存架构中是没有栈和堆这种概念。\n\n\n\n从图中可以看出栈和堆既存在于高速缓存中又存在于主内存中,所以两者并没有很直接的关系。\n\n\n# Java 线程与主内存的关系\n\nJava 内存模型是一种规范,定义了很多东西:\n\n所有的变量都存储在主内存(Main Memory)中。 每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。 不同的线程之间无法直接访问对方本地内存中的变量。 看文字太枯燥了,我又画了一张图:\n\n\n# 线程间通信\n\n如果两个线程都对一个共享变量进行操作,共享变量初始值为 1,每个线程都变量进行加 1,预期共享变量的值为 3。在 JMM 规范下会有一系列的操作。\n\n\n\n为了更好的控制主内存和本地内存的交互,Java 内存模型定义了八种操作来实现:\n\n * lock:锁定。作用于主内存的变量,把一个变量标识为一条线程独占状态。\n * unlock:解锁。作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。\n * read:读取。作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用\n * load:载入。作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。\n * use:使用。作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。\n * assign:赋值。作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。\n * store:存储。作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。\n * write:写入。作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。 注意:工作内存也就是本地内存的意思。\n\n\n# 有态度的总结\n\n由于CPU 和主内存间存在数量级的速率差,想到了引入了多级高速缓存的传统硬件内存架构来解决,多级高速缓存作为 CPU 和主内间的缓冲提升了整体性能。解决了速率差的问题,却又带来了缓存一致性问题。\n\n数据同时存在于高速缓存和主内存中,如果不加以规范势必造成灾难,因此在传统机器上又抽象出了内存模型。\n\nJava 语言在遵循内存模型的基础上推出了 JMM 规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。\n\n为了更精准控制工作内存和主内存间的交互,JMM 还定义了八种操作:lock, unlock, read, load,use,assign, store, write。\n\n好了,今天就给大家介绍到这里,简单总结下Java内存模型的定义:Java内存模型并不是一件容易的事情,这个模型必须定义得足够严谨,才能让Java的并发操作不会产生歧义;但是,也必须得足够宽松,使得虚拟机的实现能有足够的自由空间去利用硬件的各种特性(寄存器、高速缓存等)来获取更好的执行速度。经过长时间的验证和修补,在JDK1.5发布后,Java内存模型就已经成熟和完善起来了。\n\n参考: 全面学习掌握Java内存模型", - "normalizedContent": "# java 内存模型\n\n同一套内存模型规范,不同语言在实现上可能会有些差别。接下来着重讲一下 java 内存模型实现原理。\n\njava 运行时内存区域与硬件内存的关系\n\n了解过 jvm 的同学都知道,jvm 运行时内存区域是分片的,分为栈、堆等,其实这些都是 jvm 定义的逻辑概念。在传统的硬件内存架构中是没有栈和堆这种概念。\n\n\n\n从图中可以看出栈和堆既存在于高速缓存中又存在于主内存中,所以两者并没有很直接的关系。\n\n\n# java 线程与主内存的关系\n\njava 内存模型是一种规范,定义了很多东西:\n\n所有的变量都存储在主内存(main memory)中。 每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。 不同的线程之间无法直接访问对方本地内存中的变量。 看文字太枯燥了,我又画了一张图:\n\n\n# 线程间通信\n\n如果两个线程都对一个共享变量进行操作,共享变量初始值为 1,每个线程都变量进行加 1,预期共享变量的值为 3。在 jmm 规范下会有一系列的操作。\n\n\n\n为了更好的控制主内存和本地内存的交互,java 内存模型定义了八种操作来实现:\n\n * lock:锁定。作用于主内存的变量,把一个变量标识为一条线程独占状态。\n * unlock:解锁。作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。\n * read:读取。作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用\n * load:载入。作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。\n * use:使用。作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。\n * assign:赋值。作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。\n * store:存储。作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。\n * write:写入。作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。 注意:工作内存也就是本地内存的意思。\n\n\n# 有态度的总结\n\n由于cpu 和主内存间存在数量级的速率差,想到了引入了多级高速缓存的传统硬件内存架构来解决,多级高速缓存作为 cpu 和主内间的缓冲提升了整体性能。解决了速率差的问题,却又带来了缓存一致性问题。\n\n数据同时存在于高速缓存和主内存中,如果不加以规范势必造成灾难,因此在传统机器上又抽象出了内存模型。\n\njava 语言在遵循内存模型的基础上推出了 jmm 规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。\n\n为了更精准控制工作内存和主内存间的交互,jmm 还定义了八种操作:lock, unlock, read, load,use,assign, store, write。\n\n好了,今天就给大家介绍到这里,简单总结下java内存模型的定义:java内存模型并不是一件容易的事情,这个模型必须定义得足够严谨,才能让java的并发操作不会产生歧义;但是,也必须得足够宽松,使得虚拟机的实现能有足够的自由空间去利用硬件的各种特性(寄存器、高速缓存等)来获取更好的执行速度。经过长时间的验证和修补,在jdk1.5发布后,java内存模型就已经成熟和完善起来了。\n\n参考: 全面学习掌握java内存模型", + "headersStr": "单例模式的定义 单例模式的类图 单例模式的实现 饿汉式 懒汉式 静态内部类 枚举 单例模式在JDK 中的源码分析 单例模式注意事项和细节", + "content": "> 面试官:带笔了吧,那写两种单例模式的实现方法吧 沙沙沙刷刷刷~~~ 写好了 面试官:怎样防止new 对象出来?\n\n有一些对象我们确实只需要一个,比如,线程池、数据库连接、缓存、日志对象等,如果有多个的话,会造成程序的行为异常,资源使用过量或者不一致的问题。你也许会说,这种我用全局变量不也能实现吗,还整个单例模式,好像你很流弊的样子,如果将对象赋值给一个全局变量,那程序启动就会创建好对象,万一这个对象很耗资源,我们还可能在某些时候用不到,这就造成了资源的浪费,不合理,所以就有了单例模式。\n\n\n# 单例模式的定义\n\n单例模式确保一个类只有一个实例,并提供一个全局唯一访问点\n\n\n# 单例模式的类图\n\n\n\n\n# 单例模式的实现\n\n\n# 饿汉式\n\nstatic 变量在类装载的时候进行初始化 多个实例的 static 变量会共享同一块内存区域 用这两个知识点写出的单例类就是饿汉式了,初始化类的时候就创建,饥不择食,饿汉\n\npublic class Singleton {\n\n //构造私有化,防止直接new\n private Singleton(){}\n\n //静态初始化器(static initializer)中创建实例,保证线程安全\n private static Singleton instance = new Singleton();\n\n public static Singleton getInstance(){\n return instance;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n饿汉式是线程安全的,JVM在加载类时马上创建唯一的实例对象,且只会装载一次。\n\nJava 实现的单例是一个虚拟机的范围,因为装载类的功能是虚拟机的,所以一个虚拟机通过自己的ClassLoader 装载饿汉式实现单例类的时候就会创建一个类实例。(如果一个虚拟机里有多个ClassLoader的话,就会有多个实例)\n\n\n# 懒汉式\n\n懒汉式,就是实例在用到的时候才去创建,比较“懒”\n\n单例模式的懒汉式实现方式体现了延迟加载的思想(延迟加载也称懒加载Lazy Load,就是一开始不要加载资源或数据,等到要使用的时候才加载)\n\n同步方法\n\npublic class Singleton {\n private static Singleton singleton;\n\n private Singleton(){}\n\n \t//解决了线程不安全问题,但是效率太低了,每个线程想获得类的实例的时候,都需要同步方法,不推荐\n public static synchronized Singleton getInstance(){\n if(singleton == null){\n singleton = new Singleton();\n }\n return singleton;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n双重检查加锁\n\npublic class Singleton {\n\n \t//volatitle关键词确保,多线程正确处理singleton\n private static volatile Singleton singleton;\n \n private Singleton(){}\n \n public static Singleton getInstance(){\n if(singleton ==null){\n synchronized (Singleton.class){\n if(singleton == null){\n singleton = new Singleton();\n }\n }\n }\n return singleton;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n\n\nDouble-Check 概念(进行两次检查)是多线程开发中经常使用的,为什么需要双重检查锁呢?因为第一次检查是确保之前是一个空对象,而非空对象就不需要同步了,空对象的线程然后进入同步代码块,如果不加第二次空对象检查,两个线程同时获取同步代码块,一个线程进入同步代码块,另一个线程就会等待,而这两个线程就会创建两个实例化对象,所以需要在线程进入同步代码块后再次进行空对象检查,才能确保只创建一个实例化对象。\n\n双重检查加锁(double checked locking)线程安全、延迟加载、效率比较高\n\nvolatile:volatile一般用于多线程的可见性,这里用来防止指令重排(防止new Singleton时指令重排序导致其他线程获取到未初始化完的对象)。被volatile 修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。\n\n指令重排\n\n指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序。\n\nJava中创建一个对象,往往包含三个过程。对于singleton = new Singleton(),这不是一个原子操作,在 JVM 中包含如下三个过程。\n\n给 singleton 分配内存\n\n调用 Singleton 的构造函数来初始化成员变量,形成实例\n\n将 singleton 对象指向分配的内存空间(执行完这步 singleton才是非 null 了)\n\n但是,由于JVM会进行指令重排序,所以上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3,也可能是 1-3-2。\n\n如果是 1-3-2,则在 3 执行完毕,2 未执行之前,被另一个线程抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以这个线程会直接返回 instance,然后使用,那肯定就会报错了,所以要加入 volatile关键字。\n\n\n# 静态内部类\n\npublic class Singleton {\n\n private Singleton(){}\n\n private static class SingletonInstance{\n private static final Singleton INSTANCE = new Singleton();\n }\n \n public static Singleton getInstance(){\n return SingletonInstance.INSTANCE;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n采用类加载的机制来保证初始化实例时只有一个线程;\n\n静态内部类方式在Singleton 类被装载的时候并不会立即实例化,而是在调用getInstance的时候,才去装载内部类SingletonInstance ,从而完成Singleton的实例化\n\n类的静态属性只会在第一次加载类的时候初始化,所以,JVM帮我们保证了线程的安全性,在类初始化时,其他线程无法进入\n\n优点:线程安全,利用静态内部类实现延迟加载,效率较高,推荐使用\n\n\n# 枚举\n\nenum Singleton{\n INSTANCE;\n public void method(){}\n}\n\n\n1\n2\n3\n4\n\n\n借助JDK5 添加的枚举实现单例,不仅可以避免多线程同步问题,还能防止反序列化重新创建新的对象,但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。不过,由于Java1.5中才加入enum特性,所以使用的人并不多。\n\n这种方式是《Effective Java》 作者Josh Bloch 提倡的方式。\n\n\n# 单例模式在JDK 中的源码分析\n\nJDK 中,java.lang.Runtime 就是经典的单例模式(饿汉式)\n\n\n# 单例模式注意事项和细节\n\n单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使 用new 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或 耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数 据库或文件的对象(比如数据源、session工厂等)", + "normalizedContent": "> 面试官:带笔了吧,那写两种单例模式的实现方法吧 沙沙沙刷刷刷~~~ 写好了 面试官:怎样防止new 对象出来?\n\n有一些对象我们确实只需要一个,比如,线程池、数据库连接、缓存、日志对象等,如果有多个的话,会造成程序的行为异常,资源使用过量或者不一致的问题。你也许会说,这种我用全局变量不也能实现吗,还整个单例模式,好像你很流弊的样子,如果将对象赋值给一个全局变量,那程序启动就会创建好对象,万一这个对象很耗资源,我们还可能在某些时候用不到,这就造成了资源的浪费,不合理,所以就有了单例模式。\n\n\n# 单例模式的定义\n\n单例模式确保一个类只有一个实例,并提供一个全局唯一访问点\n\n\n# 单例模式的类图\n\n\n\n\n# 单例模式的实现\n\n\n# 饿汉式\n\nstatic 变量在类装载的时候进行初始化 多个实例的 static 变量会共享同一块内存区域 用这两个知识点写出的单例类就是饿汉式了,初始化类的时候就创建,饥不择食,饿汉\n\npublic class singleton {\n\n //构造私有化,防止直接new\n private singleton(){}\n\n //静态初始化器(static initializer)中创建实例,保证线程安全\n private static singleton instance = new singleton();\n\n public static singleton getinstance(){\n return instance;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n饿汉式是线程安全的,jvm在加载类时马上创建唯一的实例对象,且只会装载一次。\n\njava 实现的单例是一个虚拟机的范围,因为装载类的功能是虚拟机的,所以一个虚拟机通过自己的classloader 装载饿汉式实现单例类的时候就会创建一个类实例。(如果一个虚拟机里有多个classloader的话,就会有多个实例)\n\n\n# 懒汉式\n\n懒汉式,就是实例在用到的时候才去创建,比较“懒”\n\n单例模式的懒汉式实现方式体现了延迟加载的思想(延迟加载也称懒加载lazy load,就是一开始不要加载资源或数据,等到要使用的时候才加载)\n\n同步方法\n\npublic class singleton {\n private static singleton singleton;\n\n private singleton(){}\n\n \t//解决了线程不安全问题,但是效率太低了,每个线程想获得类的实例的时候,都需要同步方法,不推荐\n public static synchronized singleton getinstance(){\n if(singleton == null){\n singleton = new singleton();\n }\n return singleton;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n双重检查加锁\n\npublic class singleton {\n\n \t//volatitle关键词确保,多线程正确处理singleton\n private static volatile singleton singleton;\n \n private singleton(){}\n \n public static singleton getinstance(){\n if(singleton ==null){\n synchronized (singleton.class){\n if(singleton == null){\n singleton = new singleton();\n }\n }\n }\n return singleton;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n\n\ndouble-check 概念(进行两次检查)是多线程开发中经常使用的,为什么需要双重检查锁呢?因为第一次检查是确保之前是一个空对象,而非空对象就不需要同步了,空对象的线程然后进入同步代码块,如果不加第二次空对象检查,两个线程同时获取同步代码块,一个线程进入同步代码块,另一个线程就会等待,而这两个线程就会创建两个实例化对象,所以需要在线程进入同步代码块后再次进行空对象检查,才能确保只创建一个实例化对象。\n\n双重检查加锁(double checked locking)线程安全、延迟加载、效率比较高\n\nvolatile:volatile一般用于多线程的可见性,这里用来防止指令重排(防止new singleton时指令重排序导致其他线程获取到未初始化完的对象)。被volatile 修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。\n\n指令重排\n\n指令重排是指在程序执行过程中, 为了性能考虑, 编译器和cpu可能会对指令重新排序。\n\njava中创建一个对象,往往包含三个过程。对于singleton = new singleton(),这不是一个原子操作,在 jvm 中包含如下三个过程。\n\n给 singleton 分配内存\n\n调用 singleton 的构造函数来初始化成员变量,形成实例\n\n将 singleton 对象指向分配的内存空间(执行完这步 singleton才是非 null 了)\n\n但是,由于jvm会进行指令重排序,所以上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3,也可能是 1-3-2。\n\n如果是 1-3-2,则在 3 执行完毕,2 未执行之前,被另一个线程抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以这个线程会直接返回 instance,然后使用,那肯定就会报错了,所以要加入 volatile关键字。\n\n\n# 静态内部类\n\npublic class singleton {\n\n private singleton(){}\n\n private static class singletoninstance{\n private static final singleton instance = new singleton();\n }\n \n public static singleton getinstance(){\n return singletoninstance.instance;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n采用类加载的机制来保证初始化实例时只有一个线程;\n\n静态内部类方式在singleton 类被装载的时候并不会立即实例化,而是在调用getinstance的时候,才去装载内部类singletoninstance ,从而完成singleton的实例化\n\n类的静态属性只会在第一次加载类的时候初始化,所以,jvm帮我们保证了线程的安全性,在类初始化时,其他线程无法进入\n\n优点:线程安全,利用静态内部类实现延迟加载,效率较高,推荐使用\n\n\n# 枚举\n\nenum singleton{\n instance;\n public void method(){}\n}\n\n\n1\n2\n3\n4\n\n\n借助jdk5 添加的枚举实现单例,不仅可以避免多线程同步问题,还能防止反序列化重新创建新的对象,但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。不过,由于java1.5中才加入enum特性,所以使用的人并不多。\n\n这种方式是《effective java》 作者josh bloch 提倡的方式。\n\n\n# 单例模式在jdk 中的源码分析\n\njdk 中,java.lang.runtime 就是经典的单例模式(饿汉式)\n\n\n# 单例模式注意事项和细节\n\n单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使 用new 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或 耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数 据库或文件的对象(比如数据源、session工厂等)", "charsets": { "cjk": true }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 + "lastUpdated": "2022/06/20, 22:47:41", + "lastUpdatedTimestamp": 1655736461000 }, { - "title": "什么是内存泄漏,怎么确定内存泄漏?", + "title": "简述 CMS 与 G1 机制的区别", "frontmatter": { - "title": "什么是内存泄漏,怎么确定内存泄漏?", - "date": "2022-05-21T17:33:27.000Z", - "permalink": "/pages/3ab5f5/", + "title": "简述 CMS 与 G1 机制的区别", + "date": "2022-05-21T17:33:40.000Z", + "permalink": "/pages/be69af/", "categories": [ "Java相关", "JVM" @@ -2341,125 +2437,97 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/060.%E4%BB%80%E4%B9%88%E6%98%AF%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%EF%BC%8C%E6%80%8E%E4%B9%88%E7%A1%AE%E5%AE%9A%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%EF%BC%9F.html", - "relativePath": "01.Java相关/30.JVM/060.什么是内存泄漏,怎么确定内存泄漏?.md", - "key": "v-15fba8f1", - "path": "/pages/3ab5f5/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/070.%E7%AE%80%E8%BF%B0%20CMS%20%E4%B8%8E%20G1%20%E6%9C%BA%E5%88%B6%E7%9A%84%E5%8C%BA%E5%88%AB.html", + "relativePath": "01.Java相关/30.JVM/070.简述 CMS 与 G1 机制的区别.md", + "key": "v-28c1d6d9", + "path": "/pages/be69af/", "headers": [ { "level": 2, - "title": "前言", - "slug": "前言", - "normalizedTitle": "前言", + "title": "G1特有数据结构和算法", + "slug": "g1特有数据结构和算法", + "normalizedTitle": "g1特有数据结构和算法", "charIndex": 2 }, - { - "level": 2, - "title": "一、介绍", - "slug": "一、介绍", - "normalizedTitle": "一、介绍", - "charIndex": 178 - }, - { - "level": 3, - "title": "1、什么是内存泄漏", - "slug": "_1、什么是内存泄漏", - "normalizedTitle": "1、什么是内存泄漏", - "charIndex": 187 - }, { "level": 3, - "title": "2、内存泄漏的危害", - "slug": "_2、内存泄漏的危害", - "normalizedTitle": "2、内存泄漏的危害", - "charIndex": 404 - }, - { - "level": 2, - "title": "二、内存泄漏原因", - "slug": "二、内存泄漏原因", - "normalizedTitle": "二、内存泄漏原因", - "charIndex": 491 + "title": "Region", + "slug": "region", + "normalizedTitle": "region", + "charIndex": 18 }, { "level": 3, - "title": "1、大量使用静态变量", - "slug": "_1、大量使用静态变量", - "normalizedTitle": "1、大量使用静态变量", - "charIndex": 583 + "title": "SATB算法", + "slug": "satb算法", + "normalizedTitle": "satb算法", + "charIndex": 179 }, { "level": 3, - "title": "2、连接资源未关闭", - "slug": "_2、连接资源未关闭", - "normalizedTitle": "2、连接资源未关闭", - "charIndex": 1310 + "title": "三色标记法", + "slug": "三色标记法", + "normalizedTitle": "三色标记法", + "charIndex": 248 }, { "level": 3, - "title": "4、内部类持有外部类", - "slug": "_4、内部类持有外部类", - "normalizedTitle": "4、内部类持有外部类", - "charIndex": 2418 + "title": "RSet", + "slug": "rset", + "normalizedTitle": "rset", + "charIndex": 670 }, { "level": 3, - "title": "5、finalize方法", - "slug": "_5、finalize方法", - "normalizedTitle": "5、finalize方法", - "charIndex": 2445 + "title": "垃圾回收过程", + "slug": "垃圾回收过程", + "normalizedTitle": "垃圾回收过程", + "charIndex": 1013 }, { "level": 3, - "title": "6、ThreadLocal的错误使用", - "slug": "_6、threadlocal的错误使用", - "normalizedTitle": "6、threadlocal的错误使用", - "charIndex": 2595 + "title": "与CMS相比的优势", + "slug": "与cms相比的优势", + "normalizedTitle": "与cms相比的优势", + "charIndex": 1517 }, { "level": 2, - "title": "三、检测内存泄漏", - "slug": "三、检测内存泄漏", - "normalizedTitle": "三、检测内存泄漏", - "charIndex": 3062 + "title": "CMS", + "slug": "cms", + "normalizedTitle": "cms", + "charIndex": 255 }, { "level": 3, - "title": "1、工具分析", - "slug": "_1、工具分析", - "normalizedTitle": "1、工具分析", - "charIndex": 3106 + "title": "什么是CMS?", + "slug": "什么是cms", + "normalizedTitle": "什么是cms?", + "charIndex": 1699 }, { "level": 3, - "title": "2、垃圾回收分析", - "slug": "_2、垃圾回收分析", - "normalizedTitle": "2、垃圾回收分析", - "charIndex": 3232 + "title": "CMS有什么用?", + "slug": "cms有什么用", + "normalizedTitle": "cms有什么用?", + "charIndex": 1785 }, { "level": 3, - "title": "3、基准测试", - "slug": "_3、基准测试", - "normalizedTitle": "3、基准测试", - "charIndex": 3374 - }, - { - "level": 2, - "title": "四、结论", - "slug": "四、结论", - "normalizedTitle": "四、结论", - "charIndex": 3418 + "title": "CMS如何执行?", + "slug": "cms如何执行", + "normalizedTitle": "cms如何执行?", + "charIndex": 1865 } ], - "headersStr": "前言 一、介绍 1、什么是内存泄漏 2、内存泄漏的危害 二、内存泄漏原因 1、大量使用静态变量 2、连接资源未关闭 4、内部类持有外部类 5、finalize方法 6、ThreadLocal的错误使用 三、检测内存泄漏 1、工具分析 2、垃圾回收分析 3、基准测试 四、结论", - "content": "# 前言\n\n这个问题是我之前翻看面经的时候见到的。那位小姐姐把内存泄漏当成了内存溢出问题去解答的,结果当场挂掉了。为此总结一下,之前和一位老哥也讨论过这个问题。可见不管是面试还是工作这都是一个极为重要的点。\n\n我也曾在面阿里的时候也遇到过原题,题目是写出俩内存泄漏案例,然后问如何排查?如何解决?\n\n本篇文章大体结构来自外国大佬baeldung;\n\n\n# 一、介绍\n\n\n# 1、什么是内存泄漏\n\njava的优势之一就是内置了垃圾回收器GC,它帮助我们实现了自动化内存管理。但是GC再好,也有老马失前蹄的时候,它不能保证提供一个解决内存泄漏的万无一失的解决方案。什么是内存泄漏?可以看看下面这张图,\n\n也就是一部分内存空间我明明已经使用了,却没有引用指向这部分空间。造成这片已经使用的空间无法处理的情况。\n\n正规点的理解:动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。\n\n\n# 2、内存泄漏的危害\n\n长时间运行,程序变卡,性能严重下降 程序莫名其妙挂掉 OutOfMemoryError错误 乱七八糟的错误,还不易排查 反正内存泄漏不是好事。\n\n\n# 二、内存泄漏原因\n\n内存泄漏原因太多了。说不定就是某一行代码不对就会出现这种情况,因此这里给出最常见的几种。关键的还是如何找出哪个地方出现了内存泄漏,代码好修改,错误不易查。\n\n\n# 1、大量使用静态变量\n\n静态变量的生命周期与程序一致。因此常驻内存。\n\npublic class Static Test{\n public static List list = new ArrayList<>();\n public void populateList(){\n for (int i = 0; i < 10000000; i++) {\n list.add((int)Math.random()); \n } \n System.out.println(\"running......\"); \n }\n public static void main(String[] args){ \n System.out.println(\"before......\");\n new StaticTest().populateList(); \n System.out.println(\"after......\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n\n\n现在可以使用jvisualvm运行一边,看看内存效果。\n\n * 带static关键字(使用静态变量)\n\n从上图可以看到,堆内存从一开始的135M左右飙升了到了200M。直接占据了65M的内存。\n\n * 不使用static关键字(不使用静态变量)\n\n由于全局变量与程序周期不一致,因此不使用时,就会进行回收。此时内存最高150M。\n\n总结:由于静态变量与程序生命周期一致,因此对象常驻内存,造成内存泄漏\n\n\n# 2、连接资源未关闭\n\n每当建立一个连接,jvm就会为这么资源分配内存。比如数据库连接、文件输入输出流、网络连接等等。\n\npublic class File Test{\n public static void main(String[] args)throws IOException {\n File f=new File(\"G:\\\\nginx配套资料\\\\笔记资料.zip\"); \n System.out.println(f.exists());\n System.out.println(f.isDirectory());\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n依然使用jvisualvm运行一边,看看内存效果。\n\n可以看出,在连接文件资源时,jvm会为本资源分配内存。\n\n3、equals()和hashCode()方法使用不当\n\n定义新类时,如果没有重新equals()和hashCode()方法,也有可能会造成内存泄漏。主要原因是没有这两个方法时,很容易造成重复的数据添加。看例子:\n\npublic class User{\n public String name;\n publicint age;\n public User(String name, int age){\n this.name = name;\n this.age = age;}\n }\n public class EqualTest{\n public static void main(String[] args){ \n Map map = new HashMap<>();\n for(int i=0; i<100; i++) { \n map.put(new User(\"\", 1), 1);\n } \n System.out.println(map.size() == 1);//输出为false \n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\n然后运行一下,看看内存情况:\n\n内存从150M一下子飙升到225M,可见飙升的厉害。输出为false,说明user对象被重复添加了。我们知道像HashMap在添加新的对象时,会对其hashcode进行比较,如果一样,那就不插入。如果一样那就插入。此时说明这100个User其hashcode不同。\n\n\n# 4、内部类持有外部类\n\n这个场景和上面类似。\n\n\n# 5、finalize方法\n\n这个方法之前曾经专门花过文章写过,这个问题很简单。看一张图\n\n这就是整个过程。不过在这里我们主要看的是finalize方法对垃圾回收的影响,其实就是在第三步,也就是这个对象含有finalize,进入了队列但一直没有被调用的这段时间,会一直占用内存。造成内存泄漏。\n\n\n# 6、ThreadLocal的错误使用\n\nThreadLocal主要用于创建本地线程变量,不合理的使用也有可能会造成内存泄漏。\n\n上面这张图详细的揭示了ThreadLocal和Thread以及ThreadLocalMap三者的关系。\n\n1、Thread中有一个map,就是ThreadLocalMap\n\n2、ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。\n\n3、ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收\n\n4、重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。\n\n解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。\n\n现在介绍了几种常见的内存泄漏情况,上面的知识点比较常见,最主要的是如何检测出来。\n\n\n# 三、检测内存泄漏\n\n检测的目的是定位内存泄漏出现的位置,常见的有以下几种方法:\n\n\n# 1、工具分析\n\n这个工具比较多,比如说JProfiler、YourKit、Java VisualVM和Netbeans Profiler。他可以帮助我们分析是哪一个对象或者是类内存的飙升。也可以看到内存CPU的等等各种情况。上面多次演示到了。\n\n\n# 2、垃圾回收分析\n\n这个其实也可以用工具进行分析。上面的VisualVM中,可以打印堆。也可以从外部导入dump文件进行分析。\n\n如果不用工具的话,我们可以通过IDE看到。JVM配置添加-verbose:gc。然后就会打印出相关信息。下面这张图非原创,来自Baeldung。\n\n\n# 3、基准测试\n\n也就是使用科学的方式进行分析java代码的性能。进而判断分析。\n\n\n# 四、结论\n\n内存泄漏是个很严重的问题,也比较常见。\n\n最主要的原因是动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。因此良好的代码规范,可以有效地避免这些错误。\n\n参考: 什么是内存泄漏?该如何检测?又该如何解决?", - "normalizedContent": "# 前言\n\n这个问题是我之前翻看面经的时候见到的。那位小姐姐把内存泄漏当成了内存溢出问题去解答的,结果当场挂掉了。为此总结一下,之前和一位老哥也讨论过这个问题。可见不管是面试还是工作这都是一个极为重要的点。\n\n我也曾在面阿里的时候也遇到过原题,题目是写出俩内存泄漏案例,然后问如何排查?如何解决?\n\n本篇文章大体结构来自外国大佬baeldung;\n\n\n# 一、介绍\n\n\n# 1、什么是内存泄漏\n\njava的优势之一就是内置了垃圾回收器gc,它帮助我们实现了自动化内存管理。但是gc再好,也有老马失前蹄的时候,它不能保证提供一个解决内存泄漏的万无一失的解决方案。什么是内存泄漏?可以看看下面这张图,\n\n也就是一部分内存空间我明明已经使用了,却没有引用指向这部分空间。造成这片已经使用的空间无法处理的情况。\n\n正规点的理解:动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。\n\n\n# 2、内存泄漏的危害\n\n长时间运行,程序变卡,性能严重下降 程序莫名其妙挂掉 outofmemoryerror错误 乱七八糟的错误,还不易排查 反正内存泄漏不是好事。\n\n\n# 二、内存泄漏原因\n\n内存泄漏原因太多了。说不定就是某一行代码不对就会出现这种情况,因此这里给出最常见的几种。关键的还是如何找出哪个地方出现了内存泄漏,代码好修改,错误不易查。\n\n\n# 1、大量使用静态变量\n\n静态变量的生命周期与程序一致。因此常驻内存。\n\npublic class static test{\n public static list list = new arraylist<>();\n public void populatelist(){\n for (int i = 0; i < 10000000; i++) {\n list.add((int)math.random()); \n } \n system.out.println(\"running......\"); \n }\n public static void main(string[] args){ \n system.out.println(\"before......\");\n new statictest().populatelist(); \n system.out.println(\"after......\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n\n\n现在可以使用jvisualvm运行一边,看看内存效果。\n\n * 带static关键字(使用静态变量)\n\n从上图可以看到,堆内存从一开始的135m左右飙升了到了200m。直接占据了65m的内存。\n\n * 不使用static关键字(不使用静态变量)\n\n由于全局变量与程序周期不一致,因此不使用时,就会进行回收。此时内存最高150m。\n\n总结:由于静态变量与程序生命周期一致,因此对象常驻内存,造成内存泄漏\n\n\n# 2、连接资源未关闭\n\n每当建立一个连接,jvm就会为这么资源分配内存。比如数据库连接、文件输入输出流、网络连接等等。\n\npublic class file test{\n public static void main(string[] args)throws ioexception {\n file f=new file(\"g:\\\\nginx配套资料\\\\笔记资料.zip\"); \n system.out.println(f.exists());\n system.out.println(f.isdirectory());\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n依然使用jvisualvm运行一边,看看内存效果。\n\n可以看出,在连接文件资源时,jvm会为本资源分配内存。\n\n3、equals()和hashcode()方法使用不当\n\n定义新类时,如果没有重新equals()和hashcode()方法,也有可能会造成内存泄漏。主要原因是没有这两个方法时,很容易造成重复的数据添加。看例子:\n\npublic class user{\n public string name;\n publicint age;\n public user(string name, int age){\n this.name = name;\n this.age = age;}\n }\n public class equaltest{\n public static void main(string[] args){ \n map map = new hashmap<>();\n for(int i=0; i<100; i++) { \n map.put(new user(\"\", 1), 1);\n } \n system.out.println(map.size() == 1);//输出为false \n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\n然后运行一下,看看内存情况:\n\n内存从150m一下子飙升到225m,可见飙升的厉害。输出为false,说明user对象被重复添加了。我们知道像hashmap在添加新的对象时,会对其hashcode进行比较,如果一样,那就不插入。如果一样那就插入。此时说明这100个user其hashcode不同。\n\n\n# 4、内部类持有外部类\n\n这个场景和上面类似。\n\n\n# 5、finalize方法\n\n这个方法之前曾经专门花过文章写过,这个问题很简单。看一张图\n\n这就是整个过程。不过在这里我们主要看的是finalize方法对垃圾回收的影响,其实就是在第三步,也就是这个对象含有finalize,进入了队列但一直没有被调用的这段时间,会一直占用内存。造成内存泄漏。\n\n\n# 6、threadlocal的错误使用\n\nthreadlocal主要用于创建本地线程变量,不合理的使用也有可能会造成内存泄漏。\n\n上面这张图详细的揭示了threadlocal和thread以及threadlocalmap三者的关系。\n\n1、thread中有一个map,就是threadlocalmap\n\n2、threadlocalmap的key是threadlocal,值是我们自己设定的。\n\n3、threadlocal是一个弱引用,当为null时,会被当成垃圾回收\n\n4、重点来了,突然我们threadlocal是null了,也就是要被垃圾回收器回收了,但是此时我们的threadlocalmap生命周期和thread的一样,它不会回收,这时候就出现了一个现象。那就是threadlocalmap的key没了,但是value还在,这就造成了内存泄漏。\n\n解决办法:使用完threadlocal后,执行remove操作,避免出现内存溢出情况。\n\n现在介绍了几种常见的内存泄漏情况,上面的知识点比较常见,最主要的是如何检测出来。\n\n\n# 三、检测内存泄漏\n\n检测的目的是定位内存泄漏出现的位置,常见的有以下几种方法:\n\n\n# 1、工具分析\n\n这个工具比较多,比如说jprofiler、yourkit、java visualvm和netbeans profiler。他可以帮助我们分析是哪一个对象或者是类内存的飙升。也可以看到内存cpu的等等各种情况。上面多次演示到了。\n\n\n# 2、垃圾回收分析\n\n这个其实也可以用工具进行分析。上面的visualvm中,可以打印堆。也可以从外部导入dump文件进行分析。\n\n如果不用工具的话,我们可以通过ide看到。jvm配置添加-verbose:gc。然后就会打印出相关信息。下面这张图非原创,来自baeldung。\n\n\n# 3、基准测试\n\n也就是使用科学的方式进行分析java代码的性能。进而判断分析。\n\n\n# 四、结论\n\n内存泄漏是个很严重的问题,也比较常见。\n\n最主要的原因是动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。因此良好的代码规范,可以有效地避免这些错误。\n\n参考: 什么是内存泄漏?该如何检测?又该如何解决?", - "charsets": { + "headersStr": "G1特有数据结构和算法 Region SATB算法 三色标记法 RSet 垃圾回收过程 与CMS相比的优势 CMS 什么是CMS? CMS有什么用? CMS如何执行?", + "content": "# G1特有数据结构和算法\n\n\n# Region\n\n堆仍然有新生代(eden、survivor)、老年代的划分,但是不再要求它们是内存连续的。每个区都由多个Region组成。 部分老年代Region存储Humongous对象(即下图的H),这种对象大小大于等于Region的一半。\n\n(图片来源-Java Hotspot G1 GC的一些关键技术)\n\n\n# SATB算法\n\n全称Snapshot-At-The-Beginning,起始时活对象的快照。在理解SATB前需要先了解以下知识。\n\n\n# 三色标记法\n\nCMS和G1的算法都是通过对gc root 进行遍历,并进行三色标记。标记规则为\n\n黑色(black): 节点被遍历完成,而且子节点都遍历完成。 灰色(gray): 当前正在遍历的节点,而且子节点(即对象的域)还没有遍历。遍历完所有子节点后,将成为黑色 白色(white): 还没有遍历到的节点,即灰色节点的子节点。扫描结束仍是白色时会被回收。 并发扫描时,对于白色有两种情况同时发生时,可能会漏标导致被误回收:\n\n增加了被黑色引用的关系。 被灰色下应用,删除了到它的引用 具体执行过程:https://www.cnblogs.com/javaadu/p/10713956.html\n\n按照R大的说法:CMS的incremental update设计使得它在remark阶段必须重新扫描所有线程栈和整个young gen作为root;G1的SATB设计在remark阶段则只需要扫描剩下的satb_mark_queue。\n\n\n# RSet\n\n全称Remember Set,记录一个Region里的对象被哪些其他Region引用。 相对应地,有另一种辅助数据结构Collection Set(CSet),它记录了GC要收集的Region集合。GC时只需扫描CSet中各个Rset即可。\n\n(Tips for Tuning the Garbage First Garbage Collector)\n\n更详细的访问机制和回收过程这里不再展开,有兴趣可以参考后文引用文献。\n\nPause Prediction Model 暂停预测模型,G1根据它计算出的历史数据来预测本次收集需要选择的Region数量,从而尽量满足用户设定的目标停顿时间。 具体算法和公式略,可见Java Hotspot G1 GC的一些关键技术\n\n\n# 垃圾回收过程\n\n分为以下几步:\n\n初始标记(Initial Mark)—— 标记GC root能直接关联的对象(短暂STW) 并发标记(Concurrent mark)—— GCRootsTracing,从并发标记中的root遍历,对不可达的对象进行标记,耗时长但可并行 最终标记(Final Remark)—— 收集并发标记期间产生的新垃圾(短暂STW),采用了SATB算法比CMS更快 筛选回收(Live Data Counting and Evacuation)—— 对各个Region的回收性价比排序,在保证时间可控的情况下清除失活对象,清除Remember Sets 作为对比,CMS的回收过程\n\n初始标记(CMS Initial Mark)—— 标记GC root能直接关联的对象(短暂STW) 并发标记(CMS Concurrent Mark)—— GCRootsTracing,从并发标记中的root遍历,对不可达的对象进行标记 重新标记(CMS Remark)—— 修正并发标记期间因为用户操作导致标记发生变更的对象,有STW 并发清除(CMS Concurrent Sweep)\n\n\n# 与CMS相比的优势\n\n并发度更高,充分利用CPU多线程 —— CMS对CPU资源敏感,需要占用25%的线程,如果核数小于4更会占用一半的资源。 整体上是标记-整理(分代),局部是复制(分Region),运行期不产生碎片 —— CMS是标记-清除,会产生空间碎片和本次回收期间产生导致本次无法回收的浮动垃圾 可预测的停顿(基于Region)\n\n\n# CMS\n\n\n# 什么是CMS?\n\nConcurrent Mark Sweep。\n\n看名字就知道,CMS是一款并发、使用标记-清除算法的gc。\n\nCMS是针对老年代进行回收的GC。\n\n\n# CMS有什么用?\n\nCMS以获取最小停顿时间为目的。\n\n在一些对响应时间有很高要求的应用或网站中,用户程序不能有长时间的停顿,CMS 可以用于此场景。\n\n\n# CMS如何执行?\n\n总体来说CMS的执行过程可以分为以下几个阶段:\n\n 3.1 初始标记(STW)\n\n 3.2 并发标记\n\n 3.3 并发预清理\n\n 3.4 重标记(STW)\n\n 3.5 并发清理\n\n 3.6 重置\n\n\n参考: 关于GC(下):CMS和G1GC的比较 详解CMS垃圾回收机制", + "normalizedContent": "# g1特有数据结构和算法\n\n\n# region\n\n堆仍然有新生代(eden、survivor)、老年代的划分,但是不再要求它们是内存连续的。每个区都由多个region组成。 部分老年代region存储humongous对象(即下图的h),这种对象大小大于等于region的一半。\n\n(图片来源-java hotspot g1 gc的一些关键技术)\n\n\n# satb算法\n\n全称snapshot-at-the-beginning,起始时活对象的快照。在理解satb前需要先了解以下知识。\n\n\n# 三色标记法\n\ncms和g1的算法都是通过对gc root 进行遍历,并进行三色标记。标记规则为\n\n黑色(black): 节点被遍历完成,而且子节点都遍历完成。 灰色(gray): 当前正在遍历的节点,而且子节点(即对象的域)还没有遍历。遍历完所有子节点后,将成为黑色 白色(white): 还没有遍历到的节点,即灰色节点的子节点。扫描结束仍是白色时会被回收。 并发扫描时,对于白色有两种情况同时发生时,可能会漏标导致被误回收:\n\n增加了被黑色引用的关系。 被灰色下应用,删除了到它的引用 具体执行过程:https://www.cnblogs.com/javaadu/p/10713956.html\n\n按照r大的说法:cms的incremental update设计使得它在remark阶段必须重新扫描所有线程栈和整个young gen作为root;g1的satb设计在remark阶段则只需要扫描剩下的satb_mark_queue。\n\n\n# rset\n\n全称remember set,记录一个region里的对象被哪些其他region引用。 相对应地,有另一种辅助数据结构collection set(cset),它记录了gc要收集的region集合。gc时只需扫描cset中各个rset即可。\n\n(tips for tuning the garbage first garbage collector)\n\n更详细的访问机制和回收过程这里不再展开,有兴趣可以参考后文引用文献。\n\npause prediction model 暂停预测模型,g1根据它计算出的历史数据来预测本次收集需要选择的region数量,从而尽量满足用户设定的目标停顿时间。 具体算法和公式略,可见java hotspot g1 gc的一些关键技术\n\n\n# 垃圾回收过程\n\n分为以下几步:\n\n初始标记(initial mark)—— 标记gc root能直接关联的对象(短暂stw) 并发标记(concurrent mark)—— gcrootstracing,从并发标记中的root遍历,对不可达的对象进行标记,耗时长但可并行 最终标记(final remark)—— 收集并发标记期间产生的新垃圾(短暂stw),采用了satb算法比cms更快 筛选回收(live data counting and evacuation)—— 对各个region的回收性价比排序,在保证时间可控的情况下清除失活对象,清除remember sets 作为对比,cms的回收过程\n\n初始标记(cms initial mark)—— 标记gc root能直接关联的对象(短暂stw) 并发标记(cms concurrent mark)—— gcrootstracing,从并发标记中的root遍历,对不可达的对象进行标记 重新标记(cms remark)—— 修正并发标记期间因为用户操作导致标记发生变更的对象,有stw 并发清除(cms concurrent sweep)\n\n\n# 与cms相比的优势\n\n并发度更高,充分利用cpu多线程 —— cms对cpu资源敏感,需要占用25%的线程,如果核数小于4更会占用一半的资源。 整体上是标记-整理(分代),局部是复制(分region),运行期不产生碎片 —— cms是标记-清除,会产生空间碎片和本次回收期间产生导致本次无法回收的浮动垃圾 可预测的停顿(基于region)\n\n\n# cms\n\n\n# 什么是cms?\n\nconcurrent mark sweep。\n\n看名字就知道,cms是一款并发、使用标记-清除算法的gc。\n\ncms是针对老年代进行回收的gc。\n\n\n# cms有什么用?\n\ncms以获取最小停顿时间为目的。\n\n在一些对响应时间有很高要求的应用或网站中,用户程序不能有长时间的停顿,cms 可以用于此场景。\n\n\n# cms如何执行?\n\n总体来说cms的执行过程可以分为以下几个阶段:\n\n 3.1 初始标记(stw)\n\n 3.2 并发标记\n\n 3.3 并发预清理\n\n 3.4 重标记(stw)\n\n 3.5 并发清理\n\n 3.6 重置\n\n\n参考: 关于gc(下):cms和g1gc的比较 详解cms垃圾回收机制", + "charsets": { "cjk": true }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 + "lastUpdated": "2022/09/18, 21:28:02", + "lastUpdatedTimestamp": 1663507682000 }, { "title": "JVM 是怎么去调优的?了解哪些参数和指令?", @@ -2515,157 +2583,264 @@ export const siteData = { "charsets": { "cjk": true }, + "lastUpdated": "2022/09/18, 21:28:02", + "lastUpdatedTimestamp": 1663507682000 + }, + { + "title": "代理模式", + "frontmatter": { + "title": "代理模式", + "date": "2022-05-21T17:28:27.000Z", + "permalink": "/pages/dd3a6f/", + "categories": [ + "Java相关", + "设计模式" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/020.%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F.html", + "relativePath": "01.Java相关/40.设计模式/020.代理模式.md", + "key": "v-0564ba27", + "path": "/pages/dd3a6f/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述 CMS 与 G1 机制的区别", + "title": "什么是设计模式", "frontmatter": { - "title": "简述 CMS 与 G1 机制的区别", - "date": "2022-05-21T17:33:40.000Z", - "permalink": "/pages/be69af/", + "title": "什么是设计模式", + "date": "2022-05-21T17:26:19.000Z", + "permalink": "/pages/3d768c/", "categories": [ "Java相关", - "JVM" + "设计模式" ], "tags": [ null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/070.%E7%AE%80%E8%BF%B0%20CMS%20%E4%B8%8E%20G1%20%E6%9C%BA%E5%88%B6%E7%9A%84%E5%8C%BA%E5%88%AB.html", - "relativePath": "01.Java相关/30.JVM/070.简述 CMS 与 G1 机制的区别.md", - "key": "v-28c1d6d9", - "path": "/pages/be69af/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/003.%E4%BB%80%E4%B9%88%E6%98%AF%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20.html", + "relativePath": "01.Java相关/40.设计模式/003.什么是设计模式 .md", + "key": "v-88b087a6", + "path": "/pages/3d768c/", "headers": [ { "level": 2, - "title": "G1特有数据结构和算法", - "slug": "g1特有数据结构和算法", - "normalizedTitle": "g1特有数据结构和算法", + "title": "什么是设计模式?", + "slug": "什么是设计模式", + "normalizedTitle": "什么是设计模式?", "charIndex": 2 }, { - "level": 3, - "title": "Region", - "slug": "region", - "normalizedTitle": "region", - "charIndex": 18 - }, - { - "level": 3, - "title": "SATB算法", - "slug": "satb算法", - "normalizedTitle": "satb算法", - "charIndex": 179 + "level": 2, + "title": "设计模式之六大原则", + "slug": "设计模式之六大原则", + "normalizedTitle": "设计模式之六大原则", + "charIndex": 286 }, { "level": 3, - "title": "三色标记法", - "slug": "三色标记法", - "normalizedTitle": "三色标记法", - "charIndex": 248 + "title": "开闭原则(Open Close Principle)", + "slug": "开闭原则-open-close-principle", + "normalizedTitle": "开闭原则(open close principle)", + "charIndex": 300 }, { "level": 3, - "title": "RSet", - "slug": "rset", - "normalizedTitle": "rset", - "charIndex": 670 + "title": "里氏代换原则(Liskov Substitution Principle)", + "slug": "里氏代换原则-liskov-substitution-principle", + "normalizedTitle": "里氏代换原则(liskov substitution principle)", + "charIndex": 628 }, { "level": 3, - "title": "垃圾回收过程", - "slug": "垃圾回收过程", - "normalizedTitle": "垃圾回收过程", - "charIndex": 1013 + "title": "依赖倒转原则(Dependence Inversion Principle)", + "slug": "依赖倒转原则-dependence-inversion-principle", + "normalizedTitle": "依赖倒转原则(dependence inversion principle)", + "charIndex": 861 }, { "level": 3, - "title": "与CMS相比的优势", - "slug": "与cms相比的优势", - "normalizedTitle": "与cms相比的优势", - "charIndex": 1517 - }, - { - "level": 2, - "title": "CMS", - "slug": "cms", - "normalizedTitle": "cms", - "charIndex": 255 + "title": "接口隔离原则(Interface Segregation Principle)", + "slug": "接口隔离原则-interface-segregation-principle", + "normalizedTitle": "接口隔离原则(interface segregation principle)", + "charIndex": 1024 }, { "level": 3, - "title": "什么是CMS?", - "slug": "什么是cms", - "normalizedTitle": "什么是cms?", - "charIndex": 1699 + "title": "迪米特法则(最少知道原则)(Demeter Principle)", + "slug": "迪米特法则-最少知道原则-demeter-principle", + "normalizedTitle": "迪米特法则(最少知道原则)(demeter principle)", + "charIndex": 1138 }, { "level": 3, - "title": "CMS有什么用?", - "slug": "cms有什么用", - "normalizedTitle": "cms有什么用?", - "charIndex": 1785 + "title": "单一职责原则( Single responsibility principle )", + "slug": "单一职责原则-single-responsibility-principle", + "normalizedTitle": "单一职责原则( single responsibility principle )", + "charIndex": 1403 }, { - "level": 3, - "title": "CMS如何执行?", - "slug": "cms如何执行", - "normalizedTitle": "cms如何执行?", - "charIndex": 1865 + "level": 2, + "title": "高内聚,低耦合?", + "slug": "高内聚-低耦合", + "normalizedTitle": "高内聚,低耦合?", + "charIndex": 1768 } ], - "headersStr": "G1特有数据结构和算法 Region SATB算法 三色标记法 RSet 垃圾回收过程 与CMS相比的优势 CMS 什么是CMS? CMS有什么用? CMS如何执行?", - "content": "# G1特有数据结构和算法\n\n\n# Region\n\n堆仍然有新生代(eden、survivor)、老年代的划分,但是不再要求它们是内存连续的。每个区都由多个Region组成。 部分老年代Region存储Humongous对象(即下图的H),这种对象大小大于等于Region的一半。\n\n(图片来源-Java Hotspot G1 GC的一些关键技术)\n\n\n# SATB算法\n\n全称Snapshot-At-The-Beginning,起始时活对象的快照。在理解SATB前需要先了解以下知识。\n\n\n# 三色标记法\n\nCMS和G1的算法都是通过对gc root 进行遍历,并进行三色标记。标记规则为\n\n黑色(black): 节点被遍历完成,而且子节点都遍历完成。 灰色(gray): 当前正在遍历的节点,而且子节点(即对象的域)还没有遍历。遍历完所有子节点后,将成为黑色 白色(white): 还没有遍历到的节点,即灰色节点的子节点。扫描结束仍是白色时会被回收。 并发扫描时,对于白色有两种情况同时发生时,可能会漏标导致被误回收:\n\n增加了被黑色引用的关系。 被灰色下应用,删除了到它的引用 具体执行过程:https://www.cnblogs.com/javaadu/p/10713956.html\n\n按照R大的说法:CMS的incremental update设计使得它在remark阶段必须重新扫描所有线程栈和整个young gen作为root;G1的SATB设计在remark阶段则只需要扫描剩下的satb_mark_queue。\n\n\n# RSet\n\n全称Remember Set,记录一个Region里的对象被哪些其他Region引用。 相对应地,有另一种辅助数据结构Collection Set(CSet),它记录了GC要收集的Region集合。GC时只需扫描CSet中各个Rset即可。\n\n(Tips for Tuning the Garbage First Garbage Collector)\n\n更详细的访问机制和回收过程这里不再展开,有兴趣可以参考后文引用文献。\n\nPause Prediction Model 暂停预测模型,G1根据它计算出的历史数据来预测本次收集需要选择的Region数量,从而尽量满足用户设定的目标停顿时间。 具体算法和公式略,可见Java Hotspot G1 GC的一些关键技术\n\n\n# 垃圾回收过程\n\n分为以下几步:\n\n初始标记(Initial Mark)—— 标记GC root能直接关联的对象(短暂STW) 并发标记(Concurrent mark)—— GCRootsTracing,从并发标记中的root遍历,对不可达的对象进行标记,耗时长但可并行 最终标记(Final Remark)—— 收集并发标记期间产生的新垃圾(短暂STW),采用了SATB算法比CMS更快 筛选回收(Live Data Counting and Evacuation)—— 对各个Region的回收性价比排序,在保证时间可控的情况下清除失活对象,清除Remember Sets 作为对比,CMS的回收过程\n\n初始标记(CMS Initial Mark)—— 标记GC root能直接关联的对象(短暂STW) 并发标记(CMS Concurrent Mark)—— GCRootsTracing,从并发标记中的root遍历,对不可达的对象进行标记 重新标记(CMS Remark)—— 修正并发标记期间因为用户操作导致标记发生变更的对象,有STW 并发清除(CMS Concurrent Sweep)\n\n\n# 与CMS相比的优势\n\n并发度更高,充分利用CPU多线程 —— CMS对CPU资源敏感,需要占用25%的线程,如果核数小于4更会占用一半的资源。 整体上是标记-整理(分代),局部是复制(分Region),运行期不产生碎片 —— CMS是标记-清除,会产生空间碎片和本次回收期间产生导致本次无法回收的浮动垃圾 可预测的停顿(基于Region)\n\n\n# CMS\n\n\n# 什么是CMS?\n\nConcurrent Mark Sweep。\n\n看名字就知道,CMS是一款并发、使用标记-清除算法的gc。\n\nCMS是针对老年代进行回收的GC。\n\n\n# CMS有什么用?\n\nCMS以获取最小停顿时间为目的。\n\n在一些对响应时间有很高要求的应用或网站中,用户程序不能有长时间的停顿,CMS 可以用于此场景。\n\n\n# CMS如何执行?\n\n总体来说CMS的执行过程可以分为以下几个阶段:\n\n 3.1 初始标记(STW)\n\n 3.2 并发标记\n\n 3.3 并发预清理\n\n 3.4 重标记(STW)\n\n 3.5 并发清理\n\n 3.6 重置\n\n\n参考: 关于GC(下):CMS和G1GC的比较 详解CMS垃圾回收机制", - "normalizedContent": "# g1特有数据结构和算法\n\n\n# region\n\n堆仍然有新生代(eden、survivor)、老年代的划分,但是不再要求它们是内存连续的。每个区都由多个region组成。 部分老年代region存储humongous对象(即下图的h),这种对象大小大于等于region的一半。\n\n(图片来源-java hotspot g1 gc的一些关键技术)\n\n\n# satb算法\n\n全称snapshot-at-the-beginning,起始时活对象的快照。在理解satb前需要先了解以下知识。\n\n\n# 三色标记法\n\ncms和g1的算法都是通过对gc root 进行遍历,并进行三色标记。标记规则为\n\n黑色(black): 节点被遍历完成,而且子节点都遍历完成。 灰色(gray): 当前正在遍历的节点,而且子节点(即对象的域)还没有遍历。遍历完所有子节点后,将成为黑色 白色(white): 还没有遍历到的节点,即灰色节点的子节点。扫描结束仍是白色时会被回收。 并发扫描时,对于白色有两种情况同时发生时,可能会漏标导致被误回收:\n\n增加了被黑色引用的关系。 被灰色下应用,删除了到它的引用 具体执行过程:https://www.cnblogs.com/javaadu/p/10713956.html\n\n按照r大的说法:cms的incremental update设计使得它在remark阶段必须重新扫描所有线程栈和整个young gen作为root;g1的satb设计在remark阶段则只需要扫描剩下的satb_mark_queue。\n\n\n# rset\n\n全称remember set,记录一个region里的对象被哪些其他region引用。 相对应地,有另一种辅助数据结构collection set(cset),它记录了gc要收集的region集合。gc时只需扫描cset中各个rset即可。\n\n(tips for tuning the garbage first garbage collector)\n\n更详细的访问机制和回收过程这里不再展开,有兴趣可以参考后文引用文献。\n\npause prediction model 暂停预测模型,g1根据它计算出的历史数据来预测本次收集需要选择的region数量,从而尽量满足用户设定的目标停顿时间。 具体算法和公式略,可见java hotspot g1 gc的一些关键技术\n\n\n# 垃圾回收过程\n\n分为以下几步:\n\n初始标记(initial mark)—— 标记gc root能直接关联的对象(短暂stw) 并发标记(concurrent mark)—— gcrootstracing,从并发标记中的root遍历,对不可达的对象进行标记,耗时长但可并行 最终标记(final remark)—— 收集并发标记期间产生的新垃圾(短暂stw),采用了satb算法比cms更快 筛选回收(live data counting and evacuation)—— 对各个region的回收性价比排序,在保证时间可控的情况下清除失活对象,清除remember sets 作为对比,cms的回收过程\n\n初始标记(cms initial mark)—— 标记gc root能直接关联的对象(短暂stw) 并发标记(cms concurrent mark)—— gcrootstracing,从并发标记中的root遍历,对不可达的对象进行标记 重新标记(cms remark)—— 修正并发标记期间因为用户操作导致标记发生变更的对象,有stw 并发清除(cms concurrent sweep)\n\n\n# 与cms相比的优势\n\n并发度更高,充分利用cpu多线程 —— cms对cpu资源敏感,需要占用25%的线程,如果核数小于4更会占用一半的资源。 整体上是标记-整理(分代),局部是复制(分region),运行期不产生碎片 —— cms是标记-清除,会产生空间碎片和本次回收期间产生导致本次无法回收的浮动垃圾 可预测的停顿(基于region)\n\n\n# cms\n\n\n# 什么是cms?\n\nconcurrent mark sweep。\n\n看名字就知道,cms是一款并发、使用标记-清除算法的gc。\n\ncms是针对老年代进行回收的gc。\n\n\n# cms有什么用?\n\ncms以获取最小停顿时间为目的。\n\n在一些对响应时间有很高要求的应用或网站中,用户程序不能有长时间的停顿,cms 可以用于此场景。\n\n\n# cms如何执行?\n\n总体来说cms的执行过程可以分为以下几个阶段:\n\n 3.1 初始标记(stw)\n\n 3.2 并发标记\n\n 3.3 并发预清理\n\n 3.4 重标记(stw)\n\n 3.5 并发清理\n\n 3.6 重置\n\n\n参考: 关于gc(下):cms和g1gc的比较 详解cms垃圾回收机制", + "headersStr": "什么是设计模式? 设计模式之六大原则 开闭原则(Open Close Principle) 里氏代换原则(Liskov Substitution Principle) 依赖倒转原则(Dependence Inversion Principle) 接口隔离原则(Interface Segregation Principle) 迪米特法则(最少知道原则)(Demeter Principle) 单一职责原则( Single responsibility principle ) 高内聚,低耦合?", + "content": "# 什么是设计模式?\n\n设计模式是一套代码设计「经验的总结」。项目中「合理的」运用设计模式可以「巧妙的解决很多问题」。\n\n * 经验的总结:抱着「代码虐我千百遍,我待代码如初恋」的心态,最终得出来的「套路」。\n\n * 合理的:要对设计模式的使用场景有一定的认识后才使用,「不要滥用」。如:输出一句“hello world”,非要强行给加上各种模式。 问:“为什么”,答:“总感觉少了模式!”。\n\n * 巧妙的解决了很多问题:被广泛应用的原因。\n\n> 为什么要提倡“Design Pattern呢?根本原因是为了代码复用,增加可维护性。那么怎么才能实现代码复用呢?\n\n\n# 设计模式之六大原则\n\n\n# 开闭原则(Open Close Principle)\n\n1988年,勃兰特·梅耶(Bertrand Meyer)在他的著作《面向对象软件构造(Object Oriented Software Construction)》中提出了开闭原则,它的原文是这样:“Software entities should be open for extension,but closed for modification”。 意思:软件模块应该对扩展开放,对修改关闭。 举例:在程序需要进行新增功能的时候,不能去修改原有的代码,而是新增代码,实现一个热插拔的效果(热插拔:灵活的去除或添加功能,不影响到原有的功能)。 目的:为了使程序的扩展性好,易于维护和升级。\n\n\n# 里氏代换原则(Liskov Substitution Principle)\n\n意思:里氏代换原则是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。 举例:球类,原本是一种体育用品,它的衍生类有篮球、足球、排球、羽毛球等等,如果衍生类替换了基类的原本方法,如把体育用品改成了食用品(那么软件单位的功能受到影响),就不符合里氏代换原则。 目的:对实现抽象化的具体步骤的规范。\n\n\n# 依赖倒转原则(Dependence Inversion Principle)\n\n意思:针对接口编程,而不是针对实现编程。 举例:以计算机系统为例,无论主板、CPU、内存、硬件都是在针对接口设计的,如果针对实现来设计,内存就要对应到针对某个品牌的主板,那么会出现换内存需要把主板也换掉的尴尬。 目的:降低模块间的耦合。\n\n\n# 接口隔离原则(Interface Segregation Principle)\n\n使用多个隔离的接口,比使用单个接口要好。 举例:比如:登录,注册时属于用户模块的两个接口,比写成一个接口好。 目的:提高程序设计灵活性。\n\n\n# 迪米特法则(最少知道原则)(Demeter Principle)\n\n1987年秋天由美国Northeastern University的Ian Holland提出,被UML的创始者之一[Booch]等普及。后来,因为在经典著作《 The Pragmatic Programmer》而广为人知。 意思:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。 举例:一个类公开的public属性或方法越多,修改时涉及的面也就越大,变更引起的风险扩散也就越大。 目的:降低类之间的耦合,减少对其他类的依赖。\n\n\n# 单一职责原则( Single responsibility principle )\n\n该原则由罗伯特·C·马丁(Robert C. Martin)于《敏捷软件开发:原则、模式和实践》一书中给出的。马丁表示此原则是基于汤姆·狄马克(Tom DeMarco)和Meilir Page-Jones的著作中的内聚性原则发展出的。 意思:一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。 举例:该原则意思简单到不需要举例! 目的:类的复杂性降低,可读性提高,可维护性提高。\n\n刚入行的时候,在想什么样的代码是好代码?看到很多前辈的文字都说好的代码要符合「高内聚,低耦合」,但是我听到这样的解释,是这样的\n\n而现在对设计模式有了一定程度上的学习,感觉懂了一些,小伙伴们你们学会了吗?\n\n\n# 高内聚,低耦合?\n\n内聚是从功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事。它描述的是模块内的功能联系; 耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及通过接口的数据。\n\n参考: 图解九种常见的设计模式 什么是「设计模式」?", + "normalizedContent": "# 什么是设计模式?\n\n设计模式是一套代码设计「经验的总结」。项目中「合理的」运用设计模式可以「巧妙的解决很多问题」。\n\n * 经验的总结:抱着「代码虐我千百遍,我待代码如初恋」的心态,最终得出来的「套路」。\n\n * 合理的:要对设计模式的使用场景有一定的认识后才使用,「不要滥用」。如:输出一句“hello world”,非要强行给加上各种模式。 问:“为什么”,答:“总感觉少了模式!”。\n\n * 巧妙的解决了很多问题:被广泛应用的原因。\n\n> 为什么要提倡“design pattern呢?根本原因是为了代码复用,增加可维护性。那么怎么才能实现代码复用呢?\n\n\n# 设计模式之六大原则\n\n\n# 开闭原则(open close principle)\n\n1988年,勃兰特·梅耶(bertrand meyer)在他的著作《面向对象软件构造(object oriented software construction)》中提出了开闭原则,它的原文是这样:“software entities should be open for extension,but closed for modification”。 意思:软件模块应该对扩展开放,对修改关闭。 举例:在程序需要进行新增功能的时候,不能去修改原有的代码,而是新增代码,实现一个热插拔的效果(热插拔:灵活的去除或添加功能,不影响到原有的功能)。 目的:为了使程序的扩展性好,易于维护和升级。\n\n\n# 里氏代换原则(liskov substitution principle)\n\n意思:里氏代换原则是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。 举例:球类,原本是一种体育用品,它的衍生类有篮球、足球、排球、羽毛球等等,如果衍生类替换了基类的原本方法,如把体育用品改成了食用品(那么软件单位的功能受到影响),就不符合里氏代换原则。 目的:对实现抽象化的具体步骤的规范。\n\n\n# 依赖倒转原则(dependence inversion principle)\n\n意思:针对接口编程,而不是针对实现编程。 举例:以计算机系统为例,无论主板、cpu、内存、硬件都是在针对接口设计的,如果针对实现来设计,内存就要对应到针对某个品牌的主板,那么会出现换内存需要把主板也换掉的尴尬。 目的:降低模块间的耦合。\n\n\n# 接口隔离原则(interface segregation principle)\n\n使用多个隔离的接口,比使用单个接口要好。 举例:比如:登录,注册时属于用户模块的两个接口,比写成一个接口好。 目的:提高程序设计灵活性。\n\n\n# 迪米特法则(最少知道原则)(demeter principle)\n\n1987年秋天由美国northeastern university的ian holland提出,被uml的创始者之一[booch]等普及。后来,因为在经典著作《 the pragmatic programmer》而广为人知。 意思:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。 举例:一个类公开的public属性或方法越多,修改时涉及的面也就越大,变更引起的风险扩散也就越大。 目的:降低类之间的耦合,减少对其他类的依赖。\n\n\n# 单一职责原则( single responsibility principle )\n\n该原则由罗伯特·c·马丁(robert c. martin)于《敏捷软件开发:原则、模式和实践》一书中给出的。马丁表示此原则是基于汤姆·狄马克(tom demarco)和meilir page-jones的著作中的内聚性原则发展出的。 意思:一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。 举例:该原则意思简单到不需要举例! 目的:类的复杂性降低,可读性提高,可维护性提高。\n\n刚入行的时候,在想什么样的代码是好代码?看到很多前辈的文字都说好的代码要符合「高内聚,低耦合」,但是我听到这样的解释,是这样的\n\n而现在对设计模式有了一定程度上的学习,感觉懂了一些,小伙伴们你们学会了吗?\n\n\n# 高内聚,低耦合?\n\n内聚是从功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事。它描述的是模块内的功能联系; 耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及通过接口的数据。\n\n参考: 图解九种常见的设计模式 什么是「设计模式」?", "charsets": { "cjk": true - }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 + } }, { - "title": "实现单例设计模式(懒汉,饿汉)", + "title": "什么是内存泄漏,怎么确定内存泄漏?", "frontmatter": { - "title": "实现单例设计模式(懒汉,饿汉)", - "date": "2022-05-21T17:26:00.000Z", - "permalink": "/pages/b37ad7/", + "title": "什么是内存泄漏,怎么确定内存泄漏?", + "date": "2022-05-21T17:33:27.000Z", + "permalink": "/pages/3ab5f5/", "categories": [ "Java相关", - "设计模式" + "JVM" ], "tags": [ null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/002.%E5%AE%9E%E7%8E%B0%E5%8D%95%E4%BE%8B%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%EF%BC%88%E6%87%92%E6%B1%89%EF%BC%8C%E9%A5%BF%E6%B1%89%EF%BC%89.html", - "relativePath": "01.Java相关/40.设计模式/002.实现单例设计模式(懒汉,饿汉).md", - "key": "v-ba94c306", - "path": "/pages/b37ad7/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/060.%E4%BB%80%E4%B9%88%E6%98%AF%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%EF%BC%8C%E6%80%8E%E4%B9%88%E7%A1%AE%E5%AE%9A%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%EF%BC%9F.html", + "relativePath": "01.Java相关/30.JVM/060.什么是内存泄漏,怎么确定内存泄漏?.md", + "key": "v-15fba8f1", + "path": "/pages/3ab5f5/", "headers": [ { "level": 2, - "title": "饿汉式", - "slug": "饿汉式", - "normalizedTitle": "饿汉式", - "charIndex": 311 + "title": "前言", + "slug": "前言", + "normalizedTitle": "前言", + "charIndex": 2 + }, + { + "level": 2, + "title": "一、介绍", + "slug": "一、介绍", + "normalizedTitle": "一、介绍", + "charIndex": 178 + }, + { + "level": 3, + "title": "1、什么是内存泄漏", + "slug": "_1、什么是内存泄漏", + "normalizedTitle": "1、什么是内存泄漏", + "charIndex": 187 + }, + { + "level": 3, + "title": "2、内存泄漏的危害", + "slug": "_2、内存泄漏的危害", + "normalizedTitle": "2、内存泄漏的危害", + "charIndex": 404 + }, + { + "level": 2, + "title": "二、内存泄漏原因", + "slug": "二、内存泄漏原因", + "normalizedTitle": "二、内存泄漏原因", + "charIndex": 491 + }, + { + "level": 3, + "title": "1、大量使用静态变量", + "slug": "_1、大量使用静态变量", + "normalizedTitle": "1、大量使用静态变量", + "charIndex": 583 + }, + { + "level": 3, + "title": "2、连接资源未关闭", + "slug": "_2、连接资源未关闭", + "normalizedTitle": "2、连接资源未关闭", + "charIndex": 1310 + }, + { + "level": 3, + "title": "4、内部类持有外部类", + "slug": "_4、内部类持有外部类", + "normalizedTitle": "4、内部类持有外部类", + "charIndex": 2418 + }, + { + "level": 3, + "title": "5、finalize方法", + "slug": "_5、finalize方法", + "normalizedTitle": "5、finalize方法", + "charIndex": 2445 + }, + { + "level": 3, + "title": "6、ThreadLocal的错误使用", + "slug": "_6、threadlocal的错误使用", + "normalizedTitle": "6、threadlocal的错误使用", + "charIndex": 2595 + }, + { + "level": 2, + "title": "三、检测内存泄漏", + "slug": "三、检测内存泄漏", + "normalizedTitle": "三、检测内存泄漏", + "charIndex": 3062 + }, + { + "level": 3, + "title": "1、工具分析", + "slug": "_1、工具分析", + "normalizedTitle": "1、工具分析", + "charIndex": 3106 + }, + { + "level": 3, + "title": "2、垃圾回收分析", + "slug": "_2、垃圾回收分析", + "normalizedTitle": "2、垃圾回收分析", + "charIndex": 3232 + }, + { + "level": 3, + "title": "3、基准测试", + "slug": "_3、基准测试", + "normalizedTitle": "3、基准测试", + "charIndex": 3374 + }, + { + "level": 2, + "title": "四、结论", + "slug": "四、结论", + "normalizedTitle": "四、结论", + "charIndex": 3418 } ], - "headersStr": "饿汉式", - "content": "> 面试官:带笔了吧,那写两种单例模式的实现方法吧 沙沙沙刷刷刷~~~ 写好了 面试官:怎样防止new 对象出来?\n\n有一些对象我们确实只需要一个,比如,线程池、数据库连接、缓存、日志对象等,如果有多个的话,会造成程序的行为异常,资源使用过量或者不一致的问题。你也许会说,这种我用全局变量不也能实现吗,还整个单例模式,好像你很流弊的样子,如果将对象赋值给一个全局变量,那程序启动就会创建好对象,万一这个对象很耗资源,我们还可能在某些时候用不到,这就造成了资源的浪费,不合理,所以就有了单例模式。\n\n#单例模式的定义 单例模式确保一个类只有一个实例,并提供一个全局唯一访问点\n\n#单例模式的类图\n\n#单例模式的实现\n\n\n# 饿汉式\n\nstatic 变量在类装载的时候进行初始化 多个实例的 static 变量会共享同一块内存区域 用这两个知识点写出的单例类就是饿汉式了,初始化类的时候就创建,饥不择食,饿汉\n\npublic class Singleton {\n\n //构造私有化,防止直接new\n private Singleton(){}\n\n //静态初始化器(static initializer)中创建实例,保证线程安全\n private static Singleton instance = new Singleton();\n\n public static Singleton getInstance(){\n return instance;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n饿汉式是线程安全的,JVM在加载类时马上创建唯一的实例对象,且只会装载一次。\n\nJava 实现的单例是一个虚拟机的范围,因为装载类的功能是虚拟机的,所以一个虚拟机通过自己的ClassLoader 装载饿汉式实现单例类的时候就会创建一个类实例。(如果一个虚拟机里有多个ClassLoader的话,就会有多个实例)\n\n##懒汉式 懒汉式,就是实例在用到的时候才去创建,比较“懒”\n\n单例模式的懒汉式实现方式体现了延迟加载的思想(延迟加载也称懒加载Lazy Load,就是一开始不要加载资源或数据,等到要使用的时候才加载)\n\n同步方法\n\npublic class Singleton {\n private static Singleton singleton;\n\n private Singleton(){}\n\n \t//解决了线程不安全问题,但是效率太低了,每个线程想获得类的实例的时候,都需要同步方法,不推荐\n public static synchronized Singleton getInstance(){\n if(singleton == null){\n singleton = new Singleton();\n }\n return singleton;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n双重检查加锁\n\npublic class Singleton {\n\n \t//volatitle关键词确保,多线程正确处理singleton\n private static volatile Singleton singleton;\n \n private Singleton(){}\n \n public static Singleton getInstance(){\n if(singleton ==null){\n synchronized (Singleton.class){\n if(singleton == null){\n singleton = new Singleton();\n }\n }\n }\n return singleton;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n\n\nDouble-Check 概念(进行两次检查)是多线程开发中经常使用的,为什么需要双重检查锁呢?因为第一次检查是确保之前是一个空对象,而非空对象就不需要同步了,空对象的线程然后进入同步代码块,如果不加第二次空对象检查,两个线程同时获取同步代码块,一个线程进入同步代码块,另一个线程就会等待,而这两个线程就会创建两个实例化对象,所以需要在线程进入同步代码块后再次进行空对象检查,才能确保只创建一个实例化对象。\n\n双重检查加锁(double checked locking)线程安全、延迟加载、效率比较高\n\nvolatile:volatile一般用于多线程的可见性,这里用来防止指令重排(防止new Singleton时指令重排序导致其他线程获取到未初始化完的对象)。被volatile 修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。\n\n指令重排\n\n指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序。\n\nJava中创建一个对象,往往包含三个过程。对于singleton = new Singleton(),这不是一个原子操作,在 JVM 中包含如下三个过程。\n\n给 singleton 分配内存\n\n调用 Singleton 的构造函数来初始化成员变量,形成实例\n\n将 singleton 对象指向分配的内存空间(执行完这步 singleton才是非 null 了)\n\n但是,由于JVM会进行指令重排序,所以上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3,也可能是 1-3-2。\n\n如果是 1-3-2,则在 3 执行完毕,2 未执行之前,被另一个线程抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以这个线程会直接返回 instance,然后使用,那肯定就会报错了,所以要加入 volatile关键字。\n\n##静态内部类\n\npublic class Singleton {\n\n private Singleton(){}\n\n private static class SingletonInstance{\n private static final Singleton INSTANCE = new Singleton();\n }\n \n public static Singleton getInstance(){\n return SingletonInstance.INSTANCE;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n采用类加载的机制来保证初始化实例时只有一个线程;\n\n静态内部类方式在Singleton 类被装载的时候并不会立即实例化,而是在调用getInstance的时候,才去装载内部类SingletonInstance ,从而完成Singleton的实例化\n\n类的静态属性只会在第一次加载类的时候初始化,所以,JVM帮我们保证了线程的安全性,在类初始化时,其他线程无法进入\n\n优点:线程安全,利用静态内部类实现延迟加载,效率较高,推荐使用\n\n##枚举\n\nenum Singleton{\n INSTANCE;\n public void method(){}\n}\n\n\n1\n2\n3\n4\n\n\n借助JDK5 添加的枚举实现单例,不仅可以避免多线程同步问题,还能防止反序列化重新创建新的对象,但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。不过,由于Java1.5中才加入enum特性,所以使用的人并不多。\n\n这种方式是《Effective Java》 作者Josh Bloch 提倡的方式。\n\n#单例模式在JDK 中的源码分析 JDK 中,java.lang.Runtime 就是经典的单例模式(饿汉式)\n\n#单例模式注意事项和细节 单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使 用new 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或 耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数 据库或文件的对象(比如数据源、session工厂等)", - "normalizedContent": "> 面试官:带笔了吧,那写两种单例模式的实现方法吧 沙沙沙刷刷刷~~~ 写好了 面试官:怎样防止new 对象出来?\n\n有一些对象我们确实只需要一个,比如,线程池、数据库连接、缓存、日志对象等,如果有多个的话,会造成程序的行为异常,资源使用过量或者不一致的问题。你也许会说,这种我用全局变量不也能实现吗,还整个单例模式,好像你很流弊的样子,如果将对象赋值给一个全局变量,那程序启动就会创建好对象,万一这个对象很耗资源,我们还可能在某些时候用不到,这就造成了资源的浪费,不合理,所以就有了单例模式。\n\n#单例模式的定义 单例模式确保一个类只有一个实例,并提供一个全局唯一访问点\n\n#单例模式的类图\n\n#单例模式的实现\n\n\n# 饿汉式\n\nstatic 变量在类装载的时候进行初始化 多个实例的 static 变量会共享同一块内存区域 用这两个知识点写出的单例类就是饿汉式了,初始化类的时候就创建,饥不择食,饿汉\n\npublic class singleton {\n\n //构造私有化,防止直接new\n private singleton(){}\n\n //静态初始化器(static initializer)中创建实例,保证线程安全\n private static singleton instance = new singleton();\n\n public static singleton getinstance(){\n return instance;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n饿汉式是线程安全的,jvm在加载类时马上创建唯一的实例对象,且只会装载一次。\n\njava 实现的单例是一个虚拟机的范围,因为装载类的功能是虚拟机的,所以一个虚拟机通过自己的classloader 装载饿汉式实现单例类的时候就会创建一个类实例。(如果一个虚拟机里有多个classloader的话,就会有多个实例)\n\n##懒汉式 懒汉式,就是实例在用到的时候才去创建,比较“懒”\n\n单例模式的懒汉式实现方式体现了延迟加载的思想(延迟加载也称懒加载lazy load,就是一开始不要加载资源或数据,等到要使用的时候才加载)\n\n同步方法\n\npublic class singleton {\n private static singleton singleton;\n\n private singleton(){}\n\n \t//解决了线程不安全问题,但是效率太低了,每个线程想获得类的实例的时候,都需要同步方法,不推荐\n public static synchronized singleton getinstance(){\n if(singleton == null){\n singleton = new singleton();\n }\n return singleton;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n双重检查加锁\n\npublic class singleton {\n\n \t//volatitle关键词确保,多线程正确处理singleton\n private static volatile singleton singleton;\n \n private singleton(){}\n \n public static singleton getinstance(){\n if(singleton ==null){\n synchronized (singleton.class){\n if(singleton == null){\n singleton = new singleton();\n }\n }\n }\n return singleton;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n\n\ndouble-check 概念(进行两次检查)是多线程开发中经常使用的,为什么需要双重检查锁呢?因为第一次检查是确保之前是一个空对象,而非空对象就不需要同步了,空对象的线程然后进入同步代码块,如果不加第二次空对象检查,两个线程同时获取同步代码块,一个线程进入同步代码块,另一个线程就会等待,而这两个线程就会创建两个实例化对象,所以需要在线程进入同步代码块后再次进行空对象检查,才能确保只创建一个实例化对象。\n\n双重检查加锁(double checked locking)线程安全、延迟加载、效率比较高\n\nvolatile:volatile一般用于多线程的可见性,这里用来防止指令重排(防止new singleton时指令重排序导致其他线程获取到未初始化完的对象)。被volatile 修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。\n\n指令重排\n\n指令重排是指在程序执行过程中, 为了性能考虑, 编译器和cpu可能会对指令重新排序。\n\njava中创建一个对象,往往包含三个过程。对于singleton = new singleton(),这不是一个原子操作,在 jvm 中包含如下三个过程。\n\n给 singleton 分配内存\n\n调用 singleton 的构造函数来初始化成员变量,形成实例\n\n将 singleton 对象指向分配的内存空间(执行完这步 singleton才是非 null 了)\n\n但是,由于jvm会进行指令重排序,所以上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3,也可能是 1-3-2。\n\n如果是 1-3-2,则在 3 执行完毕,2 未执行之前,被另一个线程抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以这个线程会直接返回 instance,然后使用,那肯定就会报错了,所以要加入 volatile关键字。\n\n##静态内部类\n\npublic class singleton {\n\n private singleton(){}\n\n private static class singletoninstance{\n private static final singleton instance = new singleton();\n }\n \n public static singleton getinstance(){\n return singletoninstance.instance;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n采用类加载的机制来保证初始化实例时只有一个线程;\n\n静态内部类方式在singleton 类被装载的时候并不会立即实例化,而是在调用getinstance的时候,才去装载内部类singletoninstance ,从而完成singleton的实例化\n\n类的静态属性只会在第一次加载类的时候初始化,所以,jvm帮我们保证了线程的安全性,在类初始化时,其他线程无法进入\n\n优点:线程安全,利用静态内部类实现延迟加载,效率较高,推荐使用\n\n##枚举\n\nenum singleton{\n instance;\n public void method(){}\n}\n\n\n1\n2\n3\n4\n\n\n借助jdk5 添加的枚举实现单例,不仅可以避免多线程同步问题,还能防止反序列化重新创建新的对象,但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。不过,由于java1.5中才加入enum特性,所以使用的人并不多。\n\n这种方式是《effective java》 作者josh bloch 提倡的方式。\n\n#单例模式在jdk 中的源码分析 jdk 中,java.lang.runtime 就是经典的单例模式(饿汉式)\n\n#单例模式注意事项和细节 单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使 用new 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或 耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数 据库或文件的对象(比如数据源、session工厂等)", + "headersStr": "前言 一、介绍 1、什么是内存泄漏 2、内存泄漏的危害 二、内存泄漏原因 1、大量使用静态变量 2、连接资源未关闭 4、内部类持有外部类 5、finalize方法 6、ThreadLocal的错误使用 三、检测内存泄漏 1、工具分析 2、垃圾回收分析 3、基准测试 四、结论", + "content": "# 前言\n\n这个问题是我之前翻看面经的时候见到的。那位小姐姐把内存泄漏当成了内存溢出问题去解答的,结果当场挂掉了。为此总结一下,之前和一位老哥也讨论过这个问题。可见不管是面试还是工作这都是一个极为重要的点。\n\n我也曾在面阿里的时候也遇到过原题,题目是写出俩内存泄漏案例,然后问如何排查?如何解决?\n\n本篇文章大体结构来自外国大佬baeldung;\n\n\n# 一、介绍\n\n\n# 1、什么是内存泄漏\n\njava的优势之一就是内置了垃圾回收器GC,它帮助我们实现了自动化内存管理。但是GC再好,也有老马失前蹄的时候,它不能保证提供一个解决内存泄漏的万无一失的解决方案。什么是内存泄漏?可以看看下面这张图,\n\n也就是一部分内存空间我明明已经使用了,却没有引用指向这部分空间。造成这片已经使用的空间无法处理的情况。\n\n正规点的理解:动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。\n\n\n# 2、内存泄漏的危害\n\n长时间运行,程序变卡,性能严重下降 程序莫名其妙挂掉 OutOfMemoryError错误 乱七八糟的错误,还不易排查 反正内存泄漏不是好事。\n\n\n# 二、内存泄漏原因\n\n内存泄漏原因太多了。说不定就是某一行代码不对就会出现这种情况,因此这里给出最常见的几种。关键的还是如何找出哪个地方出现了内存泄漏,代码好修改,错误不易查。\n\n\n# 1、大量使用静态变量\n\n静态变量的生命周期与程序一致。因此常驻内存。\n\npublic class Static Test{\n public static List list = new ArrayList<>();\n public void populateList(){\n for (int i = 0; i < 10000000; i++) {\n list.add((int)Math.random()); \n } \n System.out.println(\"running......\"); \n }\n public static void main(String[] args){ \n System.out.println(\"before......\");\n new StaticTest().populateList(); \n System.out.println(\"after......\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n\n\n现在可以使用jvisualvm运行一边,看看内存效果。\n\n * 带static关键字(使用静态变量)\n\n从上图可以看到,堆内存从一开始的135M左右飙升了到了200M。直接占据了65M的内存。\n\n * 不使用static关键字(不使用静态变量)\n\n由于全局变量与程序周期不一致,因此不使用时,就会进行回收。此时内存最高150M。\n\n总结:由于静态变量与程序生命周期一致,因此对象常驻内存,造成内存泄漏\n\n\n# 2、连接资源未关闭\n\n每当建立一个连接,jvm就会为这么资源分配内存。比如数据库连接、文件输入输出流、网络连接等等。\n\npublic class File Test{\n public static void main(String[] args)throws IOException {\n File f=new File(\"G:\\\\nginx配套资料\\\\笔记资料.zip\"); \n System.out.println(f.exists());\n System.out.println(f.isDirectory());\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n依然使用jvisualvm运行一边,看看内存效果。\n\n可以看出,在连接文件资源时,jvm会为本资源分配内存。\n\n3、equals()和hashCode()方法使用不当\n\n定义新类时,如果没有重新equals()和hashCode()方法,也有可能会造成内存泄漏。主要原因是没有这两个方法时,很容易造成重复的数据添加。看例子:\n\npublic class User{\n public String name;\n publicint age;\n public User(String name, int age){\n this.name = name;\n this.age = age;}\n }\n public class EqualTest{\n public static void main(String[] args){ \n Map map = new HashMap<>();\n for(int i=0; i<100; i++) { \n map.put(new User(\"\", 1), 1);\n } \n System.out.println(map.size() == 1);//输出为false \n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\n然后运行一下,看看内存情况:\n\n内存从150M一下子飙升到225M,可见飙升的厉害。输出为false,说明user对象被重复添加了。我们知道像HashMap在添加新的对象时,会对其hashcode进行比较,如果一样,那就不插入。如果一样那就插入。此时说明这100个User其hashcode不同。\n\n\n# 4、内部类持有外部类\n\n这个场景和上面类似。\n\n\n# 5、finalize方法\n\n这个方法之前曾经专门花过文章写过,这个问题很简单。看一张图\n\n这就是整个过程。不过在这里我们主要看的是finalize方法对垃圾回收的影响,其实就是在第三步,也就是这个对象含有finalize,进入了队列但一直没有被调用的这段时间,会一直占用内存。造成内存泄漏。\n\n\n# 6、ThreadLocal的错误使用\n\nThreadLocal主要用于创建本地线程变量,不合理的使用也有可能会造成内存泄漏。\n\n上面这张图详细的揭示了ThreadLocal和Thread以及ThreadLocalMap三者的关系。\n\n1、Thread中有一个map,就是ThreadLocalMap\n\n2、ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。\n\n3、ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收\n\n4、重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。\n\n解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。\n\n现在介绍了几种常见的内存泄漏情况,上面的知识点比较常见,最主要的是如何检测出来。\n\n\n# 三、检测内存泄漏\n\n检测的目的是定位内存泄漏出现的位置,常见的有以下几种方法:\n\n\n# 1、工具分析\n\n这个工具比较多,比如说JProfiler、YourKit、Java VisualVM和Netbeans Profiler。他可以帮助我们分析是哪一个对象或者是类内存的飙升。也可以看到内存CPU的等等各种情况。上面多次演示到了。\n\n\n# 2、垃圾回收分析\n\n这个其实也可以用工具进行分析。上面的VisualVM中,可以打印堆。也可以从外部导入dump文件进行分析。\n\n如果不用工具的话,我们可以通过IDE看到。JVM配置添加-verbose:gc。然后就会打印出相关信息。下面这张图非原创,来自Baeldung。\n\n\n# 3、基准测试\n\n也就是使用科学的方式进行分析java代码的性能。进而判断分析。\n\n\n# 四、结论\n\n内存泄漏是个很严重的问题,也比较常见。\n\n最主要的原因是动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。因此良好的代码规范,可以有效地避免这些错误。\n\n参考: 什么是内存泄漏?该如何检测?又该如何解决?", + "normalizedContent": "# 前言\n\n这个问题是我之前翻看面经的时候见到的。那位小姐姐把内存泄漏当成了内存溢出问题去解答的,结果当场挂掉了。为此总结一下,之前和一位老哥也讨论过这个问题。可见不管是面试还是工作这都是一个极为重要的点。\n\n我也曾在面阿里的时候也遇到过原题,题目是写出俩内存泄漏案例,然后问如何排查?如何解决?\n\n本篇文章大体结构来自外国大佬baeldung;\n\n\n# 一、介绍\n\n\n# 1、什么是内存泄漏\n\njava的优势之一就是内置了垃圾回收器gc,它帮助我们实现了自动化内存管理。但是gc再好,也有老马失前蹄的时候,它不能保证提供一个解决内存泄漏的万无一失的解决方案。什么是内存泄漏?可以看看下面这张图,\n\n也就是一部分内存空间我明明已经使用了,却没有引用指向这部分空间。造成这片已经使用的空间无法处理的情况。\n\n正规点的理解:动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。\n\n\n# 2、内存泄漏的危害\n\n长时间运行,程序变卡,性能严重下降 程序莫名其妙挂掉 outofmemoryerror错误 乱七八糟的错误,还不易排查 反正内存泄漏不是好事。\n\n\n# 二、内存泄漏原因\n\n内存泄漏原因太多了。说不定就是某一行代码不对就会出现这种情况,因此这里给出最常见的几种。关键的还是如何找出哪个地方出现了内存泄漏,代码好修改,错误不易查。\n\n\n# 1、大量使用静态变量\n\n静态变量的生命周期与程序一致。因此常驻内存。\n\npublic class static test{\n public static list list = new arraylist<>();\n public void populatelist(){\n for (int i = 0; i < 10000000; i++) {\n list.add((int)math.random()); \n } \n system.out.println(\"running......\"); \n }\n public static void main(string[] args){ \n system.out.println(\"before......\");\n new statictest().populatelist(); \n system.out.println(\"after......\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n\n\n现在可以使用jvisualvm运行一边,看看内存效果。\n\n * 带static关键字(使用静态变量)\n\n从上图可以看到,堆内存从一开始的135m左右飙升了到了200m。直接占据了65m的内存。\n\n * 不使用static关键字(不使用静态变量)\n\n由于全局变量与程序周期不一致,因此不使用时,就会进行回收。此时内存最高150m。\n\n总结:由于静态变量与程序生命周期一致,因此对象常驻内存,造成内存泄漏\n\n\n# 2、连接资源未关闭\n\n每当建立一个连接,jvm就会为这么资源分配内存。比如数据库连接、文件输入输出流、网络连接等等。\n\npublic class file test{\n public static void main(string[] args)throws ioexception {\n file f=new file(\"g:\\\\nginx配套资料\\\\笔记资料.zip\"); \n system.out.println(f.exists());\n system.out.println(f.isdirectory());\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n依然使用jvisualvm运行一边,看看内存效果。\n\n可以看出,在连接文件资源时,jvm会为本资源分配内存。\n\n3、equals()和hashcode()方法使用不当\n\n定义新类时,如果没有重新equals()和hashcode()方法,也有可能会造成内存泄漏。主要原因是没有这两个方法时,很容易造成重复的数据添加。看例子:\n\npublic class user{\n public string name;\n publicint age;\n public user(string name, int age){\n this.name = name;\n this.age = age;}\n }\n public class equaltest{\n public static void main(string[] args){ \n map map = new hashmap<>();\n for(int i=0; i<100; i++) { \n map.put(new user(\"\", 1), 1);\n } \n system.out.println(map.size() == 1);//输出为false \n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\n然后运行一下,看看内存情况:\n\n内存从150m一下子飙升到225m,可见飙升的厉害。输出为false,说明user对象被重复添加了。我们知道像hashmap在添加新的对象时,会对其hashcode进行比较,如果一样,那就不插入。如果一样那就插入。此时说明这100个user其hashcode不同。\n\n\n# 4、内部类持有外部类\n\n这个场景和上面类似。\n\n\n# 5、finalize方法\n\n这个方法之前曾经专门花过文章写过,这个问题很简单。看一张图\n\n这就是整个过程。不过在这里我们主要看的是finalize方法对垃圾回收的影响,其实就是在第三步,也就是这个对象含有finalize,进入了队列但一直没有被调用的这段时间,会一直占用内存。造成内存泄漏。\n\n\n# 6、threadlocal的错误使用\n\nthreadlocal主要用于创建本地线程变量,不合理的使用也有可能会造成内存泄漏。\n\n上面这张图详细的揭示了threadlocal和thread以及threadlocalmap三者的关系。\n\n1、thread中有一个map,就是threadlocalmap\n\n2、threadlocalmap的key是threadlocal,值是我们自己设定的。\n\n3、threadlocal是一个弱引用,当为null时,会被当成垃圾回收\n\n4、重点来了,突然我们threadlocal是null了,也就是要被垃圾回收器回收了,但是此时我们的threadlocalmap生命周期和thread的一样,它不会回收,这时候就出现了一个现象。那就是threadlocalmap的key没了,但是value还在,这就造成了内存泄漏。\n\n解决办法:使用完threadlocal后,执行remove操作,避免出现内存溢出情况。\n\n现在介绍了几种常见的内存泄漏情况,上面的知识点比较常见,最主要的是如何检测出来。\n\n\n# 三、检测内存泄漏\n\n检测的目的是定位内存泄漏出现的位置,常见的有以下几种方法:\n\n\n# 1、工具分析\n\n这个工具比较多,比如说jprofiler、yourkit、java visualvm和netbeans profiler。他可以帮助我们分析是哪一个对象或者是类内存的飙升。也可以看到内存cpu的等等各种情况。上面多次演示到了。\n\n\n# 2、垃圾回收分析\n\n这个其实也可以用工具进行分析。上面的visualvm中,可以打印堆。也可以从外部导入dump文件进行分析。\n\n如果不用工具的话,我们可以通过ide看到。jvm配置添加-verbose:gc。然后就会打印出相关信息。下面这张图非原创,来自baeldung。\n\n\n# 3、基准测试\n\n也就是使用科学的方式进行分析java代码的性能。进而判断分析。\n\n\n# 四、结论\n\n内存泄漏是个很严重的问题,也比较常见。\n\n最主要的原因是动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。因此良好的代码规范,可以有效地避免这些错误。\n\n参考: 什么是内存泄漏?该如何检测?又该如何解决?", "charsets": { "cjk": true }, - "lastUpdated": "2022/06/20, 22:47:41", - "lastUpdatedTimestamp": 1655736461000 + "lastUpdated": "2022/09/18, 21:28:02", + "lastUpdatedTimestamp": 1663507682000 }, { - "title": "什么是设计模式,描述几个常用的设计模式", + "title": "责任链模式", "frontmatter": { - "title": "什么是设计模式,描述几个常用的设计模式", - "date": "2022-05-21T17:26:19.000Z", - "permalink": "/pages/3d768c/", + "title": "责任链模式", + "date": "2022-05-21T17:28:46.000Z", + "permalink": "/pages/d3ff35/", "categories": [ "Java相关", "设计模式" @@ -2674,10 +2849,10 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/003.%E4%BB%80%E4%B9%88%E6%98%AF%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%EF%BC%8C%E6%8F%8F%E8%BF%B0%E5%87%A0%E4%B8%AA%E5%B8%B8%E7%94%A8%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20.html", - "relativePath": "01.Java相关/40.设计模式/003.什么是设计模式,描述几个常用的设计模式 .md", - "key": "v-504a5213", - "path": "/pages/3d768c/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/030.%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F.html", + "relativePath": "01.Java相关/40.设计模式/030.责任链模式.md", + "key": "v-34f8aaa2", + "path": "/pages/d3ff35/", "headersStr": null, "content": "", "normalizedContent": "", @@ -2686,11 +2861,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "工厂模式使用场景 能举个例子吗", + "title": "简述装饰者模式", "frontmatter": { - "title": "工厂模式使用场景 能举个例子吗", - "date": "2022-05-21T17:26:32.000Z", - "permalink": "/pages/f15489/", + "title": "简述装饰者模式", + "date": "2022-05-21T17:26:42.000Z", + "permalink": "/pages/47a4e3/", "categories": [ "Java相关", "设计模式" @@ -2699,78 +2874,458 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/004.%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF%20%E8%83%BD%E4%B8%BE%E4%B8%AA%E4%BE%8B%E5%AD%90%E5%90%97.html", - "relativePath": "01.Java相关/40.设计模式/004.工厂模式使用场景 能举个例子吗.md", - "key": "v-2d6ed001", - "path": "/pages/f15489/", - "headers": [ - { - "level": 2, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/005.%E8%A3%85%E9%A5%B0%E8%80%85%E6%A8%A1%E5%BC%8F.html", + "relativePath": "01.Java相关/40.设计模式/005.装饰者模式.md", + "key": "v-22a9eaef", + "path": "/pages/47a4e3/", + "headers": [ + { + "level": 2, "title": "模式动机", "slug": "模式动机", "normalizedTitle": "模式动机", - "charIndex": 192 + "charIndex": 186 + }, + { + "level": 2, + "title": "定义", + "slug": "定义", + "normalizedTitle": "定义", + "charIndex": 489 + }, + { + "level": 2, + "title": "角色", + "slug": "角色", + "normalizedTitle": "角色", + "charIndex": 632 + }, + { + "level": 2, + "title": "类图", + "slug": "类图", + "normalizedTitle": "类图", + "charIndex": 1194 + }, + { + "level": 2, + "title": "实例", + "slug": "实例", + "normalizedTitle": "实例", + "charIndex": 1203 + }, + { + "level": 2, + "title": "应用", + "slug": "应用", + "normalizedTitle": "应用", + "charIndex": 3775 + }, + { + "level": 3, + "title": "Java I/O 中的装饰者模式", + "slug": "java-i-o-中的装饰者模式", + "normalizedTitle": "java i/o 中的装饰者模式", + "charIndex": 3782 + }, + { + "level": 3, + "title": "Servlet 中的装饰者模式", + "slug": "servlet-中的装饰者模式", + "normalizedTitle": "servlet 中的装饰者模式", + "charIndex": 4933 + }, + { + "level": 3, + "title": "spring 中的装饰者模式", + "slug": "spring-中的装饰者模式", + "normalizedTitle": "spring 中的装饰者模式", + "charIndex": 5565 + }, + { + "level": 3, + "title": "Mybatis 缓存中的装饰者模式", + "slug": "mybatis-缓存中的装饰者模式", + "normalizedTitle": "mybatis 缓存中的装饰者模式", + "charIndex": 6062 + }, + { + "level": 2, + "title": "总结", + "slug": "总结", + "normalizedTitle": "总结", + "charIndex": 6192 + }, + { + "level": 2, + "title": "优缺点", + "slug": "优缺点", + "normalizedTitle": "优缺点", + "charIndex": 6300 + }, + { + "level": 2, + "title": "何时选用", + "slug": "何时选用", + "normalizedTitle": "何时选用", + "charIndex": 6776 + } + ], + "headersStr": "模式动机 定义 角色 类图 实例 应用 Java I/O 中的装饰者模式 Servlet 中的装饰者模式 spring 中的装饰者模式 Mybatis 缓存中的装饰者模式 总结 优缺点 何时选用", + "content": "《Head First 设计模式》中是这么形容装饰者模式的——“给爱用继承的人一个全新的设计眼界”,拒绝继承滥用,从装饰者模式开始。\n\n装饰者模式允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。\n\n这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。\n\n\n# 模式动机\n\n一般有两种方式可以实现给一个类或对象增加行为:\n\n * 继承机制,使用继承机制是给现有类添加功能的一种有效途径,通过继承一个现有类可以使得子类在拥有自身方法的同时还拥有父类的方法。但是这种方法是静态的,用户不能控制增加行为的方式和时机。\n\n * 关联机制,即将一个类的对象嵌入另一个对象中,由另一个对象来决定是否调用嵌入对象的行为以便扩展自己的行为,我们称这个嵌入的对象为装饰器(Decorator)\n\n装饰模式以对客户透明的方式动态地给一个对象附加上更多的责任,换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。\n\n\n# 定义\n\n装饰模式(Decorator Pattern) :动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类(继承)实现更为灵活。其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。\n\n\n# 角色\n\n * Component: 抽象组件,装饰者和被装饰者共同的父类,是一个接口或者抽象类,用来定义基本行为,可以给这些对象动态添加职责\n\n * ConcreteComponent: 具体的组件对象,实现类 ,即被装饰者,通常就是被装饰器装饰的原始对象,也就是可以给这个对象添加职责\n\n * Decorator: 所有装饰器的抽象父类,一般是抽象类,实现接口;它的属性必然有个指向 Conponent 抽象组件的对象 ,其实就是持有一个被装饰的对象\n\n * ConcreteDecorator: 具体的装饰对象,实现具体要被装饰对象添加的功能。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为。\n\n装饰者和被装饰者对象有相同的父类,因为装饰者和被装饰者必须是一样的类型,这里利用继承是为了达到类型匹配,而不是利用继承获得行为。\n\n利用继承设计子类,只能在编译时静态决定,并且所有子类都会继承相同的行为;利用组合的做法扩展对象,就可以在运行时动态的进行扩展。装饰者模式遵循开放-关闭原则:**类应该对扩展开放,对修改关闭。**利用装饰者,我们可以实现新的装饰者增加新的行为而不用修改现有代码,而如果单纯依赖继承,每当需要新行为时,还得修改现有的代码。\n\n\n# 类图\n\n\n\n\n# 实例\n\n看了好多资料的例子,比如\n\n * 公司发放奖金,不同的员工类型对应不同的奖金计算规则,用各种计算规则去装饰统一的奖金计算类\n * 星巴克售卖用咖啡,用摩卡、奶泡去装饰咖啡,实现不同的计费\n * 变形金刚在变形之前是一辆汽车,它可以在陆地上移动。当它变成机器人之后除了能够在陆地上移动之外,还可以说话;如果需要,它还可以变成飞机,除了在陆地上移动还可以在天空中飞翔\n\n我还是比较喜欢卖煎饼的例子\n\n\n\n1、定义抽象组件\n\npublic abstract class Pancake {\n\n String description = \"普通煎饼\";\n\n public String getDescription(){\n return description;\n }\n\n public abstract double cost();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n2、定义具体的被装饰者,这里是煎饼果子,当然还可以有鸡蛋灌饼、手抓饼等其他被装饰者\n\npublic class Battercake extends Pancake {\n @Override\n public double cost() {\n return 8;\n }\n\n public Battercake(){\n description = \"煎饼果子\";\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n3、抽象的装饰器对象,定义一个调料抽象类\n\npublic abstract class CondimentDecorator extends Pancake {\n\n // 持有组件对象\n protected Pancake pancake;\n public CondimentDecorator(Pancake pancake){\n this.pancake = pancake;\n }\n\n public abstract String getDescription();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n4、具体的装饰者,我们定义一个鸡蛋装饰器,一个火腿装饰器\n\npublic class Egg extends CondimentDecorator {\n public Egg(Pancake pancake){\n super(pancake);\n }\n\n @Override\n public String getDescription() {\n return pancake.getDescription() + \"加鸡蛋\";\n }\n\n @Override\n public double cost() {\n return pancake.cost() + 1;\n }\n}\npublic class Sausage extends CondimentDecorator{\n public Sausage(Pancake pancake){\n super(pancake);\n }\n @Override\n public String getDescription() {\n return pancake.getDescription() + \"加火腿\";\n }\n\n @Override\n public double cost() {\n return pancake.cost() + 2;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n\n\n5、测试煎饼交易,over\n\npublic class Client {\n\n public static void main(String[] args) {\n //买一个普通的煎饼果子\n Pancake battercake = new Battercake();\n System.out.println(battercake.getDescription() + \"花费:\"+battercake.cost() + \"元\");\n\n //买一个加双蛋的煎饼果子\n Pancake doubleEgg = new Battercake();\n doubleEgg = new Egg(doubleEgg);\n doubleEgg = new Egg(doubleEgg);\n System.out.println(doubleEgg.getDescription() + \"花费\" + doubleEgg.cost() + \"元\");\n\n //加火腿和鸡蛋\n Pancake battercakePlus = new Battercake();\n battercakePlus = new Egg(battercakePlus);\n battercakePlus = new Sausage(battercakePlus);\n System.out.println(battercakePlus.getDescription() + \"花费\" + battercakePlus.cost() + \"元\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\n输出:\n\n煎饼果子花费:8.0元\n煎饼果子加鸡蛋加鸡蛋花费10.0元\n煎饼果子加鸡蛋加火腿花费11.0元\n\n\n1\n2\n3\n\n\n顺便看下通过 IDEA 生成的 UML 类图(和我们画的类图一样哈)\n\n\n# 应用\n\n\n# Java I/O 中的装饰者模式\n\n我们使用 java.io 包下的各种输入流、输出流、字节流、字符流、缓冲流等各种各样的流,他们中的许多类都是装饰者,下面是一个典型的对象集合,用装饰者将功能结合起来,以读取文件数据\n\n\n\nBufferedInputStream 和 LinerNumberInputStream 都是扩展自 FilterInputStream,而 FilterInputStream 是一个抽象的装饰类。\n\n在 idea 中选中一些常见 InputStream 类,生成 UML 图如下:\n\n我们平时读取一个文件中的内容其实就使用到了装饰模式的思想,简化《Head First 设计模式》的例子,我们自定义一个装饰者,把输入流中的所有大写字符转换为小写\n\npublic class LowerCaseInputStream extends FilterInputStream {\n \n protected LowerCaseInputStream(InputStream in) {\n super(in);\n }\n\n public int read() throws IOException {\n int c = super.read();\n return (c == -1 ? c:Character.toLowerCase(c));\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\npublic class InputTest {\n\n public static void main(String[] args) throws IOException {\n int c;\n //装饰器的组装过程\n InputStream in = new LowerCaseInputStream(new BufferedInputStream(new FileInputStream(\"JavaKeeper.txt\"))); \n\n while ((c = in.read()) >= 0){\n System.out.print((char) c);\n }\n in.close();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n采用装饰者模式在实例化组件时,将增加代码的复杂度,一旦使用装饰者模式,不只需要实例化组件,还把把此组件包装进装饰者中,天晓得有几个,所以在某些复杂情况下,我们还会结合工厂模式和生成器模式。比如Spring中的装饰者模式。\n\n\n# Servlet 中的装饰者模式\n\nServlet API 源自于 4 个实现类,它很少被使用,但是十分强大:ServletRequestWrapper、ServletResponseWrapper以及 HttpServletRequestWrapper、HttpServletResponseWrapper。\n\n比如 ServletRequestWrapper 是 ServletRequest 接口的简单实现,开发者可以继承 ServletRequestWrapper 去扩展原来的 request\n\npublic class ServletRequestWrapper implements ServletRequest {\n private ServletRequest request;\n\n public ServletRequestWrapper(ServletRequest request) {\n if (request == null) {\n throw new IllegalArgumentException(\"Request cannot be null\");\n } else {\n this.request = request;\n }\n }\n\t//.......\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n\n# spring 中的装饰者模式\n\nSpring 的 ApplicationContext 中配置所有的 DataSource。 这些 DataSource 可能是各种不同类型的, 比如不同的数据库: Oracle、 SQL Server、 MySQL 等, 也可能是不同的数据源。 然后 SessionFactory 根据客户的每次请求, 将 DataSource 属性设置成不同的数据源, 以达到切换数据源的目的。\n\n在 Spring 的命名体现:Spring 中用到的包装器模式在类名上有两种表现: 一种是类名中含有 Wrapper, 另一种是类名中含有 Decorator。 基本上都是动态地给一个对象添加一些额外的职责,比如\n\norg.springframework.cache.transaction 包下的 TransactionAwareCacheDecorator 类 org.springframework.session.web.http 包下的 SessionRepositoryFilter 内部类 SessionRepositoryRequestWrapper\n\n\n# Mybatis 缓存中的装饰者模式\n\nMybatis 的缓存模块中,使用了装饰器模式的变体,其中将 Decorator 接口和 Componet 接口合并为一个 Component 接口。org.apache.ibatis.cache 包下的结构\n\n\n\n\n# 总结\n\n装饰模式的本质:动态组合\n\n动态组合是手段,组合才是目的。这里的组合有两个意思,一个是动态功能的组合,也就是动态进行装饰器的组合;另外一个是指对象组合,通过对象组合来实现为被装饰对象透明的增加功能。\n\n\n# 优缺点\n\n装饰模式的优点:\n\n * 装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。\n * 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的装饰器,从而实现不同的行为。\n * 通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更为强大的对象。\n * 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开闭原则”\n\n装饰模式的缺点:\n\n * 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度。\n * 这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。\n\n\n# 何时选用\n\n * 如果需要在不影响其他对象的情况下,以动态、透明的方式给对象添加职责,可以使用装饰模式\n * 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类已定义为不能被继承(如 Java 语言中的 final 类)\n\n参考: 装饰模式——看看 JDK 和 Spring 是如何杜绝继承滥用的", + "normalizedContent": "《head first 设计模式》中是这么形容装饰者模式的——“给爱用继承的人一个全新的设计眼界”,拒绝继承滥用,从装饰者模式开始。\n\n装饰者模式允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。\n\n这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。\n\n\n# 模式动机\n\n一般有两种方式可以实现给一个类或对象增加行为:\n\n * 继承机制,使用继承机制是给现有类添加功能的一种有效途径,通过继承一个现有类可以使得子类在拥有自身方法的同时还拥有父类的方法。但是这种方法是静态的,用户不能控制增加行为的方式和时机。\n\n * 关联机制,即将一个类的对象嵌入另一个对象中,由另一个对象来决定是否调用嵌入对象的行为以便扩展自己的行为,我们称这个嵌入的对象为装饰器(decorator)\n\n装饰模式以对客户透明的方式动态地给一个对象附加上更多的责任,换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。\n\n\n# 定义\n\n装饰模式(decorator pattern) :动态地给一个对象增加一些额外的职责(responsibility),就增加对象功能来说,装饰模式比生成子类(继承)实现更为灵活。其别名也可以称为包装器(wrapper),与适配器模式的别名相同,但它们适用于不同的场合。\n\n\n# 角色\n\n * component: 抽象组件,装饰者和被装饰者共同的父类,是一个接口或者抽象类,用来定义基本行为,可以给这些对象动态添加职责\n\n * concretecomponent: 具体的组件对象,实现类 ,即被装饰者,通常就是被装饰器装饰的原始对象,也就是可以给这个对象添加职责\n\n * decorator: 所有装饰器的抽象父类,一般是抽象类,实现接口;它的属性必然有个指向 conponent 抽象组件的对象 ,其实就是持有一个被装饰的对象\n\n * concretedecorator: 具体的装饰对象,实现具体要被装饰对象添加的功能。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为。\n\n装饰者和被装饰者对象有相同的父类,因为装饰者和被装饰者必须是一样的类型,这里利用继承是为了达到类型匹配,而不是利用继承获得行为。\n\n利用继承设计子类,只能在编译时静态决定,并且所有子类都会继承相同的行为;利用组合的做法扩展对象,就可以在运行时动态的进行扩展。装饰者模式遵循开放-关闭原则:**类应该对扩展开放,对修改关闭。**利用装饰者,我们可以实现新的装饰者增加新的行为而不用修改现有代码,而如果单纯依赖继承,每当需要新行为时,还得修改现有的代码。\n\n\n# 类图\n\n\n\n\n# 实例\n\n看了好多资料的例子,比如\n\n * 公司发放奖金,不同的员工类型对应不同的奖金计算规则,用各种计算规则去装饰统一的奖金计算类\n * 星巴克售卖用咖啡,用摩卡、奶泡去装饰咖啡,实现不同的计费\n * 变形金刚在变形之前是一辆汽车,它可以在陆地上移动。当它变成机器人之后除了能够在陆地上移动之外,还可以说话;如果需要,它还可以变成飞机,除了在陆地上移动还可以在天空中飞翔\n\n我还是比较喜欢卖煎饼的例子\n\n\n\n1、定义抽象组件\n\npublic abstract class pancake {\n\n string description = \"普通煎饼\";\n\n public string getdescription(){\n return description;\n }\n\n public abstract double cost();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n2、定义具体的被装饰者,这里是煎饼果子,当然还可以有鸡蛋灌饼、手抓饼等其他被装饰者\n\npublic class battercake extends pancake {\n @override\n public double cost() {\n return 8;\n }\n\n public battercake(){\n description = \"煎饼果子\";\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n3、抽象的装饰器对象,定义一个调料抽象类\n\npublic abstract class condimentdecorator extends pancake {\n\n // 持有组件对象\n protected pancake pancake;\n public condimentdecorator(pancake pancake){\n this.pancake = pancake;\n }\n\n public abstract string getdescription();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n4、具体的装饰者,我们定义一个鸡蛋装饰器,一个火腿装饰器\n\npublic class egg extends condimentdecorator {\n public egg(pancake pancake){\n super(pancake);\n }\n\n @override\n public string getdescription() {\n return pancake.getdescription() + \"加鸡蛋\";\n }\n\n @override\n public double cost() {\n return pancake.cost() + 1;\n }\n}\npublic class sausage extends condimentdecorator{\n public sausage(pancake pancake){\n super(pancake);\n }\n @override\n public string getdescription() {\n return pancake.getdescription() + \"加火腿\";\n }\n\n @override\n public double cost() {\n return pancake.cost() + 2;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n\n\n5、测试煎饼交易,over\n\npublic class client {\n\n public static void main(string[] args) {\n //买一个普通的煎饼果子\n pancake battercake = new battercake();\n system.out.println(battercake.getdescription() + \"花费:\"+battercake.cost() + \"元\");\n\n //买一个加双蛋的煎饼果子\n pancake doubleegg = new battercake();\n doubleegg = new egg(doubleegg);\n doubleegg = new egg(doubleegg);\n system.out.println(doubleegg.getdescription() + \"花费\" + doubleegg.cost() + \"元\");\n\n //加火腿和鸡蛋\n pancake battercakeplus = new battercake();\n battercakeplus = new egg(battercakeplus);\n battercakeplus = new sausage(battercakeplus);\n system.out.println(battercakeplus.getdescription() + \"花费\" + battercakeplus.cost() + \"元\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\n输出:\n\n煎饼果子花费:8.0元\n煎饼果子加鸡蛋加鸡蛋花费10.0元\n煎饼果子加鸡蛋加火腿花费11.0元\n\n\n1\n2\n3\n\n\n顺便看下通过 idea 生成的 uml 类图(和我们画的类图一样哈)\n\n\n# 应用\n\n\n# java i/o 中的装饰者模式\n\n我们使用 java.io 包下的各种输入流、输出流、字节流、字符流、缓冲流等各种各样的流,他们中的许多类都是装饰者,下面是一个典型的对象集合,用装饰者将功能结合起来,以读取文件数据\n\n\n\nbufferedinputstream 和 linernumberinputstream 都是扩展自 filterinputstream,而 filterinputstream 是一个抽象的装饰类。\n\n在 idea 中选中一些常见 inputstream 类,生成 uml 图如下:\n\n我们平时读取一个文件中的内容其实就使用到了装饰模式的思想,简化《head first 设计模式》的例子,我们自定义一个装饰者,把输入流中的所有大写字符转换为小写\n\npublic class lowercaseinputstream extends filterinputstream {\n \n protected lowercaseinputstream(inputstream in) {\n super(in);\n }\n\n public int read() throws ioexception {\n int c = super.read();\n return (c == -1 ? c:character.tolowercase(c));\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\npublic class inputtest {\n\n public static void main(string[] args) throws ioexception {\n int c;\n //装饰器的组装过程\n inputstream in = new lowercaseinputstream(new bufferedinputstream(new fileinputstream(\"javakeeper.txt\"))); \n\n while ((c = in.read()) >= 0){\n system.out.print((char) c);\n }\n in.close();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n采用装饰者模式在实例化组件时,将增加代码的复杂度,一旦使用装饰者模式,不只需要实例化组件,还把把此组件包装进装饰者中,天晓得有几个,所以在某些复杂情况下,我们还会结合工厂模式和生成器模式。比如spring中的装饰者模式。\n\n\n# servlet 中的装饰者模式\n\nservlet api 源自于 4 个实现类,它很少被使用,但是十分强大:servletrequestwrapper、servletresponsewrapper以及 httpservletrequestwrapper、httpservletresponsewrapper。\n\n比如 servletrequestwrapper 是 servletrequest 接口的简单实现,开发者可以继承 servletrequestwrapper 去扩展原来的 request\n\npublic class servletrequestwrapper implements servletrequest {\n private servletrequest request;\n\n public servletrequestwrapper(servletrequest request) {\n if (request == null) {\n throw new illegalargumentexception(\"request cannot be null\");\n } else {\n this.request = request;\n }\n }\n\t//.......\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n\n# spring 中的装饰者模式\n\nspring 的 applicationcontext 中配置所有的 datasource。 这些 datasource 可能是各种不同类型的, 比如不同的数据库: oracle、 sql server、 mysql 等, 也可能是不同的数据源。 然后 sessionfactory 根据客户的每次请求, 将 datasource 属性设置成不同的数据源, 以达到切换数据源的目的。\n\n在 spring 的命名体现:spring 中用到的包装器模式在类名上有两种表现: 一种是类名中含有 wrapper, 另一种是类名中含有 decorator。 基本上都是动态地给一个对象添加一些额外的职责,比如\n\norg.springframework.cache.transaction 包下的 transactionawarecachedecorator 类 org.springframework.session.web.http 包下的 sessionrepositoryfilter 内部类 sessionrepositoryrequestwrapper\n\n\n# mybatis 缓存中的装饰者模式\n\nmybatis 的缓存模块中,使用了装饰器模式的变体,其中将 decorator 接口和 componet 接口合并为一个 component 接口。org.apache.ibatis.cache 包下的结构\n\n\n\n\n# 总结\n\n装饰模式的本质:动态组合\n\n动态组合是手段,组合才是目的。这里的组合有两个意思,一个是动态功能的组合,也就是动态进行装饰器的组合;另外一个是指对象组合,通过对象组合来实现为被装饰对象透明的增加功能。\n\n\n# 优缺点\n\n装饰模式的优点:\n\n * 装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。\n * 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的装饰器,从而实现不同的行为。\n * 通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更为强大的对象。\n * 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开闭原则”\n\n装饰模式的缺点:\n\n * 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度。\n * 这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。\n\n\n# 何时选用\n\n * 如果需要在不影响其他对象的情况下,以动态、透明的方式给对象添加职责,可以使用装饰模式\n * 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类已定义为不能被继承(如 java 语言中的 final 类)\n\n参考: 装饰模式——看看 jdk 和 spring 是如何杜绝继承滥用的", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "适配器模式", + "frontmatter": { + "title": "适配器模式", + "date": "2022-05-21T17:28:10.000Z", + "permalink": "/pages/827fc4/", + "categories": [ + "Java相关", + "设计模式" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/010.%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8F.html", + "relativePath": "01.Java相关/40.设计模式/010.适配器模式.md", + "key": "v-31509c80", + "path": "/pages/827fc4/", + "headers": [ + { + "level": 2, + "title": "问题", + "slug": "问题", + "normalizedTitle": "问题", + "charIndex": 2 + }, + { + "level": 2, + "title": "真实世界类比", + "slug": "真实世界类比", + "normalizedTitle": "真实世界类比", + "charIndex": 233 + }, + { + "level": 2, + "title": "基本介绍", + "slug": "基本介绍", + "normalizedTitle": "基本介绍", + "charIndex": 312 + }, + { + "level": 2, + "title": "工作原理", + "slug": "工作原理", + "normalizedTitle": "工作原理", + "charIndex": 455 + }, + { + "level": 2, + "title": "适配器模式结构", + "slug": "适配器模式结构", + "normalizedTitle": "适配器模式结构", + "charIndex": 591 + }, + { + "level": 3, + "title": "对象适配器", + "slug": "对象适配器", + "normalizedTitle": "对象适配器", + "charIndex": 443 + }, + { + "level": 3, + "title": "Coding", + "slug": "coding", + "normalizedTitle": "coding", + "charIndex": 1064 }, { "level": 2, + "title": "类适配器", + "slug": "类适配器", + "normalizedTitle": "类适配器", + "charIndex": 436 + }, + { + "level": 2, + "title": "Coding", + "slug": "coding-2", + "normalizedTitle": "coding", + "charIndex": 1064 + }, + { + "level": 2, + "title": "适配器模式适合应用场景", + "slug": "适配器模式适合应用场景", + "normalizedTitle": "适配器模式适合应用场景", + "charIndex": 3145 + }, + { + "level": 2, + "title": "demo", + "slug": "demo", + "normalizedTitle": "demo", + "charIndex": 3518 + }, + { + "level": 2, + "title": "适配器模式优缺点", + "slug": "适配器模式优缺点", + "normalizedTitle": "适配器模式优缺点", + "charIndex": 5027 + }, + { + "level": 2, + "title": "Spring 中的适配器", + "slug": "spring-中的适配器", + "normalizedTitle": "spring 中的适配器", + "charIndex": 5191 + } + ], + "headersStr": "问题 真实世界类比 基本介绍 工作原理 适配器模式结构 对象适配器 Coding 类适配器 Coding 适配器模式适合应用场景 demo 适配器模式优缺点 Spring 中的适配器", + "content": "# 问题\n\n假设我们在做一套股票看盘系统,数据提供方给我们提供 XML 格式数据,我们获取数据用来显示,随着系统的迭代,我们要整合一些第三方系统的对外数据,但是他们只提供获取 JSON 格式的数据接口。\n\n在不想改变原有代码逻辑的情况下,如何解决呢?\n\n这时候我们就可以创建一个「适配器」。这是一个特殊的对象, 能够转换对象接口, 使其能与其他对象进行交互。\n\n适配器模式通过封装对象将复杂的转换过程隐藏于幕后。 被封装的对象甚至察觉不到适配器的存在。\n\n\n\n\n# 真实世界类比\n\n适配器是什么,不难理解,生活中也随处可见。比如,笔记本电脑的电源适配器、万能充(曾经的它真有一个这么牛逼的名字)、一拖十数据线等等。\n\n\n# 基本介绍\n\n * 适配器模式将一个类的接口,转换成客户期望的另外一个接口。适配器让原本接口不兼容的类可以合作无间。也可以叫包装器(Wrapper)\n\n * 适配器模式是一种结构型设计模式, 它能使接口不兼容的对象能够相互合作\n\n * 主要分为两类:类适配器模式、对象适配器模式\n\n\n# 工作原理\n\n * 适配器模式:将一个类的接口转换成另一种接口,让原本接口不兼容的类可以兼容\n * 从用户的角度看不到被适配者,是解耦的\n * 用户调用适配器转化出来的目标接口方法,适配器再调用被适配者的相关接口方法\n * 用户收到反馈结果,感觉只是和目标接口交互\n\n\n# 适配器模式结构\n\n\n# 对象适配器\n\n实现时使用了构成原则: 适配器实现了其中一个对象的接口, 并对另一个对象进行封装。 所有流行的编程语言都可以实现适配器。\n\n适配器设计模式的结构(对象适配器)\n\n * 客户端 (Client) 是包含当前程序业务逻辑的类。\n * 客户端接口 (Target) 描述了其他类与客户端代码合作时必须遵循的协议。\n * 服务 (Service) 中有一些功能类 (通常来自第三方或遗留系统)。 客户端与其接口不兼容, 因此无法直接调用其功能,也可以叫适配者类(Adaptee)。\n * 适配器 (Adapter) 是一个可以同时与客户端和服务交互的类: 它在实现客户端接口的同时封装了服务对象。 适配器接受客户端通过适配器接口发起的调用, 并将其转换为适用于被封装服务对象的调用。\n * 客户端代码只需通过接口与适配器交互即可, 无需与具体的适配器类耦合。 因此, 你可以向程序中添加新类型的适配器而无需修改已有代码。 这在服务类的接口被更改或替换时很有用: 你无需修改客户端代码就可以创建新的适配器类。\n\n\n# Coding\n\n定义客户端使用的接口,与业务相关\n\npublic interface Target {\n\n /*\n * 客户端请求处理的方法\n */\n void request();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n已经存在的接口,这个接口需要配置\n\npublic class Adaptee {\n\n /*\n * 原本存在的方法\n */\n public void specificRequest(){\n //业务代码\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n适配器类\n\npublic class Adapter implements Target {\n\n /*\n * 持有需要被适配的接口对象\n */\n private Adaptee adaptee;\n\n /*\n * 构造方法,传入需要被适配的对象\n * @param adaptee 需要被适配的对象\n */\n public Adapter(Adaptee adaptee) {\n this.adaptee = adaptee;\n }\n\n @Override\n public void request() {\n // TODO Auto-generated method stub\n adaptee.specificRequest();\n }\n\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n\n\n使用适配器的客户端\n\npublic class Client {\n\n public static void main(String[] args) {\n //创建需要被适配的对象\n Adaptee adaptee = new Adaptee();\n //创建客户端需要调用的接口对象\n Target target = new Adapter(adaptee);\n //请求处理\n target.request();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n\n# 类适配器\n\n这一实现使用了继承机制: 适配器同时继承两个对象的接口。 请注意, 这种方式仅能在支持多重继承的编程语言中实现,例如 C++, Java 不支持多重继承,也就没有这种适配器了。\n\n适配器设计模式(类适配器)\n\n类适配器不需要封装任何对象, 因为它同时继承了客户端和服务的行为。 适配功能在重写的方法中完成。 最后生成的适配器可替代已有的客户端类进行使用。\n\n\n# Coding\n\nJava 虽然不能实现标准的类适配器,但是有一种变通的方式,也能够使用继承来实现接口的适配,那就是让适配器去实现 Target 的接口,然后继承 Adaptee 的实现,虽然不是十分标准,但意思差不多。\n\n首先有一个已存在的将被适配的类\n\npublic class Adaptee {\n public void adapteeRequest() {\n System.out.println(\"被适配者的方法\");\n }\n}\n\n\n1\n2\n3\n4\n5\n\n\n定义客户端使用的接口,与业务相关\n\npublic interface Target {\n\n void request();\n}\n\n\n1\n2\n3\n4\n\n\n怎么才可以在目标接口中的 request() 调用 Adaptee 的 adapteeRequest() 方法呢?直接实现 Target 肯定是不行的,所以我们通过一个适配器类,实现 Target 接口,同时继承了 Adaptee 类,然后在实现的 request() 方法中调用父类的 adapteeRequest() 即可\n\npublic class Adapter extends Adaptee implements Target{\n @Override\n public void request() {\n //...一些操作...\n super.adapteeRequest();\n //...一些操作...\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\n使用适配器的客户端\n\npublic class Client {\n public static void main(String[] args) {\n\n Target adapterTarget = new Adapter();\n adapterTarget.request();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 适配器模式适合应用场景\n\n当你希望使用某个类, 但是其接口与其他代码不兼容时, 可以使用适配器类。\n\n适配器模式允许你创建一个中间层类, 其可作为代码与遗留类、 第三方类或提供怪异接口的类之间的转换器。\n\n如果您需要复用这样一些类, 他们处于同一个继承体系, 并且他们又有了额外的一些共同的方法, 但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。\n\n你可以扩展每个子类, 将缺少的功能添加到新的子类中。 但是, 你必须在所有新子类中重复添加这些代码, 这样会使得代码有坏味道。\n\n将缺失功能添加到一个适配器类中是一种优雅得多的解决方案。 然后你可以将缺少功能的对象封装在适配器中, 从而动态地获取所需功能。 如要这一点正常运作, 目标类必须要有通用接口, 适配器的成员变量应当遵循该通用接口。 这种方式同装饰模式非常相似。\n\n\n# demo\n\n用一个生活中的充电器的例子来讲解下适配器,我国民用电都是 220V,而我们的手机充电一般需要 5V。\n\n220V 的交流电相当于被适配者 Adaptee,我们的目标 Target 是 5V 直流电,充电器本身相当于一个 Adapter,将220V 的输入电压变换为 5V 输出。\n\n首先是我们的民用电(我国是 220V,当然还可以有其他国家的其他准备,可随时扩展)\n\npublic class Volatage220V {\n\n public final int output = 220;\n\n public int output220v() {\n System.out.println(\"输出电压 \" + output);\n return output;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n适配接口\n\npublic interface IVoltage5V {\n int output5V();\n}\n\n\n1\n2\n3\n\n\n我们的手机充电,只支持 5V 电压\n\npublic class Phone {\n\n public void charging(IVoltage5V v) {\n if (v.output5V() == 5) {\n System.out.println(\"电压 5V ,符合充电标准,开始充电\");\n } else {\n System.out.println(\"电压不符合标准,无法充电\");\n }\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n适配器\n\npublic class VoltageAdapter implements IVoltage5V {\n\n private Volatage220V volatage220V; //聚合\n\n public VoltageAdapter(Volatage220V v) {\n this.volatage220V = v;\n }\n\n @Override\n public int output5V() {\n int dst = 0;\n if (null != volatage220V) {\n int src = volatage220V.output220v();\n System.out.println(\"适配器工作~~~~~\");\n dst = src / 44;\n System.out.println(\"适配器工作完成,输出电压\" + dst);\n }\n return dst;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\n工作,如果去国外旅游,有不同的电压,只需要扩展适配器即可。\n\npublic class Client {\n public static void main(String[] args) {\n Phone phone = new Phone();\n phone.charging(new VoltageAdapter(new Volatage220V()));\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n\n# 适配器模式优缺点\n\n单一职责原则,你可以将接口或数据转换代码从程序主要业务逻辑中分离。\n\n开闭原则。 只要客户端代码通过客户端接口与适配器进行交互, 你就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。\n\n代码整体复杂度增加, 因为你需要新增一系列接口和类。 有时直接更改服务类使其与其他代码兼容会更简单。\n\n\n# Spring 中的适配器\n\nSpring 源码中搜关键字Adapter 会出现很多实现类,SpringMVC 中的 HandlerAdapter ,就是适配器的应用。\n\n我们先回顾下 SpringMVC 处理流程:\n\nSpring MVC 中的适配器模式主要用于执行目标 Controller 中的请求处理方法。\n\n在Spring MVC中,DispatcherServlet 作为用户,HandlerAdapter 作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller 作为需要适配的类。\n\n为什么要在 Spring MVC 中使用适配器模式?Spring MVC 中的 Controller 种类众多,不同类型的 Controller 通过不同的方法来对请求进行处理。如果不利用适配器模式的话,DispatcherServlet 直接获取对应类型的 Controller,需要的自行来判断,像下面这段代码一样:\n\nif(mappedHandler.getHandler() instanceof MultiActionController){ \n ((MultiActionController)mappedHandler.getHandler()).xxx \n}else if(mappedHandler.getHandler() instanceof XXX){ \n ... \n}else if(...){ \n ... \n} \n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n这样假设如果我们增加一个 Controller,就要在代码中加入一行 if 语句,这种形式就使得程序难以维护,也违反了设计模式中的开闭原则 – 对扩展开放,对修改关闭。\n\n我们通过源码看看 SpringMVC 是如何实现的,首先看下核心类 DispatcherServlet:\n\npublic class DispatcherServlet extends FrameworkServlet {\n \t//......\n\t//维护所有HandlerAdapter类的集合\n @Nullable\n private List handlerAdapters;\n \n\t//初始化handlerAdapters\n private void initHandlerAdapters(ApplicationContext context) {\n this.handlerAdapters = null;\n if (this.detectAllHandlerAdapters) {\n Map matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);\n if (!matchingBeans.isEmpty()) {\n this.handlerAdapters = new ArrayList(matchingBeans.values());\n AnnotationAwareOrderComparator.sort(this.handlerAdapters);\n }\n } else {\n try {\n HandlerAdapter ha = (HandlerAdapter)context.getBean(\"handlerAdapter\", HandlerAdapter.class);\n this.handlerAdapters = Collections.singletonList(ha);\n } catch (NoSuchBeanDefinitionException var3) {\n }\n }\n\n if (this.handlerAdapters == null) {\n this.handlerAdapters = this.getDefaultStrategies(context, HandlerAdapter.class);\n if (this.logger.isTraceEnabled()) {\n this.logger.trace(\"No HandlerAdapters declared for servlet '\" + this.getServletName() + \"': using default strategies from DispatcherServlet.properties\");\n }\n }\n }\n \n //dispatch 方法中会获取 HandlerAdapter\n protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {\n //...\n \n\t\t//获得controller对应的适配器\n HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler()); \n\t\t\t\t\n\t\t//调用适配器的handler方法处理请求,并返回ModelAndView\n mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); \n //...\n }\n \t\n\t //返回对应的controller的处理器\n protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {\n if (this.handlerAdapters != null) {\n Iterator var2 = this.handlerAdapters.iterator();\n\n while(var2.hasNext()) {\n HandlerAdapter adapter = (HandlerAdapter)var2.next();\n if (adapter.supports(handler)) {\n return adapter;\n }\n }\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n\n\n接着看下 HandlerAdapter 的源码,也就是适配器接口:\n\npublic interface HandlerAdapter {\n boolean supports(Object var1);\n\n @Nullable\n ModelAndView handle(HttpServletRequest var1, HttpServletResponse var2, Object var3) throws Exception;\n\n long getLastModified(HttpServletRequest var1, Object var2);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\n再来屡一下这个流程:\n\n首先是适配器接口 DispatchServlet 中有一个集合维护所有的 HandlerAdapter,如果配置文件中没有对适配器进行配置,那么 DispatchServlet 会在创建时对该变量进行初始化,注册所有默认的 HandlerAdapter。\n\n当一个请求过来时,DispatchServlet 会根据传过来的 handler 类型从该集合中寻找对应的 HandlerAdapter子类进行处理,并且调用它的 handler() 方法 对应的 HandlerAdapter 中的 handler() 方法又会执行对应 Controller 的 handleRequest() 方法\n\n适配器与 handler 有对应关系,而各个适配器又都是适配器接口的实现类,因此,它们都遵循相同的适配器标准,所以用户可以按照相同的方式,通过不同的 handler 去处理请求。\n\n当然了,Spring 框架中也为我们定义了一些默认的 Handler 对应的适配器。\n\n通过适配器模式我们将所有的 controller 统一交给 HandlerAdapter 处理,免去了写大量的 if-else 语句对 Controller 进行判断,也更利于扩展新的 Controller 类型。\n\n参考: 随遇而安的适配器模式 | Spring 中的适配器", + "normalizedContent": "# 问题\n\n假设我们在做一套股票看盘系统,数据提供方给我们提供 xml 格式数据,我们获取数据用来显示,随着系统的迭代,我们要整合一些第三方系统的对外数据,但是他们只提供获取 json 格式的数据接口。\n\n在不想改变原有代码逻辑的情况下,如何解决呢?\n\n这时候我们就可以创建一个「适配器」。这是一个特殊的对象, 能够转换对象接口, 使其能与其他对象进行交互。\n\n适配器模式通过封装对象将复杂的转换过程隐藏于幕后。 被封装的对象甚至察觉不到适配器的存在。\n\n\n\n\n# 真实世界类比\n\n适配器是什么,不难理解,生活中也随处可见。比如,笔记本电脑的电源适配器、万能充(曾经的它真有一个这么牛逼的名字)、一拖十数据线等等。\n\n\n# 基本介绍\n\n * 适配器模式将一个类的接口,转换成客户期望的另外一个接口。适配器让原本接口不兼容的类可以合作无间。也可以叫包装器(wrapper)\n\n * 适配器模式是一种结构型设计模式, 它能使接口不兼容的对象能够相互合作\n\n * 主要分为两类:类适配器模式、对象适配器模式\n\n\n# 工作原理\n\n * 适配器模式:将一个类的接口转换成另一种接口,让原本接口不兼容的类可以兼容\n * 从用户的角度看不到被适配者,是解耦的\n * 用户调用适配器转化出来的目标接口方法,适配器再调用被适配者的相关接口方法\n * 用户收到反馈结果,感觉只是和目标接口交互\n\n\n# 适配器模式结构\n\n\n# 对象适配器\n\n实现时使用了构成原则: 适配器实现了其中一个对象的接口, 并对另一个对象进行封装。 所有流行的编程语言都可以实现适配器。\n\n适配器设计模式的结构(对象适配器)\n\n * 客户端 (client) 是包含当前程序业务逻辑的类。\n * 客户端接口 (target) 描述了其他类与客户端代码合作时必须遵循的协议。\n * 服务 (service) 中有一些功能类 (通常来自第三方或遗留系统)。 客户端与其接口不兼容, 因此无法直接调用其功能,也可以叫适配者类(adaptee)。\n * 适配器 (adapter) 是一个可以同时与客户端和服务交互的类: 它在实现客户端接口的同时封装了服务对象。 适配器接受客户端通过适配器接口发起的调用, 并将其转换为适用于被封装服务对象的调用。\n * 客户端代码只需通过接口与适配器交互即可, 无需与具体的适配器类耦合。 因此, 你可以向程序中添加新类型的适配器而无需修改已有代码。 这在服务类的接口被更改或替换时很有用: 你无需修改客户端代码就可以创建新的适配器类。\n\n\n# coding\n\n定义客户端使用的接口,与业务相关\n\npublic interface target {\n\n /*\n * 客户端请求处理的方法\n */\n void request();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n已经存在的接口,这个接口需要配置\n\npublic class adaptee {\n\n /*\n * 原本存在的方法\n */\n public void specificrequest(){\n //业务代码\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n适配器类\n\npublic class adapter implements target {\n\n /*\n * 持有需要被适配的接口对象\n */\n private adaptee adaptee;\n\n /*\n * 构造方法,传入需要被适配的对象\n * @param adaptee 需要被适配的对象\n */\n public adapter(adaptee adaptee) {\n this.adaptee = adaptee;\n }\n\n @override\n public void request() {\n // todo auto-generated method stub\n adaptee.specificrequest();\n }\n\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n\n\n使用适配器的客户端\n\npublic class client {\n\n public static void main(string[] args) {\n //创建需要被适配的对象\n adaptee adaptee = new adaptee();\n //创建客户端需要调用的接口对象\n target target = new adapter(adaptee);\n //请求处理\n target.request();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n\n# 类适配器\n\n这一实现使用了继承机制: 适配器同时继承两个对象的接口。 请注意, 这种方式仅能在支持多重继承的编程语言中实现,例如 c++, java 不支持多重继承,也就没有这种适配器了。\n\n适配器设计模式(类适配器)\n\n类适配器不需要封装任何对象, 因为它同时继承了客户端和服务的行为。 适配功能在重写的方法中完成。 最后生成的适配器可替代已有的客户端类进行使用。\n\n\n# coding\n\njava 虽然不能实现标准的类适配器,但是有一种变通的方式,也能够使用继承来实现接口的适配,那就是让适配器去实现 target 的接口,然后继承 adaptee 的实现,虽然不是十分标准,但意思差不多。\n\n首先有一个已存在的将被适配的类\n\npublic class adaptee {\n public void adapteerequest() {\n system.out.println(\"被适配者的方法\");\n }\n}\n\n\n1\n2\n3\n4\n5\n\n\n定义客户端使用的接口,与业务相关\n\npublic interface target {\n\n void request();\n}\n\n\n1\n2\n3\n4\n\n\n怎么才可以在目标接口中的 request() 调用 adaptee 的 adapteerequest() 方法呢?直接实现 target 肯定是不行的,所以我们通过一个适配器类,实现 target 接口,同时继承了 adaptee 类,然后在实现的 request() 方法中调用父类的 adapteerequest() 即可\n\npublic class adapter extends adaptee implements target{\n @override\n public void request() {\n //...一些操作...\n super.adapteerequest();\n //...一些操作...\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\n使用适配器的客户端\n\npublic class client {\n public static void main(string[] args) {\n\n target adaptertarget = new adapter();\n adaptertarget.request();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 适配器模式适合应用场景\n\n当你希望使用某个类, 但是其接口与其他代码不兼容时, 可以使用适配器类。\n\n适配器模式允许你创建一个中间层类, 其可作为代码与遗留类、 第三方类或提供怪异接口的类之间的转换器。\n\n如果您需要复用这样一些类, 他们处于同一个继承体系, 并且他们又有了额外的一些共同的方法, 但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。\n\n你可以扩展每个子类, 将缺少的功能添加到新的子类中。 但是, 你必须在所有新子类中重复添加这些代码, 这样会使得代码有坏味道。\n\n将缺失功能添加到一个适配器类中是一种优雅得多的解决方案。 然后你可以将缺少功能的对象封装在适配器中, 从而动态地获取所需功能。 如要这一点正常运作, 目标类必须要有通用接口, 适配器的成员变量应当遵循该通用接口。 这种方式同装饰模式非常相似。\n\n\n# demo\n\n用一个生活中的充电器的例子来讲解下适配器,我国民用电都是 220v,而我们的手机充电一般需要 5v。\n\n220v 的交流电相当于被适配者 adaptee,我们的目标 target 是 5v 直流电,充电器本身相当于一个 adapter,将220v 的输入电压变换为 5v 输出。\n\n首先是我们的民用电(我国是 220v,当然还可以有其他国家的其他准备,可随时扩展)\n\npublic class volatage220v {\n\n public final int output = 220;\n\n public int output220v() {\n system.out.println(\"输出电压 \" + output);\n return output;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n适配接口\n\npublic interface ivoltage5v {\n int output5v();\n}\n\n\n1\n2\n3\n\n\n我们的手机充电,只支持 5v 电压\n\npublic class phone {\n\n public void charging(ivoltage5v v) {\n if (v.output5v() == 5) {\n system.out.println(\"电压 5v ,符合充电标准,开始充电\");\n } else {\n system.out.println(\"电压不符合标准,无法充电\");\n }\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n适配器\n\npublic class voltageadapter implements ivoltage5v {\n\n private volatage220v volatage220v; //聚合\n\n public voltageadapter(volatage220v v) {\n this.volatage220v = v;\n }\n\n @override\n public int output5v() {\n int dst = 0;\n if (null != volatage220v) {\n int src = volatage220v.output220v();\n system.out.println(\"适配器工作~~~~~\");\n dst = src / 44;\n system.out.println(\"适配器工作完成,输出电压\" + dst);\n }\n return dst;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\n工作,如果去国外旅游,有不同的电压,只需要扩展适配器即可。\n\npublic class client {\n public static void main(string[] args) {\n phone phone = new phone();\n phone.charging(new voltageadapter(new volatage220v()));\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n\n# 适配器模式优缺点\n\n单一职责原则,你可以将接口或数据转换代码从程序主要业务逻辑中分离。\n\n开闭原则。 只要客户端代码通过客户端接口与适配器进行交互, 你就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。\n\n代码整体复杂度增加, 因为你需要新增一系列接口和类。 有时直接更改服务类使其与其他代码兼容会更简单。\n\n\n# spring 中的适配器\n\nspring 源码中搜关键字adapter 会出现很多实现类,springmvc 中的 handleradapter ,就是适配器的应用。\n\n我们先回顾下 springmvc 处理流程:\n\nspring mvc 中的适配器模式主要用于执行目标 controller 中的请求处理方法。\n\n在spring mvc中,dispatcherservlet 作为用户,handleradapter 作为期望接口,具体的适配器实现类用于对目标类进行适配,controller 作为需要适配的类。\n\n为什么要在 spring mvc 中使用适配器模式?spring mvc 中的 controller 种类众多,不同类型的 controller 通过不同的方法来对请求进行处理。如果不利用适配器模式的话,dispatcherservlet 直接获取对应类型的 controller,需要的自行来判断,像下面这段代码一样:\n\nif(mappedhandler.gethandler() instanceof multiactioncontroller){ \n ((multiactioncontroller)mappedhandler.gethandler()).xxx \n}else if(mappedhandler.gethandler() instanceof xxx){ \n ... \n}else if(...){ \n ... \n} \n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n这样假设如果我们增加一个 controller,就要在代码中加入一行 if 语句,这种形式就使得程序难以维护,也违反了设计模式中的开闭原则 – 对扩展开放,对修改关闭。\n\n我们通过源码看看 springmvc 是如何实现的,首先看下核心类 dispatcherservlet:\n\npublic class dispatcherservlet extends frameworkservlet {\n \t//......\n\t//维护所有handleradapter类的集合\n @nullable\n private list handleradapters;\n \n\t//初始化handleradapters\n private void inithandleradapters(applicationcontext context) {\n this.handleradapters = null;\n if (this.detectallhandleradapters) {\n map matchingbeans = beanfactoryutils.beansoftypeincludingancestors(context, handleradapter.class, true, false);\n if (!matchingbeans.isempty()) {\n this.handleradapters = new arraylist(matchingbeans.values());\n annotationawareordercomparator.sort(this.handleradapters);\n }\n } else {\n try {\n handleradapter ha = (handleradapter)context.getbean(\"handleradapter\", handleradapter.class);\n this.handleradapters = collections.singletonlist(ha);\n } catch (nosuchbeandefinitionexception var3) {\n }\n }\n\n if (this.handleradapters == null) {\n this.handleradapters = this.getdefaultstrategies(context, handleradapter.class);\n if (this.logger.istraceenabled()) {\n this.logger.trace(\"no handleradapters declared for servlet '\" + this.getservletname() + \"': using default strategies from dispatcherservlet.properties\");\n }\n }\n }\n \n //dispatch 方法中会获取 handleradapter\n protected void dodispatch(httpservletrequest request, httpservletresponse response) throws exception {\n //...\n \n\t\t//获得controller对应的适配器\n handleradapter ha = this.gethandleradapter(mappedhandler.gethandler()); \n\t\t\t\t\n\t\t//调用适配器的handler方法处理请求,并返回modelandview\n mv = ha.handle(processedrequest, response, mappedhandler.gethandler()); \n //...\n }\n \t\n\t //返回对应的controller的处理器\n protected handleradapter gethandleradapter(object handler) throws servletexception {\n if (this.handleradapters != null) {\n iterator var2 = this.handleradapters.iterator();\n\n while(var2.hasnext()) {\n handleradapter adapter = (handleradapter)var2.next();\n if (adapter.supports(handler)) {\n return adapter;\n }\n }\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n\n\n接着看下 handleradapter 的源码,也就是适配器接口:\n\npublic interface handleradapter {\n boolean supports(object var1);\n\n @nullable\n modelandview handle(httpservletrequest var1, httpservletresponse var2, object var3) throws exception;\n\n long getlastmodified(httpservletrequest var1, object var2);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\n再来屡一下这个流程:\n\n首先是适配器接口 dispatchservlet 中有一个集合维护所有的 handleradapter,如果配置文件中没有对适配器进行配置,那么 dispatchservlet 会在创建时对该变量进行初始化,注册所有默认的 handleradapter。\n\n当一个请求过来时,dispatchservlet 会根据传过来的 handler 类型从该集合中寻找对应的 handleradapter子类进行处理,并且调用它的 handler() 方法 对应的 handleradapter 中的 handler() 方法又会执行对应 controller 的 handlerequest() 方法\n\n适配器与 handler 有对应关系,而各个适配器又都是适配器接口的实现类,因此,它们都遵循相同的适配器标准,所以用户可以按照相同的方式,通过不同的 handler 去处理请求。\n\n当然了,spring 框架中也为我们定义了一些默认的 handler 对应的适配器。\n\n通过适配器模式我们将所有的 controller 统一交给 handleradapter 处理,免去了写大量的 if-else 语句对 controller 进行判断,也更利于扩展新的 controller 类型。\n\n参考: 随遇而安的适配器模式 | spring 中的适配器", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "工厂模式使用场景 能举个例子吗", + "frontmatter": { + "title": "工厂模式使用场景 能举个例子吗", + "date": "2022-05-21T17:26:32.000Z", + "permalink": "/pages/f15489/", + "categories": [ + "Java相关", + "设计模式" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/004.%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF%20%E8%83%BD%E4%B8%BE%E4%B8%AA%E4%BE%8B%E5%AD%90%E5%90%97.html", + "relativePath": "01.Java相关/40.设计模式/004.工厂模式使用场景 能举个例子吗.md", + "key": "v-2d6ed001", + "path": "/pages/f15489/", + "headers": [ + { + "level": 2, + "title": "工厂模式", + "slug": "工厂模式", + "normalizedTitle": "工厂模式", + "charIndex": 15 + }, + { + "level": 3, + "title": "模式动机", + "slug": "模式动机", + "normalizedTitle": "模式动机", + "charIndex": 195 + }, + { + "level": 3, "title": "模式定义", "slug": "模式定义", "normalizedTitle": "模式定义", - "charIndex": 403 + "charIndex": 406 }, { - "level": 2, + "level": 3, "title": "模式结构", "slug": "模式结构", "normalizedTitle": "模式结构", - "charIndex": 559 + "charIndex": 562 }, { - "level": 2, + "level": 3, "title": "模式分析", "slug": "模式分析", "normalizedTitle": "模式分析", - "charIndex": 737 + "charIndex": 740 + }, + { + "level": 2, + "title": "工厂模式可以分为三类:", + "slug": "工厂模式可以分为三类", + "normalizedTitle": "工厂模式可以分为三类:", + "charIndex": 1026 }, { "level": 2, + "title": "一、简单工厂模式", + "slug": "一、简单工厂模式", + "normalizedTitle": "一、简单工厂模式", + "charIndex": 2247 + }, + { + "level": 3, "title": "定义", "slug": "定义", "normalizedTitle": "定义", - "charIndex": 405 + "charIndex": 408 }, { - "level": 2, + "level": 3, "title": "简单工厂模式实现方式", "slug": "简单工厂模式实现方式", "normalizedTitle": "简单工厂模式实现方式", - "charIndex": 3742 + "charIndex": 4042 + }, + { + "level": 2, + "title": "UML类图", + "slug": "uml类图", + "normalizedTitle": "uml类图", + "charIndex": 4353 }, { "level": 2, + "title": "实例", + "slug": "实例", + "normalizedTitle": "实例", + "charIndex": 512 + }, + { + "level": 3, "title": "简单工厂模式存在的问题", "slug": "简单工厂模式存在的问题", "normalizedTitle": "简单工厂模式存在的问题", - "charIndex": 5167 + "charIndex": 5475 }, { - "level": 2, + "level": 3, "title": "简单工厂模式总结", "slug": "简单工厂模式总结", "normalizedTitle": "简单工厂模式总结", - "charIndex": 5373 + "charIndex": 5681 }, { "level": 2, + "title": "二、工厂方法模式", + "slug": "二、工厂方法模式", + "normalizedTitle": "二、工厂方法模式", + "charIndex": 6056 + }, + { + "level": 3, "title": "2.1 定义", "slug": "_2-1-定义", "normalizedTitle": "2.1 定义", - "charIndex": 5803 + "charIndex": 6114 + }, + { + "level": 3, + "title": "2.2 工厂方法模式实现方式", + "slug": "_2-2-工厂方法模式实现方式", + "normalizedTitle": "2.2 工厂方法模式实现方式", + "charIndex": 6178 + }, + { + "level": 2, + "title": "UML类图", + "slug": "uml类图-2", + "normalizedTitle": "uml类图", + "charIndex": 4353 + }, + { + "level": 2, + "title": "实例", + "slug": "实例-2", + "normalizedTitle": "实例", + "charIndex": 512 + }, + { + "level": 3, + "title": "2.3 工厂方法模式适用场景", + "slug": "_2-3-工厂方法模式适用场景", + "normalizedTitle": "2.3 工厂方法模式适用场景", + "charIndex": 7923 + }, + { + "level": 3, + "title": "2.4 工厂方法模式总结", + "slug": "_2-4-工厂方法模式总结", + "normalizedTitle": "2.4 工厂方法模式总结", + "charIndex": 8499 + }, + { + "level": 2, + "title": "三、抽象工厂模式", + "slug": "三、抽象工厂模式", + "normalizedTitle": "三、抽象工厂模式", + "charIndex": 8875 + }, + { + "level": 3, + "title": "3.1 定义", + "slug": "_3-1-定义", + "normalizedTitle": "3.1 定义", + "charIndex": 9033 + }, + { + "level": 3, + "title": "3.2 抽象工厂模式实现方式", + "slug": "_3-2-抽象工厂模式实现方式", + "normalizedTitle": "3.2 抽象工厂模式实现方式", + "charIndex": 9130 + }, + { + "level": 2, + "title": "UML类图", + "slug": "uml类图-3", + "normalizedTitle": "uml类图", + "charIndex": 4353 + }, + { + "level": 2, + "title": "实例", + "slug": "实例-3", + "normalizedTitle": "实例", + "charIndex": 512 + }, + { + "level": 2, + "title": "3.3 抽象工厂模式适用场景", + "slug": "_3-3-抽象工厂模式适用场景", + "normalizedTitle": "3.3 抽象工厂模式适用场景", + "charIndex": 11705 + }, + { + "level": 2, + "title": "3.4 抽象工厂模式总结", + "slug": "_3-4-抽象工厂模式总结", + "normalizedTitle": "3.4 抽象工厂模式总结", + "charIndex": 12519 + }, + { + "level": 2, + "title": "工厂模式的退化", + "slug": "工厂模式的退化", + "normalizedTitle": "工厂模式的退化", + "charIndex": 12987 + }, + { + "level": 2, + "title": "四、我们身边的工厂模式", + "slug": "四、我们身边的工厂模式", + "normalizedTitle": "四、我们身边的工厂模式", + "charIndex": 13132 } ], - "headersStr": "模式动机 模式定义 模式结构 模式分析 定义 简单工厂模式实现方式 简单工厂模式存在的问题 简单工厂模式总结 2.1 定义", - "content": "> 3年工作经验是吧? 你知道工厂模式分为几类吗?能举例子吗? 那你说说你们项目中是怎么使用工厂模式的?\n\n#工厂模式 工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。\n\n在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。\n\n\n# 模式动机\n\n考虑一个简单的软件应用场景,一个软件系统可以提供多个外观不同的按钮(如圆形按钮、矩形按钮、菱形按钮等), 这些按钮都源自同一个基类,不过在继承基类后不同的子类修改了部分属性从而使得它们可以呈现不同的外观,如果我们希望在使用这些按钮时,不需要知道这些具体按钮类的名字,只需要知道表示该按钮类的一个参数,并提供一个调用方便的方法,把该参数传入方法即可返回一个相应的按钮对象,此时,就可以使用简单工厂模式。\n\n\n# 模式定义\n\n简单工厂模式(Simple Factory Pattern):又称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。\n\n\n# 模式结构\n\n简单工厂模式包含如下角色:\n\nFactory:工厂角色\n工厂角色负责实现创建所有实例的内部逻辑 Product:抽象产品角色\n抽象产品角色是所创建的所有对象的父类,负责描述所有实例所共有的公共接口 ConcreteProduct:具体产品角色\n具体产品角色是创建目标,所有创建的对象都充当这个角色的某个具体类的实例。 1.4. 时序图\n\n\n# 模式分析\n\n将对象的创建和对象本身业务处理分离可以降低系统的耦合度,使得两者修改起来都相对容易。\n\n在调用工厂类的工厂方法时,由于工厂方法是静态方法,使用起来很方便,可通过类名直接调用,而且只需要传入一个简单的参数即可.\n\n在实际开发中,还可以在调用时将所传入的参数保存在XML等格式的配置文件中,修改参数时无须修改任何源代码。\n\n简单工厂模式最大的问题在于工厂类的职责相对过重,增加新的产品需要修改工厂类的判断逻辑,这一点与开闭原则是相违背的。\n\n简单工厂模式的要点在于:当你需要什么,只需要传入一个正确的参数,就可以获取你所需要的对象,而无须知道其创建细节。\n\n#工厂模式可以分为三类: 简单工厂模式(Simple Factory) 工厂方法模式(Factory Method) 抽象工厂模式(Abstract Factory) 简单工厂其实不是一个标准的的设计模式。GOF 23种设计模式中只有「工厂方法模式」与「抽象工厂模式」。简单工厂模式可以看为工厂方法模式的一种特例,为了统一整理学习,就都归为工厂模式。\n\n这三种工厂模式在设计模式的分类中都属于创建型模式,三种模式从上到下逐步抽象。\n\n创建型模式 创建型模式(Creational Pattern)对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。为了使软件的结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。\n\n创建型模式在创建什么(What),由谁创建(Who),何时创建(When)等方面都为软件设计者提供了尽可能大的灵活性。\n\n创建型模式隐藏了类的实例的创建细节,通过隐藏对象如何被创建和组合在一起达到使整个系统独立的目的。\n\n工厂模式是创建型模式中比较重要的。工厂模式的主要功能就是帮助我们实例化对象。之所以名字中包含工厂模式四个字,是因为对象的实例化过程是通过工厂实现的,是用工厂代替new操作的。\n\n工厂模式优点 可以使代码结构清晰,有效地封装变化。在编程中,产品类的实例化有时候是比较复杂和多变的,通过工厂模式,将产品的实例化封装起来,使得调用者根本无需关心产品的实例化过程,只需依赖工厂即可得到自己想要的产品。 对调用者屏蔽具体的产品类。如果使用工厂模式,调用者只关心产品的接口就可以了,至于具体的实现,调用者根本无需关心。即使变更了具体的实现,对调用者来说没有任何影响。 降低耦合度。产品类的实例化通常来说是很复杂的,它需要依赖很多的类,而这些类对于调用者来说根本无需知道,如果使用了工厂方法,我们需要做的仅仅是实例化好产品类,然后交给调用者使用。对调用者来说,产品所依赖的类都是透明的。 适用场景 不管是简单工厂模式,工厂方法模式还是抽象工厂模式,他们具有类似的特性,所以他们的适用场景也是类似的。\n\n首先,作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过new就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。\n\n其次,工厂模式是一种典型的解耦模式,迪米特法则在工厂模式中表现的尤为明显。假如调用者自己组装产品需要增加依赖关系时,可以考虑使用工厂模式。将会大大降低对象之间的耦合度。\n\n再次,由于工厂模式是依靠抽象架构的,它把实例化产品的任务交由实现类完成,扩展性比较好。也就是说,当需要系统有比较好的扩展性时,可以考虑工厂模式,不同的产品用不同的实现工厂来组装。\n\n#一、简单工厂模式 在介绍简单工厂模式之前,我们尝试解决以下问题:\n\n现在我们要使用面向对象的形式定义计算器,为了实现各算法之间的解耦。我们一般会这么写:\n\n// 计算类的基类 @Setter @Getter public abstract class Operation { private double value1 = 0; private double value2 = 0; protected abstract double getResule(); }\n\n//加法 public class OperationAdd extends Operation { @Override protected double getResule() { return getValue1() + getValue2(); } } //减法 public class OperationSub extends Operation { @Override protected double getResule() { return getValue1() - getValue2(); } } //乘法 public class OperationMul extends Operation { @Override protected double getResule() { return getValue1() * getValue2(); } } //除法 public class OperationDiv extends Operation { @Override protected double getResule() { if (getValue2() != 0) { return getValue1() / getValue2(); } throw new IllegalArgumentException(\"除数不能为零\"); } } 当我们要使用这个计算器的时候,又会这么写:\n\npublic static void main(String[] args) { //计算两数之和 OperationAdd operationAdd = new OperationAdd(); operationAdd.setValue1(1); operationAdd.setValue2(2); System.out.println(\"sum:\"+operationAdd.getResule()); //计算两数乘积 OperationMul operationMul = new OperationMul(); operationMul.setValue1(3); operationMul.setValue2(5); System.out.println(\"multiply:\"+operationMul.getResule()); //计算两数之差。。。 } 想要使用不同的运算的时候就要创建不同的类,并且要明确知道该类的名字。那么这种重复的创建类的工作其实可以放到一个统一的类中去管理。这样的方法我们就叫做「简单工厂模式」,在简单工厂模式中用于创建实例的方法是静态(static)方法,因此简单工厂模式又被称为「静态工厂方法」模式。。简单工厂模式有以下优点:\n\n一个调用者想创建一个对象,只要知道其名称就可以了。 屏蔽产品的具体实现,调用者只关心产品的接口。\n\n\n# 定义\n\n提供一个创建对象实例的功能,而无需关心其具体实现。被创建实例的类型可以是接口、抽象类,也可以是具体的类。\n\n\n# 简单工厂模式实现方式\n\n没骗你,简单工厂模式,真是因为简单才被叫做简单工厂模式的。\n\n简单工厂模式包含 3 个角色(要素):\n\nFactory:即工厂类, 简单工厂模式的核心部分,负责实现创建所有产品的内部逻辑;工厂类可以被外界直接调用,创建所需对象 Product:抽象类产品, 它是工厂类所创建的所有对象的父类,封装了各种产品对象的公有方法,它的引入将提高系统的灵活性,使得在工厂类中只需定义一个通用的工厂方法,因为所有创建的具体产品对象都是其子类对象 ConcreteProduct:具体产品, 它是简单工厂模式的创建目标,所有被创建的对象都充当这个角色的某个具体类的实例。它要实现抽象产品中声明的抽象方法 #UML类图\n\n#实例 现在我们定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。\n\n//工厂类\npublic class OperationFactory {\n\n public static Operation createOperation(String operation) {\n Operation oper = null;\n switch (operation) {\n case \"add\":\n oper = new OperationAdd();\n break;\n case \"sub\":\n oper = new OperationSub();\n break;\n case \"mul\":\n oper = new OperationMul();\n break;\n\n case \"div\":\n oper = new OperationDiv();\n break;\n default:\n throw new UnsupportedOperationException(\"不支持该操作\");\n }\n return oper;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n\n\n有了工厂类之后,可以使用工厂创建对象:\n\npublic static void main(String[] args) {\n Operation operationAdd = OperationFactory.createOperation(\"add\");\n operationAdd.setValue1(1);\n operationAdd.setValue2(2)\n System.out.println(operationAdd.getResule());\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n通过简单工厂模式,该计算器的使用者不需要关系实现加法逻辑的那个类的具体名字,只要知道该类对应的参数\"add\"就可以了。这就体现了之前提到的工厂模式的优点。\n\n\n# 简单工厂模式存在的问题\n\n当我们需要增加一种计算时,例如开平方。这个时候我们需要先定义一个类继承Operation类,其中实现平方的代码。除此之外我们还要修改 OperationFactory 类的代码,增加一个 case。这显然是违背开闭原则的。可想而知对于新产品的加入,工厂类是很被动的。\n\n我们举的例子是最简单的情况。而在实际应用中,很可能产品是一个多层次的树状结构。 简单工厂可能就不太适用了。\n\n\n# 简单工厂模式总结\n\n工厂类是整个简单工厂模式的关键。包含了必要的逻辑判断,根据外界给定的信息,决定究竟应该创建哪个具体类的对象。通过使用工厂类,外界可以从直接创建具体产品对象的尴尬局面摆脱出来,仅仅需要负责“消费”对象就可以了。而不必管这些对象究竟如何创建及如何组织的。明确了各自的职责和权利,有利于整个软件体系结构的优化。\n\n但是由于工厂类集中了所有实例的创建逻辑,违反了高内聚责任分配原则,将全部创建逻辑集中到了一个工厂类中;它所能创建的类只能是事先考虑到的,如果需要添加新的类,则就需要改变工厂类了。\n\n当系统中的具体产品类不断增多时候,可能会出现要求工厂类根据不同条件创建不同实例的需求.这种对条件的判断和对具体产品类型的判断交错在一起,很难避免模块功能的蔓延,对系统的维护和扩展非常不利;\n\n为了解决这些缺点,就有了工厂方法模式。\n\n#二、工厂方法模式 我们常说的工厂模式,就是指「工厂方法模式」,也叫「虚拟构造器模式」或「多态工厂模式」。\n\n\n# 2.1 定义\n\n定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类。工厂方法让类的实例化推迟到子类中进行。\n\n##2.2 工厂方法模式实现方式 工厂方法模式包含 4 个角色(要素):\n\nProduct:抽象产品,定义工厂方法所创建的对象的接口,也就是实际需要使用的对象的接口 ConcreteProduct:具体产品,具体的Product接口的实现对象 Factory:工厂接口,也可以叫 Creator(创建器),申明工厂方法,通常返回一个 Product 类型的实例对象 ConcreteFactory:工厂实现,或者叫 ConcreteCreator(创建器对象),覆盖 Factory 定义的工厂方法,返回具体的 Product 实例 #UML类图\n\n#实例 从 UML 类图可以看出,每种产品实现,我们都要增加一个继承于工厂接口 IFactory 的工厂类 Factory ,修改简单工厂模式代码中的工厂类如下:\n\n//工厂接口\npublic interface IFactory {\n Operation CreateOption();\n}\n\n//加法类工厂\npublic class AddFactory implements IFactory {\n public Operation CreateOption() {\n return new OperationAdd();\n }\n}\n\n//减法类工厂\npublic class SubFactory implements IFactory {\n public Operation CreateOption() {\n return new OperationSub();\n }\n}\n\n//乘法类工厂\npublic class MulFactory implements IFactory {\n public Operation CreateOption() {\n return new OperationMul();\n }\n}\n\n//除法类工厂\npublic class DivFactory implements IFactory {\n public Operation CreateOption() {\n return new OperationDiv();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n\n\n这时,我们使用计算器的时候,要为每种运算方法增加一个工厂对象\n\npublic class Client {\n public static void main(String[] args) {\n //减法\n IFactory subFactory = new SubFactory();\n Operation operationSub = subFactory.CreateOption();\n operationSub.setValue1(22);\n operationSub.setValue2(20);\n System.out.println(\"sub:\"+operationSub.getResult());\n //除法\n IFactory Divfactory = new DivFactory();\n Operation operationDiv = Divfactory.CreateOption();\n operationDiv.setValue1(99);\n operationDiv.setValue2(33);\n System.out.println(\"div:\"+operationSub.getResult());\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\n纳尼,这不是更复杂了吗,每个产品对应一个工厂,我又不是按代码量赚钱的。。。\n\n##2.3 工厂方法模式适用场景 工厂方法模式和简单工厂模式虽然都是通过工厂来创建对象,他们之间最大的不同是——工厂方法模式在设计上完全完全符合“开闭原则”。\n\n在以下情况下可以使用工厂方法模式:\n\n一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。 一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。 将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定,可将具体工厂类的类名存储在配置文件或数据库中。 使用场景 日志记录器:日志可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方。 数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。 设计一个连接服务器的框架,需要三个协议,\"POP3\"、\"IMAP\"、\"HTTP\",可以把这三个作为产品类,共同实现一个接口。 比如 Hibernate 换数据库只需换方言和驱动就可以 ##2.4 工厂方法模式总结 工厂方法模式是简单工厂模式的进一步抽象和推广。\n\n由于使用了面向对象的多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。\n\n在工厂方法模式中,核心的工厂类不再负责所有产品的创建,而是将具体创建工作交给子类去做。这个核心类仅仅负责给出具体工厂必须实现的接口,而不负责产品类被实例化这种细节,这使得工厂方法模式可以允许系统在不修改工厂角色的情况下引进新产品。\n\n优点:\n\n一个调用者想创建一个对象,只要知道其名称就可以了。 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。 屏蔽产品的具体实现,调用者只关心产品的接口。 缺点:\n\n每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。\n\n#三、抽象工厂模式 工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。此时,我们可以考虑将一些相关的产品组成一个“产品族”,由同一个工厂来统一生产,这就是抽象工厂模式的基本思想。\n\n##3.1 定义 为创建一组相关或相互依赖的对象提供一个接口,而且无需指定他们的具体类。\n\n抽象工厂(Abstract Factory)模式,又称工具箱(Kit 或Toolkit)模式。\n\n##3.2 抽象工厂模式实现方式 抽象工厂模式是工厂方法模式的升级版本,他用来创建一组相关或者相互依赖的对象。他与工厂方法模式的区别就在于,工厂方法模式针对的是一个产品等级结构;而抽象工厂模式则是针对的多个产品等级结构。在编程中,通常一个产品结构,表现为一个接口或者抽象类,也就是说,工厂方法模式提供的所有产品都是衍生自同一个接口或抽象类,而抽象工厂模式所提供的产品则是衍生自不同的接口或抽象类。\n\n在抽象工厂模式中,有一个产品族的概念:所谓的产品族,是指位于不同产品等级结构中功能相关联的产品组成的家族。抽象工厂模式所提供的一系列产品就组成一个产品族;而工厂方法提供的一系列产品称为一个等级结构。\n\n也没骗你,抽象工厂模式确实是抽象。\n\n抽象工厂模式包含的角色(要素):\n\nAbstractFactory:抽象工厂,用于声明生成抽象产品的方法 ConcreteFactory:具体工厂,实现抽象工厂定义的方法,具体实现一系列产品对象的创建 AbstractProduct:抽象产品,定义一类产品对象的接口 ConcreteProduct:具体产品,通常在具体工厂里,会选择具体的产品实现,来创建符合抽象工厂定义的方法返回的产品类型的对象。 Client:客户端,使用抽象工厂来获取一系列所需要的产品对象 #UML类图\n\n#实例 我把维基百科的例子改下用于理解,假设我们要生产两种产品,键盘(Keyboard)和鼠标(Mouse) ,每一种产品都支持多种系列,比如 Mac 系列和 Windows 系列。这样每个系列的产品分别是 MacKeyboard WinKeyboard, MacMouse, WinMouse 。为了可以在运行时刻创建一个系列的产品族,我们可以为每个系列的产品族创建一个工厂 MacFactory 和 WinFactory 。每个工厂都有两个方法 CreateMouse 和 CreateKeyboard 并返回对应的产品,可以将这两个方法抽象成一个接口 HardWare 。这样在运行时刻我们可以选择创建需要的产品系列。\n\n抽象产品\n\npublic interface Keyboard { void input(); } public interface Mouse { void click(); } 具体产品\n\n//具体产品 public class MacKeyboard implements Keyboard { @Override public void input() { System.out.println(\"Mac 专用键盘\"); } }\n\npublic class MacMouse implements Mouse { @Override public void click() { System.out.println(\"Mac 专用鼠标\"); } }\n\npublic class WinKeyboard implements Keyboard { @Override public void input() { System.out.println(\"Win 专用键盘\"); } }\n\npublic class WinMouse implements Mouse { @Override public void click() { System.out.println(\"win 专用鼠标\"); } } 抽象工厂\n\npublic interface Hardware { Keyboard createKyeBoard(); Mouse createMouse(); } 具体的工厂类\n\npublic class MacFactory implements Hardware{ @Override public Keyboard createKyeBoard() { return new MacKeyboard(); }\n\n@Override\npublic Mouse createMouse() {\n return new MacMouse();\n}\n\n\n}\n\npublic class WinFactory implements Hardware{ @Override public Keyboard createKyeBoard() { return new WinKeyboard(); }\n\n@Override\npublic Mouse createMouse() {\n return new WinMouse();\n}\n\n\n} 使用\n\npublic class Client { public static void main(String[] args) { Hardware macFactory = new MacFactory(); Keyboard keyboard = macFactory.createKyeBoard(); keyboard.input(); //Mac 专用键盘\n\nHardware winFactory = new WinFactory();\nMouse mouse = winFactory.createMouse();\nmouse.click(); //win 专用鼠标\n\n\n} } #3.3 抽象工厂模式适用场景 抽象工厂模式和工厂方法模式一样,都符合开闭原则。但是不同的是,工厂方法模式在增加一个具体产品的时候,都要增加对应的工厂。但是抽象工厂模式只有在新增一个类型的具体产品时才需要新增工厂。也就是说,工厂方法模式的一个工厂只能创建一个具体产品。而抽象工厂模式的一个工厂可以创建属于一类类型的多种具体产品。工厂创建产品的个数介于简单工厂模式和工厂方法模式之间。\n\n在以下情况下可以使用抽象工厂模式:\n\n一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是重要的。 系统中有多于一个的产品族,而每次只使用其中某一产品族。 属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。 系统结构稳定,不会频繁的增加对象。 “开闭原则”的倾斜性\n\n在抽象工厂模式中,增加新的产品族很方便,但是增加新的产品等级结构很麻烦,抽象工厂模式的这种性质称为**“开闭原则”的倾斜性**。“开闭原则”要求系统对扩展开放,对修改封闭,通过扩展达到增强其功能的目的,对于涉及到多个产品族与多个产品等级结构的系统,其功能增强包括两方面:\n\n增加产品族:对于增加新的产品族,工厂方法模式很好的支持了“开闭原则”,对于新增加的产品族,只需要对应增加一个新的具体工厂即可,对已有代码无须做任何修改。 增加新的产品等级结构:对于增加新的产品等级结构,需要修改所有的工厂角色,包括抽象工厂类,在所有的工厂类中都需要增加生产新产品的方法,违背了“开闭原则”。 正因为抽象工厂模式存在“开闭原则”的倾斜性,它以一种倾斜的方式来满足“开闭原则”,为增加新产品族提供方便,但不能为增加新产品结构提供这样的方便,因此要求设计人员在设计之初就能够全面考虑,不会在设计完成之后向系统中增加新的产品等级结构,也不会删除已有的产品等级结构,否则将会导致系统出现较大的修改,为后续维护工作带来诸多麻烦。\n\n#3.4 抽象工厂模式总结 抽象工厂模式是工厂方法模式的进一步延伸,由于它提供了功能更为强大的工厂类并且具备较好的可扩展性,在软件开发中得以广泛应用,尤其是在一些框架和API类库的设计中,例如在Java语言的AWT(抽象窗口工具包)中就使用了抽象工厂模式,它使用抽象工厂模式来实现在不同的操作系统中应用程序呈现与所在操作系统一致的外观界面。抽象工厂模式也是在软件开发中最常用的设计模式之一。\n\n优点:\n\n抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易,所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。 增加新的产品族很方便,无须修改已有系统,符合“开闭原则”。 缺点:\n\n增加新的产品等级结构麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,违背了“开闭原则”。\n\n#工厂模式的退化 当抽象工厂模式中每一个具体工厂类只创建一个产品对象,也就是只存在一个产品等级结构时,抽象工厂模式退化成工厂方法模式;当工厂方法模式中抽象工厂与具体工厂合并,提供一个统一的工厂来创建产品对象,并将创建对象的工厂方法设计为静态方法时,工厂方法模式退化成简单工厂模式。\n\n#四、我们身边的工厂模式 工厂模式在Java码农身边真是无处不在,不信打开你的项目,搜索 Factory\n\n我们最常用的 Spring 就是一个最大的 Bean 工厂,IOC 通过BeanFactory对Bean 进行管理。\n\n我们使用的日志门面框架slf4j,点进去就可以看到熟悉的味道\n\nprivate final static Logger logger = LoggerFactory.getLogger(HelloWord.class); JDK 的 Calendar 使用了简单工厂模式\n\n Calendar calendar = Calendar.getInstance();\n\n\n1\n\n\n> 参考\n> https://javakeeper.starfish.ink/design-pattern/Factory-Pattern.html", - "normalizedContent": "> 3年工作经验是吧? 你知道工厂模式分为几类吗?能举例子吗? 那你说说你们项目中是怎么使用工厂模式的?\n\n#工厂模式 工厂模式(factory pattern)是 java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。\n\n在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。\n\n\n# 模式动机\n\n考虑一个简单的软件应用场景,一个软件系统可以提供多个外观不同的按钮(如圆形按钮、矩形按钮、菱形按钮等), 这些按钮都源自同一个基类,不过在继承基类后不同的子类修改了部分属性从而使得它们可以呈现不同的外观,如果我们希望在使用这些按钮时,不需要知道这些具体按钮类的名字,只需要知道表示该按钮类的一个参数,并提供一个调用方便的方法,把该参数传入方法即可返回一个相应的按钮对象,此时,就可以使用简单工厂模式。\n\n\n# 模式定义\n\n简单工厂模式(simple factory pattern):又称为静态工厂方法(static factory method)模式,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。\n\n\n# 模式结构\n\n简单工厂模式包含如下角色:\n\nfactory:工厂角色\n工厂角色负责实现创建所有实例的内部逻辑 product:抽象产品角色\n抽象产品角色是所创建的所有对象的父类,负责描述所有实例所共有的公共接口 concreteproduct:具体产品角色\n具体产品角色是创建目标,所有创建的对象都充当这个角色的某个具体类的实例。 1.4. 时序图\n\n\n# 模式分析\n\n将对象的创建和对象本身业务处理分离可以降低系统的耦合度,使得两者修改起来都相对容易。\n\n在调用工厂类的工厂方法时,由于工厂方法是静态方法,使用起来很方便,可通过类名直接调用,而且只需要传入一个简单的参数即可.\n\n在实际开发中,还可以在调用时将所传入的参数保存在xml等格式的配置文件中,修改参数时无须修改任何源代码。\n\n简单工厂模式最大的问题在于工厂类的职责相对过重,增加新的产品需要修改工厂类的判断逻辑,这一点与开闭原则是相违背的。\n\n简单工厂模式的要点在于:当你需要什么,只需要传入一个正确的参数,就可以获取你所需要的对象,而无须知道其创建细节。\n\n#工厂模式可以分为三类: 简单工厂模式(simple factory) 工厂方法模式(factory method) 抽象工厂模式(abstract factory) 简单工厂其实不是一个标准的的设计模式。gof 23种设计模式中只有「工厂方法模式」与「抽象工厂模式」。简单工厂模式可以看为工厂方法模式的一种特例,为了统一整理学习,就都归为工厂模式。\n\n这三种工厂模式在设计模式的分类中都属于创建型模式,三种模式从上到下逐步抽象。\n\n创建型模式 创建型模式(creational pattern)对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。为了使软件的结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。\n\n创建型模式在创建什么(what),由谁创建(who),何时创建(when)等方面都为软件设计者提供了尽可能大的灵活性。\n\n创建型模式隐藏了类的实例的创建细节,通过隐藏对象如何被创建和组合在一起达到使整个系统独立的目的。\n\n工厂模式是创建型模式中比较重要的。工厂模式的主要功能就是帮助我们实例化对象。之所以名字中包含工厂模式四个字,是因为对象的实例化过程是通过工厂实现的,是用工厂代替new操作的。\n\n工厂模式优点 可以使代码结构清晰,有效地封装变化。在编程中,产品类的实例化有时候是比较复杂和多变的,通过工厂模式,将产品的实例化封装起来,使得调用者根本无需关心产品的实例化过程,只需依赖工厂即可得到自己想要的产品。 对调用者屏蔽具体的产品类。如果使用工厂模式,调用者只关心产品的接口就可以了,至于具体的实现,调用者根本无需关心。即使变更了具体的实现,对调用者来说没有任何影响。 降低耦合度。产品类的实例化通常来说是很复杂的,它需要依赖很多的类,而这些类对于调用者来说根本无需知道,如果使用了工厂方法,我们需要做的仅仅是实例化好产品类,然后交给调用者使用。对调用者来说,产品所依赖的类都是透明的。 适用场景 不管是简单工厂模式,工厂方法模式还是抽象工厂模式,他们具有类似的特性,所以他们的适用场景也是类似的。\n\n首先,作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过new就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。\n\n其次,工厂模式是一种典型的解耦模式,迪米特法则在工厂模式中表现的尤为明显。假如调用者自己组装产品需要增加依赖关系时,可以考虑使用工厂模式。将会大大降低对象之间的耦合度。\n\n再次,由于工厂模式是依靠抽象架构的,它把实例化产品的任务交由实现类完成,扩展性比较好。也就是说,当需要系统有比较好的扩展性时,可以考虑工厂模式,不同的产品用不同的实现工厂来组装。\n\n#一、简单工厂模式 在介绍简单工厂模式之前,我们尝试解决以下问题:\n\n现在我们要使用面向对象的形式定义计算器,为了实现各算法之间的解耦。我们一般会这么写:\n\n// 计算类的基类 @setter @getter public abstract class operation { private double value1 = 0; private double value2 = 0; protected abstract double getresule(); }\n\n//加法 public class operationadd extends operation { @override protected double getresule() { return getvalue1() + getvalue2(); } } //减法 public class operationsub extends operation { @override protected double getresule() { return getvalue1() - getvalue2(); } } //乘法 public class operationmul extends operation { @override protected double getresule() { return getvalue1() * getvalue2(); } } //除法 public class operationdiv extends operation { @override protected double getresule() { if (getvalue2() != 0) { return getvalue1() / getvalue2(); } throw new illegalargumentexception(\"除数不能为零\"); } } 当我们要使用这个计算器的时候,又会这么写:\n\npublic static void main(string[] args) { //计算两数之和 operationadd operationadd = new operationadd(); operationadd.setvalue1(1); operationadd.setvalue2(2); system.out.println(\"sum:\"+operationadd.getresule()); //计算两数乘积 operationmul operationmul = new operationmul(); operationmul.setvalue1(3); operationmul.setvalue2(5); system.out.println(\"multiply:\"+operationmul.getresule()); //计算两数之差。。。 } 想要使用不同的运算的时候就要创建不同的类,并且要明确知道该类的名字。那么这种重复的创建类的工作其实可以放到一个统一的类中去管理。这样的方法我们就叫做「简单工厂模式」,在简单工厂模式中用于创建实例的方法是静态(static)方法,因此简单工厂模式又被称为「静态工厂方法」模式。。简单工厂模式有以下优点:\n\n一个调用者想创建一个对象,只要知道其名称就可以了。 屏蔽产品的具体实现,调用者只关心产品的接口。\n\n\n# 定义\n\n提供一个创建对象实例的功能,而无需关心其具体实现。被创建实例的类型可以是接口、抽象类,也可以是具体的类。\n\n\n# 简单工厂模式实现方式\n\n没骗你,简单工厂模式,真是因为简单才被叫做简单工厂模式的。\n\n简单工厂模式包含 3 个角色(要素):\n\nfactory:即工厂类, 简单工厂模式的核心部分,负责实现创建所有产品的内部逻辑;工厂类可以被外界直接调用,创建所需对象 product:抽象类产品, 它是工厂类所创建的所有对象的父类,封装了各种产品对象的公有方法,它的引入将提高系统的灵活性,使得在工厂类中只需定义一个通用的工厂方法,因为所有创建的具体产品对象都是其子类对象 concreteproduct:具体产品, 它是简单工厂模式的创建目标,所有被创建的对象都充当这个角色的某个具体类的实例。它要实现抽象产品中声明的抽象方法 #uml类图\n\n#实例 现在我们定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。\n\n//工厂类\npublic class operationfactory {\n\n public static operation createoperation(string operation) {\n operation oper = null;\n switch (operation) {\n case \"add\":\n oper = new operationadd();\n break;\n case \"sub\":\n oper = new operationsub();\n break;\n case \"mul\":\n oper = new operationmul();\n break;\n\n case \"div\":\n oper = new operationdiv();\n break;\n default:\n throw new unsupportedoperationexception(\"不支持该操作\");\n }\n return oper;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n\n\n有了工厂类之后,可以使用工厂创建对象:\n\npublic static void main(string[] args) {\n operation operationadd = operationfactory.createoperation(\"add\");\n operationadd.setvalue1(1);\n operationadd.setvalue2(2)\n system.out.println(operationadd.getresule());\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n通过简单工厂模式,该计算器的使用者不需要关系实现加法逻辑的那个类的具体名字,只要知道该类对应的参数\"add\"就可以了。这就体现了之前提到的工厂模式的优点。\n\n\n# 简单工厂模式存在的问题\n\n当我们需要增加一种计算时,例如开平方。这个时候我们需要先定义一个类继承operation类,其中实现平方的代码。除此之外我们还要修改 operationfactory 类的代码,增加一个 case。这显然是违背开闭原则的。可想而知对于新产品的加入,工厂类是很被动的。\n\n我们举的例子是最简单的情况。而在实际应用中,很可能产品是一个多层次的树状结构。 简单工厂可能就不太适用了。\n\n\n# 简单工厂模式总结\n\n工厂类是整个简单工厂模式的关键。包含了必要的逻辑判断,根据外界给定的信息,决定究竟应该创建哪个具体类的对象。通过使用工厂类,外界可以从直接创建具体产品对象的尴尬局面摆脱出来,仅仅需要负责“消费”对象就可以了。而不必管这些对象究竟如何创建及如何组织的。明确了各自的职责和权利,有利于整个软件体系结构的优化。\n\n但是由于工厂类集中了所有实例的创建逻辑,违反了高内聚责任分配原则,将全部创建逻辑集中到了一个工厂类中;它所能创建的类只能是事先考虑到的,如果需要添加新的类,则就需要改变工厂类了。\n\n当系统中的具体产品类不断增多时候,可能会出现要求工厂类根据不同条件创建不同实例的需求.这种对条件的判断和对具体产品类型的判断交错在一起,很难避免模块功能的蔓延,对系统的维护和扩展非常不利;\n\n为了解决这些缺点,就有了工厂方法模式。\n\n#二、工厂方法模式 我们常说的工厂模式,就是指「工厂方法模式」,也叫「虚拟构造器模式」或「多态工厂模式」。\n\n\n# 2.1 定义\n\n定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类。工厂方法让类的实例化推迟到子类中进行。\n\n##2.2 工厂方法模式实现方式 工厂方法模式包含 4 个角色(要素):\n\nproduct:抽象产品,定义工厂方法所创建的对象的接口,也就是实际需要使用的对象的接口 concreteproduct:具体产品,具体的product接口的实现对象 factory:工厂接口,也可以叫 creator(创建器),申明工厂方法,通常返回一个 product 类型的实例对象 concretefactory:工厂实现,或者叫 concretecreator(创建器对象),覆盖 factory 定义的工厂方法,返回具体的 product 实例 #uml类图\n\n#实例 从 uml 类图可以看出,每种产品实现,我们都要增加一个继承于工厂接口 ifactory 的工厂类 factory ,修改简单工厂模式代码中的工厂类如下:\n\n//工厂接口\npublic interface ifactory {\n operation createoption();\n}\n\n//加法类工厂\npublic class addfactory implements ifactory {\n public operation createoption() {\n return new operationadd();\n }\n}\n\n//减法类工厂\npublic class subfactory implements ifactory {\n public operation createoption() {\n return new operationsub();\n }\n}\n\n//乘法类工厂\npublic class mulfactory implements ifactory {\n public operation createoption() {\n return new operationmul();\n }\n}\n\n//除法类工厂\npublic class divfactory implements ifactory {\n public operation createoption() {\n return new operationdiv();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n\n\n这时,我们使用计算器的时候,要为每种运算方法增加一个工厂对象\n\npublic class client {\n public static void main(string[] args) {\n //减法\n ifactory subfactory = new subfactory();\n operation operationsub = subfactory.createoption();\n operationsub.setvalue1(22);\n operationsub.setvalue2(20);\n system.out.println(\"sub:\"+operationsub.getresult());\n //除法\n ifactory divfactory = new divfactory();\n operation operationdiv = divfactory.createoption();\n operationdiv.setvalue1(99);\n operationdiv.setvalue2(33);\n system.out.println(\"div:\"+operationsub.getresult());\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\n纳尼,这不是更复杂了吗,每个产品对应一个工厂,我又不是按代码量赚钱的。。。\n\n##2.3 工厂方法模式适用场景 工厂方法模式和简单工厂模式虽然都是通过工厂来创建对象,他们之间最大的不同是——工厂方法模式在设计上完全完全符合“开闭原则”。\n\n在以下情况下可以使用工厂方法模式:\n\n一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。 一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。 将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定,可将具体工厂类的类名存储在配置文件或数据库中。 使用场景 日志记录器:日志可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方。 数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。 设计一个连接服务器的框架,需要三个协议,\"pop3\"、\"imap\"、\"http\",可以把这三个作为产品类,共同实现一个接口。 比如 hibernate 换数据库只需换方言和驱动就可以 ##2.4 工厂方法模式总结 工厂方法模式是简单工厂模式的进一步抽象和推广。\n\n由于使用了面向对象的多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。\n\n在工厂方法模式中,核心的工厂类不再负责所有产品的创建,而是将具体创建工作交给子类去做。这个核心类仅仅负责给出具体工厂必须实现的接口,而不负责产品类被实例化这种细节,这使得工厂方法模式可以允许系统在不修改工厂角色的情况下引进新产品。\n\n优点:\n\n一个调用者想创建一个对象,只要知道其名称就可以了。 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。 屏蔽产品的具体实现,调用者只关心产品的接口。 缺点:\n\n每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。\n\n#三、抽象工厂模式 工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。此时,我们可以考虑将一些相关的产品组成一个“产品族”,由同一个工厂来统一生产,这就是抽象工厂模式的基本思想。\n\n##3.1 定义 为创建一组相关或相互依赖的对象提供一个接口,而且无需指定他们的具体类。\n\n抽象工厂(abstract factory)模式,又称工具箱(kit 或toolkit)模式。\n\n##3.2 抽象工厂模式实现方式 抽象工厂模式是工厂方法模式的升级版本,他用来创建一组相关或者相互依赖的对象。他与工厂方法模式的区别就在于,工厂方法模式针对的是一个产品等级结构;而抽象工厂模式则是针对的多个产品等级结构。在编程中,通常一个产品结构,表现为一个接口或者抽象类,也就是说,工厂方法模式提供的所有产品都是衍生自同一个接口或抽象类,而抽象工厂模式所提供的产品则是衍生自不同的接口或抽象类。\n\n在抽象工厂模式中,有一个产品族的概念:所谓的产品族,是指位于不同产品等级结构中功能相关联的产品组成的家族。抽象工厂模式所提供的一系列产品就组成一个产品族;而工厂方法提供的一系列产品称为一个等级结构。\n\n也没骗你,抽象工厂模式确实是抽象。\n\n抽象工厂模式包含的角色(要素):\n\nabstractfactory:抽象工厂,用于声明生成抽象产品的方法 concretefactory:具体工厂,实现抽象工厂定义的方法,具体实现一系列产品对象的创建 abstractproduct:抽象产品,定义一类产品对象的接口 concreteproduct:具体产品,通常在具体工厂里,会选择具体的产品实现,来创建符合抽象工厂定义的方法返回的产品类型的对象。 client:客户端,使用抽象工厂来获取一系列所需要的产品对象 #uml类图\n\n#实例 我把维基百科的例子改下用于理解,假设我们要生产两种产品,键盘(keyboard)和鼠标(mouse) ,每一种产品都支持多种系列,比如 mac 系列和 windows 系列。这样每个系列的产品分别是 mackeyboard winkeyboard, macmouse, winmouse 。为了可以在运行时刻创建一个系列的产品族,我们可以为每个系列的产品族创建一个工厂 macfactory 和 winfactory 。每个工厂都有两个方法 createmouse 和 createkeyboard 并返回对应的产品,可以将这两个方法抽象成一个接口 hardware 。这样在运行时刻我们可以选择创建需要的产品系列。\n\n抽象产品\n\npublic interface keyboard { void input(); } public interface mouse { void click(); } 具体产品\n\n//具体产品 public class mackeyboard implements keyboard { @override public void input() { system.out.println(\"mac 专用键盘\"); } }\n\npublic class macmouse implements mouse { @override public void click() { system.out.println(\"mac 专用鼠标\"); } }\n\npublic class winkeyboard implements keyboard { @override public void input() { system.out.println(\"win 专用键盘\"); } }\n\npublic class winmouse implements mouse { @override public void click() { system.out.println(\"win 专用鼠标\"); } } 抽象工厂\n\npublic interface hardware { keyboard createkyeboard(); mouse createmouse(); } 具体的工厂类\n\npublic class macfactory implements hardware{ @override public keyboard createkyeboard() { return new mackeyboard(); }\n\n@override\npublic mouse createmouse() {\n return new macmouse();\n}\n\n\n}\n\npublic class winfactory implements hardware{ @override public keyboard createkyeboard() { return new winkeyboard(); }\n\n@override\npublic mouse createmouse() {\n return new winmouse();\n}\n\n\n} 使用\n\npublic class client { public static void main(string[] args) { hardware macfactory = new macfactory(); keyboard keyboard = macfactory.createkyeboard(); keyboard.input(); //mac 专用键盘\n\nhardware winfactory = new winfactory();\nmouse mouse = winfactory.createmouse();\nmouse.click(); //win 专用鼠标\n\n\n} } #3.3 抽象工厂模式适用场景 抽象工厂模式和工厂方法模式一样,都符合开闭原则。但是不同的是,工厂方法模式在增加一个具体产品的时候,都要增加对应的工厂。但是抽象工厂模式只有在新增一个类型的具体产品时才需要新增工厂。也就是说,工厂方法模式的一个工厂只能创建一个具体产品。而抽象工厂模式的一个工厂可以创建属于一类类型的多种具体产品。工厂创建产品的个数介于简单工厂模式和工厂方法模式之间。\n\n在以下情况下可以使用抽象工厂模式:\n\n一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是重要的。 系统中有多于一个的产品族,而每次只使用其中某一产品族。 属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。 系统结构稳定,不会频繁的增加对象。 “开闭原则”的倾斜性\n\n在抽象工厂模式中,增加新的产品族很方便,但是增加新的产品等级结构很麻烦,抽象工厂模式的这种性质称为**“开闭原则”的倾斜性**。“开闭原则”要求系统对扩展开放,对修改封闭,通过扩展达到增强其功能的目的,对于涉及到多个产品族与多个产品等级结构的系统,其功能增强包括两方面:\n\n增加产品族:对于增加新的产品族,工厂方法模式很好的支持了“开闭原则”,对于新增加的产品族,只需要对应增加一个新的具体工厂即可,对已有代码无须做任何修改。 增加新的产品等级结构:对于增加新的产品等级结构,需要修改所有的工厂角色,包括抽象工厂类,在所有的工厂类中都需要增加生产新产品的方法,违背了“开闭原则”。 正因为抽象工厂模式存在“开闭原则”的倾斜性,它以一种倾斜的方式来满足“开闭原则”,为增加新产品族提供方便,但不能为增加新产品结构提供这样的方便,因此要求设计人员在设计之初就能够全面考虑,不会在设计完成之后向系统中增加新的产品等级结构,也不会删除已有的产品等级结构,否则将会导致系统出现较大的修改,为后续维护工作带来诸多麻烦。\n\n#3.4 抽象工厂模式总结 抽象工厂模式是工厂方法模式的进一步延伸,由于它提供了功能更为强大的工厂类并且具备较好的可扩展性,在软件开发中得以广泛应用,尤其是在一些框架和api类库的设计中,例如在java语言的awt(抽象窗口工具包)中就使用了抽象工厂模式,它使用抽象工厂模式来实现在不同的操作系统中应用程序呈现与所在操作系统一致的外观界面。抽象工厂模式也是在软件开发中最常用的设计模式之一。\n\n优点:\n\n抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易,所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。 增加新的产品族很方便,无须修改已有系统,符合“开闭原则”。 缺点:\n\n增加新的产品等级结构麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,违背了“开闭原则”。\n\n#工厂模式的退化 当抽象工厂模式中每一个具体工厂类只创建一个产品对象,也就是只存在一个产品等级结构时,抽象工厂模式退化成工厂方法模式;当工厂方法模式中抽象工厂与具体工厂合并,提供一个统一的工厂来创建产品对象,并将创建对象的工厂方法设计为静态方法时,工厂方法模式退化成简单工厂模式。\n\n#四、我们身边的工厂模式 工厂模式在java码农身边真是无处不在,不信打开你的项目,搜索 factory\n\n我们最常用的 spring 就是一个最大的 bean 工厂,ioc 通过beanfactory对bean 进行管理。\n\n我们使用的日志门面框架slf4j,点进去就可以看到熟悉的味道\n\nprivate final static logger logger = loggerfactory.getlogger(helloword.class); jdk 的 calendar 使用了简单工厂模式\n\n calendar calendar = calendar.getinstance();\n\n\n1\n\n\n> 参考\n> https://javakeeper.starfish.ink/design-pattern/factory-pattern.html", + "headersStr": "工厂模式 模式动机 模式定义 模式结构 模式分析 工厂模式可以分为三类: 一、简单工厂模式 定义 简单工厂模式实现方式 UML类图 实例 简单工厂模式存在的问题 简单工厂模式总结 二、工厂方法模式 2.1 定义 2.2 工厂方法模式实现方式 UML类图 实例 2.3 工厂方法模式适用场景 2.4 工厂方法模式总结 三、抽象工厂模式 3.1 定义 3.2 抽象工厂模式实现方式 UML类图 实例 3.3 抽象工厂模式适用场景 3.4 抽象工厂模式总结 工厂模式的退化 四、我们身边的工厂模式", + "content": "> 3年工作经验是吧? 你知道工厂模式分为几类吗?能举例子吗? 那你说说你们项目中是怎么使用工厂模式的?\n\n\n# 工厂模式\n\n工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。\n\n在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。\n\n\n# 模式动机\n\n考虑一个简单的软件应用场景,一个软件系统可以提供多个外观不同的按钮(如圆形按钮、矩形按钮、菱形按钮等), 这些按钮都源自同一个基类,不过在继承基类后不同的子类修改了部分属性从而使得它们可以呈现不同的外观,如果我们希望在使用这些按钮时,不需要知道这些具体按钮类的名字,只需要知道表示该按钮类的一个参数,并提供一个调用方便的方法,把该参数传入方法即可返回一个相应的按钮对象,此时,就可以使用简单工厂模式。\n\n\n# 模式定义\n\n简单工厂模式(Simple Factory Pattern):又称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。\n\n\n# 模式结构\n\n简单工厂模式包含如下角色:\n\nFactory:工厂角色\n工厂角色负责实现创建所有实例的内部逻辑 Product:抽象产品角色\n抽象产品角色是所创建的所有对象的父类,负责描述所有实例所共有的公共接口 ConcreteProduct:具体产品角色\n具体产品角色是创建目标,所有创建的对象都充当这个角色的某个具体类的实例。 1.4. 时序图\n\n\n# 模式分析\n\n将对象的创建和对象本身业务处理分离可以降低系统的耦合度,使得两者修改起来都相对容易。\n\n在调用工厂类的工厂方法时,由于工厂方法是静态方法,使用起来很方便,可通过类名直接调用,而且只需要传入一个简单的参数即可.\n\n在实际开发中,还可以在调用时将所传入的参数保存在XML等格式的配置文件中,修改参数时无须修改任何源代码。\n\n简单工厂模式最大的问题在于工厂类的职责相对过重,增加新的产品需要修改工厂类的判断逻辑,这一点与开闭原则是相违背的。\n\n简单工厂模式的要点在于:当你需要什么,只需要传入一个正确的参数,就可以获取你所需要的对象,而无须知道其创建细节。\n\n\n# 工厂模式可以分为三类:\n\n简单工厂模式(Simple Factory) 工厂方法模式(Factory Method) 抽象工厂模式(Abstract Factory) 简单工厂其实不是一个标准的的设计模式。GOF 23种设计模式中只有「工厂方法模式」与「抽象工厂模式」。简单工厂模式可以看为工厂方法模式的一种特例,为了统一整理学习,就都归为工厂模式。\n\n这三种工厂模式在设计模式的分类中都属于创建型模式,三种模式从上到下逐步抽象。\n\n创建型模式 创建型模式(Creational Pattern)对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。为了使软件的结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。\n\n创建型模式在创建什么(What),由谁创建(Who),何时创建(When)等方面都为软件设计者提供了尽可能大的灵活性。\n\n创建型模式隐藏了类的实例的创建细节,通过隐藏对象如何被创建和组合在一起达到使整个系统独立的目的。\n\n工厂模式是创建型模式中比较重要的。工厂模式的主要功能就是帮助我们实例化对象。之所以名字中包含工厂模式四个字,是因为对象的实例化过程是通过工厂实现的,是用工厂代替new操作的。\n\n工厂模式优点 可以使代码结构清晰,有效地封装变化。在编程中,产品类的实例化有时候是比较复杂和多变的,通过工厂模式,将产品的实例化封装起来,使得调用者根本无需关心产品的实例化过程,只需依赖工厂即可得到自己想要的产品。 对调用者屏蔽具体的产品类。如果使用工厂模式,调用者只关心产品的接口就可以了,至于具体的实现,调用者根本无需关心。即使变更了具体的实现,对调用者来说没有任何影响。 降低耦合度。产品类的实例化通常来说是很复杂的,它需要依赖很多的类,而这些类对于调用者来说根本无需知道,如果使用了工厂方法,我们需要做的仅仅是实例化好产品类,然后交给调用者使用。对调用者来说,产品所依赖的类都是透明的。 适用场景 不管是简单工厂模式,工厂方法模式还是抽象工厂模式,他们具有类似的特性,所以他们的适用场景也是类似的。\n\n首先,作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过new就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。\n\n其次,工厂模式是一种典型的解耦模式,迪米特法则在工厂模式中表现的尤为明显。假如调用者自己组装产品需要增加依赖关系时,可以考虑使用工厂模式。将会大大降低对象之间的耦合度。\n\n再次,由于工厂模式是依靠抽象架构的,它把实例化产品的任务交由实现类完成,扩展性比较好。也就是说,当需要系统有比较好的扩展性时,可以考虑工厂模式,不同的产品用不同的实现工厂来组装。\n\n\n# 一、简单工厂模式\n\n在介绍简单工厂模式之前,我们尝试解决以下问题:\n\n现在我们要使用面向对象的形式定义计算器,为了实现各算法之间的解耦。我们一般会这么写:\n\n// 计算类的基类\n@Setter\n@Getter\npublic abstract class Operation {\n private double value1 = 0;\n private double value2 = 0;\n protected abstract double getResule();\n}\n\n//加法\npublic class OperationAdd extends Operation {\n @Override\n protected double getResule() {\n return getValue1() + getValue2();\n }\n}\n//减法\npublic class OperationSub extends Operation {\n @Override\n protected double getResule() {\n return getValue1() - getValue2();\n }\n}\n//乘法\npublic class OperationMul extends Operation {\n @Override\n protected double getResule() {\n return getValue1() * getValue2();\n }\n}\n//除法\npublic class OperationDiv extends Operation {\n @Override\n protected double getResule() {\n if (getValue2() != 0) {\n return getValue1() / getValue2();\n }\n throw new IllegalArgumentException(\"除数不能为零\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n\n\n当我们要使用这个计算器的时候,又会这么写:\n\npublic static void main(String[] args) {\n //计算两数之和\n OperationAdd operationAdd = new OperationAdd();\n operationAdd.setValue1(1);\n operationAdd.setValue2(2);\n System.out.println(\"sum:\"+operationAdd.getResule());\n //计算两数乘积\n OperationMul operationMul = new OperationMul();\n operationMul.setValue1(3);\n operationMul.setValue2(5);\n System.out.println(\"multiply:\"+operationMul.getResule());\n //计算两数之差。。。\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n想要使用不同的运算的时候就要创建不同的类,并且要明确知道该类的名字。那么这种重复的创建类的工作其实可以放到一个统一的类中去管理。这样的方法我们就叫做「简单工厂模式」,在简单工厂模式中用于创建实例的方法是静态(static)方法,因此简单工厂模式又被称为「静态工厂方法」模式。。简单工厂模式有以下优点:\n\n一个调用者想创建一个对象,只要知道其名称就可以了。 屏蔽产品的具体实现,调用者只关心产品的接口。\n\n\n# 定义\n\n提供一个创建对象实例的功能,而无需关心其具体实现。被创建实例的类型可以是接口、抽象类,也可以是具体的类。\n\n\n# 简单工厂模式实现方式\n\n没骗你,简单工厂模式,真是因为简单才被叫做简单工厂模式的。\n\n简单工厂模式包含 3 个角色(要素):\n\nFactory:即工厂类, 简单工厂模式的核心部分,负责实现创建所有产品的内部逻辑;工厂类可以被外界直接调用,创建所需对象 Product:抽象类产品, 它是工厂类所创建的所有对象的父类,封装了各种产品对象的公有方法,它的引入将提高系统的灵活性,使得在工厂类中只需定义一个通用的工厂方法,因为所有创建的具体产品对象都是其子类对象 ConcreteProduct:具体产品, 它是简单工厂模式的创建目标,所有被创建的对象都充当这个角色的某个具体类的实例。它要实现抽象产品中声明的抽象方法\n\n\n# UML类图\n\n\n\n\n# 实例\n\n现在我们定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。\n\n//工厂类\npublic class OperationFactory {\n\n public static Operation createOperation(String operation) {\n Operation oper = null;\n switch (operation) {\n case \"add\":\n oper = new OperationAdd();\n break;\n case \"sub\":\n oper = new OperationSub();\n break;\n case \"mul\":\n oper = new OperationMul();\n break;\n\n case \"div\":\n oper = new OperationDiv();\n break;\n default:\n throw new UnsupportedOperationException(\"不支持该操作\");\n }\n return oper;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n\n\n有了工厂类之后,可以使用工厂创建对象:\n\npublic static void main(String[] args) {\n Operation operationAdd = OperationFactory.createOperation(\"add\");\n operationAdd.setValue1(1);\n operationAdd.setValue2(2)\n System.out.println(operationAdd.getResule());\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n通过简单工厂模式,该计算器的使用者不需要关系实现加法逻辑的那个类的具体名字,只要知道该类对应的参数\"add\"就可以了。这就体现了之前提到的工厂模式的优点。\n\n\n# 简单工厂模式存在的问题\n\n当我们需要增加一种计算时,例如开平方。这个时候我们需要先定义一个类继承Operation类,其中实现平方的代码。除此之外我们还要修改 OperationFactory 类的代码,增加一个 case。这显然是违背开闭原则的。可想而知对于新产品的加入,工厂类是很被动的。\n\n我们举的例子是最简单的情况。而在实际应用中,很可能产品是一个多层次的树状结构。 简单工厂可能就不太适用了。\n\n\n# 简单工厂模式总结\n\n工厂类是整个简单工厂模式的关键。包含了必要的逻辑判断,根据外界给定的信息,决定究竟应该创建哪个具体类的对象。通过使用工厂类,外界可以从直接创建具体产品对象的尴尬局面摆脱出来,仅仅需要负责“消费”对象就可以了。而不必管这些对象究竟如何创建及如何组织的。明确了各自的职责和权利,有利于整个软件体系结构的优化。\n\n但是由于工厂类集中了所有实例的创建逻辑,违反了高内聚责任分配原则,将全部创建逻辑集中到了一个工厂类中;它所能创建的类只能是事先考虑到的,如果需要添加新的类,则就需要改变工厂类了。\n\n当系统中的具体产品类不断增多时候,可能会出现要求工厂类根据不同条件创建不同实例的需求.这种对条件的判断和对具体产品类型的判断交错在一起,很难避免模块功能的蔓延,对系统的维护和扩展非常不利;\n\n为了解决这些缺点,就有了工厂方法模式。\n\n\n# 二、工厂方法模式\n\n我们常说的工厂模式,就是指「工厂方法模式」,也叫「虚拟构造器模式」或「多态工厂模式」。\n\n\n# 2.1 定义\n\n定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类。工厂方法让类的实例化推迟到子类中进行。\n\n\n# 2.2 工厂方法模式实现方式\n\n工厂方法模式包含 4 个角色(要素):\n\nProduct:抽象产品,定义工厂方法所创建的对象的接口,也就是实际需要使用的对象的接口 ConcreteProduct:具体产品,具体的Product接口的实现对象 Factory:工厂接口,也可以叫 Creator(创建器),申明工厂方法,通常返回一个 Product 类型的实例对象 ConcreteFactory:工厂实现,或者叫 ConcreteCreator(创建器对象),覆盖 Factory 定义的工厂方法,返回具体的 Product 实例\n\n\n# UML类图\n\n\n\n\n# 实例\n\n从 UML 类图可以看出,每种产品实现,我们都要增加一个继承于工厂接口 IFactory 的工厂类 Factory ,修改简单工厂模式代码中的工厂类如下:\n\n//工厂接口\npublic interface IFactory {\n Operation CreateOption();\n}\n\n//加法类工厂\npublic class AddFactory implements IFactory {\n public Operation CreateOption() {\n return new OperationAdd();\n }\n}\n\n//减法类工厂\npublic class SubFactory implements IFactory {\n public Operation CreateOption() {\n return new OperationSub();\n }\n}\n\n//乘法类工厂\npublic class MulFactory implements IFactory {\n public Operation CreateOption() {\n return new OperationMul();\n }\n}\n\n//除法类工厂\npublic class DivFactory implements IFactory {\n public Operation CreateOption() {\n return new OperationDiv();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n\n\n这时,我们使用计算器的时候,要为每种运算方法增加一个工厂对象\n\npublic class Client {\n public static void main(String[] args) {\n //减法\n IFactory subFactory = new SubFactory();\n Operation operationSub = subFactory.CreateOption();\n operationSub.setValue1(22);\n operationSub.setValue2(20);\n System.out.println(\"sub:\"+operationSub.getResult());\n //除法\n IFactory Divfactory = new DivFactory();\n Operation operationDiv = Divfactory.CreateOption();\n operationDiv.setValue1(99);\n operationDiv.setValue2(33);\n System.out.println(\"div:\"+operationSub.getResult());\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\n纳尼,这不是更复杂了吗,每个产品对应一个工厂,我又不是按代码量赚钱的。。。\n\n\n# 2.3 工厂方法模式适用场景\n\n工厂方法模式和简单工厂模式虽然都是通过工厂来创建对象,他们之间最大的不同是——工厂方法模式在设计上完全完全符合“开闭原则”。\n\n在以下情况下可以使用工厂方法模式:\n\n一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。 一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。 将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定,可将具体工厂类的类名存储在配置文件或数据库中。 使用场景 日志记录器:日志可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方。 数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。 设计一个连接服务器的框架,需要三个协议,\"POP3\"、\"IMAP\"、\"HTTP\",可以把这三个作为产品类,共同实现一个接口。 比如 Hibernate 换数据库只需换方言和驱动就可以\n\n\n# 2.4 工厂方法模式总结\n\n工厂方法模式是简单工厂模式的进一步抽象和推广。\n\n由于使用了面向对象的多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。\n\n在工厂方法模式中,核心的工厂类不再负责所有产品的创建,而是将具体创建工作交给子类去做。这个核心类仅仅负责给出具体工厂必须实现的接口,而不负责产品类被实例化这种细节,这使得工厂方法模式可以允许系统在不修改工厂角色的情况下引进新产品。\n\n优点:\n\n一个调用者想创建一个对象,只要知道其名称就可以了。 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。 屏蔽产品的具体实现,调用者只关心产品的接口。 缺点:\n\n每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。\n\n\n# 三、抽象工厂模式\n\n工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。此时,我们可以考虑将一些相关的产品组成一个“产品族”,由同一个工厂来统一生产,这就是抽象工厂模式的基本思想。\n\n\n# 3.1 定义\n\n为创建一组相关或相互依赖的对象提供一个接口,而且无需指定他们的具体类。\n\n抽象工厂(Abstract Factory)模式,又称工具箱(Kit 或Toolkit)模式。\n\n\n# 3.2 抽象工厂模式实现方式\n\n抽象工厂模式是工厂方法模式的升级版本,他用来创建一组相关或者相互依赖的对象。他与工厂方法模式的区别就在于,工厂方法模式针对的是一个产品等级结构;而抽象工厂模式则是针对的多个产品等级结构。在编程中,通常一个产品结构,表现为一个接口或者抽象类,也就是说,工厂方法模式提供的所有产品都是衍生自同一个接口或抽象类,而抽象工厂模式所提供的产品则是衍生自不同的接口或抽象类。\n\n在抽象工厂模式中,有一个产品族的概念:所谓的产品族,是指位于不同产品等级结构中功能相关联的产品组成的家族。抽象工厂模式所提供的一系列产品就组成一个产品族;而工厂方法提供的一系列产品称为一个等级结构。\n\n也没骗你,抽象工厂模式确实是抽象。\n\n抽象工厂模式包含的角色(要素):\n\nAbstractFactory:抽象工厂,用于声明生成抽象产品的方法 ConcreteFactory:具体工厂,实现抽象工厂定义的方法,具体实现一系列产品对象的创建 AbstractProduct:抽象产品,定义一类产品对象的接口 ConcreteProduct:具体产品,通常在具体工厂里,会选择具体的产品实现,来创建符合抽象工厂定义的方法返回的产品类型的对象。 Client:客户端,使用抽象工厂来获取一系列所需要的产品对象\n\n\n# UML类图\n\n\n# 实例\n\n我把维基百科的例子改下用于理解,假设我们要生产两种产品,键盘(Keyboard)和鼠标(Mouse) ,每一种产品都支持多种系列,比如 Mac 系列和 Windows 系列。这样每个系列的产品分别是 MacKeyboard WinKeyboard, MacMouse, WinMouse 。为了可以在运行时刻创建一个系列的产品族,我们可以为每个系列的产品族创建一个工厂 MacFactory 和 WinFactory 。每个工厂都有两个方法 CreateMouse 和 CreateKeyboard 并返回对应的产品,可以将这两个方法抽象成一个接口 HardWare 。这样在运行时刻我们可以选择创建需要的产品系列。\n\n抽象产品\n\npublic interface Keyboard {\n void input();\n}\npublic interface Mouse {\n void click();\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n具体产品\n\n//具体产品\n\npublic class MacKeyboard implements Keyboard {\n @Override\n public void input() {\n System.out.println(\"Mac 专用键盘\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\npublic class MacMouse implements Mouse {\n @Override\n public void click() {\n System.out.println(\"Mac 专用鼠标\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\npublic class WinKeyboard implements Keyboard {\n @Override\n public void input() {\n System.out.println(\"Win 专用键盘\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\npublic class WinMouse implements Mouse {\n @Override\n public void click() {\n System.out.println(\"win 专用鼠标\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n抽象工厂\n\npublic interface Hardware {\n Keyboard createKyeBoard();\n Mouse createMouse();\n}\n\n\n1\n2\n3\n4\n\n\n具体的工厂类\n\npublic class MacFactory implements Hardware{\n @Override\n public Keyboard createKyeBoard() {\n return new MacKeyboard();\n }\n\n @Override\n public Mouse createMouse() {\n return new MacMouse();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\npublic class WinFactory implements Hardware{\n @Override\n public Keyboard createKyeBoard() {\n return new WinKeyboard();\n }\n\n @Override\n public Mouse createMouse() {\n return new WinMouse();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n使用\n\npublic class Client {\n public static void main(String[] args) {\n Hardware macFactory = new MacFactory();\n Keyboard keyboard = macFactory.createKyeBoard();\n keyboard.input(); //Mac 专用键盘\n\n Hardware winFactory = new WinFactory();\n Mouse mouse = winFactory.createMouse();\n mouse.click(); //win 专用鼠标\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n\n# 3.3 抽象工厂模式适用场景\n\n抽象工厂模式和工厂方法模式一样,都符合开闭原则。但是不同的是,工厂方法模式在增加一个具体产品的时候,都要增加对应的工厂。但是抽象工厂模式只有在新增一个类型的具体产品时才需要新增工厂。也就是说,工厂方法模式的一个工厂只能创建一个具体产品。而抽象工厂模式的一个工厂可以创建属于一类类型的多种具体产品。工厂创建产品的个数介于简单工厂模式和工厂方法模式之间。\n\n在以下情况下可以使用抽象工厂模式:\n\n一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是重要的。 系统中有多于一个的产品族,而每次只使用其中某一产品族。 属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。 系统结构稳定,不会频繁的增加对象。 “开闭原则”的倾斜性\n\n在抽象工厂模式中,增加新的产品族很方便,但是增加新的产品等级结构很麻烦,抽象工厂模式的这种性质称为**“开闭原则”的倾斜性**。“开闭原则”要求系统对扩展开放,对修改封闭,通过扩展达到增强其功能的目的,对于涉及到多个产品族与多个产品等级结构的系统,其功能增强包括两方面:\n\n增加产品族:对于增加新的产品族,工厂方法模式很好的支持了“开闭原则”,对于新增加的产品族,只需要对应增加一个新的具体工厂即可,对已有代码无须做任何修改。 增加新的产品等级结构:对于增加新的产品等级结构,需要修改所有的工厂角色,包括抽象工厂类,在所有的工厂类中都需要增加生产新产品的方法,违背了“开闭原则”。 正因为抽象工厂模式存在“开闭原则”的倾斜性,它以一种倾斜的方式来满足“开闭原则”,为增加新产品族提供方便,但不能为增加新产品结构提供这样的方便,因此要求设计人员在设计之初就能够全面考虑,不会在设计完成之后向系统中增加新的产品等级结构,也不会删除已有的产品等级结构,否则将会导致系统出现较大的修改,为后续维护工作带来诸多麻烦。\n\n\n# 3.4 抽象工厂模式总结\n\n抽象工厂模式是工厂方法模式的进一步延伸,由于它提供了功能更为强大的工厂类并且具备较好的可扩展性,在软件开发中得以广泛应用,尤其是在一些框架和API类库的设计中,例如在Java语言的AWT(抽象窗口工具包)中就使用了抽象工厂模式,它使用抽象工厂模式来实现在不同的操作系统中应用程序呈现与所在操作系统一致的外观界面。抽象工厂模式也是在软件开发中最常用的设计模式之一。\n\n优点:\n\n抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易,所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。 增加新的产品族很方便,无须修改已有系统,符合“开闭原则”。 缺点:\n\n增加新的产品等级结构麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,违背了“开闭原则”。\n\n\n# 工厂模式的退化\n\n当抽象工厂模式中每一个具体工厂类只创建一个产品对象,也就是只存在一个产品等级结构时,抽象工厂模式退化成工厂方法模式;当工厂方法模式中抽象工厂与具体工厂合并,提供一个统一的工厂来创建产品对象,并将创建对象的工厂方法设计为静态方法时,工厂方法模式退化成简单工厂模式。\n\n\n# 四、我们身边的工厂模式\n\n工厂模式在Java码农身边真是无处不在,不信打开你的项目,搜索 Factory\n\n我们最常用的 Spring 就是一个最大的 Bean 工厂,IOC 通过BeanFactory对Bean 进行管理。\n\n我们使用的日志门面框架slf4j,点进去就可以看到熟悉的味道\n\nprivate final static Logger logger = LoggerFactory.getLogger(HelloWord.class); JDK 的 Calendar 使用了简单工厂模式\n\n Calendar calendar = Calendar.getInstance();\n\n\n1\n\n\n> 参考\n> https://javakeeper.starfish.ink/design-pattern/Factory-Pattern.html", + "normalizedContent": "> 3年工作经验是吧? 你知道工厂模式分为几类吗?能举例子吗? 那你说说你们项目中是怎么使用工厂模式的?\n\n\n# 工厂模式\n\n工厂模式(factory pattern)是 java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。\n\n在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。\n\n\n# 模式动机\n\n考虑一个简单的软件应用场景,一个软件系统可以提供多个外观不同的按钮(如圆形按钮、矩形按钮、菱形按钮等), 这些按钮都源自同一个基类,不过在继承基类后不同的子类修改了部分属性从而使得它们可以呈现不同的外观,如果我们希望在使用这些按钮时,不需要知道这些具体按钮类的名字,只需要知道表示该按钮类的一个参数,并提供一个调用方便的方法,把该参数传入方法即可返回一个相应的按钮对象,此时,就可以使用简单工厂模式。\n\n\n# 模式定义\n\n简单工厂模式(simple factory pattern):又称为静态工厂方法(static factory method)模式,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。\n\n\n# 模式结构\n\n简单工厂模式包含如下角色:\n\nfactory:工厂角色\n工厂角色负责实现创建所有实例的内部逻辑 product:抽象产品角色\n抽象产品角色是所创建的所有对象的父类,负责描述所有实例所共有的公共接口 concreteproduct:具体产品角色\n具体产品角色是创建目标,所有创建的对象都充当这个角色的某个具体类的实例。 1.4. 时序图\n\n\n# 模式分析\n\n将对象的创建和对象本身业务处理分离可以降低系统的耦合度,使得两者修改起来都相对容易。\n\n在调用工厂类的工厂方法时,由于工厂方法是静态方法,使用起来很方便,可通过类名直接调用,而且只需要传入一个简单的参数即可.\n\n在实际开发中,还可以在调用时将所传入的参数保存在xml等格式的配置文件中,修改参数时无须修改任何源代码。\n\n简单工厂模式最大的问题在于工厂类的职责相对过重,增加新的产品需要修改工厂类的判断逻辑,这一点与开闭原则是相违背的。\n\n简单工厂模式的要点在于:当你需要什么,只需要传入一个正确的参数,就可以获取你所需要的对象,而无须知道其创建细节。\n\n\n# 工厂模式可以分为三类:\n\n简单工厂模式(simple factory) 工厂方法模式(factory method) 抽象工厂模式(abstract factory) 简单工厂其实不是一个标准的的设计模式。gof 23种设计模式中只有「工厂方法模式」与「抽象工厂模式」。简单工厂模式可以看为工厂方法模式的一种特例,为了统一整理学习,就都归为工厂模式。\n\n这三种工厂模式在设计模式的分类中都属于创建型模式,三种模式从上到下逐步抽象。\n\n创建型模式 创建型模式(creational pattern)对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。为了使软件的结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。\n\n创建型模式在创建什么(what),由谁创建(who),何时创建(when)等方面都为软件设计者提供了尽可能大的灵活性。\n\n创建型模式隐藏了类的实例的创建细节,通过隐藏对象如何被创建和组合在一起达到使整个系统独立的目的。\n\n工厂模式是创建型模式中比较重要的。工厂模式的主要功能就是帮助我们实例化对象。之所以名字中包含工厂模式四个字,是因为对象的实例化过程是通过工厂实现的,是用工厂代替new操作的。\n\n工厂模式优点 可以使代码结构清晰,有效地封装变化。在编程中,产品类的实例化有时候是比较复杂和多变的,通过工厂模式,将产品的实例化封装起来,使得调用者根本无需关心产品的实例化过程,只需依赖工厂即可得到自己想要的产品。 对调用者屏蔽具体的产品类。如果使用工厂模式,调用者只关心产品的接口就可以了,至于具体的实现,调用者根本无需关心。即使变更了具体的实现,对调用者来说没有任何影响。 降低耦合度。产品类的实例化通常来说是很复杂的,它需要依赖很多的类,而这些类对于调用者来说根本无需知道,如果使用了工厂方法,我们需要做的仅仅是实例化好产品类,然后交给调用者使用。对调用者来说,产品所依赖的类都是透明的。 适用场景 不管是简单工厂模式,工厂方法模式还是抽象工厂模式,他们具有类似的特性,所以他们的适用场景也是类似的。\n\n首先,作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过new就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。\n\n其次,工厂模式是一种典型的解耦模式,迪米特法则在工厂模式中表现的尤为明显。假如调用者自己组装产品需要增加依赖关系时,可以考虑使用工厂模式。将会大大降低对象之间的耦合度。\n\n再次,由于工厂模式是依靠抽象架构的,它把实例化产品的任务交由实现类完成,扩展性比较好。也就是说,当需要系统有比较好的扩展性时,可以考虑工厂模式,不同的产品用不同的实现工厂来组装。\n\n\n# 一、简单工厂模式\n\n在介绍简单工厂模式之前,我们尝试解决以下问题:\n\n现在我们要使用面向对象的形式定义计算器,为了实现各算法之间的解耦。我们一般会这么写:\n\n// 计算类的基类\n@setter\n@getter\npublic abstract class operation {\n private double value1 = 0;\n private double value2 = 0;\n protected abstract double getresule();\n}\n\n//加法\npublic class operationadd extends operation {\n @override\n protected double getresule() {\n return getvalue1() + getvalue2();\n }\n}\n//减法\npublic class operationsub extends operation {\n @override\n protected double getresule() {\n return getvalue1() - getvalue2();\n }\n}\n//乘法\npublic class operationmul extends operation {\n @override\n protected double getresule() {\n return getvalue1() * getvalue2();\n }\n}\n//除法\npublic class operationdiv extends operation {\n @override\n protected double getresule() {\n if (getvalue2() != 0) {\n return getvalue1() / getvalue2();\n }\n throw new illegalargumentexception(\"除数不能为零\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n\n\n当我们要使用这个计算器的时候,又会这么写:\n\npublic static void main(string[] args) {\n //计算两数之和\n operationadd operationadd = new operationadd();\n operationadd.setvalue1(1);\n operationadd.setvalue2(2);\n system.out.println(\"sum:\"+operationadd.getresule());\n //计算两数乘积\n operationmul operationmul = new operationmul();\n operationmul.setvalue1(3);\n operationmul.setvalue2(5);\n system.out.println(\"multiply:\"+operationmul.getresule());\n //计算两数之差。。。\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n想要使用不同的运算的时候就要创建不同的类,并且要明确知道该类的名字。那么这种重复的创建类的工作其实可以放到一个统一的类中去管理。这样的方法我们就叫做「简单工厂模式」,在简单工厂模式中用于创建实例的方法是静态(static)方法,因此简单工厂模式又被称为「静态工厂方法」模式。。简单工厂模式有以下优点:\n\n一个调用者想创建一个对象,只要知道其名称就可以了。 屏蔽产品的具体实现,调用者只关心产品的接口。\n\n\n# 定义\n\n提供一个创建对象实例的功能,而无需关心其具体实现。被创建实例的类型可以是接口、抽象类,也可以是具体的类。\n\n\n# 简单工厂模式实现方式\n\n没骗你,简单工厂模式,真是因为简单才被叫做简单工厂模式的。\n\n简单工厂模式包含 3 个角色(要素):\n\nfactory:即工厂类, 简单工厂模式的核心部分,负责实现创建所有产品的内部逻辑;工厂类可以被外界直接调用,创建所需对象 product:抽象类产品, 它是工厂类所创建的所有对象的父类,封装了各种产品对象的公有方法,它的引入将提高系统的灵活性,使得在工厂类中只需定义一个通用的工厂方法,因为所有创建的具体产品对象都是其子类对象 concreteproduct:具体产品, 它是简单工厂模式的创建目标,所有被创建的对象都充当这个角色的某个具体类的实例。它要实现抽象产品中声明的抽象方法\n\n\n# uml类图\n\n\n\n\n# 实例\n\n现在我们定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。\n\n//工厂类\npublic class operationfactory {\n\n public static operation createoperation(string operation) {\n operation oper = null;\n switch (operation) {\n case \"add\":\n oper = new operationadd();\n break;\n case \"sub\":\n oper = new operationsub();\n break;\n case \"mul\":\n oper = new operationmul();\n break;\n\n case \"div\":\n oper = new operationdiv();\n break;\n default:\n throw new unsupportedoperationexception(\"不支持该操作\");\n }\n return oper;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n\n\n有了工厂类之后,可以使用工厂创建对象:\n\npublic static void main(string[] args) {\n operation operationadd = operationfactory.createoperation(\"add\");\n operationadd.setvalue1(1);\n operationadd.setvalue2(2)\n system.out.println(operationadd.getresule());\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n通过简单工厂模式,该计算器的使用者不需要关系实现加法逻辑的那个类的具体名字,只要知道该类对应的参数\"add\"就可以了。这就体现了之前提到的工厂模式的优点。\n\n\n# 简单工厂模式存在的问题\n\n当我们需要增加一种计算时,例如开平方。这个时候我们需要先定义一个类继承operation类,其中实现平方的代码。除此之外我们还要修改 operationfactory 类的代码,增加一个 case。这显然是违背开闭原则的。可想而知对于新产品的加入,工厂类是很被动的。\n\n我们举的例子是最简单的情况。而在实际应用中,很可能产品是一个多层次的树状结构。 简单工厂可能就不太适用了。\n\n\n# 简单工厂模式总结\n\n工厂类是整个简单工厂模式的关键。包含了必要的逻辑判断,根据外界给定的信息,决定究竟应该创建哪个具体类的对象。通过使用工厂类,外界可以从直接创建具体产品对象的尴尬局面摆脱出来,仅仅需要负责“消费”对象就可以了。而不必管这些对象究竟如何创建及如何组织的。明确了各自的职责和权利,有利于整个软件体系结构的优化。\n\n但是由于工厂类集中了所有实例的创建逻辑,违反了高内聚责任分配原则,将全部创建逻辑集中到了一个工厂类中;它所能创建的类只能是事先考虑到的,如果需要添加新的类,则就需要改变工厂类了。\n\n当系统中的具体产品类不断增多时候,可能会出现要求工厂类根据不同条件创建不同实例的需求.这种对条件的判断和对具体产品类型的判断交错在一起,很难避免模块功能的蔓延,对系统的维护和扩展非常不利;\n\n为了解决这些缺点,就有了工厂方法模式。\n\n\n# 二、工厂方法模式\n\n我们常说的工厂模式,就是指「工厂方法模式」,也叫「虚拟构造器模式」或「多态工厂模式」。\n\n\n# 2.1 定义\n\n定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类。工厂方法让类的实例化推迟到子类中进行。\n\n\n# 2.2 工厂方法模式实现方式\n\n工厂方法模式包含 4 个角色(要素):\n\nproduct:抽象产品,定义工厂方法所创建的对象的接口,也就是实际需要使用的对象的接口 concreteproduct:具体产品,具体的product接口的实现对象 factory:工厂接口,也可以叫 creator(创建器),申明工厂方法,通常返回一个 product 类型的实例对象 concretefactory:工厂实现,或者叫 concretecreator(创建器对象),覆盖 factory 定义的工厂方法,返回具体的 product 实例\n\n\n# uml类图\n\n\n\n\n# 实例\n\n从 uml 类图可以看出,每种产品实现,我们都要增加一个继承于工厂接口 ifactory 的工厂类 factory ,修改简单工厂模式代码中的工厂类如下:\n\n//工厂接口\npublic interface ifactory {\n operation createoption();\n}\n\n//加法类工厂\npublic class addfactory implements ifactory {\n public operation createoption() {\n return new operationadd();\n }\n}\n\n//减法类工厂\npublic class subfactory implements ifactory {\n public operation createoption() {\n return new operationsub();\n }\n}\n\n//乘法类工厂\npublic class mulfactory implements ifactory {\n public operation createoption() {\n return new operationmul();\n }\n}\n\n//除法类工厂\npublic class divfactory implements ifactory {\n public operation createoption() {\n return new operationdiv();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n\n\n这时,我们使用计算器的时候,要为每种运算方法增加一个工厂对象\n\npublic class client {\n public static void main(string[] args) {\n //减法\n ifactory subfactory = new subfactory();\n operation operationsub = subfactory.createoption();\n operationsub.setvalue1(22);\n operationsub.setvalue2(20);\n system.out.println(\"sub:\"+operationsub.getresult());\n //除法\n ifactory divfactory = new divfactory();\n operation operationdiv = divfactory.createoption();\n operationdiv.setvalue1(99);\n operationdiv.setvalue2(33);\n system.out.println(\"div:\"+operationsub.getresult());\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\n纳尼,这不是更复杂了吗,每个产品对应一个工厂,我又不是按代码量赚钱的。。。\n\n\n# 2.3 工厂方法模式适用场景\n\n工厂方法模式和简单工厂模式虽然都是通过工厂来创建对象,他们之间最大的不同是——工厂方法模式在设计上完全完全符合“开闭原则”。\n\n在以下情况下可以使用工厂方法模式:\n\n一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。 一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。 将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定,可将具体工厂类的类名存储在配置文件或数据库中。 使用场景 日志记录器:日志可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方。 数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。 设计一个连接服务器的框架,需要三个协议,\"pop3\"、\"imap\"、\"http\",可以把这三个作为产品类,共同实现一个接口。 比如 hibernate 换数据库只需换方言和驱动就可以\n\n\n# 2.4 工厂方法模式总结\n\n工厂方法模式是简单工厂模式的进一步抽象和推广。\n\n由于使用了面向对象的多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。\n\n在工厂方法模式中,核心的工厂类不再负责所有产品的创建,而是将具体创建工作交给子类去做。这个核心类仅仅负责给出具体工厂必须实现的接口,而不负责产品类被实例化这种细节,这使得工厂方法模式可以允许系统在不修改工厂角色的情况下引进新产品。\n\n优点:\n\n一个调用者想创建一个对象,只要知道其名称就可以了。 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。 屏蔽产品的具体实现,调用者只关心产品的接口。 缺点:\n\n每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。\n\n\n# 三、抽象工厂模式\n\n工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。此时,我们可以考虑将一些相关的产品组成一个“产品族”,由同一个工厂来统一生产,这就是抽象工厂模式的基本思想。\n\n\n# 3.1 定义\n\n为创建一组相关或相互依赖的对象提供一个接口,而且无需指定他们的具体类。\n\n抽象工厂(abstract factory)模式,又称工具箱(kit 或toolkit)模式。\n\n\n# 3.2 抽象工厂模式实现方式\n\n抽象工厂模式是工厂方法模式的升级版本,他用来创建一组相关或者相互依赖的对象。他与工厂方法模式的区别就在于,工厂方法模式针对的是一个产品等级结构;而抽象工厂模式则是针对的多个产品等级结构。在编程中,通常一个产品结构,表现为一个接口或者抽象类,也就是说,工厂方法模式提供的所有产品都是衍生自同一个接口或抽象类,而抽象工厂模式所提供的产品则是衍生自不同的接口或抽象类。\n\n在抽象工厂模式中,有一个产品族的概念:所谓的产品族,是指位于不同产品等级结构中功能相关联的产品组成的家族。抽象工厂模式所提供的一系列产品就组成一个产品族;而工厂方法提供的一系列产品称为一个等级结构。\n\n也没骗你,抽象工厂模式确实是抽象。\n\n抽象工厂模式包含的角色(要素):\n\nabstractfactory:抽象工厂,用于声明生成抽象产品的方法 concretefactory:具体工厂,实现抽象工厂定义的方法,具体实现一系列产品对象的创建 abstractproduct:抽象产品,定义一类产品对象的接口 concreteproduct:具体产品,通常在具体工厂里,会选择具体的产品实现,来创建符合抽象工厂定义的方法返回的产品类型的对象。 client:客户端,使用抽象工厂来获取一系列所需要的产品对象\n\n\n# uml类图\n\n\n# 实例\n\n我把维基百科的例子改下用于理解,假设我们要生产两种产品,键盘(keyboard)和鼠标(mouse) ,每一种产品都支持多种系列,比如 mac 系列和 windows 系列。这样每个系列的产品分别是 mackeyboard winkeyboard, macmouse, winmouse 。为了可以在运行时刻创建一个系列的产品族,我们可以为每个系列的产品族创建一个工厂 macfactory 和 winfactory 。每个工厂都有两个方法 createmouse 和 createkeyboard 并返回对应的产品,可以将这两个方法抽象成一个接口 hardware 。这样在运行时刻我们可以选择创建需要的产品系列。\n\n抽象产品\n\npublic interface keyboard {\n void input();\n}\npublic interface mouse {\n void click();\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n具体产品\n\n//具体产品\n\npublic class mackeyboard implements keyboard {\n @override\n public void input() {\n system.out.println(\"mac 专用键盘\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\npublic class macmouse implements mouse {\n @override\n public void click() {\n system.out.println(\"mac 专用鼠标\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\npublic class winkeyboard implements keyboard {\n @override\n public void input() {\n system.out.println(\"win 专用键盘\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\npublic class winmouse implements mouse {\n @override\n public void click() {\n system.out.println(\"win 专用鼠标\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n抽象工厂\n\npublic interface hardware {\n keyboard createkyeboard();\n mouse createmouse();\n}\n\n\n1\n2\n3\n4\n\n\n具体的工厂类\n\npublic class macfactory implements hardware{\n @override\n public keyboard createkyeboard() {\n return new mackeyboard();\n }\n\n @override\n public mouse createmouse() {\n return new macmouse();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\npublic class winfactory implements hardware{\n @override\n public keyboard createkyeboard() {\n return new winkeyboard();\n }\n\n @override\n public mouse createmouse() {\n return new winmouse();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n使用\n\npublic class client {\n public static void main(string[] args) {\n hardware macfactory = new macfactory();\n keyboard keyboard = macfactory.createkyeboard();\n keyboard.input(); //mac 专用键盘\n\n hardware winfactory = new winfactory();\n mouse mouse = winfactory.createmouse();\n mouse.click(); //win 专用鼠标\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n\n# 3.3 抽象工厂模式适用场景\n\n抽象工厂模式和工厂方法模式一样,都符合开闭原则。但是不同的是,工厂方法模式在增加一个具体产品的时候,都要增加对应的工厂。但是抽象工厂模式只有在新增一个类型的具体产品时才需要新增工厂。也就是说,工厂方法模式的一个工厂只能创建一个具体产品。而抽象工厂模式的一个工厂可以创建属于一类类型的多种具体产品。工厂创建产品的个数介于简单工厂模式和工厂方法模式之间。\n\n在以下情况下可以使用抽象工厂模式:\n\n一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是重要的。 系统中有多于一个的产品族,而每次只使用其中某一产品族。 属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。 系统结构稳定,不会频繁的增加对象。 “开闭原则”的倾斜性\n\n在抽象工厂模式中,增加新的产品族很方便,但是增加新的产品等级结构很麻烦,抽象工厂模式的这种性质称为**“开闭原则”的倾斜性**。“开闭原则”要求系统对扩展开放,对修改封闭,通过扩展达到增强其功能的目的,对于涉及到多个产品族与多个产品等级结构的系统,其功能增强包括两方面:\n\n增加产品族:对于增加新的产品族,工厂方法模式很好的支持了“开闭原则”,对于新增加的产品族,只需要对应增加一个新的具体工厂即可,对已有代码无须做任何修改。 增加新的产品等级结构:对于增加新的产品等级结构,需要修改所有的工厂角色,包括抽象工厂类,在所有的工厂类中都需要增加生产新产品的方法,违背了“开闭原则”。 正因为抽象工厂模式存在“开闭原则”的倾斜性,它以一种倾斜的方式来满足“开闭原则”,为增加新产品族提供方便,但不能为增加新产品结构提供这样的方便,因此要求设计人员在设计之初就能够全面考虑,不会在设计完成之后向系统中增加新的产品等级结构,也不会删除已有的产品等级结构,否则将会导致系统出现较大的修改,为后续维护工作带来诸多麻烦。\n\n\n# 3.4 抽象工厂模式总结\n\n抽象工厂模式是工厂方法模式的进一步延伸,由于它提供了功能更为强大的工厂类并且具备较好的可扩展性,在软件开发中得以广泛应用,尤其是在一些框架和api类库的设计中,例如在java语言的awt(抽象窗口工具包)中就使用了抽象工厂模式,它使用抽象工厂模式来实现在不同的操作系统中应用程序呈现与所在操作系统一致的外观界面。抽象工厂模式也是在软件开发中最常用的设计模式之一。\n\n优点:\n\n抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易,所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。 增加新的产品族很方便,无须修改已有系统,符合“开闭原则”。 缺点:\n\n增加新的产品等级结构麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,违背了“开闭原则”。\n\n\n# 工厂模式的退化\n\n当抽象工厂模式中每一个具体工厂类只创建一个产品对象,也就是只存在一个产品等级结构时,抽象工厂模式退化成工厂方法模式;当工厂方法模式中抽象工厂与具体工厂合并,提供一个统一的工厂来创建产品对象,并将创建对象的工厂方法设计为静态方法时,工厂方法模式退化成简单工厂模式。\n\n\n# 四、我们身边的工厂模式\n\n工厂模式在java码农身边真是无处不在,不信打开你的项目,搜索 factory\n\n我们最常用的 spring 就是一个最大的 bean 工厂,ioc 通过beanfactory对bean 进行管理。\n\n我们使用的日志门面框架slf4j,点进去就可以看到熟悉的味道\n\nprivate final static logger logger = loggerfactory.getlogger(helloword.class); jdk 的 calendar 使用了简单工厂模式\n\n calendar calendar = calendar.getinstance();\n\n\n1\n\n\n> 参考\n> https://javakeeper.starfish.ink/design-pattern/factory-pattern.html", "charsets": { "cjk": true }, @@ -2778,11 +3333,11 @@ export const siteData = { "lastUpdatedTimestamp": 1655736461000 }, { - "title": "简述装饰者模式以及适配器模式", + "title": "模板方法模式", "frontmatter": { - "title": "简述装饰者模式以及适配器模式", - "date": "2022-05-21T17:26:42.000Z", - "permalink": "/pages/47a4e3/", + "title": "模板方法模式", + "date": "2022-05-21T17:29:10.000Z", + "permalink": "/pages/43b656/", "categories": [ "Java相关", "设计模式" @@ -2791,10 +3346,10 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/005.%E8%A3%85%E9%A5%B0%E8%80%85%E6%A8%A1%E5%BC%8F.html", - "relativePath": "01.Java相关/40.设计模式/005.装饰者模式.md", - "key": "v-22a9eaef", - "path": "/pages/47a4e3/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/040.%E6%A8%A1%E6%9D%BF%E6%96%B9%E6%B3%95%E6%A8%A1%E5%BC%8F.html", + "relativePath": "01.Java相关/40.设计模式/040.模板方法模式.md", + "key": "v-28deb9da", + "path": "/pages/43b656/", "headersStr": null, "content": "", "normalizedContent": "", @@ -2803,11 +3358,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "适配器模式", + "title": "代理模式", "frontmatter": { - "title": "适配器模式", - "date": "2022-05-21T17:28:10.000Z", - "permalink": "/pages/827fc4/", + "title": "代理模式", + "date": "2022-05-21T17:30:05.000Z", + "permalink": "/pages/a3c735/", "categories": [ "Java相关", "设计模式" @@ -2816,10 +3371,10 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/010.%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8F.html", - "relativePath": "01.Java相关/40.设计模式/010.适配器模式.md", - "key": "v-31509c80", - "path": "/pages/827fc4/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/070.%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F.html", + "relativePath": "01.Java相关/40.设计模式/070.代理模式.md", + "key": "v-2279e49d", + "path": "/pages/a3c735/", "headersStr": null, "content": "", "normalizedContent": "", @@ -2828,61 +3383,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "代理模式", + "title": "观察模式", "frontmatter": { - "title": "代理模式", - "date": "2022-05-21T17:28:27.000Z", - "permalink": "/pages/dd3a6f/", - "categories": [ - "Java相关", - "设计模式" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/020.%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F.html", - "relativePath": "01.Java相关/40.设计模式/020.代理模式.md", - "key": "v-0564ba27", - "path": "/pages/dd3a6f/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "责任链模式", - "frontmatter": { - "title": "责任链模式", - "date": "2022-05-21T17:28:46.000Z", - "permalink": "/pages/d3ff35/", - "categories": [ - "Java相关", - "设计模式" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/030.%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F.html", - "relativePath": "01.Java相关/40.设计模式/030.责任链模式.md", - "key": "v-34f8aaa2", - "path": "/pages/d3ff35/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "观察模式", - "frontmatter": { - "title": "观察模式", - "date": "2022-05-21T17:29:45.000Z", - "permalink": "/pages/cbff3c/", + "title": "观察模式", + "date": "2022-05-21T17:29:45.000Z", + "permalink": "/pages/cbff3c/", "categories": [ "Java相关", "设计模式" @@ -2903,36 +3408,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "模板方法模式", - "frontmatter": { - "title": "模板方法模式", - "date": "2022-05-21T17:29:10.000Z", - "permalink": "/pages/43b656/", - "categories": [ - "Java相关", - "设计模式" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/040.%E6%A8%A1%E6%9D%BF%E6%96%B9%E6%B3%95%E6%A8%A1%E5%BC%8F.html", - "relativePath": "01.Java相关/40.设计模式/040.模板方法模式.md", - "key": "v-28deb9da", - "path": "/pages/43b656/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "代理模式", + "title": "策略模式", "frontmatter": { - "title": "代理模式", - "date": "2022-05-21T17:30:05.000Z", - "permalink": "/pages/a3c735/", + "title": "策略模式", + "date": "2022-05-21T17:29:26.000Z", + "permalink": "/pages/78c7aa/", "categories": [ "Java相关", "设计模式" @@ -2941,10 +3421,10 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/070.%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F.html", - "relativePath": "01.Java相关/40.设计模式/070.代理模式.md", - "key": "v-2279e49d", - "path": "/pages/a3c735/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/050.%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F.html", + "relativePath": "01.Java相关/40.设计模式/050.策略模式.md", + "key": "v-5b0157cf", + "path": "/pages/78c7aa/", "headersStr": null, "content": "", "normalizedContent": "", @@ -2953,11 +3433,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "策略模式", + "title": "建造者模式", "frontmatter": { - "title": "策略模式", - "date": "2022-05-21T17:29:26.000Z", - "permalink": "/pages/78c7aa/", + "title": "建造者模式", + "date": "2022-05-21T17:30:20.000Z", + "permalink": "/pages/c332ae/", "categories": [ "Java相关", "设计模式" @@ -2966,10 +3446,10 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/050.%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F.html", - "relativePath": "01.Java相关/40.设计模式/050.策略模式.md", - "key": "v-5b0157cf", - "path": "/pages/78c7aa/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/080.%E5%BB%BA%E9%80%A0%E8%80%85%E6%A8%A1%E5%BC%8F.html", + "relativePath": "01.Java相关/40.设计模式/080.建造者模式.md", + "key": "v-dd5f4440", + "path": "/pages/c332ae/", "headersStr": null, "content": "", "normalizedContent": "", @@ -2978,269 +3458,188 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "计算机网络", + "title": "操作系统", "frontmatter": { - "title": "计算机网络", - "date": "2022-03-11T00:51:01.000Z", - "permalink": "/pages/e00207/", + "title": "操作系统", + "date": "2022-03-11T00:51:48.000Z", + "permalink": "/pages/e5f2d7/", "categories": [ "计算机基础", - "HTML" + "CSS" ], "tags": [ null ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/01.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C.html", - "relativePath": "02.计算机基础/10.计算机网络/01.计算机网络.md", - "key": "v-1e653940", - "path": "/pages/e00207/", + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/01.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F.html", + "relativePath": "02.计算机基础/20.操作系统/01.操作系统.md", + "key": "v-6f178cbc", + "path": "/pages/e5f2d7/", "headers": [ { "level": 2, - "title": "简述 TCP 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手?", - "slug": "简述-tcp-三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手", - "normalizedTitle": "简述 tcp 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手?", + "title": "进程间有哪些通信方式?", + "slug": "进程间有哪些通信方式", + "normalizedTitle": "进程间有哪些通信方式?", "charIndex": 2 }, { "level": 2, - "title": "从输入 URL 到展现页面的全过程", - "slug": "从输入-url-到展现页面的全过程", - "normalizedTitle": "从输入 url 到展现页面的全过程", - "charIndex": 44 - }, - { - "level": 2, - "title": "HTTP 中 GET 和 POST 区别", - "slug": "http-中-get-和-post-区别", - "normalizedTitle": "http 中 get 和 post 区别", - "charIndex": 66 + "title": "简述自旋锁与互斥锁的使用场景", + "slug": "简述自旋锁与互斥锁的使用场景", + "normalizedTitle": "简述自旋锁与互斥锁的使用场景", + "charIndex": 18 }, { "level": 2, - "title": "TCP 怎么保证可靠传输", - "slug": "tcp-怎么保证可靠传输", - "normalizedTitle": "tcp 怎么保证可靠传输", - "charIndex": 91 + "title": "进程和线程之间有什么区别?", + "slug": "进程和线程之间有什么区别", + "normalizedTitle": "进程和线程之间有什么区别?", + "charIndex": 37 }, { "level": 2, - "title": "什么是 TCP 粘包和拆包?", - "slug": "什么是-tcp-粘包和拆包", - "normalizedTitle": "什么是 tcp 粘包和拆包?", - "charIndex": 108 + "title": "简述几个常用的 Linux 命令以及他们的功能", + "slug": "简述几个常用的-linux-命令以及他们的功能", + "normalizedTitle": "简述几个常用的 linux 命令以及他们的功能", + "charIndex": 55 }, { "level": 2, - "title": "简述 HTTPS 的加密与认证过程", - "slug": "简述-https-的加密与认证过程", - "normalizedTitle": "简述 https 的加密与认证过程", - "charIndex": 127 + "title": "创建线程有多少种方式?", + "slug": "创建线程有多少种方式", + "normalizedTitle": "创建线程有多少种方式?", + "charIndex": 83 }, { "level": 2, - "title": "简述对称与非对称加密的概念", - "slug": "简述对称与非对称加密的概念", - "normalizedTitle": "简述对称与非对称加密的概念", - "charIndex": 149 + "title": "线程有多少种状态,状态之间如何转换", + "slug": "线程有多少种状态-状态之间如何转换", + "normalizedTitle": "线程有多少种状态,状态之间如何转换", + "charIndex": 99 }, { "level": 2, - "title": "TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别?", - "slug": "tcp-与-udp-在网络协议中的哪一层-他们之间有什么区别", - "normalizedTitle": "tcp 与 udp 在网络协议中的哪一层,他们之间有什么区别?", - "charIndex": 167 + "title": "进程有多少种状态?", + "slug": "进程有多少种状态", + "normalizedTitle": "进程有多少种状态?", + "charIndex": 121 }, { "level": 2, - "title": "如何解决 TCP 传输丢包问题?", - "slug": "如何解决-tcp-传输丢包问题", - "normalizedTitle": "如何解决 tcp 传输丢包问题?", - "charIndex": 203 + "title": "简述 select, poll, epoll 的使用场景以及区别,epoll 中水平触发以及边缘触发有什么不同?", + "slug": "简述-select-poll-epoll-的使用场景以及区别-epoll-中水平触发以及边缘触发有什么不同", + "normalizedTitle": "简述 select, poll, epoll 的使用场景以及区别,epoll 中水平触发以及边缘触发有什么不同?", + "charIndex": 135 }, { "level": 2, - "title": "TCP 长连接和短连接有那么不同的使用场景?", - "slug": "tcp-长连接和短连接有那么不同的使用场景", - "normalizedTitle": "tcp 长连接和短连接有那么不同的使用场景?", - "charIndex": 224 + "title": "Linux 下如何排查 CPU 以及 内存占用过多?", + "slug": "linux-下如何排查-cpu-以及-内存占用过多", + "normalizedTitle": "linux 下如何排查 cpu 以及 内存占用过多?", + "charIndex": 196 }, { "level": 2, - "title": "TCP 的 keepalive 了解吗?说一说它和 HTTP 的 keepalive 的区别?", - "slug": "tcp-的-keepalive-了解吗-说一说它和-http-的-keepalive-的区别", - "normalizedTitle": "tcp 的 keepalive 了解吗?说一说它和 http 的 keepalive 的区别?", - "charIndex": 251 + "title": "进程通信中的管道实现原理是什么?", + "slug": "进程通信中的管道实现原理是什么", + "normalizedTitle": "进程通信中的管道实现原理是什么?", + "charIndex": 227 }, { "level": 2, - "title": "HTTP 与 HTTPS 有哪些区别?", - "slug": "http-与-https-有哪些区别", - "normalizedTitle": "http 与 https 有哪些区别?", - "charIndex": 303 + "title": "Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程?", + "slug": "linux-下如何查看-cpu-荷载-正在运行的进程-某个端口对应的进程", + "normalizedTitle": "linux 下如何查看 cpu 荷载,正在运行的进程,某个端口对应的进程?", + "charIndex": 248 }, { "level": 2, - "title": "TCP 中常见的拥塞控制算法有哪些?", - "slug": "tcp-中常见的拥塞控制算法有哪些", - "normalizedTitle": "tcp 中常见的拥塞控制算法有哪些?", - "charIndex": 327 + "title": "如何调试服务器内存占用过高的问题?", + "slug": "如何调试服务器内存占用过高的问题", + "normalizedTitle": "如何调试服务器内存占用过高的问题?", + "charIndex": 290 }, { "level": 2, - "title": "简述 HTTP 1.0,1.1,2.0 的主要区别", - "slug": "简述-http-1-0-1-1-2-0-的主要区别", - "normalizedTitle": "简述 http 1.0,1.1,2.0 的主要区别", - "charIndex": 350 + "title": "Linux 如何查看实时的滚动日志?", + "slug": "linux-如何查看实时的滚动日志", + "normalizedTitle": "linux 如何查看实时的滚动日志?", + "charIndex": 312 }, { "level": 2, - "title": "从系统层面上,UDP如何保证尽量可靠?", - "slug": "从系统层面上-udp如何保证尽量可靠", - "normalizedTitle": "从系统层面上,udp如何保证尽量可靠?", - "charIndex": 380 + "title": "简述 Linux 零拷贝的原理", + "slug": "简述-linux-零拷贝的原理", + "normalizedTitle": "简述 linux 零拷贝的原理", + "charIndex": 335 }, { "level": 2, - "title": "TCP 挥手时出现大量 CLOSEWAIT 或 TIMEWAIT 怎么解决?", - "slug": "tcp-挥手时出现大量-close-wait-或-time-wait-怎么解决", - "normalizedTitle": "tcp 挥手时出现大量 closewait 或 timewait 怎么解决?", - "charIndex": null + "title": "为什么进程切换慢,线程切换快?", + "slug": "为什么进程切换慢-线程切换快", + "normalizedTitle": "为什么进程切换慢,线程切换快?", + "charIndex": 355 }, { "level": 2, - "title": "简述 TCP 滑动窗口以及重传机制", - "slug": "简述-tcp-滑动窗口以及重传机制", - "normalizedTitle": "简述 tcp 滑动窗口以及重传机制", - "charIndex": 449 + "title": "简述创建进程的流程", + "slug": "简述创建进程的流程", + "normalizedTitle": "简述创建进程的流程", + "charIndex": 375 }, { "level": 2, - "title": "简述 JWT 的原理和校验机制", - "slug": "简述-jwt-的原理和校验机制", - "normalizedTitle": "简述 jwt 的原理和校验机制", - "charIndex": 471 - }, - { - "level": 2, - "title": "为什么需要序列化?有什么序列化的方式?", - "slug": "为什么需要序列化-有什么序列化的方式", - "normalizedTitle": "为什么需要序列化?有什么序列化的方式?", - "charIndex": 491 - }, - { - "level": 2, - "title": "简述 iPv4 和 iPv6 的区别", - "slug": "简述-ipv4-和-ipv6-的区别", - "normalizedTitle": "简述 ipv4 和 ipv6 的区别", - "charIndex": 515 - }, - { - "level": 2, - "title": "TCP 中 SYN 攻击是什么?如何防止?", - "slug": "tcp-中-syn-攻击是什么-如何防止", - "normalizedTitle": "tcp 中 syn 攻击是什么?如何防止?", - "charIndex": 538 - }, - { - "level": 2, - "title": "简述 DDOS 攻击原理,如何防范它?", - "slug": "简述-ddos-攻击原理-如何防范它", - "normalizedTitle": "简述 ddos 攻击原理,如何防范它?", - "charIndex": 564 - }, - { - "level": 2, - "title": "什么是 ARP 协议?简述其使用场景", - "slug": "什么是-arp-协议-简述其使用场景", - "normalizedTitle": "什么是 arp 协议?简述其使用场景", - "charIndex": 588 - }, - { - "level": 2, - "title": "什么是跨域,什么情况下会发生跨域请求?", - "slug": "什么是跨域-什么情况下会发生跨域请求", - "normalizedTitle": "什么是跨域,什么情况下会发生跨域请求?", - "charIndex": 611 - }, - { - "level": 2, - "title": "Cookie 和 Session 的关系和区别是什么?", - "slug": "cookie-和-session-的关系和区别是什么", - "normalizedTitle": "cookie 和 session 的关系和区别是什么?", - "charIndex": 635 + "title": "简述 Linux 虚拟内存的页面置换算法", + "slug": "简述-linux-虚拟内存的页面置换算法", + "normalizedTitle": "简述 linux 虚拟内存的页面置换算法", + "charIndex": 389 }, { "level": 2, - "title": "简述 WebSocket 是如何进行传输的", - "slug": "简述-websocket-是如何进行传输的", - "normalizedTitle": "简述 websocket 是如何进行传输的", - "charIndex": 667 + "title": "简述 Linux 进程调度的算法", + "slug": "简述-linux-进程调度的算法", + "normalizedTitle": "简述 linux 进程调度的算法", + "charIndex": 414 }, { "level": 2, - "title": "DNS 查询服务器的基本流程是什么?DNS 劫持是什么?", - "slug": "dns-查询服务器的基本流程是什么-dns-劫持是什么", - "normalizedTitle": "dns 查询服务器的基本流程是什么?dns 劫持是什么?", - "charIndex": 693 + "title": "什么时候会由用户态陷入内核态?", + "slug": "什么时候会由用户态陷入内核态", + "normalizedTitle": "什么时候会由用户态陷入内核态?", + "charIndex": 435 }, { "level": 2, - "title": "RestFul 是什么?RestFul 请求的 URL 有什么特点?", - "slug": "restful-是什么-restful-请求的-url-有什么特点", - "normalizedTitle": "restful 是什么?restful 请求的 url 有什么特点?", - "charIndex": 726 + "title": "Linux 下如何查看端口被哪个进程占用?", + "slug": "linux-下如何查看端口被哪个进程占用", + "normalizedTitle": "linux 下如何查看端口被哪个进程占用?", + "charIndex": 455 }, { "level": 2, - "title": "简述 HTTP 报文头部的组成结构", - "slug": "简述-http-报文头部的组成结构", - "normalizedTitle": "简述 http 报文头部的组成结构", - "charIndex": 765 + "title": "Linux 中虚拟内存和物理内存有什么区别?有什么优点?", + "slug": "linux-中虚拟内存和物理内存有什么区别-有什么优点", + "normalizedTitle": "linux 中虚拟内存和物理内存有什么区别?有什么优点?", + "charIndex": 481 }, { "level": 2, - "title": "什么是 SYN flood,如何防止这类攻击?", - "slug": "什么是-syn-flood-如何防止这类攻击", - "normalizedTitle": "什么是 syn flood,如何防止这类攻击?", - "charIndex": 787 + "title": "简述 traceroute 命令的原理", + "slug": "简述-traceroute-命令的原理", + "normalizedTitle": "简述 traceroute 命令的原理", + "charIndex": 514 } ], - "headersStr": "简述 TCP 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手? 从输入 URL 到展现页面的全过程 HTTP 中 GET 和 POST 区别 TCP 怎么保证可靠传输 什么是 TCP 粘包和拆包? 简述 HTTPS 的加密与认证过程 简述对称与非对称加密的概念 TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别? 如何解决 TCP 传输丢包问题? TCP 长连接和短连接有那么不同的使用场景? TCP 的 keepalive 了解吗?说一说它和 HTTP 的 keepalive 的区别? HTTP 与 HTTPS 有哪些区别? TCP 中常见的拥塞控制算法有哪些? 简述 HTTP 1.0,1.1,2.0 的主要区别 从系统层面上,UDP如何保证尽量可靠? TCP 挥手时出现大量 CLOSEWAIT 或 TIMEWAIT 怎么解决? 简述 TCP 滑动窗口以及重传机制 简述 JWT 的原理和校验机制 为什么需要序列化?有什么序列化的方式? 简述 iPv4 和 iPv6 的区别 TCP 中 SYN 攻击是什么?如何防止? 简述 DDOS 攻击原理,如何防范它? 什么是 ARP 协议?简述其使用场景 什么是跨域,什么情况下会发生跨域请求? Cookie 和 Session 的关系和区别是什么? 简述 WebSocket 是如何进行传输的 DNS 查询服务器的基本流程是什么?DNS 劫持是什么? RestFul 是什么?RestFul 请求的 URL 有什么特点? 简述 HTTP 报文头部的组成结构 什么是 SYN flood,如何防止这类攻击?", - "content": "# 简述 TCP 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手?\n\n\n# 从输入 URL 到展现页面的全过程\n\n\n# HTTP 中 GET 和 POST 区别\n\n\n# TCP 怎么保证可靠传输\n\n\n# 什么是 TCP 粘包和拆包?\n\n\n# 简述 HTTPS 的加密与认证过程\n\n\n# 简述对称与非对称加密的概念\n\n\n# TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别?\n\n\n# 如何解决 TCP 传输丢包问题?\n\n\n# TCP 长连接和短连接有那么不同的使用场景?\n\n\n# TCP 的 keepalive 了解吗?说一说它和 HTTP 的 keepalive 的区别?\n\n\n# HTTP 与 HTTPS 有哪些区别?\n\n\n# TCP 中常见的拥塞控制算法有哪些?\n\n\n# 简述 HTTP 1.0,1.1,2.0 的主要区别\n\n\n# 从系统层面上,UDP如何保证尽量可靠?\n\n\n# TCP 挥手时出现大量 CLOSE_WAIT 或 TIME_WAIT 怎么解决?\n\n\n# 简述 TCP 滑动窗口以及重传机制\n\n\n# 简述 JWT 的原理和校验机制\n\n\n# 为什么需要序列化?有什么序列化的方式?\n\n\n# 简述 iPv4 和 iPv6 的区别\n\n\n# TCP 中 SYN 攻击是什么?如何防止?\n\n\n# 简述 DDOS 攻击原理,如何防范它?\n\n\n# 什么是 ARP 协议?简述其使用场景\n\n\n# 什么是跨域,什么情况下会发生跨域请求?\n\n\n# Cookie 和 Session 的关系和区别是什么?\n\n\n# 简述 WebSocket 是如何进行传输的\n\n\n# DNS 查询服务器的基本流程是什么?DNS 劫持是什么?\n\n\n# RestFul 是什么?RestFul 请求的 URL 有什么特点?\n\n\n# 简述 HTTP 报文头部的组成结构\n\n\n# 什么是 SYN flood,如何防止这类攻击?", - "normalizedContent": "# 简述 tcp 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手?\n\n\n# 从输入 url 到展现页面的全过程\n\n\n# http 中 get 和 post 区别\n\n\n# tcp 怎么保证可靠传输\n\n\n# 什么是 tcp 粘包和拆包?\n\n\n# 简述 https 的加密与认证过程\n\n\n# 简述对称与非对称加密的概念\n\n\n# tcp 与 udp 在网络协议中的哪一层,他们之间有什么区别?\n\n\n# 如何解决 tcp 传输丢包问题?\n\n\n# tcp 长连接和短连接有那么不同的使用场景?\n\n\n# tcp 的 keepalive 了解吗?说一说它和 http 的 keepalive 的区别?\n\n\n# http 与 https 有哪些区别?\n\n\n# tcp 中常见的拥塞控制算法有哪些?\n\n\n# 简述 http 1.0,1.1,2.0 的主要区别\n\n\n# 从系统层面上,udp如何保证尽量可靠?\n\n\n# tcp 挥手时出现大量 close_wait 或 time_wait 怎么解决?\n\n\n# 简述 tcp 滑动窗口以及重传机制\n\n\n# 简述 jwt 的原理和校验机制\n\n\n# 为什么需要序列化?有什么序列化的方式?\n\n\n# 简述 ipv4 和 ipv6 的区别\n\n\n# tcp 中 syn 攻击是什么?如何防止?\n\n\n# 简述 ddos 攻击原理,如何防范它?\n\n\n# 什么是 arp 协议?简述其使用场景\n\n\n# 什么是跨域,什么情况下会发生跨域请求?\n\n\n# cookie 和 session 的关系和区别是什么?\n\n\n# 简述 websocket 是如何进行传输的\n\n\n# dns 查询服务器的基本流程是什么?dns 劫持是什么?\n\n\n# restful 是什么?restful 请求的 url 有什么特点?\n\n\n# 简述 http 报文头部的组成结构\n\n\n# 什么是 syn flood,如何防止这类攻击?", + "headersStr": "进程间有哪些通信方式? 简述自旋锁与互斥锁的使用场景 进程和线程之间有什么区别? 简述几个常用的 Linux 命令以及他们的功能 创建线程有多少种方式? 线程有多少种状态,状态之间如何转换 进程有多少种状态? 简述 select, poll, epoll 的使用场景以及区别,epoll 中水平触发以及边缘触发有什么不同? Linux 下如何排查 CPU 以及 内存占用过多? 进程通信中的管道实现原理是什么? Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程? 如何调试服务器内存占用过高的问题? Linux 如何查看实时的滚动日志? 简述 Linux 零拷贝的原理 为什么进程切换慢,线程切换快? 简述创建进程的流程 简述 Linux 虚拟内存的页面置换算法 简述 Linux 进程调度的算法 什么时候会由用户态陷入内核态? Linux 下如何查看端口被哪个进程占用? Linux 中虚拟内存和物理内存有什么区别?有什么优点? 简述 traceroute 命令的原理", + "content": "# 进程间有哪些通信方式?\n\n\n# 简述自旋锁与互斥锁的使用场景\n\n\n# 进程和线程之间有什么区别?\n\n\n# 简述几个常用的 Linux 命令以及他们的功能\n\n\n# 创建线程有多少种方式?\n\n\n# 线程有多少种状态,状态之间如何转换\n\n\n# 进程有多少种状态?\n\n\n# 简述 select, poll, epoll 的使用场景以及区别,epoll 中水平触发以及边缘触发有什么不同?\n\n\n# Linux 下如何排查 CPU 以及 内存占用过多?\n\n\n# 进程通信中的管道实现原理是什么?\n\n\n# Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程?\n\n\n# 如何调试服务器内存占用过高的问题?\n\n\n# Linux 如何查看实时的滚动日志?\n\n\n# 简述 Linux 零拷贝的原理\n\n\n# 为什么进程切换慢,线程切换快?\n\n\n# 简述创建进程的流程\n\n\n# 简述 Linux 虚拟内存的页面置换算法\n\n\n# 简述 Linux 进程调度的算法\n\n\n# 什么时候会由用户态陷入内核态?\n\n\n# Linux 下如何查看端口被哪个进程占用?\n\n\n# Linux 中虚拟内存和物理内存有什么区别?有什么优点?\n\n\n# 简述 traceroute 命令的原理", + "normalizedContent": "# 进程间有哪些通信方式?\n\n\n# 简述自旋锁与互斥锁的使用场景\n\n\n# 进程和线程之间有什么区别?\n\n\n# 简述几个常用的 linux 命令以及他们的功能\n\n\n# 创建线程有多少种方式?\n\n\n# 线程有多少种状态,状态之间如何转换\n\n\n# 进程有多少种状态?\n\n\n# 简述 select, poll, epoll 的使用场景以及区别,epoll 中水平触发以及边缘触发有什么不同?\n\n\n# linux 下如何排查 cpu 以及 内存占用过多?\n\n\n# 进程通信中的管道实现原理是什么?\n\n\n# linux 下如何查看 cpu 荷载,正在运行的进程,某个端口对应的进程?\n\n\n# 如何调试服务器内存占用过高的问题?\n\n\n# linux 如何查看实时的滚动日志?\n\n\n# 简述 linux 零拷贝的原理\n\n\n# 为什么进程切换慢,线程切换快?\n\n\n# 简述创建进程的流程\n\n\n# 简述 linux 虚拟内存的页面置换算法\n\n\n# 简述 linux 进程调度的算法\n\n\n# 什么时候会由用户态陷入内核态?\n\n\n# linux 下如何查看端口被哪个进程占用?\n\n\n# linux 中虚拟内存和物理内存有什么区别?有什么优点?\n\n\n# 简述 traceroute 命令的原理", "charsets": { "cjk": true }, "lastUpdated": "2022/06/20, 22:47:41", "lastUpdatedTimestamp": 1655736461000 }, - { - "title": "建造者模式", - "frontmatter": { - "title": "建造者模式", - "date": "2022-05-21T17:30:20.000Z", - "permalink": "/pages/c332ae/", - "categories": [ - "Java相关", - "设计模式" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/080.%E5%BB%BA%E9%80%A0%E8%80%85%E6%A8%A1%E5%BC%8F.html", - "relativePath": "01.Java相关/40.设计模式/080.建造者模式.md", - "key": "v-dd5f4440", - "path": "/pages/c332ae/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, { "title": "简述常见的 HTTP 状态码的含义", "frontmatter": { @@ -3294,97 +3693,87 @@ export const siteData = { "normalizedContent": "# 2xx 成功\n\n200 ok,表示从客户端发来的请求在服务器端被正确处理 204 no content,表示请求成功,但响应报文不含实体的主体部分 206 partial content,进行范围请求\n\n\n# 3xx 重定向\n\n301 moved permanently,永久性重定向,表示资源已被分配了新的 url 302 found,临时性重定向,表示资源临时被分配了新的 url 303 see other,表示资源存在着另一个 url,应使用 get 方法定向获取资源 304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况 307 temporary redirect,临时重定向,和302含义相同\n\n\n# 4xx 客户端错误\n\n400 bad request,请求报文存在语法错误 401 unauthorized,表示发送的请求需要有通过 http 认证的认证信息 403 forbidden,表示对请求资源的访问被服务器拒绝 404 not found,表示在服务器上没有找到请求的资源\n\n\n# 5xx 服务器错误\n\n500 internal sever error,表示服务器端在执行请求时发生了错误 503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求", "charsets": { "cjk": true - } + }, + "lastUpdated": "2022/09/18, 21:28:02", + "lastUpdatedTimestamp": 1663507682000 }, { - "title": "从输入 URL 到展现页面的全过程", + "title": "算法", "frontmatter": { - "title": "从输入 URL 到展现页面的全过程", - "date": "2022-06-23T21:10:44.000Z", - "permalink": "/pages/ab48ba/", + "title": "算法", + "date": "2022-03-11T00:38:43.000Z", + "permalink": "/pages/92288a/", "categories": [ "计算机基础", - "计算机网络" + "CSS" ], "tags": [ null ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/020.%E4%BB%8E%E8%BE%93%E5%85%A5%20URL%20%E5%88%B0%E5%B1%95%E7%8E%B0%E9%A1%B5%E9%9D%A2%E7%9A%84%E5%85%A8%E8%BF%87%E7%A8%8B.html", - "relativePath": "02.计算机基础/10.计算机网络/020.从输入 URL 到展现页面的全过程.md", - "key": "v-6602e3da", - "path": "/pages/ab48ba/", + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/40.%E7%AE%97%E6%B3%95/01.%E7%AE%97%E6%B3%95.html", + "relativePath": "02.计算机基础/40.算法/01.算法.md", + "key": "v-2e6d42b4", + "path": "/pages/92288a/", "headers": [ { "level": 2, - "title": "什么是IP地址", - "slug": "什么是ip地址", - "normalizedTitle": "什么是ip地址", - "charIndex": 459 - }, - { - "level": 2, - "title": "什么是DNS", - "slug": "什么是dns", - "normalizedTitle": "什么是dns", - "charIndex": 608 - }, - { - "level": 2, - "title": "DNS解析", - "slug": "dns解析-2", - "normalizedTitle": "dns解析", - "charIndex": 57 + "title": "数据结构与算法知识", + "slug": "数据结构与算法知识", + "normalizedTitle": "数据结构与算法知识", + "charIndex": 2 }, { "level": 2, - "title": "DNS查询的两种方式:递归查询和迭代查询", - "slug": "dns查询的两种方式-递归查询和迭代查询", - "normalizedTitle": "dns查询的两种方式:递归查询和迭代查询", - "charIndex": 752 + "title": "全排列", + "slug": "全排列", + "normalizedTitle": "全排列", + "charIndex": 16 }, { "level": 2, - "title": "DNS域名称空间的组织方式", - "slug": "dns域名称空间的组织方式", - "normalizedTitle": "dns域名称空间的组织方式", - "charIndex": 788 + "title": "贪心算法", + "slug": "贪心算法", + "normalizedTitle": "贪心算法", + "charIndex": 24 }, { "level": 2, - "title": "DNS负载均衡", - "slug": "dns负载均衡", - "normalizedTitle": "dns负载均衡", - "charIndex": 808 + "title": "KMP算法", + "slug": "kmp算法", + "normalizedTitle": "kmp算法", + "charIndex": 33 }, { "level": 2, - "title": "”三次握手”的详解", - "slug": "三次握手-的详解", - "normalizedTitle": "”三次握手”的详解", - "charIndex": 1033 + "title": "hash算法", + "slug": "hash算法", + "normalizedTitle": "hash算法", + "charIndex": 43 }, { "level": 2, - "title": "为什么要三次握手", - "slug": "为什么要三次握手", - "normalizedTitle": "为什么要三次握手", - "charIndex": 1049 + "title": "海量数据处理", + "slug": "海量数据处理", + "normalizedTitle": "海量数据处理", + "charIndex": 54 } ], - "headersStr": "什么是IP地址 什么是DNS DNS解析 DNS查询的两种方式:递归查询和迭代查询 DNS域名称空间的组织方式 DNS负载均衡 ”三次握手”的详解 为什么要三次握手", - "content": "从我们输入一个网址到页面展现的过程到底发生了什么呢?\n\n\n# 总体来说分为以下几个过程:\n\n * 输入网址\n * DNS解析\n * 建立TCP/IP链接\n * 发送HTTP请求\n * 服务器处理请求\n * 服务器返回HTTP响应\n * 浏览器渲染页面并展现\n * 断开连接\n\n\n# 输入网址\n\n当我们想要打开一个页面时,很简单的会想到去输入一个网址 我们以本网址为例,解释一下它各部分的组成:\n\nhttps://www.baidu.com\n\n\n1\n\n\n * https :https是协议(Protocol),它表明了浏览器使用何种协议,有http、https、ftp、mailto等,其中https是加密认证版的http(详见《图解HTTP》)\n\n * www.baidu.com :这是域名(Domain name),它表明正在请求哪个Web服务器,其中www是主机名(host)\n\n\n# DNS解析\n\n输入网址(URL)这一步我们很好理解,但是接下来所谓的域名解析是什么呢?又是为什么要进行域名解析呢?\n\n\n# 什么是IP地址\n\nIP 地址(Internet Protocol Address)是互联网协议特有的一种地址,它是 IP 协议提供的一种统一的地址格式。IP 地址为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。 像192.168.59.0就是一个主机IP地址\n\n\n# 什么是DNS\n\nDNS服务器上存储着映射<域名-IP>,DNS解析就是去DNS服务器查询IP地址。\n\n\n# DNS解析\n\nDNS解析按照下面顺序逐个查找。\n\n * 本地(hosts文件)\n * DNS服务器(本地DNS | 根DNS)\n * 域服务器\n * 解析服务器的地址\n\n\n\n\n# DNS查询的两种方式:递归查询和迭代查询\n\n递归解析 迭代解析\n\n\n# DNS域名称空间的组织方式\n\n\n\n\n# DNS负载均衡\n\n当一个网站有足够多的用户的时候,假如每次请求的资源都位于同一台机器上面,那么这台机器随时可能会蹦掉。\n\n处理办法就是用DNS负载均衡技术,它的原理是在DNS服务器中为同一个主机名配置多个IP地址 在应答DNS查询时,DNS服务器对每个查询将以DNS文件中主机记录的IP地址按顺序返回不同的解析结果,将客户端的访问引导到不同的机器上去,使得不同的客户端访问不同的服务器,从而达到负载均衡的目的。\n\n\n# 建立TCP/IP链接\n\n\n# ”三次握手”的详解\n\n\n\n\n# 为什么要三次握手\n\n * clien端 防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。 client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致连接释放以后的某个时间才到达server。 本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。 于是就向client发出确认报文段,同意建立连接。\n\n * server端 假设不采用“三次握手”。 如果只要server发出确认,新的连接就建立了。并一直等待client发来数据。 事实是client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。 但server端就会有许多这样的等待,server的很多资源就白白浪费掉了。\n\n采用“三次握手”的办法可以防止上述现象发生。主要目的防止server端一直等待,浪费资源。", - "normalizedContent": "从我们输入一个网址到页面展现的过程到底发生了什么呢?\n\n\n# 总体来说分为以下几个过程:\n\n * 输入网址\n * dns解析\n * 建立tcp/ip链接\n * 发送http请求\n * 服务器处理请求\n * 服务器返回http响应\n * 浏览器渲染页面并展现\n * 断开连接\n\n\n# 输入网址\n\n当我们想要打开一个页面时,很简单的会想到去输入一个网址 我们以本网址为例,解释一下它各部分的组成:\n\nhttps://www.baidu.com\n\n\n1\n\n\n * https :https是协议(protocol),它表明了浏览器使用何种协议,有http、https、ftp、mailto等,其中https是加密认证版的http(详见《图解http》)\n\n * www.baidu.com :这是域名(domain name),它表明正在请求哪个web服务器,其中www是主机名(host)\n\n\n# dns解析\n\n输入网址(url)这一步我们很好理解,但是接下来所谓的域名解析是什么呢?又是为什么要进行域名解析呢?\n\n\n# 什么是ip地址\n\nip 地址(internet protocol address)是互联网协议特有的一种地址,它是 ip 协议提供的一种统一的地址格式。ip 地址为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。 像192.168.59.0就是一个主机ip地址\n\n\n# 什么是dns\n\ndns服务器上存储着映射<域名-ip>,dns解析就是去dns服务器查询ip地址。\n\n\n# dns解析\n\ndns解析按照下面顺序逐个查找。\n\n * 本地(hosts文件)\n * dns服务器(本地dns | 根dns)\n * 域服务器\n * 解析服务器的地址\n\n\n\n\n# dns查询的两种方式:递归查询和迭代查询\n\n递归解析 迭代解析\n\n\n# dns域名称空间的组织方式\n\n\n\n\n# dns负载均衡\n\n当一个网站有足够多的用户的时候,假如每次请求的资源都位于同一台机器上面,那么这台机器随时可能会蹦掉。\n\n处理办法就是用dns负载均衡技术,它的原理是在dns服务器中为同一个主机名配置多个ip地址 在应答dns查询时,dns服务器对每个查询将以dns文件中主机记录的ip地址按顺序返回不同的解析结果,将客户端的访问引导到不同的机器上去,使得不同的客户端访问不同的服务器,从而达到负载均衡的目的。\n\n\n# 建立tcp/ip链接\n\n\n# ”三次握手”的详解\n\n\n\n\n# 为什么要三次握手\n\n * clien端 防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。 client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致连接释放以后的某个时间才到达server。 本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。 于是就向client发出确认报文段,同意建立连接。\n\n * server端 假设不采用“三次握手”。 如果只要server发出确认,新的连接就建立了。并一直等待client发来数据。 事实是client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。 但server端就会有许多这样的等待,server的很多资源就白白浪费掉了。\n\n采用“三次握手”的办法可以防止上述现象发生。主要目的防止server端一直等待,浪费资源。", + "headersStr": "数据结构与算法知识 全排列 贪心算法 KMP算法 hash算法 海量数据处理", + "content": "# 数据结构与算法知识\n\n\n# 全排列\n\n\n# 贪心算法\n\n\n# KMP算法\n\n\n# hash算法\n\n\n# 海量数据处理\n\n* 分治\n* hash映射\n* 堆排序\n* 双层桶划分\n* Bloom Filter\n* bitmap\n* 数据库索引\n* mapreduce等。", + "normalizedContent": "# 数据结构与算法知识\n\n\n# 全排列\n\n\n# 贪心算法\n\n\n# kmp算法\n\n\n# hash算法\n\n\n# 海量数据处理\n\n* 分治\n* hash映射\n* 堆排序\n* 双层桶划分\n* bloom filter\n* bitmap\n* 数据库索引\n* mapreduce等。", "charsets": { "cjk": true - } + }, + "lastUpdated": "2022/06/20, 22:47:41", + "lastUpdatedTimestamp": 1655736461000 }, { - "title": "操作系统", + "title": "数据结构", "frontmatter": { - "title": "操作系统", - "date": "2022-03-11T00:51:48.000Z", - "permalink": "/pages/e5f2d7/", + "title": "数据结构", + "date": "2022-03-11T00:38:43.000Z", + "permalink": "/pages/a25c3a/", "categories": [ "计算机基础", "CSS" @@ -3393,330 +3782,383 @@ export const siteData = { null ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/01.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F.html", - "relativePath": "02.计算机基础/20.操作系统/01.操作系统.md", - "key": "v-6f178cbc", - "path": "/pages/e5f2d7/", + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/30.%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/01.%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.html", + "relativePath": "02.计算机基础/30.数据结构/01.数据结构.md", + "key": "v-49688930", + "path": "/pages/a25c3a/", "headers": [ { "level": 2, - "title": "进程间有哪些通信方式?", - "slug": "进程间有哪些通信方式", - "normalizedTitle": "进程间有哪些通信方式?", + "title": "简单的数据结构", + "slug": "简单的数据结构", + "normalizedTitle": "简单的数据结构", "charIndex": 2 }, { "level": 2, - "title": "简述自旋锁与互斥锁的使用场景", - "slug": "简述自旋锁与互斥锁的使用场景", - "normalizedTitle": "简述自旋锁与互斥锁的使用场景", - "charIndex": 18 + "title": "树", + "slug": "树", + "normalizedTitle": "树", + "charIndex": 96 }, { "level": 2, - "title": "进程和线程之间有什么区别?", - "slug": "进程和线程之间有什么区别", - "normalizedTitle": "进程和线程之间有什么区别?", - "charIndex": 37 - }, - { + "title": "堆", + "slug": "堆", + "normalizedTitle": "堆", + "charIndex": 164 + }, + { "level": 2, - "title": "简述几个常用的 Linux 命令以及他们的功能", - "slug": "简述几个常用的-linux-命令以及他们的功能", - "normalizedTitle": "简述几个常用的 linux 命令以及他们的功能", - "charIndex": 55 + "title": "图", + "slug": "图", + "normalizedTitle": "图", + "charIndex": 185 }, { "level": 2, - "title": "创建线程有多少种方式?", - "slug": "创建线程有多少种方式", - "normalizedTitle": "创建线程有多少种方式?", - "charIndex": 83 + "title": "稳定的排序算法", + "slug": "稳定的排序算法", + "normalizedTitle": "稳定的排序算法", + "charIndex": 212 }, { "level": 2, - "title": "线程有多少种状态,状态之间如何转换", - "slug": "线程有多少种状态-状态之间如何转换", - "normalizedTitle": "线程有多少种状态,状态之间如何转换", - "charIndex": 99 + "title": "不稳定的排序算法", + "slug": "不稳定的排序算法", + "normalizedTitle": "不稳定的排序算法", + "charIndex": 322 }, { "level": 2, - "title": "进程有多少种状态?", - "slug": "进程有多少种状态", - "normalizedTitle": "进程有多少种状态?", - "charIndex": 121 + "title": "各种排序算法和时间复杂度", + "slug": "各种排序算法和时间复杂度", + "normalizedTitle": "各种排序算法和时间复杂度", + "charIndex": 404 }, { "level": 2, - "title": "简述 select, poll, epoll 的使用场景以及区别,epoll 中水平触发以及边缘触发有什么不同?", - "slug": "简述-select-poll-epoll-的使用场景以及区别-epoll-中水平触发以及边缘触发有什么不同", - "normalizedTitle": "简述 select, poll, epoll 的使用场景以及区别,epoll 中水平触发以及边缘触发有什么不同?", - "charIndex": 135 + "title": "深度优先和广度优先搜索", + "slug": "深度优先和广度优先搜索", + "normalizedTitle": "深度优先和广度优先搜索", + "charIndex": 421 + } + ], + "headersStr": "简单的数据结构 树 堆 图 稳定的排序算法 不稳定的排序算法 各种排序算法和时间复杂度 深度优先和广度优先搜索", + "content": "# 简单的数据结构\n\n* 栈\n* 队列\n\n* 链表\n\n* 数组\n\n* 哈希表\n\n* 栈和队列的相同和不同之处\n\n* 栈通常采用的两种存储结构\n\n* 两个栈实现队列,和两个队列实现栈\n\n\n\n# 树\n\n* 二叉树\n\n* 字典树\n\n* 平衡树\n\n* 排序树\n\n* B树\n\n* B+树\n\n* R树\n\n* 多路树\n\n* 红黑树\n\n\n\n# 堆\n\n* 大根堆\n\n* 小根堆\n\n\n\n# 图\n\n* 有向图\n\n* 无向图\n\n* 拓扑\n\n\n\n# 稳定的排序算法\n\n* 冒泡排序\n* 插入排序\n* 鸡尾酒排序\n* 桶排序\n* 计数排序\n* 归并排序\n* 原地归并排序\n* 二叉排序树排序\n* 鸽巢排序\n* 基数排序\n* 侏儒排序\n* 图书馆排序\n* 块排序\n\n\n\n# 不稳定的排序算法\n\n* 选择排序\n* 希尔排序\n* Clover排序算法\n* 梳排序\n* 堆排序\n* 平滑排序\n* 快速排序\n* 内省排序\n* 耐心排序\n\n\n\n# 各种排序算法和时间复杂度\n\n\n# 深度优先和广度优先搜索", + "normalizedContent": "# 简单的数据结构\n\n* 栈\n* 队列\n\n* 链表\n\n* 数组\n\n* 哈希表\n\n* 栈和队列的相同和不同之处\n\n* 栈通常采用的两种存储结构\n\n* 两个栈实现队列,和两个队列实现栈\n\n\n\n# 树\n\n* 二叉树\n\n* 字典树\n\n* 平衡树\n\n* 排序树\n\n* b树\n\n* b+树\n\n* r树\n\n* 多路树\n\n* 红黑树\n\n\n\n# 堆\n\n* 大根堆\n\n* 小根堆\n\n\n\n# 图\n\n* 有向图\n\n* 无向图\n\n* 拓扑\n\n\n\n# 稳定的排序算法\n\n* 冒泡排序\n* 插入排序\n* 鸡尾酒排序\n* 桶排序\n* 计数排序\n* 归并排序\n* 原地归并排序\n* 二叉排序树排序\n* 鸽巢排序\n* 基数排序\n* 侏儒排序\n* 图书馆排序\n* 块排序\n\n\n\n# 不稳定的排序算法\n\n* 选择排序\n* 希尔排序\n* clover排序算法\n* 梳排序\n* 堆排序\n* 平滑排序\n* 快速排序\n* 内省排序\n* 耐心排序\n\n\n\n# 各种排序算法和时间复杂度\n\n\n# 深度优先和广度优先搜索", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/06/20, 22:47:41", + "lastUpdatedTimestamp": 1655736461000 + }, + { + "title": "MySQL中 InnoDB 和 MylSAM 的区别是什么?", + "frontmatter": { + "title": "MySQL中 InnoDB 和 MylSAM 的区别是什么?", + "date": "2022-05-21T17:07:47.000Z", + "permalink": "/pages/3a8659/", + "categories": [ + "数据库", + "MYSQL相关" + ], + "tags": [ + null + ] + }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/001.MySQL%E4%B8%AD%20InnoDB%20%E5%92%8C%20MylSAM%20%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/001.MySQL中 InnoDB 和 MylSAM 的区别是什么?.md", + "key": "v-674454ac", + "path": "/pages/3a8659/", + "headers": [ + { + "level": 2, + "title": "区别", + "slug": "区别", + "normalizedTitle": "区别", + "charIndex": 37 }, { "level": 2, - "title": "Linux 下如何排查 CPU 以及 内存占用过多?", - "slug": "linux-下如何排查-cpu-以及-内存占用过多", - "normalizedTitle": "linux 下如何排查 cpu 以及 内存占用过多?", - "charIndex": 196 + "title": "场景选择", + "slug": "场景选择", + "normalizedTitle": "场景选择", + "charIndex": 253 + } + ], + "headersStr": "区别 场景选择", + "content": "MySQL5.5版本开始Innodb已经成为Mysql的默认引擎\n\n\n# 区别\n\n * InnoDB 支持事务,MyISAM 不支持事务。 这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一\n * MyISAM锁的粒度是表级的,而InnoDB支持行级锁\n * MyISAM支持全文类型索引,而InnoDB不支持全文索引\n * MyISAM相对简单,效率上要优于InnoDB,小型应用可以考虑使用MyISAM\n * MyISAM表保存成文件形式,跨平台使用更加方便\n\n\n# 场景选择\n\nMyISAM管理非事务表,提供高速存储和检索以及全文搜索能力,如果再应用中执行大量select操作,应该选择MyISAM InnoDB用于事务处理,具有ACID事务支持等特性,如果在应用中执行大量insert和update操作,应该选择InnoDB", + "normalizedContent": "mysql5.5版本开始innodb已经成为mysql的默认引擎\n\n\n# 区别\n\n * innodb 支持事务,myisam 不支持事务。 这是 mysql 将默认存储引擎从 myisam 变成 innodb 的重要原因之一\n * myisam锁的粒度是表级的,而innodb支持行级锁\n * myisam支持全文类型索引,而innodb不支持全文索引\n * myisam相对简单,效率上要优于innodb,小型应用可以考虑使用myisam\n * myisam表保存成文件形式,跨平台使用更加方便\n\n\n# 场景选择\n\nmyisam管理非事务表,提供高速存储和检索以及全文搜索能力,如果再应用中执行大量select操作,应该选择myisam innodb用于事务处理,具有acid事务支持等特性,如果在应用中执行大量insert和update操作,应该选择innodb", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "简述 MySQL 常见索引类型,介绍一下覆盖索引", + "frontmatter": { + "title": "简述 MySQL 常见索引类型,介绍一下覆盖索引", + "date": "2022-05-21T17:08:24.000Z", + "permalink": "/pages/17b014/", + "categories": [ + "数据库", + "MYSQL相关" + ], + "tags": [ + null + ] + }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/007.%E7%AE%80%E8%BF%B0%20MySQL%20%E5%B8%B8%E8%A7%81%E7%B4%A2%E5%BC%95%E7%B1%BB%E5%9E%8B%EF%BC%8C%E4%BB%8B%E7%BB%8D%E4%B8%80%E4%B8%8B%E8%A6%86%E7%9B%96%E7%B4%A2%E5%BC%95.html", + "relativePath": "03.数据库/01.MYSQL相关/007.简述 MySQL 常见索引类型,介绍一下覆盖索引.md", + "key": "v-824b776c", + "path": "/pages/17b014/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "计算机网络", + "frontmatter": { + "title": "计算机网络", + "date": "2022-03-11T00:51:01.000Z", + "permalink": "/pages/e00207/", + "categories": [ + "计算机基础", + "HTML" + ], + "tags": [ + null + ] + }, + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/01.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C.html", + "relativePath": "02.计算机基础/10.计算机网络/01.计算机网络.md", + "key": "v-1e653940", + "path": "/pages/e00207/", + "headers": [ + { + "level": 2, + "title": "简述 TCP 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手?", + "slug": "简述-tcp-三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手", + "normalizedTitle": "简述 tcp 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手?", + "charIndex": 2 }, { "level": 2, - "title": "进程通信中的管道实现原理是什么?", - "slug": "进程通信中的管道实现原理是什么", - "normalizedTitle": "进程通信中的管道实现原理是什么?", - "charIndex": 227 + "title": "从输入 URL 到展现页面的全过程", + "slug": "从输入-url-到展现页面的全过程", + "normalizedTitle": "从输入 url 到展现页面的全过程", + "charIndex": 44 }, { "level": 2, - "title": "Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程?", - "slug": "linux-下如何查看-cpu-荷载-正在运行的进程-某个端口对应的进程", - "normalizedTitle": "linux 下如何查看 cpu 荷载,正在运行的进程,某个端口对应的进程?", - "charIndex": 248 + "title": "HTTP 中 GET 和 POST 区别", + "slug": "http-中-get-和-post-区别", + "normalizedTitle": "http 中 get 和 post 区别", + "charIndex": 66 }, { "level": 2, - "title": "如何调试服务器内存占用过高的问题?", - "slug": "如何调试服务器内存占用过高的问题", - "normalizedTitle": "如何调试服务器内存占用过高的问题?", - "charIndex": 290 + "title": "TCP 怎么保证可靠传输", + "slug": "tcp-怎么保证可靠传输", + "normalizedTitle": "tcp 怎么保证可靠传输", + "charIndex": 91 }, { "level": 2, - "title": "Linux 如何查看实时的滚动日志?", - "slug": "linux-如何查看实时的滚动日志", - "normalizedTitle": "linux 如何查看实时的滚动日志?", - "charIndex": 312 + "title": "什么是 TCP 粘包和拆包?", + "slug": "什么是-tcp-粘包和拆包", + "normalizedTitle": "什么是 tcp 粘包和拆包?", + "charIndex": 108 }, { "level": 2, - "title": "简述 Linux 零拷贝的原理", - "slug": "简述-linux-零拷贝的原理", - "normalizedTitle": "简述 linux 零拷贝的原理", - "charIndex": 335 + "title": "简述 HTTPS 的加密与认证过程", + "slug": "简述-https-的加密与认证过程", + "normalizedTitle": "简述 https 的加密与认证过程", + "charIndex": 127 }, { "level": 2, - "title": "为什么进程切换慢,线程切换快?", - "slug": "为什么进程切换慢-线程切换快", - "normalizedTitle": "为什么进程切换慢,线程切换快?", - "charIndex": 355 + "title": "简述对称与非对称加密的概念", + "slug": "简述对称与非对称加密的概念", + "normalizedTitle": "简述对称与非对称加密的概念", + "charIndex": 149 }, { "level": 2, - "title": "简述创建进程的流程", - "slug": "简述创建进程的流程", - "normalizedTitle": "简述创建进程的流程", - "charIndex": 375 + "title": "TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别?", + "slug": "tcp-与-udp-在网络协议中的哪一层-他们之间有什么区别", + "normalizedTitle": "tcp 与 udp 在网络协议中的哪一层,他们之间有什么区别?", + "charIndex": 167 }, { "level": 2, - "title": "简述 Linux 虚拟内存的页面置换算法", - "slug": "简述-linux-虚拟内存的页面置换算法", - "normalizedTitle": "简述 linux 虚拟内存的页面置换算法", - "charIndex": 389 + "title": "如何解决 TCP 传输丢包问题?", + "slug": "如何解决-tcp-传输丢包问题", + "normalizedTitle": "如何解决 tcp 传输丢包问题?", + "charIndex": 203 }, { "level": 2, - "title": "简述 Linux 进程调度的算法", - "slug": "简述-linux-进程调度的算法", - "normalizedTitle": "简述 linux 进程调度的算法", - "charIndex": 414 + "title": "TCP 长连接和短连接有那么不同的使用场景?", + "slug": "tcp-长连接和短连接有那么不同的使用场景", + "normalizedTitle": "tcp 长连接和短连接有那么不同的使用场景?", + "charIndex": 224 }, { "level": 2, - "title": "什么时候会由用户态陷入内核态?", - "slug": "什么时候会由用户态陷入内核态", - "normalizedTitle": "什么时候会由用户态陷入内核态?", - "charIndex": 435 + "title": "TCP 的 keepalive 了解吗?说一说它和 HTTP 的 keepalive 的区别?", + "slug": "tcp-的-keepalive-了解吗-说一说它和-http-的-keepalive-的区别", + "normalizedTitle": "tcp 的 keepalive 了解吗?说一说它和 http 的 keepalive 的区别?", + "charIndex": 251 }, { "level": 2, - "title": "Linux 下如何查看端口被哪个进程占用?", - "slug": "linux-下如何查看端口被哪个进程占用", - "normalizedTitle": "linux 下如何查看端口被哪个进程占用?", - "charIndex": 455 + "title": "HTTP 与 HTTPS 有哪些区别?", + "slug": "http-与-https-有哪些区别", + "normalizedTitle": "http 与 https 有哪些区别?", + "charIndex": 303 }, { "level": 2, - "title": "Linux 中虚拟内存和物理内存有什么区别?有什么优点?", - "slug": "linux-中虚拟内存和物理内存有什么区别-有什么优点", - "normalizedTitle": "linux 中虚拟内存和物理内存有什么区别?有什么优点?", - "charIndex": 481 + "title": "TCP 中常见的拥塞控制算法有哪些?", + "slug": "tcp-中常见的拥塞控制算法有哪些", + "normalizedTitle": "tcp 中常见的拥塞控制算法有哪些?", + "charIndex": 327 }, { "level": 2, - "title": "简述 traceroute 命令的原理", - "slug": "简述-traceroute-命令的原理", - "normalizedTitle": "简述 traceroute 命令的原理", - "charIndex": 514 - } - ], - "headersStr": "进程间有哪些通信方式? 简述自旋锁与互斥锁的使用场景 进程和线程之间有什么区别? 简述几个常用的 Linux 命令以及他们的功能 创建线程有多少种方式? 线程有多少种状态,状态之间如何转换 进程有多少种状态? 简述 select, poll, epoll 的使用场景以及区别,epoll 中水平触发以及边缘触发有什么不同? Linux 下如何排查 CPU 以及 内存占用过多? 进程通信中的管道实现原理是什么? Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程? 如何调试服务器内存占用过高的问题? Linux 如何查看实时的滚动日志? 简述 Linux 零拷贝的原理 为什么进程切换慢,线程切换快? 简述创建进程的流程 简述 Linux 虚拟内存的页面置换算法 简述 Linux 进程调度的算法 什么时候会由用户态陷入内核态? Linux 下如何查看端口被哪个进程占用? Linux 中虚拟内存和物理内存有什么区别?有什么优点? 简述 traceroute 命令的原理", - "content": "# 进程间有哪些通信方式?\n\n\n# 简述自旋锁与互斥锁的使用场景\n\n\n# 进程和线程之间有什么区别?\n\n\n# 简述几个常用的 Linux 命令以及他们的功能\n\n\n# 创建线程有多少种方式?\n\n\n# 线程有多少种状态,状态之间如何转换\n\n\n# 进程有多少种状态?\n\n\n# 简述 select, poll, epoll 的使用场景以及区别,epoll 中水平触发以及边缘触发有什么不同?\n\n\n# Linux 下如何排查 CPU 以及 内存占用过多?\n\n\n# 进程通信中的管道实现原理是什么?\n\n\n# Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程?\n\n\n# 如何调试服务器内存占用过高的问题?\n\n\n# Linux 如何查看实时的滚动日志?\n\n\n# 简述 Linux 零拷贝的原理\n\n\n# 为什么进程切换慢,线程切换快?\n\n\n# 简述创建进程的流程\n\n\n# 简述 Linux 虚拟内存的页面置换算法\n\n\n# 简述 Linux 进程调度的算法\n\n\n# 什么时候会由用户态陷入内核态?\n\n\n# Linux 下如何查看端口被哪个进程占用?\n\n\n# Linux 中虚拟内存和物理内存有什么区别?有什么优点?\n\n\n# 简述 traceroute 命令的原理", - "normalizedContent": "# 进程间有哪些通信方式?\n\n\n# 简述自旋锁与互斥锁的使用场景\n\n\n# 进程和线程之间有什么区别?\n\n\n# 简述几个常用的 linux 命令以及他们的功能\n\n\n# 创建线程有多少种方式?\n\n\n# 线程有多少种状态,状态之间如何转换\n\n\n# 进程有多少种状态?\n\n\n# 简述 select, poll, epoll 的使用场景以及区别,epoll 中水平触发以及边缘触发有什么不同?\n\n\n# linux 下如何排查 cpu 以及 内存占用过多?\n\n\n# 进程通信中的管道实现原理是什么?\n\n\n# linux 下如何查看 cpu 荷载,正在运行的进程,某个端口对应的进程?\n\n\n# 如何调试服务器内存占用过高的问题?\n\n\n# linux 如何查看实时的滚动日志?\n\n\n# 简述 linux 零拷贝的原理\n\n\n# 为什么进程切换慢,线程切换快?\n\n\n# 简述创建进程的流程\n\n\n# 简述 linux 虚拟内存的页面置换算法\n\n\n# 简述 linux 进程调度的算法\n\n\n# 什么时候会由用户态陷入内核态?\n\n\n# linux 下如何查看端口被哪个进程占用?\n\n\n# linux 中虚拟内存和物理内存有什么区别?有什么优点?\n\n\n# 简述 traceroute 命令的原理", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/06/20, 22:47:41", - "lastUpdatedTimestamp": 1655736461000 - }, - { - "title": "数据结构", - "frontmatter": { - "title": "数据结构", - "date": "2022-03-11T00:38:43.000Z", - "permalink": "/pages/a25c3a/", - "categories": [ - "计算机基础", - "CSS" - ], - "tags": [ - null - ] - }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/30.%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/01.%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.html", - "relativePath": "02.计算机基础/30.数据结构/01.数据结构.md", - "key": "v-49688930", - "path": "/pages/a25c3a/", - "headers": [ + "title": "简述 HTTP 1.0,1.1,2.0 的主要区别", + "slug": "简述-http-1-0-1-1-2-0-的主要区别", + "normalizedTitle": "简述 http 1.0,1.1,2.0 的主要区别", + "charIndex": 350 + }, { "level": 2, - "title": "简单的数据结构", - "slug": "简单的数据结构", - "normalizedTitle": "简单的数据结构", - "charIndex": 2 + "title": "从系统层面上,UDP如何保证尽量可靠?", + "slug": "从系统层面上-udp如何保证尽量可靠", + "normalizedTitle": "从系统层面上,udp如何保证尽量可靠?", + "charIndex": 380 }, { "level": 2, - "title": "树", - "slug": "树", - "normalizedTitle": "树", - "charIndex": 96 + "title": "TCP 挥手时出现大量 CLOSEWAIT 或 TIMEWAIT 怎么解决?", + "slug": "tcp-挥手时出现大量-close-wait-或-time-wait-怎么解决", + "normalizedTitle": "tcp 挥手时出现大量 closewait 或 timewait 怎么解决?", + "charIndex": null }, { "level": 2, - "title": "堆", - "slug": "堆", - "normalizedTitle": "堆", - "charIndex": 164 + "title": "简述 TCP 滑动窗口以及重传机制", + "slug": "简述-tcp-滑动窗口以及重传机制", + "normalizedTitle": "简述 tcp 滑动窗口以及重传机制", + "charIndex": 449 }, { "level": 2, - "title": "图", - "slug": "图", - "normalizedTitle": "图", - "charIndex": 185 + "title": "简述 JWT 的原理和校验机制", + "slug": "简述-jwt-的原理和校验机制", + "normalizedTitle": "简述 jwt 的原理和校验机制", + "charIndex": 471 }, { "level": 2, - "title": "稳定的排序算法", - "slug": "稳定的排序算法", - "normalizedTitle": "稳定的排序算法", - "charIndex": 212 + "title": "为什么需要序列化?有什么序列化的方式?", + "slug": "为什么需要序列化-有什么序列化的方式", + "normalizedTitle": "为什么需要序列化?有什么序列化的方式?", + "charIndex": 491 }, { "level": 2, - "title": "不稳定的排序算法", - "slug": "不稳定的排序算法", - "normalizedTitle": "不稳定的排序算法", - "charIndex": 322 + "title": "简述 iPv4 和 iPv6 的区别", + "slug": "简述-ipv4-和-ipv6-的区别", + "normalizedTitle": "简述 ipv4 和 ipv6 的区别", + "charIndex": 515 }, { "level": 2, - "title": "各种排序算法和时间复杂度", - "slug": "各种排序算法和时间复杂度", - "normalizedTitle": "各种排序算法和时间复杂度", - "charIndex": 404 + "title": "TCP 中 SYN 攻击是什么?如何防止?", + "slug": "tcp-中-syn-攻击是什么-如何防止", + "normalizedTitle": "tcp 中 syn 攻击是什么?如何防止?", + "charIndex": 538 }, { "level": 2, - "title": "深度优先和广度优先搜索", - "slug": "深度优先和广度优先搜索", - "normalizedTitle": "深度优先和广度优先搜索", - "charIndex": 421 - } - ], - "headersStr": "简单的数据结构 树 堆 图 稳定的排序算法 不稳定的排序算法 各种排序算法和时间复杂度 深度优先和广度优先搜索", - "content": "# 简单的数据结构\n\n* 栈\n* 队列\n\n* 链表\n\n* 数组\n\n* 哈希表\n\n* 栈和队列的相同和不同之处\n\n* 栈通常采用的两种存储结构\n\n* 两个栈实现队列,和两个队列实现栈\n\n\n\n# 树\n\n* 二叉树\n\n* 字典树\n\n* 平衡树\n\n* 排序树\n\n* B树\n\n* B+树\n\n* R树\n\n* 多路树\n\n* 红黑树\n\n\n\n# 堆\n\n* 大根堆\n\n* 小根堆\n\n\n\n# 图\n\n* 有向图\n\n* 无向图\n\n* 拓扑\n\n\n\n# 稳定的排序算法\n\n* 冒泡排序\n* 插入排序\n* 鸡尾酒排序\n* 桶排序\n* 计数排序\n* 归并排序\n* 原地归并排序\n* 二叉排序树排序\n* 鸽巢排序\n* 基数排序\n* 侏儒排序\n* 图书馆排序\n* 块排序\n\n\n\n# 不稳定的排序算法\n\n* 选择排序\n* 希尔排序\n* Clover排序算法\n* 梳排序\n* 堆排序\n* 平滑排序\n* 快速排序\n* 内省排序\n* 耐心排序\n\n\n\n# 各种排序算法和时间复杂度\n\n\n# 深度优先和广度优先搜索", - "normalizedContent": "# 简单的数据结构\n\n* 栈\n* 队列\n\n* 链表\n\n* 数组\n\n* 哈希表\n\n* 栈和队列的相同和不同之处\n\n* 栈通常采用的两种存储结构\n\n* 两个栈实现队列,和两个队列实现栈\n\n\n\n# 树\n\n* 二叉树\n\n* 字典树\n\n* 平衡树\n\n* 排序树\n\n* b树\n\n* b+树\n\n* r树\n\n* 多路树\n\n* 红黑树\n\n\n\n# 堆\n\n* 大根堆\n\n* 小根堆\n\n\n\n# 图\n\n* 有向图\n\n* 无向图\n\n* 拓扑\n\n\n\n# 稳定的排序算法\n\n* 冒泡排序\n* 插入排序\n* 鸡尾酒排序\n* 桶排序\n* 计数排序\n* 归并排序\n* 原地归并排序\n* 二叉排序树排序\n* 鸽巢排序\n* 基数排序\n* 侏儒排序\n* 图书馆排序\n* 块排序\n\n\n\n# 不稳定的排序算法\n\n* 选择排序\n* 希尔排序\n* clover排序算法\n* 梳排序\n* 堆排序\n* 平滑排序\n* 快速排序\n* 内省排序\n* 耐心排序\n\n\n\n# 各种排序算法和时间复杂度\n\n\n# 深度优先和广度优先搜索", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/06/20, 22:47:41", - "lastUpdatedTimestamp": 1655736461000 - }, - { - "title": "算法", - "frontmatter": { - "title": "算法", - "date": "2022-03-11T00:38:43.000Z", - "permalink": "/pages/92288a/", - "categories": [ - "计算机基础", - "CSS" - ], - "tags": [ - null - ] - }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/40.%E7%AE%97%E6%B3%95/01.%E7%AE%97%E6%B3%95.html", - "relativePath": "02.计算机基础/40.算法/01.算法.md", - "key": "v-2e6d42b4", - "path": "/pages/92288a/", - "headers": [ + "title": "简述 DDOS 攻击原理,如何防范它?", + "slug": "简述-ddos-攻击原理-如何防范它", + "normalizedTitle": "简述 ddos 攻击原理,如何防范它?", + "charIndex": 564 + }, { "level": 2, - "title": "数据结构与算法知识", - "slug": "数据结构与算法知识", - "normalizedTitle": "数据结构与算法知识", - "charIndex": 2 + "title": "什么是 ARP 协议?简述其使用场景", + "slug": "什么是-arp-协议-简述其使用场景", + "normalizedTitle": "什么是 arp 协议?简述其使用场景", + "charIndex": 588 }, { "level": 2, - "title": "全排列", - "slug": "全排列", - "normalizedTitle": "全排列", - "charIndex": 16 + "title": "什么是跨域,什么情况下会发生跨域请求?", + "slug": "什么是跨域-什么情况下会发生跨域请求", + "normalizedTitle": "什么是跨域,什么情况下会发生跨域请求?", + "charIndex": 611 }, { "level": 2, - "title": "贪心算法", - "slug": "贪心算法", - "normalizedTitle": "贪心算法", - "charIndex": 24 + "title": "Cookie 和 Session 的关系和区别是什么?", + "slug": "cookie-和-session-的关系和区别是什么", + "normalizedTitle": "cookie 和 session 的关系和区别是什么?", + "charIndex": 635 }, { "level": 2, - "title": "KMP算法", - "slug": "kmp算法", - "normalizedTitle": "kmp算法", - "charIndex": 33 + "title": "简述 WebSocket 是如何进行传输的", + "slug": "简述-websocket-是如何进行传输的", + "normalizedTitle": "简述 websocket 是如何进行传输的", + "charIndex": 667 }, { "level": 2, - "title": "hash算法", - "slug": "hash算法", - "normalizedTitle": "hash算法", - "charIndex": 43 + "title": "DNS 查询服务器的基本流程是什么?DNS 劫持是什么?", + "slug": "dns-查询服务器的基本流程是什么-dns-劫持是什么", + "normalizedTitle": "dns 查询服务器的基本流程是什么?dns 劫持是什么?", + "charIndex": 693 }, { "level": 2, - "title": "海量数据处理", - "slug": "海量数据处理", - "normalizedTitle": "海量数据处理", - "charIndex": 54 + "title": "RestFul 是什么?RestFul 请求的 URL 有什么特点?", + "slug": "restful-是什么-restful-请求的-url-有什么特点", + "normalizedTitle": "restful 是什么?restful 请求的 url 有什么特点?", + "charIndex": 726 + }, + { + "level": 2, + "title": "简述 HTTP 报文头部的组成结构", + "slug": "简述-http-报文头部的组成结构", + "normalizedTitle": "简述 http 报文头部的组成结构", + "charIndex": 765 + }, + { + "level": 2, + "title": "什么是 SYN flood,如何防止这类攻击?", + "slug": "什么是-syn-flood-如何防止这类攻击", + "normalizedTitle": "什么是 syn flood,如何防止这类攻击?", + "charIndex": 787 } ], - "headersStr": "数据结构与算法知识 全排列 贪心算法 KMP算法 hash算法 海量数据处理", - "content": "# 数据结构与算法知识\n\n\n# 全排列\n\n\n# 贪心算法\n\n\n# KMP算法\n\n\n# hash算法\n\n\n# 海量数据处理\n\n* 分治\n* hash映射\n* 堆排序\n* 双层桶划分\n* Bloom Filter\n* bitmap\n* 数据库索引\n* mapreduce等。", - "normalizedContent": "# 数据结构与算法知识\n\n\n# 全排列\n\n\n# 贪心算法\n\n\n# kmp算法\n\n\n# hash算法\n\n\n# 海量数据处理\n\n* 分治\n* hash映射\n* 堆排序\n* 双层桶划分\n* bloom filter\n* bitmap\n* 数据库索引\n* mapreduce等。", + "headersStr": "简述 TCP 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手? 从输入 URL 到展现页面的全过程 HTTP 中 GET 和 POST 区别 TCP 怎么保证可靠传输 什么是 TCP 粘包和拆包? 简述 HTTPS 的加密与认证过程 简述对称与非对称加密的概念 TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别? 如何解决 TCP 传输丢包问题? TCP 长连接和短连接有那么不同的使用场景? TCP 的 keepalive 了解吗?说一说它和 HTTP 的 keepalive 的区别? HTTP 与 HTTPS 有哪些区别? TCP 中常见的拥塞控制算法有哪些? 简述 HTTP 1.0,1.1,2.0 的主要区别 从系统层面上,UDP如何保证尽量可靠? TCP 挥手时出现大量 CLOSEWAIT 或 TIMEWAIT 怎么解决? 简述 TCP 滑动窗口以及重传机制 简述 JWT 的原理和校验机制 为什么需要序列化?有什么序列化的方式? 简述 iPv4 和 iPv6 的区别 TCP 中 SYN 攻击是什么?如何防止? 简述 DDOS 攻击原理,如何防范它? 什么是 ARP 协议?简述其使用场景 什么是跨域,什么情况下会发生跨域请求? Cookie 和 Session 的关系和区别是什么? 简述 WebSocket 是如何进行传输的 DNS 查询服务器的基本流程是什么?DNS 劫持是什么? RestFul 是什么?RestFul 请求的 URL 有什么特点? 简述 HTTP 报文头部的组成结构 什么是 SYN flood,如何防止这类攻击?", + "content": "# 简述 TCP 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手?\n\n\n# 从输入 URL 到展现页面的全过程\n\n\n# HTTP 中 GET 和 POST 区别\n\n\n# TCP 怎么保证可靠传输\n\n\n# 什么是 TCP 粘包和拆包?\n\n\n# 简述 HTTPS 的加密与认证过程\n\n\n# 简述对称与非对称加密的概念\n\n\n# TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别?\n\n\n# 如何解决 TCP 传输丢包问题?\n\n\n# TCP 长连接和短连接有那么不同的使用场景?\n\n\n# TCP 的 keepalive 了解吗?说一说它和 HTTP 的 keepalive 的区别?\n\n\n# HTTP 与 HTTPS 有哪些区别?\n\n\n# TCP 中常见的拥塞控制算法有哪些?\n\n\n# 简述 HTTP 1.0,1.1,2.0 的主要区别\n\n\n# 从系统层面上,UDP如何保证尽量可靠?\n\n\n# TCP 挥手时出现大量 CLOSE_WAIT 或 TIME_WAIT 怎么解决?\n\n\n# 简述 TCP 滑动窗口以及重传机制\n\n\n# 简述 JWT 的原理和校验机制\n\n\n# 为什么需要序列化?有什么序列化的方式?\n\n\n# 简述 iPv4 和 iPv6 的区别\n\n\n# TCP 中 SYN 攻击是什么?如何防止?\n\n\n# 简述 DDOS 攻击原理,如何防范它?\n\n\n# 什么是 ARP 协议?简述其使用场景\n\n\n# 什么是跨域,什么情况下会发生跨域请求?\n\n\n# Cookie 和 Session 的关系和区别是什么?\n\n\n# 简述 WebSocket 是如何进行传输的\n\n\n# DNS 查询服务器的基本流程是什么?DNS 劫持是什么?\n\n\n# RestFul 是什么?RestFul 请求的 URL 有什么特点?\n\n\n# 简述 HTTP 报文头部的组成结构\n\n\n# 什么是 SYN flood,如何防止这类攻击?", + "normalizedContent": "# 简述 tcp 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手?\n\n\n# 从输入 url 到展现页面的全过程\n\n\n# http 中 get 和 post 区别\n\n\n# tcp 怎么保证可靠传输\n\n\n# 什么是 tcp 粘包和拆包?\n\n\n# 简述 https 的加密与认证过程\n\n\n# 简述对称与非对称加密的概念\n\n\n# tcp 与 udp 在网络协议中的哪一层,他们之间有什么区别?\n\n\n# 如何解决 tcp 传输丢包问题?\n\n\n# tcp 长连接和短连接有那么不同的使用场景?\n\n\n# tcp 的 keepalive 了解吗?说一说它和 http 的 keepalive 的区别?\n\n\n# http 与 https 有哪些区别?\n\n\n# tcp 中常见的拥塞控制算法有哪些?\n\n\n# 简述 http 1.0,1.1,2.0 的主要区别\n\n\n# 从系统层面上,udp如何保证尽量可靠?\n\n\n# tcp 挥手时出现大量 close_wait 或 time_wait 怎么解决?\n\n\n# 简述 tcp 滑动窗口以及重传机制\n\n\n# 简述 jwt 的原理和校验机制\n\n\n# 为什么需要序列化?有什么序列化的方式?\n\n\n# 简述 ipv4 和 ipv6 的区别\n\n\n# tcp 中 syn 攻击是什么?如何防止?\n\n\n# 简述 ddos 攻击原理,如何防范它?\n\n\n# 什么是 arp 协议?简述其使用场景\n\n\n# 什么是跨域,什么情况下会发生跨域请求?\n\n\n# cookie 和 session 的关系和区别是什么?\n\n\n# 简述 websocket 是如何进行传输的\n\n\n# dns 查询服务器的基本流程是什么?dns 劫持是什么?\n\n\n# restful 是什么?restful 请求的 url 有什么特点?\n\n\n# 简述 http 报文头部的组成结构\n\n\n# 什么是 syn flood,如何防止这类攻击?", "charsets": { "cjk": true }, - "lastUpdated": "2022/06/20, 22:47:41", - "lastUpdatedTimestamp": 1655736461000 + "lastUpdated": "2022/09/18, 21:28:02", + "lastUpdatedTimestamp": 1663507682000 }, { "title": "聚簇索引和非聚簇索引有什么区别?", @@ -3761,74 +4203,6 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, - { - "title": "MySQL中 InnoDB 和 MylSAM 的区别是什么?", - "frontmatter": { - "title": "MySQL中 InnoDB 和 MylSAM 的区别是什么?", - "date": "2022-05-21T17:07:47.000Z", - "permalink": "/pages/3a8659/", - "categories": [ - "数据库", - "MYSQL相关" - ], - "tags": [ - null - ] - }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/001.MySQL%E4%B8%AD%20InnoDB%20%E5%92%8C%20MylSAM%20%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/001.MySQL中 InnoDB 和 MylSAM 的区别是什么?.md", - "key": "v-674454ac", - "path": "/pages/3a8659/", - "headers": [ - { - "level": 2, - "title": "区别", - "slug": "区别", - "normalizedTitle": "区别", - "charIndex": 37 - }, - { - "level": 2, - "title": "场景选择", - "slug": "场景选择", - "normalizedTitle": "场景选择", - "charIndex": 253 - } - ], - "headersStr": "区别 场景选择", - "content": "MySQL5.5版本开始Innodb已经成为Mysql的默认引擎\n\n\n# 区别\n\n * InnoDB 支持事务,MyISAM 不支持事务。 这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一\n * MyISAM锁的粒度是表级的,而InnoDB支持行级锁\n * MyISAM支持全文类型索引,而InnoDB不支持全文索引\n * MyISAM相对简单,效率上要优于InnoDB,小型应用可以考虑使用MyISAM\n * MyISAM表保存成文件形式,跨平台使用更加方便\n\n\n# 场景选择\n\nMyISAM管理非事务表,提供高速存储和检索以及全文搜索能力,如果再应用中执行大量select操作,应该选择MyISAM InnoDB用于事务处理,具有ACID事务支持等特性,如果在应用中执行大量insert和update操作,应该选择InnoDB", - "normalizedContent": "mysql5.5版本开始innodb已经成为mysql的默认引擎\n\n\n# 区别\n\n * innodb 支持事务,myisam 不支持事务。 这是 mysql 将默认存储引擎从 myisam 变成 innodb 的重要原因之一\n * myisam锁的粒度是表级的,而innodb支持行级锁\n * myisam支持全文类型索引,而innodb不支持全文索引\n * myisam相对简单,效率上要优于innodb,小型应用可以考虑使用myisam\n * myisam表保存成文件形式,跨平台使用更加方便\n\n\n# 场景选择\n\nmyisam管理非事务表,提供高速存储和检索以及全文搜索能力,如果再应用中执行大量select操作,应该选择myisam innodb用于事务处理,具有acid事务支持等特性,如果在应用中执行大量insert和update操作,应该选择innodb", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "简述 MySQL 常见索引类型,介绍一下覆盖索引", - "frontmatter": { - "title": "简述 MySQL 常见索引类型,介绍一下覆盖索引", - "date": "2022-05-21T17:08:24.000Z", - "permalink": "/pages/17b014/", - "categories": [ - "数据库", - "MYSQL相关" - ], - "tags": [ - null - ] - }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/007.%E7%AE%80%E8%BF%B0%20MySQL%20%E5%B8%B8%E8%A7%81%E7%B4%A2%E5%BC%95%E7%B1%BB%E5%9E%8B%EF%BC%8C%E4%BB%8B%E7%BB%8D%E4%B8%80%E4%B8%8B%E8%A6%86%E7%9B%96%E7%B4%A2%E5%BC%95.html", - "relativePath": "03.数据库/01.MYSQL相关/007.简述 MySQL 常见索引类型,介绍一下覆盖索引.md", - "key": "v-824b776c", - "path": "/pages/17b014/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, { "title": "说一下Mysql锁", "frontmatter": { @@ -3886,6 +4260,31 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, + { + "title": "简述乐观锁以及悲观锁的区别以及使用场景", + "frontmatter": { + "title": "简述乐观锁以及悲观锁的区别以及使用场景", + "date": "2022-05-21T17:05:33.000Z", + "permalink": "/pages/57561d/", + "categories": [ + "数据库", + "MYSQL相关" + ], + "tags": [ + null + ] + }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/030.%E7%AE%80%E8%BF%B0%E4%B9%90%E8%A7%82%E9%94%81%E4%BB%A5%E5%8F%8A%E6%82%B2%E8%A7%82%E9%94%81%E7%9A%84%E5%8C%BA%E5%88%AB%E4%BB%A5%E5%8F%8A%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF.html", + "relativePath": "03.数据库/01.MYSQL相关/030.简述乐观锁以及悲观锁的区别以及使用场景.md", + "key": "v-c39c4fe2", + "path": "/pages/57561d/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, { "title": "简述Mysql的事务隔离级别", "frontmatter": { @@ -3943,6 +4342,31 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, + { + "title": "简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?", + "frontmatter": { + "title": "简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?", + "date": "2022-05-21T17:05:55.000Z", + "permalink": "/pages/36672c/", + "categories": [ + "数据库", + "MYSQL相关" + ], + "tags": [ + null + ] + }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/040.%E7%AE%80%E8%BF%B0%E8%84%8F%E8%AF%BB%E5%92%8C%E5%B9%BB%E8%AF%BB%E7%9A%84%E5%8F%91%E7%94%9F%E5%9C%BA%E6%99%AF%EF%BC%8CInnoDB%20%E6%98%AF%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E5%B9%BB%E8%AF%BB%E7%9A%84%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/040.简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?.md", + "key": "v-689e142a", + "path": "/pages/36672c/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, { "title": "MySQL 为什么使用 B+ 树来作索引,对比 B 树它的优点和缺点是什么", "frontmatter": { @@ -3987,11 +4411,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述乐观锁以及悲观锁的区别以及使用场景", + "title": "简述什么是最左匹配原则", "frontmatter": { - "title": "简述乐观锁以及悲观锁的区别以及使用场景", - "date": "2022-05-21T17:05:33.000Z", - "permalink": "/pages/57561d/", + "title": "简述什么是最左匹配原则", + "date": "2022-05-21T17:06:10.000Z", + "permalink": "/pages/7a1f1e/", "categories": [ "数据库", "MYSQL相关" @@ -4000,10 +4424,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/030.%E7%AE%80%E8%BF%B0%E4%B9%90%E8%A7%82%E9%94%81%E4%BB%A5%E5%8F%8A%E6%82%B2%E8%A7%82%E9%94%81%E7%9A%84%E5%8C%BA%E5%88%AB%E4%BB%A5%E5%8F%8A%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF.html", - "relativePath": "03.数据库/01.MYSQL相关/030.简述乐观锁以及悲观锁的区别以及使用场景.md", - "key": "v-c39c4fe2", - "path": "/pages/57561d/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/050.%E7%AE%80%E8%BF%B0%E4%BB%80%E4%B9%88%E6%98%AF%E6%9C%80%E5%B7%A6%E5%8C%B9%E9%85%8D%E5%8E%9F%E5%88%99.html", + "relativePath": "03.数据库/01.MYSQL相关/050.简述什么是最左匹配原则.md", + "key": "v-4ecddbec", + "path": "/pages/7a1f1e/", "headersStr": null, "content": "", "normalizedContent": "", @@ -4012,11 +4436,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?", + "title": "产生死锁的必要条件有哪些?如何解决死锁?", "frontmatter": { - "title": "简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?", - "date": "2022-05-21T17:05:55.000Z", - "permalink": "/pages/36672c/", + "title": "产生死锁的必要条件有哪些?如何解决死锁?", + "date": "2022-05-21T17:06:48.000Z", + "permalink": "/pages/f4cf50/", "categories": [ "数据库", "MYSQL相关" @@ -4025,10 +4449,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/040.%E7%AE%80%E8%BF%B0%E8%84%8F%E8%AF%BB%E5%92%8C%E5%B9%BB%E8%AF%BB%E7%9A%84%E5%8F%91%E7%94%9F%E5%9C%BA%E6%99%AF%EF%BC%8CInnoDB%20%E6%98%AF%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E5%B9%BB%E8%AF%BB%E7%9A%84%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/040.简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?.md", - "key": "v-689e142a", - "path": "/pages/36672c/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/070.%E4%BA%A7%E7%94%9F%E6%AD%BB%E9%94%81%E7%9A%84%E5%BF%85%E8%A6%81%E6%9D%A1%E4%BB%B6%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E6%AD%BB%E9%94%81%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/070.产生死锁的必要条件有哪些?如何解决死锁?.md", + "key": "v-0dd47f09", + "path": "/pages/f4cf50/", "headersStr": null, "content": "", "normalizedContent": "", @@ -4086,56 +4510,6 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, - { - "title": "产生死锁的必要条件有哪些?如何解决死锁?", - "frontmatter": { - "title": "产生死锁的必要条件有哪些?如何解决死锁?", - "date": "2022-05-21T17:06:48.000Z", - "permalink": "/pages/f4cf50/", - "categories": [ - "数据库", - "MYSQL相关" - ], - "tags": [ - null - ] - }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/070.%E4%BA%A7%E7%94%9F%E6%AD%BB%E9%94%81%E7%9A%84%E5%BF%85%E8%A6%81%E6%9D%A1%E4%BB%B6%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E6%AD%BB%E9%94%81%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/070.产生死锁的必要条件有哪些?如何解决死锁?.md", - "key": "v-0dd47f09", - "path": "/pages/f4cf50/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "简述什么是最左匹配原则", - "frontmatter": { - "title": "简述什么是最左匹配原则", - "date": "2022-05-21T17:06:10.000Z", - "permalink": "/pages/7a1f1e/", - "categories": [ - "数据库", - "MYSQL相关" - ], - "tags": [ - null - ] - }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/050.%E7%AE%80%E8%BF%B0%E4%BB%80%E4%B9%88%E6%98%AF%E6%9C%80%E5%B7%A6%E5%8C%B9%E9%85%8D%E5%8E%9F%E5%88%99.html", - "relativePath": "03.数据库/01.MYSQL相关/050.简述什么是最左匹配原则.md", - "key": "v-4ecddbec", - "path": "/pages/7a1f1e/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, { "title": "简述 MySQL 的主从同步机制,如果同步失败会怎么样?", "frontmatter": { @@ -4175,99 +4549,23 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/110.%E7%AE%80%E8%BF%B0%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B8%AD%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E8%BF%9B%E8%A1%8C%E5%88%86%E5%BA%93%EF%BC%8C%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E8%BF%9B%E8%A1%8C%E5%88%86%E8%A1%A8%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/110.简述数据库中什么情况下进行分库,什么情况下进行分表?.md", - "key": "v-ed0c93b8", - "path": "/pages/ac0bb8/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "数据库的读写分离的作用是什么?如何实现?", - "frontmatter": { - "title": "数据库的读写分离的作用是什么?如何实现?", - "date": "2022-05-21T17:09:05.000Z", - "permalink": "/pages/f62359/", - "categories": [ - "数据库", - "MYSQL相关" - ], - "tags": [ - null - ] - }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/120.%E6%95%B0%E6%8D%AE%E5%BA%93%E7%9A%84%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB%E7%9A%84%E4%BD%9C%E7%94%A8%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/120.数据库的读写分离的作用是什么?如何实现?.md", - "key": "v-d6bc240a", - "path": "/pages/f62359/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "简述 undo log 和 redo log 的作用", - "frontmatter": { - "title": "简述 undo log 和 redo log 的作用", - "date": "2022-05-21T17:09:29.000Z", - "permalink": "/pages/7bbbf6/", - "categories": [ - "数据库", - "MYSQL相关" - ], - "tags": [ - "binlog", - "redo log" - ] - }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/130.%E7%AE%80%E8%BF%B0%20undo%20log%20%E5%92%8C%20redo%20log%20%E7%9A%84%E4%BD%9C%E7%94%A8.html", - "relativePath": "03.数据库/01.MYSQL相关/130.简述 undo log 和 redo log 的作用.md", - "key": "v-f3acaa1a", - "path": "/pages/7bbbf6/", - "headers": [ - { - "level": 2, - "title": "redo log 的写入机制", - "slug": "redo-log-的写入机制", - "normalizedTitle": "redo log 的写入机制", - "charIndex": 456 - }, - { - "level": 2, - "title": "binlog 的写入机制", - "slug": "binlog-的写入机制", - "normalizedTitle": "binlog 的写入机制", - "charIndex": 711 - }, - { - "level": 2, - "title": "组提交机制(group commit)", - "slug": "组提交机制-group-commit", - "normalizedTitle": "组提交机制(group commit)", - "charIndex": 971 - } - ], - "headersStr": "redo log 的写入机制 binlog 的写入机制 组提交机制(group commit)", - "content": "# binlog和redo log是什么,有什么用\n\n这两个日志都可以用于数据库的备份和恢复,以及主从复制。区别在于\n\nBINLOG REDO LOG 备注\n在sql 层产生 在innoDB存储引擎层产生 \n记录数据行修改 记录数据页修改 \n先提交事务,再产生binlog 先产生redo log再提交事务 \n用于主从复制 在在mysql崩溃时,恢复未提交事务数据 \n \n\n\n# binlog和redo log写入机制\n\n先说共性,这两个log的写入都要走这个过程: 先写入mysql server的buffer,再写入os buffer,再写入磁盘。 两个buffer之间的写入是内存操作,很快的,耗费io的操作是调用fsync写入磁盘的操作。\n\n\n# redo log 的写入机制\n\ninnodb_flush_log_at_trx_commit的值有三种0、1、2。控制log写入磁盘的频率。 innodb_flush_log_at_trx_commit的值 | 写入策略\n\n-------- | ----- 1 |事务commit后马上将log buffer写入os buffer并且调用fsync写入磁盘 0 | 每秒写入os buffer并立刻调用fsync写入磁盘 2 | commit后立即写入os buffer,然后每秒调用fsync写磁盘\n\n\n# binlog 的写入机制\n\n每个线程一个binlog缓存,因为binlog写入要求事务的完整性,即一个事务完成才能把事务binlog完整的写入。\n\nSYNC_BINLOG的值 写入策略\n1 事务commit后马上将log buffer写入os buffer并且调用fsync写入磁盘\n0 每秒写入os buffer并立刻调用fsync写入磁盘\nN(N>1) commit后立即写入os buffer,积累N个事务调用fsync写磁盘\n\n\n# 组提交机制(group commit)\n\n> 双1配置是说将sync_binlog和innodb_flush_log_at_trx_commit都设置为1.那是不是说每次commit都要两次刷盘?但是是否定的。用组提交机制来实现。\n\n如果你想提升 binlog 组提交的效果,可以通过设置binlog_group_commit_sync_delay和binlog_group_commit_sync_no_delay_count实现:\n\nBINLOG_GROUP_COMMIT_SYNC_DELAY BINLOG_GROUP_COMMIT_SYNC_NO_DELAY_COUNT\n调用多少微妙后再调用fsync 积累多少次后调用fsync\n\n\n# 应对IO性能瓶颈\n\n如果IO出现性能瓶颈,可以考虑以下三个解决办法\n\n * 调大binlog_group_commit_sync_delay和binlog_group_commit_sync_no_delay_count,后果是可能增加sql执行时间,但是数据不会丢失。\n * sync_binlog设置大于1的数,风险是,断电后悔丢掉binlog日志\n * innodb_flush_log_at_trx_commit设置为2,风险是,断电后会丢失数据。", - "normalizedContent": "# binlog和redo log是什么,有什么用\n\n这两个日志都可以用于数据库的备份和恢复,以及主从复制。区别在于\n\nbinlog redo log 备注\n在sql 层产生 在innodb存储引擎层产生 \n记录数据行修改 记录数据页修改 \n先提交事务,再产生binlog 先产生redo log再提交事务 \n用于主从复制 在在mysql崩溃时,恢复未提交事务数据 \n \n\n\n# binlog和redo log写入机制\n\n先说共性,这两个log的写入都要走这个过程: 先写入mysql server的buffer,再写入os buffer,再写入磁盘。 两个buffer之间的写入是内存操作,很快的,耗费io的操作是调用fsync写入磁盘的操作。\n\n\n# redo log 的写入机制\n\ninnodb_flush_log_at_trx_commit的值有三种0、1、2。控制log写入磁盘的频率。 innodb_flush_log_at_trx_commit的值 | 写入策略\n\n-------- | ----- 1 |事务commit后马上将log buffer写入os buffer并且调用fsync写入磁盘 0 | 每秒写入os buffer并立刻调用fsync写入磁盘 2 | commit后立即写入os buffer,然后每秒调用fsync写磁盘\n\n\n# binlog 的写入机制\n\n每个线程一个binlog缓存,因为binlog写入要求事务的完整性,即一个事务完成才能把事务binlog完整的写入。\n\nsync_binlog的值 写入策略\n1 事务commit后马上将log buffer写入os buffer并且调用fsync写入磁盘\n0 每秒写入os buffer并立刻调用fsync写入磁盘\nn(n>1) commit后立即写入os buffer,积累n个事务调用fsync写磁盘\n\n\n# 组提交机制(group commit)\n\n> 双1配置是说将sync_binlog和innodb_flush_log_at_trx_commit都设置为1.那是不是说每次commit都要两次刷盘?但是是否定的。用组提交机制来实现。\n\n如果你想提升 binlog 组提交的效果,可以通过设置binlog_group_commit_sync_delay和binlog_group_commit_sync_no_delay_count实现:\n\nbinlog_group_commit_sync_delay binlog_group_commit_sync_no_delay_count\n调用多少微妙后再调用fsync 积累多少次后调用fsync\n\n\n# 应对io性能瓶颈\n\n如果io出现性能瓶颈,可以考虑以下三个解决办法\n\n * 调大binlog_group_commit_sync_delay和binlog_group_commit_sync_no_delay_count,后果是可能增加sql执行时间,但是数据不会丢失。\n * sync_binlog设置大于1的数,风险是,断电后悔丢掉binlog日志\n * innodb_flush_log_at_trx_commit设置为2,风险是,断电后会丢失数据。", - "charsets": { - "cjk": true - }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/110.%E7%AE%80%E8%BF%B0%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B8%AD%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E8%BF%9B%E8%A1%8C%E5%88%86%E5%BA%93%EF%BC%8C%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E8%BF%9B%E8%A1%8C%E5%88%86%E8%A1%A8%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/110.简述数据库中什么情况下进行分库,什么情况下进行分表?.md", + "key": "v-ed0c93b8", + "path": "/pages/ac0bb8/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, { - "title": "如何设计数据库压测方案?", + "title": "数据库的读写分离的作用是什么?如何实现?", "frontmatter": { - "title": "如何设计数据库压测方案?", - "date": "2022-05-21T17:08:40.000Z", - "permalink": "/pages/a8eb47/", + "title": "数据库的读写分离的作用是什么?如何实现?", + "date": "2022-05-21T17:09:05.000Z", + "permalink": "/pages/f62359/", "categories": [ "数据库", "MYSQL相关" @@ -4276,10 +4574,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/100.%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%8B%E6%B5%8B%E6%96%B9%E6%A1%88%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/100.如何设计数据库压测方案?.md", - "key": "v-4cef944a", - "path": "/pages/a8eb47/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/120.%E6%95%B0%E6%8D%AE%E5%BA%93%E7%9A%84%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB%E7%9A%84%E4%BD%9C%E7%94%A8%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/120.数据库的读写分离的作用是什么?如何实现?.md", + "key": "v-d6bc240a", + "path": "/pages/f62359/", "headersStr": null, "content": "", "normalizedContent": "", @@ -4338,27 +4636,53 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "MySQL 中 varchar 和 char 的区别是什么?", + "title": "简述 undo log 和 redo log 的作用", "frontmatter": { - "title": "MySQL 中 varchar 和 char 的区别是什么?", - "date": "2022-05-21T17:10:21.000Z", - "permalink": "/pages/d97948/", + "title": "简述 undo log 和 redo log 的作用", + "date": "2022-05-21T17:09:29.000Z", + "permalink": "/pages/7bbbf6/", "categories": [ "数据库", "MYSQL相关" ], "tags": [ - null + "binlog", + "redo log" ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/170.MySQL%20%E4%B8%AD%20varchar%20%E5%92%8C%20char%20%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/170.MySQL 中 varchar 和 char 的区别是什么?.md", - "key": "v-0a7e6416", - "path": "/pages/d97948/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/130.%E7%AE%80%E8%BF%B0%20undo%20log%20%E5%92%8C%20redo%20log%20%E7%9A%84%E4%BD%9C%E7%94%A8.html", + "relativePath": "03.数据库/01.MYSQL相关/130.简述 undo log 和 redo log 的作用.md", + "key": "v-f3acaa1a", + "path": "/pages/7bbbf6/", + "headers": [ + { + "level": 2, + "title": "redo log 的写入机制", + "slug": "redo-log-的写入机制", + "normalizedTitle": "redo log 的写入机制", + "charIndex": 456 + }, + { + "level": 2, + "title": "binlog 的写入机制", + "slug": "binlog-的写入机制", + "normalizedTitle": "binlog 的写入机制", + "charIndex": 711 + }, + { + "level": 2, + "title": "组提交机制(group commit)", + "slug": "组提交机制-group-commit", + "normalizedTitle": "组提交机制(group commit)", + "charIndex": 971 + } + ], + "headersStr": "redo log 的写入机制 binlog 的写入机制 组提交机制(group commit)", + "content": "# binlog和redo log是什么,有什么用\n\n这两个日志都可以用于数据库的备份和恢复,以及主从复制。区别在于\n\nBINLOG REDO LOG 备注\n在sql 层产生 在innoDB存储引擎层产生 \n记录数据行修改 记录数据页修改 \n先提交事务,再产生binlog 先产生redo log再提交事务 \n用于主从复制 在在mysql崩溃时,恢复未提交事务数据 \n \n\n\n# binlog和redo log写入机制\n\n先说共性,这两个log的写入都要走这个过程: 先写入mysql server的buffer,再写入os buffer,再写入磁盘。 两个buffer之间的写入是内存操作,很快的,耗费io的操作是调用fsync写入磁盘的操作。\n\n\n# redo log 的写入机制\n\ninnodb_flush_log_at_trx_commit的值有三种0、1、2。控制log写入磁盘的频率。 innodb_flush_log_at_trx_commit的值 | 写入策略\n\n-------- | ----- 1 |事务commit后马上将log buffer写入os buffer并且调用fsync写入磁盘 0 | 每秒写入os buffer并立刻调用fsync写入磁盘 2 | commit后立即写入os buffer,然后每秒调用fsync写磁盘\n\n\n# binlog 的写入机制\n\n每个线程一个binlog缓存,因为binlog写入要求事务的完整性,即一个事务完成才能把事务binlog完整的写入。\n\nSYNC_BINLOG的值 写入策略\n1 事务commit后马上将log buffer写入os buffer并且调用fsync写入磁盘\n0 每秒写入os buffer并立刻调用fsync写入磁盘\nN(N>1) commit后立即写入os buffer,积累N个事务调用fsync写磁盘\n\n\n# 组提交机制(group commit)\n\n> 双1配置是说将sync_binlog和innodb_flush_log_at_trx_commit都设置为1.那是不是说每次commit都要两次刷盘?但是是否定的。用组提交机制来实现。\n\n如果你想提升 binlog 组提交的效果,可以通过设置binlog_group_commit_sync_delay和binlog_group_commit_sync_no_delay_count实现:\n\nBINLOG_GROUP_COMMIT_SYNC_DELAY BINLOG_GROUP_COMMIT_SYNC_NO_DELAY_COUNT\n调用多少微妙后再调用fsync 积累多少次后调用fsync\n\n\n# 应对IO性能瓶颈\n\n如果IO出现性能瓶颈,可以考虑以下三个解决办法\n\n * 调大binlog_group_commit_sync_delay和binlog_group_commit_sync_no_delay_count,后果是可能增加sql执行时间,但是数据不会丢失。\n * sync_binlog设置大于1的数,风险是,断电后悔丢掉binlog日志\n * innodb_flush_log_at_trx_commit设置为2,风险是,断电后会丢失数据。", + "normalizedContent": "# binlog和redo log是什么,有什么用\n\n这两个日志都可以用于数据库的备份和恢复,以及主从复制。区别在于\n\nbinlog redo log 备注\n在sql 层产生 在innodb存储引擎层产生 \n记录数据行修改 记录数据页修改 \n先提交事务,再产生binlog 先产生redo log再提交事务 \n用于主从复制 在在mysql崩溃时,恢复未提交事务数据 \n \n\n\n# binlog和redo log写入机制\n\n先说共性,这两个log的写入都要走这个过程: 先写入mysql server的buffer,再写入os buffer,再写入磁盘。 两个buffer之间的写入是内存操作,很快的,耗费io的操作是调用fsync写入磁盘的操作。\n\n\n# redo log 的写入机制\n\ninnodb_flush_log_at_trx_commit的值有三种0、1、2。控制log写入磁盘的频率。 innodb_flush_log_at_trx_commit的值 | 写入策略\n\n-------- | ----- 1 |事务commit后马上将log buffer写入os buffer并且调用fsync写入磁盘 0 | 每秒写入os buffer并立刻调用fsync写入磁盘 2 | commit后立即写入os buffer,然后每秒调用fsync写磁盘\n\n\n# binlog 的写入机制\n\n每个线程一个binlog缓存,因为binlog写入要求事务的完整性,即一个事务完成才能把事务binlog完整的写入。\n\nsync_binlog的值 写入策略\n1 事务commit后马上将log buffer写入os buffer并且调用fsync写入磁盘\n0 每秒写入os buffer并立刻调用fsync写入磁盘\nn(n>1) commit后立即写入os buffer,积累n个事务调用fsync写磁盘\n\n\n# 组提交机制(group commit)\n\n> 双1配置是说将sync_binlog和innodb_flush_log_at_trx_commit都设置为1.那是不是说每次commit都要两次刷盘?但是是否定的。用组提交机制来实现。\n\n如果你想提升 binlog 组提交的效果,可以通过设置binlog_group_commit_sync_delay和binlog_group_commit_sync_no_delay_count实现:\n\nbinlog_group_commit_sync_delay binlog_group_commit_sync_no_delay_count\n调用多少微妙后再调用fsync 积累多少次后调用fsync\n\n\n# 应对io性能瓶颈\n\n如果io出现性能瓶颈,可以考虑以下三个解决办法\n\n * 调大binlog_group_commit_sync_delay和binlog_group_commit_sync_no_delay_count,后果是可能增加sql执行时间,但是数据不会丢失。\n * sync_binlog设置大于1的数,风险是,断电后悔丢掉binlog日志\n * innodb_flush_log_at_trx_commit设置为2,风险是,断电后会丢失数据。", + "charsets": { + "cjk": true + }, "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, @@ -4475,6 +4799,56 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, + { + "title": "如何设计数据库压测方案?", + "frontmatter": { + "title": "如何设计数据库压测方案?", + "date": "2022-05-21T17:08:40.000Z", + "permalink": "/pages/a8eb47/", + "categories": [ + "数据库", + "MYSQL相关" + ], + "tags": [ + null + ] + }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/100.%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%8B%E6%B5%8B%E6%96%B9%E6%A1%88%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/100.如何设计数据库压测方案?.md", + "key": "v-4cef944a", + "path": "/pages/a8eb47/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "MySQL 中 varchar 和 char 的区别是什么?", + "frontmatter": { + "title": "MySQL 中 varchar 和 char 的区别是什么?", + "date": "2022-05-21T17:10:21.000Z", + "permalink": "/pages/d97948/", + "categories": [ + "数据库", + "MYSQL相关" + ], + "tags": [ + null + ] + }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/170.MySQL%20%E4%B8%AD%20varchar%20%E5%92%8C%20char%20%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/170.MySQL 中 varchar 和 char 的区别是什么?.md", + "key": "v-0a7e6416", + "path": "/pages/d97948/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, { "title": "模糊查询是如何实现的?", "frontmatter": { @@ -4500,6 +4874,31 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, + { + "title": "为什么 Redis 在单线程下能如此快?", + "frontmatter": { + "title": "为什么 Redis 在单线程下能如此快?", + "date": "2022-05-21T17:20:12.000Z", + "permalink": "/pages/d66211/", + "categories": [ + "数据库", + "REDIS相关" + ], + "tags": [ + null + ] + }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/002.%E4%B8%BA%E4%BB%80%E4%B9%88%20Redis%20%E5%9C%A8%E5%8D%95%E7%BA%BF%E7%A8%8B%E4%B8%8B%E8%83%BD%E5%A6%82%E6%AD%A4%E5%BF%AB%EF%BC%9F.html", + "relativePath": "03.数据库/02.REDIS相关/002.为什么 Redis 在单线程下能如此快?.md", + "key": "v-cb742458", + "path": "/pages/d66211/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, { "title": "Redis 有几种数据结构?Zset 是如何实现的?", "frontmatter": { @@ -4544,87 +4943,37 @@ export const siteData = { "level": 2, "title": "跳跃表", "slug": "跳跃表", - "normalizedTitle": "跳跃表", - "charIndex": 2338 - }, - { - "level": 2, - "title": "整数集合", - "slug": "整数集合", - "normalizedTitle": "整数集合", - "charIndex": 3010 - }, - { - "level": 2, - "title": "压缩列表", - "slug": "压缩列表", - "normalizedTitle": "压缩列表", - "charIndex": 3402 - }, - { - "level": 2, - "title": "RedisObject", - "slug": "redisobject", - "normalizedTitle": "redisobject", - "charIndex": 4119 - } - ], - "headersStr": "字符串 链表 hash 跳跃表 整数集合 压缩列表 RedisObject", - "content": "> 字符串(String)、列表(list)、字典(hash)、集合(set)、有序集合(sortSet).\n\n\n# 字符串\n\n 1. 源码\n\n/*\n- 保存字符串对象的结构\n*/\nstruct sdshdr {\n\n // buf 中已占用空间的长度\n int len;\n\n // buf 中剩余可用空间的长度\n int free;\n\n // 数据空间\n char buf[];\n};\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n\n 2. sds与c字符串的区别\n\n * sds获取字符串长度的复杂度为O(1),c的是O(n)\n * c的api是不安全的,可能造成缓冲区溢出,sds的是安全的。\n * 修改N次字符串,c需要N次内存分配。sds最多需要N次分配。sds有空间预分配和惰性回收。\n * sds可以使用部分c的字符串库。\n\n\n# 链表\n\n 1. 源码\n\n/*\n* 双端链表节点\n*/\ntypedef struct listNode {\n\n // 前置节点\n struct listNode *prev;\n\n // 后置节点\n struct listNode *next;\n\n // 节点的值\n void *value;\n\n} listNode;\n\n/*\n* 双端链表结构\n*/\ntypedef struct list {\n\n // 表头节点\n listNode *head;\n\n // 表尾节点\n listNode *tail;\n\n // 节点值复制函数\n void *(*dup)(void *ptr);\n\n // 节点值释放函数\n void (*free)(void *ptr);\n\n // 节点值对比函数\n int (*match)(void *ptr, void *key);\n\n // 链表所包含的节点数量\n unsigned long len;\n\n} list;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n\n\n[外链图片转存中...(img-ua4uHhOc-1653145947289)]\n\n\n# hash\n\n 1. 源码\n\n/*\n* 哈希表节点\n*/\ntypedef struct dictEntry {\n \n // 键\n void *key;\n\n // 值\n union {\n void *val;\n uint64_t u64;\n int64_t s64;\n } v;\n\n // 指向下个哈希表节点,形成链表\n struct dictEntry *next;\n\n} dictEntry;\n\n/*\n* 哈希表\n*\n* 每个字典都使用两个哈希表,从而实现渐进式 rehash 。\n*/\ntypedef struct dictht {\n \n // 哈希表数组\n dictEntry **table;\n\n // 哈希表大小\n unsigned long size;\n \n // 哈希表大小掩码,用于计算索引值\n // 总是等于 size - 1\n unsigned long sizemask;\n\n // 该哈希表已有节点的数量\n unsigned long used;\n\n} dictht;\n\n/*\n* 字典\n*/\ntypedef struct dict {\n\n // 类型特定函数\n dictType *type;\n\n // 私有数据\n void *privdata;\n\n // 哈希表\n dictht ht[2];\n\n // rehash 索引\n // 当 rehash 不在进行时,值为 -1\n int rehashidx; /* rehashing not in progress if rehashidx == -1 */\n\n // 目前正在运行的安全迭代器的数量\n int iterators; /* number of iterators currently running */\n\n} dict;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n57\n58\n59\n60\n61\n62\n63\n64\n\n\n[外链图片转存中...(img-3IEt2xy5-1653145947290)] 2. reHash\n\n> 扩容场景: 有BGSAVE或者BGREWRITEAOF时,负载因子超过5. 无BGSAVE或者BGREWRITEAOF时,负载因子超过1. 扩容为第一个大于等于ht[0].used*2的2的幂\n\n> 缩容场景: 负载因子小于0.1 缩容为第一个大于等于ht[0].used的2的幂\n\n> 负载因子:ht[0].used/ht[0].size\n\n> 渐进式rehash: rehashidx指向正在hash的索引。rehashidx=-1表示未进行rehash 每次访问dict结构时rehash一个索引\n\n[外链图片转存中...(img-A0e54HI9-1653145947290)]\n\n\n# 跳跃表\n\n 1. 源码\n\n/*\n* 跳跃表节点\n*/\ntypedef struct zskiplistNode {\n\n // 成员对象\n robj *obj;\n\n // 分值\n double score;\n\n // 后退指针\n struct zskiplistNode *backward;\n\n // 层\n struct zskiplistLevel {\n\n // 前进指针\n struct zskiplistNode *forward;\n\n // 跨度\n unsigned int span;\n\n } level[];\n\n} zskiplistNode;\n\n/*\n* 跳跃表\n*/\ntypedef struct zskiplist {\n\n // 表头节点和表尾节点\n struct zskiplistNode *header, *tail;\n\n // 表中节点的数量\n unsigned long length;\n\n // 表中层数最大的节点的层数\n int level;\n\n} zskiplist;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n\n\n[外链图片转存中...(img-MbZS62dp-1653145947291)]\n\n\n# 整数集合\n\n 1. 源码\n\ntypedef struct intset {\n \n // 编码方式\n uint32_t encoding;\n\n // 集合包含的元素数量\n uint32_t length;\n\n // 保存元素的数组\n int8_t contents[];\n\n} intset;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n[外链图片转存中...(img-hdTbZZhS-1653145947291)] 2. 编码升级 添加一个元素可能导致编码升级。编码升级需要做三件事\n\n * 扩展空间\n * 转变现有元素的类型并发至在合适的位置上。保证原有顺序防止\n * 防止新加入的元素。新加入的元素一定是最大或最小的,所以放在最前或者最后\n\n[外链图片转存中...(img-0R00I0BO-1653145947291)]\n\n\n# 压缩列表\n\n> 压缩列表是 Redis 为了节约内存而开发的, 由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。 一个压缩列表可以包含任意多个节点(entry), 每个节点可以保存一个字节数组或者一个整数值\n\n 1. 源码\n\n/*\n* 保存 ziplist 节点信息的结构\n*/\ntypedef struct zlentry {\n\n // prevrawlen :前置节点的长度\n // prevrawlensize :编码 prevrawlen 所需的字节大小\n unsigned int prevrawlensize, prevrawlen;\n\n // len :当前节点值的长度\n // lensize :编码 len 所需的字节大小\n unsigned int lensize, len;\n\n // 当前节点 header 的大小\n // 等于 prevrawlensize + lensize\n unsigned int headersize;\n\n // 当前节点值所使用的编码类型\n unsigned char encoding;\n\n // 指向当前节点的指针\n unsigned char *p;\n\n} zlentry;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n\n 2. 示意图 [外链图片转存中...(img-NMDF8Bnt-1653145947292)] [外链图片转存中...(img-5KdqaS0e-1653145947292)]\n\n\n# RedisObject\n\n> Redis 使用对象来表示数据库中的键和值, 每次当我们在 Redis 的数据库中新创建一个键值对时, 我们至少会创建两个对象, 一个对象用作键值对的键(键对象), 另一个对象用作键值对的值(值对象)。\n\n 1. 源码\n\ntypedef struct redisObject {\n\n // 类型\n unsigned type:4;\n\n // 编码\n unsigned encoding:4;\n\n // 对象最后一次被访问的时间\n unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */\n\n // 引用计数\n int refcount;\n\n // 指向实际值的指针\n void *ptr;\n\n} robj;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n\n\n[外链图片转存中...(img-uT4NW4Ki-1653145947292)] 2. 类型和编码的对应关系 [外链图片转存中...(img-jkooRnC4-1653145947293)] 3. 字符串对象\n\n值 编码\n可以用long保存的整形 int\n可以用long,double保存的浮点 embstr或row\n长度太长,不可以用long,double保存的浮点 embstr或row\n小于39字节的字符串 embstr\n大于39字节的字符串 row\n\n> row要两次申请内存,两次释放内存。为这俩对象RedisObject、SdsStr。embstr申请一次释放一次,而且申请的是连续的内存空间,能更好的利用缓存。\n\n 4. 链表对象\n\n> inkedList和zipList的转换临界值:每个元素长度都小于64、元素个数小于512。满足以上两个条件才能用zipList\n\n 5. hash对象\n\n> hashtable和zipList的转换临界值:每个元素长度都小于64、元素个数小于512。满足以上两个条件才能用zipList\n\n 6. 集合对象\n\n> hashtable和intset的转换临界值:每个元素都是int、元素个数小于512。满足以上两个条件才能用intset\n\n 7. 内存回收\n\n> redis是用C实现的,C没有自动回收内存的机制。RedisObject中的refCount记录对象的引用个数,当refCount=0的时候自动释放内存。\n\n 8. 对象共享\n\n> 0- 9999这1w个整数是共享对象。字符串不做共享对象,因为对比匹配太复杂", - "normalizedContent": "> 字符串(string)、列表(list)、字典(hash)、集合(set)、有序集合(sortset).\n\n\n# 字符串\n\n 1. 源码\n\n/*\n- 保存字符串对象的结构\n*/\nstruct sdshdr {\n\n // buf 中已占用空间的长度\n int len;\n\n // buf 中剩余可用空间的长度\n int free;\n\n // 数据空间\n char buf[];\n};\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n\n 2. sds与c字符串的区别\n\n * sds获取字符串长度的复杂度为o(1),c的是o(n)\n * c的api是不安全的,可能造成缓冲区溢出,sds的是安全的。\n * 修改n次字符串,c需要n次内存分配。sds最多需要n次分配。sds有空间预分配和惰性回收。\n * sds可以使用部分c的字符串库。\n\n\n# 链表\n\n 1. 源码\n\n/*\n* 双端链表节点\n*/\ntypedef struct listnode {\n\n // 前置节点\n struct listnode *prev;\n\n // 后置节点\n struct listnode *next;\n\n // 节点的值\n void *value;\n\n} listnode;\n\n/*\n* 双端链表结构\n*/\ntypedef struct list {\n\n // 表头节点\n listnode *head;\n\n // 表尾节点\n listnode *tail;\n\n // 节点值复制函数\n void *(*dup)(void *ptr);\n\n // 节点值释放函数\n void (*free)(void *ptr);\n\n // 节点值对比函数\n int (*match)(void *ptr, void *key);\n\n // 链表所包含的节点数量\n unsigned long len;\n\n} list;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n\n\n[外链图片转存中...(img-ua4uhhoc-1653145947289)]\n\n\n# hash\n\n 1. 源码\n\n/*\n* 哈希表节点\n*/\ntypedef struct dictentry {\n \n // 键\n void *key;\n\n // 值\n union {\n void *val;\n uint64_t u64;\n int64_t s64;\n } v;\n\n // 指向下个哈希表节点,形成链表\n struct dictentry *next;\n\n} dictentry;\n\n/*\n* 哈希表\n*\n* 每个字典都使用两个哈希表,从而实现渐进式 rehash 。\n*/\ntypedef struct dictht {\n \n // 哈希表数组\n dictentry **table;\n\n // 哈希表大小\n unsigned long size;\n \n // 哈希表大小掩码,用于计算索引值\n // 总是等于 size - 1\n unsigned long sizemask;\n\n // 该哈希表已有节点的数量\n unsigned long used;\n\n} dictht;\n\n/*\n* 字典\n*/\ntypedef struct dict {\n\n // 类型特定函数\n dicttype *type;\n\n // 私有数据\n void *privdata;\n\n // 哈希表\n dictht ht[2];\n\n // rehash 索引\n // 当 rehash 不在进行时,值为 -1\n int rehashidx; /* rehashing not in progress if rehashidx == -1 */\n\n // 目前正在运行的安全迭代器的数量\n int iterators; /* number of iterators currently running */\n\n} dict;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n57\n58\n59\n60\n61\n62\n63\n64\n\n\n[外链图片转存中...(img-3iet2xy5-1653145947290)] 2. rehash\n\n> 扩容场景: 有bgsave或者bgrewriteaof时,负载因子超过5. 无bgsave或者bgrewriteaof时,负载因子超过1. 扩容为第一个大于等于ht[0].used*2的2的幂\n\n> 缩容场景: 负载因子小于0.1 缩容为第一个大于等于ht[0].used的2的幂\n\n> 负载因子:ht[0].used/ht[0].size\n\n> 渐进式rehash: rehashidx指向正在hash的索引。rehashidx=-1表示未进行rehash 每次访问dict结构时rehash一个索引\n\n[外链图片转存中...(img-a0e54hi9-1653145947290)]\n\n\n# 跳跃表\n\n 1. 源码\n\n/*\n* 跳跃表节点\n*/\ntypedef struct zskiplistnode {\n\n // 成员对象\n robj *obj;\n\n // 分值\n double score;\n\n // 后退指针\n struct zskiplistnode *backward;\n\n // 层\n struct zskiplistlevel {\n\n // 前进指针\n struct zskiplistnode *forward;\n\n // 跨度\n unsigned int span;\n\n } level[];\n\n} zskiplistnode;\n\n/*\n* 跳跃表\n*/\ntypedef struct zskiplist {\n\n // 表头节点和表尾节点\n struct zskiplistnode *header, *tail;\n\n // 表中节点的数量\n unsigned long length;\n\n // 表中层数最大的节点的层数\n int level;\n\n} zskiplist;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n\n\n[外链图片转存中...(img-mbzs62dp-1653145947291)]\n\n\n# 整数集合\n\n 1. 源码\n\ntypedef struct intset {\n \n // 编码方式\n uint32_t encoding;\n\n // 集合包含的元素数量\n uint32_t length;\n\n // 保存元素的数组\n int8_t contents[];\n\n} intset;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n[外链图片转存中...(img-hdtbzzhs-1653145947291)] 2. 编码升级 添加一个元素可能导致编码升级。编码升级需要做三件事\n\n * 扩展空间\n * 转变现有元素的类型并发至在合适的位置上。保证原有顺序防止\n * 防止新加入的元素。新加入的元素一定是最大或最小的,所以放在最前或者最后\n\n[外链图片转存中...(img-0r00i0bo-1653145947291)]\n\n\n# 压缩列表\n\n> 压缩列表是 redis 为了节约内存而开发的, 由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。 一个压缩列表可以包含任意多个节点(entry), 每个节点可以保存一个字节数组或者一个整数值\n\n 1. 源码\n\n/*\n* 保存 ziplist 节点信息的结构\n*/\ntypedef struct zlentry {\n\n // prevrawlen :前置节点的长度\n // prevrawlensize :编码 prevrawlen 所需的字节大小\n unsigned int prevrawlensize, prevrawlen;\n\n // len :当前节点值的长度\n // lensize :编码 len 所需的字节大小\n unsigned int lensize, len;\n\n // 当前节点 header 的大小\n // 等于 prevrawlensize + lensize\n unsigned int headersize;\n\n // 当前节点值所使用的编码类型\n unsigned char encoding;\n\n // 指向当前节点的指针\n unsigned char *p;\n\n} zlentry;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n\n 2. 示意图 [外链图片转存中...(img-nmdf8bnt-1653145947292)] [外链图片转存中...(img-5kdqas0e-1653145947292)]\n\n\n# redisobject\n\n> redis 使用对象来表示数据库中的键和值, 每次当我们在 redis 的数据库中新创建一个键值对时, 我们至少会创建两个对象, 一个对象用作键值对的键(键对象), 另一个对象用作键值对的值(值对象)。\n\n 1. 源码\n\ntypedef struct redisobject {\n\n // 类型\n unsigned type:4;\n\n // 编码\n unsigned encoding:4;\n\n // 对象最后一次被访问的时间\n unsigned lru:redis_lru_bits; /* lru time (relative to server.lruclock) */\n\n // 引用计数\n int refcount;\n\n // 指向实际值的指针\n void *ptr;\n\n} robj;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n\n\n[外链图片转存中...(img-ut4nw4ki-1653145947292)] 2. 类型和编码的对应关系 [外链图片转存中...(img-jkoornc4-1653145947293)] 3. 字符串对象\n\n值 编码\n可以用long保存的整形 int\n可以用long,double保存的浮点 embstr或row\n长度太长,不可以用long,double保存的浮点 embstr或row\n小于39字节的字符串 embstr\n大于39字节的字符串 row\n\n> row要两次申请内存,两次释放内存。为这俩对象redisobject、sdsstr。embstr申请一次释放一次,而且申请的是连续的内存空间,能更好的利用缓存。\n\n 4. 链表对象\n\n> inkedlist和ziplist的转换临界值:每个元素长度都小于64、元素个数小于512。满足以上两个条件才能用ziplist\n\n 5. hash对象\n\n> hashtable和ziplist的转换临界值:每个元素长度都小于64、元素个数小于512。满足以上两个条件才能用ziplist\n\n 6. 集合对象\n\n> hashtable和intset的转换临界值:每个元素都是int、元素个数小于512。满足以上两个条件才能用intset\n\n 7. 内存回收\n\n> redis是用c实现的,c没有自动回收内存的机制。redisobject中的refcount记录对象的引用个数,当refcount=0的时候自动释放内存。\n\n 8. 对象共享\n\n> 0- 9999这1w个整数是共享对象。字符串不做共享对象,因为对比匹配太复杂", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "为什么 Redis 在单线程下能如此快?", - "frontmatter": { - "title": "为什么 Redis 在单线程下能如此快?", - "date": "2022-05-21T17:20:12.000Z", - "permalink": "/pages/d66211/", - "categories": [ - "数据库", - "REDIS相关" - ], - "tags": [ - null - ] - }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/002.%E4%B8%BA%E4%BB%80%E4%B9%88%20Redis%20%E5%9C%A8%E5%8D%95%E7%BA%BF%E7%A8%8B%E4%B8%8B%E8%83%BD%E5%A6%82%E6%AD%A4%E5%BF%AB%EF%BC%9F.html", - "relativePath": "03.数据库/02.REDIS相关/002.为什么 Redis 在单线程下能如此快?.md", - "key": "v-cb742458", - "path": "/pages/d66211/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "简述 Redis 字符串的底层结构", - "frontmatter": { - "title": "简述 Redis 字符串的底层结构", - "date": "2022-05-21T17:20:49.000Z", - "permalink": "/pages/34aec2/", - "categories": [ - "数据库", - "REDIS相关" - ], - "tags": [ - null - ] + "normalizedTitle": "跳跃表", + "charIndex": 2338 + }, + { + "level": 2, + "title": "整数集合", + "slug": "整数集合", + "normalizedTitle": "整数集合", + "charIndex": 3010 + }, + { + "level": 2, + "title": "压缩列表", + "slug": "压缩列表", + "normalizedTitle": "压缩列表", + "charIndex": 3402 + }, + { + "level": 2, + "title": "RedisObject", + "slug": "redisobject", + "normalizedTitle": "redisobject", + "charIndex": 4119 + } + ], + "headersStr": "字符串 链表 hash 跳跃表 整数集合 压缩列表 RedisObject", + "content": "> 字符串(String)、列表(list)、字典(hash)、集合(set)、有序集合(sortSet).\n\n\n# 字符串\n\n 1. 源码\n\n/*\n- 保存字符串对象的结构\n*/\nstruct sdshdr {\n\n // buf 中已占用空间的长度\n int len;\n\n // buf 中剩余可用空间的长度\n int free;\n\n // 数据空间\n char buf[];\n};\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n\n 2. sds与c字符串的区别\n\n * sds获取字符串长度的复杂度为O(1),c的是O(n)\n * c的api是不安全的,可能造成缓冲区溢出,sds的是安全的。\n * 修改N次字符串,c需要N次内存分配。sds最多需要N次分配。sds有空间预分配和惰性回收。\n * sds可以使用部分c的字符串库。\n\n\n# 链表\n\n 1. 源码\n\n/*\n* 双端链表节点\n*/\ntypedef struct listNode {\n\n // 前置节点\n struct listNode *prev;\n\n // 后置节点\n struct listNode *next;\n\n // 节点的值\n void *value;\n\n} listNode;\n\n/*\n* 双端链表结构\n*/\ntypedef struct list {\n\n // 表头节点\n listNode *head;\n\n // 表尾节点\n listNode *tail;\n\n // 节点值复制函数\n void *(*dup)(void *ptr);\n\n // 节点值释放函数\n void (*free)(void *ptr);\n\n // 节点值对比函数\n int (*match)(void *ptr, void *key);\n\n // 链表所包含的节点数量\n unsigned long len;\n\n} list;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n\n\n[外链图片转存中...(img-ua4uHhOc-1653145947289)]\n\n\n# hash\n\n 1. 源码\n\n/*\n* 哈希表节点\n*/\ntypedef struct dictEntry {\n \n // 键\n void *key;\n\n // 值\n union {\n void *val;\n uint64_t u64;\n int64_t s64;\n } v;\n\n // 指向下个哈希表节点,形成链表\n struct dictEntry *next;\n\n} dictEntry;\n\n/*\n* 哈希表\n*\n* 每个字典都使用两个哈希表,从而实现渐进式 rehash 。\n*/\ntypedef struct dictht {\n \n // 哈希表数组\n dictEntry **table;\n\n // 哈希表大小\n unsigned long size;\n \n // 哈希表大小掩码,用于计算索引值\n // 总是等于 size - 1\n unsigned long sizemask;\n\n // 该哈希表已有节点的数量\n unsigned long used;\n\n} dictht;\n\n/*\n* 字典\n*/\ntypedef struct dict {\n\n // 类型特定函数\n dictType *type;\n\n // 私有数据\n void *privdata;\n\n // 哈希表\n dictht ht[2];\n\n // rehash 索引\n // 当 rehash 不在进行时,值为 -1\n int rehashidx; /* rehashing not in progress if rehashidx == -1 */\n\n // 目前正在运行的安全迭代器的数量\n int iterators; /* number of iterators currently running */\n\n} dict;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n57\n58\n59\n60\n61\n62\n63\n64\n\n\n[外链图片转存中...(img-3IEt2xy5-1653145947290)] 2. reHash\n\n> 扩容场景: 有BGSAVE或者BGREWRITEAOF时,负载因子超过5. 无BGSAVE或者BGREWRITEAOF时,负载因子超过1. 扩容为第一个大于等于ht[0].used*2的2的幂\n\n> 缩容场景: 负载因子小于0.1 缩容为第一个大于等于ht[0].used的2的幂\n\n> 负载因子:ht[0].used/ht[0].size\n\n> 渐进式rehash: rehashidx指向正在hash的索引。rehashidx=-1表示未进行rehash 每次访问dict结构时rehash一个索引\n\n[外链图片转存中...(img-A0e54HI9-1653145947290)]\n\n\n# 跳跃表\n\n 1. 源码\n\n/*\n* 跳跃表节点\n*/\ntypedef struct zskiplistNode {\n\n // 成员对象\n robj *obj;\n\n // 分值\n double score;\n\n // 后退指针\n struct zskiplistNode *backward;\n\n // 层\n struct zskiplistLevel {\n\n // 前进指针\n struct zskiplistNode *forward;\n\n // 跨度\n unsigned int span;\n\n } level[];\n\n} zskiplistNode;\n\n/*\n* 跳跃表\n*/\ntypedef struct zskiplist {\n\n // 表头节点和表尾节点\n struct zskiplistNode *header, *tail;\n\n // 表中节点的数量\n unsigned long length;\n\n // 表中层数最大的节点的层数\n int level;\n\n} zskiplist;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n\n\n[外链图片转存中...(img-MbZS62dp-1653145947291)]\n\n\n# 整数集合\n\n 1. 源码\n\ntypedef struct intset {\n \n // 编码方式\n uint32_t encoding;\n\n // 集合包含的元素数量\n uint32_t length;\n\n // 保存元素的数组\n int8_t contents[];\n\n} intset;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n[外链图片转存中...(img-hdTbZZhS-1653145947291)] 2. 编码升级 添加一个元素可能导致编码升级。编码升级需要做三件事\n\n * 扩展空间\n * 转变现有元素的类型并发至在合适的位置上。保证原有顺序防止\n * 防止新加入的元素。新加入的元素一定是最大或最小的,所以放在最前或者最后\n\n[外链图片转存中...(img-0R00I0BO-1653145947291)]\n\n\n# 压缩列表\n\n> 压缩列表是 Redis 为了节约内存而开发的, 由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。 一个压缩列表可以包含任意多个节点(entry), 每个节点可以保存一个字节数组或者一个整数值\n\n 1. 源码\n\n/*\n* 保存 ziplist 节点信息的结构\n*/\ntypedef struct zlentry {\n\n // prevrawlen :前置节点的长度\n // prevrawlensize :编码 prevrawlen 所需的字节大小\n unsigned int prevrawlensize, prevrawlen;\n\n // len :当前节点值的长度\n // lensize :编码 len 所需的字节大小\n unsigned int lensize, len;\n\n // 当前节点 header 的大小\n // 等于 prevrawlensize + lensize\n unsigned int headersize;\n\n // 当前节点值所使用的编码类型\n unsigned char encoding;\n\n // 指向当前节点的指针\n unsigned char *p;\n\n} zlentry;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n\n 2. 示意图 [外链图片转存中...(img-NMDF8Bnt-1653145947292)] [外链图片转存中...(img-5KdqaS0e-1653145947292)]\n\n\n# RedisObject\n\n> Redis 使用对象来表示数据库中的键和值, 每次当我们在 Redis 的数据库中新创建一个键值对时, 我们至少会创建两个对象, 一个对象用作键值对的键(键对象), 另一个对象用作键值对的值(值对象)。\n\n 1. 源码\n\ntypedef struct redisObject {\n\n // 类型\n unsigned type:4;\n\n // 编码\n unsigned encoding:4;\n\n // 对象最后一次被访问的时间\n unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */\n\n // 引用计数\n int refcount;\n\n // 指向实际值的指针\n void *ptr;\n\n} robj;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n\n\n[外链图片转存中...(img-uT4NW4Ki-1653145947292)] 2. 类型和编码的对应关系 [外链图片转存中...(img-jkooRnC4-1653145947293)] 3. 字符串对象\n\n值 编码\n可以用long保存的整形 int\n可以用long,double保存的浮点 embstr或row\n长度太长,不可以用long,double保存的浮点 embstr或row\n小于39字节的字符串 embstr\n大于39字节的字符串 row\n\n> row要两次申请内存,两次释放内存。为这俩对象RedisObject、SdsStr。embstr申请一次释放一次,而且申请的是连续的内存空间,能更好的利用缓存。\n\n 4. 链表对象\n\n> inkedList和zipList的转换临界值:每个元素长度都小于64、元素个数小于512。满足以上两个条件才能用zipList\n\n 5. hash对象\n\n> hashtable和zipList的转换临界值:每个元素长度都小于64、元素个数小于512。满足以上两个条件才能用zipList\n\n 6. 集合对象\n\n> hashtable和intset的转换临界值:每个元素都是int、元素个数小于512。满足以上两个条件才能用intset\n\n 7. 内存回收\n\n> redis是用C实现的,C没有自动回收内存的机制。RedisObject中的refCount记录对象的引用个数,当refCount=0的时候自动释放内存。\n\n 8. 对象共享\n\n> 0- 9999这1w个整数是共享对象。字符串不做共享对象,因为对比匹配太复杂", + "normalizedContent": "> 字符串(string)、列表(list)、字典(hash)、集合(set)、有序集合(sortset).\n\n\n# 字符串\n\n 1. 源码\n\n/*\n- 保存字符串对象的结构\n*/\nstruct sdshdr {\n\n // buf 中已占用空间的长度\n int len;\n\n // buf 中剩余可用空间的长度\n int free;\n\n // 数据空间\n char buf[];\n};\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n\n 2. sds与c字符串的区别\n\n * sds获取字符串长度的复杂度为o(1),c的是o(n)\n * c的api是不安全的,可能造成缓冲区溢出,sds的是安全的。\n * 修改n次字符串,c需要n次内存分配。sds最多需要n次分配。sds有空间预分配和惰性回收。\n * sds可以使用部分c的字符串库。\n\n\n# 链表\n\n 1. 源码\n\n/*\n* 双端链表节点\n*/\ntypedef struct listnode {\n\n // 前置节点\n struct listnode *prev;\n\n // 后置节点\n struct listnode *next;\n\n // 节点的值\n void *value;\n\n} listnode;\n\n/*\n* 双端链表结构\n*/\ntypedef struct list {\n\n // 表头节点\n listnode *head;\n\n // 表尾节点\n listnode *tail;\n\n // 节点值复制函数\n void *(*dup)(void *ptr);\n\n // 节点值释放函数\n void (*free)(void *ptr);\n\n // 节点值对比函数\n int (*match)(void *ptr, void *key);\n\n // 链表所包含的节点数量\n unsigned long len;\n\n} list;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n\n\n[外链图片转存中...(img-ua4uhhoc-1653145947289)]\n\n\n# hash\n\n 1. 源码\n\n/*\n* 哈希表节点\n*/\ntypedef struct dictentry {\n \n // 键\n void *key;\n\n // 值\n union {\n void *val;\n uint64_t u64;\n int64_t s64;\n } v;\n\n // 指向下个哈希表节点,形成链表\n struct dictentry *next;\n\n} dictentry;\n\n/*\n* 哈希表\n*\n* 每个字典都使用两个哈希表,从而实现渐进式 rehash 。\n*/\ntypedef struct dictht {\n \n // 哈希表数组\n dictentry **table;\n\n // 哈希表大小\n unsigned long size;\n \n // 哈希表大小掩码,用于计算索引值\n // 总是等于 size - 1\n unsigned long sizemask;\n\n // 该哈希表已有节点的数量\n unsigned long used;\n\n} dictht;\n\n/*\n* 字典\n*/\ntypedef struct dict {\n\n // 类型特定函数\n dicttype *type;\n\n // 私有数据\n void *privdata;\n\n // 哈希表\n dictht ht[2];\n\n // rehash 索引\n // 当 rehash 不在进行时,值为 -1\n int rehashidx; /* rehashing not in progress if rehashidx == -1 */\n\n // 目前正在运行的安全迭代器的数量\n int iterators; /* number of iterators currently running */\n\n} dict;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n57\n58\n59\n60\n61\n62\n63\n64\n\n\n[外链图片转存中...(img-3iet2xy5-1653145947290)] 2. rehash\n\n> 扩容场景: 有bgsave或者bgrewriteaof时,负载因子超过5. 无bgsave或者bgrewriteaof时,负载因子超过1. 扩容为第一个大于等于ht[0].used*2的2的幂\n\n> 缩容场景: 负载因子小于0.1 缩容为第一个大于等于ht[0].used的2的幂\n\n> 负载因子:ht[0].used/ht[0].size\n\n> 渐进式rehash: rehashidx指向正在hash的索引。rehashidx=-1表示未进行rehash 每次访问dict结构时rehash一个索引\n\n[外链图片转存中...(img-a0e54hi9-1653145947290)]\n\n\n# 跳跃表\n\n 1. 源码\n\n/*\n* 跳跃表节点\n*/\ntypedef struct zskiplistnode {\n\n // 成员对象\n robj *obj;\n\n // 分值\n double score;\n\n // 后退指针\n struct zskiplistnode *backward;\n\n // 层\n struct zskiplistlevel {\n\n // 前进指针\n struct zskiplistnode *forward;\n\n // 跨度\n unsigned int span;\n\n } level[];\n\n} zskiplistnode;\n\n/*\n* 跳跃表\n*/\ntypedef struct zskiplist {\n\n // 表头节点和表尾节点\n struct zskiplistnode *header, *tail;\n\n // 表中节点的数量\n unsigned long length;\n\n // 表中层数最大的节点的层数\n int level;\n\n} zskiplist;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n\n\n[外链图片转存中...(img-mbzs62dp-1653145947291)]\n\n\n# 整数集合\n\n 1. 源码\n\ntypedef struct intset {\n \n // 编码方式\n uint32_t encoding;\n\n // 集合包含的元素数量\n uint32_t length;\n\n // 保存元素的数组\n int8_t contents[];\n\n} intset;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n[外链图片转存中...(img-hdtbzzhs-1653145947291)] 2. 编码升级 添加一个元素可能导致编码升级。编码升级需要做三件事\n\n * 扩展空间\n * 转变现有元素的类型并发至在合适的位置上。保证原有顺序防止\n * 防止新加入的元素。新加入的元素一定是最大或最小的,所以放在最前或者最后\n\n[外链图片转存中...(img-0r00i0bo-1653145947291)]\n\n\n# 压缩列表\n\n> 压缩列表是 redis 为了节约内存而开发的, 由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。 一个压缩列表可以包含任意多个节点(entry), 每个节点可以保存一个字节数组或者一个整数值\n\n 1. 源码\n\n/*\n* 保存 ziplist 节点信息的结构\n*/\ntypedef struct zlentry {\n\n // prevrawlen :前置节点的长度\n // prevrawlensize :编码 prevrawlen 所需的字节大小\n unsigned int prevrawlensize, prevrawlen;\n\n // len :当前节点值的长度\n // lensize :编码 len 所需的字节大小\n unsigned int lensize, len;\n\n // 当前节点 header 的大小\n // 等于 prevrawlensize + lensize\n unsigned int headersize;\n\n // 当前节点值所使用的编码类型\n unsigned char encoding;\n\n // 指向当前节点的指针\n unsigned char *p;\n\n} zlentry;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n\n 2. 示意图 [外链图片转存中...(img-nmdf8bnt-1653145947292)] [外链图片转存中...(img-5kdqas0e-1653145947292)]\n\n\n# redisobject\n\n> redis 使用对象来表示数据库中的键和值, 每次当我们在 redis 的数据库中新创建一个键值对时, 我们至少会创建两个对象, 一个对象用作键值对的键(键对象), 另一个对象用作键值对的值(值对象)。\n\n 1. 源码\n\ntypedef struct redisobject {\n\n // 类型\n unsigned type:4;\n\n // 编码\n unsigned encoding:4;\n\n // 对象最后一次被访问的时间\n unsigned lru:redis_lru_bits; /* lru time (relative to server.lruclock) */\n\n // 引用计数\n int refcount;\n\n // 指向实际值的指针\n void *ptr;\n\n} robj;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n\n\n[外链图片转存中...(img-ut4nw4ki-1653145947292)] 2. 类型和编码的对应关系 [外链图片转存中...(img-jkoornc4-1653145947293)] 3. 字符串对象\n\n值 编码\n可以用long保存的整形 int\n可以用long,double保存的浮点 embstr或row\n长度太长,不可以用long,double保存的浮点 embstr或row\n小于39字节的字符串 embstr\n大于39字节的字符串 row\n\n> row要两次申请内存,两次释放内存。为这俩对象redisobject、sdsstr。embstr申请一次释放一次,而且申请的是连续的内存空间,能更好的利用缓存。\n\n 4. 链表对象\n\n> inkedlist和ziplist的转换临界值:每个元素长度都小于64、元素个数小于512。满足以上两个条件才能用ziplist\n\n 5. hash对象\n\n> hashtable和ziplist的转换临界值:每个元素长度都小于64、元素个数小于512。满足以上两个条件才能用ziplist\n\n 6. 集合对象\n\n> hashtable和intset的转换临界值:每个元素都是int、元素个数小于512。满足以上两个条件才能用intset\n\n 7. 内存回收\n\n> redis是用c实现的,c没有自动回收内存的机制。redisobject中的refcount记录对象的引用个数,当refcount=0的时候自动释放内存。\n\n 8. 对象共享\n\n> 0- 9999这1w个整数是共享对象。字符串不做共享对象,因为对比匹配太复杂", + "charsets": { + "cjk": true }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/005.%E7%AE%80%E8%BF%B0%20Redis%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84.html", - "relativePath": "03.数据库/02.REDIS相关/005.简述 Redis 字符串的底层结构.md", - "key": "v-3e917471", - "path": "/pages/34aec2/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, @@ -4672,11 +5021,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点", + "title": "简述 Redis 字符串的底层结构", "frontmatter": { - "title": "简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点", - "date": "2022-05-21T17:21:43.000Z", - "permalink": "/pages/7e708d/", + "title": "简述 Redis 字符串的底层结构", + "date": "2022-05-21T17:20:49.000Z", + "permalink": "/pages/34aec2/", "categories": [ "数据库", "REDIS相关" @@ -4685,32 +5034,39 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/020.%E7%AE%80%E8%BF%B0%20Redis%20%E6%8C%81%E4%B9%85%E5%8C%96%E4%B8%AD%20RDB%20%E4%BB%A5%E5%8F%8A%20AOF%20%E6%96%B9%E6%A1%88%E7%9A%84%E4%BC%98%E7%BC%BA%E7%82%B9.html", - "relativePath": "03.数据库/02.REDIS相关/020.简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点.md", - "key": "v-7c214d00", - "path": "/pages/7e708d/", - "headers": [ - { - "level": 2, - "title": "RDB持久化", - "slug": "rdb持久化", - "normalizedTitle": "rdb持久化", - "charIndex": 2 - }, - { - "level": 2, - "title": "AOF持久化", - "slug": "aof持久化", - "normalizedTitle": "aof持久化", - "charIndex": 1345 - } - ], - "headersStr": "RDB持久化 AOF持久化", - "content": "# RDB持久化\n\n 1. save和bgsave源码\n\nvoid saveCommand(redisClient *c) {\n\n // BGSAVE 已经在执行中,不能再执行 SAVE\n // 否则将产生竞争条件\n if (server.rdb_child_pid != -1) {\n addReplyError(c,\"Background save already in progress\");\n return;\n }\n\n // 执行 \n if (rdbSave(server.rdb_filename) == REDIS_OK) {\n addReply(c,shared.ok);\n } else {\n addReply(c,shared.err);\n }\n}\n\nvoid bgsaveCommand(redisClient *c) {\n\n // 不能重复执行 BGSAVE\n if (server.rdb_child_pid != -1) {\n addReplyError(c,\"Background save already in progress\");\n\n // 不能在 BGREWRITEAOF 正在运行时执行\n } else if (server.aof_child_pid != -1) {\n addReplyError(c,\"Can't BGSAVE while AOF log rewriting is in progress\");\n\n // 执行 BGSAVE\n } else if (rdbSaveBackground(server.rdb_filename) == REDIS_OK) {\n addReplyStatus(c,\"Background saving started\");\n\n } else {\n addReply(c,shared.err);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n\n 2. 自动间隔性保存\n\n> 服务器间隔性检车saveparams中的任意一个条件是否得到满足,如果得到满足,执行bgsave.saveparams[0]表示距离上次bgsave900s内,至少有一次数据库变化。\n\n[外链图片转存中...(img-SwAqAX8z-1653145947293)]\n\n 3. rdb文件结构 [外链图片转存中...(img-Prs54pGE-1653145947294)] 其中database结构如下: [外链图片转存中...(img-rn1gUv6w-1653145947294)] 其中ksy_value_pairs结构如下 [外链图片转存中...(img-B62S2gV5-1653145947295)]\n\n> type指定了value的类型。value的编码方式不同期存储结构也不同。\n\n\n# AOF持久化\n\n 1. aof的实现 命令追加:所有客户端命令都会被存到redis_server的aof_buf缓冲区。 文件写入:操作系统在写入文件的时候,先写入缓存,缓存满了才写入文件。 文件同步:flushAppendOnlyFile()负责将aof_buf写入文件aof。\n\n> flushAppendOnlyFile中配置的appendfsync决定同步策略。appendfsync有三个取值:always(每次写入都同步)、everysec(每次事件都写入但不同步,每秒同步)、 no(每次事件都写入但不同步,操作系统决定同步);\n\n 2. aof的载入与还原 [外链图片转存中...(img-pNMyXRLJ-1653145947295)]\n 3. AOF重写 [外链图片转存中...(img-i1nhwgNs-1653145947295)]", - "normalizedContent": "# rdb持久化\n\n 1. save和bgsave源码\n\nvoid savecommand(redisclient *c) {\n\n // bgsave 已经在执行中,不能再执行 save\n // 否则将产生竞争条件\n if (server.rdb_child_pid != -1) {\n addreplyerror(c,\"background save already in progress\");\n return;\n }\n\n // 执行 \n if (rdbsave(server.rdb_filename) == redis_ok) {\n addreply(c,shared.ok);\n } else {\n addreply(c,shared.err);\n }\n}\n\nvoid bgsavecommand(redisclient *c) {\n\n // 不能重复执行 bgsave\n if (server.rdb_child_pid != -1) {\n addreplyerror(c,\"background save already in progress\");\n\n // 不能在 bgrewriteaof 正在运行时执行\n } else if (server.aof_child_pid != -1) {\n addreplyerror(c,\"can't bgsave while aof log rewriting is in progress\");\n\n // 执行 bgsave\n } else if (rdbsavebackground(server.rdb_filename) == redis_ok) {\n addreplystatus(c,\"background saving started\");\n\n } else {\n addreply(c,shared.err);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n\n 2. 自动间隔性保存\n\n> 服务器间隔性检车saveparams中的任意一个条件是否得到满足,如果得到满足,执行bgsave.saveparams[0]表示距离上次bgsave900s内,至少有一次数据库变化。\n\n[外链图片转存中...(img-swaqax8z-1653145947293)]\n\n 3. rdb文件结构 [外链图片转存中...(img-prs54pge-1653145947294)] 其中database结构如下: [外链图片转存中...(img-rn1guv6w-1653145947294)] 其中ksy_value_pairs结构如下 [外链图片转存中...(img-b62s2gv5-1653145947295)]\n\n> type指定了value的类型。value的编码方式不同期存储结构也不同。\n\n\n# aof持久化\n\n 1. aof的实现 命令追加:所有客户端命令都会被存到redis_server的aof_buf缓冲区。 文件写入:操作系统在写入文件的时候,先写入缓存,缓存满了才写入文件。 文件同步:flushappendonlyfile()负责将aof_buf写入文件aof。\n\n> flushappendonlyfile中配置的appendfsync决定同步策略。appendfsync有三个取值:always(每次写入都同步)、everysec(每次事件都写入但不同步,每秒同步)、 no(每次事件都写入但不同步,操作系统决定同步);\n\n 2. aof的载入与还原 [外链图片转存中...(img-pnmyxrlj-1653145947295)]\n 3. aof重写 [外链图片转存中...(img-i1nhwgns-1653145947295)]", - "charsets": { - "cjk": true + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/005.%E7%AE%80%E8%BF%B0%20Redis%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84.html", + "relativePath": "03.数据库/02.REDIS相关/005.简述 Redis 字符串的底层结构.md", + "key": "v-3e917471", + "path": "/pages/34aec2/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "Redis 如何实现分布式锁?", + "frontmatter": { + "title": "Redis 如何实现分布式锁?", + "date": "2022-05-21T17:21:32.000Z", + "permalink": "/pages/656f46/", + "categories": [ + "数据库", + "REDIS相关" + ], + "tags": [ + null + ] }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/030.Redis%20%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%EF%BC%9F.html", + "relativePath": "03.数据库/02.REDIS相关/030.Redis 如何实现分布式锁?.md", + "key": "v-2c6b511c", + "path": "/pages/656f46/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, @@ -4751,11 +5107,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "Redis 如何实现分布式锁?", + "title": "简述 Redis 中跳表的应用以及优缺点", "frontmatter": { - "title": "Redis 如何实现分布式锁?", - "date": "2022-05-21T17:21:32.000Z", - "permalink": "/pages/656f46/", + "title": "简述 Redis 中跳表的应用以及优缺点", + "date": "2022-05-21T17:22:09.000Z", + "permalink": "/pages/a42826/", "categories": [ "数据库", "REDIS相关" @@ -4764,10 +5120,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/030.Redis%20%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%EF%BC%9F.html", - "relativePath": "03.数据库/02.REDIS相关/030.Redis 如何实现分布式锁?.md", - "key": "v-2c6b511c", - "path": "/pages/656f46/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/050.%E7%AE%80%E8%BF%B0%20Redis%20%E4%B8%AD%E8%B7%B3%E8%A1%A8%E7%9A%84%E5%BA%94%E7%94%A8%E4%BB%A5%E5%8F%8A%E4%BC%98%E7%BC%BA%E7%82%B9.html", + "relativePath": "03.数据库/02.REDIS相关/050.简述 Redis 中跳表的应用以及优缺点.md", + "key": "v-18b427a0", + "path": "/pages/a42826/", "headersStr": null, "content": "", "normalizedContent": "", @@ -4776,11 +5132,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述 Redis 中跳表的应用以及优缺点", + "title": "Redis 中,sentinel 和 cluster 的区别和适用场景是什么?", "frontmatter": { - "title": "简述 Redis 中跳表的应用以及优缺点", - "date": "2022-05-21T17:22:09.000Z", - "permalink": "/pages/a42826/", + "title": "Redis 中,sentinel 和 cluster 的区别和适用场景是什么?", + "date": "2022-05-21T17:22:27.000Z", + "permalink": "/pages/e2e910/", "categories": [ "数据库", "REDIS相关" @@ -4789,10 +5145,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/050.%E7%AE%80%E8%BF%B0%20Redis%20%E4%B8%AD%E8%B7%B3%E8%A1%A8%E7%9A%84%E5%BA%94%E7%94%A8%E4%BB%A5%E5%8F%8A%E4%BC%98%E7%BC%BA%E7%82%B9.html", - "relativePath": "03.数据库/02.REDIS相关/050.简述 Redis 中跳表的应用以及优缺点.md", - "key": "v-18b427a0", - "path": "/pages/a42826/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/060.Redis%20%E4%B8%AD%EF%BC%8Csentinel%20%E5%92%8C%20cluster%20%E7%9A%84%E5%8C%BA%E5%88%AB%E5%92%8C%E9%80%82%E7%94%A8%E5%9C%BA%E6%99%AF%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", + "relativePath": "03.数据库/02.REDIS相关/060.Redis 中,sentinel 和 cluster 的区别和适用场景是什么?.md", + "key": "v-6a45f68d", + "path": "/pages/e2e910/", "headersStr": null, "content": "", "normalizedContent": "", @@ -4801,11 +5157,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "Redis 中,sentinel 和 cluster 的区别和适用场景是什么?", + "title": "简述 Redis 中如何防止缓存雪崩和缓存击穿", "frontmatter": { - "title": "Redis 中,sentinel 和 cluster 的区别和适用场景是什么?", - "date": "2022-05-21T17:22:27.000Z", - "permalink": "/pages/e2e910/", + "title": "简述 Redis 中如何防止缓存雪崩和缓存击穿", + "date": "2022-05-21T17:22:52.000Z", + "permalink": "/pages/03cd25/", "categories": [ "数据库", "REDIS相关" @@ -4814,10 +5170,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/060.Redis%20%E4%B8%AD%EF%BC%8Csentinel%20%E5%92%8C%20cluster%20%E7%9A%84%E5%8C%BA%E5%88%AB%E5%92%8C%E9%80%82%E7%94%A8%E5%9C%BA%E6%99%AF%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", - "relativePath": "03.数据库/02.REDIS相关/060.Redis 中,sentinel 和 cluster 的区别和适用场景是什么?.md", - "key": "v-6a45f68d", - "path": "/pages/e2e910/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/070.%E7%AE%80%E8%BF%B0%20Redis%20%E4%B8%AD%E5%A6%82%E4%BD%95%E9%98%B2%E6%AD%A2%E7%BC%93%E5%AD%98%E9%9B%AA%E5%B4%A9%E5%92%8C%E7%BC%93%E5%AD%98%E5%87%BB%E7%A9%BF.html", + "relativePath": "03.数据库/02.REDIS相关/070.简述 Redis 中如何防止缓存雪崩和缓存击穿.md", + "key": "v-c2fe5e1a", + "path": "/pages/03cd25/", "headersStr": null, "content": "", "normalizedContent": "", @@ -4851,11 +5207,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述 Redis 中如何防止缓存雪崩和缓存击穿", + "title": "简述 Redis 如何处理热点 key 访问", "frontmatter": { - "title": "简述 Redis 中如何防止缓存雪崩和缓存击穿", - "date": "2022-05-21T17:22:52.000Z", - "permalink": "/pages/03cd25/", + "title": "简述 Redis 如何处理热点 key 访问", + "date": "2022-05-21T17:23:43.000Z", + "permalink": "/pages/795c09/", "categories": [ "数据库", "REDIS相关" @@ -4864,10 +5220,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/070.%E7%AE%80%E8%BF%B0%20Redis%20%E4%B8%AD%E5%A6%82%E4%BD%95%E9%98%B2%E6%AD%A2%E7%BC%93%E5%AD%98%E9%9B%AA%E5%B4%A9%E5%92%8C%E7%BC%93%E5%AD%98%E5%87%BB%E7%A9%BF.html", - "relativePath": "03.数据库/02.REDIS相关/070.简述 Redis 中如何防止缓存雪崩和缓存击穿.md", - "key": "v-c2fe5e1a", - "path": "/pages/03cd25/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/110.%E7%AE%80%E8%BF%B0%20Redis%20%E5%A6%82%E4%BD%95%E5%A4%84%E7%90%86%E7%83%AD%E7%82%B9%20key%20%E8%AE%BF%E9%97%AE.html", + "relativePath": "03.数据库/02.REDIS相关/110.简述 Redis 如何处理热点 key 访问.md", + "key": "v-37f158ba", + "path": "/pages/795c09/", "headersStr": null, "content": "", "normalizedContent": "", @@ -4896,46 +5252,128 @@ export const siteData = { "headers": [ { "level": 2, - "title": "sentinel", - "slug": "sentinel", - "normalizedTitle": "sentinel", - "charIndex": 2 + "title": "sentinel", + "slug": "sentinel", + "normalizedTitle": "sentinel", + "charIndex": 2 + } + ], + "headersStr": "sentinel", + "content": "# sentinel\n\n> 负责redis高可用,检测主服务器下线,然后选举一个从服务器成为新的主服务器。\n\n 1. 源码\n\n// Sentinel 会为每个被监视的 Redis 实例创建相应的 sentinelRedisInstance 实例\n// (被监视的实例可以是主服务器、从服务器、或者其他 Sentinel )\ntypedef struct sentinelRedisInstance {\n \n // 标识值,记录了实例的类型,以及该实例的当前状态\n int flags; /* See SRI_... defines */\n \n // 实例的名字\n // 主服务器的名字由用户在配置文件中设置\n // 从服务器以及 Sentinel 的名字由 Sentinel 自动设置\n // 格式为 ip:port ,例如 \"127.0.0.1:26379\"\n char *name; /* Master name from the point of view of this sentinel. */\n\n // 实例的运行 ID\n char *runid; /* run ID of this instance. */\n\n // 配置纪元,用于实现故障转移\n uint64_t config_epoch; /* Configuration epoch. */\n\n // 实例的地址\n sentinelAddr *addr; /* Master host. */\n\n // 用于发送命令的异步连接\n redisAsyncContext *cc; /* Hiredis context for commands. */\n\n // 用于执行 SUBSCRIBE 命令、接收频道信息的异步连接\n // 仅在实例为主服务器时使用\n redisAsyncContext *pc; /* Hiredis context for Pub / Sub. */\n\n // 已发送但尚未回复的命令数量\n int pending_commands; /* Number of commands sent waiting for a reply. */\n\n // cc 连接的创建时间\n mstime_t cc_conn_time; /* cc connection time. */\n \n // pc 连接的创建时间\n mstime_t pc_conn_time; /* pc connection time. */\n\n // 最后一次从这个实例接收信息的时间\n mstime_t pc_last_activity; /* Last time we received any message. */\n\n // 实例最后一次返回正确的 PING 命令回复的时间\n mstime_t last_avail_time; /* Last time the instance replied to ping with\n a reply we consider valid. */\n // 实例最后一次发送 PING 命令的时间\n mstime_t last_ping_time; /* Last time a pending ping was sent in the\n context of the current command connection\n with the instance. 0 if still not sent or\n if pong already received. */\n // 实例最后一次返回 PING 命令的时间,无论内容正确与否\n mstime_t last_pong_time; /* Last time the instance replied to ping,\n whatever the reply was. That's used to check\n if the link is idle and must be reconnected. */\n\n // 最后一次向频道发送问候信息的时间\n // 只在当前实例为 sentinel 时使用\n mstime_t last_pub_time; /* Last time we sent hello via Pub/Sub. */\n\n // 最后一次接收到这个 sentinel 发来的问候信息的时间\n // 只在当前实例为 sentinel 时使用\n mstime_t last_hello_time; /* Only used if SRI_SENTINEL is set. Last time\n we received a hello from this Sentinel\n via Pub/Sub. */\n\n // 最后一次回复 SENTINEL is-master-down-by-addr 命令的时间\n // 只在当前实例为 sentinel 时使用\n mstime_t last_master_down_reply_time; /* Time of last reply to\n SENTINEL is-master-down command. */\n\n // 实例被判断为 SDOWN 状态的时间\n mstime_t s_down_since_time; /* Subjectively down since time. */\n\n // 实例被判断为 ODOWN 状态的时间\n mstime_t o_down_since_time; /* Objectively down since time. */\n\n // SENTINEL down-after-milliseconds 选项所设定的值\n // 实例无响应多少毫秒之后才会被判断为主观下线(subjectively down)\n mstime_t down_after_period; /* Consider it down after that period. */\n\n // 从实例获取 INFO 命令的回复的时间\n mstime_t info_refresh; /* Time at which we received INFO output from it. */\n\n /* Role and the first time we observed it.\n * This is useful in order to delay replacing what the instance reports\n * with our own configuration. We need to always wait some time in order\n * to give a chance to the leader to report the new configuration before\n * we do silly things. */\n // 实例的角色\n int role_reported;\n // 角色的更新时间\n mstime_t role_reported_time;\n\n // 最后一次从服务器的主服务器地址变更的时间\n mstime_t slave_conf_change_time; /* Last time slave master addr changed. */\n\n /* Master specific. */\n /* 主服务器实例特有的属性 -------------------------------------------------------------*/\n\n // 其他同样监控这个主服务器的所有 sentinel\n dict *sentinels; /* Other sentinels monitoring the same master. */\n\n // 如果这个实例代表的是一个主服务器\n // 那么这个字典保存着主服务器属下的从服务器\n // 字典的键是从服务器的名字,字典的值是从服务器对应的 sentinelRedisInstance 结构\n dict *slaves; /* Slaves for this master instance. */\n\n // SENTINEL monitor 选项中的 quorum 参数\n // 判断这个实例为客观下线(objectively down)所需的支持投票数量\n int quorum; /* Number of sentinels that need to agree on failure. */\n\n // SENTINEL parallel-syncs 选项的值\n // 在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量\n int parallel_syncs; /* How many slaves to reconfigure at same time. */\n\n // 连接主服务器和从服务器所需的密码\n char *auth_pass; /* Password to use for AUTH against master & slaves. */\n\n /* Slave specific. */\n /* 从服务器实例特有的属性 -------------------------------------------------------------*/\n\n // 主从服务器连接断开的时间\n mstime_t master_link_down_time; /* Slave replication link down time. */\n\n // 从服务器优先级\n int slave_priority; /* Slave priority according to its INFO output. */\n\n // 执行故障转移操作时,从服务器发送 SLAVEOF 命令的时间\n mstime_t slave_reconf_sent_time; /* Time at which we sent SLAVE OF */\n\n // 主服务器的实例(在本实例为从服务器时使用)\n struct sentinelRedisInstance *master; /* Master instance if it's slave. */\n\n // INFO 命令的回复中记录的主服务器 IP\n char *slave_master_host; /* Master host as reported by INFO */\n \n // INFO 命令的回复中记录的主服务器端口号\n int slave_master_port; /* Master port as reported by INFO */\n\n // INFO 命令的回复中记录的主从服务器连接状态\n int slave_master_link_status; /* Master link status as reported by INFO */\n\n // 从服务器的复制偏移量\n unsigned long long slave_repl_offset; /* Slave replication offset. */\n\n /* Failover */\n /* 故障转移相关属性 -------------------------------------------------------------------*/\n\n\n // 如果这是一个主服务器实例,那么 leader 将是负责进行故障转移的 Sentinel 的运行 ID 。\n // 如果这是一个 Sentinel 实例,那么 leader 就是被选举出来的领头 Sentinel 。\n // 这个域只在 Sentinel 实例的 flags 属性的 SRI_MASTER_DOWN 标志处于打开状态时才有效。\n char *leader; /* If this is a master instance, this is the runid of\n the Sentinel that should perform the failover. If\n this is a Sentinel, this is the runid of the Sentinel\n that this Sentinel voted as leader. */\n // 领头的纪元\n uint64_t leader_epoch; /* Epoch of the 'leader' field. */\n // 当前执行中的故障转移的纪元\n uint64_t failover_epoch; /* Epoch of the currently started failover. */\n // 故障转移操作的当前状态\n int failover_state; /* See SENTINEL_FAILOVER_STATE_* defines. */\n\n // 状态改变的时间\n mstime_t failover_state_change_time;\n\n // 最后一次进行故障迁移的时间\n mstime_t failover_start_time; /* Last failover attempt start time. */\n\n // SENTINEL failover-timeout 选项的值\n // 刷新故障迁移状态的最大时限\n mstime_t failover_timeout; /* Max time to refresh failover state. */\n\n mstime_t failover_delay_logged; /* For what failover_start_time value we\n logged the failover delay. */\n // 指向被提升为新主服务器的从服务器的指针\n struct sentinelRedisInstance *promoted_slave; /* Promoted slave instance. */\n\n /* Scripts executed to notify admin or reconfigure clients: when they\n * are set to NULL no script is executed. */\n // 一个文件路径,保存着 WARNING 级别的事件发生时执行的,\n // 用于通知管理员的脚本的地址\n char *notification_script;\n\n // 一个文件路径,保存着故障转移执行之前、之后、或者被中止时,\n // 需要执行的脚本的地址\n char *client_reconfig_script;\n\n} sentinelRedisInstance;\n\n/* Main state. */\n/* Sentinel 的状态结构 */\nstruct sentinelState {\n\n // 当前纪元\n uint64_t current_epoch; /* Current epoch. */\n\n // 保存了所有被这个 sentinel 监视的主服务器\n // 字典的键是主服务器的名字\n // 字典的值则是一个指向 sentinelRedisInstance 结构的指针\n dict *masters; /* Dictionary of master sentinelRedisInstances.\n Key is the instance name, value is the\n sentinelRedisInstance structure pointer. */\n\n // 是否进入了 TILT 模式?\n int tilt; /* Are we in TILT mode? */\n\n // 目前正在执行的脚本的数量\n int running_scripts; /* Number of scripts in execution right now. */\n\n // 进入 TILT 模式的时间\n mstime_t tilt_start_time; /* When TITL started. */\n\n // 最后一次执行时间处理器的时间\n mstime_t previous_time; /* Last time we ran the time handler. */\n\n // 一个 FIFO 队列,包含了所有需要执行的用户脚本\n list *scripts_queue; /* Queue of user scripts to execute. */\n\n} sentinel;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n57\n58\n59\n60\n61\n62\n63\n64\n65\n66\n67\n68\n69\n70\n71\n72\n73\n74\n75\n76\n77\n78\n79\n80\n81\n82\n83\n84\n85\n86\n87\n88\n89\n90\n91\n92\n93\n94\n95\n96\n97\n98\n99\n100\n101\n102\n103\n104\n105\n106\n107\n108\n109\n110\n111\n112\n113\n114\n115\n116\n117\n118\n119\n120\n121\n122\n123\n124\n125\n126\n127\n128\n129\n130\n131\n132\n133\n134\n135\n136\n137\n138\n139\n140\n141\n142\n143\n144\n145\n146\n147\n148\n149\n150\n151\n152\n153\n154\n155\n156\n157\n158\n159\n160\n161\n162\n163\n164\n165\n166\n167\n168\n169\n170\n171\n172\n173\n174\n175\n176\n177\n178\n179\n180\n181\n182\n183\n184\n185\n186\n187\n188\n189\n190\n191\n192\n193\n194\n195\n196\n197\n198\n199\n200\n201\n202\n203\n204\n205\n206\n207\n208\n209\n210\n211\n212\n213\n214\n215\n216\n217\n218\n219\n\n 2. 启动并初始化sentinel\n\n * 启动命令:Redis-sentinel /path/sentinel.conf\n * 初始化服务器\n * 使用sentinel专用代码\n * 初始化sentinel状态。sentinelState{}\n * 初始化masters属性。根据配置文件来生成。 [外链图片转存中...(img-gEx8X9Le-1653145947298)]\n * 创建两个链接主服务器的异步网络:一个命令链接;一个订阅链接。\n * 获取服务器信息。10s/次向主服务器发送info命令。获取主从信息。更新主从信息。从信息的保存结构如下: [外链图片转存中...(img-aBn6piJg-1653145947298)]\n * 获取从服务器信息。获取从服务器信息。sentinel发现主服务器有从服务器时,保存从服务器并创建两个异步链接:命令链接和订阅链接。10s/次向从服务器发送命令获取信息更新本地存储。\n * 向主从发送信息。2s/次通过命令连接发送信息。发送频道_sentinel_:hello\n * 接收信息。接收频道 sentinel:hello(用于发现新的sentinel)\n * 更新sentinel字典。sentinelResidInstance结构中的sentinels保存这个主服务器的所有sentinel。接收到_sentinel_:hello频道的消息时,更新sentinel.\n * 创建sentinel之间的连接。(不创建订阅链接)\n\n 3. 检测主观下线状态\n\n> sentinel每秒向它连接的实例发送ping命令。返回+pong、-loading、-masterdown是有效回复。其他回复或者指定时间没回复则认为无效。配置项down-after-milliseconds指定了sentinel判断主观下线时间长度。如果一个实例在down-after-milliseconds时间内连续向sentinel返回无效,则认为主观下线。\n\n * sentinelRedisInstance中的master属性有主观下线标识。\n * down-after-milliseconds是master下线的标准,也是master下slaves下线的标准,也是其他sentinel判断master下线的标准。\n * 多个sentinel设置同一个msater的down-after-milliseconds可能不同\n\n 4. 客观下线\n\n> sentinel判断一个master已经主观下线,会询问其他sentinel,如果一定数量 的都认为这个master已经主观下线,则判定这个master客观下线。然后做故障转移。\n\n * 发送sentinel is-master-down-by-addr命令询问其他sentinel\n * sentinel配置中的quorum参数决定有几个sentinel判断下线才能算是下线\n * 当一个msater被判定为客观下线时,监视这个master的各个sentinel之间会选举一个为领头sentinel,做故障转移。\n * 故障转移:选一个先master、slaves同步新的master、旧的master成为新master的slave\n\n 5. 选举领头sentinel\n\n> 当一个主服务器下线后,在监管这个主服务器的sentinel中选举一个领头sentinel。用于故障转移\n\n[外链图片转存中...(img-5XFvVtzH-1653145947298)] 6. 故障转移 领头sentinel完成故障转移需要三个步骤:\n\n * 在从服务器中选一个成为新的主服务器。sentinel在从服务器中选举一个,向其发送命令slaveof no one,使之成为主服务器。发送slaveof no one明后后每秒发送info命令观察role是不是变成了master。\n * 让从服务器同步新的主服务器. [外链图片转存中...(img-jz4GYou9-1653145947299)]\n * 让旧的主服务器成为新的主服务器的从服务器,当其上线时,成为从服务器。", + "normalizedContent": "# sentinel\n\n> 负责redis高可用,检测主服务器下线,然后选举一个从服务器成为新的主服务器。\n\n 1. 源码\n\n// sentinel 会为每个被监视的 redis 实例创建相应的 sentinelredisinstance 实例\n// (被监视的实例可以是主服务器、从服务器、或者其他 sentinel )\ntypedef struct sentinelredisinstance {\n \n // 标识值,记录了实例的类型,以及该实例的当前状态\n int flags; /* see sri_... defines */\n \n // 实例的名字\n // 主服务器的名字由用户在配置文件中设置\n // 从服务器以及 sentinel 的名字由 sentinel 自动设置\n // 格式为 ip:port ,例如 \"127.0.0.1:26379\"\n char *name; /* master name from the point of view of this sentinel. */\n\n // 实例的运行 id\n char *runid; /* run id of this instance. */\n\n // 配置纪元,用于实现故障转移\n uint64_t config_epoch; /* configuration epoch. */\n\n // 实例的地址\n sentineladdr *addr; /* master host. */\n\n // 用于发送命令的异步连接\n redisasynccontext *cc; /* hiredis context for commands. */\n\n // 用于执行 subscribe 命令、接收频道信息的异步连接\n // 仅在实例为主服务器时使用\n redisasynccontext *pc; /* hiredis context for pub / sub. */\n\n // 已发送但尚未回复的命令数量\n int pending_commands; /* number of commands sent waiting for a reply. */\n\n // cc 连接的创建时间\n mstime_t cc_conn_time; /* cc connection time. */\n \n // pc 连接的创建时间\n mstime_t pc_conn_time; /* pc connection time. */\n\n // 最后一次从这个实例接收信息的时间\n mstime_t pc_last_activity; /* last time we received any message. */\n\n // 实例最后一次返回正确的 ping 命令回复的时间\n mstime_t last_avail_time; /* last time the instance replied to ping with\n a reply we consider valid. */\n // 实例最后一次发送 ping 命令的时间\n mstime_t last_ping_time; /* last time a pending ping was sent in the\n context of the current command connection\n with the instance. 0 if still not sent or\n if pong already received. */\n // 实例最后一次返回 ping 命令的时间,无论内容正确与否\n mstime_t last_pong_time; /* last time the instance replied to ping,\n whatever the reply was. that's used to check\n if the link is idle and must be reconnected. */\n\n // 最后一次向频道发送问候信息的时间\n // 只在当前实例为 sentinel 时使用\n mstime_t last_pub_time; /* last time we sent hello via pub/sub. */\n\n // 最后一次接收到这个 sentinel 发来的问候信息的时间\n // 只在当前实例为 sentinel 时使用\n mstime_t last_hello_time; /* only used if sri_sentinel is set. last time\n we received a hello from this sentinel\n via pub/sub. */\n\n // 最后一次回复 sentinel is-master-down-by-addr 命令的时间\n // 只在当前实例为 sentinel 时使用\n mstime_t last_master_down_reply_time; /* time of last reply to\n sentinel is-master-down command. */\n\n // 实例被判断为 sdown 状态的时间\n mstime_t s_down_since_time; /* subjectively down since time. */\n\n // 实例被判断为 odown 状态的时间\n mstime_t o_down_since_time; /* objectively down since time. */\n\n // sentinel down-after-milliseconds 选项所设定的值\n // 实例无响应多少毫秒之后才会被判断为主观下线(subjectively down)\n mstime_t down_after_period; /* consider it down after that period. */\n\n // 从实例获取 info 命令的回复的时间\n mstime_t info_refresh; /* time at which we received info output from it. */\n\n /* role and the first time we observed it.\n * this is useful in order to delay replacing what the instance reports\n * with our own configuration. we need to always wait some time in order\n * to give a chance to the leader to report the new configuration before\n * we do silly things. */\n // 实例的角色\n int role_reported;\n // 角色的更新时间\n mstime_t role_reported_time;\n\n // 最后一次从服务器的主服务器地址变更的时间\n mstime_t slave_conf_change_time; /* last time slave master addr changed. */\n\n /* master specific. */\n /* 主服务器实例特有的属性 -------------------------------------------------------------*/\n\n // 其他同样监控这个主服务器的所有 sentinel\n dict *sentinels; /* other sentinels monitoring the same master. */\n\n // 如果这个实例代表的是一个主服务器\n // 那么这个字典保存着主服务器属下的从服务器\n // 字典的键是从服务器的名字,字典的值是从服务器对应的 sentinelredisinstance 结构\n dict *slaves; /* slaves for this master instance. */\n\n // sentinel monitor 选项中的 quorum 参数\n // 判断这个实例为客观下线(objectively down)所需的支持投票数量\n int quorum; /* number of sentinels that need to agree on failure. */\n\n // sentinel parallel-syncs 选项的值\n // 在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量\n int parallel_syncs; /* how many slaves to reconfigure at same time. */\n\n // 连接主服务器和从服务器所需的密码\n char *auth_pass; /* password to use for auth against master & slaves. */\n\n /* slave specific. */\n /* 从服务器实例特有的属性 -------------------------------------------------------------*/\n\n // 主从服务器连接断开的时间\n mstime_t master_link_down_time; /* slave replication link down time. */\n\n // 从服务器优先级\n int slave_priority; /* slave priority according to its info output. */\n\n // 执行故障转移操作时,从服务器发送 slaveof 命令的时间\n mstime_t slave_reconf_sent_time; /* time at which we sent slave of */\n\n // 主服务器的实例(在本实例为从服务器时使用)\n struct sentinelredisinstance *master; /* master instance if it's slave. */\n\n // info 命令的回复中记录的主服务器 ip\n char *slave_master_host; /* master host as reported by info */\n \n // info 命令的回复中记录的主服务器端口号\n int slave_master_port; /* master port as reported by info */\n\n // info 命令的回复中记录的主从服务器连接状态\n int slave_master_link_status; /* master link status as reported by info */\n\n // 从服务器的复制偏移量\n unsigned long long slave_repl_offset; /* slave replication offset. */\n\n /* failover */\n /* 故障转移相关属性 -------------------------------------------------------------------*/\n\n\n // 如果这是一个主服务器实例,那么 leader 将是负责进行故障转移的 sentinel 的运行 id 。\n // 如果这是一个 sentinel 实例,那么 leader 就是被选举出来的领头 sentinel 。\n // 这个域只在 sentinel 实例的 flags 属性的 sri_master_down 标志处于打开状态时才有效。\n char *leader; /* if this is a master instance, this is the runid of\n the sentinel that should perform the failover. if\n this is a sentinel, this is the runid of the sentinel\n that this sentinel voted as leader. */\n // 领头的纪元\n uint64_t leader_epoch; /* epoch of the 'leader' field. */\n // 当前执行中的故障转移的纪元\n uint64_t failover_epoch; /* epoch of the currently started failover. */\n // 故障转移操作的当前状态\n int failover_state; /* see sentinel_failover_state_* defines. */\n\n // 状态改变的时间\n mstime_t failover_state_change_time;\n\n // 最后一次进行故障迁移的时间\n mstime_t failover_start_time; /* last failover attempt start time. */\n\n // sentinel failover-timeout 选项的值\n // 刷新故障迁移状态的最大时限\n mstime_t failover_timeout; /* max time to refresh failover state. */\n\n mstime_t failover_delay_logged; /* for what failover_start_time value we\n logged the failover delay. */\n // 指向被提升为新主服务器的从服务器的指针\n struct sentinelredisinstance *promoted_slave; /* promoted slave instance. */\n\n /* scripts executed to notify admin or reconfigure clients: when they\n * are set to null no script is executed. */\n // 一个文件路径,保存着 warning 级别的事件发生时执行的,\n // 用于通知管理员的脚本的地址\n char *notification_script;\n\n // 一个文件路径,保存着故障转移执行之前、之后、或者被中止时,\n // 需要执行的脚本的地址\n char *client_reconfig_script;\n\n} sentinelredisinstance;\n\n/* main state. */\n/* sentinel 的状态结构 */\nstruct sentinelstate {\n\n // 当前纪元\n uint64_t current_epoch; /* current epoch. */\n\n // 保存了所有被这个 sentinel 监视的主服务器\n // 字典的键是主服务器的名字\n // 字典的值则是一个指向 sentinelredisinstance 结构的指针\n dict *masters; /* dictionary of master sentinelredisinstances.\n key is the instance name, value is the\n sentinelredisinstance structure pointer. */\n\n // 是否进入了 tilt 模式?\n int tilt; /* are we in tilt mode? */\n\n // 目前正在执行的脚本的数量\n int running_scripts; /* number of scripts in execution right now. */\n\n // 进入 tilt 模式的时间\n mstime_t tilt_start_time; /* when titl started. */\n\n // 最后一次执行时间处理器的时间\n mstime_t previous_time; /* last time we ran the time handler. */\n\n // 一个 fifo 队列,包含了所有需要执行的用户脚本\n list *scripts_queue; /* queue of user scripts to execute. */\n\n} sentinel;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n57\n58\n59\n60\n61\n62\n63\n64\n65\n66\n67\n68\n69\n70\n71\n72\n73\n74\n75\n76\n77\n78\n79\n80\n81\n82\n83\n84\n85\n86\n87\n88\n89\n90\n91\n92\n93\n94\n95\n96\n97\n98\n99\n100\n101\n102\n103\n104\n105\n106\n107\n108\n109\n110\n111\n112\n113\n114\n115\n116\n117\n118\n119\n120\n121\n122\n123\n124\n125\n126\n127\n128\n129\n130\n131\n132\n133\n134\n135\n136\n137\n138\n139\n140\n141\n142\n143\n144\n145\n146\n147\n148\n149\n150\n151\n152\n153\n154\n155\n156\n157\n158\n159\n160\n161\n162\n163\n164\n165\n166\n167\n168\n169\n170\n171\n172\n173\n174\n175\n176\n177\n178\n179\n180\n181\n182\n183\n184\n185\n186\n187\n188\n189\n190\n191\n192\n193\n194\n195\n196\n197\n198\n199\n200\n201\n202\n203\n204\n205\n206\n207\n208\n209\n210\n211\n212\n213\n214\n215\n216\n217\n218\n219\n\n 2. 启动并初始化sentinel\n\n * 启动命令:redis-sentinel /path/sentinel.conf\n * 初始化服务器\n * 使用sentinel专用代码\n * 初始化sentinel状态。sentinelstate{}\n * 初始化masters属性。根据配置文件来生成。 [外链图片转存中...(img-gex8x9le-1653145947298)]\n * 创建两个链接主服务器的异步网络:一个命令链接;一个订阅链接。\n * 获取服务器信息。10s/次向主服务器发送info命令。获取主从信息。更新主从信息。从信息的保存结构如下: [外链图片转存中...(img-abn6pijg-1653145947298)]\n * 获取从服务器信息。获取从服务器信息。sentinel发现主服务器有从服务器时,保存从服务器并创建两个异步链接:命令链接和订阅链接。10s/次向从服务器发送命令获取信息更新本地存储。\n * 向主从发送信息。2s/次通过命令连接发送信息。发送频道_sentinel_:hello\n * 接收信息。接收频道 sentinel:hello(用于发现新的sentinel)\n * 更新sentinel字典。sentinelresidinstance结构中的sentinels保存这个主服务器的所有sentinel。接收到_sentinel_:hello频道的消息时,更新sentinel.\n * 创建sentinel之间的连接。(不创建订阅链接)\n\n 3. 检测主观下线状态\n\n> sentinel每秒向它连接的实例发送ping命令。返回+pong、-loading、-masterdown是有效回复。其他回复或者指定时间没回复则认为无效。配置项down-after-milliseconds指定了sentinel判断主观下线时间长度。如果一个实例在down-after-milliseconds时间内连续向sentinel返回无效,则认为主观下线。\n\n * sentinelredisinstance中的master属性有主观下线标识。\n * down-after-milliseconds是master下线的标准,也是master下slaves下线的标准,也是其他sentinel判断master下线的标准。\n * 多个sentinel设置同一个msater的down-after-milliseconds可能不同\n\n 4. 客观下线\n\n> sentinel判断一个master已经主观下线,会询问其他sentinel,如果一定数量 的都认为这个master已经主观下线,则判定这个master客观下线。然后做故障转移。\n\n * 发送sentinel is-master-down-by-addr命令询问其他sentinel\n * sentinel配置中的quorum参数决定有几个sentinel判断下线才能算是下线\n * 当一个msater被判定为客观下线时,监视这个master的各个sentinel之间会选举一个为领头sentinel,做故障转移。\n * 故障转移:选一个先master、slaves同步新的master、旧的master成为新master的slave\n\n 5. 选举领头sentinel\n\n> 当一个主服务器下线后,在监管这个主服务器的sentinel中选举一个领头sentinel。用于故障转移\n\n[外链图片转存中...(img-5xfvvtzh-1653145947298)] 6. 故障转移 领头sentinel完成故障转移需要三个步骤:\n\n * 在从服务器中选一个成为新的主服务器。sentinel在从服务器中选举一个,向其发送命令slaveof no one,使之成为主服务器。发送slaveof no one明后后每秒发送info命令观察role是不是变成了master。\n * 让从服务器同步新的主服务器. [外链图片转存中...(img-jz4gyou9-1653145947299)]\n * 让旧的主服务器成为新的主服务器的从服务器,当其上线时,成为从服务器。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点", + "frontmatter": { + "title": "简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点", + "date": "2022-05-21T17:21:43.000Z", + "permalink": "/pages/7e708d/", + "categories": [ + "数据库", + "REDIS相关" + ], + "tags": [ + null + ] + }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/020.%E7%AE%80%E8%BF%B0%20Redis%20%E6%8C%81%E4%B9%85%E5%8C%96%E4%B8%AD%20RDB%20%E4%BB%A5%E5%8F%8A%20AOF%20%E6%96%B9%E6%A1%88%E7%9A%84%E4%BC%98%E7%BC%BA%E7%82%B9.html", + "relativePath": "03.数据库/02.REDIS相关/020.简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点.md", + "key": "v-7c214d00", + "path": "/pages/7e708d/", + "headers": [ + { + "level": 2, + "title": "RDB持久化", + "slug": "rdb持久化", + "normalizedTitle": "rdb持久化", + "charIndex": 2 + }, + { + "level": 2, + "title": "AOF持久化", + "slug": "aof持久化", + "normalizedTitle": "aof持久化", + "charIndex": 1345 + } + ], + "headersStr": "RDB持久化 AOF持久化", + "content": "# RDB持久化\n\n 1. save和bgsave源码\n\nvoid saveCommand(redisClient *c) {\n\n // BGSAVE 已经在执行中,不能再执行 SAVE\n // 否则将产生竞争条件\n if (server.rdb_child_pid != -1) {\n addReplyError(c,\"Background save already in progress\");\n return;\n }\n\n // 执行 \n if (rdbSave(server.rdb_filename) == REDIS_OK) {\n addReply(c,shared.ok);\n } else {\n addReply(c,shared.err);\n }\n}\n\nvoid bgsaveCommand(redisClient *c) {\n\n // 不能重复执行 BGSAVE\n if (server.rdb_child_pid != -1) {\n addReplyError(c,\"Background save already in progress\");\n\n // 不能在 BGREWRITEAOF 正在运行时执行\n } else if (server.aof_child_pid != -1) {\n addReplyError(c,\"Can't BGSAVE while AOF log rewriting is in progress\");\n\n // 执行 BGSAVE\n } else if (rdbSaveBackground(server.rdb_filename) == REDIS_OK) {\n addReplyStatus(c,\"Background saving started\");\n\n } else {\n addReply(c,shared.err);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n\n 2. 自动间隔性保存\n\n> 服务器间隔性检车saveparams中的任意一个条件是否得到满足,如果得到满足,执行bgsave.saveparams[0]表示距离上次bgsave900s内,至少有一次数据库变化。\n\n[外链图片转存中...(img-SwAqAX8z-1653145947293)]\n\n 3. rdb文件结构 [外链图片转存中...(img-Prs54pGE-1653145947294)] 其中database结构如下: [外链图片转存中...(img-rn1gUv6w-1653145947294)] 其中ksy_value_pairs结构如下 [外链图片转存中...(img-B62S2gV5-1653145947295)]\n\n> type指定了value的类型。value的编码方式不同期存储结构也不同。\n\n\n# AOF持久化\n\n 1. aof的实现 命令追加:所有客户端命令都会被存到redis_server的aof_buf缓冲区。 文件写入:操作系统在写入文件的时候,先写入缓存,缓存满了才写入文件。 文件同步:flushAppendOnlyFile()负责将aof_buf写入文件aof。\n\n> flushAppendOnlyFile中配置的appendfsync决定同步策略。appendfsync有三个取值:always(每次写入都同步)、everysec(每次事件都写入但不同步,每秒同步)、 no(每次事件都写入但不同步,操作系统决定同步);\n\n 2. aof的载入与还原 [外链图片转存中...(img-pNMyXRLJ-1653145947295)]\n 3. AOF重写 [外链图片转存中...(img-i1nhwgNs-1653145947295)]", + "normalizedContent": "# rdb持久化\n\n 1. save和bgsave源码\n\nvoid savecommand(redisclient *c) {\n\n // bgsave 已经在执行中,不能再执行 save\n // 否则将产生竞争条件\n if (server.rdb_child_pid != -1) {\n addreplyerror(c,\"background save already in progress\");\n return;\n }\n\n // 执行 \n if (rdbsave(server.rdb_filename) == redis_ok) {\n addreply(c,shared.ok);\n } else {\n addreply(c,shared.err);\n }\n}\n\nvoid bgsavecommand(redisclient *c) {\n\n // 不能重复执行 bgsave\n if (server.rdb_child_pid != -1) {\n addreplyerror(c,\"background save already in progress\");\n\n // 不能在 bgrewriteaof 正在运行时执行\n } else if (server.aof_child_pid != -1) {\n addreplyerror(c,\"can't bgsave while aof log rewriting is in progress\");\n\n // 执行 bgsave\n } else if (rdbsavebackground(server.rdb_filename) == redis_ok) {\n addreplystatus(c,\"background saving started\");\n\n } else {\n addreply(c,shared.err);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n\n 2. 自动间隔性保存\n\n> 服务器间隔性检车saveparams中的任意一个条件是否得到满足,如果得到满足,执行bgsave.saveparams[0]表示距离上次bgsave900s内,至少有一次数据库变化。\n\n[外链图片转存中...(img-swaqax8z-1653145947293)]\n\n 3. rdb文件结构 [外链图片转存中...(img-prs54pge-1653145947294)] 其中database结构如下: [外链图片转存中...(img-rn1guv6w-1653145947294)] 其中ksy_value_pairs结构如下 [外链图片转存中...(img-b62s2gv5-1653145947295)]\n\n> type指定了value的类型。value的编码方式不同期存储结构也不同。\n\n\n# aof持久化\n\n 1. aof的实现 命令追加:所有客户端命令都会被存到redis_server的aof_buf缓冲区。 文件写入:操作系统在写入文件的时候,先写入缓存,缓存满了才写入文件。 文件同步:flushappendonlyfile()负责将aof_buf写入文件aof。\n\n> flushappendonlyfile中配置的appendfsync决定同步策略。appendfsync有三个取值:always(每次写入都同步)、everysec(每次事件都写入但不同步,每秒同步)、 no(每次事件都写入但不同步,操作系统决定同步);\n\n 2. aof的载入与还原 [外链图片转存中...(img-pnmyxrlj-1653145947295)]\n 3. aof重写 [外链图片转存中...(img-i1nhwgns-1653145947295)]", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "TCP粘包拆包问题的解决之道", + "frontmatter": { + "title": "TCP粘包拆包问题的解决之道", + "date": "2022-05-21T23:32:33.000Z", + "permalink": "/pages/3db019/", + "categories": [ + "中间件", + "Netty" + ], + "tags": [ + null + ] + }, + "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/10.Netty/010.TCP%E7%B2%98%E5%8C%85%E6%8B%86%E5%8C%85%E9%97%AE%E9%A2%98%E7%9A%84%E8%A7%A3%E5%86%B3%E4%B9%8B%E9%81%93.html", + "relativePath": "04.中间件/10.Netty/010.TCP粘包拆包问题的解决之道.md", + "key": "v-5acdab4d", + "path": "/pages/3db019/", + "headers": [ + { + "level": 2, + "title": "4.1 TCP粘包/拆包", + "slug": "_4-1-tcp粘包-拆包", + "normalizedTitle": "4.1 tcp粘包/拆包", + "charIndex": 2 + }, + { + "level": 3, + "title": "4.1.1 TCP粘包/拆包问题说明", + "slug": "_4-1-1-tcp粘包-拆包问题说明", + "normalizedTitle": "4.1.1 tcp粘包/拆包问题说明", + "charIndex": 19 + }, + { + "level": 3, + "title": "4.1.2 TCP粘包/拆包发生的原因", + "slug": "_4-1-2-tcp粘包-拆包发生的原因", + "normalizedTitle": "4.1.2 tcp粘包/拆包发生的原因", + "charIndex": 111 + }, + { + "level": 3, + "title": "4.1.3 TCP粘包/拆包问题的解决策略", + "slug": "_4-1-3-tcp粘包-拆包问题的解决策略", + "normalizedTitle": "4.1.3 tcp粘包/拆包问题的解决策略", + "charIndex": 181 + }, + { + "level": 2, + "title": "4.3 Netty解决tcp粘包问题", + "slug": "_4-3-netty解决tcp粘包问题", + "normalizedTitle": "4.3 netty解决tcp粘包问题", + "charIndex": 322 } ], - "headersStr": "sentinel", - "content": "# sentinel\n\n> 负责redis高可用,检测主服务器下线,然后选举一个从服务器成为新的主服务器。\n\n 1. 源码\n\n// Sentinel 会为每个被监视的 Redis 实例创建相应的 sentinelRedisInstance 实例\n// (被监视的实例可以是主服务器、从服务器、或者其他 Sentinel )\ntypedef struct sentinelRedisInstance {\n \n // 标识值,记录了实例的类型,以及该实例的当前状态\n int flags; /* See SRI_... defines */\n \n // 实例的名字\n // 主服务器的名字由用户在配置文件中设置\n // 从服务器以及 Sentinel 的名字由 Sentinel 自动设置\n // 格式为 ip:port ,例如 \"127.0.0.1:26379\"\n char *name; /* Master name from the point of view of this sentinel. */\n\n // 实例的运行 ID\n char *runid; /* run ID of this instance. */\n\n // 配置纪元,用于实现故障转移\n uint64_t config_epoch; /* Configuration epoch. */\n\n // 实例的地址\n sentinelAddr *addr; /* Master host. */\n\n // 用于发送命令的异步连接\n redisAsyncContext *cc; /* Hiredis context for commands. */\n\n // 用于执行 SUBSCRIBE 命令、接收频道信息的异步连接\n // 仅在实例为主服务器时使用\n redisAsyncContext *pc; /* Hiredis context for Pub / Sub. */\n\n // 已发送但尚未回复的命令数量\n int pending_commands; /* Number of commands sent waiting for a reply. */\n\n // cc 连接的创建时间\n mstime_t cc_conn_time; /* cc connection time. */\n \n // pc 连接的创建时间\n mstime_t pc_conn_time; /* pc connection time. */\n\n // 最后一次从这个实例接收信息的时间\n mstime_t pc_last_activity; /* Last time we received any message. */\n\n // 实例最后一次返回正确的 PING 命令回复的时间\n mstime_t last_avail_time; /* Last time the instance replied to ping with\n a reply we consider valid. */\n // 实例最后一次发送 PING 命令的时间\n mstime_t last_ping_time; /* Last time a pending ping was sent in the\n context of the current command connection\n with the instance. 0 if still not sent or\n if pong already received. */\n // 实例最后一次返回 PING 命令的时间,无论内容正确与否\n mstime_t last_pong_time; /* Last time the instance replied to ping,\n whatever the reply was. That's used to check\n if the link is idle and must be reconnected. */\n\n // 最后一次向频道发送问候信息的时间\n // 只在当前实例为 sentinel 时使用\n mstime_t last_pub_time; /* Last time we sent hello via Pub/Sub. */\n\n // 最后一次接收到这个 sentinel 发来的问候信息的时间\n // 只在当前实例为 sentinel 时使用\n mstime_t last_hello_time; /* Only used if SRI_SENTINEL is set. Last time\n we received a hello from this Sentinel\n via Pub/Sub. */\n\n // 最后一次回复 SENTINEL is-master-down-by-addr 命令的时间\n // 只在当前实例为 sentinel 时使用\n mstime_t last_master_down_reply_time; /* Time of last reply to\n SENTINEL is-master-down command. */\n\n // 实例被判断为 SDOWN 状态的时间\n mstime_t s_down_since_time; /* Subjectively down since time. */\n\n // 实例被判断为 ODOWN 状态的时间\n mstime_t o_down_since_time; /* Objectively down since time. */\n\n // SENTINEL down-after-milliseconds 选项所设定的值\n // 实例无响应多少毫秒之后才会被判断为主观下线(subjectively down)\n mstime_t down_after_period; /* Consider it down after that period. */\n\n // 从实例获取 INFO 命令的回复的时间\n mstime_t info_refresh; /* Time at which we received INFO output from it. */\n\n /* Role and the first time we observed it.\n * This is useful in order to delay replacing what the instance reports\n * with our own configuration. We need to always wait some time in order\n * to give a chance to the leader to report the new configuration before\n * we do silly things. */\n // 实例的角色\n int role_reported;\n // 角色的更新时间\n mstime_t role_reported_time;\n\n // 最后一次从服务器的主服务器地址变更的时间\n mstime_t slave_conf_change_time; /* Last time slave master addr changed. */\n\n /* Master specific. */\n /* 主服务器实例特有的属性 -------------------------------------------------------------*/\n\n // 其他同样监控这个主服务器的所有 sentinel\n dict *sentinels; /* Other sentinels monitoring the same master. */\n\n // 如果这个实例代表的是一个主服务器\n // 那么这个字典保存着主服务器属下的从服务器\n // 字典的键是从服务器的名字,字典的值是从服务器对应的 sentinelRedisInstance 结构\n dict *slaves; /* Slaves for this master instance. */\n\n // SENTINEL monitor 选项中的 quorum 参数\n // 判断这个实例为客观下线(objectively down)所需的支持投票数量\n int quorum; /* Number of sentinels that need to agree on failure. */\n\n // SENTINEL parallel-syncs 选项的值\n // 在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量\n int parallel_syncs; /* How many slaves to reconfigure at same time. */\n\n // 连接主服务器和从服务器所需的密码\n char *auth_pass; /* Password to use for AUTH against master & slaves. */\n\n /* Slave specific. */\n /* 从服务器实例特有的属性 -------------------------------------------------------------*/\n\n // 主从服务器连接断开的时间\n mstime_t master_link_down_time; /* Slave replication link down time. */\n\n // 从服务器优先级\n int slave_priority; /* Slave priority according to its INFO output. */\n\n // 执行故障转移操作时,从服务器发送 SLAVEOF 命令的时间\n mstime_t slave_reconf_sent_time; /* Time at which we sent SLAVE OF */\n\n // 主服务器的实例(在本实例为从服务器时使用)\n struct sentinelRedisInstance *master; /* Master instance if it's slave. */\n\n // INFO 命令的回复中记录的主服务器 IP\n char *slave_master_host; /* Master host as reported by INFO */\n \n // INFO 命令的回复中记录的主服务器端口号\n int slave_master_port; /* Master port as reported by INFO */\n\n // INFO 命令的回复中记录的主从服务器连接状态\n int slave_master_link_status; /* Master link status as reported by INFO */\n\n // 从服务器的复制偏移量\n unsigned long long slave_repl_offset; /* Slave replication offset. */\n\n /* Failover */\n /* 故障转移相关属性 -------------------------------------------------------------------*/\n\n\n // 如果这是一个主服务器实例,那么 leader 将是负责进行故障转移的 Sentinel 的运行 ID 。\n // 如果这是一个 Sentinel 实例,那么 leader 就是被选举出来的领头 Sentinel 。\n // 这个域只在 Sentinel 实例的 flags 属性的 SRI_MASTER_DOWN 标志处于打开状态时才有效。\n char *leader; /* If this is a master instance, this is the runid of\n the Sentinel that should perform the failover. If\n this is a Sentinel, this is the runid of the Sentinel\n that this Sentinel voted as leader. */\n // 领头的纪元\n uint64_t leader_epoch; /* Epoch of the 'leader' field. */\n // 当前执行中的故障转移的纪元\n uint64_t failover_epoch; /* Epoch of the currently started failover. */\n // 故障转移操作的当前状态\n int failover_state; /* See SENTINEL_FAILOVER_STATE_* defines. */\n\n // 状态改变的时间\n mstime_t failover_state_change_time;\n\n // 最后一次进行故障迁移的时间\n mstime_t failover_start_time; /* Last failover attempt start time. */\n\n // SENTINEL failover-timeout 选项的值\n // 刷新故障迁移状态的最大时限\n mstime_t failover_timeout; /* Max time to refresh failover state. */\n\n mstime_t failover_delay_logged; /* For what failover_start_time value we\n logged the failover delay. */\n // 指向被提升为新主服务器的从服务器的指针\n struct sentinelRedisInstance *promoted_slave; /* Promoted slave instance. */\n\n /* Scripts executed to notify admin or reconfigure clients: when they\n * are set to NULL no script is executed. */\n // 一个文件路径,保存着 WARNING 级别的事件发生时执行的,\n // 用于通知管理员的脚本的地址\n char *notification_script;\n\n // 一个文件路径,保存着故障转移执行之前、之后、或者被中止时,\n // 需要执行的脚本的地址\n char *client_reconfig_script;\n\n} sentinelRedisInstance;\n\n/* Main state. */\n/* Sentinel 的状态结构 */\nstruct sentinelState {\n\n // 当前纪元\n uint64_t current_epoch; /* Current epoch. */\n\n // 保存了所有被这个 sentinel 监视的主服务器\n // 字典的键是主服务器的名字\n // 字典的值则是一个指向 sentinelRedisInstance 结构的指针\n dict *masters; /* Dictionary of master sentinelRedisInstances.\n Key is the instance name, value is the\n sentinelRedisInstance structure pointer. */\n\n // 是否进入了 TILT 模式?\n int tilt; /* Are we in TILT mode? */\n\n // 目前正在执行的脚本的数量\n int running_scripts; /* Number of scripts in execution right now. */\n\n // 进入 TILT 模式的时间\n mstime_t tilt_start_time; /* When TITL started. */\n\n // 最后一次执行时间处理器的时间\n mstime_t previous_time; /* Last time we ran the time handler. */\n\n // 一个 FIFO 队列,包含了所有需要执行的用户脚本\n list *scripts_queue; /* Queue of user scripts to execute. */\n\n} sentinel;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n57\n58\n59\n60\n61\n62\n63\n64\n65\n66\n67\n68\n69\n70\n71\n72\n73\n74\n75\n76\n77\n78\n79\n80\n81\n82\n83\n84\n85\n86\n87\n88\n89\n90\n91\n92\n93\n94\n95\n96\n97\n98\n99\n100\n101\n102\n103\n104\n105\n106\n107\n108\n109\n110\n111\n112\n113\n114\n115\n116\n117\n118\n119\n120\n121\n122\n123\n124\n125\n126\n127\n128\n129\n130\n131\n132\n133\n134\n135\n136\n137\n138\n139\n140\n141\n142\n143\n144\n145\n146\n147\n148\n149\n150\n151\n152\n153\n154\n155\n156\n157\n158\n159\n160\n161\n162\n163\n164\n165\n166\n167\n168\n169\n170\n171\n172\n173\n174\n175\n176\n177\n178\n179\n180\n181\n182\n183\n184\n185\n186\n187\n188\n189\n190\n191\n192\n193\n194\n195\n196\n197\n198\n199\n200\n201\n202\n203\n204\n205\n206\n207\n208\n209\n210\n211\n212\n213\n214\n215\n216\n217\n218\n219\n\n 2. 启动并初始化sentinel\n\n * 启动命令:Redis-sentinel /path/sentinel.conf\n * 初始化服务器\n * 使用sentinel专用代码\n * 初始化sentinel状态。sentinelState{}\n * 初始化masters属性。根据配置文件来生成。 [外链图片转存中...(img-gEx8X9Le-1653145947298)]\n * 创建两个链接主服务器的异步网络:一个命令链接;一个订阅链接。\n * 获取服务器信息。10s/次向主服务器发送info命令。获取主从信息。更新主从信息。从信息的保存结构如下: [外链图片转存中...(img-aBn6piJg-1653145947298)]\n * 获取从服务器信息。获取从服务器信息。sentinel发现主服务器有从服务器时,保存从服务器并创建两个异步链接:命令链接和订阅链接。10s/次向从服务器发送命令获取信息更新本地存储。\n * 向主从发送信息。2s/次通过命令连接发送信息。发送频道_sentinel_:hello\n * 接收信息。接收频道 sentinel:hello(用于发现新的sentinel)\n * 更新sentinel字典。sentinelResidInstance结构中的sentinels保存这个主服务器的所有sentinel。接收到_sentinel_:hello频道的消息时,更新sentinel.\n * 创建sentinel之间的连接。(不创建订阅链接)\n\n 3. 检测主观下线状态\n\n> sentinel每秒向它连接的实例发送ping命令。返回+pong、-loading、-masterdown是有效回复。其他回复或者指定时间没回复则认为无效。配置项down-after-milliseconds指定了sentinel判断主观下线时间长度。如果一个实例在down-after-milliseconds时间内连续向sentinel返回无效,则认为主观下线。\n\n * sentinelRedisInstance中的master属性有主观下线标识。\n * down-after-milliseconds是master下线的标准,也是master下slaves下线的标准,也是其他sentinel判断master下线的标准。\n * 多个sentinel设置同一个msater的down-after-milliseconds可能不同\n\n 4. 客观下线\n\n> sentinel判断一个master已经主观下线,会询问其他sentinel,如果一定数量 的都认为这个master已经主观下线,则判定这个master客观下线。然后做故障转移。\n\n * 发送sentinel is-master-down-by-addr命令询问其他sentinel\n * sentinel配置中的quorum参数决定有几个sentinel判断下线才能算是下线\n * 当一个msater被判定为客观下线时,监视这个master的各个sentinel之间会选举一个为领头sentinel,做故障转移。\n * 故障转移:选一个先master、slaves同步新的master、旧的master成为新master的slave\n\n 5. 选举领头sentinel\n\n> 当一个主服务器下线后,在监管这个主服务器的sentinel中选举一个领头sentinel。用于故障转移\n\n[外链图片转存中...(img-5XFvVtzH-1653145947298)] 6. 故障转移 领头sentinel完成故障转移需要三个步骤:\n\n * 在从服务器中选一个成为新的主服务器。sentinel在从服务器中选举一个,向其发送命令slaveof no one,使之成为主服务器。发送slaveof no one明后后每秒发送info命令观察role是不是变成了master。\n * 让从服务器同步新的主服务器. [外链图片转存中...(img-jz4GYou9-1653145947299)]\n * 让旧的主服务器成为新的主服务器的从服务器,当其上线时,成为从服务器。", - "normalizedContent": "# sentinel\n\n> 负责redis高可用,检测主服务器下线,然后选举一个从服务器成为新的主服务器。\n\n 1. 源码\n\n// sentinel 会为每个被监视的 redis 实例创建相应的 sentinelredisinstance 实例\n// (被监视的实例可以是主服务器、从服务器、或者其他 sentinel )\ntypedef struct sentinelredisinstance {\n \n // 标识值,记录了实例的类型,以及该实例的当前状态\n int flags; /* see sri_... defines */\n \n // 实例的名字\n // 主服务器的名字由用户在配置文件中设置\n // 从服务器以及 sentinel 的名字由 sentinel 自动设置\n // 格式为 ip:port ,例如 \"127.0.0.1:26379\"\n char *name; /* master name from the point of view of this sentinel. */\n\n // 实例的运行 id\n char *runid; /* run id of this instance. */\n\n // 配置纪元,用于实现故障转移\n uint64_t config_epoch; /* configuration epoch. */\n\n // 实例的地址\n sentineladdr *addr; /* master host. */\n\n // 用于发送命令的异步连接\n redisasynccontext *cc; /* hiredis context for commands. */\n\n // 用于执行 subscribe 命令、接收频道信息的异步连接\n // 仅在实例为主服务器时使用\n redisasynccontext *pc; /* hiredis context for pub / sub. */\n\n // 已发送但尚未回复的命令数量\n int pending_commands; /* number of commands sent waiting for a reply. */\n\n // cc 连接的创建时间\n mstime_t cc_conn_time; /* cc connection time. */\n \n // pc 连接的创建时间\n mstime_t pc_conn_time; /* pc connection time. */\n\n // 最后一次从这个实例接收信息的时间\n mstime_t pc_last_activity; /* last time we received any message. */\n\n // 实例最后一次返回正确的 ping 命令回复的时间\n mstime_t last_avail_time; /* last time the instance replied to ping with\n a reply we consider valid. */\n // 实例最后一次发送 ping 命令的时间\n mstime_t last_ping_time; /* last time a pending ping was sent in the\n context of the current command connection\n with the instance. 0 if still not sent or\n if pong already received. */\n // 实例最后一次返回 ping 命令的时间,无论内容正确与否\n mstime_t last_pong_time; /* last time the instance replied to ping,\n whatever the reply was. that's used to check\n if the link is idle and must be reconnected. */\n\n // 最后一次向频道发送问候信息的时间\n // 只在当前实例为 sentinel 时使用\n mstime_t last_pub_time; /* last time we sent hello via pub/sub. */\n\n // 最后一次接收到这个 sentinel 发来的问候信息的时间\n // 只在当前实例为 sentinel 时使用\n mstime_t last_hello_time; /* only used if sri_sentinel is set. last time\n we received a hello from this sentinel\n via pub/sub. */\n\n // 最后一次回复 sentinel is-master-down-by-addr 命令的时间\n // 只在当前实例为 sentinel 时使用\n mstime_t last_master_down_reply_time; /* time of last reply to\n sentinel is-master-down command. */\n\n // 实例被判断为 sdown 状态的时间\n mstime_t s_down_since_time; /* subjectively down since time. */\n\n // 实例被判断为 odown 状态的时间\n mstime_t o_down_since_time; /* objectively down since time. */\n\n // sentinel down-after-milliseconds 选项所设定的值\n // 实例无响应多少毫秒之后才会被判断为主观下线(subjectively down)\n mstime_t down_after_period; /* consider it down after that period. */\n\n // 从实例获取 info 命令的回复的时间\n mstime_t info_refresh; /* time at which we received info output from it. */\n\n /* role and the first time we observed it.\n * this is useful in order to delay replacing what the instance reports\n * with our own configuration. we need to always wait some time in order\n * to give a chance to the leader to report the new configuration before\n * we do silly things. */\n // 实例的角色\n int role_reported;\n // 角色的更新时间\n mstime_t role_reported_time;\n\n // 最后一次从服务器的主服务器地址变更的时间\n mstime_t slave_conf_change_time; /* last time slave master addr changed. */\n\n /* master specific. */\n /* 主服务器实例特有的属性 -------------------------------------------------------------*/\n\n // 其他同样监控这个主服务器的所有 sentinel\n dict *sentinels; /* other sentinels monitoring the same master. */\n\n // 如果这个实例代表的是一个主服务器\n // 那么这个字典保存着主服务器属下的从服务器\n // 字典的键是从服务器的名字,字典的值是从服务器对应的 sentinelredisinstance 结构\n dict *slaves; /* slaves for this master instance. */\n\n // sentinel monitor 选项中的 quorum 参数\n // 判断这个实例为客观下线(objectively down)所需的支持投票数量\n int quorum; /* number of sentinels that need to agree on failure. */\n\n // sentinel parallel-syncs 选项的值\n // 在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量\n int parallel_syncs; /* how many slaves to reconfigure at same time. */\n\n // 连接主服务器和从服务器所需的密码\n char *auth_pass; /* password to use for auth against master & slaves. */\n\n /* slave specific. */\n /* 从服务器实例特有的属性 -------------------------------------------------------------*/\n\n // 主从服务器连接断开的时间\n mstime_t master_link_down_time; /* slave replication link down time. */\n\n // 从服务器优先级\n int slave_priority; /* slave priority according to its info output. */\n\n // 执行故障转移操作时,从服务器发送 slaveof 命令的时间\n mstime_t slave_reconf_sent_time; /* time at which we sent slave of */\n\n // 主服务器的实例(在本实例为从服务器时使用)\n struct sentinelredisinstance *master; /* master instance if it's slave. */\n\n // info 命令的回复中记录的主服务器 ip\n char *slave_master_host; /* master host as reported by info */\n \n // info 命令的回复中记录的主服务器端口号\n int slave_master_port; /* master port as reported by info */\n\n // info 命令的回复中记录的主从服务器连接状态\n int slave_master_link_status; /* master link status as reported by info */\n\n // 从服务器的复制偏移量\n unsigned long long slave_repl_offset; /* slave replication offset. */\n\n /* failover */\n /* 故障转移相关属性 -------------------------------------------------------------------*/\n\n\n // 如果这是一个主服务器实例,那么 leader 将是负责进行故障转移的 sentinel 的运行 id 。\n // 如果这是一个 sentinel 实例,那么 leader 就是被选举出来的领头 sentinel 。\n // 这个域只在 sentinel 实例的 flags 属性的 sri_master_down 标志处于打开状态时才有效。\n char *leader; /* if this is a master instance, this is the runid of\n the sentinel that should perform the failover. if\n this is a sentinel, this is the runid of the sentinel\n that this sentinel voted as leader. */\n // 领头的纪元\n uint64_t leader_epoch; /* epoch of the 'leader' field. */\n // 当前执行中的故障转移的纪元\n uint64_t failover_epoch; /* epoch of the currently started failover. */\n // 故障转移操作的当前状态\n int failover_state; /* see sentinel_failover_state_* defines. */\n\n // 状态改变的时间\n mstime_t failover_state_change_time;\n\n // 最后一次进行故障迁移的时间\n mstime_t failover_start_time; /* last failover attempt start time. */\n\n // sentinel failover-timeout 选项的值\n // 刷新故障迁移状态的最大时限\n mstime_t failover_timeout; /* max time to refresh failover state. */\n\n mstime_t failover_delay_logged; /* for what failover_start_time value we\n logged the failover delay. */\n // 指向被提升为新主服务器的从服务器的指针\n struct sentinelredisinstance *promoted_slave; /* promoted slave instance. */\n\n /* scripts executed to notify admin or reconfigure clients: when they\n * are set to null no script is executed. */\n // 一个文件路径,保存着 warning 级别的事件发生时执行的,\n // 用于通知管理员的脚本的地址\n char *notification_script;\n\n // 一个文件路径,保存着故障转移执行之前、之后、或者被中止时,\n // 需要执行的脚本的地址\n char *client_reconfig_script;\n\n} sentinelredisinstance;\n\n/* main state. */\n/* sentinel 的状态结构 */\nstruct sentinelstate {\n\n // 当前纪元\n uint64_t current_epoch; /* current epoch. */\n\n // 保存了所有被这个 sentinel 监视的主服务器\n // 字典的键是主服务器的名字\n // 字典的值则是一个指向 sentinelredisinstance 结构的指针\n dict *masters; /* dictionary of master sentinelredisinstances.\n key is the instance name, value is the\n sentinelredisinstance structure pointer. */\n\n // 是否进入了 tilt 模式?\n int tilt; /* are we in tilt mode? */\n\n // 目前正在执行的脚本的数量\n int running_scripts; /* number of scripts in execution right now. */\n\n // 进入 tilt 模式的时间\n mstime_t tilt_start_time; /* when titl started. */\n\n // 最后一次执行时间处理器的时间\n mstime_t previous_time; /* last time we ran the time handler. */\n\n // 一个 fifo 队列,包含了所有需要执行的用户脚本\n list *scripts_queue; /* queue of user scripts to execute. */\n\n} sentinel;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n57\n58\n59\n60\n61\n62\n63\n64\n65\n66\n67\n68\n69\n70\n71\n72\n73\n74\n75\n76\n77\n78\n79\n80\n81\n82\n83\n84\n85\n86\n87\n88\n89\n90\n91\n92\n93\n94\n95\n96\n97\n98\n99\n100\n101\n102\n103\n104\n105\n106\n107\n108\n109\n110\n111\n112\n113\n114\n115\n116\n117\n118\n119\n120\n121\n122\n123\n124\n125\n126\n127\n128\n129\n130\n131\n132\n133\n134\n135\n136\n137\n138\n139\n140\n141\n142\n143\n144\n145\n146\n147\n148\n149\n150\n151\n152\n153\n154\n155\n156\n157\n158\n159\n160\n161\n162\n163\n164\n165\n166\n167\n168\n169\n170\n171\n172\n173\n174\n175\n176\n177\n178\n179\n180\n181\n182\n183\n184\n185\n186\n187\n188\n189\n190\n191\n192\n193\n194\n195\n196\n197\n198\n199\n200\n201\n202\n203\n204\n205\n206\n207\n208\n209\n210\n211\n212\n213\n214\n215\n216\n217\n218\n219\n\n 2. 启动并初始化sentinel\n\n * 启动命令:redis-sentinel /path/sentinel.conf\n * 初始化服务器\n * 使用sentinel专用代码\n * 初始化sentinel状态。sentinelstate{}\n * 初始化masters属性。根据配置文件来生成。 [外链图片转存中...(img-gex8x9le-1653145947298)]\n * 创建两个链接主服务器的异步网络:一个命令链接;一个订阅链接。\n * 获取服务器信息。10s/次向主服务器发送info命令。获取主从信息。更新主从信息。从信息的保存结构如下: [外链图片转存中...(img-abn6pijg-1653145947298)]\n * 获取从服务器信息。获取从服务器信息。sentinel发现主服务器有从服务器时,保存从服务器并创建两个异步链接:命令链接和订阅链接。10s/次向从服务器发送命令获取信息更新本地存储。\n * 向主从发送信息。2s/次通过命令连接发送信息。发送频道_sentinel_:hello\n * 接收信息。接收频道 sentinel:hello(用于发现新的sentinel)\n * 更新sentinel字典。sentinelresidinstance结构中的sentinels保存这个主服务器的所有sentinel。接收到_sentinel_:hello频道的消息时,更新sentinel.\n * 创建sentinel之间的连接。(不创建订阅链接)\n\n 3. 检测主观下线状态\n\n> sentinel每秒向它连接的实例发送ping命令。返回+pong、-loading、-masterdown是有效回复。其他回复或者指定时间没回复则认为无效。配置项down-after-milliseconds指定了sentinel判断主观下线时间长度。如果一个实例在down-after-milliseconds时间内连续向sentinel返回无效,则认为主观下线。\n\n * sentinelredisinstance中的master属性有主观下线标识。\n * down-after-milliseconds是master下线的标准,也是master下slaves下线的标准,也是其他sentinel判断master下线的标准。\n * 多个sentinel设置同一个msater的down-after-milliseconds可能不同\n\n 4. 客观下线\n\n> sentinel判断一个master已经主观下线,会询问其他sentinel,如果一定数量 的都认为这个master已经主观下线,则判定这个master客观下线。然后做故障转移。\n\n * 发送sentinel is-master-down-by-addr命令询问其他sentinel\n * sentinel配置中的quorum参数决定有几个sentinel判断下线才能算是下线\n * 当一个msater被判定为客观下线时,监视这个master的各个sentinel之间会选举一个为领头sentinel,做故障转移。\n * 故障转移:选一个先master、slaves同步新的master、旧的master成为新master的slave\n\n 5. 选举领头sentinel\n\n> 当一个主服务器下线后,在监管这个主服务器的sentinel中选举一个领头sentinel。用于故障转移\n\n[外链图片转存中...(img-5xfvvtzh-1653145947298)] 6. 故障转移 领头sentinel完成故障转移需要三个步骤:\n\n * 在从服务器中选一个成为新的主服务器。sentinel在从服务器中选举一个,向其发送命令slaveof no one,使之成为主服务器。发送slaveof no one明后后每秒发送info命令观察role是不是变成了master。\n * 让从服务器同步新的主服务器. [外链图片转存中...(img-jz4gyou9-1653145947299)]\n * 让旧的主服务器成为新的主服务器的从服务器,当其上线时,成为从服务器。", + "headersStr": "4.1 TCP粘包/拆包 4.1.1 TCP粘包/拆包问题说明 4.1.2 TCP粘包/拆包发生的原因 4.1.3 TCP粘包/拆包问题的解决策略 4.3 Netty解决tcp粘包问题", + "content": "# 4.1 TCP粘包/拆包\n\n\n# 4.1.1 TCP粘包/拆包问题说明\n\nTCP协议是”流“协议,流是没有间隔的。tcp会根据缓存大小将业务上的大包划分成多个小包发送出去、也可能多个小包合成一个大包发送出去。\n\n\n# 4.1.2 TCP粘包/拆包发生的原因\n\n * 应用层:大于套接字接口缓冲区大小\n * TCP层:MSS\n * IP层:MTU\n\n\n\n\n# 4.1.3 TCP粘包/拆包问题的解决策略\n\n * 消息定长len,例如每个报文固定200字节。那么读取到定长len后就重置计数器开始读取下一个包。\n * 包尾加换行符分割,如ftp。\n * 消息头+消息体。消息头包含消息长度信息。\n * 更复杂的应用协议,如netty.\n\n\n# 4.3 Netty解决tcp粘包问题\n\n * LineBasedFrameDecoder:原理是遍历ByteBuf中字节,以换行符分割\n * StringDecoder:将接收的byte对象转换为字符串,然后调用后面的handler 如果发送的消息不是以换行符结束的,netty也有其他解码器支持。", + "normalizedContent": "# 4.1 tcp粘包/拆包\n\n\n# 4.1.1 tcp粘包/拆包问题说明\n\ntcp协议是”流“协议,流是没有间隔的。tcp会根据缓存大小将业务上的大包划分成多个小包发送出去、也可能多个小包合成一个大包发送出去。\n\n\n# 4.1.2 tcp粘包/拆包发生的原因\n\n * 应用层:大于套接字接口缓冲区大小\n * tcp层:mss\n * ip层:mtu\n\n\n\n\n# 4.1.3 tcp粘包/拆包问题的解决策略\n\n * 消息定长len,例如每个报文固定200字节。那么读取到定长len后就重置计数器开始读取下一个包。\n * 包尾加换行符分割,如ftp。\n * 消息头+消息体。消息头包含消息长度信息。\n * 更复杂的应用协议,如netty.\n\n\n# 4.3 netty解决tcp粘包问题\n\n * linebasedframedecoder:原理是遍历bytebuf中字节,以换行符分割\n * stringdecoder:将接收的byte对象转换为字符串,然后调用后面的handler 如果发送的消息不是以换行符结束的,netty也有其他解码器支持。", "charsets": { "cjk": true }, "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, - { - "title": "简述 Redis 如何处理热点 key 访问", - "frontmatter": { - "title": "简述 Redis 如何处理热点 key 访问", - "date": "2022-05-21T17:23:43.000Z", - "permalink": "/pages/795c09/", - "categories": [ - "数据库", - "REDIS相关" - ], - "tags": [ - null - ] - }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/110.%E7%AE%80%E8%BF%B0%20Redis%20%E5%A6%82%E4%BD%95%E5%A4%84%E7%90%86%E7%83%AD%E7%82%B9%20key%20%E8%AE%BF%E9%97%AE.html", - "relativePath": "03.数据库/02.REDIS相关/110.简述 Redis 如何处理热点 key 访问.md", - "key": "v-37f158ba", - "path": "/pages/795c09/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, { "title": "Netty", "frontmatter": { @@ -5067,70 +5505,6 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, - { - "title": "TCP粘包拆包问题的解决之道", - "frontmatter": { - "title": "TCP粘包拆包问题的解决之道", - "date": "2022-05-21T23:32:33.000Z", - "permalink": "/pages/3db019/", - "categories": [ - "中间件", - "Netty" - ], - "tags": [ - null - ] - }, - "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/10.Netty/010.TCP%E7%B2%98%E5%8C%85%E6%8B%86%E5%8C%85%E9%97%AE%E9%A2%98%E7%9A%84%E8%A7%A3%E5%86%B3%E4%B9%8B%E9%81%93.html", - "relativePath": "04.中间件/10.Netty/010.TCP粘包拆包问题的解决之道.md", - "key": "v-5acdab4d", - "path": "/pages/3db019/", - "headers": [ - { - "level": 2, - "title": "4.1 TCP粘包/拆包", - "slug": "_4-1-tcp粘包-拆包", - "normalizedTitle": "4.1 tcp粘包/拆包", - "charIndex": 2 - }, - { - "level": 3, - "title": "4.1.1 TCP粘包/拆包问题说明", - "slug": "_4-1-1-tcp粘包-拆包问题说明", - "normalizedTitle": "4.1.1 tcp粘包/拆包问题说明", - "charIndex": 19 - }, - { - "level": 3, - "title": "4.1.2 TCP粘包/拆包发生的原因", - "slug": "_4-1-2-tcp粘包-拆包发生的原因", - "normalizedTitle": "4.1.2 tcp粘包/拆包发生的原因", - "charIndex": 111 - }, - { - "level": 3, - "title": "4.1.3 TCP粘包/拆包问题的解决策略", - "slug": "_4-1-3-tcp粘包-拆包问题的解决策略", - "normalizedTitle": "4.1.3 tcp粘包/拆包问题的解决策略", - "charIndex": 181 - }, - { - "level": 2, - "title": "4.3 Netty解决tcp粘包问题", - "slug": "_4-3-netty解决tcp粘包问题", - "normalizedTitle": "4.3 netty解决tcp粘包问题", - "charIndex": 322 - } - ], - "headersStr": "4.1 TCP粘包/拆包 4.1.1 TCP粘包/拆包问题说明 4.1.2 TCP粘包/拆包发生的原因 4.1.3 TCP粘包/拆包问题的解决策略 4.3 Netty解决tcp粘包问题", - "content": "# 4.1 TCP粘包/拆包\n\n\n# 4.1.1 TCP粘包/拆包问题说明\n\nTCP协议是”流“协议,流是没有间隔的。tcp会根据缓存大小将业务上的大包划分成多个小包发送出去、也可能多个小包合成一个大包发送出去。\n\n\n# 4.1.2 TCP粘包/拆包发生的原因\n\n * 应用层:大于套接字接口缓冲区大小\n * TCP层:MSS\n * IP层:MTU\n\n\n\n\n# 4.1.3 TCP粘包/拆包问题的解决策略\n\n * 消息定长len,例如每个报文固定200字节。那么读取到定长len后就重置计数器开始读取下一个包。\n * 包尾加换行符分割,如ftp。\n * 消息头+消息体。消息头包含消息长度信息。\n * 更复杂的应用协议,如netty.\n\n\n# 4.3 Netty解决tcp粘包问题\n\n * LineBasedFrameDecoder:原理是遍历ByteBuf中字节,以换行符分割\n * StringDecoder:将接收的byte对象转换为字符串,然后调用后面的handler 如果发送的消息不是以换行符结束的,netty也有其他解码器支持。", - "normalizedContent": "# 4.1 tcp粘包/拆包\n\n\n# 4.1.1 tcp粘包/拆包问题说明\n\ntcp协议是”流“协议,流是没有间隔的。tcp会根据缓存大小将业务上的大包划分成多个小包发送出去、也可能多个小包合成一个大包发送出去。\n\n\n# 4.1.2 tcp粘包/拆包发生的原因\n\n * 应用层:大于套接字接口缓冲区大小\n * tcp层:mss\n * ip层:mtu\n\n\n\n\n# 4.1.3 tcp粘包/拆包问题的解决策略\n\n * 消息定长len,例如每个报文固定200字节。那么读取到定长len后就重置计数器开始读取下一个包。\n * 包尾加换行符分割,如ftp。\n * 消息头+消息体。消息头包含消息长度信息。\n * 更复杂的应用协议,如netty.\n\n\n# 4.3 netty解决tcp粘包问题\n\n * linebasedframedecoder:原理是遍历bytebuf中字节,以换行符分割\n * stringdecoder:将接收的byte对象转换为字符串,然后调用后面的handler 如果发送的消息不是以换行符结束的,netty也有其他解码器支持。", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, { "title": "Netty中有哪些解码器?", "frontmatter": { @@ -5174,49 +5548,6 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, - { - "title": "为什么Netty要自己写一个ByteBuf", - "frontmatter": { - "title": "为什么Netty要自己写一个ByteBuf", - "date": "2022-05-21T23:35:32.000Z", - "permalink": "/pages/ef17ee/", - "categories": [ - "中间件", - "Netty" - ], - "tags": [ - null - ] - }, - "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/10.Netty/020.%E4%B8%BA%E4%BB%80%E4%B9%88Netty%E8%A6%81%E8%87%AA%E5%B7%B1%E5%86%99%E4%B8%80%E4%B8%AAByteBuf.html", - "relativePath": "04.中间件/10.Netty/020.为什么Netty要自己写一个ByteBuf.md", - "key": "v-bbcf6e5a", - "path": "/pages/ef17ee/", - "headers": [ - { - "level": 2, - "title": "15.2 ByteBuf源码分析", - "slug": "_15-2-bytebuf源码分析", - "normalizedTitle": "15.2 bytebuf源码分析", - "charIndex": 2 - }, - { - "level": 2, - "title": "15.3 ByteBuffer相关辅助类", - "slug": "_15-3-bytebuffer相关辅助类", - "normalizedTitle": "15.3 bytebuffer相关辅助类", - "charIndex": 8055 - } - ], - "headersStr": "15.2 ByteBuf源码分析 15.3 ByteBuffer相关辅助类", - "content": "# 15.2 ByteBuf源码分析\n\n继承关系 从内存分配的角度看,byteBuf分为两类:\n\n * 堆内存缓冲区:优点是内存的分配和回收快。缺点是进行IO读写时需要一次内存复制,用户空间和内核空间的复制。\n * 直接内存缓冲区:优缺点和堆内存缓冲区整好相反。 经验表明ByteBuf的最佳实践是在IO通信线程的读写缓冲区使用DirectByteBuf,后端业务的编解码模块使用HeapByteBuf,这样的组合可以达到性能最优。 从内存回收的角度看、ByteBuf分两类,基于对象池的ByteBuf和普通ByteBuf。使用内存池后的Netty在高并发和高负载环境下内存和GC更加平稳。 15.2.1 AbstractByteBuf源码分析 实现ByteBuf的一些公共属性和功能。 主要成员变量\n\n static final ResourceLeakDetector leakDetector =\n ResourceLeakDetectorFactory.instance().newResourceLeakDetector(ByteBuf.class);//用于对象是否泄漏,定义为static,意味着所有byteBuf共享\n\n int readerIndex;//读索引\n int writerIndex;//写索引\n private int markedReaderIndex;//读mark\n private int markedWriterIndex;//写mark\n private int maxCapacity;//最大容量\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\nByte数组不在这里,因为AbstractByteBuf无法确定使用直接内存还是堆内存。 readBytes(ByteBuf dst, int dstIndex, int length)\n\npublic ByteBuf readBytes(ByteBuf dst, int dstIndex, int length) {\n checkReadableBytes(length);//校验可读性\n getBytes(readerIndex, dst, dstIndex, length);//读取。从readerIndex开始读取length个字节到目标数组中\n readerIndex += length;//移动读指针\n return this;\n }\n\n\n1\n2\n3\n4\n5\n6\n\n\n再看一下checkReadableBytes():\n\n/**\n * Throws an {@link IndexOutOfBoundsException} if the current\n * {@linkplain #readableBytes() readable bytes} of this buffer is less\n * than the specified value.\n */\n protected final void checkReadableBytes(int minimumReadableBytes) {\n if (minimumReadableBytes < 0) {\n throw new IllegalArgumentException(\"minimumReadableBytes: \" + minimumReadableBytes + \" (expected: >= 0)\");\n }\n checkReadableBytes0(minimumReadableBytes);\n }\n \nprivate void checkReadableBytes0(int minimumReadableBytes) {\n ensureAccessible();\n if (readerIndex > writerIndex - minimumReadableBytes) {\n throw new IndexOutOfBoundsException(String.format(\n \"readerIndex(%d) + length(%d) exceeds writerIndex(%d): %s\",\n readerIndex, minimumReadableBytes, writerIndex, this));\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\nwriteBytes(byte[] src, int srcIndex, int length)\n\npublic ByteBuf writeBytes(byte[] src, int srcIndex, int length) {\n ensureWritable(length);//可写校验和扩容\n setBytes(writerIndex, src, srcIndex, length);//从writerIndex开始写length长度\n writerIndex += length;//移动写指针\n return this;\n }\n\n\n1\n2\n3\n4\n5\n6\n\n\nensureWritable(int minWritableBytes)\n\npublic ByteBuf ensureWritable(int minWritableBytes) {\n if (minWritableBytes < 0) {\n throw new IllegalArgumentException(String.format(\n \"minWritableBytes: %d (expected: >= 0)\", minWritableBytes));\n }\n ensureWritable0(minWritableBytes);\n return this;\n }\n \nfinal void ensureWritable0(int minWritableBytes) {\n ensureAccessible();//检查这个buf是否还有引用(如果已经没有引用那就没必要在写了)\n if (minWritableBytes <= writableBytes()) {//写入的字节小于可写字节,校验通过\n return;\n }\n\n if (minWritableBytes > maxCapacity - writerIndex) {//写入的字节大于最大可写入字节,抛异常\n throw new IndexOutOfBoundsException(String.format(\n \"writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s\",\n writerIndex, minWritableBytes, maxCapacity, this));\n }\n\n // Normalize the current capacity to the power of 2.\n int newCapacity = alloc().calculateNewCapacity(writerIndex + minWritableBytes, maxCapacity);\n\n // Adjust to the new capacity.\n capacity(newCapacity);\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n\n\n重用缓冲区\n\npublic ByteBuf discardReadBytes() {\n ensureAccessible();\n if (readerIndex == 0) {\n return this;\n }\n\n if (readerIndex != writerIndex) {\n setBytes(0, this, readerIndex, writerIndex - readerIndex);//复制缓冲区\n writerIndex -= readerIndex;//重置写指针\n adjustMarkers(readerIndex);//调整mark指针\n readerIndex = 0;//重置读指针\n } else {\n adjustMarkers(readerIndex);\n writerIndex = readerIndex = 0;\n }\n return this;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n\n\n15.2.2 AbstractReferenceCountedByteBuf源码分析\n\npublic abstract class AbstractReferenceCountedByteBuf extends AbstractByteBuf {\n\n private static final AtomicIntegerFieldUpdater refCntUpdater =\n AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, \"refCnt\");//通过原子的方式对成员变量进行更新,消除锁\n\n private volatile int refCnt;//跟踪对象的引用次数,采用CAS对其自增1,默认值为1\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n15.2.2 UnPooledHeapByteBuf源码分析 非池化的基于堆内存,频繁的大块内存分配和回收会对性能造成影响,但是相比对外内存的申请和释放,成本还是低一些。 相比HeapByteBuf, UnPooledHeapByteBuf的实现更加加单,也不容易出现内存管理方面的问题,因此在满足性能的条件下,推荐使用UnPooledHeapByteBuf。\n\npublic class UnpooledHeapByteBuf extends AbstractReferenceCountedByteBuf {\nprivate final ByteBufAllocator alloc;\n byte[] array;//这里直接使用JDK的ByteBuffer也可以,之所以使用Byte数组是因为性能和便捷的位操作\n private ByteBuffer tmpNioBuf;//用于实现将netty的byteBuf转换为JDK的ByteBuffer\n }\n\n\n1\n2\n3\n4\n5\n\n\n转换JDK Buffer netty基于byte数组实现 jdk的nio buf提供wrap方法,可直接实现 转换 看下转换方法\n\npublic ByteBuffer nioBuffer(int index, int length) {\n ensureAccessible();\n return ByteBuffer.wrap(array, index, length).slice();\n }\npublic ByteBuffer slice() {\n return new HeapByteBuffer(hb,//仍然使用的是原buffer的全局数组,只是改变了position和limit的位置,所以新buf和原buf内容是相互影响的\n -1,\n 0,\n this.remaining(),\n this.remaining(),\n this.position() + offset);\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\nslice方法的作用:copy position到limit之间的内容, 15.2.3 pooledByteBuf内存池原理分析 PoolArena是netty的内存池显现类。 为了集中管理内存,提供内存申请是释放的效率,很多框架会申请一大块内存,提供相应的接口分配和释放内存,这样就不再频繁的使用系统调用来使用内存,可以提高性能。预先申请的那块内存就叫Memory Arena。PoolArena是netty对Memory Arena的实现。 Netty的PoolArena由多个chunk组成,每个chunk由多个Page组成。 PoolArena源码:\n\nabstract class PoolArena implements PoolArenaMetric {\n static final boolean HAS_UNSAFE = PlatformDependent.hasUnsafe();\n\n enum SizeClass {\n Tiny,\n Small,\n Normal\n }\n\n static final int numTinySubpagePools = 512 >>> 4;\n\n final PooledByteBufAllocator parent;\n\n private final int maxOrder;\n final int pageSize;\n final int pageShifts;\n final int chunkSize;\n final int subpageOverflowMask;\n final int numSmallSubpagePools;\n final int directMemoryCacheAlignment;\n final int directMemoryCacheAlignmentMask;\n private final PoolSubpage[] tinySubpagePools;\n private final PoolSubpage[] smallSubpagePools;\n\n private final PoolChunkList q050;\n private final PoolChunkList q025;\n private final PoolChunkList q000;\n private final PoolChunkList qInit;\n private final PoolChunkList q075;\n private final PoolChunkList q100;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n\n\nPoolChunk的实现 PoolChunk负责多个Page的内存管理,PoolChunk将其负责的多个Page构建成一棵二叉树。 假设一个chunk由16个page组成,则其组织形式: Page大小是4字节,chunk大小是64字节。 每个节点都记录自己在整个Memory Arena中的偏移地址,一旦被分配,则该节点及其子节点在接下来的内存分配过程中会被忽略。 举例来说,我们申请16个字节空间,则第三层的某个节点会被标记为已分配,则再次分配内存的时候会从其他三个节点中分配。 分配内存时对树采用深度优先算法,但是从哪棵子树开始深度遍历是随机的。 PoolSubPage的实现 申请内存小于一个page,则内存分配在page中完成,每个page会被分为大小相等的多个块。 被分的单位块大小等于第一次申请的内存大小,例如一个Page8字节,第一次申请2字节,则该page被切分成4块,每块2字节。而且这个page以后只能用于分配2字节的内存申请,如果再来一个4字节的内存申请,只能在另一个Page中申请。 Page使用标识位来表示内存块是否可用。维护一个long数组,每个位表示一个块的使用情况。 例如page为128字节,第一次申请内存为1字节,则该page被分为128块,则long数组中有2个元素,(每个long64位,两个long可以表示128位)。0、1表示该块是否可用。 15.2.4 PooledDirectByteBuf内存池原理分析 创建字节缓冲区 由于采用内存池实现,所以创建PooledDirectByteBuf对象不能new一个实例,而是从内存池获取。然后设置引用计数器。\n\nstatic PooledDirectByteBuf newInstance(int maxCapacity) {\n PooledDirectByteBuf buf = RECYCLER.get();\n buf.reuse(maxCapacity);\n return buf;\n }\nfinal void reuse(int maxCapacity) {\n maxCapacity(maxCapacity);\n setRefCnt(1);\n setIndex0(0, 0);\n discardMarks();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n复制字节缓冲区 会从内存池中获取一个新的buffer而不是new一个。\n\n\n# 15.3 ByteBuffer相关辅助类\n\n15.3.1 ByteBufHolder 对消息体进行包装和抽象,不同的子类有不同的实现。 实现ByteBufHolder的子类可以自己实现一些实用的方法。 Netty也有一些子类继承自ByteBufHolder。 15.3.2 ByteBufAllocator 字节缓冲区分配器,其实现类有两种:基于池的和普通的。 其API: 15.3.3 CompositeByteBuf 允许将多个ByteBuf组装到一起。 使用场景:如某协议包含消息头和消息体,当对消息进行编码的时候需要进行整合。 这种场景有两种实现方式:\n\n * 将一个buf复制到另一个buf。或者创建一个新的buf将两个buf都放到新的buf。\n * 通过List或其他集合容器,将两个buf都放入容器统一维护和处理。\n\n看下源码:\n\npublic class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements Iterable {\n\n private static final ByteBuffer EMPTY_NIO_BUFFER = Unpooled.EMPTY_BUFFER.nioBuffer();\n private static final Iterator EMPTY_ITERATOR = Collections.emptyList().iterator();\n\n private final ByteBufAllocator alloc;\n private final boolean direct;\n private final ComponentList components;//维护buf的容器\n private final int maxNumComponents;\n\n private boolean freed;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n再看下ComponentList:\n\nprivate static final class ComponentList extends ArrayList {\n\n ComponentList(int initialCapacity) {\n super(initialCapacity);\n }\n\n // Expose this methods so we not need to create a new subList just to remove a range of elements.\n @Override\n public void removeRange(int fromIndex, int toIndex) {\n super.removeRange(fromIndex, toIndex);\n }\n }\n\nprivate static final class Component {\n final ByteBuf buf;\n final int length;\n int offset;//在集合中的位置偏移\n int endOffset;\n\n Component(ByteBuf buf) {\n this.buf = buf;\n length = buf.readableBytes();\n }\n\n void freeIfNecessary() {\n buf.release(); // We should not get a NPE here. If so, it must be a bug.\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n\n\n15.3.4 ByteBufUtil 几个常用的工具方法:\n\n * encodeString\n * decodeString\n * hexDump", - "normalizedContent": "# 15.2 bytebuf源码分析\n\n继承关系 从内存分配的角度看,bytebuf分为两类:\n\n * 堆内存缓冲区:优点是内存的分配和回收快。缺点是进行io读写时需要一次内存复制,用户空间和内核空间的复制。\n * 直接内存缓冲区:优缺点和堆内存缓冲区整好相反。 经验表明bytebuf的最佳实践是在io通信线程的读写缓冲区使用directbytebuf,后端业务的编解码模块使用heapbytebuf,这样的组合可以达到性能最优。 从内存回收的角度看、bytebuf分两类,基于对象池的bytebuf和普通bytebuf。使用内存池后的netty在高并发和高负载环境下内存和gc更加平稳。 15.2.1 abstractbytebuf源码分析 实现bytebuf的一些公共属性和功能。 主要成员变量\n\n static final resourceleakdetector leakdetector =\n resourceleakdetectorfactory.instance().newresourceleakdetector(bytebuf.class);//用于对象是否泄漏,定义为static,意味着所有bytebuf共享\n\n int readerindex;//读索引\n int writerindex;//写索引\n private int markedreaderindex;//读mark\n private int markedwriterindex;//写mark\n private int maxcapacity;//最大容量\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\nbyte数组不在这里,因为abstractbytebuf无法确定使用直接内存还是堆内存。 readbytes(bytebuf dst, int dstindex, int length)\n\npublic bytebuf readbytes(bytebuf dst, int dstindex, int length) {\n checkreadablebytes(length);//校验可读性\n getbytes(readerindex, dst, dstindex, length);//读取。从readerindex开始读取length个字节到目标数组中\n readerindex += length;//移动读指针\n return this;\n }\n\n\n1\n2\n3\n4\n5\n6\n\n\n再看一下checkreadablebytes():\n\n/**\n * throws an {@link indexoutofboundsexception} if the current\n * {@linkplain #readablebytes() readable bytes} of this buffer is less\n * than the specified value.\n */\n protected final void checkreadablebytes(int minimumreadablebytes) {\n if (minimumreadablebytes < 0) {\n throw new illegalargumentexception(\"minimumreadablebytes: \" + minimumreadablebytes + \" (expected: >= 0)\");\n }\n checkreadablebytes0(minimumreadablebytes);\n }\n \nprivate void checkreadablebytes0(int minimumreadablebytes) {\n ensureaccessible();\n if (readerindex > writerindex - minimumreadablebytes) {\n throw new indexoutofboundsexception(string.format(\n \"readerindex(%d) + length(%d) exceeds writerindex(%d): %s\",\n readerindex, minimumreadablebytes, writerindex, this));\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\nwritebytes(byte[] src, int srcindex, int length)\n\npublic bytebuf writebytes(byte[] src, int srcindex, int length) {\n ensurewritable(length);//可写校验和扩容\n setbytes(writerindex, src, srcindex, length);//从writerindex开始写length长度\n writerindex += length;//移动写指针\n return this;\n }\n\n\n1\n2\n3\n4\n5\n6\n\n\nensurewritable(int minwritablebytes)\n\npublic bytebuf ensurewritable(int minwritablebytes) {\n if (minwritablebytes < 0) {\n throw new illegalargumentexception(string.format(\n \"minwritablebytes: %d (expected: >= 0)\", minwritablebytes));\n }\n ensurewritable0(minwritablebytes);\n return this;\n }\n \nfinal void ensurewritable0(int minwritablebytes) {\n ensureaccessible();//检查这个buf是否还有引用(如果已经没有引用那就没必要在写了)\n if (minwritablebytes <= writablebytes()) {//写入的字节小于可写字节,校验通过\n return;\n }\n\n if (minwritablebytes > maxcapacity - writerindex) {//写入的字节大于最大可写入字节,抛异常\n throw new indexoutofboundsexception(string.format(\n \"writerindex(%d) + minwritablebytes(%d) exceeds maxcapacity(%d): %s\",\n writerindex, minwritablebytes, maxcapacity, this));\n }\n\n // normalize the current capacity to the power of 2.\n int newcapacity = alloc().calculatenewcapacity(writerindex + minwritablebytes, maxcapacity);\n\n // adjust to the new capacity.\n capacity(newcapacity);\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n\n\n重用缓冲区\n\npublic bytebuf discardreadbytes() {\n ensureaccessible();\n if (readerindex == 0) {\n return this;\n }\n\n if (readerindex != writerindex) {\n setbytes(0, this, readerindex, writerindex - readerindex);//复制缓冲区\n writerindex -= readerindex;//重置写指针\n adjustmarkers(readerindex);//调整mark指针\n readerindex = 0;//重置读指针\n } else {\n adjustmarkers(readerindex);\n writerindex = readerindex = 0;\n }\n return this;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n\n\n15.2.2 abstractreferencecountedbytebuf源码分析\n\npublic abstract class abstractreferencecountedbytebuf extends abstractbytebuf {\n\n private static final atomicintegerfieldupdater refcntupdater =\n atomicintegerfieldupdater.newupdater(abstractreferencecountedbytebuf.class, \"refcnt\");//通过原子的方式对成员变量进行更新,消除锁\n\n private volatile int refcnt;//跟踪对象的引用次数,采用cas对其自增1,默认值为1\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n15.2.2 unpooledheapbytebuf源码分析 非池化的基于堆内存,频繁的大块内存分配和回收会对性能造成影响,但是相比对外内存的申请和释放,成本还是低一些。 相比heapbytebuf, unpooledheapbytebuf的实现更加加单,也不容易出现内存管理方面的问题,因此在满足性能的条件下,推荐使用unpooledheapbytebuf。\n\npublic class unpooledheapbytebuf extends abstractreferencecountedbytebuf {\nprivate final bytebufallocator alloc;\n byte[] array;//这里直接使用jdk的bytebuffer也可以,之所以使用byte数组是因为性能和便捷的位操作\n private bytebuffer tmpniobuf;//用于实现将netty的bytebuf转换为jdk的bytebuffer\n }\n\n\n1\n2\n3\n4\n5\n\n\n转换jdk buffer netty基于byte数组实现 jdk的nio buf提供wrap方法,可直接实现 转换 看下转换方法\n\npublic bytebuffer niobuffer(int index, int length) {\n ensureaccessible();\n return bytebuffer.wrap(array, index, length).slice();\n }\npublic bytebuffer slice() {\n return new heapbytebuffer(hb,//仍然使用的是原buffer的全局数组,只是改变了position和limit的位置,所以新buf和原buf内容是相互影响的\n -1,\n 0,\n this.remaining(),\n this.remaining(),\n this.position() + offset);\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\nslice方法的作用:copy position到limit之间的内容, 15.2.3 pooledbytebuf内存池原理分析 poolarena是netty的内存池显现类。 为了集中管理内存,提供内存申请是释放的效率,很多框架会申请一大块内存,提供相应的接口分配和释放内存,这样就不再频繁的使用系统调用来使用内存,可以提高性能。预先申请的那块内存就叫memory arena。poolarena是netty对memory arena的实现。 netty的poolarena由多个chunk组成,每个chunk由多个page组成。 poolarena源码:\n\nabstract class poolarena implements poolarenametric {\n static final boolean has_unsafe = platformdependent.hasunsafe();\n\n enum sizeclass {\n tiny,\n small,\n normal\n }\n\n static final int numtinysubpagepools = 512 >>> 4;\n\n final pooledbytebufallocator parent;\n\n private final int maxorder;\n final int pagesize;\n final int pageshifts;\n final int chunksize;\n final int subpageoverflowmask;\n final int numsmallsubpagepools;\n final int directmemorycachealignment;\n final int directmemorycachealignmentmask;\n private final poolsubpage[] tinysubpagepools;\n private final poolsubpage[] smallsubpagepools;\n\n private final poolchunklist q050;\n private final poolchunklist q025;\n private final poolchunklist q000;\n private final poolchunklist qinit;\n private final poolchunklist q075;\n private final poolchunklist q100;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n\n\npoolchunk的实现 poolchunk负责多个page的内存管理,poolchunk将其负责的多个page构建成一棵二叉树。 假设一个chunk由16个page组成,则其组织形式: page大小是4字节,chunk大小是64字节。 每个节点都记录自己在整个memory arena中的偏移地址,一旦被分配,则该节点及其子节点在接下来的内存分配过程中会被忽略。 举例来说,我们申请16个字节空间,则第三层的某个节点会被标记为已分配,则再次分配内存的时候会从其他三个节点中分配。 分配内存时对树采用深度优先算法,但是从哪棵子树开始深度遍历是随机的。 poolsubpage的实现 申请内存小于一个page,则内存分配在page中完成,每个page会被分为大小相等的多个块。 被分的单位块大小等于第一次申请的内存大小,例如一个page8字节,第一次申请2字节,则该page被切分成4块,每块2字节。而且这个page以后只能用于分配2字节的内存申请,如果再来一个4字节的内存申请,只能在另一个page中申请。 page使用标识位来表示内存块是否可用。维护一个long数组,每个位表示一个块的使用情况。 例如page为128字节,第一次申请内存为1字节,则该page被分为128块,则long数组中有2个元素,(每个long64位,两个long可以表示128位)。0、1表示该块是否可用。 15.2.4 pooleddirectbytebuf内存池原理分析 创建字节缓冲区 由于采用内存池实现,所以创建pooleddirectbytebuf对象不能new一个实例,而是从内存池获取。然后设置引用计数器。\n\nstatic pooleddirectbytebuf newinstance(int maxcapacity) {\n pooleddirectbytebuf buf = recycler.get();\n buf.reuse(maxcapacity);\n return buf;\n }\nfinal void reuse(int maxcapacity) {\n maxcapacity(maxcapacity);\n setrefcnt(1);\n setindex0(0, 0);\n discardmarks();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n复制字节缓冲区 会从内存池中获取一个新的buffer而不是new一个。\n\n\n# 15.3 bytebuffer相关辅助类\n\n15.3.1 bytebufholder 对消息体进行包装和抽象,不同的子类有不同的实现。 实现bytebufholder的子类可以自己实现一些实用的方法。 netty也有一些子类继承自bytebufholder。 15.3.2 bytebufallocator 字节缓冲区分配器,其实现类有两种:基于池的和普通的。 其api: 15.3.3 compositebytebuf 允许将多个bytebuf组装到一起。 使用场景:如某协议包含消息头和消息体,当对消息进行编码的时候需要进行整合。 这种场景有两种实现方式:\n\n * 将一个buf复制到另一个buf。或者创建一个新的buf将两个buf都放到新的buf。\n * 通过list或其他集合容器,将两个buf都放入容器统一维护和处理。\n\n看下源码:\n\npublic class compositebytebuf extends abstractreferencecountedbytebuf implements iterable {\n\n private static final bytebuffer empty_nio_buffer = unpooled.empty_buffer.niobuffer();\n private static final iterator empty_iterator = collections.emptylist().iterator();\n\n private final bytebufallocator alloc;\n private final boolean direct;\n private final componentlist components;//维护buf的容器\n private final int maxnumcomponents;\n\n private boolean freed;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n再看下componentlist:\n\nprivate static final class componentlist extends arraylist {\n\n componentlist(int initialcapacity) {\n super(initialcapacity);\n }\n\n // expose this methods so we not need to create a new sublist just to remove a range of elements.\n @override\n public void removerange(int fromindex, int toindex) {\n super.removerange(fromindex, toindex);\n }\n }\n\nprivate static final class component {\n final bytebuf buf;\n final int length;\n int offset;//在集合中的位置偏移\n int endoffset;\n\n component(bytebuf buf) {\n this.buf = buf;\n length = buf.readablebytes();\n }\n\n void freeifnecessary() {\n buf.release(); // we should not get a npe here. if so, it must be a bug.\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n\n\n15.3.4 bytebufutil 几个常用的工具方法:\n\n * encodestring\n * decodestring\n * hexdump", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, { "title": "ChannelPipeline和ChannelHandler", "frontmatter": { @@ -5267,42 +5598,6 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, - { - "title": "EventLoop和EventLoopGroup", - "frontmatter": { - "title": "EventLoop和EventLoopGroup", - "date": "2022-05-21T23:37:23.000Z", - "permalink": "/pages/e7aa60/", - "categories": [ - "中间件", - "Netty" - ], - "tags": [ - null - ] - }, - "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/10.Netty/040.EventLoop%E5%92%8CEventLoopGroup.html", - "relativePath": "04.中间件/10.Netty/040.EventLoop和EventLoopGroup.md", - "key": "v-62f3fa3e", - "path": "/pages/e7aa60/", - "headers": [ - { - "level": 2, - "title": "EventLoop和EventLoopGroup", - "slug": "eventloop和eventloopgroup", - "normalizedTitle": "eventloop和eventloopgroup", - "charIndex": 2 - } - ], - "headersStr": "EventLoop和EventLoopGroup", - "content": "# EventLoop和EventLoopGroup\n\nEventLoop负责处理连接中的事件, 和EvenvLoop有关的几个概念是:Channel, EventLoopGroup。\n\n * 一个EventLoopGroup包含一个或多个EventLoop。\n * 一个EventLoop对应于一个线程,所有EventLoop处理的I/O事件都在这个线程中完成。\n * 一个Channel对应唯一个一个EventLoop。\n * 一个EventLoop可以对应多个Channel。\n\n对于基于Netty的网络服务,Client端启动需要一个EventLoopGroup, Server端启动需要两个EventLoopGroup, 因为Server端需要两种Channel, 一种是ServerChannel, 只有一个,负责接受连接,另一种是用于处理连接的一组Channel。\n\nEventLoop继承了concurrent包里的 ScheduledExecutorService,这使得它可以接受Callable或者Runnable并执行。EventLoop中的parent()方法返回包含这个EventLoop的EventLoopGroup", - "normalizedContent": "# eventloop和eventloopgroup\n\neventloop负责处理连接中的事件, 和evenvloop有关的几个概念是:channel, eventloopgroup。\n\n * 一个eventloopgroup包含一个或多个eventloop。\n * 一个eventloop对应于一个线程,所有eventloop处理的i/o事件都在这个线程中完成。\n * 一个channel对应唯一个一个eventloop。\n * 一个eventloop可以对应多个channel。\n\n对于基于netty的网络服务,client端启动需要一个eventloopgroup, server端启动需要两个eventloopgroup, 因为server端需要两种channel, 一种是serverchannel, 只有一个,负责接受连接,另一种是用于处理连接的一组channel。\n\neventloop继承了concurrent包里的 scheduledexecutorservice,这使得它可以接受callable或者runnable并执行。eventloop中的parent()方法返回包含这个eventloop的eventloopgroup", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, { "title": "Java NIO了解吗?", "frontmatter": { @@ -5590,22 +5885,101 @@ export const siteData = { }, { "level": 2, - "title": "4.1 基础", - "slug": "_4-1-基础", - "normalizedTitle": "4.1 基础", - "charIndex": 13917 + "title": "4.1 基础", + "slug": "_4-1-基础", + "normalizedTitle": "4.1 基础", + "charIndex": 13917 + }, + { + "level": 2, + "title": "4.2 示例", + "slug": "_4-2-示例", + "normalizedTitle": "4.2 示例", + "charIndex": 14101 + } + ], + "headersStr": "1.1 IO概念 1.1.1 Buffer Handler 1.1.1 scatter/gather 1.1.2 虚拟内存 1.1.3 内存页 1.1.4 文件IO 1.1.5 IO流 2.1 Buffer Basic 2.1.1 Buffer的几个属性 2.1.2 Buffer API 2.1.3 访问Buffer 2.1.4 write 2.1.5 Flipping 2.1.6 Draining 2.1.7 Compacting 2.1.9 Mark 2.1.9 Comparing 2.2 创建Buffer 2.3 Duplicating Buffers 2.4 Byte Buffers 2.4.1 Byte Ordering 2.4.2 Direct Buffers 2.4.3 View Buffers 2.4.4 Data Element Views 3.1 basic 3.1.1 Open channel 3.1.2 Using Channels 3.1.3 close Channels 3.2 Scatter/Gather 3.3 File Channels 3.3.1 Accessing Files 3.3.2 File Locking 3.4 Memory-Mapped Files 3.5 Socket Channels 3.5.1 Nonblocking Mode 3.5.2 ServerSocketChannel 3.5.4 DatagramChannel 3.5.5 Pipes 4.1 基础 4.2 示例", + "content": "@TOC\n\n\n# 第一章 IO介绍\n\n\n# 1.1 IO概念\n\n\n# 1.1.1 Buffer Handler\n\n内存分为用户空间(User space),内核空间(Kernel space)。 用户空间的数据读写需要通过内存空间传递。 如下图所示,用户需要读数据,则向cpu发送一个读请求,cpu相应这个请求控制disk controller从disk读取数据到内核buffer,然后再把内核buffer的数据推送给用户空间的buffer。 为什么要区分用户空间和内核空间呢? 每个进程有独立的内存空间好处在于多个进程的内存分配互不影响。如果多个进程共享一块内存,那么内存分配的时候就得排队。\n\n为什么用户空间不能直接从disk读数据呢?\n\n * 硬件控制器只能处理固定大小的数据,用户空间需要的可能是数据块大小异常或者不对齐的数据,内核空间起作到数拆分、重组的作用。\n * 硬件设备通常不允许直接操作虚拟内存,即用户空间\n\nDMA是什么? DMA(Direct Memory Access,直接存储器访问)。 在DMA之前通过中断CPU来传输数据。CPU响应中断,控制总线传输数据。 DMA不需要CPU参与数据传输。DMA可以和CPU交互请求控制总线传输数据。\n\n\n# 1.1.1 scatter/gather\n\n分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。 聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。\n\n比如某个协议的消息固定消息头128字节,消息体1024字节,消息尾128字节。我们想要分别处理消息头,消息体,消息尾。 使用scatter示例代码:\n\nByteBuffer header = ByteBuffer.allocate(128);\nByteBuffer body = ByteBuffer.allocate(1024);\nByteBuffer tail = ByteBuffer.allocate(128);\n\nByteBuffer[] bufferArray = { header, body,tail };\nchannel.read(bufferArray);\n\n\n1\n2\n3\n4\n5\n6\n\n\n注意buffer首先被插入到数组,然后再将数组作为channel.read() 的输入参数。read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。 所以。\n\nscatter不适合处理动态消息,相反,gather适合处理动态消息。 buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与Scattering Reads相反,Gathering Writes能较好的处理动态消息。\n\n\n# 1.1.2 虚拟内存\n\n虚拟内存是指使用虚拟内存地址代替物理内存地址。\n\n * 会有多个虚拟内存指向同一个物理内存\n * 虚拟内存可能会大于物理内存 上面说disk controller不可以直接把数据读到用户空间。虚拟内存可以通过虚拟地址映射内核空间的方式做到这一点。 如下图,内核空间的buffer对用户空间也是可见的。这就是netty零拷贝的原理,这减少了buffer在用户空间和内核空间的拷贝,非常有意义。\n\n\n# 1.1.3 内存页\n\n操作系统将内存按固定字节分页。 内存读取的基本单位是页。\n\nMMU(Memory Management Unit):内存管理单元,保存虚拟内存和物理内存的映射关系,处在CPU和内存之间,起到将虚拟内存转换为物理内存的作用。\n\n\n# 1.1.4 文件IO\n\n文件系统:文件IO发生在文件系统。文件保存在磁盘,磁盘扇区类似内存分页的概念。内存文件读取通过文件系统间接操作磁盘文件。 文件零拷贝如下图。避免了文件系统内存页和用户内存页之间的拷贝。\n\n\n# 1.1.5 IO流\n\n上面说的IO都是面向块的IO。还有面向流的IO。 大多数操作系统支持将流置为非阻塞模式,额外设置一个进程用于检查流上是否有输入,因此流本身是不被阻塞的。 网络IO一般都是流IO。 多路复用:用一个进程管理多个非阻塞IO流的状态。\n\n\n# 第二章 Buffer\n\n标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。\n\n\n# 2.1 Buffer Basic\n\n\n# 2.1.1 Buffer的几个属性\n\n * capacity\n * limit\n * position\n * mark\n\n\n# 2.1.2 Buffer API\n\npackage java.nio;\npublic abstract class Buffer {\npublic final int capacity()\npublic final int position()\npublic final Buffer position (int newPosition) public final int limit()\n }\npublic final Buffer limit (int newLimit)\npublic final Buffer mark()\npublic final Buffer reset()\npublic final Buffer clear()\npublic final Buffer flip()\npublic final Buffer rewind()\npublic final int remaining()\npublic final boolean hasRemaining()\npublic abstract boolean isReadOnly();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n\n\n\n# 2.1.3 访问Buffer\n\npublic abstract class ByteBuffer\n extends Buffer implements Comparable\n {\n// This is a partial API listing\npublic abstract byte get();\npublic abstract byte get (int index);\npublic abstract ByteBuffer put (byte b);\npublic abstract ByteBuffer put (int index, byte b);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n\n# 2.1.4 write\n\nFigure 2-2的buffer经过下面这个代码会变成Figure 2-3的样子:\n\nbuffer.put((byte)'H').put((byte)'e').put((byte)'l')\n.put((byte)'l').put( (byte)'o');\n\n\n1\n2\n\n\n也可以按绝对位置写入:\n\nbuffer.put(0, (byte)'M').put((byte)'w');\n\n\n1\n\n\n\n\n\n# 2.1.5 Flipping\n\n当我们想要写出buffer的内容时,我们需要用position和limit指针指向时机内容的起始位置。类似这样:\n\nbuffer.limit(buffer.position()).position(0);\n\n\n1\n\n\n这等同于buffer.flip(); 调用flip()后2-4会变成2-5 说白了,flip()改变position和limit两个指针。 rewind()只改变position指针,等同于:\n\nbuffer.position(0);\n\n\n1\n\n\n\n# 2.1.6 Draining\n\nhasRemaining()可以判断是否到达limit指针 下面是一个实现,将buffer内容读到一个数组中\n\nfor (int i = 0; buffer.hasRemaining(), i++) {\n myByteArray [i] = buffer.get();\n}\n\n\n1\n2\n3\n\n\nremaining()会返回position到limit的数量。 将buffer内容读到一个数组中另一种实现方式:\n\nint count = buffer.remaining();\nfor (int i = 0; i < count, i++) {\n myByteArray [i] = buffer.get();\n}\n\n\n1\n2\n3\n4\n\n\n\n# 2.1.7 Compacting\n\n当你drain一部分数据想要继续fill数据的时候,你需要把还未drain的数据整体前移。 buffer.compact();可以完成这个工作。 调用buffer.compact()的效果就是2-6到2-7\n\n\n# 2.1.9 Mark\n\nmark()方法会用mark指针指向position的位置。\n\nbuffer.position(2).mark().position(4);\n\n\n1\n\n\nreset()方法会把position指针指向mark的位置。\n\nbuffer.reset();\n\n\n1\n\n\n\n\n\n# 2.1.9 Comparing\n\npublic abstract class ByteBuffer\n extends Buffer implements Comparable\n {\n // This is a partial API listing\npublic boolean equals (Object ob)\npublic int compareTo (Object ob)\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nequals()返回true需要满足三个条件:\n\n * Buffer包含相同类型的元素\n * 两个Buffer的remaining()返回值相同\n * remaining data的序列相同 下面是两个例子:\n\n\n# 2.2 创建Buffer\n\n创建新buffer的两种方式:allocation or wrapping\n\npublic abstract class CharBuffer\nextends Buffer implements CharSequence, Comparable\n{\n}\n// This is a partial API listing\npublic static CharBuffer allocate (int capacity)\npublic static CharBuffer wrap (char [] array)\npublic static CharBuffer wrap (char [] array, int offset, int length)\npublic final boolean hasArray()\npublic final char [] array()\npublic final int arrayOffset()\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n通过allocation创建的buffer是分配一块固定大小的堆内存作为buffer存储空间。 wrapping创建的buffer是创建一个buffer对象指向一个array空间。\n\n\n# 2.3 Duplicating Buffers\n\npublic abstract class CharBuffer\nextends Buffer implements CharSequence, Comparable\n {\n}\n// This is a partial API listing\npublic abstract CharBuffer duplicate();\npublic abstract CharBuffer asReadOnlyBuffer(); \npublic abstract CharBuffer slice();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\nduplicate()创建一个新的buffer,和原buffer一样。 两个buffer共享数据元素 但是他们有独立的position, limit, and mark 一个buffer更改了数据元素对另一个buffer可见 只读、直接内存这两个属性也可以通过duplicate()继承 举个例子:\n\nCharBuffer buffer = CharBuffer.allocate (8); \nbuffer.position (3).limit (6).mark().position (5); \nCharBuffer dupeBuffer = buffer.duplicate(); buffer.clear();\n\n\n1\n2\n3\n\n\nasReadOnlyBuffer()和duplicate()功能类似,只是asReadOnlyBuffer()返回一个只读的视图。 slice() 返回一个新的buffer视图,不过 new_position = original_position new_capacity = original_limit - original_position 举个例子来看slice():\n\nCharBuffer buffer = CharBuffer.allocate (8); \nbuffer.position (3).limit (5);\nCharBuffer sliceBuffer = buffer.slice();\n\n\n1\n2\n3\n\n\n\n\n\n# 2.4 Byte Buffers\n\n\n# 2.4.1 Byte Ordering\n\n原始数据类型在内存中的存储方式是多个字节的连续序列。 举个例子一个32-bit int 值为 0x037FB4C7的内存存储序列可能为2-14也可能为2-15: 这就是所谓的大端设计和小端设计。 采用大端还是小端通常由硬件的设计者决定而不是小端的设计者。 IP协议定义的字节顺序是大端。所有使用IP协议的多字节数值必须在网络字节序和本地主机字节序做转换。\n\npackage java.nio;\npublic final class ByteOrder\n{\npublic static final ByteOrder BIG_ENDIAN public static final ByteOrder LITTLE_ENDIAN\npublic static ByteOrder nativeOrder()\npublic String toString()\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n除了ByteBuffer之外,其他buffer的order()适中返回同一个值,ByteOrder.nativeOrder()。\n\n\n# 2.4.2 Direct Buffers\n\npublic abstract class ByteBuffer extends Buffer implements Comparable {\n// This is a partial API listing\npublic static ByteBuffer allocate (int capacity) ;\npublic static ByteBuffer allocateDirect (int capacity) ;\npublic abstract boolean isDirect();\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\nallocateDirect可以直接在堆外开辟一个buffer。 堆外buffer节省了用户空间和系统空间的buffer拷贝,提升效率。\n\n\n# 2.4.3 View Buffers\n\n收到数据的时候你可能要先看一下数据再决定做什么操作,这就需要用到ByteBuffer的视图API。\n\npublic abstract class ByteBuffer extends Buffer implements Comparable {\n// This is a partial API listing\npublic abstract CharBuffer asCharBuffer(); \npublic abstract ShortBuffer asShortBuffer(); \npublic abstract IntBuffer asIntBuffer();\n public abstract LongBuffer asLongBuffer();\n public abstract FloatBuffer asFloatBuffer();\n public abstract DoubleBuffer asDoubleBuffer();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n视图在直接内存buffer上操作更高效。 当本地硬件的字节顺序和buffer中的字节顺序一致时,通过低级代码就可以操作buffer中的数据,而不需要经过字节码的编码解码。\n\n\n# 2.4.4 Data Element Views\n\n当getInt() 被调用时,那么从当前位置开始的4个字节将被打包成一个int返回。 int value = buffer.getInt();将返回buffer中的1-4位,具体返回的数据值与字节序有关系。 int value = buffer.order (ByteOrder.BIG_ENDIAN).getInt(); 返回0x3BC5315E。 int value = buffer.order (ByteOrder.LITTLE_ENDIAN).getInt(); 返回0x5E31C53B。\n\n\n# 第三章 Channels\n\n\n# 3.1 basic\n\n在NIO接口中我们需要使用Channel和Buffer进行IO操作,Channel模拟了流的概念,但是又有不同。数据总是从一个Channel读到一个buffer中,或者从一个buffer中写到channel中。 channel接口的主要实现类如下:\n\n * FileChannel\n * DatagramChannel\n * SocketChannel\n * ServerSocketChannel\n\nJavaNIO Channels和流有一些相似,但是又有些不同:\n\n你可以同时读和写Channels,流Stream只支持单向的读或写(InputStream/OutputStream)\n\n * Channels可以异步的读和写,流Stream是同步的\n * Channels总是读取到buffer或者从buffer中写入\n\n下面分别介绍一下Channel最重要的一些实现类:\n\n * FileChannel : 可以读写文件中的数据\n * DatagramChannel:可以通过UDP协议读写数据\n * SocketChannel:可以通过TCP协议读写数据\n * ServerSocketChannel:允许我们像一个web服务器那样监听TCP链接请求,为每一个链接请求创建一个SocketChannel\n\n\n# 3.1.1 Open channel\n\nFileChannel只能通过getChannel()得到。 SocketChannel有工厂方法可以得到。\n\nSocketChannel sc = SocketChannel.open();\nsc.connect (new InetSocketAddress (\"somehost\", someport));\n\nServerSocketChannel ssc = ServerSocketChannel.open(); ssc.socket().bind (new InetSocketAddress (somelocalport));\nDatagramChannel dc = DatagramChannel.open();\n\nRandomAccessFile raf = new RandomAccessFile (\"somefile\", \"r\"); FileChannel fc = raf.getChannel();\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 3.1.2 Using Channels\n\npublic interface ReadableByteChannel extends Channel {\npublic int read (ByteBuffer dst) throws IOException;\n}\n\npublic interface WritableByteChannel extends Channel {\npublic int write (ByteBuffer src) throws IOException;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nChannels 可以是单向或者双向的。 如果一个class实现了上面两个接口,就是双向的。 Channels可以在阻塞模式和非阻塞模式工作。 只有socket和pipes可以工作在非阻塞模式。\n\n\n# 3.1.3 close Channels\n\n\n# 3.2 Scatter/Gather\n\nScatter:将多个buffer按照顺序合成一个 Gather:将一个字符串按照顺序拆分成多个。逐个填满buffer。\n\n\n# 3.3 File Channels\n\nFile channels 只能以阻塞模式工作。\n\n\n# 3.3.1 Accessing Files\n\npublic abstract class FileChannel extends AbstractChannel \nimplements ByteChannel, GatheringByteChannel, ScatteringByteChannel{\n// This is a partial API listing\n//获取position位置\npublic abstract long position()\n//设置position\npublic abstract void position (long newPosition)\n//从position位置开始读\npublic abstract int read (ByteBuffer dst)\n//从position位置开始读\npublic abstract int read (ByteBuffer dst, long position)\n//从position位置开始写\n public abstract int write (ByteBuffer src)\n//从position位置开始写\npublic abstract int write (ByteBuffer src, long position)\npublic abstract long size()\n//截取文件保留size大小,并移动position到size的位置\npublic abstract void truncate (long size) \n//强制刷新缓存到磁盘文件\npublic abstract void force (boolean metaData)\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n\n\n\n# 3.3.2 File Locking\n\n\n# 3.4 Memory-Mapped Files\n\n\n# 3.5 Socket Channels\n\nThe new socket channels can operate in nonblocking mode and are selectable. These two capabilities enable tremendous scalability and flexibility in large applications. ServerSocketChannel没有实现read和write接口,说明它不需要转发数据。它只负责家庭socket连接,创建SocketChannel。\n\na channel is a conduit to an I/O service and provides methods for interacting with that service\n\n每个socket channels都有一个socket与之对应,调用socket()可以获得。 但并不是所有的sockets都有channels。socket的getChannel()可能但会null;\n\n\n# 3.5.1 Nonblocking Mode\n\n调用configureBlocking (false);设置channel为非阻塞模式。 但是只有持有blockingLock() 返回的对象锁的线程才能修改阻塞模式。\n\nSocket socket = null;\nObject lockObj = serverChannel.blockingLock();\n// have a handle to the lock object, but haven't locked it yet\n// may block here until lock is acquired\nsynchronize (lockObj){\n// This thread now owns the lock; mode can't be changed boolean prevState = serverChannel.isBlocking();\nserverChannel.configureBlocking (false); socket = serverChannel.accept(); serverChannel.configureBlocking (prevState);\n}\n// lock is now released, mode is allowed to change\nif (socket != null) {\ndoSomethingWithTheSocket (socket);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n\n# 3.5.2 ServerSocketChannel\n\nThe ServerSocketChannel class is a channel-based socket listener. 它负责处理socket任务并添加channel。\n\npublic abstract class SocketChannel\n extends AbstractSelectableChannel\n implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel\n{\n//SocketChannel被创建但是并没有连接,open()只是创建一个SocketChannel,还需要调用connect()连接到一个地址\npublic static SocketChannel open() throws IOException\n//创建并连接\npublic static SocketChannel open(SocketAddress remote) throws IOException\n//支持的操作(读、写、连接)\npublic final int validOps()\n、、\npublic abstract Socket socket();\n//是否连接到远程\npublic abstract boolean isConnected();\n//Connect()方法是耗时的,因为需要数据包对话(TCP握手)。如果SocketChannel身上有并发、connect(),isConnectionPending返回true\npublic abstract boolean isConnectionPending();\n//建立连接\npublic abstract boolean connect(SocketAddress remote) throws IOException;\n//任何时候都可以安全的调用\npublic abstract boolean finishConnect() throws IOException;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\nSocket channels是线程安全的。 Socket是面向流的而不是面向数据包的,它能保证到达的顺序是发送的顺序,不能保证维护分组。发送方发送20个字节,接收方read可能直接受到7个字节,其余字节在传输中。所以,多个线程共享一个socket的一端不是好的设计。\n\n\n# 3.5.4 DatagramChannel\n\nsocketchannel为面向连接的流协议(如tcp/ip) datagramchannel为面向无连接的数据包协议(如udp/ip)\n\n\n# 3.5.5 Pipes\n\nPipe用于同一个JVM内不同线程之间的数据交换问题 不同JVM之间的数据交换问题应该使用SocketChannel Pipe类创建了一对channel对象,它们提供了一种回送机制。 Pipe实现两个进程之间的单向数据连接。 Pipe有一个SinkChannel和一个SourceChannel 数据会从SourceChannel读取、被写到SinkChannel\n\npublic abstract class Pipe {\n public static abstract class SourceChannel\n extends AbstractSelectableChannel\n implements ReadableByteChannel, ScatteringByteChannel\n {\n protected SourceChannel(SelectorProvider provider) {\n super(provider);\n }\n public final int validOps() {\n return SelectionKey.OP_READ;\n }\n }\n\n public static abstract class SinkChannel\n extends AbstractSelectableChannel\n implements WritableByteChannel, GatheringByteChannel\n {\n protected SinkChannel(SelectorProvider provider) {\n super(provider);\n }\n public final int validOps() {\n return SelectionKey.OP_WRITE;\n }\n\n }\n protected Pipe() { }\n //从管道读取数据,要访问source通道\n public abstract SourceChannel source();\n //向管道写入数据,要访问Sink通道\n public abstract SinkChannel sink();\n //管道创建\n public static Pipe open() throws IOException {\n return SelectorProvider.provider().openPipe();\n }\n}\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n\n\n\n# 第四章 Selectors\n\nSelector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。\n\n\n# 4.1 基础\n\n * Selector:一个selector可以注册多个channel,一个线程通过管理Selector实现管理多个channel。\n * SelectableChannel:提供channle selectability的通用方法。\n * SelectionKey:封装了channle和selector的注册关系,包含注册的感兴趣的事件。\n\n\n# 4.2 示例\n\n//创建Selector\nSelector selector = Selector.open();\nchannel.configureBlocking(false);\n//注册Selector\nSelectionKey key = channel.register(selector, SelectionKey.OP_READ);\nwhile(true) {\n//单线程阻塞检查Selector中就绪的事件\n int readyChannels = selector.select();\n if(readyChannels == 0) continue;\n Set selectedKeys = selector.selectedKeys();\n Iterator keyIterator = selectedKeys.iterator();\n //循环处理就绪的事件\n while(keyIterator.hasNext()) {\n SelectionKey key = keyIterator.next();\n if(key.isAcceptable()) {\n // a connection was accepted by a ServerSocketChannel.\n } else if (key.isConnectable()) {\n // a connection was established with a remote server.\n } else if (key.isReadable()) {\n // a channel is ready for reading\n } else if (key.isWritable()) {\n // a channel is ready for writing\n }\n keyIterator.remove();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n", + "normalizedContent": "@toc\n\n\n# 第一章 io介绍\n\n\n# 1.1 io概念\n\n\n# 1.1.1 buffer handler\n\n内存分为用户空间(user space),内核空间(kernel space)。 用户空间的数据读写需要通过内存空间传递。 如下图所示,用户需要读数据,则向cpu发送一个读请求,cpu相应这个请求控制disk controller从disk读取数据到内核buffer,然后再把内核buffer的数据推送给用户空间的buffer。 为什么要区分用户空间和内核空间呢? 每个进程有独立的内存空间好处在于多个进程的内存分配互不影响。如果多个进程共享一块内存,那么内存分配的时候就得排队。\n\n为什么用户空间不能直接从disk读数据呢?\n\n * 硬件控制器只能处理固定大小的数据,用户空间需要的可能是数据块大小异常或者不对齐的数据,内核空间起作到数拆分、重组的作用。\n * 硬件设备通常不允许直接操作虚拟内存,即用户空间\n\ndma是什么? dma(direct memory access,直接存储器访问)。 在dma之前通过中断cpu来传输数据。cpu响应中断,控制总线传输数据。 dma不需要cpu参与数据传输。dma可以和cpu交互请求控制总线传输数据。\n\n\n# 1.1.1 scatter/gather\n\n分散(scatter)从channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,channel将从channel中读取的数据“分散(scatter)”到多个buffer中。 聚集(gather)写入channel是指在写操作时将多个buffer的数据写入同一个channel,因此,channel 将多个buffer中的数据“聚集(gather)”后发送到channel。\n\n比如某个协议的消息固定消息头128字节,消息体1024字节,消息尾128字节。我们想要分别处理消息头,消息体,消息尾。 使用scatter示例代码:\n\nbytebuffer header = bytebuffer.allocate(128);\nbytebuffer body = bytebuffer.allocate(1024);\nbytebuffer tail = bytebuffer.allocate(128);\n\nbytebuffer[] bufferarray = { header, body,tail };\nchannel.read(bufferarray);\n\n\n1\n2\n3\n4\n5\n6\n\n\n注意buffer首先被插入到数组,然后再将数组作为channel.read() 的输入参数。read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。 所以。\n\nscatter不适合处理动态消息,相反,gather适合处理动态消息。 buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与scattering reads相反,gathering writes能较好的处理动态消息。\n\n\n# 1.1.2 虚拟内存\n\n虚拟内存是指使用虚拟内存地址代替物理内存地址。\n\n * 会有多个虚拟内存指向同一个物理内存\n * 虚拟内存可能会大于物理内存 上面说disk controller不可以直接把数据读到用户空间。虚拟内存可以通过虚拟地址映射内核空间的方式做到这一点。 如下图,内核空间的buffer对用户空间也是可见的。这就是netty零拷贝的原理,这减少了buffer在用户空间和内核空间的拷贝,非常有意义。\n\n\n# 1.1.3 内存页\n\n操作系统将内存按固定字节分页。 内存读取的基本单位是页。\n\nmmu(memory management unit):内存管理单元,保存虚拟内存和物理内存的映射关系,处在cpu和内存之间,起到将虚拟内存转换为物理内存的作用。\n\n\n# 1.1.4 文件io\n\n文件系统:文件io发生在文件系统。文件保存在磁盘,磁盘扇区类似内存分页的概念。内存文件读取通过文件系统间接操作磁盘文件。 文件零拷贝如下图。避免了文件系统内存页和用户内存页之间的拷贝。\n\n\n# 1.1.5 io流\n\n上面说的io都是面向块的io。还有面向流的io。 大多数操作系统支持将流置为非阻塞模式,额外设置一个进程用于检查流上是否有输入,因此流本身是不被阻塞的。 网络io一般都是流io。 多路复用:用一个进程管理多个非阻塞io流的状态。\n\n\n# 第二章 buffer\n\n标准的io基于字节流和字符流进行操作的,而nio是基于通道(channel)和缓冲区(buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。\n\n\n# 2.1 buffer basic\n\n\n# 2.1.1 buffer的几个属性\n\n * capacity\n * limit\n * position\n * mark\n\n\n# 2.1.2 buffer api\n\npackage java.nio;\npublic abstract class buffer {\npublic final int capacity()\npublic final int position()\npublic final buffer position (int newposition) public final int limit()\n }\npublic final buffer limit (int newlimit)\npublic final buffer mark()\npublic final buffer reset()\npublic final buffer clear()\npublic final buffer flip()\npublic final buffer rewind()\npublic final int remaining()\npublic final boolean hasremaining()\npublic abstract boolean isreadonly();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n\n\n\n# 2.1.3 访问buffer\n\npublic abstract class bytebuffer\n extends buffer implements comparable\n {\n// this is a partial api listing\npublic abstract byte get();\npublic abstract byte get (int index);\npublic abstract bytebuffer put (byte b);\npublic abstract bytebuffer put (int index, byte b);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n\n# 2.1.4 write\n\nfigure 2-2的buffer经过下面这个代码会变成figure 2-3的样子:\n\nbuffer.put((byte)'h').put((byte)'e').put((byte)'l')\n.put((byte)'l').put( (byte)'o');\n\n\n1\n2\n\n\n也可以按绝对位置写入:\n\nbuffer.put(0, (byte)'m').put((byte)'w');\n\n\n1\n\n\n\n\n\n# 2.1.5 flipping\n\n当我们想要写出buffer的内容时,我们需要用position和limit指针指向时机内容的起始位置。类似这样:\n\nbuffer.limit(buffer.position()).position(0);\n\n\n1\n\n\n这等同于buffer.flip(); 调用flip()后2-4会变成2-5 说白了,flip()改变position和limit两个指针。 rewind()只改变position指针,等同于:\n\nbuffer.position(0);\n\n\n1\n\n\n\n# 2.1.6 draining\n\nhasremaining()可以判断是否到达limit指针 下面是一个实现,将buffer内容读到一个数组中\n\nfor (int i = 0; buffer.hasremaining(), i++) {\n mybytearray [i] = buffer.get();\n}\n\n\n1\n2\n3\n\n\nremaining()会返回position到limit的数量。 将buffer内容读到一个数组中另一种实现方式:\n\nint count = buffer.remaining();\nfor (int i = 0; i < count, i++) {\n mybytearray [i] = buffer.get();\n}\n\n\n1\n2\n3\n4\n\n\n\n# 2.1.7 compacting\n\n当你drain一部分数据想要继续fill数据的时候,你需要把还未drain的数据整体前移。 buffer.compact();可以完成这个工作。 调用buffer.compact()的效果就是2-6到2-7\n\n\n# 2.1.9 mark\n\nmark()方法会用mark指针指向position的位置。\n\nbuffer.position(2).mark().position(4);\n\n\n1\n\n\nreset()方法会把position指针指向mark的位置。\n\nbuffer.reset();\n\n\n1\n\n\n\n\n\n# 2.1.9 comparing\n\npublic abstract class bytebuffer\n extends buffer implements comparable\n {\n // this is a partial api listing\npublic boolean equals (object ob)\npublic int compareto (object ob)\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nequals()返回true需要满足三个条件:\n\n * buffer包含相同类型的元素\n * 两个buffer的remaining()返回值相同\n * remaining data的序列相同 下面是两个例子:\n\n\n# 2.2 创建buffer\n\n创建新buffer的两种方式:allocation or wrapping\n\npublic abstract class charbuffer\nextends buffer implements charsequence, comparable\n{\n}\n// this is a partial api listing\npublic static charbuffer allocate (int capacity)\npublic static charbuffer wrap (char [] array)\npublic static charbuffer wrap (char [] array, int offset, int length)\npublic final boolean hasarray()\npublic final char [] array()\npublic final int arrayoffset()\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n通过allocation创建的buffer是分配一块固定大小的堆内存作为buffer存储空间。 wrapping创建的buffer是创建一个buffer对象指向一个array空间。\n\n\n# 2.3 duplicating buffers\n\npublic abstract class charbuffer\nextends buffer implements charsequence, comparable\n {\n}\n// this is a partial api listing\npublic abstract charbuffer duplicate();\npublic abstract charbuffer asreadonlybuffer(); \npublic abstract charbuffer slice();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\nduplicate()创建一个新的buffer,和原buffer一样。 两个buffer共享数据元素 但是他们有独立的position, limit, and mark 一个buffer更改了数据元素对另一个buffer可见 只读、直接内存这两个属性也可以通过duplicate()继承 举个例子:\n\ncharbuffer buffer = charbuffer.allocate (8); \nbuffer.position (3).limit (6).mark().position (5); \ncharbuffer dupebuffer = buffer.duplicate(); buffer.clear();\n\n\n1\n2\n3\n\n\nasreadonlybuffer()和duplicate()功能类似,只是asreadonlybuffer()返回一个只读的视图。 slice() 返回一个新的buffer视图,不过 new_position = original_position new_capacity = original_limit - original_position 举个例子来看slice():\n\ncharbuffer buffer = charbuffer.allocate (8); \nbuffer.position (3).limit (5);\ncharbuffer slicebuffer = buffer.slice();\n\n\n1\n2\n3\n\n\n\n\n\n# 2.4 byte buffers\n\n\n# 2.4.1 byte ordering\n\n原始数据类型在内存中的存储方式是多个字节的连续序列。 举个例子一个32-bit int 值为 0x037fb4c7的内存存储序列可能为2-14也可能为2-15: 这就是所谓的大端设计和小端设计。 采用大端还是小端通常由硬件的设计者决定而不是小端的设计者。 ip协议定义的字节顺序是大端。所有使用ip协议的多字节数值必须在网络字节序和本地主机字节序做转换。\n\npackage java.nio;\npublic final class byteorder\n{\npublic static final byteorder big_endian public static final byteorder little_endian\npublic static byteorder nativeorder()\npublic string tostring()\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n除了bytebuffer之外,其他buffer的order()适中返回同一个值,byteorder.nativeorder()。\n\n\n# 2.4.2 direct buffers\n\npublic abstract class bytebuffer extends buffer implements comparable {\n// this is a partial api listing\npublic static bytebuffer allocate (int capacity) ;\npublic static bytebuffer allocatedirect (int capacity) ;\npublic abstract boolean isdirect();\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\nallocatedirect可以直接在堆外开辟一个buffer。 堆外buffer节省了用户空间和系统空间的buffer拷贝,提升效率。\n\n\n# 2.4.3 view buffers\n\n收到数据的时候你可能要先看一下数据再决定做什么操作,这就需要用到bytebuffer的视图api。\n\npublic abstract class bytebuffer extends buffer implements comparable {\n// this is a partial api listing\npublic abstract charbuffer ascharbuffer(); \npublic abstract shortbuffer asshortbuffer(); \npublic abstract intbuffer asintbuffer();\n public abstract longbuffer aslongbuffer();\n public abstract floatbuffer asfloatbuffer();\n public abstract doublebuffer asdoublebuffer();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n视图在直接内存buffer上操作更高效。 当本地硬件的字节顺序和buffer中的字节顺序一致时,通过低级代码就可以操作buffer中的数据,而不需要经过字节码的编码解码。\n\n\n# 2.4.4 data element views\n\n当getint() 被调用时,那么从当前位置开始的4个字节将被打包成一个int返回。 int value = buffer.getint();将返回buffer中的1-4位,具体返回的数据值与字节序有关系。 int value = buffer.order (byteorder.big_endian).getint(); 返回0x3bc5315e。 int value = buffer.order (byteorder.little_endian).getint(); 返回0x5e31c53b。\n\n\n# 第三章 channels\n\n\n# 3.1 basic\n\n在nio接口中我们需要使用channel和buffer进行io操作,channel模拟了流的概念,但是又有不同。数据总是从一个channel读到一个buffer中,或者从一个buffer中写到channel中。 channel接口的主要实现类如下:\n\n * filechannel\n * datagramchannel\n * socketchannel\n * serversocketchannel\n\njavanio channels和流有一些相似,但是又有些不同:\n\n你可以同时读和写channels,流stream只支持单向的读或写(inputstream/outputstream)\n\n * channels可以异步的读和写,流stream是同步的\n * channels总是读取到buffer或者从buffer中写入\n\n下面分别介绍一下channel最重要的一些实现类:\n\n * filechannel : 可以读写文件中的数据\n * datagramchannel:可以通过udp协议读写数据\n * socketchannel:可以通过tcp协议读写数据\n * serversocketchannel:允许我们像一个web服务器那样监听tcp链接请求,为每一个链接请求创建一个socketchannel\n\n\n# 3.1.1 open channel\n\nfilechannel只能通过getchannel()得到。 socketchannel有工厂方法可以得到。\n\nsocketchannel sc = socketchannel.open();\nsc.connect (new inetsocketaddress (\"somehost\", someport));\n\nserversocketchannel ssc = serversocketchannel.open(); ssc.socket().bind (new inetsocketaddress (somelocalport));\ndatagramchannel dc = datagramchannel.open();\n\nrandomaccessfile raf = new randomaccessfile (\"somefile\", \"r\"); filechannel fc = raf.getchannel();\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 3.1.2 using channels\n\npublic interface readablebytechannel extends channel {\npublic int read (bytebuffer dst) throws ioexception;\n}\n\npublic interface writablebytechannel extends channel {\npublic int write (bytebuffer src) throws ioexception;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nchannels 可以是单向或者双向的。 如果一个class实现了上面两个接口,就是双向的。 channels可以在阻塞模式和非阻塞模式工作。 只有socket和pipes可以工作在非阻塞模式。\n\n\n# 3.1.3 close channels\n\n\n# 3.2 scatter/gather\n\nscatter:将多个buffer按照顺序合成一个 gather:将一个字符串按照顺序拆分成多个。逐个填满buffer。\n\n\n# 3.3 file channels\n\nfile channels 只能以阻塞模式工作。\n\n\n# 3.3.1 accessing files\n\npublic abstract class filechannel extends abstractchannel \nimplements bytechannel, gatheringbytechannel, scatteringbytechannel{\n// this is a partial api listing\n//获取position位置\npublic abstract long position()\n//设置position\npublic abstract void position (long newposition)\n//从position位置开始读\npublic abstract int read (bytebuffer dst)\n//从position位置开始读\npublic abstract int read (bytebuffer dst, long position)\n//从position位置开始写\n public abstract int write (bytebuffer src)\n//从position位置开始写\npublic abstract int write (bytebuffer src, long position)\npublic abstract long size()\n//截取文件保留size大小,并移动position到size的位置\npublic abstract void truncate (long size) \n//强制刷新缓存到磁盘文件\npublic abstract void force (boolean metadata)\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n\n\n\n# 3.3.2 file locking\n\n\n# 3.4 memory-mapped files\n\n\n# 3.5 socket channels\n\nthe new socket channels can operate in nonblocking mode and are selectable. these two capabilities enable tremendous scalability and flexibility in large applications. serversocketchannel没有实现read和write接口,说明它不需要转发数据。它只负责家庭socket连接,创建socketchannel。\n\na channel is a conduit to an i/o service and provides methods for interacting with that service\n\n每个socket channels都有一个socket与之对应,调用socket()可以获得。 但并不是所有的sockets都有channels。socket的getchannel()可能但会null;\n\n\n# 3.5.1 nonblocking mode\n\n调用configureblocking (false);设置channel为非阻塞模式。 但是只有持有blockinglock() 返回的对象锁的线程才能修改阻塞模式。\n\nsocket socket = null;\nobject lockobj = serverchannel.blockinglock();\n// have a handle to the lock object, but haven't locked it yet\n// may block here until lock is acquired\nsynchronize (lockobj){\n// this thread now owns the lock; mode can't be changed boolean prevstate = serverchannel.isblocking();\nserverchannel.configureblocking (false); socket = serverchannel.accept(); serverchannel.configureblocking (prevstate);\n}\n// lock is now released, mode is allowed to change\nif (socket != null) {\ndosomethingwiththesocket (socket);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n\n# 3.5.2 serversocketchannel\n\nthe serversocketchannel class is a channel-based socket listener. 它负责处理socket任务并添加channel。\n\npublic abstract class socketchannel\n extends abstractselectablechannel\n implements bytechannel, scatteringbytechannel, gatheringbytechannel, networkchannel\n{\n//socketchannel被创建但是并没有连接,open()只是创建一个socketchannel,还需要调用connect()连接到一个地址\npublic static socketchannel open() throws ioexception\n//创建并连接\npublic static socketchannel open(socketaddress remote) throws ioexception\n//支持的操作(读、写、连接)\npublic final int validops()\n、、\npublic abstract socket socket();\n//是否连接到远程\npublic abstract boolean isconnected();\n//connect()方法是耗时的,因为需要数据包对话(tcp握手)。如果socketchannel身上有并发、connect(),isconnectionpending返回true\npublic abstract boolean isconnectionpending();\n//建立连接\npublic abstract boolean connect(socketaddress remote) throws ioexception;\n//任何时候都可以安全的调用\npublic abstract boolean finishconnect() throws ioexception;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\nsocket channels是线程安全的。 socket是面向流的而不是面向数据包的,它能保证到达的顺序是发送的顺序,不能保证维护分组。发送方发送20个字节,接收方read可能直接受到7个字节,其余字节在传输中。所以,多个线程共享一个socket的一端不是好的设计。\n\n\n# 3.5.4 datagramchannel\n\nsocketchannel为面向连接的流协议(如tcp/ip) datagramchannel为面向无连接的数据包协议(如udp/ip)\n\n\n# 3.5.5 pipes\n\npipe用于同一个jvm内不同线程之间的数据交换问题 不同jvm之间的数据交换问题应该使用socketchannel pipe类创建了一对channel对象,它们提供了一种回送机制。 pipe实现两个进程之间的单向数据连接。 pipe有一个sinkchannel和一个sourcechannel 数据会从sourcechannel读取、被写到sinkchannel\n\npublic abstract class pipe {\n public static abstract class sourcechannel\n extends abstractselectablechannel\n implements readablebytechannel, scatteringbytechannel\n {\n protected sourcechannel(selectorprovider provider) {\n super(provider);\n }\n public final int validops() {\n return selectionkey.op_read;\n }\n }\n\n public static abstract class sinkchannel\n extends abstractselectablechannel\n implements writablebytechannel, gatheringbytechannel\n {\n protected sinkchannel(selectorprovider provider) {\n super(provider);\n }\n public final int validops() {\n return selectionkey.op_write;\n }\n\n }\n protected pipe() { }\n //从管道读取数据,要访问source通道\n public abstract sourcechannel source();\n //向管道写入数据,要访问sink通道\n public abstract sinkchannel sink();\n //管道创建\n public static pipe open() throws ioexception {\n return selectorprovider.provider().openpipe();\n }\n}\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n\n\n\n# 第四章 selectors\n\nselector(选择器)是java nio中能够检测一到多个nio通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,仅用单个线程来处理多个channels的好处是,只需要更少的线程来处理通道。\n\n\n# 4.1 基础\n\n * selector:一个selector可以注册多个channel,一个线程通过管理selector实现管理多个channel。\n * selectablechannel:提供channle selectability的通用方法。\n * selectionkey:封装了channle和selector的注册关系,包含注册的感兴趣的事件。\n\n\n# 4.2 示例\n\n//创建selector\nselector selector = selector.open();\nchannel.configureblocking(false);\n//注册selector\nselectionkey key = channel.register(selector, selectionkey.op_read);\nwhile(true) {\n//单线程阻塞检查selector中就绪的事件\n int readychannels = selector.select();\n if(readychannels == 0) continue;\n set selectedkeys = selector.selectedkeys();\n iterator keyiterator = selectedkeys.iterator();\n //循环处理就绪的事件\n while(keyiterator.hasnext()) {\n selectionkey key = keyiterator.next();\n if(key.isacceptable()) {\n // a connection was accepted by a serversocketchannel.\n } else if (key.isconnectable()) {\n // a connection was established with a remote server.\n } else if (key.isreadable()) {\n // a channel is ready for reading\n } else if (key.iswritable()) {\n // a channel is ready for writing\n }\n keyiterator.remove();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "EventLoop和EventLoopGroup", + "frontmatter": { + "title": "EventLoop和EventLoopGroup", + "date": "2022-05-21T23:37:23.000Z", + "permalink": "/pages/e7aa60/", + "categories": [ + "中间件", + "Netty" + ], + "tags": [ + null + ] + }, + "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/10.Netty/040.EventLoop%E5%92%8CEventLoopGroup.html", + "relativePath": "04.中间件/10.Netty/040.EventLoop和EventLoopGroup.md", + "key": "v-62f3fa3e", + "path": "/pages/e7aa60/", + "headers": [ + { + "level": 2, + "title": "EventLoop和EventLoopGroup", + "slug": "eventloop和eventloopgroup", + "normalizedTitle": "eventloop和eventloopgroup", + "charIndex": 2 + } + ], + "headersStr": "EventLoop和EventLoopGroup", + "content": "# EventLoop和EventLoopGroup\n\nEventLoop负责处理连接中的事件, 和EvenvLoop有关的几个概念是:Channel, EventLoopGroup。\n\n * 一个EventLoopGroup包含一个或多个EventLoop。\n * 一个EventLoop对应于一个线程,所有EventLoop处理的I/O事件都在这个线程中完成。\n * 一个Channel对应唯一个一个EventLoop。\n * 一个EventLoop可以对应多个Channel。\n\n对于基于Netty的网络服务,Client端启动需要一个EventLoopGroup, Server端启动需要两个EventLoopGroup, 因为Server端需要两种Channel, 一种是ServerChannel, 只有一个,负责接受连接,另一种是用于处理连接的一组Channel。\n\nEventLoop继承了concurrent包里的 ScheduledExecutorService,这使得它可以接受Callable或者Runnable并执行。EventLoop中的parent()方法返回包含这个EventLoop的EventLoopGroup", + "normalizedContent": "# eventloop和eventloopgroup\n\neventloop负责处理连接中的事件, 和evenvloop有关的几个概念是:channel, eventloopgroup。\n\n * 一个eventloopgroup包含一个或多个eventloop。\n * 一个eventloop对应于一个线程,所有eventloop处理的i/o事件都在这个线程中完成。\n * 一个channel对应唯一个一个eventloop。\n * 一个eventloop可以对应多个channel。\n\n对于基于netty的网络服务,client端启动需要一个eventloopgroup, server端启动需要两个eventloopgroup, 因为server端需要两种channel, 一种是serverchannel, 只有一个,负责接受连接,另一种是用于处理连接的一组channel。\n\neventloop继承了concurrent包里的 scheduledexecutorservice,这使得它可以接受callable或者runnable并执行。eventloop中的parent()方法返回包含这个eventloop的eventloopgroup", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "为什么Netty要自己写一个ByteBuf", + "frontmatter": { + "title": "为什么Netty要自己写一个ByteBuf", + "date": "2022-05-21T23:35:32.000Z", + "permalink": "/pages/ef17ee/", + "categories": [ + "中间件", + "Netty" + ], + "tags": [ + null + ] + }, + "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/10.Netty/020.%E4%B8%BA%E4%BB%80%E4%B9%88Netty%E8%A6%81%E8%87%AA%E5%B7%B1%E5%86%99%E4%B8%80%E4%B8%AAByteBuf.html", + "relativePath": "04.中间件/10.Netty/020.为什么Netty要自己写一个ByteBuf.md", + "key": "v-bbcf6e5a", + "path": "/pages/ef17ee/", + "headers": [ + { + "level": 2, + "title": "15.2 ByteBuf源码分析", + "slug": "_15-2-bytebuf源码分析", + "normalizedTitle": "15.2 bytebuf源码分析", + "charIndex": 2 }, { "level": 2, - "title": "4.2 示例", - "slug": "_4-2-示例", - "normalizedTitle": "4.2 示例", - "charIndex": 14101 + "title": "15.3 ByteBuffer相关辅助类", + "slug": "_15-3-bytebuffer相关辅助类", + "normalizedTitle": "15.3 bytebuffer相关辅助类", + "charIndex": 8055 } ], - "headersStr": "1.1 IO概念 1.1.1 Buffer Handler 1.1.1 scatter/gather 1.1.2 虚拟内存 1.1.3 内存页 1.1.4 文件IO 1.1.5 IO流 2.1 Buffer Basic 2.1.1 Buffer的几个属性 2.1.2 Buffer API 2.1.3 访问Buffer 2.1.4 write 2.1.5 Flipping 2.1.6 Draining 2.1.7 Compacting 2.1.9 Mark 2.1.9 Comparing 2.2 创建Buffer 2.3 Duplicating Buffers 2.4 Byte Buffers 2.4.1 Byte Ordering 2.4.2 Direct Buffers 2.4.3 View Buffers 2.4.4 Data Element Views 3.1 basic 3.1.1 Open channel 3.1.2 Using Channels 3.1.3 close Channels 3.2 Scatter/Gather 3.3 File Channels 3.3.1 Accessing Files 3.3.2 File Locking 3.4 Memory-Mapped Files 3.5 Socket Channels 3.5.1 Nonblocking Mode 3.5.2 ServerSocketChannel 3.5.4 DatagramChannel 3.5.5 Pipes 4.1 基础 4.2 示例", - "content": "@TOC\n\n\n# 第一章 IO介绍\n\n\n# 1.1 IO概念\n\n\n# 1.1.1 Buffer Handler\n\n内存分为用户空间(User space),内核空间(Kernel space)。 用户空间的数据读写需要通过内存空间传递。 如下图所示,用户需要读数据,则向cpu发送一个读请求,cpu相应这个请求控制disk controller从disk读取数据到内核buffer,然后再把内核buffer的数据推送给用户空间的buffer。 为什么要区分用户空间和内核空间呢? 每个进程有独立的内存空间好处在于多个进程的内存分配互不影响。如果多个进程共享一块内存,那么内存分配的时候就得排队。\n\n为什么用户空间不能直接从disk读数据呢?\n\n * 硬件控制器只能处理固定大小的数据,用户空间需要的可能是数据块大小异常或者不对齐的数据,内核空间起作到数拆分、重组的作用。\n * 硬件设备通常不允许直接操作虚拟内存,即用户空间\n\nDMA是什么? DMA(Direct Memory Access,直接存储器访问)。 在DMA之前通过中断CPU来传输数据。CPU响应中断,控制总线传输数据。 DMA不需要CPU参与数据传输。DMA可以和CPU交互请求控制总线传输数据。\n\n\n# 1.1.1 scatter/gather\n\n分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。 聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。\n\n比如某个协议的消息固定消息头128字节,消息体1024字节,消息尾128字节。我们想要分别处理消息头,消息体,消息尾。 使用scatter示例代码:\n\nByteBuffer header = ByteBuffer.allocate(128);\nByteBuffer body = ByteBuffer.allocate(1024);\nByteBuffer tail = ByteBuffer.allocate(128);\n\nByteBuffer[] bufferArray = { header, body,tail };\nchannel.read(bufferArray);\n\n\n1\n2\n3\n4\n5\n6\n\n\n注意buffer首先被插入到数组,然后再将数组作为channel.read() 的输入参数。read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。 所以。\n\nscatter不适合处理动态消息,相反,gather适合处理动态消息。 buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与Scattering Reads相反,Gathering Writes能较好的处理动态消息。\n\n\n# 1.1.2 虚拟内存\n\n虚拟内存是指使用虚拟内存地址代替物理内存地址。\n\n * 会有多个虚拟内存指向同一个物理内存\n * 虚拟内存可能会大于物理内存 上面说disk controller不可以直接把数据读到用户空间。虚拟内存可以通过虚拟地址映射内核空间的方式做到这一点。 如下图,内核空间的buffer对用户空间也是可见的。这就是netty零拷贝的原理,这减少了buffer在用户空间和内核空间的拷贝,非常有意义。\n\n\n# 1.1.3 内存页\n\n操作系统将内存按固定字节分页。 内存读取的基本单位是页。\n\nMMU(Memory Management Unit):内存管理单元,保存虚拟内存和物理内存的映射关系,处在CPU和内存之间,起到将虚拟内存转换为物理内存的作用。\n\n\n# 1.1.4 文件IO\n\n文件系统:文件IO发生在文件系统。文件保存在磁盘,磁盘扇区类似内存分页的概念。内存文件读取通过文件系统间接操作磁盘文件。 文件零拷贝如下图。避免了文件系统内存页和用户内存页之间的拷贝。\n\n\n# 1.1.5 IO流\n\n上面说的IO都是面向块的IO。还有面向流的IO。 大多数操作系统支持将流置为非阻塞模式,额外设置一个进程用于检查流上是否有输入,因此流本身是不被阻塞的。 网络IO一般都是流IO。 多路复用:用一个进程管理多个非阻塞IO流的状态。\n\n\n# 第二章 Buffer\n\n标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。\n\n\n# 2.1 Buffer Basic\n\n\n# 2.1.1 Buffer的几个属性\n\n * capacity\n * limit\n * position\n * mark\n\n\n# 2.1.2 Buffer API\n\npackage java.nio;\npublic abstract class Buffer {\npublic final int capacity()\npublic final int position()\npublic final Buffer position (int newPosition) public final int limit()\n }\npublic final Buffer limit (int newLimit)\npublic final Buffer mark()\npublic final Buffer reset()\npublic final Buffer clear()\npublic final Buffer flip()\npublic final Buffer rewind()\npublic final int remaining()\npublic final boolean hasRemaining()\npublic abstract boolean isReadOnly();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n\n\n\n# 2.1.3 访问Buffer\n\npublic abstract class ByteBuffer\n extends Buffer implements Comparable\n {\n// This is a partial API listing\npublic abstract byte get();\npublic abstract byte get (int index);\npublic abstract ByteBuffer put (byte b);\npublic abstract ByteBuffer put (int index, byte b);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n\n# 2.1.4 write\n\nFigure 2-2的buffer经过下面这个代码会变成Figure 2-3的样子:\n\nbuffer.put((byte)'H').put((byte)'e').put((byte)'l')\n.put((byte)'l').put( (byte)'o');\n\n\n1\n2\n\n\n也可以按绝对位置写入:\n\nbuffer.put(0, (byte)'M').put((byte)'w');\n\n\n1\n\n\n\n\n\n# 2.1.5 Flipping\n\n当我们想要写出buffer的内容时,我们需要用position和limit指针指向时机内容的起始位置。类似这样:\n\nbuffer.limit(buffer.position()).position(0);\n\n\n1\n\n\n这等同于buffer.flip(); 调用flip()后2-4会变成2-5 说白了,flip()改变position和limit两个指针。 rewind()只改变position指针,等同于:\n\nbuffer.position(0);\n\n\n1\n\n\n\n# 2.1.6 Draining\n\nhasRemaining()可以判断是否到达limit指针 下面是一个实现,将buffer内容读到一个数组中\n\nfor (int i = 0; buffer.hasRemaining(), i++) {\n myByteArray [i] = buffer.get();\n}\n\n\n1\n2\n3\n\n\nremaining()会返回position到limit的数量。 将buffer内容读到一个数组中另一种实现方式:\n\nint count = buffer.remaining();\nfor (int i = 0; i < count, i++) {\n myByteArray [i] = buffer.get();\n}\n\n\n1\n2\n3\n4\n\n\n\n# 2.1.7 Compacting\n\n当你drain一部分数据想要继续fill数据的时候,你需要把还未drain的数据整体前移。 buffer.compact();可以完成这个工作。 调用buffer.compact()的效果就是2-6到2-7\n\n\n# 2.1.9 Mark\n\nmark()方法会用mark指针指向position的位置。\n\nbuffer.position(2).mark().position(4);\n\n\n1\n\n\nreset()方法会把position指针指向mark的位置。\n\nbuffer.reset();\n\n\n1\n\n\n\n\n\n# 2.1.9 Comparing\n\npublic abstract class ByteBuffer\n extends Buffer implements Comparable\n {\n // This is a partial API listing\npublic boolean equals (Object ob)\npublic int compareTo (Object ob)\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nequals()返回true需要满足三个条件:\n\n * Buffer包含相同类型的元素\n * 两个Buffer的remaining()返回值相同\n * remaining data的序列相同 下面是两个例子:\n\n\n# 2.2 创建Buffer\n\n创建新buffer的两种方式:allocation or wrapping\n\npublic abstract class CharBuffer\nextends Buffer implements CharSequence, Comparable\n{\n}\n// This is a partial API listing\npublic static CharBuffer allocate (int capacity)\npublic static CharBuffer wrap (char [] array)\npublic static CharBuffer wrap (char [] array, int offset, int length)\npublic final boolean hasArray()\npublic final char [] array()\npublic final int arrayOffset()\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n通过allocation创建的buffer是分配一块固定大小的堆内存作为buffer存储空间。 wrapping创建的buffer是创建一个buffer对象指向一个array空间。\n\n\n# 2.3 Duplicating Buffers\n\npublic abstract class CharBuffer\nextends Buffer implements CharSequence, Comparable\n {\n}\n// This is a partial API listing\npublic abstract CharBuffer duplicate();\npublic abstract CharBuffer asReadOnlyBuffer(); \npublic abstract CharBuffer slice();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\nduplicate()创建一个新的buffer,和原buffer一样。 两个buffer共享数据元素 但是他们有独立的position, limit, and mark 一个buffer更改了数据元素对另一个buffer可见 只读、直接内存这两个属性也可以通过duplicate()继承 举个例子:\n\nCharBuffer buffer = CharBuffer.allocate (8); \nbuffer.position (3).limit (6).mark().position (5); \nCharBuffer dupeBuffer = buffer.duplicate(); buffer.clear();\n\n\n1\n2\n3\n\n\nasReadOnlyBuffer()和duplicate()功能类似,只是asReadOnlyBuffer()返回一个只读的视图。 slice() 返回一个新的buffer视图,不过 new_position = original_position new_capacity = original_limit - original_position 举个例子来看slice():\n\nCharBuffer buffer = CharBuffer.allocate (8); \nbuffer.position (3).limit (5);\nCharBuffer sliceBuffer = buffer.slice();\n\n\n1\n2\n3\n\n\n\n\n\n# 2.4 Byte Buffers\n\n\n# 2.4.1 Byte Ordering\n\n原始数据类型在内存中的存储方式是多个字节的连续序列。 举个例子一个32-bit int 值为 0x037FB4C7的内存存储序列可能为2-14也可能为2-15: 这就是所谓的大端设计和小端设计。 采用大端还是小端通常由硬件的设计者决定而不是小端的设计者。 IP协议定义的字节顺序是大端。所有使用IP协议的多字节数值必须在网络字节序和本地主机字节序做转换。\n\npackage java.nio;\npublic final class ByteOrder\n{\npublic static final ByteOrder BIG_ENDIAN public static final ByteOrder LITTLE_ENDIAN\npublic static ByteOrder nativeOrder()\npublic String toString()\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n除了ByteBuffer之外,其他buffer的order()适中返回同一个值,ByteOrder.nativeOrder()。\n\n\n# 2.4.2 Direct Buffers\n\npublic abstract class ByteBuffer extends Buffer implements Comparable {\n// This is a partial API listing\npublic static ByteBuffer allocate (int capacity) ;\npublic static ByteBuffer allocateDirect (int capacity) ;\npublic abstract boolean isDirect();\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\nallocateDirect可以直接在堆外开辟一个buffer。 堆外buffer节省了用户空间和系统空间的buffer拷贝,提升效率。\n\n\n# 2.4.3 View Buffers\n\n收到数据的时候你可能要先看一下数据再决定做什么操作,这就需要用到ByteBuffer的视图API。\n\npublic abstract class ByteBuffer extends Buffer implements Comparable {\n// This is a partial API listing\npublic abstract CharBuffer asCharBuffer(); \npublic abstract ShortBuffer asShortBuffer(); \npublic abstract IntBuffer asIntBuffer();\n public abstract LongBuffer asLongBuffer();\n public abstract FloatBuffer asFloatBuffer();\n public abstract DoubleBuffer asDoubleBuffer();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n视图在直接内存buffer上操作更高效。 当本地硬件的字节顺序和buffer中的字节顺序一致时,通过低级代码就可以操作buffer中的数据,而不需要经过字节码的编码解码。\n\n\n# 2.4.4 Data Element Views\n\n当getInt() 被调用时,那么从当前位置开始的4个字节将被打包成一个int返回。 int value = buffer.getInt();将返回buffer中的1-4位,具体返回的数据值与字节序有关系。 int value = buffer.order (ByteOrder.BIG_ENDIAN).getInt(); 返回0x3BC5315E。 int value = buffer.order (ByteOrder.LITTLE_ENDIAN).getInt(); 返回0x5E31C53B。\n\n\n# 第三章 Channels\n\n\n# 3.1 basic\n\n在NIO接口中我们需要使用Channel和Buffer进行IO操作,Channel模拟了流的概念,但是又有不同。数据总是从一个Channel读到一个buffer中,或者从一个buffer中写到channel中。 channel接口的主要实现类如下:\n\n * FileChannel\n * DatagramChannel\n * SocketChannel\n * ServerSocketChannel\n\nJavaNIO Channels和流有一些相似,但是又有些不同:\n\n你可以同时读和写Channels,流Stream只支持单向的读或写(InputStream/OutputStream)\n\n * Channels可以异步的读和写,流Stream是同步的\n * Channels总是读取到buffer或者从buffer中写入\n\n下面分别介绍一下Channel最重要的一些实现类:\n\n * FileChannel : 可以读写文件中的数据\n * DatagramChannel:可以通过UDP协议读写数据\n * SocketChannel:可以通过TCP协议读写数据\n * ServerSocketChannel:允许我们像一个web服务器那样监听TCP链接请求,为每一个链接请求创建一个SocketChannel\n\n\n# 3.1.1 Open channel\n\nFileChannel只能通过getChannel()得到。 SocketChannel有工厂方法可以得到。\n\nSocketChannel sc = SocketChannel.open();\nsc.connect (new InetSocketAddress (\"somehost\", someport));\n\nServerSocketChannel ssc = ServerSocketChannel.open(); ssc.socket().bind (new InetSocketAddress (somelocalport));\nDatagramChannel dc = DatagramChannel.open();\n\nRandomAccessFile raf = new RandomAccessFile (\"somefile\", \"r\"); FileChannel fc = raf.getChannel();\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 3.1.2 Using Channels\n\npublic interface ReadableByteChannel extends Channel {\npublic int read (ByteBuffer dst) throws IOException;\n}\n\npublic interface WritableByteChannel extends Channel {\npublic int write (ByteBuffer src) throws IOException;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nChannels 可以是单向或者双向的。 如果一个class实现了上面两个接口,就是双向的。 Channels可以在阻塞模式和非阻塞模式工作。 只有socket和pipes可以工作在非阻塞模式。\n\n\n# 3.1.3 close Channels\n\n\n# 3.2 Scatter/Gather\n\nScatter:将多个buffer按照顺序合成一个 Gather:将一个字符串按照顺序拆分成多个。逐个填满buffer。\n\n\n# 3.3 File Channels\n\nFile channels 只能以阻塞模式工作。\n\n\n# 3.3.1 Accessing Files\n\npublic abstract class FileChannel extends AbstractChannel \nimplements ByteChannel, GatheringByteChannel, ScatteringByteChannel{\n// This is a partial API listing\n//获取position位置\npublic abstract long position()\n//设置position\npublic abstract void position (long newPosition)\n//从position位置开始读\npublic abstract int read (ByteBuffer dst)\n//从position位置开始读\npublic abstract int read (ByteBuffer dst, long position)\n//从position位置开始写\n public abstract int write (ByteBuffer src)\n//从position位置开始写\npublic abstract int write (ByteBuffer src, long position)\npublic abstract long size()\n//截取文件保留size大小,并移动position到size的位置\npublic abstract void truncate (long size) \n//强制刷新缓存到磁盘文件\npublic abstract void force (boolean metaData)\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n\n\n\n# 3.3.2 File Locking\n\n\n# 3.4 Memory-Mapped Files\n\n\n# 3.5 Socket Channels\n\nThe new socket channels can operate in nonblocking mode and are selectable. These two capabilities enable tremendous scalability and flexibility in large applications. ServerSocketChannel没有实现read和write接口,说明它不需要转发数据。它只负责家庭socket连接,创建SocketChannel。\n\na channel is a conduit to an I/O service and provides methods for interacting with that service\n\n每个socket channels都有一个socket与之对应,调用socket()可以获得。 但并不是所有的sockets都有channels。socket的getChannel()可能但会null;\n\n\n# 3.5.1 Nonblocking Mode\n\n调用configureBlocking (false);设置channel为非阻塞模式。 但是只有持有blockingLock() 返回的对象锁的线程才能修改阻塞模式。\n\nSocket socket = null;\nObject lockObj = serverChannel.blockingLock();\n// have a handle to the lock object, but haven't locked it yet\n// may block here until lock is acquired\nsynchronize (lockObj){\n// This thread now owns the lock; mode can't be changed boolean prevState = serverChannel.isBlocking();\nserverChannel.configureBlocking (false); socket = serverChannel.accept(); serverChannel.configureBlocking (prevState);\n}\n// lock is now released, mode is allowed to change\nif (socket != null) {\ndoSomethingWithTheSocket (socket);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n\n# 3.5.2 ServerSocketChannel\n\nThe ServerSocketChannel class is a channel-based socket listener. 它负责处理socket任务并添加channel。\n\npublic abstract class SocketChannel\n extends AbstractSelectableChannel\n implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel\n{\n//SocketChannel被创建但是并没有连接,open()只是创建一个SocketChannel,还需要调用connect()连接到一个地址\npublic static SocketChannel open() throws IOException\n//创建并连接\npublic static SocketChannel open(SocketAddress remote) throws IOException\n//支持的操作(读、写、连接)\npublic final int validOps()\n、、\npublic abstract Socket socket();\n//是否连接到远程\npublic abstract boolean isConnected();\n//Connect()方法是耗时的,因为需要数据包对话(TCP握手)。如果SocketChannel身上有并发、connect(),isConnectionPending返回true\npublic abstract boolean isConnectionPending();\n//建立连接\npublic abstract boolean connect(SocketAddress remote) throws IOException;\n//任何时候都可以安全的调用\npublic abstract boolean finishConnect() throws IOException;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\nSocket channels是线程安全的。 Socket是面向流的而不是面向数据包的,它能保证到达的顺序是发送的顺序,不能保证维护分组。发送方发送20个字节,接收方read可能直接受到7个字节,其余字节在传输中。所以,多个线程共享一个socket的一端不是好的设计。\n\n\n# 3.5.4 DatagramChannel\n\nsocketchannel为面向连接的流协议(如tcp/ip) datagramchannel为面向无连接的数据包协议(如udp/ip)\n\n\n# 3.5.5 Pipes\n\nPipe用于同一个JVM内不同线程之间的数据交换问题 不同JVM之间的数据交换问题应该使用SocketChannel Pipe类创建了一对channel对象,它们提供了一种回送机制。 Pipe实现两个进程之间的单向数据连接。 Pipe有一个SinkChannel和一个SourceChannel 数据会从SourceChannel读取、被写到SinkChannel\n\npublic abstract class Pipe {\n public static abstract class SourceChannel\n extends AbstractSelectableChannel\n implements ReadableByteChannel, ScatteringByteChannel\n {\n protected SourceChannel(SelectorProvider provider) {\n super(provider);\n }\n public final int validOps() {\n return SelectionKey.OP_READ;\n }\n }\n\n public static abstract class SinkChannel\n extends AbstractSelectableChannel\n implements WritableByteChannel, GatheringByteChannel\n {\n protected SinkChannel(SelectorProvider provider) {\n super(provider);\n }\n public final int validOps() {\n return SelectionKey.OP_WRITE;\n }\n\n }\n protected Pipe() { }\n //从管道读取数据,要访问source通道\n public abstract SourceChannel source();\n //向管道写入数据,要访问Sink通道\n public abstract SinkChannel sink();\n //管道创建\n public static Pipe open() throws IOException {\n return SelectorProvider.provider().openPipe();\n }\n}\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n\n\n\n# 第四章 Selectors\n\nSelector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。\n\n\n# 4.1 基础\n\n * Selector:一个selector可以注册多个channel,一个线程通过管理Selector实现管理多个channel。\n * SelectableChannel:提供channle selectability的通用方法。\n * SelectionKey:封装了channle和selector的注册关系,包含注册的感兴趣的事件。\n\n\n# 4.2 示例\n\n//创建Selector\nSelector selector = Selector.open();\nchannel.configureBlocking(false);\n//注册Selector\nSelectionKey key = channel.register(selector, SelectionKey.OP_READ);\nwhile(true) {\n//单线程阻塞检查Selector中就绪的事件\n int readyChannels = selector.select();\n if(readyChannels == 0) continue;\n Set selectedKeys = selector.selectedKeys();\n Iterator keyIterator = selectedKeys.iterator();\n //循环处理就绪的事件\n while(keyIterator.hasNext()) {\n SelectionKey key = keyIterator.next();\n if(key.isAcceptable()) {\n // a connection was accepted by a ServerSocketChannel.\n } else if (key.isConnectable()) {\n // a connection was established with a remote server.\n } else if (key.isReadable()) {\n // a channel is ready for reading\n } else if (key.isWritable()) {\n // a channel is ready for writing\n }\n keyIterator.remove();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n", - "normalizedContent": "@toc\n\n\n# 第一章 io介绍\n\n\n# 1.1 io概念\n\n\n# 1.1.1 buffer handler\n\n内存分为用户空间(user space),内核空间(kernel space)。 用户空间的数据读写需要通过内存空间传递。 如下图所示,用户需要读数据,则向cpu发送一个读请求,cpu相应这个请求控制disk controller从disk读取数据到内核buffer,然后再把内核buffer的数据推送给用户空间的buffer。 为什么要区分用户空间和内核空间呢? 每个进程有独立的内存空间好处在于多个进程的内存分配互不影响。如果多个进程共享一块内存,那么内存分配的时候就得排队。\n\n为什么用户空间不能直接从disk读数据呢?\n\n * 硬件控制器只能处理固定大小的数据,用户空间需要的可能是数据块大小异常或者不对齐的数据,内核空间起作到数拆分、重组的作用。\n * 硬件设备通常不允许直接操作虚拟内存,即用户空间\n\ndma是什么? dma(direct memory access,直接存储器访问)。 在dma之前通过中断cpu来传输数据。cpu响应中断,控制总线传输数据。 dma不需要cpu参与数据传输。dma可以和cpu交互请求控制总线传输数据。\n\n\n# 1.1.1 scatter/gather\n\n分散(scatter)从channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,channel将从channel中读取的数据“分散(scatter)”到多个buffer中。 聚集(gather)写入channel是指在写操作时将多个buffer的数据写入同一个channel,因此,channel 将多个buffer中的数据“聚集(gather)”后发送到channel。\n\n比如某个协议的消息固定消息头128字节,消息体1024字节,消息尾128字节。我们想要分别处理消息头,消息体,消息尾。 使用scatter示例代码:\n\nbytebuffer header = bytebuffer.allocate(128);\nbytebuffer body = bytebuffer.allocate(1024);\nbytebuffer tail = bytebuffer.allocate(128);\n\nbytebuffer[] bufferarray = { header, body,tail };\nchannel.read(bufferarray);\n\n\n1\n2\n3\n4\n5\n6\n\n\n注意buffer首先被插入到数组,然后再将数组作为channel.read() 的输入参数。read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。 所以。\n\nscatter不适合处理动态消息,相反,gather适合处理动态消息。 buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与scattering reads相反,gathering writes能较好的处理动态消息。\n\n\n# 1.1.2 虚拟内存\n\n虚拟内存是指使用虚拟内存地址代替物理内存地址。\n\n * 会有多个虚拟内存指向同一个物理内存\n * 虚拟内存可能会大于物理内存 上面说disk controller不可以直接把数据读到用户空间。虚拟内存可以通过虚拟地址映射内核空间的方式做到这一点。 如下图,内核空间的buffer对用户空间也是可见的。这就是netty零拷贝的原理,这减少了buffer在用户空间和内核空间的拷贝,非常有意义。\n\n\n# 1.1.3 内存页\n\n操作系统将内存按固定字节分页。 内存读取的基本单位是页。\n\nmmu(memory management unit):内存管理单元,保存虚拟内存和物理内存的映射关系,处在cpu和内存之间,起到将虚拟内存转换为物理内存的作用。\n\n\n# 1.1.4 文件io\n\n文件系统:文件io发生在文件系统。文件保存在磁盘,磁盘扇区类似内存分页的概念。内存文件读取通过文件系统间接操作磁盘文件。 文件零拷贝如下图。避免了文件系统内存页和用户内存页之间的拷贝。\n\n\n# 1.1.5 io流\n\n上面说的io都是面向块的io。还有面向流的io。 大多数操作系统支持将流置为非阻塞模式,额外设置一个进程用于检查流上是否有输入,因此流本身是不被阻塞的。 网络io一般都是流io。 多路复用:用一个进程管理多个非阻塞io流的状态。\n\n\n# 第二章 buffer\n\n标准的io基于字节流和字符流进行操作的,而nio是基于通道(channel)和缓冲区(buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。\n\n\n# 2.1 buffer basic\n\n\n# 2.1.1 buffer的几个属性\n\n * capacity\n * limit\n * position\n * mark\n\n\n# 2.1.2 buffer api\n\npackage java.nio;\npublic abstract class buffer {\npublic final int capacity()\npublic final int position()\npublic final buffer position (int newposition) public final int limit()\n }\npublic final buffer limit (int newlimit)\npublic final buffer mark()\npublic final buffer reset()\npublic final buffer clear()\npublic final buffer flip()\npublic final buffer rewind()\npublic final int remaining()\npublic final boolean hasremaining()\npublic abstract boolean isreadonly();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n\n\n\n# 2.1.3 访问buffer\n\npublic abstract class bytebuffer\n extends buffer implements comparable\n {\n// this is a partial api listing\npublic abstract byte get();\npublic abstract byte get (int index);\npublic abstract bytebuffer put (byte b);\npublic abstract bytebuffer put (int index, byte b);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n\n# 2.1.4 write\n\nfigure 2-2的buffer经过下面这个代码会变成figure 2-3的样子:\n\nbuffer.put((byte)'h').put((byte)'e').put((byte)'l')\n.put((byte)'l').put( (byte)'o');\n\n\n1\n2\n\n\n也可以按绝对位置写入:\n\nbuffer.put(0, (byte)'m').put((byte)'w');\n\n\n1\n\n\n\n\n\n# 2.1.5 flipping\n\n当我们想要写出buffer的内容时,我们需要用position和limit指针指向时机内容的起始位置。类似这样:\n\nbuffer.limit(buffer.position()).position(0);\n\n\n1\n\n\n这等同于buffer.flip(); 调用flip()后2-4会变成2-5 说白了,flip()改变position和limit两个指针。 rewind()只改变position指针,等同于:\n\nbuffer.position(0);\n\n\n1\n\n\n\n# 2.1.6 draining\n\nhasremaining()可以判断是否到达limit指针 下面是一个实现,将buffer内容读到一个数组中\n\nfor (int i = 0; buffer.hasremaining(), i++) {\n mybytearray [i] = buffer.get();\n}\n\n\n1\n2\n3\n\n\nremaining()会返回position到limit的数量。 将buffer内容读到一个数组中另一种实现方式:\n\nint count = buffer.remaining();\nfor (int i = 0; i < count, i++) {\n mybytearray [i] = buffer.get();\n}\n\n\n1\n2\n3\n4\n\n\n\n# 2.1.7 compacting\n\n当你drain一部分数据想要继续fill数据的时候,你需要把还未drain的数据整体前移。 buffer.compact();可以完成这个工作。 调用buffer.compact()的效果就是2-6到2-7\n\n\n# 2.1.9 mark\n\nmark()方法会用mark指针指向position的位置。\n\nbuffer.position(2).mark().position(4);\n\n\n1\n\n\nreset()方法会把position指针指向mark的位置。\n\nbuffer.reset();\n\n\n1\n\n\n\n\n\n# 2.1.9 comparing\n\npublic abstract class bytebuffer\n extends buffer implements comparable\n {\n // this is a partial api listing\npublic boolean equals (object ob)\npublic int compareto (object ob)\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nequals()返回true需要满足三个条件:\n\n * buffer包含相同类型的元素\n * 两个buffer的remaining()返回值相同\n * remaining data的序列相同 下面是两个例子:\n\n\n# 2.2 创建buffer\n\n创建新buffer的两种方式:allocation or wrapping\n\npublic abstract class charbuffer\nextends buffer implements charsequence, comparable\n{\n}\n// this is a partial api listing\npublic static charbuffer allocate (int capacity)\npublic static charbuffer wrap (char [] array)\npublic static charbuffer wrap (char [] array, int offset, int length)\npublic final boolean hasarray()\npublic final char [] array()\npublic final int arrayoffset()\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n通过allocation创建的buffer是分配一块固定大小的堆内存作为buffer存储空间。 wrapping创建的buffer是创建一个buffer对象指向一个array空间。\n\n\n# 2.3 duplicating buffers\n\npublic abstract class charbuffer\nextends buffer implements charsequence, comparable\n {\n}\n// this is a partial api listing\npublic abstract charbuffer duplicate();\npublic abstract charbuffer asreadonlybuffer(); \npublic abstract charbuffer slice();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\nduplicate()创建一个新的buffer,和原buffer一样。 两个buffer共享数据元素 但是他们有独立的position, limit, and mark 一个buffer更改了数据元素对另一个buffer可见 只读、直接内存这两个属性也可以通过duplicate()继承 举个例子:\n\ncharbuffer buffer = charbuffer.allocate (8); \nbuffer.position (3).limit (6).mark().position (5); \ncharbuffer dupebuffer = buffer.duplicate(); buffer.clear();\n\n\n1\n2\n3\n\n\nasreadonlybuffer()和duplicate()功能类似,只是asreadonlybuffer()返回一个只读的视图。 slice() 返回一个新的buffer视图,不过 new_position = original_position new_capacity = original_limit - original_position 举个例子来看slice():\n\ncharbuffer buffer = charbuffer.allocate (8); \nbuffer.position (3).limit (5);\ncharbuffer slicebuffer = buffer.slice();\n\n\n1\n2\n3\n\n\n\n\n\n# 2.4 byte buffers\n\n\n# 2.4.1 byte ordering\n\n原始数据类型在内存中的存储方式是多个字节的连续序列。 举个例子一个32-bit int 值为 0x037fb4c7的内存存储序列可能为2-14也可能为2-15: 这就是所谓的大端设计和小端设计。 采用大端还是小端通常由硬件的设计者决定而不是小端的设计者。 ip协议定义的字节顺序是大端。所有使用ip协议的多字节数值必须在网络字节序和本地主机字节序做转换。\n\npackage java.nio;\npublic final class byteorder\n{\npublic static final byteorder big_endian public static final byteorder little_endian\npublic static byteorder nativeorder()\npublic string tostring()\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n除了bytebuffer之外,其他buffer的order()适中返回同一个值,byteorder.nativeorder()。\n\n\n# 2.4.2 direct buffers\n\npublic abstract class bytebuffer extends buffer implements comparable {\n// this is a partial api listing\npublic static bytebuffer allocate (int capacity) ;\npublic static bytebuffer allocatedirect (int capacity) ;\npublic abstract boolean isdirect();\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\nallocatedirect可以直接在堆外开辟一个buffer。 堆外buffer节省了用户空间和系统空间的buffer拷贝,提升效率。\n\n\n# 2.4.3 view buffers\n\n收到数据的时候你可能要先看一下数据再决定做什么操作,这就需要用到bytebuffer的视图api。\n\npublic abstract class bytebuffer extends buffer implements comparable {\n// this is a partial api listing\npublic abstract charbuffer ascharbuffer(); \npublic abstract shortbuffer asshortbuffer(); \npublic abstract intbuffer asintbuffer();\n public abstract longbuffer aslongbuffer();\n public abstract floatbuffer asfloatbuffer();\n public abstract doublebuffer asdoublebuffer();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n视图在直接内存buffer上操作更高效。 当本地硬件的字节顺序和buffer中的字节顺序一致时,通过低级代码就可以操作buffer中的数据,而不需要经过字节码的编码解码。\n\n\n# 2.4.4 data element views\n\n当getint() 被调用时,那么从当前位置开始的4个字节将被打包成一个int返回。 int value = buffer.getint();将返回buffer中的1-4位,具体返回的数据值与字节序有关系。 int value = buffer.order (byteorder.big_endian).getint(); 返回0x3bc5315e。 int value = buffer.order (byteorder.little_endian).getint(); 返回0x5e31c53b。\n\n\n# 第三章 channels\n\n\n# 3.1 basic\n\n在nio接口中我们需要使用channel和buffer进行io操作,channel模拟了流的概念,但是又有不同。数据总是从一个channel读到一个buffer中,或者从一个buffer中写到channel中。 channel接口的主要实现类如下:\n\n * filechannel\n * datagramchannel\n * socketchannel\n * serversocketchannel\n\njavanio channels和流有一些相似,但是又有些不同:\n\n你可以同时读和写channels,流stream只支持单向的读或写(inputstream/outputstream)\n\n * channels可以异步的读和写,流stream是同步的\n * channels总是读取到buffer或者从buffer中写入\n\n下面分别介绍一下channel最重要的一些实现类:\n\n * filechannel : 可以读写文件中的数据\n * datagramchannel:可以通过udp协议读写数据\n * socketchannel:可以通过tcp协议读写数据\n * serversocketchannel:允许我们像一个web服务器那样监听tcp链接请求,为每一个链接请求创建一个socketchannel\n\n\n# 3.1.1 open channel\n\nfilechannel只能通过getchannel()得到。 socketchannel有工厂方法可以得到。\n\nsocketchannel sc = socketchannel.open();\nsc.connect (new inetsocketaddress (\"somehost\", someport));\n\nserversocketchannel ssc = serversocketchannel.open(); ssc.socket().bind (new inetsocketaddress (somelocalport));\ndatagramchannel dc = datagramchannel.open();\n\nrandomaccessfile raf = new randomaccessfile (\"somefile\", \"r\"); filechannel fc = raf.getchannel();\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 3.1.2 using channels\n\npublic interface readablebytechannel extends channel {\npublic int read (bytebuffer dst) throws ioexception;\n}\n\npublic interface writablebytechannel extends channel {\npublic int write (bytebuffer src) throws ioexception;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nchannels 可以是单向或者双向的。 如果一个class实现了上面两个接口,就是双向的。 channels可以在阻塞模式和非阻塞模式工作。 只有socket和pipes可以工作在非阻塞模式。\n\n\n# 3.1.3 close channels\n\n\n# 3.2 scatter/gather\n\nscatter:将多个buffer按照顺序合成一个 gather:将一个字符串按照顺序拆分成多个。逐个填满buffer。\n\n\n# 3.3 file channels\n\nfile channels 只能以阻塞模式工作。\n\n\n# 3.3.1 accessing files\n\npublic abstract class filechannel extends abstractchannel \nimplements bytechannel, gatheringbytechannel, scatteringbytechannel{\n// this is a partial api listing\n//获取position位置\npublic abstract long position()\n//设置position\npublic abstract void position (long newposition)\n//从position位置开始读\npublic abstract int read (bytebuffer dst)\n//从position位置开始读\npublic abstract int read (bytebuffer dst, long position)\n//从position位置开始写\n public abstract int write (bytebuffer src)\n//从position位置开始写\npublic abstract int write (bytebuffer src, long position)\npublic abstract long size()\n//截取文件保留size大小,并移动position到size的位置\npublic abstract void truncate (long size) \n//强制刷新缓存到磁盘文件\npublic abstract void force (boolean metadata)\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n\n\n\n# 3.3.2 file locking\n\n\n# 3.4 memory-mapped files\n\n\n# 3.5 socket channels\n\nthe new socket channels can operate in nonblocking mode and are selectable. these two capabilities enable tremendous scalability and flexibility in large applications. serversocketchannel没有实现read和write接口,说明它不需要转发数据。它只负责家庭socket连接,创建socketchannel。\n\na channel is a conduit to an i/o service and provides methods for interacting with that service\n\n每个socket channels都有一个socket与之对应,调用socket()可以获得。 但并不是所有的sockets都有channels。socket的getchannel()可能但会null;\n\n\n# 3.5.1 nonblocking mode\n\n调用configureblocking (false);设置channel为非阻塞模式。 但是只有持有blockinglock() 返回的对象锁的线程才能修改阻塞模式。\n\nsocket socket = null;\nobject lockobj = serverchannel.blockinglock();\n// have a handle to the lock object, but haven't locked it yet\n// may block here until lock is acquired\nsynchronize (lockobj){\n// this thread now owns the lock; mode can't be changed boolean prevstate = serverchannel.isblocking();\nserverchannel.configureblocking (false); socket = serverchannel.accept(); serverchannel.configureblocking (prevstate);\n}\n// lock is now released, mode is allowed to change\nif (socket != null) {\ndosomethingwiththesocket (socket);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n\n# 3.5.2 serversocketchannel\n\nthe serversocketchannel class is a channel-based socket listener. 它负责处理socket任务并添加channel。\n\npublic abstract class socketchannel\n extends abstractselectablechannel\n implements bytechannel, scatteringbytechannel, gatheringbytechannel, networkchannel\n{\n//socketchannel被创建但是并没有连接,open()只是创建一个socketchannel,还需要调用connect()连接到一个地址\npublic static socketchannel open() throws ioexception\n//创建并连接\npublic static socketchannel open(socketaddress remote) throws ioexception\n//支持的操作(读、写、连接)\npublic final int validops()\n、、\npublic abstract socket socket();\n//是否连接到远程\npublic abstract boolean isconnected();\n//connect()方法是耗时的,因为需要数据包对话(tcp握手)。如果socketchannel身上有并发、connect(),isconnectionpending返回true\npublic abstract boolean isconnectionpending();\n//建立连接\npublic abstract boolean connect(socketaddress remote) throws ioexception;\n//任何时候都可以安全的调用\npublic abstract boolean finishconnect() throws ioexception;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\nsocket channels是线程安全的。 socket是面向流的而不是面向数据包的,它能保证到达的顺序是发送的顺序,不能保证维护分组。发送方发送20个字节,接收方read可能直接受到7个字节,其余字节在传输中。所以,多个线程共享一个socket的一端不是好的设计。\n\n\n# 3.5.4 datagramchannel\n\nsocketchannel为面向连接的流协议(如tcp/ip) datagramchannel为面向无连接的数据包协议(如udp/ip)\n\n\n# 3.5.5 pipes\n\npipe用于同一个jvm内不同线程之间的数据交换问题 不同jvm之间的数据交换问题应该使用socketchannel pipe类创建了一对channel对象,它们提供了一种回送机制。 pipe实现两个进程之间的单向数据连接。 pipe有一个sinkchannel和一个sourcechannel 数据会从sourcechannel读取、被写到sinkchannel\n\npublic abstract class pipe {\n public static abstract class sourcechannel\n extends abstractselectablechannel\n implements readablebytechannel, scatteringbytechannel\n {\n protected sourcechannel(selectorprovider provider) {\n super(provider);\n }\n public final int validops() {\n return selectionkey.op_read;\n }\n }\n\n public static abstract class sinkchannel\n extends abstractselectablechannel\n implements writablebytechannel, gatheringbytechannel\n {\n protected sinkchannel(selectorprovider provider) {\n super(provider);\n }\n public final int validops() {\n return selectionkey.op_write;\n }\n\n }\n protected pipe() { }\n //从管道读取数据,要访问source通道\n public abstract sourcechannel source();\n //向管道写入数据,要访问sink通道\n public abstract sinkchannel sink();\n //管道创建\n public static pipe open() throws ioexception {\n return selectorprovider.provider().openpipe();\n }\n}\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n\n\n\n# 第四章 selectors\n\nselector(选择器)是java nio中能够检测一到多个nio通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,仅用单个线程来处理多个channels的好处是,只需要更少的线程来处理通道。\n\n\n# 4.1 基础\n\n * selector:一个selector可以注册多个channel,一个线程通过管理selector实现管理多个channel。\n * selectablechannel:提供channle selectability的通用方法。\n * selectionkey:封装了channle和selector的注册关系,包含注册的感兴趣的事件。\n\n\n# 4.2 示例\n\n//创建selector\nselector selector = selector.open();\nchannel.configureblocking(false);\n//注册selector\nselectionkey key = channel.register(selector, selectionkey.op_read);\nwhile(true) {\n//单线程阻塞检查selector中就绪的事件\n int readychannels = selector.select();\n if(readychannels == 0) continue;\n set selectedkeys = selector.selectedkeys();\n iterator keyiterator = selectedkeys.iterator();\n //循环处理就绪的事件\n while(keyiterator.hasnext()) {\n selectionkey key = keyiterator.next();\n if(key.isacceptable()) {\n // a connection was accepted by a serversocketchannel.\n } else if (key.isconnectable()) {\n // a connection was established with a remote server.\n } else if (key.isreadable()) {\n // a channel is ready for reading\n } else if (key.iswritable()) {\n // a channel is ready for writing\n }\n keyiterator.remove();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n", + "headersStr": "15.2 ByteBuf源码分析 15.3 ByteBuffer相关辅助类", + "content": "# 15.2 ByteBuf源码分析\n\n继承关系 从内存分配的角度看,byteBuf分为两类:\n\n * 堆内存缓冲区:优点是内存的分配和回收快。缺点是进行IO读写时需要一次内存复制,用户空间和内核空间的复制。\n * 直接内存缓冲区:优缺点和堆内存缓冲区整好相反。 经验表明ByteBuf的最佳实践是在IO通信线程的读写缓冲区使用DirectByteBuf,后端业务的编解码模块使用HeapByteBuf,这样的组合可以达到性能最优。 从内存回收的角度看、ByteBuf分两类,基于对象池的ByteBuf和普通ByteBuf。使用内存池后的Netty在高并发和高负载环境下内存和GC更加平稳。 15.2.1 AbstractByteBuf源码分析 实现ByteBuf的一些公共属性和功能。 主要成员变量\n\n static final ResourceLeakDetector leakDetector =\n ResourceLeakDetectorFactory.instance().newResourceLeakDetector(ByteBuf.class);//用于对象是否泄漏,定义为static,意味着所有byteBuf共享\n\n int readerIndex;//读索引\n int writerIndex;//写索引\n private int markedReaderIndex;//读mark\n private int markedWriterIndex;//写mark\n private int maxCapacity;//最大容量\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\nByte数组不在这里,因为AbstractByteBuf无法确定使用直接内存还是堆内存。 readBytes(ByteBuf dst, int dstIndex, int length)\n\npublic ByteBuf readBytes(ByteBuf dst, int dstIndex, int length) {\n checkReadableBytes(length);//校验可读性\n getBytes(readerIndex, dst, dstIndex, length);//读取。从readerIndex开始读取length个字节到目标数组中\n readerIndex += length;//移动读指针\n return this;\n }\n\n\n1\n2\n3\n4\n5\n6\n\n\n再看一下checkReadableBytes():\n\n/**\n * Throws an {@link IndexOutOfBoundsException} if the current\n * {@linkplain #readableBytes() readable bytes} of this buffer is less\n * than the specified value.\n */\n protected final void checkReadableBytes(int minimumReadableBytes) {\n if (minimumReadableBytes < 0) {\n throw new IllegalArgumentException(\"minimumReadableBytes: \" + minimumReadableBytes + \" (expected: >= 0)\");\n }\n checkReadableBytes0(minimumReadableBytes);\n }\n \nprivate void checkReadableBytes0(int minimumReadableBytes) {\n ensureAccessible();\n if (readerIndex > writerIndex - minimumReadableBytes) {\n throw new IndexOutOfBoundsException(String.format(\n \"readerIndex(%d) + length(%d) exceeds writerIndex(%d): %s\",\n readerIndex, minimumReadableBytes, writerIndex, this));\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\nwriteBytes(byte[] src, int srcIndex, int length)\n\npublic ByteBuf writeBytes(byte[] src, int srcIndex, int length) {\n ensureWritable(length);//可写校验和扩容\n setBytes(writerIndex, src, srcIndex, length);//从writerIndex开始写length长度\n writerIndex += length;//移动写指针\n return this;\n }\n\n\n1\n2\n3\n4\n5\n6\n\n\nensureWritable(int minWritableBytes)\n\npublic ByteBuf ensureWritable(int minWritableBytes) {\n if (minWritableBytes < 0) {\n throw new IllegalArgumentException(String.format(\n \"minWritableBytes: %d (expected: >= 0)\", minWritableBytes));\n }\n ensureWritable0(minWritableBytes);\n return this;\n }\n \nfinal void ensureWritable0(int minWritableBytes) {\n ensureAccessible();//检查这个buf是否还有引用(如果已经没有引用那就没必要在写了)\n if (minWritableBytes <= writableBytes()) {//写入的字节小于可写字节,校验通过\n return;\n }\n\n if (minWritableBytes > maxCapacity - writerIndex) {//写入的字节大于最大可写入字节,抛异常\n throw new IndexOutOfBoundsException(String.format(\n \"writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s\",\n writerIndex, minWritableBytes, maxCapacity, this));\n }\n\n // Normalize the current capacity to the power of 2.\n int newCapacity = alloc().calculateNewCapacity(writerIndex + minWritableBytes, maxCapacity);\n\n // Adjust to the new capacity.\n capacity(newCapacity);\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n\n\n重用缓冲区\n\npublic ByteBuf discardReadBytes() {\n ensureAccessible();\n if (readerIndex == 0) {\n return this;\n }\n\n if (readerIndex != writerIndex) {\n setBytes(0, this, readerIndex, writerIndex - readerIndex);//复制缓冲区\n writerIndex -= readerIndex;//重置写指针\n adjustMarkers(readerIndex);//调整mark指针\n readerIndex = 0;//重置读指针\n } else {\n adjustMarkers(readerIndex);\n writerIndex = readerIndex = 0;\n }\n return this;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n\n\n15.2.2 AbstractReferenceCountedByteBuf源码分析\n\npublic abstract class AbstractReferenceCountedByteBuf extends AbstractByteBuf {\n\n private static final AtomicIntegerFieldUpdater refCntUpdater =\n AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, \"refCnt\");//通过原子的方式对成员变量进行更新,消除锁\n\n private volatile int refCnt;//跟踪对象的引用次数,采用CAS对其自增1,默认值为1\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n15.2.2 UnPooledHeapByteBuf源码分析 非池化的基于堆内存,频繁的大块内存分配和回收会对性能造成影响,但是相比对外内存的申请和释放,成本还是低一些。 相比HeapByteBuf, UnPooledHeapByteBuf的实现更加加单,也不容易出现内存管理方面的问题,因此在满足性能的条件下,推荐使用UnPooledHeapByteBuf。\n\npublic class UnpooledHeapByteBuf extends AbstractReferenceCountedByteBuf {\nprivate final ByteBufAllocator alloc;\n byte[] array;//这里直接使用JDK的ByteBuffer也可以,之所以使用Byte数组是因为性能和便捷的位操作\n private ByteBuffer tmpNioBuf;//用于实现将netty的byteBuf转换为JDK的ByteBuffer\n }\n\n\n1\n2\n3\n4\n5\n\n\n转换JDK Buffer netty基于byte数组实现 jdk的nio buf提供wrap方法,可直接实现 转换 看下转换方法\n\npublic ByteBuffer nioBuffer(int index, int length) {\n ensureAccessible();\n return ByteBuffer.wrap(array, index, length).slice();\n }\npublic ByteBuffer slice() {\n return new HeapByteBuffer(hb,//仍然使用的是原buffer的全局数组,只是改变了position和limit的位置,所以新buf和原buf内容是相互影响的\n -1,\n 0,\n this.remaining(),\n this.remaining(),\n this.position() + offset);\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\nslice方法的作用:copy position到limit之间的内容, 15.2.3 pooledByteBuf内存池原理分析 PoolArena是netty的内存池显现类。 为了集中管理内存,提供内存申请是释放的效率,很多框架会申请一大块内存,提供相应的接口分配和释放内存,这样就不再频繁的使用系统调用来使用内存,可以提高性能。预先申请的那块内存就叫Memory Arena。PoolArena是netty对Memory Arena的实现。 Netty的PoolArena由多个chunk组成,每个chunk由多个Page组成。 PoolArena源码:\n\nabstract class PoolArena implements PoolArenaMetric {\n static final boolean HAS_UNSAFE = PlatformDependent.hasUnsafe();\n\n enum SizeClass {\n Tiny,\n Small,\n Normal\n }\n\n static final int numTinySubpagePools = 512 >>> 4;\n\n final PooledByteBufAllocator parent;\n\n private final int maxOrder;\n final int pageSize;\n final int pageShifts;\n final int chunkSize;\n final int subpageOverflowMask;\n final int numSmallSubpagePools;\n final int directMemoryCacheAlignment;\n final int directMemoryCacheAlignmentMask;\n private final PoolSubpage[] tinySubpagePools;\n private final PoolSubpage[] smallSubpagePools;\n\n private final PoolChunkList q050;\n private final PoolChunkList q025;\n private final PoolChunkList q000;\n private final PoolChunkList qInit;\n private final PoolChunkList q075;\n private final PoolChunkList q100;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n\n\nPoolChunk的实现 PoolChunk负责多个Page的内存管理,PoolChunk将其负责的多个Page构建成一棵二叉树。 假设一个chunk由16个page组成,则其组织形式: Page大小是4字节,chunk大小是64字节。 每个节点都记录自己在整个Memory Arena中的偏移地址,一旦被分配,则该节点及其子节点在接下来的内存分配过程中会被忽略。 举例来说,我们申请16个字节空间,则第三层的某个节点会被标记为已分配,则再次分配内存的时候会从其他三个节点中分配。 分配内存时对树采用深度优先算法,但是从哪棵子树开始深度遍历是随机的。 PoolSubPage的实现 申请内存小于一个page,则内存分配在page中完成,每个page会被分为大小相等的多个块。 被分的单位块大小等于第一次申请的内存大小,例如一个Page8字节,第一次申请2字节,则该page被切分成4块,每块2字节。而且这个page以后只能用于分配2字节的内存申请,如果再来一个4字节的内存申请,只能在另一个Page中申请。 Page使用标识位来表示内存块是否可用。维护一个long数组,每个位表示一个块的使用情况。 例如page为128字节,第一次申请内存为1字节,则该page被分为128块,则long数组中有2个元素,(每个long64位,两个long可以表示128位)。0、1表示该块是否可用。 15.2.4 PooledDirectByteBuf内存池原理分析 创建字节缓冲区 由于采用内存池实现,所以创建PooledDirectByteBuf对象不能new一个实例,而是从内存池获取。然后设置引用计数器。\n\nstatic PooledDirectByteBuf newInstance(int maxCapacity) {\n PooledDirectByteBuf buf = RECYCLER.get();\n buf.reuse(maxCapacity);\n return buf;\n }\nfinal void reuse(int maxCapacity) {\n maxCapacity(maxCapacity);\n setRefCnt(1);\n setIndex0(0, 0);\n discardMarks();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n复制字节缓冲区 会从内存池中获取一个新的buffer而不是new一个。\n\n\n# 15.3 ByteBuffer相关辅助类\n\n15.3.1 ByteBufHolder 对消息体进行包装和抽象,不同的子类有不同的实现。 实现ByteBufHolder的子类可以自己实现一些实用的方法。 Netty也有一些子类继承自ByteBufHolder。 15.3.2 ByteBufAllocator 字节缓冲区分配器,其实现类有两种:基于池的和普通的。 其API: 15.3.3 CompositeByteBuf 允许将多个ByteBuf组装到一起。 使用场景:如某协议包含消息头和消息体,当对消息进行编码的时候需要进行整合。 这种场景有两种实现方式:\n\n * 将一个buf复制到另一个buf。或者创建一个新的buf将两个buf都放到新的buf。\n * 通过List或其他集合容器,将两个buf都放入容器统一维护和处理。\n\n看下源码:\n\npublic class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements Iterable {\n\n private static final ByteBuffer EMPTY_NIO_BUFFER = Unpooled.EMPTY_BUFFER.nioBuffer();\n private static final Iterator EMPTY_ITERATOR = Collections.emptyList().iterator();\n\n private final ByteBufAllocator alloc;\n private final boolean direct;\n private final ComponentList components;//维护buf的容器\n private final int maxNumComponents;\n\n private boolean freed;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n再看下ComponentList:\n\nprivate static final class ComponentList extends ArrayList {\n\n ComponentList(int initialCapacity) {\n super(initialCapacity);\n }\n\n // Expose this methods so we not need to create a new subList just to remove a range of elements.\n @Override\n public void removeRange(int fromIndex, int toIndex) {\n super.removeRange(fromIndex, toIndex);\n }\n }\n\nprivate static final class Component {\n final ByteBuf buf;\n final int length;\n int offset;//在集合中的位置偏移\n int endOffset;\n\n Component(ByteBuf buf) {\n this.buf = buf;\n length = buf.readableBytes();\n }\n\n void freeIfNecessary() {\n buf.release(); // We should not get a NPE here. If so, it must be a bug.\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n\n\n15.3.4 ByteBufUtil 几个常用的工具方法:\n\n * encodeString\n * decodeString\n * hexDump", + "normalizedContent": "# 15.2 bytebuf源码分析\n\n继承关系 从内存分配的角度看,bytebuf分为两类:\n\n * 堆内存缓冲区:优点是内存的分配和回收快。缺点是进行io读写时需要一次内存复制,用户空间和内核空间的复制。\n * 直接内存缓冲区:优缺点和堆内存缓冲区整好相反。 经验表明bytebuf的最佳实践是在io通信线程的读写缓冲区使用directbytebuf,后端业务的编解码模块使用heapbytebuf,这样的组合可以达到性能最优。 从内存回收的角度看、bytebuf分两类,基于对象池的bytebuf和普通bytebuf。使用内存池后的netty在高并发和高负载环境下内存和gc更加平稳。 15.2.1 abstractbytebuf源码分析 实现bytebuf的一些公共属性和功能。 主要成员变量\n\n static final resourceleakdetector leakdetector =\n resourceleakdetectorfactory.instance().newresourceleakdetector(bytebuf.class);//用于对象是否泄漏,定义为static,意味着所有bytebuf共享\n\n int readerindex;//读索引\n int writerindex;//写索引\n private int markedreaderindex;//读mark\n private int markedwriterindex;//写mark\n private int maxcapacity;//最大容量\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\nbyte数组不在这里,因为abstractbytebuf无法确定使用直接内存还是堆内存。 readbytes(bytebuf dst, int dstindex, int length)\n\npublic bytebuf readbytes(bytebuf dst, int dstindex, int length) {\n checkreadablebytes(length);//校验可读性\n getbytes(readerindex, dst, dstindex, length);//读取。从readerindex开始读取length个字节到目标数组中\n readerindex += length;//移动读指针\n return this;\n }\n\n\n1\n2\n3\n4\n5\n6\n\n\n再看一下checkreadablebytes():\n\n/**\n * throws an {@link indexoutofboundsexception} if the current\n * {@linkplain #readablebytes() readable bytes} of this buffer is less\n * than the specified value.\n */\n protected final void checkreadablebytes(int minimumreadablebytes) {\n if (minimumreadablebytes < 0) {\n throw new illegalargumentexception(\"minimumreadablebytes: \" + minimumreadablebytes + \" (expected: >= 0)\");\n }\n checkreadablebytes0(minimumreadablebytes);\n }\n \nprivate void checkreadablebytes0(int minimumreadablebytes) {\n ensureaccessible();\n if (readerindex > writerindex - minimumreadablebytes) {\n throw new indexoutofboundsexception(string.format(\n \"readerindex(%d) + length(%d) exceeds writerindex(%d): %s\",\n readerindex, minimumreadablebytes, writerindex, this));\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\nwritebytes(byte[] src, int srcindex, int length)\n\npublic bytebuf writebytes(byte[] src, int srcindex, int length) {\n ensurewritable(length);//可写校验和扩容\n setbytes(writerindex, src, srcindex, length);//从writerindex开始写length长度\n writerindex += length;//移动写指针\n return this;\n }\n\n\n1\n2\n3\n4\n5\n6\n\n\nensurewritable(int minwritablebytes)\n\npublic bytebuf ensurewritable(int minwritablebytes) {\n if (minwritablebytes < 0) {\n throw new illegalargumentexception(string.format(\n \"minwritablebytes: %d (expected: >= 0)\", minwritablebytes));\n }\n ensurewritable0(minwritablebytes);\n return this;\n }\n \nfinal void ensurewritable0(int minwritablebytes) {\n ensureaccessible();//检查这个buf是否还有引用(如果已经没有引用那就没必要在写了)\n if (minwritablebytes <= writablebytes()) {//写入的字节小于可写字节,校验通过\n return;\n }\n\n if (minwritablebytes > maxcapacity - writerindex) {//写入的字节大于最大可写入字节,抛异常\n throw new indexoutofboundsexception(string.format(\n \"writerindex(%d) + minwritablebytes(%d) exceeds maxcapacity(%d): %s\",\n writerindex, minwritablebytes, maxcapacity, this));\n }\n\n // normalize the current capacity to the power of 2.\n int newcapacity = alloc().calculatenewcapacity(writerindex + minwritablebytes, maxcapacity);\n\n // adjust to the new capacity.\n capacity(newcapacity);\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n\n\n重用缓冲区\n\npublic bytebuf discardreadbytes() {\n ensureaccessible();\n if (readerindex == 0) {\n return this;\n }\n\n if (readerindex != writerindex) {\n setbytes(0, this, readerindex, writerindex - readerindex);//复制缓冲区\n writerindex -= readerindex;//重置写指针\n adjustmarkers(readerindex);//调整mark指针\n readerindex = 0;//重置读指针\n } else {\n adjustmarkers(readerindex);\n writerindex = readerindex = 0;\n }\n return this;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n\n\n15.2.2 abstractreferencecountedbytebuf源码分析\n\npublic abstract class abstractreferencecountedbytebuf extends abstractbytebuf {\n\n private static final atomicintegerfieldupdater refcntupdater =\n atomicintegerfieldupdater.newupdater(abstractreferencecountedbytebuf.class, \"refcnt\");//通过原子的方式对成员变量进行更新,消除锁\n\n private volatile int refcnt;//跟踪对象的引用次数,采用cas对其自增1,默认值为1\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n15.2.2 unpooledheapbytebuf源码分析 非池化的基于堆内存,频繁的大块内存分配和回收会对性能造成影响,但是相比对外内存的申请和释放,成本还是低一些。 相比heapbytebuf, unpooledheapbytebuf的实现更加加单,也不容易出现内存管理方面的问题,因此在满足性能的条件下,推荐使用unpooledheapbytebuf。\n\npublic class unpooledheapbytebuf extends abstractreferencecountedbytebuf {\nprivate final bytebufallocator alloc;\n byte[] array;//这里直接使用jdk的bytebuffer也可以,之所以使用byte数组是因为性能和便捷的位操作\n private bytebuffer tmpniobuf;//用于实现将netty的bytebuf转换为jdk的bytebuffer\n }\n\n\n1\n2\n3\n4\n5\n\n\n转换jdk buffer netty基于byte数组实现 jdk的nio buf提供wrap方法,可直接实现 转换 看下转换方法\n\npublic bytebuffer niobuffer(int index, int length) {\n ensureaccessible();\n return bytebuffer.wrap(array, index, length).slice();\n }\npublic bytebuffer slice() {\n return new heapbytebuffer(hb,//仍然使用的是原buffer的全局数组,只是改变了position和limit的位置,所以新buf和原buf内容是相互影响的\n -1,\n 0,\n this.remaining(),\n this.remaining(),\n this.position() + offset);\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\nslice方法的作用:copy position到limit之间的内容, 15.2.3 pooledbytebuf内存池原理分析 poolarena是netty的内存池显现类。 为了集中管理内存,提供内存申请是释放的效率,很多框架会申请一大块内存,提供相应的接口分配和释放内存,这样就不再频繁的使用系统调用来使用内存,可以提高性能。预先申请的那块内存就叫memory arena。poolarena是netty对memory arena的实现。 netty的poolarena由多个chunk组成,每个chunk由多个page组成。 poolarena源码:\n\nabstract class poolarena implements poolarenametric {\n static final boolean has_unsafe = platformdependent.hasunsafe();\n\n enum sizeclass {\n tiny,\n small,\n normal\n }\n\n static final int numtinysubpagepools = 512 >>> 4;\n\n final pooledbytebufallocator parent;\n\n private final int maxorder;\n final int pagesize;\n final int pageshifts;\n final int chunksize;\n final int subpageoverflowmask;\n final int numsmallsubpagepools;\n final int directmemorycachealignment;\n final int directmemorycachealignmentmask;\n private final poolsubpage[] tinysubpagepools;\n private final poolsubpage[] smallsubpagepools;\n\n private final poolchunklist q050;\n private final poolchunklist q025;\n private final poolchunklist q000;\n private final poolchunklist qinit;\n private final poolchunklist q075;\n private final poolchunklist q100;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n\n\npoolchunk的实现 poolchunk负责多个page的内存管理,poolchunk将其负责的多个page构建成一棵二叉树。 假设一个chunk由16个page组成,则其组织形式: page大小是4字节,chunk大小是64字节。 每个节点都记录自己在整个memory arena中的偏移地址,一旦被分配,则该节点及其子节点在接下来的内存分配过程中会被忽略。 举例来说,我们申请16个字节空间,则第三层的某个节点会被标记为已分配,则再次分配内存的时候会从其他三个节点中分配。 分配内存时对树采用深度优先算法,但是从哪棵子树开始深度遍历是随机的。 poolsubpage的实现 申请内存小于一个page,则内存分配在page中完成,每个page会被分为大小相等的多个块。 被分的单位块大小等于第一次申请的内存大小,例如一个page8字节,第一次申请2字节,则该page被切分成4块,每块2字节。而且这个page以后只能用于分配2字节的内存申请,如果再来一个4字节的内存申请,只能在另一个page中申请。 page使用标识位来表示内存块是否可用。维护一个long数组,每个位表示一个块的使用情况。 例如page为128字节,第一次申请内存为1字节,则该page被分为128块,则long数组中有2个元素,(每个long64位,两个long可以表示128位)。0、1表示该块是否可用。 15.2.4 pooleddirectbytebuf内存池原理分析 创建字节缓冲区 由于采用内存池实现,所以创建pooleddirectbytebuf对象不能new一个实例,而是从内存池获取。然后设置引用计数器。\n\nstatic pooleddirectbytebuf newinstance(int maxcapacity) {\n pooleddirectbytebuf buf = recycler.get();\n buf.reuse(maxcapacity);\n return buf;\n }\nfinal void reuse(int maxcapacity) {\n maxcapacity(maxcapacity);\n setrefcnt(1);\n setindex0(0, 0);\n discardmarks();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n复制字节缓冲区 会从内存池中获取一个新的buffer而不是new一个。\n\n\n# 15.3 bytebuffer相关辅助类\n\n15.3.1 bytebufholder 对消息体进行包装和抽象,不同的子类有不同的实现。 实现bytebufholder的子类可以自己实现一些实用的方法。 netty也有一些子类继承自bytebufholder。 15.3.2 bytebufallocator 字节缓冲区分配器,其实现类有两种:基于池的和普通的。 其api: 15.3.3 compositebytebuf 允许将多个bytebuf组装到一起。 使用场景:如某协议包含消息头和消息体,当对消息进行编码的时候需要进行整合。 这种场景有两种实现方式:\n\n * 将一个buf复制到另一个buf。或者创建一个新的buf将两个buf都放到新的buf。\n * 通过list或其他集合容器,将两个buf都放入容器统一维护和处理。\n\n看下源码:\n\npublic class compositebytebuf extends abstractreferencecountedbytebuf implements iterable {\n\n private static final bytebuffer empty_nio_buffer = unpooled.empty_buffer.niobuffer();\n private static final iterator empty_iterator = collections.emptylist().iterator();\n\n private final bytebufallocator alloc;\n private final boolean direct;\n private final componentlist components;//维护buf的容器\n private final int maxnumcomponents;\n\n private boolean freed;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n再看下componentlist:\n\nprivate static final class componentlist extends arraylist {\n\n componentlist(int initialcapacity) {\n super(initialcapacity);\n }\n\n // expose this methods so we not need to create a new sublist just to remove a range of elements.\n @override\n public void removerange(int fromindex, int toindex) {\n super.removerange(fromindex, toindex);\n }\n }\n\nprivate static final class component {\n final bytebuf buf;\n final int length;\n int offset;//在集合中的位置偏移\n int endoffset;\n\n component(bytebuf buf) {\n this.buf = buf;\n length = buf.readablebytes();\n }\n\n void freeifnecessary() {\n buf.release(); // we should not get a npe here. if so, it must be a bug.\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n\n\n15.3.4 bytebufutil 几个常用的工具方法:\n\n * encodestring\n * decodestring\n * hexdump", "charsets": { "cjk": true }, @@ -5737,26 +6111,76 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "如何保证Kafka的可靠性", + "title": "什么是 CAP ?什么是最终一致性?什么是幂等操作?", "frontmatter": { - "title": "如何保证Kafka的可靠性", - "date": "2022-05-21T23:47:03.000Z", - "permalink": "/pages/76a94c/", + "title": "什么是 CAP ?什么是最终一致性?什么是幂等操作?", + "date": "2022-05-21T17:11:49.000Z", + "permalink": "/pages/20c647/", "categories": [ - "中间件", - "Kafka" + "系统设计" ], "tags": [ null ] }, - "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/40.Kafka/040.%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81Kafka%E7%9A%84%E5%8F%AF%E9%9D%A0%E6%80%A7.html", - "relativePath": "04.中间件/40.Kafka/040.如何保证Kafka的可靠性.md", - "key": "v-684c4815", - "path": "/pages/76a94c/", - "headersStr": null, - "content": "# 可靠性保证\n\nkafka在哪些方面做出保证呢?\n\n * 单生产者,kafka可以保证顺序消费\n * 只有当写入的消息被写入所有的副本时,才认为是“已提交”的\n * 只要有一个活跃的分区副本,那么,已提交的数据就不会丢失。\n * 消费者只能读取已经提交的消息\n\n如何保证topic元数据信息安全 replication.factor配置topic的复制系数\n\n所有的分区副本都没有正常同步,分区首领又挂掉了,如何做选举? 方案有两个\n\n * 在未正常同步的副中选举一个作为首领,缺点是存在丢失数据的风险。\n * 等待旧的首领恢复。缺点是可用性低。\n\n配置unclean.leader.election.enable=true表示允许不同步的副本称为首领,这也将面临丢失消息的风险。\n\n\n# producer如何做可靠性保证\n\n如何保证可靠投递消息?\n\n 1. ack=all表示broker要等待所有分区副本同步完消息才返回给producer response,是最可靠的投递。\n 2. 设置重试次数,注意要做业务幂等。\n 3. 有些小时不可重试错误,比如消息太大、认证错误等。根据业务来特殊处理这类消息。\n\n\n# consumer如何做可靠性保证\n\nconsumer请求的offset在broker上不存在时怎么处理? 配置auto.offset.reset指定了两种策略:\n\n * earliest:从分区的开始读取\n * latest:从分区的末尾读取", - "normalizedContent": "# 可靠性保证\n\nkafka在哪些方面做出保证呢?\n\n * 单生产者,kafka可以保证顺序消费\n * 只有当写入的消息被写入所有的副本时,才认为是“已提交”的\n * 只要有一个活跃的分区副本,那么,已提交的数据就不会丢失。\n * 消费者只能读取已经提交的消息\n\n如何保证topic元数据信息安全 replication.factor配置topic的复制系数\n\n所有的分区副本都没有正常同步,分区首领又挂掉了,如何做选举? 方案有两个\n\n * 在未正常同步的副中选举一个作为首领,缺点是存在丢失数据的风险。\n * 等待旧的首领恢复。缺点是可用性低。\n\n配置unclean.leader.election.enable=true表示允许不同步的副本称为首领,这也将面临丢失消息的风险。\n\n\n# producer如何做可靠性保证\n\n如何保证可靠投递消息?\n\n 1. ack=all表示broker要等待所有分区副本同步完消息才返回给producer response,是最可靠的投递。\n 2. 设置重试次数,注意要做业务幂等。\n 3. 有些小时不可重试错误,比如消息太大、认证错误等。根据业务来特殊处理这类消息。\n\n\n# consumer如何做可靠性保证\n\nconsumer请求的offset在broker上不存在时怎么处理? 配置auto.offset.reset指定了两种策略:\n\n * earliest:从分区的开始读取\n * latest:从分区的末尾读取", + "regularPath": "/08.%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/010.%E4%BB%80%E4%B9%88%E6%98%AF%20CAP%20%EF%BC%9F%E4%BB%80%E4%B9%88%E6%98%AF%E6%9C%80%E7%BB%88%E4%B8%80%E8%87%B4%E6%80%A7%EF%BC%9F%E4%BB%80%E4%B9%88%E6%98%AF%E5%B9%82%E7%AD%89%E6%93%8D%E4%BD%9C%EF%BC%9F.html", + "relativePath": "08.系统设计/010.什么是 CAP ?什么是最终一致性?什么是幂等操作?.md", + "key": "v-5675b06b", + "path": "/pages/20c647/", + "headers": [ + { + "level": 3, + "title": "CAP理论", + "slug": "cap理论", + "normalizedTitle": "cap理论", + "charIndex": 2 + }, + { + "level": 3, + "title": "为什么这三者如此重要?", + "slug": "为什么这三者如此重要", + "normalizedTitle": "为什么这三者如此重要?", + "charIndex": 261 + }, + { + "level": 3, + "title": "CAP权衡", + "slug": "cap权衡", + "normalizedTitle": "cap权衡", + "charIndex": 577 + }, + { + "level": 3, + "title": "三种组合", + "slug": "三种组合", + "normalizedTitle": "三种组合", + "charIndex": 784 + }, + { + "level": 3, + "title": "一致性的妥协——最终一致性和Base原则", + "slug": "一致性的妥协-最终一致性和base原则", + "normalizedTitle": "一致性的妥协——最终一致性和base原则", + "charIndex": 1070 + }, + { + "level": 3, + "title": "幂等性", + "slug": "幂等性", + "normalizedTitle": "幂等性", + "charIndex": 1420 + }, + { + "level": 3, + "title": "如何解决幂等性问题", + "slug": "如何解决幂等性问题", + "normalizedTitle": "如何解决幂等性问题", + "charIndex": 1560 + } + ], + "headersStr": "CAP理论 为什么这三者如此重要? CAP权衡 三种组合 一致性的妥协——最终一致性和Base原则 幂等性 如何解决幂等性问题", + "content": "# CAP理论\n\nCAP的结论非常简单:在分布式系统里,有3个属性非常重要,但只能同时满足其中的2个。\n\nConsistency:all nodes在任何时刻看到的data都是一样的(或说client的read操作总是返回最新写入的那个value) Availability:系统时刻都允许操作,并且操作总会快速被Coordinator响应,最终client很快就得到返回的结果 Partition-tolerance:尽管网路有时候会因为故障导致被分隔开,但是系统依然在正常工作(或者说在满足前述的条件下工作)\n\n\n# 为什么这三者如此重要?\n\nFor Availability:\n\n经统计,对于Google、Amazon这样的数据公司,系统增加500ms的延迟就会损失公司20%的收益,所以必须快速且可靠地进行 Read/Write。\n\nFor Consistency:\n\n比如在银行系统,任何client都必须查看到最新的updated data item,不然就会给交易造成致命的影响。\n\nFor Partition-tolerance:\n\n因为Internet可能因为某些原因随时断开(Router故障、海地线缆断开、DNS故障);即使在同一个data center里,故障都会随时随地地发生,比如 Rack switcher 宕机。\n\n\n# CAP权衡\n\n * 如今的云计算环境里,因为网络随时都会被隔离开来,这是无法避免的,P是必须满足的,那么CAP暗示一个system要在C和A中做出抉择。\n * A和C并不是一个硬币的两面,只能选择其中一个;A和C应该看成天平,系统可以选择向哪边倾斜,但另一边也应该一定程度的保留。\n * 对于A和C之间的选择,不应该粗粒度的整个系统级别进行选取,而应该针对系统中的不同子系统,针对性的采取不同的取舍策略。\n\n\n# 三种组合\n\n * CA: 保证可用性和一致性,放弃分区:除非不是分布式架构,或者应用在一个永不会通信故障的网络中(理想),只有个别场景符合,当前的互联网架构显然不符合使用\n * CP: 保证一致性和分区容忍性,放弃可用性:当节点间不可通信时,进行阻塞,直到通信恢复,期间无法再对外提供服务,用户体验不好,如A转账给B,只有A扣款成功并B收款成功,整个事务才算完成,显然耗费资源\n * AP: 保证可用性和分区容忍性,放弃强一致性(使用最终一致性):给出一个用户可以忍受的时间,时间内达成数据的最终一致性,比如跨行转账,并不是立刻到账,可能是明天,或者2小时内到账\n\n\n# 一致性的妥协——最终一致性和Base原则\n\n * BA(Basically Available)基本可用:系统在绝大部分时间应处于可用状态,允许出现故障损失部分可用性,但保证核心可用。\n * S(Soft State)软状态:数据状态不要求在任何时刻都保持一致,允许存在中间状态,而该状态不影响系统可用性。对于多副本的存储系统而言,就是允许副本之间的同步存在延时,并且在这个过程中系统依旧可以响应客户端请求。\n * E(Eventual Consistency)最终一致性:尽管软状态不要求分布式数据在任何时刻都保持一致,但经过一定时间后,这些数据最终能达到一致性状态。 BASE理论的核心思想是:把分布式系统的可用性放在首位,放弃CAP中对数据强一致性的追求,只要系统能保证数据最终一致。\n\n\n# 幂等性\n\n在微服务架构下,不同微服务间会有大量的基于http,rpc或者mq消息的网络通信,接口的重复调用以及消息的重复消费可能会经常发生。\n\n微服务架构应该具有幂等性,当接口被重复调用时,消息被重复消费时,对系统的产生的影响应该和接口被调用一次,消息被消费一次时一样。\n\n\n# 如何解决幂等性问题\n\n * 全局唯一ID。根据业务生成一个全局唯一ID,在调用接口时会传入该ID,接口提供方会从相应的存储系统比如Redis中去检索这个全局ID是否存在,如果存在则说明该操作已经执行过了,将拒绝本次服务请求;否则将相应该服务请求并将全局ID存入存储系统中,之后包含相同业务ID参数的请求将被拒绝。\n\n * 数据库表唯一键。这种方法适用于在业务中有唯一标识的插入场景。比如在支付场景中,一个订单只会支付一次,可以建立一张去重表,将订单ID作为唯一索引。把支付并且写入支付单据到去重表放入一个事务中,这样当出现重复支付时,数据库就会抛出唯一约束异常,操作就会回滚。这样保证了订单只会被支付一次。\n\n * 多版本并发控制适合对更新请求作幂等性控制,比如要更新商品的名字,这是就可以在更新的接口中增加一个版本号来做幂等性控制", + "normalizedContent": "# cap理论\n\ncap的结论非常简单:在分布式系统里,有3个属性非常重要,但只能同时满足其中的2个。\n\nconsistency:all nodes在任何时刻看到的data都是一样的(或说client的read操作总是返回最新写入的那个value) availability:系统时刻都允许操作,并且操作总会快速被coordinator响应,最终client很快就得到返回的结果 partition-tolerance:尽管网路有时候会因为故障导致被分隔开,但是系统依然在正常工作(或者说在满足前述的条件下工作)\n\n\n# 为什么这三者如此重要?\n\nfor availability:\n\n经统计,对于google、amazon这样的数据公司,系统增加500ms的延迟就会损失公司20%的收益,所以必须快速且可靠地进行 read/write。\n\nfor consistency:\n\n比如在银行系统,任何client都必须查看到最新的updated data item,不然就会给交易造成致命的影响。\n\nfor partition-tolerance:\n\n因为internet可能因为某些原因随时断开(router故障、海地线缆断开、dns故障);即使在同一个data center里,故障都会随时随地地发生,比如 rack switcher 宕机。\n\n\n# cap权衡\n\n * 如今的云计算环境里,因为网络随时都会被隔离开来,这是无法避免的,p是必须满足的,那么cap暗示一个system要在c和a中做出抉择。\n * a和c并不是一个硬币的两面,只能选择其中一个;a和c应该看成天平,系统可以选择向哪边倾斜,但另一边也应该一定程度的保留。\n * 对于a和c之间的选择,不应该粗粒度的整个系统级别进行选取,而应该针对系统中的不同子系统,针对性的采取不同的取舍策略。\n\n\n# 三种组合\n\n * ca: 保证可用性和一致性,放弃分区:除非不是分布式架构,或者应用在一个永不会通信故障的网络中(理想),只有个别场景符合,当前的互联网架构显然不符合使用\n * cp: 保证一致性和分区容忍性,放弃可用性:当节点间不可通信时,进行阻塞,直到通信恢复,期间无法再对外提供服务,用户体验不好,如a转账给b,只有a扣款成功并b收款成功,整个事务才算完成,显然耗费资源\n * ap: 保证可用性和分区容忍性,放弃强一致性(使用最终一致性):给出一个用户可以忍受的时间,时间内达成数据的最终一致性,比如跨行转账,并不是立刻到账,可能是明天,或者2小时内到账\n\n\n# 一致性的妥协——最终一致性和base原则\n\n * ba(basically available)基本可用:系统在绝大部分时间应处于可用状态,允许出现故障损失部分可用性,但保证核心可用。\n * s(soft state)软状态:数据状态不要求在任何时刻都保持一致,允许存在中间状态,而该状态不影响系统可用性。对于多副本的存储系统而言,就是允许副本之间的同步存在延时,并且在这个过程中系统依旧可以响应客户端请求。\n * e(eventual consistency)最终一致性:尽管软状态不要求分布式数据在任何时刻都保持一致,但经过一定时间后,这些数据最终能达到一致性状态。 base理论的核心思想是:把分布式系统的可用性放在首位,放弃cap中对数据强一致性的追求,只要系统能保证数据最终一致。\n\n\n# 幂等性\n\n在微服务架构下,不同微服务间会有大量的基于http,rpc或者mq消息的网络通信,接口的重复调用以及消息的重复消费可能会经常发生。\n\n微服务架构应该具有幂等性,当接口被重复调用时,消息被重复消费时,对系统的产生的影响应该和接口被调用一次,消息被消费一次时一样。\n\n\n# 如何解决幂等性问题\n\n * 全局唯一id。根据业务生成一个全局唯一id,在调用接口时会传入该id,接口提供方会从相应的存储系统比如redis中去检索这个全局id是否存在,如果存在则说明该操作已经执行过了,将拒绝本次服务请求;否则将相应该服务请求并将全局id存入存储系统中,之后包含相同业务id参数的请求将被拒绝。\n\n * 数据库表唯一键。这种方法适用于在业务中有唯一标识的插入场景。比如在支付场景中,一个订单只会支付一次,可以建立一张去重表,将订单id作为唯一索引。把支付并且写入支付单据到去重表放入一个事务中,这样当出现重复支付时,数据库就会抛出唯一约束异常,操作就会回滚。这样保证了订单只会被支付一次。\n\n * 多版本并发控制适合对更新请求作幂等性控制,比如要更新商品的名字,这是就可以在更新的接口中增加一个版本号来做幂等性控制", "charsets": { "cjk": true }, @@ -5918,76 +6342,26 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "什么是 CAP ?什么是最终一致性?什么是幂等操作?", + "title": "如何保证Kafka的可靠性", "frontmatter": { - "title": "什么是 CAP ?什么是最终一致性?什么是幂等操作?", - "date": "2022-05-21T17:11:49.000Z", - "permalink": "/pages/20c647/", + "title": "如何保证Kafka的可靠性", + "date": "2022-05-21T23:47:03.000Z", + "permalink": "/pages/76a94c/", "categories": [ - "系统设计" + "中间件", + "Kafka" ], "tags": [ null ] }, - "regularPath": "/08.%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/010.%E4%BB%80%E4%B9%88%E6%98%AF%20CAP%20%EF%BC%9F%E4%BB%80%E4%B9%88%E6%98%AF%E6%9C%80%E7%BB%88%E4%B8%80%E8%87%B4%E6%80%A7%EF%BC%9F%E4%BB%80%E4%B9%88%E6%98%AF%E5%B9%82%E7%AD%89%E6%93%8D%E4%BD%9C%EF%BC%9F.html", - "relativePath": "08.系统设计/010.什么是 CAP ?什么是最终一致性?什么是幂等操作?.md", - "key": "v-5675b06b", - "path": "/pages/20c647/", - "headers": [ - { - "level": 3, - "title": "CAP理论", - "slug": "cap理论", - "normalizedTitle": "cap理论", - "charIndex": 2 - }, - { - "level": 3, - "title": "为什么这三者如此重要?", - "slug": "为什么这三者如此重要", - "normalizedTitle": "为什么这三者如此重要?", - "charIndex": 261 - }, - { - "level": 3, - "title": "CAP权衡", - "slug": "cap权衡", - "normalizedTitle": "cap权衡", - "charIndex": 577 - }, - { - "level": 3, - "title": "三种组合", - "slug": "三种组合", - "normalizedTitle": "三种组合", - "charIndex": 784 - }, - { - "level": 3, - "title": "一致性的妥协——最终一致性和Base原则", - "slug": "一致性的妥协-最终一致性和base原则", - "normalizedTitle": "一致性的妥协——最终一致性和base原则", - "charIndex": 1070 - }, - { - "level": 3, - "title": "幂等性", - "slug": "幂等性", - "normalizedTitle": "幂等性", - "charIndex": 1420 - }, - { - "level": 3, - "title": "如何解决幂等性问题", - "slug": "如何解决幂等性问题", - "normalizedTitle": "如何解决幂等性问题", - "charIndex": 1560 - } - ], - "headersStr": "CAP理论 为什么这三者如此重要? CAP权衡 三种组合 一致性的妥协——最终一致性和Base原则 幂等性 如何解决幂等性问题", - "content": "# CAP理论\n\nCAP的结论非常简单:在分布式系统里,有3个属性非常重要,但只能同时满足其中的2个。\n\nConsistency:all nodes在任何时刻看到的data都是一样的(或说client的read操作总是返回最新写入的那个value) Availability:系统时刻都允许操作,并且操作总会快速被Coordinator响应,最终client很快就得到返回的结果 Partition-tolerance:尽管网路有时候会因为故障导致被分隔开,但是系统依然在正常工作(或者说在满足前述的条件下工作)\n\n\n# 为什么这三者如此重要?\n\nFor Availability:\n\n经统计,对于Google、Amazon这样的数据公司,系统增加500ms的延迟就会损失公司20%的收益,所以必须快速且可靠地进行 Read/Write。\n\nFor Consistency:\n\n比如在银行系统,任何client都必须查看到最新的updated data item,不然就会给交易造成致命的影响。\n\nFor Partition-tolerance:\n\n因为Internet可能因为某些原因随时断开(Router故障、海地线缆断开、DNS故障);即使在同一个data center里,故障都会随时随地地发生,比如 Rack switcher 宕机。\n\n\n# CAP权衡\n\n * 如今的云计算环境里,因为网络随时都会被隔离开来,这是无法避免的,P是必须满足的,那么CAP暗示一个system要在C和A中做出抉择。\n * A和C并不是一个硬币的两面,只能选择其中一个;A和C应该看成天平,系统可以选择向哪边倾斜,但另一边也应该一定程度的保留。\n * 对于A和C之间的选择,不应该粗粒度的整个系统级别进行选取,而应该针对系统中的不同子系统,针对性的采取不同的取舍策略。\n\n\n# 三种组合\n\n * CA: 保证可用性和一致性,放弃分区:除非不是分布式架构,或者应用在一个永不会通信故障的网络中(理想),只有个别场景符合,当前的互联网架构显然不符合使用\n * CP: 保证一致性和分区容忍性,放弃可用性:当节点间不可通信时,进行阻塞,直到通信恢复,期间无法再对外提供服务,用户体验不好,如A转账给B,只有A扣款成功并B收款成功,整个事务才算完成,显然耗费资源\n * AP: 保证可用性和分区容忍性,放弃强一致性(使用最终一致性):给出一个用户可以忍受的时间,时间内达成数据的最终一致性,比如跨行转账,并不是立刻到账,可能是明天,或者2小时内到账\n\n\n# 一致性的妥协——最终一致性和Base原则\n\n * BA(Basically Available)基本可用:系统在绝大部分时间应处于可用状态,允许出现故障损失部分可用性,但保证核心可用。\n * S(Soft State)软状态:数据状态不要求在任何时刻都保持一致,允许存在中间状态,而该状态不影响系统可用性。对于多副本的存储系统而言,就是允许副本之间的同步存在延时,并且在这个过程中系统依旧可以响应客户端请求。\n * E(Eventual Consistency)最终一致性:尽管软状态不要求分布式数据在任何时刻都保持一致,但经过一定时间后,这些数据最终能达到一致性状态。 BASE理论的核心思想是:把分布式系统的可用性放在首位,放弃CAP中对数据强一致性的追求,只要系统能保证数据最终一致。\n\n\n# 幂等性\n\n在微服务架构下,不同微服务间会有大量的基于http,rpc或者mq消息的网络通信,接口的重复调用以及消息的重复消费可能会经常发生。\n\n微服务架构应该具有幂等性,当接口被重复调用时,消息被重复消费时,对系统的产生的影响应该和接口被调用一次,消息被消费一次时一样。\n\n\n# 如何解决幂等性问题\n\n * 全局唯一ID。根据业务生成一个全局唯一ID,在调用接口时会传入该ID,接口提供方会从相应的存储系统比如Redis中去检索这个全局ID是否存在,如果存在则说明该操作已经执行过了,将拒绝本次服务请求;否则将相应该服务请求并将全局ID存入存储系统中,之后包含相同业务ID参数的请求将被拒绝。\n\n * 数据库表唯一键。这种方法适用于在业务中有唯一标识的插入场景。比如在支付场景中,一个订单只会支付一次,可以建立一张去重表,将订单ID作为唯一索引。把支付并且写入支付单据到去重表放入一个事务中,这样当出现重复支付时,数据库就会抛出唯一约束异常,操作就会回滚。这样保证了订单只会被支付一次。\n\n * 多版本并发控制适合对更新请求作幂等性控制,比如要更新商品的名字,这是就可以在更新的接口中增加一个版本号来做幂等性控制", - "normalizedContent": "# cap理论\n\ncap的结论非常简单:在分布式系统里,有3个属性非常重要,但只能同时满足其中的2个。\n\nconsistency:all nodes在任何时刻看到的data都是一样的(或说client的read操作总是返回最新写入的那个value) availability:系统时刻都允许操作,并且操作总会快速被coordinator响应,最终client很快就得到返回的结果 partition-tolerance:尽管网路有时候会因为故障导致被分隔开,但是系统依然在正常工作(或者说在满足前述的条件下工作)\n\n\n# 为什么这三者如此重要?\n\nfor availability:\n\n经统计,对于google、amazon这样的数据公司,系统增加500ms的延迟就会损失公司20%的收益,所以必须快速且可靠地进行 read/write。\n\nfor consistency:\n\n比如在银行系统,任何client都必须查看到最新的updated data item,不然就会给交易造成致命的影响。\n\nfor partition-tolerance:\n\n因为internet可能因为某些原因随时断开(router故障、海地线缆断开、dns故障);即使在同一个data center里,故障都会随时随地地发生,比如 rack switcher 宕机。\n\n\n# cap权衡\n\n * 如今的云计算环境里,因为网络随时都会被隔离开来,这是无法避免的,p是必须满足的,那么cap暗示一个system要在c和a中做出抉择。\n * a和c并不是一个硬币的两面,只能选择其中一个;a和c应该看成天平,系统可以选择向哪边倾斜,但另一边也应该一定程度的保留。\n * 对于a和c之间的选择,不应该粗粒度的整个系统级别进行选取,而应该针对系统中的不同子系统,针对性的采取不同的取舍策略。\n\n\n# 三种组合\n\n * ca: 保证可用性和一致性,放弃分区:除非不是分布式架构,或者应用在一个永不会通信故障的网络中(理想),只有个别场景符合,当前的互联网架构显然不符合使用\n * cp: 保证一致性和分区容忍性,放弃可用性:当节点间不可通信时,进行阻塞,直到通信恢复,期间无法再对外提供服务,用户体验不好,如a转账给b,只有a扣款成功并b收款成功,整个事务才算完成,显然耗费资源\n * ap: 保证可用性和分区容忍性,放弃强一致性(使用最终一致性):给出一个用户可以忍受的时间,时间内达成数据的最终一致性,比如跨行转账,并不是立刻到账,可能是明天,或者2小时内到账\n\n\n# 一致性的妥协——最终一致性和base原则\n\n * ba(basically available)基本可用:系统在绝大部分时间应处于可用状态,允许出现故障损失部分可用性,但保证核心可用。\n * s(soft state)软状态:数据状态不要求在任何时刻都保持一致,允许存在中间状态,而该状态不影响系统可用性。对于多副本的存储系统而言,就是允许副本之间的同步存在延时,并且在这个过程中系统依旧可以响应客户端请求。\n * e(eventual consistency)最终一致性:尽管软状态不要求分布式数据在任何时刻都保持一致,但经过一定时间后,这些数据最终能达到一致性状态。 base理论的核心思想是:把分布式系统的可用性放在首位,放弃cap中对数据强一致性的追求,只要系统能保证数据最终一致。\n\n\n# 幂等性\n\n在微服务架构下,不同微服务间会有大量的基于http,rpc或者mq消息的网络通信,接口的重复调用以及消息的重复消费可能会经常发生。\n\n微服务架构应该具有幂等性,当接口被重复调用时,消息被重复消费时,对系统的产生的影响应该和接口被调用一次,消息被消费一次时一样。\n\n\n# 如何解决幂等性问题\n\n * 全局唯一id。根据业务生成一个全局唯一id,在调用接口时会传入该id,接口提供方会从相应的存储系统比如redis中去检索这个全局id是否存在,如果存在则说明该操作已经执行过了,将拒绝本次服务请求;否则将相应该服务请求并将全局id存入存储系统中,之后包含相同业务id参数的请求将被拒绝。\n\n * 数据库表唯一键。这种方法适用于在业务中有唯一标识的插入场景。比如在支付场景中,一个订单只会支付一次,可以建立一张去重表,将订单id作为唯一索引。把支付并且写入支付单据到去重表放入一个事务中,这样当出现重复支付时,数据库就会抛出唯一约束异常,操作就会回滚。这样保证了订单只会被支付一次。\n\n * 多版本并发控制适合对更新请求作幂等性控制,比如要更新商品的名字,这是就可以在更新的接口中增加一个版本号来做幂等性控制", + "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/40.Kafka/040.%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81Kafka%E7%9A%84%E5%8F%AF%E9%9D%A0%E6%80%A7.html", + "relativePath": "04.中间件/40.Kafka/040.如何保证Kafka的可靠性.md", + "key": "v-684c4815", + "path": "/pages/76a94c/", + "headersStr": null, + "content": "# 可靠性保证\n\nkafka在哪些方面做出保证呢?\n\n * 单生产者,kafka可以保证顺序消费\n * 只有当写入的消息被写入所有的副本时,才认为是“已提交”的\n * 只要有一个活跃的分区副本,那么,已提交的数据就不会丢失。\n * 消费者只能读取已经提交的消息\n\n如何保证topic元数据信息安全 replication.factor配置topic的复制系数\n\n所有的分区副本都没有正常同步,分区首领又挂掉了,如何做选举? 方案有两个\n\n * 在未正常同步的副中选举一个作为首领,缺点是存在丢失数据的风险。\n * 等待旧的首领恢复。缺点是可用性低。\n\n配置unclean.leader.election.enable=true表示允许不同步的副本称为首领,这也将面临丢失消息的风险。\n\n\n# producer如何做可靠性保证\n\n如何保证可靠投递消息?\n\n 1. ack=all表示broker要等待所有分区副本同步完消息才返回给producer response,是最可靠的投递。\n 2. 设置重试次数,注意要做业务幂等。\n 3. 有些小时不可重试错误,比如消息太大、认证错误等。根据业务来特殊处理这类消息。\n\n\n# consumer如何做可靠性保证\n\nconsumer请求的offset在broker上不存在时怎么处理? 配置auto.offset.reset指定了两种策略:\n\n * earliest:从分区的开始读取\n * latest:从分区的末尾读取", + "normalizedContent": "# 可靠性保证\n\nkafka在哪些方面做出保证呢?\n\n * 单生产者,kafka可以保证顺序消费\n * 只有当写入的消息被写入所有的副本时,才认为是“已提交”的\n * 只要有一个活跃的分区副本,那么,已提交的数据就不会丢失。\n * 消费者只能读取已经提交的消息\n\n如何保证topic元数据信息安全 replication.factor配置topic的复制系数\n\n所有的分区副本都没有正常同步,分区首领又挂掉了,如何做选举? 方案有两个\n\n * 在未正常同步的副中选举一个作为首领,缺点是存在丢失数据的风险。\n * 等待旧的首领恢复。缺点是可用性低。\n\n配置unclean.leader.election.enable=true表示允许不同步的副本称为首领,这也将面临丢失消息的风险。\n\n\n# producer如何做可靠性保证\n\n如何保证可靠投递消息?\n\n 1. ack=all表示broker要等待所有分区副本同步完消息才返回给producer response,是最可靠的投递。\n 2. 设置重试次数,注意要做业务幂等。\n 3. 有些小时不可重试错误,比如消息太大、认证错误等。根据业务来特殊处理这类消息。\n\n\n# consumer如何做可靠性保证\n\nconsumer请求的offset在broker上不存在时怎么处理? 配置auto.offset.reset指定了两种策略:\n\n * earliest:从分区的开始读取\n * latest:从分区的末尾读取", "charsets": { "cjk": true }, @@ -6176,34 +6550,99 @@ export const siteData = { }, { "level": 2, - "title": "目前为止,坚持得最久一件事情是什么?", - "slug": "目前为止-坚持得最久一件事情是什么", - "normalizedTitle": "目前为止,坚持得最久一件事情是什么?", - "charIndex": 200 + "title": "目前为止,坚持得最久一件事情是什么?", + "slug": "目前为止-坚持得最久一件事情是什么", + "normalizedTitle": "目前为止,坚持得最久一件事情是什么?", + "charIndex": 200 + }, + { + "level": 2, + "title": "能接受多大强度的加班?", + "slug": "能接受多大强度的加班", + "normalizedTitle": "能接受多大强度的加班?", + "charIndex": 223 + }, + { + "level": 2, + "title": "最近在看什么书吗,有没有接触过什么新技术?", + "slug": "最近在看什么书吗-有没有接触过什么新技术", + "normalizedTitle": "最近在看什么书吗,有没有接触过什么新技术?", + "charIndex": 239 + } + ], + "headersStr": "最有技术难度的项目是什么?遇到技术难点怎么解决? 为什么要离开现在的公司? 未来的职业规划是什么? 下一份工作希望学习到什么? 团队中对技术选型有冲突的话怎么解决? 你的性格和技能上有什么缺点 简单描述一下自己是怎么样的人? 学习中遇到的最大的困难是什么? 最有成就感的项目是什么? 团队合作沟通中遇到过什么问题? 目前为止,坚持得最久一件事情是什么? 能接受多大强度的加班? 最近在看什么书吗,有没有接触过什么新技术?", + "content": "# 最有技术难度的项目是什么?遇到技术难点怎么解决?\n\n\n# 为什么要离开现在的公司?\n\n\n# 未来的职业规划是什么?\n\n\n# 下一份工作希望学习到什么?\n\n\n# 团队中对技术选型有冲突的话怎么解决?\n\n\n# 你的性格和技能上有什么缺点\n\n\n# 简单描述一下自己是怎么样的人?\n\n\n# 学习中遇到的最大的困难是什么?\n\n\n# 最有成就感的项目是什么?\n\n\n# 团队合作沟通中遇到过什么问题?\n\n\n# 目前为止,坚持得最久一件事情是什么?\n\n\n# 能接受多大强度的加班?\n\n\n# 最近在看什么书吗,有没有接触过什么新技术?", + "normalizedContent": "# 最有技术难度的项目是什么?遇到技术难点怎么解决?\n\n\n# 为什么要离开现在的公司?\n\n\n# 未来的职业规划是什么?\n\n\n# 下一份工作希望学习到什么?\n\n\n# 团队中对技术选型有冲突的话怎么解决?\n\n\n# 你的性格和技能上有什么缺点\n\n\n# 简单描述一下自己是怎么样的人?\n\n\n# 学习中遇到的最大的困难是什么?\n\n\n# 最有成就感的项目是什么?\n\n\n# 团队合作沟通中遇到过什么问题?\n\n\n# 目前为止,坚持得最久一件事情是什么?\n\n\n# 能接受多大强度的加班?\n\n\n# 最近在看什么书吗,有没有接触过什么新技术?", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, + { + "title": "常用的前端轮子", + "frontmatter": { + "title": "常用的前端轮子", + "date": "2021-10-09T19:42:02.000Z", + "permalink": "/pages/47cf96/", + "article": false + }, + "regularPath": "/@pages/20.%E6%94%B6%E8%97%8F%E5%A4%B9/02.%E5%B8%B8%E7%94%A8%E7%9A%84%E5%89%8D%E7%AB%AF%E8%BD%AE%E5%AD%90.html", + "relativePath": "@pages/20.收藏夹/02.常用的前端轮子.md", + "key": "v-09c150e6", + "path": "/pages/47cf96/", + "headers": [ + { + "level": 2, + "title": "React UI 组件库", + "slug": "react-ui-组件库", + "normalizedTitle": "react ui 组件库", + "charIndex": 2 + }, + { + "level": 2, + "title": "Vue UI组件库", + "slug": "vue-ui组件库", + "normalizedTitle": "vue ui组件库", + "charIndex": 68 + }, + { + "level": 2, + "title": "常用效果组件", + "slug": "常用效果组件", + "normalizedTitle": "常用效果组件", + "charIndex": 124 + }, + { + "level": 2, + "title": "工具类", + "slug": "工具类", + "normalizedTitle": "工具类", + "charIndex": 201 }, { "level": 2, - "title": "能接受多大强度的加班?", - "slug": "能接受多大强度的加班", - "normalizedTitle": "能接受多大强度的加班?", - "charIndex": 223 + "title": "Vue工具类", + "slug": "vue工具类", + "normalizedTitle": "vue工具类", + "charIndex": 331 }, { "level": 2, - "title": "最近在看什么书吗,有没有接触过什么新技术?", - "slug": "最近在看什么书吗-有没有接触过什么新技术", - "normalizedTitle": "最近在看什么书吗,有没有接触过什么新技术?", - "charIndex": 239 + "title": "其他", + "slug": "其他", + "normalizedTitle": "其他", + "charIndex": 469 } ], - "headersStr": "最有技术难度的项目是什么?遇到技术难点怎么解决? 为什么要离开现在的公司? 未来的职业规划是什么? 下一份工作希望学习到什么? 团队中对技术选型有冲突的话怎么解决? 你的性格和技能上有什么缺点 简单描述一下自己是怎么样的人? 学习中遇到的最大的困难是什么? 最有成就感的项目是什么? 团队合作沟通中遇到过什么问题? 目前为止,坚持得最久一件事情是什么? 能接受多大强度的加班? 最近在看什么书吗,有没有接触过什么新技术?", - "content": "# 最有技术难度的项目是什么?遇到技术难点怎么解决?\n\n\n# 为什么要离开现在的公司?\n\n\n# 未来的职业规划是什么?\n\n\n# 下一份工作希望学习到什么?\n\n\n# 团队中对技术选型有冲突的话怎么解决?\n\n\n# 你的性格和技能上有什么缺点\n\n\n# 简单描述一下自己是怎么样的人?\n\n\n# 学习中遇到的最大的困难是什么?\n\n\n# 最有成就感的项目是什么?\n\n\n# 团队合作沟通中遇到过什么问题?\n\n\n# 目前为止,坚持得最久一件事情是什么?\n\n\n# 能接受多大强度的加班?\n\n\n# 最近在看什么书吗,有没有接触过什么新技术?", - "normalizedContent": "# 最有技术难度的项目是什么?遇到技术难点怎么解决?\n\n\n# 为什么要离开现在的公司?\n\n\n# 未来的职业规划是什么?\n\n\n# 下一份工作希望学习到什么?\n\n\n# 团队中对技术选型有冲突的话怎么解决?\n\n\n# 你的性格和技能上有什么缺点\n\n\n# 简单描述一下自己是怎么样的人?\n\n\n# 学习中遇到的最大的困难是什么?\n\n\n# 最有成就感的项目是什么?\n\n\n# 团队合作沟通中遇到过什么问题?\n\n\n# 目前为止,坚持得最久一件事情是什么?\n\n\n# 能接受多大强度的加班?\n\n\n# 最近在看什么书吗,有没有接触过什么新技术?", + "headersStr": "React UI 组件库 Vue UI组件库 常用效果组件 工具类 Vue工具类 其他", + "content": "# React UI 组件库\n\n * Ant Design\n * React Bootstrap\n * MATERIAL-UI\n\n\n# Vue UI组件库\n\n * Element UI PC端\n * Vant 移动端\n * View UI\n\n\n# 常用效果组件\n\n * Animate.css 动画库\n * Swiper 轮播组件\n * mescroll 下拉刷新和上拉加载框架-基于原生JS\n\n\n# 工具类\n\n * Lodash.js\n * Day.js 处理日期\n * Timeago.js 相对时间,如N小时前\n * Echarts 百度图表\n * Meditor.md 开源在线 Markdown 编辑器\n * validator.js 验证库\n\n\n# Vue工具类\n\n * vue-draggable 基于Sortable.js实现的vue拖拽插件\n * vue-qr 文本转二维码\n * vue-cropper 图片裁剪插件\n * vue-lazyload 懒加载\n * vue-simple-upload 上传组件\n\n\n# 其他\n\n * H5带笔锋手写签名,支持PC端和移动端", + "normalizedContent": "# react ui 组件库\n\n * ant design\n * react bootstrap\n * material-ui\n\n\n# vue ui组件库\n\n * element ui pc端\n * vant 移动端\n * view ui\n\n\n# 常用效果组件\n\n * animate.css 动画库\n * swiper 轮播组件\n * mescroll 下拉刷新和上拉加载框架-基于原生js\n\n\n# 工具类\n\n * lodash.js\n * day.js 处理日期\n * timeago.js 相对时间,如n小时前\n * echarts 百度图表\n * meditor.md 开源在线 markdown 编辑器\n * validator.js 验证库\n\n\n# vue工具类\n\n * vue-draggable 基于sortable.js实现的vue拖拽插件\n * vue-qr 文本转二维码\n * vue-cropper 图片裁剪插件\n * vue-lazyload 懒加载\n * vue-simple-upload 上传组件\n\n\n# 其他\n\n * h5带笔锋手写签名,支持pc端和移动端", "charsets": { "cjk": true }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 + "lastUpdated": "2022/06/20, 22:47:41", + "lastUpdatedTimestamp": 1655736461000 }, { "title": "归档", @@ -6493,6 +6932,40 @@ export const siteData = { "lastUpdated": "2022/06/20, 22:47:41", "lastUpdatedTimestamp": 1655736461000 }, + { + "title": "你知道的越多,不知道的也就越多", + "frontmatter": { + "title": "你知道的越多,不知道的也就越多", + "date": "2020-05-06T15:52:40.000Z", + "permalink": "/pages/f2e63f", + "sidebar": "auto", + "categories": [ + "随笔" + ], + "tags": [ + "学习", + "知识", + "鸡汤" + ], + "author": { + "name": "xugaoyi", + "link": "https://github.com/xugaoyi" + } + }, + "regularPath": "/_posts/%E9%9A%8F%E7%AC%94/%E4%BD%A0%E7%9F%A5%E9%81%93%E7%9A%84%E8%B6%8A%E5%A4%9A%EF%BC%8C%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84%E4%B9%9F%E5%B0%B1%E8%B6%8A%E5%A4%9A.html", + "relativePath": "_posts/随笔/你知道的越多,不知道的也就越多.md", + "key": "v-2db11cb2", + "path": "/pages/f2e63f/", + "excerpt": "

不知道大家有没有发现,我们身边经常有这样的人,他们越是有能力的,越是有知识的,越是低调,越是谦逊,因为他们深知,知道的越多,不知道的也就越多。

\n", + "headersStr": null, + "content": "不知道大家有没有发现,我们身边经常有这样的人,他们越是有能力的,越是有知识的,越是低调,越是谦逊,因为他们深知,知道的越多,不知道的也就越多。\n\n你知道的越多,你不知道的也就越多,这是一句非常有哲理的话。\n\n每个人的知识面都是有限的,你有可能在某个领域会有较深的研究,成为这个领域里的专家,等到你站在高处的时候,才会发现,自己是多么的渺小,才知道自己有多少没有涉及的领域。知道的越多,疑惑、问题就会越来越多,对已知的质疑、疑虑、困惑就会越来越多。\n\n即使如此,我们也应该努力,至少可以成为某个领域的佼佼者。\n\n鸡汤1\n\n弱小的人,才习惯嘲讽与否定,而内心强大的人,从不吝啬赞美与鼓励。\n\n鸡汤2\n\n当代青年人都应该摆脱冷气,只管向上走,不必听从自暴自弃者的流言。能做事的做事,能发声的发声。有一份热,发一份光,就像萤火一般,也可以在黑暗里发一点光,不必等候炬火。", + "normalizedContent": "不知道大家有没有发现,我们身边经常有这样的人,他们越是有能力的,越是有知识的,越是低调,越是谦逊,因为他们深知,知道的越多,不知道的也就越多。\n\n你知道的越多,你不知道的也就越多,这是一句非常有哲理的话。\n\n每个人的知识面都是有限的,你有可能在某个领域会有较深的研究,成为这个领域里的专家,等到你站在高处的时候,才会发现,自己是多么的渺小,才知道自己有多少没有涉及的领域。知道的越多,疑惑、问题就会越来越多,对已知的质疑、疑虑、困惑就会越来越多。\n\n即使如此,我们也应该努力,至少可以成为某个领域的佼佼者。\n\n鸡汤1\n\n弱小的人,才习惯嘲讽与否定,而内心强大的人,从不吝啬赞美与鼓励。\n\n鸡汤2\n\n当代青年人都应该摆脱冷气,只管向上走,不必听从自暴自弃者的流言。能做事的做事,能发声的发声。有一份热,发一份光,就像萤火一般,也可以在黑暗里发一点光,不必等候炬火。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, { "title": "我做了一个手写春联小网页,祝大家虎年暴富", "frontmatter": { @@ -6552,40 +7025,6 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, - { - "title": "你知道的越多,不知道的也就越多", - "frontmatter": { - "title": "你知道的越多,不知道的也就越多", - "date": "2020-05-06T15:52:40.000Z", - "permalink": "/pages/f2e63f", - "sidebar": "auto", - "categories": [ - "随笔" - ], - "tags": [ - "学习", - "知识", - "鸡汤" - ], - "author": { - "name": "xugaoyi", - "link": "https://github.com/xugaoyi" - } - }, - "regularPath": "/_posts/%E9%9A%8F%E7%AC%94/%E4%BD%A0%E7%9F%A5%E9%81%93%E7%9A%84%E8%B6%8A%E5%A4%9A%EF%BC%8C%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84%E4%B9%9F%E5%B0%B1%E8%B6%8A%E5%A4%9A.html", - "relativePath": "_posts/随笔/你知道的越多,不知道的也就越多.md", - "key": "v-2db11cb2", - "path": "/pages/f2e63f/", - "excerpt": "

不知道大家有没有发现,我们身边经常有这样的人,他们越是有能力的,越是有知识的,越是低调,越是谦逊,因为他们深知,知道的越多,不知道的也就越多。

\n", - "headersStr": null, - "content": "不知道大家有没有发现,我们身边经常有这样的人,他们越是有能力的,越是有知识的,越是低调,越是谦逊,因为他们深知,知道的越多,不知道的也就越多。\n\n你知道的越多,你不知道的也就越多,这是一句非常有哲理的话。\n\n每个人的知识面都是有限的,你有可能在某个领域会有较深的研究,成为这个领域里的专家,等到你站在高处的时候,才会发现,自己是多么的渺小,才知道自己有多少没有涉及的领域。知道的越多,疑惑、问题就会越来越多,对已知的质疑、疑虑、困惑就会越来越多。\n\n即使如此,我们也应该努力,至少可以成为某个领域的佼佼者。\n\n鸡汤1\n\n弱小的人,才习惯嘲讽与否定,而内心强大的人,从不吝啬赞美与鼓励。\n\n鸡汤2\n\n当代青年人都应该摆脱冷气,只管向上走,不必听从自暴自弃者的流言。能做事的做事,能发声的发声。有一份热,发一份光,就像萤火一般,也可以在黑暗里发一点光,不必等候炬火。", - "normalizedContent": "不知道大家有没有发现,我们身边经常有这样的人,他们越是有能力的,越是有知识的,越是低调,越是谦逊,因为他们深知,知道的越多,不知道的也就越多。\n\n你知道的越多,你不知道的也就越多,这是一句非常有哲理的话。\n\n每个人的知识面都是有限的,你有可能在某个领域会有较深的研究,成为这个领域里的专家,等到你站在高处的时候,才会发现,自己是多么的渺小,才知道自己有多少没有涉及的领域。知道的越多,疑惑、问题就会越来越多,对已知的质疑、疑虑、困惑就会越来越多。\n\n即使如此,我们也应该努力,至少可以成为某个领域的佼佼者。\n\n鸡汤1\n\n弱小的人,才习惯嘲讽与否定,而内心强大的人,从不吝啬赞美与鼓励。\n\n鸡汤2\n\n当代青年人都应该摆脱冷气,只管向上走,不必听从自暴自弃者的流言。能做事的做事,能发声的发声。有一份热,发一份光,就像萤火一般,也可以在黑暗里发一点光,不必等候炬火。", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, { "title": "拥抱生活,拥抱快乐", "frontmatter": { @@ -6687,71 +7126,6 @@ export const siteData = { "lastUpdated": "2022/06/20, 22:47:41", "lastUpdatedTimestamp": 1655736461000 }, - { - "title": "常用的前端轮子", - "frontmatter": { - "title": "常用的前端轮子", - "date": "2021-10-09T19:42:02.000Z", - "permalink": "/pages/47cf96/", - "article": false - }, - "regularPath": "/@pages/20.%E6%94%B6%E8%97%8F%E5%A4%B9/02.%E5%B8%B8%E7%94%A8%E7%9A%84%E5%89%8D%E7%AB%AF%E8%BD%AE%E5%AD%90.html", - "relativePath": "@pages/20.收藏夹/02.常用的前端轮子.md", - "key": "v-09c150e6", - "path": "/pages/47cf96/", - "headers": [ - { - "level": 2, - "title": "React UI 组件库", - "slug": "react-ui-组件库", - "normalizedTitle": "react ui 组件库", - "charIndex": 2 - }, - { - "level": 2, - "title": "Vue UI组件库", - "slug": "vue-ui组件库", - "normalizedTitle": "vue ui组件库", - "charIndex": 68 - }, - { - "level": 2, - "title": "常用效果组件", - "slug": "常用效果组件", - "normalizedTitle": "常用效果组件", - "charIndex": 124 - }, - { - "level": 2, - "title": "工具类", - "slug": "工具类", - "normalizedTitle": "工具类", - "charIndex": 201 - }, - { - "level": 2, - "title": "Vue工具类", - "slug": "vue工具类", - "normalizedTitle": "vue工具类", - "charIndex": 331 - }, - { - "level": 2, - "title": "其他", - "slug": "其他", - "normalizedTitle": "其他", - "charIndex": 469 - } - ], - "headersStr": "React UI 组件库 Vue UI组件库 常用效果组件 工具类 Vue工具类 其他", - "content": "# React UI 组件库\n\n * Ant Design\n * React Bootstrap\n * MATERIAL-UI\n\n\n# Vue UI组件库\n\n * Element UI PC端\n * Vant 移动端\n * View UI\n\n\n# 常用效果组件\n\n * Animate.css 动画库\n * Swiper 轮播组件\n * mescroll 下拉刷新和上拉加载框架-基于原生JS\n\n\n# 工具类\n\n * Lodash.js\n * Day.js 处理日期\n * Timeago.js 相对时间,如N小时前\n * Echarts 百度图表\n * Meditor.md 开源在线 Markdown 编辑器\n * validator.js 验证库\n\n\n# Vue工具类\n\n * vue-draggable 基于Sortable.js实现的vue拖拽插件\n * vue-qr 文本转二维码\n * vue-cropper 图片裁剪插件\n * vue-lazyload 懒加载\n * vue-simple-upload 上传组件\n\n\n# 其他\n\n * H5带笔锋手写签名,支持PC端和移动端", - "normalizedContent": "# react ui 组件库\n\n * ant design\n * react bootstrap\n * material-ui\n\n\n# vue ui组件库\n\n * element ui pc端\n * vant 移动端\n * view ui\n\n\n# 常用效果组件\n\n * animate.css 动画库\n * swiper 轮播组件\n * mescroll 下拉刷新和上拉加载框架-基于原生js\n\n\n# 工具类\n\n * lodash.js\n * day.js 处理日期\n * timeago.js 相对时间,如n小时前\n * echarts 百度图表\n * meditor.md 开源在线 markdown 编辑器\n * validator.js 验证库\n\n\n# vue工具类\n\n * vue-draggable 基于sortable.js实现的vue拖拽插件\n * vue-qr 文本转二维码\n * vue-cropper 图片裁剪插件\n * vue-lazyload 懒加载\n * vue-simple-upload 上传组件\n\n\n# 其他\n\n * h5带笔锋手写签名,支持pc端和移动端", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/06/20, 22:47:41", - "lastUpdatedTimestamp": 1655736461000 - }, { "title": "标签", "frontmatter": { @@ -6789,6 +7163,91 @@ export const siteData = { "charsets": {}, "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 + }, + { + "title": "从输入 URL 到展现页面的全过程", + "frontmatter": { + "title": "从输入 URL 到展现页面的全过程", + "date": "2022-06-23T21:10:44.000Z", + "permalink": "/pages/ab48ba/", + "categories": [ + "计算机基础", + "计算机网络" + ], + "tags": [ + null + ] + }, + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/020.%E4%BB%8E%E8%BE%93%E5%85%A5%20URL%20%E5%88%B0%E5%B1%95%E7%8E%B0%E9%A1%B5%E9%9D%A2%E7%9A%84%E5%85%A8%E8%BF%87%E7%A8%8B.html", + "relativePath": "02.计算机基础/10.计算机网络/020.从输入 URL 到展现页面的全过程.md", + "key": "v-6602e3da", + "path": "/pages/ab48ba/", + "headers": [ + { + "level": 2, + "title": "什么是IP地址", + "slug": "什么是ip地址", + "normalizedTitle": "什么是ip地址", + "charIndex": 459 + }, + { + "level": 2, + "title": "什么是DNS", + "slug": "什么是dns", + "normalizedTitle": "什么是dns", + "charIndex": 608 + }, + { + "level": 2, + "title": "DNS解析", + "slug": "dns解析-2", + "normalizedTitle": "dns解析", + "charIndex": 57 + }, + { + "level": 2, + "title": "DNS查询的两种方式:递归查询和迭代查询", + "slug": "dns查询的两种方式-递归查询和迭代查询", + "normalizedTitle": "dns查询的两种方式:递归查询和迭代查询", + "charIndex": 752 + }, + { + "level": 2, + "title": "DNS域名称空间的组织方式", + "slug": "dns域名称空间的组织方式", + "normalizedTitle": "dns域名称空间的组织方式", + "charIndex": 788 + }, + { + "level": 2, + "title": "DNS负载均衡", + "slug": "dns负载均衡", + "normalizedTitle": "dns负载均衡", + "charIndex": 808 + }, + { + "level": 2, + "title": "”三次握手”的详解", + "slug": "三次握手-的详解", + "normalizedTitle": "”三次握手”的详解", + "charIndex": 1033 + }, + { + "level": 2, + "title": "为什么要三次握手", + "slug": "为什么要三次握手", + "normalizedTitle": "为什么要三次握手", + "charIndex": 1049 + } + ], + "headersStr": "什么是IP地址 什么是DNS DNS解析 DNS查询的两种方式:递归查询和迭代查询 DNS域名称空间的组织方式 DNS负载均衡 ”三次握手”的详解 为什么要三次握手", + "content": "从我们输入一个网址到页面展现的过程到底发生了什么呢?\n\n\n# 总体来说分为以下几个过程:\n\n * 输入网址\n * DNS解析\n * 建立TCP/IP链接\n * 发送HTTP请求\n * 服务器处理请求\n * 服务器返回HTTP响应\n * 浏览器渲染页面并展现\n * 断开连接\n\n\n# 输入网址\n\n当我们想要打开一个页面时,很简单的会想到去输入一个网址 我们以本网址为例,解释一下它各部分的组成:\n\nhttps://www.baidu.com\n\n\n1\n\n\n * https :https是协议(Protocol),它表明了浏览器使用何种协议,有http、https、ftp、mailto等,其中https是加密认证版的http(详见《图解HTTP》)\n\n * www.baidu.com :这是域名(Domain name),它表明正在请求哪个Web服务器,其中www是主机名(host)\n\n\n# DNS解析\n\n输入网址(URL)这一步我们很好理解,但是接下来所谓的域名解析是什么呢?又是为什么要进行域名解析呢?\n\n\n# 什么是IP地址\n\nIP 地址(Internet Protocol Address)是互联网协议特有的一种地址,它是 IP 协议提供的一种统一的地址格式。IP 地址为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。 像192.168.59.0就是一个主机IP地址\n\n\n# 什么是DNS\n\nDNS服务器上存储着映射<域名-IP>,DNS解析就是去DNS服务器查询IP地址。\n\n\n# DNS解析\n\nDNS解析按照下面顺序逐个查找。\n\n * 本地(hosts文件)\n * DNS服务器(本地DNS | 根DNS)\n * 域服务器\n * 解析服务器的地址\n\n\n\n\n# DNS查询的两种方式:递归查询和迭代查询\n\n递归解析 迭代解析\n\n\n# DNS域名称空间的组织方式\n\n\n\n\n# DNS负载均衡\n\n当一个网站有足够多的用户的时候,假如每次请求的资源都位于同一台机器上面,那么这台机器随时可能会蹦掉。\n\n处理办法就是用DNS负载均衡技术,它的原理是在DNS服务器中为同一个主机名配置多个IP地址 在应答DNS查询时,DNS服务器对每个查询将以DNS文件中主机记录的IP地址按顺序返回不同的解析结果,将客户端的访问引导到不同的机器上去,使得不同的客户端访问不同的服务器,从而达到负载均衡的目的。\n\n\n# 建立TCP/IP链接\n\n\n# ”三次握手”的详解\n\n\n\n\n# 为什么要三次握手\n\n * clien端 防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。 client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致连接释放以后的某个时间才到达server。 本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。 于是就向client发出确认报文段,同意建立连接。\n\n * server端 假设不采用“三次握手”。 如果只要server发出确认,新的连接就建立了。并一直等待client发来数据。 事实是client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。 但server端就会有许多这样的等待,server的很多资源就白白浪费掉了。\n\n采用“三次握手”的办法可以防止上述现象发生。主要目的防止server端一直等待,浪费资源。", + "normalizedContent": "从我们输入一个网址到页面展现的过程到底发生了什么呢?\n\n\n# 总体来说分为以下几个过程:\n\n * 输入网址\n * dns解析\n * 建立tcp/ip链接\n * 发送http请求\n * 服务器处理请求\n * 服务器返回http响应\n * 浏览器渲染页面并展现\n * 断开连接\n\n\n# 输入网址\n\n当我们想要打开一个页面时,很简单的会想到去输入一个网址 我们以本网址为例,解释一下它各部分的组成:\n\nhttps://www.baidu.com\n\n\n1\n\n\n * https :https是协议(protocol),它表明了浏览器使用何种协议,有http、https、ftp、mailto等,其中https是加密认证版的http(详见《图解http》)\n\n * www.baidu.com :这是域名(domain name),它表明正在请求哪个web服务器,其中www是主机名(host)\n\n\n# dns解析\n\n输入网址(url)这一步我们很好理解,但是接下来所谓的域名解析是什么呢?又是为什么要进行域名解析呢?\n\n\n# 什么是ip地址\n\nip 地址(internet protocol address)是互联网协议特有的一种地址,它是 ip 协议提供的一种统一的地址格式。ip 地址为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。 像192.168.59.0就是一个主机ip地址\n\n\n# 什么是dns\n\ndns服务器上存储着映射<域名-ip>,dns解析就是去dns服务器查询ip地址。\n\n\n# dns解析\n\ndns解析按照下面顺序逐个查找。\n\n * 本地(hosts文件)\n * dns服务器(本地dns | 根dns)\n * 域服务器\n * 解析服务器的地址\n\n\n\n\n# dns查询的两种方式:递归查询和迭代查询\n\n递归解析 迭代解析\n\n\n# dns域名称空间的组织方式\n\n\n\n\n# dns负载均衡\n\n当一个网站有足够多的用户的时候,假如每次请求的资源都位于同一台机器上面,那么这台机器随时可能会蹦掉。\n\n处理办法就是用dns负载均衡技术,它的原理是在dns服务器中为同一个主机名配置多个ip地址 在应答dns查询时,dns服务器对每个查询将以dns文件中主机记录的ip地址按顺序返回不同的解析结果,将客户端的访问引导到不同的机器上去,使得不同的客户端访问不同的服务器,从而达到负载均衡的目的。\n\n\n# 建立tcp/ip链接\n\n\n# ”三次握手”的详解\n\n\n\n\n# 为什么要三次握手\n\n * clien端 防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。 client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致连接释放以后的某个时间才到达server。 本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。 于是就向client发出确认报文段,同意建立连接。\n\n * server端 假设不采用“三次握手”。 如果只要server发出确认,新的连接就建立了。并一直等待client发来数据。 事实是client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。 但server端就会有许多这样的等待,server的很多资源就白白浪费掉了。\n\n采用“三次握手”的办法可以防止上述现象发生。主要目的防止server端一直等待,浪费资源。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/18, 21:28:02", + "lastUpdatedTimestamp": 1663507682000 } ], "themeConfig": { @@ -7181,8 +7640,8 @@ export const siteData = { "/pages/b37ad7/" ], [ - "40.设计模式/003.什么是设计模式,描述几个常用的设计模式 .md", - "什么是设计模式,描述几个常用的设计模式", + "40.设计模式/003.什么是设计模式 .md", + "什么是设计模式", "/pages/3d768c/" ], [ @@ -7192,7 +7651,7 @@ export const siteData = { ], [ "40.设计模式/005.装饰者模式.md", - "简述装饰者模式以及适配器模式", + "简述装饰者模式", "/pages/47a4e3/" ], [ From 0873e96883225c8df5b5377d5af52bc2a34fea06 Mon Sep 17 00:00:00 2001 From: admirean <854818303@qq.com> Date: Tue, 20 Sep 2022 23:34:59 +0800 Subject: [PATCH 13/15] =?UTF-8?q?=E5=AE=8C=E5=96=84=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E7=AB=A0=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...43\347\220\206\346\250\241\345\274\217.md" | 361 ++ ...73\351\223\276\346\250\241\345\274\217.md" | 274 + ...71\346\263\225\346\250\241\345\274\217.md" | 310 + ...26\347\225\245\346\250\241\345\274\217.md" | 200 + ...02\345\257\237\346\250\241\345\274\217.md" | 203 + ...43\347\220\206\346\250\241\345\274\217.md" | 10 - ...40\350\200\205\346\250\241\345\274\217.md" | 10 - .../core/.temp/internal/app-enhancers.js | 2 +- .../core/.temp/internal/page-components.js | 108 +- .../@vuepress/core/.temp/internal/routes.js | 1122 ++-- .../@vuepress/core/.temp/internal/siteData.js | 5506 +++++++++-------- 11 files changed, 4863 insertions(+), 3243 deletions(-) delete mode 100644 "docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/070.\344\273\243\347\220\206\346\250\241\345\274\217.md" delete mode 100644 "docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/080.\345\273\272\351\200\240\350\200\205\346\250\241\345\274\217.md" diff --git "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/020.\344\273\243\347\220\206\346\250\241\345\274\217.md" "b/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/020.\344\273\243\347\220\206\346\250\241\345\274\217.md" index e7eed88f0..9e8e1b4ce 100644 --- "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/020.\344\273\243\347\220\206\346\250\241\345\274\217.md" +++ "b/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/020.\344\273\243\347\220\206\346\250\241\345\274\217.md" @@ -8,3 +8,364 @@ categories: tags: - --- +## 基本介绍 +代理模式是一种结构型设计模式。为对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象,并允许在将请求提交给对象前后进行一些处理。 + +被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象。 + +代理模式主要有三种不同的形式: + +* 静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的 .class 文件就已经存在了 +* 动态代理(JDK 代理、接口代理):在程序运行时运用反射机制动态创建而成,动态就是在程序运行时生成的,而不是编译时。 +* cglib 代理(可以在内存动态的创建对象,而不是实现接口,属于动态代理的范畴) +## 问题 +为什么要控制对于某个对象的访问呢? 举个例子: 有这样一个消耗大量系统资源的巨型对象, 你只是偶尔需要使用它, 并非总是需要。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6dg4cy830j20e604g3ym.jpg) + +你可以实现延迟初始化: 在实际有需要时再创建该对象。 对象的所有客户端都要执行延迟初始代码。 不幸的是, 这很可能会带来很多重复代码。 + +在理想情况下, 我们希望将代码直接放入对象的类中, 但这并非总是能实现: 比如类可能是第三方封闭库的一部分。 + +## 解决方案 +代理模式建议新建一个与原服务对象接口相同的代理类, 然后更新应用以将代理对象传递给所有原始对象客户端。 代理类接收到客户端请求后会创建实际的服务对象, 并将所有工作委派给它。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6dg4jkyzzj20e604gwek.jpg) + +代理将自己伪装成数据库对象, 可在客户端或实际数据库对象不知情的情况下处理延迟初始化和缓存查询结果的工作。 + +这有什么好处呢? 如果需要在类的主要业务逻辑前后执行一些工作, 你无需修改类就能完成这项工作。 由于代理实现的接口与原类相同, 因此你可将其传递给任何一个使用实际服务对象的客户端。 + +#代理模式结构 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6dg4sm9tvj20aa0aaq37.jpg) + +1.服务接口 (Service Interface) 声明了服务接口。 代理必须遵循该接口才能伪装成服务对象。 +2.服务 (Service) 类提供了一些实用的业务逻辑。 +3.代理 (Proxy) 类包含一个指向服务对象的引用成员变量。 代理完成其任务 (例如延迟初始化、 记录日志、 访问控制和缓存等) 后会将请求传递给服务对象。 通常情况下, 代理会对其服务对象的整个生命周期进行管理。 +4.客户端 (Client) 能通过同一接口与服务或代理进行交互, 所以你可在一切需要服务对象的代码中使用代理。 +打游戏有代练、买卖房子有中介代理、再比如一般公司投互联网广告也可以找代理公司,这里的代练、中介、广告代理公司扮演的角色都是代理。 + +这里举个更接近程序员的例子,比如有些变态的公司不允许在公司刷微博,看视频,可以通过一层代理来限制我们访问这些网站。 + +废话不多说,先来个静态代理。 + +## 静态代理 +1、定义网络接口 +```java +public interface Internet { + void connectTo(String serverHost) throws Exception; +} +``` +2、真正的网络连接 +```java +public class RealInternet implements Internet{ + + @Override + public void connectTo(String serverHost) throws Exception { + System.out.println("Connecting to "+ serverHost); + } +} +``` +3、公司的网络代理 +```java +public class ProxyInternet implements Internet { + + //目标对象,通过接口聚合 + private Internet internet; + + // 通过构造方法传入目标对象 + public ProxyInternet(Internet internet){ + this.internet = internet; + } + //网络黑名单 + private static List bannedSites; + + static + { + bannedSites = new ArrayList(); + bannedSites.add("bilibili.com"); + bannedSites.add("youtube.com"); + bannedSites.add("weibo.com"); + bannedSites.add("qq.com"); + } + + @Override + public void connectTo(String serverhost) throws Exception { + // 添加限制功能 + if(bannedSites.contains(serverhost.toLowerCase())) + { + throw new Exception("Access Denied:"+serverhost); + } + internet.connectTo(serverhost); + } +} +``` +4、客户端验证 +```java +public class Client { + + public static void main(String[] args) { + Internet internet = new ProxyInternet(new RealInternet()); + try { + internet.connectTo("so.com"); + internet.connectTo("qq.com"); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } +} +``` +5、输出 + +```java +Connecting to so.com +Access Denied:qq.com +``` +不能访问娱乐性网站,但是可以用 360 搜索,SO 靠谱,哈哈 + +## 静态代理类优缺点 +### 优点: +在不修改目标对象的前提下,可以通过代理对象对目标对象功能扩展 + +代理使客户端不需要知道实现类是什么,怎么做的,而客户端只需知道代理即可(解耦合),对于如上的客户端代码,`RealInterner()` 可以应用工厂将它隐藏。 + +### 缺点: +代理类和委托类实现了相同的接口,代理类通过委托类实现了相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。 + +代理对象只服务于一种类型的对象,如果要服务多类型的对象。势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了。 + +## 动态代理 +静态代理会产生很多静态类,所以我们要想办法可以通过一个代理类完成全部的代理功能,这就引出了动态代理。 + +### JDK 原生动态代理 +* 代理对象,不需要实现接口,但是目标对象要实现接口,否则不能用动态代理 +* 代理对象的生成,是通过 JDK 的 API(反射机制),动态的在内存中构建代理对象 +在 Java 中要想实现动态代理机制,需要 `java.lang.reflect.InvocationHandler` 接口和 `java.lang.reflect.Proxy` 类的支持 + +### Coding +1、网络接口不变 + +public interface Internet { + void connectTo(String serverHost) throws Exception; +} +2、真正的网络连接,也不会改变 + +public class RealInternet implements Internet{ + + @Override + public void connectTo(String serverHost) throws Exception { + System.out.println("Connecting to "+ serverHost); + } +} +3、动态代理,需要实现 InvocationHandler,我们用 Lambda 表达式简化下 + +public class ProxyFactory { + + /** + * 维护一个目标对象 + **/ + private Object target; + + /** + * 构造器,初始化目标对象 + **/ + public ProxyFactory(Object target) { + this.target = target; + } + + public Object getProxyInstance() { + + /** + 被代理对象target通过参数传递进来, + 通过target.getClass().getClassLoader()获取ClassLoader对象, + 然后通过target.getClass().getInterfaces()获取它实现的所有接口, + 再将target包装到实现了InvocationHandler接口的对象中。 + 通过newProxyInstance函数我们就获得了一个动态代理对象。 + */ + return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if(bannedSites.contains(args[0].toString().toLowerCase())) + { + throw new Exception("Access Denied:"+args[0]); + } + //反射机制调用目标对象的方法 + Object obj = method.invoke(target, args); + return obj; + } + }); + } + + private static List bannedSites; + + static + { + bannedSites = new ArrayList(); + bannedSites.add("bilibili.com"); + bannedSites.add("youtube.com"); + bannedSites.add("weibo.com"); + bannedSites.add("qq.com"); + } +} +4、客户端 + +public class Client { + + public static void main(String[] args) { + Internet internet = new ProxyInternet(new RealInternet()); + try { + internet.connectTo("360.cn"); + internet.connectTo("qq.com"); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } +} +动态代理的方式中,所有的函数调用最终都会经过 `invoke` 函数的转发,因此我们就可以在这里做一些自己想做的操作,比如日志系统、事务、拦截器、权限控制等。 + +## cglib 代理 +静态代理和 JDK 代理模式都要求目标对象实现一个接口,但有时候目标对象只是一个单独的对象,并没有实现任何接口,这个时候就可以使用目标对象子类来实现代理,这就是 `cglib` 代理。 + +`cglib` (opens new window)(Code Generation Library)是一个基于 `ASM` 的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。`cglib` 通过继承方式实现代理。它广泛的被许多 AOP 的框架使用,比如我们的 Spring AOP。 + +`cglib` 包的底层是通过使用字节码处理框架 `ASM` 来转换字节码并生成新的类。 + +`cglib` 代理也被叫做子类代理,它是在内存中构建一个子类对象从而实现目标对象功能扩展。 + +### Coding +添加 cglib 依赖 + +```java + + cglib + cglib + 3.3.0 + +``` +1、不需要接口 +```java +public class RealInternet{ + + public void connectTo(String serverHost) { + System.out.println("Connecting to "+ serverHost); + } +} +``` +2、代理工厂类 +```java +public class ProxyFactory implements MethodInterceptor { + + private Object target; + + public ProxyFactory(Object target){ + this.target = target; + } + + @Override + public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { + System.out.println("cglib 代理开始,可以添加逻辑"); + Object obj = method.invoke(target,objects); + System.out.println("cglib 代理结束"); + return obj; + } + + + public Object getProxyInstance(){ + //工具类,类似于JDK动态代理的Proxy类 + Enhancer enhancer = new Enhancer(); + //设置父类 + enhancer.setSuperclass(target.getClass()); + //设置回调函数 + enhancer.setCallback(this); + //创建子类对象,即代理对象 + return enhancer.create(); + } +} +``` +3、客户端 +```java +public class Client { + + public static void main(String[] args) { + + //目标对象 + RealInternet target = new RealInternet(); + //获取代理对象,并且将目标对象传递给代理对象 + RealInternet internet = (RealInternet) new ProxyFactory(target).getProxyInstance(); + internet.connectTo("so.cn"); + } +} +``` +4、输出 + +```java +cglib 代理开始,可以添加逻辑 +Connecting to so.cn +cglib 代理结束 +``` +## 代理模式适合应用场景 +使用代理模式的方式多种多样, 我们来看看最常见的几种。 + +* 延迟初始化 (虚拟代理):如果你有一个偶尔使用的重量级服务对象, 一直保持该对象运行会消耗系统资源时, 可使用代理模式。 + +你无需在程序启动时就创建该对象, 可将对象的初始化延迟到真正有需要的时候。 + +* 访问控制 (保护代理):如果你只希望特定客户端使用服务对象, 这里的对象可以是操作系统中非常重要的部分, 而客户端则是各种已启动的程序 (包括恶意程序), 此时可使用代理模式。 + +代理可仅在客户端凭据满足要求时将请求传递给服务对象。 + +* 本地执行远程服务 (远程代理):适用于服务对象位于远程服务器上的情形。 + +在这种情形中, 代理通过网络传递客户端请求, 负责处理所有与网络相关的复杂细节。 + +* 记录日志请求 (日志记录代理):适用于当你需要保存对于服务对象的请求历史记录时。 代理可以在向服务传递请求前进行记录。 + +* 缓存请求结果 (缓存代理):适用于需要缓存客户请求结果并对缓存生命周期进行管理时, 特别是当返回结果的体积非常大时。 + +代理可对重复请求所需的相同结果进行缓存, 还可使用请求参数作为索引缓存的键值。比如请求图片、文件等资源时,先到代理缓存取,如果没有就去公网取并缓存到代理服务器 + +* 智能引用:可在没有客户端使用某个重量级对象时立即销毁该对象。 + +代理会将所有获取了指向服务对象或其结果的客户端记录在案。 代理会时不时地遍历各个客户端, 检查它们是否仍在运行。 如果相应的客户端列表为空, 代理就会销毁该服务对象, 释放底层系统资源。 + +代理还可以记录客户端是否修改了服务对象。 其他客户端还可以复用未修改的对象。 + +## AOP 中的代理模式 +`AOP`(面向切面编程)主要的的实现技术主要有 `Spring AOP` 和 `AspectJ` + +`AspectJ` 的底层技术就是静态代理,用一种 `AspectJ` 支持的特定语言编写切面,通过一个命令来编译,生成一个新的代理类,该代理类增强了业务类,这是在编译时增强,相对于下面说的运行时增强,编译时增强的性能更好。(AspectJ 的静态代理,不像我们前边介绍的需要为每一个目标类手动编写一个代理类,AspectJ 框架可以在编译时就生成目标类的“代理类”,在这里加了个冒号,是因为实际上它并没有生成一个新的类,而是把代理逻辑直接编译到目标类里面了) + +`Spring AOP` 采用的是动态代理,在运行期间对业务方法进行增强,所以不会生成新类,对于动态代理技术,`Spring AOP` 提供了对 `JDK` 动态代理的支持以及 `CGLib` 的支持。 + +默认情况下,`Spring` 对实现了接口的类使用 `JDK Proxy` 方式,否则的话使用 `CGLib`。不过可以通过配置指定 `Spring AOP` 都通过 `CGLib` 来生成代理类。 + + + +具体逻辑在 `org.springframework.aop.framework.DefaultAopProxyFactory` 类中,使用哪种方式生成由 `AopProxy` 根据 `AdvisedSupport` 对象的配置来决定源码如下: + +```java +public class DefaultAopProxyFactory implements AopProxyFactory, Serializable { + public DefaultAopProxyFactory() { + } + + public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { + if (!config.isOptimize() && !config.isProxyTargetClass() && !this.hasNoUserSuppliedProxyInterfaces(config)) { + return new JdkDynamicAopProxy(config); + } else { + Class targetClass = config.getTargetClass(); + if (targetClass == null) { + throw new AopConfigException("TargetSource cannot determine target class: Either an interface or a target is required for proxy creation."); + } else { + //如果目标类是接口且是代理类, 使用JDK动态代理类,否则使用Cglib生成代理类 + return (AopProxy)(!targetClass.isInterface() && !Proxy.isProxyClass(targetClass) ? new ObjenesisCglibAopProxy(config) : new JdkDynamicAopProxy(config)); + } + } + } + + private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) { + } +} +``` + +参考: +[代理模式](https://javakeeper.starfish.ink/design-pattern/Proxy-Pattern.html#%E5%9F%BA%E6%9C%AC%E4%BB%8B%E7%BB%8D) \ No newline at end of file diff --git "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/030.\350\264\243\344\273\273\351\223\276\346\250\241\345\274\217.md" "b/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/030.\350\264\243\344\273\273\351\223\276\346\250\241\345\274\217.md" index b0ec55f3e..37039009c 100644 --- "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/030.\350\264\243\344\273\273\351\223\276\346\250\241\345\274\217.md" +++ "b/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/030.\350\264\243\344\273\273\351\223\276\346\250\241\345\274\217.md" @@ -8,3 +8,277 @@ categories: tags: - --- +责任链,顾名思义,就是用来处理相关事务责任的一条执行链,执行链上有多个节点,每个节点都有机会(条件匹配)处理请求事务,如果某个节点处理完了就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。 + +这种模式给予请求的类型,对请求的发送者和接收者进行解耦。属于行为型模式。 + +在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6dhktjapzj20hs0b4wg5.jpg) + +先来看一段代码 + +```java +public void test(int i, Request request){ + if(i==1){ + Handler1.response(request); + }else if(i == 2){ + Handler2.response(request); + }else if(i == 3){ + Handler3.response(request); + }else if(i == 4){ + Handler4.response(request); + }else{ + Handler5.response(request); + } +} +``` +代码的业务逻辑是这样的,方法有两个参数:整数 i 和一个请求 request,根据 i 的值来决定由谁来处理 request,如果 i==1,由 Handler1来处理,如果 i==2,由 Handler2 来处理,以此类推。在编程中,这种处理业务的方法非常常见,所有处理请求的类由 if…else… 条件判断语句连成一条责任链来对请求进行处理,相信大家都经常用到。这种方法的优点是非常直观,简单明了,并且比较容易维护,但是这种方法也存在着几个比较令人头疼的问题: + +**代码臃肿**:实际应用中的判定条件通常不是这么简单地判断是否为1或者是否为2,也许需要复杂的计算,也许需要查询数据库等等,这就会有很多额外的代码,如果判断条件再比较多,那么这个if…else…语句基本上就没法看了。 + +**耦合度高**:如果我们想继续添加处理请求的类,那么就要继续添加if…else…判定条件;另外,这个条件判定的顺序也是写死的,如果想改变顺序,那么也只能修改这个条件语句。 +既然缺点我们已经清楚了,就要想办法来解决。这个场景的业务逻辑很简单:如果满足条件1,则由 Handler1 来处理,不满足则向下传递;如果满足条件2,则由 Handler2 来处理,不满足则继续向下传递,以此类推,直到条件结束。其实改进的方法也很简单,就是把判定条件的部分放到处理类中,这就是责任连模式的原理。 + +## 定义 +**责任链模式(Chain of Responsibility Pattern)**:使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。 + +## 角色 +**Handler**: 抽象处理类,抽象处理类中主要包含一个指向下一处理类的成员变量 nextHandler 和一个处理请求的方法 handRequest,handRequest 方法的主要主要思想是,如果满足处理的条件,则由本处理类来进行处理,否则由 nextHandler 来处理 + +**ConcreteHandler**: 具体处理类主要是对具体的处理逻辑和处理的适用条件进行实现。具体处理者接到请求后,可以选择将请求处理掉,或者将请求传给下家。由于具体处理者持有对下家的引用,因此,如果需要,具体处理者可以访问下家 + +**Client**:客户端 +## 类图 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6dhmp35f7j21eg0mkq3l.jpg) + +## coding +```java +public abstract class Handler { + private Handler nextHandler; + private int level; + + public Handler(int level) { + this.level = level; + } + + public void setNextHandler(Handler handler){ + this.nextHandler = handler; + } + + public final void handlerRequest(Request request){ + if(level == request.getLevel()){ + this.response(request); + }else{ + if (this.nextHandler != null){ + this.nextHandler.handlerRequest(request); + }else{ + System.out.println("===已经没有处理器了==="); + } + } + + } + // 抽象方法,子类实现 + public abstract void response(Request request); +} +``` +```java +class Request { + int level = 0; + public Request(int level){ + this.level = level; + } + public int getLevel() { + return level; + } +} +public class ConcreteHandler1 extends Handler { + public ConcreteHandler1(int level) { + super(level); + } + + @Override + public void response(Request request) { + System.out.println("请求由处理器1进行处理"); + } +} + +public class ConcreteHandler2 extends Handler { + //... +} + +public class ConcreteHandler2 extends Handler { + //... +} +public class Client { + public static void main(String[] args) { + ConcreteHandler1 handler1 = new ConcreteHandler1(1); + ConcreteHandler2 handler2 = new ConcreteHandler2(2); + ConcreteHandler3 handler3 = new ConcreteHandler3(3); + //处理者构成一个环形 + handler1.setNextHandler(handler2); + handler2.setNextHandler(handler3); + + handler1.handlerRequest(new Request(1)); + } +} +``` +## 实例 +当你想要让一个以上的对象有机会能够处理某个请求的时候,就是用责任链模式。 + +通过责任链模式,你可以为某个请求创建一个对象链。每个对象依序检查此请求,并对其进行处理,或者将它传给链中的下一个对象。 + +比如 + +* 程序员要请 3 天以上的假期,在 OA 申请,需要直接主管、总监、HR 层层审批后才生效。类似的采购审批、报销审批。。。 + +* 美团在[外卖营销业务](https://tech.meituan.com/2020/03/19/design-pattern-practice-in-marketing.html)中资源位展示的逻辑 +## 应用 +JAVA 中的异常处理机制、JAVA WEB 中 Apache Tomcat 对 Encoding 的处理,Struts2 的拦截器,JSP、Servlet 的 Filter 均是责任链的典型应用。 + +### Servlet 中的责任链 +```java +public final class ApplicationFilterChain implements FilterChain { + private static final ThreadLocal lastServicedRequest; + private static final ThreadLocal lastServicedResponse; + public static final int INCREMENT = 10; + private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0]; + private int pos = 0; //下一个要执行的filter的位置 + private int n = 0; //filter个数 + private Servlet servlet = null; + public ApplicationFilterChain() { + } + + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + if (Globals.IS_SECURITY_ENABLED) { + final ServletRequest req = request; + final ServletResponse res = response; + + try { + AccessController.doPrivileged(new PrivilegedExceptionAction() { + public Void run() throws ServletException, IOException { + ApplicationFilterChain.this.internalDoFilter(req, res); + return null; + } + }); + } catch (PrivilegedActionException var7) { + Exception e = var7.getException(); + if (e instanceof ServletException) { + throw (ServletException)e; + } + + if (e instanceof IOException) { + throw (IOException)e; + } + + if (e instanceof RuntimeException) { + throw (RuntimeException)e; + } + + throw new ServletException(e.getMessage(), e); + } + } else { + this.internalDoFilter(request, response); + } + } +``` +FilterChain 就是一条过滤链。其中每个过滤器(Filter)都可以决定是否执行下一步。过滤分两个方向,进和出: + +**进**:在把 ServletRequest 和 ServletResponse 交给 Servlet 的 service 方法之前,需要进行过滤 + +**出**:在service方法完成后,往客户端发送之前,需要进行过滤 + +### Spring MVC 中的责任链 +Spring MVC 的 diapatcherServlet 的 doDispatch 方法中,获取与请求匹配的处理器 `HandlerExecutionChain` 就是用到了责任链模式。 + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + HttpServletRequest processedRequest = request; + HandlerExecutionChain mappedHandler = null; //使用到了责任链模式 + boolean multipartRequestParsed = false; + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); + + try { + try { + ModelAndView mv = null; + Object dispatchException = null; + + try { + processedRequest = this.checkMultipart(request); + multipartRequestParsed = processedRequest != request; + mappedHandler = this.getHandler(processedRequest); + if (mappedHandler == null) { + this.noHandlerFound(processedRequest, response); + return; + } + + HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler()); + String method = request.getMethod(); + boolean isGet = "GET".equals(method); + if (isGet || "HEAD".equals(method)) { + long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); + if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) { + return; + } + } + //责任链模式执行预处理方法,其实是将请求交给注册的拦截器执行 + if (!mappedHandler.applyPreHandle(processedRequest, response)) { + return; + } + + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + if (asyncManager.isConcurrentHandlingStarted()) { + return; + } + + this.applyDefaultViewName(processedRequest, mv); + //责任链执行后处理方法 + mappedHandler.applyPostHandle(processedRequest, response, mv); + } catch (Exception var22) { + //... + } finally { + } + } +``` + +* SpringMVC 请求的流程中,执行了拦截器相关方法 `interceptor.preHandler` 等等 + +* 在处理 SpringMVC 请求时,使用到职责链模式还使用到适配器模式 + +* HandlerExecutionChain 主要负责的是请求拦截器的执行和请求处理,但是他本身不处理请求,只是将请求分配给链上注册处理器执行,这是职责链实现方式,减少职责链本身与处理逻辑之间的耦合,规范了处理流程 + +* HandlerExecutionChain 维护了 HandlerInterceptor 的集合, 可以向其中注册相应的拦截器 + +## 总结 + +**责任链模式其实就是一个灵活版的 if…else…语句**,它就是将这些判定条件的语句放到了各个处理类中,这样做的优点是比较灵活了,但同样也带来了风险,比如设置处理类前后关系时,一定要特别仔细,搞对处理类前后逻辑的条件判断关系,并且**注意不要在链中出现循环引用**的问题。 + +**优点**: + +降低耦合度:将请求和处理分开,实现解耦,提高了系统的灵活性。 + +简化了对象:对象不需要知道链的结构 + +良好的扩展性:增加处理者的实现很简单,只需重写处理请求业务逻辑的方法。 + +**缺点**: + +从链头发出,直到有处理者响应,在责任链比较长的时候会影响系统性能,一般需要在 Handler 中设置一个最大节点数。 + +请求递归,调试排错比较麻烦。 + +**使用场景**: + +有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。 + +在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。 + +可动态指定一组对象处理请求。 + +**模式的扩展**: + +职责链模式存在以下两种情况。 + +* 纯的职责链模式:一个请求必须被某一个处理者对象所接收,且一个具体处理者对某个请求的处理只能采用以下两种行为之一:自己处理(承担责任);把责任推给下家处理。 + +* 不纯的职责链模式:允许出现某一个具体处理者对象在承担了请求的一部分责任后又将剩余的责任传给下家的情况,且一个请求可以最终不被任何接收端对象所接收。 \ No newline at end of file diff --git "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/040.\346\250\241\346\235\277\346\226\271\346\263\225\346\250\241\345\274\217.md" "b/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/040.\346\250\241\346\235\277\346\226\271\346\263\225\346\250\241\345\274\217.md" index f4b30a56f..bf184017e 100644 --- "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/040.\346\250\241\346\235\277\346\226\271\346\263\225\346\250\241\345\274\217.md" +++ "b/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/040.\346\250\241\346\235\277\346\226\271\346\263\225\346\250\241\345\274\217.md" @@ -8,3 +8,313 @@ categories: tags: - --- +## 前言 +模板,顾名思义,它是一个固定化、标准化的东西。 + +**模板方法模式**是一种行为设计模式, 它在超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。 + +## 场景问题 +程序员不愿多扯,上来先干两行代码 + +网上模板方法的场景示例特别多,个人感觉还是《Head First 设计模式》中的例子比较好。 + +假设我们是一家饮品店的师傅,起码需要以下两个手艺 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6dgvckosfj22gj0skdjg.jpg) + + +真简单哈,这么看,步骤大同小异,我的第一反应就是写个业务接口,不同的饮品实现其中的方法就行,像这样 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6dgvmuevqj21pk0u0gng.jpg) + + +画完类图,猛地发现,第一步和第三步没什么差别,而且做饮品是个流程式的工作,我希望使用时,直接调用一个方法,就去执行对应的制作步骤。 + +灵机一动,不用接口了,用一个抽象父类,把步骤方法放在一个大的流程方法 `makingDrinks()` 中,且第一步和第三步,完全一样,没必要在子类实现,改进如下 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6dgw6r6ltj21pf0u0gm7.jpg) + + +再看下我们的设计,感觉还不错,现在用同一个 `makingDrinks()` 方法来处理咖啡和茶的制作,而且我们不希望子类覆盖这个方法,所以可以申明为 final,不同的制作步骤,我们希望子类来提供,必须在父类申明为抽象方法,而第一步和第三步我们不希望子类重写,所以我们声明为非抽象方法 +```java +public abstract class Drinks { + + void boilWater() { + System.out.println("将水煮沸"); + } + + abstract void brew(); + + void pourInCup() { + System.out.println("倒入杯子"); + } + + abstract void addCondiments(); + + public final void makingDrinks() { + //热水 + boilWater(); + //冲泡 + brew(); + //倒进杯子 + pourInCup(); + //加料 + addCondiments(); + } +} +``` +接着,我们分别处理咖啡和茶,这两个类只需要继承父类,重写其中的抽象方法即可(实现各自的冲泡和添加调料) +```java +public class Tea extends Drinks { + @Override + void brew() { + System.out.println("冲茶叶"); + } + @Override + void addCondiments() { + System.out.println("加柠檬片"); + } +} +public class Coffee extends Drinks { + @Override + void brew() { + System.out.println("冲咖啡粉"); + } + + @Override + void addCondiments() { + System.out.println("加奶加糖"); + } +} +``` +现在可以上岗了,试着制作下咖啡和茶吧 + +```java +public static void main(String[] args) { + Drinks coffee = new Coffee(); + coffee.makingDrinks(); + System.out.println(); + Drinks tea = new Tea(); + tea.makingDrinks(); +} +``` +好嘞,又学会一个设计模式,这就是模板方法模式,我们的 `makingDrinks()` 就是模板方法。我们可以看到相同的步骤 `boilWater()` 和 `pourInCup()` 只在父类中进行即可,不同的步骤放在子类实现。 + +## 认识模板方法 +在阎宏博士的《JAVA与模式》一书中开头是这样描述模板方法(Template Method)模式的: + +>模板方法模式是类的行为模式。 +>准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现, +>然后声明一些抽象方法来迫使子类实现剩余的逻辑。 +>不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。 +>这就是模板方法模式的用意。 + +写代码的一个很重要的思考点就是“**变与不变**”,程序中哪些功能是可变的,哪些功能是不变的,我们可以把不变的部分抽象出来,进行公共的实现,把变化的部分分离出来,用接口来封装隔离,或用抽象类约束子类行为。模板方法就很好的体现了这一点。 + +模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。 + +模板方法模式是所有模式中最为常见的几个模式之一,是**基于继承**的代码复用的基本技术,我们再看下类图 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6dgyinr2sj21ug0u0mze.jpg) + + +模板方法模式就是用来创建一个算法的模板,这个模板就是方法,该方法将算法定义成一组步骤,其中的任意步骤都可能是抽象的,由子类负责实现。这样可以**确保算法的结构保持不变,同时由子类提供部分实现**。 + +再回顾下我们制作咖啡和茶的例子,有些顾客要不希望咖啡加糖或者不希望茶里加柠檬,我们要改造下模板方法,在加相应的调料之前,问下顾客 +```java +public abstract class Drinks { + + void boilWater() { + System.out.println("将水煮沸"); + } + + abstract void brew(); + + void pourInCup() { + System.out.println("倒入杯子"); + } + + abstract void addCondiments(); + + public final void makingDrinks() { + boilWater(); + brew(); + pourInCup(); + + //如果顾客需要,才加料 + if (customerLike()) { + addCondiments(); + } + } + + //定义一个空的缺省方法,只返回 true + boolean customerLike() { + return true; + } +} +``` +如上,我们加了一个逻辑判断,逻辑判断的方法是一个只返回 true 的方法,这个方法我们叫做 钩子方法。 + +>钩子:在模板方法的父类中,我们可以定义一个方法,它默认不做任何事,子类可以视情况要不要覆盖它,该方法称为“钩子”。 + +钩子方法一般是空的或者有默认实现。钩子的存在,可以让子类有能力对算法的不同点进行挂钩。而要不要挂钩,又由子类去决定。 + +是不是很有用呢,我们再看下咖啡的制作 + +```java +public class Coffee extends Drinks { + @Override + void brew() { + System.out.println("冲咖啡粉"); + } + + @Override + void addCondiments() { + System.out.println("加奶加糖"); + } + //覆盖了钩子,提供了自己的询问功能,让用户输入是否需要加料 + boolean customerLike() { + String answer = getUserInput(); + if (answer.toLowerCase().startsWith("y")) { + return true; + } else { + return false; + } + } + + //处理用户的输入 + private String getUserInput() { + String answer = null; + System.out.println("您想要加奶加糖吗?输入 YES 或 NO"); + BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); + try { + answer = reader.readLine(); + } catch (IOException e) { + e.printStackTrace(); + } + if (answer == null) { + return "no"; + } + return answer; + } +} +``` +接着再去测试下代码,看看结果吧。 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6dgzoyok0j215y07uaaw.jpg) + + +我想你应该知道钩子的好处了吧,它可以作为条件控制,影响抽象类中的算法流程,当然也可以什么都不做。 + +模板方法有很多种实现,有时看起来可能不是我们所谓的“中规中矩”的设计。接下来我们看下 JDK 和 Spring 中是怎么使用模板方法的。 + +## JDK 中的模板方法 +我们写代码经常会用到 comparable 比较器来对数组对象进行排序,我们都会实现它的 `compareTo()` 方法,之后就可以通过 `Collections.sort()` 或者 `Arrays.sort()` 方法进行排序了。 + +具体的实现类就不写了(可以去 github:starfish-learning 上看我的代码),看下使用 + +```java +@Override +public int compareTo(Object o) { + Coffee coffee = (Coffee) o; + if(this.price < (coffee.price)){ + return -1; + }else if(this.price == coffee.price){ + return 0; + }else{ + return 1; + } +} +``` + +```java +public static void main(String[] args) { + Coffee[] coffees = {new Coffee("星冰乐",38), + new Coffee("拿铁",32), + new Coffee("摩卡",35)}; + + Arrays.sort(coffees); + + for (Coffee coffee1 : coffees) { + System.out.println(coffee1); + } + +} +``` + +你可能会说,这个看着不像我们常规的模板方法,是的。我们看下比较器实现的步骤 + +1. 构建对象数组 +2. 通过 `Arrays.sort` 方法对数组排序,传参为 `Comparable` 接口的实例 +3. 比较时候会调用我们的实现类的 `compareTo()` 方法 +4. 将排好序的数组设置进原数组中,排序完成 +一脸懵逼,这个实现竟然也是模板方法。 + +这个模式的重点在于提供了一个固定算法框架,并让子类实现某些步骤,虽然使用继承是标准的实现方式,但通过回调来实现,也不能说这就不是模板方法。 + +其实并发编程中最常见,也是面试必问的 AQS 就是一个典型的模板方法。 + +## Spring 中的模板方法 +Spring 中的设计模式太多了,而且大部分扩展功能都可以看到模板方法模式的影子。 + +我们看下 `IOC` 容器初始化时的模板方法,不管是 `XML` 还是注解的方式,对于核心容器启动流程都是一致的。 + +`AbstractApplicationContext` 的 `refresh` 方法实现了 `IOC` 容器启动的主要逻辑。 + +一个 `refresh()` 方法包含了好多其他步骤方法,像不像我们说的 模板方法,`getBeanFactory()` 、`refreshBeanFactory()` 是子类必须实现的抽象方法,`postProcessBeanFactory()` 是钩子方法。 + +```java +public abstract class AbstractApplicationContext extends DefaultResourceLoader + implements ConfigurableApplicationContext { + @Override + public void refresh() throws BeansException, IllegalStateException { + synchronized (this.startupShutdownMonitor) { + prepareRefresh(); + ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); + prepareBeanFactory(beanFactory); + postProcessBeanFactory(beanFactory); + invokeBeanFactoryPostProcessors(beanFactory); + registerBeanPostProcessors(beanFactory); + initMessageSource(); + initApplicationEventMulticaster(); + onRefresh(); + registerListeners(); + finishBeanFactoryInitialization(beanFactory); + finishRefresh(); + } + } + // 两个抽象方法 + @Override + public abstract ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException; + + protected abstract void refreshBeanFactory() throws BeansException, IllegalStateException; + + //钩子方法 + protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { + } + } +``` +打开你的 IDEA,我们会发现常用的 `ClassPathXmlApplicationContext` 和 `AnnotationConfigApplicationContext` 启动入口,都是它的实现类(子类的子类的子类的...)。 + +`AbstractApplicationContext` 的一个子类 `AbstractRefreshableWebApplicationContext` 中有钩子方法 `onRefresh()` 的实现: + +```java +public abstract class AbstractRefreshableWebApplicationContext extends …… { + /** + * Initialize the theme capability. + */ + @Override + protected void onRefresh() { + this.themeSource = UiApplicationContextUtils.initThemeSource(this); + } +} +``` +看下大概的类图: +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6dh6vigbaj21c20u0mz9.jpg) + + +## 小总结 +* 优点:1、封装不变部分,扩展可变部分。 2、提取公共代码,便于维护。 3、行为由父类控制,子类实现。 + +* 缺点:每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。 + +## 使用场景 +1. 有多个子类共有的方法,且逻辑相同。 +2. 重要的、复杂的方法,可以考虑作为模板方法。 + +注意事项:为防止恶意操作,一般模板方法都加上 final 关键词。 \ No newline at end of file diff --git "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/050.\347\255\226\347\225\245\346\250\241\345\274\217.md" "b/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/050.\347\255\226\347\225\245\346\250\241\345\274\217.md" index a0d360c1e..75c4c82e3 100644 --- "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/050.\347\255\226\347\225\245\346\250\241\345\274\217.md" +++ "b/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/050.\347\255\226\347\225\245\346\250\241\345\274\217.md" @@ -8,3 +8,203 @@ categories: tags: - --- +## 定义 +**策略模式**(Strategy Design Pattern):封装可以互换的行为,并使用委托来决定要使用哪一个。 + +策略模式是一种行为**设计模式**, 它能让你定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换。 + +>用人话翻译后就是:运行时我给你这个类的方法传不同的 “key”,你这个方法就去执行不同的业务逻辑。 +>你品,你细品,这不就是 if else 干的事吗? + + + +先直观的看下传统的多重 `if else` 代码 +```java +public String getCheckResult(String type) { + if ("校验1".equals(type)) { + return "执行业务逻辑1"; + } else if ("校验2".equals(type)) { + return "执行业务逻辑2"; + } else if ("校验3".equals(type)) { + return "执行业务逻辑3"; + } else if ("校验4".equals(type)) { + return "执行业务逻辑4"; + } else if ("校验5".equals(type)) { + return "执行业务逻辑5"; + } else if ("校验6".equals(type)) { + return "执行业务逻辑6"; + } else if ("校验7".equals(type)) { + return "执行业务逻辑7"; + } else if ("校验8".equals(type)) { + return "执行业务逻辑8"; + } else if ("校验9".equals(type)) { + return "执行业务逻辑9"; + } + return "不在处理的逻辑中返回业务错误"; +} +``` +这么看,你要是还觉得挺清晰的话,想象下这些 return 里是各种复杂的业务逻辑方法~~ +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6dhbjxq5ej206m05s0su.jpg) + + +当然,策略模式的作用可不止是避免冗长的 `if-else` 或者 `switch` 分支,它还可以像模板方法模式那样提供框架的扩展点等。 + +网上的示例很多,比如不同路线的规划、不同支付方式的选择 都是典型的 `if-else` 问题,也都是典型的策略模式问题,栗子我们待会看,先看下策略模式的类图,然后去改造多重判断~ + +## 角色 + + +策略模式涉及到三个角色: + +1. **Strategy**:策略接口或者策略抽象类,用来约束一系列的策略算法(Context 使用这个接口来调用具体的策略实现算法) +2. **ConcreateStrategy**:具体的策略类(实现策略接口或继承抽象策略类) +3. **Context**:上下文类,持有具体策略类的实例,并负责调用相关的算法 +应用策略模式来解决问题的思路 + +## 实例 +先看看最简单的策略模式 demo: + +1、策略接口(定义策略) +```java +public interface Strategy { + void operate(); +} +``` +2、具体的算法实现 +```java +public class ConcreteStrategyA implements Strategy { + @Override + public void operate() { + //具体的算法实现 + System.out.println("执行业务逻辑A"); + } +} +``` +```java +public class ConcreteStrategyB implements Strategy { + @Override + public void operate() { + //具体的算法实现 + System.out.println("执行业务逻辑B"); + } +} +``` +3、上下文的实现 +```java +public class Context { + + //持有一个具体的策略对象 + private Strategy strategy; + + //构造方法,传入具体的策略对象 + public Context(Strategy strategy){ + this.strategy = strategy; + } + + public void doSomething(){ + //调用具体的策略对象进操作 + strategy.operate(); + } +} +``` +4、客户端使用(策略的使用) +```java +public static void main(String[] args) { + Context context = new Context(new ConcreteStrategyA()); + context.doSomething(); +} +``` +>ps:这种策略的使用方式其实很死板,真正使用的时候如果还这么写, +>和写一大推 `if-else` 没什么区别,所以我们一般会结合工厂类, +>在运行时动态确定使用哪种策略。策略模式侧重如何选择策略、工厂模式侧重如何创建策略。 + +## 解析策略模式 +策略模式的功能就是把具体的算法实现从具体的业务处理中独立出来,把它们实现成单独的算法类,从而形成一系列算法,并让这些算法可以互相替换。 + +>策略模式的重心不是如何来实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活,具有更好的维护性和扩展性。 + +实际上,每个策略算法具体实现的功能,就是原来在 `if-else` 结构中的具体实现,每个 `if-else` 语句都是一个平等的功能结构,可以说是兄弟关系。 + +策略模式呢,就是把各个平等的具体实现封装到单独的策略实现类了,然后通过上下文与具体的策略类进行交互。 + +**『 策略模式 = 实现策略接口(或抽象类)的每个策略类 + 上下文的逻辑分派 』** + + + +>策略模式的本质:分离算法,选择实现 ——《研磨设计模式》 + +所以说,策略模式只是在代码结构上的一个调整,即使用了策略模式,该写的逻辑一个也少不了,到逻辑分派的时候,只是变相的 `if-else`。 + +而它的优化点是抽象了出了接口,将业务逻辑封装成一个一个的实现类,任意地替换。在复杂场景(业务逻辑较多)时比直接 `if-else` 更好维护和扩展些。 + +## 谁来选择具体的策略算法 +如果你手写了上边的 demo,就会发现,这玩意不及 `if-else` 来的顺手,尤其是在判断逻辑的时候,每个逻辑都要要构造一个上下文对象,费劲。 + +其实,策略模式中,我们可以自己定义谁来选择具体的策略算法,有两种: + +* 客户端:当使用上下文时,由客户端选择,像我们上边的 demo +* 上下文:客户端不用选,由上下文来选具体的策略算法,可以在构造器中指定 +## 优缺点 +### 优点: +避免多重条件语句:也就是避免大量的 `if-else` +更好的扩展性(完全符合开闭原则):策略模式中扩展新的策略实现很容易,无需对上下文修改,只增加新的策略实现类就可以 +### 缺点: +客户必须了解每种策略的不同(这个可以通过 IOC、依赖注入的方式解决) +增加了对象数:每个具体策略都封装成了类,可能备选的策略会很多 +只适合扁平的算法结构:策略模式的一系列算法是平等的,也就是在运行时刻只有一个算法会被使用,这就限制了算法使用的层级,不能嵌套使用 +## 思考 +实际使用中,往往不会只是单一的某个设计模式的套用,一般都会混合使用,而且模式之间的结合也是没有定势的,要具体问题具体分析。 + +策略模式往往会结合其他模式一起使用,比如工厂、模板等,具体使用需要结合自己的业务。 + +切记,不要为了使用设计模式而强行模式,不要把简单问题复杂化。 + +策略模式也不是专为消除 `if-else` 而生的,不要和 `if-else` 划等号。它体现了“对修改关闭,对扩展开放“的原则。 + +并不是说,看到 `if-else` 就想着用策略模式去优化,业务逻辑简单,可能几个枚举,或者几个卫语句就搞定的场景,就不用非得硬套设计模式了。 + +## 策略模式在 JDK 中的应用 +在 JDK 中,Comparator 比较器是一个策略接口,我们常用的 `compare()` 方法就是一个具体的策略实现,用于定义排序规则。 +```java +public interface Comparator { + int compare(T o1, T o2); + //...... +} +``` +当我们想自定义排序规则的时候,就可以实现 Comparator 。 + +这时候我们重写了接口中的 `compare()` 方法,就是具体的策略类(只不过这里可能是内部类)。当我们在调用 Arrays 的排序方法 `sort()` 时,可以用默认的排序规则,也可以用自定义的规则。 + +```java +public static void main(String[] args) { + Integer[] data = {4,2,7,5,1,9}; + Comparator comparator = new Comparator() { + @Override + public int compare(Integer o1, Integer o2) { + if(o1 > o2){ + return 1; + } else { + return -1; + } + } + }; + + Arrays.sort(data,comparator); + System.out.println(Arrays.toString(data)); +} +``` +Arrays 的 `sort()` 方法,有自定义规则就按自己的方法排序,反之走源码逻辑。 + +```java +public static void sort(T[] a, Comparator c) { + if (c == null) { + sort(a); + } else { + if (LegacyMergeSort.userRequested) + legacyMergeSort(a, c); + else + TimSort.sort(a, 0, a.length, c, null, 0, 0); + } +} +``` +还有,ThreadPoolExecutor 中的拒绝策略 RejectedExecutionHandler 也是典型的策略模式,感兴趣的也可以再看看源码。 \ No newline at end of file diff --git "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/060.\350\247\202\345\257\237\346\250\241\345\274\217.md" "b/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/060.\350\247\202\345\257\237\346\250\241\345\274\217.md" index b7efb9fac..cf7138588 100644 --- "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/060.\350\247\202\345\257\237\346\250\241\345\274\217.md" +++ "b/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/060.\350\247\202\345\257\237\346\250\241\345\274\217.md" @@ -8,3 +8,206 @@ categories: tags: - --- +在软件系统中经常会有这样的需求:如果一个对象的状态发生改变,某些与它相关的对象也要随之做出相应的变化。 +举个例子: + +* 微信公众号,如果一个用户订阅了某个公众号,那么便会收到公众号发来的消息,那么,公众号就是『被观察者』,而用户就是『观察者』 +* 气象站可以将每天预测到的温度、湿度、气压等以公告的形式发布给各种第三方网站,如果天气数据有更新,要能够实时的通知给第三方,这里的气象局就是『被观察者』,第三方网站就是『观察者』 +* MVC 模式中的模型与视图的关系也属于观察与被观察关系 + +观察者模式是使用频率较高的设计模式之一。 + +观察者模式包含观察目标和观察者两类对象,一个目标可以有任意数目的与之相依赖的观察者,一旦观察目标的状态发生改变,所有的观察者都将得到通知。 + +## 定义 +**观察者模式(Observer Pattern**): 定义对象间一种一对多的依赖关系,使得当每一个对象改变状态,则所有依赖于它的对象都会得到通知并自动更新。 + +观察者模式是一种**对象行为型模式**。 + +观察者模式的别名包括发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。 + +细究的话,发布订阅和观察者有些不同,可以理解成发布订阅模式属于广义上的观察者模式。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6dgl8xcg2j20ip0ba74p.jpg) + +## 角色 +* **Subject(目标)**:被观察者,它是指被观察的对象。 从类图中可以看到,类中有一个用来存放观察者对象的Vector 容器(Vector在是安全的,而List则是不安全的),这个 Vector 容器是被观察者类的核心,另外还有三个方法:attach 方法是向这个容器中添加观察者对象;detach 方法是从容器中移除观察者对象;notify 方法是依次调用观察者对象的对应方法。这个角色可以是接口,也可以是抽象类或者具体的类,因为很多情况下会与其他的模式混用,所以使用抽象类的情况比较多。 + +* **ConcreteSubject(具体目标)**:具体目标是目标类的子类,通常它包含经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知。同时它还实现了在目标类中定义的抽象业务逻辑方法(如果有的话)。如果无须扩展目标类,则具体目标类可以省略。 + +* **Observer(观察者)**:观察者将对观察目标的改变做出反应,观察者一般定义为接口,该接口声明了更新数据的方法 update(),因此又称为抽象观察者。 + +* **ConcreteObserver(具体观察者)**:在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者 Observer 中定义的 update() 方法。通常在实现时,可以调用具体目标类的 attach() 方法将自己添加到目标类的集合中或通过 detach() 方法将自己从目标类的集合中删除。 + +## 类图 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6dgm2wtetj211t0lnjtb.jpg) + +再记录下 UML 类图的注意事项,这里我的 Subject 是**抽象方法**,所以用***斜体***,抽象方法也要用斜体,具体的各种箭头意义,我之前也总结过《设计模式前传——学设计模式前你要知道这些》(被网上各种帖子毒害过的自己,认真记录~~~)。 + +## 实例 +1、定义观察者接口 +```java +interface Observer { + public void update(); +} +``` +2、定义被观察者 +```java +abstract class Subject { + private Vector obs = new Vector(); + + public void addObserver(Observer obs){ + this.obs.add(obs); + } + public void delObserver(Observer obs){ + this.obs.remove(obs); + } + protected void notifyObserver(){ + for(Observer o: obs){ + o.update(); + } + } + public abstract void doSomething(); +} +``` +3、具体的被观察者 +```java +class ConcreteSubject extends Subject { + public void doSomething(){ + System.out.println("被观察者事件发生改变"); + this.notifyObserver(); + } +} +``` +4、具体的观察者 +```java +class ConcreteObserver1 implements Observer { + public void update() { + System.out.println("观察者1收到信息,并进行处理"); + } +} +class ConcreteObserver2 implements Observer { + public void update() { + System.out.println("观察者2收到信息,并进行处理"); + } +} +``` +5、客户端 +```java +public class Client { + public static void main(String[] args){ + Subject sub = new ConcreteSubject(); + sub.addObserver(new ConcreteObserver1()); //添加观察者1 + sub.addObserver(new ConcreteObserver2()); //添加观察者2 + sub.doSomething(); + } +} +``` +输出 + +```java +被观察者事件发生改变 +观察者1收到信息,并进行处理 +观察者2收到信息,并进行处理 +``` + +通过运行结果可以看到,我们只调用了 Subject 的方法,但同时两个观察者的相关方法都被调用了。仔细看一下代码,其实很简单,就是在 Subject 类中关联一下 Observer 类,并且在 doSomething() 方法中遍历一下 Observer 的 update() 方法就行了。 + +## 优缺点 +### 优点 +降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系 +目标与观察者之间建立了一套触发机制 +支持广播通信 +符合“开闭原则”的要求 +### 缺点 +目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用 +当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率 +## 应用 +### JDK 中的观察者模式 +观察者模式在 Java 语言中的地位非常重要。在 JDK 的 java.util 包中,提供了 Observable 类以及 Observer 接口,它们构成了 JDK 对观察者模式的支持(可以去查看下源码,写的比较严谨)。but,在 Java9 被弃用了。 + +### Spring 中的观察者模式 +Spring 事件驱动模型也是观察者模式很经典的应用。就是我们项目中最常见的事件监听器。 + +#### 1. Spring 中观察者模式的四个角色 +* 事件:`ApplicationEvent` 是所有事件对象的父类。`ApplicationEvent` 继承自 `jdk` 的 `EventObject`,所有的事件都需要继承 `ApplicationEvent`,并且通过 `source` 得到事件源。 + +* Spring 也为我们提供了很多内置事件,`ContextRefreshedEvent`、`ContextStartedEvent`、`ContextStoppedEvent`、`ContextClosedEvent`、`RequestHandledEvent`。 + +* 事件监听:`ApplicationListener`,也就是观察者,继承自 `jdk` 的 `EventListener`,该类中只有一个方法 `onApplicationEvent`。当监听的事件发生后该方法会被执行。 + +* 事件源:`ApplicationContext`,`ApplicationContext` 是 `Spring` 中的核心容器,在事件监听中 `ApplicationContext` 可以作为事件的发布者,也就是事件源。因为 `ApplicationContext` 继承自 `ApplicationEventPublisher`。在 `ApplicationEventPublisher` 中定义了事件发布的方法:`publishEvent(Object event)` + +* 事件管理:`ApplicationEventMulticaster`,用于事件监听器的注册和事件的广播。监听器的注册就是通过它来实现的,它的作用是把 `Applicationcontext` 发布的 `Event` 广播给它的监听器列表。 + +#### 2. coding~~~~~~ +1、定义事件 +```java +public class MyEvent extends ApplicationEvent { + public MyEvent(Object source) { + super(source); + System.out.println("my Event"); + } +} +``` +2、实现事件监听器 +```java +@Component +class MyListenerA implements ApplicationListener { + public void onApplicationEvent(MyEvent AyEvent) { + System.out.println("ListenerA received"); + } +} + +@Component +class MyListenerB implements ApplicationListener { + public void onApplicationEvent(MyEvent AyEvent) { + System.out.println("ListenerB received"); + } +} +``` +3、事件发布者 +```java +@Component +public class MyPublisher implements ApplicationContextAware { + private ApplicationContext applicationContext; + + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext=applicationContext; + } + + public void publishEvent(ApplicationEvent event){ + System.out.println("publish event"); + applicationContext.publishEvent(event); + } +} +``` +4、测试,先用注解方式将 MyPublisher 注入 Spring +```java +@Configuration +@ComponentScan +public class AppConfig { + + @Bean(name = "myPublisher") + public MyPublisher myPublisher(){ + return new MyPublisher(); + } +} +public class Client { + + @Test + public void main() { + ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); + MyPublisher myPublisher = (MyPublisher) context.getBean("myPublisher"); + myPublisher.publishEvent(new MyEvent(this)); + } +} +``` +5、输出 +```java +my Event +publish event +ListenerA received +ListenerB received +``` \ No newline at end of file diff --git "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/070.\344\273\243\347\220\206\346\250\241\345\274\217.md" "b/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/070.\344\273\243\347\220\206\346\250\241\345\274\217.md" deleted file mode 100644 index 291177d63..000000000 --- "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/070.\344\273\243\347\220\206\346\250\241\345\274\217.md" +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: 代理模式 -date: 2022-05-21 17:30:05 -permalink: /pages/a3c735/ -categories: - - Java相关 - - 设计模式 -tags: - - ---- diff --git "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/080.\345\273\272\351\200\240\350\200\205\346\250\241\345\274\217.md" "b/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/080.\345\273\272\351\200\240\350\200\205\346\250\241\345\274\217.md" deleted file mode 100644 index d5ab750d1..000000000 --- "a/docs/01.Java\347\233\270\345\205\263/40.\350\256\276\350\256\241\346\250\241\345\274\217/080.\345\273\272\351\200\240\350\200\205\346\250\241\345\274\217.md" +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: 建造者模式 -date: 2022-05-21 17:30:20 -permalink: /pages/c332ae/ -categories: - - Java相关 - - 设计模式 -tags: - - ---- diff --git a/node_modules/@vuepress/core/.temp/internal/app-enhancers.js b/node_modules/@vuepress/core/.temp/internal/app-enhancers.js index 256fdff06..8800a4b52 100644 --- a/node_modules/@vuepress/core/.temp/internal/app-enhancers.js +++ b/node_modules/@vuepress/core/.temp/internal/app-enhancers.js @@ -2,7 +2,7 @@ import m0 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_module import m1 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_modules/@vuepress/core/.temp/app-enhancers/1.js" import m2 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_modules/@vuepress/core/.temp/app-enhancers/2.js" import m3 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_modules/@vuepress/core/.temp/app-enhancers/data-block.js" -import m4 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_modules/@vuepress/core/.temp/app-enhancers/global-components-23.js" +import m4 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_modules/@vuepress/core/.temp/app-enhancers/global-components-54.js" import m5 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_modules/@vuepress/core/.temp/app-enhancers/3.js" import m6 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_modules/@vuepress/core/.temp/app-enhancers/4.js" import m7 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_modules/@vuepress/core/.temp/app-enhancers/5.js" diff --git a/node_modules/@vuepress/core/.temp/internal/page-components.js b/node_modules/@vuepress/core/.temp/internal/page-components.js index 3beb00e19..6f39eeb36 100644 --- a/node_modules/@vuepress/core/.temp/internal/page-components.js +++ b/node_modules/@vuepress/core/.temp/internal/page-components.js @@ -4,129 +4,127 @@ export default { "v-36edf5bb": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/00.目录页/01.Java相关.md"), "v-a612ff08": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/00.目录页/03.数据库.md"), + "v-7abe58c4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/00.目录页/04.中间件.md"), "v-10f5d0e5": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/00.目录页/02.计算机基础.md"), + "v-0734374b": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/010.HashMap 与 ConcurrentHashMap 的实现原理是怎样的?.md"), "v-8bf153e4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/001.Java 异常有哪些类型?.md"), - "v-7abe58c4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/00.目录页/04.中间件.md"), "v-4e1fd39c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/020.Java 中接口和抽象类的区别.md"), "v-7f48b402": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/050.String,StringBuffer,StringBuilder 之间有什么区别?.md"), + "v-57b2551a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/060.简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度.md"), "v-d8a2f15c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/040.== 和 equals() 的区别?.md"), "v-57012c44": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/030.为什么重写 equals 必须重写 hashcode ?.md"), - "v-57b2551a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/060.简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度.md"), "v-d4e20148": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/070.简述 BIO, NIO, AIO 的区别.md"), - "v-0734374b": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/010.HashMap 与 ConcurrentHashMap 的实现原理是怎样的?.md"), "v-d32e07ea": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/080.在一个静态方法内调用一个非静态成员为什么是非法的?.md"), "v-85b06162": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/090.简述 Java 的反射机制及其应用场景.md"), "v-4c273a66": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/100.Java 类的加载流程是怎样的?.md"), - "v-6ff6e2f0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/120.Java 中 sleep() 与 wait() 的区别 .md"), - "v-5290507e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/140.String 类能不能被继承?为什么?.md"), - "v-620ebadc": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/130.简述 Java 中 final 关键字的作用.md"), "v-7665e30e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/110.什么是双亲委派机制?.md"), + "v-6ff6e2f0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/120.Java 中 sleep() 与 wait() 的区别 .md"), "v-6f5c6512": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/150.Java 缓冲流 buffer 的用途和原理是什么?.md"), "v-75844e97": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/160.简述动态代理与静态代理 .md"), + "v-5290507e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/140.String 类能不能被继承?为什么?.md"), + "v-620ebadc": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/130.简述 Java 中 final 关键字的作用.md"), "v-1218fa85": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/170.阻塞队列都有哪几种,有什么区别?.md"), "v-b21bbc4c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/180.简述 HashSet 实现原理.md"), + "v-45fc1217": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/001.线程池是如何实现的?.md"), "v-5390e3d4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/020.简述 CAS 原理,什么是 ABA 问题,怎么解决?.md"), - "v-352f36cc": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/040.Synchronized 关键字底层是如何实现的?.md"), "v-35324a50": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/030.简述 Synchronized,Volatile,可重入锁的不同使用场景及优缺点.md"), - "v-f5bcd180": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/060.volatile 关键字解决了什么问题,它的实现原理是什么?.md"), - "v-45fc1217": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/001.线程池是如何实现的?.md"), + "v-352f36cc": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/040.Synchronized 关键字底层是如何实现的?.md"), "v-4927c049": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/050.重入锁是如何实现的?.md"), - "v-134f28dc": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/090.什么是公平锁?什么是非公平锁?.md"), - "v-2dbdb772": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/080.简述 Java AQS 的原理以及使用场景.md"), + "v-f5bcd180": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/060.volatile 关键字解决了什么问题,它的实现原理是什么?.md"), "v-1065f7be": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/070.简述 Java 锁升级的机制.md"), - "v-03e420c4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/100.Java 的线程有哪些状态,转换关系是怎么样的?.md"), + "v-2dbdb772": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/080.简述 Java AQS 的原理以及使用场景.md"), + "v-134f28dc": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/090.什么是公平锁?什么是非公平锁?.md"), "v-625fbeda": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/110.Java 是如何实现线程安全的,哪些数据结构是线程安全的?.md"), + "v-03e420c4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/100.Java 的线程有哪些状态,转换关系是怎么样的?.md"), "v-7b2a2ed4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/115.手写死锁.md"), "v-9baaad8e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/120.为什么我们不能直接调用 run() 方法?.md"), "v-75c00225": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/130.Java 线程有哪些常用方法?.md"), - "v-083aef82": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/140.手写生产者消费者模型.md"), "v-f406b4e6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/150.ThreadLocal 实现原理是什么?为什么要使用弱引用?.md"), + "v-083aef82": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/140.手写生产者消费者模型.md"), "v-7cd97c82": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/001.Java 中垃圾回收机制中如何判断对象需要回收?.md"), + "v-5474bec0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/020.Java 中如何进行 GC 调优?.md"), "v-af821fc8": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/010.常见的 GC 回收算法有哪些?.md"), "v-2de19204": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/040.JVM 内存是如何对应到操作系统内存的?.md"), - "v-5474bec0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/020.Java 中如何进行 GC 调优?.md"), "v-0970cd54": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/030.JMM 中内存模型是怎样的?什么是指令序列重排序?.md"), - "v-ba94c306": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/002.实现单例设计模式(懒汉,饿汉).md"), - "v-28c1d6d9": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/070.简述 CMS 与 G1 机制的区别.md"), "v-37fcb440": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/050.JVM 是怎么去调优的?了解哪些参数和指令?.md"), - "v-0564ba27": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/020.代理模式.md"), - "v-88b087a6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/003.什么是设计模式 .md"), + "v-28c1d6d9": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/070.简述 CMS 与 G1 机制的区别.md"), "v-15fba8f1": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/060.什么是内存泄漏,怎么确定内存泄漏?.md"), - "v-34f8aaa2": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/030.责任链模式.md"), + "v-ba94c306": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/002.实现单例设计模式(懒汉,饿汉).md"), + "v-88b087a6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/003.什么是设计模式 .md"), + "v-2d6ed001": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/004.工厂模式使用场景 能举个例子吗.md"), "v-22a9eaef": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/005.装饰者模式.md"), + "v-0564ba27": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/020.代理模式.md"), "v-31509c80": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/010.适配器模式.md"), - "v-2d6ed001": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/004.工厂模式使用场景 能举个例子吗.md"), + "v-34f8aaa2": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/030.责任链模式.md"), + "v-5b0157cf": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/050.策略模式.md"), "v-28deb9da": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/040.模板方法模式.md"), - "v-2279e49d": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/070.代理模式.md"), + "v-1e653940": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/01.计算机网络.md"), "v-64f1031c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/060.观察模式.md"), - "v-5b0157cf": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/050.策略模式.md"), - "v-dd5f4440": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/080.建造者模式.md"), - "v-6f178cbc": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/01.操作系统.md"), "v-0a1d1fb4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/010.简述常见的 HTTP 状态码的含义.md"), - "v-2e6d42b4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/40.算法/01.算法.md"), + "v-6602e3da": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/020.从输入 URL 到展现页面的全过程.md"), + "v-6f178cbc": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/01.操作系统.md"), "v-49688930": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/30.数据结构/01.数据结构.md"), + "v-2e6d42b4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/40.算法/01.算法.md"), + "v-f0f711c4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/006.聚簇索引和非聚簇索引有什么区别?.md"), "v-674454ac": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/001.MySQL中 InnoDB 和 MylSAM 的区别是什么?.md"), + "v-7e02c4a1": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/010.简述Mysql的事务隔离级别.md"), "v-824b776c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/007.简述 MySQL 常见索引类型,介绍一下覆盖索引.md"), - "v-1e653940": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/01.计算机网络.md"), - "v-f0f711c4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/006.聚簇索引和非聚簇索引有什么区别?.md"), "v-5b0e09c6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/015.说一下Mysql锁.md"), "v-c39c4fe2": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/030.简述乐观锁以及悲观锁的区别以及使用场景.md"), - "v-7e02c4a1": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/010.简述Mysql的事务隔离级别.md"), - "v-689e142a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/040.简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?.md"), "v-6c681639": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/020.MySQL 为什么使用 B+ 树来作索引,对比 B 树它的优点和缺点是什么.md"), "v-4ecddbec": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/050.简述什么是最左匹配原则.md"), "v-0dd47f09": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/070.产生死锁的必要条件有哪些?如何解决死锁?.md"), + "v-689e142a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/040.简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?.md"), "v-505fbe2e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/060.MySQL 联合索引底层原理是什么?.md"), "v-53bcbb7a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/080.简述常见的负载均衡算法.md"), - "v-c383b96e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/090.简述 MySQL 的主从同步机制,如果同步失败会怎么样?.md"), - "v-ed0c93b8": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/110.简述数据库中什么情况下进行分库,什么情况下进行分表?.md"), + "v-4cef944a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/100.如何设计数据库压测方案?.md"), "v-d6bc240a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/120.数据库的读写分离的作用是什么?如何实现?.md"), - "v-ceed6ba6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/140.什么是 SQL 注入攻击?如何防止这类攻击?.md"), - "v-77790024": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/150.MySQL 中 join 与 left join 的区别是什么?.md"), + "v-ed0c93b8": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/110.简述数据库中什么情况下进行分库,什么情况下进行分表?.md"), "v-f3acaa1a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/130.简述 undo log 和 redo log 的作用.md"), + "v-c383b96e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/090.简述 MySQL 的主从同步机制,如果同步失败会怎么样?.md"), + "v-ceed6ba6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/140.什么是 SQL 注入攻击?如何防止这类攻击?.md"), "v-76f5e79a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/160.简述 MySQL MVCC 的实现原理 .md"), - "v-4cef944a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/100.如何设计数据库压测方案?.md"), - "v-0a7e6416": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/170.MySQL 中 varchar 和 char 的区别是什么?.md"), + "v-77790024": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/150.MySQL 中 join 与 left join 的区别是什么?.md"), "v-6b967a6e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/180.模糊查询是如何实现的?.md"), - "v-cb742458": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/002.为什么 Redis 在单线程下能如此快?.md"), + "v-0a7e6416": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/170.MySQL 中 varchar 和 char 的区别是什么?.md"), "v-d804fba0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/001.Redis 有几种数据结构?Zset 是如何实现的?.md"), - "v-c0bc7b7a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/010.Redis的缓存淘汰策略有哪些?.md"), + "v-cb742458": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/002.为什么 Redis 在单线程下能如此快?.md"), "v-3e917471": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/005.简述 Redis 字符串的底层结构.md"), - "v-2c6b511c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/030.Redis 如何实现分布式锁?.md"), + "v-c0bc7b7a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/010.Redis的缓存淘汰策略有哪些?.md"), + "v-7c214d00": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/020.简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点.md"), "v-3d2fdd6a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/040.简述 Redis 集群配置以及基础原理.md"), - "v-18b427a0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/050.简述 Redis 中跳表的应用以及优缺点.md"), + "v-2c6b511c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/030.Redis 如何实现分布式锁?.md"), "v-6a45f68d": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/060.Redis 中,sentinel 和 cluster 的区别和适用场景是什么?.md"), + "v-18b427a0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/050.简述 Redis 中跳表的应用以及优缺点.md"), "v-c2fe5e1a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/070.简述 Redis 中如何防止缓存雪崩和缓存击穿.md"), - "v-35718f4e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/080.简述 Redis 的线程模型以及底层架构设计.md"), - "v-37f158ba": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/110.简述 Redis 如何处理热点 key 访问.md"), "v-57957421": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/100.简述 Redis 的哨兵机制.md"), - "v-7c214d00": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/020.简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点.md"), - "v-5acdab4d": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/010.TCP粘包拆包问题的解决之道.md"), - "v-be02b3c0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/001.你熟悉哪些IO模型?.md"), + "v-37f158ba": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/110.简述 Redis 如何处理热点 key 访问.md"), + "v-35718f4e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/080.简述 Redis 的线程模型以及底层架构设计.md"), "v-02ea33c3": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/120.Redis 序列化有哪些方式?.md"), + "v-be02b3c0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/001.你熟悉哪些IO模型?.md"), + "v-5acdab4d": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/010.TCP粘包拆包问题的解决之道.md"), + "v-bbcf6e5a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/020.为什么Netty要自己写一个ByteBuf.md"), "v-073f22cd": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/015.Netty中有哪些解码器?.md"), "v-5157927c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/030.ChannelPipeline和ChannelHandler.md"), - "v-6b32ada0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/050.Java NIO了解吗?.md"), "v-62f3fa3e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/040.EventLoop和EventLoopGroup.md"), - "v-bbcf6e5a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/020.为什么Netty要自己写一个ByteBuf.md"), + "v-6b32ada0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/050.Java NIO了解吗?.md"), "v-7c0136cf": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/010.生产者.md"), "v-0df39df8": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/001.Kafka包含那几部分.md"), "v-04af9e42": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/020.消费者.md"), "v-0c440466": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/030.集群下的Kafka要考虑哪些事情.md"), - "v-5675b06b": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/08.系统设计/010.什么是 CAP ?什么是最终一致性?什么是幂等操作?.md"), - "v-a9558ec0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/08.系统设计/001.系统设计.md"), "v-684c4815": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/040.如何保证Kafka的可靠性.md"), + "v-5675b06b": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/08.系统设计/010.什么是 CAP ?什么是最终一致性?什么是幂等操作?.md"), "v-f88544f4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/08.系统设计/020.简述什么是两阶段提交?.md"), - "v-918dfaf8": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/15.关于/01.关于.md"), "v-2a52a762": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/10.非技术/01.非技术问题.md"), + "v-a9558ec0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/08.系统设计/001.系统设计.md"), + "v-918dfaf8": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/15.关于/01.关于.md"), + "v-7ab50b9f": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/20.收藏夹/01.网站.md"), "v-09c150e6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/20.收藏夹/02.常用的前端轮子.md"), "v-48f7eec0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/archivesPage.md"), - "v-7ab50b9f": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/20.收藏夹/01.网站.md"), + "v-5f8dfe80": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/categoriesPage.md"), + "v-64f9b920": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/tagsPage.md"), "v-2db11cb2": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/_posts/随笔/你知道的越多,不知道的也就越多.md"), - "v-a5c5f278": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/_posts/随笔/我做了一个手写春联小网页,祝大家虎年暴富.md"), "v-7d77d69c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/_posts/随笔/拥抱生活,拥抱快乐.md"), - "v-2cf94a0e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/index.md"), - "v-64f9b920": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/tagsPage.md"), - "v-5f8dfe80": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/categoriesPage.md"), - "v-6602e3da": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/020.从输入 URL 到展现页面的全过程.md") + "v-a5c5f278": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/_posts/随笔/我做了一个手写春联小网页,祝大家虎年暴富.md"), + "v-2cf94a0e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/index.md") } \ No newline at end of file diff --git a/node_modules/@vuepress/core/.temp/internal/routes.js b/node_modules/@vuepress/core/.temp/internal/routes.js index 1d59a8b45..e6da51ceb 100644 --- a/node_modules/@vuepress/core/.temp/internal/routes.js +++ b/node_modules/@vuepress/core/.temp/internal/routes.js @@ -40,6 +40,22 @@ export const routes = [ path: "/00.目录页/03.数据库.html", redirect: "/database/" }, + { + name: "v-7abe58c4", + path: "/middleware/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-7abe58c4").then(next) + }, + }, + { + path: "/middleware/index.html", + redirect: "/middleware/" + }, + { + path: "/00.目录页/04.中间件.html", + redirect: "/middleware/" + }, { name: "v-10f5d0e5", path: "/basic/", @@ -57,36 +73,36 @@ export const routes = [ redirect: "/basic/" }, { - name: "v-8bf153e4", - path: "/pages/a030ff/", + name: "v-0734374b", + path: "/pages/d1c564/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-8bf153e4").then(next) + ensureAsyncComponentsLoaded("Layout", "v-0734374b").then(next) }, }, { - path: "/pages/a030ff/index.html", - redirect: "/pages/a030ff/" + path: "/pages/d1c564/index.html", + redirect: "/pages/d1c564/" }, { - path: "/01.Java相关/10.基础/001.Java 异常有哪些类型?.html", - redirect: "/pages/a030ff/" + path: "/01.Java相关/10.基础/010.HashMap 与 ConcurrentHashMap 的实现原理是怎样的?.html", + redirect: "/pages/d1c564/" }, { - name: "v-7abe58c4", - path: "/middleware/", + name: "v-8bf153e4", + path: "/pages/a030ff/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-7abe58c4").then(next) + ensureAsyncComponentsLoaded("Layout", "v-8bf153e4").then(next) }, }, { - path: "/middleware/index.html", - redirect: "/middleware/" + path: "/pages/a030ff/index.html", + redirect: "/pages/a030ff/" }, { - path: "/00.目录页/04.中间件.html", - redirect: "/middleware/" + path: "/01.Java相关/10.基础/001.Java 异常有哪些类型?.html", + redirect: "/pages/a030ff/" }, { name: "v-4e1fd39c", @@ -120,6 +136,22 @@ export const routes = [ path: "/01.Java相关/10.基础/050.String,StringBuffer,StringBuilder 之间有什么区别?.html", redirect: "/pages/211d8c/" }, + { + name: "v-57b2551a", + path: "/pages/305919/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-57b2551a").then(next) + }, + }, + { + path: "/pages/305919/index.html", + redirect: "/pages/305919/" + }, + { + path: "/01.Java相关/10.基础/060.简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度.html", + redirect: "/pages/305919/" + }, { name: "v-d8a2f15c", path: "/pages/06b28a/", @@ -152,22 +184,6 @@ export const routes = [ path: "/01.Java相关/10.基础/030.为什么重写 equals 必须重写 hashcode ?.html", redirect: "/pages/0d22ad/" }, - { - name: "v-57b2551a", - path: "/pages/305919/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-57b2551a").then(next) - }, - }, - { - path: "/pages/305919/index.html", - redirect: "/pages/305919/" - }, - { - path: "/01.Java相关/10.基础/060.简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度.html", - redirect: "/pages/305919/" - }, { name: "v-d4e20148", path: "/pages/276740/", @@ -184,22 +200,6 @@ export const routes = [ path: "/01.Java相关/10.基础/070.简述 BIO, NIO, AIO 的区别.html", redirect: "/pages/276740/" }, - { - name: "v-0734374b", - path: "/pages/d1c564/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-0734374b").then(next) - }, - }, - { - path: "/pages/d1c564/index.html", - redirect: "/pages/d1c564/" - }, - { - path: "/01.Java相关/10.基础/010.HashMap 与 ConcurrentHashMap 的实现原理是怎样的?.html", - redirect: "/pages/d1c564/" - }, { name: "v-d32e07ea", path: "/pages/56314c/", @@ -249,100 +249,100 @@ export const routes = [ redirect: "/pages/33c5d6/" }, { - name: "v-6ff6e2f0", - path: "/pages/d69890/", + name: "v-7665e30e", + path: "/pages/38bcaf/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-6ff6e2f0").then(next) + ensureAsyncComponentsLoaded("Layout", "v-7665e30e").then(next) }, }, { - path: "/pages/d69890/index.html", - redirect: "/pages/d69890/" + path: "/pages/38bcaf/index.html", + redirect: "/pages/38bcaf/" }, { - path: "/01.Java相关/10.基础/120.Java 中 sleep() 与 wait() 的区别 .html", - redirect: "/pages/d69890/" + path: "/01.Java相关/10.基础/110.什么是双亲委派机制?.html", + redirect: "/pages/38bcaf/" }, { - name: "v-5290507e", - path: "/pages/73113f/", + name: "v-6ff6e2f0", + path: "/pages/d69890/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-5290507e").then(next) + ensureAsyncComponentsLoaded("Layout", "v-6ff6e2f0").then(next) }, }, { - path: "/pages/73113f/index.html", - redirect: "/pages/73113f/" + path: "/pages/d69890/index.html", + redirect: "/pages/d69890/" }, { - path: "/01.Java相关/10.基础/140.String 类能不能被继承?为什么?.html", - redirect: "/pages/73113f/" + path: "/01.Java相关/10.基础/120.Java 中 sleep() 与 wait() 的区别 .html", + redirect: "/pages/d69890/" }, { - name: "v-620ebadc", - path: "/pages/2b00a3/", + name: "v-6f5c6512", + path: "/pages/115772/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-620ebadc").then(next) + ensureAsyncComponentsLoaded("Layout", "v-6f5c6512").then(next) }, }, { - path: "/pages/2b00a3/index.html", - redirect: "/pages/2b00a3/" + path: "/pages/115772/index.html", + redirect: "/pages/115772/" }, { - path: "/01.Java相关/10.基础/130.简述 Java 中 final 关键字的作用.html", - redirect: "/pages/2b00a3/" + path: "/01.Java相关/10.基础/150.Java 缓冲流 buffer 的用途和原理是什么?.html", + redirect: "/pages/115772/" }, { - name: "v-7665e30e", - path: "/pages/38bcaf/", + name: "v-75844e97", + path: "/pages/37dba4/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-7665e30e").then(next) + ensureAsyncComponentsLoaded("Layout", "v-75844e97").then(next) }, }, { - path: "/pages/38bcaf/index.html", - redirect: "/pages/38bcaf/" + path: "/pages/37dba4/index.html", + redirect: "/pages/37dba4/" }, { - path: "/01.Java相关/10.基础/110.什么是双亲委派机制?.html", - redirect: "/pages/38bcaf/" + path: "/01.Java相关/10.基础/160.简述动态代理与静态代理 .html", + redirect: "/pages/37dba4/" }, { - name: "v-6f5c6512", - path: "/pages/115772/", + name: "v-5290507e", + path: "/pages/73113f/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-6f5c6512").then(next) + ensureAsyncComponentsLoaded("Layout", "v-5290507e").then(next) }, }, { - path: "/pages/115772/index.html", - redirect: "/pages/115772/" + path: "/pages/73113f/index.html", + redirect: "/pages/73113f/" }, { - path: "/01.Java相关/10.基础/150.Java 缓冲流 buffer 的用途和原理是什么?.html", - redirect: "/pages/115772/" + path: "/01.Java相关/10.基础/140.String 类能不能被继承?为什么?.html", + redirect: "/pages/73113f/" }, { - name: "v-75844e97", - path: "/pages/37dba4/", + name: "v-620ebadc", + path: "/pages/2b00a3/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-75844e97").then(next) + ensureAsyncComponentsLoaded("Layout", "v-620ebadc").then(next) }, }, { - path: "/pages/37dba4/index.html", - redirect: "/pages/37dba4/" + path: "/pages/2b00a3/index.html", + redirect: "/pages/2b00a3/" }, { - path: "/01.Java相关/10.基础/160.简述动态代理与静态代理 .html", - redirect: "/pages/37dba4/" + path: "/01.Java相关/10.基础/130.简述 Java 中 final 关键字的作用.html", + redirect: "/pages/2b00a3/" }, { name: "v-1218fa85", @@ -376,6 +376,22 @@ export const routes = [ path: "/01.Java相关/10.基础/180.简述 HashSet 实现原理.html", redirect: "/pages/75552c/" }, + { + name: "v-45fc1217", + path: "/pages/0da4b0/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-45fc1217").then(next) + }, + }, + { + path: "/pages/0da4b0/index.html", + redirect: "/pages/0da4b0/" + }, + { + path: "/01.Java相关/20.并发/001.线程池是如何实现的?.html", + redirect: "/pages/0da4b0/" + }, { name: "v-5390e3d4", path: "/pages/3356c6/", @@ -392,6 +408,22 @@ export const routes = [ path: "/01.Java相关/20.并发/020.简述 CAS 原理,什么是 ABA 问题,怎么解决?.html", redirect: "/pages/3356c6/" }, + { + name: "v-35324a50", + path: "/pages/97245f/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-35324a50").then(next) + }, + }, + { + path: "/pages/97245f/index.html", + redirect: "/pages/97245f/" + }, + { + path: "/01.Java相关/20.并发/030.简述 Synchronized,Volatile,可重入锁的不同使用场景及优缺点.html", + redirect: "/pages/97245f/" + }, { name: "v-352f36cc", path: "/pages/d5d161/", @@ -409,20 +441,20 @@ export const routes = [ redirect: "/pages/d5d161/" }, { - name: "v-35324a50", - path: "/pages/97245f/", + name: "v-4927c049", + path: "/pages/9b59d1/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-35324a50").then(next) + ensureAsyncComponentsLoaded("Layout", "v-4927c049").then(next) }, }, { - path: "/pages/97245f/index.html", - redirect: "/pages/97245f/" + path: "/pages/9b59d1/index.html", + redirect: "/pages/9b59d1/" }, { - path: "/01.Java相关/20.并发/030.简述 Synchronized,Volatile,可重入锁的不同使用场景及优缺点.html", - redirect: "/pages/97245f/" + path: "/01.Java相关/20.并发/050.重入锁是如何实现的?.html", + redirect: "/pages/9b59d1/" }, { name: "v-f5bcd180", @@ -441,36 +473,36 @@ export const routes = [ redirect: "/pages/383264/" }, { - name: "v-45fc1217", - path: "/pages/0da4b0/", + name: "v-1065f7be", + path: "/pages/ff0616/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-45fc1217").then(next) + ensureAsyncComponentsLoaded("Layout", "v-1065f7be").then(next) }, }, { - path: "/pages/0da4b0/index.html", - redirect: "/pages/0da4b0/" + path: "/pages/ff0616/index.html", + redirect: "/pages/ff0616/" }, { - path: "/01.Java相关/20.并发/001.线程池是如何实现的?.html", - redirect: "/pages/0da4b0/" + path: "/01.Java相关/20.并发/070.简述 Java 锁升级的机制.html", + redirect: "/pages/ff0616/" }, { - name: "v-4927c049", - path: "/pages/9b59d1/", + name: "v-2dbdb772", + path: "/pages/52a687/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-4927c049").then(next) + ensureAsyncComponentsLoaded("Layout", "v-2dbdb772").then(next) }, }, { - path: "/pages/9b59d1/index.html", - redirect: "/pages/9b59d1/" + path: "/pages/52a687/index.html", + redirect: "/pages/52a687/" }, { - path: "/01.Java相关/20.并发/050.重入锁是如何实现的?.html", - redirect: "/pages/9b59d1/" + path: "/01.Java相关/20.并发/080.简述 Java AQS 的原理以及使用场景.html", + redirect: "/pages/52a687/" }, { name: "v-134f28dc", @@ -489,36 +521,20 @@ export const routes = [ redirect: "/pages/fd6a59/" }, { - name: "v-2dbdb772", - path: "/pages/52a687/", + name: "v-625fbeda", + path: "/pages/c1f760/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-2dbdb772").then(next) + ensureAsyncComponentsLoaded("Layout", "v-625fbeda").then(next) }, }, { - path: "/pages/52a687/index.html", - redirect: "/pages/52a687/" - }, - { - path: "/01.Java相关/20.并发/080.简述 Java AQS 的原理以及使用场景.html", - redirect: "/pages/52a687/" - }, - { - name: "v-1065f7be", - path: "/pages/ff0616/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-1065f7be").then(next) - }, - }, - { - path: "/pages/ff0616/index.html", - redirect: "/pages/ff0616/" + path: "/pages/c1f760/index.html", + redirect: "/pages/c1f760/" }, { - path: "/01.Java相关/20.并发/070.简述 Java 锁升级的机制.html", - redirect: "/pages/ff0616/" + path: "/01.Java相关/20.并发/110.Java 是如何实现线程安全的,哪些数据结构是线程安全的?.html", + redirect: "/pages/c1f760/" }, { name: "v-03e420c4", @@ -536,22 +552,6 @@ export const routes = [ path: "/01.Java相关/20.并发/100.Java 的线程有哪些状态,转换关系是怎么样的?.html", redirect: "/pages/52483b/" }, - { - name: "v-625fbeda", - path: "/pages/c1f760/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-625fbeda").then(next) - }, - }, - { - path: "/pages/c1f760/index.html", - redirect: "/pages/c1f760/" - }, - { - path: "/01.Java相关/20.并发/110.Java 是如何实现线程安全的,哪些数据结构是线程安全的?.html", - redirect: "/pages/c1f760/" - }, { name: "v-7b2a2ed4", path: "/pages/ce292c/", @@ -601,36 +601,36 @@ export const routes = [ redirect: "/pages/a6c84e/" }, { - name: "v-083aef82", - path: "/pages/2b8c6e/", + name: "v-f406b4e6", + path: "/pages/b3e720/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-083aef82").then(next) + ensureAsyncComponentsLoaded("Layout", "v-f406b4e6").then(next) }, }, { - path: "/pages/2b8c6e/index.html", - redirect: "/pages/2b8c6e/" + path: "/pages/b3e720/index.html", + redirect: "/pages/b3e720/" }, { - path: "/01.Java相关/20.并发/140.手写生产者消费者模型.html", - redirect: "/pages/2b8c6e/" + path: "/01.Java相关/20.并发/150.ThreadLocal 实现原理是什么?为什么要使用弱引用?.html", + redirect: "/pages/b3e720/" }, { - name: "v-f406b4e6", - path: "/pages/b3e720/", + name: "v-083aef82", + path: "/pages/2b8c6e/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-f406b4e6").then(next) + ensureAsyncComponentsLoaded("Layout", "v-083aef82").then(next) }, }, { - path: "/pages/b3e720/index.html", - redirect: "/pages/b3e720/" + path: "/pages/2b8c6e/index.html", + redirect: "/pages/2b8c6e/" }, { - path: "/01.Java相关/20.并发/150.ThreadLocal 实现原理是什么?为什么要使用弱引用?.html", - redirect: "/pages/b3e720/" + path: "/01.Java相关/20.并发/140.手写生产者消费者模型.html", + redirect: "/pages/2b8c6e/" }, { name: "v-7cd97c82", @@ -648,6 +648,22 @@ export const routes = [ path: "/01.Java相关/30.JVM/001.Java 中垃圾回收机制中如何判断对象需要回收?.html", redirect: "/pages/948850/" }, + { + name: "v-5474bec0", + path: "/pages/07a030/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-5474bec0").then(next) + }, + }, + { + path: "/pages/07a030/index.html", + redirect: "/pages/07a030/" + }, + { + path: "/01.Java相关/30.JVM/020.Java 中如何进行 GC 调优?.html", + redirect: "/pages/07a030/" + }, { name: "v-af821fc8", path: "/pages/9b26e8/", @@ -680,22 +696,6 @@ export const routes = [ path: "/01.Java相关/30.JVM/040.JVM 内存是如何对应到操作系统内存的?.html", redirect: "/pages/da65e7/" }, - { - name: "v-5474bec0", - path: "/pages/07a030/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-5474bec0").then(next) - }, - }, - { - path: "/pages/07a030/index.html", - redirect: "/pages/07a030/" - }, - { - path: "/01.Java相关/30.JVM/020.Java 中如何进行 GC 调优?.html", - redirect: "/pages/07a030/" - }, { name: "v-0970cd54", path: "/pages/c8e6cb/", @@ -713,20 +713,20 @@ export const routes = [ redirect: "/pages/c8e6cb/" }, { - name: "v-ba94c306", - path: "/pages/b37ad7/", + name: "v-37fcb440", + path: "/pages/d730dd/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-ba94c306").then(next) + ensureAsyncComponentsLoaded("Layout", "v-37fcb440").then(next) }, }, { - path: "/pages/b37ad7/index.html", - redirect: "/pages/b37ad7/" + path: "/pages/d730dd/index.html", + redirect: "/pages/d730dd/" }, { - path: "/01.Java相关/40.设计模式/002.实现单例设计模式(懒汉,饿汉).html", - redirect: "/pages/b37ad7/" + path: "/01.Java相关/30.JVM/050.JVM 是怎么去调优的?了解哪些参数和指令?.html", + redirect: "/pages/d730dd/" }, { name: "v-28c1d6d9", @@ -745,36 +745,36 @@ export const routes = [ redirect: "/pages/be69af/" }, { - name: "v-37fcb440", - path: "/pages/d730dd/", + name: "v-15fba8f1", + path: "/pages/3ab5f5/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-37fcb440").then(next) + ensureAsyncComponentsLoaded("Layout", "v-15fba8f1").then(next) }, }, { - path: "/pages/d730dd/index.html", - redirect: "/pages/d730dd/" + path: "/pages/3ab5f5/index.html", + redirect: "/pages/3ab5f5/" }, { - path: "/01.Java相关/30.JVM/050.JVM 是怎么去调优的?了解哪些参数和指令?.html", - redirect: "/pages/d730dd/" + path: "/01.Java相关/30.JVM/060.什么是内存泄漏,怎么确定内存泄漏?.html", + redirect: "/pages/3ab5f5/" }, { - name: "v-0564ba27", - path: "/pages/dd3a6f/", + name: "v-ba94c306", + path: "/pages/b37ad7/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-0564ba27").then(next) + ensureAsyncComponentsLoaded("Layout", "v-ba94c306").then(next) }, }, { - path: "/pages/dd3a6f/index.html", - redirect: "/pages/dd3a6f/" + path: "/pages/b37ad7/index.html", + redirect: "/pages/b37ad7/" }, { - path: "/01.Java相关/40.设计模式/020.代理模式.html", - redirect: "/pages/dd3a6f/" + path: "/01.Java相关/40.设计模式/002.实现单例设计模式(懒汉,饿汉).html", + redirect: "/pages/b37ad7/" }, { name: "v-88b087a6", @@ -793,52 +793,52 @@ export const routes = [ redirect: "/pages/3d768c/" }, { - name: "v-15fba8f1", - path: "/pages/3ab5f5/", + name: "v-2d6ed001", + path: "/pages/f15489/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-15fba8f1").then(next) + ensureAsyncComponentsLoaded("Layout", "v-2d6ed001").then(next) }, }, { - path: "/pages/3ab5f5/index.html", - redirect: "/pages/3ab5f5/" + path: "/pages/f15489/index.html", + redirect: "/pages/f15489/" }, { - path: "/01.Java相关/30.JVM/060.什么是内存泄漏,怎么确定内存泄漏?.html", - redirect: "/pages/3ab5f5/" + path: "/01.Java相关/40.设计模式/004.工厂模式使用场景 能举个例子吗.html", + redirect: "/pages/f15489/" }, { - name: "v-34f8aaa2", - path: "/pages/d3ff35/", + name: "v-22a9eaef", + path: "/pages/47a4e3/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-34f8aaa2").then(next) + ensureAsyncComponentsLoaded("Layout", "v-22a9eaef").then(next) }, }, { - path: "/pages/d3ff35/index.html", - redirect: "/pages/d3ff35/" + path: "/pages/47a4e3/index.html", + redirect: "/pages/47a4e3/" }, { - path: "/01.Java相关/40.设计模式/030.责任链模式.html", - redirect: "/pages/d3ff35/" + path: "/01.Java相关/40.设计模式/005.装饰者模式.html", + redirect: "/pages/47a4e3/" }, { - name: "v-22a9eaef", - path: "/pages/47a4e3/", + name: "v-0564ba27", + path: "/pages/dd3a6f/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-22a9eaef").then(next) + ensureAsyncComponentsLoaded("Layout", "v-0564ba27").then(next) }, }, { - path: "/pages/47a4e3/index.html", - redirect: "/pages/47a4e3/" + path: "/pages/dd3a6f/index.html", + redirect: "/pages/dd3a6f/" }, { - path: "/01.Java相关/40.设计模式/005.装饰者模式.html", - redirect: "/pages/47a4e3/" + path: "/01.Java相关/40.设计模式/020.代理模式.html", + redirect: "/pages/dd3a6f/" }, { name: "v-31509c80", @@ -857,20 +857,36 @@ export const routes = [ redirect: "/pages/827fc4/" }, { - name: "v-2d6ed001", - path: "/pages/f15489/", + name: "v-34f8aaa2", + path: "/pages/d3ff35/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-2d6ed001").then(next) + ensureAsyncComponentsLoaded("Layout", "v-34f8aaa2").then(next) }, }, { - path: "/pages/f15489/index.html", - redirect: "/pages/f15489/" + path: "/pages/d3ff35/index.html", + redirect: "/pages/d3ff35/" }, { - path: "/01.Java相关/40.设计模式/004.工厂模式使用场景 能举个例子吗.html", - redirect: "/pages/f15489/" + path: "/01.Java相关/40.设计模式/030.责任链模式.html", + redirect: "/pages/d3ff35/" + }, + { + name: "v-5b0157cf", + path: "/pages/78c7aa/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-5b0157cf").then(next) + }, + }, + { + path: "/pages/78c7aa/index.html", + redirect: "/pages/78c7aa/" + }, + { + path: "/01.Java相关/40.设计模式/050.策略模式.html", + redirect: "/pages/78c7aa/" }, { name: "v-28deb9da", @@ -889,20 +905,20 @@ export const routes = [ redirect: "/pages/43b656/" }, { - name: "v-2279e49d", - path: "/pages/a3c735/", + name: "v-1e653940", + path: "/pages/e00207/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-2279e49d").then(next) + ensureAsyncComponentsLoaded("Layout", "v-1e653940").then(next) }, }, { - path: "/pages/a3c735/index.html", - redirect: "/pages/a3c735/" + path: "/pages/e00207/index.html", + redirect: "/pages/e00207/" }, { - path: "/01.Java相关/40.设计模式/070.代理模式.html", - redirect: "/pages/a3c735/" + path: "/02.计算机基础/10.计算机网络/01.计算机网络.html", + redirect: "/pages/e00207/" }, { name: "v-64f1031c", @@ -921,36 +937,36 @@ export const routes = [ redirect: "/pages/cbff3c/" }, { - name: "v-5b0157cf", - path: "/pages/78c7aa/", + name: "v-0a1d1fb4", + path: "/pages/f33310/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-5b0157cf").then(next) + ensureAsyncComponentsLoaded("Layout", "v-0a1d1fb4").then(next) }, }, { - path: "/pages/78c7aa/index.html", - redirect: "/pages/78c7aa/" + path: "/pages/f33310/index.html", + redirect: "/pages/f33310/" }, { - path: "/01.Java相关/40.设计模式/050.策略模式.html", - redirect: "/pages/78c7aa/" + path: "/02.计算机基础/10.计算机网络/010.简述常见的 HTTP 状态码的含义.html", + redirect: "/pages/f33310/" }, { - name: "v-dd5f4440", - path: "/pages/c332ae/", + name: "v-6602e3da", + path: "/pages/ab48ba/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-dd5f4440").then(next) + ensureAsyncComponentsLoaded("Layout", "v-6602e3da").then(next) }, }, { - path: "/pages/c332ae/index.html", - redirect: "/pages/c332ae/" + path: "/pages/ab48ba/index.html", + redirect: "/pages/ab48ba/" }, { - path: "/01.Java相关/40.设计模式/080.建造者模式.html", - redirect: "/pages/c332ae/" + path: "/02.计算机基础/10.计算机网络/020.从输入 URL 到展现页面的全过程.html", + redirect: "/pages/ab48ba/" }, { name: "v-6f178cbc", @@ -969,20 +985,20 @@ export const routes = [ redirect: "/pages/e5f2d7/" }, { - name: "v-0a1d1fb4", - path: "/pages/f33310/", + name: "v-49688930", + path: "/pages/a25c3a/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-0a1d1fb4").then(next) + ensureAsyncComponentsLoaded("Layout", "v-49688930").then(next) }, }, { - path: "/pages/f33310/index.html", - redirect: "/pages/f33310/" + path: "/pages/a25c3a/index.html", + redirect: "/pages/a25c3a/" }, { - path: "/02.计算机基础/10.计算机网络/010.简述常见的 HTTP 状态码的含义.html", - redirect: "/pages/f33310/" + path: "/02.计算机基础/30.数据结构/01.数据结构.html", + redirect: "/pages/a25c3a/" }, { name: "v-2e6d42b4", @@ -1001,20 +1017,20 @@ export const routes = [ redirect: "/pages/92288a/" }, { - name: "v-49688930", - path: "/pages/a25c3a/", + name: "v-f0f711c4", + path: "/pages/a5adb0/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-49688930").then(next) + ensureAsyncComponentsLoaded("Layout", "v-f0f711c4").then(next) }, }, { - path: "/pages/a25c3a/index.html", - redirect: "/pages/a25c3a/" + path: "/pages/a5adb0/index.html", + redirect: "/pages/a5adb0/" }, { - path: "/02.计算机基础/30.数据结构/01.数据结构.html", - redirect: "/pages/a25c3a/" + path: "/03.数据库/01.MYSQL相关/006.聚簇索引和非聚簇索引有什么区别?.html", + redirect: "/pages/a5adb0/" }, { name: "v-674454ac", @@ -1033,52 +1049,36 @@ export const routes = [ redirect: "/pages/3a8659/" }, { - name: "v-824b776c", - path: "/pages/17b014/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-824b776c").then(next) - }, - }, - { - path: "/pages/17b014/index.html", - redirect: "/pages/17b014/" - }, - { - path: "/03.数据库/01.MYSQL相关/007.简述 MySQL 常见索引类型,介绍一下覆盖索引.html", - redirect: "/pages/17b014/" - }, - { - name: "v-1e653940", - path: "/pages/e00207/", + name: "v-7e02c4a1", + path: "/pages/9f2936/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-1e653940").then(next) + ensureAsyncComponentsLoaded("Layout", "v-7e02c4a1").then(next) }, }, { - path: "/pages/e00207/index.html", - redirect: "/pages/e00207/" + path: "/pages/9f2936/index.html", + redirect: "/pages/9f2936/" }, { - path: "/02.计算机基础/10.计算机网络/01.计算机网络.html", - redirect: "/pages/e00207/" + path: "/03.数据库/01.MYSQL相关/010.简述Mysql的事务隔离级别.html", + redirect: "/pages/9f2936/" }, { - name: "v-f0f711c4", - path: "/pages/a5adb0/", + name: "v-824b776c", + path: "/pages/17b014/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-f0f711c4").then(next) + ensureAsyncComponentsLoaded("Layout", "v-824b776c").then(next) }, }, { - path: "/pages/a5adb0/index.html", - redirect: "/pages/a5adb0/" + path: "/pages/17b014/index.html", + redirect: "/pages/17b014/" }, { - path: "/03.数据库/01.MYSQL相关/006.聚簇索引和非聚簇索引有什么区别?.html", - redirect: "/pages/a5adb0/" + path: "/03.数据库/01.MYSQL相关/007.简述 MySQL 常见索引类型,介绍一下覆盖索引.html", + redirect: "/pages/17b014/" }, { name: "v-5b0e09c6", @@ -1112,38 +1112,6 @@ export const routes = [ path: "/03.数据库/01.MYSQL相关/030.简述乐观锁以及悲观锁的区别以及使用场景.html", redirect: "/pages/57561d/" }, - { - name: "v-7e02c4a1", - path: "/pages/9f2936/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-7e02c4a1").then(next) - }, - }, - { - path: "/pages/9f2936/index.html", - redirect: "/pages/9f2936/" - }, - { - path: "/03.数据库/01.MYSQL相关/010.简述Mysql的事务隔离级别.html", - redirect: "/pages/9f2936/" - }, - { - name: "v-689e142a", - path: "/pages/36672c/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-689e142a").then(next) - }, - }, - { - path: "/pages/36672c/index.html", - redirect: "/pages/36672c/" - }, - { - path: "/03.数据库/01.MYSQL相关/040.简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?.html", - redirect: "/pages/36672c/" - }, { name: "v-6c681639", path: "/pages/bc5538/", @@ -1192,6 +1160,22 @@ export const routes = [ path: "/03.数据库/01.MYSQL相关/070.产生死锁的必要条件有哪些?如何解决死锁?.html", redirect: "/pages/f4cf50/" }, + { + name: "v-689e142a", + path: "/pages/36672c/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-689e142a").then(next) + }, + }, + { + path: "/pages/36672c/index.html", + redirect: "/pages/36672c/" + }, + { + path: "/03.数据库/01.MYSQL相关/040.简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?.html", + redirect: "/pages/36672c/" + }, { name: "v-505fbe2e", path: "/pages/52773a/", @@ -1225,36 +1209,20 @@ export const routes = [ redirect: "/pages/f2dbbe/" }, { - name: "v-c383b96e", - path: "/pages/1ac30e/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-c383b96e").then(next) - }, - }, - { - path: "/pages/1ac30e/index.html", - redirect: "/pages/1ac30e/" - }, - { - path: "/03.数据库/01.MYSQL相关/090.简述 MySQL 的主从同步机制,如果同步失败会怎么样?.html", - redirect: "/pages/1ac30e/" - }, - { - name: "v-ed0c93b8", - path: "/pages/ac0bb8/", + name: "v-4cef944a", + path: "/pages/a8eb47/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-ed0c93b8").then(next) + ensureAsyncComponentsLoaded("Layout", "v-4cef944a").then(next) }, }, { - path: "/pages/ac0bb8/index.html", - redirect: "/pages/ac0bb8/" + path: "/pages/a8eb47/index.html", + redirect: "/pages/a8eb47/" }, { - path: "/03.数据库/01.MYSQL相关/110.简述数据库中什么情况下进行分库,什么情况下进行分表?.html", - redirect: "/pages/ac0bb8/" + path: "/03.数据库/01.MYSQL相关/100.如何设计数据库压测方案?.html", + redirect: "/pages/a8eb47/" }, { name: "v-d6bc240a", @@ -1273,100 +1241,100 @@ export const routes = [ redirect: "/pages/f62359/" }, { - name: "v-ceed6ba6", - path: "/pages/b4338e/", + name: "v-ed0c93b8", + path: "/pages/ac0bb8/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-ceed6ba6").then(next) + ensureAsyncComponentsLoaded("Layout", "v-ed0c93b8").then(next) }, }, { - path: "/pages/b4338e/index.html", - redirect: "/pages/b4338e/" + path: "/pages/ac0bb8/index.html", + redirect: "/pages/ac0bb8/" }, { - path: "/03.数据库/01.MYSQL相关/140.什么是 SQL 注入攻击?如何防止这类攻击?.html", - redirect: "/pages/b4338e/" + path: "/03.数据库/01.MYSQL相关/110.简述数据库中什么情况下进行分库,什么情况下进行分表?.html", + redirect: "/pages/ac0bb8/" }, { - name: "v-77790024", - path: "/pages/429100/", + name: "v-f3acaa1a", + path: "/pages/7bbbf6/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-77790024").then(next) + ensureAsyncComponentsLoaded("Layout", "v-f3acaa1a").then(next) }, }, { - path: "/pages/429100/index.html", - redirect: "/pages/429100/" + path: "/pages/7bbbf6/index.html", + redirect: "/pages/7bbbf6/" }, { - path: "/03.数据库/01.MYSQL相关/150.MySQL 中 join 与 left join 的区别是什么?.html", - redirect: "/pages/429100/" + path: "/03.数据库/01.MYSQL相关/130.简述 undo log 和 redo log 的作用.html", + redirect: "/pages/7bbbf6/" }, { - name: "v-f3acaa1a", - path: "/pages/7bbbf6/", + name: "v-c383b96e", + path: "/pages/1ac30e/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-f3acaa1a").then(next) + ensureAsyncComponentsLoaded("Layout", "v-c383b96e").then(next) }, }, { - path: "/pages/7bbbf6/index.html", - redirect: "/pages/7bbbf6/" + path: "/pages/1ac30e/index.html", + redirect: "/pages/1ac30e/" }, { - path: "/03.数据库/01.MYSQL相关/130.简述 undo log 和 redo log 的作用.html", - redirect: "/pages/7bbbf6/" + path: "/03.数据库/01.MYSQL相关/090.简述 MySQL 的主从同步机制,如果同步失败会怎么样?.html", + redirect: "/pages/1ac30e/" }, { - name: "v-76f5e79a", - path: "/pages/1656d8/", + name: "v-ceed6ba6", + path: "/pages/b4338e/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-76f5e79a").then(next) + ensureAsyncComponentsLoaded("Layout", "v-ceed6ba6").then(next) }, }, { - path: "/pages/1656d8/index.html", - redirect: "/pages/1656d8/" + path: "/pages/b4338e/index.html", + redirect: "/pages/b4338e/" }, { - path: "/03.数据库/01.MYSQL相关/160.简述 MySQL MVCC 的实现原理 .html", - redirect: "/pages/1656d8/" + path: "/03.数据库/01.MYSQL相关/140.什么是 SQL 注入攻击?如何防止这类攻击?.html", + redirect: "/pages/b4338e/" }, { - name: "v-4cef944a", - path: "/pages/a8eb47/", + name: "v-76f5e79a", + path: "/pages/1656d8/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-4cef944a").then(next) + ensureAsyncComponentsLoaded("Layout", "v-76f5e79a").then(next) }, }, { - path: "/pages/a8eb47/index.html", - redirect: "/pages/a8eb47/" + path: "/pages/1656d8/index.html", + redirect: "/pages/1656d8/" }, { - path: "/03.数据库/01.MYSQL相关/100.如何设计数据库压测方案?.html", - redirect: "/pages/a8eb47/" + path: "/03.数据库/01.MYSQL相关/160.简述 MySQL MVCC 的实现原理 .html", + redirect: "/pages/1656d8/" }, { - name: "v-0a7e6416", - path: "/pages/d97948/", + name: "v-77790024", + path: "/pages/429100/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-0a7e6416").then(next) + ensureAsyncComponentsLoaded("Layout", "v-77790024").then(next) }, }, { - path: "/pages/d97948/index.html", - redirect: "/pages/d97948/" + path: "/pages/429100/index.html", + redirect: "/pages/429100/" }, { - path: "/03.数据库/01.MYSQL相关/170.MySQL 中 varchar 和 char 的区别是什么?.html", - redirect: "/pages/d97948/" + path: "/03.数据库/01.MYSQL相关/150.MySQL 中 join 与 left join 的区别是什么?.html", + redirect: "/pages/429100/" }, { name: "v-6b967a6e", @@ -1385,20 +1353,20 @@ export const routes = [ redirect: "/pages/32696d/" }, { - name: "v-cb742458", - path: "/pages/d66211/", + name: "v-0a7e6416", + path: "/pages/d97948/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-cb742458").then(next) + ensureAsyncComponentsLoaded("Layout", "v-0a7e6416").then(next) }, }, { - path: "/pages/d66211/index.html", - redirect: "/pages/d66211/" + path: "/pages/d97948/index.html", + redirect: "/pages/d97948/" }, { - path: "/03.数据库/02.REDIS相关/002.为什么 Redis 在单线程下能如此快?.html", - redirect: "/pages/d66211/" + path: "/03.数据库/01.MYSQL相关/170.MySQL 中 varchar 和 char 的区别是什么?.html", + redirect: "/pages/d97948/" }, { name: "v-d804fba0", @@ -1417,20 +1385,20 @@ export const routes = [ redirect: "/pages/d70c1f/" }, { - name: "v-c0bc7b7a", - path: "/pages/9326cc/", + name: "v-cb742458", + path: "/pages/d66211/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-c0bc7b7a").then(next) + ensureAsyncComponentsLoaded("Layout", "v-cb742458").then(next) }, }, { - path: "/pages/9326cc/index.html", - redirect: "/pages/9326cc/" + path: "/pages/d66211/index.html", + redirect: "/pages/d66211/" }, { - path: "/03.数据库/02.REDIS相关/010.Redis的缓存淘汰策略有哪些?.html", - redirect: "/pages/9326cc/" + path: "/03.数据库/02.REDIS相关/002.为什么 Redis 在单线程下能如此快?.html", + redirect: "/pages/d66211/" }, { name: "v-3e917471", @@ -1449,20 +1417,36 @@ export const routes = [ redirect: "/pages/34aec2/" }, { - name: "v-2c6b511c", - path: "/pages/656f46/", + name: "v-c0bc7b7a", + path: "/pages/9326cc/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-2c6b511c").then(next) + ensureAsyncComponentsLoaded("Layout", "v-c0bc7b7a").then(next) }, }, { - path: "/pages/656f46/index.html", - redirect: "/pages/656f46/" + path: "/pages/9326cc/index.html", + redirect: "/pages/9326cc/" }, { - path: "/03.数据库/02.REDIS相关/030.Redis 如何实现分布式锁?.html", - redirect: "/pages/656f46/" + path: "/03.数据库/02.REDIS相关/010.Redis的缓存淘汰策略有哪些?.html", + redirect: "/pages/9326cc/" + }, + { + name: "v-7c214d00", + path: "/pages/7e708d/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-7c214d00").then(next) + }, + }, + { + path: "/pages/7e708d/index.html", + redirect: "/pages/7e708d/" + }, + { + path: "/03.数据库/02.REDIS相关/020.简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点.html", + redirect: "/pages/7e708d/" }, { name: "v-3d2fdd6a", @@ -1481,20 +1465,20 @@ export const routes = [ redirect: "/pages/338e30/" }, { - name: "v-18b427a0", - path: "/pages/a42826/", + name: "v-2c6b511c", + path: "/pages/656f46/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-18b427a0").then(next) + ensureAsyncComponentsLoaded("Layout", "v-2c6b511c").then(next) }, }, { - path: "/pages/a42826/index.html", - redirect: "/pages/a42826/" + path: "/pages/656f46/index.html", + redirect: "/pages/656f46/" }, { - path: "/03.数据库/02.REDIS相关/050.简述 Redis 中跳表的应用以及优缺点.html", - redirect: "/pages/a42826/" + path: "/03.数据库/02.REDIS相关/030.Redis 如何实现分布式锁?.html", + redirect: "/pages/656f46/" }, { name: "v-6a45f68d", @@ -1512,6 +1496,22 @@ export const routes = [ path: "/03.数据库/02.REDIS相关/060.Redis 中,sentinel 和 cluster 的区别和适用场景是什么?.html", redirect: "/pages/e2e910/" }, + { + name: "v-18b427a0", + path: "/pages/a42826/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-18b427a0").then(next) + }, + }, + { + path: "/pages/a42826/index.html", + redirect: "/pages/a42826/" + }, + { + path: "/03.数据库/02.REDIS相关/050.简述 Redis 中跳表的应用以及优缺点.html", + redirect: "/pages/a42826/" + }, { name: "v-c2fe5e1a", path: "/pages/03cd25/", @@ -1529,20 +1529,20 @@ export const routes = [ redirect: "/pages/03cd25/" }, { - name: "v-35718f4e", - path: "/pages/9c28c9/", + name: "v-57957421", + path: "/pages/cc8507/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-35718f4e").then(next) + ensureAsyncComponentsLoaded("Layout", "v-57957421").then(next) }, }, { - path: "/pages/9c28c9/index.html", - redirect: "/pages/9c28c9/" + path: "/pages/cc8507/index.html", + redirect: "/pages/cc8507/" }, { - path: "/03.数据库/02.REDIS相关/080.简述 Redis 的线程模型以及底层架构设计.html", - redirect: "/pages/9c28c9/" + path: "/03.数据库/02.REDIS相关/100.简述 Redis 的哨兵机制.html", + redirect: "/pages/cc8507/" }, { name: "v-37f158ba", @@ -1561,52 +1561,36 @@ export const routes = [ redirect: "/pages/795c09/" }, { - name: "v-57957421", - path: "/pages/cc8507/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-57957421").then(next) - }, - }, - { - path: "/pages/cc8507/index.html", - redirect: "/pages/cc8507/" - }, - { - path: "/03.数据库/02.REDIS相关/100.简述 Redis 的哨兵机制.html", - redirect: "/pages/cc8507/" - }, - { - name: "v-7c214d00", - path: "/pages/7e708d/", + name: "v-35718f4e", + path: "/pages/9c28c9/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-7c214d00").then(next) + ensureAsyncComponentsLoaded("Layout", "v-35718f4e").then(next) }, }, { - path: "/pages/7e708d/index.html", - redirect: "/pages/7e708d/" + path: "/pages/9c28c9/index.html", + redirect: "/pages/9c28c9/" }, { - path: "/03.数据库/02.REDIS相关/020.简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点.html", - redirect: "/pages/7e708d/" + path: "/03.数据库/02.REDIS相关/080.简述 Redis 的线程模型以及底层架构设计.html", + redirect: "/pages/9c28c9/" }, { - name: "v-5acdab4d", - path: "/pages/3db019/", + name: "v-02ea33c3", + path: "/pages/0d3c6b/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-5acdab4d").then(next) + ensureAsyncComponentsLoaded("Layout", "v-02ea33c3").then(next) }, }, { - path: "/pages/3db019/index.html", - redirect: "/pages/3db019/" + path: "/pages/0d3c6b/index.html", + redirect: "/pages/0d3c6b/" }, { - path: "/04.中间件/10.Netty/010.TCP粘包拆包问题的解决之道.html", - redirect: "/pages/3db019/" + path: "/03.数据库/02.REDIS相关/120.Redis 序列化有哪些方式?.html", + redirect: "/pages/0d3c6b/" }, { name: "v-be02b3c0", @@ -1625,20 +1609,36 @@ export const routes = [ redirect: "/pages/40f3c9/" }, { - name: "v-02ea33c3", - path: "/pages/0d3c6b/", + name: "v-5acdab4d", + path: "/pages/3db019/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-02ea33c3").then(next) + ensureAsyncComponentsLoaded("Layout", "v-5acdab4d").then(next) }, }, { - path: "/pages/0d3c6b/index.html", - redirect: "/pages/0d3c6b/" + path: "/pages/3db019/index.html", + redirect: "/pages/3db019/" }, { - path: "/03.数据库/02.REDIS相关/120.Redis 序列化有哪些方式?.html", - redirect: "/pages/0d3c6b/" + path: "/04.中间件/10.Netty/010.TCP粘包拆包问题的解决之道.html", + redirect: "/pages/3db019/" + }, + { + name: "v-bbcf6e5a", + path: "/pages/ef17ee/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-bbcf6e5a").then(next) + }, + }, + { + path: "/pages/ef17ee/index.html", + redirect: "/pages/ef17ee/" + }, + { + path: "/04.中间件/10.Netty/020.为什么Netty要自己写一个ByteBuf.html", + redirect: "/pages/ef17ee/" }, { name: "v-073f22cd", @@ -1672,22 +1672,6 @@ export const routes = [ path: "/04.中间件/10.Netty/030.ChannelPipeline和ChannelHandler.html", redirect: "/pages/f851b6/" }, - { - name: "v-6b32ada0", - path: "/pages/bfdcc8/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-6b32ada0").then(next) - }, - }, - { - path: "/pages/bfdcc8/index.html", - redirect: "/pages/bfdcc8/" - }, - { - path: "/04.中间件/10.Netty/050.Java NIO了解吗?.html", - redirect: "/pages/bfdcc8/" - }, { name: "v-62f3fa3e", path: "/pages/e7aa60/", @@ -1705,20 +1689,20 @@ export const routes = [ redirect: "/pages/e7aa60/" }, { - name: "v-bbcf6e5a", - path: "/pages/ef17ee/", + name: "v-6b32ada0", + path: "/pages/bfdcc8/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-bbcf6e5a").then(next) + ensureAsyncComponentsLoaded("Layout", "v-6b32ada0").then(next) }, }, { - path: "/pages/ef17ee/index.html", - redirect: "/pages/ef17ee/" + path: "/pages/bfdcc8/index.html", + redirect: "/pages/bfdcc8/" }, { - path: "/04.中间件/10.Netty/020.为什么Netty要自己写一个ByteBuf.html", - redirect: "/pages/ef17ee/" + path: "/04.中间件/10.Netty/050.Java NIO了解吗?.html", + redirect: "/pages/bfdcc8/" }, { name: "v-7c0136cf", @@ -1784,6 +1768,22 @@ export const routes = [ path: "/04.中间件/40.Kafka/030.集群下的Kafka要考虑哪些事情.html", redirect: "/pages/8d1d04/" }, + { + name: "v-684c4815", + path: "/pages/76a94c/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-684c4815").then(next) + }, + }, + { + path: "/pages/76a94c/index.html", + redirect: "/pages/76a94c/" + }, + { + path: "/04.中间件/40.Kafka/040.如何保证Kafka的可靠性.html", + redirect: "/pages/76a94c/" + }, { name: "v-5675b06b", path: "/pages/20c647/", @@ -1801,52 +1801,52 @@ export const routes = [ redirect: "/pages/20c647/" }, { - name: "v-a9558ec0", - path: "/pages/01b9b2/", + name: "v-f88544f4", + path: "/pages/4eae5b/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-a9558ec0").then(next) + ensureAsyncComponentsLoaded("Layout", "v-f88544f4").then(next) }, }, { - path: "/pages/01b9b2/index.html", - redirect: "/pages/01b9b2/" + path: "/pages/4eae5b/index.html", + redirect: "/pages/4eae5b/" }, { - path: "/08.系统设计/001.系统设计.html", - redirect: "/pages/01b9b2/" + path: "/08.系统设计/020.简述什么是两阶段提交?.html", + redirect: "/pages/4eae5b/" }, { - name: "v-684c4815", - path: "/pages/76a94c/", + name: "v-2a52a762", + path: "/pages/5e507a/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-684c4815").then(next) + ensureAsyncComponentsLoaded("Layout", "v-2a52a762").then(next) }, }, { - path: "/pages/76a94c/index.html", - redirect: "/pages/76a94c/" + path: "/pages/5e507a/index.html", + redirect: "/pages/5e507a/" }, { - path: "/04.中间件/40.Kafka/040.如何保证Kafka的可靠性.html", - redirect: "/pages/76a94c/" + path: "/10.非技术/01.非技术问题.html", + redirect: "/pages/5e507a/" }, { - name: "v-f88544f4", - path: "/pages/4eae5b/", + name: "v-a9558ec0", + path: "/pages/01b9b2/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-f88544f4").then(next) + ensureAsyncComponentsLoaded("Layout", "v-a9558ec0").then(next) }, }, { - path: "/pages/4eae5b/index.html", - redirect: "/pages/4eae5b/" + path: "/pages/01b9b2/index.html", + redirect: "/pages/01b9b2/" }, { - path: "/08.系统设计/020.简述什么是两阶段提交?.html", - redirect: "/pages/4eae5b/" + path: "/08.系统设计/001.系统设计.html", + redirect: "/pages/01b9b2/" }, { name: "v-918dfaf8", @@ -1865,20 +1865,20 @@ export const routes = [ redirect: "/about/" }, { - name: "v-2a52a762", - path: "/pages/5e507a/", + name: "v-7ab50b9f", + path: "/pages/beb6c0bd8a66cea6/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-2a52a762").then(next) + ensureAsyncComponentsLoaded("Layout", "v-7ab50b9f").then(next) }, }, { - path: "/pages/5e507a/index.html", - redirect: "/pages/5e507a/" + path: "/pages/beb6c0bd8a66cea6/index.html", + redirect: "/pages/beb6c0bd8a66cea6/" }, { - path: "/10.非技术/01.非技术问题.html", - redirect: "/pages/5e507a/" + path: "/@pages/20.收藏夹/01.网站.html", + redirect: "/pages/beb6c0bd8a66cea6/" }, { name: "v-09c150e6", @@ -1913,52 +1913,52 @@ export const routes = [ redirect: "/archives/" }, { - name: "v-7ab50b9f", - path: "/pages/beb6c0bd8a66cea6/", + name: "v-5f8dfe80", + path: "/categories/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-7ab50b9f").then(next) + ensureAsyncComponentsLoaded("Layout", "v-5f8dfe80").then(next) }, }, { - path: "/pages/beb6c0bd8a66cea6/index.html", - redirect: "/pages/beb6c0bd8a66cea6/" + path: "/categories/index.html", + redirect: "/categories/" }, { - path: "/@pages/20.收藏夹/01.网站.html", - redirect: "/pages/beb6c0bd8a66cea6/" + path: "/@pages/categoriesPage.html", + redirect: "/categories/" }, { - name: "v-2db11cb2", - path: "/pages/f2e63f/", + name: "v-64f9b920", + path: "/tags/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-2db11cb2").then(next) + ensureAsyncComponentsLoaded("Layout", "v-64f9b920").then(next) }, }, { - path: "/pages/f2e63f/index.html", - redirect: "/pages/f2e63f/" + path: "/tags/index.html", + redirect: "/tags/" }, { - path: "/_posts/随笔/你知道的越多,不知道的也就越多.html", - redirect: "/pages/f2e63f/" + path: "/@pages/tagsPage.html", + redirect: "/tags/" }, { - name: "v-a5c5f278", - path: "/pages/829589/", + name: "v-2db11cb2", + path: "/pages/f2e63f/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-a5c5f278").then(next) + ensureAsyncComponentsLoaded("Layout", "v-2db11cb2").then(next) }, }, { - path: "/pages/829589/index.html", - redirect: "/pages/829589/" + path: "/pages/f2e63f/index.html", + redirect: "/pages/f2e63f/" }, { - path: "/_posts/随笔/我做了一个手写春联小网页,祝大家虎年暴富.html", - redirect: "/pages/829589/" + path: "/_posts/随笔/你知道的越多,不知道的也就越多.html", + redirect: "/pages/f2e63f/" }, { name: "v-7d77d69c", @@ -1977,64 +1977,32 @@ export const routes = [ redirect: "/pages/cd8bde/" }, { - name: "v-2cf94a0e", - path: "/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-2cf94a0e").then(next) - }, - }, - { - path: "/index.html", - redirect: "/" - }, - { - name: "v-64f9b920", - path: "/tags/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-64f9b920").then(next) - }, - }, - { - path: "/tags/index.html", - redirect: "/tags/" - }, - { - path: "/@pages/tagsPage.html", - redirect: "/tags/" - }, - { - name: "v-5f8dfe80", - path: "/categories/", + name: "v-a5c5f278", + path: "/pages/829589/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-5f8dfe80").then(next) + ensureAsyncComponentsLoaded("Layout", "v-a5c5f278").then(next) }, }, { - path: "/categories/index.html", - redirect: "/categories/" + path: "/pages/829589/index.html", + redirect: "/pages/829589/" }, { - path: "/@pages/categoriesPage.html", - redirect: "/categories/" + path: "/_posts/随笔/我做了一个手写春联小网页,祝大家虎年暴富.html", + redirect: "/pages/829589/" }, { - name: "v-6602e3da", - path: "/pages/ab48ba/", + name: "v-2cf94a0e", + path: "/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-6602e3da").then(next) + ensureAsyncComponentsLoaded("Layout", "v-2cf94a0e").then(next) }, }, { - path: "/pages/ab48ba/index.html", - redirect: "/pages/ab48ba/" - }, - { - path: "/02.计算机基础/10.计算机网络/020.从输入 URL 到展现页面的全过程.html", - redirect: "/pages/ab48ba/" + path: "/index.html", + redirect: "/" }, { path: '*', diff --git a/node_modules/@vuepress/core/.temp/internal/siteData.js b/node_modules/@vuepress/core/.temp/internal/siteData.js index 147418574..c025eed44 100644 --- a/node_modules/@vuepress/core/.temp/internal/siteData.js +++ b/node_modules/@vuepress/core/.temp/internal/siteData.js @@ -96,6 +96,36 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, + { + "title": "中间件", + "frontmatter": { + "pageComponent": { + "name": "Catalogue", + "data": { + "key": "04.中间件", + "imgUrl": "/img/more.png", + "description": "Dubbo、Mybatis" + } + }, + "title": "中间件", + "date": "2020-03-11T21:50:56.000Z", + "permalink": "/middleware", + "sidebar": false, + "article": false, + "comment": false, + "editLink": false + }, + "regularPath": "/00.%E7%9B%AE%E5%BD%95%E9%A1%B5/04.%E4%B8%AD%E9%97%B4%E4%BB%B6.html", + "relativePath": "00.目录页/04.中间件.md", + "key": "v-7abe58c4", + "path": "/middleware/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, { "title": "计算机基础", "frontmatter": { @@ -126,6 +156,42 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, + { + "title": "HashMap 与 ConcurrentHashMap 的实现原理是怎样的?", + "frontmatter": { + "title": "HashMap 与 ConcurrentHashMap 的实现原理是怎样的?", + "date": "2022-04-04T14:21:18.000Z", + "permalink": "/pages/d1c564/", + "categories": [ + "Java相关", + "基础" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/010.HashMap%20%E4%B8%8E%20ConcurrentHashMap%20%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E6%98%AF%E6%80%8E%E6%A0%B7%E7%9A%84%EF%BC%9F.html", + "relativePath": "01.Java相关/10.基础/010.HashMap 与 ConcurrentHashMap 的实现原理是怎样的?.md", + "key": "v-0734374b", + "path": "/pages/d1c564/", + "headers": [ + { + "level": 2, + "title": "HashMap 与 ConcurrentHashMap 的实现原理是怎样的?ConcurrentHashMap 是如何保证线程安全的?", + "slug": "hashmap-与-concurrenthashmap-的实现原理是怎样的-concurrenthashmap-是如何保证线程安全的", + "normalizedTitle": "hashmap 与 concurrenthashmap 的实现原理是怎样的?concurrenthashmap 是如何保证线程安全的?", + "charIndex": 2 + } + ], + "headersStr": "HashMap 与 ConcurrentHashMap 的实现原理是怎样的?ConcurrentHashMap 是如何保证线程安全的?", + "content": "# HashMap 与 ConcurrentHashMap 的实现原理是怎样的?ConcurrentHashMap 是如何保证线程安全的?\n\n HASHMAP CONCURRENTHASHMAP\n是否线程安全 不安全 安全\nJDK1.7-数据结构 数组+链表 数组+链表+Segment\nJDK1.7-数据结构-图示 \n锁粒度 --- 对segment加锁,最大并发修改线程数=segment数量\n--- --- ---\nJDK1.8-数据结构 数组+链表+红黑树 数组+链表+红黑树\nJDK1.8-数据结构-图示 \n锁粒度 --- HashEntry(首节点),最大并发修改线程数=数组.size\n\n> Q1:ConcurrentHashMap 中变量使用 final 和 volatile 修饰有什么用呢?\n> A:Final域使得确保初始化安全性(initialization safety)成为可能,初始化安全性让不可变形对象不需要同步就能自由地被访问和共享。\n> Node的元素val和指针next是用 volatile 修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。\n\n> Q2:ConcurrentHashMap在JDK 7和8之间的区别\n> A:去掉了Segment这种数据结构\n> 增加了红黑树结构来优化链表,解决了长链表遍历慢的问题", + "normalizedContent": "# hashmap 与 concurrenthashmap 的实现原理是怎样的?concurrenthashmap 是如何保证线程安全的?\n\n hashmap concurrenthashmap\n是否线程安全 不安全 安全\njdk1.7-数据结构 数组+链表 数组+链表+segment\njdk1.7-数据结构-图示 \n锁粒度 --- 对segment加锁,最大并发修改线程数=segment数量\n--- --- ---\njdk1.8-数据结构 数组+链表+红黑树 数组+链表+红黑树\njdk1.8-数据结构-图示 \n锁粒度 --- hashentry(首节点),最大并发修改线程数=数组.size\n\n> q1:concurrenthashmap 中变量使用 final 和 volatile 修饰有什么用呢?\n> a:final域使得确保初始化安全性(initialization safety)成为可能,初始化安全性让不可变形对象不需要同步就能自由地被访问和共享。\n> node的元素val和指针next是用 volatile 修饰的,在多线程环境下线程a修改结点的val或者新增节点的时候是对线程b可见的。\n\n> q2:concurrenthashmap在jdk 7和8之间的区别\n> a:去掉了segment这种数据结构\n> 增加了红黑树结构来优化链表,解决了长链表遍历慢的问题", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, { "title": "Java 异常有哪些类型?", "frontmatter": { @@ -162,36 +228,6 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, - { - "title": "中间件", - "frontmatter": { - "pageComponent": { - "name": "Catalogue", - "data": { - "key": "04.中间件", - "imgUrl": "/img/more.png", - "description": "Dubbo、Mybatis" - } - }, - "title": "中间件", - "date": "2020-03-11T21:50:56.000Z", - "permalink": "/middleware", - "sidebar": false, - "article": false, - "comment": false, - "editLink": false - }, - "regularPath": "/00.%E7%9B%AE%E5%BD%95%E9%A1%B5/04.%E4%B8%AD%E9%97%B4%E4%BB%B6.html", - "relativePath": "00.目录页/04.中间件.md", - "key": "v-7abe58c4", - "path": "/middleware/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, { "title": "Java 中接口和抽象类的区别", "frontmatter": { @@ -264,6 +300,42 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, + { + "title": "简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度", + "frontmatter": { + "title": "简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度", + "date": "2022-04-04T14:24:34.000Z", + "permalink": "/pages/305919/", + "categories": [ + "Java相关", + "基础" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/060.%E7%AE%80%E8%BF%B0%20ArrayList%20%E4%B8%8E%20LinkedList%20%E7%9A%84%E5%BA%95%E5%B1%82%E5%AE%9E%E7%8E%B0%E4%BB%A5%E5%8F%8A%E5%B8%B8%E8%A7%81%E6%93%8D%E4%BD%9C%E7%9A%84%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6.html", + "relativePath": "01.Java相关/10.基础/060.简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度.md", + "key": "v-57b2551a", + "path": "/pages/305919/", + "headers": [ + { + "level": 2, + "title": "简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度", + "slug": "简述-arraylist-与-linkedlist-的底层实现以及常见操作的时间复杂度", + "normalizedTitle": "简述 arraylist 与 linkedlist 的底层实现以及常见操作的时间复杂度", + "charIndex": 2 + } + ], + "headersStr": "简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度", + "content": "# 简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度\n\n ARRAYLIST LINKEDLIST\n线程安全 不安全 不安全\n底层结构 Object 数组 双向链表\n适用场景 按照数组下表快速访问元素 时间复杂度就是 O(1) 顺序访问元素\n不适用场景 在指定位置n插入、删除, 会导致位置n后面的元素移动 时间复杂度就是 O(n-1) 随机访问元素", + "normalizedContent": "# 简述 arraylist 与 linkedlist 的底层实现以及常见操作的时间复杂度\n\n arraylist linkedlist\n线程安全 不安全 不安全\n底层结构 object 数组 双向链表\n适用场景 按照数组下表快速访问元素 时间复杂度就是 o(1) 顺序访问元素\n不适用场景 在指定位置n插入、删除, 会导致位置n后面的元素移动 时间复杂度就是 o(n-1) 随机访问元素", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, { "title": "== 和 equals() 的区别?", "frontmatter": { @@ -336,42 +408,6 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, - { - "title": "简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度", - "frontmatter": { - "title": "简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度", - "date": "2022-04-04T14:24:34.000Z", - "permalink": "/pages/305919/", - "categories": [ - "Java相关", - "基础" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/060.%E7%AE%80%E8%BF%B0%20ArrayList%20%E4%B8%8E%20LinkedList%20%E7%9A%84%E5%BA%95%E5%B1%82%E5%AE%9E%E7%8E%B0%E4%BB%A5%E5%8F%8A%E5%B8%B8%E8%A7%81%E6%93%8D%E4%BD%9C%E7%9A%84%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6.html", - "relativePath": "01.Java相关/10.基础/060.简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度.md", - "key": "v-57b2551a", - "path": "/pages/305919/", - "headers": [ - { - "level": 2, - "title": "简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度", - "slug": "简述-arraylist-与-linkedlist-的底层实现以及常见操作的时间复杂度", - "normalizedTitle": "简述 arraylist 与 linkedlist 的底层实现以及常见操作的时间复杂度", - "charIndex": 2 - } - ], - "headersStr": "简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度", - "content": "# 简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度\n\n ARRAYLIST LINKEDLIST\n线程安全 不安全 不安全\n底层结构 Object 数组 双向链表\n适用场景 按照数组下表快速访问元素 时间复杂度就是 O(1) 顺序访问元素\n不适用场景 在指定位置n插入、删除, 会导致位置n后面的元素移动 时间复杂度就是 O(n-1) 随机访问元素", - "normalizedContent": "# 简述 arraylist 与 linkedlist 的底层实现以及常见操作的时间复杂度\n\n arraylist linkedlist\n线程安全 不安全 不安全\n底层结构 object 数组 双向链表\n适用场景 按照数组下表快速访问元素 时间复杂度就是 o(1) 顺序访问元素\n不适用场景 在指定位置n插入、删除, 会导致位置n后面的元素移动 时间复杂度就是 o(n-1) 随机访问元素", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, { "title": "简述 BIO, NIO, AIO 的区别", "frontmatter": { @@ -408,42 +444,6 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, - { - "title": "HashMap 与 ConcurrentHashMap 的实现原理是怎样的?", - "frontmatter": { - "title": "HashMap 与 ConcurrentHashMap 的实现原理是怎样的?", - "date": "2022-04-04T14:21:18.000Z", - "permalink": "/pages/d1c564/", - "categories": [ - "Java相关", - "基础" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/010.HashMap%20%E4%B8%8E%20ConcurrentHashMap%20%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E6%98%AF%E6%80%8E%E6%A0%B7%E7%9A%84%EF%BC%9F.html", - "relativePath": "01.Java相关/10.基础/010.HashMap 与 ConcurrentHashMap 的实现原理是怎样的?.md", - "key": "v-0734374b", - "path": "/pages/d1c564/", - "headers": [ - { - "level": 2, - "title": "HashMap 与 ConcurrentHashMap 的实现原理是怎样的?ConcurrentHashMap 是如何保证线程安全的?", - "slug": "hashmap-与-concurrenthashmap-的实现原理是怎样的-concurrenthashmap-是如何保证线程安全的", - "normalizedTitle": "hashmap 与 concurrenthashmap 的实现原理是怎样的?concurrenthashmap 是如何保证线程安全的?", - "charIndex": 2 - } - ], - "headersStr": "HashMap 与 ConcurrentHashMap 的实现原理是怎样的?ConcurrentHashMap 是如何保证线程安全的?", - "content": "# HashMap 与 ConcurrentHashMap 的实现原理是怎样的?ConcurrentHashMap 是如何保证线程安全的?\n\n HASHMAP CONCURRENTHASHMAP\n是否线程安全 不安全 安全\nJDK1.7-数据结构 数组+链表 数组+链表+Segment\nJDK1.7-数据结构-图示 \n锁粒度 --- 对segment加锁,最大并发修改线程数=segment数量\n--- --- ---\nJDK1.8-数据结构 数组+链表+红黑树 数组+链表+红黑树\nJDK1.8-数据结构-图示 \n锁粒度 --- HashEntry(首节点),最大并发修改线程数=数组.size\n\n> Q1:ConcurrentHashMap 中变量使用 final 和 volatile 修饰有什么用呢?\n> A:Final域使得确保初始化安全性(initialization safety)成为可能,初始化安全性让不可变形对象不需要同步就能自由地被访问和共享。\n> Node的元素val和指针next是用 volatile 修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。\n\n> Q2:ConcurrentHashMap在JDK 7和8之间的区别\n> A:去掉了Segment这种数据结构\n> 增加了红黑树结构来优化链表,解决了长链表遍历慢的问题", - "normalizedContent": "# hashmap 与 concurrenthashmap 的实现原理是怎样的?concurrenthashmap 是如何保证线程安全的?\n\n hashmap concurrenthashmap\n是否线程安全 不安全 安全\njdk1.7-数据结构 数组+链表 数组+链表+segment\njdk1.7-数据结构-图示 \n锁粒度 --- 对segment加锁,最大并发修改线程数=segment数量\n--- --- ---\njdk1.8-数据结构 数组+链表+红黑树 数组+链表+红黑树\njdk1.8-数据结构-图示 \n锁粒度 --- hashentry(首节点),最大并发修改线程数=数组.size\n\n> q1:concurrenthashmap 中变量使用 final 和 volatile 修饰有什么用呢?\n> a:final域使得确保初始化安全性(initialization safety)成为可能,初始化安全性让不可变形对象不需要同步就能自由地被访问和共享。\n> node的元素val和指针next是用 volatile 修饰的,在多线程环境下线程a修改结点的val或者新增节点的时候是对线程b可见的。\n\n> q2:concurrenthashmap在jdk 7和8之间的区别\n> a:去掉了segment这种数据结构\n> 增加了红黑树结构来优化链表,解决了长链表遍历慢的问题", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, { "title": "在一个静态方法内调用一个非静态成员为什么是非法的?", "frontmatter": { @@ -552,6 +552,42 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, + { + "title": "什么是双亲委派机制?", + "frontmatter": { + "title": "什么是双亲委派机制?", + "date": "2022-04-04T14:26:57.000Z", + "permalink": "/pages/38bcaf/", + "categories": [ + "Java相关", + "基础" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/110.%E4%BB%80%E4%B9%88%E6%98%AF%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE%E6%9C%BA%E5%88%B6%EF%BC%9F.html", + "relativePath": "01.Java相关/10.基础/110.什么是双亲委派机制?.md", + "key": "v-7665e30e", + "path": "/pages/38bcaf/", + "headers": [ + { + "level": 2, + "title": "什么是双亲委派机制?", + "slug": "什么是双亲委派机制", + "normalizedTitle": "什么是双亲委派机制?", + "charIndex": 2 + } + ], + "headersStr": "什么是双亲委派机制?", + "content": "# 什么是双亲委派机制?\n\n双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。\n\n\n\n加载器 加载哪些类\nbootstrap classloader 加载jre/lib/rt.jar\nextension classloader jre/lib/ext/*.jar\napplication classloader 加载应用程序目录\n自定义ClassLoader 定制化加载\n\n再来读一下java.lang.ClassLoader这段代码 是不是通透了许多?\n\n public Class loadClass(String name) throws ClassNotFoundException {\n return loadClass(name, false);\n }\n // -----??-----\n protected Class loadClass(String name, boolean resolve)\n throws ClassNotFoundException\n {\n // 首先,检查是否已经被类加载器加载过\n Class c = findLoadedClass(name);\n if (c == null) {\n try {\n // 存在父加载器,递归的交由父加载器\n if (parent != null) {\n c = parent.loadClass(name, false);\n } else {\n // 直到最上面的Bootstrap类加载器\n c = findBootstrapClassOrNull(name);\n }\n } catch (ClassNotFoundException e) {\n // ClassNotFoundException thrown if class not found\n // from the non-null parent class loader\n }\n \n if (c == null) {\n // If still not found, then invoke findClass in order\n // to find the class.\n c = findClass(name);\n }\n }\n return c;\n }\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n\n\n> Q1:使用双亲委派有什么好处?\n> A:双亲委派机制能保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同。\n> 考虑到安全因素。\n> 假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。\n\n> Q2:什么场景需要破坏双亲委派?\n> 双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载)\n> 基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API 但没有绝对,如果基础类调用会用户的代码怎么办呢? 比如JDBC驱动加载。 这种场景就需要用到SPI机制,是不符合双亲委派的。\n\n> Q3:JDBC、Tomcat中为什么要破坏双亲委派模型\n> A:JDBC\n> 先来回顾一下JDBC的用法\n\nString url = \"jdbc:mysql:///consult?serverTimezone=UTC\";\nString user = \"root\";\nString password = \"root\";\n\nClass.forName(\"com.mysql.jdbc.Driver\");\nConnection connection = DriverManager.getConnection(url, user, password);\n\n\n1\n2\n3\n4\n5\n6\n\n\n> DriverManager 类中要加载各个实现了Driver接口的类,然后进行管理,DriverManager位于 JAVA_HOME中jre/lib/rt.jar 包,由BootStrap类加载器加载.\n> JDBC的Driver接口定义在JDK中,其实现由各个数据库的服务商来提供,比如MySQL驱动包位于服务商MySQL提供的 Jar 中包含Driver的实现,按照双亲委派模型应该由application classloader加载\n> 实际上却是由BootStrap类加载器加载。\n> 这是因为:根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类。\n> 这就破坏了双亲委派模型。\n\nTomcat\n每个webappClassLoader加载自己目录下的class文件\n\n> Tomcat支持部署多个Web应用,多个Web应用可能使用不同版本的JDK、也可能在应用内部定义全限定名一样的类,所以要做到应用隔离。", + "normalizedContent": "# 什么是双亲委派机制?\n\n双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。\n\n\n\n加载器 加载哪些类\nbootstrap classloader 加载jre/lib/rt.jar\nextension classloader jre/lib/ext/*.jar\napplication classloader 加载应用程序目录\n自定义classloader 定制化加载\n\n再来读一下java.lang.classloader这段代码 是不是通透了许多?\n\n public class loadclass(string name) throws classnotfoundexception {\n return loadclass(name, false);\n }\n // -----??-----\n protected class loadclass(string name, boolean resolve)\n throws classnotfoundexception\n {\n // 首先,检查是否已经被类加载器加载过\n class c = findloadedclass(name);\n if (c == null) {\n try {\n // 存在父加载器,递归的交由父加载器\n if (parent != null) {\n c = parent.loadclass(name, false);\n } else {\n // 直到最上面的bootstrap类加载器\n c = findbootstrapclassornull(name);\n }\n } catch (classnotfoundexception e) {\n // classnotfoundexception thrown if class not found\n // from the non-null parent class loader\n }\n \n if (c == null) {\n // if still not found, then invoke findclass in order\n // to find the class.\n c = findclass(name);\n }\n }\n return c;\n }\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n\n\n> q1:使用双亲委派有什么好处?\n> a:双亲委派机制能保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同。\n> 考虑到安全因素。\n> 假设通过网络传递一个名为java.lang.integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心java api发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.integer,而直接返回已加载过的integer.class,这样便可以防止核心api库被随意篡改。\n\n> q2:什么场景需要破坏双亲委派?\n> 双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载)\n> 基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的api 但没有绝对,如果基础类调用会用户的代码怎么办呢? 比如jdbc驱动加载。 这种场景就需要用到spi机制,是不符合双亲委派的。\n\n> q3:jdbc、tomcat中为什么要破坏双亲委派模型\n> a:jdbc\n> 先来回顾一下jdbc的用法\n\nstring url = \"jdbc:mysql:///consult?servertimezone=utc\";\nstring user = \"root\";\nstring password = \"root\";\n\nclass.forname(\"com.mysql.jdbc.driver\");\nconnection connection = drivermanager.getconnection(url, user, password);\n\n\n1\n2\n3\n4\n5\n6\n\n\n> drivermanager 类中要加载各个实现了driver接口的类,然后进行管理,drivermanager位于 java_home中jre/lib/rt.jar 包,由bootstrap类加载器加载.\n> jdbc的driver接口定义在jdk中,其实现由各个数据库的服务商来提供,比如mysql驱动包位于服务商mysql提供的 jar 中包含driver的实现,按照双亲委派模型应该由application classloader加载\n> 实际上却是由bootstrap类加载器加载。\n> 这是因为:根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类。\n> 这就破坏了双亲委派模型。\n\ntomcat\n每个webappclassloader加载自己目录下的class文件\n\n> tomcat支持部署多个web应用,多个web应用可能使用不同版本的jdk、也可能在应用内部定义全限定名一样的类,所以要做到应用隔离。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, { "title": "Java 中 sleep() 与 wait() 的区别", "frontmatter": { @@ -589,11 +625,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "String 类能不能被继承?为什么?", + "title": "Java 缓冲流 buffer 的用途和原理是什么?", "frontmatter": { - "title": "String 类能不能被继承?为什么?", - "date": "2022-04-04T14:28:11.000Z", - "permalink": "/pages/73113f/", + "title": "Java 缓冲流 buffer 的用途和原理是什么?", + "date": "2022-04-04T14:29:13.000Z", + "permalink": "/pages/115772/", "categories": [ "Java相关", "基础" @@ -602,58 +638,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/140.String%20%E7%B1%BB%E8%83%BD%E4%B8%8D%E8%83%BD%E8%A2%AB%E7%BB%A7%E6%89%BF%EF%BC%9F%E4%B8%BA%E4%BB%80%E4%B9%88%EF%BC%9F.html", - "relativePath": "01.Java相关/10.基础/140.String 类能不能被继承?为什么?.md", - "key": "v-5290507e", - "path": "/pages/73113f/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/150.Java%20%E7%BC%93%E5%86%B2%E6%B5%81%20buffer%20%E7%9A%84%E7%94%A8%E9%80%94%E5%92%8C%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", + "relativePath": "01.Java相关/10.基础/150.Java 缓冲流 buffer 的用途和原理是什么?.md", + "key": "v-6f5c6512", + "path": "/pages/115772/", "headers": [ { "level": 2, - "title": "String 类能不能被继承?为什么?", - "slug": "string-类能不能被继承-为什么", - "normalizedTitle": "string 类能不能被继承?为什么?", - "charIndex": 2 - } - ], - "headersStr": "String 类能不能被继承?为什么?", - "content": "# String 类能不能被继承?为什么?\n\n不可以,String 是被final修饰的。\n\n\npublic final class String implements java.io.Serializable, Comparable, CharSequence {\n // 省略... \n}\n\n\n1\n2\n3\n\n\n> Q:String 为什么是 final 类型?\n> A:被final修饰的类,是不可以被继承的,这样做的目的可以保证该类不被修改", - "normalizedContent": "# string 类能不能被继承?为什么?\n\n不可以,string 是被final修饰的。\n\n\npublic final class string implements java.io.serializable, comparable, charsequence {\n // 省略... \n}\n\n\n1\n2\n3\n\n\n> q:string 为什么是 final 类型?\n> a:被final修饰的类,是不可以被继承的,这样做的目的可以保证该类不被修改", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, - { - "title": "简述 Java 中 final 关键字的作用", - "frontmatter": { - "title": "简述 Java 中 final 关键字的作用", - "date": "2022-04-04T14:27:44.000Z", - "permalink": "/pages/2b00a3/", - "categories": [ - "Java相关", - "基础" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/130.%E7%AE%80%E8%BF%B0%20Java%20%E4%B8%AD%20final%20%E5%85%B3%E9%94%AE%E5%AD%97%E7%9A%84%E4%BD%9C%E7%94%A8.html", - "relativePath": "01.Java相关/10.基础/130.简述 Java 中 final 关键字的作用.md", - "key": "v-620ebadc", - "path": "/pages/2b00a3/", - "headers": [ - { - "level": 2, - "title": "简述 Java 中 final 关键字的作用", - "slug": "简述-java-中-final-关键字的作用", - "normalizedTitle": "简述 java 中 final 关键字的作用", + "title": "Java 缓冲流 buffer 的用途和原理是什么?", + "slug": "java-缓冲流-buffer-的用途和原理是什么", + "normalizedTitle": "java 缓冲流 buffer 的用途和原理是什么?", "charIndex": 2 } ], - "headersStr": "简述 Java 中 final 关键字的作用", - "content": "# 简述 Java 中 final 关键字的作用\n\n\nfinal 关键字主要用在三个地方:变量、方法、类。\n\n\n修饰对象 限制 使用场景\n类 被final修饰的类,是不可以被继承的,这样做的目的可以保证该类不被修改 Java的一些核心的API都是final类,例如String、Integer、Math等\n方法 子类不可以重写父类中被final修饰的方法 比如 模板方法,可以用来固定算法\n成员变量 被修饰的成员变量必须在定义时或者构造器中进行初始化赋值,且不可再修改 \n局部变量 final修饰局部变量时只能初始化(赋值)一次,但也可以不初始化 \n方法参数 final修饰方法参数时,是在调用方法传递参数时候初始化的 \n\n> final修饰变量要说明几点:\n> 1.修饰基本类型数据,final会将值变为一个常数(创建后不能被修改);\n> 2.修饰的是引用类型,引用的指向 是不可变,但对象里面的属性可以改变 。这一限制也适用于数组,数组也属于对象,数组本身也是可以修改的。\n> 3.方法参数中的final句柄,意味着在该方法内部,我们不能改变参数句柄指向的实际东西,也就是说在方法内部不能给形参句柄再另外赋值。\n\n//这段代码可以正常编译运行 因为final修饰的是引用类型,引用的指向 是不可变,但对象里面的属性可以改变\npublic class Test {\n public static void main(String[] args) {\n final MyClass myClass = new MyClass();\n System.out.println(++myClass.i); \n \n }\n}\n\nclass MyClass {\n public int i = 0;\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n", - "normalizedContent": "# 简述 java 中 final 关键字的作用\n\n\nfinal 关键字主要用在三个地方:变量、方法、类。\n\n\n修饰对象 限制 使用场景\n类 被final修饰的类,是不可以被继承的,这样做的目的可以保证该类不被修改 java的一些核心的api都是final类,例如string、integer、math等\n方法 子类不可以重写父类中被final修饰的方法 比如 模板方法,可以用来固定算法\n成员变量 被修饰的成员变量必须在定义时或者构造器中进行初始化赋值,且不可再修改 \n局部变量 final修饰局部变量时只能初始化(赋值)一次,但也可以不初始化 \n方法参数 final修饰方法参数时,是在调用方法传递参数时候初始化的 \n\n> final修饰变量要说明几点:\n> 1.修饰基本类型数据,final会将值变为一个常数(创建后不能被修改);\n> 2.修饰的是引用类型,引用的指向 是不可变,但对象里面的属性可以改变 。这一限制也适用于数组,数组也属于对象,数组本身也是可以修改的。\n> 3.方法参数中的final句柄,意味着在该方法内部,我们不能改变参数句柄指向的实际东西,也就是说在方法内部不能给形参句柄再另外赋值。\n\n//这段代码可以正常编译运行 因为final修饰的是引用类型,引用的指向 是不可变,但对象里面的属性可以改变\npublic class test {\n public static void main(string[] args) {\n final myclass myclass = new myclass();\n system.out.println(++myclass.i); \n \n }\n}\n\nclass myclass {\n public int i = 0;\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n", + "headersStr": "Java 缓冲流 buffer 的用途和原理是什么?", + "content": "# Java 缓冲流 buffer 的用途和原理是什么?\n\nbuffer本质是一个字节数组(ByteBuff),同时提供数据的结构化访问以及维护读写位置。\n而运用BufferedInputStream,则可以一次性读n个字节到内存中的缓冲区,这样在内存中读取数据会快很多。 1.流是单向的 所以会有输入流、输出流\n2.字节流一般用于处理文件、视频、音频等,字符流一般用于处理文本数据。\n3.原理:维护一个数组、一个指针\n\n\npublic class BufferedInputStream extends FilterInputStream{\n private static int DEFAULT_BUFFER_SIZE = 8192;\n private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;\n // 缓冲数组\n protected volatile byte buf[];\n // bufUpdater提供buf的compareAndSet方法,这是必要的,因为close()可以是异步的,可以将buf是否为null作为是否close的主要指标。\n private static final AtomicReferenceFieldUpdater bufUpdater =\n AtomicReferenceFieldUpdater.newUpdater(BufferedInputStream.class, byte[].class, \"buf\");\n // buf中的有效字节数\n protected int count;\n // buf中的当前字节位置\n protected int pos;\n \n public BufferedInputStream(InputStream in) {\n this(in, DEFAULT_BUFFER_SIZE);\n }\n\n public BufferedInputStream(InputStream in, int size) {\n super(in);\n if (size <= 0) {\n throw new IllegalArgumentException(\"Buffer size <= 0\");\n }\n buf = new byte[size];\n }\n\n //读方法 -> 数据写入到数组buf中\n public synchronized int read() throws IOException {\n // 当读完buf中的数据后,就需要把InputStream的数据重新填充到buf\n if (pos >= count) {\n fill();\n if (pos >= count)\n return -1;\n }\n // 返回buf的下一个字节 指针后移\n return getBufIfOpen()[pos++] & 0xff;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n", + "normalizedContent": "# java 缓冲流 buffer 的用途和原理是什么?\n\nbuffer本质是一个字节数组(bytebuff),同时提供数据的结构化访问以及维护读写位置。\n而运用bufferedinputstream,则可以一次性读n个字节到内存中的缓冲区,这样在内存中读取数据会快很多。 1.流是单向的 所以会有输入流、输出流\n2.字节流一般用于处理文件、视频、音频等,字符流一般用于处理文本数据。\n3.原理:维护一个数组、一个指针\n\n\npublic class bufferedinputstream extends filterinputstream{\n private static int default_buffer_size = 8192;\n private static int max_buffer_size = integer.max_value - 8;\n // 缓冲数组\n protected volatile byte buf[];\n // bufupdater提供buf的compareandset方法,这是必要的,因为close()可以是异步的,可以将buf是否为null作为是否close的主要指标。\n private static final atomicreferencefieldupdater bufupdater =\n atomicreferencefieldupdater.newupdater(bufferedinputstream.class, byte[].class, \"buf\");\n // buf中的有效字节数\n protected int count;\n // buf中的当前字节位置\n protected int pos;\n \n public bufferedinputstream(inputstream in) {\n this(in, default_buffer_size);\n }\n\n public bufferedinputstream(inputstream in, int size) {\n super(in);\n if (size <= 0) {\n throw new illegalargumentexception(\"buffer size <= 0\");\n }\n buf = new byte[size];\n }\n\n //读方法 -> 数据写入到数组buf中\n public synchronized int read() throws ioexception {\n // 当读完buf中的数据后,就需要把inputstream的数据重新填充到buf\n if (pos >= count) {\n fill();\n if (pos >= count)\n return -1;\n }\n // 返回buf的下一个字节 指针后移\n return getbufifopen()[pos++] & 0xff;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n", "charsets": { "cjk": true }, @@ -661,11 +661,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "什么是双亲委派机制?", + "title": "简述动态代理与静态代理", "frontmatter": { - "title": "什么是双亲委派机制?", - "date": "2022-04-04T14:26:57.000Z", - "permalink": "/pages/38bcaf/", + "title": "简述动态代理与静态代理", + "date": "2022-04-04T14:29:39.000Z", + "permalink": "/pages/37dba4/", "categories": [ "Java相关", "基础" @@ -674,22 +674,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/110.%E4%BB%80%E4%B9%88%E6%98%AF%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE%E6%9C%BA%E5%88%B6%EF%BC%9F.html", - "relativePath": "01.Java相关/10.基础/110.什么是双亲委派机制?.md", - "key": "v-7665e30e", - "path": "/pages/38bcaf/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/160.%E7%AE%80%E8%BF%B0%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E4%B8%8E%E9%9D%99%E6%80%81%E4%BB%A3%E7%90%86%20.html", + "relativePath": "01.Java相关/10.基础/160.简述动态代理与静态代理 .md", + "key": "v-75844e97", + "path": "/pages/37dba4/", "headers": [ { "level": 2, - "title": "什么是双亲委派机制?", - "slug": "什么是双亲委派机制", - "normalizedTitle": "什么是双亲委派机制?", + "title": "简述动态代理与静态代理", + "slug": "简述动态代理与静态代理", + "normalizedTitle": "简述动态代理与静态代理", "charIndex": 2 } ], - "headersStr": "什么是双亲委派机制?", - "content": "# 什么是双亲委派机制?\n\n双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。\n\n\n\n加载器 加载哪些类\nbootstrap classloader 加载jre/lib/rt.jar\nextension classloader jre/lib/ext/*.jar\napplication classloader 加载应用程序目录\n自定义ClassLoader 定制化加载\n\n再来读一下java.lang.ClassLoader这段代码 是不是通透了许多?\n\n public Class loadClass(String name) throws ClassNotFoundException {\n return loadClass(name, false);\n }\n // -----??-----\n protected Class loadClass(String name, boolean resolve)\n throws ClassNotFoundException\n {\n // 首先,检查是否已经被类加载器加载过\n Class c = findLoadedClass(name);\n if (c == null) {\n try {\n // 存在父加载器,递归的交由父加载器\n if (parent != null) {\n c = parent.loadClass(name, false);\n } else {\n // 直到最上面的Bootstrap类加载器\n c = findBootstrapClassOrNull(name);\n }\n } catch (ClassNotFoundException e) {\n // ClassNotFoundException thrown if class not found\n // from the non-null parent class loader\n }\n \n if (c == null) {\n // If still not found, then invoke findClass in order\n // to find the class.\n c = findClass(name);\n }\n }\n return c;\n }\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n\n\n> Q1:使用双亲委派有什么好处?\n> A:双亲委派机制能保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同。\n> 考虑到安全因素。\n> 假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。\n\n> Q2:什么场景需要破坏双亲委派?\n> 双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载)\n> 基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API 但没有绝对,如果基础类调用会用户的代码怎么办呢? 比如JDBC驱动加载。 这种场景就需要用到SPI机制,是不符合双亲委派的。\n\n> Q3:JDBC、Tomcat中为什么要破坏双亲委派模型\n> A:JDBC\n> 先来回顾一下JDBC的用法\n\nString url = \"jdbc:mysql:///consult?serverTimezone=UTC\";\nString user = \"root\";\nString password = \"root\";\n\nClass.forName(\"com.mysql.jdbc.Driver\");\nConnection connection = DriverManager.getConnection(url, user, password);\n\n\n1\n2\n3\n4\n5\n6\n\n\n> DriverManager 类中要加载各个实现了Driver接口的类,然后进行管理,DriverManager位于 JAVA_HOME中jre/lib/rt.jar 包,由BootStrap类加载器加载.\n> JDBC的Driver接口定义在JDK中,其实现由各个数据库的服务商来提供,比如MySQL驱动包位于服务商MySQL提供的 Jar 中包含Driver的实现,按照双亲委派模型应该由application classloader加载\n> 实际上却是由BootStrap类加载器加载。\n> 这是因为:根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类。\n> 这就破坏了双亲委派模型。\n\nTomcat\n每个webappClassLoader加载自己目录下的class文件\n\n> Tomcat支持部署多个Web应用,多个Web应用可能使用不同版本的JDK、也可能在应用内部定义全限定名一样的类,所以要做到应用隔离。", - "normalizedContent": "# 什么是双亲委派机制?\n\n双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。\n\n\n\n加载器 加载哪些类\nbootstrap classloader 加载jre/lib/rt.jar\nextension classloader jre/lib/ext/*.jar\napplication classloader 加载应用程序目录\n自定义classloader 定制化加载\n\n再来读一下java.lang.classloader这段代码 是不是通透了许多?\n\n public class loadclass(string name) throws classnotfoundexception {\n return loadclass(name, false);\n }\n // -----??-----\n protected class loadclass(string name, boolean resolve)\n throws classnotfoundexception\n {\n // 首先,检查是否已经被类加载器加载过\n class c = findloadedclass(name);\n if (c == null) {\n try {\n // 存在父加载器,递归的交由父加载器\n if (parent != null) {\n c = parent.loadclass(name, false);\n } else {\n // 直到最上面的bootstrap类加载器\n c = findbootstrapclassornull(name);\n }\n } catch (classnotfoundexception e) {\n // classnotfoundexception thrown if class not found\n // from the non-null parent class loader\n }\n \n if (c == null) {\n // if still not found, then invoke findclass in order\n // to find the class.\n c = findclass(name);\n }\n }\n return c;\n }\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n\n\n> q1:使用双亲委派有什么好处?\n> a:双亲委派机制能保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同。\n> 考虑到安全因素。\n> 假设通过网络传递一个名为java.lang.integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心java api发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.integer,而直接返回已加载过的integer.class,这样便可以防止核心api库被随意篡改。\n\n> q2:什么场景需要破坏双亲委派?\n> 双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载)\n> 基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的api 但没有绝对,如果基础类调用会用户的代码怎么办呢? 比如jdbc驱动加载。 这种场景就需要用到spi机制,是不符合双亲委派的。\n\n> q3:jdbc、tomcat中为什么要破坏双亲委派模型\n> a:jdbc\n> 先来回顾一下jdbc的用法\n\nstring url = \"jdbc:mysql:///consult?servertimezone=utc\";\nstring user = \"root\";\nstring password = \"root\";\n\nclass.forname(\"com.mysql.jdbc.driver\");\nconnection connection = drivermanager.getconnection(url, user, password);\n\n\n1\n2\n3\n4\n5\n6\n\n\n> drivermanager 类中要加载各个实现了driver接口的类,然后进行管理,drivermanager位于 java_home中jre/lib/rt.jar 包,由bootstrap类加载器加载.\n> jdbc的driver接口定义在jdk中,其实现由各个数据库的服务商来提供,比如mysql驱动包位于服务商mysql提供的 jar 中包含driver的实现,按照双亲委派模型应该由application classloader加载\n> 实际上却是由bootstrap类加载器加载。\n> 这是因为:根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类。\n> 这就破坏了双亲委派模型。\n\ntomcat\n每个webappclassloader加载自己目录下的class文件\n\n> tomcat支持部署多个web应用,多个web应用可能使用不同版本的jdk、也可能在应用内部定义全限定名一样的类,所以要做到应用隔离。", + "headersStr": "简述动态代理与静态代理", + "content": "# 简述动态代理与静态代理\n\n 静态代理 动态代理\n 由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的 .class 文件就已经存在了 在程序运行时运用反射机制动态创建而成,动态就是在程序运行时生成的,而不是编译时。\n应用场景 AspectJ AOP Spring AOP、RPC 框架\n优点 在不修改目标对象的前提下,可以通过代理对象对目标对象功能扩展 不会会产生很多静态类,通过一个代理类完成全部的代理功能\n缺点 1.代理类和委托类实现了相同的接口,代理类通过委托类实现了相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。 \n 2.代理对象只服务于一种类型的对象,如果要服务多类型的对象。势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了。\n\n> 1.AspectJ 的底层技术就是静态代理,用一种 AspectJ 支持的特定语言编写切面,通过一个命令来编译,生成一个新的代理类,该代理类增强了业务类,这是在编译时增强,相对于下面说的运行时增强,编译时增强的性能更好。\n> 2.JDK 动态代理:\n> \n> \n> * 代理对象,不需要实现接口,但是目标对象要实现接口,否则不能用动态代理\n> \n> * 代理对象的生成,是通过 JDK 的 API(反射机制),动态的在内存中构建代理对象,在 Java 中要想实现动态代理机制,需要 java.lang.reflect.InvocationHandler 接口和 java.lang.reflect.Proxy 类的支持\n> 3.cglib 代理:\n> * 静态代理和 JDK 代理模式都要求目标对象实现一个接口,但有时候目标对象只是一个单独的对象,并没有实现任何接口,这个时候就可以使用目标对象子类来实现代理,这就是 cglib 代理。\n> *cglib (opens new window)(Code Generation Library)是一个基于 ASM 的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。cglib 通过继承方式实现代理。它广泛的被许多 AOP 的框架使用,比如我们的 Spring AOP。\n> *cglib 包的底层是通过使用字节码处理框架 ASM 来转换字节码并生成新的类。\n> *cglib 代理也被叫做子类代理,它是在内存中构建一个子类对象从而实现目标对象功能扩展。\n> ", + "normalizedContent": "# 简述动态代理与静态代理\n\n 静态代理 动态代理\n 由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的 .class 文件就已经存在了 在程序运行时运用反射机制动态创建而成,动态就是在程序运行时生成的,而不是编译时。\n应用场景 aspectj aop spring aop、rpc 框架\n优点 在不修改目标对象的前提下,可以通过代理对象对目标对象功能扩展 不会会产生很多静态类,通过一个代理类完成全部的代理功能\n缺点 1.代理类和委托类实现了相同的接口,代理类通过委托类实现了相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。 \n 2.代理对象只服务于一种类型的对象,如果要服务多类型的对象。势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了。\n\n> 1.aspectj 的底层技术就是静态代理,用一种 aspectj 支持的特定语言编写切面,通过一个命令来编译,生成一个新的代理类,该代理类增强了业务类,这是在编译时增强,相对于下面说的运行时增强,编译时增强的性能更好。\n> 2.jdk 动态代理:\n> \n> \n> * 代理对象,不需要实现接口,但是目标对象要实现接口,否则不能用动态代理\n> \n> * 代理对象的生成,是通过 jdk 的 api(反射机制),动态的在内存中构建代理对象,在 java 中要想实现动态代理机制,需要 java.lang.reflect.invocationhandler 接口和 java.lang.reflect.proxy 类的支持\n> 3.cglib 代理:\n> * 静态代理和 jdk 代理模式都要求目标对象实现一个接口,但有时候目标对象只是一个单独的对象,并没有实现任何接口,这个时候就可以使用目标对象子类来实现代理,这就是 cglib 代理。\n> *cglib (opens new window)(code generation library)是一个基于 asm 的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。cglib 通过继承方式实现代理。它广泛的被许多 aop 的框架使用,比如我们的 spring aop。\n> *cglib 包的底层是通过使用字节码处理框架 asm 来转换字节码并生成新的类。\n> *cglib 代理也被叫做子类代理,它是在内存中构建一个子类对象从而实现目标对象功能扩展。\n> ", "charsets": { "cjk": true }, @@ -697,11 +697,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "Java 缓冲流 buffer 的用途和原理是什么?", + "title": "String 类能不能被继承?为什么?", "frontmatter": { - "title": "Java 缓冲流 buffer 的用途和原理是什么?", - "date": "2022-04-04T14:29:13.000Z", - "permalink": "/pages/115772/", + "title": "String 类能不能被继承?为什么?", + "date": "2022-04-04T14:28:11.000Z", + "permalink": "/pages/73113f/", "categories": [ "Java相关", "基础" @@ -710,22 +710,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/150.Java%20%E7%BC%93%E5%86%B2%E6%B5%81%20buffer%20%E7%9A%84%E7%94%A8%E9%80%94%E5%92%8C%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", - "relativePath": "01.Java相关/10.基础/150.Java 缓冲流 buffer 的用途和原理是什么?.md", - "key": "v-6f5c6512", - "path": "/pages/115772/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/140.String%20%E7%B1%BB%E8%83%BD%E4%B8%8D%E8%83%BD%E8%A2%AB%E7%BB%A7%E6%89%BF%EF%BC%9F%E4%B8%BA%E4%BB%80%E4%B9%88%EF%BC%9F.html", + "relativePath": "01.Java相关/10.基础/140.String 类能不能被继承?为什么?.md", + "key": "v-5290507e", + "path": "/pages/73113f/", "headers": [ { "level": 2, - "title": "Java 缓冲流 buffer 的用途和原理是什么?", - "slug": "java-缓冲流-buffer-的用途和原理是什么", - "normalizedTitle": "java 缓冲流 buffer 的用途和原理是什么?", + "title": "String 类能不能被继承?为什么?", + "slug": "string-类能不能被继承-为什么", + "normalizedTitle": "string 类能不能被继承?为什么?", "charIndex": 2 } ], - "headersStr": "Java 缓冲流 buffer 的用途和原理是什么?", - "content": "# Java 缓冲流 buffer 的用途和原理是什么?\n\nbuffer本质是一个字节数组(ByteBuff),同时提供数据的结构化访问以及维护读写位置。\n而运用BufferedInputStream,则可以一次性读n个字节到内存中的缓冲区,这样在内存中读取数据会快很多。 1.流是单向的 所以会有输入流、输出流\n2.字节流一般用于处理文件、视频、音频等,字符流一般用于处理文本数据。\n3.原理:维护一个数组、一个指针\n\n\npublic class BufferedInputStream extends FilterInputStream{\n private static int DEFAULT_BUFFER_SIZE = 8192;\n private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;\n // 缓冲数组\n protected volatile byte buf[];\n // bufUpdater提供buf的compareAndSet方法,这是必要的,因为close()可以是异步的,可以将buf是否为null作为是否close的主要指标。\n private static final AtomicReferenceFieldUpdater bufUpdater =\n AtomicReferenceFieldUpdater.newUpdater(BufferedInputStream.class, byte[].class, \"buf\");\n // buf中的有效字节数\n protected int count;\n // buf中的当前字节位置\n protected int pos;\n \n public BufferedInputStream(InputStream in) {\n this(in, DEFAULT_BUFFER_SIZE);\n }\n\n public BufferedInputStream(InputStream in, int size) {\n super(in);\n if (size <= 0) {\n throw new IllegalArgumentException(\"Buffer size <= 0\");\n }\n buf = new byte[size];\n }\n\n //读方法 -> 数据写入到数组buf中\n public synchronized int read() throws IOException {\n // 当读完buf中的数据后,就需要把InputStream的数据重新填充到buf\n if (pos >= count) {\n fill();\n if (pos >= count)\n return -1;\n }\n // 返回buf的下一个字节 指针后移\n return getBufIfOpen()[pos++] & 0xff;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n", - "normalizedContent": "# java 缓冲流 buffer 的用途和原理是什么?\n\nbuffer本质是一个字节数组(bytebuff),同时提供数据的结构化访问以及维护读写位置。\n而运用bufferedinputstream,则可以一次性读n个字节到内存中的缓冲区,这样在内存中读取数据会快很多。 1.流是单向的 所以会有输入流、输出流\n2.字节流一般用于处理文件、视频、音频等,字符流一般用于处理文本数据。\n3.原理:维护一个数组、一个指针\n\n\npublic class bufferedinputstream extends filterinputstream{\n private static int default_buffer_size = 8192;\n private static int max_buffer_size = integer.max_value - 8;\n // 缓冲数组\n protected volatile byte buf[];\n // bufupdater提供buf的compareandset方法,这是必要的,因为close()可以是异步的,可以将buf是否为null作为是否close的主要指标。\n private static final atomicreferencefieldupdater bufupdater =\n atomicreferencefieldupdater.newupdater(bufferedinputstream.class, byte[].class, \"buf\");\n // buf中的有效字节数\n protected int count;\n // buf中的当前字节位置\n protected int pos;\n \n public bufferedinputstream(inputstream in) {\n this(in, default_buffer_size);\n }\n\n public bufferedinputstream(inputstream in, int size) {\n super(in);\n if (size <= 0) {\n throw new illegalargumentexception(\"buffer size <= 0\");\n }\n buf = new byte[size];\n }\n\n //读方法 -> 数据写入到数组buf中\n public synchronized int read() throws ioexception {\n // 当读完buf中的数据后,就需要把inputstream的数据重新填充到buf\n if (pos >= count) {\n fill();\n if (pos >= count)\n return -1;\n }\n // 返回buf的下一个字节 指针后移\n return getbufifopen()[pos++] & 0xff;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n", + "headersStr": "String 类能不能被继承?为什么?", + "content": "# String 类能不能被继承?为什么?\n\n不可以,String 是被final修饰的。\n\n\npublic final class String implements java.io.Serializable, Comparable, CharSequence {\n // 省略... \n}\n\n\n1\n2\n3\n\n\n> Q:String 为什么是 final 类型?\n> A:被final修饰的类,是不可以被继承的,这样做的目的可以保证该类不被修改", + "normalizedContent": "# string 类能不能被继承?为什么?\n\n不可以,string 是被final修饰的。\n\n\npublic final class string implements java.io.serializable, comparable, charsequence {\n // 省略... \n}\n\n\n1\n2\n3\n\n\n> q:string 为什么是 final 类型?\n> a:被final修饰的类,是不可以被继承的,这样做的目的可以保证该类不被修改", "charsets": { "cjk": true }, @@ -733,11 +733,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "简述动态代理与静态代理", + "title": "简述 Java 中 final 关键字的作用", "frontmatter": { - "title": "简述动态代理与静态代理", - "date": "2022-04-04T14:29:39.000Z", - "permalink": "/pages/37dba4/", + "title": "简述 Java 中 final 关键字的作用", + "date": "2022-04-04T14:27:44.000Z", + "permalink": "/pages/2b00a3/", "categories": [ "Java相关", "基础" @@ -746,22 +746,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/160.%E7%AE%80%E8%BF%B0%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E4%B8%8E%E9%9D%99%E6%80%81%E4%BB%A3%E7%90%86%20.html", - "relativePath": "01.Java相关/10.基础/160.简述动态代理与静态代理 .md", - "key": "v-75844e97", - "path": "/pages/37dba4/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/130.%E7%AE%80%E8%BF%B0%20Java%20%E4%B8%AD%20final%20%E5%85%B3%E9%94%AE%E5%AD%97%E7%9A%84%E4%BD%9C%E7%94%A8.html", + "relativePath": "01.Java相关/10.基础/130.简述 Java 中 final 关键字的作用.md", + "key": "v-620ebadc", + "path": "/pages/2b00a3/", "headers": [ { "level": 2, - "title": "简述动态代理与静态代理", - "slug": "简述动态代理与静态代理", - "normalizedTitle": "简述动态代理与静态代理", + "title": "简述 Java 中 final 关键字的作用", + "slug": "简述-java-中-final-关键字的作用", + "normalizedTitle": "简述 java 中 final 关键字的作用", "charIndex": 2 } ], - "headersStr": "简述动态代理与静态代理", - "content": "# 简述动态代理与静态代理\n\n 静态代理 动态代理\n 由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的 .class 文件就已经存在了 在程序运行时运用反射机制动态创建而成,动态就是在程序运行时生成的,而不是编译时。\n应用场景 AspectJ AOP Spring AOP、RPC 框架\n优点 在不修改目标对象的前提下,可以通过代理对象对目标对象功能扩展 不会会产生很多静态类,通过一个代理类完成全部的代理功能\n缺点 1.代理类和委托类实现了相同的接口,代理类通过委托类实现了相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。 \n 2.代理对象只服务于一种类型的对象,如果要服务多类型的对象。势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了。\n\n> 1.AspectJ 的底层技术就是静态代理,用一种 AspectJ 支持的特定语言编写切面,通过一个命令来编译,生成一个新的代理类,该代理类增强了业务类,这是在编译时增强,相对于下面说的运行时增强,编译时增强的性能更好。\n> 2.JDK 动态代理:\n> \n> \n> * 代理对象,不需要实现接口,但是目标对象要实现接口,否则不能用动态代理\n> \n> * 代理对象的生成,是通过 JDK 的 API(反射机制),动态的在内存中构建代理对象,在 Java 中要想实现动态代理机制,需要 java.lang.reflect.InvocationHandler 接口和 java.lang.reflect.Proxy 类的支持\n> 3.cglib 代理:\n> * 静态代理和 JDK 代理模式都要求目标对象实现一个接口,但有时候目标对象只是一个单独的对象,并没有实现任何接口,这个时候就可以使用目标对象子类来实现代理,这就是 cglib 代理。\n> *cglib (opens new window)(Code Generation Library)是一个基于 ASM 的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。cglib 通过继承方式实现代理。它广泛的被许多 AOP 的框架使用,比如我们的 Spring AOP。\n> *cglib 包的底层是通过使用字节码处理框架 ASM 来转换字节码并生成新的类。\n> *cglib 代理也被叫做子类代理,它是在内存中构建一个子类对象从而实现目标对象功能扩展。\n> ", - "normalizedContent": "# 简述动态代理与静态代理\n\n 静态代理 动态代理\n 由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的 .class 文件就已经存在了 在程序运行时运用反射机制动态创建而成,动态就是在程序运行时生成的,而不是编译时。\n应用场景 aspectj aop spring aop、rpc 框架\n优点 在不修改目标对象的前提下,可以通过代理对象对目标对象功能扩展 不会会产生很多静态类,通过一个代理类完成全部的代理功能\n缺点 1.代理类和委托类实现了相同的接口,代理类通过委托类实现了相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。 \n 2.代理对象只服务于一种类型的对象,如果要服务多类型的对象。势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了。\n\n> 1.aspectj 的底层技术就是静态代理,用一种 aspectj 支持的特定语言编写切面,通过一个命令来编译,生成一个新的代理类,该代理类增强了业务类,这是在编译时增强,相对于下面说的运行时增强,编译时增强的性能更好。\n> 2.jdk 动态代理:\n> \n> \n> * 代理对象,不需要实现接口,但是目标对象要实现接口,否则不能用动态代理\n> \n> * 代理对象的生成,是通过 jdk 的 api(反射机制),动态的在内存中构建代理对象,在 java 中要想实现动态代理机制,需要 java.lang.reflect.invocationhandler 接口和 java.lang.reflect.proxy 类的支持\n> 3.cglib 代理:\n> * 静态代理和 jdk 代理模式都要求目标对象实现一个接口,但有时候目标对象只是一个单独的对象,并没有实现任何接口,这个时候就可以使用目标对象子类来实现代理,这就是 cglib 代理。\n> *cglib (opens new window)(code generation library)是一个基于 asm 的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。cglib 通过继承方式实现代理。它广泛的被许多 aop 的框架使用,比如我们的 spring aop。\n> *cglib 包的底层是通过使用字节码处理框架 asm 来转换字节码并生成新的类。\n> *cglib 代理也被叫做子类代理,它是在内存中构建一个子类对象从而实现目标对象功能扩展。\n> ", + "headersStr": "简述 Java 中 final 关键字的作用", + "content": "# 简述 Java 中 final 关键字的作用\n\n\nfinal 关键字主要用在三个地方:变量、方法、类。\n\n\n修饰对象 限制 使用场景\n类 被final修饰的类,是不可以被继承的,这样做的目的可以保证该类不被修改 Java的一些核心的API都是final类,例如String、Integer、Math等\n方法 子类不可以重写父类中被final修饰的方法 比如 模板方法,可以用来固定算法\n成员变量 被修饰的成员变量必须在定义时或者构造器中进行初始化赋值,且不可再修改 \n局部变量 final修饰局部变量时只能初始化(赋值)一次,但也可以不初始化 \n方法参数 final修饰方法参数时,是在调用方法传递参数时候初始化的 \n\n> final修饰变量要说明几点:\n> 1.修饰基本类型数据,final会将值变为一个常数(创建后不能被修改);\n> 2.修饰的是引用类型,引用的指向 是不可变,但对象里面的属性可以改变 。这一限制也适用于数组,数组也属于对象,数组本身也是可以修改的。\n> 3.方法参数中的final句柄,意味着在该方法内部,我们不能改变参数句柄指向的实际东西,也就是说在方法内部不能给形参句柄再另外赋值。\n\n//这段代码可以正常编译运行 因为final修饰的是引用类型,引用的指向 是不可变,但对象里面的属性可以改变\npublic class Test {\n public static void main(String[] args) {\n final MyClass myClass = new MyClass();\n System.out.println(++myClass.i); \n \n }\n}\n\nclass MyClass {\n public int i = 0;\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n", + "normalizedContent": "# 简述 java 中 final 关键字的作用\n\n\nfinal 关键字主要用在三个地方:变量、方法、类。\n\n\n修饰对象 限制 使用场景\n类 被final修饰的类,是不可以被继承的,这样做的目的可以保证该类不被修改 java的一些核心的api都是final类,例如string、integer、math等\n方法 子类不可以重写父类中被final修饰的方法 比如 模板方法,可以用来固定算法\n成员变量 被修饰的成员变量必须在定义时或者构造器中进行初始化赋值,且不可再修改 \n局部变量 final修饰局部变量时只能初始化(赋值)一次,但也可以不初始化 \n方法参数 final修饰方法参数时,是在调用方法传递参数时候初始化的 \n\n> final修饰变量要说明几点:\n> 1.修饰基本类型数据,final会将值变为一个常数(创建后不能被修改);\n> 2.修饰的是引用类型,引用的指向 是不可变,但对象里面的属性可以改变 。这一限制也适用于数组,数组也属于对象,数组本身也是可以修改的。\n> 3.方法参数中的final句柄,意味着在该方法内部,我们不能改变参数句柄指向的实际东西,也就是说在方法内部不能给形参句柄再另外赋值。\n\n//这段代码可以正常编译运行 因为final修饰的是引用类型,引用的指向 是不可变,但对象里面的属性可以改变\npublic class test {\n public static void main(string[] args) {\n final myclass myclass = new myclass();\n system.out.println(++myclass.i); \n \n }\n}\n\nclass myclass {\n public int i = 0;\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n", "charsets": { "cjk": true }, @@ -841,11 +841,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "简述 CAS 原理,什么是 ABA 问题,怎么解决?", + "title": "线程池是如何实现的?", "frontmatter": { - "title": "简述 CAS 原理,什么是 ABA 问题,怎么解决?", - "date": "2022-04-04T14:32:48.000Z", - "permalink": "/pages/3356c6/", + "title": "线程池是如何实现的?", + "date": "2022-04-04T14:38:19.000Z", + "permalink": "/pages/0da4b0/", "categories": [ "Java相关", "并发" @@ -854,36 +854,64 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/020.%E7%AE%80%E8%BF%B0%20CAS%20%E5%8E%9F%E7%90%86%EF%BC%8C%E4%BB%80%E4%B9%88%E6%98%AF%20ABA%20%E9%97%AE%E9%A2%98%EF%BC%8C%E6%80%8E%E4%B9%88%E8%A7%A3%E5%86%B3%EF%BC%9F.html", - "relativePath": "01.Java相关/20.并发/020.简述 CAS 原理,什么是 ABA 问题,怎么解决?.md", - "key": "v-5390e3d4", - "path": "/pages/3356c6/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/001.%E7%BA%BF%E7%A8%8B%E6%B1%A0%E6%98%AF%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E7%9A%84%EF%BC%9F.html", + "relativePath": "01.Java相关/20.并发/001.线程池是如何实现的?.md", + "key": "v-45fc1217", + "path": "/pages/0da4b0/", "headers": [ { - "level": 2, - "title": "简述 CAS 原理,什么是 ABA 问题,怎么解决?", - "slug": "简述-cas-原理-什么是-aba-问题-怎么解决", - "normalizedTitle": "简述 cas 原理,什么是 aba 问题,怎么解决?", + "level": 3, + "title": "使用线程池有哪些好处", + "slug": "使用线程池有哪些好处", + "normalizedTitle": "使用线程池有哪些好处", "charIndex": 2 }, { "level": 3, - "title": "CAS实现原子操作", - "slug": "cas实现原子操作", - "normalizedTitle": "cas实现原子操作", - "charIndex": 33 + "title": "线程池的创建", + "slug": "线程池的创建", + "normalizedTitle": "线程池的创建", + "charIndex": 163 }, { "level": 3, - "title": "ABA问题", - "slug": "aba问题", - "normalizedTitle": "aba问题", - "charIndex": 256 + "title": "向线程池提交一个任务后,线程池是如何处理这个任务的呢?", + "slug": "向线程池提交一个任务后-线程池是如何处理这个任务的呢", + "normalizedTitle": "向线程池提交一个任务后,线程池是如何处理这个任务的呢?", + "charIndex": 1987 + }, + { + "level": 3, + "title": "线程池是如何工作的呢?", + "slug": "线程池是如何工作的呢", + "normalizedTitle": "线程池是如何工作的呢?", + "charIndex": 3296 + }, + { + "level": 3, + "title": "向线程池提交任务", + "slug": "向线程池提交任务", + "normalizedTitle": "向线程池提交任务", + "charIndex": 5167 + }, + { + "level": 3, + "title": "关闭线程池", + "slug": "关闭线程池", + "normalizedTitle": "关闭线程池", + "charIndex": 6580 + }, + { + "level": 3, + "title": "合理的配置线程池", + "slug": "合理的配置线程池", + "normalizedTitle": "合理的配置线程池", + "charIndex": 6963 } ], - "headersStr": "简述 CAS 原理,什么是 ABA 问题,怎么解决? CAS实现原子操作 ABA问题", - "content": "# 简述 CAS 原理,什么是 ABA 问题,怎么解决?\n\n\n# CAS实现原子操作\n\nCAS理论是 juc 包实现的基石,在intel的CPU中,CAS 通过调用本地方法(JNI)使用cmpxchg指令来实现的非阻塞算法。对比于synchronized阻塞算法,基于 CAS 实现的 juc 在性能上有了很大的提升。\n\nCAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。\n\n\n# ABA问题\n\n * 因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。\n\n * ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。\n\n * 从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。", - "normalizedContent": "# 简述 cas 原理,什么是 aba 问题,怎么解决?\n\n\n# cas实现原子操作\n\ncas理论是 juc 包实现的基石,在intel的cpu中,cas 通过调用本地方法(jni)使用cmpxchg指令来实现的非阻塞算法。对比于synchronized阻塞算法,基于 cas 实现的 juc 在性能上有了很大的提升。\n\ncas 操作包含三个操作数 —— 内存位置(v)、预期原值(a)和新值(b)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。\n\n\n# aba问题\n\n * 因为cas需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是a,变成了b,又变成了a,那么使用cas进行检查时会发现它的值没有发生变化,但是实际上却变化了。\n\n * aba问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么a-b-a 就会变成1a-2b-3a。\n\n * 从java1.5开始jdk的atomic包里提供了一个类atomicstampedreference来解决aba问题。这个类的compareandset方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。", + "headersStr": "使用线程池有哪些好处 线程池的创建 向线程池提交一个任务后,线程池是如何处理这个任务的呢? 线程池是如何工作的呢? 向线程池提交任务 关闭线程池 合理的配置线程池", + "content": "# 使用线程池有哪些好处\n\n 1. 降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁的消耗。\n 2. 提高相应速度。当任务到达时,任务可以不需要线程创建就可以执行。\n 3. 提高线程的可管理性。线程是稀缺资源、如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性。使用线程池可以统一分配、调优和监控。\n\n\n# 线程池的创建\n\n我们可以通过 ThreadPoolExecutor 来创建一个线程池\n\npublic ThreadPoolExecutor(int corePoolSize,\n int maximumPoolSize,\n long keepAliveTime,\n TimeUnit unit,\n BlockingQueue workQueue,\n ThreadFactory threadFactory,\n RejectedExecutionHandler handler) {//...}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n创建一个线程池需要传入如下几个参数:\n\n * corePoolSize: 线程池中的常驻核心线程数。\n 当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的核心线程能够执行新任务也会创建线程。当线程池中的线程数量到达 corePoolSize 时就不再创建。如果调用了线程池的 prestartAllCoreThread()方法,线程池会在启动后就创建所有核心线程。\n\n * workQueue: 存放任务的阻塞队列,被提交但尚未被执行的任务。\n 可以选择如下几个阻塞队列: 1.ArrayBlockingQueue 2.LinkedBlockingQueue:吞吐量高于LinkedBlockingQueue,是 Executors.newFixedThreadPool()创建的线程池的默认队列。 3.SynchronousQueue:一个不存储元素的队列。每个提交的任务都必须等到线程来执行,否则阻塞提交。是 Executors.newCachedThreadPool()创建的线程池的默认队列。 4.PriorityBlockingQueue:一个具有优先级的无限阻塞队列。\n\n * maximumPoolSize: 线程池最大线程数量。\n 如果队列满了并且已经创建的线程数量小于最大线程数量,则线程池会再继续创建新的线程执行任务。值得注意的是如果使用无界队列 (比如LinkedBlockingQueue)这个参数就没有意义了。\n\n * threadFactory: 用于设置创建线程的工厂\n 可以给创建的线程设置有意义的名字,可方便排查问题\n\n * handler: 拒绝策略,表示当队列满了且工作线程都满了如何来拒绝请求执行的线程的策略,默认是AbortPolicy策略,主要有四种类型。\n\n 1. AbortPolicy 直接抛出RegectedExcutionException 异常阻止系统正常进行,默认策略\n\n 2. DiscardPolicy 直接丢弃任务,不予任何处理也不抛出异常,如果允许任务丢失,这是最好的一种方案\n\n 3. DiscardOldestPolicy 抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务\n\n 4. CallerRunsPolicy 交给线程池调用所在的线程进行处理,“调用者运行”的一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量\n\n以上内置拒绝策略均实现了 RejectExcutionHandler 接口,我们也可以实现接口RejectExcutionHandler来自定义策略,如记录日志或者持久化不能拒绝的任务。\n\n * keepAliveTime: 非核心线程存活时间\n 线程池中非核心线程空闲的存活时间 当前线程池数量超过 corePoolSize 时,当空闲时间达到 keepAliveTime 值时,非核心线程会被销毁直到只剩下 corePoolSize 个线程为止。\n \n\n> tips:如果任务很多并且任务执行时间比较短,可以调大 keepAliveTime ,提高线程利用率。\n\n * unit: keepAliveTime 的时间单位\n \n\n\n# 向线程池提交一个任务后,线程池是如何处理这个任务的呢?\n\n\n\n 1. 如果当前运行的线程数=corePoolSize,则将任务加入阻塞队列。\n 3. 如果队列已满或者不能加入,接下来的处理分两种情况:\n 1. 线程池中的线程是否都处于工作状态。创建新的线程来执行任务(这一步骤需要获取全局锁)。\n 2. 线程池中有空闲的线程,则用其来执行任务。\n 4. 如果创建新线程会导致线程数量>maximumPoolSize,则执行拒绝策略。\n\n我们再从源码的角度来理解下这个过程:\nThreadPoolExecutor#execute(Runnble command) 方法\n\npublic void execute(Runnable command) {\n // 参数校验\n if (command == null)\n throw new NullPointerException();\n \n // 从 c 的值可以判断出线程池的状态, 以及线程池中线程的数量\n int c = ctl.get();\n // 1. 如果线程池的线程数量 小于 核心线程数\n if (workerCountOf(c) < corePoolSize) {\n // 添加一个核心线程 command 表示一个具体的任务, true 表示为核心线程\n if (addWorker(command, true))\n return;\n c = ctl.get();\n }\n // 2. 如果线程池处于 RUNNING 状态(只有处于此状态,才能接受新的任务)\n // 并且线程池的数量大于核心线程数, 就把任务添加到阻塞队列\n if (isRunning(c) && workQueue.offer(command)) {\n int recheck = ctl.get();\n if (! isRunning(recheck) && remove(command))\n reject(command);\n else if (workerCountOf(recheck) == 0)\n addWorker(null, false);\n }\n // 3. 如果队列也满了,就创建一个非核心线程(core==false)\n else if (!addWorker(command, false))\n // 如果创建失败,就执行拒绝策略\n reject(command);\n}\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n\n\n\n# 线程池是如何工作的呢?\n\n线程池中有一个 workers 集合,里面记录了所有的工作线程,只有拿到 mainLock 锁的线程才能访问。 ThreadPoolExecutor#workers 属性\n\nprivate final HashSet workers = new HashSet();\n\n\n1\n\n\n线程池创建线程时,会将线程包装成 Worker ,Worker 在执行任务后还会循环获取队里里的任务。我们可以从 Worker 类的 run() 方法看到这一点。\n\nfinal void runWorker(Worker w) {\n Thread wt = Thread.currentThread();\n Runnable task = w.firstTask;\n w.firstTask = null;\n w.unlock(); // allow interrupts\n boolean completedAbruptly = true;\n try {\n while (task != null || (task = getTask()) != null) {\n w.lock();\n if ((runStateAtLeast(ctl.get(), STOP) ||\n (Thread.interrupted() &&\n runStateAtLeast(ctl.get(), STOP))) &&\n !wt.isInterrupted())\n wt.interrupt();\n try {\n beforeExecute(wt, task);\n Throwable thrown = null;\n try {\n task.run();\n } catch (RuntimeException x) {\n thrown = x; throw x;\n } catch (Error x) {\n thrown = x; throw x;\n } catch (Throwable x) {\n thrown = x; throw new Error(x);\n } finally {\n afterExecute(task, thrown);\n }\n } finally {\n task = null;\n w.completedTasks++;\n w.unlock();\n }\n }\n completedAbruptly = false;\n } finally {\n processWorkerExit(w, completedAbruptly);\n }\n }\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n\n\n上面代码关键点是while循环和getTask()方法,通过循环不断的调用getTask()从阻塞队列中获取任务,通过这个方法,它与阻塞队列建立桥梁。\n\n\n# 向线程池提交任务\n\n有两个方法提交任务,分别是execute(),submit()\n\n\n * execute():execute只能提交Runnable类型的任务,用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。如果遇到异常会直接抛出。使用方法如下:\n\npublic static void main(String[] args) throws Exception{\n ExecutorService es = Executors.newSingleThreadExecutor();\n Runnable runnable = new Runnable() {\n @Override\n public void run() {\n System.out.println(\"Runnable线程处理开始...\");\n }\n };\n es.execute(runnable);\n es.shutdown();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n * submit():submit既可以提交Runnable类型的任务,也可以提交Callable类型的任务,会有一个类型为Future的返回值,但当任务类型为Runnable时,返回值为null。如果遇到异常不会直接抛出,只有在使用Future的get方法获取返回值时,才会抛出异常。使用方法:\n\npublic static void main(String[] args) throws Exception{\n ExecutorService es = Executors.newSingleThreadExecutor();\n Callable callable = new Callable() {\n @Override\n public String call() throws Exception {\n System.out.println(\"线程处理开始...\");\n return \"hello world\";\n }\n };\n Future future = es.submit(callable);\n while(true) {\n //idDone:如果任务已完成,则返回 true。 可能由于正常终止、异常或取消而完成,在所有这些情况中,此方法都将返回 true。\n if(future.isDone()) {\n System.out.println(\"任务执行完成:\" + future.get());\n break;\n }\n }\n es.shutdown();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n\n\n\n# 关闭线程池\n\n可以通过shutdown和shutdownNow方法来关闭线程池。他们的原理是遍历线程池的中工作线程,调用其interrupt()方法来中断线程,所以,无法相应中断的任务永远无法被终止。但是他们也存在一定的区别:\n\n * shutdown:将线程的状态设置为 SHUTDOWN 状态,然后中断没有执行任务的线程。\n * shutdownNow:首先将线程的状态设置为 STOP ,然后尝试停止所有正在执行或者暂停任务的线程,并返回队列中的待执行任务。\n\n只要调用了这两个方法中的任意一个,isShutdown()方法就会返回true,当所有任务都关闭后,才表示线程池关闭成功,这是调用isTerminaed()方法才会返回 true 。\n\n\n通常调用 shutdown 来关闭线程池,如果不需要等任务执行完可以调用 shutdownNow 。\n\n\n# 合理的配置线程池\n\n 1. 最大线程数 maximumPoolSize 的是指可参考如下规则:\n * CPU 密集型任务应该配置尽可能小的线程,如cpu数量+1。\n * IO密集型任务线程并不是一直占用cpu,则应该适度更多的配置线程,如2*cpu数量\n * 可以通过 Runtime.getRuntime().availableProcessors() 获得当前设备的 cpu 个数。\n 2. 建议使用有界队列。如果任务执行速度变慢,线程池不断向队列中 add 元素,会有内存溢出的风险。", + "normalizedContent": "# 使用线程池有哪些好处\n\n 1. 降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁的消耗。\n 2. 提高相应速度。当任务到达时,任务可以不需要线程创建就可以执行。\n 3. 提高线程的可管理性。线程是稀缺资源、如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性。使用线程池可以统一分配、调优和监控。\n\n\n# 线程池的创建\n\n我们可以通过 threadpoolexecutor 来创建一个线程池\n\npublic threadpoolexecutor(int corepoolsize,\n int maximumpoolsize,\n long keepalivetime,\n timeunit unit,\n blockingqueue workqueue,\n threadfactory threadfactory,\n rejectedexecutionhandler handler) {//...}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n创建一个线程池需要传入如下几个参数:\n\n * corepoolsize: 线程池中的常驻核心线程数。\n 当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的核心线程能够执行新任务也会创建线程。当线程池中的线程数量到达 corepoolsize 时就不再创建。如果调用了线程池的 prestartallcorethread()方法,线程池会在启动后就创建所有核心线程。\n\n * workqueue: 存放任务的阻塞队列,被提交但尚未被执行的任务。\n 可以选择如下几个阻塞队列: 1.arrayblockingqueue 2.linkedblockingqueue:吞吐量高于linkedblockingqueue,是 executors.newfixedthreadpool()创建的线程池的默认队列。 3.synchronousqueue:一个不存储元素的队列。每个提交的任务都必须等到线程来执行,否则阻塞提交。是 executors.newcachedthreadpool()创建的线程池的默认队列。 4.priorityblockingqueue:一个具有优先级的无限阻塞队列。\n\n * maximumpoolsize: 线程池最大线程数量。\n 如果队列满了并且已经创建的线程数量小于最大线程数量,则线程池会再继续创建新的线程执行任务。值得注意的是如果使用无界队列 (比如linkedblockingqueue)这个参数就没有意义了。\n\n * threadfactory: 用于设置创建线程的工厂\n 可以给创建的线程设置有意义的名字,可方便排查问题\n\n * handler: 拒绝策略,表示当队列满了且工作线程都满了如何来拒绝请求执行的线程的策略,默认是abortpolicy策略,主要有四种类型。\n\n 1. abortpolicy 直接抛出regectedexcutionexception 异常阻止系统正常进行,默认策略\n\n 2. discardpolicy 直接丢弃任务,不予任何处理也不抛出异常,如果允许任务丢失,这是最好的一种方案\n\n 3. discardoldestpolicy 抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务\n\n 4. callerrunspolicy 交给线程池调用所在的线程进行处理,“调用者运行”的一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量\n\n以上内置拒绝策略均实现了 rejectexcutionhandler 接口,我们也可以实现接口rejectexcutionhandler来自定义策略,如记录日志或者持久化不能拒绝的任务。\n\n * keepalivetime: 非核心线程存活时间\n 线程池中非核心线程空闲的存活时间 当前线程池数量超过 corepoolsize 时,当空闲时间达到 keepalivetime 值时,非核心线程会被销毁直到只剩下 corepoolsize 个线程为止。\n \n\n> tips:如果任务很多并且任务执行时间比较短,可以调大 keepalivetime ,提高线程利用率。\n\n * unit: keepalivetime 的时间单位\n \n\n\n# 向线程池提交一个任务后,线程池是如何处理这个任务的呢?\n\n\n\n 1. 如果当前运行的线程数=corepoolsize,则将任务加入阻塞队列。\n 3. 如果队列已满或者不能加入,接下来的处理分两种情况:\n 1. 线程池中的线程是否都处于工作状态。创建新的线程来执行任务(这一步骤需要获取全局锁)。\n 2. 线程池中有空闲的线程,则用其来执行任务。\n 4. 如果创建新线程会导致线程数量>maximumpoolsize,则执行拒绝策略。\n\n我们再从源码的角度来理解下这个过程:\nthreadpoolexecutor#execute(runnble command) 方法\n\npublic void execute(runnable command) {\n // 参数校验\n if (command == null)\n throw new nullpointerexception();\n \n // 从 c 的值可以判断出线程池的状态, 以及线程池中线程的数量\n int c = ctl.get();\n // 1. 如果线程池的线程数量 小于 核心线程数\n if (workercountof(c) < corepoolsize) {\n // 添加一个核心线程 command 表示一个具体的任务, true 表示为核心线程\n if (addworker(command, true))\n return;\n c = ctl.get();\n }\n // 2. 如果线程池处于 running 状态(只有处于此状态,才能接受新的任务)\n // 并且线程池的数量大于核心线程数, 就把任务添加到阻塞队列\n if (isrunning(c) && workqueue.offer(command)) {\n int recheck = ctl.get();\n if (! isrunning(recheck) && remove(command))\n reject(command);\n else if (workercountof(recheck) == 0)\n addworker(null, false);\n }\n // 3. 如果队列也满了,就创建一个非核心线程(core==false)\n else if (!addworker(command, false))\n // 如果创建失败,就执行拒绝策略\n reject(command);\n}\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n\n\n\n# 线程池是如何工作的呢?\n\n线程池中有一个 workers 集合,里面记录了所有的工作线程,只有拿到 mainlock 锁的线程才能访问。 threadpoolexecutor#workers 属性\n\nprivate final hashset workers = new hashset();\n\n\n1\n\n\n线程池创建线程时,会将线程包装成 worker ,worker 在执行任务后还会循环获取队里里的任务。我们可以从 worker 类的 run() 方法看到这一点。\n\nfinal void runworker(worker w) {\n thread wt = thread.currentthread();\n runnable task = w.firsttask;\n w.firsttask = null;\n w.unlock(); // allow interrupts\n boolean completedabruptly = true;\n try {\n while (task != null || (task = gettask()) != null) {\n w.lock();\n if ((runstateatleast(ctl.get(), stop) ||\n (thread.interrupted() &&\n runstateatleast(ctl.get(), stop))) &&\n !wt.isinterrupted())\n wt.interrupt();\n try {\n beforeexecute(wt, task);\n throwable thrown = null;\n try {\n task.run();\n } catch (runtimeexception x) {\n thrown = x; throw x;\n } catch (error x) {\n thrown = x; throw x;\n } catch (throwable x) {\n thrown = x; throw new error(x);\n } finally {\n afterexecute(task, thrown);\n }\n } finally {\n task = null;\n w.completedtasks++;\n w.unlock();\n }\n }\n completedabruptly = false;\n } finally {\n processworkerexit(w, completedabruptly);\n }\n }\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n\n\n上面代码关键点是while循环和gettask()方法,通过循环不断的调用gettask()从阻塞队列中获取任务,通过这个方法,它与阻塞队列建立桥梁。\n\n\n# 向线程池提交任务\n\n有两个方法提交任务,分别是execute(),submit()\n\n\n * execute():execute只能提交runnable类型的任务,用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。如果遇到异常会直接抛出。使用方法如下:\n\npublic static void main(string[] args) throws exception{\n executorservice es = executors.newsinglethreadexecutor();\n runnable runnable = new runnable() {\n @override\n public void run() {\n system.out.println(\"runnable线程处理开始...\");\n }\n };\n es.execute(runnable);\n es.shutdown();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n * submit():submit既可以提交runnable类型的任务,也可以提交callable类型的任务,会有一个类型为future的返回值,但当任务类型为runnable时,返回值为null。如果遇到异常不会直接抛出,只有在使用future的get方法获取返回值时,才会抛出异常。使用方法:\n\npublic static void main(string[] args) throws exception{\n executorservice es = executors.newsinglethreadexecutor();\n callable callable = new callable() {\n @override\n public string call() throws exception {\n system.out.println(\"线程处理开始...\");\n return \"hello world\";\n }\n };\n future future = es.submit(callable);\n while(true) {\n //iddone:如果任务已完成,则返回 true。 可能由于正常终止、异常或取消而完成,在所有这些情况中,此方法都将返回 true。\n if(future.isdone()) {\n system.out.println(\"任务执行完成:\" + future.get());\n break;\n }\n }\n es.shutdown();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n\n\n\n# 关闭线程池\n\n可以通过shutdown和shutdownnow方法来关闭线程池。他们的原理是遍历线程池的中工作线程,调用其interrupt()方法来中断线程,所以,无法相应中断的任务永远无法被终止。但是他们也存在一定的区别:\n\n * shutdown:将线程的状态设置为 shutdown 状态,然后中断没有执行任务的线程。\n * shutdownnow:首先将线程的状态设置为 stop ,然后尝试停止所有正在执行或者暂停任务的线程,并返回队列中的待执行任务。\n\n只要调用了这两个方法中的任意一个,isshutdown()方法就会返回true,当所有任务都关闭后,才表示线程池关闭成功,这是调用isterminaed()方法才会返回 true 。\n\n\n通常调用 shutdown 来关闭线程池,如果不需要等任务执行完可以调用 shutdownnow 。\n\n\n# 合理的配置线程池\n\n 1. 最大线程数 maximumpoolsize 的是指可参考如下规则:\n * cpu 密集型任务应该配置尽可能小的线程,如cpu数量+1。\n * io密集型任务线程并不是一直占用cpu,则应该适度更多的配置线程,如2*cpu数量\n * 可以通过 runtime.getruntime().availableprocessors() 获得当前设备的 cpu 个数。\n 2. 建议使用有界队列。如果任务执行速度变慢,线程池不断向队列中 add 元素,会有内存溢出的风险。", "charsets": { "cjk": true }, @@ -891,11 +919,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "Synchronized 与 Lock 相比优缺点分别是什么?", + "title": "简述 CAS 原理,什么是 ABA 问题,怎么解决?", "frontmatter": { - "title": "Synchronized 与 Lock 相比优缺点分别是什么?", - "date": "2022-04-04T14:33:27.000Z", - "permalink": "/pages/d5d161/", + "title": "简述 CAS 原理,什么是 ABA 问题,怎么解决?", + "date": "2022-04-04T14:32:48.000Z", + "permalink": "/pages/3356c6/", "categories": [ "Java相关", "并发" @@ -904,36 +932,36 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/040.Synchronized%20%E5%85%B3%E9%94%AE%E5%AD%97%E5%BA%95%E5%B1%82%E6%98%AF%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E7%9A%84%EF%BC%9F.html", - "relativePath": "01.Java相关/20.并发/040.Synchronized 关键字底层是如何实现的?.md", - "key": "v-352f36cc", - "path": "/pages/d5d161/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/020.%E7%AE%80%E8%BF%B0%20CAS%20%E5%8E%9F%E7%90%86%EF%BC%8C%E4%BB%80%E4%B9%88%E6%98%AF%20ABA%20%E9%97%AE%E9%A2%98%EF%BC%8C%E6%80%8E%E4%B9%88%E8%A7%A3%E5%86%B3%EF%BC%9F.html", + "relativePath": "01.Java相关/20.并发/020.简述 CAS 原理,什么是 ABA 问题,怎么解决?.md", + "key": "v-5390e3d4", + "path": "/pages/3356c6/", "headers": [ { "level": 2, - "title": "Lock 接口 VS Synchronized", - "slug": "lock-接口-vs-synchronized", - "normalizedTitle": "lock 接口 vs synchronized", - "charIndex": 286 + "title": "简述 CAS 原理,什么是 ABA 问题,怎么解决?", + "slug": "简述-cas-原理-什么是-aba-问题-怎么解决", + "normalizedTitle": "简述 cas 原理,什么是 aba 问题,怎么解决?", + "charIndex": 2 }, { - "level": 2, - "title": "Lock 锁的常规用法", - "slug": "lock-锁的常规用法", - "normalizedTitle": "lock 锁的常规用法", - "charIndex": 751 + "level": 3, + "title": "CAS实现原子操作", + "slug": "cas实现原子操作", + "normalizedTitle": "cas实现原子操作", + "charIndex": 33 }, { - "level": 2, - "title": "Lock 锁的API", - "slug": "lock-锁的api", - "normalizedTitle": "lock 锁的api", - "charIndex": 928 + "level": 3, + "title": "ABA问题", + "slug": "aba问题", + "normalizedTitle": "aba问题", + "charIndex": 256 } ], - "headersStr": "Lock 接口 VS Synchronized Lock 锁的常规用法 Lock 锁的API", - "content": "Lock 实现提供比使用 synchronized 方法和语句可以获得的更广泛的锁定操作。 它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象Condition 。\n\n虽然 synchronized 方法和语句的范围机制使得使用监视器锁更容易编程,并且有助于避免涉及锁的许多常见编程错误,但是有时您需要以更灵活的方式处理锁。\n\n例如,您获取节点A的锁定,然后获取节点B,然后释放A并获取C,然后释放B并获得D等。\n\n所述的实施方式中Lock接口通过允许获得并在不同的范围释放的锁,并允许获得并以任何顺序释放多个锁使得能够使用这样的技术。\n\n\n# Lock 接口 VS Synchronized\n\nJava SE 5之后,juc 包中增加了 Lock 接口,在此之前 Java 是靠 Synchronized 关键字来实现锁功能的,二者具有相同的内存同步语义,区别如下:\n\n SYNCHRONIZED LOCK接口\n加锁、解锁 隐式的加锁、解锁 显式的加锁、解锁\n是否异常自动释放锁 是 不是,必须手动unlock来释放锁\n是否支持相应中断 只能等待锁的释放,不能响应中断 等待锁过程中可以用interrupt来中断等待\n适用场景 比如这种场景:先获得A锁,再获取B锁,获取到B锁释放A锁同时获取C锁,获得C锁后释放B锁在获取D锁。这种场景用Synchronized就不好实现了。\n\nLock 相比于 Synchronized 还有如下特性:\n\n * 尝试非阻塞获取锁\n * 能被中断的获取锁\n * 超时获取锁\n\n\n# Lock 锁的常规用法\n\n Lock l = ...; \n l.lock(); \n try {\n // access the resource protected by this lock }\n finally { \n l.unlock(); \n } \n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# Lock 锁的API\n\nLock 接口定义了所得获取和释放的基本操作, Lock的API如下:\n\n方法 描述\nvoid lock() 获得锁\nvoid lockInterruptibly() 获取锁定,除非当前线程是 interrupted 。\nCondition newCondition() 返回一个新Condition绑定到该实例Lock实例。\nboolean tryLock() 只有在调用时才可以获得锁。\nboolean tryLock(long time, TimeUnit unit) 如果在给定的等待时间内是空闲的,并且当前的线程尚未得到 interrupted,则获取该锁\nvoid unlock() 释放锁。", - "normalizedContent": "lock 实现提供比使用 synchronized 方法和语句可以获得的更广泛的锁定操作。 它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象condition 。\n\n虽然 synchronized 方法和语句的范围机制使得使用监视器锁更容易编程,并且有助于避免涉及锁的许多常见编程错误,但是有时您需要以更灵活的方式处理锁。\n\n例如,您获取节点a的锁定,然后获取节点b,然后释放a并获取c,然后释放b并获得d等。\n\n所述的实施方式中lock接口通过允许获得并在不同的范围释放的锁,并允许获得并以任何顺序释放多个锁使得能够使用这样的技术。\n\n\n# lock 接口 vs synchronized\n\njava se 5之后,juc 包中增加了 lock 接口,在此之前 java 是靠 synchronized 关键字来实现锁功能的,二者具有相同的内存同步语义,区别如下:\n\n synchronized lock接口\n加锁、解锁 隐式的加锁、解锁 显式的加锁、解锁\n是否异常自动释放锁 是 不是,必须手动unlock来释放锁\n是否支持相应中断 只能等待锁的释放,不能响应中断 等待锁过程中可以用interrupt来中断等待\n适用场景 比如这种场景:先获得a锁,再获取b锁,获取到b锁释放a锁同时获取c锁,获得c锁后释放b锁在获取d锁。这种场景用synchronized就不好实现了。\n\nlock 相比于 synchronized 还有如下特性:\n\n * 尝试非阻塞获取锁\n * 能被中断的获取锁\n * 超时获取锁\n\n\n# lock 锁的常规用法\n\n lock l = ...; \n l.lock(); \n try {\n // access the resource protected by this lock }\n finally { \n l.unlock(); \n } \n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# lock 锁的api\n\nlock 接口定义了所得获取和释放的基本操作, lock的api如下:\n\n方法 描述\nvoid lock() 获得锁\nvoid lockinterruptibly() 获取锁定,除非当前线程是 interrupted 。\ncondition newcondition() 返回一个新condition绑定到该实例lock实例。\nboolean trylock() 只有在调用时才可以获得锁。\nboolean trylock(long time, timeunit unit) 如果在给定的等待时间内是空闲的,并且当前的线程尚未得到 interrupted,则获取该锁\nvoid unlock() 释放锁。", + "headersStr": "简述 CAS 原理,什么是 ABA 问题,怎么解决? CAS实现原子操作 ABA问题", + "content": "# 简述 CAS 原理,什么是 ABA 问题,怎么解决?\n\n\n# CAS实现原子操作\n\nCAS理论是 juc 包实现的基石,在intel的CPU中,CAS 通过调用本地方法(JNI)使用cmpxchg指令来实现的非阻塞算法。对比于synchronized阻塞算法,基于 CAS 实现的 juc 在性能上有了很大的提升。\n\nCAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。\n\n\n# ABA问题\n\n * 因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。\n\n * ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。\n\n * 从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。", + "normalizedContent": "# 简述 cas 原理,什么是 aba 问题,怎么解决?\n\n\n# cas实现原子操作\n\ncas理论是 juc 包实现的基石,在intel的cpu中,cas 通过调用本地方法(jni)使用cmpxchg指令来实现的非阻塞算法。对比于synchronized阻塞算法,基于 cas 实现的 juc 在性能上有了很大的提升。\n\ncas 操作包含三个操作数 —— 内存位置(v)、预期原值(a)和新值(b)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。\n\n\n# aba问题\n\n * 因为cas需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是a,变成了b,又变成了a,那么使用cas进行检查时会发现它的值没有发生变化,但是实际上却变化了。\n\n * aba问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么a-b-a 就会变成1a-2b-3a。\n\n * 从java1.5开始jdk的atomic包里提供了一个类atomicstampedreference来解决aba问题。这个类的compareandset方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。", "charsets": { "cjk": true }, @@ -1075,11 +1103,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "volatile 关键字解决了什么问题,它的实现原理是什么?", + "title": "Synchronized 与 Lock 相比优缺点分别是什么?", "frontmatter": { - "title": "volatile 关键字解决了什么问题,它的实现原理是什么?", - "date": "2022-04-04T14:34:07.000Z", - "permalink": "/pages/383264/", + "title": "Synchronized 与 Lock 相比优缺点分别是什么?", + "date": "2022-04-04T14:33:27.000Z", + "permalink": "/pages/d5d161/", "categories": [ "Java相关", "并发" @@ -1088,107 +1116,36 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/060.volatile%20%E5%85%B3%E9%94%AE%E5%AD%97%E8%A7%A3%E5%86%B3%E4%BA%86%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98%EF%BC%8C%E5%AE%83%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", - "relativePath": "01.Java相关/20.并发/060.volatile 关键字解决了什么问题,它的实现原理是什么?.md", - "key": "v-f5bcd180", - "path": "/pages/383264/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/040.Synchronized%20%E5%85%B3%E9%94%AE%E5%AD%97%E5%BA%95%E5%B1%82%E6%98%AF%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E7%9A%84%EF%BC%9F.html", + "relativePath": "01.Java相关/20.并发/040.Synchronized 关键字底层是如何实现的?.md", + "key": "v-352f36cc", + "path": "/pages/d5d161/", "headers": [ { "level": 2, - "title": "Java中的内存可见性", - "slug": "java中的内存可见性", - "normalizedTitle": "java中的内存可见性", - "charIndex": 129 + "title": "Lock 接口 VS Synchronized", + "slug": "lock-接口-vs-synchronized", + "normalizedTitle": "lock 接口 vs synchronized", + "charIndex": 286 }, { "level": 2, - "title": "Volatile 的实现原理", - "slug": "volatile-的实现原理", - "normalizedTitle": "volatile 的实现原理", - "charIndex": 335 - } - ], - "headersStr": "Java中的内存可见性 Volatile 的实现原理", - "content": "volatile 能保证 Java 中的内存可见性。 可见性的意思是:当线程A修改共享变量的值后,线程B能立刻读到这个修改后的值。\n\n\nVolatile 不会引起线程上下文的切换和调度,如果使用的恰当,会比 Synchronized 执行成本更低。\n\n\n# Java中的内存可见性\n\n * 可见性:一个线程对共享变量值的修改,能够及时被其他线程看到。\n\n * 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那这个变量就是这几个线程的共享变量。\n\n * Java内存的规定:\n\n-线程对共享变量的所有操作都必须在自己的工作内存中进行,不可直接从主内存中读写; -不同线程之间无法直接访问其他线程工作内存中的变量,线程间的变量值的传递需要通过主内存。\n\n\n\n\n# Volatile 的实现原理\n\n如果对用 Volatile 修饰的变量写操作,JVM 会向处理器发出一条 Lock 前缀的指令,Lock 前缀的指令在多核处理器下会引发两件事情:\n\n 1. 将当前处理器缓存行的数据写会到系统内存\n\n 2. 这个写会内存的操作会使其他缓存中的该内存地址的数据无效", - "normalizedContent": "volatile 能保证 java 中的内存可见性。 可见性的意思是:当线程a修改共享变量的值后,线程b能立刻读到这个修改后的值。\n\n\nvolatile 不会引起线程上下文的切换和调度,如果使用的恰当,会比 synchronized 执行成本更低。\n\n\n# java中的内存可见性\n\n * 可见性:一个线程对共享变量值的修改,能够及时被其他线程看到。\n\n * 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那这个变量就是这几个线程的共享变量。\n\n * java内存的规定:\n\n-线程对共享变量的所有操作都必须在自己的工作内存中进行,不可直接从主内存中读写; -不同线程之间无法直接访问其他线程工作内存中的变量,线程间的变量值的传递需要通过主内存。\n\n\n\n\n# volatile 的实现原理\n\n如果对用 volatile 修饰的变量写操作,jvm 会向处理器发出一条 lock 前缀的指令,lock 前缀的指令在多核处理器下会引发两件事情:\n\n 1. 将当前处理器缓存行的数据写会到系统内存\n\n 2. 这个写会内存的操作会使其他缓存中的该内存地址的数据无效", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, - { - "title": "线程池是如何实现的?", - "frontmatter": { - "title": "线程池是如何实现的?", - "date": "2022-04-04T14:38:19.000Z", - "permalink": "/pages/0da4b0/", - "categories": [ - "Java相关", - "并发" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/001.%E7%BA%BF%E7%A8%8B%E6%B1%A0%E6%98%AF%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E7%9A%84%EF%BC%9F.html", - "relativePath": "01.Java相关/20.并发/001.线程池是如何实现的?.md", - "key": "v-45fc1217", - "path": "/pages/0da4b0/", - "headers": [ - { - "level": 3, - "title": "使用线程池有哪些好处", - "slug": "使用线程池有哪些好处", - "normalizedTitle": "使用线程池有哪些好处", - "charIndex": 2 - }, - { - "level": 3, - "title": "线程池的创建", - "slug": "线程池的创建", - "normalizedTitle": "线程池的创建", - "charIndex": 163 - }, - { - "level": 3, - "title": "向线程池提交一个任务后,线程池是如何处理这个任务的呢?", - "slug": "向线程池提交一个任务后-线程池是如何处理这个任务的呢", - "normalizedTitle": "向线程池提交一个任务后,线程池是如何处理这个任务的呢?", - "charIndex": 1987 - }, - { - "level": 3, - "title": "线程池是如何工作的呢?", - "slug": "线程池是如何工作的呢", - "normalizedTitle": "线程池是如何工作的呢?", - "charIndex": 3296 - }, - { - "level": 3, - "title": "向线程池提交任务", - "slug": "向线程池提交任务", - "normalizedTitle": "向线程池提交任务", - "charIndex": 5167 - }, - { - "level": 3, - "title": "关闭线程池", - "slug": "关闭线程池", - "normalizedTitle": "关闭线程池", - "charIndex": 6580 + "title": "Lock 锁的常规用法", + "slug": "lock-锁的常规用法", + "normalizedTitle": "lock 锁的常规用法", + "charIndex": 751 }, { - "level": 3, - "title": "合理的配置线程池", - "slug": "合理的配置线程池", - "normalizedTitle": "合理的配置线程池", - "charIndex": 6963 + "level": 2, + "title": "Lock 锁的API", + "slug": "lock-锁的api", + "normalizedTitle": "lock 锁的api", + "charIndex": 928 } ], - "headersStr": "使用线程池有哪些好处 线程池的创建 向线程池提交一个任务后,线程池是如何处理这个任务的呢? 线程池是如何工作的呢? 向线程池提交任务 关闭线程池 合理的配置线程池", - "content": "# 使用线程池有哪些好处\n\n 1. 降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁的消耗。\n 2. 提高相应速度。当任务到达时,任务可以不需要线程创建就可以执行。\n 3. 提高线程的可管理性。线程是稀缺资源、如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性。使用线程池可以统一分配、调优和监控。\n\n\n# 线程池的创建\n\n我们可以通过 ThreadPoolExecutor 来创建一个线程池\n\npublic ThreadPoolExecutor(int corePoolSize,\n int maximumPoolSize,\n long keepAliveTime,\n TimeUnit unit,\n BlockingQueue workQueue,\n ThreadFactory threadFactory,\n RejectedExecutionHandler handler) {//...}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n创建一个线程池需要传入如下几个参数:\n\n * corePoolSize: 线程池中的常驻核心线程数。\n 当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的核心线程能够执行新任务也会创建线程。当线程池中的线程数量到达 corePoolSize 时就不再创建。如果调用了线程池的 prestartAllCoreThread()方法,线程池会在启动后就创建所有核心线程。\n\n * workQueue: 存放任务的阻塞队列,被提交但尚未被执行的任务。\n 可以选择如下几个阻塞队列: 1.ArrayBlockingQueue 2.LinkedBlockingQueue:吞吐量高于LinkedBlockingQueue,是 Executors.newFixedThreadPool()创建的线程池的默认队列。 3.SynchronousQueue:一个不存储元素的队列。每个提交的任务都必须等到线程来执行,否则阻塞提交。是 Executors.newCachedThreadPool()创建的线程池的默认队列。 4.PriorityBlockingQueue:一个具有优先级的无限阻塞队列。\n\n * maximumPoolSize: 线程池最大线程数量。\n 如果队列满了并且已经创建的线程数量小于最大线程数量,则线程池会再继续创建新的线程执行任务。值得注意的是如果使用无界队列 (比如LinkedBlockingQueue)这个参数就没有意义了。\n\n * threadFactory: 用于设置创建线程的工厂\n 可以给创建的线程设置有意义的名字,可方便排查问题\n\n * handler: 拒绝策略,表示当队列满了且工作线程都满了如何来拒绝请求执行的线程的策略,默认是AbortPolicy策略,主要有四种类型。\n\n 1. AbortPolicy 直接抛出RegectedExcutionException 异常阻止系统正常进行,默认策略\n\n 2. DiscardPolicy 直接丢弃任务,不予任何处理也不抛出异常,如果允许任务丢失,这是最好的一种方案\n\n 3. DiscardOldestPolicy 抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务\n\n 4. CallerRunsPolicy 交给线程池调用所在的线程进行处理,“调用者运行”的一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量\n\n以上内置拒绝策略均实现了 RejectExcutionHandler 接口,我们也可以实现接口RejectExcutionHandler来自定义策略,如记录日志或者持久化不能拒绝的任务。\n\n * keepAliveTime: 非核心线程存活时间\n 线程池中非核心线程空闲的存活时间 当前线程池数量超过 corePoolSize 时,当空闲时间达到 keepAliveTime 值时,非核心线程会被销毁直到只剩下 corePoolSize 个线程为止。\n \n\n> tips:如果任务很多并且任务执行时间比较短,可以调大 keepAliveTime ,提高线程利用率。\n\n * unit: keepAliveTime 的时间单位\n \n\n\n# 向线程池提交一个任务后,线程池是如何处理这个任务的呢?\n\n\n\n 1. 如果当前运行的线程数=corePoolSize,则将任务加入阻塞队列。\n 3. 如果队列已满或者不能加入,接下来的处理分两种情况:\n 1. 线程池中的线程是否都处于工作状态。创建新的线程来执行任务(这一步骤需要获取全局锁)。\n 2. 线程池中有空闲的线程,则用其来执行任务。\n 4. 如果创建新线程会导致线程数量>maximumPoolSize,则执行拒绝策略。\n\n我们再从源码的角度来理解下这个过程:\nThreadPoolExecutor#execute(Runnble command) 方法\n\npublic void execute(Runnable command) {\n // 参数校验\n if (command == null)\n throw new NullPointerException();\n \n // 从 c 的值可以判断出线程池的状态, 以及线程池中线程的数量\n int c = ctl.get();\n // 1. 如果线程池的线程数量 小于 核心线程数\n if (workerCountOf(c) < corePoolSize) {\n // 添加一个核心线程 command 表示一个具体的任务, true 表示为核心线程\n if (addWorker(command, true))\n return;\n c = ctl.get();\n }\n // 2. 如果线程池处于 RUNNING 状态(只有处于此状态,才能接受新的任务)\n // 并且线程池的数量大于核心线程数, 就把任务添加到阻塞队列\n if (isRunning(c) && workQueue.offer(command)) {\n int recheck = ctl.get();\n if (! isRunning(recheck) && remove(command))\n reject(command);\n else if (workerCountOf(recheck) == 0)\n addWorker(null, false);\n }\n // 3. 如果队列也满了,就创建一个非核心线程(core==false)\n else if (!addWorker(command, false))\n // 如果创建失败,就执行拒绝策略\n reject(command);\n}\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n\n\n\n# 线程池是如何工作的呢?\n\n线程池中有一个 workers 集合,里面记录了所有的工作线程,只有拿到 mainLock 锁的线程才能访问。 ThreadPoolExecutor#workers 属性\n\nprivate final HashSet workers = new HashSet();\n\n\n1\n\n\n线程池创建线程时,会将线程包装成 Worker ,Worker 在执行任务后还会循环获取队里里的任务。我们可以从 Worker 类的 run() 方法看到这一点。\n\nfinal void runWorker(Worker w) {\n Thread wt = Thread.currentThread();\n Runnable task = w.firstTask;\n w.firstTask = null;\n w.unlock(); // allow interrupts\n boolean completedAbruptly = true;\n try {\n while (task != null || (task = getTask()) != null) {\n w.lock();\n if ((runStateAtLeast(ctl.get(), STOP) ||\n (Thread.interrupted() &&\n runStateAtLeast(ctl.get(), STOP))) &&\n !wt.isInterrupted())\n wt.interrupt();\n try {\n beforeExecute(wt, task);\n Throwable thrown = null;\n try {\n task.run();\n } catch (RuntimeException x) {\n thrown = x; throw x;\n } catch (Error x) {\n thrown = x; throw x;\n } catch (Throwable x) {\n thrown = x; throw new Error(x);\n } finally {\n afterExecute(task, thrown);\n }\n } finally {\n task = null;\n w.completedTasks++;\n w.unlock();\n }\n }\n completedAbruptly = false;\n } finally {\n processWorkerExit(w, completedAbruptly);\n }\n }\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n\n\n上面代码关键点是while循环和getTask()方法,通过循环不断的调用getTask()从阻塞队列中获取任务,通过这个方法,它与阻塞队列建立桥梁。\n\n\n# 向线程池提交任务\n\n有两个方法提交任务,分别是execute(),submit()\n\n\n * execute():execute只能提交Runnable类型的任务,用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。如果遇到异常会直接抛出。使用方法如下:\n\npublic static void main(String[] args) throws Exception{\n ExecutorService es = Executors.newSingleThreadExecutor();\n Runnable runnable = new Runnable() {\n @Override\n public void run() {\n System.out.println(\"Runnable线程处理开始...\");\n }\n };\n es.execute(runnable);\n es.shutdown();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n * submit():submit既可以提交Runnable类型的任务,也可以提交Callable类型的任务,会有一个类型为Future的返回值,但当任务类型为Runnable时,返回值为null。如果遇到异常不会直接抛出,只有在使用Future的get方法获取返回值时,才会抛出异常。使用方法:\n\npublic static void main(String[] args) throws Exception{\n ExecutorService es = Executors.newSingleThreadExecutor();\n Callable callable = new Callable() {\n @Override\n public String call() throws Exception {\n System.out.println(\"线程处理开始...\");\n return \"hello world\";\n }\n };\n Future future = es.submit(callable);\n while(true) {\n //idDone:如果任务已完成,则返回 true。 可能由于正常终止、异常或取消而完成,在所有这些情况中,此方法都将返回 true。\n if(future.isDone()) {\n System.out.println(\"任务执行完成:\" + future.get());\n break;\n }\n }\n es.shutdown();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n\n\n\n# 关闭线程池\n\n可以通过shutdown和shutdownNow方法来关闭线程池。他们的原理是遍历线程池的中工作线程,调用其interrupt()方法来中断线程,所以,无法相应中断的任务永远无法被终止。但是他们也存在一定的区别:\n\n * shutdown:将线程的状态设置为 SHUTDOWN 状态,然后中断没有执行任务的线程。\n * shutdownNow:首先将线程的状态设置为 STOP ,然后尝试停止所有正在执行或者暂停任务的线程,并返回队列中的待执行任务。\n\n只要调用了这两个方法中的任意一个,isShutdown()方法就会返回true,当所有任务都关闭后,才表示线程池关闭成功,这是调用isTerminaed()方法才会返回 true 。\n\n\n通常调用 shutdown 来关闭线程池,如果不需要等任务执行完可以调用 shutdownNow 。\n\n\n# 合理的配置线程池\n\n 1. 最大线程数 maximumPoolSize 的是指可参考如下规则:\n * CPU 密集型任务应该配置尽可能小的线程,如cpu数量+1。\n * IO密集型任务线程并不是一直占用cpu,则应该适度更多的配置线程,如2*cpu数量\n * 可以通过 Runtime.getRuntime().availableProcessors() 获得当前设备的 cpu 个数。\n 2. 建议使用有界队列。如果任务执行速度变慢,线程池不断向队列中 add 元素,会有内存溢出的风险。", - "normalizedContent": "# 使用线程池有哪些好处\n\n 1. 降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁的消耗。\n 2. 提高相应速度。当任务到达时,任务可以不需要线程创建就可以执行。\n 3. 提高线程的可管理性。线程是稀缺资源、如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性。使用线程池可以统一分配、调优和监控。\n\n\n# 线程池的创建\n\n我们可以通过 threadpoolexecutor 来创建一个线程池\n\npublic threadpoolexecutor(int corepoolsize,\n int maximumpoolsize,\n long keepalivetime,\n timeunit unit,\n blockingqueue workqueue,\n threadfactory threadfactory,\n rejectedexecutionhandler handler) {//...}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n创建一个线程池需要传入如下几个参数:\n\n * corepoolsize: 线程池中的常驻核心线程数。\n 当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的核心线程能够执行新任务也会创建线程。当线程池中的线程数量到达 corepoolsize 时就不再创建。如果调用了线程池的 prestartallcorethread()方法,线程池会在启动后就创建所有核心线程。\n\n * workqueue: 存放任务的阻塞队列,被提交但尚未被执行的任务。\n 可以选择如下几个阻塞队列: 1.arrayblockingqueue 2.linkedblockingqueue:吞吐量高于linkedblockingqueue,是 executors.newfixedthreadpool()创建的线程池的默认队列。 3.synchronousqueue:一个不存储元素的队列。每个提交的任务都必须等到线程来执行,否则阻塞提交。是 executors.newcachedthreadpool()创建的线程池的默认队列。 4.priorityblockingqueue:一个具有优先级的无限阻塞队列。\n\n * maximumpoolsize: 线程池最大线程数量。\n 如果队列满了并且已经创建的线程数量小于最大线程数量,则线程池会再继续创建新的线程执行任务。值得注意的是如果使用无界队列 (比如linkedblockingqueue)这个参数就没有意义了。\n\n * threadfactory: 用于设置创建线程的工厂\n 可以给创建的线程设置有意义的名字,可方便排查问题\n\n * handler: 拒绝策略,表示当队列满了且工作线程都满了如何来拒绝请求执行的线程的策略,默认是abortpolicy策略,主要有四种类型。\n\n 1. abortpolicy 直接抛出regectedexcutionexception 异常阻止系统正常进行,默认策略\n\n 2. discardpolicy 直接丢弃任务,不予任何处理也不抛出异常,如果允许任务丢失,这是最好的一种方案\n\n 3. discardoldestpolicy 抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务\n\n 4. callerrunspolicy 交给线程池调用所在的线程进行处理,“调用者运行”的一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量\n\n以上内置拒绝策略均实现了 rejectexcutionhandler 接口,我们也可以实现接口rejectexcutionhandler来自定义策略,如记录日志或者持久化不能拒绝的任务。\n\n * keepalivetime: 非核心线程存活时间\n 线程池中非核心线程空闲的存活时间 当前线程池数量超过 corepoolsize 时,当空闲时间达到 keepalivetime 值时,非核心线程会被销毁直到只剩下 corepoolsize 个线程为止。\n \n\n> tips:如果任务很多并且任务执行时间比较短,可以调大 keepalivetime ,提高线程利用率。\n\n * unit: keepalivetime 的时间单位\n \n\n\n# 向线程池提交一个任务后,线程池是如何处理这个任务的呢?\n\n\n\n 1. 如果当前运行的线程数=corepoolsize,则将任务加入阻塞队列。\n 3. 如果队列已满或者不能加入,接下来的处理分两种情况:\n 1. 线程池中的线程是否都处于工作状态。创建新的线程来执行任务(这一步骤需要获取全局锁)。\n 2. 线程池中有空闲的线程,则用其来执行任务。\n 4. 如果创建新线程会导致线程数量>maximumpoolsize,则执行拒绝策略。\n\n我们再从源码的角度来理解下这个过程:\nthreadpoolexecutor#execute(runnble command) 方法\n\npublic void execute(runnable command) {\n // 参数校验\n if (command == null)\n throw new nullpointerexception();\n \n // 从 c 的值可以判断出线程池的状态, 以及线程池中线程的数量\n int c = ctl.get();\n // 1. 如果线程池的线程数量 小于 核心线程数\n if (workercountof(c) < corepoolsize) {\n // 添加一个核心线程 command 表示一个具体的任务, true 表示为核心线程\n if (addworker(command, true))\n return;\n c = ctl.get();\n }\n // 2. 如果线程池处于 running 状态(只有处于此状态,才能接受新的任务)\n // 并且线程池的数量大于核心线程数, 就把任务添加到阻塞队列\n if (isrunning(c) && workqueue.offer(command)) {\n int recheck = ctl.get();\n if (! isrunning(recheck) && remove(command))\n reject(command);\n else if (workercountof(recheck) == 0)\n addworker(null, false);\n }\n // 3. 如果队列也满了,就创建一个非核心线程(core==false)\n else if (!addworker(command, false))\n // 如果创建失败,就执行拒绝策略\n reject(command);\n}\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n\n\n\n# 线程池是如何工作的呢?\n\n线程池中有一个 workers 集合,里面记录了所有的工作线程,只有拿到 mainlock 锁的线程才能访问。 threadpoolexecutor#workers 属性\n\nprivate final hashset workers = new hashset();\n\n\n1\n\n\n线程池创建线程时,会将线程包装成 worker ,worker 在执行任务后还会循环获取队里里的任务。我们可以从 worker 类的 run() 方法看到这一点。\n\nfinal void runworker(worker w) {\n thread wt = thread.currentthread();\n runnable task = w.firsttask;\n w.firsttask = null;\n w.unlock(); // allow interrupts\n boolean completedabruptly = true;\n try {\n while (task != null || (task = gettask()) != null) {\n w.lock();\n if ((runstateatleast(ctl.get(), stop) ||\n (thread.interrupted() &&\n runstateatleast(ctl.get(), stop))) &&\n !wt.isinterrupted())\n wt.interrupt();\n try {\n beforeexecute(wt, task);\n throwable thrown = null;\n try {\n task.run();\n } catch (runtimeexception x) {\n thrown = x; throw x;\n } catch (error x) {\n thrown = x; throw x;\n } catch (throwable x) {\n thrown = x; throw new error(x);\n } finally {\n afterexecute(task, thrown);\n }\n } finally {\n task = null;\n w.completedtasks++;\n w.unlock();\n }\n }\n completedabruptly = false;\n } finally {\n processworkerexit(w, completedabruptly);\n }\n }\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n\n\n上面代码关键点是while循环和gettask()方法,通过循环不断的调用gettask()从阻塞队列中获取任务,通过这个方法,它与阻塞队列建立桥梁。\n\n\n# 向线程池提交任务\n\n有两个方法提交任务,分别是execute(),submit()\n\n\n * execute():execute只能提交runnable类型的任务,用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。如果遇到异常会直接抛出。使用方法如下:\n\npublic static void main(string[] args) throws exception{\n executorservice es = executors.newsinglethreadexecutor();\n runnable runnable = new runnable() {\n @override\n public void run() {\n system.out.println(\"runnable线程处理开始...\");\n }\n };\n es.execute(runnable);\n es.shutdown();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n * submit():submit既可以提交runnable类型的任务,也可以提交callable类型的任务,会有一个类型为future的返回值,但当任务类型为runnable时,返回值为null。如果遇到异常不会直接抛出,只有在使用future的get方法获取返回值时,才会抛出异常。使用方法:\n\npublic static void main(string[] args) throws exception{\n executorservice es = executors.newsinglethreadexecutor();\n callable callable = new callable() {\n @override\n public string call() throws exception {\n system.out.println(\"线程处理开始...\");\n return \"hello world\";\n }\n };\n future future = es.submit(callable);\n while(true) {\n //iddone:如果任务已完成,则返回 true。 可能由于正常终止、异常或取消而完成,在所有这些情况中,此方法都将返回 true。\n if(future.isdone()) {\n system.out.println(\"任务执行完成:\" + future.get());\n break;\n }\n }\n es.shutdown();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n\n\n\n# 关闭线程池\n\n可以通过shutdown和shutdownnow方法来关闭线程池。他们的原理是遍历线程池的中工作线程,调用其interrupt()方法来中断线程,所以,无法相应中断的任务永远无法被终止。但是他们也存在一定的区别:\n\n * shutdown:将线程的状态设置为 shutdown 状态,然后中断没有执行任务的线程。\n * shutdownnow:首先将线程的状态设置为 stop ,然后尝试停止所有正在执行或者暂停任务的线程,并返回队列中的待执行任务。\n\n只要调用了这两个方法中的任意一个,isshutdown()方法就会返回true,当所有任务都关闭后,才表示线程池关闭成功,这是调用isterminaed()方法才会返回 true 。\n\n\n通常调用 shutdown 来关闭线程池,如果不需要等任务执行完可以调用 shutdownnow 。\n\n\n# 合理的配置线程池\n\n 1. 最大线程数 maximumpoolsize 的是指可参考如下规则:\n * cpu 密集型任务应该配置尽可能小的线程,如cpu数量+1。\n * io密集型任务线程并不是一直占用cpu,则应该适度更多的配置线程,如2*cpu数量\n * 可以通过 runtime.getruntime().availableprocessors() 获得当前设备的 cpu 个数。\n 2. 建议使用有界队列。如果任务执行速度变慢,线程池不断向队列中 add 元素,会有内存溢出的风险。", + "headersStr": "Lock 接口 VS Synchronized Lock 锁的常规用法 Lock 锁的API", + "content": "Lock 实现提供比使用 synchronized 方法和语句可以获得的更广泛的锁定操作。 它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象Condition 。\n\n虽然 synchronized 方法和语句的范围机制使得使用监视器锁更容易编程,并且有助于避免涉及锁的许多常见编程错误,但是有时您需要以更灵活的方式处理锁。\n\n例如,您获取节点A的锁定,然后获取节点B,然后释放A并获取C,然后释放B并获得D等。\n\n所述的实施方式中Lock接口通过允许获得并在不同的范围释放的锁,并允许获得并以任何顺序释放多个锁使得能够使用这样的技术。\n\n\n# Lock 接口 VS Synchronized\n\nJava SE 5之后,juc 包中增加了 Lock 接口,在此之前 Java 是靠 Synchronized 关键字来实现锁功能的,二者具有相同的内存同步语义,区别如下:\n\n SYNCHRONIZED LOCK接口\n加锁、解锁 隐式的加锁、解锁 显式的加锁、解锁\n是否异常自动释放锁 是 不是,必须手动unlock来释放锁\n是否支持相应中断 只能等待锁的释放,不能响应中断 等待锁过程中可以用interrupt来中断等待\n适用场景 比如这种场景:先获得A锁,再获取B锁,获取到B锁释放A锁同时获取C锁,获得C锁后释放B锁在获取D锁。这种场景用Synchronized就不好实现了。\n\nLock 相比于 Synchronized 还有如下特性:\n\n * 尝试非阻塞获取锁\n * 能被中断的获取锁\n * 超时获取锁\n\n\n# Lock 锁的常规用法\n\n Lock l = ...; \n l.lock(); \n try {\n // access the resource protected by this lock }\n finally { \n l.unlock(); \n } \n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# Lock 锁的API\n\nLock 接口定义了所得获取和释放的基本操作, Lock的API如下:\n\n方法 描述\nvoid lock() 获得锁\nvoid lockInterruptibly() 获取锁定,除非当前线程是 interrupted 。\nCondition newCondition() 返回一个新Condition绑定到该实例Lock实例。\nboolean tryLock() 只有在调用时才可以获得锁。\nboolean tryLock(long time, TimeUnit unit) 如果在给定的等待时间内是空闲的,并且当前的线程尚未得到 interrupted,则获取该锁\nvoid unlock() 释放锁。", + "normalizedContent": "lock 实现提供比使用 synchronized 方法和语句可以获得的更广泛的锁定操作。 它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象condition 。\n\n虽然 synchronized 方法和语句的范围机制使得使用监视器锁更容易编程,并且有助于避免涉及锁的许多常见编程错误,但是有时您需要以更灵活的方式处理锁。\n\n例如,您获取节点a的锁定,然后获取节点b,然后释放a并获取c,然后释放b并获得d等。\n\n所述的实施方式中lock接口通过允许获得并在不同的范围释放的锁,并允许获得并以任何顺序释放多个锁使得能够使用这样的技术。\n\n\n# lock 接口 vs synchronized\n\njava se 5之后,juc 包中增加了 lock 接口,在此之前 java 是靠 synchronized 关键字来实现锁功能的,二者具有相同的内存同步语义,区别如下:\n\n synchronized lock接口\n加锁、解锁 隐式的加锁、解锁 显式的加锁、解锁\n是否异常自动释放锁 是 不是,必须手动unlock来释放锁\n是否支持相应中断 只能等待锁的释放,不能响应中断 等待锁过程中可以用interrupt来中断等待\n适用场景 比如这种场景:先获得a锁,再获取b锁,获取到b锁释放a锁同时获取c锁,获得c锁后释放b锁在获取d锁。这种场景用synchronized就不好实现了。\n\nlock 相比于 synchronized 还有如下特性:\n\n * 尝试非阻塞获取锁\n * 能被中断的获取锁\n * 超时获取锁\n\n\n# lock 锁的常规用法\n\n lock l = ...; \n l.lock(); \n try {\n // access the resource protected by this lock }\n finally { \n l.unlock(); \n } \n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# lock 锁的api\n\nlock 接口定义了所得获取和释放的基本操作, lock的api如下:\n\n方法 描述\nvoid lock() 获得锁\nvoid lockinterruptibly() 获取锁定,除非当前线程是 interrupted 。\ncondition newcondition() 返回一个新condition绑定到该实例lock实例。\nboolean trylock() 只有在调用时才可以获得锁。\nboolean trylock(long time, timeunit unit) 如果在给定的等待时间内是空闲的,并且当前的线程尚未得到 interrupted,则获取该锁\nvoid unlock() 释放锁。", "charsets": { "cjk": true }, @@ -1246,11 +1203,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "什么是公平锁?什么是非公平锁?", + "title": "volatile 关键字解决了什么问题,它的实现原理是什么?", "frontmatter": { - "title": "什么是公平锁?什么是非公平锁?", - "date": "2022-04-04T14:35:15.000Z", - "permalink": "/pages/fd6a59/", + "title": "volatile 关键字解决了什么问题,它的实现原理是什么?", + "date": "2022-04-04T14:34:07.000Z", + "permalink": "/pages/383264/", "categories": [ "Java相关", "并发" @@ -1259,22 +1216,93 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/090.%E4%BB%80%E4%B9%88%E6%98%AF%E5%85%AC%E5%B9%B3%E9%94%81%EF%BC%9F%E4%BB%80%E4%B9%88%E6%98%AF%E9%9D%9E%E5%85%AC%E5%B9%B3%E9%94%81%EF%BC%9F.html", - "relativePath": "01.Java相关/20.并发/090.什么是公平锁?什么是非公平锁?.md", - "key": "v-134f28dc", - "path": "/pages/fd6a59/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/060.volatile%20%E5%85%B3%E9%94%AE%E5%AD%97%E8%A7%A3%E5%86%B3%E4%BA%86%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98%EF%BC%8C%E5%AE%83%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", + "relativePath": "01.Java相关/20.并发/060.volatile 关键字解决了什么问题,它的实现原理是什么?.md", + "key": "v-f5bcd180", + "path": "/pages/383264/", "headers": [ { "level": 2, - "title": "什么是公平锁?什么是非公平锁?", - "slug": "什么是公平锁-什么是非公平锁", - "normalizedTitle": "什么是公平锁?什么是非公平锁?", - "charIndex": 2 + "title": "Java中的内存可见性", + "slug": "java中的内存可见性", + "normalizedTitle": "java中的内存可见性", + "charIndex": 129 + }, + { + "level": 2, + "title": "Volatile 的实现原理", + "slug": "volatile-的实现原理", + "normalizedTitle": "volatile 的实现原理", + "charIndex": 335 } ], - "headersStr": "什么是公平锁?什么是非公平锁?", - "content": "# 什么是公平锁?什么是非公平锁?\n\n其中sync是AbstractQueuedSynchronizer的实现,我们知道,ReentrantLock支持公平锁和非公平锁,其实现类分别是FairSync和NonfairSync,我们看看公平锁和非公平锁分别是怎么实现的\n\n// FairSync 公平锁的实现\nfinal void lock() {\n acquire(1);\n}\n\n// NonfairSync 非公平锁的实现\nfinal void lock() {\n if (compareAndSetState(0, 1))\n setExclusiveOwnerThread(Thread.currentThread());\n else\n acquire(1);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n可以看到,非公平锁的实现仅仅是多了一个步骤:通过CAS的方式(compareAndSetState)尝试改变state的状态,修改成功后设置当前线程以独占的方式获取了锁,修改失败执行的逻辑和公平锁一样。\n\n从这段代码中可以看到,独占锁加锁的核心逻辑就是acquire方法,接下来就看看这个方法\n\npublic final void acquire(int arg) {\n if (!tryAcquire(arg) &&\n acquireQueued(addWaiter(Node.EXCLUSIVE), arg))\n selfInterrupt();\n}\n\n\n1\n2\n3\n4\n5\n\n\n这个方法是 AQS 的核心代码。", - "normalizedContent": "# 什么是公平锁?什么是非公平锁?\n\n其中sync是abstractqueuedsynchronizer的实现,我们知道,reentrantlock支持公平锁和非公平锁,其实现类分别是fairsync和nonfairsync,我们看看公平锁和非公平锁分别是怎么实现的\n\n// fairsync 公平锁的实现\nfinal void lock() {\n acquire(1);\n}\n\n// nonfairsync 非公平锁的实现\nfinal void lock() {\n if (compareandsetstate(0, 1))\n setexclusiveownerthread(thread.currentthread());\n else\n acquire(1);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n可以看到,非公平锁的实现仅仅是多了一个步骤:通过cas的方式(compareandsetstate)尝试改变state的状态,修改成功后设置当前线程以独占的方式获取了锁,修改失败执行的逻辑和公平锁一样。\n\n从这段代码中可以看到,独占锁加锁的核心逻辑就是acquire方法,接下来就看看这个方法\n\npublic final void acquire(int arg) {\n if (!tryacquire(arg) &&\n acquirequeued(addwaiter(node.exclusive), arg))\n selfinterrupt();\n}\n\n\n1\n2\n3\n4\n5\n\n\n这个方法是 aqs 的核心代码。", + "headersStr": "Java中的内存可见性 Volatile 的实现原理", + "content": "volatile 能保证 Java 中的内存可见性。 可见性的意思是:当线程A修改共享变量的值后,线程B能立刻读到这个修改后的值。\n\n\nVolatile 不会引起线程上下文的切换和调度,如果使用的恰当,会比 Synchronized 执行成本更低。\n\n\n# Java中的内存可见性\n\n * 可见性:一个线程对共享变量值的修改,能够及时被其他线程看到。\n\n * 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那这个变量就是这几个线程的共享变量。\n\n * Java内存的规定:\n\n-线程对共享变量的所有操作都必须在自己的工作内存中进行,不可直接从主内存中读写; -不同线程之间无法直接访问其他线程工作内存中的变量,线程间的变量值的传递需要通过主内存。\n\n\n\n\n# Volatile 的实现原理\n\n如果对用 Volatile 修饰的变量写操作,JVM 会向处理器发出一条 Lock 前缀的指令,Lock 前缀的指令在多核处理器下会引发两件事情:\n\n 1. 将当前处理器缓存行的数据写会到系统内存\n\n 2. 这个写会内存的操作会使其他缓存中的该内存地址的数据无效", + "normalizedContent": "volatile 能保证 java 中的内存可见性。 可见性的意思是:当线程a修改共享变量的值后,线程b能立刻读到这个修改后的值。\n\n\nvolatile 不会引起线程上下文的切换和调度,如果使用的恰当,会比 synchronized 执行成本更低。\n\n\n# java中的内存可见性\n\n * 可见性:一个线程对共享变量值的修改,能够及时被其他线程看到。\n\n * 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那这个变量就是这几个线程的共享变量。\n\n * java内存的规定:\n\n-线程对共享变量的所有操作都必须在自己的工作内存中进行,不可直接从主内存中读写; -不同线程之间无法直接访问其他线程工作内存中的变量,线程间的变量值的传递需要通过主内存。\n\n\n\n\n# volatile 的实现原理\n\n如果对用 volatile 修饰的变量写操作,jvm 会向处理器发出一条 lock 前缀的指令,lock 前缀的指令在多核处理器下会引发两件事情:\n\n 1. 将当前处理器缓存行的数据写会到系统内存\n\n 2. 这个写会内存的操作会使其他缓存中的该内存地址的数据无效", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, + { + "title": "简述 Java 锁升级的机制", + "frontmatter": { + "title": "简述 Java 锁升级的机制", + "date": "2022-04-04T14:34:27.000Z", + "permalink": "/pages/ff0616/", + "categories": [ + "Java相关", + "并发" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/070.%E7%AE%80%E8%BF%B0%20Java%20%E9%94%81%E5%8D%87%E7%BA%A7%E7%9A%84%E6%9C%BA%E5%88%B6.html", + "relativePath": "01.Java相关/20.并发/070.简述 Java 锁升级的机制.md", + "key": "v-1065f7be", + "path": "/pages/ff0616/", + "headers": [ + { + "level": 3, + "title": "偏向锁", + "slug": "偏向锁", + "normalizedTitle": "偏向锁", + "charIndex": 83 + }, + { + "level": 3, + "title": "轻量级锁", + "slug": "轻量级锁", + "normalizedTitle": "轻量级锁", + "charIndex": 94 + }, + { + "level": 3, + "title": "重量级锁", + "slug": "重量级锁", + "normalizedTitle": "重量级锁", + "charIndex": 18 + }, + { + "level": 3, + "title": "锁升级", + "slug": "锁升级", + "normalizedTitle": "锁升级", + "charIndex": 659 + }, + { + "level": 3, + "title": "锁的优缺点对比", + "slug": "锁的优缺点对比", + "normalizedTitle": "锁的优缺点对比", + "charIndex": 910 + } + ], + "headersStr": "偏向锁 轻量级锁 重量级锁 锁升级 锁的优缺点对比", + "content": "Synchronized 一直被称为重量级锁。但是在JDK 1.6之后它已经变得不那么重了。JDK 1.6 对Synchronized 的优化点在于:\n\n * 引入了偏向锁\n\n * 引入了轻量级锁\n\n在JDK 1.6 中,Synchronized 锁有四种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁、重量级锁。\n\n\n这几个状态会随着锁竞争升级,但是不可以降级。\n\n\n# 偏向锁\n\n为什么引入偏向锁?\n\n\n不存锁竞争,或者总是由同一线程多次获得锁的场景,偏向锁的代价更低。\n\n当一个线程访问同步块并获取到锁时,在锁对象头记录该线程的id,以后该线程进入和退出该同步块时不需要CAS来加锁和解锁。\n\n偏向锁何时释放?\n\n\n偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。\n\n\n\n偏向锁一定起到正面作用吗?\n\n\n不是的。\n\n\n偏向锁的适用场景是:不存锁竞争,或者总是由同一线程多次获得锁的场景。\n\n如果你确定你的程序中 锁通常处于竞争状态,可以通过JVM参数关闭偏向锁。关闭后,程序回魔人进入轻量级锁状态。\n\n\n-XX:UseBiasedLocking=false\n\n\n\n1\n2\n3\n\n\n\n# 轻量级锁\n\n轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋(关于自旋的介绍见文末)的形式尝试获取锁,线程不会阻塞,从而提高性能。\n\n轻量级锁的获取主要由两种情况:① 当关闭偏向锁功能时;② 由于多个线程竞争偏向锁导致偏向锁升级为轻量级锁。\n\n轻量级锁何时升级为重量级锁?\n\n\n若当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁(锁膨胀)。\n\n另外,当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁(锁膨胀)。\n\n\n# 重量级锁\n\n重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。\n\n重量级锁的获取是释放一般会有线程上下文切换,代价是比较大的,所以说是重量级锁。\n\n\n# 锁升级\n\n\n\n\n# 锁的优缺点对比\n\n", + "normalizedContent": "synchronized 一直被称为重量级锁。但是在jdk 1.6之后它已经变得不那么重了。jdk 1.6 对synchronized 的优化点在于:\n\n * 引入了偏向锁\n\n * 引入了轻量级锁\n\n在jdk 1.6 中,synchronized 锁有四种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁、重量级锁。\n\n\n这几个状态会随着锁竞争升级,但是不可以降级。\n\n\n# 偏向锁\n\n为什么引入偏向锁?\n\n\n不存锁竞争,或者总是由同一线程多次获得锁的场景,偏向锁的代价更低。\n\n当一个线程访问同步块并获取到锁时,在锁对象头记录该线程的id,以后该线程进入和退出该同步块时不需要cas来加锁和解锁。\n\n偏向锁何时释放?\n\n\n偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。\n\n\n\n偏向锁一定起到正面作用吗?\n\n\n不是的。\n\n\n偏向锁的适用场景是:不存锁竞争,或者总是由同一线程多次获得锁的场景。\n\n如果你确定你的程序中 锁通常处于竞争状态,可以通过jvm参数关闭偏向锁。关闭后,程序回魔人进入轻量级锁状态。\n\n\n-xx:usebiasedlocking=false\n\n\n\n1\n2\n3\n\n\n\n# 轻量级锁\n\n轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋(关于自旋的介绍见文末)的形式尝试获取锁,线程不会阻塞,从而提高性能。\n\n轻量级锁的获取主要由两种情况:① 当关闭偏向锁功能时;② 由于多个线程竞争偏向锁导致偏向锁升级为轻量级锁。\n\n轻量级锁何时升级为重量级锁?\n\n\n若当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁(锁膨胀)。\n\n另外,当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁(锁膨胀)。\n\n\n# 重量级锁\n\n重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。\n\n重量级锁的获取是释放一般会有线程上下文切换,代价是比较大的,所以说是重量级锁。\n\n\n# 锁升级\n\n\n\n\n# 锁的优缺点对比\n\n", "charsets": { "cjk": true }, @@ -1395,11 +1423,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "简述 Java 锁升级的机制", + "title": "什么是公平锁?什么是非公平锁?", "frontmatter": { - "title": "简述 Java 锁升级的机制", - "date": "2022-04-04T14:34:27.000Z", - "permalink": "/pages/ff0616/", + "title": "什么是公平锁?什么是非公平锁?", + "date": "2022-04-04T14:35:15.000Z", + "permalink": "/pages/fd6a59/", "categories": [ "Java相关", "并发" @@ -1408,50 +1436,79 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/070.%E7%AE%80%E8%BF%B0%20Java%20%E9%94%81%E5%8D%87%E7%BA%A7%E7%9A%84%E6%9C%BA%E5%88%B6.html", - "relativePath": "01.Java相关/20.并发/070.简述 Java 锁升级的机制.md", - "key": "v-1065f7be", - "path": "/pages/ff0616/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/090.%E4%BB%80%E4%B9%88%E6%98%AF%E5%85%AC%E5%B9%B3%E9%94%81%EF%BC%9F%E4%BB%80%E4%B9%88%E6%98%AF%E9%9D%9E%E5%85%AC%E5%B9%B3%E9%94%81%EF%BC%9F.html", + "relativePath": "01.Java相关/20.并发/090.什么是公平锁?什么是非公平锁?.md", + "key": "v-134f28dc", + "path": "/pages/fd6a59/", "headers": [ { - "level": 3, - "title": "偏向锁", - "slug": "偏向锁", - "normalizedTitle": "偏向锁", - "charIndex": 83 - }, - { - "level": 3, - "title": "轻量级锁", - "slug": "轻量级锁", - "normalizedTitle": "轻量级锁", - "charIndex": 94 - }, - { - "level": 3, - "title": "重量级锁", - "slug": "重量级锁", - "normalizedTitle": "重量级锁", - "charIndex": 18 - }, + "level": 2, + "title": "什么是公平锁?什么是非公平锁?", + "slug": "什么是公平锁-什么是非公平锁", + "normalizedTitle": "什么是公平锁?什么是非公平锁?", + "charIndex": 2 + } + ], + "headersStr": "什么是公平锁?什么是非公平锁?", + "content": "# 什么是公平锁?什么是非公平锁?\n\n其中sync是AbstractQueuedSynchronizer的实现,我们知道,ReentrantLock支持公平锁和非公平锁,其实现类分别是FairSync和NonfairSync,我们看看公平锁和非公平锁分别是怎么实现的\n\n// FairSync 公平锁的实现\nfinal void lock() {\n acquire(1);\n}\n\n// NonfairSync 非公平锁的实现\nfinal void lock() {\n if (compareAndSetState(0, 1))\n setExclusiveOwnerThread(Thread.currentThread());\n else\n acquire(1);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n可以看到,非公平锁的实现仅仅是多了一个步骤:通过CAS的方式(compareAndSetState)尝试改变state的状态,修改成功后设置当前线程以独占的方式获取了锁,修改失败执行的逻辑和公平锁一样。\n\n从这段代码中可以看到,独占锁加锁的核心逻辑就是acquire方法,接下来就看看这个方法\n\npublic final void acquire(int arg) {\n if (!tryAcquire(arg) &&\n acquireQueued(addWaiter(Node.EXCLUSIVE), arg))\n selfInterrupt();\n}\n\n\n1\n2\n3\n4\n5\n\n\n这个方法是 AQS 的核心代码。", + "normalizedContent": "# 什么是公平锁?什么是非公平锁?\n\n其中sync是abstractqueuedsynchronizer的实现,我们知道,reentrantlock支持公平锁和非公平锁,其实现类分别是fairsync和nonfairsync,我们看看公平锁和非公平锁分别是怎么实现的\n\n// fairsync 公平锁的实现\nfinal void lock() {\n acquire(1);\n}\n\n// nonfairsync 非公平锁的实现\nfinal void lock() {\n if (compareandsetstate(0, 1))\n setexclusiveownerthread(thread.currentthread());\n else\n acquire(1);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n可以看到,非公平锁的实现仅仅是多了一个步骤:通过cas的方式(compareandsetstate)尝试改变state的状态,修改成功后设置当前线程以独占的方式获取了锁,修改失败执行的逻辑和公平锁一样。\n\n从这段代码中可以看到,独占锁加锁的核心逻辑就是acquire方法,接下来就看看这个方法\n\npublic final void acquire(int arg) {\n if (!tryacquire(arg) &&\n acquirequeued(addwaiter(node.exclusive), arg))\n selfinterrupt();\n}\n\n\n1\n2\n3\n4\n5\n\n\n这个方法是 aqs 的核心代码。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, + { + "title": "Java 是如何实现线程安全的,哪些数据结构是线程安全的?", + "frontmatter": { + "title": "Java 是如何实现线程安全的,哪些数据结构是线程安全的?", + "date": "2022-04-04T14:35:56.000Z", + "permalink": "/pages/c1f760/", + "categories": [ + "Java相关", + "并发" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/110.Java%20%E6%98%AF%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E7%9A%84%EF%BC%8C%E5%93%AA%E4%BA%9B%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E6%98%AF%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E7%9A%84%EF%BC%9F.html", + "relativePath": "01.Java相关/20.并发/110.Java 是如何实现线程安全的,哪些数据结构是线程安全的?.md", + "key": "v-625fbeda", + "path": "/pages/c1f760/", + "headers": [ { - "level": 3, - "title": "锁升级", - "slug": "锁升级", - "normalizedTitle": "锁升级", - "charIndex": 659 + "level": 2, + "title": "Java 是如何实现线程安全的,哪些数据结构是线程安全的?", + "slug": "java-是如何实现线程安全的-哪些数据结构是线程安全的", + "normalizedTitle": "java 是如何实现线程安全的,哪些数据结构是线程安全的?", + "charIndex": 2 }, { - "level": 3, - "title": "锁的优缺点对比", - "slug": "锁的优缺点对比", - "normalizedTitle": "锁的优缺点对比", - "charIndex": 910 + "level": 2, + "title": "synchronized 来实现售票场景", + "slug": "synchronized-来实现售票场景", + "normalizedTitle": "synchronized 来实现售票场景", + "charIndex": 167 + }, + { + "level": 2, + "title": "Lock 接口来实现售票场景", + "slug": "lock-接口来实现售票场景", + "normalizedTitle": "lock 接口来实现售票场景", + "charIndex": 1352 + }, + { + "level": 2, + "title": "哪些数据结构是线程安全的?", + "slug": "哪些数据结构是线程安全的", + "normalizedTitle": "哪些数据结构是线程安全的?", + "charIndex": 18 } ], - "headersStr": "偏向锁 轻量级锁 重量级锁 锁升级 锁的优缺点对比", - "content": "Synchronized 一直被称为重量级锁。但是在JDK 1.6之后它已经变得不那么重了。JDK 1.6 对Synchronized 的优化点在于:\n\n * 引入了偏向锁\n\n * 引入了轻量级锁\n\n在JDK 1.6 中,Synchronized 锁有四种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁、重量级锁。\n\n\n这几个状态会随着锁竞争升级,但是不可以降级。\n\n\n# 偏向锁\n\n为什么引入偏向锁?\n\n\n不存锁竞争,或者总是由同一线程多次获得锁的场景,偏向锁的代价更低。\n\n当一个线程访问同步块并获取到锁时,在锁对象头记录该线程的id,以后该线程进入和退出该同步块时不需要CAS来加锁和解锁。\n\n偏向锁何时释放?\n\n\n偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。\n\n\n\n偏向锁一定起到正面作用吗?\n\n\n不是的。\n\n\n偏向锁的适用场景是:不存锁竞争,或者总是由同一线程多次获得锁的场景。\n\n如果你确定你的程序中 锁通常处于竞争状态,可以通过JVM参数关闭偏向锁。关闭后,程序回魔人进入轻量级锁状态。\n\n\n-XX:UseBiasedLocking=false\n\n\n\n1\n2\n3\n\n\n\n# 轻量级锁\n\n轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋(关于自旋的介绍见文末)的形式尝试获取锁,线程不会阻塞,从而提高性能。\n\n轻量级锁的获取主要由两种情况:① 当关闭偏向锁功能时;② 由于多个线程竞争偏向锁导致偏向锁升级为轻量级锁。\n\n轻量级锁何时升级为重量级锁?\n\n\n若当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁(锁膨胀)。\n\n另外,当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁(锁膨胀)。\n\n\n# 重量级锁\n\n重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。\n\n重量级锁的获取是释放一般会有线程上下文切换,代价是比较大的,所以说是重量级锁。\n\n\n# 锁升级\n\n\n\n\n# 锁的优缺点对比\n\n", - "normalizedContent": "synchronized 一直被称为重量级锁。但是在jdk 1.6之后它已经变得不那么重了。jdk 1.6 对synchronized 的优化点在于:\n\n * 引入了偏向锁\n\n * 引入了轻量级锁\n\n在jdk 1.6 中,synchronized 锁有四种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁、重量级锁。\n\n\n这几个状态会随着锁竞争升级,但是不可以降级。\n\n\n# 偏向锁\n\n为什么引入偏向锁?\n\n\n不存锁竞争,或者总是由同一线程多次获得锁的场景,偏向锁的代价更低。\n\n当一个线程访问同步块并获取到锁时,在锁对象头记录该线程的id,以后该线程进入和退出该同步块时不需要cas来加锁和解锁。\n\n偏向锁何时释放?\n\n\n偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。\n\n\n\n偏向锁一定起到正面作用吗?\n\n\n不是的。\n\n\n偏向锁的适用场景是:不存锁竞争,或者总是由同一线程多次获得锁的场景。\n\n如果你确定你的程序中 锁通常处于竞争状态,可以通过jvm参数关闭偏向锁。关闭后,程序回魔人进入轻量级锁状态。\n\n\n-xx:usebiasedlocking=false\n\n\n\n1\n2\n3\n\n\n\n# 轻量级锁\n\n轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋(关于自旋的介绍见文末)的形式尝试获取锁,线程不会阻塞,从而提高性能。\n\n轻量级锁的获取主要由两种情况:① 当关闭偏向锁功能时;② 由于多个线程竞争偏向锁导致偏向锁升级为轻量级锁。\n\n轻量级锁何时升级为重量级锁?\n\n\n若当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁(锁膨胀)。\n\n另外,当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁(锁膨胀)。\n\n\n# 重量级锁\n\n重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。\n\n重量级锁的获取是释放一般会有线程上下文切换,代价是比较大的,所以说是重量级锁。\n\n\n# 锁升级\n\n\n\n\n# 锁的优缺点对比\n\n", + "headersStr": "Java 是如何实现线程安全的,哪些数据结构是线程安全的? synchronized 来实现售票场景 Lock 接口来实现售票场景 哪些数据结构是线程安全的?", + "content": "# Java 是如何实现线程安全的,哪些数据结构是线程安全的?\n\n如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。\n\nJava 中实现线程安全的方式有两个:\n\n * synchronized\n * Lock接口\n\n举例一个售票场景:火车站4个窗口同时售票,共有3张票,不能超卖。\n\n\n# synchronized 来实现售票场景\n\npublic class ThreadSynchronizedSecurity {\n \n static int tickets = 3;\n \n class SellTickets implements Runnable {\n @Override\n public void run() {\n // 同步代码块\n synchronized (this) {\n if (tickets <= 0) {\n System.out.println(Thread.currentThread().getName() + \"--->票已售罄!\");\n return;\n }\n System.out.println(Thread.currentThread().getName() + \"--->售出第: \" + tickets + \" 张票\");\n tickets--;\n }\n }\n }\n \n public static void main(String[] args) {\n SellTickets sell = new ThreadSynchronizedSecurity().new SellTickets();\n Thread thread1 = new Thread(sell, \"1号窗口\");\n Thread thread2 = new Thread(sell, \"2号窗口\");\n Thread thread3 = new Thread(sell, \"3号窗口\");\n Thread thread4 = new Thread(sell, \"4号窗口\");\n thread1.start();\n thread2.start();\n thread3.start();\n thread4.start();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n\n\n输出:\n\n1号窗口--->售出第: 3 张票\n2号窗口--->售出第: 2 张票\n3号窗口--->售出第: 3 张票\n4号窗口--->票已售罄!\n\n\n1\n2\n3\n4\n\n\n\n# Lock 接口来实现售票场景\n\npackage com.my.annotate.thread;\n \nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\n \npublic class ThreadLockSecurity {\n static int tickets = 3;\n \n class SellTickets implements Runnable {\n Lock lock = new ReentrantLock();\n @Override\n public void run() {\n // Lock锁机制\n if (tickets > 0) {\n try {\n lock.lock();\n if (tickets <= 0) {\n return;\n }\n System.out.println(Thread.currentThread().getName() + \"--->售出第: \" + tickets + \" 票\");\n tickets--;\n } catch (Exception e1) {\n // TODO Auto-generated catch block\n e1.printStackTrace();\n } finally {\n lock.unlock();\n }\n }\n if (tickets <= 0) {\n System.out.println(Thread.currentThread().getName() + \"--->票已售罄!\");\n }\n \n }\n }\n \n \n public static void main(String[] args) {\n SellTickets sell = new ThreadLockSecurity().new SellTickets();\n Thread thread1 = new Thread(sell, \"1号窗口\");\n Thread thread2 = new Thread(sell, \"2号窗口\");\n Thread thread3 = new Thread(sell, \"3号窗口\");\n Thread thread4 = new Thread(sell, \"4号窗口\");\n thread1.start();\n thread2.start();\n thread3.start();\n thread4.start();\n }\n \n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n\n\n输出:\n\n1号窗口--->售出第: 3 张票\n2号窗口--->售出第: 2 张票\n3号窗口--->售出第: 3 张票\n4号窗口--->票已售罄!\n\n\n1\n2\n3\n4\n\n\n\n# 哪些数据结构是线程安全的?\n\nJDK已经为大家准备好了一批好用的线程安全容器类,可以大大减少开发工作量,例如HashTable,ConcurrentHashMap,CopyOnWriteArrayList,CopyOnWriteArraySet,ConcurrentLinkedQueue,Vector,StringBuffer等。\n\n 1. HashTable\n\nHashTable实现了Map接口,为此其本身也是一个散列表,它存储的内容是基于key-value的键值对映射。\n\nHashTable中的key、value都不可以为null;具有无序特性;由于其方法函数都是同步的(采用synchronized修饰),不会出现两个线程同时对数据进行操作的情况,因此保证了线程安全性。\n\nHashTable使用synchronized来修饰方法函数来保证线程安全,但是在多线程运行环境下效率表现非常低下。\n\n因为当一个线程访问HashTable的同步方法时,其他线程也访问同步方法就会粗线阻塞状态。\n\n比如当一个线程在添加数据时候,另外一个线程即使执行获取其他数据的操作也必须被阻塞,大大降低了程序的运行效率。\n\n 2. ConcurrentHashMap\n\n我们知道HashMap是线程不安全的,ConcurrentHashMap是HashMap的线程安全版。\n\n但是与HashTable相比,ConcurrentHashMap不仅保证了多线程运行环境下的数据访问安全性,而且性能上有长足的提升。\n\nConcurrentHashMap允许多个修改操作并发运行,其原因在于使用了锁分段技术:首先讲Map存放的数据分成一段一段的存储方式,然后给每一段数据分配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。这样就保证了每一把锁只是用于锁住一部分数据,那么当多线程访问Map里的不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率。\n\n上述的处理机制明显区别于HashTable是给整体数据分配了一把锁的处理方法。\n\n为此,在多线程环境下,常用ConcurrentHashMap在需要保证数据安全的场景中去替换HashMap,而不会去使用HashTable,同时在最新版的JDK中已经推荐废弃使用HashTable。\n\n 3. CopyOnWriteArrayList\n\nCopyOnWriteArrayList实现了List接口,提供的数据更新操作都使用了ReentrantLock的lock()方法来加锁,unlock()方法来解锁。\n\n当增加元素的时候,首先使用Arrays.copyOf()来拷贝形成新的副本,在副本上增加元素,然后改变原引用指向副本。读操作不需要加锁,而写操作类实现中对其进行了加锁。因此,CopyOnWriteArrayList类是一个线程安全的List接口的实现,在高并发的情况下,可以提供高性能的并发读取,并且保证读取的内容一定是正确的,这对于读操作远远多于写操作的应用非常适合(注意: 如上述更新操作会带来较大的空间与性能开销,如果更新操太过频繁,反而不太合适使用)。\n\n 4. CopyOnWriteArraySet\n\nCopyOnWriteArraySet是对CopyOnWriteArrayList使用了装饰模式后的具体实现。所以CopyOnWriteArrayList的实现机理适用于CopyOnWriteArraySet,此处不再赘述。\n\nJava里的List和Set的之间的特性比较结论同样适用于CopyOnWriteArrayList与CopyOnWriteArraySet之间的比较;此外,CopyOnWriteArrayList与CopyOnWriteArraySet都是线程安全的。\n\n 5. ConcurrentLinkedQueue\n\nConcurrentLinkedQueue可以被看作是一个线程安全的LinkedList,使用了非阻塞算法实现的一个高效、线程安全的并发队列;其本质是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当添加一个元素时会添加到队列的尾部;当获取一个元素时,会返回队列头部的元素。\n\nConcurrentLinkedQueue应该算是在高并发环境中性能最好的队列,没有之一。\n\n 6. Vector\n\nVector通过数组保存数据,继承了Abstract,实现了List;所以,其本质上是一个队列。\n\n但是和ArrayList不同,Vector中的操作是线程安全的,它是利用synchronized同步锁机制进行实现,其实现方式与HashTable类似。\n\n 7. StringBuffer与StringBuilder\n\n在Java里面,字符串操作应该是最频繁的操作了,为此有必要把StringBuffer与StringBuilder两个方法类比较一下。\n\n首先,对于频繁的字符串拼接操作,是不推荐采用效率低下的“+”操作的。一般是采用StringBuffer与StringBuilder来实现上述功能。但是,这两者也是有区别的:前者线程安全,后者不是线程安全的。\n\nStringBuffer是通过对方法函数进行synchronized修饰实现其线程安全特性,实现方式与HashTable、Vector类似。", + "normalizedContent": "# java 是如何实现线程安全的,哪些数据结构是线程安全的?\n\n如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。\n\njava 中实现线程安全的方式有两个:\n\n * synchronized\n * lock接口\n\n举例一个售票场景:火车站4个窗口同时售票,共有3张票,不能超卖。\n\n\n# synchronized 来实现售票场景\n\npublic class threadsynchronizedsecurity {\n \n static int tickets = 3;\n \n class selltickets implements runnable {\n @override\n public void run() {\n // 同步代码块\n synchronized (this) {\n if (tickets <= 0) {\n system.out.println(thread.currentthread().getname() + \"--->票已售罄!\");\n return;\n }\n system.out.println(thread.currentthread().getname() + \"--->售出第: \" + tickets + \" 张票\");\n tickets--;\n }\n }\n }\n \n public static void main(string[] args) {\n selltickets sell = new threadsynchronizedsecurity().new selltickets();\n thread thread1 = new thread(sell, \"1号窗口\");\n thread thread2 = new thread(sell, \"2号窗口\");\n thread thread3 = new thread(sell, \"3号窗口\");\n thread thread4 = new thread(sell, \"4号窗口\");\n thread1.start();\n thread2.start();\n thread3.start();\n thread4.start();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n\n\n输出:\n\n1号窗口--->售出第: 3 张票\n2号窗口--->售出第: 2 张票\n3号窗口--->售出第: 3 张票\n4号窗口--->票已售罄!\n\n\n1\n2\n3\n4\n\n\n\n# lock 接口来实现售票场景\n\npackage com.my.annotate.thread;\n \nimport java.util.concurrent.locks.lock;\nimport java.util.concurrent.locks.reentrantlock;\n \npublic class threadlocksecurity {\n static int tickets = 3;\n \n class selltickets implements runnable {\n lock lock = new reentrantlock();\n @override\n public void run() {\n // lock锁机制\n if (tickets > 0) {\n try {\n lock.lock();\n if (tickets <= 0) {\n return;\n }\n system.out.println(thread.currentthread().getname() + \"--->售出第: \" + tickets + \" 票\");\n tickets--;\n } catch (exception e1) {\n // todo auto-generated catch block\n e1.printstacktrace();\n } finally {\n lock.unlock();\n }\n }\n if (tickets <= 0) {\n system.out.println(thread.currentthread().getname() + \"--->票已售罄!\");\n }\n \n }\n }\n \n \n public static void main(string[] args) {\n selltickets sell = new threadlocksecurity().new selltickets();\n thread thread1 = new thread(sell, \"1号窗口\");\n thread thread2 = new thread(sell, \"2号窗口\");\n thread thread3 = new thread(sell, \"3号窗口\");\n thread thread4 = new thread(sell, \"4号窗口\");\n thread1.start();\n thread2.start();\n thread3.start();\n thread4.start();\n }\n \n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n\n\n输出:\n\n1号窗口--->售出第: 3 张票\n2号窗口--->售出第: 2 张票\n3号窗口--->售出第: 3 张票\n4号窗口--->票已售罄!\n\n\n1\n2\n3\n4\n\n\n\n# 哪些数据结构是线程安全的?\n\njdk已经为大家准备好了一批好用的线程安全容器类,可以大大减少开发工作量,例如hashtable,concurrenthashmap,copyonwritearraylist,copyonwritearrayset,concurrentlinkedqueue,vector,stringbuffer等。\n\n 1. hashtable\n\nhashtable实现了map接口,为此其本身也是一个散列表,它存储的内容是基于key-value的键值对映射。\n\nhashtable中的key、value都不可以为null;具有无序特性;由于其方法函数都是同步的(采用synchronized修饰),不会出现两个线程同时对数据进行操作的情况,因此保证了线程安全性。\n\nhashtable使用synchronized来修饰方法函数来保证线程安全,但是在多线程运行环境下效率表现非常低下。\n\n因为当一个线程访问hashtable的同步方法时,其他线程也访问同步方法就会粗线阻塞状态。\n\n比如当一个线程在添加数据时候,另外一个线程即使执行获取其他数据的操作也必须被阻塞,大大降低了程序的运行效率。\n\n 2. concurrenthashmap\n\n我们知道hashmap是线程不安全的,concurrenthashmap是hashmap的线程安全版。\n\n但是与hashtable相比,concurrenthashmap不仅保证了多线程运行环境下的数据访问安全性,而且性能上有长足的提升。\n\nconcurrenthashmap允许多个修改操作并发运行,其原因在于使用了锁分段技术:首先讲map存放的数据分成一段一段的存储方式,然后给每一段数据分配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。这样就保证了每一把锁只是用于锁住一部分数据,那么当多线程访问map里的不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率。\n\n上述的处理机制明显区别于hashtable是给整体数据分配了一把锁的处理方法。\n\n为此,在多线程环境下,常用concurrenthashmap在需要保证数据安全的场景中去替换hashmap,而不会去使用hashtable,同时在最新版的jdk中已经推荐废弃使用hashtable。\n\n 3. copyonwritearraylist\n\ncopyonwritearraylist实现了list接口,提供的数据更新操作都使用了reentrantlock的lock()方法来加锁,unlock()方法来解锁。\n\n当增加元素的时候,首先使用arrays.copyof()来拷贝形成新的副本,在副本上增加元素,然后改变原引用指向副本。读操作不需要加锁,而写操作类实现中对其进行了加锁。因此,copyonwritearraylist类是一个线程安全的list接口的实现,在高并发的情况下,可以提供高性能的并发读取,并且保证读取的内容一定是正确的,这对于读操作远远多于写操作的应用非常适合(注意: 如上述更新操作会带来较大的空间与性能开销,如果更新操太过频繁,反而不太合适使用)。\n\n 4. copyonwritearrayset\n\ncopyonwritearrayset是对copyonwritearraylist使用了装饰模式后的具体实现。所以copyonwritearraylist的实现机理适用于copyonwritearrayset,此处不再赘述。\n\njava里的list和set的之间的特性比较结论同样适用于copyonwritearraylist与copyonwritearrayset之间的比较;此外,copyonwritearraylist与copyonwritearrayset都是线程安全的。\n\n 5. concurrentlinkedqueue\n\nconcurrentlinkedqueue可以被看作是一个线程安全的linkedlist,使用了非阻塞算法实现的一个高效、线程安全的并发队列;其本质是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当添加一个元素时会添加到队列的尾部;当获取一个元素时,会返回队列头部的元素。\n\nconcurrentlinkedqueue应该算是在高并发环境中性能最好的队列,没有之一。\n\n 6. vector\n\nvector通过数组保存数据,继承了abstract,实现了list;所以,其本质上是一个队列。\n\n但是和arraylist不同,vector中的操作是线程安全的,它是利用synchronized同步锁机制进行实现,其实现方式与hashtable类似。\n\n 7. stringbuffer与stringbuilder\n\n在java里面,字符串操作应该是最频繁的操作了,为此有必要把stringbuffer与stringbuilder两个方法类比较一下。\n\n首先,对于频繁的字符串拼接操作,是不推荐采用效率低下的“+”操作的。一般是采用stringbuffer与stringbuilder来实现上述功能。但是,这两者也是有区别的:前者线程安全,后者不是线程安全的。\n\nstringbuffer是通过对方法函数进行synchronized修饰实现其线程安全特性,实现方式与hashtable、vector类似。", "charsets": { "cjk": true }, @@ -1515,63 +1572,6 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, - { - "title": "Java 是如何实现线程安全的,哪些数据结构是线程安全的?", - "frontmatter": { - "title": "Java 是如何实现线程安全的,哪些数据结构是线程安全的?", - "date": "2022-04-04T14:35:56.000Z", - "permalink": "/pages/c1f760/", - "categories": [ - "Java相关", - "并发" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/110.Java%20%E6%98%AF%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E7%9A%84%EF%BC%8C%E5%93%AA%E4%BA%9B%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E6%98%AF%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E7%9A%84%EF%BC%9F.html", - "relativePath": "01.Java相关/20.并发/110.Java 是如何实现线程安全的,哪些数据结构是线程安全的?.md", - "key": "v-625fbeda", - "path": "/pages/c1f760/", - "headers": [ - { - "level": 2, - "title": "Java 是如何实现线程安全的,哪些数据结构是线程安全的?", - "slug": "java-是如何实现线程安全的-哪些数据结构是线程安全的", - "normalizedTitle": "java 是如何实现线程安全的,哪些数据结构是线程安全的?", - "charIndex": 2 - }, - { - "level": 2, - "title": "synchronized 来实现售票场景", - "slug": "synchronized-来实现售票场景", - "normalizedTitle": "synchronized 来实现售票场景", - "charIndex": 167 - }, - { - "level": 2, - "title": "Lock 接口来实现售票场景", - "slug": "lock-接口来实现售票场景", - "normalizedTitle": "lock 接口来实现售票场景", - "charIndex": 1352 - }, - { - "level": 2, - "title": "哪些数据结构是线程安全的?", - "slug": "哪些数据结构是线程安全的", - "normalizedTitle": "哪些数据结构是线程安全的?", - "charIndex": 18 - } - ], - "headersStr": "Java 是如何实现线程安全的,哪些数据结构是线程安全的? synchronized 来实现售票场景 Lock 接口来实现售票场景 哪些数据结构是线程安全的?", - "content": "# Java 是如何实现线程安全的,哪些数据结构是线程安全的?\n\n如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。\n\nJava 中实现线程安全的方式有两个:\n\n * synchronized\n * Lock接口\n\n举例一个售票场景:火车站4个窗口同时售票,共有3张票,不能超卖。\n\n\n# synchronized 来实现售票场景\n\npublic class ThreadSynchronizedSecurity {\n \n static int tickets = 3;\n \n class SellTickets implements Runnable {\n @Override\n public void run() {\n // 同步代码块\n synchronized (this) {\n if (tickets <= 0) {\n System.out.println(Thread.currentThread().getName() + \"--->票已售罄!\");\n return;\n }\n System.out.println(Thread.currentThread().getName() + \"--->售出第: \" + tickets + \" 张票\");\n tickets--;\n }\n }\n }\n \n public static void main(String[] args) {\n SellTickets sell = new ThreadSynchronizedSecurity().new SellTickets();\n Thread thread1 = new Thread(sell, \"1号窗口\");\n Thread thread2 = new Thread(sell, \"2号窗口\");\n Thread thread3 = new Thread(sell, \"3号窗口\");\n Thread thread4 = new Thread(sell, \"4号窗口\");\n thread1.start();\n thread2.start();\n thread3.start();\n thread4.start();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n\n\n输出:\n\n1号窗口--->售出第: 3 张票\n2号窗口--->售出第: 2 张票\n3号窗口--->售出第: 3 张票\n4号窗口--->票已售罄!\n\n\n1\n2\n3\n4\n\n\n\n# Lock 接口来实现售票场景\n\npackage com.my.annotate.thread;\n \nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\n \npublic class ThreadLockSecurity {\n static int tickets = 3;\n \n class SellTickets implements Runnable {\n Lock lock = new ReentrantLock();\n @Override\n public void run() {\n // Lock锁机制\n if (tickets > 0) {\n try {\n lock.lock();\n if (tickets <= 0) {\n return;\n }\n System.out.println(Thread.currentThread().getName() + \"--->售出第: \" + tickets + \" 票\");\n tickets--;\n } catch (Exception e1) {\n // TODO Auto-generated catch block\n e1.printStackTrace();\n } finally {\n lock.unlock();\n }\n }\n if (tickets <= 0) {\n System.out.println(Thread.currentThread().getName() + \"--->票已售罄!\");\n }\n \n }\n }\n \n \n public static void main(String[] args) {\n SellTickets sell = new ThreadLockSecurity().new SellTickets();\n Thread thread1 = new Thread(sell, \"1号窗口\");\n Thread thread2 = new Thread(sell, \"2号窗口\");\n Thread thread3 = new Thread(sell, \"3号窗口\");\n Thread thread4 = new Thread(sell, \"4号窗口\");\n thread1.start();\n thread2.start();\n thread3.start();\n thread4.start();\n }\n \n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n\n\n输出:\n\n1号窗口--->售出第: 3 张票\n2号窗口--->售出第: 2 张票\n3号窗口--->售出第: 3 张票\n4号窗口--->票已售罄!\n\n\n1\n2\n3\n4\n\n\n\n# 哪些数据结构是线程安全的?\n\nJDK已经为大家准备好了一批好用的线程安全容器类,可以大大减少开发工作量,例如HashTable,ConcurrentHashMap,CopyOnWriteArrayList,CopyOnWriteArraySet,ConcurrentLinkedQueue,Vector,StringBuffer等。\n\n 1. HashTable\n\nHashTable实现了Map接口,为此其本身也是一个散列表,它存储的内容是基于key-value的键值对映射。\n\nHashTable中的key、value都不可以为null;具有无序特性;由于其方法函数都是同步的(采用synchronized修饰),不会出现两个线程同时对数据进行操作的情况,因此保证了线程安全性。\n\nHashTable使用synchronized来修饰方法函数来保证线程安全,但是在多线程运行环境下效率表现非常低下。\n\n因为当一个线程访问HashTable的同步方法时,其他线程也访问同步方法就会粗线阻塞状态。\n\n比如当一个线程在添加数据时候,另外一个线程即使执行获取其他数据的操作也必须被阻塞,大大降低了程序的运行效率。\n\n 2. ConcurrentHashMap\n\n我们知道HashMap是线程不安全的,ConcurrentHashMap是HashMap的线程安全版。\n\n但是与HashTable相比,ConcurrentHashMap不仅保证了多线程运行环境下的数据访问安全性,而且性能上有长足的提升。\n\nConcurrentHashMap允许多个修改操作并发运行,其原因在于使用了锁分段技术:首先讲Map存放的数据分成一段一段的存储方式,然后给每一段数据分配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。这样就保证了每一把锁只是用于锁住一部分数据,那么当多线程访问Map里的不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率。\n\n上述的处理机制明显区别于HashTable是给整体数据分配了一把锁的处理方法。\n\n为此,在多线程环境下,常用ConcurrentHashMap在需要保证数据安全的场景中去替换HashMap,而不会去使用HashTable,同时在最新版的JDK中已经推荐废弃使用HashTable。\n\n 3. CopyOnWriteArrayList\n\nCopyOnWriteArrayList实现了List接口,提供的数据更新操作都使用了ReentrantLock的lock()方法来加锁,unlock()方法来解锁。\n\n当增加元素的时候,首先使用Arrays.copyOf()来拷贝形成新的副本,在副本上增加元素,然后改变原引用指向副本。读操作不需要加锁,而写操作类实现中对其进行了加锁。因此,CopyOnWriteArrayList类是一个线程安全的List接口的实现,在高并发的情况下,可以提供高性能的并发读取,并且保证读取的内容一定是正确的,这对于读操作远远多于写操作的应用非常适合(注意: 如上述更新操作会带来较大的空间与性能开销,如果更新操太过频繁,反而不太合适使用)。\n\n 4. CopyOnWriteArraySet\n\nCopyOnWriteArraySet是对CopyOnWriteArrayList使用了装饰模式后的具体实现。所以CopyOnWriteArrayList的实现机理适用于CopyOnWriteArraySet,此处不再赘述。\n\nJava里的List和Set的之间的特性比较结论同样适用于CopyOnWriteArrayList与CopyOnWriteArraySet之间的比较;此外,CopyOnWriteArrayList与CopyOnWriteArraySet都是线程安全的。\n\n 5. ConcurrentLinkedQueue\n\nConcurrentLinkedQueue可以被看作是一个线程安全的LinkedList,使用了非阻塞算法实现的一个高效、线程安全的并发队列;其本质是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当添加一个元素时会添加到队列的尾部;当获取一个元素时,会返回队列头部的元素。\n\nConcurrentLinkedQueue应该算是在高并发环境中性能最好的队列,没有之一。\n\n 6. Vector\n\nVector通过数组保存数据,继承了Abstract,实现了List;所以,其本质上是一个队列。\n\n但是和ArrayList不同,Vector中的操作是线程安全的,它是利用synchronized同步锁机制进行实现,其实现方式与HashTable类似。\n\n 7. StringBuffer与StringBuilder\n\n在Java里面,字符串操作应该是最频繁的操作了,为此有必要把StringBuffer与StringBuilder两个方法类比较一下。\n\n首先,对于频繁的字符串拼接操作,是不推荐采用效率低下的“+”操作的。一般是采用StringBuffer与StringBuilder来实现上述功能。但是,这两者也是有区别的:前者线程安全,后者不是线程安全的。\n\nStringBuffer是通过对方法函数进行synchronized修饰实现其线程安全特性,实现方式与HashTable、Vector类似。", - "normalizedContent": "# java 是如何实现线程安全的,哪些数据结构是线程安全的?\n\n如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。\n\njava 中实现线程安全的方式有两个:\n\n * synchronized\n * lock接口\n\n举例一个售票场景:火车站4个窗口同时售票,共有3张票,不能超卖。\n\n\n# synchronized 来实现售票场景\n\npublic class threadsynchronizedsecurity {\n \n static int tickets = 3;\n \n class selltickets implements runnable {\n @override\n public void run() {\n // 同步代码块\n synchronized (this) {\n if (tickets <= 0) {\n system.out.println(thread.currentthread().getname() + \"--->票已售罄!\");\n return;\n }\n system.out.println(thread.currentthread().getname() + \"--->售出第: \" + tickets + \" 张票\");\n tickets--;\n }\n }\n }\n \n public static void main(string[] args) {\n selltickets sell = new threadsynchronizedsecurity().new selltickets();\n thread thread1 = new thread(sell, \"1号窗口\");\n thread thread2 = new thread(sell, \"2号窗口\");\n thread thread3 = new thread(sell, \"3号窗口\");\n thread thread4 = new thread(sell, \"4号窗口\");\n thread1.start();\n thread2.start();\n thread3.start();\n thread4.start();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n\n\n输出:\n\n1号窗口--->售出第: 3 张票\n2号窗口--->售出第: 2 张票\n3号窗口--->售出第: 3 张票\n4号窗口--->票已售罄!\n\n\n1\n2\n3\n4\n\n\n\n# lock 接口来实现售票场景\n\npackage com.my.annotate.thread;\n \nimport java.util.concurrent.locks.lock;\nimport java.util.concurrent.locks.reentrantlock;\n \npublic class threadlocksecurity {\n static int tickets = 3;\n \n class selltickets implements runnable {\n lock lock = new reentrantlock();\n @override\n public void run() {\n // lock锁机制\n if (tickets > 0) {\n try {\n lock.lock();\n if (tickets <= 0) {\n return;\n }\n system.out.println(thread.currentthread().getname() + \"--->售出第: \" + tickets + \" 票\");\n tickets--;\n } catch (exception e1) {\n // todo auto-generated catch block\n e1.printstacktrace();\n } finally {\n lock.unlock();\n }\n }\n if (tickets <= 0) {\n system.out.println(thread.currentthread().getname() + \"--->票已售罄!\");\n }\n \n }\n }\n \n \n public static void main(string[] args) {\n selltickets sell = new threadlocksecurity().new selltickets();\n thread thread1 = new thread(sell, \"1号窗口\");\n thread thread2 = new thread(sell, \"2号窗口\");\n thread thread3 = new thread(sell, \"3号窗口\");\n thread thread4 = new thread(sell, \"4号窗口\");\n thread1.start();\n thread2.start();\n thread3.start();\n thread4.start();\n }\n \n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n\n\n输出:\n\n1号窗口--->售出第: 3 张票\n2号窗口--->售出第: 2 张票\n3号窗口--->售出第: 3 张票\n4号窗口--->票已售罄!\n\n\n1\n2\n3\n4\n\n\n\n# 哪些数据结构是线程安全的?\n\njdk已经为大家准备好了一批好用的线程安全容器类,可以大大减少开发工作量,例如hashtable,concurrenthashmap,copyonwritearraylist,copyonwritearrayset,concurrentlinkedqueue,vector,stringbuffer等。\n\n 1. hashtable\n\nhashtable实现了map接口,为此其本身也是一个散列表,它存储的内容是基于key-value的键值对映射。\n\nhashtable中的key、value都不可以为null;具有无序特性;由于其方法函数都是同步的(采用synchronized修饰),不会出现两个线程同时对数据进行操作的情况,因此保证了线程安全性。\n\nhashtable使用synchronized来修饰方法函数来保证线程安全,但是在多线程运行环境下效率表现非常低下。\n\n因为当一个线程访问hashtable的同步方法时,其他线程也访问同步方法就会粗线阻塞状态。\n\n比如当一个线程在添加数据时候,另外一个线程即使执行获取其他数据的操作也必须被阻塞,大大降低了程序的运行效率。\n\n 2. concurrenthashmap\n\n我们知道hashmap是线程不安全的,concurrenthashmap是hashmap的线程安全版。\n\n但是与hashtable相比,concurrenthashmap不仅保证了多线程运行环境下的数据访问安全性,而且性能上有长足的提升。\n\nconcurrenthashmap允许多个修改操作并发运行,其原因在于使用了锁分段技术:首先讲map存放的数据分成一段一段的存储方式,然后给每一段数据分配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。这样就保证了每一把锁只是用于锁住一部分数据,那么当多线程访问map里的不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率。\n\n上述的处理机制明显区别于hashtable是给整体数据分配了一把锁的处理方法。\n\n为此,在多线程环境下,常用concurrenthashmap在需要保证数据安全的场景中去替换hashmap,而不会去使用hashtable,同时在最新版的jdk中已经推荐废弃使用hashtable。\n\n 3. copyonwritearraylist\n\ncopyonwritearraylist实现了list接口,提供的数据更新操作都使用了reentrantlock的lock()方法来加锁,unlock()方法来解锁。\n\n当增加元素的时候,首先使用arrays.copyof()来拷贝形成新的副本,在副本上增加元素,然后改变原引用指向副本。读操作不需要加锁,而写操作类实现中对其进行了加锁。因此,copyonwritearraylist类是一个线程安全的list接口的实现,在高并发的情况下,可以提供高性能的并发读取,并且保证读取的内容一定是正确的,这对于读操作远远多于写操作的应用非常适合(注意: 如上述更新操作会带来较大的空间与性能开销,如果更新操太过频繁,反而不太合适使用)。\n\n 4. copyonwritearrayset\n\ncopyonwritearrayset是对copyonwritearraylist使用了装饰模式后的具体实现。所以copyonwritearraylist的实现机理适用于copyonwritearrayset,此处不再赘述。\n\njava里的list和set的之间的特性比较结论同样适用于copyonwritearraylist与copyonwritearrayset之间的比较;此外,copyonwritearraylist与copyonwritearrayset都是线程安全的。\n\n 5. concurrentlinkedqueue\n\nconcurrentlinkedqueue可以被看作是一个线程安全的linkedlist,使用了非阻塞算法实现的一个高效、线程安全的并发队列;其本质是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当添加一个元素时会添加到队列的尾部;当获取一个元素时,会返回队列头部的元素。\n\nconcurrentlinkedqueue应该算是在高并发环境中性能最好的队列,没有之一。\n\n 6. vector\n\nvector通过数组保存数据,继承了abstract,实现了list;所以,其本质上是一个队列。\n\n但是和arraylist不同,vector中的操作是线程安全的,它是利用synchronized同步锁机制进行实现,其实现方式与hashtable类似。\n\n 7. stringbuffer与stringbuilder\n\n在java里面,字符串操作应该是最频繁的操作了,为此有必要把stringbuffer与stringbuilder两个方法类比较一下。\n\n首先,对于频繁的字符串拼接操作,是不推荐采用效率低下的“+”操作的。一般是采用stringbuffer与stringbuilder来实现上述功能。但是,这两者也是有区别的:前者线程安全,后者不是线程安全的。\n\nstringbuffer是通过对方法函数进行synchronized修饰实现其线程安全特性,实现方式与hashtable、vector类似。", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, { "title": "手写死锁", "frontmatter": { @@ -1722,6 +1722,105 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, + { + "title": "ThreadLocal 实现原理是什么?为什么要使用弱引用?", + "frontmatter": { + "title": "ThreadLocal 实现原理是什么?为什么要使用弱引用?", + "date": "2022-03-11T00:45:50.000Z", + "permalink": "/pages/b3e720/", + "categories": [ + "Java相关", + "并发" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/150.ThreadLocal%20%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E4%BD%BF%E7%94%A8%E5%BC%B1%E5%BC%95%E7%94%A8%EF%BC%9F.html", + "relativePath": "01.Java相关/20.并发/150.ThreadLocal 实现原理是什么?为什么要使用弱引用?.md", + "key": "v-f406b4e6", + "path": "/pages/b3e720/", + "headers": [ + { + "level": 2, + "title": "ThreadLocal 实现原理是什么?为什么要使用弱引用?", + "slug": "threadlocal-实现原理是什么-为什么要使用弱引用", + "normalizedTitle": "threadlocal 实现原理是什么?为什么要使用弱引用?", + "charIndex": 2 + }, + { + "level": 2, + "title": "ThreadLocal解决什么问题?", + "slug": "threadlocal解决什么问题", + "normalizedTitle": "threadlocal解决什么问题?", + "charIndex": 57 + }, + { + "level": 2, + "title": "ThreadLocal底层是什么数据结构?", + "slug": "threadlocal底层是什么数据结构", + "normalizedTitle": "threadlocal底层是什么数据结构?", + "charIndex": 382 + }, + { + "level": 2, + "title": "ThreadLocal有什么应用场景?", + "slug": "threadlocal有什么应用场景", + "normalizedTitle": "threadlocal有什么应用场景?", + "charIndex": 1520 + }, + { + "level": 2, + "title": "为什么要使用弱引用?", + "slug": "为什么要使用弱引用", + "normalizedTitle": "为什么要使用弱引用?", + "charIndex": 22 + }, + { + "level": 2, + "title": "子线程可以从父线程继承 ThreadLocal 吗?", + "slug": "子线程可以从父线程继承-threadlocal-吗", + "normalizedTitle": "子线程可以从父线程继承 threadlocal 吗?", + "charIndex": 109 + }, + { + "level": 2, + "title": "ThreadLocal使用不当会有内存泄漏是怎么回事?", + "slug": "threadlocal使用不当会有内存泄漏是怎么回事", + "normalizedTitle": "threadlocal使用不当会有内存泄漏是怎么回事?", + "charIndex": 135 + }, + { + "level": 2, + "title": "使用 ThreadLocal 如何防止内存泄漏 ?", + "slug": "使用-threadlocal-如何防止内存泄漏", + "normalizedTitle": "使用 threadlocal 如何防止内存泄漏 ?", + "charIndex": 3009 + }, + { + "level": 2, + "title": "Netty 的FastThreadLocal fast在哪里?", + "slug": "netty-的fastthreadlocal-fast在哪里", + "normalizedTitle": "netty 的fastthreadlocal fast在哪里?", + "charIndex": 171 + }, + { + "level": 2, + "title": "FastThreadLocal 性能 比 ThreadLocal 高多少?", + "slug": "fastthreadlocal-性能-比-threadlocal-高多少", + "normalizedTitle": "fastthreadlocal 性能 比 threadlocal 高多少?", + "charIndex": 4196 + } + ], + "headersStr": "ThreadLocal 实现原理是什么?为什么要使用弱引用? ThreadLocal解决什么问题? ThreadLocal底层是什么数据结构? ThreadLocal有什么应用场景? 为什么要使用弱引用? 子线程可以从父线程继承 ThreadLocal 吗? ThreadLocal使用不当会有内存泄漏是怎么回事? 使用 ThreadLocal 如何防止内存泄漏 ? Netty 的FastThreadLocal fast在哪里? FastThreadLocal 性能 比 ThreadLocal 高多少?", + "content": "# ThreadLocal 实现原理是什么?为什么要使用弱引用?\n\nThreadLocal可以问的点有很多,比如:ThreadLocal解决什么问题?底层结构是什么?实现原理是什么?有什么应用场景?为什么要使用弱引用?子线程可以从父线程继承 ThreadLocal 吗?ThreadLocal使用不当会有内存泄漏是怎么回事?正确的使用姿势是?Netty 的FastThreadLocal fast在哪里?\n下面我们一个个来看下这些问题。\n\n\n\n# ThreadLocal解决什么问题?\n\n\n通常情况下,我们创建的变量任何线程来读取读到的都是同一个值,如果想实现每一个线程都有自己的专属值该如何解决呢\n?JDK中提供的ThreadLocal类正是为了解决这样的问题。\n比如SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本\n\n\n\n# ThreadLocal底层是什么数据结构?\n\n\n实际上是Map,Key为ThreadLocal变量、value为值。\n\n\n\n\n我们先看下Thread类的源码\n\npublic class Thread implements Runnable {\n ......\n//与此线程有关的ThreadLocal值。由ThreadLocal类维护\nThreadLocal.ThreadLocalMap threadLocals = null;\n\n/*\n * InheritableThreadLocal,自父线程集成而来的ThreadLocalMap,\n * 主要用于父子线程间ThreadLocal变量的传递\n * 本文主要讨论的就是这个ThreadLocalMap\n */\nThreadLocal.ThreadLocalMap inheritableThreadLocals = null;\n ......\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n我们可以把 ThreadLocal.ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap. 我们再来看下 ThreadLocal 类的源码\n\n //调用Thread.set 实际上是往当前线程的 ThreadLocalMap 里面put一个键值对\n public void set(T value) {\n Thread t = Thread.currentThread();\n ThreadLocalMap map = getMap(t);\n if (map != null)\n map.set(this, value);\n else\n createMap(t, value);\n }\n\n //调用Thread.getMap 可以获取到当前线程的 ThreadLocalMap\n ThreadLocalMap getMap(Thread t) {\n return t.threadLocals;\n }\n\n //ThreadLocalMap可以存储以ThreadLocal为key ,Object 对象为 value的键值对。\n ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {\n ......\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n\n\n\n# ThreadLocal有什么应用场景?\n\n\n * 非线程安全的工具类,需要每个线程持有一个副本,比如 SimpleDateFormat\n * 异步传递traceId\n\n\n# 为什么要使用弱引用?\n\n\nThreadLocalMap的内部类Entry被设计为实现了WeakReference,Entry用来存放数据。\n\n\n弱引用简单理解就是当垃圾回收时,该对象只被WeakReference对象的弱引用字段所引用,而未被任何强类型的对象引用,那么,该弱引用的对象就会被回收。\n注意:WeakReference引用本身是强引用,它内部的(T reference)才是真正的弱引用字段,WeakReference就是一个装弱引用的容器而已。\n那 为什么要使用弱引用呢?\n这是因为:ThreadLocalMap本身并没有为外界提供取出和存放数据的API,我们所能获得数据的方式只有通过ThreadLocal类提供的API来间接的从ThreadLocalMap取出数据,所以,当我们用不了key(ThreadLocal对象)的API也就无法从ThreadLocalMap里取出指定的数据。\n\n一般我们new 一个ThreadLocal对象的时候,它一定会有强引用,在ThreadLocalMap中也一定会有它的弱引用\n当强引用不在的时候一定是我们的程序不再需要这个ThreadLocal对象了 为什么这么说?\n比如我定义了一个 ThreadLocal 变量 formatter,formatter 对 ThreadLocal 变量的强引用关系不存在的一个case 是getDate()方法执行完了,那么当然ThreadLocal 变量是可以回收的。\n\n\npublic void getDate(){\n\n ThreadLocal formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat(\"yyyyMMdd HHmm\"));\n ......\n}\n\n\n\n1\n2\n3\n4\n5\n6\n\n\n\n# 子线程可以从父线程继承 ThreadLocal 吗?\n\n\nThreadLocal 不可以。InheritableThreadLocals 是可以的,它重写了ThreadLocal的三个方法。childValue,createMap,getMap。\n\n\nInheritableThreadLocal 不能和线程池搭配使用\n因为线程池中的线程是复用的,并没有重新初始化线程,InheritableThreadLocal之所以起作用是因为在Thread类中最终会调用init()方法去把InheritableThreadLocal的map复制到子线程中。\n由于线程池复用了已有线程,所以没有调用init()方法这个过程,也就不能将父线程中的InheritableThreadLocal值传给子线程。\n\n\n# ThreadLocal使用不当会有内存泄漏是怎么回事?\n\n\nThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。\n所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。\n这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。\n\n\n\n# 使用 ThreadLocal 如何防止内存泄漏 ?\n\n\n使用完 ThreadLocal 手动调用remove方法。 看下 ThreadLocal 的 remove() 会清理 ThreadLocalMap 中 key 为 null的键值对。\n\npublic void remove() {\n ThreadLocalMap m = getMap(Thread.currentThread());\n if (m != null)\n m.remove(this);\n}\n\n\n1\n2\n3\n4\n5\n\n\nThreadLocal 的 get() 有清除操作,那我们是不是不用手动调用 remove() 了?\n不是的。因为 get() 中清除操作 只会检测本次get的 ThreadLocal 变量是否需要清理 。而 remove() 会检测 ThreadLocalMap 中的所有键值对。\n\n\n所以,手动调用remove() 才是最保险的。\n\n\n\n# Netty 的FastThreadLocal fast在哪里?\n\n\n既然jdk已经有ThreadLocal,为何netty还要自己造个FastThreadLocal?FastThreadLocal快在哪里?\n这需要从jdk ThreadLocal的本身说起。如下图:\n在java线程中,每个线程都有一个ThreadLocalMap实例变量(如果不使用ThreadLocal,不会创建这个Map,一个线程第一次访问某个ThreadLocal变量时,才会创建)。该Map是使用线性探测的方式解决hash冲突的问题,如果没有找到空闲的slot,就不断往后尝试,直到找到一个空闲的位置,插入entry,这种方式在经常遇到hash冲突时,影响效率。\n\nFastThreadLocal(下文简称ftl)直接使用数组避免了hash冲突的发生,具体做法是:每一个FastThreadLocal实例创建时,分配一个下标index;分配index使用AtomicInteger实现,每个FastThreadLocal都能获取到一个不重复的下标。当调用ftl.get()方法获取值时,直接从数组获取返回,如return array[index],如下图:\n\nFastThreadLocal 底层结构代码\n\nstatic final ThreadLocal slowThreadLocalMap = new ThreadLocal();\nstatic final AtomicInteger nextIndex = new AtomicInteger();\nObject[] indexedVariables;\n\n\n1\n2\n3\n\n\n\n# FastThreadLocal 性能 比 ThreadLocal 高多少?\n\n\n远高于。\nFastThreadLocal 对 ThreadLocal 的优化点在于,将元素放入 ThreadLocalMap 采用数组结构随机访问代替 原来的 线性探测。 所以我们测试场景为:单线程访问多 FastThreadLocal/ThreadLocal 变量:\n\n/**\n * 单线程访问多个ThreadLocal\n */\npublic static void testThreadLocalWithMultipleThreadLocal() {\n ThreadLocal threadLocal[] = new ThreadLocal[count];\n for (int i = 0; i < count; i++) {\n threadLocal[i] = new ThreadLocal();\n }\n new Thread(new Runnable() {\n @Override\n public void run() {\n long start = System.currentTimeMillis();\n for (int i = 0; i < count; i++) {\n threadLocal[i].set(\"value\" + i);\n }\n long middle = System.currentTimeMillis();\n for (int i = 0; i < count; i++) {\n for (int j = 0; j < count; j++) {\n threadLocal[i].get();\n }\n }\n long end = System.currentTimeMillis();\n System.out.println(\"testThreadLocalWithMultipleThreadLocal set:\" + (middle - start) + \",get:\" + (end - middle));\n }\n }).start();\n}\n\n/**\n * 单线程访问多个FastThreadLocal\n */\npublic static void testFastThreadLocalWithMultipleFastThreadLocal() {\n FastThreadLocal threadLocal[] = new FastThreadLocal[count];\n for (int i = 0; i < count; i++) {\n threadLocal[i] = new FastThreadLocal();\n }\n new FastThreadLocalThread(new Runnable() {\n @Override\n public void run() {\n long start = System.currentTimeMillis();\n for (int i = 0; i < count; i++) {\n threadLocal[i].set(\"value\" + i);\n }\n long middle = System.currentTimeMillis();\n for (int i = 0; i < count; i++) {\n for (int j = 0; j < count; j++) {\n threadLocal[i].get();\n }\n }\n long end = System.currentTimeMillis();\n System.out.println(\"testFastThreadLocalWithMultipleFastThreadLocal set:\" + (middle - start) + \",get:\" + (end - middle));\n }\n }).start();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n\n\n输出:\n\ntestThreadLocalWithMultipleThreadLocal set:68,get:21492\ntestFastThreadLocalWithMultipleFastThreadLocal set:61,get:8\n\n\n\n\n1\n2\n3\n4\n\n\n有结果可知,FastThreadLocal 性能远高于 ThreadLocal。", + "normalizedContent": "# threadlocal 实现原理是什么?为什么要使用弱引用?\n\nthreadlocal可以问的点有很多,比如:threadlocal解决什么问题?底层结构是什么?实现原理是什么?有什么应用场景?为什么要使用弱引用?子线程可以从父线程继承 threadlocal 吗?threadlocal使用不当会有内存泄漏是怎么回事?正确的使用姿势是?netty 的fastthreadlocal fast在哪里?\n下面我们一个个来看下这些问题。\n\n\n\n# threadlocal解决什么问题?\n\n\n通常情况下,我们创建的变量任何线程来读取读到的都是同一个值,如果想实现每一个线程都有自己的专属值该如何解决呢\n?jdk中提供的threadlocal类正是为了解决这样的问题。\n比如simpledateformat 不是线程安全的,所以每个线程都要有自己独立的副本\n\n\n\n# threadlocal底层是什么数据结构?\n\n\n实际上是map,key为threadlocal变量、value为值。\n\n\n\n\n我们先看下thread类的源码\n\npublic class thread implements runnable {\n ......\n//与此线程有关的threadlocal值。由threadlocal类维护\nthreadlocal.threadlocalmap threadlocals = null;\n\n/*\n * inheritablethreadlocal,自父线程集成而来的threadlocalmap,\n * 主要用于父子线程间threadlocal变量的传递\n * 本文主要讨论的就是这个threadlocalmap\n */\nthreadlocal.threadlocalmap inheritablethreadlocals = null;\n ......\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n我们可以把 threadlocal.threadlocalmap 理解为threadlocal 类实现的定制化的 hashmap. 我们再来看下 threadlocal 类的源码\n\n //调用thread.set 实际上是往当前线程的 threadlocalmap 里面put一个键值对\n public void set(t value) {\n thread t = thread.currentthread();\n threadlocalmap map = getmap(t);\n if (map != null)\n map.set(this, value);\n else\n createmap(t, value);\n }\n\n //调用thread.getmap 可以获取到当前线程的 threadlocalmap\n threadlocalmap getmap(thread t) {\n return t.threadlocals;\n }\n\n //threadlocalmap可以存储以threadlocal为key ,object 对象为 value的键值对。\n threadlocalmap(threadlocal firstkey, object firstvalue) {\n ......\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n\n\n\n# threadlocal有什么应用场景?\n\n\n * 非线程安全的工具类,需要每个线程持有一个副本,比如 simpledateformat\n * 异步传递traceid\n\n\n# 为什么要使用弱引用?\n\n\nthreadlocalmap的内部类entry被设计为实现了weakreference,entry用来存放数据。\n\n\n弱引用简单理解就是当垃圾回收时,该对象只被weakreference对象的弱引用字段所引用,而未被任何强类型的对象引用,那么,该弱引用的对象就会被回收。\n注意:weakreference引用本身是强引用,它内部的(t reference)才是真正的弱引用字段,weakreference就是一个装弱引用的容器而已。\n那 为什么要使用弱引用呢?\n这是因为:threadlocalmap本身并没有为外界提供取出和存放数据的api,我们所能获得数据的方式只有通过threadlocal类提供的api来间接的从threadlocalmap取出数据,所以,当我们用不了key(threadlocal对象)的api也就无法从threadlocalmap里取出指定的数据。\n\n一般我们new 一个threadlocal对象的时候,它一定会有强引用,在threadlocalmap中也一定会有它的弱引用\n当强引用不在的时候一定是我们的程序不再需要这个threadlocal对象了 为什么这么说?\n比如我定义了一个 threadlocal 变量 formatter,formatter 对 threadlocal 变量的强引用关系不存在的一个case 是getdate()方法执行完了,那么当然threadlocal 变量是可以回收的。\n\n\npublic void getdate(){\n\n threadlocal formatter = threadlocal.withinitial(() -> new simpledateformat(\"yyyymmdd hhmm\"));\n ......\n}\n\n\n\n1\n2\n3\n4\n5\n6\n\n\n\n# 子线程可以从父线程继承 threadlocal 吗?\n\n\nthreadlocal 不可以。inheritablethreadlocals 是可以的,它重写了threadlocal的三个方法。childvalue,createmap,getmap。\n\n\ninheritablethreadlocal 不能和线程池搭配使用\n因为线程池中的线程是复用的,并没有重新初始化线程,inheritablethreadlocal之所以起作用是因为在thread类中最终会调用init()方法去把inheritablethreadlocal的map复制到子线程中。\n由于线程池复用了已有线程,所以没有调用init()方法这个过程,也就不能将父线程中的inheritablethreadlocal值传给子线程。\n\n\n# threadlocal使用不当会有内存泄漏是怎么回事?\n\n\nthreadlocalmap 中使用的 key 为 threadlocal 的弱引用,而 value 是强引用。\n所以,如果 threadlocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。\n这样一来,threadlocalmap 中就会出现key为null的entry。假如我们不做任何措施的话,value 永远无法被gc 回收,这个时候就可能会产生内存泄露。\n\n\n\n# 使用 threadlocal 如何防止内存泄漏 ?\n\n\n使用完 threadlocal 手动调用remove方法。 看下 threadlocal 的 remove() 会清理 threadlocalmap 中 key 为 null的键值对。\n\npublic void remove() {\n threadlocalmap m = getmap(thread.currentthread());\n if (m != null)\n m.remove(this);\n}\n\n\n1\n2\n3\n4\n5\n\n\nthreadlocal 的 get() 有清除操作,那我们是不是不用手动调用 remove() 了?\n不是的。因为 get() 中清除操作 只会检测本次get的 threadlocal 变量是否需要清理 。而 remove() 会检测 threadlocalmap 中的所有键值对。\n\n\n所以,手动调用remove() 才是最保险的。\n\n\n\n# netty 的fastthreadlocal fast在哪里?\n\n\n既然jdk已经有threadlocal,为何netty还要自己造个fastthreadlocal?fastthreadlocal快在哪里?\n这需要从jdk threadlocal的本身说起。如下图:\n在java线程中,每个线程都有一个threadlocalmap实例变量(如果不使用threadlocal,不会创建这个map,一个线程第一次访问某个threadlocal变量时,才会创建)。该map是使用线性探测的方式解决hash冲突的问题,如果没有找到空闲的slot,就不断往后尝试,直到找到一个空闲的位置,插入entry,这种方式在经常遇到hash冲突时,影响效率。\n\nfastthreadlocal(下文简称ftl)直接使用数组避免了hash冲突的发生,具体做法是:每一个fastthreadlocal实例创建时,分配一个下标index;分配index使用atomicinteger实现,每个fastthreadlocal都能获取到一个不重复的下标。当调用ftl.get()方法获取值时,直接从数组获取返回,如return array[index],如下图:\n\nfastthreadlocal 底层结构代码\n\nstatic final threadlocal slowthreadlocalmap = new threadlocal();\nstatic final atomicinteger nextindex = new atomicinteger();\nobject[] indexedvariables;\n\n\n1\n2\n3\n\n\n\n# fastthreadlocal 性能 比 threadlocal 高多少?\n\n\n远高于。\nfastthreadlocal 对 threadlocal 的优化点在于,将元素放入 threadlocalmap 采用数组结构随机访问代替 原来的 线性探测。 所以我们测试场景为:单线程访问多 fastthreadlocal/threadlocal 变量:\n\n/**\n * 单线程访问多个threadlocal\n */\npublic static void testthreadlocalwithmultiplethreadlocal() {\n threadlocal threadlocal[] = new threadlocal[count];\n for (int i = 0; i < count; i++) {\n threadlocal[i] = new threadlocal();\n }\n new thread(new runnable() {\n @override\n public void run() {\n long start = system.currenttimemillis();\n for (int i = 0; i < count; i++) {\n threadlocal[i].set(\"value\" + i);\n }\n long middle = system.currenttimemillis();\n for (int i = 0; i < count; i++) {\n for (int j = 0; j < count; j++) {\n threadlocal[i].get();\n }\n }\n long end = system.currenttimemillis();\n system.out.println(\"testthreadlocalwithmultiplethreadlocal set:\" + (middle - start) + \",get:\" + (end - middle));\n }\n }).start();\n}\n\n/**\n * 单线程访问多个fastthreadlocal\n */\npublic static void testfastthreadlocalwithmultiplefastthreadlocal() {\n fastthreadlocal threadlocal[] = new fastthreadlocal[count];\n for (int i = 0; i < count; i++) {\n threadlocal[i] = new fastthreadlocal();\n }\n new fastthreadlocalthread(new runnable() {\n @override\n public void run() {\n long start = system.currenttimemillis();\n for (int i = 0; i < count; i++) {\n threadlocal[i].set(\"value\" + i);\n }\n long middle = system.currenttimemillis();\n for (int i = 0; i < count; i++) {\n for (int j = 0; j < count; j++) {\n threadlocal[i].get();\n }\n }\n long end = system.currenttimemillis();\n system.out.println(\"testfastthreadlocalwithmultiplefastthreadlocal set:\" + (middle - start) + \",get:\" + (end - middle));\n }\n }).start();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n\n\n输出:\n\ntestthreadlocalwithmultiplethreadlocal set:68,get:21492\ntestfastthreadlocalwithmultiplefastthreadlocal set:61,get:8\n\n\n\n\n1\n2\n3\n4\n\n\n有结果可知,fastthreadlocal 性能远高于 threadlocal。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, { "title": "手写生产者消费者模型", "frontmatter": { @@ -1850,110 +1949,75 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "ThreadLocal 实现原理是什么?为什么要使用弱引用?", + "title": "Java 中垃圾回收机制中如何判断对象需要回收?", "frontmatter": { - "title": "ThreadLocal 实现原理是什么?为什么要使用弱引用?", - "date": "2022-03-11T00:45:50.000Z", - "permalink": "/pages/b3e720/", + "title": "Java 中垃圾回收机制中如何判断对象需要回收?", + "date": "2022-05-21T17:31:41.000Z", + "permalink": "/pages/948850/", "categories": [ "Java相关", - "并发" + "JVM" ], "tags": [ null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/150.ThreadLocal%20%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E4%BD%BF%E7%94%A8%E5%BC%B1%E5%BC%95%E7%94%A8%EF%BC%9F.html", - "relativePath": "01.Java相关/20.并发/150.ThreadLocal 实现原理是什么?为什么要使用弱引用?.md", - "key": "v-f406b4e6", - "path": "/pages/b3e720/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/001.Java%20%E4%B8%AD%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6%E4%B8%AD%E5%A6%82%E4%BD%95%E5%88%A4%E6%96%AD%E5%AF%B9%E8%B1%A1%E9%9C%80%E8%A6%81%E5%9B%9E%E6%94%B6%EF%BC%9F.html", + "relativePath": "01.Java相关/30.JVM/001.Java 中垃圾回收机制中如何判断对象需要回收?.md", + "key": "v-7cd97c82", + "path": "/pages/948850/", "headers": [ { "level": 2, - "title": "ThreadLocal 实现原理是什么?为什么要使用弱引用?", - "slug": "threadlocal-实现原理是什么-为什么要使用弱引用", - "normalizedTitle": "threadlocal 实现原理是什么?为什么要使用弱引用?", - "charIndex": 2 - }, - { - "level": 2, - "title": "ThreadLocal解决什么问题?", - "slug": "threadlocal解决什么问题", - "normalizedTitle": "threadlocal解决什么问题?", - "charIndex": 57 - }, - { - "level": 2, - "title": "ThreadLocal底层是什么数据结构?", - "slug": "threadlocal底层是什么数据结构", - "normalizedTitle": "threadlocal底层是什么数据结构?", - "charIndex": 382 - }, - { - "level": 2, - "title": "ThreadLocal有什么应用场景?", - "slug": "threadlocal有什么应用场景", - "normalizedTitle": "threadlocal有什么应用场景?", - "charIndex": 1520 - }, - { - "level": 2, - "title": "为什么要使用弱引用?", - "slug": "为什么要使用弱引用", - "normalizedTitle": "为什么要使用弱引用?", - "charIndex": 22 - }, - { - "level": 2, - "title": "子线程可以从父线程继承 ThreadLocal 吗?", - "slug": "子线程可以从父线程继承-threadlocal-吗", - "normalizedTitle": "子线程可以从父线程继承 threadlocal 吗?", - "charIndex": 109 + "title": "引用计数法", + "slug": "引用计数法", + "normalizedTitle": "引用计数法", + "charIndex": 95 }, { "level": 2, - "title": "ThreadLocal使用不当会有内存泄漏是怎么回事?", - "slug": "threadlocal使用不当会有内存泄漏是怎么回事", - "normalizedTitle": "threadlocal使用不当会有内存泄漏是怎么回事?", - "charIndex": 135 + "title": "可达性分析算法", + "slug": "可达性分析算法", + "normalizedTitle": "可达性分析算法", + "charIndex": 738 }, { "level": 2, - "title": "使用 ThreadLocal 如何防止内存泄漏 ?", - "slug": "使用-threadlocal-如何防止内存泄漏", - "normalizedTitle": "使用 threadlocal 如何防止内存泄漏 ?", - "charIndex": 3009 + "title": "引用类型总结", + "slug": "引用类型总结", + "normalizedTitle": "引用类型总结", + "charIndex": 1442 }, { "level": 2, - "title": "Netty 的FastThreadLocal fast在哪里?", - "slug": "netty-的fastthreadlocal-fast在哪里", - "normalizedTitle": "netty 的fastthreadlocal fast在哪里?", - "charIndex": 171 + "title": "如何判断一个常量是废弃常量?", + "slug": "如何判断一个常量是废弃常量", + "normalizedTitle": "如何判断一个常量是废弃常量?", + "charIndex": 2760 }, { "level": 2, - "title": "FastThreadLocal 性能 比 ThreadLocal 高多少?", - "slug": "fastthreadlocal-性能-比-threadlocal-高多少", - "normalizedTitle": "fastthreadlocal 性能 比 threadlocal 高多少?", - "charIndex": 4196 + "title": "如何判断一个类是无用的类", + "slug": "如何判断一个类是无用的类", + "normalizedTitle": "如何判断一个类是无用的类", + "charIndex": 2930 } ], - "headersStr": "ThreadLocal 实现原理是什么?为什么要使用弱引用? ThreadLocal解决什么问题? ThreadLocal底层是什么数据结构? ThreadLocal有什么应用场景? 为什么要使用弱引用? 子线程可以从父线程继承 ThreadLocal 吗? ThreadLocal使用不当会有内存泄漏是怎么回事? 使用 ThreadLocal 如何防止内存泄漏 ? Netty 的FastThreadLocal fast在哪里? FastThreadLocal 性能 比 ThreadLocal 高多少?", - "content": "# ThreadLocal 实现原理是什么?为什么要使用弱引用?\n\nThreadLocal可以问的点有很多,比如:ThreadLocal解决什么问题?底层结构是什么?实现原理是什么?有什么应用场景?为什么要使用弱引用?子线程可以从父线程继承 ThreadLocal 吗?ThreadLocal使用不当会有内存泄漏是怎么回事?正确的使用姿势是?Netty 的FastThreadLocal fast在哪里?\n下面我们一个个来看下这些问题。\n\n\n\n# ThreadLocal解决什么问题?\n\n\n通常情况下,我们创建的变量任何线程来读取读到的都是同一个值,如果想实现每一个线程都有自己的专属值该如何解决呢\n?JDK中提供的ThreadLocal类正是为了解决这样的问题。\n比如SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本\n\n\n\n# ThreadLocal底层是什么数据结构?\n\n\n实际上是Map,Key为ThreadLocal变量、value为值。\n\n\n\n\n我们先看下Thread类的源码\n\npublic class Thread implements Runnable {\n ......\n//与此线程有关的ThreadLocal值。由ThreadLocal类维护\nThreadLocal.ThreadLocalMap threadLocals = null;\n\n/*\n * InheritableThreadLocal,自父线程集成而来的ThreadLocalMap,\n * 主要用于父子线程间ThreadLocal变量的传递\n * 本文主要讨论的就是这个ThreadLocalMap\n */\nThreadLocal.ThreadLocalMap inheritableThreadLocals = null;\n ......\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n我们可以把 ThreadLocal.ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap. 我们再来看下 ThreadLocal 类的源码\n\n //调用Thread.set 实际上是往当前线程的 ThreadLocalMap 里面put一个键值对\n public void set(T value) {\n Thread t = Thread.currentThread();\n ThreadLocalMap map = getMap(t);\n if (map != null)\n map.set(this, value);\n else\n createMap(t, value);\n }\n\n //调用Thread.getMap 可以获取到当前线程的 ThreadLocalMap\n ThreadLocalMap getMap(Thread t) {\n return t.threadLocals;\n }\n\n //ThreadLocalMap可以存储以ThreadLocal为key ,Object 对象为 value的键值对。\n ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {\n ......\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n\n\n\n# ThreadLocal有什么应用场景?\n\n\n * 非线程安全的工具类,需要每个线程持有一个副本,比如 SimpleDateFormat\n * 异步传递traceId\n\n\n# 为什么要使用弱引用?\n\n\nThreadLocalMap的内部类Entry被设计为实现了WeakReference,Entry用来存放数据。\n\n\n弱引用简单理解就是当垃圾回收时,该对象只被WeakReference对象的弱引用字段所引用,而未被任何强类型的对象引用,那么,该弱引用的对象就会被回收。\n注意:WeakReference引用本身是强引用,它内部的(T reference)才是真正的弱引用字段,WeakReference就是一个装弱引用的容器而已。\n那 为什么要使用弱引用呢?\n这是因为:ThreadLocalMap本身并没有为外界提供取出和存放数据的API,我们所能获得数据的方式只有通过ThreadLocal类提供的API来间接的从ThreadLocalMap取出数据,所以,当我们用不了key(ThreadLocal对象)的API也就无法从ThreadLocalMap里取出指定的数据。\n\n一般我们new 一个ThreadLocal对象的时候,它一定会有强引用,在ThreadLocalMap中也一定会有它的弱引用\n当强引用不在的时候一定是我们的程序不再需要这个ThreadLocal对象了 为什么这么说?\n比如我定义了一个 ThreadLocal 变量 formatter,formatter 对 ThreadLocal 变量的强引用关系不存在的一个case 是getDate()方法执行完了,那么当然ThreadLocal 变量是可以回收的。\n\n\npublic void getDate(){\n\n ThreadLocal formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat(\"yyyyMMdd HHmm\"));\n ......\n}\n\n\n\n1\n2\n3\n4\n5\n6\n\n\n\n# 子线程可以从父线程继承 ThreadLocal 吗?\n\n\nThreadLocal 不可以。InheritableThreadLocals 是可以的,它重写了ThreadLocal的三个方法。childValue,createMap,getMap。\n\n\nInheritableThreadLocal 不能和线程池搭配使用\n因为线程池中的线程是复用的,并没有重新初始化线程,InheritableThreadLocal之所以起作用是因为在Thread类中最终会调用init()方法去把InheritableThreadLocal的map复制到子线程中。\n由于线程池复用了已有线程,所以没有调用init()方法这个过程,也就不能将父线程中的InheritableThreadLocal值传给子线程。\n\n\n# ThreadLocal使用不当会有内存泄漏是怎么回事?\n\n\nThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。\n所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。\n这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。\n\n\n\n# 使用 ThreadLocal 如何防止内存泄漏 ?\n\n\n使用完 ThreadLocal 手动调用remove方法。 看下 ThreadLocal 的 remove() 会清理 ThreadLocalMap 中 key 为 null的键值对。\n\npublic void remove() {\n ThreadLocalMap m = getMap(Thread.currentThread());\n if (m != null)\n m.remove(this);\n}\n\n\n1\n2\n3\n4\n5\n\n\nThreadLocal 的 get() 有清除操作,那我们是不是不用手动调用 remove() 了?\n不是的。因为 get() 中清除操作 只会检测本次get的 ThreadLocal 变量是否需要清理 。而 remove() 会检测 ThreadLocalMap 中的所有键值对。\n\n\n所以,手动调用remove() 才是最保险的。\n\n\n\n# Netty 的FastThreadLocal fast在哪里?\n\n\n既然jdk已经有ThreadLocal,为何netty还要自己造个FastThreadLocal?FastThreadLocal快在哪里?\n这需要从jdk ThreadLocal的本身说起。如下图:\n在java线程中,每个线程都有一个ThreadLocalMap实例变量(如果不使用ThreadLocal,不会创建这个Map,一个线程第一次访问某个ThreadLocal变量时,才会创建)。该Map是使用线性探测的方式解决hash冲突的问题,如果没有找到空闲的slot,就不断往后尝试,直到找到一个空闲的位置,插入entry,这种方式在经常遇到hash冲突时,影响效率。\n\nFastThreadLocal(下文简称ftl)直接使用数组避免了hash冲突的发生,具体做法是:每一个FastThreadLocal实例创建时,分配一个下标index;分配index使用AtomicInteger实现,每个FastThreadLocal都能获取到一个不重复的下标。当调用ftl.get()方法获取值时,直接从数组获取返回,如return array[index],如下图:\n\nFastThreadLocal 底层结构代码\n\nstatic final ThreadLocal slowThreadLocalMap = new ThreadLocal();\nstatic final AtomicInteger nextIndex = new AtomicInteger();\nObject[] indexedVariables;\n\n\n1\n2\n3\n\n\n\n# FastThreadLocal 性能 比 ThreadLocal 高多少?\n\n\n远高于。\nFastThreadLocal 对 ThreadLocal 的优化点在于,将元素放入 ThreadLocalMap 采用数组结构随机访问代替 原来的 线性探测。 所以我们测试场景为:单线程访问多 FastThreadLocal/ThreadLocal 变量:\n\n/**\n * 单线程访问多个ThreadLocal\n */\npublic static void testThreadLocalWithMultipleThreadLocal() {\n ThreadLocal threadLocal[] = new ThreadLocal[count];\n for (int i = 0; i < count; i++) {\n threadLocal[i] = new ThreadLocal();\n }\n new Thread(new Runnable() {\n @Override\n public void run() {\n long start = System.currentTimeMillis();\n for (int i = 0; i < count; i++) {\n threadLocal[i].set(\"value\" + i);\n }\n long middle = System.currentTimeMillis();\n for (int i = 0; i < count; i++) {\n for (int j = 0; j < count; j++) {\n threadLocal[i].get();\n }\n }\n long end = System.currentTimeMillis();\n System.out.println(\"testThreadLocalWithMultipleThreadLocal set:\" + (middle - start) + \",get:\" + (end - middle));\n }\n }).start();\n}\n\n/**\n * 单线程访问多个FastThreadLocal\n */\npublic static void testFastThreadLocalWithMultipleFastThreadLocal() {\n FastThreadLocal threadLocal[] = new FastThreadLocal[count];\n for (int i = 0; i < count; i++) {\n threadLocal[i] = new FastThreadLocal();\n }\n new FastThreadLocalThread(new Runnable() {\n @Override\n public void run() {\n long start = System.currentTimeMillis();\n for (int i = 0; i < count; i++) {\n threadLocal[i].set(\"value\" + i);\n }\n long middle = System.currentTimeMillis();\n for (int i = 0; i < count; i++) {\n for (int j = 0; j < count; j++) {\n threadLocal[i].get();\n }\n }\n long end = System.currentTimeMillis();\n System.out.println(\"testFastThreadLocalWithMultipleFastThreadLocal set:\" + (middle - start) + \",get:\" + (end - middle));\n }\n }).start();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n\n\n输出:\n\ntestThreadLocalWithMultipleThreadLocal set:68,get:21492\ntestFastThreadLocalWithMultipleFastThreadLocal set:61,get:8\n\n\n\n\n1\n2\n3\n4\n\n\n有结果可知,FastThreadLocal 性能远高于 ThreadLocal。", - "normalizedContent": "# threadlocal 实现原理是什么?为什么要使用弱引用?\n\nthreadlocal可以问的点有很多,比如:threadlocal解决什么问题?底层结构是什么?实现原理是什么?有什么应用场景?为什么要使用弱引用?子线程可以从父线程继承 threadlocal 吗?threadlocal使用不当会有内存泄漏是怎么回事?正确的使用姿势是?netty 的fastthreadlocal fast在哪里?\n下面我们一个个来看下这些问题。\n\n\n\n# threadlocal解决什么问题?\n\n\n通常情况下,我们创建的变量任何线程来读取读到的都是同一个值,如果想实现每一个线程都有自己的专属值该如何解决呢\n?jdk中提供的threadlocal类正是为了解决这样的问题。\n比如simpledateformat 不是线程安全的,所以每个线程都要有自己独立的副本\n\n\n\n# threadlocal底层是什么数据结构?\n\n\n实际上是map,key为threadlocal变量、value为值。\n\n\n\n\n我们先看下thread类的源码\n\npublic class thread implements runnable {\n ......\n//与此线程有关的threadlocal值。由threadlocal类维护\nthreadlocal.threadlocalmap threadlocals = null;\n\n/*\n * inheritablethreadlocal,自父线程集成而来的threadlocalmap,\n * 主要用于父子线程间threadlocal变量的传递\n * 本文主要讨论的就是这个threadlocalmap\n */\nthreadlocal.threadlocalmap inheritablethreadlocals = null;\n ......\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n我们可以把 threadlocal.threadlocalmap 理解为threadlocal 类实现的定制化的 hashmap. 我们再来看下 threadlocal 类的源码\n\n //调用thread.set 实际上是往当前线程的 threadlocalmap 里面put一个键值对\n public void set(t value) {\n thread t = thread.currentthread();\n threadlocalmap map = getmap(t);\n if (map != null)\n map.set(this, value);\n else\n createmap(t, value);\n }\n\n //调用thread.getmap 可以获取到当前线程的 threadlocalmap\n threadlocalmap getmap(thread t) {\n return t.threadlocals;\n }\n\n //threadlocalmap可以存储以threadlocal为key ,object 对象为 value的键值对。\n threadlocalmap(threadlocal firstkey, object firstvalue) {\n ......\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n\n\n\n# threadlocal有什么应用场景?\n\n\n * 非线程安全的工具类,需要每个线程持有一个副本,比如 simpledateformat\n * 异步传递traceid\n\n\n# 为什么要使用弱引用?\n\n\nthreadlocalmap的内部类entry被设计为实现了weakreference,entry用来存放数据。\n\n\n弱引用简单理解就是当垃圾回收时,该对象只被weakreference对象的弱引用字段所引用,而未被任何强类型的对象引用,那么,该弱引用的对象就会被回收。\n注意:weakreference引用本身是强引用,它内部的(t reference)才是真正的弱引用字段,weakreference就是一个装弱引用的容器而已。\n那 为什么要使用弱引用呢?\n这是因为:threadlocalmap本身并没有为外界提供取出和存放数据的api,我们所能获得数据的方式只有通过threadlocal类提供的api来间接的从threadlocalmap取出数据,所以,当我们用不了key(threadlocal对象)的api也就无法从threadlocalmap里取出指定的数据。\n\n一般我们new 一个threadlocal对象的时候,它一定会有强引用,在threadlocalmap中也一定会有它的弱引用\n当强引用不在的时候一定是我们的程序不再需要这个threadlocal对象了 为什么这么说?\n比如我定义了一个 threadlocal 变量 formatter,formatter 对 threadlocal 变量的强引用关系不存在的一个case 是getdate()方法执行完了,那么当然threadlocal 变量是可以回收的。\n\n\npublic void getdate(){\n\n threadlocal formatter = threadlocal.withinitial(() -> new simpledateformat(\"yyyymmdd hhmm\"));\n ......\n}\n\n\n\n1\n2\n3\n4\n5\n6\n\n\n\n# 子线程可以从父线程继承 threadlocal 吗?\n\n\nthreadlocal 不可以。inheritablethreadlocals 是可以的,它重写了threadlocal的三个方法。childvalue,createmap,getmap。\n\n\ninheritablethreadlocal 不能和线程池搭配使用\n因为线程池中的线程是复用的,并没有重新初始化线程,inheritablethreadlocal之所以起作用是因为在thread类中最终会调用init()方法去把inheritablethreadlocal的map复制到子线程中。\n由于线程池复用了已有线程,所以没有调用init()方法这个过程,也就不能将父线程中的inheritablethreadlocal值传给子线程。\n\n\n# threadlocal使用不当会有内存泄漏是怎么回事?\n\n\nthreadlocalmap 中使用的 key 为 threadlocal 的弱引用,而 value 是强引用。\n所以,如果 threadlocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。\n这样一来,threadlocalmap 中就会出现key为null的entry。假如我们不做任何措施的话,value 永远无法被gc 回收,这个时候就可能会产生内存泄露。\n\n\n\n# 使用 threadlocal 如何防止内存泄漏 ?\n\n\n使用完 threadlocal 手动调用remove方法。 看下 threadlocal 的 remove() 会清理 threadlocalmap 中 key 为 null的键值对。\n\npublic void remove() {\n threadlocalmap m = getmap(thread.currentthread());\n if (m != null)\n m.remove(this);\n}\n\n\n1\n2\n3\n4\n5\n\n\nthreadlocal 的 get() 有清除操作,那我们是不是不用手动调用 remove() 了?\n不是的。因为 get() 中清除操作 只会检测本次get的 threadlocal 变量是否需要清理 。而 remove() 会检测 threadlocalmap 中的所有键值对。\n\n\n所以,手动调用remove() 才是最保险的。\n\n\n\n# netty 的fastthreadlocal fast在哪里?\n\n\n既然jdk已经有threadlocal,为何netty还要自己造个fastthreadlocal?fastthreadlocal快在哪里?\n这需要从jdk threadlocal的本身说起。如下图:\n在java线程中,每个线程都有一个threadlocalmap实例变量(如果不使用threadlocal,不会创建这个map,一个线程第一次访问某个threadlocal变量时,才会创建)。该map是使用线性探测的方式解决hash冲突的问题,如果没有找到空闲的slot,就不断往后尝试,直到找到一个空闲的位置,插入entry,这种方式在经常遇到hash冲突时,影响效率。\n\nfastthreadlocal(下文简称ftl)直接使用数组避免了hash冲突的发生,具体做法是:每一个fastthreadlocal实例创建时,分配一个下标index;分配index使用atomicinteger实现,每个fastthreadlocal都能获取到一个不重复的下标。当调用ftl.get()方法获取值时,直接从数组获取返回,如return array[index],如下图:\n\nfastthreadlocal 底层结构代码\n\nstatic final threadlocal slowthreadlocalmap = new threadlocal();\nstatic final atomicinteger nextindex = new atomicinteger();\nobject[] indexedvariables;\n\n\n1\n2\n3\n\n\n\n# fastthreadlocal 性能 比 threadlocal 高多少?\n\n\n远高于。\nfastthreadlocal 对 threadlocal 的优化点在于,将元素放入 threadlocalmap 采用数组结构随机访问代替 原来的 线性探测。 所以我们测试场景为:单线程访问多 fastthreadlocal/threadlocal 变量:\n\n/**\n * 单线程访问多个threadlocal\n */\npublic static void testthreadlocalwithmultiplethreadlocal() {\n threadlocal threadlocal[] = new threadlocal[count];\n for (int i = 0; i < count; i++) {\n threadlocal[i] = new threadlocal();\n }\n new thread(new runnable() {\n @override\n public void run() {\n long start = system.currenttimemillis();\n for (int i = 0; i < count; i++) {\n threadlocal[i].set(\"value\" + i);\n }\n long middle = system.currenttimemillis();\n for (int i = 0; i < count; i++) {\n for (int j = 0; j < count; j++) {\n threadlocal[i].get();\n }\n }\n long end = system.currenttimemillis();\n system.out.println(\"testthreadlocalwithmultiplethreadlocal set:\" + (middle - start) + \",get:\" + (end - middle));\n }\n }).start();\n}\n\n/**\n * 单线程访问多个fastthreadlocal\n */\npublic static void testfastthreadlocalwithmultiplefastthreadlocal() {\n fastthreadlocal threadlocal[] = new fastthreadlocal[count];\n for (int i = 0; i < count; i++) {\n threadlocal[i] = new fastthreadlocal();\n }\n new fastthreadlocalthread(new runnable() {\n @override\n public void run() {\n long start = system.currenttimemillis();\n for (int i = 0; i < count; i++) {\n threadlocal[i].set(\"value\" + i);\n }\n long middle = system.currenttimemillis();\n for (int i = 0; i < count; i++) {\n for (int j = 0; j < count; j++) {\n threadlocal[i].get();\n }\n }\n long end = system.currenttimemillis();\n system.out.println(\"testfastthreadlocalwithmultiplefastthreadlocal set:\" + (middle - start) + \",get:\" + (end - middle));\n }\n }).start();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n\n\n输出:\n\ntestthreadlocalwithmultiplethreadlocal set:68,get:21492\ntestfastthreadlocalwithmultiplefastthreadlocal set:61,get:8\n\n\n\n\n1\n2\n3\n4\n\n\n有结果可知,fastthreadlocal 性能远高于 threadlocal。", + "headersStr": "引用计数法 可达性分析算法 引用类型总结 如何判断一个常量是废弃常量? 如何判断一个类是无用的类", + "content": "Java 中垃圾回收机制中如何判断对象需要回收? 即死亡对象判断方法 堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。\n\n\n# 引用计数法\n\n给对象中添加一个引用计数器:\n\n每当有一个地方引用它,计数器就加 1; 当引用失效,计数器就减 1; 任何时候计数器为 0 的对象就是不可能再被使用的。 这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。\n\n所谓对象之间的相互引用问题,如下面代码所示:除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。\n\npublic class ReferenceCountingGc {\n Object instance = null;\n public static void main(String[] args) {\n ReferenceCountingGc objA = new ReferenceCountingGc();\n ReferenceCountingGc objB = new ReferenceCountingGc();\n objA.instance = objB;\n objB.instance = objA;\n objA = null;\n objB = null;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n\n# 可达性分析算法\n\n这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。\n\n下图中的 Object 6 ~ Object 10 之间虽有引用关系,但它们到 GC Roots 不可达,因此为需要被回收的对象。\n\n哪些对象可以作为 GC Roots 呢?\n\n虚拟机栈(栈帧中的本地变量表)中引用的对象 本地方法栈(Native 方法)中引用的对象 方法区中类静态属性引用的对象 方法区中常量引用的对象 所有被同步锁持有的对象 对象可以被回收,就代表一定会被回收吗?\n\n即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 'finalize' 方法。当对象没有覆盖 'finalize' 方法,或 'finalize' 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。\n\n被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。\n\n> Object 类中的 finalize 方法一直被认为是一个糟糕的设计,成为了 Java 语言的负担,影响了 Java 语言的安全和 GC 的性能。JDK9 版本及后续版本中各个类中的 finalize 方法会被逐渐弃用移除。忘掉它的存在吧!\n\n\n# 引用类型总结\n\n无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。\n\nJDK1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。\n\nJDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)\n\n1.强引用(StrongReference)\n\n以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。\n\n2.软引用(SoftReference)\n\n如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。\n\n软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。\n\n3.弱引用(WeakReference)\n\n如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。\n\n弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。\n\n4.虚引用(PhantomReference)\n\n\"虚引用\"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。\n\n虚引用主要用来跟踪对象被垃圾回收的活动。\n\n虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。\n\n特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。\n\n\n# 如何判断一个常量是废弃常量?\n\n运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?\n\n假如在字符串常量池中存在字符串 \"abc\",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 \"abc\" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,\"abc\" 就会被系统清理出常量池了。\n\n\n# 如何判断一个类是无用的类\n\n方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?\n\n判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :\n\n * 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。\n * 加载该类的 ClassLoader 已经被回收。\n * 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。", + "normalizedContent": "java 中垃圾回收机制中如何判断对象需要回收? 即死亡对象判断方法 堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。\n\n\n# 引用计数法\n\n给对象中添加一个引用计数器:\n\n每当有一个地方引用它,计数器就加 1; 当引用失效,计数器就减 1; 任何时候计数器为 0 的对象就是不可能再被使用的。 这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。\n\n所谓对象之间的相互引用问题,如下面代码所示:除了对象 obja 和 objb 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 gc 回收器回收他们。\n\npublic class referencecountinggc {\n object instance = null;\n public static void main(string[] args) {\n referencecountinggc obja = new referencecountinggc();\n referencecountinggc objb = new referencecountinggc();\n obja.instance = objb;\n objb.instance = obja;\n obja = null;\n objb = null;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n\n# 可达性分析算法\n\n这个算法的基本思想就是通过一系列的称为 “gc roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 gc roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。\n\n下图中的 object 6 ~ object 10 之间虽有引用关系,但它们到 gc roots 不可达,因此为需要被回收的对象。\n\n哪些对象可以作为 gc roots 呢?\n\n虚拟机栈(栈帧中的本地变量表)中引用的对象 本地方法栈(native 方法)中引用的对象 方法区中类静态属性引用的对象 方法区中常量引用的对象 所有被同步锁持有的对象 对象可以被回收,就代表一定会被回收吗?\n\n即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 'finalize' 方法。当对象没有覆盖 'finalize' 方法,或 'finalize' 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。\n\n被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。\n\n> object 类中的 finalize 方法一直被认为是一个糟糕的设计,成为了 java 语言的负担,影响了 java 语言的安全和 gc 的性能。jdk9 版本及后续版本中各个类中的 finalize 方法会被逐渐弃用移除。忘掉它的存在吧!\n\n\n# 引用类型总结\n\n无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。\n\njdk1.2 之前,java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。\n\njdk1.2 以后,java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)\n\n1.强引用(strongreference)\n\n以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,java 虚拟机宁愿抛出 outofmemoryerror 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。\n\n2.软引用(softreference)\n\n如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。\n\n软引用可以和一个引用队列(referencequeue)联合使用,如果软引用所引用的对象被垃圾回收,java 虚拟机就会把这个软引用加入到与之关联的引用队列中。\n\n3.弱引用(weakreference)\n\n如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。\n\n弱引用可以和一个引用队列(referencequeue)联合使用,如果弱引用所引用的对象被垃圾回收,java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。\n\n4.虚引用(phantomreference)\n\n\"虚引用\"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。\n\n虚引用主要用来跟踪对象被垃圾回收的活动。\n\n虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(referencequeue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。\n\n特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 jvm 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(outofmemory)等问题的产生。\n\n\n# 如何判断一个常量是废弃常量?\n\n运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?\n\n假如在字符串常量池中存在字符串 \"abc\",如果当前没有任何 string 对象引用该字符串常量的话,就说明常量 \"abc\" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,\"abc\" 就会被系统清理出常量池了。\n\n\n# 如何判断一个类是无用的类\n\n方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?\n\n判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :\n\n * 该类所有的实例都已经被回收,也就是 java 堆中不存在该类的任何实例。\n * 加载该类的 classloader 已经被回收。\n * 该类对应的 java.lang.class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。", "charsets": { "cjk": true }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 + "lastUpdated": "2022/09/18, 21:28:02", + "lastUpdatedTimestamp": 1663507682000 }, { - "title": "Java 中垃圾回收机制中如何判断对象需要回收?", + "title": "Java 中如何进行 GC 调优?", "frontmatter": { - "title": "Java 中垃圾回收机制中如何判断对象需要回收?", - "date": "2022-05-21T17:31:41.000Z", - "permalink": "/pages/948850/", + "title": "Java 中如何进行 GC 调优?", + "date": "2022-05-21T17:31:53.000Z", + "permalink": "/pages/07a030/", "categories": [ "Java相关", "JVM" @@ -1962,50 +2026,64 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/001.Java%20%E4%B8%AD%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6%E4%B8%AD%E5%A6%82%E4%BD%95%E5%88%A4%E6%96%AD%E5%AF%B9%E8%B1%A1%E9%9C%80%E8%A6%81%E5%9B%9E%E6%94%B6%EF%BC%9F.html", - "relativePath": "01.Java相关/30.JVM/001.Java 中垃圾回收机制中如何判断对象需要回收?.md", - "key": "v-7cd97c82", - "path": "/pages/948850/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/020.Java%20%E4%B8%AD%E5%A6%82%E4%BD%95%E8%BF%9B%E8%A1%8C%20GC%20%E8%B0%83%E4%BC%98%EF%BC%9F.html", + "relativePath": "01.Java相关/30.JVM/020.Java 中如何进行 GC 调优?.md", + "key": "v-5474bec0", + "path": "/pages/07a030/", "headers": [ { "level": 2, - "title": "引用计数法", - "slug": "引用计数法", - "normalizedTitle": "引用计数法", - "charIndex": 95 + "title": "GC优化需知", + "slug": "gc优化需知", + "normalizedTitle": "gc优化需知", + "charIndex": 305 }, { "level": 2, - "title": "可达性分析算法", - "slug": "可达性分析算法", - "normalizedTitle": "可达性分析算法", - "charIndex": 738 + "title": "JVM内存结构", + "slug": "jvm内存结构", + "normalizedTitle": "jvm内存结构", + "charIndex": 506 }, { "level": 2, - "title": "引用类型总结", - "slug": "引用类型总结", - "normalizedTitle": "引用类型总结", - "charIndex": 1442 + "title": "常见垃圾回收器", + "slug": "常见垃圾回收器", + "normalizedTitle": "常见垃圾回收器", + "charIndex": 1401 }, { "level": 2, - "title": "如何判断一个常量是废弃常量?", - "slug": "如何判断一个常量是废弃常量", - "normalizedTitle": "如何判断一个常量是废弃常量?", - "charIndex": 2760 + "title": "参数基本策略", + "slug": "参数基本策略", + "normalizedTitle": "参数基本策略", + "charIndex": 1751 }, { "level": 2, - "title": "如何判断一个类是无用的类", - "slug": "如何判断一个类是无用的类", - "normalizedTitle": "如何判断一个类是无用的类", - "charIndex": 2930 + "title": "确定目标", + "slug": "确定目标", + "normalizedTitle": "确定目标", + "charIndex": 2165 + }, + { + "level": 2, + "title": "优化", + "slug": "优化", + "normalizedTitle": "优化", + "charIndex": 20 + }, + { + "level": 2, + "title": "验收优化结果", + "slug": "验收优化结果", + "normalizedTitle": "验收优化结果", + "charIndex": 2841 } ], - "headersStr": "引用计数法 可达性分析算法 引用类型总结 如何判断一个常量是废弃常量? 如何判断一个类是无用的类", - "content": "Java 中垃圾回收机制中如何判断对象需要回收? 即死亡对象判断方法 堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。\n\n\n# 引用计数法\n\n给对象中添加一个引用计数器:\n\n每当有一个地方引用它,计数器就加 1; 当引用失效,计数器就减 1; 任何时候计数器为 0 的对象就是不可能再被使用的。 这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。\n\n所谓对象之间的相互引用问题,如下面代码所示:除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。\n\npublic class ReferenceCountingGc {\n Object instance = null;\n public static void main(String[] args) {\n ReferenceCountingGc objA = new ReferenceCountingGc();\n ReferenceCountingGc objB = new ReferenceCountingGc();\n objA.instance = objB;\n objB.instance = objA;\n objA = null;\n objB = null;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n\n# 可达性分析算法\n\n这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。\n\n下图中的 Object 6 ~ Object 10 之间虽有引用关系,但它们到 GC Roots 不可达,因此为需要被回收的对象。\n\n哪些对象可以作为 GC Roots 呢?\n\n虚拟机栈(栈帧中的本地变量表)中引用的对象 本地方法栈(Native 方法)中引用的对象 方法区中类静态属性引用的对象 方法区中常量引用的对象 所有被同步锁持有的对象 对象可以被回收,就代表一定会被回收吗?\n\n即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 'finalize' 方法。当对象没有覆盖 'finalize' 方法,或 'finalize' 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。\n\n被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。\n\n> Object 类中的 finalize 方法一直被认为是一个糟糕的设计,成为了 Java 语言的负担,影响了 Java 语言的安全和 GC 的性能。JDK9 版本及后续版本中各个类中的 finalize 方法会被逐渐弃用移除。忘掉它的存在吧!\n\n\n# 引用类型总结\n\n无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。\n\nJDK1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。\n\nJDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)\n\n1.强引用(StrongReference)\n\n以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。\n\n2.软引用(SoftReference)\n\n如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。\n\n软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。\n\n3.弱引用(WeakReference)\n\n如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。\n\n弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。\n\n4.虚引用(PhantomReference)\n\n\"虚引用\"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。\n\n虚引用主要用来跟踪对象被垃圾回收的活动。\n\n虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。\n\n特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。\n\n\n# 如何判断一个常量是废弃常量?\n\n运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?\n\n假如在字符串常量池中存在字符串 \"abc\",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 \"abc\" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,\"abc\" 就会被系统清理出常量池了。\n\n\n# 如何判断一个类是无用的类\n\n方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?\n\n判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :\n\n * 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。\n * 加载该类的 ClassLoader 已经被回收。\n * 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。", - "normalizedContent": "java 中垃圾回收机制中如何判断对象需要回收? 即死亡对象判断方法 堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。\n\n\n# 引用计数法\n\n给对象中添加一个引用计数器:\n\n每当有一个地方引用它,计数器就加 1; 当引用失效,计数器就减 1; 任何时候计数器为 0 的对象就是不可能再被使用的。 这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。\n\n所谓对象之间的相互引用问题,如下面代码所示:除了对象 obja 和 objb 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 gc 回收器回收他们。\n\npublic class referencecountinggc {\n object instance = null;\n public static void main(string[] args) {\n referencecountinggc obja = new referencecountinggc();\n referencecountinggc objb = new referencecountinggc();\n obja.instance = objb;\n objb.instance = obja;\n obja = null;\n objb = null;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n\n# 可达性分析算法\n\n这个算法的基本思想就是通过一系列的称为 “gc roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 gc roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。\n\n下图中的 object 6 ~ object 10 之间虽有引用关系,但它们到 gc roots 不可达,因此为需要被回收的对象。\n\n哪些对象可以作为 gc roots 呢?\n\n虚拟机栈(栈帧中的本地变量表)中引用的对象 本地方法栈(native 方法)中引用的对象 方法区中类静态属性引用的对象 方法区中常量引用的对象 所有被同步锁持有的对象 对象可以被回收,就代表一定会被回收吗?\n\n即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 'finalize' 方法。当对象没有覆盖 'finalize' 方法,或 'finalize' 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。\n\n被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。\n\n> object 类中的 finalize 方法一直被认为是一个糟糕的设计,成为了 java 语言的负担,影响了 java 语言的安全和 gc 的性能。jdk9 版本及后续版本中各个类中的 finalize 方法会被逐渐弃用移除。忘掉它的存在吧!\n\n\n# 引用类型总结\n\n无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。\n\njdk1.2 之前,java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。\n\njdk1.2 以后,java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)\n\n1.强引用(strongreference)\n\n以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,java 虚拟机宁愿抛出 outofmemoryerror 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。\n\n2.软引用(softreference)\n\n如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。\n\n软引用可以和一个引用队列(referencequeue)联合使用,如果软引用所引用的对象被垃圾回收,java 虚拟机就会把这个软引用加入到与之关联的引用队列中。\n\n3.弱引用(weakreference)\n\n如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。\n\n弱引用可以和一个引用队列(referencequeue)联合使用,如果弱引用所引用的对象被垃圾回收,java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。\n\n4.虚引用(phantomreference)\n\n\"虚引用\"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。\n\n虚引用主要用来跟踪对象被垃圾回收的活动。\n\n虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(referencequeue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。\n\n特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 jvm 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(outofmemory)等问题的产生。\n\n\n# 如何判断一个常量是废弃常量?\n\n运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?\n\n假如在字符串常量池中存在字符串 \"abc\",如果当前没有任何 string 对象引用该字符串常量的话,就说明常量 \"abc\" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,\"abc\" 就会被系统清理出常量池了。\n\n\n# 如何判断一个类是无用的类\n\n方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?\n\n判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :\n\n * 该类所有的实例都已经被回收,也就是 java 堆中不存在该类的任何实例。\n * 加载该类的 classloader 已经被回收。\n * 该类对应的 java.lang.class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。", + "headersStr": "GC优化需知 JVM内存结构 常见垃圾回收器 参数基本策略 确定目标 优化 验收优化结果", + "content": "当Java程序性能达不到既定目标,且其他优化手段都已经穷尽时,通常需要调整垃圾回收器来进一步提高性能,称为GC优化。 但GC算法复杂,影响GC性能的参数众多,且参数调整又依赖于应用各自的特点,这些因素很大程度上增加了GC优化的难度。\n\n即便如此,GC调优也不是无章可循,仍然有一些通用的思考方法。\n\n本篇会介绍这些通用的GC优化策略和相关实践案例,主要包括如下内容:\n\n> 优化前准备: 简单回顾JVM相关知识、介绍GC优化的一些通用策略。\n> 优化方法: 介绍调优的一般流程:明确优化目标→优化→跟踪优化结果。\n> 优化案例: 简述笔者所在团队遇到的GC问题以及优化方案。\n\n\n# 一、优化前的准备\n\n\n# GC优化需知\n\n为了更好地理解本篇所介绍的内容,你需要了解如下内容。 1. GC相关基础知识,包括但不限于: a) GC工作原理。 b) 理解新生代、老年代、晋升等术语含义。 c) 可以看懂GC日志。\n\nGC优化不能解决一切性能问题,它是最后的调优手段。 如果对第一点中提及的知识点不是很熟悉,可以先阅读小结-JVM基础回顾;如果已经很熟悉,可以跳过该节直接往下阅读。\n\n\n# JVM基础回顾\n\n\n# JVM内存结构\n\n简单介绍一下JVM内存结构和常见的垃圾回收器。\n\n当代主流虚拟机(Hotspot VM)的垃圾回收都采用“分代回收”的算法。“分代回收”是基于这样一个事实:对象的生命周期不同,所以针对不同生命周期的对象可以采取不同的回收方式,以便提高回收效率。\n\nHotspot VM将内存划分为不同的物理区,就是“分代”思想的体现。如图所示,JVM内存主要由新生代、老年代、永久代构成。\n\n\n\n① 新生代(Young Generation):大多数对象在新生代中被创建,其中很多对象的生命周期很短。每次新生代的垃圾回收(又称Minor GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。\n\n新生代内又分三个区:一个Eden区,两个Survivor区(一般而言),大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到两个Survivor区(中的一个)。当这个Survivor区满时,此区的存活且不满足“晋升”条件的对象将被复制到另外一个Survivor区。对象每经历一次Minor GC,年龄加1,达到“晋升年龄阈值”后,被放到老年代,这个过程也称为“晋升”。显然,“晋升年龄阈值”的大小直接影响着对象在新生代中的停留时间,在Serial和ParNew GC两种回收器中,“晋升年龄阈值”通过参数MaxTenuringThreshold设定,默认值为15。\n\n② 老年代(Old Generation):在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代,该区域中对象存活率高。老年代的垃圾回收(又称Major GC)通常使用“标记-清理”或“标记-整理”算法。整堆包括新生代和老年代的垃圾回收称为Full GC(HotSpot VM里,除了CMS之外,其它能收集老年代的GC都会同时收集整个GC堆,包括新生代)。\n\n③ 永久代(Perm Generation):主要存放元数据,例如Class、Method的元信息,与垃圾回收要回收的Java对象关系不大。相对于新生代和年老代来说,该区域的划分对垃圾回收影响比较小。\n\n\n# 常见垃圾回收器\n\n不同的垃圾回收器,适用于不同的场景。常用的垃圾回收器:\n\n串行(Serial)回收器是单线程的一个回收器,简单、易实现、效率高。 并行(ParNew)回收器是Serial的多线程版,可以充分的利用CPU资源,减少回收的时间。 吞吐量优先(Parallel Scavenge)回收器,侧重于吞吐量的控制。 并发标记清除(CMS,Concurrent Mark Sweep)回收器是一种以获取最短回收停顿时间为目标的回收器,该回收器是基于“标记-清除”算法实现的。 GC日志 每一种回收器的日志格式都是由其自身的实现决定的,换而言之,每种回收器的日志格式都可以不一样。但虚拟机设计者为了方便用户阅读,将各个回收器的日志都维持一定的共性。JavaGC日志 中简单介绍了这些共性。\n\n\n# 参数基本策略\n\n各分区的大小对GC的性能影响很大。如何将各分区调整到合适的大小,分析活跃数据的大小是很好的切入点。\n\n活跃数据的大小是指,应用程序稳定运行时长期存活对象在堆中占用的空间大小,也就是Full GC后堆中老年代占用空间的大小。可以通过GC日志中Full GC之后老年代数据大小得出,比较准确的方法是在程序稳定后,多次获取GC数据,通过取平均值的方式计算活跃数据的大小。活跃数据和各分区之间的比例关系如下(见参考文献1):\n\n例如,根据GC日志获得老年代的活跃数据大小为300M,那么各分区大小可以设为:\n\n总堆:1200MB = 300MB × 4* 新生代:450MB = 300MB × 1.5* 老年代: 750MB = 1200MB - 450MB*\n\n这部分设置仅仅是堆大小的初始值,后面的优化中,可能会调整这些值,具体情况取决于应用程序的特性和需求。\n\n\n# 二、优化步骤\n\nGC优化一般步骤可以概括为:确定目标、优化参数、验收结果。\n\n\n# 确定目标\n\n明确应用程序的系统需求是性能优化的基础,系统的需求是指应用程序运行时某方面的要求,譬如: - 高可用,可用性达到几个9。 - 低延迟,请求必须多少毫秒内完成响应。 - 高吞吐,每秒完成多少次事务。\n\n明确系统需求之所以重要,是因为上述性能指标间可能冲突。比如通常情况下,缩小延迟的代价是降低吞吐量或者消耗更多的内存或者两者同时发生。\n\n由于笔者所在团队主要关注高可用和低延迟两项指标,所以接下来分析,如何量化GC时间和频率对于响应时间和可用性的影响。通过这个量化指标,可以计算出当前GC情况对服务的影响,也能评估出GC优化后对响应时间的收益,这两点对于低延迟服务很重要。\n\n举例:假设单位时间T内发生一次持续25ms的GC,接口平均响应时间为50ms,且请求均匀到达,根据下图所示:\n\n\n\n那么有(50ms+25ms)/T比例的请求会受GC影响,其中GC前的50ms内到达的请求都会增加25ms,GC期间的25ms内到达的请求,会增加0-25ms不等,如果时间T内发生N次GC,受GC影响请求占比=(接口响应时间+GC时间)×N/T 。可见无论降低单次GC时间还是降低GC次数N都可以有效减少GC对响应时间的影响。\n\n\n# 优化\n\n通过收集GC信息,结合系统需求,确定优化方案,例如选用合适的GC回收器、重新设置内存比例、调整JVM参数等。\n\n进行调整后,将不同的优化方案分别应用到多台机器上,然后比较这些机器上GC的性能差异,有针对性的做出选择,再通过不断的试验和观察,找到最合适的参数。\n\n\n# 验收优化结果\n\n将修改应用到所有服务器,判断优化结果是否符合预期,总结相关经验。\n\n参考:美团技术团队-从实际案例聊聊Java应用的GC优化", + "normalizedContent": "当java程序性能达不到既定目标,且其他优化手段都已经穷尽时,通常需要调整垃圾回收器来进一步提高性能,称为gc优化。 但gc算法复杂,影响gc性能的参数众多,且参数调整又依赖于应用各自的特点,这些因素很大程度上增加了gc优化的难度。\n\n即便如此,gc调优也不是无章可循,仍然有一些通用的思考方法。\n\n本篇会介绍这些通用的gc优化策略和相关实践案例,主要包括如下内容:\n\n> 优化前准备: 简单回顾jvm相关知识、介绍gc优化的一些通用策略。\n> 优化方法: 介绍调优的一般流程:明确优化目标→优化→跟踪优化结果。\n> 优化案例: 简述笔者所在团队遇到的gc问题以及优化方案。\n\n\n# 一、优化前的准备\n\n\n# gc优化需知\n\n为了更好地理解本篇所介绍的内容,你需要了解如下内容。 1. gc相关基础知识,包括但不限于: a) gc工作原理。 b) 理解新生代、老年代、晋升等术语含义。 c) 可以看懂gc日志。\n\ngc优化不能解决一切性能问题,它是最后的调优手段。 如果对第一点中提及的知识点不是很熟悉,可以先阅读小结-jvm基础回顾;如果已经很熟悉,可以跳过该节直接往下阅读。\n\n\n# jvm基础回顾\n\n\n# jvm内存结构\n\n简单介绍一下jvm内存结构和常见的垃圾回收器。\n\n当代主流虚拟机(hotspot vm)的垃圾回收都采用“分代回收”的算法。“分代回收”是基于这样一个事实:对象的生命周期不同,所以针对不同生命周期的对象可以采取不同的回收方式,以便提高回收效率。\n\nhotspot vm将内存划分为不同的物理区,就是“分代”思想的体现。如图所示,jvm内存主要由新生代、老年代、永久代构成。\n\n\n\n① 新生代(young generation):大多数对象在新生代中被创建,其中很多对象的生命周期很短。每次新生代的垃圾回收(又称minor gc)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。\n\n新生代内又分三个区:一个eden区,两个survivor区(一般而言),大部分对象在eden区中生成。当eden区满时,还存活的对象将被复制到两个survivor区(中的一个)。当这个survivor区满时,此区的存活且不满足“晋升”条件的对象将被复制到另外一个survivor区。对象每经历一次minor gc,年龄加1,达到“晋升年龄阈值”后,被放到老年代,这个过程也称为“晋升”。显然,“晋升年龄阈值”的大小直接影响着对象在新生代中的停留时间,在serial和parnew gc两种回收器中,“晋升年龄阈值”通过参数maxtenuringthreshold设定,默认值为15。\n\n② 老年代(old generation):在新生代中经历了n次垃圾回收后仍然存活的对象,就会被放到年老代,该区域中对象存活率高。老年代的垃圾回收(又称major gc)通常使用“标记-清理”或“标记-整理”算法。整堆包括新生代和老年代的垃圾回收称为full gc(hotspot vm里,除了cms之外,其它能收集老年代的gc都会同时收集整个gc堆,包括新生代)。\n\n③ 永久代(perm generation):主要存放元数据,例如class、method的元信息,与垃圾回收要回收的java对象关系不大。相对于新生代和年老代来说,该区域的划分对垃圾回收影响比较小。\n\n\n# 常见垃圾回收器\n\n不同的垃圾回收器,适用于不同的场景。常用的垃圾回收器:\n\n串行(serial)回收器是单线程的一个回收器,简单、易实现、效率高。 并行(parnew)回收器是serial的多线程版,可以充分的利用cpu资源,减少回收的时间。 吞吐量优先(parallel scavenge)回收器,侧重于吞吐量的控制。 并发标记清除(cms,concurrent mark sweep)回收器是一种以获取最短回收停顿时间为目标的回收器,该回收器是基于“标记-清除”算法实现的。 gc日志 每一种回收器的日志格式都是由其自身的实现决定的,换而言之,每种回收器的日志格式都可以不一样。但虚拟机设计者为了方便用户阅读,将各个回收器的日志都维持一定的共性。javagc日志 中简单介绍了这些共性。\n\n\n# 参数基本策略\n\n各分区的大小对gc的性能影响很大。如何将各分区调整到合适的大小,分析活跃数据的大小是很好的切入点。\n\n活跃数据的大小是指,应用程序稳定运行时长期存活对象在堆中占用的空间大小,也就是full gc后堆中老年代占用空间的大小。可以通过gc日志中full gc之后老年代数据大小得出,比较准确的方法是在程序稳定后,多次获取gc数据,通过取平均值的方式计算活跃数据的大小。活跃数据和各分区之间的比例关系如下(见参考文献1):\n\n例如,根据gc日志获得老年代的活跃数据大小为300m,那么各分区大小可以设为:\n\n总堆:1200mb = 300mb × 4* 新生代:450mb = 300mb × 1.5* 老年代: 750mb = 1200mb - 450mb*\n\n这部分设置仅仅是堆大小的初始值,后面的优化中,可能会调整这些值,具体情况取决于应用程序的特性和需求。\n\n\n# 二、优化步骤\n\ngc优化一般步骤可以概括为:确定目标、优化参数、验收结果。\n\n\n# 确定目标\n\n明确应用程序的系统需求是性能优化的基础,系统的需求是指应用程序运行时某方面的要求,譬如: - 高可用,可用性达到几个9。 - 低延迟,请求必须多少毫秒内完成响应。 - 高吞吐,每秒完成多少次事务。\n\n明确系统需求之所以重要,是因为上述性能指标间可能冲突。比如通常情况下,缩小延迟的代价是降低吞吐量或者消耗更多的内存或者两者同时发生。\n\n由于笔者所在团队主要关注高可用和低延迟两项指标,所以接下来分析,如何量化gc时间和频率对于响应时间和可用性的影响。通过这个量化指标,可以计算出当前gc情况对服务的影响,也能评估出gc优化后对响应时间的收益,这两点对于低延迟服务很重要。\n\n举例:假设单位时间t内发生一次持续25ms的gc,接口平均响应时间为50ms,且请求均匀到达,根据下图所示:\n\n\n\n那么有(50ms+25ms)/t比例的请求会受gc影响,其中gc前的50ms内到达的请求都会增加25ms,gc期间的25ms内到达的请求,会增加0-25ms不等,如果时间t内发生n次gc,受gc影响请求占比=(接口响应时间+gc时间)×n/t 。可见无论降低单次gc时间还是降低gc次数n都可以有效减少gc对响应时间的影响。\n\n\n# 优化\n\n通过收集gc信息,结合系统需求,确定优化方案,例如选用合适的gc回收器、重新设置内存比例、调整jvm参数等。\n\n进行调整后,将不同的优化方案分别应用到多台机器上,然后比较这些机器上gc的性能差异,有针对性的做出选择,再通过不断的试验和观察,找到最合适的参数。\n\n\n# 验收优化结果\n\n将修改应用到所有服务器,判断优化结果是否符合预期,总结相关经验。\n\n参考:美团技术团队-从实际案例聊聊java应用的gc优化", "charsets": { "cjk": true }, @@ -2189,84 +2267,6 @@ export const siteData = { "lastUpdated": "2022/09/18, 21:28:02", "lastUpdatedTimestamp": 1663507682000 }, - { - "title": "Java 中如何进行 GC 调优?", - "frontmatter": { - "title": "Java 中如何进行 GC 调优?", - "date": "2022-05-21T17:31:53.000Z", - "permalink": "/pages/07a030/", - "categories": [ - "Java相关", - "JVM" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/020.Java%20%E4%B8%AD%E5%A6%82%E4%BD%95%E8%BF%9B%E8%A1%8C%20GC%20%E8%B0%83%E4%BC%98%EF%BC%9F.html", - "relativePath": "01.Java相关/30.JVM/020.Java 中如何进行 GC 调优?.md", - "key": "v-5474bec0", - "path": "/pages/07a030/", - "headers": [ - { - "level": 2, - "title": "GC优化需知", - "slug": "gc优化需知", - "normalizedTitle": "gc优化需知", - "charIndex": 305 - }, - { - "level": 2, - "title": "JVM内存结构", - "slug": "jvm内存结构", - "normalizedTitle": "jvm内存结构", - "charIndex": 506 - }, - { - "level": 2, - "title": "常见垃圾回收器", - "slug": "常见垃圾回收器", - "normalizedTitle": "常见垃圾回收器", - "charIndex": 1401 - }, - { - "level": 2, - "title": "参数基本策略", - "slug": "参数基本策略", - "normalizedTitle": "参数基本策略", - "charIndex": 1751 - }, - { - "level": 2, - "title": "确定目标", - "slug": "确定目标", - "normalizedTitle": "确定目标", - "charIndex": 2165 - }, - { - "level": 2, - "title": "优化", - "slug": "优化", - "normalizedTitle": "优化", - "charIndex": 20 - }, - { - "level": 2, - "title": "验收优化结果", - "slug": "验收优化结果", - "normalizedTitle": "验收优化结果", - "charIndex": 2841 - } - ], - "headersStr": "GC优化需知 JVM内存结构 常见垃圾回收器 参数基本策略 确定目标 优化 验收优化结果", - "content": "当Java程序性能达不到既定目标,且其他优化手段都已经穷尽时,通常需要调整垃圾回收器来进一步提高性能,称为GC优化。 但GC算法复杂,影响GC性能的参数众多,且参数调整又依赖于应用各自的特点,这些因素很大程度上增加了GC优化的难度。\n\n即便如此,GC调优也不是无章可循,仍然有一些通用的思考方法。\n\n本篇会介绍这些通用的GC优化策略和相关实践案例,主要包括如下内容:\n\n> 优化前准备: 简单回顾JVM相关知识、介绍GC优化的一些通用策略。\n> 优化方法: 介绍调优的一般流程:明确优化目标→优化→跟踪优化结果。\n> 优化案例: 简述笔者所在团队遇到的GC问题以及优化方案。\n\n\n# 一、优化前的准备\n\n\n# GC优化需知\n\n为了更好地理解本篇所介绍的内容,你需要了解如下内容。 1. GC相关基础知识,包括但不限于: a) GC工作原理。 b) 理解新生代、老年代、晋升等术语含义。 c) 可以看懂GC日志。\n\nGC优化不能解决一切性能问题,它是最后的调优手段。 如果对第一点中提及的知识点不是很熟悉,可以先阅读小结-JVM基础回顾;如果已经很熟悉,可以跳过该节直接往下阅读。\n\n\n# JVM基础回顾\n\n\n# JVM内存结构\n\n简单介绍一下JVM内存结构和常见的垃圾回收器。\n\n当代主流虚拟机(Hotspot VM)的垃圾回收都采用“分代回收”的算法。“分代回收”是基于这样一个事实:对象的生命周期不同,所以针对不同生命周期的对象可以采取不同的回收方式,以便提高回收效率。\n\nHotspot VM将内存划分为不同的物理区,就是“分代”思想的体现。如图所示,JVM内存主要由新生代、老年代、永久代构成。\n\n\n\n① 新生代(Young Generation):大多数对象在新生代中被创建,其中很多对象的生命周期很短。每次新生代的垃圾回收(又称Minor GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。\n\n新生代内又分三个区:一个Eden区,两个Survivor区(一般而言),大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到两个Survivor区(中的一个)。当这个Survivor区满时,此区的存活且不满足“晋升”条件的对象将被复制到另外一个Survivor区。对象每经历一次Minor GC,年龄加1,达到“晋升年龄阈值”后,被放到老年代,这个过程也称为“晋升”。显然,“晋升年龄阈值”的大小直接影响着对象在新生代中的停留时间,在Serial和ParNew GC两种回收器中,“晋升年龄阈值”通过参数MaxTenuringThreshold设定,默认值为15。\n\n② 老年代(Old Generation):在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代,该区域中对象存活率高。老年代的垃圾回收(又称Major GC)通常使用“标记-清理”或“标记-整理”算法。整堆包括新生代和老年代的垃圾回收称为Full GC(HotSpot VM里,除了CMS之外,其它能收集老年代的GC都会同时收集整个GC堆,包括新生代)。\n\n③ 永久代(Perm Generation):主要存放元数据,例如Class、Method的元信息,与垃圾回收要回收的Java对象关系不大。相对于新生代和年老代来说,该区域的划分对垃圾回收影响比较小。\n\n\n# 常见垃圾回收器\n\n不同的垃圾回收器,适用于不同的场景。常用的垃圾回收器:\n\n串行(Serial)回收器是单线程的一个回收器,简单、易实现、效率高。 并行(ParNew)回收器是Serial的多线程版,可以充分的利用CPU资源,减少回收的时间。 吞吐量优先(Parallel Scavenge)回收器,侧重于吞吐量的控制。 并发标记清除(CMS,Concurrent Mark Sweep)回收器是一种以获取最短回收停顿时间为目标的回收器,该回收器是基于“标记-清除”算法实现的。 GC日志 每一种回收器的日志格式都是由其自身的实现决定的,换而言之,每种回收器的日志格式都可以不一样。但虚拟机设计者为了方便用户阅读,将各个回收器的日志都维持一定的共性。JavaGC日志 中简单介绍了这些共性。\n\n\n# 参数基本策略\n\n各分区的大小对GC的性能影响很大。如何将各分区调整到合适的大小,分析活跃数据的大小是很好的切入点。\n\n活跃数据的大小是指,应用程序稳定运行时长期存活对象在堆中占用的空间大小,也就是Full GC后堆中老年代占用空间的大小。可以通过GC日志中Full GC之后老年代数据大小得出,比较准确的方法是在程序稳定后,多次获取GC数据,通过取平均值的方式计算活跃数据的大小。活跃数据和各分区之间的比例关系如下(见参考文献1):\n\n例如,根据GC日志获得老年代的活跃数据大小为300M,那么各分区大小可以设为:\n\n总堆:1200MB = 300MB × 4* 新生代:450MB = 300MB × 1.5* 老年代: 750MB = 1200MB - 450MB*\n\n这部分设置仅仅是堆大小的初始值,后面的优化中,可能会调整这些值,具体情况取决于应用程序的特性和需求。\n\n\n# 二、优化步骤\n\nGC优化一般步骤可以概括为:确定目标、优化参数、验收结果。\n\n\n# 确定目标\n\n明确应用程序的系统需求是性能优化的基础,系统的需求是指应用程序运行时某方面的要求,譬如: - 高可用,可用性达到几个9。 - 低延迟,请求必须多少毫秒内完成响应。 - 高吞吐,每秒完成多少次事务。\n\n明确系统需求之所以重要,是因为上述性能指标间可能冲突。比如通常情况下,缩小延迟的代价是降低吞吐量或者消耗更多的内存或者两者同时发生。\n\n由于笔者所在团队主要关注高可用和低延迟两项指标,所以接下来分析,如何量化GC时间和频率对于响应时间和可用性的影响。通过这个量化指标,可以计算出当前GC情况对服务的影响,也能评估出GC优化后对响应时间的收益,这两点对于低延迟服务很重要。\n\n举例:假设单位时间T内发生一次持续25ms的GC,接口平均响应时间为50ms,且请求均匀到达,根据下图所示:\n\n\n\n那么有(50ms+25ms)/T比例的请求会受GC影响,其中GC前的50ms内到达的请求都会增加25ms,GC期间的25ms内到达的请求,会增加0-25ms不等,如果时间T内发生N次GC,受GC影响请求占比=(接口响应时间+GC时间)×N/T 。可见无论降低单次GC时间还是降低GC次数N都可以有效减少GC对响应时间的影响。\n\n\n# 优化\n\n通过收集GC信息,结合系统需求,确定优化方案,例如选用合适的GC回收器、重新设置内存比例、调整JVM参数等。\n\n进行调整后,将不同的优化方案分别应用到多台机器上,然后比较这些机器上GC的性能差异,有针对性的做出选择,再通过不断的试验和观察,找到最合适的参数。\n\n\n# 验收优化结果\n\n将修改应用到所有服务器,判断优化结果是否符合预期,总结相关经验。\n\n参考:美团技术团队-从实际案例聊聊Java应用的GC优化", - "normalizedContent": "当java程序性能达不到既定目标,且其他优化手段都已经穷尽时,通常需要调整垃圾回收器来进一步提高性能,称为gc优化。 但gc算法复杂,影响gc性能的参数众多,且参数调整又依赖于应用各自的特点,这些因素很大程度上增加了gc优化的难度。\n\n即便如此,gc调优也不是无章可循,仍然有一些通用的思考方法。\n\n本篇会介绍这些通用的gc优化策略和相关实践案例,主要包括如下内容:\n\n> 优化前准备: 简单回顾jvm相关知识、介绍gc优化的一些通用策略。\n> 优化方法: 介绍调优的一般流程:明确优化目标→优化→跟踪优化结果。\n> 优化案例: 简述笔者所在团队遇到的gc问题以及优化方案。\n\n\n# 一、优化前的准备\n\n\n# gc优化需知\n\n为了更好地理解本篇所介绍的内容,你需要了解如下内容。 1. gc相关基础知识,包括但不限于: a) gc工作原理。 b) 理解新生代、老年代、晋升等术语含义。 c) 可以看懂gc日志。\n\ngc优化不能解决一切性能问题,它是最后的调优手段。 如果对第一点中提及的知识点不是很熟悉,可以先阅读小结-jvm基础回顾;如果已经很熟悉,可以跳过该节直接往下阅读。\n\n\n# jvm基础回顾\n\n\n# jvm内存结构\n\n简单介绍一下jvm内存结构和常见的垃圾回收器。\n\n当代主流虚拟机(hotspot vm)的垃圾回收都采用“分代回收”的算法。“分代回收”是基于这样一个事实:对象的生命周期不同,所以针对不同生命周期的对象可以采取不同的回收方式,以便提高回收效率。\n\nhotspot vm将内存划分为不同的物理区,就是“分代”思想的体现。如图所示,jvm内存主要由新生代、老年代、永久代构成。\n\n\n\n① 新生代(young generation):大多数对象在新生代中被创建,其中很多对象的生命周期很短。每次新生代的垃圾回收(又称minor gc)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。\n\n新生代内又分三个区:一个eden区,两个survivor区(一般而言),大部分对象在eden区中生成。当eden区满时,还存活的对象将被复制到两个survivor区(中的一个)。当这个survivor区满时,此区的存活且不满足“晋升”条件的对象将被复制到另外一个survivor区。对象每经历一次minor gc,年龄加1,达到“晋升年龄阈值”后,被放到老年代,这个过程也称为“晋升”。显然,“晋升年龄阈值”的大小直接影响着对象在新生代中的停留时间,在serial和parnew gc两种回收器中,“晋升年龄阈值”通过参数maxtenuringthreshold设定,默认值为15。\n\n② 老年代(old generation):在新生代中经历了n次垃圾回收后仍然存活的对象,就会被放到年老代,该区域中对象存活率高。老年代的垃圾回收(又称major gc)通常使用“标记-清理”或“标记-整理”算法。整堆包括新生代和老年代的垃圾回收称为full gc(hotspot vm里,除了cms之外,其它能收集老年代的gc都会同时收集整个gc堆,包括新生代)。\n\n③ 永久代(perm generation):主要存放元数据,例如class、method的元信息,与垃圾回收要回收的java对象关系不大。相对于新生代和年老代来说,该区域的划分对垃圾回收影响比较小。\n\n\n# 常见垃圾回收器\n\n不同的垃圾回收器,适用于不同的场景。常用的垃圾回收器:\n\n串行(serial)回收器是单线程的一个回收器,简单、易实现、效率高。 并行(parnew)回收器是serial的多线程版,可以充分的利用cpu资源,减少回收的时间。 吞吐量优先(parallel scavenge)回收器,侧重于吞吐量的控制。 并发标记清除(cms,concurrent mark sweep)回收器是一种以获取最短回收停顿时间为目标的回收器,该回收器是基于“标记-清除”算法实现的。 gc日志 每一种回收器的日志格式都是由其自身的实现决定的,换而言之,每种回收器的日志格式都可以不一样。但虚拟机设计者为了方便用户阅读,将各个回收器的日志都维持一定的共性。javagc日志 中简单介绍了这些共性。\n\n\n# 参数基本策略\n\n各分区的大小对gc的性能影响很大。如何将各分区调整到合适的大小,分析活跃数据的大小是很好的切入点。\n\n活跃数据的大小是指,应用程序稳定运行时长期存活对象在堆中占用的空间大小,也就是full gc后堆中老年代占用空间的大小。可以通过gc日志中full gc之后老年代数据大小得出,比较准确的方法是在程序稳定后,多次获取gc数据,通过取平均值的方式计算活跃数据的大小。活跃数据和各分区之间的比例关系如下(见参考文献1):\n\n例如,根据gc日志获得老年代的活跃数据大小为300m,那么各分区大小可以设为:\n\n总堆:1200mb = 300mb × 4* 新生代:450mb = 300mb × 1.5* 老年代: 750mb = 1200mb - 450mb*\n\n这部分设置仅仅是堆大小的初始值,后面的优化中,可能会调整这些值,具体情况取决于应用程序的特性和需求。\n\n\n# 二、优化步骤\n\ngc优化一般步骤可以概括为:确定目标、优化参数、验收结果。\n\n\n# 确定目标\n\n明确应用程序的系统需求是性能优化的基础,系统的需求是指应用程序运行时某方面的要求,譬如: - 高可用,可用性达到几个9。 - 低延迟,请求必须多少毫秒内完成响应。 - 高吞吐,每秒完成多少次事务。\n\n明确系统需求之所以重要,是因为上述性能指标间可能冲突。比如通常情况下,缩小延迟的代价是降低吞吐量或者消耗更多的内存或者两者同时发生。\n\n由于笔者所在团队主要关注高可用和低延迟两项指标,所以接下来分析,如何量化gc时间和频率对于响应时间和可用性的影响。通过这个量化指标,可以计算出当前gc情况对服务的影响,也能评估出gc优化后对响应时间的收益,这两点对于低延迟服务很重要。\n\n举例:假设单位时间t内发生一次持续25ms的gc,接口平均响应时间为50ms,且请求均匀到达,根据下图所示:\n\n\n\n那么有(50ms+25ms)/t比例的请求会受gc影响,其中gc前的50ms内到达的请求都会增加25ms,gc期间的25ms内到达的请求,会增加0-25ms不等,如果时间t内发生n次gc,受gc影响请求占比=(接口响应时间+gc时间)×n/t 。可见无论降低单次gc时间还是降低gc次数n都可以有效减少gc对响应时间的影响。\n\n\n# 优化\n\n通过收集gc信息,结合系统需求,确定优化方案,例如选用合适的gc回收器、重新设置内存比例、调整jvm参数等。\n\n进行调整后,将不同的优化方案分别应用到多台机器上,然后比较这些机器上gc的性能差异,有针对性的做出选择,再通过不断的试验和观察,找到最合适的参数。\n\n\n# 验收优化结果\n\n将修改应用到所有服务器,判断优化结果是否符合预期,总结相关经验。\n\n参考:美团技术团队-从实际案例聊聊java应用的gc优化", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/09/18, 21:28:02", - "lastUpdatedTimestamp": 1663507682000 - }, { "title": "JMM 中内存模型是怎样的?什么是指令序列重排序?", "frontmatter": { @@ -2332,96 +2332,61 @@ export const siteData = { "lastUpdatedTimestamp": 1663507682000 }, { - "title": "实现单例设计模式(懒汉,饿汉)", + "title": "JVM 是怎么去调优的?了解哪些参数和指令?", "frontmatter": { - "title": "实现单例设计模式(懒汉,饿汉)", - "date": "2022-05-21T17:26:00.000Z", - "permalink": "/pages/b37ad7/", + "title": "JVM 是怎么去调优的?了解哪些参数和指令?", + "date": "2022-05-21T17:33:16.000Z", + "permalink": "/pages/d730dd/", "categories": [ "Java相关", - "设计模式" + "JVM" ], "tags": [ null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/002.%E5%AE%9E%E7%8E%B0%E5%8D%95%E4%BE%8B%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%EF%BC%88%E6%87%92%E6%B1%89%EF%BC%8C%E9%A5%BF%E6%B1%89%EF%BC%89.html", - "relativePath": "01.Java相关/40.设计模式/002.实现单例设计模式(懒汉,饿汉).md", - "key": "v-ba94c306", - "path": "/pages/b37ad7/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/050.JVM%20%E6%98%AF%E6%80%8E%E4%B9%88%E5%8E%BB%E8%B0%83%E4%BC%98%E7%9A%84%EF%BC%9F%E4%BA%86%E8%A7%A3%E5%93%AA%E4%BA%9B%E5%8F%82%E6%95%B0%E5%92%8C%E6%8C%87%E4%BB%A4%EF%BC%9F.html", + "relativePath": "01.Java相关/30.JVM/050.JVM 是怎么去调优的?了解哪些参数和指令?.md", + "key": "v-37fcb440", + "path": "/pages/d730dd/", "headers": [ { "level": 2, - "title": "单例模式的定义", - "slug": "单例模式的定义", - "normalizedTitle": "单例模式的定义", - "charIndex": 252 - }, - { - "level": 2, - "title": "单例模式的类图", - "slug": "单例模式的类图", - "normalizedTitle": "单例模式的类图", - "charIndex": 294 + "title": "查看进程", + "slug": "查看进程", + "normalizedTitle": "查看进程", + "charIndex": 2 }, { "level": 2, - "title": "单例模式的实现", - "slug": "单例模式的实现", - "normalizedTitle": "单例模式的实现", - "charIndex": 15 - }, - { - "level": 3, - "title": "饿汉式", - "slug": "饿汉式", - "normalizedTitle": "饿汉式", - "charIndex": 320 - }, - { - "level": 3, - "title": "懒汉式", - "slug": "懒汉式", - "normalizedTitle": "懒汉式", - "charIndex": 856 - }, - { - "level": 3, - "title": "静态内部类", - "slug": "静态内部类", - "normalizedTitle": "静态内部类", - "charIndex": 2627 - }, - { - "level": 3, - "title": "枚举", - "slug": "枚举", - "normalizedTitle": "枚举", - "charIndex": 3151 + "title": "查看JVM参数 jinfo", + "slug": "查看jvm参数-jinfo", + "normalizedTitle": "查看jvm参数 jinfo", + "charIndex": 162 }, { "level": 2, - "title": "单例模式在JDK 中的源码分析", - "slug": "单例模式在jdk-中的源码分析", - "normalizedTitle": "单例模式在jdk 中的源码分析", - "charIndex": 3399 + "title": "查看各项指标数据 jstat", + "slug": "查看各项指标数据-jstat", + "normalizedTitle": "查看各项指标数据 jstat", + "charIndex": 251 }, { "level": 2, - "title": "单例模式注意事项和细节", - "slug": "单例模式注意事项和细节", - "normalizedTitle": "单例模式注意事项和细节", - "charIndex": 3459 + "title": "jmap", + "slug": "jmap", + "normalizedTitle": "jmap", + "charIndex": 588 } ], - "headersStr": "单例模式的定义 单例模式的类图 单例模式的实现 饿汉式 懒汉式 静态内部类 枚举 单例模式在JDK 中的源码分析 单例模式注意事项和细节", - "content": "> 面试官:带笔了吧,那写两种单例模式的实现方法吧 沙沙沙刷刷刷~~~ 写好了 面试官:怎样防止new 对象出来?\n\n有一些对象我们确实只需要一个,比如,线程池、数据库连接、缓存、日志对象等,如果有多个的话,会造成程序的行为异常,资源使用过量或者不一致的问题。你也许会说,这种我用全局变量不也能实现吗,还整个单例模式,好像你很流弊的样子,如果将对象赋值给一个全局变量,那程序启动就会创建好对象,万一这个对象很耗资源,我们还可能在某些时候用不到,这就造成了资源的浪费,不合理,所以就有了单例模式。\n\n\n# 单例模式的定义\n\n单例模式确保一个类只有一个实例,并提供一个全局唯一访问点\n\n\n# 单例模式的类图\n\n\n\n\n# 单例模式的实现\n\n\n# 饿汉式\n\nstatic 变量在类装载的时候进行初始化 多个实例的 static 变量会共享同一块内存区域 用这两个知识点写出的单例类就是饿汉式了,初始化类的时候就创建,饥不择食,饿汉\n\npublic class Singleton {\n\n //构造私有化,防止直接new\n private Singleton(){}\n\n //静态初始化器(static initializer)中创建实例,保证线程安全\n private static Singleton instance = new Singleton();\n\n public static Singleton getInstance(){\n return instance;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n饿汉式是线程安全的,JVM在加载类时马上创建唯一的实例对象,且只会装载一次。\n\nJava 实现的单例是一个虚拟机的范围,因为装载类的功能是虚拟机的,所以一个虚拟机通过自己的ClassLoader 装载饿汉式实现单例类的时候就会创建一个类实例。(如果一个虚拟机里有多个ClassLoader的话,就会有多个实例)\n\n\n# 懒汉式\n\n懒汉式,就是实例在用到的时候才去创建,比较“懒”\n\n单例模式的懒汉式实现方式体现了延迟加载的思想(延迟加载也称懒加载Lazy Load,就是一开始不要加载资源或数据,等到要使用的时候才加载)\n\n同步方法\n\npublic class Singleton {\n private static Singleton singleton;\n\n private Singleton(){}\n\n \t//解决了线程不安全问题,但是效率太低了,每个线程想获得类的实例的时候,都需要同步方法,不推荐\n public static synchronized Singleton getInstance(){\n if(singleton == null){\n singleton = new Singleton();\n }\n return singleton;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n双重检查加锁\n\npublic class Singleton {\n\n \t//volatitle关键词确保,多线程正确处理singleton\n private static volatile Singleton singleton;\n \n private Singleton(){}\n \n public static Singleton getInstance(){\n if(singleton ==null){\n synchronized (Singleton.class){\n if(singleton == null){\n singleton = new Singleton();\n }\n }\n }\n return singleton;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n\n\nDouble-Check 概念(进行两次检查)是多线程开发中经常使用的,为什么需要双重检查锁呢?因为第一次检查是确保之前是一个空对象,而非空对象就不需要同步了,空对象的线程然后进入同步代码块,如果不加第二次空对象检查,两个线程同时获取同步代码块,一个线程进入同步代码块,另一个线程就会等待,而这两个线程就会创建两个实例化对象,所以需要在线程进入同步代码块后再次进行空对象检查,才能确保只创建一个实例化对象。\n\n双重检查加锁(double checked locking)线程安全、延迟加载、效率比较高\n\nvolatile:volatile一般用于多线程的可见性,这里用来防止指令重排(防止new Singleton时指令重排序导致其他线程获取到未初始化完的对象)。被volatile 修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。\n\n指令重排\n\n指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序。\n\nJava中创建一个对象,往往包含三个过程。对于singleton = new Singleton(),这不是一个原子操作,在 JVM 中包含如下三个过程。\n\n给 singleton 分配内存\n\n调用 Singleton 的构造函数来初始化成员变量,形成实例\n\n将 singleton 对象指向分配的内存空间(执行完这步 singleton才是非 null 了)\n\n但是,由于JVM会进行指令重排序,所以上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3,也可能是 1-3-2。\n\n如果是 1-3-2,则在 3 执行完毕,2 未执行之前,被另一个线程抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以这个线程会直接返回 instance,然后使用,那肯定就会报错了,所以要加入 volatile关键字。\n\n\n# 静态内部类\n\npublic class Singleton {\n\n private Singleton(){}\n\n private static class SingletonInstance{\n private static final Singleton INSTANCE = new Singleton();\n }\n \n public static Singleton getInstance(){\n return SingletonInstance.INSTANCE;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n采用类加载的机制来保证初始化实例时只有一个线程;\n\n静态内部类方式在Singleton 类被装载的时候并不会立即实例化,而是在调用getInstance的时候,才去装载内部类SingletonInstance ,从而完成Singleton的实例化\n\n类的静态属性只会在第一次加载类的时候初始化,所以,JVM帮我们保证了线程的安全性,在类初始化时,其他线程无法进入\n\n优点:线程安全,利用静态内部类实现延迟加载,效率较高,推荐使用\n\n\n# 枚举\n\nenum Singleton{\n INSTANCE;\n public void method(){}\n}\n\n\n1\n2\n3\n4\n\n\n借助JDK5 添加的枚举实现单例,不仅可以避免多线程同步问题,还能防止反序列化重新创建新的对象,但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。不过,由于Java1.5中才加入enum特性,所以使用的人并不多。\n\n这种方式是《Effective Java》 作者Josh Bloch 提倡的方式。\n\n\n# 单例模式在JDK 中的源码分析\n\nJDK 中,java.lang.Runtime 就是经典的单例模式(饿汉式)\n\n\n# 单例模式注意事项和细节\n\n单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使 用new 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或 耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数 据库或文件的对象(比如数据源、session工厂等)", - "normalizedContent": "> 面试官:带笔了吧,那写两种单例模式的实现方法吧 沙沙沙刷刷刷~~~ 写好了 面试官:怎样防止new 对象出来?\n\n有一些对象我们确实只需要一个,比如,线程池、数据库连接、缓存、日志对象等,如果有多个的话,会造成程序的行为异常,资源使用过量或者不一致的问题。你也许会说,这种我用全局变量不也能实现吗,还整个单例模式,好像你很流弊的样子,如果将对象赋值给一个全局变量,那程序启动就会创建好对象,万一这个对象很耗资源,我们还可能在某些时候用不到,这就造成了资源的浪费,不合理,所以就有了单例模式。\n\n\n# 单例模式的定义\n\n单例模式确保一个类只有一个实例,并提供一个全局唯一访问点\n\n\n# 单例模式的类图\n\n\n\n\n# 单例模式的实现\n\n\n# 饿汉式\n\nstatic 变量在类装载的时候进行初始化 多个实例的 static 变量会共享同一块内存区域 用这两个知识点写出的单例类就是饿汉式了,初始化类的时候就创建,饥不择食,饿汉\n\npublic class singleton {\n\n //构造私有化,防止直接new\n private singleton(){}\n\n //静态初始化器(static initializer)中创建实例,保证线程安全\n private static singleton instance = new singleton();\n\n public static singleton getinstance(){\n return instance;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n饿汉式是线程安全的,jvm在加载类时马上创建唯一的实例对象,且只会装载一次。\n\njava 实现的单例是一个虚拟机的范围,因为装载类的功能是虚拟机的,所以一个虚拟机通过自己的classloader 装载饿汉式实现单例类的时候就会创建一个类实例。(如果一个虚拟机里有多个classloader的话,就会有多个实例)\n\n\n# 懒汉式\n\n懒汉式,就是实例在用到的时候才去创建,比较“懒”\n\n单例模式的懒汉式实现方式体现了延迟加载的思想(延迟加载也称懒加载lazy load,就是一开始不要加载资源或数据,等到要使用的时候才加载)\n\n同步方法\n\npublic class singleton {\n private static singleton singleton;\n\n private singleton(){}\n\n \t//解决了线程不安全问题,但是效率太低了,每个线程想获得类的实例的时候,都需要同步方法,不推荐\n public static synchronized singleton getinstance(){\n if(singleton == null){\n singleton = new singleton();\n }\n return singleton;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n双重检查加锁\n\npublic class singleton {\n\n \t//volatitle关键词确保,多线程正确处理singleton\n private static volatile singleton singleton;\n \n private singleton(){}\n \n public static singleton getinstance(){\n if(singleton ==null){\n synchronized (singleton.class){\n if(singleton == null){\n singleton = new singleton();\n }\n }\n }\n return singleton;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n\n\ndouble-check 概念(进行两次检查)是多线程开发中经常使用的,为什么需要双重检查锁呢?因为第一次检查是确保之前是一个空对象,而非空对象就不需要同步了,空对象的线程然后进入同步代码块,如果不加第二次空对象检查,两个线程同时获取同步代码块,一个线程进入同步代码块,另一个线程就会等待,而这两个线程就会创建两个实例化对象,所以需要在线程进入同步代码块后再次进行空对象检查,才能确保只创建一个实例化对象。\n\n双重检查加锁(double checked locking)线程安全、延迟加载、效率比较高\n\nvolatile:volatile一般用于多线程的可见性,这里用来防止指令重排(防止new singleton时指令重排序导致其他线程获取到未初始化完的对象)。被volatile 修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。\n\n指令重排\n\n指令重排是指在程序执行过程中, 为了性能考虑, 编译器和cpu可能会对指令重新排序。\n\njava中创建一个对象,往往包含三个过程。对于singleton = new singleton(),这不是一个原子操作,在 jvm 中包含如下三个过程。\n\n给 singleton 分配内存\n\n调用 singleton 的构造函数来初始化成员变量,形成实例\n\n将 singleton 对象指向分配的内存空间(执行完这步 singleton才是非 null 了)\n\n但是,由于jvm会进行指令重排序,所以上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3,也可能是 1-3-2。\n\n如果是 1-3-2,则在 3 执行完毕,2 未执行之前,被另一个线程抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以这个线程会直接返回 instance,然后使用,那肯定就会报错了,所以要加入 volatile关键字。\n\n\n# 静态内部类\n\npublic class singleton {\n\n private singleton(){}\n\n private static class singletoninstance{\n private static final singleton instance = new singleton();\n }\n \n public static singleton getinstance(){\n return singletoninstance.instance;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n采用类加载的机制来保证初始化实例时只有一个线程;\n\n静态内部类方式在singleton 类被装载的时候并不会立即实例化,而是在调用getinstance的时候,才去装载内部类singletoninstance ,从而完成singleton的实例化\n\n类的静态属性只会在第一次加载类的时候初始化,所以,jvm帮我们保证了线程的安全性,在类初始化时,其他线程无法进入\n\n优点:线程安全,利用静态内部类实现延迟加载,效率较高,推荐使用\n\n\n# 枚举\n\nenum singleton{\n instance;\n public void method(){}\n}\n\n\n1\n2\n3\n4\n\n\n借助jdk5 添加的枚举实现单例,不仅可以避免多线程同步问题,还能防止反序列化重新创建新的对象,但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。不过,由于java1.5中才加入enum特性,所以使用的人并不多。\n\n这种方式是《effective java》 作者josh bloch 提倡的方式。\n\n\n# 单例模式在jdk 中的源码分析\n\njdk 中,java.lang.runtime 就是经典的单例模式(饿汉式)\n\n\n# 单例模式注意事项和细节\n\n单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使 用new 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或 耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数 据库或文件的对象(比如数据源、session工厂等)", + "headersStr": "查看进程 查看JVM参数 jinfo 查看各项指标数据 jstat jmap", + "content": "# 查看进程\n\n由于JVM大多数命令都是需要用到进程id的,所以我们需要先查看一下我们系统的进程id,通过使用jps -l,输出所有java相关进程。\n\n这里我们可以根据类名,大致确认出我们的系统进程是什么\n\n注:-l代表的是输出应用程序main class的完整package名或者应用程序的jar文件完整路径名\n\n\n# 查看JVM参数 jinfo\n\n通过运行jinfo -flags <端口号>查看jvm的参数。\n\n通过运行jinfo -sysprops <端口号>查看java系统参数。\n\n\n# 查看各项指标数据 jstat\n\n通过运行 -class <端口号>完成类加载统计 通过运行jstat -gc <端口号>完成垃圾回收统计 通过运行jstat -gccapacity <端口号>完成堆内存统计 通过运行jstat -gcnew <端口号>完成新生代垃圾回收统计 通过运行jstat -gcnewcapacity <端口号>完成新生代内存统计 通过运行jstat -gcold <端口号>完成老年代垃圾回收统计 通过运行jstat -gcoldcapacity <端口号>完成老年代内存统计 通过运行jstat -gcmetacapacity <端口号>完成老年代内存统计 通过运行jstat -gcutil <端口号>数据总览 查看对象及内存使用情况 通过运行jmap -histo <端口号> > ./log.txt类及其对象所占内存数据。\n\n\n# jmap\n\n通过运行jmap -heap <端口号>堆信息。\n\n参考: 深度好文:JVM调优 Java核心篇-故事里的JVM性能调优", + "normalizedContent": "# 查看进程\n\n由于jvm大多数命令都是需要用到进程id的,所以我们需要先查看一下我们系统的进程id,通过使用jps -l,输出所有java相关进程。\n\n这里我们可以根据类名,大致确认出我们的系统进程是什么\n\n注:-l代表的是输出应用程序main class的完整package名或者应用程序的jar文件完整路径名\n\n\n# 查看jvm参数 jinfo\n\n通过运行jinfo -flags <端口号>查看jvm的参数。\n\n通过运行jinfo -sysprops <端口号>查看java系统参数。\n\n\n# 查看各项指标数据 jstat\n\n通过运行 -class <端口号>完成类加载统计 通过运行jstat -gc <端口号>完成垃圾回收统计 通过运行jstat -gccapacity <端口号>完成堆内存统计 通过运行jstat -gcnew <端口号>完成新生代垃圾回收统计 通过运行jstat -gcnewcapacity <端口号>完成新生代内存统计 通过运行jstat -gcold <端口号>完成老年代垃圾回收统计 通过运行jstat -gcoldcapacity <端口号>完成老年代内存统计 通过运行jstat -gcmetacapacity <端口号>完成老年代内存统计 通过运行jstat -gcutil <端口号>数据总览 查看对象及内存使用情况 通过运行jmap -histo <端口号> > ./log.txt类及其对象所占内存数据。\n\n\n# jmap\n\n通过运行jmap -heap <端口号>堆信息。\n\n参考: 深度好文:jvm调优 java核心篇-故事里的jvm性能调优", "charsets": { "cjk": true }, - "lastUpdated": "2022/06/20, 22:47:41", - "lastUpdatedTimestamp": 1655736461000 + "lastUpdated": "2022/09/18, 21:28:02", + "lastUpdatedTimestamp": 1663507682000 }, { "title": "简述 CMS 与 G1 机制的区别", @@ -2529,178 +2494,6 @@ export const siteData = { "lastUpdated": "2022/09/18, 21:28:02", "lastUpdatedTimestamp": 1663507682000 }, - { - "title": "JVM 是怎么去调优的?了解哪些参数和指令?", - "frontmatter": { - "title": "JVM 是怎么去调优的?了解哪些参数和指令?", - "date": "2022-05-21T17:33:16.000Z", - "permalink": "/pages/d730dd/", - "categories": [ - "Java相关", - "JVM" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/050.JVM%20%E6%98%AF%E6%80%8E%E4%B9%88%E5%8E%BB%E8%B0%83%E4%BC%98%E7%9A%84%EF%BC%9F%E4%BA%86%E8%A7%A3%E5%93%AA%E4%BA%9B%E5%8F%82%E6%95%B0%E5%92%8C%E6%8C%87%E4%BB%A4%EF%BC%9F.html", - "relativePath": "01.Java相关/30.JVM/050.JVM 是怎么去调优的?了解哪些参数和指令?.md", - "key": "v-37fcb440", - "path": "/pages/d730dd/", - "headers": [ - { - "level": 2, - "title": "查看进程", - "slug": "查看进程", - "normalizedTitle": "查看进程", - "charIndex": 2 - }, - { - "level": 2, - "title": "查看JVM参数 jinfo", - "slug": "查看jvm参数-jinfo", - "normalizedTitle": "查看jvm参数 jinfo", - "charIndex": 162 - }, - { - "level": 2, - "title": "查看各项指标数据 jstat", - "slug": "查看各项指标数据-jstat", - "normalizedTitle": "查看各项指标数据 jstat", - "charIndex": 251 - }, - { - "level": 2, - "title": "jmap", - "slug": "jmap", - "normalizedTitle": "jmap", - "charIndex": 588 - } - ], - "headersStr": "查看进程 查看JVM参数 jinfo 查看各项指标数据 jstat jmap", - "content": "# 查看进程\n\n由于JVM大多数命令都是需要用到进程id的,所以我们需要先查看一下我们系统的进程id,通过使用jps -l,输出所有java相关进程。\n\n这里我们可以根据类名,大致确认出我们的系统进程是什么\n\n注:-l代表的是输出应用程序main class的完整package名或者应用程序的jar文件完整路径名\n\n\n# 查看JVM参数 jinfo\n\n通过运行jinfo -flags <端口号>查看jvm的参数。\n\n通过运行jinfo -sysprops <端口号>查看java系统参数。\n\n\n# 查看各项指标数据 jstat\n\n通过运行 -class <端口号>完成类加载统计 通过运行jstat -gc <端口号>完成垃圾回收统计 通过运行jstat -gccapacity <端口号>完成堆内存统计 通过运行jstat -gcnew <端口号>完成新生代垃圾回收统计 通过运行jstat -gcnewcapacity <端口号>完成新生代内存统计 通过运行jstat -gcold <端口号>完成老年代垃圾回收统计 通过运行jstat -gcoldcapacity <端口号>完成老年代内存统计 通过运行jstat -gcmetacapacity <端口号>完成老年代内存统计 通过运行jstat -gcutil <端口号>数据总览 查看对象及内存使用情况 通过运行jmap -histo <端口号> > ./log.txt类及其对象所占内存数据。\n\n\n# jmap\n\n通过运行jmap -heap <端口号>堆信息。\n\n参考: 深度好文:JVM调优 Java核心篇-故事里的JVM性能调优", - "normalizedContent": "# 查看进程\n\n由于jvm大多数命令都是需要用到进程id的,所以我们需要先查看一下我们系统的进程id,通过使用jps -l,输出所有java相关进程。\n\n这里我们可以根据类名,大致确认出我们的系统进程是什么\n\n注:-l代表的是输出应用程序main class的完整package名或者应用程序的jar文件完整路径名\n\n\n# 查看jvm参数 jinfo\n\n通过运行jinfo -flags <端口号>查看jvm的参数。\n\n通过运行jinfo -sysprops <端口号>查看java系统参数。\n\n\n# 查看各项指标数据 jstat\n\n通过运行 -class <端口号>完成类加载统计 通过运行jstat -gc <端口号>完成垃圾回收统计 通过运行jstat -gccapacity <端口号>完成堆内存统计 通过运行jstat -gcnew <端口号>完成新生代垃圾回收统计 通过运行jstat -gcnewcapacity <端口号>完成新生代内存统计 通过运行jstat -gcold <端口号>完成老年代垃圾回收统计 通过运行jstat -gcoldcapacity <端口号>完成老年代内存统计 通过运行jstat -gcmetacapacity <端口号>完成老年代内存统计 通过运行jstat -gcutil <端口号>数据总览 查看对象及内存使用情况 通过运行jmap -histo <端口号> > ./log.txt类及其对象所占内存数据。\n\n\n# jmap\n\n通过运行jmap -heap <端口号>堆信息。\n\n参考: 深度好文:jvm调优 java核心篇-故事里的jvm性能调优", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/09/18, 21:28:02", - "lastUpdatedTimestamp": 1663507682000 - }, - { - "title": "代理模式", - "frontmatter": { - "title": "代理模式", - "date": "2022-05-21T17:28:27.000Z", - "permalink": "/pages/dd3a6f/", - "categories": [ - "Java相关", - "设计模式" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/020.%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F.html", - "relativePath": "01.Java相关/40.设计模式/020.代理模式.md", - "key": "v-0564ba27", - "path": "/pages/dd3a6f/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "什么是设计模式", - "frontmatter": { - "title": "什么是设计模式", - "date": "2022-05-21T17:26:19.000Z", - "permalink": "/pages/3d768c/", - "categories": [ - "Java相关", - "设计模式" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/003.%E4%BB%80%E4%B9%88%E6%98%AF%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20.html", - "relativePath": "01.Java相关/40.设计模式/003.什么是设计模式 .md", - "key": "v-88b087a6", - "path": "/pages/3d768c/", - "headers": [ - { - "level": 2, - "title": "什么是设计模式?", - "slug": "什么是设计模式", - "normalizedTitle": "什么是设计模式?", - "charIndex": 2 - }, - { - "level": 2, - "title": "设计模式之六大原则", - "slug": "设计模式之六大原则", - "normalizedTitle": "设计模式之六大原则", - "charIndex": 286 - }, - { - "level": 3, - "title": "开闭原则(Open Close Principle)", - "slug": "开闭原则-open-close-principle", - "normalizedTitle": "开闭原则(open close principle)", - "charIndex": 300 - }, - { - "level": 3, - "title": "里氏代换原则(Liskov Substitution Principle)", - "slug": "里氏代换原则-liskov-substitution-principle", - "normalizedTitle": "里氏代换原则(liskov substitution principle)", - "charIndex": 628 - }, - { - "level": 3, - "title": "依赖倒转原则(Dependence Inversion Principle)", - "slug": "依赖倒转原则-dependence-inversion-principle", - "normalizedTitle": "依赖倒转原则(dependence inversion principle)", - "charIndex": 861 - }, - { - "level": 3, - "title": "接口隔离原则(Interface Segregation Principle)", - "slug": "接口隔离原则-interface-segregation-principle", - "normalizedTitle": "接口隔离原则(interface segregation principle)", - "charIndex": 1024 - }, - { - "level": 3, - "title": "迪米特法则(最少知道原则)(Demeter Principle)", - "slug": "迪米特法则-最少知道原则-demeter-principle", - "normalizedTitle": "迪米特法则(最少知道原则)(demeter principle)", - "charIndex": 1138 - }, - { - "level": 3, - "title": "单一职责原则( Single responsibility principle )", - "slug": "单一职责原则-single-responsibility-principle", - "normalizedTitle": "单一职责原则( single responsibility principle )", - "charIndex": 1403 - }, - { - "level": 2, - "title": "高内聚,低耦合?", - "slug": "高内聚-低耦合", - "normalizedTitle": "高内聚,低耦合?", - "charIndex": 1768 - } - ], - "headersStr": "什么是设计模式? 设计模式之六大原则 开闭原则(Open Close Principle) 里氏代换原则(Liskov Substitution Principle) 依赖倒转原则(Dependence Inversion Principle) 接口隔离原则(Interface Segregation Principle) 迪米特法则(最少知道原则)(Demeter Principle) 单一职责原则( Single responsibility principle ) 高内聚,低耦合?", - "content": "# 什么是设计模式?\n\n设计模式是一套代码设计「经验的总结」。项目中「合理的」运用设计模式可以「巧妙的解决很多问题」。\n\n * 经验的总结:抱着「代码虐我千百遍,我待代码如初恋」的心态,最终得出来的「套路」。\n\n * 合理的:要对设计模式的使用场景有一定的认识后才使用,「不要滥用」。如:输出一句“hello world”,非要强行给加上各种模式。 问:“为什么”,答:“总感觉少了模式!”。\n\n * 巧妙的解决了很多问题:被广泛应用的原因。\n\n> 为什么要提倡“Design Pattern呢?根本原因是为了代码复用,增加可维护性。那么怎么才能实现代码复用呢?\n\n\n# 设计模式之六大原则\n\n\n# 开闭原则(Open Close Principle)\n\n1988年,勃兰特·梅耶(Bertrand Meyer)在他的著作《面向对象软件构造(Object Oriented Software Construction)》中提出了开闭原则,它的原文是这样:“Software entities should be open for extension,but closed for modification”。 意思:软件模块应该对扩展开放,对修改关闭。 举例:在程序需要进行新增功能的时候,不能去修改原有的代码,而是新增代码,实现一个热插拔的效果(热插拔:灵活的去除或添加功能,不影响到原有的功能)。 目的:为了使程序的扩展性好,易于维护和升级。\n\n\n# 里氏代换原则(Liskov Substitution Principle)\n\n意思:里氏代换原则是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。 举例:球类,原本是一种体育用品,它的衍生类有篮球、足球、排球、羽毛球等等,如果衍生类替换了基类的原本方法,如把体育用品改成了食用品(那么软件单位的功能受到影响),就不符合里氏代换原则。 目的:对实现抽象化的具体步骤的规范。\n\n\n# 依赖倒转原则(Dependence Inversion Principle)\n\n意思:针对接口编程,而不是针对实现编程。 举例:以计算机系统为例,无论主板、CPU、内存、硬件都是在针对接口设计的,如果针对实现来设计,内存就要对应到针对某个品牌的主板,那么会出现换内存需要把主板也换掉的尴尬。 目的:降低模块间的耦合。\n\n\n# 接口隔离原则(Interface Segregation Principle)\n\n使用多个隔离的接口,比使用单个接口要好。 举例:比如:登录,注册时属于用户模块的两个接口,比写成一个接口好。 目的:提高程序设计灵活性。\n\n\n# 迪米特法则(最少知道原则)(Demeter Principle)\n\n1987年秋天由美国Northeastern University的Ian Holland提出,被UML的创始者之一[Booch]等普及。后来,因为在经典著作《 The Pragmatic Programmer》而广为人知。 意思:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。 举例:一个类公开的public属性或方法越多,修改时涉及的面也就越大,变更引起的风险扩散也就越大。 目的:降低类之间的耦合,减少对其他类的依赖。\n\n\n# 单一职责原则( Single responsibility principle )\n\n该原则由罗伯特·C·马丁(Robert C. Martin)于《敏捷软件开发:原则、模式和实践》一书中给出的。马丁表示此原则是基于汤姆·狄马克(Tom DeMarco)和Meilir Page-Jones的著作中的内聚性原则发展出的。 意思:一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。 举例:该原则意思简单到不需要举例! 目的:类的复杂性降低,可读性提高,可维护性提高。\n\n刚入行的时候,在想什么样的代码是好代码?看到很多前辈的文字都说好的代码要符合「高内聚,低耦合」,但是我听到这样的解释,是这样的\n\n而现在对设计模式有了一定程度上的学习,感觉懂了一些,小伙伴们你们学会了吗?\n\n\n# 高内聚,低耦合?\n\n内聚是从功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事。它描述的是模块内的功能联系; 耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及通过接口的数据。\n\n参考: 图解九种常见的设计模式 什么是「设计模式」?", - "normalizedContent": "# 什么是设计模式?\n\n设计模式是一套代码设计「经验的总结」。项目中「合理的」运用设计模式可以「巧妙的解决很多问题」。\n\n * 经验的总结:抱着「代码虐我千百遍,我待代码如初恋」的心态,最终得出来的「套路」。\n\n * 合理的:要对设计模式的使用场景有一定的认识后才使用,「不要滥用」。如:输出一句“hello world”,非要强行给加上各种模式。 问:“为什么”,答:“总感觉少了模式!”。\n\n * 巧妙的解决了很多问题:被广泛应用的原因。\n\n> 为什么要提倡“design pattern呢?根本原因是为了代码复用,增加可维护性。那么怎么才能实现代码复用呢?\n\n\n# 设计模式之六大原则\n\n\n# 开闭原则(open close principle)\n\n1988年,勃兰特·梅耶(bertrand meyer)在他的著作《面向对象软件构造(object oriented software construction)》中提出了开闭原则,它的原文是这样:“software entities should be open for extension,but closed for modification”。 意思:软件模块应该对扩展开放,对修改关闭。 举例:在程序需要进行新增功能的时候,不能去修改原有的代码,而是新增代码,实现一个热插拔的效果(热插拔:灵活的去除或添加功能,不影响到原有的功能)。 目的:为了使程序的扩展性好,易于维护和升级。\n\n\n# 里氏代换原则(liskov substitution principle)\n\n意思:里氏代换原则是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。 举例:球类,原本是一种体育用品,它的衍生类有篮球、足球、排球、羽毛球等等,如果衍生类替换了基类的原本方法,如把体育用品改成了食用品(那么软件单位的功能受到影响),就不符合里氏代换原则。 目的:对实现抽象化的具体步骤的规范。\n\n\n# 依赖倒转原则(dependence inversion principle)\n\n意思:针对接口编程,而不是针对实现编程。 举例:以计算机系统为例,无论主板、cpu、内存、硬件都是在针对接口设计的,如果针对实现来设计,内存就要对应到针对某个品牌的主板,那么会出现换内存需要把主板也换掉的尴尬。 目的:降低模块间的耦合。\n\n\n# 接口隔离原则(interface segregation principle)\n\n使用多个隔离的接口,比使用单个接口要好。 举例:比如:登录,注册时属于用户模块的两个接口,比写成一个接口好。 目的:提高程序设计灵活性。\n\n\n# 迪米特法则(最少知道原则)(demeter principle)\n\n1987年秋天由美国northeastern university的ian holland提出,被uml的创始者之一[booch]等普及。后来,因为在经典著作《 the pragmatic programmer》而广为人知。 意思:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。 举例:一个类公开的public属性或方法越多,修改时涉及的面也就越大,变更引起的风险扩散也就越大。 目的:降低类之间的耦合,减少对其他类的依赖。\n\n\n# 单一职责原则( single responsibility principle )\n\n该原则由罗伯特·c·马丁(robert c. martin)于《敏捷软件开发:原则、模式和实践》一书中给出的。马丁表示此原则是基于汤姆·狄马克(tom demarco)和meilir page-jones的著作中的内聚性原则发展出的。 意思:一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。 举例:该原则意思简单到不需要举例! 目的:类的复杂性降低,可读性提高,可维护性提高。\n\n刚入行的时候,在想什么样的代码是好代码?看到很多前辈的文字都说好的代码要符合「高内聚,低耦合」,但是我听到这样的解释,是这样的\n\n而现在对设计模式有了一定程度上的学习,感觉懂了一些,小伙伴们你们学会了吗?\n\n\n# 高内聚,低耦合?\n\n内聚是从功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事。它描述的是模块内的功能联系; 耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及通过接口的数据。\n\n参考: 图解九种常见的设计模式 什么是「设计模式」?", - "charsets": { - "cjk": true - } - }, { "title": "什么是内存泄漏,怎么确定内存泄漏?", "frontmatter": { @@ -2836,11 +2629,11 @@ export const siteData = { "lastUpdatedTimestamp": 1663507682000 }, { - "title": "责任链模式", + "title": "实现单例设计模式(懒汉,饿汉)", "frontmatter": { - "title": "责任链模式", - "date": "2022-05-21T17:28:46.000Z", - "permalink": "/pages/d3ff35/", + "title": "实现单例设计模式(懒汉,饿汉)", + "date": "2022-05-21T17:26:00.000Z", + "permalink": "/pages/b37ad7/", "categories": [ "Java相关", "设计模式" @@ -2849,143 +2642,90 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/030.%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F.html", - "relativePath": "01.Java相关/40.设计模式/030.责任链模式.md", - "key": "v-34f8aaa2", - "path": "/pages/d3ff35/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "简述装饰者模式", - "frontmatter": { - "title": "简述装饰者模式", - "date": "2022-05-21T17:26:42.000Z", - "permalink": "/pages/47a4e3/", - "categories": [ - "Java相关", - "设计模式" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/005.%E8%A3%85%E9%A5%B0%E8%80%85%E6%A8%A1%E5%BC%8F.html", - "relativePath": "01.Java相关/40.设计模式/005.装饰者模式.md", - "key": "v-22a9eaef", - "path": "/pages/47a4e3/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/002.%E5%AE%9E%E7%8E%B0%E5%8D%95%E4%BE%8B%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%EF%BC%88%E6%87%92%E6%B1%89%EF%BC%8C%E9%A5%BF%E6%B1%89%EF%BC%89.html", + "relativePath": "01.Java相关/40.设计模式/002.实现单例设计模式(懒汉,饿汉).md", + "key": "v-ba94c306", + "path": "/pages/b37ad7/", "headers": [ { "level": 2, - "title": "模式动机", - "slug": "模式动机", - "normalizedTitle": "模式动机", - "charIndex": 186 - }, - { - "level": 2, - "title": "定义", - "slug": "定义", - "normalizedTitle": "定义", - "charIndex": 489 - }, - { - "level": 2, - "title": "角色", - "slug": "角色", - "normalizedTitle": "角色", - "charIndex": 632 - }, - { - "level": 2, - "title": "类图", - "slug": "类图", - "normalizedTitle": "类图", - "charIndex": 1194 + "title": "单例模式的定义", + "slug": "单例模式的定义", + "normalizedTitle": "单例模式的定义", + "charIndex": 252 }, { "level": 2, - "title": "实例", - "slug": "实例", - "normalizedTitle": "实例", - "charIndex": 1203 + "title": "单例模式的类图", + "slug": "单例模式的类图", + "normalizedTitle": "单例模式的类图", + "charIndex": 294 }, { "level": 2, - "title": "应用", - "slug": "应用", - "normalizedTitle": "应用", - "charIndex": 3775 + "title": "单例模式的实现", + "slug": "单例模式的实现", + "normalizedTitle": "单例模式的实现", + "charIndex": 15 }, { "level": 3, - "title": "Java I/O 中的装饰者模式", - "slug": "java-i-o-中的装饰者模式", - "normalizedTitle": "java i/o 中的装饰者模式", - "charIndex": 3782 + "title": "饿汉式", + "slug": "饿汉式", + "normalizedTitle": "饿汉式", + "charIndex": 320 }, { "level": 3, - "title": "Servlet 中的装饰者模式", - "slug": "servlet-中的装饰者模式", - "normalizedTitle": "servlet 中的装饰者模式", - "charIndex": 4933 + "title": "懒汉式", + "slug": "懒汉式", + "normalizedTitle": "懒汉式", + "charIndex": 856 }, { "level": 3, - "title": "spring 中的装饰者模式", - "slug": "spring-中的装饰者模式", - "normalizedTitle": "spring 中的装饰者模式", - "charIndex": 5565 + "title": "静态内部类", + "slug": "静态内部类", + "normalizedTitle": "静态内部类", + "charIndex": 2627 }, { "level": 3, - "title": "Mybatis 缓存中的装饰者模式", - "slug": "mybatis-缓存中的装饰者模式", - "normalizedTitle": "mybatis 缓存中的装饰者模式", - "charIndex": 6062 - }, - { - "level": 2, - "title": "总结", - "slug": "总结", - "normalizedTitle": "总结", - "charIndex": 6192 + "title": "枚举", + "slug": "枚举", + "normalizedTitle": "枚举", + "charIndex": 3151 }, { "level": 2, - "title": "优缺点", - "slug": "优缺点", - "normalizedTitle": "优缺点", - "charIndex": 6300 + "title": "单例模式在JDK 中的源码分析", + "slug": "单例模式在jdk-中的源码分析", + "normalizedTitle": "单例模式在jdk 中的源码分析", + "charIndex": 3399 }, { "level": 2, - "title": "何时选用", - "slug": "何时选用", - "normalizedTitle": "何时选用", - "charIndex": 6776 + "title": "单例模式注意事项和细节", + "slug": "单例模式注意事项和细节", + "normalizedTitle": "单例模式注意事项和细节", + "charIndex": 3459 } ], - "headersStr": "模式动机 定义 角色 类图 实例 应用 Java I/O 中的装饰者模式 Servlet 中的装饰者模式 spring 中的装饰者模式 Mybatis 缓存中的装饰者模式 总结 优缺点 何时选用", - "content": "《Head First 设计模式》中是这么形容装饰者模式的——“给爱用继承的人一个全新的设计眼界”,拒绝继承滥用,从装饰者模式开始。\n\n装饰者模式允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。\n\n这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。\n\n\n# 模式动机\n\n一般有两种方式可以实现给一个类或对象增加行为:\n\n * 继承机制,使用继承机制是给现有类添加功能的一种有效途径,通过继承一个现有类可以使得子类在拥有自身方法的同时还拥有父类的方法。但是这种方法是静态的,用户不能控制增加行为的方式和时机。\n\n * 关联机制,即将一个类的对象嵌入另一个对象中,由另一个对象来决定是否调用嵌入对象的行为以便扩展自己的行为,我们称这个嵌入的对象为装饰器(Decorator)\n\n装饰模式以对客户透明的方式动态地给一个对象附加上更多的责任,换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。\n\n\n# 定义\n\n装饰模式(Decorator Pattern) :动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类(继承)实现更为灵活。其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。\n\n\n# 角色\n\n * Component: 抽象组件,装饰者和被装饰者共同的父类,是一个接口或者抽象类,用来定义基本行为,可以给这些对象动态添加职责\n\n * ConcreteComponent: 具体的组件对象,实现类 ,即被装饰者,通常就是被装饰器装饰的原始对象,也就是可以给这个对象添加职责\n\n * Decorator: 所有装饰器的抽象父类,一般是抽象类,实现接口;它的属性必然有个指向 Conponent 抽象组件的对象 ,其实就是持有一个被装饰的对象\n\n * ConcreteDecorator: 具体的装饰对象,实现具体要被装饰对象添加的功能。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为。\n\n装饰者和被装饰者对象有相同的父类,因为装饰者和被装饰者必须是一样的类型,这里利用继承是为了达到类型匹配,而不是利用继承获得行为。\n\n利用继承设计子类,只能在编译时静态决定,并且所有子类都会继承相同的行为;利用组合的做法扩展对象,就可以在运行时动态的进行扩展。装饰者模式遵循开放-关闭原则:**类应该对扩展开放,对修改关闭。**利用装饰者,我们可以实现新的装饰者增加新的行为而不用修改现有代码,而如果单纯依赖继承,每当需要新行为时,还得修改现有的代码。\n\n\n# 类图\n\n\n\n\n# 实例\n\n看了好多资料的例子,比如\n\n * 公司发放奖金,不同的员工类型对应不同的奖金计算规则,用各种计算规则去装饰统一的奖金计算类\n * 星巴克售卖用咖啡,用摩卡、奶泡去装饰咖啡,实现不同的计费\n * 变形金刚在变形之前是一辆汽车,它可以在陆地上移动。当它变成机器人之后除了能够在陆地上移动之外,还可以说话;如果需要,它还可以变成飞机,除了在陆地上移动还可以在天空中飞翔\n\n我还是比较喜欢卖煎饼的例子\n\n\n\n1、定义抽象组件\n\npublic abstract class Pancake {\n\n String description = \"普通煎饼\";\n\n public String getDescription(){\n return description;\n }\n\n public abstract double cost();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n2、定义具体的被装饰者,这里是煎饼果子,当然还可以有鸡蛋灌饼、手抓饼等其他被装饰者\n\npublic class Battercake extends Pancake {\n @Override\n public double cost() {\n return 8;\n }\n\n public Battercake(){\n description = \"煎饼果子\";\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n3、抽象的装饰器对象,定义一个调料抽象类\n\npublic abstract class CondimentDecorator extends Pancake {\n\n // 持有组件对象\n protected Pancake pancake;\n public CondimentDecorator(Pancake pancake){\n this.pancake = pancake;\n }\n\n public abstract String getDescription();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n4、具体的装饰者,我们定义一个鸡蛋装饰器,一个火腿装饰器\n\npublic class Egg extends CondimentDecorator {\n public Egg(Pancake pancake){\n super(pancake);\n }\n\n @Override\n public String getDescription() {\n return pancake.getDescription() + \"加鸡蛋\";\n }\n\n @Override\n public double cost() {\n return pancake.cost() + 1;\n }\n}\npublic class Sausage extends CondimentDecorator{\n public Sausage(Pancake pancake){\n super(pancake);\n }\n @Override\n public String getDescription() {\n return pancake.getDescription() + \"加火腿\";\n }\n\n @Override\n public double cost() {\n return pancake.cost() + 2;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n\n\n5、测试煎饼交易,over\n\npublic class Client {\n\n public static void main(String[] args) {\n //买一个普通的煎饼果子\n Pancake battercake = new Battercake();\n System.out.println(battercake.getDescription() + \"花费:\"+battercake.cost() + \"元\");\n\n //买一个加双蛋的煎饼果子\n Pancake doubleEgg = new Battercake();\n doubleEgg = new Egg(doubleEgg);\n doubleEgg = new Egg(doubleEgg);\n System.out.println(doubleEgg.getDescription() + \"花费\" + doubleEgg.cost() + \"元\");\n\n //加火腿和鸡蛋\n Pancake battercakePlus = new Battercake();\n battercakePlus = new Egg(battercakePlus);\n battercakePlus = new Sausage(battercakePlus);\n System.out.println(battercakePlus.getDescription() + \"花费\" + battercakePlus.cost() + \"元\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\n输出:\n\n煎饼果子花费:8.0元\n煎饼果子加鸡蛋加鸡蛋花费10.0元\n煎饼果子加鸡蛋加火腿花费11.0元\n\n\n1\n2\n3\n\n\n顺便看下通过 IDEA 生成的 UML 类图(和我们画的类图一样哈)\n\n\n# 应用\n\n\n# Java I/O 中的装饰者模式\n\n我们使用 java.io 包下的各种输入流、输出流、字节流、字符流、缓冲流等各种各样的流,他们中的许多类都是装饰者,下面是一个典型的对象集合,用装饰者将功能结合起来,以读取文件数据\n\n\n\nBufferedInputStream 和 LinerNumberInputStream 都是扩展自 FilterInputStream,而 FilterInputStream 是一个抽象的装饰类。\n\n在 idea 中选中一些常见 InputStream 类,生成 UML 图如下:\n\n我们平时读取一个文件中的内容其实就使用到了装饰模式的思想,简化《Head First 设计模式》的例子,我们自定义一个装饰者,把输入流中的所有大写字符转换为小写\n\npublic class LowerCaseInputStream extends FilterInputStream {\n \n protected LowerCaseInputStream(InputStream in) {\n super(in);\n }\n\n public int read() throws IOException {\n int c = super.read();\n return (c == -1 ? c:Character.toLowerCase(c));\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\npublic class InputTest {\n\n public static void main(String[] args) throws IOException {\n int c;\n //装饰器的组装过程\n InputStream in = new LowerCaseInputStream(new BufferedInputStream(new FileInputStream(\"JavaKeeper.txt\"))); \n\n while ((c = in.read()) >= 0){\n System.out.print((char) c);\n }\n in.close();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n采用装饰者模式在实例化组件时,将增加代码的复杂度,一旦使用装饰者模式,不只需要实例化组件,还把把此组件包装进装饰者中,天晓得有几个,所以在某些复杂情况下,我们还会结合工厂模式和生成器模式。比如Spring中的装饰者模式。\n\n\n# Servlet 中的装饰者模式\n\nServlet API 源自于 4 个实现类,它很少被使用,但是十分强大:ServletRequestWrapper、ServletResponseWrapper以及 HttpServletRequestWrapper、HttpServletResponseWrapper。\n\n比如 ServletRequestWrapper 是 ServletRequest 接口的简单实现,开发者可以继承 ServletRequestWrapper 去扩展原来的 request\n\npublic class ServletRequestWrapper implements ServletRequest {\n private ServletRequest request;\n\n public ServletRequestWrapper(ServletRequest request) {\n if (request == null) {\n throw new IllegalArgumentException(\"Request cannot be null\");\n } else {\n this.request = request;\n }\n }\n\t//.......\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n\n# spring 中的装饰者模式\n\nSpring 的 ApplicationContext 中配置所有的 DataSource。 这些 DataSource 可能是各种不同类型的, 比如不同的数据库: Oracle、 SQL Server、 MySQL 等, 也可能是不同的数据源。 然后 SessionFactory 根据客户的每次请求, 将 DataSource 属性设置成不同的数据源, 以达到切换数据源的目的。\n\n在 Spring 的命名体现:Spring 中用到的包装器模式在类名上有两种表现: 一种是类名中含有 Wrapper, 另一种是类名中含有 Decorator。 基本上都是动态地给一个对象添加一些额外的职责,比如\n\norg.springframework.cache.transaction 包下的 TransactionAwareCacheDecorator 类 org.springframework.session.web.http 包下的 SessionRepositoryFilter 内部类 SessionRepositoryRequestWrapper\n\n\n# Mybatis 缓存中的装饰者模式\n\nMybatis 的缓存模块中,使用了装饰器模式的变体,其中将 Decorator 接口和 Componet 接口合并为一个 Component 接口。org.apache.ibatis.cache 包下的结构\n\n\n\n\n# 总结\n\n装饰模式的本质:动态组合\n\n动态组合是手段,组合才是目的。这里的组合有两个意思,一个是动态功能的组合,也就是动态进行装饰器的组合;另外一个是指对象组合,通过对象组合来实现为被装饰对象透明的增加功能。\n\n\n# 优缺点\n\n装饰模式的优点:\n\n * 装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。\n * 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的装饰器,从而实现不同的行为。\n * 通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更为强大的对象。\n * 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开闭原则”\n\n装饰模式的缺点:\n\n * 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度。\n * 这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。\n\n\n# 何时选用\n\n * 如果需要在不影响其他对象的情况下,以动态、透明的方式给对象添加职责,可以使用装饰模式\n * 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类已定义为不能被继承(如 Java 语言中的 final 类)\n\n参考: 装饰模式——看看 JDK 和 Spring 是如何杜绝继承滥用的", - "normalizedContent": "《head first 设计模式》中是这么形容装饰者模式的——“给爱用继承的人一个全新的设计眼界”,拒绝继承滥用,从装饰者模式开始。\n\n装饰者模式允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。\n\n这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。\n\n\n# 模式动机\n\n一般有两种方式可以实现给一个类或对象增加行为:\n\n * 继承机制,使用继承机制是给现有类添加功能的一种有效途径,通过继承一个现有类可以使得子类在拥有自身方法的同时还拥有父类的方法。但是这种方法是静态的,用户不能控制增加行为的方式和时机。\n\n * 关联机制,即将一个类的对象嵌入另一个对象中,由另一个对象来决定是否调用嵌入对象的行为以便扩展自己的行为,我们称这个嵌入的对象为装饰器(decorator)\n\n装饰模式以对客户透明的方式动态地给一个对象附加上更多的责任,换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。\n\n\n# 定义\n\n装饰模式(decorator pattern) :动态地给一个对象增加一些额外的职责(responsibility),就增加对象功能来说,装饰模式比生成子类(继承)实现更为灵活。其别名也可以称为包装器(wrapper),与适配器模式的别名相同,但它们适用于不同的场合。\n\n\n# 角色\n\n * component: 抽象组件,装饰者和被装饰者共同的父类,是一个接口或者抽象类,用来定义基本行为,可以给这些对象动态添加职责\n\n * concretecomponent: 具体的组件对象,实现类 ,即被装饰者,通常就是被装饰器装饰的原始对象,也就是可以给这个对象添加职责\n\n * decorator: 所有装饰器的抽象父类,一般是抽象类,实现接口;它的属性必然有个指向 conponent 抽象组件的对象 ,其实就是持有一个被装饰的对象\n\n * concretedecorator: 具体的装饰对象,实现具体要被装饰对象添加的功能。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为。\n\n装饰者和被装饰者对象有相同的父类,因为装饰者和被装饰者必须是一样的类型,这里利用继承是为了达到类型匹配,而不是利用继承获得行为。\n\n利用继承设计子类,只能在编译时静态决定,并且所有子类都会继承相同的行为;利用组合的做法扩展对象,就可以在运行时动态的进行扩展。装饰者模式遵循开放-关闭原则:**类应该对扩展开放,对修改关闭。**利用装饰者,我们可以实现新的装饰者增加新的行为而不用修改现有代码,而如果单纯依赖继承,每当需要新行为时,还得修改现有的代码。\n\n\n# 类图\n\n\n\n\n# 实例\n\n看了好多资料的例子,比如\n\n * 公司发放奖金,不同的员工类型对应不同的奖金计算规则,用各种计算规则去装饰统一的奖金计算类\n * 星巴克售卖用咖啡,用摩卡、奶泡去装饰咖啡,实现不同的计费\n * 变形金刚在变形之前是一辆汽车,它可以在陆地上移动。当它变成机器人之后除了能够在陆地上移动之外,还可以说话;如果需要,它还可以变成飞机,除了在陆地上移动还可以在天空中飞翔\n\n我还是比较喜欢卖煎饼的例子\n\n\n\n1、定义抽象组件\n\npublic abstract class pancake {\n\n string description = \"普通煎饼\";\n\n public string getdescription(){\n return description;\n }\n\n public abstract double cost();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n2、定义具体的被装饰者,这里是煎饼果子,当然还可以有鸡蛋灌饼、手抓饼等其他被装饰者\n\npublic class battercake extends pancake {\n @override\n public double cost() {\n return 8;\n }\n\n public battercake(){\n description = \"煎饼果子\";\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n3、抽象的装饰器对象,定义一个调料抽象类\n\npublic abstract class condimentdecorator extends pancake {\n\n // 持有组件对象\n protected pancake pancake;\n public condimentdecorator(pancake pancake){\n this.pancake = pancake;\n }\n\n public abstract string getdescription();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n4、具体的装饰者,我们定义一个鸡蛋装饰器,一个火腿装饰器\n\npublic class egg extends condimentdecorator {\n public egg(pancake pancake){\n super(pancake);\n }\n\n @override\n public string getdescription() {\n return pancake.getdescription() + \"加鸡蛋\";\n }\n\n @override\n public double cost() {\n return pancake.cost() + 1;\n }\n}\npublic class sausage extends condimentdecorator{\n public sausage(pancake pancake){\n super(pancake);\n }\n @override\n public string getdescription() {\n return pancake.getdescription() + \"加火腿\";\n }\n\n @override\n public double cost() {\n return pancake.cost() + 2;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n\n\n5、测试煎饼交易,over\n\npublic class client {\n\n public static void main(string[] args) {\n //买一个普通的煎饼果子\n pancake battercake = new battercake();\n system.out.println(battercake.getdescription() + \"花费:\"+battercake.cost() + \"元\");\n\n //买一个加双蛋的煎饼果子\n pancake doubleegg = new battercake();\n doubleegg = new egg(doubleegg);\n doubleegg = new egg(doubleegg);\n system.out.println(doubleegg.getdescription() + \"花费\" + doubleegg.cost() + \"元\");\n\n //加火腿和鸡蛋\n pancake battercakeplus = new battercake();\n battercakeplus = new egg(battercakeplus);\n battercakeplus = new sausage(battercakeplus);\n system.out.println(battercakeplus.getdescription() + \"花费\" + battercakeplus.cost() + \"元\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\n输出:\n\n煎饼果子花费:8.0元\n煎饼果子加鸡蛋加鸡蛋花费10.0元\n煎饼果子加鸡蛋加火腿花费11.0元\n\n\n1\n2\n3\n\n\n顺便看下通过 idea 生成的 uml 类图(和我们画的类图一样哈)\n\n\n# 应用\n\n\n# java i/o 中的装饰者模式\n\n我们使用 java.io 包下的各种输入流、输出流、字节流、字符流、缓冲流等各种各样的流,他们中的许多类都是装饰者,下面是一个典型的对象集合,用装饰者将功能结合起来,以读取文件数据\n\n\n\nbufferedinputstream 和 linernumberinputstream 都是扩展自 filterinputstream,而 filterinputstream 是一个抽象的装饰类。\n\n在 idea 中选中一些常见 inputstream 类,生成 uml 图如下:\n\n我们平时读取一个文件中的内容其实就使用到了装饰模式的思想,简化《head first 设计模式》的例子,我们自定义一个装饰者,把输入流中的所有大写字符转换为小写\n\npublic class lowercaseinputstream extends filterinputstream {\n \n protected lowercaseinputstream(inputstream in) {\n super(in);\n }\n\n public int read() throws ioexception {\n int c = super.read();\n return (c == -1 ? c:character.tolowercase(c));\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\npublic class inputtest {\n\n public static void main(string[] args) throws ioexception {\n int c;\n //装饰器的组装过程\n inputstream in = new lowercaseinputstream(new bufferedinputstream(new fileinputstream(\"javakeeper.txt\"))); \n\n while ((c = in.read()) >= 0){\n system.out.print((char) c);\n }\n in.close();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n采用装饰者模式在实例化组件时,将增加代码的复杂度,一旦使用装饰者模式,不只需要实例化组件,还把把此组件包装进装饰者中,天晓得有几个,所以在某些复杂情况下,我们还会结合工厂模式和生成器模式。比如spring中的装饰者模式。\n\n\n# servlet 中的装饰者模式\n\nservlet api 源自于 4 个实现类,它很少被使用,但是十分强大:servletrequestwrapper、servletresponsewrapper以及 httpservletrequestwrapper、httpservletresponsewrapper。\n\n比如 servletrequestwrapper 是 servletrequest 接口的简单实现,开发者可以继承 servletrequestwrapper 去扩展原来的 request\n\npublic class servletrequestwrapper implements servletrequest {\n private servletrequest request;\n\n public servletrequestwrapper(servletrequest request) {\n if (request == null) {\n throw new illegalargumentexception(\"request cannot be null\");\n } else {\n this.request = request;\n }\n }\n\t//.......\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n\n# spring 中的装饰者模式\n\nspring 的 applicationcontext 中配置所有的 datasource。 这些 datasource 可能是各种不同类型的, 比如不同的数据库: oracle、 sql server、 mysql 等, 也可能是不同的数据源。 然后 sessionfactory 根据客户的每次请求, 将 datasource 属性设置成不同的数据源, 以达到切换数据源的目的。\n\n在 spring 的命名体现:spring 中用到的包装器模式在类名上有两种表现: 一种是类名中含有 wrapper, 另一种是类名中含有 decorator。 基本上都是动态地给一个对象添加一些额外的职责,比如\n\norg.springframework.cache.transaction 包下的 transactionawarecachedecorator 类 org.springframework.session.web.http 包下的 sessionrepositoryfilter 内部类 sessionrepositoryrequestwrapper\n\n\n# mybatis 缓存中的装饰者模式\n\nmybatis 的缓存模块中,使用了装饰器模式的变体,其中将 decorator 接口和 componet 接口合并为一个 component 接口。org.apache.ibatis.cache 包下的结构\n\n\n\n\n# 总结\n\n装饰模式的本质:动态组合\n\n动态组合是手段,组合才是目的。这里的组合有两个意思,一个是动态功能的组合,也就是动态进行装饰器的组合;另外一个是指对象组合,通过对象组合来实现为被装饰对象透明的增加功能。\n\n\n# 优缺点\n\n装饰模式的优点:\n\n * 装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。\n * 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的装饰器,从而实现不同的行为。\n * 通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更为强大的对象。\n * 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开闭原则”\n\n装饰模式的缺点:\n\n * 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度。\n * 这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。\n\n\n# 何时选用\n\n * 如果需要在不影响其他对象的情况下,以动态、透明的方式给对象添加职责,可以使用装饰模式\n * 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类已定义为不能被继承(如 java 语言中的 final 类)\n\n参考: 装饰模式——看看 jdk 和 spring 是如何杜绝继承滥用的", + "headersStr": "单例模式的定义 单例模式的类图 单例模式的实现 饿汉式 懒汉式 静态内部类 枚举 单例模式在JDK 中的源码分析 单例模式注意事项和细节", + "content": "> 面试官:带笔了吧,那写两种单例模式的实现方法吧 沙沙沙刷刷刷~~~ 写好了 面试官:怎样防止new 对象出来?\n\n有一些对象我们确实只需要一个,比如,线程池、数据库连接、缓存、日志对象等,如果有多个的话,会造成程序的行为异常,资源使用过量或者不一致的问题。你也许会说,这种我用全局变量不也能实现吗,还整个单例模式,好像你很流弊的样子,如果将对象赋值给一个全局变量,那程序启动就会创建好对象,万一这个对象很耗资源,我们还可能在某些时候用不到,这就造成了资源的浪费,不合理,所以就有了单例模式。\n\n\n# 单例模式的定义\n\n单例模式确保一个类只有一个实例,并提供一个全局唯一访问点\n\n\n# 单例模式的类图\n\n\n\n\n# 单例模式的实现\n\n\n# 饿汉式\n\nstatic 变量在类装载的时候进行初始化 多个实例的 static 变量会共享同一块内存区域 用这两个知识点写出的单例类就是饿汉式了,初始化类的时候就创建,饥不择食,饿汉\n\npublic class Singleton {\n\n //构造私有化,防止直接new\n private Singleton(){}\n\n //静态初始化器(static initializer)中创建实例,保证线程安全\n private static Singleton instance = new Singleton();\n\n public static Singleton getInstance(){\n return instance;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n饿汉式是线程安全的,JVM在加载类时马上创建唯一的实例对象,且只会装载一次。\n\nJava 实现的单例是一个虚拟机的范围,因为装载类的功能是虚拟机的,所以一个虚拟机通过自己的ClassLoader 装载饿汉式实现单例类的时候就会创建一个类实例。(如果一个虚拟机里有多个ClassLoader的话,就会有多个实例)\n\n\n# 懒汉式\n\n懒汉式,就是实例在用到的时候才去创建,比较“懒”\n\n单例模式的懒汉式实现方式体现了延迟加载的思想(延迟加载也称懒加载Lazy Load,就是一开始不要加载资源或数据,等到要使用的时候才加载)\n\n同步方法\n\npublic class Singleton {\n private static Singleton singleton;\n\n private Singleton(){}\n\n \t//解决了线程不安全问题,但是效率太低了,每个线程想获得类的实例的时候,都需要同步方法,不推荐\n public static synchronized Singleton getInstance(){\n if(singleton == null){\n singleton = new Singleton();\n }\n return singleton;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n双重检查加锁\n\npublic class Singleton {\n\n \t//volatitle关键词确保,多线程正确处理singleton\n private static volatile Singleton singleton;\n \n private Singleton(){}\n \n public static Singleton getInstance(){\n if(singleton ==null){\n synchronized (Singleton.class){\n if(singleton == null){\n singleton = new Singleton();\n }\n }\n }\n return singleton;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n\n\nDouble-Check 概念(进行两次检查)是多线程开发中经常使用的,为什么需要双重检查锁呢?因为第一次检查是确保之前是一个空对象,而非空对象就不需要同步了,空对象的线程然后进入同步代码块,如果不加第二次空对象检查,两个线程同时获取同步代码块,一个线程进入同步代码块,另一个线程就会等待,而这两个线程就会创建两个实例化对象,所以需要在线程进入同步代码块后再次进行空对象检查,才能确保只创建一个实例化对象。\n\n双重检查加锁(double checked locking)线程安全、延迟加载、效率比较高\n\nvolatile:volatile一般用于多线程的可见性,这里用来防止指令重排(防止new Singleton时指令重排序导致其他线程获取到未初始化完的对象)。被volatile 修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。\n\n指令重排\n\n指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序。\n\nJava中创建一个对象,往往包含三个过程。对于singleton = new Singleton(),这不是一个原子操作,在 JVM 中包含如下三个过程。\n\n给 singleton 分配内存\n\n调用 Singleton 的构造函数来初始化成员变量,形成实例\n\n将 singleton 对象指向分配的内存空间(执行完这步 singleton才是非 null 了)\n\n但是,由于JVM会进行指令重排序,所以上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3,也可能是 1-3-2。\n\n如果是 1-3-2,则在 3 执行完毕,2 未执行之前,被另一个线程抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以这个线程会直接返回 instance,然后使用,那肯定就会报错了,所以要加入 volatile关键字。\n\n\n# 静态内部类\n\npublic class Singleton {\n\n private Singleton(){}\n\n private static class SingletonInstance{\n private static final Singleton INSTANCE = new Singleton();\n }\n \n public static Singleton getInstance(){\n return SingletonInstance.INSTANCE;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n采用类加载的机制来保证初始化实例时只有一个线程;\n\n静态内部类方式在Singleton 类被装载的时候并不会立即实例化,而是在调用getInstance的时候,才去装载内部类SingletonInstance ,从而完成Singleton的实例化\n\n类的静态属性只会在第一次加载类的时候初始化,所以,JVM帮我们保证了线程的安全性,在类初始化时,其他线程无法进入\n\n优点:线程安全,利用静态内部类实现延迟加载,效率较高,推荐使用\n\n\n# 枚举\n\nenum Singleton{\n INSTANCE;\n public void method(){}\n}\n\n\n1\n2\n3\n4\n\n\n借助JDK5 添加的枚举实现单例,不仅可以避免多线程同步问题,还能防止反序列化重新创建新的对象,但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。不过,由于Java1.5中才加入enum特性,所以使用的人并不多。\n\n这种方式是《Effective Java》 作者Josh Bloch 提倡的方式。\n\n\n# 单例模式在JDK 中的源码分析\n\nJDK 中,java.lang.Runtime 就是经典的单例模式(饿汉式)\n\n\n# 单例模式注意事项和细节\n\n单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使 用new 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或 耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数 据库或文件的对象(比如数据源、session工厂等)", + "normalizedContent": "> 面试官:带笔了吧,那写两种单例模式的实现方法吧 沙沙沙刷刷刷~~~ 写好了 面试官:怎样防止new 对象出来?\n\n有一些对象我们确实只需要一个,比如,线程池、数据库连接、缓存、日志对象等,如果有多个的话,会造成程序的行为异常,资源使用过量或者不一致的问题。你也许会说,这种我用全局变量不也能实现吗,还整个单例模式,好像你很流弊的样子,如果将对象赋值给一个全局变量,那程序启动就会创建好对象,万一这个对象很耗资源,我们还可能在某些时候用不到,这就造成了资源的浪费,不合理,所以就有了单例模式。\n\n\n# 单例模式的定义\n\n单例模式确保一个类只有一个实例,并提供一个全局唯一访问点\n\n\n# 单例模式的类图\n\n\n\n\n# 单例模式的实现\n\n\n# 饿汉式\n\nstatic 变量在类装载的时候进行初始化 多个实例的 static 变量会共享同一块内存区域 用这两个知识点写出的单例类就是饿汉式了,初始化类的时候就创建,饥不择食,饿汉\n\npublic class singleton {\n\n //构造私有化,防止直接new\n private singleton(){}\n\n //静态初始化器(static initializer)中创建实例,保证线程安全\n private static singleton instance = new singleton();\n\n public static singleton getinstance(){\n return instance;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n饿汉式是线程安全的,jvm在加载类时马上创建唯一的实例对象,且只会装载一次。\n\njava 实现的单例是一个虚拟机的范围,因为装载类的功能是虚拟机的,所以一个虚拟机通过自己的classloader 装载饿汉式实现单例类的时候就会创建一个类实例。(如果一个虚拟机里有多个classloader的话,就会有多个实例)\n\n\n# 懒汉式\n\n懒汉式,就是实例在用到的时候才去创建,比较“懒”\n\n单例模式的懒汉式实现方式体现了延迟加载的思想(延迟加载也称懒加载lazy load,就是一开始不要加载资源或数据,等到要使用的时候才加载)\n\n同步方法\n\npublic class singleton {\n private static singleton singleton;\n\n private singleton(){}\n\n \t//解决了线程不安全问题,但是效率太低了,每个线程想获得类的实例的时候,都需要同步方法,不推荐\n public static synchronized singleton getinstance(){\n if(singleton == null){\n singleton = new singleton();\n }\n return singleton;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n双重检查加锁\n\npublic class singleton {\n\n \t//volatitle关键词确保,多线程正确处理singleton\n private static volatile singleton singleton;\n \n private singleton(){}\n \n public static singleton getinstance(){\n if(singleton ==null){\n synchronized (singleton.class){\n if(singleton == null){\n singleton = new singleton();\n }\n }\n }\n return singleton;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n\n\ndouble-check 概念(进行两次检查)是多线程开发中经常使用的,为什么需要双重检查锁呢?因为第一次检查是确保之前是一个空对象,而非空对象就不需要同步了,空对象的线程然后进入同步代码块,如果不加第二次空对象检查,两个线程同时获取同步代码块,一个线程进入同步代码块,另一个线程就会等待,而这两个线程就会创建两个实例化对象,所以需要在线程进入同步代码块后再次进行空对象检查,才能确保只创建一个实例化对象。\n\n双重检查加锁(double checked locking)线程安全、延迟加载、效率比较高\n\nvolatile:volatile一般用于多线程的可见性,这里用来防止指令重排(防止new singleton时指令重排序导致其他线程获取到未初始化完的对象)。被volatile 修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。\n\n指令重排\n\n指令重排是指在程序执行过程中, 为了性能考虑, 编译器和cpu可能会对指令重新排序。\n\njava中创建一个对象,往往包含三个过程。对于singleton = new singleton(),这不是一个原子操作,在 jvm 中包含如下三个过程。\n\n给 singleton 分配内存\n\n调用 singleton 的构造函数来初始化成员变量,形成实例\n\n将 singleton 对象指向分配的内存空间(执行完这步 singleton才是非 null 了)\n\n但是,由于jvm会进行指令重排序,所以上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3,也可能是 1-3-2。\n\n如果是 1-3-2,则在 3 执行完毕,2 未执行之前,被另一个线程抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以这个线程会直接返回 instance,然后使用,那肯定就会报错了,所以要加入 volatile关键字。\n\n\n# 静态内部类\n\npublic class singleton {\n\n private singleton(){}\n\n private static class singletoninstance{\n private static final singleton instance = new singleton();\n }\n \n public static singleton getinstance(){\n return singletoninstance.instance;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n采用类加载的机制来保证初始化实例时只有一个线程;\n\n静态内部类方式在singleton 类被装载的时候并不会立即实例化,而是在调用getinstance的时候,才去装载内部类singletoninstance ,从而完成singleton的实例化\n\n类的静态属性只会在第一次加载类的时候初始化,所以,jvm帮我们保证了线程的安全性,在类初始化时,其他线程无法进入\n\n优点:线程安全,利用静态内部类实现延迟加载,效率较高,推荐使用\n\n\n# 枚举\n\nenum singleton{\n instance;\n public void method(){}\n}\n\n\n1\n2\n3\n4\n\n\n借助jdk5 添加的枚举实现单例,不仅可以避免多线程同步问题,还能防止反序列化重新创建新的对象,但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。不过,由于java1.5中才加入enum特性,所以使用的人并不多。\n\n这种方式是《effective java》 作者josh bloch 提倡的方式。\n\n\n# 单例模式在jdk 中的源码分析\n\njdk 中,java.lang.runtime 就是经典的单例模式(饿汉式)\n\n\n# 单例模式注意事项和细节\n\n单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使 用new 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或 耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数 据库或文件的对象(比如数据源、session工厂等)", "charsets": { "cjk": true }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 + "lastUpdated": "2022/09/20, 22:28:30", + "lastUpdatedTimestamp": 1663684110000 }, { - "title": "适配器模式", + "title": "什么是设计模式", "frontmatter": { - "title": "适配器模式", - "date": "2022-05-21T17:28:10.000Z", - "permalink": "/pages/827fc4/", + "title": "什么是设计模式", + "date": "2022-05-21T17:26:19.000Z", + "permalink": "/pages/3d768c/", "categories": [ "Java相关", "设计模式" @@ -2994,111 +2734,83 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/010.%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8F.html", - "relativePath": "01.Java相关/40.设计模式/010.适配器模式.md", - "key": "v-31509c80", - "path": "/pages/827fc4/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/003.%E4%BB%80%E4%B9%88%E6%98%AF%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20.html", + "relativePath": "01.Java相关/40.设计模式/003.什么是设计模式 .md", + "key": "v-88b087a6", + "path": "/pages/3d768c/", "headers": [ { "level": 2, - "title": "问题", - "slug": "问题", - "normalizedTitle": "问题", + "title": "什么是设计模式?", + "slug": "什么是设计模式", + "normalizedTitle": "什么是设计模式?", "charIndex": 2 }, { "level": 2, - "title": "真实世界类比", - "slug": "真实世界类比", - "normalizedTitle": "真实世界类比", - "charIndex": 233 - }, - { - "level": 2, - "title": "基本介绍", - "slug": "基本介绍", - "normalizedTitle": "基本介绍", - "charIndex": 312 - }, - { - "level": 2, - "title": "工作原理", - "slug": "工作原理", - "normalizedTitle": "工作原理", - "charIndex": 455 - }, - { - "level": 2, - "title": "适配器模式结构", - "slug": "适配器模式结构", - "normalizedTitle": "适配器模式结构", - "charIndex": 591 + "title": "设计模式之六大原则", + "slug": "设计模式之六大原则", + "normalizedTitle": "设计模式之六大原则", + "charIndex": 286 }, { "level": 3, - "title": "对象适配器", - "slug": "对象适配器", - "normalizedTitle": "对象适配器", - "charIndex": 443 + "title": "开闭原则(Open Close Principle)", + "slug": "开闭原则-open-close-principle", + "normalizedTitle": "开闭原则(open close principle)", + "charIndex": 300 }, { "level": 3, - "title": "Coding", - "slug": "coding", - "normalizedTitle": "coding", - "charIndex": 1064 - }, - { - "level": 2, - "title": "类适配器", - "slug": "类适配器", - "normalizedTitle": "类适配器", - "charIndex": 436 + "title": "里氏代换原则(Liskov Substitution Principle)", + "slug": "里氏代换原则-liskov-substitution-principle", + "normalizedTitle": "里氏代换原则(liskov substitution principle)", + "charIndex": 628 }, { - "level": 2, - "title": "Coding", - "slug": "coding-2", - "normalizedTitle": "coding", - "charIndex": 1064 + "level": 3, + "title": "依赖倒转原则(Dependence Inversion Principle)", + "slug": "依赖倒转原则-dependence-inversion-principle", + "normalizedTitle": "依赖倒转原则(dependence inversion principle)", + "charIndex": 861 }, { - "level": 2, - "title": "适配器模式适合应用场景", - "slug": "适配器模式适合应用场景", - "normalizedTitle": "适配器模式适合应用场景", - "charIndex": 3145 + "level": 3, + "title": "接口隔离原则(Interface Segregation Principle)", + "slug": "接口隔离原则-interface-segregation-principle", + "normalizedTitle": "接口隔离原则(interface segregation principle)", + "charIndex": 1024 }, { - "level": 2, - "title": "demo", - "slug": "demo", - "normalizedTitle": "demo", - "charIndex": 3518 + "level": 3, + "title": "迪米特法则(最少知道原则)(Demeter Principle)", + "slug": "迪米特法则-最少知道原则-demeter-principle", + "normalizedTitle": "迪米特法则(最少知道原则)(demeter principle)", + "charIndex": 1138 }, { - "level": 2, - "title": "适配器模式优缺点", - "slug": "适配器模式优缺点", - "normalizedTitle": "适配器模式优缺点", - "charIndex": 5027 + "level": 3, + "title": "单一职责原则( Single responsibility principle )", + "slug": "单一职责原则-single-responsibility-principle", + "normalizedTitle": "单一职责原则( single responsibility principle )", + "charIndex": 1403 }, { "level": 2, - "title": "Spring 中的适配器", - "slug": "spring-中的适配器", - "normalizedTitle": "spring 中的适配器", - "charIndex": 5191 - } - ], - "headersStr": "问题 真实世界类比 基本介绍 工作原理 适配器模式结构 对象适配器 Coding 类适配器 Coding 适配器模式适合应用场景 demo 适配器模式优缺点 Spring 中的适配器", - "content": "# 问题\n\n假设我们在做一套股票看盘系统,数据提供方给我们提供 XML 格式数据,我们获取数据用来显示,随着系统的迭代,我们要整合一些第三方系统的对外数据,但是他们只提供获取 JSON 格式的数据接口。\n\n在不想改变原有代码逻辑的情况下,如何解决呢?\n\n这时候我们就可以创建一个「适配器」。这是一个特殊的对象, 能够转换对象接口, 使其能与其他对象进行交互。\n\n适配器模式通过封装对象将复杂的转换过程隐藏于幕后。 被封装的对象甚至察觉不到适配器的存在。\n\n\n\n\n# 真实世界类比\n\n适配器是什么,不难理解,生活中也随处可见。比如,笔记本电脑的电源适配器、万能充(曾经的它真有一个这么牛逼的名字)、一拖十数据线等等。\n\n\n# 基本介绍\n\n * 适配器模式将一个类的接口,转换成客户期望的另外一个接口。适配器让原本接口不兼容的类可以合作无间。也可以叫包装器(Wrapper)\n\n * 适配器模式是一种结构型设计模式, 它能使接口不兼容的对象能够相互合作\n\n * 主要分为两类:类适配器模式、对象适配器模式\n\n\n# 工作原理\n\n * 适配器模式:将一个类的接口转换成另一种接口,让原本接口不兼容的类可以兼容\n * 从用户的角度看不到被适配者,是解耦的\n * 用户调用适配器转化出来的目标接口方法,适配器再调用被适配者的相关接口方法\n * 用户收到反馈结果,感觉只是和目标接口交互\n\n\n# 适配器模式结构\n\n\n# 对象适配器\n\n实现时使用了构成原则: 适配器实现了其中一个对象的接口, 并对另一个对象进行封装。 所有流行的编程语言都可以实现适配器。\n\n适配器设计模式的结构(对象适配器)\n\n * 客户端 (Client) 是包含当前程序业务逻辑的类。\n * 客户端接口 (Target) 描述了其他类与客户端代码合作时必须遵循的协议。\n * 服务 (Service) 中有一些功能类 (通常来自第三方或遗留系统)。 客户端与其接口不兼容, 因此无法直接调用其功能,也可以叫适配者类(Adaptee)。\n * 适配器 (Adapter) 是一个可以同时与客户端和服务交互的类: 它在实现客户端接口的同时封装了服务对象。 适配器接受客户端通过适配器接口发起的调用, 并将其转换为适用于被封装服务对象的调用。\n * 客户端代码只需通过接口与适配器交互即可, 无需与具体的适配器类耦合。 因此, 你可以向程序中添加新类型的适配器而无需修改已有代码。 这在服务类的接口被更改或替换时很有用: 你无需修改客户端代码就可以创建新的适配器类。\n\n\n# Coding\n\n定义客户端使用的接口,与业务相关\n\npublic interface Target {\n\n /*\n * 客户端请求处理的方法\n */\n void request();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n已经存在的接口,这个接口需要配置\n\npublic class Adaptee {\n\n /*\n * 原本存在的方法\n */\n public void specificRequest(){\n //业务代码\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n适配器类\n\npublic class Adapter implements Target {\n\n /*\n * 持有需要被适配的接口对象\n */\n private Adaptee adaptee;\n\n /*\n * 构造方法,传入需要被适配的对象\n * @param adaptee 需要被适配的对象\n */\n public Adapter(Adaptee adaptee) {\n this.adaptee = adaptee;\n }\n\n @Override\n public void request() {\n // TODO Auto-generated method stub\n adaptee.specificRequest();\n }\n\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n\n\n使用适配器的客户端\n\npublic class Client {\n\n public static void main(String[] args) {\n //创建需要被适配的对象\n Adaptee adaptee = new Adaptee();\n //创建客户端需要调用的接口对象\n Target target = new Adapter(adaptee);\n //请求处理\n target.request();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n\n# 类适配器\n\n这一实现使用了继承机制: 适配器同时继承两个对象的接口。 请注意, 这种方式仅能在支持多重继承的编程语言中实现,例如 C++, Java 不支持多重继承,也就没有这种适配器了。\n\n适配器设计模式(类适配器)\n\n类适配器不需要封装任何对象, 因为它同时继承了客户端和服务的行为。 适配功能在重写的方法中完成。 最后生成的适配器可替代已有的客户端类进行使用。\n\n\n# Coding\n\nJava 虽然不能实现标准的类适配器,但是有一种变通的方式,也能够使用继承来实现接口的适配,那就是让适配器去实现 Target 的接口,然后继承 Adaptee 的实现,虽然不是十分标准,但意思差不多。\n\n首先有一个已存在的将被适配的类\n\npublic class Adaptee {\n public void adapteeRequest() {\n System.out.println(\"被适配者的方法\");\n }\n}\n\n\n1\n2\n3\n4\n5\n\n\n定义客户端使用的接口,与业务相关\n\npublic interface Target {\n\n void request();\n}\n\n\n1\n2\n3\n4\n\n\n怎么才可以在目标接口中的 request() 调用 Adaptee 的 adapteeRequest() 方法呢?直接实现 Target 肯定是不行的,所以我们通过一个适配器类,实现 Target 接口,同时继承了 Adaptee 类,然后在实现的 request() 方法中调用父类的 adapteeRequest() 即可\n\npublic class Adapter extends Adaptee implements Target{\n @Override\n public void request() {\n //...一些操作...\n super.adapteeRequest();\n //...一些操作...\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\n使用适配器的客户端\n\npublic class Client {\n public static void main(String[] args) {\n\n Target adapterTarget = new Adapter();\n adapterTarget.request();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 适配器模式适合应用场景\n\n当你希望使用某个类, 但是其接口与其他代码不兼容时, 可以使用适配器类。\n\n适配器模式允许你创建一个中间层类, 其可作为代码与遗留类、 第三方类或提供怪异接口的类之间的转换器。\n\n如果您需要复用这样一些类, 他们处于同一个继承体系, 并且他们又有了额外的一些共同的方法, 但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。\n\n你可以扩展每个子类, 将缺少的功能添加到新的子类中。 但是, 你必须在所有新子类中重复添加这些代码, 这样会使得代码有坏味道。\n\n将缺失功能添加到一个适配器类中是一种优雅得多的解决方案。 然后你可以将缺少功能的对象封装在适配器中, 从而动态地获取所需功能。 如要这一点正常运作, 目标类必须要有通用接口, 适配器的成员变量应当遵循该通用接口。 这种方式同装饰模式非常相似。\n\n\n# demo\n\n用一个生活中的充电器的例子来讲解下适配器,我国民用电都是 220V,而我们的手机充电一般需要 5V。\n\n220V 的交流电相当于被适配者 Adaptee,我们的目标 Target 是 5V 直流电,充电器本身相当于一个 Adapter,将220V 的输入电压变换为 5V 输出。\n\n首先是我们的民用电(我国是 220V,当然还可以有其他国家的其他准备,可随时扩展)\n\npublic class Volatage220V {\n\n public final int output = 220;\n\n public int output220v() {\n System.out.println(\"输出电压 \" + output);\n return output;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n适配接口\n\npublic interface IVoltage5V {\n int output5V();\n}\n\n\n1\n2\n3\n\n\n我们的手机充电,只支持 5V 电压\n\npublic class Phone {\n\n public void charging(IVoltage5V v) {\n if (v.output5V() == 5) {\n System.out.println(\"电压 5V ,符合充电标准,开始充电\");\n } else {\n System.out.println(\"电压不符合标准,无法充电\");\n }\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n适配器\n\npublic class VoltageAdapter implements IVoltage5V {\n\n private Volatage220V volatage220V; //聚合\n\n public VoltageAdapter(Volatage220V v) {\n this.volatage220V = v;\n }\n\n @Override\n public int output5V() {\n int dst = 0;\n if (null != volatage220V) {\n int src = volatage220V.output220v();\n System.out.println(\"适配器工作~~~~~\");\n dst = src / 44;\n System.out.println(\"适配器工作完成,输出电压\" + dst);\n }\n return dst;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\n工作,如果去国外旅游,有不同的电压,只需要扩展适配器即可。\n\npublic class Client {\n public static void main(String[] args) {\n Phone phone = new Phone();\n phone.charging(new VoltageAdapter(new Volatage220V()));\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n\n# 适配器模式优缺点\n\n单一职责原则,你可以将接口或数据转换代码从程序主要业务逻辑中分离。\n\n开闭原则。 只要客户端代码通过客户端接口与适配器进行交互, 你就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。\n\n代码整体复杂度增加, 因为你需要新增一系列接口和类。 有时直接更改服务类使其与其他代码兼容会更简单。\n\n\n# Spring 中的适配器\n\nSpring 源码中搜关键字Adapter 会出现很多实现类,SpringMVC 中的 HandlerAdapter ,就是适配器的应用。\n\n我们先回顾下 SpringMVC 处理流程:\n\nSpring MVC 中的适配器模式主要用于执行目标 Controller 中的请求处理方法。\n\n在Spring MVC中,DispatcherServlet 作为用户,HandlerAdapter 作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller 作为需要适配的类。\n\n为什么要在 Spring MVC 中使用适配器模式?Spring MVC 中的 Controller 种类众多,不同类型的 Controller 通过不同的方法来对请求进行处理。如果不利用适配器模式的话,DispatcherServlet 直接获取对应类型的 Controller,需要的自行来判断,像下面这段代码一样:\n\nif(mappedHandler.getHandler() instanceof MultiActionController){ \n ((MultiActionController)mappedHandler.getHandler()).xxx \n}else if(mappedHandler.getHandler() instanceof XXX){ \n ... \n}else if(...){ \n ... \n} \n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n这样假设如果我们增加一个 Controller,就要在代码中加入一行 if 语句,这种形式就使得程序难以维护,也违反了设计模式中的开闭原则 – 对扩展开放,对修改关闭。\n\n我们通过源码看看 SpringMVC 是如何实现的,首先看下核心类 DispatcherServlet:\n\npublic class DispatcherServlet extends FrameworkServlet {\n \t//......\n\t//维护所有HandlerAdapter类的集合\n @Nullable\n private List handlerAdapters;\n \n\t//初始化handlerAdapters\n private void initHandlerAdapters(ApplicationContext context) {\n this.handlerAdapters = null;\n if (this.detectAllHandlerAdapters) {\n Map matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);\n if (!matchingBeans.isEmpty()) {\n this.handlerAdapters = new ArrayList(matchingBeans.values());\n AnnotationAwareOrderComparator.sort(this.handlerAdapters);\n }\n } else {\n try {\n HandlerAdapter ha = (HandlerAdapter)context.getBean(\"handlerAdapter\", HandlerAdapter.class);\n this.handlerAdapters = Collections.singletonList(ha);\n } catch (NoSuchBeanDefinitionException var3) {\n }\n }\n\n if (this.handlerAdapters == null) {\n this.handlerAdapters = this.getDefaultStrategies(context, HandlerAdapter.class);\n if (this.logger.isTraceEnabled()) {\n this.logger.trace(\"No HandlerAdapters declared for servlet '\" + this.getServletName() + \"': using default strategies from DispatcherServlet.properties\");\n }\n }\n }\n \n //dispatch 方法中会获取 HandlerAdapter\n protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {\n //...\n \n\t\t//获得controller对应的适配器\n HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler()); \n\t\t\t\t\n\t\t//调用适配器的handler方法处理请求,并返回ModelAndView\n mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); \n //...\n }\n \t\n\t //返回对应的controller的处理器\n protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {\n if (this.handlerAdapters != null) {\n Iterator var2 = this.handlerAdapters.iterator();\n\n while(var2.hasNext()) {\n HandlerAdapter adapter = (HandlerAdapter)var2.next();\n if (adapter.supports(handler)) {\n return adapter;\n }\n }\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n\n\n接着看下 HandlerAdapter 的源码,也就是适配器接口:\n\npublic interface HandlerAdapter {\n boolean supports(Object var1);\n\n @Nullable\n ModelAndView handle(HttpServletRequest var1, HttpServletResponse var2, Object var3) throws Exception;\n\n long getLastModified(HttpServletRequest var1, Object var2);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\n再来屡一下这个流程:\n\n首先是适配器接口 DispatchServlet 中有一个集合维护所有的 HandlerAdapter,如果配置文件中没有对适配器进行配置,那么 DispatchServlet 会在创建时对该变量进行初始化,注册所有默认的 HandlerAdapter。\n\n当一个请求过来时,DispatchServlet 会根据传过来的 handler 类型从该集合中寻找对应的 HandlerAdapter子类进行处理,并且调用它的 handler() 方法 对应的 HandlerAdapter 中的 handler() 方法又会执行对应 Controller 的 handleRequest() 方法\n\n适配器与 handler 有对应关系,而各个适配器又都是适配器接口的实现类,因此,它们都遵循相同的适配器标准,所以用户可以按照相同的方式,通过不同的 handler 去处理请求。\n\n当然了,Spring 框架中也为我们定义了一些默认的 Handler 对应的适配器。\n\n通过适配器模式我们将所有的 controller 统一交给 HandlerAdapter 处理,免去了写大量的 if-else 语句对 Controller 进行判断,也更利于扩展新的 Controller 类型。\n\n参考: 随遇而安的适配器模式 | Spring 中的适配器", - "normalizedContent": "# 问题\n\n假设我们在做一套股票看盘系统,数据提供方给我们提供 xml 格式数据,我们获取数据用来显示,随着系统的迭代,我们要整合一些第三方系统的对外数据,但是他们只提供获取 json 格式的数据接口。\n\n在不想改变原有代码逻辑的情况下,如何解决呢?\n\n这时候我们就可以创建一个「适配器」。这是一个特殊的对象, 能够转换对象接口, 使其能与其他对象进行交互。\n\n适配器模式通过封装对象将复杂的转换过程隐藏于幕后。 被封装的对象甚至察觉不到适配器的存在。\n\n\n\n\n# 真实世界类比\n\n适配器是什么,不难理解,生活中也随处可见。比如,笔记本电脑的电源适配器、万能充(曾经的它真有一个这么牛逼的名字)、一拖十数据线等等。\n\n\n# 基本介绍\n\n * 适配器模式将一个类的接口,转换成客户期望的另外一个接口。适配器让原本接口不兼容的类可以合作无间。也可以叫包装器(wrapper)\n\n * 适配器模式是一种结构型设计模式, 它能使接口不兼容的对象能够相互合作\n\n * 主要分为两类:类适配器模式、对象适配器模式\n\n\n# 工作原理\n\n * 适配器模式:将一个类的接口转换成另一种接口,让原本接口不兼容的类可以兼容\n * 从用户的角度看不到被适配者,是解耦的\n * 用户调用适配器转化出来的目标接口方法,适配器再调用被适配者的相关接口方法\n * 用户收到反馈结果,感觉只是和目标接口交互\n\n\n# 适配器模式结构\n\n\n# 对象适配器\n\n实现时使用了构成原则: 适配器实现了其中一个对象的接口, 并对另一个对象进行封装。 所有流行的编程语言都可以实现适配器。\n\n适配器设计模式的结构(对象适配器)\n\n * 客户端 (client) 是包含当前程序业务逻辑的类。\n * 客户端接口 (target) 描述了其他类与客户端代码合作时必须遵循的协议。\n * 服务 (service) 中有一些功能类 (通常来自第三方或遗留系统)。 客户端与其接口不兼容, 因此无法直接调用其功能,也可以叫适配者类(adaptee)。\n * 适配器 (adapter) 是一个可以同时与客户端和服务交互的类: 它在实现客户端接口的同时封装了服务对象。 适配器接受客户端通过适配器接口发起的调用, 并将其转换为适用于被封装服务对象的调用。\n * 客户端代码只需通过接口与适配器交互即可, 无需与具体的适配器类耦合。 因此, 你可以向程序中添加新类型的适配器而无需修改已有代码。 这在服务类的接口被更改或替换时很有用: 你无需修改客户端代码就可以创建新的适配器类。\n\n\n# coding\n\n定义客户端使用的接口,与业务相关\n\npublic interface target {\n\n /*\n * 客户端请求处理的方法\n */\n void request();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n已经存在的接口,这个接口需要配置\n\npublic class adaptee {\n\n /*\n * 原本存在的方法\n */\n public void specificrequest(){\n //业务代码\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n适配器类\n\npublic class adapter implements target {\n\n /*\n * 持有需要被适配的接口对象\n */\n private adaptee adaptee;\n\n /*\n * 构造方法,传入需要被适配的对象\n * @param adaptee 需要被适配的对象\n */\n public adapter(adaptee adaptee) {\n this.adaptee = adaptee;\n }\n\n @override\n public void request() {\n // todo auto-generated method stub\n adaptee.specificrequest();\n }\n\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n\n\n使用适配器的客户端\n\npublic class client {\n\n public static void main(string[] args) {\n //创建需要被适配的对象\n adaptee adaptee = new adaptee();\n //创建客户端需要调用的接口对象\n target target = new adapter(adaptee);\n //请求处理\n target.request();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n\n# 类适配器\n\n这一实现使用了继承机制: 适配器同时继承两个对象的接口。 请注意, 这种方式仅能在支持多重继承的编程语言中实现,例如 c++, java 不支持多重继承,也就没有这种适配器了。\n\n适配器设计模式(类适配器)\n\n类适配器不需要封装任何对象, 因为它同时继承了客户端和服务的行为。 适配功能在重写的方法中完成。 最后生成的适配器可替代已有的客户端类进行使用。\n\n\n# coding\n\njava 虽然不能实现标准的类适配器,但是有一种变通的方式,也能够使用继承来实现接口的适配,那就是让适配器去实现 target 的接口,然后继承 adaptee 的实现,虽然不是十分标准,但意思差不多。\n\n首先有一个已存在的将被适配的类\n\npublic class adaptee {\n public void adapteerequest() {\n system.out.println(\"被适配者的方法\");\n }\n}\n\n\n1\n2\n3\n4\n5\n\n\n定义客户端使用的接口,与业务相关\n\npublic interface target {\n\n void request();\n}\n\n\n1\n2\n3\n4\n\n\n怎么才可以在目标接口中的 request() 调用 adaptee 的 adapteerequest() 方法呢?直接实现 target 肯定是不行的,所以我们通过一个适配器类,实现 target 接口,同时继承了 adaptee 类,然后在实现的 request() 方法中调用父类的 adapteerequest() 即可\n\npublic class adapter extends adaptee implements target{\n @override\n public void request() {\n //...一些操作...\n super.adapteerequest();\n //...一些操作...\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\n使用适配器的客户端\n\npublic class client {\n public static void main(string[] args) {\n\n target adaptertarget = new adapter();\n adaptertarget.request();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 适配器模式适合应用场景\n\n当你希望使用某个类, 但是其接口与其他代码不兼容时, 可以使用适配器类。\n\n适配器模式允许你创建一个中间层类, 其可作为代码与遗留类、 第三方类或提供怪异接口的类之间的转换器。\n\n如果您需要复用这样一些类, 他们处于同一个继承体系, 并且他们又有了额外的一些共同的方法, 但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。\n\n你可以扩展每个子类, 将缺少的功能添加到新的子类中。 但是, 你必须在所有新子类中重复添加这些代码, 这样会使得代码有坏味道。\n\n将缺失功能添加到一个适配器类中是一种优雅得多的解决方案。 然后你可以将缺少功能的对象封装在适配器中, 从而动态地获取所需功能。 如要这一点正常运作, 目标类必须要有通用接口, 适配器的成员变量应当遵循该通用接口。 这种方式同装饰模式非常相似。\n\n\n# demo\n\n用一个生活中的充电器的例子来讲解下适配器,我国民用电都是 220v,而我们的手机充电一般需要 5v。\n\n220v 的交流电相当于被适配者 adaptee,我们的目标 target 是 5v 直流电,充电器本身相当于一个 adapter,将220v 的输入电压变换为 5v 输出。\n\n首先是我们的民用电(我国是 220v,当然还可以有其他国家的其他准备,可随时扩展)\n\npublic class volatage220v {\n\n public final int output = 220;\n\n public int output220v() {\n system.out.println(\"输出电压 \" + output);\n return output;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n适配接口\n\npublic interface ivoltage5v {\n int output5v();\n}\n\n\n1\n2\n3\n\n\n我们的手机充电,只支持 5v 电压\n\npublic class phone {\n\n public void charging(ivoltage5v v) {\n if (v.output5v() == 5) {\n system.out.println(\"电压 5v ,符合充电标准,开始充电\");\n } else {\n system.out.println(\"电压不符合标准,无法充电\");\n }\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n适配器\n\npublic class voltageadapter implements ivoltage5v {\n\n private volatage220v volatage220v; //聚合\n\n public voltageadapter(volatage220v v) {\n this.volatage220v = v;\n }\n\n @override\n public int output5v() {\n int dst = 0;\n if (null != volatage220v) {\n int src = volatage220v.output220v();\n system.out.println(\"适配器工作~~~~~\");\n dst = src / 44;\n system.out.println(\"适配器工作完成,输出电压\" + dst);\n }\n return dst;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\n工作,如果去国外旅游,有不同的电压,只需要扩展适配器即可。\n\npublic class client {\n public static void main(string[] args) {\n phone phone = new phone();\n phone.charging(new voltageadapter(new volatage220v()));\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n\n# 适配器模式优缺点\n\n单一职责原则,你可以将接口或数据转换代码从程序主要业务逻辑中分离。\n\n开闭原则。 只要客户端代码通过客户端接口与适配器进行交互, 你就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。\n\n代码整体复杂度增加, 因为你需要新增一系列接口和类。 有时直接更改服务类使其与其他代码兼容会更简单。\n\n\n# spring 中的适配器\n\nspring 源码中搜关键字adapter 会出现很多实现类,springmvc 中的 handleradapter ,就是适配器的应用。\n\n我们先回顾下 springmvc 处理流程:\n\nspring mvc 中的适配器模式主要用于执行目标 controller 中的请求处理方法。\n\n在spring mvc中,dispatcherservlet 作为用户,handleradapter 作为期望接口,具体的适配器实现类用于对目标类进行适配,controller 作为需要适配的类。\n\n为什么要在 spring mvc 中使用适配器模式?spring mvc 中的 controller 种类众多,不同类型的 controller 通过不同的方法来对请求进行处理。如果不利用适配器模式的话,dispatcherservlet 直接获取对应类型的 controller,需要的自行来判断,像下面这段代码一样:\n\nif(mappedhandler.gethandler() instanceof multiactioncontroller){ \n ((multiactioncontroller)mappedhandler.gethandler()).xxx \n}else if(mappedhandler.gethandler() instanceof xxx){ \n ... \n}else if(...){ \n ... \n} \n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n这样假设如果我们增加一个 controller,就要在代码中加入一行 if 语句,这种形式就使得程序难以维护,也违反了设计模式中的开闭原则 – 对扩展开放,对修改关闭。\n\n我们通过源码看看 springmvc 是如何实现的,首先看下核心类 dispatcherservlet:\n\npublic class dispatcherservlet extends frameworkservlet {\n \t//......\n\t//维护所有handleradapter类的集合\n @nullable\n private list handleradapters;\n \n\t//初始化handleradapters\n private void inithandleradapters(applicationcontext context) {\n this.handleradapters = null;\n if (this.detectallhandleradapters) {\n map matchingbeans = beanfactoryutils.beansoftypeincludingancestors(context, handleradapter.class, true, false);\n if (!matchingbeans.isempty()) {\n this.handleradapters = new arraylist(matchingbeans.values());\n annotationawareordercomparator.sort(this.handleradapters);\n }\n } else {\n try {\n handleradapter ha = (handleradapter)context.getbean(\"handleradapter\", handleradapter.class);\n this.handleradapters = collections.singletonlist(ha);\n } catch (nosuchbeandefinitionexception var3) {\n }\n }\n\n if (this.handleradapters == null) {\n this.handleradapters = this.getdefaultstrategies(context, handleradapter.class);\n if (this.logger.istraceenabled()) {\n this.logger.trace(\"no handleradapters declared for servlet '\" + this.getservletname() + \"': using default strategies from dispatcherservlet.properties\");\n }\n }\n }\n \n //dispatch 方法中会获取 handleradapter\n protected void dodispatch(httpservletrequest request, httpservletresponse response) throws exception {\n //...\n \n\t\t//获得controller对应的适配器\n handleradapter ha = this.gethandleradapter(mappedhandler.gethandler()); \n\t\t\t\t\n\t\t//调用适配器的handler方法处理请求,并返回modelandview\n mv = ha.handle(processedrequest, response, mappedhandler.gethandler()); \n //...\n }\n \t\n\t //返回对应的controller的处理器\n protected handleradapter gethandleradapter(object handler) throws servletexception {\n if (this.handleradapters != null) {\n iterator var2 = this.handleradapters.iterator();\n\n while(var2.hasnext()) {\n handleradapter adapter = (handleradapter)var2.next();\n if (adapter.supports(handler)) {\n return adapter;\n }\n }\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n\n\n接着看下 handleradapter 的源码,也就是适配器接口:\n\npublic interface handleradapter {\n boolean supports(object var1);\n\n @nullable\n modelandview handle(httpservletrequest var1, httpservletresponse var2, object var3) throws exception;\n\n long getlastmodified(httpservletrequest var1, object var2);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\n再来屡一下这个流程:\n\n首先是适配器接口 dispatchservlet 中有一个集合维护所有的 handleradapter,如果配置文件中没有对适配器进行配置,那么 dispatchservlet 会在创建时对该变量进行初始化,注册所有默认的 handleradapter。\n\n当一个请求过来时,dispatchservlet 会根据传过来的 handler 类型从该集合中寻找对应的 handleradapter子类进行处理,并且调用它的 handler() 方法 对应的 handleradapter 中的 handler() 方法又会执行对应 controller 的 handlerequest() 方法\n\n适配器与 handler 有对应关系,而各个适配器又都是适配器接口的实现类,因此,它们都遵循相同的适配器标准,所以用户可以按照相同的方式,通过不同的 handler 去处理请求。\n\n当然了,spring 框架中也为我们定义了一些默认的 handler 对应的适配器。\n\n通过适配器模式我们将所有的 controller 统一交给 handleradapter 处理,免去了写大量的 if-else 语句对 controller 进行判断,也更利于扩展新的 controller 类型。\n\n参考: 随遇而安的适配器模式 | spring 中的适配器", + "title": "高内聚,低耦合?", + "slug": "高内聚-低耦合", + "normalizedTitle": "高内聚,低耦合?", + "charIndex": 1768 + } + ], + "headersStr": "什么是设计模式? 设计模式之六大原则 开闭原则(Open Close Principle) 里氏代换原则(Liskov Substitution Principle) 依赖倒转原则(Dependence Inversion Principle) 接口隔离原则(Interface Segregation Principle) 迪米特法则(最少知道原则)(Demeter Principle) 单一职责原则( Single responsibility principle ) 高内聚,低耦合?", + "content": "# 什么是设计模式?\n\n设计模式是一套代码设计「经验的总结」。项目中「合理的」运用设计模式可以「巧妙的解决很多问题」。\n\n * 经验的总结:抱着「代码虐我千百遍,我待代码如初恋」的心态,最终得出来的「套路」。\n\n * 合理的:要对设计模式的使用场景有一定的认识后才使用,「不要滥用」。如:输出一句“hello world”,非要强行给加上各种模式。 问:“为什么”,答:“总感觉少了模式!”。\n\n * 巧妙的解决了很多问题:被广泛应用的原因。\n\n> 为什么要提倡“Design Pattern呢?根本原因是为了代码复用,增加可维护性。那么怎么才能实现代码复用呢?\n\n\n# 设计模式之六大原则\n\n\n# 开闭原则(Open Close Principle)\n\n1988年,勃兰特·梅耶(Bertrand Meyer)在他的著作《面向对象软件构造(Object Oriented Software Construction)》中提出了开闭原则,它的原文是这样:“Software entities should be open for extension,but closed for modification”。 意思:软件模块应该对扩展开放,对修改关闭。 举例:在程序需要进行新增功能的时候,不能去修改原有的代码,而是新增代码,实现一个热插拔的效果(热插拔:灵活的去除或添加功能,不影响到原有的功能)。 目的:为了使程序的扩展性好,易于维护和升级。\n\n\n# 里氏代换原则(Liskov Substitution Principle)\n\n意思:里氏代换原则是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。 举例:球类,原本是一种体育用品,它的衍生类有篮球、足球、排球、羽毛球等等,如果衍生类替换了基类的原本方法,如把体育用品改成了食用品(那么软件单位的功能受到影响),就不符合里氏代换原则。 目的:对实现抽象化的具体步骤的规范。\n\n\n# 依赖倒转原则(Dependence Inversion Principle)\n\n意思:针对接口编程,而不是针对实现编程。 举例:以计算机系统为例,无论主板、CPU、内存、硬件都是在针对接口设计的,如果针对实现来设计,内存就要对应到针对某个品牌的主板,那么会出现换内存需要把主板也换掉的尴尬。 目的:降低模块间的耦合。\n\n\n# 接口隔离原则(Interface Segregation Principle)\n\n使用多个隔离的接口,比使用单个接口要好。 举例:比如:登录,注册时属于用户模块的两个接口,比写成一个接口好。 目的:提高程序设计灵活性。\n\n\n# 迪米特法则(最少知道原则)(Demeter Principle)\n\n1987年秋天由美国Northeastern University的Ian Holland提出,被UML的创始者之一[Booch]等普及。后来,因为在经典著作《 The Pragmatic Programmer》而广为人知。 意思:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。 举例:一个类公开的public属性或方法越多,修改时涉及的面也就越大,变更引起的风险扩散也就越大。 目的:降低类之间的耦合,减少对其他类的依赖。\n\n\n# 单一职责原则( Single responsibility principle )\n\n该原则由罗伯特·C·马丁(Robert C. Martin)于《敏捷软件开发:原则、模式和实践》一书中给出的。马丁表示此原则是基于汤姆·狄马克(Tom DeMarco)和Meilir Page-Jones的著作中的内聚性原则发展出的。 意思:一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。 举例:该原则意思简单到不需要举例! 目的:类的复杂性降低,可读性提高,可维护性提高。\n\n刚入行的时候,在想什么样的代码是好代码?看到很多前辈的文字都说好的代码要符合「高内聚,低耦合」,但是我听到这样的解释,是这样的\n\n而现在对设计模式有了一定程度上的学习,感觉懂了一些,小伙伴们你们学会了吗?\n\n\n# 高内聚,低耦合?\n\n内聚是从功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事。它描述的是模块内的功能联系; 耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及通过接口的数据。\n\n参考: 图解九种常见的设计模式 什么是「设计模式」?", + "normalizedContent": "# 什么是设计模式?\n\n设计模式是一套代码设计「经验的总结」。项目中「合理的」运用设计模式可以「巧妙的解决很多问题」。\n\n * 经验的总结:抱着「代码虐我千百遍,我待代码如初恋」的心态,最终得出来的「套路」。\n\n * 合理的:要对设计模式的使用场景有一定的认识后才使用,「不要滥用」。如:输出一句“hello world”,非要强行给加上各种模式。 问:“为什么”,答:“总感觉少了模式!”。\n\n * 巧妙的解决了很多问题:被广泛应用的原因。\n\n> 为什么要提倡“design pattern呢?根本原因是为了代码复用,增加可维护性。那么怎么才能实现代码复用呢?\n\n\n# 设计模式之六大原则\n\n\n# 开闭原则(open close principle)\n\n1988年,勃兰特·梅耶(bertrand meyer)在他的著作《面向对象软件构造(object oriented software construction)》中提出了开闭原则,它的原文是这样:“software entities should be open for extension,but closed for modification”。 意思:软件模块应该对扩展开放,对修改关闭。 举例:在程序需要进行新增功能的时候,不能去修改原有的代码,而是新增代码,实现一个热插拔的效果(热插拔:灵活的去除或添加功能,不影响到原有的功能)。 目的:为了使程序的扩展性好,易于维护和升级。\n\n\n# 里氏代换原则(liskov substitution principle)\n\n意思:里氏代换原则是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。 举例:球类,原本是一种体育用品,它的衍生类有篮球、足球、排球、羽毛球等等,如果衍生类替换了基类的原本方法,如把体育用品改成了食用品(那么软件单位的功能受到影响),就不符合里氏代换原则。 目的:对实现抽象化的具体步骤的规范。\n\n\n# 依赖倒转原则(dependence inversion principle)\n\n意思:针对接口编程,而不是针对实现编程。 举例:以计算机系统为例,无论主板、cpu、内存、硬件都是在针对接口设计的,如果针对实现来设计,内存就要对应到针对某个品牌的主板,那么会出现换内存需要把主板也换掉的尴尬。 目的:降低模块间的耦合。\n\n\n# 接口隔离原则(interface segregation principle)\n\n使用多个隔离的接口,比使用单个接口要好。 举例:比如:登录,注册时属于用户模块的两个接口,比写成一个接口好。 目的:提高程序设计灵活性。\n\n\n# 迪米特法则(最少知道原则)(demeter principle)\n\n1987年秋天由美国northeastern university的ian holland提出,被uml的创始者之一[booch]等普及。后来,因为在经典著作《 the pragmatic programmer》而广为人知。 意思:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。 举例:一个类公开的public属性或方法越多,修改时涉及的面也就越大,变更引起的风险扩散也就越大。 目的:降低类之间的耦合,减少对其他类的依赖。\n\n\n# 单一职责原则( single responsibility principle )\n\n该原则由罗伯特·c·马丁(robert c. martin)于《敏捷软件开发:原则、模式和实践》一书中给出的。马丁表示此原则是基于汤姆·狄马克(tom demarco)和meilir page-jones的著作中的内聚性原则发展出的。 意思:一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。 举例:该原则意思简单到不需要举例! 目的:类的复杂性降低,可读性提高,可维护性提高。\n\n刚入行的时候,在想什么样的代码是好代码?看到很多前辈的文字都说好的代码要符合「高内聚,低耦合」,但是我听到这样的解释,是这样的\n\n而现在对设计模式有了一定程度上的学习,感觉懂了一些,小伙伴们你们学会了吗?\n\n\n# 高内聚,低耦合?\n\n内聚是从功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事。它描述的是模块内的功能联系; 耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及通过接口的数据。\n\n参考: 图解九种常见的设计模式 什么是「设计模式」?", "charsets": { "cjk": true }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 + "lastUpdated": "2022/09/20, 22:28:30", + "lastUpdatedTimestamp": 1663684110000 }, { "title": "工厂模式使用场景 能举个例子吗", @@ -3329,115 +3041,15 @@ export const siteData = { "charsets": { "cjk": true }, - "lastUpdated": "2022/06/20, 22:47:41", - "lastUpdatedTimestamp": 1655736461000 - }, - { - "title": "模板方法模式", - "frontmatter": { - "title": "模板方法模式", - "date": "2022-05-21T17:29:10.000Z", - "permalink": "/pages/43b656/", - "categories": [ - "Java相关", - "设计模式" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/040.%E6%A8%A1%E6%9D%BF%E6%96%B9%E6%B3%95%E6%A8%A1%E5%BC%8F.html", - "relativePath": "01.Java相关/40.设计模式/040.模板方法模式.md", - "key": "v-28deb9da", - "path": "/pages/43b656/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "代理模式", - "frontmatter": { - "title": "代理模式", - "date": "2022-05-21T17:30:05.000Z", - "permalink": "/pages/a3c735/", - "categories": [ - "Java相关", - "设计模式" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/070.%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F.html", - "relativePath": "01.Java相关/40.设计模式/070.代理模式.md", - "key": "v-2279e49d", - "path": "/pages/a3c735/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "观察模式", - "frontmatter": { - "title": "观察模式", - "date": "2022-05-21T17:29:45.000Z", - "permalink": "/pages/cbff3c/", - "categories": [ - "Java相关", - "设计模式" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/060.%E8%A7%82%E5%AF%9F%E6%A8%A1%E5%BC%8F.html", - "relativePath": "01.Java相关/40.设计模式/060.观察模式.md", - "key": "v-64f1031c", - "path": "/pages/cbff3c/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "策略模式", - "frontmatter": { - "title": "策略模式", - "date": "2022-05-21T17:29:26.000Z", - "permalink": "/pages/78c7aa/", - "categories": [ - "Java相关", - "设计模式" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/050.%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F.html", - "relativePath": "01.Java相关/40.设计模式/050.策略模式.md", - "key": "v-5b0157cf", - "path": "/pages/78c7aa/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 + "lastUpdated": "2022/09/20, 22:28:30", + "lastUpdatedTimestamp": 1663684110000 }, { - "title": "建造者模式", + "title": "简述装饰者模式", "frontmatter": { - "title": "建造者模式", - "date": "2022-05-21T17:30:20.000Z", - "permalink": "/pages/c332ae/", + "title": "简述装饰者模式", + "date": "2022-05-21T17:26:42.000Z", + "permalink": "/pages/47a4e3/", "categories": [ "Java相关", "设计模式" @@ -3446,199 +3058,979 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/080.%E5%BB%BA%E9%80%A0%E8%80%85%E6%A8%A1%E5%BC%8F.html", - "relativePath": "01.Java相关/40.设计模式/080.建造者模式.md", - "key": "v-dd5f4440", - "path": "/pages/c332ae/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "操作系统", - "frontmatter": { - "title": "操作系统", - "date": "2022-03-11T00:51:48.000Z", - "permalink": "/pages/e5f2d7/", - "categories": [ - "计算机基础", - "CSS" - ], - "tags": [ - null - ] - }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/01.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F.html", - "relativePath": "02.计算机基础/20.操作系统/01.操作系统.md", - "key": "v-6f178cbc", - "path": "/pages/e5f2d7/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/005.%E8%A3%85%E9%A5%B0%E8%80%85%E6%A8%A1%E5%BC%8F.html", + "relativePath": "01.Java相关/40.设计模式/005.装饰者模式.md", + "key": "v-22a9eaef", + "path": "/pages/47a4e3/", "headers": [ { "level": 2, - "title": "进程间有哪些通信方式?", - "slug": "进程间有哪些通信方式", - "normalizedTitle": "进程间有哪些通信方式?", - "charIndex": 2 - }, - { - "level": 2, - "title": "简述自旋锁与互斥锁的使用场景", - "slug": "简述自旋锁与互斥锁的使用场景", - "normalizedTitle": "简述自旋锁与互斥锁的使用场景", - "charIndex": 18 + "title": "模式动机", + "slug": "模式动机", + "normalizedTitle": "模式动机", + "charIndex": 186 }, { "level": 2, - "title": "进程和线程之间有什么区别?", - "slug": "进程和线程之间有什么区别", - "normalizedTitle": "进程和线程之间有什么区别?", - "charIndex": 37 + "title": "定义", + "slug": "定义", + "normalizedTitle": "定义", + "charIndex": 489 }, { "level": 2, - "title": "简述几个常用的 Linux 命令以及他们的功能", - "slug": "简述几个常用的-linux-命令以及他们的功能", - "normalizedTitle": "简述几个常用的 linux 命令以及他们的功能", - "charIndex": 55 + "title": "角色", + "slug": "角色", + "normalizedTitle": "角色", + "charIndex": 632 }, { "level": 2, - "title": "创建线程有多少种方式?", - "slug": "创建线程有多少种方式", - "normalizedTitle": "创建线程有多少种方式?", - "charIndex": 83 + "title": "类图", + "slug": "类图", + "normalizedTitle": "类图", + "charIndex": 1194 }, { "level": 2, - "title": "线程有多少种状态,状态之间如何转换", - "slug": "线程有多少种状态-状态之间如何转换", - "normalizedTitle": "线程有多少种状态,状态之间如何转换", - "charIndex": 99 + "title": "实例", + "slug": "实例", + "normalizedTitle": "实例", + "charIndex": 1203 }, { "level": 2, - "title": "进程有多少种状态?", - "slug": "进程有多少种状态", - "normalizedTitle": "进程有多少种状态?", - "charIndex": 121 + "title": "应用", + "slug": "应用", + "normalizedTitle": "应用", + "charIndex": 3775 }, { - "level": 2, - "title": "简述 select, poll, epoll 的使用场景以及区别,epoll 中水平触发以及边缘触发有什么不同?", - "slug": "简述-select-poll-epoll-的使用场景以及区别-epoll-中水平触发以及边缘触发有什么不同", - "normalizedTitle": "简述 select, poll, epoll 的使用场景以及区别,epoll 中水平触发以及边缘触发有什么不同?", - "charIndex": 135 + "level": 3, + "title": "Java I/O 中的装饰者模式", + "slug": "java-i-o-中的装饰者模式", + "normalizedTitle": "java i/o 中的装饰者模式", + "charIndex": 3782 }, { - "level": 2, - "title": "Linux 下如何排查 CPU 以及 内存占用过多?", - "slug": "linux-下如何排查-cpu-以及-内存占用过多", - "normalizedTitle": "linux 下如何排查 cpu 以及 内存占用过多?", - "charIndex": 196 + "level": 3, + "title": "Servlet 中的装饰者模式", + "slug": "servlet-中的装饰者模式", + "normalizedTitle": "servlet 中的装饰者模式", + "charIndex": 4933 }, { - "level": 2, - "title": "进程通信中的管道实现原理是什么?", - "slug": "进程通信中的管道实现原理是什么", - "normalizedTitle": "进程通信中的管道实现原理是什么?", - "charIndex": 227 + "level": 3, + "title": "spring 中的装饰者模式", + "slug": "spring-中的装饰者模式", + "normalizedTitle": "spring 中的装饰者模式", + "charIndex": 5565 }, { - "level": 2, - "title": "Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程?", - "slug": "linux-下如何查看-cpu-荷载-正在运行的进程-某个端口对应的进程", - "normalizedTitle": "linux 下如何查看 cpu 荷载,正在运行的进程,某个端口对应的进程?", - "charIndex": 248 + "level": 3, + "title": "Mybatis 缓存中的装饰者模式", + "slug": "mybatis-缓存中的装饰者模式", + "normalizedTitle": "mybatis 缓存中的装饰者模式", + "charIndex": 6062 }, { "level": 2, - "title": "如何调试服务器内存占用过高的问题?", - "slug": "如何调试服务器内存占用过高的问题", - "normalizedTitle": "如何调试服务器内存占用过高的问题?", - "charIndex": 290 + "title": "总结", + "slug": "总结", + "normalizedTitle": "总结", + "charIndex": 6192 }, { "level": 2, - "title": "Linux 如何查看实时的滚动日志?", - "slug": "linux-如何查看实时的滚动日志", - "normalizedTitle": "linux 如何查看实时的滚动日志?", - "charIndex": 312 + "title": "优缺点", + "slug": "优缺点", + "normalizedTitle": "优缺点", + "charIndex": 6300 }, { "level": 2, - "title": "简述 Linux 零拷贝的原理", - "slug": "简述-linux-零拷贝的原理", - "normalizedTitle": "简述 linux 零拷贝的原理", - "charIndex": 335 + "title": "何时选用", + "slug": "何时选用", + "normalizedTitle": "何时选用", + "charIndex": 6776 + } + ], + "headersStr": "模式动机 定义 角色 类图 实例 应用 Java I/O 中的装饰者模式 Servlet 中的装饰者模式 spring 中的装饰者模式 Mybatis 缓存中的装饰者模式 总结 优缺点 何时选用", + "content": "《Head First 设计模式》中是这么形容装饰者模式的——“给爱用继承的人一个全新的设计眼界”,拒绝继承滥用,从装饰者模式开始。\n\n装饰者模式允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。\n\n这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。\n\n\n# 模式动机\n\n一般有两种方式可以实现给一个类或对象增加行为:\n\n * 继承机制,使用继承机制是给现有类添加功能的一种有效途径,通过继承一个现有类可以使得子类在拥有自身方法的同时还拥有父类的方法。但是这种方法是静态的,用户不能控制增加行为的方式和时机。\n\n * 关联机制,即将一个类的对象嵌入另一个对象中,由另一个对象来决定是否调用嵌入对象的行为以便扩展自己的行为,我们称这个嵌入的对象为装饰器(Decorator)\n\n装饰模式以对客户透明的方式动态地给一个对象附加上更多的责任,换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。\n\n\n# 定义\n\n装饰模式(Decorator Pattern) :动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类(继承)实现更为灵活。其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。\n\n\n# 角色\n\n * Component: 抽象组件,装饰者和被装饰者共同的父类,是一个接口或者抽象类,用来定义基本行为,可以给这些对象动态添加职责\n\n * ConcreteComponent: 具体的组件对象,实现类 ,即被装饰者,通常就是被装饰器装饰的原始对象,也就是可以给这个对象添加职责\n\n * Decorator: 所有装饰器的抽象父类,一般是抽象类,实现接口;它的属性必然有个指向 Conponent 抽象组件的对象 ,其实就是持有一个被装饰的对象\n\n * ConcreteDecorator: 具体的装饰对象,实现具体要被装饰对象添加的功能。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为。\n\n装饰者和被装饰者对象有相同的父类,因为装饰者和被装饰者必须是一样的类型,这里利用继承是为了达到类型匹配,而不是利用继承获得行为。\n\n利用继承设计子类,只能在编译时静态决定,并且所有子类都会继承相同的行为;利用组合的做法扩展对象,就可以在运行时动态的进行扩展。装饰者模式遵循开放-关闭原则:**类应该对扩展开放,对修改关闭。**利用装饰者,我们可以实现新的装饰者增加新的行为而不用修改现有代码,而如果单纯依赖继承,每当需要新行为时,还得修改现有的代码。\n\n\n# 类图\n\n\n\n\n# 实例\n\n看了好多资料的例子,比如\n\n * 公司发放奖金,不同的员工类型对应不同的奖金计算规则,用各种计算规则去装饰统一的奖金计算类\n * 星巴克售卖用咖啡,用摩卡、奶泡去装饰咖啡,实现不同的计费\n * 变形金刚在变形之前是一辆汽车,它可以在陆地上移动。当它变成机器人之后除了能够在陆地上移动之外,还可以说话;如果需要,它还可以变成飞机,除了在陆地上移动还可以在天空中飞翔\n\n我还是比较喜欢卖煎饼的例子\n\n\n\n1、定义抽象组件\n\npublic abstract class Pancake {\n\n String description = \"普通煎饼\";\n\n public String getDescription(){\n return description;\n }\n\n public abstract double cost();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n2、定义具体的被装饰者,这里是煎饼果子,当然还可以有鸡蛋灌饼、手抓饼等其他被装饰者\n\npublic class Battercake extends Pancake {\n @Override\n public double cost() {\n return 8;\n }\n\n public Battercake(){\n description = \"煎饼果子\";\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n3、抽象的装饰器对象,定义一个调料抽象类\n\npublic abstract class CondimentDecorator extends Pancake {\n\n // 持有组件对象\n protected Pancake pancake;\n public CondimentDecorator(Pancake pancake){\n this.pancake = pancake;\n }\n\n public abstract String getDescription();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n4、具体的装饰者,我们定义一个鸡蛋装饰器,一个火腿装饰器\n\npublic class Egg extends CondimentDecorator {\n public Egg(Pancake pancake){\n super(pancake);\n }\n\n @Override\n public String getDescription() {\n return pancake.getDescription() + \"加鸡蛋\";\n }\n\n @Override\n public double cost() {\n return pancake.cost() + 1;\n }\n}\npublic class Sausage extends CondimentDecorator{\n public Sausage(Pancake pancake){\n super(pancake);\n }\n @Override\n public String getDescription() {\n return pancake.getDescription() + \"加火腿\";\n }\n\n @Override\n public double cost() {\n return pancake.cost() + 2;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n\n\n5、测试煎饼交易,over\n\npublic class Client {\n\n public static void main(String[] args) {\n //买一个普通的煎饼果子\n Pancake battercake = new Battercake();\n System.out.println(battercake.getDescription() + \"花费:\"+battercake.cost() + \"元\");\n\n //买一个加双蛋的煎饼果子\n Pancake doubleEgg = new Battercake();\n doubleEgg = new Egg(doubleEgg);\n doubleEgg = new Egg(doubleEgg);\n System.out.println(doubleEgg.getDescription() + \"花费\" + doubleEgg.cost() + \"元\");\n\n //加火腿和鸡蛋\n Pancake battercakePlus = new Battercake();\n battercakePlus = new Egg(battercakePlus);\n battercakePlus = new Sausage(battercakePlus);\n System.out.println(battercakePlus.getDescription() + \"花费\" + battercakePlus.cost() + \"元\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\n输出:\n\n煎饼果子花费:8.0元\n煎饼果子加鸡蛋加鸡蛋花费10.0元\n煎饼果子加鸡蛋加火腿花费11.0元\n\n\n1\n2\n3\n\n\n顺便看下通过 IDEA 生成的 UML 类图(和我们画的类图一样哈)\n\n\n# 应用\n\n\n# Java I/O 中的装饰者模式\n\n我们使用 java.io 包下的各种输入流、输出流、字节流、字符流、缓冲流等各种各样的流,他们中的许多类都是装饰者,下面是一个典型的对象集合,用装饰者将功能结合起来,以读取文件数据\n\n\n\nBufferedInputStream 和 LinerNumberInputStream 都是扩展自 FilterInputStream,而 FilterInputStream 是一个抽象的装饰类。\n\n在 idea 中选中一些常见 InputStream 类,生成 UML 图如下:\n\n我们平时读取一个文件中的内容其实就使用到了装饰模式的思想,简化《Head First 设计模式》的例子,我们自定义一个装饰者,把输入流中的所有大写字符转换为小写\n\npublic class LowerCaseInputStream extends FilterInputStream {\n \n protected LowerCaseInputStream(InputStream in) {\n super(in);\n }\n\n public int read() throws IOException {\n int c = super.read();\n return (c == -1 ? c:Character.toLowerCase(c));\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\npublic class InputTest {\n\n public static void main(String[] args) throws IOException {\n int c;\n //装饰器的组装过程\n InputStream in = new LowerCaseInputStream(new BufferedInputStream(new FileInputStream(\"JavaKeeper.txt\"))); \n\n while ((c = in.read()) >= 0){\n System.out.print((char) c);\n }\n in.close();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n采用装饰者模式在实例化组件时,将增加代码的复杂度,一旦使用装饰者模式,不只需要实例化组件,还把把此组件包装进装饰者中,天晓得有几个,所以在某些复杂情况下,我们还会结合工厂模式和生成器模式。比如Spring中的装饰者模式。\n\n\n# Servlet 中的装饰者模式\n\nServlet API 源自于 4 个实现类,它很少被使用,但是十分强大:ServletRequestWrapper、ServletResponseWrapper以及 HttpServletRequestWrapper、HttpServletResponseWrapper。\n\n比如 ServletRequestWrapper 是 ServletRequest 接口的简单实现,开发者可以继承 ServletRequestWrapper 去扩展原来的 request\n\npublic class ServletRequestWrapper implements ServletRequest {\n private ServletRequest request;\n\n public ServletRequestWrapper(ServletRequest request) {\n if (request == null) {\n throw new IllegalArgumentException(\"Request cannot be null\");\n } else {\n this.request = request;\n }\n }\n\t//.......\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n\n# spring 中的装饰者模式\n\nSpring 的 ApplicationContext 中配置所有的 DataSource。 这些 DataSource 可能是各种不同类型的, 比如不同的数据库: Oracle、 SQL Server、 MySQL 等, 也可能是不同的数据源。 然后 SessionFactory 根据客户的每次请求, 将 DataSource 属性设置成不同的数据源, 以达到切换数据源的目的。\n\n在 Spring 的命名体现:Spring 中用到的包装器模式在类名上有两种表现: 一种是类名中含有 Wrapper, 另一种是类名中含有 Decorator。 基本上都是动态地给一个对象添加一些额外的职责,比如\n\norg.springframework.cache.transaction 包下的 TransactionAwareCacheDecorator 类 org.springframework.session.web.http 包下的 SessionRepositoryFilter 内部类 SessionRepositoryRequestWrapper\n\n\n# Mybatis 缓存中的装饰者模式\n\nMybatis 的缓存模块中,使用了装饰器模式的变体,其中将 Decorator 接口和 Componet 接口合并为一个 Component 接口。org.apache.ibatis.cache 包下的结构\n\n\n\n\n# 总结\n\n装饰模式的本质:动态组合\n\n动态组合是手段,组合才是目的。这里的组合有两个意思,一个是动态功能的组合,也就是动态进行装饰器的组合;另外一个是指对象组合,通过对象组合来实现为被装饰对象透明的增加功能。\n\n\n# 优缺点\n\n装饰模式的优点:\n\n * 装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。\n * 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的装饰器,从而实现不同的行为。\n * 通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更为强大的对象。\n * 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开闭原则”\n\n装饰模式的缺点:\n\n * 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度。\n * 这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。\n\n\n# 何时选用\n\n * 如果需要在不影响其他对象的情况下,以动态、透明的方式给对象添加职责,可以使用装饰模式\n * 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类已定义为不能被继承(如 Java 语言中的 final 类)\n\n参考: 装饰模式——看看 JDK 和 Spring 是如何杜绝继承滥用的", + "normalizedContent": "《head first 设计模式》中是这么形容装饰者模式的——“给爱用继承的人一个全新的设计眼界”,拒绝继承滥用,从装饰者模式开始。\n\n装饰者模式允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。\n\n这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。\n\n\n# 模式动机\n\n一般有两种方式可以实现给一个类或对象增加行为:\n\n * 继承机制,使用继承机制是给现有类添加功能的一种有效途径,通过继承一个现有类可以使得子类在拥有自身方法的同时还拥有父类的方法。但是这种方法是静态的,用户不能控制增加行为的方式和时机。\n\n * 关联机制,即将一个类的对象嵌入另一个对象中,由另一个对象来决定是否调用嵌入对象的行为以便扩展自己的行为,我们称这个嵌入的对象为装饰器(decorator)\n\n装饰模式以对客户透明的方式动态地给一个对象附加上更多的责任,换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。\n\n\n# 定义\n\n装饰模式(decorator pattern) :动态地给一个对象增加一些额外的职责(responsibility),就增加对象功能来说,装饰模式比生成子类(继承)实现更为灵活。其别名也可以称为包装器(wrapper),与适配器模式的别名相同,但它们适用于不同的场合。\n\n\n# 角色\n\n * component: 抽象组件,装饰者和被装饰者共同的父类,是一个接口或者抽象类,用来定义基本行为,可以给这些对象动态添加职责\n\n * concretecomponent: 具体的组件对象,实现类 ,即被装饰者,通常就是被装饰器装饰的原始对象,也就是可以给这个对象添加职责\n\n * decorator: 所有装饰器的抽象父类,一般是抽象类,实现接口;它的属性必然有个指向 conponent 抽象组件的对象 ,其实就是持有一个被装饰的对象\n\n * concretedecorator: 具体的装饰对象,实现具体要被装饰对象添加的功能。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为。\n\n装饰者和被装饰者对象有相同的父类,因为装饰者和被装饰者必须是一样的类型,这里利用继承是为了达到类型匹配,而不是利用继承获得行为。\n\n利用继承设计子类,只能在编译时静态决定,并且所有子类都会继承相同的行为;利用组合的做法扩展对象,就可以在运行时动态的进行扩展。装饰者模式遵循开放-关闭原则:**类应该对扩展开放,对修改关闭。**利用装饰者,我们可以实现新的装饰者增加新的行为而不用修改现有代码,而如果单纯依赖继承,每当需要新行为时,还得修改现有的代码。\n\n\n# 类图\n\n\n\n\n# 实例\n\n看了好多资料的例子,比如\n\n * 公司发放奖金,不同的员工类型对应不同的奖金计算规则,用各种计算规则去装饰统一的奖金计算类\n * 星巴克售卖用咖啡,用摩卡、奶泡去装饰咖啡,实现不同的计费\n * 变形金刚在变形之前是一辆汽车,它可以在陆地上移动。当它变成机器人之后除了能够在陆地上移动之外,还可以说话;如果需要,它还可以变成飞机,除了在陆地上移动还可以在天空中飞翔\n\n我还是比较喜欢卖煎饼的例子\n\n\n\n1、定义抽象组件\n\npublic abstract class pancake {\n\n string description = \"普通煎饼\";\n\n public string getdescription(){\n return description;\n }\n\n public abstract double cost();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n2、定义具体的被装饰者,这里是煎饼果子,当然还可以有鸡蛋灌饼、手抓饼等其他被装饰者\n\npublic class battercake extends pancake {\n @override\n public double cost() {\n return 8;\n }\n\n public battercake(){\n description = \"煎饼果子\";\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n3、抽象的装饰器对象,定义一个调料抽象类\n\npublic abstract class condimentdecorator extends pancake {\n\n // 持有组件对象\n protected pancake pancake;\n public condimentdecorator(pancake pancake){\n this.pancake = pancake;\n }\n\n public abstract string getdescription();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n4、具体的装饰者,我们定义一个鸡蛋装饰器,一个火腿装饰器\n\npublic class egg extends condimentdecorator {\n public egg(pancake pancake){\n super(pancake);\n }\n\n @override\n public string getdescription() {\n return pancake.getdescription() + \"加鸡蛋\";\n }\n\n @override\n public double cost() {\n return pancake.cost() + 1;\n }\n}\npublic class sausage extends condimentdecorator{\n public sausage(pancake pancake){\n super(pancake);\n }\n @override\n public string getdescription() {\n return pancake.getdescription() + \"加火腿\";\n }\n\n @override\n public double cost() {\n return pancake.cost() + 2;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n\n\n5、测试煎饼交易,over\n\npublic class client {\n\n public static void main(string[] args) {\n //买一个普通的煎饼果子\n pancake battercake = new battercake();\n system.out.println(battercake.getdescription() + \"花费:\"+battercake.cost() + \"元\");\n\n //买一个加双蛋的煎饼果子\n pancake doubleegg = new battercake();\n doubleegg = new egg(doubleegg);\n doubleegg = new egg(doubleegg);\n system.out.println(doubleegg.getdescription() + \"花费\" + doubleegg.cost() + \"元\");\n\n //加火腿和鸡蛋\n pancake battercakeplus = new battercake();\n battercakeplus = new egg(battercakeplus);\n battercakeplus = new sausage(battercakeplus);\n system.out.println(battercakeplus.getdescription() + \"花费\" + battercakeplus.cost() + \"元\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\n输出:\n\n煎饼果子花费:8.0元\n煎饼果子加鸡蛋加鸡蛋花费10.0元\n煎饼果子加鸡蛋加火腿花费11.0元\n\n\n1\n2\n3\n\n\n顺便看下通过 idea 生成的 uml 类图(和我们画的类图一样哈)\n\n\n# 应用\n\n\n# java i/o 中的装饰者模式\n\n我们使用 java.io 包下的各种输入流、输出流、字节流、字符流、缓冲流等各种各样的流,他们中的许多类都是装饰者,下面是一个典型的对象集合,用装饰者将功能结合起来,以读取文件数据\n\n\n\nbufferedinputstream 和 linernumberinputstream 都是扩展自 filterinputstream,而 filterinputstream 是一个抽象的装饰类。\n\n在 idea 中选中一些常见 inputstream 类,生成 uml 图如下:\n\n我们平时读取一个文件中的内容其实就使用到了装饰模式的思想,简化《head first 设计模式》的例子,我们自定义一个装饰者,把输入流中的所有大写字符转换为小写\n\npublic class lowercaseinputstream extends filterinputstream {\n \n protected lowercaseinputstream(inputstream in) {\n super(in);\n }\n\n public int read() throws ioexception {\n int c = super.read();\n return (c == -1 ? c:character.tolowercase(c));\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\npublic class inputtest {\n\n public static void main(string[] args) throws ioexception {\n int c;\n //装饰器的组装过程\n inputstream in = new lowercaseinputstream(new bufferedinputstream(new fileinputstream(\"javakeeper.txt\"))); \n\n while ((c = in.read()) >= 0){\n system.out.print((char) c);\n }\n in.close();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n采用装饰者模式在实例化组件时,将增加代码的复杂度,一旦使用装饰者模式,不只需要实例化组件,还把把此组件包装进装饰者中,天晓得有几个,所以在某些复杂情况下,我们还会结合工厂模式和生成器模式。比如spring中的装饰者模式。\n\n\n# servlet 中的装饰者模式\n\nservlet api 源自于 4 个实现类,它很少被使用,但是十分强大:servletrequestwrapper、servletresponsewrapper以及 httpservletrequestwrapper、httpservletresponsewrapper。\n\n比如 servletrequestwrapper 是 servletrequest 接口的简单实现,开发者可以继承 servletrequestwrapper 去扩展原来的 request\n\npublic class servletrequestwrapper implements servletrequest {\n private servletrequest request;\n\n public servletrequestwrapper(servletrequest request) {\n if (request == null) {\n throw new illegalargumentexception(\"request cannot be null\");\n } else {\n this.request = request;\n }\n }\n\t//.......\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n\n# spring 中的装饰者模式\n\nspring 的 applicationcontext 中配置所有的 datasource。 这些 datasource 可能是各种不同类型的, 比如不同的数据库: oracle、 sql server、 mysql 等, 也可能是不同的数据源。 然后 sessionfactory 根据客户的每次请求, 将 datasource 属性设置成不同的数据源, 以达到切换数据源的目的。\n\n在 spring 的命名体现:spring 中用到的包装器模式在类名上有两种表现: 一种是类名中含有 wrapper, 另一种是类名中含有 decorator。 基本上都是动态地给一个对象添加一些额外的职责,比如\n\norg.springframework.cache.transaction 包下的 transactionawarecachedecorator 类 org.springframework.session.web.http 包下的 sessionrepositoryfilter 内部类 sessionrepositoryrequestwrapper\n\n\n# mybatis 缓存中的装饰者模式\n\nmybatis 的缓存模块中,使用了装饰器模式的变体,其中将 decorator 接口和 componet 接口合并为一个 component 接口。org.apache.ibatis.cache 包下的结构\n\n\n\n\n# 总结\n\n装饰模式的本质:动态组合\n\n动态组合是手段,组合才是目的。这里的组合有两个意思,一个是动态功能的组合,也就是动态进行装饰器的组合;另外一个是指对象组合,通过对象组合来实现为被装饰对象透明的增加功能。\n\n\n# 优缺点\n\n装饰模式的优点:\n\n * 装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。\n * 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的装饰器,从而实现不同的行为。\n * 通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更为强大的对象。\n * 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开闭原则”\n\n装饰模式的缺点:\n\n * 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度。\n * 这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。\n\n\n# 何时选用\n\n * 如果需要在不影响其他对象的情况下,以动态、透明的方式给对象添加职责,可以使用装饰模式\n * 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类已定义为不能被继承(如 java 语言中的 final 类)\n\n参考: 装饰模式——看看 jdk 和 spring 是如何杜绝继承滥用的", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/20, 22:28:30", + "lastUpdatedTimestamp": 1663684110000 + }, + { + "title": "代理模式", + "frontmatter": { + "title": "代理模式", + "date": "2022-05-21T17:28:27.000Z", + "permalink": "/pages/dd3a6f/", + "categories": [ + "Java相关", + "设计模式" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/020.%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F.html", + "relativePath": "01.Java相关/40.设计模式/020.代理模式.md", + "key": "v-0564ba27", + "path": "/pages/dd3a6f/", + "headers": [ + { + "level": 2, + "title": "基本介绍", + "slug": "基本介绍", + "normalizedTitle": "基本介绍", + "charIndex": 2 + }, + { + "level": 2, + "title": "问题", + "slug": "问题", + "normalizedTitle": "问题", + "charIndex": 306 + }, + { + "level": 2, + "title": "解决方案", + "slug": "解决方案", + "normalizedTitle": "解决方案", + "charIndex": 505 + }, + { + "level": 2, + "title": "静态代理", + "slug": "静态代理", + "normalizedTitle": "静态代理", + "charIndex": 137 + }, + { + "level": 2, + "title": "静态代理类优缺点", + "slug": "静态代理类优缺点", + "normalizedTitle": "静态代理类优缺点", + "charIndex": 2823 + }, + { + "level": 3, + "title": "优点:", + "slug": "优点", + "normalizedTitle": "优点:", + "charIndex": 2836 + }, + { + "level": 3, + "title": "缺点:", + "slug": "缺点", + "normalizedTitle": "缺点:", + "charIndex": 2956 + }, + { + "level": 2, + "title": "动态代理", + "slug": "动态代理", + "normalizedTitle": "动态代理", + "charIndex": 199 + }, + { + "level": 3, + "title": "JDK 原生动态代理", + "slug": "jdk-原生动态代理", + "normalizedTitle": "jdk 原生动态代理", + "charIndex": 3192 + }, + { + "level": 3, + "title": "Coding", + "slug": "coding", + "normalizedTitle": "coding", + "charIndex": 3382 + }, + { + "level": 2, + "title": "cglib 代理", + "slug": "cglib-代理", + "normalizedTitle": "cglib 代理", + "charIndex": 261 + }, + { + "level": 3, + "title": "Coding", + "slug": "coding-2", + "normalizedTitle": "coding", + "charIndex": 3382 + }, + { + "level": 2, + "title": "代理模式适合应用场景", + "slug": "代理模式适合应用场景", + "normalizedTitle": "代理模式适合应用场景", + "charIndex": 7232 + }, + { + "level": 2, + "title": "AOP 中的代理模式", + "slug": "aop-中的代理模式", + "normalizedTitle": "aop 中的代理模式", + "charIndex": 7961 + } + ], + "headersStr": "基本介绍 问题 解决方案 静态代理 静态代理类优缺点 优点: 缺点: 动态代理 JDK 原生动态代理 Coding cglib 代理 Coding 代理模式适合应用场景 AOP 中的代理模式", + "content": "# 基本介绍\n\n代理模式是一种结构型设计模式。为对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象,并允许在将请求提交给对象前后进行一些处理。\n\n被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象。\n\n代理模式主要有三种不同的形式:\n\n * 静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的 .class 文件就已经存在了\n * 动态代理(JDK 代理、接口代理):在程序运行时运用反射机制动态创建而成,动态就是在程序运行时生成的,而不是编译时。\n * cglib 代理(可以在内存动态的创建对象,而不是实现接口,属于动态代理的范畴)\n\n\n# 问题\n\n为什么要控制对于某个对象的访问呢? 举个例子: 有这样一个消耗大量系统资源的巨型对象, 你只是偶尔需要使用它, 并非总是需要。\n\n\n\n你可以实现延迟初始化: 在实际有需要时再创建该对象。 对象的所有客户端都要执行延迟初始代码。 不幸的是, 这很可能会带来很多重复代码。\n\n在理想情况下, 我们希望将代码直接放入对象的类中, 但这并非总是能实现: 比如类可能是第三方封闭库的一部分。\n\n\n# 解决方案\n\n代理模式建议新建一个与原服务对象接口相同的代理类, 然后更新应用以将代理对象传递给所有原始对象客户端。 代理类接收到客户端请求后会创建实际的服务对象, 并将所有工作委派给它。\n\n\n\n代理将自己伪装成数据库对象, 可在客户端或实际数据库对象不知情的情况下处理延迟初始化和缓存查询结果的工作。\n\n这有什么好处呢? 如果需要在类的主要业务逻辑前后执行一些工作, 你无需修改类就能完成这项工作。 由于代理实现的接口与原类相同, 因此你可将其传递给任何一个使用实际服务对象的客户端。\n\n#代理模式结构\n\n1.服务接口 (Service Interface) 声明了服务接口。 代理必须遵循该接口才能伪装成服务对象。 2.服务 (Service) 类提供了一些实用的业务逻辑。 3.代理 (Proxy) 类包含一个指向服务对象的引用成员变量。 代理完成其任务 (例如延迟初始化、 记录日志、 访问控制和缓存等) 后会将请求传递给服务对象。 通常情况下, 代理会对其服务对象的整个生命周期进行管理。 4.客户端 (Client) 能通过同一接口与服务或代理进行交互, 所以你可在一切需要服务对象的代码中使用代理。 打游戏有代练、买卖房子有中介代理、再比如一般公司投互联网广告也可以找代理公司,这里的代练、中介、广告代理公司扮演的角色都是代理。\n\n这里举个更接近程序员的例子,比如有些变态的公司不允许在公司刷微博,看视频,可以通过一层代理来限制我们访问这些网站。\n\n废话不多说,先来个静态代理。\n\n\n# 静态代理\n\n1、定义网络接口\n\npublic interface Internet {\n void connectTo(String serverHost) throws Exception;\n}\n\n\n1\n2\n3\n\n\n2、真正的网络连接\n\npublic class RealInternet implements Internet{\n\n @Override\n public void connectTo(String serverHost) throws Exception {\n System.out.println(\"Connecting to \"+ serverHost);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n3、公司的网络代理\n\npublic class ProxyInternet implements Internet {\n\n //目标对象,通过接口聚合\n private Internet internet;\n\n // 通过构造方法传入目标对象\n public ProxyInternet(Internet internet){\n this.internet = internet;\n }\n //网络黑名单\n private static List bannedSites;\n\n static\n {\n bannedSites = new ArrayList();\n bannedSites.add(\"bilibili.com\");\n bannedSites.add(\"youtube.com\");\n bannedSites.add(\"weibo.com\");\n bannedSites.add(\"qq.com\");\n }\n\n @Override\n public void connectTo(String serverhost) throws Exception {\n // 添加限制功能\n if(bannedSites.contains(serverhost.toLowerCase()))\n {\n throw new Exception(\"Access Denied:\"+serverhost);\n }\n internet.connectTo(serverhost);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n\n\n4、客户端验证\n\npublic class Client {\n\n public static void main(String[] args) {\n Internet internet = new ProxyInternet(new RealInternet());\n try {\n internet.connectTo(\"so.com\");\n internet.connectTo(\"qq.com\");\n } catch (Exception e) {\n System.out.println(e.getMessage());\n }\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n5、输出\n\nConnecting to so.com\nAccess Denied:qq.com\n\n\n1\n2\n\n\n不能访问娱乐性网站,但是可以用 360 搜索,SO 靠谱,哈哈\n\n\n# 静态代理类优缺点\n\n\n# 优点:\n\n在不修改目标对象的前提下,可以通过代理对象对目标对象功能扩展\n\n代理使客户端不需要知道实现类是什么,怎么做的,而客户端只需知道代理即可(解耦合),对于如上的客户端代码,RealInterner() 可以应用工厂将它隐藏。\n\n\n# 缺点:\n\n代理类和委托类实现了相同的接口,代理类通过委托类实现了相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。\n\n代理对象只服务于一种类型的对象,如果要服务多类型的对象。势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了。\n\n\n# 动态代理\n\n静态代理会产生很多静态类,所以我们要想办法可以通过一个代理类完成全部的代理功能,这就引出了动态代理。\n\n\n# JDK 原生动态代理\n\n * 代理对象,不需要实现接口,但是目标对象要实现接口,否则不能用动态代理\n * 代理对象的生成,是通过 JDK 的 API(反射机制),动态的在内存中构建代理对象 在 Java 中要想实现动态代理机制,需要 java.lang.reflect.InvocationHandler 接口和 java.lang.reflect.Proxy 类的支持\n\n\n# Coding\n\n1、网络接口不变\n\npublic interface Internet { void connectTo(String serverHost) throws Exception; } 2、真正的网络连接,也不会改变\n\npublic class RealInternet implements Internet{\n\n@Override\npublic void connectTo(String serverHost) throws Exception {\n System.out.println(\"Connecting to \"+ serverHost);\n}\n\n\n} 3、动态代理,需要实现 InvocationHandler,我们用 Lambda 表达式简化下\n\npublic class ProxyFactory {\n\n/**\n * 维护一个目标对象\n **/\nprivate Object target;\n\n/**\n * 构造器,初始化目标对象\n **/\npublic ProxyFactory(Object target) {\n this.target = target;\n}\n\npublic Object getProxyInstance() {\n\n /**\n 被代理对象target通过参数传递进来,\n 通过target.getClass().getClassLoader()获取ClassLoader对象,\n 然后通过target.getClass().getInterfaces()获取它实现的所有接口,\n 再将target包装到实现了InvocationHandler接口的对象中。\n 通过newProxyInstance函数我们就获得了一个动态代理对象。\n */\n return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {\n @Override\n public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {\n if(bannedSites.contains(args[0].toString().toLowerCase()))\n {\n throw new Exception(\"Access Denied:\"+args[0]);\n }\n //反射机制调用目标对象的方法\n Object obj = method.invoke(target, args);\n return obj;\n }\n });\n}\n\nprivate static List bannedSites;\n\nstatic\n{\n bannedSites = new ArrayList();\n bannedSites.add(\"bilibili.com\");\n bannedSites.add(\"youtube.com\");\n bannedSites.add(\"weibo.com\");\n bannedSites.add(\"qq.com\");\n}\n\n\n} 4、客户端\n\npublic class Client {\n\npublic static void main(String[] args) {\n Internet internet = new ProxyInternet(new RealInternet());\n try {\n internet.connectTo(\"360.cn\");\n internet.connectTo(\"qq.com\");\n } catch (Exception e) {\n System.out.println(e.getMessage());\n }\n}\n\n\n} 动态代理的方式中,所有的函数调用最终都会经过 invoke 函数的转发,因此我们就可以在这里做一些自己想做的操作,比如日志系统、事务、拦截器、权限控制等。\n\n\n# cglib 代理\n\n静态代理和 JDK 代理模式都要求目标对象实现一个接口,但有时候目标对象只是一个单独的对象,并没有实现任何接口,这个时候就可以使用目标对象子类来实现代理,这就是 cglib 代理。\n\ncglib (opens new window)(Code Generation Library)是一个基于 ASM 的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。cglib 通过继承方式实现代理。它广泛的被许多 AOP 的框架使用,比如我们的 Spring AOP。\n\ncglib 包的底层是通过使用字节码处理框架 ASM 来转换字节码并生成新的类。\n\ncglib 代理也被叫做子类代理,它是在内存中构建一个子类对象从而实现目标对象功能扩展。\n\n\n# Coding\n\n添加 cglib 依赖\n\n\n cglib\n cglib\n 3.3.0\n\n\n\n1\n2\n3\n4\n5\n\n\n1、不需要接口\n\npublic class RealInternet{\n\n public void connectTo(String serverHost) {\n System.out.println(\"Connecting to \"+ serverHost);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n2、代理工厂类\n\npublic class ProxyFactory implements MethodInterceptor {\n\n private Object target;\n\n public ProxyFactory(Object target){\n this.target = target;\n }\n\n @Override\n public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {\n System.out.println(\"cglib 代理开始,可以添加逻辑\");\n Object obj = method.invoke(target,objects);\n System.out.println(\"cglib 代理结束\");\n return obj;\n }\n\n\n public Object getProxyInstance(){\n //工具类,类似于JDK动态代理的Proxy类\n Enhancer enhancer = new Enhancer();\n //设置父类\n enhancer.setSuperclass(target.getClass());\n //设置回调函数\n enhancer.setCallback(this);\n //创建子类对象,即代理对象\n return enhancer.create();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n\n\n3、客户端\n\npublic class Client {\n\n public static void main(String[] args) {\n\n //目标对象\n RealInternet target = new RealInternet();\n //获取代理对象,并且将目标对象传递给代理对象\n RealInternet internet = (RealInternet) new ProxyFactory(target).getProxyInstance();\n internet.connectTo(\"so.cn\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n4、输出\n\ncglib 代理开始,可以添加逻辑\nConnecting to so.cn\ncglib 代理结束\n\n\n1\n2\n3\n\n\n\n# 代理模式适合应用场景\n\n使用代理模式的方式多种多样, 我们来看看最常见的几种。\n\n * 延迟初始化 (虚拟代理):如果你有一个偶尔使用的重量级服务对象, 一直保持该对象运行会消耗系统资源时, 可使用代理模式。\n\n你无需在程序启动时就创建该对象, 可将对象的初始化延迟到真正有需要的时候。\n\n * 访问控制 (保护代理):如果你只希望特定客户端使用服务对象, 这里的对象可以是操作系统中非常重要的部分, 而客户端则是各种已启动的程序 (包括恶意程序), 此时可使用代理模式。\n\n代理可仅在客户端凭据满足要求时将请求传递给服务对象。\n\n * 本地执行远程服务 (远程代理):适用于服务对象位于远程服务器上的情形。\n\n在这种情形中, 代理通过网络传递客户端请求, 负责处理所有与网络相关的复杂细节。\n\n * 记录日志请求 (日志记录代理):适用于当你需要保存对于服务对象的请求历史记录时。 代理可以在向服务传递请求前进行记录。\n\n * 缓存请求结果 (缓存代理):适用于需要缓存客户请求结果并对缓存生命周期进行管理时, 特别是当返回结果的体积非常大时。\n\n代理可对重复请求所需的相同结果进行缓存, 还可使用请求参数作为索引缓存的键值。比如请求图片、文件等资源时,先到代理缓存取,如果没有就去公网取并缓存到代理服务器\n\n * 智能引用:可在没有客户端使用某个重量级对象时立即销毁该对象。\n\n代理会将所有获取了指向服务对象或其结果的客户端记录在案。 代理会时不时地遍历各个客户端, 检查它们是否仍在运行。 如果相应的客户端列表为空, 代理就会销毁该服务对象, 释放底层系统资源。\n\n代理还可以记录客户端是否修改了服务对象。 其他客户端还可以复用未修改的对象。\n\n\n# AOP 中的代理模式\n\nAOP(面向切面编程)主要的的实现技术主要有 Spring AOP 和 AspectJ\n\nAspectJ 的底层技术就是静态代理,用一种 AspectJ 支持的特定语言编写切面,通过一个命令来编译,生成一个新的代理类,该代理类增强了业务类,这是在编译时增强,相对于下面说的运行时增强,编译时增强的性能更好。(AspectJ 的静态代理,不像我们前边介绍的需要为每一个目标类手动编写一个代理类,AspectJ 框架可以在编译时就生成目标类的“代理类”,在这里加了个冒号,是因为实际上它并没有生成一个新的类,而是把代理逻辑直接编译到目标类里面了)\n\nSpring AOP 采用的是动态代理,在运行期间对业务方法进行增强,所以不会生成新类,对于动态代理技术,Spring AOP 提供了对 JDK 动态代理的支持以及 CGLib 的支持。\n\n默认情况下,Spring 对实现了接口的类使用 JDK Proxy 方式,否则的话使用 CGLib。不过可以通过配置指定 Spring AOP 都通过 CGLib 来生成代理类。\n\n具体逻辑在 org.springframework.aop.framework.DefaultAopProxyFactory 类中,使用哪种方式生成由 AopProxy 根据 AdvisedSupport 对象的配置来决定源码如下:\n\npublic class DefaultAopProxyFactory implements AopProxyFactory, Serializable {\n public DefaultAopProxyFactory() {\n }\n\n public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {\n if (!config.isOptimize() && !config.isProxyTargetClass() && !this.hasNoUserSuppliedProxyInterfaces(config)) {\n return new JdkDynamicAopProxy(config);\n } else {\n Class targetClass = config.getTargetClass();\n if (targetClass == null) {\n throw new AopConfigException(\"TargetSource cannot determine target class: Either an interface or a target is required for proxy creation.\");\n } else {\n //如果目标类是接口且是代理类, 使用JDK动态代理类,否则使用Cglib生成代理类\n return (AopProxy)(!targetClass.isInterface() && !Proxy.isProxyClass(targetClass) ? new ObjenesisCglibAopProxy(config) : new JdkDynamicAopProxy(config));\n }\n }\n }\n\n private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n\n\n参考: 代理模式", + "normalizedContent": "# 基本介绍\n\n代理模式是一种结构型设计模式。为对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象,并允许在将请求提交给对象前后进行一些处理。\n\n被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象。\n\n代理模式主要有三种不同的形式:\n\n * 静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的 .class 文件就已经存在了\n * 动态代理(jdk 代理、接口代理):在程序运行时运用反射机制动态创建而成,动态就是在程序运行时生成的,而不是编译时。\n * cglib 代理(可以在内存动态的创建对象,而不是实现接口,属于动态代理的范畴)\n\n\n# 问题\n\n为什么要控制对于某个对象的访问呢? 举个例子: 有这样一个消耗大量系统资源的巨型对象, 你只是偶尔需要使用它, 并非总是需要。\n\n\n\n你可以实现延迟初始化: 在实际有需要时再创建该对象。 对象的所有客户端都要执行延迟初始代码。 不幸的是, 这很可能会带来很多重复代码。\n\n在理想情况下, 我们希望将代码直接放入对象的类中, 但这并非总是能实现: 比如类可能是第三方封闭库的一部分。\n\n\n# 解决方案\n\n代理模式建议新建一个与原服务对象接口相同的代理类, 然后更新应用以将代理对象传递给所有原始对象客户端。 代理类接收到客户端请求后会创建实际的服务对象, 并将所有工作委派给它。\n\n\n\n代理将自己伪装成数据库对象, 可在客户端或实际数据库对象不知情的情况下处理延迟初始化和缓存查询结果的工作。\n\n这有什么好处呢? 如果需要在类的主要业务逻辑前后执行一些工作, 你无需修改类就能完成这项工作。 由于代理实现的接口与原类相同, 因此你可将其传递给任何一个使用实际服务对象的客户端。\n\n#代理模式结构\n\n1.服务接口 (service interface) 声明了服务接口。 代理必须遵循该接口才能伪装成服务对象。 2.服务 (service) 类提供了一些实用的业务逻辑。 3.代理 (proxy) 类包含一个指向服务对象的引用成员变量。 代理完成其任务 (例如延迟初始化、 记录日志、 访问控制和缓存等) 后会将请求传递给服务对象。 通常情况下, 代理会对其服务对象的整个生命周期进行管理。 4.客户端 (client) 能通过同一接口与服务或代理进行交互, 所以你可在一切需要服务对象的代码中使用代理。 打游戏有代练、买卖房子有中介代理、再比如一般公司投互联网广告也可以找代理公司,这里的代练、中介、广告代理公司扮演的角色都是代理。\n\n这里举个更接近程序员的例子,比如有些变态的公司不允许在公司刷微博,看视频,可以通过一层代理来限制我们访问这些网站。\n\n废话不多说,先来个静态代理。\n\n\n# 静态代理\n\n1、定义网络接口\n\npublic interface internet {\n void connectto(string serverhost) throws exception;\n}\n\n\n1\n2\n3\n\n\n2、真正的网络连接\n\npublic class realinternet implements internet{\n\n @override\n public void connectto(string serverhost) throws exception {\n system.out.println(\"connecting to \"+ serverhost);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n3、公司的网络代理\n\npublic class proxyinternet implements internet {\n\n //目标对象,通过接口聚合\n private internet internet;\n\n // 通过构造方法传入目标对象\n public proxyinternet(internet internet){\n this.internet = internet;\n }\n //网络黑名单\n private static list bannedsites;\n\n static\n {\n bannedsites = new arraylist();\n bannedsites.add(\"bilibili.com\");\n bannedsites.add(\"youtube.com\");\n bannedsites.add(\"weibo.com\");\n bannedsites.add(\"qq.com\");\n }\n\n @override\n public void connectto(string serverhost) throws exception {\n // 添加限制功能\n if(bannedsites.contains(serverhost.tolowercase()))\n {\n throw new exception(\"access denied:\"+serverhost);\n }\n internet.connectto(serverhost);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n\n\n4、客户端验证\n\npublic class client {\n\n public static void main(string[] args) {\n internet internet = new proxyinternet(new realinternet());\n try {\n internet.connectto(\"so.com\");\n internet.connectto(\"qq.com\");\n } catch (exception e) {\n system.out.println(e.getmessage());\n }\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n5、输出\n\nconnecting to so.com\naccess denied:qq.com\n\n\n1\n2\n\n\n不能访问娱乐性网站,但是可以用 360 搜索,so 靠谱,哈哈\n\n\n# 静态代理类优缺点\n\n\n# 优点:\n\n在不修改目标对象的前提下,可以通过代理对象对目标对象功能扩展\n\n代理使客户端不需要知道实现类是什么,怎么做的,而客户端只需知道代理即可(解耦合),对于如上的客户端代码,realinterner() 可以应用工厂将它隐藏。\n\n\n# 缺点:\n\n代理类和委托类实现了相同的接口,代理类通过委托类实现了相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。\n\n代理对象只服务于一种类型的对象,如果要服务多类型的对象。势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了。\n\n\n# 动态代理\n\n静态代理会产生很多静态类,所以我们要想办法可以通过一个代理类完成全部的代理功能,这就引出了动态代理。\n\n\n# jdk 原生动态代理\n\n * 代理对象,不需要实现接口,但是目标对象要实现接口,否则不能用动态代理\n * 代理对象的生成,是通过 jdk 的 api(反射机制),动态的在内存中构建代理对象 在 java 中要想实现动态代理机制,需要 java.lang.reflect.invocationhandler 接口和 java.lang.reflect.proxy 类的支持\n\n\n# coding\n\n1、网络接口不变\n\npublic interface internet { void connectto(string serverhost) throws exception; } 2、真正的网络连接,也不会改变\n\npublic class realinternet implements internet{\n\n@override\npublic void connectto(string serverhost) throws exception {\n system.out.println(\"connecting to \"+ serverhost);\n}\n\n\n} 3、动态代理,需要实现 invocationhandler,我们用 lambda 表达式简化下\n\npublic class proxyfactory {\n\n/**\n * 维护一个目标对象\n **/\nprivate object target;\n\n/**\n * 构造器,初始化目标对象\n **/\npublic proxyfactory(object target) {\n this.target = target;\n}\n\npublic object getproxyinstance() {\n\n /**\n 被代理对象target通过参数传递进来,\n 通过target.getclass().getclassloader()获取classloader对象,\n 然后通过target.getclass().getinterfaces()获取它实现的所有接口,\n 再将target包装到实现了invocationhandler接口的对象中。\n 通过newproxyinstance函数我们就获得了一个动态代理对象。\n */\n return proxy.newproxyinstance(target.getclass().getclassloader(), target.getclass().getinterfaces(), new invocationhandler() {\n @override\n public object invoke(object proxy, method method, object[] args) throws throwable {\n if(bannedsites.contains(args[0].tostring().tolowercase()))\n {\n throw new exception(\"access denied:\"+args[0]);\n }\n //反射机制调用目标对象的方法\n object obj = method.invoke(target, args);\n return obj;\n }\n });\n}\n\nprivate static list bannedsites;\n\nstatic\n{\n bannedsites = new arraylist();\n bannedsites.add(\"bilibili.com\");\n bannedsites.add(\"youtube.com\");\n bannedsites.add(\"weibo.com\");\n bannedsites.add(\"qq.com\");\n}\n\n\n} 4、客户端\n\npublic class client {\n\npublic static void main(string[] args) {\n internet internet = new proxyinternet(new realinternet());\n try {\n internet.connectto(\"360.cn\");\n internet.connectto(\"qq.com\");\n } catch (exception e) {\n system.out.println(e.getmessage());\n }\n}\n\n\n} 动态代理的方式中,所有的函数调用最终都会经过 invoke 函数的转发,因此我们就可以在这里做一些自己想做的操作,比如日志系统、事务、拦截器、权限控制等。\n\n\n# cglib 代理\n\n静态代理和 jdk 代理模式都要求目标对象实现一个接口,但有时候目标对象只是一个单独的对象,并没有实现任何接口,这个时候就可以使用目标对象子类来实现代理,这就是 cglib 代理。\n\ncglib (opens new window)(code generation library)是一个基于 asm 的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。cglib 通过继承方式实现代理。它广泛的被许多 aop 的框架使用,比如我们的 spring aop。\n\ncglib 包的底层是通过使用字节码处理框架 asm 来转换字节码并生成新的类。\n\ncglib 代理也被叫做子类代理,它是在内存中构建一个子类对象从而实现目标对象功能扩展。\n\n\n# coding\n\n添加 cglib 依赖\n\n\n cglib\n cglib\n 3.3.0\n\n\n\n1\n2\n3\n4\n5\n\n\n1、不需要接口\n\npublic class realinternet{\n\n public void connectto(string serverhost) {\n system.out.println(\"connecting to \"+ serverhost);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n2、代理工厂类\n\npublic class proxyfactory implements methodinterceptor {\n\n private object target;\n\n public proxyfactory(object target){\n this.target = target;\n }\n\n @override\n public object intercept(object o, method method, object[] objects, methodproxy methodproxy) throws throwable {\n system.out.println(\"cglib 代理开始,可以添加逻辑\");\n object obj = method.invoke(target,objects);\n system.out.println(\"cglib 代理结束\");\n return obj;\n }\n\n\n public object getproxyinstance(){\n //工具类,类似于jdk动态代理的proxy类\n enhancer enhancer = new enhancer();\n //设置父类\n enhancer.setsuperclass(target.getclass());\n //设置回调函数\n enhancer.setcallback(this);\n //创建子类对象,即代理对象\n return enhancer.create();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n\n\n3、客户端\n\npublic class client {\n\n public static void main(string[] args) {\n\n //目标对象\n realinternet target = new realinternet();\n //获取代理对象,并且将目标对象传递给代理对象\n realinternet internet = (realinternet) new proxyfactory(target).getproxyinstance();\n internet.connectto(\"so.cn\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n4、输出\n\ncglib 代理开始,可以添加逻辑\nconnecting to so.cn\ncglib 代理结束\n\n\n1\n2\n3\n\n\n\n# 代理模式适合应用场景\n\n使用代理模式的方式多种多样, 我们来看看最常见的几种。\n\n * 延迟初始化 (虚拟代理):如果你有一个偶尔使用的重量级服务对象, 一直保持该对象运行会消耗系统资源时, 可使用代理模式。\n\n你无需在程序启动时就创建该对象, 可将对象的初始化延迟到真正有需要的时候。\n\n * 访问控制 (保护代理):如果你只希望特定客户端使用服务对象, 这里的对象可以是操作系统中非常重要的部分, 而客户端则是各种已启动的程序 (包括恶意程序), 此时可使用代理模式。\n\n代理可仅在客户端凭据满足要求时将请求传递给服务对象。\n\n * 本地执行远程服务 (远程代理):适用于服务对象位于远程服务器上的情形。\n\n在这种情形中, 代理通过网络传递客户端请求, 负责处理所有与网络相关的复杂细节。\n\n * 记录日志请求 (日志记录代理):适用于当你需要保存对于服务对象的请求历史记录时。 代理可以在向服务传递请求前进行记录。\n\n * 缓存请求结果 (缓存代理):适用于需要缓存客户请求结果并对缓存生命周期进行管理时, 特别是当返回结果的体积非常大时。\n\n代理可对重复请求所需的相同结果进行缓存, 还可使用请求参数作为索引缓存的键值。比如请求图片、文件等资源时,先到代理缓存取,如果没有就去公网取并缓存到代理服务器\n\n * 智能引用:可在没有客户端使用某个重量级对象时立即销毁该对象。\n\n代理会将所有获取了指向服务对象或其结果的客户端记录在案。 代理会时不时地遍历各个客户端, 检查它们是否仍在运行。 如果相应的客户端列表为空, 代理就会销毁该服务对象, 释放底层系统资源。\n\n代理还可以记录客户端是否修改了服务对象。 其他客户端还可以复用未修改的对象。\n\n\n# aop 中的代理模式\n\naop(面向切面编程)主要的的实现技术主要有 spring aop 和 aspectj\n\naspectj 的底层技术就是静态代理,用一种 aspectj 支持的特定语言编写切面,通过一个命令来编译,生成一个新的代理类,该代理类增强了业务类,这是在编译时增强,相对于下面说的运行时增强,编译时增强的性能更好。(aspectj 的静态代理,不像我们前边介绍的需要为每一个目标类手动编写一个代理类,aspectj 框架可以在编译时就生成目标类的“代理类”,在这里加了个冒号,是因为实际上它并没有生成一个新的类,而是把代理逻辑直接编译到目标类里面了)\n\nspring aop 采用的是动态代理,在运行期间对业务方法进行增强,所以不会生成新类,对于动态代理技术,spring aop 提供了对 jdk 动态代理的支持以及 cglib 的支持。\n\n默认情况下,spring 对实现了接口的类使用 jdk proxy 方式,否则的话使用 cglib。不过可以通过配置指定 spring aop 都通过 cglib 来生成代理类。\n\n具体逻辑在 org.springframework.aop.framework.defaultaopproxyfactory 类中,使用哪种方式生成由 aopproxy 根据 advisedsupport 对象的配置来决定源码如下:\n\npublic class defaultaopproxyfactory implements aopproxyfactory, serializable {\n public defaultaopproxyfactory() {\n }\n\n public aopproxy createaopproxy(advisedsupport config) throws aopconfigexception {\n if (!config.isoptimize() && !config.isproxytargetclass() && !this.hasnousersuppliedproxyinterfaces(config)) {\n return new jdkdynamicaopproxy(config);\n } else {\n class targetclass = config.gettargetclass();\n if (targetclass == null) {\n throw new aopconfigexception(\"targetsource cannot determine target class: either an interface or a target is required for proxy creation.\");\n } else {\n //如果目标类是接口且是代理类, 使用jdk动态代理类,否则使用cglib生成代理类\n return (aopproxy)(!targetclass.isinterface() && !proxy.isproxyclass(targetclass) ? new objenesiscglibaopproxy(config) : new jdkdynamicaopproxy(config));\n }\n }\n }\n\n private boolean hasnousersuppliedproxyinterfaces(advisedsupport config) {\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n\n\n参考: 代理模式", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "适配器模式", + "frontmatter": { + "title": "适配器模式", + "date": "2022-05-21T17:28:10.000Z", + "permalink": "/pages/827fc4/", + "categories": [ + "Java相关", + "设计模式" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/010.%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8F.html", + "relativePath": "01.Java相关/40.设计模式/010.适配器模式.md", + "key": "v-31509c80", + "path": "/pages/827fc4/", + "headers": [ + { + "level": 2, + "title": "问题", + "slug": "问题", + "normalizedTitle": "问题", + "charIndex": 2 + }, + { + "level": 2, + "title": "真实世界类比", + "slug": "真实世界类比", + "normalizedTitle": "真实世界类比", + "charIndex": 233 + }, + { + "level": 2, + "title": "基本介绍", + "slug": "基本介绍", + "normalizedTitle": "基本介绍", + "charIndex": 312 + }, + { + "level": 2, + "title": "工作原理", + "slug": "工作原理", + "normalizedTitle": "工作原理", + "charIndex": 455 + }, + { + "level": 2, + "title": "适配器模式结构", + "slug": "适配器模式结构", + "normalizedTitle": "适配器模式结构", + "charIndex": 591 + }, + { + "level": 3, + "title": "对象适配器", + "slug": "对象适配器", + "normalizedTitle": "对象适配器", + "charIndex": 443 + }, + { + "level": 3, + "title": "Coding", + "slug": "coding", + "normalizedTitle": "coding", + "charIndex": 1064 + }, + { + "level": 2, + "title": "类适配器", + "slug": "类适配器", + "normalizedTitle": "类适配器", + "charIndex": 436 + }, + { + "level": 2, + "title": "Coding", + "slug": "coding-2", + "normalizedTitle": "coding", + "charIndex": 1064 + }, + { + "level": 2, + "title": "适配器模式适合应用场景", + "slug": "适配器模式适合应用场景", + "normalizedTitle": "适配器模式适合应用场景", + "charIndex": 3145 + }, + { + "level": 2, + "title": "demo", + "slug": "demo", + "normalizedTitle": "demo", + "charIndex": 3518 + }, + { + "level": 2, + "title": "适配器模式优缺点", + "slug": "适配器模式优缺点", + "normalizedTitle": "适配器模式优缺点", + "charIndex": 5027 + }, + { + "level": 2, + "title": "Spring 中的适配器", + "slug": "spring-中的适配器", + "normalizedTitle": "spring 中的适配器", + "charIndex": 5191 + } + ], + "headersStr": "问题 真实世界类比 基本介绍 工作原理 适配器模式结构 对象适配器 Coding 类适配器 Coding 适配器模式适合应用场景 demo 适配器模式优缺点 Spring 中的适配器", + "content": "# 问题\n\n假设我们在做一套股票看盘系统,数据提供方给我们提供 XML 格式数据,我们获取数据用来显示,随着系统的迭代,我们要整合一些第三方系统的对外数据,但是他们只提供获取 JSON 格式的数据接口。\n\n在不想改变原有代码逻辑的情况下,如何解决呢?\n\n这时候我们就可以创建一个「适配器」。这是一个特殊的对象, 能够转换对象接口, 使其能与其他对象进行交互。\n\n适配器模式通过封装对象将复杂的转换过程隐藏于幕后。 被封装的对象甚至察觉不到适配器的存在。\n\n\n\n\n# 真实世界类比\n\n适配器是什么,不难理解,生活中也随处可见。比如,笔记本电脑的电源适配器、万能充(曾经的它真有一个这么牛逼的名字)、一拖十数据线等等。\n\n\n# 基本介绍\n\n * 适配器模式将一个类的接口,转换成客户期望的另外一个接口。适配器让原本接口不兼容的类可以合作无间。也可以叫包装器(Wrapper)\n\n * 适配器模式是一种结构型设计模式, 它能使接口不兼容的对象能够相互合作\n\n * 主要分为两类:类适配器模式、对象适配器模式\n\n\n# 工作原理\n\n * 适配器模式:将一个类的接口转换成另一种接口,让原本接口不兼容的类可以兼容\n * 从用户的角度看不到被适配者,是解耦的\n * 用户调用适配器转化出来的目标接口方法,适配器再调用被适配者的相关接口方法\n * 用户收到反馈结果,感觉只是和目标接口交互\n\n\n# 适配器模式结构\n\n\n# 对象适配器\n\n实现时使用了构成原则: 适配器实现了其中一个对象的接口, 并对另一个对象进行封装。 所有流行的编程语言都可以实现适配器。\n\n适配器设计模式的结构(对象适配器)\n\n * 客户端 (Client) 是包含当前程序业务逻辑的类。\n * 客户端接口 (Target) 描述了其他类与客户端代码合作时必须遵循的协议。\n * 服务 (Service) 中有一些功能类 (通常来自第三方或遗留系统)。 客户端与其接口不兼容, 因此无法直接调用其功能,也可以叫适配者类(Adaptee)。\n * 适配器 (Adapter) 是一个可以同时与客户端和服务交互的类: 它在实现客户端接口的同时封装了服务对象。 适配器接受客户端通过适配器接口发起的调用, 并将其转换为适用于被封装服务对象的调用。\n * 客户端代码只需通过接口与适配器交互即可, 无需与具体的适配器类耦合。 因此, 你可以向程序中添加新类型的适配器而无需修改已有代码。 这在服务类的接口被更改或替换时很有用: 你无需修改客户端代码就可以创建新的适配器类。\n\n\n# Coding\n\n定义客户端使用的接口,与业务相关\n\npublic interface Target {\n\n /*\n * 客户端请求处理的方法\n */\n void request();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n已经存在的接口,这个接口需要配置\n\npublic class Adaptee {\n\n /*\n * 原本存在的方法\n */\n public void specificRequest(){\n //业务代码\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n适配器类\n\npublic class Adapter implements Target {\n\n /*\n * 持有需要被适配的接口对象\n */\n private Adaptee adaptee;\n\n /*\n * 构造方法,传入需要被适配的对象\n * @param adaptee 需要被适配的对象\n */\n public Adapter(Adaptee adaptee) {\n this.adaptee = adaptee;\n }\n\n @Override\n public void request() {\n // TODO Auto-generated method stub\n adaptee.specificRequest();\n }\n\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n\n\n使用适配器的客户端\n\npublic class Client {\n\n public static void main(String[] args) {\n //创建需要被适配的对象\n Adaptee adaptee = new Adaptee();\n //创建客户端需要调用的接口对象\n Target target = new Adapter(adaptee);\n //请求处理\n target.request();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n\n# 类适配器\n\n这一实现使用了继承机制: 适配器同时继承两个对象的接口。 请注意, 这种方式仅能在支持多重继承的编程语言中实现,例如 C++, Java 不支持多重继承,也就没有这种适配器了。\n\n适配器设计模式(类适配器)\n\n类适配器不需要封装任何对象, 因为它同时继承了客户端和服务的行为。 适配功能在重写的方法中完成。 最后生成的适配器可替代已有的客户端类进行使用。\n\n\n# Coding\n\nJava 虽然不能实现标准的类适配器,但是有一种变通的方式,也能够使用继承来实现接口的适配,那就是让适配器去实现 Target 的接口,然后继承 Adaptee 的实现,虽然不是十分标准,但意思差不多。\n\n首先有一个已存在的将被适配的类\n\npublic class Adaptee {\n public void adapteeRequest() {\n System.out.println(\"被适配者的方法\");\n }\n}\n\n\n1\n2\n3\n4\n5\n\n\n定义客户端使用的接口,与业务相关\n\npublic interface Target {\n\n void request();\n}\n\n\n1\n2\n3\n4\n\n\n怎么才可以在目标接口中的 request() 调用 Adaptee 的 adapteeRequest() 方法呢?直接实现 Target 肯定是不行的,所以我们通过一个适配器类,实现 Target 接口,同时继承了 Adaptee 类,然后在实现的 request() 方法中调用父类的 adapteeRequest() 即可\n\npublic class Adapter extends Adaptee implements Target{\n @Override\n public void request() {\n //...一些操作...\n super.adapteeRequest();\n //...一些操作...\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\n使用适配器的客户端\n\npublic class Client {\n public static void main(String[] args) {\n\n Target adapterTarget = new Adapter();\n adapterTarget.request();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 适配器模式适合应用场景\n\n当你希望使用某个类, 但是其接口与其他代码不兼容时, 可以使用适配器类。\n\n适配器模式允许你创建一个中间层类, 其可作为代码与遗留类、 第三方类或提供怪异接口的类之间的转换器。\n\n如果您需要复用这样一些类, 他们处于同一个继承体系, 并且他们又有了额外的一些共同的方法, 但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。\n\n你可以扩展每个子类, 将缺少的功能添加到新的子类中。 但是, 你必须在所有新子类中重复添加这些代码, 这样会使得代码有坏味道。\n\n将缺失功能添加到一个适配器类中是一种优雅得多的解决方案。 然后你可以将缺少功能的对象封装在适配器中, 从而动态地获取所需功能。 如要这一点正常运作, 目标类必须要有通用接口, 适配器的成员变量应当遵循该通用接口。 这种方式同装饰模式非常相似。\n\n\n# demo\n\n用一个生活中的充电器的例子来讲解下适配器,我国民用电都是 220V,而我们的手机充电一般需要 5V。\n\n220V 的交流电相当于被适配者 Adaptee,我们的目标 Target 是 5V 直流电,充电器本身相当于一个 Adapter,将220V 的输入电压变换为 5V 输出。\n\n首先是我们的民用电(我国是 220V,当然还可以有其他国家的其他准备,可随时扩展)\n\npublic class Volatage220V {\n\n public final int output = 220;\n\n public int output220v() {\n System.out.println(\"输出电压 \" + output);\n return output;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n适配接口\n\npublic interface IVoltage5V {\n int output5V();\n}\n\n\n1\n2\n3\n\n\n我们的手机充电,只支持 5V 电压\n\npublic class Phone {\n\n public void charging(IVoltage5V v) {\n if (v.output5V() == 5) {\n System.out.println(\"电压 5V ,符合充电标准,开始充电\");\n } else {\n System.out.println(\"电压不符合标准,无法充电\");\n }\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n适配器\n\npublic class VoltageAdapter implements IVoltage5V {\n\n private Volatage220V volatage220V; //聚合\n\n public VoltageAdapter(Volatage220V v) {\n this.volatage220V = v;\n }\n\n @Override\n public int output5V() {\n int dst = 0;\n if (null != volatage220V) {\n int src = volatage220V.output220v();\n System.out.println(\"适配器工作~~~~~\");\n dst = src / 44;\n System.out.println(\"适配器工作完成,输出电压\" + dst);\n }\n return dst;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\n工作,如果去国外旅游,有不同的电压,只需要扩展适配器即可。\n\npublic class Client {\n public static void main(String[] args) {\n Phone phone = new Phone();\n phone.charging(new VoltageAdapter(new Volatage220V()));\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n\n# 适配器模式优缺点\n\n单一职责原则,你可以将接口或数据转换代码从程序主要业务逻辑中分离。\n\n开闭原则。 只要客户端代码通过客户端接口与适配器进行交互, 你就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。\n\n代码整体复杂度增加, 因为你需要新增一系列接口和类。 有时直接更改服务类使其与其他代码兼容会更简单。\n\n\n# Spring 中的适配器\n\nSpring 源码中搜关键字Adapter 会出现很多实现类,SpringMVC 中的 HandlerAdapter ,就是适配器的应用。\n\n我们先回顾下 SpringMVC 处理流程:\n\nSpring MVC 中的适配器模式主要用于执行目标 Controller 中的请求处理方法。\n\n在Spring MVC中,DispatcherServlet 作为用户,HandlerAdapter 作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller 作为需要适配的类。\n\n为什么要在 Spring MVC 中使用适配器模式?Spring MVC 中的 Controller 种类众多,不同类型的 Controller 通过不同的方法来对请求进行处理。如果不利用适配器模式的话,DispatcherServlet 直接获取对应类型的 Controller,需要的自行来判断,像下面这段代码一样:\n\nif(mappedHandler.getHandler() instanceof MultiActionController){ \n ((MultiActionController)mappedHandler.getHandler()).xxx \n}else if(mappedHandler.getHandler() instanceof XXX){ \n ... \n}else if(...){ \n ... \n} \n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n这样假设如果我们增加一个 Controller,就要在代码中加入一行 if 语句,这种形式就使得程序难以维护,也违反了设计模式中的开闭原则 – 对扩展开放,对修改关闭。\n\n我们通过源码看看 SpringMVC 是如何实现的,首先看下核心类 DispatcherServlet:\n\npublic class DispatcherServlet extends FrameworkServlet {\n \t//......\n\t//维护所有HandlerAdapter类的集合\n @Nullable\n private List handlerAdapters;\n \n\t//初始化handlerAdapters\n private void initHandlerAdapters(ApplicationContext context) {\n this.handlerAdapters = null;\n if (this.detectAllHandlerAdapters) {\n Map matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);\n if (!matchingBeans.isEmpty()) {\n this.handlerAdapters = new ArrayList(matchingBeans.values());\n AnnotationAwareOrderComparator.sort(this.handlerAdapters);\n }\n } else {\n try {\n HandlerAdapter ha = (HandlerAdapter)context.getBean(\"handlerAdapter\", HandlerAdapter.class);\n this.handlerAdapters = Collections.singletonList(ha);\n } catch (NoSuchBeanDefinitionException var3) {\n }\n }\n\n if (this.handlerAdapters == null) {\n this.handlerAdapters = this.getDefaultStrategies(context, HandlerAdapter.class);\n if (this.logger.isTraceEnabled()) {\n this.logger.trace(\"No HandlerAdapters declared for servlet '\" + this.getServletName() + \"': using default strategies from DispatcherServlet.properties\");\n }\n }\n }\n \n //dispatch 方法中会获取 HandlerAdapter\n protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {\n //...\n \n\t\t//获得controller对应的适配器\n HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler()); \n\t\t\t\t\n\t\t//调用适配器的handler方法处理请求,并返回ModelAndView\n mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); \n //...\n }\n \t\n\t //返回对应的controller的处理器\n protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {\n if (this.handlerAdapters != null) {\n Iterator var2 = this.handlerAdapters.iterator();\n\n while(var2.hasNext()) {\n HandlerAdapter adapter = (HandlerAdapter)var2.next();\n if (adapter.supports(handler)) {\n return adapter;\n }\n }\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n\n\n接着看下 HandlerAdapter 的源码,也就是适配器接口:\n\npublic interface HandlerAdapter {\n boolean supports(Object var1);\n\n @Nullable\n ModelAndView handle(HttpServletRequest var1, HttpServletResponse var2, Object var3) throws Exception;\n\n long getLastModified(HttpServletRequest var1, Object var2);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\n再来屡一下这个流程:\n\n首先是适配器接口 DispatchServlet 中有一个集合维护所有的 HandlerAdapter,如果配置文件中没有对适配器进行配置,那么 DispatchServlet 会在创建时对该变量进行初始化,注册所有默认的 HandlerAdapter。\n\n当一个请求过来时,DispatchServlet 会根据传过来的 handler 类型从该集合中寻找对应的 HandlerAdapter子类进行处理,并且调用它的 handler() 方法 对应的 HandlerAdapter 中的 handler() 方法又会执行对应 Controller 的 handleRequest() 方法\n\n适配器与 handler 有对应关系,而各个适配器又都是适配器接口的实现类,因此,它们都遵循相同的适配器标准,所以用户可以按照相同的方式,通过不同的 handler 去处理请求。\n\n当然了,Spring 框架中也为我们定义了一些默认的 Handler 对应的适配器。\n\n通过适配器模式我们将所有的 controller 统一交给 HandlerAdapter 处理,免去了写大量的 if-else 语句对 Controller 进行判断,也更利于扩展新的 Controller 类型。\n\n参考: 随遇而安的适配器模式 | Spring 中的适配器", + "normalizedContent": "# 问题\n\n假设我们在做一套股票看盘系统,数据提供方给我们提供 xml 格式数据,我们获取数据用来显示,随着系统的迭代,我们要整合一些第三方系统的对外数据,但是他们只提供获取 json 格式的数据接口。\n\n在不想改变原有代码逻辑的情况下,如何解决呢?\n\n这时候我们就可以创建一个「适配器」。这是一个特殊的对象, 能够转换对象接口, 使其能与其他对象进行交互。\n\n适配器模式通过封装对象将复杂的转换过程隐藏于幕后。 被封装的对象甚至察觉不到适配器的存在。\n\n\n\n\n# 真实世界类比\n\n适配器是什么,不难理解,生活中也随处可见。比如,笔记本电脑的电源适配器、万能充(曾经的它真有一个这么牛逼的名字)、一拖十数据线等等。\n\n\n# 基本介绍\n\n * 适配器模式将一个类的接口,转换成客户期望的另外一个接口。适配器让原本接口不兼容的类可以合作无间。也可以叫包装器(wrapper)\n\n * 适配器模式是一种结构型设计模式, 它能使接口不兼容的对象能够相互合作\n\n * 主要分为两类:类适配器模式、对象适配器模式\n\n\n# 工作原理\n\n * 适配器模式:将一个类的接口转换成另一种接口,让原本接口不兼容的类可以兼容\n * 从用户的角度看不到被适配者,是解耦的\n * 用户调用适配器转化出来的目标接口方法,适配器再调用被适配者的相关接口方法\n * 用户收到反馈结果,感觉只是和目标接口交互\n\n\n# 适配器模式结构\n\n\n# 对象适配器\n\n实现时使用了构成原则: 适配器实现了其中一个对象的接口, 并对另一个对象进行封装。 所有流行的编程语言都可以实现适配器。\n\n适配器设计模式的结构(对象适配器)\n\n * 客户端 (client) 是包含当前程序业务逻辑的类。\n * 客户端接口 (target) 描述了其他类与客户端代码合作时必须遵循的协议。\n * 服务 (service) 中有一些功能类 (通常来自第三方或遗留系统)。 客户端与其接口不兼容, 因此无法直接调用其功能,也可以叫适配者类(adaptee)。\n * 适配器 (adapter) 是一个可以同时与客户端和服务交互的类: 它在实现客户端接口的同时封装了服务对象。 适配器接受客户端通过适配器接口发起的调用, 并将其转换为适用于被封装服务对象的调用。\n * 客户端代码只需通过接口与适配器交互即可, 无需与具体的适配器类耦合。 因此, 你可以向程序中添加新类型的适配器而无需修改已有代码。 这在服务类的接口被更改或替换时很有用: 你无需修改客户端代码就可以创建新的适配器类。\n\n\n# coding\n\n定义客户端使用的接口,与业务相关\n\npublic interface target {\n\n /*\n * 客户端请求处理的方法\n */\n void request();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n已经存在的接口,这个接口需要配置\n\npublic class adaptee {\n\n /*\n * 原本存在的方法\n */\n public void specificrequest(){\n //业务代码\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n适配器类\n\npublic class adapter implements target {\n\n /*\n * 持有需要被适配的接口对象\n */\n private adaptee adaptee;\n\n /*\n * 构造方法,传入需要被适配的对象\n * @param adaptee 需要被适配的对象\n */\n public adapter(adaptee adaptee) {\n this.adaptee = adaptee;\n }\n\n @override\n public void request() {\n // todo auto-generated method stub\n adaptee.specificrequest();\n }\n\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n\n\n使用适配器的客户端\n\npublic class client {\n\n public static void main(string[] args) {\n //创建需要被适配的对象\n adaptee adaptee = new adaptee();\n //创建客户端需要调用的接口对象\n target target = new adapter(adaptee);\n //请求处理\n target.request();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n\n# 类适配器\n\n这一实现使用了继承机制: 适配器同时继承两个对象的接口。 请注意, 这种方式仅能在支持多重继承的编程语言中实现,例如 c++, java 不支持多重继承,也就没有这种适配器了。\n\n适配器设计模式(类适配器)\n\n类适配器不需要封装任何对象, 因为它同时继承了客户端和服务的行为。 适配功能在重写的方法中完成。 最后生成的适配器可替代已有的客户端类进行使用。\n\n\n# coding\n\njava 虽然不能实现标准的类适配器,但是有一种变通的方式,也能够使用继承来实现接口的适配,那就是让适配器去实现 target 的接口,然后继承 adaptee 的实现,虽然不是十分标准,但意思差不多。\n\n首先有一个已存在的将被适配的类\n\npublic class adaptee {\n public void adapteerequest() {\n system.out.println(\"被适配者的方法\");\n }\n}\n\n\n1\n2\n3\n4\n5\n\n\n定义客户端使用的接口,与业务相关\n\npublic interface target {\n\n void request();\n}\n\n\n1\n2\n3\n4\n\n\n怎么才可以在目标接口中的 request() 调用 adaptee 的 adapteerequest() 方法呢?直接实现 target 肯定是不行的,所以我们通过一个适配器类,实现 target 接口,同时继承了 adaptee 类,然后在实现的 request() 方法中调用父类的 adapteerequest() 即可\n\npublic class adapter extends adaptee implements target{\n @override\n public void request() {\n //...一些操作...\n super.adapteerequest();\n //...一些操作...\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\n使用适配器的客户端\n\npublic class client {\n public static void main(string[] args) {\n\n target adaptertarget = new adapter();\n adaptertarget.request();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 适配器模式适合应用场景\n\n当你希望使用某个类, 但是其接口与其他代码不兼容时, 可以使用适配器类。\n\n适配器模式允许你创建一个中间层类, 其可作为代码与遗留类、 第三方类或提供怪异接口的类之间的转换器。\n\n如果您需要复用这样一些类, 他们处于同一个继承体系, 并且他们又有了额外的一些共同的方法, 但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。\n\n你可以扩展每个子类, 将缺少的功能添加到新的子类中。 但是, 你必须在所有新子类中重复添加这些代码, 这样会使得代码有坏味道。\n\n将缺失功能添加到一个适配器类中是一种优雅得多的解决方案。 然后你可以将缺少功能的对象封装在适配器中, 从而动态地获取所需功能。 如要这一点正常运作, 目标类必须要有通用接口, 适配器的成员变量应当遵循该通用接口。 这种方式同装饰模式非常相似。\n\n\n# demo\n\n用一个生活中的充电器的例子来讲解下适配器,我国民用电都是 220v,而我们的手机充电一般需要 5v。\n\n220v 的交流电相当于被适配者 adaptee,我们的目标 target 是 5v 直流电,充电器本身相当于一个 adapter,将220v 的输入电压变换为 5v 输出。\n\n首先是我们的民用电(我国是 220v,当然还可以有其他国家的其他准备,可随时扩展)\n\npublic class volatage220v {\n\n public final int output = 220;\n\n public int output220v() {\n system.out.println(\"输出电压 \" + output);\n return output;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n适配接口\n\npublic interface ivoltage5v {\n int output5v();\n}\n\n\n1\n2\n3\n\n\n我们的手机充电,只支持 5v 电压\n\npublic class phone {\n\n public void charging(ivoltage5v v) {\n if (v.output5v() == 5) {\n system.out.println(\"电压 5v ,符合充电标准,开始充电\");\n } else {\n system.out.println(\"电压不符合标准,无法充电\");\n }\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n适配器\n\npublic class voltageadapter implements ivoltage5v {\n\n private volatage220v volatage220v; //聚合\n\n public voltageadapter(volatage220v v) {\n this.volatage220v = v;\n }\n\n @override\n public int output5v() {\n int dst = 0;\n if (null != volatage220v) {\n int src = volatage220v.output220v();\n system.out.println(\"适配器工作~~~~~\");\n dst = src / 44;\n system.out.println(\"适配器工作完成,输出电压\" + dst);\n }\n return dst;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\n工作,如果去国外旅游,有不同的电压,只需要扩展适配器即可。\n\npublic class client {\n public static void main(string[] args) {\n phone phone = new phone();\n phone.charging(new voltageadapter(new volatage220v()));\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n\n# 适配器模式优缺点\n\n单一职责原则,你可以将接口或数据转换代码从程序主要业务逻辑中分离。\n\n开闭原则。 只要客户端代码通过客户端接口与适配器进行交互, 你就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。\n\n代码整体复杂度增加, 因为你需要新增一系列接口和类。 有时直接更改服务类使其与其他代码兼容会更简单。\n\n\n# spring 中的适配器\n\nspring 源码中搜关键字adapter 会出现很多实现类,springmvc 中的 handleradapter ,就是适配器的应用。\n\n我们先回顾下 springmvc 处理流程:\n\nspring mvc 中的适配器模式主要用于执行目标 controller 中的请求处理方法。\n\n在spring mvc中,dispatcherservlet 作为用户,handleradapter 作为期望接口,具体的适配器实现类用于对目标类进行适配,controller 作为需要适配的类。\n\n为什么要在 spring mvc 中使用适配器模式?spring mvc 中的 controller 种类众多,不同类型的 controller 通过不同的方法来对请求进行处理。如果不利用适配器模式的话,dispatcherservlet 直接获取对应类型的 controller,需要的自行来判断,像下面这段代码一样:\n\nif(mappedhandler.gethandler() instanceof multiactioncontroller){ \n ((multiactioncontroller)mappedhandler.gethandler()).xxx \n}else if(mappedhandler.gethandler() instanceof xxx){ \n ... \n}else if(...){ \n ... \n} \n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n这样假设如果我们增加一个 controller,就要在代码中加入一行 if 语句,这种形式就使得程序难以维护,也违反了设计模式中的开闭原则 – 对扩展开放,对修改关闭。\n\n我们通过源码看看 springmvc 是如何实现的,首先看下核心类 dispatcherservlet:\n\npublic class dispatcherservlet extends frameworkservlet {\n \t//......\n\t//维护所有handleradapter类的集合\n @nullable\n private list handleradapters;\n \n\t//初始化handleradapters\n private void inithandleradapters(applicationcontext context) {\n this.handleradapters = null;\n if (this.detectallhandleradapters) {\n map matchingbeans = beanfactoryutils.beansoftypeincludingancestors(context, handleradapter.class, true, false);\n if (!matchingbeans.isempty()) {\n this.handleradapters = new arraylist(matchingbeans.values());\n annotationawareordercomparator.sort(this.handleradapters);\n }\n } else {\n try {\n handleradapter ha = (handleradapter)context.getbean(\"handleradapter\", handleradapter.class);\n this.handleradapters = collections.singletonlist(ha);\n } catch (nosuchbeandefinitionexception var3) {\n }\n }\n\n if (this.handleradapters == null) {\n this.handleradapters = this.getdefaultstrategies(context, handleradapter.class);\n if (this.logger.istraceenabled()) {\n this.logger.trace(\"no handleradapters declared for servlet '\" + this.getservletname() + \"': using default strategies from dispatcherservlet.properties\");\n }\n }\n }\n \n //dispatch 方法中会获取 handleradapter\n protected void dodispatch(httpservletrequest request, httpservletresponse response) throws exception {\n //...\n \n\t\t//获得controller对应的适配器\n handleradapter ha = this.gethandleradapter(mappedhandler.gethandler()); \n\t\t\t\t\n\t\t//调用适配器的handler方法处理请求,并返回modelandview\n mv = ha.handle(processedrequest, response, mappedhandler.gethandler()); \n //...\n }\n \t\n\t //返回对应的controller的处理器\n protected handleradapter gethandleradapter(object handler) throws servletexception {\n if (this.handleradapters != null) {\n iterator var2 = this.handleradapters.iterator();\n\n while(var2.hasnext()) {\n handleradapter adapter = (handleradapter)var2.next();\n if (adapter.supports(handler)) {\n return adapter;\n }\n }\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n\n\n接着看下 handleradapter 的源码,也就是适配器接口:\n\npublic interface handleradapter {\n boolean supports(object var1);\n\n @nullable\n modelandview handle(httpservletrequest var1, httpservletresponse var2, object var3) throws exception;\n\n long getlastmodified(httpservletrequest var1, object var2);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\n再来屡一下这个流程:\n\n首先是适配器接口 dispatchservlet 中有一个集合维护所有的 handleradapter,如果配置文件中没有对适配器进行配置,那么 dispatchservlet 会在创建时对该变量进行初始化,注册所有默认的 handleradapter。\n\n当一个请求过来时,dispatchservlet 会根据传过来的 handler 类型从该集合中寻找对应的 handleradapter子类进行处理,并且调用它的 handler() 方法 对应的 handleradapter 中的 handler() 方法又会执行对应 controller 的 handlerequest() 方法\n\n适配器与 handler 有对应关系,而各个适配器又都是适配器接口的实现类,因此,它们都遵循相同的适配器标准,所以用户可以按照相同的方式,通过不同的 handler 去处理请求。\n\n当然了,spring 框架中也为我们定义了一些默认的 handler 对应的适配器。\n\n通过适配器模式我们将所有的 controller 统一交给 handleradapter 处理,免去了写大量的 if-else 语句对 controller 进行判断,也更利于扩展新的 controller 类型。\n\n参考: 随遇而安的适配器模式 | spring 中的适配器", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/20, 22:28:30", + "lastUpdatedTimestamp": 1663684110000 + }, + { + "title": "责任链模式", + "frontmatter": { + "title": "责任链模式", + "date": "2022-05-21T17:28:46.000Z", + "permalink": "/pages/d3ff35/", + "categories": [ + "Java相关", + "设计模式" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/030.%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F.html", + "relativePath": "01.Java相关/40.设计模式/030.责任链模式.md", + "key": "v-34f8aaa2", + "path": "/pages/d3ff35/", + "headers": [ + { + "level": 2, + "title": "定义", + "slug": "定义", + "normalizedTitle": "定义", + "charIndex": 1126 + }, + { + "level": 2, + "title": "角色", + "slug": "角色", + "normalizedTitle": "角色", + "charIndex": 1245 + }, + { + "level": 2, + "title": "类图", + "slug": "类图", + "normalizedTitle": "类图", + "charIndex": 1518 + }, + { + "level": 2, + "title": "coding", + "slug": "coding", + "normalizedTitle": "coding", + "charIndex": 1527 + }, + { + "level": 2, + "title": "实例", + "slug": "实例", + "normalizedTitle": "实例", + "charIndex": 3306 + }, + { + "level": 2, + "title": "应用", + "slug": "应用", + "normalizedTitle": "应用", + "charIndex": 784 + }, + { + "level": 3, + "title": "Servlet 中的责任链", + "slug": "servlet-中的责任链", + "normalizedTitle": "servlet 中的责任链", + "charIndex": 3611 + }, + { + "level": 3, + "title": "Spring MVC 中的责任链", + "slug": "spring-mvc-中的责任链", + "normalizedTitle": "spring mvc 中的责任链", + "charIndex": 5637 + }, + { + "level": 2, + "title": "总结", + "slug": "总结", + "normalizedTitle": "总结", + "charIndex": 8291 + } + ], + "headersStr": "定义 角色 类图 coding 实例 应用 Servlet 中的责任链 Spring MVC 中的责任链 总结", + "content": "责任链,顾名思义,就是用来处理相关事务责任的一条执行链,执行链上有多个节点,每个节点都有机会(条件匹配)处理请求事务,如果某个节点处理完了就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。\n\n这种模式给予请求的类型,对请求的发送者和接收者进行解耦。属于行为型模式。\n\n在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。\n\n\n\n先来看一段代码\n\npublic void test(int i, Request request){\n if(i==1){\n Handler1.response(request);\n }else if(i == 2){\n Handler2.response(request);\n }else if(i == 3){\n Handler3.response(request);\n }else if(i == 4){\n Handler4.response(request);\n }else{\n Handler5.response(request);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n代码的业务逻辑是这样的,方法有两个参数:整数 i 和一个请求 request,根据 i 的值来决定由谁来处理 request,如果 i==1,由 Handler1来处理,如果 i==2,由 Handler2 来处理,以此类推。在编程中,这种处理业务的方法非常常见,所有处理请求的类由 if…else… 条件判断语句连成一条责任链来对请求进行处理,相信大家都经常用到。这种方法的优点是非常直观,简单明了,并且比较容易维护,但是这种方法也存在着几个比较令人头疼的问题:\n\n代码臃肿:实际应用中的判定条件通常不是这么简单地判断是否为1或者是否为2,也许需要复杂的计算,也许需要查询数据库等等,这就会有很多额外的代码,如果判断条件再比较多,那么这个if…else…语句基本上就没法看了。 耦合度高:如果我们想继续添加处理请求的类,那么就要继续添加if…else…判定条件;另外,这个条件判定的顺序也是写死的,如果想改变顺序,那么也只能修改这个条件语句。 既然缺点我们已经清楚了,就要想办法来解决。这个场景的业务逻辑很简单:如果满足条件1,则由 Handler1 来处理,不满足则向下传递;如果满足条件2,则由 Handler2 来处理,不满足则继续向下传递,以此类推,直到条件结束。其实改进的方法也很简单,就是把判定条件的部分放到处理类中,这就是责任连模式的原理。\n\n\n# 定义\n\n责任链模式(Chain of Responsibility Pattern):使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。\n\n\n# 角色\n\nHandler: 抽象处理类,抽象处理类中主要包含一个指向下一处理类的成员变量 nextHandler 和一个处理请求的方法 handRequest,handRequest 方法的主要主要思想是,如果满足处理的条件,则由本处理类来进行处理,否则由 nextHandler 来处理 ConcreteHandler: 具体处理类主要是对具体的处理逻辑和处理的适用条件进行实现。具体处理者接到请求后,可以选择将请求处理掉,或者将请求传给下家。由于具体处理者持有对下家的引用,因此,如果需要,具体处理者可以访问下家 Client:客户端\n\n\n# 类图\n\n\n\n\n# coding\n\npublic abstract class Handler {\n private Handler nextHandler;\n private int level;\n\n public Handler(int level) {\n this.level = level;\n }\n\n public void setNextHandler(Handler handler){\n this.nextHandler = handler;\n }\n\n public final void handlerRequest(Request request){\n if(level == request.getLevel()){\n this.response(request);\n }else{\n if (this.nextHandler != null){\n this.nextHandler.handlerRequest(request);\n }else{\n System.out.println(\"===已经没有处理器了===\");\n }\n }\n\n }\n // 抽象方法,子类实现\n public abstract void response(Request request);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n\n\nclass Request {\n int level = 0;\n public Request(int level){\n this.level = level;\n }\n public int getLevel() {\n return level;\n }\n}\npublic class ConcreteHandler1 extends Handler {\n public ConcreteHandler1(int level) {\n super(level);\n }\n\n @Override\n public void response(Request request) {\n System.out.println(\"请求由处理器1进行处理\");\n }\n}\n\npublic class ConcreteHandler2 extends Handler {\n\t//...\n}\n\npublic class ConcreteHandler2 extends Handler {\n\t//...\n}\npublic class Client {\n public static void main(String[] args) {\n ConcreteHandler1 handler1 = new ConcreteHandler1(1);\n ConcreteHandler2 handler2 = new ConcreteHandler2(2);\n ConcreteHandler3 handler3 = new ConcreteHandler3(3);\n\t\t//处理者构成一个环形\n handler1.setNextHandler(handler2);\n handler2.setNextHandler(handler3);\n\n handler1.handlerRequest(new Request(1));\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n\n\n\n# 实例\n\n当你想要让一个以上的对象有机会能够处理某个请求的时候,就是用责任链模式。\n\n通过责任链模式,你可以为某个请求创建一个对象链。每个对象依序检查此请求,并对其进行处理,或者将它传给链中的下一个对象。\n\n比如\n\n * 程序员要请 3 天以上的假期,在 OA 申请,需要直接主管、总监、HR 层层审批后才生效。类似的采购审批、报销审批。。。\n * 美团在外卖营销业务中资源位展示的逻辑\n\n\n# 应用\n\nJAVA 中的异常处理机制、JAVA WEB 中 Apache Tomcat 对 Encoding 的处理,Struts2 的拦截器,JSP、Servlet 的 Filter 均是责任链的典型应用。\n\n\n# Servlet 中的责任链\n\npublic final class ApplicationFilterChain implements FilterChain {\n private static final ThreadLocal lastServicedRequest;\n private static final ThreadLocal lastServicedResponse;\n public static final int INCREMENT = 10;\n private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];\n private int pos = 0; //下一个要执行的filter的位置\n private int n = 0; //filter个数\n private Servlet servlet = null;\n public ApplicationFilterChain() {\n }\n\n public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {\n if (Globals.IS_SECURITY_ENABLED) {\n final ServletRequest req = request;\n final ServletResponse res = response;\n\n try {\n AccessController.doPrivileged(new PrivilegedExceptionAction() {\n public Void run() throws ServletException, IOException {\n ApplicationFilterChain.this.internalDoFilter(req, res);\n return null;\n }\n });\n } catch (PrivilegedActionException var7) {\n Exception e = var7.getException();\n if (e instanceof ServletException) {\n throw (ServletException)e;\n }\n\n if (e instanceof IOException) {\n throw (IOException)e;\n }\n\n if (e instanceof RuntimeException) {\n throw (RuntimeException)e;\n }\n\n throw new ServletException(e.getMessage(), e);\n }\n } else {\n this.internalDoFilter(request, response);\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n\n\nFilterChain 就是一条过滤链。其中每个过滤器(Filter)都可以决定是否执行下一步。过滤分两个方向,进和出:\n\n进:在把 ServletRequest 和 ServletResponse 交给 Servlet 的 service 方法之前,需要进行过滤\n\n出:在service方法完成后,往客户端发送之前,需要进行过滤\n\n\n# Spring MVC 中的责任链\n\nSpring MVC 的 diapatcherServlet 的 doDispatch 方法中,获取与请求匹配的处理器 HandlerExecutionChain 就是用到了责任链模式。\n\nprotected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {\n HttpServletRequest processedRequest = request;\n HandlerExecutionChain mappedHandler = null; //使用到了责任链模式\n boolean multipartRequestParsed = false;\n WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);\n\n try {\n try {\n ModelAndView mv = null;\n Object dispatchException = null;\n\n try {\n processedRequest = this.checkMultipart(request);\n multipartRequestParsed = processedRequest != request;\n mappedHandler = this.getHandler(processedRequest); \n if (mappedHandler == null) {\n this.noHandlerFound(processedRequest, response);\n return;\n }\n\n HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());\n String method = request.getMethod();\n boolean isGet = \"GET\".equals(method);\n if (isGet || \"HEAD\".equals(method)) {\n long lastModified = ha.getLastModified(request, mappedHandler.getHandler());\n if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {\n return;\n }\n }\n\t\t\t\t\t\t\t\t\t\t//责任链模式执行预处理方法,其实是将请求交给注册的拦截器执行\n if (!mappedHandler.applyPreHandle(processedRequest, response)) {\n return;\n }\n\n mv = ha.handle(processedRequest, response, mappedHandler.getHandler());\n if (asyncManager.isConcurrentHandlingStarted()) {\n return;\n }\n\n this.applyDefaultViewName(processedRequest, mv);\n //责任链执行后处理方法\n mappedHandler.applyPostHandle(processedRequest, response, mv);\n } catch (Exception var22) {\n //...\n } finally {\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n\n * SpringMVC 请求的流程中,执行了拦截器相关方法 interceptor.preHandler 等等\n * 在处理 SpringMVC 请求时,使用到职责链模式还使用到适配器模式\n * HandlerExecutionChain 主要负责的是请求拦截器的执行和请求处理,但是他本身不处理请求,只是将请求分配给链上注册处理器执行,这是职责链实现方式,减少职责链本身与处理逻辑之间的耦合,规范了处理流程\n * HandlerExecutionChain 维护了 HandlerInterceptor 的集合, 可以向其中注册相应的拦截器\n\n\n# 总结\n\n责任链模式其实就是一个灵活版的 if…else…语句,它就是将这些判定条件的语句放到了各个处理类中,这样做的优点是比较灵活了,但同样也带来了风险,比如设置处理类前后关系时,一定要特别仔细,搞对处理类前后逻辑的条件判断关系,并且注意不要在链中出现循环引用的问题。\n\n优点:\n\n降低耦合度:将请求和处理分开,实现解耦,提高了系统的灵活性。 简化了对象:对象不需要知道链的结构 良好的扩展性:增加处理者的实现很简单,只需重写处理请求业务逻辑的方法。 缺点:\n\n从链头发出,直到有处理者响应,在责任链比较长的时候会影响系统性能,一般需要在 Handler 中设置一个最大节点数。 请求递归,调试排错比较麻烦。 使用场景:\n\n有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。 可动态指定一组对象处理请求。 模式的扩展:\n\n职责链模式存在以下两种情况。\n\n纯的职责链模式:一个请求必须被某一个处理者对象所接收,且一个具体处理者对某个请求的处理只能采用以下两种行为之一:自己处理(承担责任);把责任推给下家处理。 不纯的职责链模式:允许出现某一个具体处理者对象在承担了请求的一部分责任后又将剩余的责任传给下家的情况,且一个请求可以最终不被任何接收端对象所接收。", + "normalizedContent": "责任链,顾名思义,就是用来处理相关事务责任的一条执行链,执行链上有多个节点,每个节点都有机会(条件匹配)处理请求事务,如果某个节点处理完了就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。\n\n这种模式给予请求的类型,对请求的发送者和接收者进行解耦。属于行为型模式。\n\n在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。\n\n\n\n先来看一段代码\n\npublic void test(int i, request request){\n if(i==1){\n handler1.response(request);\n }else if(i == 2){\n handler2.response(request);\n }else if(i == 3){\n handler3.response(request);\n }else if(i == 4){\n handler4.response(request);\n }else{\n handler5.response(request);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n代码的业务逻辑是这样的,方法有两个参数:整数 i 和一个请求 request,根据 i 的值来决定由谁来处理 request,如果 i==1,由 handler1来处理,如果 i==2,由 handler2 来处理,以此类推。在编程中,这种处理业务的方法非常常见,所有处理请求的类由 if…else… 条件判断语句连成一条责任链来对请求进行处理,相信大家都经常用到。这种方法的优点是非常直观,简单明了,并且比较容易维护,但是这种方法也存在着几个比较令人头疼的问题:\n\n代码臃肿:实际应用中的判定条件通常不是这么简单地判断是否为1或者是否为2,也许需要复杂的计算,也许需要查询数据库等等,这就会有很多额外的代码,如果判断条件再比较多,那么这个if…else…语句基本上就没法看了。 耦合度高:如果我们想继续添加处理请求的类,那么就要继续添加if…else…判定条件;另外,这个条件判定的顺序也是写死的,如果想改变顺序,那么也只能修改这个条件语句。 既然缺点我们已经清楚了,就要想办法来解决。这个场景的业务逻辑很简单:如果满足条件1,则由 handler1 来处理,不满足则向下传递;如果满足条件2,则由 handler2 来处理,不满足则继续向下传递,以此类推,直到条件结束。其实改进的方法也很简单,就是把判定条件的部分放到处理类中,这就是责任连模式的原理。\n\n\n# 定义\n\n责任链模式(chain of responsibility pattern):使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。\n\n\n# 角色\n\nhandler: 抽象处理类,抽象处理类中主要包含一个指向下一处理类的成员变量 nexthandler 和一个处理请求的方法 handrequest,handrequest 方法的主要主要思想是,如果满足处理的条件,则由本处理类来进行处理,否则由 nexthandler 来处理 concretehandler: 具体处理类主要是对具体的处理逻辑和处理的适用条件进行实现。具体处理者接到请求后,可以选择将请求处理掉,或者将请求传给下家。由于具体处理者持有对下家的引用,因此,如果需要,具体处理者可以访问下家 client:客户端\n\n\n# 类图\n\n\n\n\n# coding\n\npublic abstract class handler {\n private handler nexthandler;\n private int level;\n\n public handler(int level) {\n this.level = level;\n }\n\n public void setnexthandler(handler handler){\n this.nexthandler = handler;\n }\n\n public final void handlerrequest(request request){\n if(level == request.getlevel()){\n this.response(request);\n }else{\n if (this.nexthandler != null){\n this.nexthandler.handlerrequest(request);\n }else{\n system.out.println(\"===已经没有处理器了===\");\n }\n }\n\n }\n // 抽象方法,子类实现\n public abstract void response(request request);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n\n\nclass request {\n int level = 0;\n public request(int level){\n this.level = level;\n }\n public int getlevel() {\n return level;\n }\n}\npublic class concretehandler1 extends handler {\n public concretehandler1(int level) {\n super(level);\n }\n\n @override\n public void response(request request) {\n system.out.println(\"请求由处理器1进行处理\");\n }\n}\n\npublic class concretehandler2 extends handler {\n\t//...\n}\n\npublic class concretehandler2 extends handler {\n\t//...\n}\npublic class client {\n public static void main(string[] args) {\n concretehandler1 handler1 = new concretehandler1(1);\n concretehandler2 handler2 = new concretehandler2(2);\n concretehandler3 handler3 = new concretehandler3(3);\n\t\t//处理者构成一个环形\n handler1.setnexthandler(handler2);\n handler2.setnexthandler(handler3);\n\n handler1.handlerrequest(new request(1));\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n\n\n\n# 实例\n\n当你想要让一个以上的对象有机会能够处理某个请求的时候,就是用责任链模式。\n\n通过责任链模式,你可以为某个请求创建一个对象链。每个对象依序检查此请求,并对其进行处理,或者将它传给链中的下一个对象。\n\n比如\n\n * 程序员要请 3 天以上的假期,在 oa 申请,需要直接主管、总监、hr 层层审批后才生效。类似的采购审批、报销审批。。。\n * 美团在外卖营销业务中资源位展示的逻辑\n\n\n# 应用\n\njava 中的异常处理机制、java web 中 apache tomcat 对 encoding 的处理,struts2 的拦截器,jsp、servlet 的 filter 均是责任链的典型应用。\n\n\n# servlet 中的责任链\n\npublic final class applicationfilterchain implements filterchain {\n private static final threadlocal lastservicedrequest;\n private static final threadlocal lastservicedresponse;\n public static final int increment = 10;\n private applicationfilterconfig[] filters = new applicationfilterconfig[0];\n private int pos = 0; //下一个要执行的filter的位置\n private int n = 0; //filter个数\n private servlet servlet = null;\n public applicationfilterchain() {\n }\n\n public void dofilter(servletrequest request, servletresponse response) throws ioexception, servletexception {\n if (globals.is_security_enabled) {\n final servletrequest req = request;\n final servletresponse res = response;\n\n try {\n accesscontroller.doprivileged(new privilegedexceptionaction() {\n public void run() throws servletexception, ioexception {\n applicationfilterchain.this.internaldofilter(req, res);\n return null;\n }\n });\n } catch (privilegedactionexception var7) {\n exception e = var7.getexception();\n if (e instanceof servletexception) {\n throw (servletexception)e;\n }\n\n if (e instanceof ioexception) {\n throw (ioexception)e;\n }\n\n if (e instanceof runtimeexception) {\n throw (runtimeexception)e;\n }\n\n throw new servletexception(e.getmessage(), e);\n }\n } else {\n this.internaldofilter(request, response);\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n\n\nfilterchain 就是一条过滤链。其中每个过滤器(filter)都可以决定是否执行下一步。过滤分两个方向,进和出:\n\n进:在把 servletrequest 和 servletresponse 交给 servlet 的 service 方法之前,需要进行过滤\n\n出:在service方法完成后,往客户端发送之前,需要进行过滤\n\n\n# spring mvc 中的责任链\n\nspring mvc 的 diapatcherservlet 的 dodispatch 方法中,获取与请求匹配的处理器 handlerexecutionchain 就是用到了责任链模式。\n\nprotected void dodispatch(httpservletrequest request, httpservletresponse response) throws exception {\n httpservletrequest processedrequest = request;\n handlerexecutionchain mappedhandler = null; //使用到了责任链模式\n boolean multipartrequestparsed = false;\n webasyncmanager asyncmanager = webasyncutils.getasyncmanager(request);\n\n try {\n try {\n modelandview mv = null;\n object dispatchexception = null;\n\n try {\n processedrequest = this.checkmultipart(request);\n multipartrequestparsed = processedrequest != request;\n mappedhandler = this.gethandler(processedrequest); \n if (mappedhandler == null) {\n this.nohandlerfound(processedrequest, response);\n return;\n }\n\n handleradapter ha = this.gethandleradapter(mappedhandler.gethandler());\n string method = request.getmethod();\n boolean isget = \"get\".equals(method);\n if (isget || \"head\".equals(method)) {\n long lastmodified = ha.getlastmodified(request, mappedhandler.gethandler());\n if ((new servletwebrequest(request, response)).checknotmodified(lastmodified) && isget) {\n return;\n }\n }\n\t\t\t\t\t\t\t\t\t\t//责任链模式执行预处理方法,其实是将请求交给注册的拦截器执行\n if (!mappedhandler.applyprehandle(processedrequest, response)) {\n return;\n }\n\n mv = ha.handle(processedrequest, response, mappedhandler.gethandler());\n if (asyncmanager.isconcurrenthandlingstarted()) {\n return;\n }\n\n this.applydefaultviewname(processedrequest, mv);\n //责任链执行后处理方法\n mappedhandler.applyposthandle(processedrequest, response, mv);\n } catch (exception var22) {\n //...\n } finally {\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n\n * springmvc 请求的流程中,执行了拦截器相关方法 interceptor.prehandler 等等\n * 在处理 springmvc 请求时,使用到职责链模式还使用到适配器模式\n * handlerexecutionchain 主要负责的是请求拦截器的执行和请求处理,但是他本身不处理请求,只是将请求分配给链上注册处理器执行,这是职责链实现方式,减少职责链本身与处理逻辑之间的耦合,规范了处理流程\n * handlerexecutionchain 维护了 handlerinterceptor 的集合, 可以向其中注册相应的拦截器\n\n\n# 总结\n\n责任链模式其实就是一个灵活版的 if…else…语句,它就是将这些判定条件的语句放到了各个处理类中,这样做的优点是比较灵活了,但同样也带来了风险,比如设置处理类前后关系时,一定要特别仔细,搞对处理类前后逻辑的条件判断关系,并且注意不要在链中出现循环引用的问题。\n\n优点:\n\n降低耦合度:将请求和处理分开,实现解耦,提高了系统的灵活性。 简化了对象:对象不需要知道链的结构 良好的扩展性:增加处理者的实现很简单,只需重写处理请求业务逻辑的方法。 缺点:\n\n从链头发出,直到有处理者响应,在责任链比较长的时候会影响系统性能,一般需要在 handler 中设置一个最大节点数。 请求递归,调试排错比较麻烦。 使用场景:\n\n有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。 可动态指定一组对象处理请求。 模式的扩展:\n\n职责链模式存在以下两种情况。\n\n纯的职责链模式:一个请求必须被某一个处理者对象所接收,且一个具体处理者对某个请求的处理只能采用以下两种行为之一:自己处理(承担责任);把责任推给下家处理。 不纯的职责链模式:允许出现某一个具体处理者对象在承担了请求的一部分责任后又将剩余的责任传给下家的情况,且一个请求可以最终不被任何接收端对象所接收。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "策略模式", + "frontmatter": { + "title": "策略模式", + "date": "2022-05-21T17:29:26.000Z", + "permalink": "/pages/78c7aa/", + "categories": [ + "Java相关", + "设计模式" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/050.%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F.html", + "relativePath": "01.Java相关/40.设计模式/050.策略模式.md", + "key": "v-5b0157cf", + "path": "/pages/78c7aa/", + "headers": [ + { + "level": 2, + "title": "定义", + "slug": "定义", + "normalizedTitle": "定义", + "charIndex": 2 + }, + { + "level": 2, + "title": "角色", + "slug": "角色", + "normalizedTitle": "角色", + "charIndex": 1075 + }, + { + "level": 2, + "title": "实例", + "slug": "实例", + "normalizedTitle": "实例", + "charIndex": 1229 + }, + { + "level": 2, + "title": "解析策略模式", + "slug": "解析策略模式", + "normalizedTitle": "解析策略模式", + "charIndex": 2310 + }, + { + "level": 2, + "title": "谁来选择具体的策略算法", + "slug": "谁来选择具体的策略算法", + "normalizedTitle": "谁来选择具体的策略算法", + "charIndex": 2795 + }, + { + "level": 2, + "title": "优缺点", + "slug": "优缺点", + "normalizedTitle": "优缺点", + "charIndex": 2992 + }, + { + "level": 3, + "title": "优点:", + "slug": "优点", + "normalizedTitle": "优点:", + "charIndex": 3000 + }, + { + "level": 3, + "title": "缺点:", + "slug": "缺点", + "normalizedTitle": "缺点:", + "charIndex": 3092 + }, + { + "level": 2, + "title": "思考", + "slug": "思考", + "normalizedTitle": "思考", + "charIndex": 3233 + }, + { + "level": 2, + "title": "策略模式在 JDK 中的应用", + "slug": "策略模式在-jdk-中的应用", + "normalizedTitle": "策略模式在 jdk 中的应用", + "charIndex": 3512 + } + ], + "headersStr": "定义 角色 实例 解析策略模式 谁来选择具体的策略算法 优缺点 优点: 缺点: 思考 策略模式在 JDK 中的应用", + "content": "# 定义\n\n策略模式(Strategy Design Pattern):封装可以互换的行为,并使用委托来决定要使用哪一个。\n\n策略模式是一种行为设计模式, 它能让你定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换。\n\n> 用人话翻译后就是:运行时我给你这个类的方法传不同的 “key”,你这个方法就去执行不同的业务逻辑。 你品,你细品,这不就是 if else 干的事吗?\n\n先直观的看下传统的多重 if else 代码\n\npublic String getCheckResult(String type) {\n if (\"校验1\".equals(type)) {\n return \"执行业务逻辑1\";\n } else if (\"校验2\".equals(type)) {\n return \"执行业务逻辑2\";\n } else if (\"校验3\".equals(type)) {\n return \"执行业务逻辑3\";\n } else if (\"校验4\".equals(type)) {\n return \"执行业务逻辑4\";\n } else if (\"校验5\".equals(type)) {\n return \"执行业务逻辑5\";\n } else if (\"校验6\".equals(type)) {\n return \"执行业务逻辑6\";\n } else if (\"校验7\".equals(type)) {\n return \"执行业务逻辑7\";\n } else if (\"校验8\".equals(type)) {\n return \"执行业务逻辑8\";\n } else if (\"校验9\".equals(type)) {\n return \"执行业务逻辑9\";\n }\n return \"不在处理的逻辑中返回业务错误\";\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n\n\n这么看,你要是还觉得挺清晰的话,想象下这些 return 里是各种复杂的业务逻辑方法~~\n\n当然,策略模式的作用可不止是避免冗长的 if-else 或者 switch 分支,它还可以像模板方法模式那样提供框架的扩展点等。\n\n网上的示例很多,比如不同路线的规划、不同支付方式的选择 都是典型的 if-else 问题,也都是典型的策略模式问题,栗子我们待会看,先看下策略模式的类图,然后去改造多重判断~\n\n\n# 角色\n\n策略模式涉及到三个角色:\n\n 1. Strategy:策略接口或者策略抽象类,用来约束一系列的策略算法(Context 使用这个接口来调用具体的策略实现算法)\n 2. ConcreateStrategy:具体的策略类(实现策略接口或继承抽象策略类)\n 3. Context:上下文类,持有具体策略类的实例,并负责调用相关的算法 应用策略模式来解决问题的思路\n\n\n# 实例\n\n先看看最简单的策略模式 demo:\n\n1、策略接口(定义策略)\n\npublic interface Strategy {\n void operate();\n}\n\n\n1\n2\n3\n\n\n2、具体的算法实现\n\npublic class ConcreteStrategyA implements Strategy {\n @Override\n public void operate() {\n //具体的算法实现\n System.out.println(\"执行业务逻辑A\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\npublic class ConcreteStrategyB implements Strategy {\n @Override\n public void operate() {\n //具体的算法实现\n System.out.println(\"执行业务逻辑B\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n3、上下文的实现\n\npublic class Context {\n\n //持有一个具体的策略对象\n private Strategy strategy;\n\n //构造方法,传入具体的策略对象\n public Context(Strategy strategy){\n this.strategy = strategy;\n }\n\n public void doSomething(){\n //调用具体的策略对象进操作\n strategy.operate();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n\n\n4、客户端使用(策略的使用)\n\npublic static void main(String[] args) {\n Context context = new Context(new ConcreteStrategyA());\n context.doSomething();\n}\n\n\n1\n2\n3\n4\n\n\n> ps:这种策略的使用方式其实很死板,真正使用的时候如果还这么写, 和写一大推 if-else 没什么区别,所以我们一般会结合工厂类, 在运行时动态确定使用哪种策略。策略模式侧重如何选择策略、工厂模式侧重如何创建策略。\n\n\n# 解析策略模式\n\n策略模式的功能就是把具体的算法实现从具体的业务处理中独立出来,把它们实现成单独的算法类,从而形成一系列算法,并让这些算法可以互相替换。\n\n> 策略模式的重心不是如何来实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活,具有更好的维护性和扩展性。\n\n实际上,每个策略算法具体实现的功能,就是原来在 if-else 结构中的具体实现,每个 if-else 语句都是一个平等的功能结构,可以说是兄弟关系。\n\n策略模式呢,就是把各个平等的具体实现封装到单独的策略实现类了,然后通过上下文与具体的策略类进行交互。\n\n『 策略模式 = 实现策略接口(或抽象类)的每个策略类 + 上下文的逻辑分派 』\n\n> 策略模式的本质:分离算法,选择实现 ——《研磨设计模式》\n\n所以说,策略模式只是在代码结构上的一个调整,即使用了策略模式,该写的逻辑一个也少不了,到逻辑分派的时候,只是变相的 if-else。\n\n而它的优化点是抽象了出了接口,将业务逻辑封装成一个一个的实现类,任意地替换。在复杂场景(业务逻辑较多)时比直接 if-else 更好维护和扩展些。\n\n\n# 谁来选择具体的策略算法\n\n如果你手写了上边的 demo,就会发现,这玩意不及 if-else 来的顺手,尤其是在判断逻辑的时候,每个逻辑都要要构造一个上下文对象,费劲。\n\n其实,策略模式中,我们可以自己定义谁来选择具体的策略算法,有两种:\n\n * 客户端:当使用上下文时,由客户端选择,像我们上边的 demo\n * 上下文:客户端不用选,由上下文来选具体的策略算法,可以在构造器中指定\n\n\n# 优缺点\n\n\n# 优点:\n\n避免多重条件语句:也就是避免大量的 if-else 更好的扩展性(完全符合开闭原则):策略模式中扩展新的策略实现很容易,无需对上下文修改,只增加新的策略实现类就可以\n\n\n# 缺点:\n\n客户必须了解每种策略的不同(这个可以通过 IOC、依赖注入的方式解决) 增加了对象数:每个具体策略都封装成了类,可能备选的策略会很多 只适合扁平的算法结构:策略模式的一系列算法是平等的,也就是在运行时刻只有一个算法会被使用,这就限制了算法使用的层级,不能嵌套使用\n\n\n# 思考\n\n实际使用中,往往不会只是单一的某个设计模式的套用,一般都会混合使用,而且模式之间的结合也是没有定势的,要具体问题具体分析。\n\n策略模式往往会结合其他模式一起使用,比如工厂、模板等,具体使用需要结合自己的业务。\n\n切记,不要为了使用设计模式而强行模式,不要把简单问题复杂化。\n\n策略模式也不是专为消除 if-else 而生的,不要和 if-else 划等号。它体现了“对修改关闭,对扩展开放“的原则。\n\n并不是说,看到 if-else 就想着用策略模式去优化,业务逻辑简单,可能几个枚举,或者几个卫语句就搞定的场景,就不用非得硬套设计模式了。\n\n\n# 策略模式在 JDK 中的应用\n\n在 JDK 中,Comparator 比较器是一个策略接口,我们常用的 compare() 方法就是一个具体的策略实现,用于定义排序规则。\n\npublic interface Comparator {\n int compare(T o1, T o2);\n //......\n}\n\n\n1\n2\n3\n4\n\n\n当我们想自定义排序规则的时候,就可以实现 Comparator 。\n\n这时候我们重写了接口中的 compare() 方法,就是具体的策略类(只不过这里可能是内部类)。当我们在调用 Arrays 的排序方法 sort() 时,可以用默认的排序规则,也可以用自定义的规则。\n\npublic static void main(String[] args) {\n Integer[] data = {4,2,7,5,1,9};\n Comparator comparator = new Comparator() {\n @Override\n public int compare(Integer o1, Integer o2) {\n if(o1 > o2){\n return 1;\n } else {\n return -1;\n }\n }\n };\n\n Arrays.sort(data,comparator);\n System.out.println(Arrays.toString(data));\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\nArrays 的 sort() 方法,有自定义规则就按自己的方法排序,反之走源码逻辑。\n\npublic static void sort(T[] a, Comparator c) {\n if (c == null) {\n sort(a);\n } else {\n if (LegacyMergeSort.userRequested)\n legacyMergeSort(a, c);\n else\n TimSort.sort(a, 0, a.length, c, null, 0, 0);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n还有,ThreadPoolExecutor 中的拒绝策略 RejectedExecutionHandler 也是典型的策略模式,感兴趣的也可以再看看源码。", + "normalizedContent": "# 定义\n\n策略模式(strategy design pattern):封装可以互换的行为,并使用委托来决定要使用哪一个。\n\n策略模式是一种行为设计模式, 它能让你定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换。\n\n> 用人话翻译后就是:运行时我给你这个类的方法传不同的 “key”,你这个方法就去执行不同的业务逻辑。 你品,你细品,这不就是 if else 干的事吗?\n\n先直观的看下传统的多重 if else 代码\n\npublic string getcheckresult(string type) {\n if (\"校验1\".equals(type)) {\n return \"执行业务逻辑1\";\n } else if (\"校验2\".equals(type)) {\n return \"执行业务逻辑2\";\n } else if (\"校验3\".equals(type)) {\n return \"执行业务逻辑3\";\n } else if (\"校验4\".equals(type)) {\n return \"执行业务逻辑4\";\n } else if (\"校验5\".equals(type)) {\n return \"执行业务逻辑5\";\n } else if (\"校验6\".equals(type)) {\n return \"执行业务逻辑6\";\n } else if (\"校验7\".equals(type)) {\n return \"执行业务逻辑7\";\n } else if (\"校验8\".equals(type)) {\n return \"执行业务逻辑8\";\n } else if (\"校验9\".equals(type)) {\n return \"执行业务逻辑9\";\n }\n return \"不在处理的逻辑中返回业务错误\";\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n\n\n这么看,你要是还觉得挺清晰的话,想象下这些 return 里是各种复杂的业务逻辑方法~~\n\n当然,策略模式的作用可不止是避免冗长的 if-else 或者 switch 分支,它还可以像模板方法模式那样提供框架的扩展点等。\n\n网上的示例很多,比如不同路线的规划、不同支付方式的选择 都是典型的 if-else 问题,也都是典型的策略模式问题,栗子我们待会看,先看下策略模式的类图,然后去改造多重判断~\n\n\n# 角色\n\n策略模式涉及到三个角色:\n\n 1. strategy:策略接口或者策略抽象类,用来约束一系列的策略算法(context 使用这个接口来调用具体的策略实现算法)\n 2. concreatestrategy:具体的策略类(实现策略接口或继承抽象策略类)\n 3. context:上下文类,持有具体策略类的实例,并负责调用相关的算法 应用策略模式来解决问题的思路\n\n\n# 实例\n\n先看看最简单的策略模式 demo:\n\n1、策略接口(定义策略)\n\npublic interface strategy {\n void operate();\n}\n\n\n1\n2\n3\n\n\n2、具体的算法实现\n\npublic class concretestrategya implements strategy {\n @override\n public void operate() {\n //具体的算法实现\n system.out.println(\"执行业务逻辑a\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\npublic class concretestrategyb implements strategy {\n @override\n public void operate() {\n //具体的算法实现\n system.out.println(\"执行业务逻辑b\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n3、上下文的实现\n\npublic class context {\n\n //持有一个具体的策略对象\n private strategy strategy;\n\n //构造方法,传入具体的策略对象\n public context(strategy strategy){\n this.strategy = strategy;\n }\n\n public void dosomething(){\n //调用具体的策略对象进操作\n strategy.operate();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n\n\n4、客户端使用(策略的使用)\n\npublic static void main(string[] args) {\n context context = new context(new concretestrategya());\n context.dosomething();\n}\n\n\n1\n2\n3\n4\n\n\n> ps:这种策略的使用方式其实很死板,真正使用的时候如果还这么写, 和写一大推 if-else 没什么区别,所以我们一般会结合工厂类, 在运行时动态确定使用哪种策略。策略模式侧重如何选择策略、工厂模式侧重如何创建策略。\n\n\n# 解析策略模式\n\n策略模式的功能就是把具体的算法实现从具体的业务处理中独立出来,把它们实现成单独的算法类,从而形成一系列算法,并让这些算法可以互相替换。\n\n> 策略模式的重心不是如何来实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活,具有更好的维护性和扩展性。\n\n实际上,每个策略算法具体实现的功能,就是原来在 if-else 结构中的具体实现,每个 if-else 语句都是一个平等的功能结构,可以说是兄弟关系。\n\n策略模式呢,就是把各个平等的具体实现封装到单独的策略实现类了,然后通过上下文与具体的策略类进行交互。\n\n『 策略模式 = 实现策略接口(或抽象类)的每个策略类 + 上下文的逻辑分派 』\n\n> 策略模式的本质:分离算法,选择实现 ——《研磨设计模式》\n\n所以说,策略模式只是在代码结构上的一个调整,即使用了策略模式,该写的逻辑一个也少不了,到逻辑分派的时候,只是变相的 if-else。\n\n而它的优化点是抽象了出了接口,将业务逻辑封装成一个一个的实现类,任意地替换。在复杂场景(业务逻辑较多)时比直接 if-else 更好维护和扩展些。\n\n\n# 谁来选择具体的策略算法\n\n如果你手写了上边的 demo,就会发现,这玩意不及 if-else 来的顺手,尤其是在判断逻辑的时候,每个逻辑都要要构造一个上下文对象,费劲。\n\n其实,策略模式中,我们可以自己定义谁来选择具体的策略算法,有两种:\n\n * 客户端:当使用上下文时,由客户端选择,像我们上边的 demo\n * 上下文:客户端不用选,由上下文来选具体的策略算法,可以在构造器中指定\n\n\n# 优缺点\n\n\n# 优点:\n\n避免多重条件语句:也就是避免大量的 if-else 更好的扩展性(完全符合开闭原则):策略模式中扩展新的策略实现很容易,无需对上下文修改,只增加新的策略实现类就可以\n\n\n# 缺点:\n\n客户必须了解每种策略的不同(这个可以通过 ioc、依赖注入的方式解决) 增加了对象数:每个具体策略都封装成了类,可能备选的策略会很多 只适合扁平的算法结构:策略模式的一系列算法是平等的,也就是在运行时刻只有一个算法会被使用,这就限制了算法使用的层级,不能嵌套使用\n\n\n# 思考\n\n实际使用中,往往不会只是单一的某个设计模式的套用,一般都会混合使用,而且模式之间的结合也是没有定势的,要具体问题具体分析。\n\n策略模式往往会结合其他模式一起使用,比如工厂、模板等,具体使用需要结合自己的业务。\n\n切记,不要为了使用设计模式而强行模式,不要把简单问题复杂化。\n\n策略模式也不是专为消除 if-else 而生的,不要和 if-else 划等号。它体现了“对修改关闭,对扩展开放“的原则。\n\n并不是说,看到 if-else 就想着用策略模式去优化,业务逻辑简单,可能几个枚举,或者几个卫语句就搞定的场景,就不用非得硬套设计模式了。\n\n\n# 策略模式在 jdk 中的应用\n\n在 jdk 中,comparator 比较器是一个策略接口,我们常用的 compare() 方法就是一个具体的策略实现,用于定义排序规则。\n\npublic interface comparator {\n int compare(t o1, t o2);\n //......\n}\n\n\n1\n2\n3\n4\n\n\n当我们想自定义排序规则的时候,就可以实现 comparator 。\n\n这时候我们重写了接口中的 compare() 方法,就是具体的策略类(只不过这里可能是内部类)。当我们在调用 arrays 的排序方法 sort() 时,可以用默认的排序规则,也可以用自定义的规则。\n\npublic static void main(string[] args) {\n integer[] data = {4,2,7,5,1,9};\n comparator comparator = new comparator() {\n @override\n public int compare(integer o1, integer o2) {\n if(o1 > o2){\n return 1;\n } else {\n return -1;\n }\n }\n };\n\n arrays.sort(data,comparator);\n system.out.println(arrays.tostring(data));\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\narrays 的 sort() 方法,有自定义规则就按自己的方法排序,反之走源码逻辑。\n\npublic static void sort(t[] a, comparator c) {\n if (c == null) {\n sort(a);\n } else {\n if (legacymergesort.userrequested)\n legacymergesort(a, c);\n else\n timsort.sort(a, 0, a.length, c, null, 0, 0);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n还有,threadpoolexecutor 中的拒绝策略 rejectedexecutionhandler 也是典型的策略模式,感兴趣的也可以再看看源码。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "模板方法模式", + "frontmatter": { + "title": "模板方法模式", + "date": "2022-05-21T17:29:10.000Z", + "permalink": "/pages/43b656/", + "categories": [ + "Java相关", + "设计模式" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/040.%E6%A8%A1%E6%9D%BF%E6%96%B9%E6%B3%95%E6%A8%A1%E5%BC%8F.html", + "relativePath": "01.Java相关/40.设计模式/040.模板方法模式.md", + "key": "v-28deb9da", + "path": "/pages/43b656/", + "headers": [ + { + "level": 2, + "title": "前言", + "slug": "前言", + "normalizedTitle": "前言", + "charIndex": 2 + }, + { + "level": 2, + "title": "场景问题", + "slug": "场景问题", + "normalizedTitle": "场景问题", + "charIndex": 94 + }, + { + "level": 2, + "title": "认识模板方法", + "slug": "认识模板方法", + "normalizedTitle": "认识模板方法", + "charIndex": 1853 + }, + { + "level": 2, + "title": "JDK 中的模板方法", + "slug": "jdk-中的模板方法", + "normalizedTitle": "jdk 中的模板方法", + "charIndex": 4308 + }, + { + "level": 2, + "title": "Spring 中的模板方法", + "slug": "spring-中的模板方法", + "normalizedTitle": "spring 中的模板方法", + "charIndex": 5336 + }, + { + "level": 2, + "title": "小总结", + "slug": "小总结", + "normalizedTitle": "小总结", + "charIndex": 7326 + }, + { + "level": 2, + "title": "使用场景", + "slug": "使用场景", + "normalizedTitle": "使用场景", + "charIndex": 7434 + } + ], + "headersStr": "前言 场景问题 认识模板方法 JDK 中的模板方法 Spring 中的模板方法 小总结 使用场景", + "content": "# 前言\n\n模板,顾名思义,它是一个固定化、标准化的东西。\n\n模板方法模式是一种行为设计模式, 它在超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。\n\n\n# 场景问题\n\n程序员不愿多扯,上来先干两行代码\n\n网上模板方法的场景示例特别多,个人感觉还是《Head First 设计模式》中的例子比较好。\n\n假设我们是一家饮品店的师傅,起码需要以下两个手艺\n\n真简单哈,这么看,步骤大同小异,我的第一反应就是写个业务接口,不同的饮品实现其中的方法就行,像这样\n\n画完类图,猛地发现,第一步和第三步没什么差别,而且做饮品是个流程式的工作,我希望使用时,直接调用一个方法,就去执行对应的制作步骤。\n\n灵机一动,不用接口了,用一个抽象父类,把步骤方法放在一个大的流程方法 makingDrinks() 中,且第一步和第三步,完全一样,没必要在子类实现,改进如下\n\n再看下我们的设计,感觉还不错,现在用同一个 makingDrinks() 方法来处理咖啡和茶的制作,而且我们不希望子类覆盖这个方法,所以可以申明为 final,不同的制作步骤,我们希望子类来提供,必须在父类申明为抽象方法,而第一步和第三步我们不希望子类重写,所以我们声明为非抽象方法\n\npublic abstract class Drinks {\n\n void boilWater() {\n System.out.println(\"将水煮沸\");\n }\n\n abstract void brew();\n\n void pourInCup() {\n System.out.println(\"倒入杯子\");\n }\n\n abstract void addCondiments();\n \n public final void makingDrinks() {\n //热水\n boilWater();\n //冲泡\n brew();\n //倒进杯子\n pourInCup();\n //加料\n addCondiments();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n\n\n接着,我们分别处理咖啡和茶,这两个类只需要继承父类,重写其中的抽象方法即可(实现各自的冲泡和添加调料)\n\npublic class Tea extends Drinks {\n @Override\n void brew() {\n System.out.println(\"冲茶叶\");\n }\n @Override\n void addCondiments() {\n System.out.println(\"加柠檬片\");\n }\n}\npublic class Coffee extends Drinks {\n @Override\n void brew() {\n System.out.println(\"冲咖啡粉\");\n }\n\n @Override\n void addCondiments() {\n System.out.println(\"加奶加糖\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n\n\n现在可以上岗了,试着制作下咖啡和茶吧\n\npublic static void main(String[] args) {\n Drinks coffee = new Coffee();\n coffee.makingDrinks();\n System.out.println();\n Drinks tea = new Tea();\n tea.makingDrinks();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n好嘞,又学会一个设计模式,这就是模板方法模式,我们的 makingDrinks() 就是模板方法。我们可以看到相同的步骤 boilWater() 和 pourInCup() 只在父类中进行即可,不同的步骤放在子类实现。\n\n\n# 认识模板方法\n\n在阎宏博士的《JAVA与模式》一书中开头是这样描述模板方法(Template Method)模式的:\n\n> 模板方法模式是类的行为模式。 准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现, 然后声明一些抽象方法来迫使子类实现剩余的逻辑。 不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。 这就是模板方法模式的用意。\n\n写代码的一个很重要的思考点就是“变与不变”,程序中哪些功能是可变的,哪些功能是不变的,我们可以把不变的部分抽象出来,进行公共的实现,把变化的部分分离出来,用接口来封装隔离,或用抽象类约束子类行为。模板方法就很好的体现了这一点。\n\n模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。\n\n模板方法模式是所有模式中最为常见的几个模式之一,是基于继承的代码复用的基本技术,我们再看下类图\n\n模板方法模式就是用来创建一个算法的模板,这个模板就是方法,该方法将算法定义成一组步骤,其中的任意步骤都可能是抽象的,由子类负责实现。这样可以确保算法的结构保持不变,同时由子类提供部分实现。\n\n再回顾下我们制作咖啡和茶的例子,有些顾客要不希望咖啡加糖或者不希望茶里加柠檬,我们要改造下模板方法,在加相应的调料之前,问下顾客\n\npublic abstract class Drinks {\n\n void boilWater() {\n System.out.println(\"将水煮沸\");\n }\n\n abstract void brew();\n\n void pourInCup() {\n System.out.println(\"倒入杯子\");\n }\n\n abstract void addCondiments();\n\n public final void makingDrinks() {\n boilWater();\n brew();\n pourInCup();\n\n //如果顾客需要,才加料\n if (customerLike()) {\n addCondiments();\n }\n }\n\n //定义一个空的缺省方法,只返回 true\n boolean customerLike() {\n return true;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n\n\n如上,我们加了一个逻辑判断,逻辑判断的方法是一个只返回 true 的方法,这个方法我们叫做 钩子方法。\n\n> 钩子:在模板方法的父类中,我们可以定义一个方法,它默认不做任何事,子类可以视情况要不要覆盖它,该方法称为“钩子”。\n\n钩子方法一般是空的或者有默认实现。钩子的存在,可以让子类有能力对算法的不同点进行挂钩。而要不要挂钩,又由子类去决定。\n\n是不是很有用呢,我们再看下咖啡的制作\n\npublic class Coffee extends Drinks {\n @Override\n void brew() {\n System.out.println(\"冲咖啡粉\");\n }\n\n @Override\n void addCondiments() {\n System.out.println(\"加奶加糖\");\n }\n\t//覆盖了钩子,提供了自己的询问功能,让用户输入是否需要加料\n boolean customerLike() {\n String answer = getUserInput();\n if (answer.toLowerCase().startsWith(\"y\")) {\n return true;\n } else {\n return false;\n }\n }\n\n //处理用户的输入\n private String getUserInput() {\n String answer = null;\n System.out.println(\"您想要加奶加糖吗?输入 YES 或 NO\");\n BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));\n try {\n answer = reader.readLine();\n } catch (IOException e) {\n e.printStackTrace();\n }\n if (answer == null) {\n return \"no\";\n }\n return answer;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n\n\n接着再去测试下代码,看看结果吧。\n\n我想你应该知道钩子的好处了吧,它可以作为条件控制,影响抽象类中的算法流程,当然也可以什么都不做。\n\n模板方法有很多种实现,有时看起来可能不是我们所谓的“中规中矩”的设计。接下来我们看下 JDK 和 Spring 中是怎么使用模板方法的。\n\n\n# JDK 中的模板方法\n\n我们写代码经常会用到 comparable 比较器来对数组对象进行排序,我们都会实现它的 compareTo() 方法,之后就可以通过 Collections.sort() 或者 Arrays.sort() 方法进行排序了。\n\n具体的实现类就不写了(可以去 github:starfish-learning 上看我的代码),看下使用\n\n@Override\npublic int compareTo(Object o) {\n Coffee coffee = (Coffee) o;\n if(this.price < (coffee.price)){\n return -1;\n }else if(this.price == coffee.price){\n return 0;\n }else{\n return 1;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\npublic static void main(String[] args) {\n Coffee[] coffees = {new Coffee(\"星冰乐\",38),\n new Coffee(\"拿铁\",32),\n new Coffee(\"摩卡\",35)};\n \n Arrays.sort(coffees);\n\n for (Coffee coffee1 : coffees) {\n System.out.println(coffee1);\n }\n\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n你可能会说,这个看着不像我们常规的模板方法,是的。我们看下比较器实现的步骤\n\n 1. 构建对象数组\n 2. 通过 Arrays.sort 方法对数组排序,传参为 Comparable 接口的实例\n 3. 比较时候会调用我们的实现类的 compareTo() 方法\n 4. 将排好序的数组设置进原数组中,排序完成 一脸懵逼,这个实现竟然也是模板方法。\n\n这个模式的重点在于提供了一个固定算法框架,并让子类实现某些步骤,虽然使用继承是标准的实现方式,但通过回调来实现,也不能说这就不是模板方法。\n\n其实并发编程中最常见,也是面试必问的 AQS 就是一个典型的模板方法。\n\n\n# Spring 中的模板方法\n\nSpring 中的设计模式太多了,而且大部分扩展功能都可以看到模板方法模式的影子。\n\n我们看下 IOC 容器初始化时的模板方法,不管是 XML 还是注解的方式,对于核心容器启动流程都是一致的。\n\nAbstractApplicationContext 的 refresh 方法实现了 IOC 容器启动的主要逻辑。\n\n一个 refresh() 方法包含了好多其他步骤方法,像不像我们说的 模板方法,getBeanFactory() 、refreshBeanFactory() 是子类必须实现的抽象方法,postProcessBeanFactory() 是钩子方法。\n\npublic abstract class AbstractApplicationContext extends DefaultResourceLoader\n implements ConfigurableApplicationContext {\n\t@Override\n\tpublic void refresh() throws BeansException, IllegalStateException {\n\t\tsynchronized (this.startupShutdownMonitor) {\n\t\t\tprepareRefresh();\n\t\t\tConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();\n\t\t\tprepareBeanFactory(beanFactory);\n postProcessBeanFactory(beanFactory);\n invokeBeanFactoryPostProcessors(beanFactory);\n registerBeanPostProcessors(beanFactory);\n initMessageSource();\n initApplicationEventMulticaster();\n onRefresh();\n registerListeners();\n finishBeanFactoryInitialization(beanFactory);\n finishRefresh();\n\t\t}\n\t}\n // 两个抽象方法\n @Override\n\tpublic abstract ConfigurableListableBeanFactory getBeanFactory() throws \t\tIllegalStateException;\t\n \n protected abstract void refreshBeanFactory() throws BeansException, IllegalStateException;\n \n //钩子方法\n protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {\n\t}\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n\n\n打开你的 IDEA,我们会发现常用的 ClassPathXmlApplicationContext 和 AnnotationConfigApplicationContext 启动入口,都是它的实现类(子类的子类的子类的...)。\n\nAbstractApplicationContext 的一个子类 AbstractRefreshableWebApplicationContext 中有钩子方法 onRefresh() 的实现:\n\npublic abstract class AbstractRefreshableWebApplicationContext extends …… {\n /**\n\t * Initialize the theme capability.\n\t */\n\t@Override\n\tprotected void onRefresh() {\n\t\tthis.themeSource = UiApplicationContextUtils.initThemeSource(this);\n\t}\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n看下大概的类图:\n\n\n# 小总结\n\n * 优点:1、封装不变部分,扩展可变部分。 2、提取公共代码,便于维护。 3、行为由父类控制,子类实现。\n\n * 缺点:每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。\n\n\n# 使用场景\n\n 1. 有多个子类共有的方法,且逻辑相同。\n 2. 重要的、复杂的方法,可以考虑作为模板方法。\n\n注意事项:为防止恶意操作,一般模板方法都加上 final 关键词。", + "normalizedContent": "# 前言\n\n模板,顾名思义,它是一个固定化、标准化的东西。\n\n模板方法模式是一种行为设计模式, 它在超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。\n\n\n# 场景问题\n\n程序员不愿多扯,上来先干两行代码\n\n网上模板方法的场景示例特别多,个人感觉还是《head first 设计模式》中的例子比较好。\n\n假设我们是一家饮品店的师傅,起码需要以下两个手艺\n\n真简单哈,这么看,步骤大同小异,我的第一反应就是写个业务接口,不同的饮品实现其中的方法就行,像这样\n\n画完类图,猛地发现,第一步和第三步没什么差别,而且做饮品是个流程式的工作,我希望使用时,直接调用一个方法,就去执行对应的制作步骤。\n\n灵机一动,不用接口了,用一个抽象父类,把步骤方法放在一个大的流程方法 makingdrinks() 中,且第一步和第三步,完全一样,没必要在子类实现,改进如下\n\n再看下我们的设计,感觉还不错,现在用同一个 makingdrinks() 方法来处理咖啡和茶的制作,而且我们不希望子类覆盖这个方法,所以可以申明为 final,不同的制作步骤,我们希望子类来提供,必须在父类申明为抽象方法,而第一步和第三步我们不希望子类重写,所以我们声明为非抽象方法\n\npublic abstract class drinks {\n\n void boilwater() {\n system.out.println(\"将水煮沸\");\n }\n\n abstract void brew();\n\n void pourincup() {\n system.out.println(\"倒入杯子\");\n }\n\n abstract void addcondiments();\n \n public final void makingdrinks() {\n //热水\n boilwater();\n //冲泡\n brew();\n //倒进杯子\n pourincup();\n //加料\n addcondiments();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n\n\n接着,我们分别处理咖啡和茶,这两个类只需要继承父类,重写其中的抽象方法即可(实现各自的冲泡和添加调料)\n\npublic class tea extends drinks {\n @override\n void brew() {\n system.out.println(\"冲茶叶\");\n }\n @override\n void addcondiments() {\n system.out.println(\"加柠檬片\");\n }\n}\npublic class coffee extends drinks {\n @override\n void brew() {\n system.out.println(\"冲咖啡粉\");\n }\n\n @override\n void addcondiments() {\n system.out.println(\"加奶加糖\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n\n\n现在可以上岗了,试着制作下咖啡和茶吧\n\npublic static void main(string[] args) {\n drinks coffee = new coffee();\n coffee.makingdrinks();\n system.out.println();\n drinks tea = new tea();\n tea.makingdrinks();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n好嘞,又学会一个设计模式,这就是模板方法模式,我们的 makingdrinks() 就是模板方法。我们可以看到相同的步骤 boilwater() 和 pourincup() 只在父类中进行即可,不同的步骤放在子类实现。\n\n\n# 认识模板方法\n\n在阎宏博士的《java与模式》一书中开头是这样描述模板方法(template method)模式的:\n\n> 模板方法模式是类的行为模式。 准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现, 然后声明一些抽象方法来迫使子类实现剩余的逻辑。 不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。 这就是模板方法模式的用意。\n\n写代码的一个很重要的思考点就是“变与不变”,程序中哪些功能是可变的,哪些功能是不变的,我们可以把不变的部分抽象出来,进行公共的实现,把变化的部分分离出来,用接口来封装隔离,或用抽象类约束子类行为。模板方法就很好的体现了这一点。\n\n模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。\n\n模板方法模式是所有模式中最为常见的几个模式之一,是基于继承的代码复用的基本技术,我们再看下类图\n\n模板方法模式就是用来创建一个算法的模板,这个模板就是方法,该方法将算法定义成一组步骤,其中的任意步骤都可能是抽象的,由子类负责实现。这样可以确保算法的结构保持不变,同时由子类提供部分实现。\n\n再回顾下我们制作咖啡和茶的例子,有些顾客要不希望咖啡加糖或者不希望茶里加柠檬,我们要改造下模板方法,在加相应的调料之前,问下顾客\n\npublic abstract class drinks {\n\n void boilwater() {\n system.out.println(\"将水煮沸\");\n }\n\n abstract void brew();\n\n void pourincup() {\n system.out.println(\"倒入杯子\");\n }\n\n abstract void addcondiments();\n\n public final void makingdrinks() {\n boilwater();\n brew();\n pourincup();\n\n //如果顾客需要,才加料\n if (customerlike()) {\n addcondiments();\n }\n }\n\n //定义一个空的缺省方法,只返回 true\n boolean customerlike() {\n return true;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n\n\n如上,我们加了一个逻辑判断,逻辑判断的方法是一个只返回 true 的方法,这个方法我们叫做 钩子方法。\n\n> 钩子:在模板方法的父类中,我们可以定义一个方法,它默认不做任何事,子类可以视情况要不要覆盖它,该方法称为“钩子”。\n\n钩子方法一般是空的或者有默认实现。钩子的存在,可以让子类有能力对算法的不同点进行挂钩。而要不要挂钩,又由子类去决定。\n\n是不是很有用呢,我们再看下咖啡的制作\n\npublic class coffee extends drinks {\n @override\n void brew() {\n system.out.println(\"冲咖啡粉\");\n }\n\n @override\n void addcondiments() {\n system.out.println(\"加奶加糖\");\n }\n\t//覆盖了钩子,提供了自己的询问功能,让用户输入是否需要加料\n boolean customerlike() {\n string answer = getuserinput();\n if (answer.tolowercase().startswith(\"y\")) {\n return true;\n } else {\n return false;\n }\n }\n\n //处理用户的输入\n private string getuserinput() {\n string answer = null;\n system.out.println(\"您想要加奶加糖吗?输入 yes 或 no\");\n bufferedreader reader = new bufferedreader(new inputstreamreader(system.in));\n try {\n answer = reader.readline();\n } catch (ioexception e) {\n e.printstacktrace();\n }\n if (answer == null) {\n return \"no\";\n }\n return answer;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n\n\n接着再去测试下代码,看看结果吧。\n\n我想你应该知道钩子的好处了吧,它可以作为条件控制,影响抽象类中的算法流程,当然也可以什么都不做。\n\n模板方法有很多种实现,有时看起来可能不是我们所谓的“中规中矩”的设计。接下来我们看下 jdk 和 spring 中是怎么使用模板方法的。\n\n\n# jdk 中的模板方法\n\n我们写代码经常会用到 comparable 比较器来对数组对象进行排序,我们都会实现它的 compareto() 方法,之后就可以通过 collections.sort() 或者 arrays.sort() 方法进行排序了。\n\n具体的实现类就不写了(可以去 github:starfish-learning 上看我的代码),看下使用\n\n@override\npublic int compareto(object o) {\n coffee coffee = (coffee) o;\n if(this.price < (coffee.price)){\n return -1;\n }else if(this.price == coffee.price){\n return 0;\n }else{\n return 1;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\npublic static void main(string[] args) {\n coffee[] coffees = {new coffee(\"星冰乐\",38),\n new coffee(\"拿铁\",32),\n new coffee(\"摩卡\",35)};\n \n arrays.sort(coffees);\n\n for (coffee coffee1 : coffees) {\n system.out.println(coffee1);\n }\n\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n你可能会说,这个看着不像我们常规的模板方法,是的。我们看下比较器实现的步骤\n\n 1. 构建对象数组\n 2. 通过 arrays.sort 方法对数组排序,传参为 comparable 接口的实例\n 3. 比较时候会调用我们的实现类的 compareto() 方法\n 4. 将排好序的数组设置进原数组中,排序完成 一脸懵逼,这个实现竟然也是模板方法。\n\n这个模式的重点在于提供了一个固定算法框架,并让子类实现某些步骤,虽然使用继承是标准的实现方式,但通过回调来实现,也不能说这就不是模板方法。\n\n其实并发编程中最常见,也是面试必问的 aqs 就是一个典型的模板方法。\n\n\n# spring 中的模板方法\n\nspring 中的设计模式太多了,而且大部分扩展功能都可以看到模板方法模式的影子。\n\n我们看下 ioc 容器初始化时的模板方法,不管是 xml 还是注解的方式,对于核心容器启动流程都是一致的。\n\nabstractapplicationcontext 的 refresh 方法实现了 ioc 容器启动的主要逻辑。\n\n一个 refresh() 方法包含了好多其他步骤方法,像不像我们说的 模板方法,getbeanfactory() 、refreshbeanfactory() 是子类必须实现的抽象方法,postprocessbeanfactory() 是钩子方法。\n\npublic abstract class abstractapplicationcontext extends defaultresourceloader\n implements configurableapplicationcontext {\n\t@override\n\tpublic void refresh() throws beansexception, illegalstateexception {\n\t\tsynchronized (this.startupshutdownmonitor) {\n\t\t\tpreparerefresh();\n\t\t\tconfigurablelistablebeanfactory beanfactory = obtainfreshbeanfactory();\n\t\t\tpreparebeanfactory(beanfactory);\n postprocessbeanfactory(beanfactory);\n invokebeanfactorypostprocessors(beanfactory);\n registerbeanpostprocessors(beanfactory);\n initmessagesource();\n initapplicationeventmulticaster();\n onrefresh();\n registerlisteners();\n finishbeanfactoryinitialization(beanfactory);\n finishrefresh();\n\t\t}\n\t}\n // 两个抽象方法\n @override\n\tpublic abstract configurablelistablebeanfactory getbeanfactory() throws \t\tillegalstateexception;\t\n \n protected abstract void refreshbeanfactory() throws beansexception, illegalstateexception;\n \n //钩子方法\n protected void postprocessbeanfactory(configurablelistablebeanfactory beanfactory) {\n\t}\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n\n\n打开你的 idea,我们会发现常用的 classpathxmlapplicationcontext 和 annotationconfigapplicationcontext 启动入口,都是它的实现类(子类的子类的子类的...)。\n\nabstractapplicationcontext 的一个子类 abstractrefreshablewebapplicationcontext 中有钩子方法 onrefresh() 的实现:\n\npublic abstract class abstractrefreshablewebapplicationcontext extends …… {\n /**\n\t * initialize the theme capability.\n\t */\n\t@override\n\tprotected void onrefresh() {\n\t\tthis.themesource = uiapplicationcontextutils.initthemesource(this);\n\t}\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n看下大概的类图:\n\n\n# 小总结\n\n * 优点:1、封装不变部分,扩展可变部分。 2、提取公共代码,便于维护。 3、行为由父类控制,子类实现。\n\n * 缺点:每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。\n\n\n# 使用场景\n\n 1. 有多个子类共有的方法,且逻辑相同。\n 2. 重要的、复杂的方法,可以考虑作为模板方法。\n\n注意事项:为防止恶意操作,一般模板方法都加上 final 关键词。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "计算机网络", + "frontmatter": { + "title": "计算机网络", + "date": "2022-03-11T00:51:01.000Z", + "permalink": "/pages/e00207/", + "categories": [ + "计算机基础", + "HTML" + ], + "tags": [ + null + ] + }, + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/01.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C.html", + "relativePath": "02.计算机基础/10.计算机网络/01.计算机网络.md", + "key": "v-1e653940", + "path": "/pages/e00207/", + "headers": [ + { + "level": 2, + "title": "简述 TCP 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手?", + "slug": "简述-tcp-三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手", + "normalizedTitle": "简述 tcp 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手?", + "charIndex": 2 + }, + { + "level": 2, + "title": "从输入 URL 到展现页面的全过程", + "slug": "从输入-url-到展现页面的全过程", + "normalizedTitle": "从输入 url 到展现页面的全过程", + "charIndex": 44 + }, + { + "level": 2, + "title": "HTTP 中 GET 和 POST 区别", + "slug": "http-中-get-和-post-区别", + "normalizedTitle": "http 中 get 和 post 区别", + "charIndex": 66 + }, + { + "level": 2, + "title": "TCP 怎么保证可靠传输", + "slug": "tcp-怎么保证可靠传输", + "normalizedTitle": "tcp 怎么保证可靠传输", + "charIndex": 91 + }, + { + "level": 2, + "title": "什么是 TCP 粘包和拆包?", + "slug": "什么是-tcp-粘包和拆包", + "normalizedTitle": "什么是 tcp 粘包和拆包?", + "charIndex": 108 }, { "level": 2, - "title": "为什么进程切换慢,线程切换快?", - "slug": "为什么进程切换慢-线程切换快", - "normalizedTitle": "为什么进程切换慢,线程切换快?", - "charIndex": 355 + "title": "简述 HTTPS 的加密与认证过程", + "slug": "简述-https-的加密与认证过程", + "normalizedTitle": "简述 https 的加密与认证过程", + "charIndex": 127 }, { "level": 2, - "title": "简述创建进程的流程", - "slug": "简述创建进程的流程", - "normalizedTitle": "简述创建进程的流程", - "charIndex": 375 + "title": "简述对称与非对称加密的概念", + "slug": "简述对称与非对称加密的概念", + "normalizedTitle": "简述对称与非对称加密的概念", + "charIndex": 149 }, { "level": 2, - "title": "简述 Linux 虚拟内存的页面置换算法", - "slug": "简述-linux-虚拟内存的页面置换算法", - "normalizedTitle": "简述 linux 虚拟内存的页面置换算法", - "charIndex": 389 + "title": "TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别?", + "slug": "tcp-与-udp-在网络协议中的哪一层-他们之间有什么区别", + "normalizedTitle": "tcp 与 udp 在网络协议中的哪一层,他们之间有什么区别?", + "charIndex": 167 }, { "level": 2, - "title": "简述 Linux 进程调度的算法", - "slug": "简述-linux-进程调度的算法", - "normalizedTitle": "简述 linux 进程调度的算法", - "charIndex": 414 + "title": "如何解决 TCP 传输丢包问题?", + "slug": "如何解决-tcp-传输丢包问题", + "normalizedTitle": "如何解决 tcp 传输丢包问题?", + "charIndex": 203 }, { "level": 2, - "title": "什么时候会由用户态陷入内核态?", - "slug": "什么时候会由用户态陷入内核态", - "normalizedTitle": "什么时候会由用户态陷入内核态?", - "charIndex": 435 + "title": "TCP 长连接和短连接有那么不同的使用场景?", + "slug": "tcp-长连接和短连接有那么不同的使用场景", + "normalizedTitle": "tcp 长连接和短连接有那么不同的使用场景?", + "charIndex": 224 + }, + { + "level": 2, + "title": "TCP 的 keepalive 了解吗?说一说它和 HTTP 的 keepalive 的区别?", + "slug": "tcp-的-keepalive-了解吗-说一说它和-http-的-keepalive-的区别", + "normalizedTitle": "tcp 的 keepalive 了解吗?说一说它和 http 的 keepalive 的区别?", + "charIndex": 251 + }, + { + "level": 2, + "title": "HTTP 与 HTTPS 有哪些区别?", + "slug": "http-与-https-有哪些区别", + "normalizedTitle": "http 与 https 有哪些区别?", + "charIndex": 303 + }, + { + "level": 2, + "title": "TCP 中常见的拥塞控制算法有哪些?", + "slug": "tcp-中常见的拥塞控制算法有哪些", + "normalizedTitle": "tcp 中常见的拥塞控制算法有哪些?", + "charIndex": 327 + }, + { + "level": 2, + "title": "简述 HTTP 1.0,1.1,2.0 的主要区别", + "slug": "简述-http-1-0-1-1-2-0-的主要区别", + "normalizedTitle": "简述 http 1.0,1.1,2.0 的主要区别", + "charIndex": 350 + }, + { + "level": 2, + "title": "从系统层面上,UDP如何保证尽量可靠?", + "slug": "从系统层面上-udp如何保证尽量可靠", + "normalizedTitle": "从系统层面上,udp如何保证尽量可靠?", + "charIndex": 380 + }, + { + "level": 2, + "title": "TCP 挥手时出现大量 CLOSEWAIT 或 TIMEWAIT 怎么解决?", + "slug": "tcp-挥手时出现大量-close-wait-或-time-wait-怎么解决", + "normalizedTitle": "tcp 挥手时出现大量 closewait 或 timewait 怎么解决?", + "charIndex": null + }, + { + "level": 2, + "title": "简述 TCP 滑动窗口以及重传机制", + "slug": "简述-tcp-滑动窗口以及重传机制", + "normalizedTitle": "简述 tcp 滑动窗口以及重传机制", + "charIndex": 449 + }, + { + "level": 2, + "title": "简述 JWT 的原理和校验机制", + "slug": "简述-jwt-的原理和校验机制", + "normalizedTitle": "简述 jwt 的原理和校验机制", + "charIndex": 471 + }, + { + "level": 2, + "title": "为什么需要序列化?有什么序列化的方式?", + "slug": "为什么需要序列化-有什么序列化的方式", + "normalizedTitle": "为什么需要序列化?有什么序列化的方式?", + "charIndex": 491 + }, + { + "level": 2, + "title": "简述 iPv4 和 iPv6 的区别", + "slug": "简述-ipv4-和-ipv6-的区别", + "normalizedTitle": "简述 ipv4 和 ipv6 的区别", + "charIndex": 515 + }, + { + "level": 2, + "title": "TCP 中 SYN 攻击是什么?如何防止?", + "slug": "tcp-中-syn-攻击是什么-如何防止", + "normalizedTitle": "tcp 中 syn 攻击是什么?如何防止?", + "charIndex": 538 + }, + { + "level": 2, + "title": "简述 DDOS 攻击原理,如何防范它?", + "slug": "简述-ddos-攻击原理-如何防范它", + "normalizedTitle": "简述 ddos 攻击原理,如何防范它?", + "charIndex": 564 + }, + { + "level": 2, + "title": "什么是 ARP 协议?简述其使用场景", + "slug": "什么是-arp-协议-简述其使用场景", + "normalizedTitle": "什么是 arp 协议?简述其使用场景", + "charIndex": 588 + }, + { + "level": 2, + "title": "什么是跨域,什么情况下会发生跨域请求?", + "slug": "什么是跨域-什么情况下会发生跨域请求", + "normalizedTitle": "什么是跨域,什么情况下会发生跨域请求?", + "charIndex": 611 + }, + { + "level": 2, + "title": "Cookie 和 Session 的关系和区别是什么?", + "slug": "cookie-和-session-的关系和区别是什么", + "normalizedTitle": "cookie 和 session 的关系和区别是什么?", + "charIndex": 635 + }, + { + "level": 2, + "title": "简述 WebSocket 是如何进行传输的", + "slug": "简述-websocket-是如何进行传输的", + "normalizedTitle": "简述 websocket 是如何进行传输的", + "charIndex": 667 + }, + { + "level": 2, + "title": "DNS 查询服务器的基本流程是什么?DNS 劫持是什么?", + "slug": "dns-查询服务器的基本流程是什么-dns-劫持是什么", + "normalizedTitle": "dns 查询服务器的基本流程是什么?dns 劫持是什么?", + "charIndex": 693 + }, + { + "level": 2, + "title": "RestFul 是什么?RestFul 请求的 URL 有什么特点?", + "slug": "restful-是什么-restful-请求的-url-有什么特点", + "normalizedTitle": "restful 是什么?restful 请求的 url 有什么特点?", + "charIndex": 726 + }, + { + "level": 2, + "title": "简述 HTTP 报文头部的组成结构", + "slug": "简述-http-报文头部的组成结构", + "normalizedTitle": "简述 http 报文头部的组成结构", + "charIndex": 765 + }, + { + "level": 2, + "title": "什么是 SYN flood,如何防止这类攻击?", + "slug": "什么是-syn-flood-如何防止这类攻击", + "normalizedTitle": "什么是 syn flood,如何防止这类攻击?", + "charIndex": 787 + } + ], + "headersStr": "简述 TCP 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手? 从输入 URL 到展现页面的全过程 HTTP 中 GET 和 POST 区别 TCP 怎么保证可靠传输 什么是 TCP 粘包和拆包? 简述 HTTPS 的加密与认证过程 简述对称与非对称加密的概念 TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别? 如何解决 TCP 传输丢包问题? TCP 长连接和短连接有那么不同的使用场景? TCP 的 keepalive 了解吗?说一说它和 HTTP 的 keepalive 的区别? HTTP 与 HTTPS 有哪些区别? TCP 中常见的拥塞控制算法有哪些? 简述 HTTP 1.0,1.1,2.0 的主要区别 从系统层面上,UDP如何保证尽量可靠? TCP 挥手时出现大量 CLOSEWAIT 或 TIMEWAIT 怎么解决? 简述 TCP 滑动窗口以及重传机制 简述 JWT 的原理和校验机制 为什么需要序列化?有什么序列化的方式? 简述 iPv4 和 iPv6 的区别 TCP 中 SYN 攻击是什么?如何防止? 简述 DDOS 攻击原理,如何防范它? 什么是 ARP 协议?简述其使用场景 什么是跨域,什么情况下会发生跨域请求? Cookie 和 Session 的关系和区别是什么? 简述 WebSocket 是如何进行传输的 DNS 查询服务器的基本流程是什么?DNS 劫持是什么? RestFul 是什么?RestFul 请求的 URL 有什么特点? 简述 HTTP 报文头部的组成结构 什么是 SYN flood,如何防止这类攻击?", + "content": "# 简述 TCP 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手?\n\n\n# 从输入 URL 到展现页面的全过程\n\n\n# HTTP 中 GET 和 POST 区别\n\n\n# TCP 怎么保证可靠传输\n\n\n# 什么是 TCP 粘包和拆包?\n\n\n# 简述 HTTPS 的加密与认证过程\n\n\n# 简述对称与非对称加密的概念\n\n\n# TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别?\n\n\n# 如何解决 TCP 传输丢包问题?\n\n\n# TCP 长连接和短连接有那么不同的使用场景?\n\n\n# TCP 的 keepalive 了解吗?说一说它和 HTTP 的 keepalive 的区别?\n\n\n# HTTP 与 HTTPS 有哪些区别?\n\n\n# TCP 中常见的拥塞控制算法有哪些?\n\n\n# 简述 HTTP 1.0,1.1,2.0 的主要区别\n\n\n# 从系统层面上,UDP如何保证尽量可靠?\n\n\n# TCP 挥手时出现大量 CLOSE_WAIT 或 TIME_WAIT 怎么解决?\n\n\n# 简述 TCP 滑动窗口以及重传机制\n\n\n# 简述 JWT 的原理和校验机制\n\n\n# 为什么需要序列化?有什么序列化的方式?\n\n\n# 简述 iPv4 和 iPv6 的区别\n\n\n# TCP 中 SYN 攻击是什么?如何防止?\n\n\n# 简述 DDOS 攻击原理,如何防范它?\n\n\n# 什么是 ARP 协议?简述其使用场景\n\n\n# 什么是跨域,什么情况下会发生跨域请求?\n\n\n# Cookie 和 Session 的关系和区别是什么?\n\n\n# 简述 WebSocket 是如何进行传输的\n\n\n# DNS 查询服务器的基本流程是什么?DNS 劫持是什么?\n\n\n# RestFul 是什么?RestFul 请求的 URL 有什么特点?\n\n\n# 简述 HTTP 报文头部的组成结构\n\n\n# 什么是 SYN flood,如何防止这类攻击?", + "normalizedContent": "# 简述 tcp 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手?\n\n\n# 从输入 url 到展现页面的全过程\n\n\n# http 中 get 和 post 区别\n\n\n# tcp 怎么保证可靠传输\n\n\n# 什么是 tcp 粘包和拆包?\n\n\n# 简述 https 的加密与认证过程\n\n\n# 简述对称与非对称加密的概念\n\n\n# tcp 与 udp 在网络协议中的哪一层,他们之间有什么区别?\n\n\n# 如何解决 tcp 传输丢包问题?\n\n\n# tcp 长连接和短连接有那么不同的使用场景?\n\n\n# tcp 的 keepalive 了解吗?说一说它和 http 的 keepalive 的区别?\n\n\n# http 与 https 有哪些区别?\n\n\n# tcp 中常见的拥塞控制算法有哪些?\n\n\n# 简述 http 1.0,1.1,2.0 的主要区别\n\n\n# 从系统层面上,udp如何保证尽量可靠?\n\n\n# tcp 挥手时出现大量 close_wait 或 time_wait 怎么解决?\n\n\n# 简述 tcp 滑动窗口以及重传机制\n\n\n# 简述 jwt 的原理和校验机制\n\n\n# 为什么需要序列化?有什么序列化的方式?\n\n\n# 简述 ipv4 和 ipv6 的区别\n\n\n# tcp 中 syn 攻击是什么?如何防止?\n\n\n# 简述 ddos 攻击原理,如何防范它?\n\n\n# 什么是 arp 协议?简述其使用场景\n\n\n# 什么是跨域,什么情况下会发生跨域请求?\n\n\n# cookie 和 session 的关系和区别是什么?\n\n\n# 简述 websocket 是如何进行传输的\n\n\n# dns 查询服务器的基本流程是什么?dns 劫持是什么?\n\n\n# restful 是什么?restful 请求的 url 有什么特点?\n\n\n# 简述 http 报文头部的组成结构\n\n\n# 什么是 syn flood,如何防止这类攻击?", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/18, 21:28:02", + "lastUpdatedTimestamp": 1663507682000 + }, + { + "title": "观察模式", + "frontmatter": { + "title": "观察模式", + "date": "2022-05-21T17:29:45.000Z", + "permalink": "/pages/cbff3c/", + "categories": [ + "Java相关", + "设计模式" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/060.%E8%A7%82%E5%AF%9F%E6%A8%A1%E5%BC%8F.html", + "relativePath": "01.Java相关/40.设计模式/060.观察模式.md", + "key": "v-64f1031c", + "path": "/pages/cbff3c/", + "headers": [ + { + "level": 2, + "title": "定义", + "slug": "定义", + "normalizedTitle": "定义", + "charIndex": 344 + }, + { + "level": 2, + "title": "角色", + "slug": "角色", + "normalizedTitle": "角色", + "charIndex": 594 + }, + { + "level": 2, + "title": "类图", + "slug": "类图", + "normalizedTitle": "类图", + "charIndex": 630 + }, + { + "level": 2, + "title": "实例", + "slug": "实例", + "normalizedTitle": "实例", + "charIndex": 1386 + }, + { + "level": 2, + "title": "优缺点", + "slug": "优缺点", + "normalizedTitle": "优缺点", + "charIndex": 2856 + }, + { + "level": 3, + "title": "优点", + "slug": "优点", + "normalizedTitle": "优点", + "charIndex": 2864 + }, + { + "level": 3, + "title": "缺点", + "slug": "缺点", + "normalizedTitle": "缺点", + "charIndex": 2857 + }, + { + "level": 2, + "title": "应用", + "slug": "应用", + "normalizedTitle": "应用", + "charIndex": 3010 + }, + { + "level": 3, + "title": "JDK 中的观察者模式", + "slug": "jdk-中的观察者模式", + "normalizedTitle": "jdk 中的观察者模式", + "charIndex": 3017 }, { - "level": 2, - "title": "Linux 下如何查看端口被哪个进程占用?", - "slug": "linux-下如何查看端口被哪个进程占用", - "normalizedTitle": "linux 下如何查看端口被哪个进程占用?", - "charIndex": 455 + "level": 3, + "title": "Spring 中的观察者模式", + "slug": "spring-中的观察者模式", + "normalizedTitle": "spring 中的观察者模式", + "charIndex": 3164 }, { - "level": 2, - "title": "Linux 中虚拟内存和物理内存有什么区别?有什么优点?", - "slug": "linux-中虚拟内存和物理内存有什么区别-有什么优点", - "normalizedTitle": "linux 中虚拟内存和物理内存有什么区别?有什么优点?", - "charIndex": 481 + "level": 4, + "title": "1. Spring 中观察者模式的四个角色", + "slug": "_1-spring-中观察者模式的四个角色", + "normalizedTitle": "1. spring 中观察者模式的四个角色", + "charIndex": 3228 }, { - "level": 2, - "title": "简述 traceroute 命令的原理", - "slug": "简述-traceroute-命令的原理", - "normalizedTitle": "简述 traceroute 命令的原理", - "charIndex": 514 + "level": 4, + "title": "2. coding~~~~~~", + "slug": "_2-coding", + "normalizedTitle": "2. coding~~~~~~", + "charIndex": 3951 } ], - "headersStr": "进程间有哪些通信方式? 简述自旋锁与互斥锁的使用场景 进程和线程之间有什么区别? 简述几个常用的 Linux 命令以及他们的功能 创建线程有多少种方式? 线程有多少种状态,状态之间如何转换 进程有多少种状态? 简述 select, poll, epoll 的使用场景以及区别,epoll 中水平触发以及边缘触发有什么不同? Linux 下如何排查 CPU 以及 内存占用过多? 进程通信中的管道实现原理是什么? Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程? 如何调试服务器内存占用过高的问题? Linux 如何查看实时的滚动日志? 简述 Linux 零拷贝的原理 为什么进程切换慢,线程切换快? 简述创建进程的流程 简述 Linux 虚拟内存的页面置换算法 简述 Linux 进程调度的算法 什么时候会由用户态陷入内核态? Linux 下如何查看端口被哪个进程占用? Linux 中虚拟内存和物理内存有什么区别?有什么优点? 简述 traceroute 命令的原理", - "content": "# 进程间有哪些通信方式?\n\n\n# 简述自旋锁与互斥锁的使用场景\n\n\n# 进程和线程之间有什么区别?\n\n\n# 简述几个常用的 Linux 命令以及他们的功能\n\n\n# 创建线程有多少种方式?\n\n\n# 线程有多少种状态,状态之间如何转换\n\n\n# 进程有多少种状态?\n\n\n# 简述 select, poll, epoll 的使用场景以及区别,epoll 中水平触发以及边缘触发有什么不同?\n\n\n# Linux 下如何排查 CPU 以及 内存占用过多?\n\n\n# 进程通信中的管道实现原理是什么?\n\n\n# Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程?\n\n\n# 如何调试服务器内存占用过高的问题?\n\n\n# Linux 如何查看实时的滚动日志?\n\n\n# 简述 Linux 零拷贝的原理\n\n\n# 为什么进程切换慢,线程切换快?\n\n\n# 简述创建进程的流程\n\n\n# 简述 Linux 虚拟内存的页面置换算法\n\n\n# 简述 Linux 进程调度的算法\n\n\n# 什么时候会由用户态陷入内核态?\n\n\n# Linux 下如何查看端口被哪个进程占用?\n\n\n# Linux 中虚拟内存和物理内存有什么区别?有什么优点?\n\n\n# 简述 traceroute 命令的原理", - "normalizedContent": "# 进程间有哪些通信方式?\n\n\n# 简述自旋锁与互斥锁的使用场景\n\n\n# 进程和线程之间有什么区别?\n\n\n# 简述几个常用的 linux 命令以及他们的功能\n\n\n# 创建线程有多少种方式?\n\n\n# 线程有多少种状态,状态之间如何转换\n\n\n# 进程有多少种状态?\n\n\n# 简述 select, poll, epoll 的使用场景以及区别,epoll 中水平触发以及边缘触发有什么不同?\n\n\n# linux 下如何排查 cpu 以及 内存占用过多?\n\n\n# 进程通信中的管道实现原理是什么?\n\n\n# linux 下如何查看 cpu 荷载,正在运行的进程,某个端口对应的进程?\n\n\n# 如何调试服务器内存占用过高的问题?\n\n\n# linux 如何查看实时的滚动日志?\n\n\n# 简述 linux 零拷贝的原理\n\n\n# 为什么进程切换慢,线程切换快?\n\n\n# 简述创建进程的流程\n\n\n# 简述 linux 虚拟内存的页面置换算法\n\n\n# 简述 linux 进程调度的算法\n\n\n# 什么时候会由用户态陷入内核态?\n\n\n# linux 下如何查看端口被哪个进程占用?\n\n\n# linux 中虚拟内存和物理内存有什么区别?有什么优点?\n\n\n# 简述 traceroute 命令的原理", + "headersStr": "定义 角色 类图 实例 优缺点 优点 缺点 应用 JDK 中的观察者模式 Spring 中的观察者模式 1. Spring 中观察者模式的四个角色 2. coding~~~~~~", + "content": "在软件系统中经常会有这样的需求:如果一个对象的状态发生改变,某些与它相关的对象也要随之做出相应的变化。 举个例子:\n\n * 微信公众号,如果一个用户订阅了某个公众号,那么便会收到公众号发来的消息,那么,公众号就是『被观察者』,而用户就是『观察者』\n * 气象站可以将每天预测到的温度、湿度、气压等以公告的形式发布给各种第三方网站,如果天气数据有更新,要能够实时的通知给第三方,这里的气象局就是『被观察者』,第三方网站就是『观察者』\n * MVC 模式中的模型与视图的关系也属于观察与被观察关系\n\n观察者模式是使用频率较高的设计模式之一。\n\n观察者模式包含观察目标和观察者两类对象,一个目标可以有任意数目的与之相依赖的观察者,一旦观察目标的状态发生改变,所有的观察者都将得到通知。\n\n\n# 定义\n\n观察者模式(Observer Pattern): 定义对象间一种一对多的依赖关系,使得当每一个对象改变状态,则所有依赖于它的对象都会得到通知并自动更新。\n\n观察者模式是一种对象行为型模式。\n\n观察者模式的别名包括发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。\n\n细究的话,发布订阅和观察者有些不同,可以理解成发布订阅模式属于广义上的观察者模式。\n\n\n\n\n# 角色\n\n * Subject(目标):被观察者,它是指被观察的对象。 从类图中可以看到,类中有一个用来存放观察者对象的Vector 容器(Vector在是安全的,而List则是不安全的),这个 Vector 容器是被观察者类的核心,另外还有三个方法:attach 方法是向这个容器中添加观察者对象;detach 方法是从容器中移除观察者对象;notify 方法是依次调用观察者对象的对应方法。这个角色可以是接口,也可以是抽象类或者具体的类,因为很多情况下会与其他的模式混用,所以使用抽象类的情况比较多。\n\n * ConcreteSubject(具体目标):具体目标是目标类的子类,通常它包含经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知。同时它还实现了在目标类中定义的抽象业务逻辑方法(如果有的话)。如果无须扩展目标类,则具体目标类可以省略。\n\n * Observer(观察者):观察者将对观察目标的改变做出反应,观察者一般定义为接口,该接口声明了更新数据的方法 update(),因此又称为抽象观察者。\n\n * ConcreteObserver(具体观察者):在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者 Observer 中定义的 update() 方法。通常在实现时,可以调用具体目标类的 attach() 方法将自己添加到目标类的集合中或通过 detach() 方法将自己从目标类的集合中删除。\n\n\n# 类图\n\n\n\n再记录下 UML 类图的注意事项,这里我的 Subject 是抽象方法,所以用***斜体***,抽象方法也要用斜体,具体的各种箭头意义,我之前也总结过《设计模式前传——学设计模式前你要知道这些》(被网上各种帖子毒害过的自己,认真记录~~~)。\n\n\n# 实例\n\n1、定义观察者接口\n\ninterface Observer {\n public void update();\n}\n\n\n1\n2\n3\n\n\n2、定义被观察者\n\nabstract class Subject {\n private Vector obs = new Vector();\n\n public void addObserver(Observer obs){\n this.obs.add(obs);\n }\n public void delObserver(Observer obs){\n this.obs.remove(obs);\n }\n protected void notifyObserver(){\n for(Observer o: obs){\n o.update();\n }\n }\n public abstract void doSomething();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\n3、具体的被观察者\n\nclass ConcreteSubject extends Subject {\n public void doSomething(){\n System.out.println(\"被观察者事件发生改变\");\n this.notifyObserver();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n4、具体的观察者\n\nclass ConcreteObserver1 implements Observer {\n public void update() {\n System.out.println(\"观察者1收到信息,并进行处理\");\n }\n}\nclass ConcreteObserver2 implements Observer {\n public void update() {\n System.out.println(\"观察者2收到信息,并进行处理\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n5、客户端\n\npublic class Client {\n public static void main(String[] args){\n Subject sub = new ConcreteSubject();\n sub.addObserver(new ConcreteObserver1()); //添加观察者1\n sub.addObserver(new ConcreteObserver2()); //添加观察者2\n sub.doSomething();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\n输出\n\n被观察者事件发生改变\n观察者1收到信息,并进行处理\n观察者2收到信息,并进行处理\n\n\n1\n2\n3\n\n\n通过运行结果可以看到,我们只调用了 Subject 的方法,但同时两个观察者的相关方法都被调用了。仔细看一下代码,其实很简单,就是在 Subject 类中关联一下 Observer 类,并且在 doSomething() 方法中遍历一下 Observer 的 update() 方法就行了。\n\n\n# 优缺点\n\n\n# 优点\n\n降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系 目标与观察者之间建立了一套触发机制 支持广播通信 符合“开闭原则”的要求\n\n\n# 缺点\n\n目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率\n\n\n# 应用\n\n\n# JDK 中的观察者模式\n\n观察者模式在 Java 语言中的地位非常重要。在 JDK 的 java.util 包中,提供了 Observable 类以及 Observer 接口,它们构成了 JDK 对观察者模式的支持(可以去查看下源码,写的比较严谨)。but,在 Java9 被弃用了。\n\n\n# Spring 中的观察者模式\n\nSpring 事件驱动模型也是观察者模式很经典的应用。就是我们项目中最常见的事件监听器。\n\n# 1. Spring 中观察者模式的四个角色\n\n * 事件:ApplicationEvent 是所有事件对象的父类。ApplicationEvent 继承自 jdk 的 EventObject,所有的事件都需要继承 ApplicationEvent,并且通过 source 得到事件源。\n\n * Spring 也为我们提供了很多内置事件,ContextRefreshedEvent、ContextStartedEvent、ContextStoppedEvent、ContextClosedEvent、RequestHandledEvent。\n\n * 事件监听:ApplicationListener,也就是观察者,继承自 jdk 的 EventListener,该类中只有一个方法 onApplicationEvent。当监听的事件发生后该方法会被执行。\n\n * 事件源:ApplicationContext,ApplicationContext 是 Spring 中的核心容器,在事件监听中 ApplicationContext 可以作为事件的发布者,也就是事件源。因为 ApplicationContext 继承自 ApplicationEventPublisher。在 ApplicationEventPublisher 中定义了事件发布的方法:publishEvent(Object event)\n\n * 事件管理:ApplicationEventMulticaster,用于事件监听器的注册和事件的广播。监听器的注册就是通过它来实现的,它的作用是把 Applicationcontext 发布的 Event 广播给它的监听器列表。\n\n# 2. coding~~~~~~\n\n1、定义事件\n\npublic class MyEvent extends ApplicationEvent {\n public MyEvent(Object source) {\n super(source);\n System.out.println(\"my Event\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n2、实现事件监听器\n\n@Component\nclass MyListenerA implements ApplicationListener {\n public void onApplicationEvent(MyEvent AyEvent) {\n System.out.println(\"ListenerA received\");\n }\n}\n\n@Component\nclass MyListenerB implements ApplicationListener {\n public void onApplicationEvent(MyEvent AyEvent) {\n System.out.println(\"ListenerB received\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n3、事件发布者\n\n@Component\npublic class MyPublisher implements ApplicationContextAware {\n private ApplicationContext applicationContext;\n \n public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {\n this.applicationContext=applicationContext;\n }\n \n public void publishEvent(ApplicationEvent event){\n System.out.println(\"publish event\");\n applicationContext.publishEvent(event);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n4、测试,先用注解方式将 MyPublisher 注入 Spring\n\n@Configuration\n@ComponentScan\npublic class AppConfig {\n\n @Bean(name = \"myPublisher\")\n public MyPublisher myPublisher(){\n return new MyPublisher();\n }\n}\npublic class Client {\n\n @Test\n public void main() {\n ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);\n MyPublisher myPublisher = (MyPublisher) context.getBean(\"myPublisher\");\n myPublisher.publishEvent(new MyEvent(this));\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n\n\n5、输出\n\nmy Event\npublish event\nListenerA received\nListenerB received\n\n\n1\n2\n3\n4\n", + "normalizedContent": "在软件系统中经常会有这样的需求:如果一个对象的状态发生改变,某些与它相关的对象也要随之做出相应的变化。 举个例子:\n\n * 微信公众号,如果一个用户订阅了某个公众号,那么便会收到公众号发来的消息,那么,公众号就是『被观察者』,而用户就是『观察者』\n * 气象站可以将每天预测到的温度、湿度、气压等以公告的形式发布给各种第三方网站,如果天气数据有更新,要能够实时的通知给第三方,这里的气象局就是『被观察者』,第三方网站就是『观察者』\n * mvc 模式中的模型与视图的关系也属于观察与被观察关系\n\n观察者模式是使用频率较高的设计模式之一。\n\n观察者模式包含观察目标和观察者两类对象,一个目标可以有任意数目的与之相依赖的观察者,一旦观察目标的状态发生改变,所有的观察者都将得到通知。\n\n\n# 定义\n\n观察者模式(observer pattern): 定义对象间一种一对多的依赖关系,使得当每一个对象改变状态,则所有依赖于它的对象都会得到通知并自动更新。\n\n观察者模式是一种对象行为型模式。\n\n观察者模式的别名包括发布-订阅(publish/subscribe)模式、模型-视图(model/view)模式、源-监听器(source/listener)模式或从属者(dependents)模式。\n\n细究的话,发布订阅和观察者有些不同,可以理解成发布订阅模式属于广义上的观察者模式。\n\n\n\n\n# 角色\n\n * subject(目标):被观察者,它是指被观察的对象。 从类图中可以看到,类中有一个用来存放观察者对象的vector 容器(vector在是安全的,而list则是不安全的),这个 vector 容器是被观察者类的核心,另外还有三个方法:attach 方法是向这个容器中添加观察者对象;detach 方法是从容器中移除观察者对象;notify 方法是依次调用观察者对象的对应方法。这个角色可以是接口,也可以是抽象类或者具体的类,因为很多情况下会与其他的模式混用,所以使用抽象类的情况比较多。\n\n * concretesubject(具体目标):具体目标是目标类的子类,通常它包含经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知。同时它还实现了在目标类中定义的抽象业务逻辑方法(如果有的话)。如果无须扩展目标类,则具体目标类可以省略。\n\n * observer(观察者):观察者将对观察目标的改变做出反应,观察者一般定义为接口,该接口声明了更新数据的方法 update(),因此又称为抽象观察者。\n\n * concreteobserver(具体观察者):在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者 observer 中定义的 update() 方法。通常在实现时,可以调用具体目标类的 attach() 方法将自己添加到目标类的集合中或通过 detach() 方法将自己从目标类的集合中删除。\n\n\n# 类图\n\n\n\n再记录下 uml 类图的注意事项,这里我的 subject 是抽象方法,所以用***斜体***,抽象方法也要用斜体,具体的各种箭头意义,我之前也总结过《设计模式前传——学设计模式前你要知道这些》(被网上各种帖子毒害过的自己,认真记录~~~)。\n\n\n# 实例\n\n1、定义观察者接口\n\ninterface observer {\n public void update();\n}\n\n\n1\n2\n3\n\n\n2、定义被观察者\n\nabstract class subject {\n private vector obs = new vector();\n\n public void addobserver(observer obs){\n this.obs.add(obs);\n }\n public void delobserver(observer obs){\n this.obs.remove(obs);\n }\n protected void notifyobserver(){\n for(observer o: obs){\n o.update();\n }\n }\n public abstract void dosomething();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\n3、具体的被观察者\n\nclass concretesubject extends subject {\n public void dosomething(){\n system.out.println(\"被观察者事件发生改变\");\n this.notifyobserver();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n4、具体的观察者\n\nclass concreteobserver1 implements observer {\n public void update() {\n system.out.println(\"观察者1收到信息,并进行处理\");\n }\n}\nclass concreteobserver2 implements observer {\n public void update() {\n system.out.println(\"观察者2收到信息,并进行处理\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n5、客户端\n\npublic class client {\n public static void main(string[] args){\n subject sub = new concretesubject();\n sub.addobserver(new concreteobserver1()); //添加观察者1\n sub.addobserver(new concreteobserver2()); //添加观察者2\n sub.dosomething();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\n输出\n\n被观察者事件发生改变\n观察者1收到信息,并进行处理\n观察者2收到信息,并进行处理\n\n\n1\n2\n3\n\n\n通过运行结果可以看到,我们只调用了 subject 的方法,但同时两个观察者的相关方法都被调用了。仔细看一下代码,其实很简单,就是在 subject 类中关联一下 observer 类,并且在 dosomething() 方法中遍历一下 observer 的 update() 方法就行了。\n\n\n# 优缺点\n\n\n# 优点\n\n降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系 目标与观察者之间建立了一套触发机制 支持广播通信 符合“开闭原则”的要求\n\n\n# 缺点\n\n目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率\n\n\n# 应用\n\n\n# jdk 中的观察者模式\n\n观察者模式在 java 语言中的地位非常重要。在 jdk 的 java.util 包中,提供了 observable 类以及 observer 接口,它们构成了 jdk 对观察者模式的支持(可以去查看下源码,写的比较严谨)。but,在 java9 被弃用了。\n\n\n# spring 中的观察者模式\n\nspring 事件驱动模型也是观察者模式很经典的应用。就是我们项目中最常见的事件监听器。\n\n# 1. spring 中观察者模式的四个角色\n\n * 事件:applicationevent 是所有事件对象的父类。applicationevent 继承自 jdk 的 eventobject,所有的事件都需要继承 applicationevent,并且通过 source 得到事件源。\n\n * spring 也为我们提供了很多内置事件,contextrefreshedevent、contextstartedevent、contextstoppedevent、contextclosedevent、requesthandledevent。\n\n * 事件监听:applicationlistener,也就是观察者,继承自 jdk 的 eventlistener,该类中只有一个方法 onapplicationevent。当监听的事件发生后该方法会被执行。\n\n * 事件源:applicationcontext,applicationcontext 是 spring 中的核心容器,在事件监听中 applicationcontext 可以作为事件的发布者,也就是事件源。因为 applicationcontext 继承自 applicationeventpublisher。在 applicationeventpublisher 中定义了事件发布的方法:publishevent(object event)\n\n * 事件管理:applicationeventmulticaster,用于事件监听器的注册和事件的广播。监听器的注册就是通过它来实现的,它的作用是把 applicationcontext 发布的 event 广播给它的监听器列表。\n\n# 2. coding~~~~~~\n\n1、定义事件\n\npublic class myevent extends applicationevent {\n public myevent(object source) {\n super(source);\n system.out.println(\"my event\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n2、实现事件监听器\n\n@component\nclass mylistenera implements applicationlistener {\n public void onapplicationevent(myevent ayevent) {\n system.out.println(\"listenera received\");\n }\n}\n\n@component\nclass mylistenerb implements applicationlistener {\n public void onapplicationevent(myevent ayevent) {\n system.out.println(\"listenerb received\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n3、事件发布者\n\n@component\npublic class mypublisher implements applicationcontextaware {\n private applicationcontext applicationcontext;\n \n public void setapplicationcontext(applicationcontext applicationcontext) throws beansexception {\n this.applicationcontext=applicationcontext;\n }\n \n public void publishevent(applicationevent event){\n system.out.println(\"publish event\");\n applicationcontext.publishevent(event);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n4、测试,先用注解方式将 mypublisher 注入 spring\n\n@configuration\n@componentscan\npublic class appconfig {\n\n @bean(name = \"mypublisher\")\n public mypublisher mypublisher(){\n return new mypublisher();\n }\n}\npublic class client {\n\n @test\n public void main() {\n applicationcontext context = new annotationconfigapplicationcontext(appconfig.class);\n mypublisher mypublisher = (mypublisher) context.getbean(\"mypublisher\");\n mypublisher.publishevent(new myevent(this));\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n\n\n5、输出\n\nmy event\npublish event\nlistenera received\nlistenerb received\n\n\n1\n2\n3\n4\n", "charsets": { "cjk": true }, - "lastUpdated": "2022/06/20, 22:47:41", - "lastUpdatedTimestamp": 1655736461000 + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 }, { "title": "简述常见的 HTTP 状态码的含义", @@ -3698,467 +4090,428 @@ export const siteData = { "lastUpdatedTimestamp": 1663507682000 }, { - "title": "算法", - "frontmatter": { - "title": "算法", - "date": "2022-03-11T00:38:43.000Z", - "permalink": "/pages/92288a/", - "categories": [ - "计算机基础", - "CSS" - ], - "tags": [ - null - ] - }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/40.%E7%AE%97%E6%B3%95/01.%E7%AE%97%E6%B3%95.html", - "relativePath": "02.计算机基础/40.算法/01.算法.md", - "key": "v-2e6d42b4", - "path": "/pages/92288a/", - "headers": [ - { - "level": 2, - "title": "数据结构与算法知识", - "slug": "数据结构与算法知识", - "normalizedTitle": "数据结构与算法知识", - "charIndex": 2 - }, - { - "level": 2, - "title": "全排列", - "slug": "全排列", - "normalizedTitle": "全排列", - "charIndex": 16 - }, - { - "level": 2, - "title": "贪心算法", - "slug": "贪心算法", - "normalizedTitle": "贪心算法", - "charIndex": 24 - }, - { - "level": 2, - "title": "KMP算法", - "slug": "kmp算法", - "normalizedTitle": "kmp算法", - "charIndex": 33 - }, - { - "level": 2, - "title": "hash算法", - "slug": "hash算法", - "normalizedTitle": "hash算法", - "charIndex": 43 - }, - { - "level": 2, - "title": "海量数据处理", - "slug": "海量数据处理", - "normalizedTitle": "海量数据处理", - "charIndex": 54 - } - ], - "headersStr": "数据结构与算法知识 全排列 贪心算法 KMP算法 hash算法 海量数据处理", - "content": "# 数据结构与算法知识\n\n\n# 全排列\n\n\n# 贪心算法\n\n\n# KMP算法\n\n\n# hash算法\n\n\n# 海量数据处理\n\n* 分治\n* hash映射\n* 堆排序\n* 双层桶划分\n* Bloom Filter\n* bitmap\n* 数据库索引\n* mapreduce等。", - "normalizedContent": "# 数据结构与算法知识\n\n\n# 全排列\n\n\n# 贪心算法\n\n\n# kmp算法\n\n\n# hash算法\n\n\n# 海量数据处理\n\n* 分治\n* hash映射\n* 堆排序\n* 双层桶划分\n* bloom filter\n* bitmap\n* 数据库索引\n* mapreduce等。", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/06/20, 22:47:41", - "lastUpdatedTimestamp": 1655736461000 - }, - { - "title": "数据结构", + "title": "从输入 URL 到展现页面的全过程", "frontmatter": { - "title": "数据结构", - "date": "2022-03-11T00:38:43.000Z", - "permalink": "/pages/a25c3a/", + "title": "从输入 URL 到展现页面的全过程", + "date": "2022-06-23T21:10:44.000Z", + "permalink": "/pages/ab48ba/", "categories": [ "计算机基础", - "CSS" + "计算机网络" ], "tags": [ null ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/30.%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/01.%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.html", - "relativePath": "02.计算机基础/30.数据结构/01.数据结构.md", - "key": "v-49688930", - "path": "/pages/a25c3a/", + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/020.%E4%BB%8E%E8%BE%93%E5%85%A5%20URL%20%E5%88%B0%E5%B1%95%E7%8E%B0%E9%A1%B5%E9%9D%A2%E7%9A%84%E5%85%A8%E8%BF%87%E7%A8%8B.html", + "relativePath": "02.计算机基础/10.计算机网络/020.从输入 URL 到展现页面的全过程.md", + "key": "v-6602e3da", + "path": "/pages/ab48ba/", "headers": [ { "level": 2, - "title": "简单的数据结构", - "slug": "简单的数据结构", - "normalizedTitle": "简单的数据结构", - "charIndex": 2 - }, - { - "level": 2, - "title": "树", - "slug": "树", - "normalizedTitle": "树", - "charIndex": 96 + "title": "什么是IP地址", + "slug": "什么是ip地址", + "normalizedTitle": "什么是ip地址", + "charIndex": 459 }, { "level": 2, - "title": "堆", - "slug": "堆", - "normalizedTitle": "堆", - "charIndex": 164 + "title": "什么是DNS", + "slug": "什么是dns", + "normalizedTitle": "什么是dns", + "charIndex": 608 }, { "level": 2, - "title": "图", - "slug": "图", - "normalizedTitle": "图", - "charIndex": 185 + "title": "DNS解析", + "slug": "dns解析-2", + "normalizedTitle": "dns解析", + "charIndex": 57 }, { "level": 2, - "title": "稳定的排序算法", - "slug": "稳定的排序算法", - "normalizedTitle": "稳定的排序算法", - "charIndex": 212 + "title": "DNS查询的两种方式:递归查询和迭代查询", + "slug": "dns查询的两种方式-递归查询和迭代查询", + "normalizedTitle": "dns查询的两种方式:递归查询和迭代查询", + "charIndex": 752 }, { "level": 2, - "title": "不稳定的排序算法", - "slug": "不稳定的排序算法", - "normalizedTitle": "不稳定的排序算法", - "charIndex": 322 + "title": "DNS域名称空间的组织方式", + "slug": "dns域名称空间的组织方式", + "normalizedTitle": "dns域名称空间的组织方式", + "charIndex": 788 }, { "level": 2, - "title": "各种排序算法和时间复杂度", - "slug": "各种排序算法和时间复杂度", - "normalizedTitle": "各种排序算法和时间复杂度", - "charIndex": 404 + "title": "DNS负载均衡", + "slug": "dns负载均衡", + "normalizedTitle": "dns负载均衡", + "charIndex": 808 }, { "level": 2, - "title": "深度优先和广度优先搜索", - "slug": "深度优先和广度优先搜索", - "normalizedTitle": "深度优先和广度优先搜索", - "charIndex": 421 - } - ], - "headersStr": "简单的数据结构 树 堆 图 稳定的排序算法 不稳定的排序算法 各种排序算法和时间复杂度 深度优先和广度优先搜索", - "content": "# 简单的数据结构\n\n* 栈\n* 队列\n\n* 链表\n\n* 数组\n\n* 哈希表\n\n* 栈和队列的相同和不同之处\n\n* 栈通常采用的两种存储结构\n\n* 两个栈实现队列,和两个队列实现栈\n\n\n\n# 树\n\n* 二叉树\n\n* 字典树\n\n* 平衡树\n\n* 排序树\n\n* B树\n\n* B+树\n\n* R树\n\n* 多路树\n\n* 红黑树\n\n\n\n# 堆\n\n* 大根堆\n\n* 小根堆\n\n\n\n# 图\n\n* 有向图\n\n* 无向图\n\n* 拓扑\n\n\n\n# 稳定的排序算法\n\n* 冒泡排序\n* 插入排序\n* 鸡尾酒排序\n* 桶排序\n* 计数排序\n* 归并排序\n* 原地归并排序\n* 二叉排序树排序\n* 鸽巢排序\n* 基数排序\n* 侏儒排序\n* 图书馆排序\n* 块排序\n\n\n\n# 不稳定的排序算法\n\n* 选择排序\n* 希尔排序\n* Clover排序算法\n* 梳排序\n* 堆排序\n* 平滑排序\n* 快速排序\n* 内省排序\n* 耐心排序\n\n\n\n# 各种排序算法和时间复杂度\n\n\n# 深度优先和广度优先搜索", - "normalizedContent": "# 简单的数据结构\n\n* 栈\n* 队列\n\n* 链表\n\n* 数组\n\n* 哈希表\n\n* 栈和队列的相同和不同之处\n\n* 栈通常采用的两种存储结构\n\n* 两个栈实现队列,和两个队列实现栈\n\n\n\n# 树\n\n* 二叉树\n\n* 字典树\n\n* 平衡树\n\n* 排序树\n\n* b树\n\n* b+树\n\n* r树\n\n* 多路树\n\n* 红黑树\n\n\n\n# 堆\n\n* 大根堆\n\n* 小根堆\n\n\n\n# 图\n\n* 有向图\n\n* 无向图\n\n* 拓扑\n\n\n\n# 稳定的排序算法\n\n* 冒泡排序\n* 插入排序\n* 鸡尾酒排序\n* 桶排序\n* 计数排序\n* 归并排序\n* 原地归并排序\n* 二叉排序树排序\n* 鸽巢排序\n* 基数排序\n* 侏儒排序\n* 图书馆排序\n* 块排序\n\n\n\n# 不稳定的排序算法\n\n* 选择排序\n* 希尔排序\n* clover排序算法\n* 梳排序\n* 堆排序\n* 平滑排序\n* 快速排序\n* 内省排序\n* 耐心排序\n\n\n\n# 各种排序算法和时间复杂度\n\n\n# 深度优先和广度优先搜索", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/06/20, 22:47:41", - "lastUpdatedTimestamp": 1655736461000 - }, - { - "title": "MySQL中 InnoDB 和 MylSAM 的区别是什么?", - "frontmatter": { - "title": "MySQL中 InnoDB 和 MylSAM 的区别是什么?", - "date": "2022-05-21T17:07:47.000Z", - "permalink": "/pages/3a8659/", - "categories": [ - "数据库", - "MYSQL相关" - ], - "tags": [ - null - ] - }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/001.MySQL%E4%B8%AD%20InnoDB%20%E5%92%8C%20MylSAM%20%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/001.MySQL中 InnoDB 和 MylSAM 的区别是什么?.md", - "key": "v-674454ac", - "path": "/pages/3a8659/", - "headers": [ - { - "level": 2, - "title": "区别", - "slug": "区别", - "normalizedTitle": "区别", - "charIndex": 37 + "title": "”三次握手”的详解", + "slug": "三次握手-的详解", + "normalizedTitle": "”三次握手”的详解", + "charIndex": 1033 }, { "level": 2, - "title": "场景选择", - "slug": "场景选择", - "normalizedTitle": "场景选择", - "charIndex": 253 + "title": "为什么要三次握手", + "slug": "为什么要三次握手", + "normalizedTitle": "为什么要三次握手", + "charIndex": 1049 } ], - "headersStr": "区别 场景选择", - "content": "MySQL5.5版本开始Innodb已经成为Mysql的默认引擎\n\n\n# 区别\n\n * InnoDB 支持事务,MyISAM 不支持事务。 这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一\n * MyISAM锁的粒度是表级的,而InnoDB支持行级锁\n * MyISAM支持全文类型索引,而InnoDB不支持全文索引\n * MyISAM相对简单,效率上要优于InnoDB,小型应用可以考虑使用MyISAM\n * MyISAM表保存成文件形式,跨平台使用更加方便\n\n\n# 场景选择\n\nMyISAM管理非事务表,提供高速存储和检索以及全文搜索能力,如果再应用中执行大量select操作,应该选择MyISAM InnoDB用于事务处理,具有ACID事务支持等特性,如果在应用中执行大量insert和update操作,应该选择InnoDB", - "normalizedContent": "mysql5.5版本开始innodb已经成为mysql的默认引擎\n\n\n# 区别\n\n * innodb 支持事务,myisam 不支持事务。 这是 mysql 将默认存储引擎从 myisam 变成 innodb 的重要原因之一\n * myisam锁的粒度是表级的,而innodb支持行级锁\n * myisam支持全文类型索引,而innodb不支持全文索引\n * myisam相对简单,效率上要优于innodb,小型应用可以考虑使用myisam\n * myisam表保存成文件形式,跨平台使用更加方便\n\n\n# 场景选择\n\nmyisam管理非事务表,提供高速存储和检索以及全文搜索能力,如果再应用中执行大量select操作,应该选择myisam innodb用于事务处理,具有acid事务支持等特性,如果在应用中执行大量insert和update操作,应该选择innodb", + "headersStr": "什么是IP地址 什么是DNS DNS解析 DNS查询的两种方式:递归查询和迭代查询 DNS域名称空间的组织方式 DNS负载均衡 ”三次握手”的详解 为什么要三次握手", + "content": "从我们输入一个网址到页面展现的过程到底发生了什么呢?\n\n\n# 总体来说分为以下几个过程:\n\n * 输入网址\n * DNS解析\n * 建立TCP/IP链接\n * 发送HTTP请求\n * 服务器处理请求\n * 服务器返回HTTP响应\n * 浏览器渲染页面并展现\n * 断开连接\n\n\n# 输入网址\n\n当我们想要打开一个页面时,很简单的会想到去输入一个网址 我们以本网址为例,解释一下它各部分的组成:\n\nhttps://www.baidu.com\n\n\n1\n\n\n * https :https是协议(Protocol),它表明了浏览器使用何种协议,有http、https、ftp、mailto等,其中https是加密认证版的http(详见《图解HTTP》)\n\n * www.baidu.com :这是域名(Domain name),它表明正在请求哪个Web服务器,其中www是主机名(host)\n\n\n# DNS解析\n\n输入网址(URL)这一步我们很好理解,但是接下来所谓的域名解析是什么呢?又是为什么要进行域名解析呢?\n\n\n# 什么是IP地址\n\nIP 地址(Internet Protocol Address)是互联网协议特有的一种地址,它是 IP 协议提供的一种统一的地址格式。IP 地址为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。 像192.168.59.0就是一个主机IP地址\n\n\n# 什么是DNS\n\nDNS服务器上存储着映射<域名-IP>,DNS解析就是去DNS服务器查询IP地址。\n\n\n# DNS解析\n\nDNS解析按照下面顺序逐个查找。\n\n * 本地(hosts文件)\n * DNS服务器(本地DNS | 根DNS)\n * 域服务器\n * 解析服务器的地址\n\n\n\n\n# DNS查询的两种方式:递归查询和迭代查询\n\n递归解析 迭代解析\n\n\n# DNS域名称空间的组织方式\n\n\n\n\n# DNS负载均衡\n\n当一个网站有足够多的用户的时候,假如每次请求的资源都位于同一台机器上面,那么这台机器随时可能会蹦掉。\n\n处理办法就是用DNS负载均衡技术,它的原理是在DNS服务器中为同一个主机名配置多个IP地址 在应答DNS查询时,DNS服务器对每个查询将以DNS文件中主机记录的IP地址按顺序返回不同的解析结果,将客户端的访问引导到不同的机器上去,使得不同的客户端访问不同的服务器,从而达到负载均衡的目的。\n\n\n# 建立TCP/IP链接\n\n\n# ”三次握手”的详解\n\n\n\n\n# 为什么要三次握手\n\n * clien端 防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。 client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致连接释放以后的某个时间才到达server。 本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。 于是就向client发出确认报文段,同意建立连接。\n\n * server端 假设不采用“三次握手”。 如果只要server发出确认,新的连接就建立了。并一直等待client发来数据。 事实是client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。 但server端就会有许多这样的等待,server的很多资源就白白浪费掉了。\n\n采用“三次握手”的办法可以防止上述现象发生。主要目的防止server端一直等待,浪费资源。", + "normalizedContent": "从我们输入一个网址到页面展现的过程到底发生了什么呢?\n\n\n# 总体来说分为以下几个过程:\n\n * 输入网址\n * dns解析\n * 建立tcp/ip链接\n * 发送http请求\n * 服务器处理请求\n * 服务器返回http响应\n * 浏览器渲染页面并展现\n * 断开连接\n\n\n# 输入网址\n\n当我们想要打开一个页面时,很简单的会想到去输入一个网址 我们以本网址为例,解释一下它各部分的组成:\n\nhttps://www.baidu.com\n\n\n1\n\n\n * https :https是协议(protocol),它表明了浏览器使用何种协议,有http、https、ftp、mailto等,其中https是加密认证版的http(详见《图解http》)\n\n * www.baidu.com :这是域名(domain name),它表明正在请求哪个web服务器,其中www是主机名(host)\n\n\n# dns解析\n\n输入网址(url)这一步我们很好理解,但是接下来所谓的域名解析是什么呢?又是为什么要进行域名解析呢?\n\n\n# 什么是ip地址\n\nip 地址(internet protocol address)是互联网协议特有的一种地址,它是 ip 协议提供的一种统一的地址格式。ip 地址为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。 像192.168.59.0就是一个主机ip地址\n\n\n# 什么是dns\n\ndns服务器上存储着映射<域名-ip>,dns解析就是去dns服务器查询ip地址。\n\n\n# dns解析\n\ndns解析按照下面顺序逐个查找。\n\n * 本地(hosts文件)\n * dns服务器(本地dns | 根dns)\n * 域服务器\n * 解析服务器的地址\n\n\n\n\n# dns查询的两种方式:递归查询和迭代查询\n\n递归解析 迭代解析\n\n\n# dns域名称空间的组织方式\n\n\n\n\n# dns负载均衡\n\n当一个网站有足够多的用户的时候,假如每次请求的资源都位于同一台机器上面,那么这台机器随时可能会蹦掉。\n\n处理办法就是用dns负载均衡技术,它的原理是在dns服务器中为同一个主机名配置多个ip地址 在应答dns查询时,dns服务器对每个查询将以dns文件中主机记录的ip地址按顺序返回不同的解析结果,将客户端的访问引导到不同的机器上去,使得不同的客户端访问不同的服务器,从而达到负载均衡的目的。\n\n\n# 建立tcp/ip链接\n\n\n# ”三次握手”的详解\n\n\n\n\n# 为什么要三次握手\n\n * clien端 防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。 client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致连接释放以后的某个时间才到达server。 本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。 于是就向client发出确认报文段,同意建立连接。\n\n * server端 假设不采用“三次握手”。 如果只要server发出确认,新的连接就建立了。并一直等待client发来数据。 事实是client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。 但server端就会有许多这样的等待,server的很多资源就白白浪费掉了。\n\n采用“三次握手”的办法可以防止上述现象发生。主要目的防止server端一直等待,浪费资源。", "charsets": { "cjk": true }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "简述 MySQL 常见索引类型,介绍一下覆盖索引", - "frontmatter": { - "title": "简述 MySQL 常见索引类型,介绍一下覆盖索引", - "date": "2022-05-21T17:08:24.000Z", - "permalink": "/pages/17b014/", - "categories": [ - "数据库", - "MYSQL相关" - ], - "tags": [ - null - ] - }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/007.%E7%AE%80%E8%BF%B0%20MySQL%20%E5%B8%B8%E8%A7%81%E7%B4%A2%E5%BC%95%E7%B1%BB%E5%9E%8B%EF%BC%8C%E4%BB%8B%E7%BB%8D%E4%B8%80%E4%B8%8B%E8%A6%86%E7%9B%96%E7%B4%A2%E5%BC%95.html", - "relativePath": "03.数据库/01.MYSQL相关/007.简述 MySQL 常见索引类型,介绍一下覆盖索引.md", - "key": "v-824b776c", - "path": "/pages/17b014/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 + "lastUpdated": "2022/09/18, 21:28:02", + "lastUpdatedTimestamp": 1663507682000 }, { - "title": "计算机网络", + "title": "操作系统", "frontmatter": { - "title": "计算机网络", - "date": "2022-03-11T00:51:01.000Z", - "permalink": "/pages/e00207/", + "title": "操作系统", + "date": "2022-03-11T00:51:48.000Z", + "permalink": "/pages/e5f2d7/", "categories": [ "计算机基础", - "HTML" + "CSS" ], "tags": [ null ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/01.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C.html", - "relativePath": "02.计算机基础/10.计算机网络/01.计算机网络.md", - "key": "v-1e653940", - "path": "/pages/e00207/", + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/01.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F.html", + "relativePath": "02.计算机基础/20.操作系统/01.操作系统.md", + "key": "v-6f178cbc", + "path": "/pages/e5f2d7/", "headers": [ { "level": 2, - "title": "简述 TCP 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手?", - "slug": "简述-tcp-三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手", - "normalizedTitle": "简述 tcp 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手?", + "title": "进程间有哪些通信方式?", + "slug": "进程间有哪些通信方式", + "normalizedTitle": "进程间有哪些通信方式?", "charIndex": 2 }, { "level": 2, - "title": "从输入 URL 到展现页面的全过程", - "slug": "从输入-url-到展现页面的全过程", - "normalizedTitle": "从输入 url 到展现页面的全过程", - "charIndex": 44 + "title": "简述自旋锁与互斥锁的使用场景", + "slug": "简述自旋锁与互斥锁的使用场景", + "normalizedTitle": "简述自旋锁与互斥锁的使用场景", + "charIndex": 18 }, { "level": 2, - "title": "HTTP 中 GET 和 POST 区别", - "slug": "http-中-get-和-post-区别", - "normalizedTitle": "http 中 get 和 post 区别", - "charIndex": 66 + "title": "进程和线程之间有什么区别?", + "slug": "进程和线程之间有什么区别", + "normalizedTitle": "进程和线程之间有什么区别?", + "charIndex": 37 }, { "level": 2, - "title": "TCP 怎么保证可靠传输", - "slug": "tcp-怎么保证可靠传输", - "normalizedTitle": "tcp 怎么保证可靠传输", - "charIndex": 91 + "title": "简述几个常用的 Linux 命令以及他们的功能", + "slug": "简述几个常用的-linux-命令以及他们的功能", + "normalizedTitle": "简述几个常用的 linux 命令以及他们的功能", + "charIndex": 55 }, { "level": 2, - "title": "什么是 TCP 粘包和拆包?", - "slug": "什么是-tcp-粘包和拆包", - "normalizedTitle": "什么是 tcp 粘包和拆包?", - "charIndex": 108 + "title": "创建线程有多少种方式?", + "slug": "创建线程有多少种方式", + "normalizedTitle": "创建线程有多少种方式?", + "charIndex": 83 }, { "level": 2, - "title": "简述 HTTPS 的加密与认证过程", - "slug": "简述-https-的加密与认证过程", - "normalizedTitle": "简述 https 的加密与认证过程", - "charIndex": 127 + "title": "线程有多少种状态,状态之间如何转换", + "slug": "线程有多少种状态-状态之间如何转换", + "normalizedTitle": "线程有多少种状态,状态之间如何转换", + "charIndex": 99 }, { "level": 2, - "title": "简述对称与非对称加密的概念", - "slug": "简述对称与非对称加密的概念", - "normalizedTitle": "简述对称与非对称加密的概念", - "charIndex": 149 + "title": "进程有多少种状态?", + "slug": "进程有多少种状态", + "normalizedTitle": "进程有多少种状态?", + "charIndex": 121 }, { "level": 2, - "title": "TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别?", - "slug": "tcp-与-udp-在网络协议中的哪一层-他们之间有什么区别", - "normalizedTitle": "tcp 与 udp 在网络协议中的哪一层,他们之间有什么区别?", - "charIndex": 167 + "title": "简述 select, poll, epoll 的使用场景以及区别,epoll 中水平触发以及边缘触发有什么不同?", + "slug": "简述-select-poll-epoll-的使用场景以及区别-epoll-中水平触发以及边缘触发有什么不同", + "normalizedTitle": "简述 select, poll, epoll 的使用场景以及区别,epoll 中水平触发以及边缘触发有什么不同?", + "charIndex": 135 }, { "level": 2, - "title": "如何解决 TCP 传输丢包问题?", - "slug": "如何解决-tcp-传输丢包问题", - "normalizedTitle": "如何解决 tcp 传输丢包问题?", - "charIndex": 203 + "title": "Linux 下如何排查 CPU 以及 内存占用过多?", + "slug": "linux-下如何排查-cpu-以及-内存占用过多", + "normalizedTitle": "linux 下如何排查 cpu 以及 内存占用过多?", + "charIndex": 196 }, { "level": 2, - "title": "TCP 长连接和短连接有那么不同的使用场景?", - "slug": "tcp-长连接和短连接有那么不同的使用场景", - "normalizedTitle": "tcp 长连接和短连接有那么不同的使用场景?", - "charIndex": 224 + "title": "进程通信中的管道实现原理是什么?", + "slug": "进程通信中的管道实现原理是什么", + "normalizedTitle": "进程通信中的管道实现原理是什么?", + "charIndex": 227 }, { "level": 2, - "title": "TCP 的 keepalive 了解吗?说一说它和 HTTP 的 keepalive 的区别?", - "slug": "tcp-的-keepalive-了解吗-说一说它和-http-的-keepalive-的区别", - "normalizedTitle": "tcp 的 keepalive 了解吗?说一说它和 http 的 keepalive 的区别?", - "charIndex": 251 + "title": "Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程?", + "slug": "linux-下如何查看-cpu-荷载-正在运行的进程-某个端口对应的进程", + "normalizedTitle": "linux 下如何查看 cpu 荷载,正在运行的进程,某个端口对应的进程?", + "charIndex": 248 }, { "level": 2, - "title": "HTTP 与 HTTPS 有哪些区别?", - "slug": "http-与-https-有哪些区别", - "normalizedTitle": "http 与 https 有哪些区别?", - "charIndex": 303 + "title": "如何调试服务器内存占用过高的问题?", + "slug": "如何调试服务器内存占用过高的问题", + "normalizedTitle": "如何调试服务器内存占用过高的问题?", + "charIndex": 290 }, { "level": 2, - "title": "TCP 中常见的拥塞控制算法有哪些?", - "slug": "tcp-中常见的拥塞控制算法有哪些", - "normalizedTitle": "tcp 中常见的拥塞控制算法有哪些?", - "charIndex": 327 + "title": "Linux 如何查看实时的滚动日志?", + "slug": "linux-如何查看实时的滚动日志", + "normalizedTitle": "linux 如何查看实时的滚动日志?", + "charIndex": 312 }, { "level": 2, - "title": "简述 HTTP 1.0,1.1,2.0 的主要区别", - "slug": "简述-http-1-0-1-1-2-0-的主要区别", - "normalizedTitle": "简述 http 1.0,1.1,2.0 的主要区别", - "charIndex": 350 + "title": "简述 Linux 零拷贝的原理", + "slug": "简述-linux-零拷贝的原理", + "normalizedTitle": "简述 linux 零拷贝的原理", + "charIndex": 335 }, { "level": 2, - "title": "从系统层面上,UDP如何保证尽量可靠?", - "slug": "从系统层面上-udp如何保证尽量可靠", - "normalizedTitle": "从系统层面上,udp如何保证尽量可靠?", - "charIndex": 380 + "title": "为什么进程切换慢,线程切换快?", + "slug": "为什么进程切换慢-线程切换快", + "normalizedTitle": "为什么进程切换慢,线程切换快?", + "charIndex": 355 }, { "level": 2, - "title": "TCP 挥手时出现大量 CLOSEWAIT 或 TIMEWAIT 怎么解决?", - "slug": "tcp-挥手时出现大量-close-wait-或-time-wait-怎么解决", - "normalizedTitle": "tcp 挥手时出现大量 closewait 或 timewait 怎么解决?", - "charIndex": null + "title": "简述创建进程的流程", + "slug": "简述创建进程的流程", + "normalizedTitle": "简述创建进程的流程", + "charIndex": 375 }, { "level": 2, - "title": "简述 TCP 滑动窗口以及重传机制", - "slug": "简述-tcp-滑动窗口以及重传机制", - "normalizedTitle": "简述 tcp 滑动窗口以及重传机制", - "charIndex": 449 + "title": "简述 Linux 虚拟内存的页面置换算法", + "slug": "简述-linux-虚拟内存的页面置换算法", + "normalizedTitle": "简述 linux 虚拟内存的页面置换算法", + "charIndex": 389 }, { "level": 2, - "title": "简述 JWT 的原理和校验机制", - "slug": "简述-jwt-的原理和校验机制", - "normalizedTitle": "简述 jwt 的原理和校验机制", - "charIndex": 471 + "title": "简述 Linux 进程调度的算法", + "slug": "简述-linux-进程调度的算法", + "normalizedTitle": "简述 linux 进程调度的算法", + "charIndex": 414 }, { "level": 2, - "title": "为什么需要序列化?有什么序列化的方式?", - "slug": "为什么需要序列化-有什么序列化的方式", - "normalizedTitle": "为什么需要序列化?有什么序列化的方式?", - "charIndex": 491 + "title": "什么时候会由用户态陷入内核态?", + "slug": "什么时候会由用户态陷入内核态", + "normalizedTitle": "什么时候会由用户态陷入内核态?", + "charIndex": 435 }, { "level": 2, - "title": "简述 iPv4 和 iPv6 的区别", - "slug": "简述-ipv4-和-ipv6-的区别", - "normalizedTitle": "简述 ipv4 和 ipv6 的区别", - "charIndex": 515 + "title": "Linux 下如何查看端口被哪个进程占用?", + "slug": "linux-下如何查看端口被哪个进程占用", + "normalizedTitle": "linux 下如何查看端口被哪个进程占用?", + "charIndex": 455 }, { "level": 2, - "title": "TCP 中 SYN 攻击是什么?如何防止?", - "slug": "tcp-中-syn-攻击是什么-如何防止", - "normalizedTitle": "tcp 中 syn 攻击是什么?如何防止?", - "charIndex": 538 + "title": "Linux 中虚拟内存和物理内存有什么区别?有什么优点?", + "slug": "linux-中虚拟内存和物理内存有什么区别-有什么优点", + "normalizedTitle": "linux 中虚拟内存和物理内存有什么区别?有什么优点?", + "charIndex": 481 }, { "level": 2, - "title": "简述 DDOS 攻击原理,如何防范它?", - "slug": "简述-ddos-攻击原理-如何防范它", - "normalizedTitle": "简述 ddos 攻击原理,如何防范它?", - "charIndex": 564 + "title": "简述 traceroute 命令的原理", + "slug": "简述-traceroute-命令的原理", + "normalizedTitle": "简述 traceroute 命令的原理", + "charIndex": 514 + } + ], + "headersStr": "进程间有哪些通信方式? 简述自旋锁与互斥锁的使用场景 进程和线程之间有什么区别? 简述几个常用的 Linux 命令以及他们的功能 创建线程有多少种方式? 线程有多少种状态,状态之间如何转换 进程有多少种状态? 简述 select, poll, epoll 的使用场景以及区别,epoll 中水平触发以及边缘触发有什么不同? Linux 下如何排查 CPU 以及 内存占用过多? 进程通信中的管道实现原理是什么? Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程? 如何调试服务器内存占用过高的问题? Linux 如何查看实时的滚动日志? 简述 Linux 零拷贝的原理 为什么进程切换慢,线程切换快? 简述创建进程的流程 简述 Linux 虚拟内存的页面置换算法 简述 Linux 进程调度的算法 什么时候会由用户态陷入内核态? Linux 下如何查看端口被哪个进程占用? Linux 中虚拟内存和物理内存有什么区别?有什么优点? 简述 traceroute 命令的原理", + "content": "# 进程间有哪些通信方式?\n\n\n# 简述自旋锁与互斥锁的使用场景\n\n\n# 进程和线程之间有什么区别?\n\n\n# 简述几个常用的 Linux 命令以及他们的功能\n\n\n# 创建线程有多少种方式?\n\n\n# 线程有多少种状态,状态之间如何转换\n\n\n# 进程有多少种状态?\n\n\n# 简述 select, poll, epoll 的使用场景以及区别,epoll 中水平触发以及边缘触发有什么不同?\n\n\n# Linux 下如何排查 CPU 以及 内存占用过多?\n\n\n# 进程通信中的管道实现原理是什么?\n\n\n# Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程?\n\n\n# 如何调试服务器内存占用过高的问题?\n\n\n# Linux 如何查看实时的滚动日志?\n\n\n# 简述 Linux 零拷贝的原理\n\n\n# 为什么进程切换慢,线程切换快?\n\n\n# 简述创建进程的流程\n\n\n# 简述 Linux 虚拟内存的页面置换算法\n\n\n# 简述 Linux 进程调度的算法\n\n\n# 什么时候会由用户态陷入内核态?\n\n\n# Linux 下如何查看端口被哪个进程占用?\n\n\n# Linux 中虚拟内存和物理内存有什么区别?有什么优点?\n\n\n# 简述 traceroute 命令的原理", + "normalizedContent": "# 进程间有哪些通信方式?\n\n\n# 简述自旋锁与互斥锁的使用场景\n\n\n# 进程和线程之间有什么区别?\n\n\n# 简述几个常用的 linux 命令以及他们的功能\n\n\n# 创建线程有多少种方式?\n\n\n# 线程有多少种状态,状态之间如何转换\n\n\n# 进程有多少种状态?\n\n\n# 简述 select, poll, epoll 的使用场景以及区别,epoll 中水平触发以及边缘触发有什么不同?\n\n\n# linux 下如何排查 cpu 以及 内存占用过多?\n\n\n# 进程通信中的管道实现原理是什么?\n\n\n# linux 下如何查看 cpu 荷载,正在运行的进程,某个端口对应的进程?\n\n\n# 如何调试服务器内存占用过高的问题?\n\n\n# linux 如何查看实时的滚动日志?\n\n\n# 简述 linux 零拷贝的原理\n\n\n# 为什么进程切换慢,线程切换快?\n\n\n# 简述创建进程的流程\n\n\n# 简述 linux 虚拟内存的页面置换算法\n\n\n# 简述 linux 进程调度的算法\n\n\n# 什么时候会由用户态陷入内核态?\n\n\n# linux 下如何查看端口被哪个进程占用?\n\n\n# linux 中虚拟内存和物理内存有什么区别?有什么优点?\n\n\n# 简述 traceroute 命令的原理", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/06/20, 22:47:41", + "lastUpdatedTimestamp": 1655736461000 + }, + { + "title": "数据结构", + "frontmatter": { + "title": "数据结构", + "date": "2022-03-11T00:38:43.000Z", + "permalink": "/pages/a25c3a/", + "categories": [ + "计算机基础", + "CSS" + ], + "tags": [ + null + ] + }, + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/30.%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/01.%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.html", + "relativePath": "02.计算机基础/30.数据结构/01.数据结构.md", + "key": "v-49688930", + "path": "/pages/a25c3a/", + "headers": [ + { + "level": 2, + "title": "简单的数据结构", + "slug": "简单的数据结构", + "normalizedTitle": "简单的数据结构", + "charIndex": 2 + }, + { + "level": 2, + "title": "树", + "slug": "树", + "normalizedTitle": "树", + "charIndex": 96 + }, + { + "level": 2, + "title": "堆", + "slug": "堆", + "normalizedTitle": "堆", + "charIndex": 164 + }, + { + "level": 2, + "title": "图", + "slug": "图", + "normalizedTitle": "图", + "charIndex": 185 + }, + { + "level": 2, + "title": "稳定的排序算法", + "slug": "稳定的排序算法", + "normalizedTitle": "稳定的排序算法", + "charIndex": 212 }, { "level": 2, - "title": "什么是 ARP 协议?简述其使用场景", - "slug": "什么是-arp-协议-简述其使用场景", - "normalizedTitle": "什么是 arp 协议?简述其使用场景", - "charIndex": 588 + "title": "不稳定的排序算法", + "slug": "不稳定的排序算法", + "normalizedTitle": "不稳定的排序算法", + "charIndex": 322 }, { "level": 2, - "title": "什么是跨域,什么情况下会发生跨域请求?", - "slug": "什么是跨域-什么情况下会发生跨域请求", - "normalizedTitle": "什么是跨域,什么情况下会发生跨域请求?", - "charIndex": 611 + "title": "各种排序算法和时间复杂度", + "slug": "各种排序算法和时间复杂度", + "normalizedTitle": "各种排序算法和时间复杂度", + "charIndex": 404 }, { "level": 2, - "title": "Cookie 和 Session 的关系和区别是什么?", - "slug": "cookie-和-session-的关系和区别是什么", - "normalizedTitle": "cookie 和 session 的关系和区别是什么?", - "charIndex": 635 + "title": "深度优先和广度优先搜索", + "slug": "深度优先和广度优先搜索", + "normalizedTitle": "深度优先和广度优先搜索", + "charIndex": 421 + } + ], + "headersStr": "简单的数据结构 树 堆 图 稳定的排序算法 不稳定的排序算法 各种排序算法和时间复杂度 深度优先和广度优先搜索", + "content": "# 简单的数据结构\n\n* 栈\n* 队列\n\n* 链表\n\n* 数组\n\n* 哈希表\n\n* 栈和队列的相同和不同之处\n\n* 栈通常采用的两种存储结构\n\n* 两个栈实现队列,和两个队列实现栈\n\n\n\n# 树\n\n* 二叉树\n\n* 字典树\n\n* 平衡树\n\n* 排序树\n\n* B树\n\n* B+树\n\n* R树\n\n* 多路树\n\n* 红黑树\n\n\n\n# 堆\n\n* 大根堆\n\n* 小根堆\n\n\n\n# 图\n\n* 有向图\n\n* 无向图\n\n* 拓扑\n\n\n\n# 稳定的排序算法\n\n* 冒泡排序\n* 插入排序\n* 鸡尾酒排序\n* 桶排序\n* 计数排序\n* 归并排序\n* 原地归并排序\n* 二叉排序树排序\n* 鸽巢排序\n* 基数排序\n* 侏儒排序\n* 图书馆排序\n* 块排序\n\n\n\n# 不稳定的排序算法\n\n* 选择排序\n* 希尔排序\n* Clover排序算法\n* 梳排序\n* 堆排序\n* 平滑排序\n* 快速排序\n* 内省排序\n* 耐心排序\n\n\n\n# 各种排序算法和时间复杂度\n\n\n# 深度优先和广度优先搜索", + "normalizedContent": "# 简单的数据结构\n\n* 栈\n* 队列\n\n* 链表\n\n* 数组\n\n* 哈希表\n\n* 栈和队列的相同和不同之处\n\n* 栈通常采用的两种存储结构\n\n* 两个栈实现队列,和两个队列实现栈\n\n\n\n# 树\n\n* 二叉树\n\n* 字典树\n\n* 平衡树\n\n* 排序树\n\n* b树\n\n* b+树\n\n* r树\n\n* 多路树\n\n* 红黑树\n\n\n\n# 堆\n\n* 大根堆\n\n* 小根堆\n\n\n\n# 图\n\n* 有向图\n\n* 无向图\n\n* 拓扑\n\n\n\n# 稳定的排序算法\n\n* 冒泡排序\n* 插入排序\n* 鸡尾酒排序\n* 桶排序\n* 计数排序\n* 归并排序\n* 原地归并排序\n* 二叉排序树排序\n* 鸽巢排序\n* 基数排序\n* 侏儒排序\n* 图书馆排序\n* 块排序\n\n\n\n# 不稳定的排序算法\n\n* 选择排序\n* 希尔排序\n* clover排序算法\n* 梳排序\n* 堆排序\n* 平滑排序\n* 快速排序\n* 内省排序\n* 耐心排序\n\n\n\n# 各种排序算法和时间复杂度\n\n\n# 深度优先和广度优先搜索", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/06/20, 22:47:41", + "lastUpdatedTimestamp": 1655736461000 + }, + { + "title": "算法", + "frontmatter": { + "title": "算法", + "date": "2022-03-11T00:38:43.000Z", + "permalink": "/pages/92288a/", + "categories": [ + "计算机基础", + "CSS" + ], + "tags": [ + null + ] + }, + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/40.%E7%AE%97%E6%B3%95/01.%E7%AE%97%E6%B3%95.html", + "relativePath": "02.计算机基础/40.算法/01.算法.md", + "key": "v-2e6d42b4", + "path": "/pages/92288a/", + "headers": [ + { + "level": 2, + "title": "数据结构与算法知识", + "slug": "数据结构与算法知识", + "normalizedTitle": "数据结构与算法知识", + "charIndex": 2 }, { "level": 2, - "title": "简述 WebSocket 是如何进行传输的", - "slug": "简述-websocket-是如何进行传输的", - "normalizedTitle": "简述 websocket 是如何进行传输的", - "charIndex": 667 + "title": "全排列", + "slug": "全排列", + "normalizedTitle": "全排列", + "charIndex": 16 }, { "level": 2, - "title": "DNS 查询服务器的基本流程是什么?DNS 劫持是什么?", - "slug": "dns-查询服务器的基本流程是什么-dns-劫持是什么", - "normalizedTitle": "dns 查询服务器的基本流程是什么?dns 劫持是什么?", - "charIndex": 693 + "title": "贪心算法", + "slug": "贪心算法", + "normalizedTitle": "贪心算法", + "charIndex": 24 }, { "level": 2, - "title": "RestFul 是什么?RestFul 请求的 URL 有什么特点?", - "slug": "restful-是什么-restful-请求的-url-有什么特点", - "normalizedTitle": "restful 是什么?restful 请求的 url 有什么特点?", - "charIndex": 726 + "title": "KMP算法", + "slug": "kmp算法", + "normalizedTitle": "kmp算法", + "charIndex": 33 }, { "level": 2, - "title": "简述 HTTP 报文头部的组成结构", - "slug": "简述-http-报文头部的组成结构", - "normalizedTitle": "简述 http 报文头部的组成结构", - "charIndex": 765 + "title": "hash算法", + "slug": "hash算法", + "normalizedTitle": "hash算法", + "charIndex": 43 }, { "level": 2, - "title": "什么是 SYN flood,如何防止这类攻击?", - "slug": "什么是-syn-flood-如何防止这类攻击", - "normalizedTitle": "什么是 syn flood,如何防止这类攻击?", - "charIndex": 787 + "title": "海量数据处理", + "slug": "海量数据处理", + "normalizedTitle": "海量数据处理", + "charIndex": 54 } ], - "headersStr": "简述 TCP 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手? 从输入 URL 到展现页面的全过程 HTTP 中 GET 和 POST 区别 TCP 怎么保证可靠传输 什么是 TCP 粘包和拆包? 简述 HTTPS 的加密与认证过程 简述对称与非对称加密的概念 TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别? 如何解决 TCP 传输丢包问题? TCP 长连接和短连接有那么不同的使用场景? TCP 的 keepalive 了解吗?说一说它和 HTTP 的 keepalive 的区别? HTTP 与 HTTPS 有哪些区别? TCP 中常见的拥塞控制算法有哪些? 简述 HTTP 1.0,1.1,2.0 的主要区别 从系统层面上,UDP如何保证尽量可靠? TCP 挥手时出现大量 CLOSEWAIT 或 TIMEWAIT 怎么解决? 简述 TCP 滑动窗口以及重传机制 简述 JWT 的原理和校验机制 为什么需要序列化?有什么序列化的方式? 简述 iPv4 和 iPv6 的区别 TCP 中 SYN 攻击是什么?如何防止? 简述 DDOS 攻击原理,如何防范它? 什么是 ARP 协议?简述其使用场景 什么是跨域,什么情况下会发生跨域请求? Cookie 和 Session 的关系和区别是什么? 简述 WebSocket 是如何进行传输的 DNS 查询服务器的基本流程是什么?DNS 劫持是什么? RestFul 是什么?RestFul 请求的 URL 有什么特点? 简述 HTTP 报文头部的组成结构 什么是 SYN flood,如何防止这类攻击?", - "content": "# 简述 TCP 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手?\n\n\n# 从输入 URL 到展现页面的全过程\n\n\n# HTTP 中 GET 和 POST 区别\n\n\n# TCP 怎么保证可靠传输\n\n\n# 什么是 TCP 粘包和拆包?\n\n\n# 简述 HTTPS 的加密与认证过程\n\n\n# 简述对称与非对称加密的概念\n\n\n# TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别?\n\n\n# 如何解决 TCP 传输丢包问题?\n\n\n# TCP 长连接和短连接有那么不同的使用场景?\n\n\n# TCP 的 keepalive 了解吗?说一说它和 HTTP 的 keepalive 的区别?\n\n\n# HTTP 与 HTTPS 有哪些区别?\n\n\n# TCP 中常见的拥塞控制算法有哪些?\n\n\n# 简述 HTTP 1.0,1.1,2.0 的主要区别\n\n\n# 从系统层面上,UDP如何保证尽量可靠?\n\n\n# TCP 挥手时出现大量 CLOSE_WAIT 或 TIME_WAIT 怎么解决?\n\n\n# 简述 TCP 滑动窗口以及重传机制\n\n\n# 简述 JWT 的原理和校验机制\n\n\n# 为什么需要序列化?有什么序列化的方式?\n\n\n# 简述 iPv4 和 iPv6 的区别\n\n\n# TCP 中 SYN 攻击是什么?如何防止?\n\n\n# 简述 DDOS 攻击原理,如何防范它?\n\n\n# 什么是 ARP 协议?简述其使用场景\n\n\n# 什么是跨域,什么情况下会发生跨域请求?\n\n\n# Cookie 和 Session 的关系和区别是什么?\n\n\n# 简述 WebSocket 是如何进行传输的\n\n\n# DNS 查询服务器的基本流程是什么?DNS 劫持是什么?\n\n\n# RestFul 是什么?RestFul 请求的 URL 有什么特点?\n\n\n# 简述 HTTP 报文头部的组成结构\n\n\n# 什么是 SYN flood,如何防止这类攻击?", - "normalizedContent": "# 简述 tcp 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手?\n\n\n# 从输入 url 到展现页面的全过程\n\n\n# http 中 get 和 post 区别\n\n\n# tcp 怎么保证可靠传输\n\n\n# 什么是 tcp 粘包和拆包?\n\n\n# 简述 https 的加密与认证过程\n\n\n# 简述对称与非对称加密的概念\n\n\n# tcp 与 udp 在网络协议中的哪一层,他们之间有什么区别?\n\n\n# 如何解决 tcp 传输丢包问题?\n\n\n# tcp 长连接和短连接有那么不同的使用场景?\n\n\n# tcp 的 keepalive 了解吗?说一说它和 http 的 keepalive 的区别?\n\n\n# http 与 https 有哪些区别?\n\n\n# tcp 中常见的拥塞控制算法有哪些?\n\n\n# 简述 http 1.0,1.1,2.0 的主要区别\n\n\n# 从系统层面上,udp如何保证尽量可靠?\n\n\n# tcp 挥手时出现大量 close_wait 或 time_wait 怎么解决?\n\n\n# 简述 tcp 滑动窗口以及重传机制\n\n\n# 简述 jwt 的原理和校验机制\n\n\n# 为什么需要序列化?有什么序列化的方式?\n\n\n# 简述 ipv4 和 ipv6 的区别\n\n\n# tcp 中 syn 攻击是什么?如何防止?\n\n\n# 简述 ddos 攻击原理,如何防范它?\n\n\n# 什么是 arp 协议?简述其使用场景\n\n\n# 什么是跨域,什么情况下会发生跨域请求?\n\n\n# cookie 和 session 的关系和区别是什么?\n\n\n# 简述 websocket 是如何进行传输的\n\n\n# dns 查询服务器的基本流程是什么?dns 劫持是什么?\n\n\n# restful 是什么?restful 请求的 url 有什么特点?\n\n\n# 简述 http 报文头部的组成结构\n\n\n# 什么是 syn flood,如何防止这类攻击?", + "headersStr": "数据结构与算法知识 全排列 贪心算法 KMP算法 hash算法 海量数据处理", + "content": "# 数据结构与算法知识\n\n\n# 全排列\n\n\n# 贪心算法\n\n\n# KMP算法\n\n\n# hash算法\n\n\n# 海量数据处理\n\n* 分治\n* hash映射\n* 堆排序\n* 双层桶划分\n* Bloom Filter\n* bitmap\n* 数据库索引\n* mapreduce等。", + "normalizedContent": "# 数据结构与算法知识\n\n\n# 全排列\n\n\n# 贪心算法\n\n\n# kmp算法\n\n\n# hash算法\n\n\n# 海量数据处理\n\n* 分治\n* hash映射\n* 堆排序\n* 双层桶划分\n* bloom filter\n* bitmap\n* 数据库索引\n* mapreduce等。", "charsets": { "cjk": true }, - "lastUpdated": "2022/09/18, 21:28:02", - "lastUpdatedTimestamp": 1663507682000 + "lastUpdated": "2022/06/20, 22:47:41", + "lastUpdatedTimestamp": 1655736461000 }, { "title": "聚簇索引和非聚簇索引有什么区别?", @@ -4204,11 +4557,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "说一下Mysql锁", + "title": "MySQL中 InnoDB 和 MylSAM 的区别是什么?", "frontmatter": { - "title": "说一下Mysql锁", - "date": "2022-05-21T22:59:15.000Z", - "permalink": "/pages/b17e88/", + "title": "MySQL中 InnoDB 和 MylSAM 的区别是什么?", + "date": "2022-05-21T17:07:47.000Z", + "permalink": "/pages/3a8659/", "categories": [ "数据库", "MYSQL相关" @@ -4217,43 +4570,86 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/015.%E8%AF%B4%E4%B8%80%E4%B8%8BMysql%E9%94%81.html", - "relativePath": "03.数据库/01.MYSQL相关/015.说一下Mysql锁.md", - "key": "v-5b0e09c6", - "path": "/pages/b17e88/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/001.MySQL%E4%B8%AD%20InnoDB%20%E5%92%8C%20MylSAM%20%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/001.MySQL中 InnoDB 和 MylSAM 的区别是什么?.md", + "key": "v-674454ac", + "path": "/pages/3a8659/", "headers": [ { "level": 2, - "title": "锁类型", - "slug": "锁类型", - "normalizedTitle": "锁类型", + "title": "区别", + "slug": "区别", + "normalizedTitle": "区别", + "charIndex": 37 + }, + { + "level": 2, + "title": "场景选择", + "slug": "场景选择", + "normalizedTitle": "场景选择", + "charIndex": 253 + } + ], + "headersStr": "区别 场景选择", + "content": "MySQL5.5版本开始Innodb已经成为Mysql的默认引擎\n\n\n# 区别\n\n * InnoDB 支持事务,MyISAM 不支持事务。 这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一\n * MyISAM锁的粒度是表级的,而InnoDB支持行级锁\n * MyISAM支持全文类型索引,而InnoDB不支持全文索引\n * MyISAM相对简单,效率上要优于InnoDB,小型应用可以考虑使用MyISAM\n * MyISAM表保存成文件形式,跨平台使用更加方便\n\n\n# 场景选择\n\nMyISAM管理非事务表,提供高速存储和检索以及全文搜索能力,如果再应用中执行大量select操作,应该选择MyISAM InnoDB用于事务处理,具有ACID事务支持等特性,如果在应用中执行大量insert和update操作,应该选择InnoDB", + "normalizedContent": "mysql5.5版本开始innodb已经成为mysql的默认引擎\n\n\n# 区别\n\n * innodb 支持事务,myisam 不支持事务。 这是 mysql 将默认存储引擎从 myisam 变成 innodb 的重要原因之一\n * myisam锁的粒度是表级的,而innodb支持行级锁\n * myisam支持全文类型索引,而innodb不支持全文索引\n * myisam相对简单,效率上要优于innodb,小型应用可以考虑使用myisam\n * myisam表保存成文件形式,跨平台使用更加方便\n\n\n# 场景选择\n\nmyisam管理非事务表,提供高速存储和检索以及全文搜索能力,如果再应用中执行大量select操作,应该选择myisam innodb用于事务处理,具有acid事务支持等特性,如果在应用中执行大量insert和update操作,应该选择innodb", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "简述Mysql的事务隔离级别", + "frontmatter": { + "title": "简述Mysql的事务隔离级别", + "date": "2022-05-20T11:35:30.000Z", + "permalink": "/pages/9f2936/", + "categories": [ + "数据库", + "MYSQL相关" + ], + "tags": [ + null + ] + }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/010.%E7%AE%80%E8%BF%B0Mysql%E7%9A%84%E4%BA%8B%E5%8A%A1%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB.html", + "relativePath": "03.数据库/01.MYSQL相关/010.简述Mysql的事务隔离级别.md", + "key": "v-7e02c4a1", + "path": "/pages/9f2936/", + "headers": [ + { + "level": 2, + "title": "READ UNCOMMITTED (未提交读)", + "slug": "read-uncommitted-未提交读", + "normalizedTitle": "read uncommitted (未提交读)", "charIndex": 2 }, { "level": 2, - "title": "Record lock、gap lock、next-key lock", - "slug": "record-lock、gap-lock、next-key-lock", - "normalizedTitle": "record lock、gap lock、next-key lock", - "charIndex": 1049 + "title": "READ COMMITTED (提交读)", + "slug": "read-committed-提交读", + "normalizedTitle": "read committed (提交读)", + "charIndex": 55 }, { "level": 2, - "title": "锁都加在哪些资源上", - "slug": "锁都加在哪些资源上", - "normalizedTitle": "锁都加在哪些资源上", - "charIndex": 1599 + "title": "REPEATABLE READ (可重复读)", + "slug": "repeatable-read-可重复读", + "normalizedTitle": "repeatable read (可重复读)", + "charIndex": 117 }, { "level": 2, - "title": "4.4 各种条件下的加锁", - "slug": "_4-4-各种条件下的加锁", - "normalizedTitle": "4.4 各种条件下的加锁", - "charIndex": 1867 + "title": "SERIALIZABLE (可串行化)", + "slug": "serializable-可串行化", + "normalizedTitle": "serializable (可串行化)", + "charIndex": 168 } ], - "headersStr": "锁类型 Record lock、gap lock、next-key lock 锁都加在哪些资源上 4.4 各种条件下的加锁", - "content": "# 锁类型\n\n * 共享锁(S):锁住一行,阻止其他事物获得相同数据集的排他锁\n * 排他锁(X):锁住一行,阻止其他事物获得相同数据集的共享锁和排他锁\n\n为了实现表锁和行锁共存,InnoDB内部还使用了意向锁,这两种意向锁都是表锁。\n\n * 意向共享锁(IS):获得数据行的S锁前要先获得表的IS锁\n * 意向排他锁(IX):获得数据行的X锁前要先获得表的IX锁\n\n兼容矩阵\n\n项目 X IX S IS\nX 冲突 冲突 冲突 冲突\nIX 冲突 冲突 \nS 冲突 冲突 \nIS 冲突 \n\n为什么要使用意向锁? 在没有意向锁情况下,事物T1申请了行排他锁,如果事物T2要申请表排他锁,就要遍历表的所有锁看有没有行排他锁,这是耗费性能的。有了意向锁,判断是否有意向排他锁就可以了。\n\n意向共享锁可以多个并存,意向排他锁只能有一个。\n\n在兼容矩阵中,为什么IX和IX是兼容的? 因为IX存在的条件是表中有行拍他锁,IX存在的目的是阻止其他事物申请表排他锁,但是不组织其他事物申请行排他锁。 所以,当表中存在IX锁,在申请行排他锁时,会先申请IX锁并且申请成功 但是如果申请表排他锁则申请失败。\n\n什么SQL加什么锁? 意向锁是InnoDB自动加的,无需用户干预。 update、delete、insert,InnoDB自动加排他锁。 非显式加锁的select,InnoDB不会加锁。\n\nselect加锁: 共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。 排他锁(X):SELECT * FROM table_name WHERE ... FOR UPDATE。\n\ninsert加锁: insert会在insert所在行加排他锁,这是一个record lock。 不过,在insert之前会先加一种锁,官方称为insertion intention gap lock,也就是意向gap锁。意向gap锁可以同时存在多个,两个事物可以给相同的gap加意向gap锁,只要插入的记录唯一键不同,两个事物的insert都会成功。 为什么需要意向gap锁? (个人理解)如果没有意向gap锁,insert时需要对比所有未提交的insert记录,看是不是唯一键冲突。有gap锁,则只需要和获得这个gap锁的事务比较唯一键。\n\n\n# Record lock、gap lock、next-key lock\n\n三种类型的排他锁,锁定数据范围不同: Record lock:只锁一行数据,按唯一键加锁 gap lock:锁住两个索引之间的间隙,但不包含记录本身 只有两种情况才有单纯的gap锁: next-key lock:Record lock+gap lock。\n\n举个例子来看这三种排他锁.如下表,id为PK。\n\nID NAME\n10 a\n20 b\n50 c\n\nRecord Lock:select * from tab where id = 10 for update; //对id=10单行进行加锁 Gap Lock锁范围:(- ∞,10)(10,20)(20,50)(50,+∞) Next-Key Lock锁范围:(- ∞,10)[10,20)[20,50)[50,+∞)\n\n只有两种场景会有单纯的gap lock:\n\n * update t set name ='d' where id > 60; 锁定 (60,+∞),这里只有gap锁,没有索引本身的锁。\n * update t set name ='d' where id < 1; 锁定 (- ∞,1),这里只有gap锁,没有索引本身的锁。\n\n\n# 锁都加在哪些资源上\n\n * 表\n * 索引(主键索引、唯一索引、普通索引)\n * 索引之间的gap\n\n为什么任何session都insert不进去? 可能是,表数据少,有一个事物在执行update时全表扫描,把表锁住了。\n\n这种逻辑:先select再update,有什么问题? select是快照读,update是当前读,可能会出现幻读导致数据不一致。 解决办法:\n\n * 乐观锁 update set version=v+1 where version=v\n * 悲观锁 select * from t for update\n\n\n# 4.4 各种条件下的加锁\n\n给定SQL:delete from t1 where id = 10; 那么,当前的事物隔离级别RC、RR。id的索引情况。有如下讨论:\n\n- RC RR\n主键索引 [外链图片转存中...(img-vkCidSBQ-1653142673824)]只需要将主键上,id = [外链图片转存中...(img-IznzsLkw-1653142673825)]只需要将主键上,id =\n 10的记录加上X锁即可 10的记录加上X锁即可\n唯一索引 [外链图片转存中...(img-I8ESnpgb-1653142673825)]此组合中,id是unique索引,而主键是name列。首先会将unique索引上的id=10索引记录加上X锁,同时,会根据读取到的name列,回主键索引(聚簇索引),然后将聚簇索引上的name [外链图片转存中...(img-ULIYfRJG-1653142673826)]此组合中,id是unique索引,而主键是name列。首先会将unique索引上的id=10索引记录加上X锁,同时,会根据读取到的name列,回主键索引(聚簇索引),然后将聚簇索引上的name\n = ‘d’ 对应的主键索引项加X锁。 = ‘d’ 对应的主键索引项加X锁。\n普通索引 [外链图片转存中...(img-LMDZXlly-1653142673826)]id列索引上,满足id = [外链图片转存中...(img-KS9pBMEu-1653142673827)]与左图的唯一不同在于:在主键上加了gap锁。\n 10查询条件的记录,均已加锁。同时,这些记录对应的主键索引上的记录也都加上了锁\n非索引 [外链图片转存中...(img-RmaEu3Nd-1653142673827)]进行全部扫描。聚簇索引上所有的记录,都被加上了X锁 [外链图片转存中...(img-cffdiN3C-1653142673827)]全表记录加X锁,聚簇索引的每个gap都加gap锁", - "normalizedContent": "# 锁类型\n\n * 共享锁(s):锁住一行,阻止其他事物获得相同数据集的排他锁\n * 排他锁(x):锁住一行,阻止其他事物获得相同数据集的共享锁和排他锁\n\n为了实现表锁和行锁共存,innodb内部还使用了意向锁,这两种意向锁都是表锁。\n\n * 意向共享锁(is):获得数据行的s锁前要先获得表的is锁\n * 意向排他锁(ix):获得数据行的x锁前要先获得表的ix锁\n\n兼容矩阵\n\n项目 x ix s is\nx 冲突 冲突 冲突 冲突\nix 冲突 冲突 \ns 冲突 冲突 \nis 冲突 \n\n为什么要使用意向锁? 在没有意向锁情况下,事物t1申请了行排他锁,如果事物t2要申请表排他锁,就要遍历表的所有锁看有没有行排他锁,这是耗费性能的。有了意向锁,判断是否有意向排他锁就可以了。\n\n意向共享锁可以多个并存,意向排他锁只能有一个。\n\n在兼容矩阵中,为什么ix和ix是兼容的? 因为ix存在的条件是表中有行拍他锁,ix存在的目的是阻止其他事物申请表排他锁,但是不组织其他事物申请行排他锁。 所以,当表中存在ix锁,在申请行排他锁时,会先申请ix锁并且申请成功 但是如果申请表排他锁则申请失败。\n\n什么sql加什么锁? 意向锁是innodb自动加的,无需用户干预。 update、delete、insert,innodb自动加排他锁。 非显式加锁的select,innodb不会加锁。\n\nselect加锁: 共享锁(s):select * from table_name where ... lock in share mode。 排他锁(x):select * from table_name where ... for update。\n\ninsert加锁: insert会在insert所在行加排他锁,这是一个record lock。 不过,在insert之前会先加一种锁,官方称为insertion intention gap lock,也就是意向gap锁。意向gap锁可以同时存在多个,两个事物可以给相同的gap加意向gap锁,只要插入的记录唯一键不同,两个事物的insert都会成功。 为什么需要意向gap锁? (个人理解)如果没有意向gap锁,insert时需要对比所有未提交的insert记录,看是不是唯一键冲突。有gap锁,则只需要和获得这个gap锁的事务比较唯一键。\n\n\n# record lock、gap lock、next-key lock\n\n三种类型的排他锁,锁定数据范围不同: record lock:只锁一行数据,按唯一键加锁 gap lock:锁住两个索引之间的间隙,但不包含记录本身 只有两种情况才有单纯的gap锁: next-key lock:record lock+gap lock。\n\n举个例子来看这三种排他锁.如下表,id为pk。\n\nid name\n10 a\n20 b\n50 c\n\nrecord lock:select * from tab where id = 10 for update; //对id=10单行进行加锁 gap lock锁范围:(- ∞,10)(10,20)(20,50)(50,+∞) next-key lock锁范围:(- ∞,10)[10,20)[20,50)[50,+∞)\n\n只有两种场景会有单纯的gap lock:\n\n * update t set name ='d' where id > 60; 锁定 (60,+∞),这里只有gap锁,没有索引本身的锁。\n * update t set name ='d' where id < 1; 锁定 (- ∞,1),这里只有gap锁,没有索引本身的锁。\n\n\n# 锁都加在哪些资源上\n\n * 表\n * 索引(主键索引、唯一索引、普通索引)\n * 索引之间的gap\n\n为什么任何session都insert不进去? 可能是,表数据少,有一个事物在执行update时全表扫描,把表锁住了。\n\n这种逻辑:先select再update,有什么问题? select是快照读,update是当前读,可能会出现幻读导致数据不一致。 解决办法:\n\n * 乐观锁 update set version=v+1 where version=v\n * 悲观锁 select * from t for update\n\n\n# 4.4 各种条件下的加锁\n\n给定sql:delete from t1 where id = 10; 那么,当前的事物隔离级别rc、rr。id的索引情况。有如下讨论:\n\n- rc rr\n主键索引 [外链图片转存中...(img-vkcidsbq-1653142673824)]只需要将主键上,id = [外链图片转存中...(img-iznzslkw-1653142673825)]只需要将主键上,id =\n 10的记录加上x锁即可 10的记录加上x锁即可\n唯一索引 [外链图片转存中...(img-i8esnpgb-1653142673825)]此组合中,id是unique索引,而主键是name列。首先会将unique索引上的id=10索引记录加上x锁,同时,会根据读取到的name列,回主键索引(聚簇索引),然后将聚簇索引上的name [外链图片转存中...(img-uliyfrjg-1653142673826)]此组合中,id是unique索引,而主键是name列。首先会将unique索引上的id=10索引记录加上x锁,同时,会根据读取到的name列,回主键索引(聚簇索引),然后将聚簇索引上的name\n = ‘d’ 对应的主键索引项加x锁。 = ‘d’ 对应的主键索引项加x锁。\n普通索引 [外链图片转存中...(img-lmdzxlly-1653142673826)]id列索引上,满足id = [外链图片转存中...(img-ks9pbmeu-1653142673827)]与左图的唯一不同在于:在主键上加了gap锁。\n 10查询条件的记录,均已加锁。同时,这些记录对应的主键索引上的记录也都加上了锁\n非索引 [外链图片转存中...(img-rmaeu3nd-1653142673827)]进行全部扫描。聚簇索引上所有的记录,都被加上了x锁 [外链图片转存中...(img-cffdin3c-1653142673827)]全表记录加x锁,聚簇索引的每个gap都加gap锁", + "headersStr": "READ UNCOMMITTED (未提交读) READ COMMITTED (提交读) REPEATABLE READ (可重复读) SERIALIZABLE (可串行化)", + "content": "# READ UNCOMMITTED (未提交读)\n\n存在脏读问题 存在不可重复读问题 存在幻读问题\n\n\n# READ COMMITTED (提交读)\n\n只读已经事物提交的数据,不存在脏读。 存在不可重复读问题 存在幻读问题\n\n\n# REPEATABLE READ (可重复读)\n\n是mysql默认的隔离级别 mvcc解决幻读\n\n\n# SERIALIZABLE (可串行化)\n\n读写串行化,效率太低,没人使用。", + "normalizedContent": "# read uncommitted (未提交读)\n\n存在脏读问题 存在不可重复读问题 存在幻读问题\n\n\n# read committed (提交读)\n\n只读已经事物提交的数据,不存在脏读。 存在不可重复读问题 存在幻读问题\n\n\n# repeatable read (可重复读)\n\n是mysql默认的隔离级别 mvcc解决幻读\n\n\n# serializable (可串行化)\n\n读写串行化,效率太低,没人使用。", "charsets": { "cjk": true }, @@ -4261,11 +4657,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述乐观锁以及悲观锁的区别以及使用场景", + "title": "简述 MySQL 常见索引类型,介绍一下覆盖索引", "frontmatter": { - "title": "简述乐观锁以及悲观锁的区别以及使用场景", - "date": "2022-05-21T17:05:33.000Z", - "permalink": "/pages/57561d/", + "title": "简述 MySQL 常见索引类型,介绍一下覆盖索引", + "date": "2022-05-21T17:08:24.000Z", + "permalink": "/pages/17b014/", "categories": [ "数据库", "MYSQL相关" @@ -4274,10 +4670,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/030.%E7%AE%80%E8%BF%B0%E4%B9%90%E8%A7%82%E9%94%81%E4%BB%A5%E5%8F%8A%E6%82%B2%E8%A7%82%E9%94%81%E7%9A%84%E5%8C%BA%E5%88%AB%E4%BB%A5%E5%8F%8A%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF.html", - "relativePath": "03.数据库/01.MYSQL相关/030.简述乐观锁以及悲观锁的区别以及使用场景.md", - "key": "v-c39c4fe2", - "path": "/pages/57561d/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/007.%E7%AE%80%E8%BF%B0%20MySQL%20%E5%B8%B8%E8%A7%81%E7%B4%A2%E5%BC%95%E7%B1%BB%E5%9E%8B%EF%BC%8C%E4%BB%8B%E7%BB%8D%E4%B8%80%E4%B8%8B%E8%A6%86%E7%9B%96%E7%B4%A2%E5%BC%95.html", + "relativePath": "03.数据库/01.MYSQL相关/007.简述 MySQL 常见索引类型,介绍一下覆盖索引.md", + "key": "v-824b776c", + "path": "/pages/17b014/", "headersStr": null, "content": "", "normalizedContent": "", @@ -4286,11 +4682,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述Mysql的事务隔离级别", + "title": "说一下Mysql锁", "frontmatter": { - "title": "简述Mysql的事务隔离级别", - "date": "2022-05-20T11:35:30.000Z", - "permalink": "/pages/9f2936/", + "title": "说一下Mysql锁", + "date": "2022-05-21T22:59:15.000Z", + "permalink": "/pages/b17e88/", "categories": [ "数据库", "MYSQL相关" @@ -4299,43 +4695,43 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/010.%E7%AE%80%E8%BF%B0Mysql%E7%9A%84%E4%BA%8B%E5%8A%A1%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB.html", - "relativePath": "03.数据库/01.MYSQL相关/010.简述Mysql的事务隔离级别.md", - "key": "v-7e02c4a1", - "path": "/pages/9f2936/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/015.%E8%AF%B4%E4%B8%80%E4%B8%8BMysql%E9%94%81.html", + "relativePath": "03.数据库/01.MYSQL相关/015.说一下Mysql锁.md", + "key": "v-5b0e09c6", + "path": "/pages/b17e88/", "headers": [ { "level": 2, - "title": "READ UNCOMMITTED (未提交读)", - "slug": "read-uncommitted-未提交读", - "normalizedTitle": "read uncommitted (未提交读)", + "title": "锁类型", + "slug": "锁类型", + "normalizedTitle": "锁类型", "charIndex": 2 }, { "level": 2, - "title": "READ COMMITTED (提交读)", - "slug": "read-committed-提交读", - "normalizedTitle": "read committed (提交读)", - "charIndex": 55 + "title": "Record lock、gap lock、next-key lock", + "slug": "record-lock、gap-lock、next-key-lock", + "normalizedTitle": "record lock、gap lock、next-key lock", + "charIndex": 1049 }, { "level": 2, - "title": "REPEATABLE READ (可重复读)", - "slug": "repeatable-read-可重复读", - "normalizedTitle": "repeatable read (可重复读)", - "charIndex": 117 + "title": "锁都加在哪些资源上", + "slug": "锁都加在哪些资源上", + "normalizedTitle": "锁都加在哪些资源上", + "charIndex": 1599 }, { "level": 2, - "title": "SERIALIZABLE (可串行化)", - "slug": "serializable-可串行化", - "normalizedTitle": "serializable (可串行化)", - "charIndex": 168 + "title": "4.4 各种条件下的加锁", + "slug": "_4-4-各种条件下的加锁", + "normalizedTitle": "4.4 各种条件下的加锁", + "charIndex": 1867 } ], - "headersStr": "READ UNCOMMITTED (未提交读) READ COMMITTED (提交读) REPEATABLE READ (可重复读) SERIALIZABLE (可串行化)", - "content": "# READ UNCOMMITTED (未提交读)\n\n存在脏读问题 存在不可重复读问题 存在幻读问题\n\n\n# READ COMMITTED (提交读)\n\n只读已经事物提交的数据,不存在脏读。 存在不可重复读问题 存在幻读问题\n\n\n# REPEATABLE READ (可重复读)\n\n是mysql默认的隔离级别 mvcc解决幻读\n\n\n# SERIALIZABLE (可串行化)\n\n读写串行化,效率太低,没人使用。", - "normalizedContent": "# read uncommitted (未提交读)\n\n存在脏读问题 存在不可重复读问题 存在幻读问题\n\n\n# read committed (提交读)\n\n只读已经事物提交的数据,不存在脏读。 存在不可重复读问题 存在幻读问题\n\n\n# repeatable read (可重复读)\n\n是mysql默认的隔离级别 mvcc解决幻读\n\n\n# serializable (可串行化)\n\n读写串行化,效率太低,没人使用。", + "headersStr": "锁类型 Record lock、gap lock、next-key lock 锁都加在哪些资源上 4.4 各种条件下的加锁", + "content": "# 锁类型\n\n * 共享锁(S):锁住一行,阻止其他事物获得相同数据集的排他锁\n * 排他锁(X):锁住一行,阻止其他事物获得相同数据集的共享锁和排他锁\n\n为了实现表锁和行锁共存,InnoDB内部还使用了意向锁,这两种意向锁都是表锁。\n\n * 意向共享锁(IS):获得数据行的S锁前要先获得表的IS锁\n * 意向排他锁(IX):获得数据行的X锁前要先获得表的IX锁\n\n兼容矩阵\n\n项目 X IX S IS\nX 冲突 冲突 冲突 冲突\nIX 冲突 冲突 \nS 冲突 冲突 \nIS 冲突 \n\n为什么要使用意向锁? 在没有意向锁情况下,事物T1申请了行排他锁,如果事物T2要申请表排他锁,就要遍历表的所有锁看有没有行排他锁,这是耗费性能的。有了意向锁,判断是否有意向排他锁就可以了。\n\n意向共享锁可以多个并存,意向排他锁只能有一个。\n\n在兼容矩阵中,为什么IX和IX是兼容的? 因为IX存在的条件是表中有行拍他锁,IX存在的目的是阻止其他事物申请表排他锁,但是不组织其他事物申请行排他锁。 所以,当表中存在IX锁,在申请行排他锁时,会先申请IX锁并且申请成功 但是如果申请表排他锁则申请失败。\n\n什么SQL加什么锁? 意向锁是InnoDB自动加的,无需用户干预。 update、delete、insert,InnoDB自动加排他锁。 非显式加锁的select,InnoDB不会加锁。\n\nselect加锁: 共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。 排他锁(X):SELECT * FROM table_name WHERE ... FOR UPDATE。\n\ninsert加锁: insert会在insert所在行加排他锁,这是一个record lock。 不过,在insert之前会先加一种锁,官方称为insertion intention gap lock,也就是意向gap锁。意向gap锁可以同时存在多个,两个事物可以给相同的gap加意向gap锁,只要插入的记录唯一键不同,两个事物的insert都会成功。 为什么需要意向gap锁? (个人理解)如果没有意向gap锁,insert时需要对比所有未提交的insert记录,看是不是唯一键冲突。有gap锁,则只需要和获得这个gap锁的事务比较唯一键。\n\n\n# Record lock、gap lock、next-key lock\n\n三种类型的排他锁,锁定数据范围不同: Record lock:只锁一行数据,按唯一键加锁 gap lock:锁住两个索引之间的间隙,但不包含记录本身 只有两种情况才有单纯的gap锁: next-key lock:Record lock+gap lock。\n\n举个例子来看这三种排他锁.如下表,id为PK。\n\nID NAME\n10 a\n20 b\n50 c\n\nRecord Lock:select * from tab where id = 10 for update; //对id=10单行进行加锁 Gap Lock锁范围:(- ∞,10)(10,20)(20,50)(50,+∞) Next-Key Lock锁范围:(- ∞,10)[10,20)[20,50)[50,+∞)\n\n只有两种场景会有单纯的gap lock:\n\n * update t set name ='d' where id > 60; 锁定 (60,+∞),这里只有gap锁,没有索引本身的锁。\n * update t set name ='d' where id < 1; 锁定 (- ∞,1),这里只有gap锁,没有索引本身的锁。\n\n\n# 锁都加在哪些资源上\n\n * 表\n * 索引(主键索引、唯一索引、普通索引)\n * 索引之间的gap\n\n为什么任何session都insert不进去? 可能是,表数据少,有一个事物在执行update时全表扫描,把表锁住了。\n\n这种逻辑:先select再update,有什么问题? select是快照读,update是当前读,可能会出现幻读导致数据不一致。 解决办法:\n\n * 乐观锁 update set version=v+1 where version=v\n * 悲观锁 select * from t for update\n\n\n# 4.4 各种条件下的加锁\n\n给定SQL:delete from t1 where id = 10; 那么,当前的事物隔离级别RC、RR。id的索引情况。有如下讨论:\n\n- RC RR\n主键索引 [外链图片转存中...(img-vkCidSBQ-1653142673824)]只需要将主键上,id = [外链图片转存中...(img-IznzsLkw-1653142673825)]只需要将主键上,id =\n 10的记录加上X锁即可 10的记录加上X锁即可\n唯一索引 [外链图片转存中...(img-I8ESnpgb-1653142673825)]此组合中,id是unique索引,而主键是name列。首先会将unique索引上的id=10索引记录加上X锁,同时,会根据读取到的name列,回主键索引(聚簇索引),然后将聚簇索引上的name [外链图片转存中...(img-ULIYfRJG-1653142673826)]此组合中,id是unique索引,而主键是name列。首先会将unique索引上的id=10索引记录加上X锁,同时,会根据读取到的name列,回主键索引(聚簇索引),然后将聚簇索引上的name\n = ‘d’ 对应的主键索引项加X锁。 = ‘d’ 对应的主键索引项加X锁。\n普通索引 [外链图片转存中...(img-LMDZXlly-1653142673826)]id列索引上,满足id = [外链图片转存中...(img-KS9pBMEu-1653142673827)]与左图的唯一不同在于:在主键上加了gap锁。\n 10查询条件的记录,均已加锁。同时,这些记录对应的主键索引上的记录也都加上了锁\n非索引 [外链图片转存中...(img-RmaEu3Nd-1653142673827)]进行全部扫描。聚簇索引上所有的记录,都被加上了X锁 [外链图片转存中...(img-cffdiN3C-1653142673827)]全表记录加X锁,聚簇索引的每个gap都加gap锁", + "normalizedContent": "# 锁类型\n\n * 共享锁(s):锁住一行,阻止其他事物获得相同数据集的排他锁\n * 排他锁(x):锁住一行,阻止其他事物获得相同数据集的共享锁和排他锁\n\n为了实现表锁和行锁共存,innodb内部还使用了意向锁,这两种意向锁都是表锁。\n\n * 意向共享锁(is):获得数据行的s锁前要先获得表的is锁\n * 意向排他锁(ix):获得数据行的x锁前要先获得表的ix锁\n\n兼容矩阵\n\n项目 x ix s is\nx 冲突 冲突 冲突 冲突\nix 冲突 冲突 \ns 冲突 冲突 \nis 冲突 \n\n为什么要使用意向锁? 在没有意向锁情况下,事物t1申请了行排他锁,如果事物t2要申请表排他锁,就要遍历表的所有锁看有没有行排他锁,这是耗费性能的。有了意向锁,判断是否有意向排他锁就可以了。\n\n意向共享锁可以多个并存,意向排他锁只能有一个。\n\n在兼容矩阵中,为什么ix和ix是兼容的? 因为ix存在的条件是表中有行拍他锁,ix存在的目的是阻止其他事物申请表排他锁,但是不组织其他事物申请行排他锁。 所以,当表中存在ix锁,在申请行排他锁时,会先申请ix锁并且申请成功 但是如果申请表排他锁则申请失败。\n\n什么sql加什么锁? 意向锁是innodb自动加的,无需用户干预。 update、delete、insert,innodb自动加排他锁。 非显式加锁的select,innodb不会加锁。\n\nselect加锁: 共享锁(s):select * from table_name where ... lock in share mode。 排他锁(x):select * from table_name where ... for update。\n\ninsert加锁: insert会在insert所在行加排他锁,这是一个record lock。 不过,在insert之前会先加一种锁,官方称为insertion intention gap lock,也就是意向gap锁。意向gap锁可以同时存在多个,两个事物可以给相同的gap加意向gap锁,只要插入的记录唯一键不同,两个事物的insert都会成功。 为什么需要意向gap锁? (个人理解)如果没有意向gap锁,insert时需要对比所有未提交的insert记录,看是不是唯一键冲突。有gap锁,则只需要和获得这个gap锁的事务比较唯一键。\n\n\n# record lock、gap lock、next-key lock\n\n三种类型的排他锁,锁定数据范围不同: record lock:只锁一行数据,按唯一键加锁 gap lock:锁住两个索引之间的间隙,但不包含记录本身 只有两种情况才有单纯的gap锁: next-key lock:record lock+gap lock。\n\n举个例子来看这三种排他锁.如下表,id为pk。\n\nid name\n10 a\n20 b\n50 c\n\nrecord lock:select * from tab where id = 10 for update; //对id=10单行进行加锁 gap lock锁范围:(- ∞,10)(10,20)(20,50)(50,+∞) next-key lock锁范围:(- ∞,10)[10,20)[20,50)[50,+∞)\n\n只有两种场景会有单纯的gap lock:\n\n * update t set name ='d' where id > 60; 锁定 (60,+∞),这里只有gap锁,没有索引本身的锁。\n * update t set name ='d' where id < 1; 锁定 (- ∞,1),这里只有gap锁,没有索引本身的锁。\n\n\n# 锁都加在哪些资源上\n\n * 表\n * 索引(主键索引、唯一索引、普通索引)\n * 索引之间的gap\n\n为什么任何session都insert不进去? 可能是,表数据少,有一个事物在执行update时全表扫描,把表锁住了。\n\n这种逻辑:先select再update,有什么问题? select是快照读,update是当前读,可能会出现幻读导致数据不一致。 解决办法:\n\n * 乐观锁 update set version=v+1 where version=v\n * 悲观锁 select * from t for update\n\n\n# 4.4 各种条件下的加锁\n\n给定sql:delete from t1 where id = 10; 那么,当前的事物隔离级别rc、rr。id的索引情况。有如下讨论:\n\n- rc rr\n主键索引 [外链图片转存中...(img-vkcidsbq-1653142673824)]只需要将主键上,id = [外链图片转存中...(img-iznzslkw-1653142673825)]只需要将主键上,id =\n 10的记录加上x锁即可 10的记录加上x锁即可\n唯一索引 [外链图片转存中...(img-i8esnpgb-1653142673825)]此组合中,id是unique索引,而主键是name列。首先会将unique索引上的id=10索引记录加上x锁,同时,会根据读取到的name列,回主键索引(聚簇索引),然后将聚簇索引上的name [外链图片转存中...(img-uliyfrjg-1653142673826)]此组合中,id是unique索引,而主键是name列。首先会将unique索引上的id=10索引记录加上x锁,同时,会根据读取到的name列,回主键索引(聚簇索引),然后将聚簇索引上的name\n = ‘d’ 对应的主键索引项加x锁。 = ‘d’ 对应的主键索引项加x锁。\n普通索引 [外链图片转存中...(img-lmdzxlly-1653142673826)]id列索引上,满足id = [外链图片转存中...(img-ks9pbmeu-1653142673827)]与左图的唯一不同在于:在主键上加了gap锁。\n 10查询条件的记录,均已加锁。同时,这些记录对应的主键索引上的记录也都加上了锁\n非索引 [外链图片转存中...(img-rmaeu3nd-1653142673827)]进行全部扫描。聚簇索引上所有的记录,都被加上了x锁 [外链图片转存中...(img-cffdin3c-1653142673827)]全表记录加x锁,聚簇索引的每个gap都加gap锁", "charsets": { "cjk": true }, @@ -4343,11 +4739,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?", + "title": "简述乐观锁以及悲观锁的区别以及使用场景", "frontmatter": { - "title": "简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?", - "date": "2022-05-21T17:05:55.000Z", - "permalink": "/pages/36672c/", + "title": "简述乐观锁以及悲观锁的区别以及使用场景", + "date": "2022-05-21T17:05:33.000Z", + "permalink": "/pages/57561d/", "categories": [ "数据库", "MYSQL相关" @@ -4356,10 +4752,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/040.%E7%AE%80%E8%BF%B0%E8%84%8F%E8%AF%BB%E5%92%8C%E5%B9%BB%E8%AF%BB%E7%9A%84%E5%8F%91%E7%94%9F%E5%9C%BA%E6%99%AF%EF%BC%8CInnoDB%20%E6%98%AF%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E5%B9%BB%E8%AF%BB%E7%9A%84%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/040.简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?.md", - "key": "v-689e142a", - "path": "/pages/36672c/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/030.%E7%AE%80%E8%BF%B0%E4%B9%90%E8%A7%82%E9%94%81%E4%BB%A5%E5%8F%8A%E6%82%B2%E8%A7%82%E9%94%81%E7%9A%84%E5%8C%BA%E5%88%AB%E4%BB%A5%E5%8F%8A%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF.html", + "relativePath": "03.数据库/01.MYSQL相关/030.简述乐观锁以及悲观锁的区别以及使用场景.md", + "key": "v-c39c4fe2", + "path": "/pages/57561d/", "headersStr": null, "content": "", "normalizedContent": "", @@ -4461,11 +4857,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "MySQL 联合索引底层原理是什么?", + "title": "简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?", "frontmatter": { - "title": "MySQL 联合索引底层原理是什么?", - "date": "2022-05-21T17:06:26.000Z", - "permalink": "/pages/52773a/", + "title": "简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?", + "date": "2022-05-21T17:05:55.000Z", + "permalink": "/pages/36672c/", "categories": [ "数据库", "MYSQL相关" @@ -4474,10 +4870,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/060.MySQL%20%E8%81%94%E5%90%88%E7%B4%A2%E5%BC%95%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/060.MySQL 联合索引底层原理是什么?.md", - "key": "v-505fbe2e", - "path": "/pages/52773a/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/040.%E7%AE%80%E8%BF%B0%E8%84%8F%E8%AF%BB%E5%92%8C%E5%B9%BB%E8%AF%BB%E7%9A%84%E5%8F%91%E7%94%9F%E5%9C%BA%E6%99%AF%EF%BC%8CInnoDB%20%E6%98%AF%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E5%B9%BB%E8%AF%BB%E7%9A%84%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/040.简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?.md", + "key": "v-689e142a", + "path": "/pages/36672c/", "headersStr": null, "content": "", "normalizedContent": "", @@ -4486,11 +4882,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述常见的负载均衡算法", + "title": "MySQL 联合索引底层原理是什么?", "frontmatter": { - "title": "简述常见的负载均衡算法", - "date": "2022-05-21T17:07:07.000Z", - "permalink": "/pages/f2dbbe/", + "title": "MySQL 联合索引底层原理是什么?", + "date": "2022-05-21T17:06:26.000Z", + "permalink": "/pages/52773a/", "categories": [ "数据库", "MYSQL相关" @@ -4499,10 +4895,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/080.%E7%AE%80%E8%BF%B0%E5%B8%B8%E8%A7%81%E7%9A%84%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1%E7%AE%97%E6%B3%95.html", - "relativePath": "03.数据库/01.MYSQL相关/080.简述常见的负载均衡算法.md", - "key": "v-53bcbb7a", - "path": "/pages/f2dbbe/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/060.MySQL%20%E8%81%94%E5%90%88%E7%B4%A2%E5%BC%95%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/060.MySQL 联合索引底层原理是什么?.md", + "key": "v-505fbe2e", + "path": "/pages/52773a/", "headersStr": null, "content": "", "normalizedContent": "", @@ -4511,11 +4907,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述 MySQL 的主从同步机制,如果同步失败会怎么样?", + "title": "简述常见的负载均衡算法", "frontmatter": { - "title": "简述 MySQL 的主从同步机制,如果同步失败会怎么样?", - "date": "2022-05-21T17:07:22.000Z", - "permalink": "/pages/1ac30e/", + "title": "简述常见的负载均衡算法", + "date": "2022-05-21T17:07:07.000Z", + "permalink": "/pages/f2dbbe/", "categories": [ "数据库", "MYSQL相关" @@ -4524,10 +4920,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/090.%E7%AE%80%E8%BF%B0%20MySQL%20%E7%9A%84%E4%B8%BB%E4%BB%8E%E5%90%8C%E6%AD%A5%E6%9C%BA%E5%88%B6%EF%BC%8C%E5%A6%82%E6%9E%9C%E5%90%8C%E6%AD%A5%E5%A4%B1%E8%B4%A5%E4%BC%9A%E6%80%8E%E4%B9%88%E6%A0%B7%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/090.简述 MySQL 的主从同步机制,如果同步失败会怎么样?.md", - "key": "v-c383b96e", - "path": "/pages/1ac30e/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/080.%E7%AE%80%E8%BF%B0%E5%B8%B8%E8%A7%81%E7%9A%84%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1%E7%AE%97%E6%B3%95.html", + "relativePath": "03.数据库/01.MYSQL相关/080.简述常见的负载均衡算法.md", + "key": "v-53bcbb7a", + "path": "/pages/f2dbbe/", "headersStr": null, "content": "", "normalizedContent": "", @@ -4536,11 +4932,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述数据库中什么情况下进行分库,什么情况下进行分表?", + "title": "如何设计数据库压测方案?", "frontmatter": { - "title": "简述数据库中什么情况下进行分库,什么情况下进行分表?", - "date": "2022-05-21T17:08:52.000Z", - "permalink": "/pages/ac0bb8/", + "title": "如何设计数据库压测方案?", + "date": "2022-05-21T17:08:40.000Z", + "permalink": "/pages/a8eb47/", "categories": [ "数据库", "MYSQL相关" @@ -4549,10 +4945,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/110.%E7%AE%80%E8%BF%B0%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B8%AD%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E8%BF%9B%E8%A1%8C%E5%88%86%E5%BA%93%EF%BC%8C%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E8%BF%9B%E8%A1%8C%E5%88%86%E8%A1%A8%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/110.简述数据库中什么情况下进行分库,什么情况下进行分表?.md", - "key": "v-ed0c93b8", - "path": "/pages/ac0bb8/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/100.%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%8B%E6%B5%8B%E6%96%B9%E6%A1%88%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/100.如何设计数据库压测方案?.md", + "key": "v-4cef944a", + "path": "/pages/a8eb47/", "headersStr": null, "content": "", "normalizedContent": "", @@ -4586,36 +4982,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "什么是 SQL 注入攻击?如何防止这类攻击?", - "frontmatter": { - "title": "什么是 SQL 注入攻击?如何防止这类攻击?", - "date": "2022-05-21T17:09:41.000Z", - "permalink": "/pages/b4338e/", - "categories": [ - "数据库", - "MYSQL相关" - ], - "tags": [ - null - ] - }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/140.%E4%BB%80%E4%B9%88%E6%98%AF%20SQL%20%E6%B3%A8%E5%85%A5%E6%94%BB%E5%87%BB%EF%BC%9F%E5%A6%82%E4%BD%95%E9%98%B2%E6%AD%A2%E8%BF%99%E7%B1%BB%E6%94%BB%E5%87%BB%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/140.什么是 SQL 注入攻击?如何防止这类攻击?.md", - "key": "v-ceed6ba6", - "path": "/pages/b4338e/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "MySQL 中 join 与 left join 的区别是什么?", + "title": "简述数据库中什么情况下进行分库,什么情况下进行分表?", "frontmatter": { - "title": "MySQL 中 join 与 left join 的区别是什么?", - "date": "2022-05-21T17:09:52.000Z", - "permalink": "/pages/429100/", + "title": "简述数据库中什么情况下进行分库,什么情况下进行分表?", + "date": "2022-05-21T17:08:52.000Z", + "permalink": "/pages/ac0bb8/", "categories": [ "数据库", "MYSQL相关" @@ -4624,10 +4995,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/150.MySQL%20%E4%B8%AD%20join%20%E4%B8%8E%20left%20join%20%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/150.MySQL 中 join 与 left join 的区别是什么?.md", - "key": "v-77790024", - "path": "/pages/429100/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/110.%E7%AE%80%E8%BF%B0%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B8%AD%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E8%BF%9B%E8%A1%8C%E5%88%86%E5%BA%93%EF%BC%8C%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E8%BF%9B%E8%A1%8C%E5%88%86%E8%A1%A8%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/110.简述数据库中什么情况下进行分库,什么情况下进行分表?.md", + "key": "v-ed0c93b8", + "path": "/pages/ac0bb8/", "headersStr": null, "content": "", "normalizedContent": "", @@ -4686,6 +5057,56 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, + { + "title": "简述 MySQL 的主从同步机制,如果同步失败会怎么样?", + "frontmatter": { + "title": "简述 MySQL 的主从同步机制,如果同步失败会怎么样?", + "date": "2022-05-21T17:07:22.000Z", + "permalink": "/pages/1ac30e/", + "categories": [ + "数据库", + "MYSQL相关" + ], + "tags": [ + null + ] + }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/090.%E7%AE%80%E8%BF%B0%20MySQL%20%E7%9A%84%E4%B8%BB%E4%BB%8E%E5%90%8C%E6%AD%A5%E6%9C%BA%E5%88%B6%EF%BC%8C%E5%A6%82%E6%9E%9C%E5%90%8C%E6%AD%A5%E5%A4%B1%E8%B4%A5%E4%BC%9A%E6%80%8E%E4%B9%88%E6%A0%B7%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/090.简述 MySQL 的主从同步机制,如果同步失败会怎么样?.md", + "key": "v-c383b96e", + "path": "/pages/1ac30e/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "什么是 SQL 注入攻击?如何防止这类攻击?", + "frontmatter": { + "title": "什么是 SQL 注入攻击?如何防止这类攻击?", + "date": "2022-05-21T17:09:41.000Z", + "permalink": "/pages/b4338e/", + "categories": [ + "数据库", + "MYSQL相关" + ], + "tags": [ + null + ] + }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/140.%E4%BB%80%E4%B9%88%E6%98%AF%20SQL%20%E6%B3%A8%E5%85%A5%E6%94%BB%E5%87%BB%EF%BC%9F%E5%A6%82%E4%BD%95%E9%98%B2%E6%AD%A2%E8%BF%99%E7%B1%BB%E6%94%BB%E5%87%BB%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/140.什么是 SQL 注入攻击?如何防止这类攻击?.md", + "key": "v-ceed6ba6", + "path": "/pages/b4338e/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, { "title": "简述 MySQL MVCC 的实现原理", "frontmatter": { @@ -4800,36 +5221,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "如何设计数据库压测方案?", - "frontmatter": { - "title": "如何设计数据库压测方案?", - "date": "2022-05-21T17:08:40.000Z", - "permalink": "/pages/a8eb47/", - "categories": [ - "数据库", - "MYSQL相关" - ], - "tags": [ - null - ] - }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/100.%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%8B%E6%B5%8B%E6%96%B9%E6%A1%88%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/100.如何设计数据库压测方案?.md", - "key": "v-4cef944a", - "path": "/pages/a8eb47/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "MySQL 中 varchar 和 char 的区别是什么?", + "title": "MySQL 中 join 与 left join 的区别是什么?", "frontmatter": { - "title": "MySQL 中 varchar 和 char 的区别是什么?", - "date": "2022-05-21T17:10:21.000Z", - "permalink": "/pages/d97948/", + "title": "MySQL 中 join 与 left join 的区别是什么?", + "date": "2022-05-21T17:09:52.000Z", + "permalink": "/pages/429100/", "categories": [ "数据库", "MYSQL相关" @@ -4838,10 +5234,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/170.MySQL%20%E4%B8%AD%20varchar%20%E5%92%8C%20char%20%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/170.MySQL 中 varchar 和 char 的区别是什么?.md", - "key": "v-0a7e6416", - "path": "/pages/d97948/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/150.MySQL%20%E4%B8%AD%20join%20%E4%B8%8E%20left%20join%20%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/150.MySQL 中 join 与 left join 的区别是什么?.md", + "key": "v-77790024", + "path": "/pages/429100/", "headersStr": null, "content": "", "normalizedContent": "", @@ -4875,23 +5271,23 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "为什么 Redis 在单线程下能如此快?", + "title": "MySQL 中 varchar 和 char 的区别是什么?", "frontmatter": { - "title": "为什么 Redis 在单线程下能如此快?", - "date": "2022-05-21T17:20:12.000Z", - "permalink": "/pages/d66211/", + "title": "MySQL 中 varchar 和 char 的区别是什么?", + "date": "2022-05-21T17:10:21.000Z", + "permalink": "/pages/d97948/", "categories": [ "数据库", - "REDIS相关" + "MYSQL相关" ], "tags": [ null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/002.%E4%B8%BA%E4%BB%80%E4%B9%88%20Redis%20%E5%9C%A8%E5%8D%95%E7%BA%BF%E7%A8%8B%E4%B8%8B%E8%83%BD%E5%A6%82%E6%AD%A4%E5%BF%AB%EF%BC%9F.html", - "relativePath": "03.数据库/02.REDIS相关/002.为什么 Redis 在单线程下能如此快?.md", - "key": "v-cb742458", - "path": "/pages/d66211/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/170.MySQL%20%E4%B8%AD%20varchar%20%E5%92%8C%20char%20%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/170.MySQL 中 varchar 和 char 的区别是什么?.md", + "key": "v-0a7e6416", + "path": "/pages/d97948/", "headersStr": null, "content": "", "normalizedContent": "", @@ -4977,6 +5373,56 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, + { + "title": "为什么 Redis 在单线程下能如此快?", + "frontmatter": { + "title": "为什么 Redis 在单线程下能如此快?", + "date": "2022-05-21T17:20:12.000Z", + "permalink": "/pages/d66211/", + "categories": [ + "数据库", + "REDIS相关" + ], + "tags": [ + null + ] + }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/002.%E4%B8%BA%E4%BB%80%E4%B9%88%20Redis%20%E5%9C%A8%E5%8D%95%E7%BA%BF%E7%A8%8B%E4%B8%8B%E8%83%BD%E5%A6%82%E6%AD%A4%E5%BF%AB%EF%BC%9F.html", + "relativePath": "03.数据库/02.REDIS相关/002.为什么 Redis 在单线程下能如此快?.md", + "key": "v-cb742458", + "path": "/pages/d66211/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "简述 Redis 字符串的底层结构", + "frontmatter": { + "title": "简述 Redis 字符串的底层结构", + "date": "2022-05-21T17:20:49.000Z", + "permalink": "/pages/34aec2/", + "categories": [ + "数据库", + "REDIS相关" + ], + "tags": [ + null + ] + }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/005.%E7%AE%80%E8%BF%B0%20Redis%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84.html", + "relativePath": "03.数据库/02.REDIS相关/005.简述 Redis 字符串的底层结构.md", + "key": "v-3e917471", + "path": "/pages/34aec2/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, { "title": "Redis的缓存淘汰策略有哪些?", "frontmatter": { @@ -5021,11 +5467,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述 Redis 字符串的底层结构", + "title": "简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点", "frontmatter": { - "title": "简述 Redis 字符串的底层结构", - "date": "2022-05-21T17:20:49.000Z", - "permalink": "/pages/34aec2/", + "title": "简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点", + "date": "2022-05-21T17:21:43.000Z", + "permalink": "/pages/7e708d/", "categories": [ "数据库", "REDIS相关" @@ -5034,39 +5480,32 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/005.%E7%AE%80%E8%BF%B0%20Redis%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84.html", - "relativePath": "03.数据库/02.REDIS相关/005.简述 Redis 字符串的底层结构.md", - "key": "v-3e917471", - "path": "/pages/34aec2/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "Redis 如何实现分布式锁?", - "frontmatter": { - "title": "Redis 如何实现分布式锁?", - "date": "2022-05-21T17:21:32.000Z", - "permalink": "/pages/656f46/", - "categories": [ - "数据库", - "REDIS相关" - ], - "tags": [ - null - ] + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/020.%E7%AE%80%E8%BF%B0%20Redis%20%E6%8C%81%E4%B9%85%E5%8C%96%E4%B8%AD%20RDB%20%E4%BB%A5%E5%8F%8A%20AOF%20%E6%96%B9%E6%A1%88%E7%9A%84%E4%BC%98%E7%BC%BA%E7%82%B9.html", + "relativePath": "03.数据库/02.REDIS相关/020.简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点.md", + "key": "v-7c214d00", + "path": "/pages/7e708d/", + "headers": [ + { + "level": 2, + "title": "RDB持久化", + "slug": "rdb持久化", + "normalizedTitle": "rdb持久化", + "charIndex": 2 + }, + { + "level": 2, + "title": "AOF持久化", + "slug": "aof持久化", + "normalizedTitle": "aof持久化", + "charIndex": 1345 + } + ], + "headersStr": "RDB持久化 AOF持久化", + "content": "# RDB持久化\n\n 1. save和bgsave源码\n\nvoid saveCommand(redisClient *c) {\n\n // BGSAVE 已经在执行中,不能再执行 SAVE\n // 否则将产生竞争条件\n if (server.rdb_child_pid != -1) {\n addReplyError(c,\"Background save already in progress\");\n return;\n }\n\n // 执行 \n if (rdbSave(server.rdb_filename) == REDIS_OK) {\n addReply(c,shared.ok);\n } else {\n addReply(c,shared.err);\n }\n}\n\nvoid bgsaveCommand(redisClient *c) {\n\n // 不能重复执行 BGSAVE\n if (server.rdb_child_pid != -1) {\n addReplyError(c,\"Background save already in progress\");\n\n // 不能在 BGREWRITEAOF 正在运行时执行\n } else if (server.aof_child_pid != -1) {\n addReplyError(c,\"Can't BGSAVE while AOF log rewriting is in progress\");\n\n // 执行 BGSAVE\n } else if (rdbSaveBackground(server.rdb_filename) == REDIS_OK) {\n addReplyStatus(c,\"Background saving started\");\n\n } else {\n addReply(c,shared.err);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n\n 2. 自动间隔性保存\n\n> 服务器间隔性检车saveparams中的任意一个条件是否得到满足,如果得到满足,执行bgsave.saveparams[0]表示距离上次bgsave900s内,至少有一次数据库变化。\n\n[外链图片转存中...(img-SwAqAX8z-1653145947293)]\n\n 3. rdb文件结构 [外链图片转存中...(img-Prs54pGE-1653145947294)] 其中database结构如下: [外链图片转存中...(img-rn1gUv6w-1653145947294)] 其中ksy_value_pairs结构如下 [外链图片转存中...(img-B62S2gV5-1653145947295)]\n\n> type指定了value的类型。value的编码方式不同期存储结构也不同。\n\n\n# AOF持久化\n\n 1. aof的实现 命令追加:所有客户端命令都会被存到redis_server的aof_buf缓冲区。 文件写入:操作系统在写入文件的时候,先写入缓存,缓存满了才写入文件。 文件同步:flushAppendOnlyFile()负责将aof_buf写入文件aof。\n\n> flushAppendOnlyFile中配置的appendfsync决定同步策略。appendfsync有三个取值:always(每次写入都同步)、everysec(每次事件都写入但不同步,每秒同步)、 no(每次事件都写入但不同步,操作系统决定同步);\n\n 2. aof的载入与还原 [外链图片转存中...(img-pNMyXRLJ-1653145947295)]\n 3. AOF重写 [外链图片转存中...(img-i1nhwgNs-1653145947295)]", + "normalizedContent": "# rdb持久化\n\n 1. save和bgsave源码\n\nvoid savecommand(redisclient *c) {\n\n // bgsave 已经在执行中,不能再执行 save\n // 否则将产生竞争条件\n if (server.rdb_child_pid != -1) {\n addreplyerror(c,\"background save already in progress\");\n return;\n }\n\n // 执行 \n if (rdbsave(server.rdb_filename) == redis_ok) {\n addreply(c,shared.ok);\n } else {\n addreply(c,shared.err);\n }\n}\n\nvoid bgsavecommand(redisclient *c) {\n\n // 不能重复执行 bgsave\n if (server.rdb_child_pid != -1) {\n addreplyerror(c,\"background save already in progress\");\n\n // 不能在 bgrewriteaof 正在运行时执行\n } else if (server.aof_child_pid != -1) {\n addreplyerror(c,\"can't bgsave while aof log rewriting is in progress\");\n\n // 执行 bgsave\n } else if (rdbsavebackground(server.rdb_filename) == redis_ok) {\n addreplystatus(c,\"background saving started\");\n\n } else {\n addreply(c,shared.err);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n\n 2. 自动间隔性保存\n\n> 服务器间隔性检车saveparams中的任意一个条件是否得到满足,如果得到满足,执行bgsave.saveparams[0]表示距离上次bgsave900s内,至少有一次数据库变化。\n\n[外链图片转存中...(img-swaqax8z-1653145947293)]\n\n 3. rdb文件结构 [外链图片转存中...(img-prs54pge-1653145947294)] 其中database结构如下: [外链图片转存中...(img-rn1guv6w-1653145947294)] 其中ksy_value_pairs结构如下 [外链图片转存中...(img-b62s2gv5-1653145947295)]\n\n> type指定了value的类型。value的编码方式不同期存储结构也不同。\n\n\n# aof持久化\n\n 1. aof的实现 命令追加:所有客户端命令都会被存到redis_server的aof_buf缓冲区。 文件写入:操作系统在写入文件的时候,先写入缓存,缓存满了才写入文件。 文件同步:flushappendonlyfile()负责将aof_buf写入文件aof。\n\n> flushappendonlyfile中配置的appendfsync决定同步策略。appendfsync有三个取值:always(每次写入都同步)、everysec(每次事件都写入但不同步,每秒同步)、 no(每次事件都写入但不同步,操作系统决定同步);\n\n 2. aof的载入与还原 [外链图片转存中...(img-pnmyxrlj-1653145947295)]\n 3. aof重写 [外链图片转存中...(img-i1nhwgns-1653145947295)]", + "charsets": { + "cjk": true }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/030.Redis%20%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%EF%BC%9F.html", - "relativePath": "03.数据库/02.REDIS相关/030.Redis 如何实现分布式锁?.md", - "key": "v-2c6b511c", - "path": "/pages/656f46/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, @@ -5107,11 +5546,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述 Redis 中跳表的应用以及优缺点", + "title": "Redis 如何实现分布式锁?", "frontmatter": { - "title": "简述 Redis 中跳表的应用以及优缺点", - "date": "2022-05-21T17:22:09.000Z", - "permalink": "/pages/a42826/", + "title": "Redis 如何实现分布式锁?", + "date": "2022-05-21T17:21:32.000Z", + "permalink": "/pages/656f46/", "categories": [ "数据库", "REDIS相关" @@ -5120,10 +5559,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/050.%E7%AE%80%E8%BF%B0%20Redis%20%E4%B8%AD%E8%B7%B3%E8%A1%A8%E7%9A%84%E5%BA%94%E7%94%A8%E4%BB%A5%E5%8F%8A%E4%BC%98%E7%BC%BA%E7%82%B9.html", - "relativePath": "03.数据库/02.REDIS相关/050.简述 Redis 中跳表的应用以及优缺点.md", - "key": "v-18b427a0", - "path": "/pages/a42826/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/030.Redis%20%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%EF%BC%9F.html", + "relativePath": "03.数据库/02.REDIS相关/030.Redis 如何实现分布式锁?.md", + "key": "v-2c6b511c", + "path": "/pages/656f46/", "headersStr": null, "content": "", "normalizedContent": "", @@ -5157,36 +5596,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述 Redis 中如何防止缓存雪崩和缓存击穿", - "frontmatter": { - "title": "简述 Redis 中如何防止缓存雪崩和缓存击穿", - "date": "2022-05-21T17:22:52.000Z", - "permalink": "/pages/03cd25/", - "categories": [ - "数据库", - "REDIS相关" - ], - "tags": [ - null - ] - }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/070.%E7%AE%80%E8%BF%B0%20Redis%20%E4%B8%AD%E5%A6%82%E4%BD%95%E9%98%B2%E6%AD%A2%E7%BC%93%E5%AD%98%E9%9B%AA%E5%B4%A9%E5%92%8C%E7%BC%93%E5%AD%98%E5%87%BB%E7%A9%BF.html", - "relativePath": "03.数据库/02.REDIS相关/070.简述 Redis 中如何防止缓存雪崩和缓存击穿.md", - "key": "v-c2fe5e1a", - "path": "/pages/03cd25/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "简述 Redis 的线程模型以及底层架构设计", + "title": "简述 Redis 中跳表的应用以及优缺点", "frontmatter": { - "title": "简述 Redis 的线程模型以及底层架构设计", - "date": "2022-05-21T17:23:08.000Z", - "permalink": "/pages/9c28c9/", + "title": "简述 Redis 中跳表的应用以及优缺点", + "date": "2022-05-21T17:22:09.000Z", + "permalink": "/pages/a42826/", "categories": [ "数据库", "REDIS相关" @@ -5195,10 +5609,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/080.%E7%AE%80%E8%BF%B0%20Redis%20%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B%E4%BB%A5%E5%8F%8A%E5%BA%95%E5%B1%82%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1.html", - "relativePath": "03.数据库/02.REDIS相关/080.简述 Redis 的线程模型以及底层架构设计.md", - "key": "v-35718f4e", - "path": "/pages/9c28c9/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/050.%E7%AE%80%E8%BF%B0%20Redis%20%E4%B8%AD%E8%B7%B3%E8%A1%A8%E7%9A%84%E5%BA%94%E7%94%A8%E4%BB%A5%E5%8F%8A%E4%BC%98%E7%BC%BA%E7%82%B9.html", + "relativePath": "03.数据库/02.REDIS相关/050.简述 Redis 中跳表的应用以及优缺点.md", + "key": "v-18b427a0", + "path": "/pages/a42826/", "headersStr": null, "content": "", "normalizedContent": "", @@ -5207,11 +5621,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述 Redis 如何处理热点 key 访问", + "title": "简述 Redis 中如何防止缓存雪崩和缓存击穿", "frontmatter": { - "title": "简述 Redis 如何处理热点 key 访问", - "date": "2022-05-21T17:23:43.000Z", - "permalink": "/pages/795c09/", + "title": "简述 Redis 中如何防止缓存雪崩和缓存击穿", + "date": "2022-05-21T17:22:52.000Z", + "permalink": "/pages/03cd25/", "categories": [ "数据库", "REDIS相关" @@ -5220,10 +5634,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/110.%E7%AE%80%E8%BF%B0%20Redis%20%E5%A6%82%E4%BD%95%E5%A4%84%E7%90%86%E7%83%AD%E7%82%B9%20key%20%E8%AE%BF%E9%97%AE.html", - "relativePath": "03.数据库/02.REDIS相关/110.简述 Redis 如何处理热点 key 访问.md", - "key": "v-37f158ba", - "path": "/pages/795c09/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/070.%E7%AE%80%E8%BF%B0%20Redis%20%E4%B8%AD%E5%A6%82%E4%BD%95%E9%98%B2%E6%AD%A2%E7%BC%93%E5%AD%98%E9%9B%AA%E5%B4%A9%E5%92%8C%E7%BC%93%E5%AD%98%E5%87%BB%E7%A9%BF.html", + "relativePath": "03.数据库/02.REDIS相关/070.简述 Redis 中如何防止缓存雪崩和缓存击穿.md", + "key": "v-c2fe5e1a", + "path": "/pages/03cd25/", "headersStr": null, "content": "", "normalizedContent": "", @@ -5268,11 +5682,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点", + "title": "简述 Redis 如何处理热点 key 访问", "frontmatter": { - "title": "简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点", - "date": "2022-05-21T17:21:43.000Z", - "permalink": "/pages/7e708d/", + "title": "简述 Redis 如何处理热点 key 访问", + "date": "2022-05-21T17:23:43.000Z", + "permalink": "/pages/795c09/", "categories": [ "数据库", "REDIS相关" @@ -5281,96 +5695,64 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/020.%E7%AE%80%E8%BF%B0%20Redis%20%E6%8C%81%E4%B9%85%E5%8C%96%E4%B8%AD%20RDB%20%E4%BB%A5%E5%8F%8A%20AOF%20%E6%96%B9%E6%A1%88%E7%9A%84%E4%BC%98%E7%BC%BA%E7%82%B9.html", - "relativePath": "03.数据库/02.REDIS相关/020.简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点.md", - "key": "v-7c214d00", - "path": "/pages/7e708d/", - "headers": [ - { - "level": 2, - "title": "RDB持久化", - "slug": "rdb持久化", - "normalizedTitle": "rdb持久化", - "charIndex": 2 - }, - { - "level": 2, - "title": "AOF持久化", - "slug": "aof持久化", - "normalizedTitle": "aof持久化", - "charIndex": 1345 - } - ], - "headersStr": "RDB持久化 AOF持久化", - "content": "# RDB持久化\n\n 1. save和bgsave源码\n\nvoid saveCommand(redisClient *c) {\n\n // BGSAVE 已经在执行中,不能再执行 SAVE\n // 否则将产生竞争条件\n if (server.rdb_child_pid != -1) {\n addReplyError(c,\"Background save already in progress\");\n return;\n }\n\n // 执行 \n if (rdbSave(server.rdb_filename) == REDIS_OK) {\n addReply(c,shared.ok);\n } else {\n addReply(c,shared.err);\n }\n}\n\nvoid bgsaveCommand(redisClient *c) {\n\n // 不能重复执行 BGSAVE\n if (server.rdb_child_pid != -1) {\n addReplyError(c,\"Background save already in progress\");\n\n // 不能在 BGREWRITEAOF 正在运行时执行\n } else if (server.aof_child_pid != -1) {\n addReplyError(c,\"Can't BGSAVE while AOF log rewriting is in progress\");\n\n // 执行 BGSAVE\n } else if (rdbSaveBackground(server.rdb_filename) == REDIS_OK) {\n addReplyStatus(c,\"Background saving started\");\n\n } else {\n addReply(c,shared.err);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n\n 2. 自动间隔性保存\n\n> 服务器间隔性检车saveparams中的任意一个条件是否得到满足,如果得到满足,执行bgsave.saveparams[0]表示距离上次bgsave900s内,至少有一次数据库变化。\n\n[外链图片转存中...(img-SwAqAX8z-1653145947293)]\n\n 3. rdb文件结构 [外链图片转存中...(img-Prs54pGE-1653145947294)] 其中database结构如下: [外链图片转存中...(img-rn1gUv6w-1653145947294)] 其中ksy_value_pairs结构如下 [外链图片转存中...(img-B62S2gV5-1653145947295)]\n\n> type指定了value的类型。value的编码方式不同期存储结构也不同。\n\n\n# AOF持久化\n\n 1. aof的实现 命令追加:所有客户端命令都会被存到redis_server的aof_buf缓冲区。 文件写入:操作系统在写入文件的时候,先写入缓存,缓存满了才写入文件。 文件同步:flushAppendOnlyFile()负责将aof_buf写入文件aof。\n\n> flushAppendOnlyFile中配置的appendfsync决定同步策略。appendfsync有三个取值:always(每次写入都同步)、everysec(每次事件都写入但不同步,每秒同步)、 no(每次事件都写入但不同步,操作系统决定同步);\n\n 2. aof的载入与还原 [外链图片转存中...(img-pNMyXRLJ-1653145947295)]\n 3. AOF重写 [外链图片转存中...(img-i1nhwgNs-1653145947295)]", - "normalizedContent": "# rdb持久化\n\n 1. save和bgsave源码\n\nvoid savecommand(redisclient *c) {\n\n // bgsave 已经在执行中,不能再执行 save\n // 否则将产生竞争条件\n if (server.rdb_child_pid != -1) {\n addreplyerror(c,\"background save already in progress\");\n return;\n }\n\n // 执行 \n if (rdbsave(server.rdb_filename) == redis_ok) {\n addreply(c,shared.ok);\n } else {\n addreply(c,shared.err);\n }\n}\n\nvoid bgsavecommand(redisclient *c) {\n\n // 不能重复执行 bgsave\n if (server.rdb_child_pid != -1) {\n addreplyerror(c,\"background save already in progress\");\n\n // 不能在 bgrewriteaof 正在运行时执行\n } else if (server.aof_child_pid != -1) {\n addreplyerror(c,\"can't bgsave while aof log rewriting is in progress\");\n\n // 执行 bgsave\n } else if (rdbsavebackground(server.rdb_filename) == redis_ok) {\n addreplystatus(c,\"background saving started\");\n\n } else {\n addreply(c,shared.err);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n\n 2. 自动间隔性保存\n\n> 服务器间隔性检车saveparams中的任意一个条件是否得到满足,如果得到满足,执行bgsave.saveparams[0]表示距离上次bgsave900s内,至少有一次数据库变化。\n\n[外链图片转存中...(img-swaqax8z-1653145947293)]\n\n 3. rdb文件结构 [外链图片转存中...(img-prs54pge-1653145947294)] 其中database结构如下: [外链图片转存中...(img-rn1guv6w-1653145947294)] 其中ksy_value_pairs结构如下 [外链图片转存中...(img-b62s2gv5-1653145947295)]\n\n> type指定了value的类型。value的编码方式不同期存储结构也不同。\n\n\n# aof持久化\n\n 1. aof的实现 命令追加:所有客户端命令都会被存到redis_server的aof_buf缓冲区。 文件写入:操作系统在写入文件的时候,先写入缓存,缓存满了才写入文件。 文件同步:flushappendonlyfile()负责将aof_buf写入文件aof。\n\n> flushappendonlyfile中配置的appendfsync决定同步策略。appendfsync有三个取值:always(每次写入都同步)、everysec(每次事件都写入但不同步,每秒同步)、 no(每次事件都写入但不同步,操作系统决定同步);\n\n 2. aof的载入与还原 [外链图片转存中...(img-pnmyxrlj-1653145947295)]\n 3. aof重写 [外链图片转存中...(img-i1nhwgns-1653145947295)]", - "charsets": { - "cjk": true + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/110.%E7%AE%80%E8%BF%B0%20Redis%20%E5%A6%82%E4%BD%95%E5%A4%84%E7%90%86%E7%83%AD%E7%82%B9%20key%20%E8%AE%BF%E9%97%AE.html", + "relativePath": "03.数据库/02.REDIS相关/110.简述 Redis 如何处理热点 key 访问.md", + "key": "v-37f158ba", + "path": "/pages/795c09/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "简述 Redis 的线程模型以及底层架构设计", + "frontmatter": { + "title": "简述 Redis 的线程模型以及底层架构设计", + "date": "2022-05-21T17:23:08.000Z", + "permalink": "/pages/9c28c9/", + "categories": [ + "数据库", + "REDIS相关" + ], + "tags": [ + null + ] }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/080.%E7%AE%80%E8%BF%B0%20Redis%20%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B%E4%BB%A5%E5%8F%8A%E5%BA%95%E5%B1%82%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1.html", + "relativePath": "03.数据库/02.REDIS相关/080.简述 Redis 的线程模型以及底层架构设计.md", + "key": "v-35718f4e", + "path": "/pages/9c28c9/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, { - "title": "TCP粘包拆包问题的解决之道", + "title": "Redis 序列化有哪些方式?", "frontmatter": { - "title": "TCP粘包拆包问题的解决之道", - "date": "2022-05-21T23:32:33.000Z", - "permalink": "/pages/3db019/", + "title": "Redis 序列化有哪些方式?", + "date": "2022-05-21T17:23:58.000Z", + "permalink": "/pages/0d3c6b/", "categories": [ - "中间件", - "Netty" + "数据库", + "REDIS相关" ], "tags": [ null ] }, - "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/10.Netty/010.TCP%E7%B2%98%E5%8C%85%E6%8B%86%E5%8C%85%E9%97%AE%E9%A2%98%E7%9A%84%E8%A7%A3%E5%86%B3%E4%B9%8B%E9%81%93.html", - "relativePath": "04.中间件/10.Netty/010.TCP粘包拆包问题的解决之道.md", - "key": "v-5acdab4d", - "path": "/pages/3db019/", - "headers": [ - { - "level": 2, - "title": "4.1 TCP粘包/拆包", - "slug": "_4-1-tcp粘包-拆包", - "normalizedTitle": "4.1 tcp粘包/拆包", - "charIndex": 2 - }, - { - "level": 3, - "title": "4.1.1 TCP粘包/拆包问题说明", - "slug": "_4-1-1-tcp粘包-拆包问题说明", - "normalizedTitle": "4.1.1 tcp粘包/拆包问题说明", - "charIndex": 19 - }, - { - "level": 3, - "title": "4.1.2 TCP粘包/拆包发生的原因", - "slug": "_4-1-2-tcp粘包-拆包发生的原因", - "normalizedTitle": "4.1.2 tcp粘包/拆包发生的原因", - "charIndex": 111 - }, - { - "level": 3, - "title": "4.1.3 TCP粘包/拆包问题的解决策略", - "slug": "_4-1-3-tcp粘包-拆包问题的解决策略", - "normalizedTitle": "4.1.3 tcp粘包/拆包问题的解决策略", - "charIndex": 181 - }, - { - "level": 2, - "title": "4.3 Netty解决tcp粘包问题", - "slug": "_4-3-netty解决tcp粘包问题", - "normalizedTitle": "4.3 netty解决tcp粘包问题", - "charIndex": 322 - } - ], - "headersStr": "4.1 TCP粘包/拆包 4.1.1 TCP粘包/拆包问题说明 4.1.2 TCP粘包/拆包发生的原因 4.1.3 TCP粘包/拆包问题的解决策略 4.3 Netty解决tcp粘包问题", - "content": "# 4.1 TCP粘包/拆包\n\n\n# 4.1.1 TCP粘包/拆包问题说明\n\nTCP协议是”流“协议,流是没有间隔的。tcp会根据缓存大小将业务上的大包划分成多个小包发送出去、也可能多个小包合成一个大包发送出去。\n\n\n# 4.1.2 TCP粘包/拆包发生的原因\n\n * 应用层:大于套接字接口缓冲区大小\n * TCP层:MSS\n * IP层:MTU\n\n\n\n\n# 4.1.3 TCP粘包/拆包问题的解决策略\n\n * 消息定长len,例如每个报文固定200字节。那么读取到定长len后就重置计数器开始读取下一个包。\n * 包尾加换行符分割,如ftp。\n * 消息头+消息体。消息头包含消息长度信息。\n * 更复杂的应用协议,如netty.\n\n\n# 4.3 Netty解决tcp粘包问题\n\n * LineBasedFrameDecoder:原理是遍历ByteBuf中字节,以换行符分割\n * StringDecoder:将接收的byte对象转换为字符串,然后调用后面的handler 如果发送的消息不是以换行符结束的,netty也有其他解码器支持。", - "normalizedContent": "# 4.1 tcp粘包/拆包\n\n\n# 4.1.1 tcp粘包/拆包问题说明\n\ntcp协议是”流“协议,流是没有间隔的。tcp会根据缓存大小将业务上的大包划分成多个小包发送出去、也可能多个小包合成一个大包发送出去。\n\n\n# 4.1.2 tcp粘包/拆包发生的原因\n\n * 应用层:大于套接字接口缓冲区大小\n * tcp层:mss\n * ip层:mtu\n\n\n\n\n# 4.1.3 tcp粘包/拆包问题的解决策略\n\n * 消息定长len,例如每个报文固定200字节。那么读取到定长len后就重置计数器开始读取下一个包。\n * 包尾加换行符分割,如ftp。\n * 消息头+消息体。消息头包含消息长度信息。\n * 更复杂的应用协议,如netty.\n\n\n# 4.3 netty解决tcp粘包问题\n\n * linebasedframedecoder:原理是遍历bytebuf中字节,以换行符分割\n * stringdecoder:将接收的byte对象转换为字符串,然后调用后面的handler 如果发送的消息不是以换行符结束的,netty也有其他解码器支持。", - "charsets": { - "cjk": true - }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/120.Redis%20%E5%BA%8F%E5%88%97%E5%8C%96%E6%9C%89%E5%93%AA%E4%BA%9B%E6%96%B9%E5%BC%8F%EF%BC%9F.html", + "relativePath": "03.数据库/02.REDIS相关/120.Redis 序列化有哪些方式?.md", + "key": "v-02ea33c3", + "path": "/pages/0d3c6b/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, @@ -5481,27 +5863,109 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "Redis 序列化有哪些方式?", + "title": "TCP粘包拆包问题的解决之道", "frontmatter": { - "title": "Redis 序列化有哪些方式?", - "date": "2022-05-21T17:23:58.000Z", - "permalink": "/pages/0d3c6b/", + "title": "TCP粘包拆包问题的解决之道", + "date": "2022-05-21T23:32:33.000Z", + "permalink": "/pages/3db019/", "categories": [ - "数据库", - "REDIS相关" + "中间件", + "Netty" ], "tags": [ null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/120.Redis%20%E5%BA%8F%E5%88%97%E5%8C%96%E6%9C%89%E5%93%AA%E4%BA%9B%E6%96%B9%E5%BC%8F%EF%BC%9F.html", - "relativePath": "03.数据库/02.REDIS相关/120.Redis 序列化有哪些方式?.md", - "key": "v-02ea33c3", - "path": "/pages/0d3c6b/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, + "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/10.Netty/010.TCP%E7%B2%98%E5%8C%85%E6%8B%86%E5%8C%85%E9%97%AE%E9%A2%98%E7%9A%84%E8%A7%A3%E5%86%B3%E4%B9%8B%E9%81%93.html", + "relativePath": "04.中间件/10.Netty/010.TCP粘包拆包问题的解决之道.md", + "key": "v-5acdab4d", + "path": "/pages/3db019/", + "headers": [ + { + "level": 2, + "title": "4.1 TCP粘包/拆包", + "slug": "_4-1-tcp粘包-拆包", + "normalizedTitle": "4.1 tcp粘包/拆包", + "charIndex": 2 + }, + { + "level": 3, + "title": "4.1.1 TCP粘包/拆包问题说明", + "slug": "_4-1-1-tcp粘包-拆包问题说明", + "normalizedTitle": "4.1.1 tcp粘包/拆包问题说明", + "charIndex": 19 + }, + { + "level": 3, + "title": "4.1.2 TCP粘包/拆包发生的原因", + "slug": "_4-1-2-tcp粘包-拆包发生的原因", + "normalizedTitle": "4.1.2 tcp粘包/拆包发生的原因", + "charIndex": 111 + }, + { + "level": 3, + "title": "4.1.3 TCP粘包/拆包问题的解决策略", + "slug": "_4-1-3-tcp粘包-拆包问题的解决策略", + "normalizedTitle": "4.1.3 tcp粘包/拆包问题的解决策略", + "charIndex": 181 + }, + { + "level": 2, + "title": "4.3 Netty解决tcp粘包问题", + "slug": "_4-3-netty解决tcp粘包问题", + "normalizedTitle": "4.3 netty解决tcp粘包问题", + "charIndex": 322 + } + ], + "headersStr": "4.1 TCP粘包/拆包 4.1.1 TCP粘包/拆包问题说明 4.1.2 TCP粘包/拆包发生的原因 4.1.3 TCP粘包/拆包问题的解决策略 4.3 Netty解决tcp粘包问题", + "content": "# 4.1 TCP粘包/拆包\n\n\n# 4.1.1 TCP粘包/拆包问题说明\n\nTCP协议是”流“协议,流是没有间隔的。tcp会根据缓存大小将业务上的大包划分成多个小包发送出去、也可能多个小包合成一个大包发送出去。\n\n\n# 4.1.2 TCP粘包/拆包发生的原因\n\n * 应用层:大于套接字接口缓冲区大小\n * TCP层:MSS\n * IP层:MTU\n\n\n\n\n# 4.1.3 TCP粘包/拆包问题的解决策略\n\n * 消息定长len,例如每个报文固定200字节。那么读取到定长len后就重置计数器开始读取下一个包。\n * 包尾加换行符分割,如ftp。\n * 消息头+消息体。消息头包含消息长度信息。\n * 更复杂的应用协议,如netty.\n\n\n# 4.3 Netty解决tcp粘包问题\n\n * LineBasedFrameDecoder:原理是遍历ByteBuf中字节,以换行符分割\n * StringDecoder:将接收的byte对象转换为字符串,然后调用后面的handler 如果发送的消息不是以换行符结束的,netty也有其他解码器支持。", + "normalizedContent": "# 4.1 tcp粘包/拆包\n\n\n# 4.1.1 tcp粘包/拆包问题说明\n\ntcp协议是”流“协议,流是没有间隔的。tcp会根据缓存大小将业务上的大包划分成多个小包发送出去、也可能多个小包合成一个大包发送出去。\n\n\n# 4.1.2 tcp粘包/拆包发生的原因\n\n * 应用层:大于套接字接口缓冲区大小\n * tcp层:mss\n * ip层:mtu\n\n\n\n\n# 4.1.3 tcp粘包/拆包问题的解决策略\n\n * 消息定长len,例如每个报文固定200字节。那么读取到定长len后就重置计数器开始读取下一个包。\n * 包尾加换行符分割,如ftp。\n * 消息头+消息体。消息头包含消息长度信息。\n * 更复杂的应用协议,如netty.\n\n\n# 4.3 netty解决tcp粘包问题\n\n * linebasedframedecoder:原理是遍历bytebuf中字节,以换行符分割\n * stringdecoder:将接收的byte对象转换为字符串,然后调用后面的handler 如果发送的消息不是以换行符结束的,netty也有其他解码器支持。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "为什么Netty要自己写一个ByteBuf", + "frontmatter": { + "title": "为什么Netty要自己写一个ByteBuf", + "date": "2022-05-21T23:35:32.000Z", + "permalink": "/pages/ef17ee/", + "categories": [ + "中间件", + "Netty" + ], + "tags": [ + null + ] + }, + "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/10.Netty/020.%E4%B8%BA%E4%BB%80%E4%B9%88Netty%E8%A6%81%E8%87%AA%E5%B7%B1%E5%86%99%E4%B8%80%E4%B8%AAByteBuf.html", + "relativePath": "04.中间件/10.Netty/020.为什么Netty要自己写一个ByteBuf.md", + "key": "v-bbcf6e5a", + "path": "/pages/ef17ee/", + "headers": [ + { + "level": 2, + "title": "15.2 ByteBuf源码分析", + "slug": "_15-2-bytebuf源码分析", + "normalizedTitle": "15.2 bytebuf源码分析", + "charIndex": 2 + }, + { + "level": 2, + "title": "15.3 ByteBuffer相关辅助类", + "slug": "_15-3-bytebuffer相关辅助类", + "normalizedTitle": "15.3 bytebuffer相关辅助类", + "charIndex": 8055 + } + ], + "headersStr": "15.2 ByteBuf源码分析 15.3 ByteBuffer相关辅助类", + "content": "# 15.2 ByteBuf源码分析\n\n继承关系 从内存分配的角度看,byteBuf分为两类:\n\n * 堆内存缓冲区:优点是内存的分配和回收快。缺点是进行IO读写时需要一次内存复制,用户空间和内核空间的复制。\n * 直接内存缓冲区:优缺点和堆内存缓冲区整好相反。 经验表明ByteBuf的最佳实践是在IO通信线程的读写缓冲区使用DirectByteBuf,后端业务的编解码模块使用HeapByteBuf,这样的组合可以达到性能最优。 从内存回收的角度看、ByteBuf分两类,基于对象池的ByteBuf和普通ByteBuf。使用内存池后的Netty在高并发和高负载环境下内存和GC更加平稳。 15.2.1 AbstractByteBuf源码分析 实现ByteBuf的一些公共属性和功能。 主要成员变量\n\n static final ResourceLeakDetector leakDetector =\n ResourceLeakDetectorFactory.instance().newResourceLeakDetector(ByteBuf.class);//用于对象是否泄漏,定义为static,意味着所有byteBuf共享\n\n int readerIndex;//读索引\n int writerIndex;//写索引\n private int markedReaderIndex;//读mark\n private int markedWriterIndex;//写mark\n private int maxCapacity;//最大容量\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\nByte数组不在这里,因为AbstractByteBuf无法确定使用直接内存还是堆内存。 readBytes(ByteBuf dst, int dstIndex, int length)\n\npublic ByteBuf readBytes(ByteBuf dst, int dstIndex, int length) {\n checkReadableBytes(length);//校验可读性\n getBytes(readerIndex, dst, dstIndex, length);//读取。从readerIndex开始读取length个字节到目标数组中\n readerIndex += length;//移动读指针\n return this;\n }\n\n\n1\n2\n3\n4\n5\n6\n\n\n再看一下checkReadableBytes():\n\n/**\n * Throws an {@link IndexOutOfBoundsException} if the current\n * {@linkplain #readableBytes() readable bytes} of this buffer is less\n * than the specified value.\n */\n protected final void checkReadableBytes(int minimumReadableBytes) {\n if (minimumReadableBytes < 0) {\n throw new IllegalArgumentException(\"minimumReadableBytes: \" + minimumReadableBytes + \" (expected: >= 0)\");\n }\n checkReadableBytes0(minimumReadableBytes);\n }\n \nprivate void checkReadableBytes0(int minimumReadableBytes) {\n ensureAccessible();\n if (readerIndex > writerIndex - minimumReadableBytes) {\n throw new IndexOutOfBoundsException(String.format(\n \"readerIndex(%d) + length(%d) exceeds writerIndex(%d): %s\",\n readerIndex, minimumReadableBytes, writerIndex, this));\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\nwriteBytes(byte[] src, int srcIndex, int length)\n\npublic ByteBuf writeBytes(byte[] src, int srcIndex, int length) {\n ensureWritable(length);//可写校验和扩容\n setBytes(writerIndex, src, srcIndex, length);//从writerIndex开始写length长度\n writerIndex += length;//移动写指针\n return this;\n }\n\n\n1\n2\n3\n4\n5\n6\n\n\nensureWritable(int minWritableBytes)\n\npublic ByteBuf ensureWritable(int minWritableBytes) {\n if (minWritableBytes < 0) {\n throw new IllegalArgumentException(String.format(\n \"minWritableBytes: %d (expected: >= 0)\", minWritableBytes));\n }\n ensureWritable0(minWritableBytes);\n return this;\n }\n \nfinal void ensureWritable0(int minWritableBytes) {\n ensureAccessible();//检查这个buf是否还有引用(如果已经没有引用那就没必要在写了)\n if (minWritableBytes <= writableBytes()) {//写入的字节小于可写字节,校验通过\n return;\n }\n\n if (minWritableBytes > maxCapacity - writerIndex) {//写入的字节大于最大可写入字节,抛异常\n throw new IndexOutOfBoundsException(String.format(\n \"writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s\",\n writerIndex, minWritableBytes, maxCapacity, this));\n }\n\n // Normalize the current capacity to the power of 2.\n int newCapacity = alloc().calculateNewCapacity(writerIndex + minWritableBytes, maxCapacity);\n\n // Adjust to the new capacity.\n capacity(newCapacity);\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n\n\n重用缓冲区\n\npublic ByteBuf discardReadBytes() {\n ensureAccessible();\n if (readerIndex == 0) {\n return this;\n }\n\n if (readerIndex != writerIndex) {\n setBytes(0, this, readerIndex, writerIndex - readerIndex);//复制缓冲区\n writerIndex -= readerIndex;//重置写指针\n adjustMarkers(readerIndex);//调整mark指针\n readerIndex = 0;//重置读指针\n } else {\n adjustMarkers(readerIndex);\n writerIndex = readerIndex = 0;\n }\n return this;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n\n\n15.2.2 AbstractReferenceCountedByteBuf源码分析\n\npublic abstract class AbstractReferenceCountedByteBuf extends AbstractByteBuf {\n\n private static final AtomicIntegerFieldUpdater refCntUpdater =\n AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, \"refCnt\");//通过原子的方式对成员变量进行更新,消除锁\n\n private volatile int refCnt;//跟踪对象的引用次数,采用CAS对其自增1,默认值为1\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n15.2.2 UnPooledHeapByteBuf源码分析 非池化的基于堆内存,频繁的大块内存分配和回收会对性能造成影响,但是相比对外内存的申请和释放,成本还是低一些。 相比HeapByteBuf, UnPooledHeapByteBuf的实现更加加单,也不容易出现内存管理方面的问题,因此在满足性能的条件下,推荐使用UnPooledHeapByteBuf。\n\npublic class UnpooledHeapByteBuf extends AbstractReferenceCountedByteBuf {\nprivate final ByteBufAllocator alloc;\n byte[] array;//这里直接使用JDK的ByteBuffer也可以,之所以使用Byte数组是因为性能和便捷的位操作\n private ByteBuffer tmpNioBuf;//用于实现将netty的byteBuf转换为JDK的ByteBuffer\n }\n\n\n1\n2\n3\n4\n5\n\n\n转换JDK Buffer netty基于byte数组实现 jdk的nio buf提供wrap方法,可直接实现 转换 看下转换方法\n\npublic ByteBuffer nioBuffer(int index, int length) {\n ensureAccessible();\n return ByteBuffer.wrap(array, index, length).slice();\n }\npublic ByteBuffer slice() {\n return new HeapByteBuffer(hb,//仍然使用的是原buffer的全局数组,只是改变了position和limit的位置,所以新buf和原buf内容是相互影响的\n -1,\n 0,\n this.remaining(),\n this.remaining(),\n this.position() + offset);\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\nslice方法的作用:copy position到limit之间的内容, 15.2.3 pooledByteBuf内存池原理分析 PoolArena是netty的内存池显现类。 为了集中管理内存,提供内存申请是释放的效率,很多框架会申请一大块内存,提供相应的接口分配和释放内存,这样就不再频繁的使用系统调用来使用内存,可以提高性能。预先申请的那块内存就叫Memory Arena。PoolArena是netty对Memory Arena的实现。 Netty的PoolArena由多个chunk组成,每个chunk由多个Page组成。 PoolArena源码:\n\nabstract class PoolArena implements PoolArenaMetric {\n static final boolean HAS_UNSAFE = PlatformDependent.hasUnsafe();\n\n enum SizeClass {\n Tiny,\n Small,\n Normal\n }\n\n static final int numTinySubpagePools = 512 >>> 4;\n\n final PooledByteBufAllocator parent;\n\n private final int maxOrder;\n final int pageSize;\n final int pageShifts;\n final int chunkSize;\n final int subpageOverflowMask;\n final int numSmallSubpagePools;\n final int directMemoryCacheAlignment;\n final int directMemoryCacheAlignmentMask;\n private final PoolSubpage[] tinySubpagePools;\n private final PoolSubpage[] smallSubpagePools;\n\n private final PoolChunkList q050;\n private final PoolChunkList q025;\n private final PoolChunkList q000;\n private final PoolChunkList qInit;\n private final PoolChunkList q075;\n private final PoolChunkList q100;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n\n\nPoolChunk的实现 PoolChunk负责多个Page的内存管理,PoolChunk将其负责的多个Page构建成一棵二叉树。 假设一个chunk由16个page组成,则其组织形式: Page大小是4字节,chunk大小是64字节。 每个节点都记录自己在整个Memory Arena中的偏移地址,一旦被分配,则该节点及其子节点在接下来的内存分配过程中会被忽略。 举例来说,我们申请16个字节空间,则第三层的某个节点会被标记为已分配,则再次分配内存的时候会从其他三个节点中分配。 分配内存时对树采用深度优先算法,但是从哪棵子树开始深度遍历是随机的。 PoolSubPage的实现 申请内存小于一个page,则内存分配在page中完成,每个page会被分为大小相等的多个块。 被分的单位块大小等于第一次申请的内存大小,例如一个Page8字节,第一次申请2字节,则该page被切分成4块,每块2字节。而且这个page以后只能用于分配2字节的内存申请,如果再来一个4字节的内存申请,只能在另一个Page中申请。 Page使用标识位来表示内存块是否可用。维护一个long数组,每个位表示一个块的使用情况。 例如page为128字节,第一次申请内存为1字节,则该page被分为128块,则long数组中有2个元素,(每个long64位,两个long可以表示128位)。0、1表示该块是否可用。 15.2.4 PooledDirectByteBuf内存池原理分析 创建字节缓冲区 由于采用内存池实现,所以创建PooledDirectByteBuf对象不能new一个实例,而是从内存池获取。然后设置引用计数器。\n\nstatic PooledDirectByteBuf newInstance(int maxCapacity) {\n PooledDirectByteBuf buf = RECYCLER.get();\n buf.reuse(maxCapacity);\n return buf;\n }\nfinal void reuse(int maxCapacity) {\n maxCapacity(maxCapacity);\n setRefCnt(1);\n setIndex0(0, 0);\n discardMarks();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n复制字节缓冲区 会从内存池中获取一个新的buffer而不是new一个。\n\n\n# 15.3 ByteBuffer相关辅助类\n\n15.3.1 ByteBufHolder 对消息体进行包装和抽象,不同的子类有不同的实现。 实现ByteBufHolder的子类可以自己实现一些实用的方法。 Netty也有一些子类继承自ByteBufHolder。 15.3.2 ByteBufAllocator 字节缓冲区分配器,其实现类有两种:基于池的和普通的。 其API: 15.3.3 CompositeByteBuf 允许将多个ByteBuf组装到一起。 使用场景:如某协议包含消息头和消息体,当对消息进行编码的时候需要进行整合。 这种场景有两种实现方式:\n\n * 将一个buf复制到另一个buf。或者创建一个新的buf将两个buf都放到新的buf。\n * 通过List或其他集合容器,将两个buf都放入容器统一维护和处理。\n\n看下源码:\n\npublic class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements Iterable {\n\n private static final ByteBuffer EMPTY_NIO_BUFFER = Unpooled.EMPTY_BUFFER.nioBuffer();\n private static final Iterator EMPTY_ITERATOR = Collections.emptyList().iterator();\n\n private final ByteBufAllocator alloc;\n private final boolean direct;\n private final ComponentList components;//维护buf的容器\n private final int maxNumComponents;\n\n private boolean freed;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n再看下ComponentList:\n\nprivate static final class ComponentList extends ArrayList {\n\n ComponentList(int initialCapacity) {\n super(initialCapacity);\n }\n\n // Expose this methods so we not need to create a new subList just to remove a range of elements.\n @Override\n public void removeRange(int fromIndex, int toIndex) {\n super.removeRange(fromIndex, toIndex);\n }\n }\n\nprivate static final class Component {\n final ByteBuf buf;\n final int length;\n int offset;//在集合中的位置偏移\n int endOffset;\n\n Component(ByteBuf buf) {\n this.buf = buf;\n length = buf.readableBytes();\n }\n\n void freeIfNecessary() {\n buf.release(); // We should not get a NPE here. If so, it must be a bug.\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n\n\n15.3.4 ByteBufUtil 几个常用的工具方法:\n\n * encodeString\n * decodeString\n * hexDump", + "normalizedContent": "# 15.2 bytebuf源码分析\n\n继承关系 从内存分配的角度看,bytebuf分为两类:\n\n * 堆内存缓冲区:优点是内存的分配和回收快。缺点是进行io读写时需要一次内存复制,用户空间和内核空间的复制。\n * 直接内存缓冲区:优缺点和堆内存缓冲区整好相反。 经验表明bytebuf的最佳实践是在io通信线程的读写缓冲区使用directbytebuf,后端业务的编解码模块使用heapbytebuf,这样的组合可以达到性能最优。 从内存回收的角度看、bytebuf分两类,基于对象池的bytebuf和普通bytebuf。使用内存池后的netty在高并发和高负载环境下内存和gc更加平稳。 15.2.1 abstractbytebuf源码分析 实现bytebuf的一些公共属性和功能。 主要成员变量\n\n static final resourceleakdetector leakdetector =\n resourceleakdetectorfactory.instance().newresourceleakdetector(bytebuf.class);//用于对象是否泄漏,定义为static,意味着所有bytebuf共享\n\n int readerindex;//读索引\n int writerindex;//写索引\n private int markedreaderindex;//读mark\n private int markedwriterindex;//写mark\n private int maxcapacity;//最大容量\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\nbyte数组不在这里,因为abstractbytebuf无法确定使用直接内存还是堆内存。 readbytes(bytebuf dst, int dstindex, int length)\n\npublic bytebuf readbytes(bytebuf dst, int dstindex, int length) {\n checkreadablebytes(length);//校验可读性\n getbytes(readerindex, dst, dstindex, length);//读取。从readerindex开始读取length个字节到目标数组中\n readerindex += length;//移动读指针\n return this;\n }\n\n\n1\n2\n3\n4\n5\n6\n\n\n再看一下checkreadablebytes():\n\n/**\n * throws an {@link indexoutofboundsexception} if the current\n * {@linkplain #readablebytes() readable bytes} of this buffer is less\n * than the specified value.\n */\n protected final void checkreadablebytes(int minimumreadablebytes) {\n if (minimumreadablebytes < 0) {\n throw new illegalargumentexception(\"minimumreadablebytes: \" + minimumreadablebytes + \" (expected: >= 0)\");\n }\n checkreadablebytes0(minimumreadablebytes);\n }\n \nprivate void checkreadablebytes0(int minimumreadablebytes) {\n ensureaccessible();\n if (readerindex > writerindex - minimumreadablebytes) {\n throw new indexoutofboundsexception(string.format(\n \"readerindex(%d) + length(%d) exceeds writerindex(%d): %s\",\n readerindex, minimumreadablebytes, writerindex, this));\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\nwritebytes(byte[] src, int srcindex, int length)\n\npublic bytebuf writebytes(byte[] src, int srcindex, int length) {\n ensurewritable(length);//可写校验和扩容\n setbytes(writerindex, src, srcindex, length);//从writerindex开始写length长度\n writerindex += length;//移动写指针\n return this;\n }\n\n\n1\n2\n3\n4\n5\n6\n\n\nensurewritable(int minwritablebytes)\n\npublic bytebuf ensurewritable(int minwritablebytes) {\n if (minwritablebytes < 0) {\n throw new illegalargumentexception(string.format(\n \"minwritablebytes: %d (expected: >= 0)\", minwritablebytes));\n }\n ensurewritable0(minwritablebytes);\n return this;\n }\n \nfinal void ensurewritable0(int minwritablebytes) {\n ensureaccessible();//检查这个buf是否还有引用(如果已经没有引用那就没必要在写了)\n if (minwritablebytes <= writablebytes()) {//写入的字节小于可写字节,校验通过\n return;\n }\n\n if (minwritablebytes > maxcapacity - writerindex) {//写入的字节大于最大可写入字节,抛异常\n throw new indexoutofboundsexception(string.format(\n \"writerindex(%d) + minwritablebytes(%d) exceeds maxcapacity(%d): %s\",\n writerindex, minwritablebytes, maxcapacity, this));\n }\n\n // normalize the current capacity to the power of 2.\n int newcapacity = alloc().calculatenewcapacity(writerindex + minwritablebytes, maxcapacity);\n\n // adjust to the new capacity.\n capacity(newcapacity);\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n\n\n重用缓冲区\n\npublic bytebuf discardreadbytes() {\n ensureaccessible();\n if (readerindex == 0) {\n return this;\n }\n\n if (readerindex != writerindex) {\n setbytes(0, this, readerindex, writerindex - readerindex);//复制缓冲区\n writerindex -= readerindex;//重置写指针\n adjustmarkers(readerindex);//调整mark指针\n readerindex = 0;//重置读指针\n } else {\n adjustmarkers(readerindex);\n writerindex = readerindex = 0;\n }\n return this;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n\n\n15.2.2 abstractreferencecountedbytebuf源码分析\n\npublic abstract class abstractreferencecountedbytebuf extends abstractbytebuf {\n\n private static final atomicintegerfieldupdater refcntupdater =\n atomicintegerfieldupdater.newupdater(abstractreferencecountedbytebuf.class, \"refcnt\");//通过原子的方式对成员变量进行更新,消除锁\n\n private volatile int refcnt;//跟踪对象的引用次数,采用cas对其自增1,默认值为1\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n15.2.2 unpooledheapbytebuf源码分析 非池化的基于堆内存,频繁的大块内存分配和回收会对性能造成影响,但是相比对外内存的申请和释放,成本还是低一些。 相比heapbytebuf, unpooledheapbytebuf的实现更加加单,也不容易出现内存管理方面的问题,因此在满足性能的条件下,推荐使用unpooledheapbytebuf。\n\npublic class unpooledheapbytebuf extends abstractreferencecountedbytebuf {\nprivate final bytebufallocator alloc;\n byte[] array;//这里直接使用jdk的bytebuffer也可以,之所以使用byte数组是因为性能和便捷的位操作\n private bytebuffer tmpniobuf;//用于实现将netty的bytebuf转换为jdk的bytebuffer\n }\n\n\n1\n2\n3\n4\n5\n\n\n转换jdk buffer netty基于byte数组实现 jdk的nio buf提供wrap方法,可直接实现 转换 看下转换方法\n\npublic bytebuffer niobuffer(int index, int length) {\n ensureaccessible();\n return bytebuffer.wrap(array, index, length).slice();\n }\npublic bytebuffer slice() {\n return new heapbytebuffer(hb,//仍然使用的是原buffer的全局数组,只是改变了position和limit的位置,所以新buf和原buf内容是相互影响的\n -1,\n 0,\n this.remaining(),\n this.remaining(),\n this.position() + offset);\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\nslice方法的作用:copy position到limit之间的内容, 15.2.3 pooledbytebuf内存池原理分析 poolarena是netty的内存池显现类。 为了集中管理内存,提供内存申请是释放的效率,很多框架会申请一大块内存,提供相应的接口分配和释放内存,这样就不再频繁的使用系统调用来使用内存,可以提高性能。预先申请的那块内存就叫memory arena。poolarena是netty对memory arena的实现。 netty的poolarena由多个chunk组成,每个chunk由多个page组成。 poolarena源码:\n\nabstract class poolarena implements poolarenametric {\n static final boolean has_unsafe = platformdependent.hasunsafe();\n\n enum sizeclass {\n tiny,\n small,\n normal\n }\n\n static final int numtinysubpagepools = 512 >>> 4;\n\n final pooledbytebufallocator parent;\n\n private final int maxorder;\n final int pagesize;\n final int pageshifts;\n final int chunksize;\n final int subpageoverflowmask;\n final int numsmallsubpagepools;\n final int directmemorycachealignment;\n final int directmemorycachealignmentmask;\n private final poolsubpage[] tinysubpagepools;\n private final poolsubpage[] smallsubpagepools;\n\n private final poolchunklist q050;\n private final poolchunklist q025;\n private final poolchunklist q000;\n private final poolchunklist qinit;\n private final poolchunklist q075;\n private final poolchunklist q100;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n\n\npoolchunk的实现 poolchunk负责多个page的内存管理,poolchunk将其负责的多个page构建成一棵二叉树。 假设一个chunk由16个page组成,则其组织形式: page大小是4字节,chunk大小是64字节。 每个节点都记录自己在整个memory arena中的偏移地址,一旦被分配,则该节点及其子节点在接下来的内存分配过程中会被忽略。 举例来说,我们申请16个字节空间,则第三层的某个节点会被标记为已分配,则再次分配内存的时候会从其他三个节点中分配。 分配内存时对树采用深度优先算法,但是从哪棵子树开始深度遍历是随机的。 poolsubpage的实现 申请内存小于一个page,则内存分配在page中完成,每个page会被分为大小相等的多个块。 被分的单位块大小等于第一次申请的内存大小,例如一个page8字节,第一次申请2字节,则该page被切分成4块,每块2字节。而且这个page以后只能用于分配2字节的内存申请,如果再来一个4字节的内存申请,只能在另一个page中申请。 page使用标识位来表示内存块是否可用。维护一个long数组,每个位表示一个块的使用情况。 例如page为128字节,第一次申请内存为1字节,则该page被分为128块,则long数组中有2个元素,(每个long64位,两个long可以表示128位)。0、1表示该块是否可用。 15.2.4 pooleddirectbytebuf内存池原理分析 创建字节缓冲区 由于采用内存池实现,所以创建pooleddirectbytebuf对象不能new一个实例,而是从内存池获取。然后设置引用计数器。\n\nstatic pooleddirectbytebuf newinstance(int maxcapacity) {\n pooleddirectbytebuf buf = recycler.get();\n buf.reuse(maxcapacity);\n return buf;\n }\nfinal void reuse(int maxcapacity) {\n maxcapacity(maxcapacity);\n setrefcnt(1);\n setindex0(0, 0);\n discardmarks();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n复制字节缓冲区 会从内存池中获取一个新的buffer而不是new一个。\n\n\n# 15.3 bytebuffer相关辅助类\n\n15.3.1 bytebufholder 对消息体进行包装和抽象,不同的子类有不同的实现。 实现bytebufholder的子类可以自己实现一些实用的方法。 netty也有一些子类继承自bytebufholder。 15.3.2 bytebufallocator 字节缓冲区分配器,其实现类有两种:基于池的和普通的。 其api: 15.3.3 compositebytebuf 允许将多个bytebuf组装到一起。 使用场景:如某协议包含消息头和消息体,当对消息进行编码的时候需要进行整合。 这种场景有两种实现方式:\n\n * 将一个buf复制到另一个buf。或者创建一个新的buf将两个buf都放到新的buf。\n * 通过list或其他集合容器,将两个buf都放入容器统一维护和处理。\n\n看下源码:\n\npublic class compositebytebuf extends abstractreferencecountedbytebuf implements iterable {\n\n private static final bytebuffer empty_nio_buffer = unpooled.empty_buffer.niobuffer();\n private static final iterator empty_iterator = collections.emptylist().iterator();\n\n private final bytebufallocator alloc;\n private final boolean direct;\n private final componentlist components;//维护buf的容器\n private final int maxnumcomponents;\n\n private boolean freed;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n再看下componentlist:\n\nprivate static final class componentlist extends arraylist {\n\n componentlist(int initialcapacity) {\n super(initialcapacity);\n }\n\n // expose this methods so we not need to create a new sublist just to remove a range of elements.\n @override\n public void removerange(int fromindex, int toindex) {\n super.removerange(fromindex, toindex);\n }\n }\n\nprivate static final class component {\n final bytebuf buf;\n final int length;\n int offset;//在集合中的位置偏移\n int endoffset;\n\n component(bytebuf buf) {\n this.buf = buf;\n length = buf.readablebytes();\n }\n\n void freeifnecessary() {\n buf.release(); // we should not get a npe here. if so, it must be a bug.\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n\n\n15.3.4 bytebufutil 几个常用的工具方法:\n\n * encodestring\n * decodestring\n * hexdump", + "charsets": { + "cjk": true + }, "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, @@ -5598,6 +6062,42 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, + { + "title": "EventLoop和EventLoopGroup", + "frontmatter": { + "title": "EventLoop和EventLoopGroup", + "date": "2022-05-21T23:37:23.000Z", + "permalink": "/pages/e7aa60/", + "categories": [ + "中间件", + "Netty" + ], + "tags": [ + null + ] + }, + "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/10.Netty/040.EventLoop%E5%92%8CEventLoopGroup.html", + "relativePath": "04.中间件/10.Netty/040.EventLoop和EventLoopGroup.md", + "key": "v-62f3fa3e", + "path": "/pages/e7aa60/", + "headers": [ + { + "level": 2, + "title": "EventLoop和EventLoopGroup", + "slug": "eventloop和eventloopgroup", + "normalizedTitle": "eventloop和eventloopgroup", + "charIndex": 2 + } + ], + "headersStr": "EventLoop和EventLoopGroup", + "content": "# EventLoop和EventLoopGroup\n\nEventLoop负责处理连接中的事件, 和EvenvLoop有关的几个概念是:Channel, EventLoopGroup。\n\n * 一个EventLoopGroup包含一个或多个EventLoop。\n * 一个EventLoop对应于一个线程,所有EventLoop处理的I/O事件都在这个线程中完成。\n * 一个Channel对应唯一个一个EventLoop。\n * 一个EventLoop可以对应多个Channel。\n\n对于基于Netty的网络服务,Client端启动需要一个EventLoopGroup, Server端启动需要两个EventLoopGroup, 因为Server端需要两种Channel, 一种是ServerChannel, 只有一个,负责接受连接,另一种是用于处理连接的一组Channel。\n\nEventLoop继承了concurrent包里的 ScheduledExecutorService,这使得它可以接受Callable或者Runnable并执行。EventLoop中的parent()方法返回包含这个EventLoop的EventLoopGroup", + "normalizedContent": "# eventloop和eventloopgroup\n\neventloop负责处理连接中的事件, 和evenvloop有关的几个概念是:channel, eventloopgroup。\n\n * 一个eventloopgroup包含一个或多个eventloop。\n * 一个eventloop对应于一个线程,所有eventloop处理的i/o事件都在这个线程中完成。\n * 一个channel对应唯一个一个eventloop。\n * 一个eventloop可以对应多个channel。\n\n对于基于netty的网络服务,client端启动需要一个eventloopgroup, server端启动需要两个eventloopgroup, 因为server端需要两种channel, 一种是serverchannel, 只有一个,负责接受连接,另一种是用于处理连接的一组channel。\n\neventloop继承了concurrent包里的 scheduledexecutorservice,这使得它可以接受callable或者runnable并执行。eventloop中的parent()方法返回包含这个eventloop的eventloopgroup", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, { "title": "Java NIO了解吗?", "frontmatter": { @@ -5871,115 +6371,36 @@ export const siteData = { }, { "level": 3, - "title": "3.5.4 DatagramChannel", - "slug": "_3-5-4-datagramchannel", - "normalizedTitle": "3.5.4 datagramchannel", - "charIndex": 12348 - }, - { - "level": 3, - "title": "3.5.5 Pipes", - "slug": "_3-5-5-pipes", - "normalizedTitle": "3.5.5 pipes", - "charIndex": 12444 - }, - { - "level": 2, - "title": "4.1 基础", - "slug": "_4-1-基础", - "normalizedTitle": "4.1 基础", - "charIndex": 13917 - }, - { - "level": 2, - "title": "4.2 示例", - "slug": "_4-2-示例", - "normalizedTitle": "4.2 示例", - "charIndex": 14101 - } - ], - "headersStr": "1.1 IO概念 1.1.1 Buffer Handler 1.1.1 scatter/gather 1.1.2 虚拟内存 1.1.3 内存页 1.1.4 文件IO 1.1.5 IO流 2.1 Buffer Basic 2.1.1 Buffer的几个属性 2.1.2 Buffer API 2.1.3 访问Buffer 2.1.4 write 2.1.5 Flipping 2.1.6 Draining 2.1.7 Compacting 2.1.9 Mark 2.1.9 Comparing 2.2 创建Buffer 2.3 Duplicating Buffers 2.4 Byte Buffers 2.4.1 Byte Ordering 2.4.2 Direct Buffers 2.4.3 View Buffers 2.4.4 Data Element Views 3.1 basic 3.1.1 Open channel 3.1.2 Using Channels 3.1.3 close Channels 3.2 Scatter/Gather 3.3 File Channels 3.3.1 Accessing Files 3.3.2 File Locking 3.4 Memory-Mapped Files 3.5 Socket Channels 3.5.1 Nonblocking Mode 3.5.2 ServerSocketChannel 3.5.4 DatagramChannel 3.5.5 Pipes 4.1 基础 4.2 示例", - "content": "@TOC\n\n\n# 第一章 IO介绍\n\n\n# 1.1 IO概念\n\n\n# 1.1.1 Buffer Handler\n\n内存分为用户空间(User space),内核空间(Kernel space)。 用户空间的数据读写需要通过内存空间传递。 如下图所示,用户需要读数据,则向cpu发送一个读请求,cpu相应这个请求控制disk controller从disk读取数据到内核buffer,然后再把内核buffer的数据推送给用户空间的buffer。 为什么要区分用户空间和内核空间呢? 每个进程有独立的内存空间好处在于多个进程的内存分配互不影响。如果多个进程共享一块内存,那么内存分配的时候就得排队。\n\n为什么用户空间不能直接从disk读数据呢?\n\n * 硬件控制器只能处理固定大小的数据,用户空间需要的可能是数据块大小异常或者不对齐的数据,内核空间起作到数拆分、重组的作用。\n * 硬件设备通常不允许直接操作虚拟内存,即用户空间\n\nDMA是什么? DMA(Direct Memory Access,直接存储器访问)。 在DMA之前通过中断CPU来传输数据。CPU响应中断,控制总线传输数据。 DMA不需要CPU参与数据传输。DMA可以和CPU交互请求控制总线传输数据。\n\n\n# 1.1.1 scatter/gather\n\n分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。 聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。\n\n比如某个协议的消息固定消息头128字节,消息体1024字节,消息尾128字节。我们想要分别处理消息头,消息体,消息尾。 使用scatter示例代码:\n\nByteBuffer header = ByteBuffer.allocate(128);\nByteBuffer body = ByteBuffer.allocate(1024);\nByteBuffer tail = ByteBuffer.allocate(128);\n\nByteBuffer[] bufferArray = { header, body,tail };\nchannel.read(bufferArray);\n\n\n1\n2\n3\n4\n5\n6\n\n\n注意buffer首先被插入到数组,然后再将数组作为channel.read() 的输入参数。read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。 所以。\n\nscatter不适合处理动态消息,相反,gather适合处理动态消息。 buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与Scattering Reads相反,Gathering Writes能较好的处理动态消息。\n\n\n# 1.1.2 虚拟内存\n\n虚拟内存是指使用虚拟内存地址代替物理内存地址。\n\n * 会有多个虚拟内存指向同一个物理内存\n * 虚拟内存可能会大于物理内存 上面说disk controller不可以直接把数据读到用户空间。虚拟内存可以通过虚拟地址映射内核空间的方式做到这一点。 如下图,内核空间的buffer对用户空间也是可见的。这就是netty零拷贝的原理,这减少了buffer在用户空间和内核空间的拷贝,非常有意义。\n\n\n# 1.1.3 内存页\n\n操作系统将内存按固定字节分页。 内存读取的基本单位是页。\n\nMMU(Memory Management Unit):内存管理单元,保存虚拟内存和物理内存的映射关系,处在CPU和内存之间,起到将虚拟内存转换为物理内存的作用。\n\n\n# 1.1.4 文件IO\n\n文件系统:文件IO发生在文件系统。文件保存在磁盘,磁盘扇区类似内存分页的概念。内存文件读取通过文件系统间接操作磁盘文件。 文件零拷贝如下图。避免了文件系统内存页和用户内存页之间的拷贝。\n\n\n# 1.1.5 IO流\n\n上面说的IO都是面向块的IO。还有面向流的IO。 大多数操作系统支持将流置为非阻塞模式,额外设置一个进程用于检查流上是否有输入,因此流本身是不被阻塞的。 网络IO一般都是流IO。 多路复用:用一个进程管理多个非阻塞IO流的状态。\n\n\n# 第二章 Buffer\n\n标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。\n\n\n# 2.1 Buffer Basic\n\n\n# 2.1.1 Buffer的几个属性\n\n * capacity\n * limit\n * position\n * mark\n\n\n# 2.1.2 Buffer API\n\npackage java.nio;\npublic abstract class Buffer {\npublic final int capacity()\npublic final int position()\npublic final Buffer position (int newPosition) public final int limit()\n }\npublic final Buffer limit (int newLimit)\npublic final Buffer mark()\npublic final Buffer reset()\npublic final Buffer clear()\npublic final Buffer flip()\npublic final Buffer rewind()\npublic final int remaining()\npublic final boolean hasRemaining()\npublic abstract boolean isReadOnly();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n\n\n\n# 2.1.3 访问Buffer\n\npublic abstract class ByteBuffer\n extends Buffer implements Comparable\n {\n// This is a partial API listing\npublic abstract byte get();\npublic abstract byte get (int index);\npublic abstract ByteBuffer put (byte b);\npublic abstract ByteBuffer put (int index, byte b);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n\n# 2.1.4 write\n\nFigure 2-2的buffer经过下面这个代码会变成Figure 2-3的样子:\n\nbuffer.put((byte)'H').put((byte)'e').put((byte)'l')\n.put((byte)'l').put( (byte)'o');\n\n\n1\n2\n\n\n也可以按绝对位置写入:\n\nbuffer.put(0, (byte)'M').put((byte)'w');\n\n\n1\n\n\n\n\n\n# 2.1.5 Flipping\n\n当我们想要写出buffer的内容时,我们需要用position和limit指针指向时机内容的起始位置。类似这样:\n\nbuffer.limit(buffer.position()).position(0);\n\n\n1\n\n\n这等同于buffer.flip(); 调用flip()后2-4会变成2-5 说白了,flip()改变position和limit两个指针。 rewind()只改变position指针,等同于:\n\nbuffer.position(0);\n\n\n1\n\n\n\n# 2.1.6 Draining\n\nhasRemaining()可以判断是否到达limit指针 下面是一个实现,将buffer内容读到一个数组中\n\nfor (int i = 0; buffer.hasRemaining(), i++) {\n myByteArray [i] = buffer.get();\n}\n\n\n1\n2\n3\n\n\nremaining()会返回position到limit的数量。 将buffer内容读到一个数组中另一种实现方式:\n\nint count = buffer.remaining();\nfor (int i = 0; i < count, i++) {\n myByteArray [i] = buffer.get();\n}\n\n\n1\n2\n3\n4\n\n\n\n# 2.1.7 Compacting\n\n当你drain一部分数据想要继续fill数据的时候,你需要把还未drain的数据整体前移。 buffer.compact();可以完成这个工作。 调用buffer.compact()的效果就是2-6到2-7\n\n\n# 2.1.9 Mark\n\nmark()方法会用mark指针指向position的位置。\n\nbuffer.position(2).mark().position(4);\n\n\n1\n\n\nreset()方法会把position指针指向mark的位置。\n\nbuffer.reset();\n\n\n1\n\n\n\n\n\n# 2.1.9 Comparing\n\npublic abstract class ByteBuffer\n extends Buffer implements Comparable\n {\n // This is a partial API listing\npublic boolean equals (Object ob)\npublic int compareTo (Object ob)\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nequals()返回true需要满足三个条件:\n\n * Buffer包含相同类型的元素\n * 两个Buffer的remaining()返回值相同\n * remaining data的序列相同 下面是两个例子:\n\n\n# 2.2 创建Buffer\n\n创建新buffer的两种方式:allocation or wrapping\n\npublic abstract class CharBuffer\nextends Buffer implements CharSequence, Comparable\n{\n}\n// This is a partial API listing\npublic static CharBuffer allocate (int capacity)\npublic static CharBuffer wrap (char [] array)\npublic static CharBuffer wrap (char [] array, int offset, int length)\npublic final boolean hasArray()\npublic final char [] array()\npublic final int arrayOffset()\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n通过allocation创建的buffer是分配一块固定大小的堆内存作为buffer存储空间。 wrapping创建的buffer是创建一个buffer对象指向一个array空间。\n\n\n# 2.3 Duplicating Buffers\n\npublic abstract class CharBuffer\nextends Buffer implements CharSequence, Comparable\n {\n}\n// This is a partial API listing\npublic abstract CharBuffer duplicate();\npublic abstract CharBuffer asReadOnlyBuffer(); \npublic abstract CharBuffer slice();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\nduplicate()创建一个新的buffer,和原buffer一样。 两个buffer共享数据元素 但是他们有独立的position, limit, and mark 一个buffer更改了数据元素对另一个buffer可见 只读、直接内存这两个属性也可以通过duplicate()继承 举个例子:\n\nCharBuffer buffer = CharBuffer.allocate (8); \nbuffer.position (3).limit (6).mark().position (5); \nCharBuffer dupeBuffer = buffer.duplicate(); buffer.clear();\n\n\n1\n2\n3\n\n\nasReadOnlyBuffer()和duplicate()功能类似,只是asReadOnlyBuffer()返回一个只读的视图。 slice() 返回一个新的buffer视图,不过 new_position = original_position new_capacity = original_limit - original_position 举个例子来看slice():\n\nCharBuffer buffer = CharBuffer.allocate (8); \nbuffer.position (3).limit (5);\nCharBuffer sliceBuffer = buffer.slice();\n\n\n1\n2\n3\n\n\n\n\n\n# 2.4 Byte Buffers\n\n\n# 2.4.1 Byte Ordering\n\n原始数据类型在内存中的存储方式是多个字节的连续序列。 举个例子一个32-bit int 值为 0x037FB4C7的内存存储序列可能为2-14也可能为2-15: 这就是所谓的大端设计和小端设计。 采用大端还是小端通常由硬件的设计者决定而不是小端的设计者。 IP协议定义的字节顺序是大端。所有使用IP协议的多字节数值必须在网络字节序和本地主机字节序做转换。\n\npackage java.nio;\npublic final class ByteOrder\n{\npublic static final ByteOrder BIG_ENDIAN public static final ByteOrder LITTLE_ENDIAN\npublic static ByteOrder nativeOrder()\npublic String toString()\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n除了ByteBuffer之外,其他buffer的order()适中返回同一个值,ByteOrder.nativeOrder()。\n\n\n# 2.4.2 Direct Buffers\n\npublic abstract class ByteBuffer extends Buffer implements Comparable {\n// This is a partial API listing\npublic static ByteBuffer allocate (int capacity) ;\npublic static ByteBuffer allocateDirect (int capacity) ;\npublic abstract boolean isDirect();\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\nallocateDirect可以直接在堆外开辟一个buffer。 堆外buffer节省了用户空间和系统空间的buffer拷贝,提升效率。\n\n\n# 2.4.3 View Buffers\n\n收到数据的时候你可能要先看一下数据再决定做什么操作,这就需要用到ByteBuffer的视图API。\n\npublic abstract class ByteBuffer extends Buffer implements Comparable {\n// This is a partial API listing\npublic abstract CharBuffer asCharBuffer(); \npublic abstract ShortBuffer asShortBuffer(); \npublic abstract IntBuffer asIntBuffer();\n public abstract LongBuffer asLongBuffer();\n public abstract FloatBuffer asFloatBuffer();\n public abstract DoubleBuffer asDoubleBuffer();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n视图在直接内存buffer上操作更高效。 当本地硬件的字节顺序和buffer中的字节顺序一致时,通过低级代码就可以操作buffer中的数据,而不需要经过字节码的编码解码。\n\n\n# 2.4.4 Data Element Views\n\n当getInt() 被调用时,那么从当前位置开始的4个字节将被打包成一个int返回。 int value = buffer.getInt();将返回buffer中的1-4位,具体返回的数据值与字节序有关系。 int value = buffer.order (ByteOrder.BIG_ENDIAN).getInt(); 返回0x3BC5315E。 int value = buffer.order (ByteOrder.LITTLE_ENDIAN).getInt(); 返回0x5E31C53B。\n\n\n# 第三章 Channels\n\n\n# 3.1 basic\n\n在NIO接口中我们需要使用Channel和Buffer进行IO操作,Channel模拟了流的概念,但是又有不同。数据总是从一个Channel读到一个buffer中,或者从一个buffer中写到channel中。 channel接口的主要实现类如下:\n\n * FileChannel\n * DatagramChannel\n * SocketChannel\n * ServerSocketChannel\n\nJavaNIO Channels和流有一些相似,但是又有些不同:\n\n你可以同时读和写Channels,流Stream只支持单向的读或写(InputStream/OutputStream)\n\n * Channels可以异步的读和写,流Stream是同步的\n * Channels总是读取到buffer或者从buffer中写入\n\n下面分别介绍一下Channel最重要的一些实现类:\n\n * FileChannel : 可以读写文件中的数据\n * DatagramChannel:可以通过UDP协议读写数据\n * SocketChannel:可以通过TCP协议读写数据\n * ServerSocketChannel:允许我们像一个web服务器那样监听TCP链接请求,为每一个链接请求创建一个SocketChannel\n\n\n# 3.1.1 Open channel\n\nFileChannel只能通过getChannel()得到。 SocketChannel有工厂方法可以得到。\n\nSocketChannel sc = SocketChannel.open();\nsc.connect (new InetSocketAddress (\"somehost\", someport));\n\nServerSocketChannel ssc = ServerSocketChannel.open(); ssc.socket().bind (new InetSocketAddress (somelocalport));\nDatagramChannel dc = DatagramChannel.open();\n\nRandomAccessFile raf = new RandomAccessFile (\"somefile\", \"r\"); FileChannel fc = raf.getChannel();\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 3.1.2 Using Channels\n\npublic interface ReadableByteChannel extends Channel {\npublic int read (ByteBuffer dst) throws IOException;\n}\n\npublic interface WritableByteChannel extends Channel {\npublic int write (ByteBuffer src) throws IOException;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nChannels 可以是单向或者双向的。 如果一个class实现了上面两个接口,就是双向的。 Channels可以在阻塞模式和非阻塞模式工作。 只有socket和pipes可以工作在非阻塞模式。\n\n\n# 3.1.3 close Channels\n\n\n# 3.2 Scatter/Gather\n\nScatter:将多个buffer按照顺序合成一个 Gather:将一个字符串按照顺序拆分成多个。逐个填满buffer。\n\n\n# 3.3 File Channels\n\nFile channels 只能以阻塞模式工作。\n\n\n# 3.3.1 Accessing Files\n\npublic abstract class FileChannel extends AbstractChannel \nimplements ByteChannel, GatheringByteChannel, ScatteringByteChannel{\n// This is a partial API listing\n//获取position位置\npublic abstract long position()\n//设置position\npublic abstract void position (long newPosition)\n//从position位置开始读\npublic abstract int read (ByteBuffer dst)\n//从position位置开始读\npublic abstract int read (ByteBuffer dst, long position)\n//从position位置开始写\n public abstract int write (ByteBuffer src)\n//从position位置开始写\npublic abstract int write (ByteBuffer src, long position)\npublic abstract long size()\n//截取文件保留size大小,并移动position到size的位置\npublic abstract void truncate (long size) \n//强制刷新缓存到磁盘文件\npublic abstract void force (boolean metaData)\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n\n\n\n# 3.3.2 File Locking\n\n\n# 3.4 Memory-Mapped Files\n\n\n# 3.5 Socket Channels\n\nThe new socket channels can operate in nonblocking mode and are selectable. These two capabilities enable tremendous scalability and flexibility in large applications. ServerSocketChannel没有实现read和write接口,说明它不需要转发数据。它只负责家庭socket连接,创建SocketChannel。\n\na channel is a conduit to an I/O service and provides methods for interacting with that service\n\n每个socket channels都有一个socket与之对应,调用socket()可以获得。 但并不是所有的sockets都有channels。socket的getChannel()可能但会null;\n\n\n# 3.5.1 Nonblocking Mode\n\n调用configureBlocking (false);设置channel为非阻塞模式。 但是只有持有blockingLock() 返回的对象锁的线程才能修改阻塞模式。\n\nSocket socket = null;\nObject lockObj = serverChannel.blockingLock();\n// have a handle to the lock object, but haven't locked it yet\n// may block here until lock is acquired\nsynchronize (lockObj){\n// This thread now owns the lock; mode can't be changed boolean prevState = serverChannel.isBlocking();\nserverChannel.configureBlocking (false); socket = serverChannel.accept(); serverChannel.configureBlocking (prevState);\n}\n// lock is now released, mode is allowed to change\nif (socket != null) {\ndoSomethingWithTheSocket (socket);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n\n# 3.5.2 ServerSocketChannel\n\nThe ServerSocketChannel class is a channel-based socket listener. 它负责处理socket任务并添加channel。\n\npublic abstract class SocketChannel\n extends AbstractSelectableChannel\n implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel\n{\n//SocketChannel被创建但是并没有连接,open()只是创建一个SocketChannel,还需要调用connect()连接到一个地址\npublic static SocketChannel open() throws IOException\n//创建并连接\npublic static SocketChannel open(SocketAddress remote) throws IOException\n//支持的操作(读、写、连接)\npublic final int validOps()\n、、\npublic abstract Socket socket();\n//是否连接到远程\npublic abstract boolean isConnected();\n//Connect()方法是耗时的,因为需要数据包对话(TCP握手)。如果SocketChannel身上有并发、connect(),isConnectionPending返回true\npublic abstract boolean isConnectionPending();\n//建立连接\npublic abstract boolean connect(SocketAddress remote) throws IOException;\n//任何时候都可以安全的调用\npublic abstract boolean finishConnect() throws IOException;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\nSocket channels是线程安全的。 Socket是面向流的而不是面向数据包的,它能保证到达的顺序是发送的顺序,不能保证维护分组。发送方发送20个字节,接收方read可能直接受到7个字节,其余字节在传输中。所以,多个线程共享一个socket的一端不是好的设计。\n\n\n# 3.5.4 DatagramChannel\n\nsocketchannel为面向连接的流协议(如tcp/ip) datagramchannel为面向无连接的数据包协议(如udp/ip)\n\n\n# 3.5.5 Pipes\n\nPipe用于同一个JVM内不同线程之间的数据交换问题 不同JVM之间的数据交换问题应该使用SocketChannel Pipe类创建了一对channel对象,它们提供了一种回送机制。 Pipe实现两个进程之间的单向数据连接。 Pipe有一个SinkChannel和一个SourceChannel 数据会从SourceChannel读取、被写到SinkChannel\n\npublic abstract class Pipe {\n public static abstract class SourceChannel\n extends AbstractSelectableChannel\n implements ReadableByteChannel, ScatteringByteChannel\n {\n protected SourceChannel(SelectorProvider provider) {\n super(provider);\n }\n public final int validOps() {\n return SelectionKey.OP_READ;\n }\n }\n\n public static abstract class SinkChannel\n extends AbstractSelectableChannel\n implements WritableByteChannel, GatheringByteChannel\n {\n protected SinkChannel(SelectorProvider provider) {\n super(provider);\n }\n public final int validOps() {\n return SelectionKey.OP_WRITE;\n }\n\n }\n protected Pipe() { }\n //从管道读取数据,要访问source通道\n public abstract SourceChannel source();\n //向管道写入数据,要访问Sink通道\n public abstract SinkChannel sink();\n //管道创建\n public static Pipe open() throws IOException {\n return SelectorProvider.provider().openPipe();\n }\n}\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n\n\n\n# 第四章 Selectors\n\nSelector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。\n\n\n# 4.1 基础\n\n * Selector:一个selector可以注册多个channel,一个线程通过管理Selector实现管理多个channel。\n * SelectableChannel:提供channle selectability的通用方法。\n * SelectionKey:封装了channle和selector的注册关系,包含注册的感兴趣的事件。\n\n\n# 4.2 示例\n\n//创建Selector\nSelector selector = Selector.open();\nchannel.configureBlocking(false);\n//注册Selector\nSelectionKey key = channel.register(selector, SelectionKey.OP_READ);\nwhile(true) {\n//单线程阻塞检查Selector中就绪的事件\n int readyChannels = selector.select();\n if(readyChannels == 0) continue;\n Set selectedKeys = selector.selectedKeys();\n Iterator keyIterator = selectedKeys.iterator();\n //循环处理就绪的事件\n while(keyIterator.hasNext()) {\n SelectionKey key = keyIterator.next();\n if(key.isAcceptable()) {\n // a connection was accepted by a ServerSocketChannel.\n } else if (key.isConnectable()) {\n // a connection was established with a remote server.\n } else if (key.isReadable()) {\n // a channel is ready for reading\n } else if (key.isWritable()) {\n // a channel is ready for writing\n }\n keyIterator.remove();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n", - "normalizedContent": "@toc\n\n\n# 第一章 io介绍\n\n\n# 1.1 io概念\n\n\n# 1.1.1 buffer handler\n\n内存分为用户空间(user space),内核空间(kernel space)。 用户空间的数据读写需要通过内存空间传递。 如下图所示,用户需要读数据,则向cpu发送一个读请求,cpu相应这个请求控制disk controller从disk读取数据到内核buffer,然后再把内核buffer的数据推送给用户空间的buffer。 为什么要区分用户空间和内核空间呢? 每个进程有独立的内存空间好处在于多个进程的内存分配互不影响。如果多个进程共享一块内存,那么内存分配的时候就得排队。\n\n为什么用户空间不能直接从disk读数据呢?\n\n * 硬件控制器只能处理固定大小的数据,用户空间需要的可能是数据块大小异常或者不对齐的数据,内核空间起作到数拆分、重组的作用。\n * 硬件设备通常不允许直接操作虚拟内存,即用户空间\n\ndma是什么? dma(direct memory access,直接存储器访问)。 在dma之前通过中断cpu来传输数据。cpu响应中断,控制总线传输数据。 dma不需要cpu参与数据传输。dma可以和cpu交互请求控制总线传输数据。\n\n\n# 1.1.1 scatter/gather\n\n分散(scatter)从channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,channel将从channel中读取的数据“分散(scatter)”到多个buffer中。 聚集(gather)写入channel是指在写操作时将多个buffer的数据写入同一个channel,因此,channel 将多个buffer中的数据“聚集(gather)”后发送到channel。\n\n比如某个协议的消息固定消息头128字节,消息体1024字节,消息尾128字节。我们想要分别处理消息头,消息体,消息尾。 使用scatter示例代码:\n\nbytebuffer header = bytebuffer.allocate(128);\nbytebuffer body = bytebuffer.allocate(1024);\nbytebuffer tail = bytebuffer.allocate(128);\n\nbytebuffer[] bufferarray = { header, body,tail };\nchannel.read(bufferarray);\n\n\n1\n2\n3\n4\n5\n6\n\n\n注意buffer首先被插入到数组,然后再将数组作为channel.read() 的输入参数。read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。 所以。\n\nscatter不适合处理动态消息,相反,gather适合处理动态消息。 buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与scattering reads相反,gathering writes能较好的处理动态消息。\n\n\n# 1.1.2 虚拟内存\n\n虚拟内存是指使用虚拟内存地址代替物理内存地址。\n\n * 会有多个虚拟内存指向同一个物理内存\n * 虚拟内存可能会大于物理内存 上面说disk controller不可以直接把数据读到用户空间。虚拟内存可以通过虚拟地址映射内核空间的方式做到这一点。 如下图,内核空间的buffer对用户空间也是可见的。这就是netty零拷贝的原理,这减少了buffer在用户空间和内核空间的拷贝,非常有意义。\n\n\n# 1.1.3 内存页\n\n操作系统将内存按固定字节分页。 内存读取的基本单位是页。\n\nmmu(memory management unit):内存管理单元,保存虚拟内存和物理内存的映射关系,处在cpu和内存之间,起到将虚拟内存转换为物理内存的作用。\n\n\n# 1.1.4 文件io\n\n文件系统:文件io发生在文件系统。文件保存在磁盘,磁盘扇区类似内存分页的概念。内存文件读取通过文件系统间接操作磁盘文件。 文件零拷贝如下图。避免了文件系统内存页和用户内存页之间的拷贝。\n\n\n# 1.1.5 io流\n\n上面说的io都是面向块的io。还有面向流的io。 大多数操作系统支持将流置为非阻塞模式,额外设置一个进程用于检查流上是否有输入,因此流本身是不被阻塞的。 网络io一般都是流io。 多路复用:用一个进程管理多个非阻塞io流的状态。\n\n\n# 第二章 buffer\n\n标准的io基于字节流和字符流进行操作的,而nio是基于通道(channel)和缓冲区(buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。\n\n\n# 2.1 buffer basic\n\n\n# 2.1.1 buffer的几个属性\n\n * capacity\n * limit\n * position\n * mark\n\n\n# 2.1.2 buffer api\n\npackage java.nio;\npublic abstract class buffer {\npublic final int capacity()\npublic final int position()\npublic final buffer position (int newposition) public final int limit()\n }\npublic final buffer limit (int newlimit)\npublic final buffer mark()\npublic final buffer reset()\npublic final buffer clear()\npublic final buffer flip()\npublic final buffer rewind()\npublic final int remaining()\npublic final boolean hasremaining()\npublic abstract boolean isreadonly();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n\n\n\n# 2.1.3 访问buffer\n\npublic abstract class bytebuffer\n extends buffer implements comparable\n {\n// this is a partial api listing\npublic abstract byte get();\npublic abstract byte get (int index);\npublic abstract bytebuffer put (byte b);\npublic abstract bytebuffer put (int index, byte b);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n\n# 2.1.4 write\n\nfigure 2-2的buffer经过下面这个代码会变成figure 2-3的样子:\n\nbuffer.put((byte)'h').put((byte)'e').put((byte)'l')\n.put((byte)'l').put( (byte)'o');\n\n\n1\n2\n\n\n也可以按绝对位置写入:\n\nbuffer.put(0, (byte)'m').put((byte)'w');\n\n\n1\n\n\n\n\n\n# 2.1.5 flipping\n\n当我们想要写出buffer的内容时,我们需要用position和limit指针指向时机内容的起始位置。类似这样:\n\nbuffer.limit(buffer.position()).position(0);\n\n\n1\n\n\n这等同于buffer.flip(); 调用flip()后2-4会变成2-5 说白了,flip()改变position和limit两个指针。 rewind()只改变position指针,等同于:\n\nbuffer.position(0);\n\n\n1\n\n\n\n# 2.1.6 draining\n\nhasremaining()可以判断是否到达limit指针 下面是一个实现,将buffer内容读到一个数组中\n\nfor (int i = 0; buffer.hasremaining(), i++) {\n mybytearray [i] = buffer.get();\n}\n\n\n1\n2\n3\n\n\nremaining()会返回position到limit的数量。 将buffer内容读到一个数组中另一种实现方式:\n\nint count = buffer.remaining();\nfor (int i = 0; i < count, i++) {\n mybytearray [i] = buffer.get();\n}\n\n\n1\n2\n3\n4\n\n\n\n# 2.1.7 compacting\n\n当你drain一部分数据想要继续fill数据的时候,你需要把还未drain的数据整体前移。 buffer.compact();可以完成这个工作。 调用buffer.compact()的效果就是2-6到2-7\n\n\n# 2.1.9 mark\n\nmark()方法会用mark指针指向position的位置。\n\nbuffer.position(2).mark().position(4);\n\n\n1\n\n\nreset()方法会把position指针指向mark的位置。\n\nbuffer.reset();\n\n\n1\n\n\n\n\n\n# 2.1.9 comparing\n\npublic abstract class bytebuffer\n extends buffer implements comparable\n {\n // this is a partial api listing\npublic boolean equals (object ob)\npublic int compareto (object ob)\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nequals()返回true需要满足三个条件:\n\n * buffer包含相同类型的元素\n * 两个buffer的remaining()返回值相同\n * remaining data的序列相同 下面是两个例子:\n\n\n# 2.2 创建buffer\n\n创建新buffer的两种方式:allocation or wrapping\n\npublic abstract class charbuffer\nextends buffer implements charsequence, comparable\n{\n}\n// this is a partial api listing\npublic static charbuffer allocate (int capacity)\npublic static charbuffer wrap (char [] array)\npublic static charbuffer wrap (char [] array, int offset, int length)\npublic final boolean hasarray()\npublic final char [] array()\npublic final int arrayoffset()\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n通过allocation创建的buffer是分配一块固定大小的堆内存作为buffer存储空间。 wrapping创建的buffer是创建一个buffer对象指向一个array空间。\n\n\n# 2.3 duplicating buffers\n\npublic abstract class charbuffer\nextends buffer implements charsequence, comparable\n {\n}\n// this is a partial api listing\npublic abstract charbuffer duplicate();\npublic abstract charbuffer asreadonlybuffer(); \npublic abstract charbuffer slice();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\nduplicate()创建一个新的buffer,和原buffer一样。 两个buffer共享数据元素 但是他们有独立的position, limit, and mark 一个buffer更改了数据元素对另一个buffer可见 只读、直接内存这两个属性也可以通过duplicate()继承 举个例子:\n\ncharbuffer buffer = charbuffer.allocate (8); \nbuffer.position (3).limit (6).mark().position (5); \ncharbuffer dupebuffer = buffer.duplicate(); buffer.clear();\n\n\n1\n2\n3\n\n\nasreadonlybuffer()和duplicate()功能类似,只是asreadonlybuffer()返回一个只读的视图。 slice() 返回一个新的buffer视图,不过 new_position = original_position new_capacity = original_limit - original_position 举个例子来看slice():\n\ncharbuffer buffer = charbuffer.allocate (8); \nbuffer.position (3).limit (5);\ncharbuffer slicebuffer = buffer.slice();\n\n\n1\n2\n3\n\n\n\n\n\n# 2.4 byte buffers\n\n\n# 2.4.1 byte ordering\n\n原始数据类型在内存中的存储方式是多个字节的连续序列。 举个例子一个32-bit int 值为 0x037fb4c7的内存存储序列可能为2-14也可能为2-15: 这就是所谓的大端设计和小端设计。 采用大端还是小端通常由硬件的设计者决定而不是小端的设计者。 ip协议定义的字节顺序是大端。所有使用ip协议的多字节数值必须在网络字节序和本地主机字节序做转换。\n\npackage java.nio;\npublic final class byteorder\n{\npublic static final byteorder big_endian public static final byteorder little_endian\npublic static byteorder nativeorder()\npublic string tostring()\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n除了bytebuffer之外,其他buffer的order()适中返回同一个值,byteorder.nativeorder()。\n\n\n# 2.4.2 direct buffers\n\npublic abstract class bytebuffer extends buffer implements comparable {\n// this is a partial api listing\npublic static bytebuffer allocate (int capacity) ;\npublic static bytebuffer allocatedirect (int capacity) ;\npublic abstract boolean isdirect();\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\nallocatedirect可以直接在堆外开辟一个buffer。 堆外buffer节省了用户空间和系统空间的buffer拷贝,提升效率。\n\n\n# 2.4.3 view buffers\n\n收到数据的时候你可能要先看一下数据再决定做什么操作,这就需要用到bytebuffer的视图api。\n\npublic abstract class bytebuffer extends buffer implements comparable {\n// this is a partial api listing\npublic abstract charbuffer ascharbuffer(); \npublic abstract shortbuffer asshortbuffer(); \npublic abstract intbuffer asintbuffer();\n public abstract longbuffer aslongbuffer();\n public abstract floatbuffer asfloatbuffer();\n public abstract doublebuffer asdoublebuffer();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n视图在直接内存buffer上操作更高效。 当本地硬件的字节顺序和buffer中的字节顺序一致时,通过低级代码就可以操作buffer中的数据,而不需要经过字节码的编码解码。\n\n\n# 2.4.4 data element views\n\n当getint() 被调用时,那么从当前位置开始的4个字节将被打包成一个int返回。 int value = buffer.getint();将返回buffer中的1-4位,具体返回的数据值与字节序有关系。 int value = buffer.order (byteorder.big_endian).getint(); 返回0x3bc5315e。 int value = buffer.order (byteorder.little_endian).getint(); 返回0x5e31c53b。\n\n\n# 第三章 channels\n\n\n# 3.1 basic\n\n在nio接口中我们需要使用channel和buffer进行io操作,channel模拟了流的概念,但是又有不同。数据总是从一个channel读到一个buffer中,或者从一个buffer中写到channel中。 channel接口的主要实现类如下:\n\n * filechannel\n * datagramchannel\n * socketchannel\n * serversocketchannel\n\njavanio channels和流有一些相似,但是又有些不同:\n\n你可以同时读和写channels,流stream只支持单向的读或写(inputstream/outputstream)\n\n * channels可以异步的读和写,流stream是同步的\n * channels总是读取到buffer或者从buffer中写入\n\n下面分别介绍一下channel最重要的一些实现类:\n\n * filechannel : 可以读写文件中的数据\n * datagramchannel:可以通过udp协议读写数据\n * socketchannel:可以通过tcp协议读写数据\n * serversocketchannel:允许我们像一个web服务器那样监听tcp链接请求,为每一个链接请求创建一个socketchannel\n\n\n# 3.1.1 open channel\n\nfilechannel只能通过getchannel()得到。 socketchannel有工厂方法可以得到。\n\nsocketchannel sc = socketchannel.open();\nsc.connect (new inetsocketaddress (\"somehost\", someport));\n\nserversocketchannel ssc = serversocketchannel.open(); ssc.socket().bind (new inetsocketaddress (somelocalport));\ndatagramchannel dc = datagramchannel.open();\n\nrandomaccessfile raf = new randomaccessfile (\"somefile\", \"r\"); filechannel fc = raf.getchannel();\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 3.1.2 using channels\n\npublic interface readablebytechannel extends channel {\npublic int read (bytebuffer dst) throws ioexception;\n}\n\npublic interface writablebytechannel extends channel {\npublic int write (bytebuffer src) throws ioexception;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nchannels 可以是单向或者双向的。 如果一个class实现了上面两个接口,就是双向的。 channels可以在阻塞模式和非阻塞模式工作。 只有socket和pipes可以工作在非阻塞模式。\n\n\n# 3.1.3 close channels\n\n\n# 3.2 scatter/gather\n\nscatter:将多个buffer按照顺序合成一个 gather:将一个字符串按照顺序拆分成多个。逐个填满buffer。\n\n\n# 3.3 file channels\n\nfile channels 只能以阻塞模式工作。\n\n\n# 3.3.1 accessing files\n\npublic abstract class filechannel extends abstractchannel \nimplements bytechannel, gatheringbytechannel, scatteringbytechannel{\n// this is a partial api listing\n//获取position位置\npublic abstract long position()\n//设置position\npublic abstract void position (long newposition)\n//从position位置开始读\npublic abstract int read (bytebuffer dst)\n//从position位置开始读\npublic abstract int read (bytebuffer dst, long position)\n//从position位置开始写\n public abstract int write (bytebuffer src)\n//从position位置开始写\npublic abstract int write (bytebuffer src, long position)\npublic abstract long size()\n//截取文件保留size大小,并移动position到size的位置\npublic abstract void truncate (long size) \n//强制刷新缓存到磁盘文件\npublic abstract void force (boolean metadata)\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n\n\n\n# 3.3.2 file locking\n\n\n# 3.4 memory-mapped files\n\n\n# 3.5 socket channels\n\nthe new socket channels can operate in nonblocking mode and are selectable. these two capabilities enable tremendous scalability and flexibility in large applications. serversocketchannel没有实现read和write接口,说明它不需要转发数据。它只负责家庭socket连接,创建socketchannel。\n\na channel is a conduit to an i/o service and provides methods for interacting with that service\n\n每个socket channels都有一个socket与之对应,调用socket()可以获得。 但并不是所有的sockets都有channels。socket的getchannel()可能但会null;\n\n\n# 3.5.1 nonblocking mode\n\n调用configureblocking (false);设置channel为非阻塞模式。 但是只有持有blockinglock() 返回的对象锁的线程才能修改阻塞模式。\n\nsocket socket = null;\nobject lockobj = serverchannel.blockinglock();\n// have a handle to the lock object, but haven't locked it yet\n// may block here until lock is acquired\nsynchronize (lockobj){\n// this thread now owns the lock; mode can't be changed boolean prevstate = serverchannel.isblocking();\nserverchannel.configureblocking (false); socket = serverchannel.accept(); serverchannel.configureblocking (prevstate);\n}\n// lock is now released, mode is allowed to change\nif (socket != null) {\ndosomethingwiththesocket (socket);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n\n# 3.5.2 serversocketchannel\n\nthe serversocketchannel class is a channel-based socket listener. 它负责处理socket任务并添加channel。\n\npublic abstract class socketchannel\n extends abstractselectablechannel\n implements bytechannel, scatteringbytechannel, gatheringbytechannel, networkchannel\n{\n//socketchannel被创建但是并没有连接,open()只是创建一个socketchannel,还需要调用connect()连接到一个地址\npublic static socketchannel open() throws ioexception\n//创建并连接\npublic static socketchannel open(socketaddress remote) throws ioexception\n//支持的操作(读、写、连接)\npublic final int validops()\n、、\npublic abstract socket socket();\n//是否连接到远程\npublic abstract boolean isconnected();\n//connect()方法是耗时的,因为需要数据包对话(tcp握手)。如果socketchannel身上有并发、connect(),isconnectionpending返回true\npublic abstract boolean isconnectionpending();\n//建立连接\npublic abstract boolean connect(socketaddress remote) throws ioexception;\n//任何时候都可以安全的调用\npublic abstract boolean finishconnect() throws ioexception;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\nsocket channels是线程安全的。 socket是面向流的而不是面向数据包的,它能保证到达的顺序是发送的顺序,不能保证维护分组。发送方发送20个字节,接收方read可能直接受到7个字节,其余字节在传输中。所以,多个线程共享一个socket的一端不是好的设计。\n\n\n# 3.5.4 datagramchannel\n\nsocketchannel为面向连接的流协议(如tcp/ip) datagramchannel为面向无连接的数据包协议(如udp/ip)\n\n\n# 3.5.5 pipes\n\npipe用于同一个jvm内不同线程之间的数据交换问题 不同jvm之间的数据交换问题应该使用socketchannel pipe类创建了一对channel对象,它们提供了一种回送机制。 pipe实现两个进程之间的单向数据连接。 pipe有一个sinkchannel和一个sourcechannel 数据会从sourcechannel读取、被写到sinkchannel\n\npublic abstract class pipe {\n public static abstract class sourcechannel\n extends abstractselectablechannel\n implements readablebytechannel, scatteringbytechannel\n {\n protected sourcechannel(selectorprovider provider) {\n super(provider);\n }\n public final int validops() {\n return selectionkey.op_read;\n }\n }\n\n public static abstract class sinkchannel\n extends abstractselectablechannel\n implements writablebytechannel, gatheringbytechannel\n {\n protected sinkchannel(selectorprovider provider) {\n super(provider);\n }\n public final int validops() {\n return selectionkey.op_write;\n }\n\n }\n protected pipe() { }\n //从管道读取数据,要访问source通道\n public abstract sourcechannel source();\n //向管道写入数据,要访问sink通道\n public abstract sinkchannel sink();\n //管道创建\n public static pipe open() throws ioexception {\n return selectorprovider.provider().openpipe();\n }\n}\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n\n\n\n# 第四章 selectors\n\nselector(选择器)是java nio中能够检测一到多个nio通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,仅用单个线程来处理多个channels的好处是,只需要更少的线程来处理通道。\n\n\n# 4.1 基础\n\n * selector:一个selector可以注册多个channel,一个线程通过管理selector实现管理多个channel。\n * selectablechannel:提供channle selectability的通用方法。\n * selectionkey:封装了channle和selector的注册关系,包含注册的感兴趣的事件。\n\n\n# 4.2 示例\n\n//创建selector\nselector selector = selector.open();\nchannel.configureblocking(false);\n//注册selector\nselectionkey key = channel.register(selector, selectionkey.op_read);\nwhile(true) {\n//单线程阻塞检查selector中就绪的事件\n int readychannels = selector.select();\n if(readychannels == 0) continue;\n set selectedkeys = selector.selectedkeys();\n iterator keyiterator = selectedkeys.iterator();\n //循环处理就绪的事件\n while(keyiterator.hasnext()) {\n selectionkey key = keyiterator.next();\n if(key.isacceptable()) {\n // a connection was accepted by a serversocketchannel.\n } else if (key.isconnectable()) {\n // a connection was established with a remote server.\n } else if (key.isreadable()) {\n // a channel is ready for reading\n } else if (key.iswritable()) {\n // a channel is ready for writing\n }\n keyiterator.remove();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "EventLoop和EventLoopGroup", - "frontmatter": { - "title": "EventLoop和EventLoopGroup", - "date": "2022-05-21T23:37:23.000Z", - "permalink": "/pages/e7aa60/", - "categories": [ - "中间件", - "Netty" - ], - "tags": [ - null - ] - }, - "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/10.Netty/040.EventLoop%E5%92%8CEventLoopGroup.html", - "relativePath": "04.中间件/10.Netty/040.EventLoop和EventLoopGroup.md", - "key": "v-62f3fa3e", - "path": "/pages/e7aa60/", - "headers": [ + "title": "3.5.4 DatagramChannel", + "slug": "_3-5-4-datagramchannel", + "normalizedTitle": "3.5.4 datagramchannel", + "charIndex": 12348 + }, { - "level": 2, - "title": "EventLoop和EventLoopGroup", - "slug": "eventloop和eventloopgroup", - "normalizedTitle": "eventloop和eventloopgroup", - "charIndex": 2 - } - ], - "headersStr": "EventLoop和EventLoopGroup", - "content": "# EventLoop和EventLoopGroup\n\nEventLoop负责处理连接中的事件, 和EvenvLoop有关的几个概念是:Channel, EventLoopGroup。\n\n * 一个EventLoopGroup包含一个或多个EventLoop。\n * 一个EventLoop对应于一个线程,所有EventLoop处理的I/O事件都在这个线程中完成。\n * 一个Channel对应唯一个一个EventLoop。\n * 一个EventLoop可以对应多个Channel。\n\n对于基于Netty的网络服务,Client端启动需要一个EventLoopGroup, Server端启动需要两个EventLoopGroup, 因为Server端需要两种Channel, 一种是ServerChannel, 只有一个,负责接受连接,另一种是用于处理连接的一组Channel。\n\nEventLoop继承了concurrent包里的 ScheduledExecutorService,这使得它可以接受Callable或者Runnable并执行。EventLoop中的parent()方法返回包含这个EventLoop的EventLoopGroup", - "normalizedContent": "# eventloop和eventloopgroup\n\neventloop负责处理连接中的事件, 和evenvloop有关的几个概念是:channel, eventloopgroup。\n\n * 一个eventloopgroup包含一个或多个eventloop。\n * 一个eventloop对应于一个线程,所有eventloop处理的i/o事件都在这个线程中完成。\n * 一个channel对应唯一个一个eventloop。\n * 一个eventloop可以对应多个channel。\n\n对于基于netty的网络服务,client端启动需要一个eventloopgroup, server端启动需要两个eventloopgroup, 因为server端需要两种channel, 一种是serverchannel, 只有一个,负责接受连接,另一种是用于处理连接的一组channel。\n\neventloop继承了concurrent包里的 scheduledexecutorservice,这使得它可以接受callable或者runnable并执行。eventloop中的parent()方法返回包含这个eventloop的eventloopgroup", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "为什么Netty要自己写一个ByteBuf", - "frontmatter": { - "title": "为什么Netty要自己写一个ByteBuf", - "date": "2022-05-21T23:35:32.000Z", - "permalink": "/pages/ef17ee/", - "categories": [ - "中间件", - "Netty" - ], - "tags": [ - null - ] - }, - "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/10.Netty/020.%E4%B8%BA%E4%BB%80%E4%B9%88Netty%E8%A6%81%E8%87%AA%E5%B7%B1%E5%86%99%E4%B8%80%E4%B8%AAByteBuf.html", - "relativePath": "04.中间件/10.Netty/020.为什么Netty要自己写一个ByteBuf.md", - "key": "v-bbcf6e5a", - "path": "/pages/ef17ee/", - "headers": [ + "level": 3, + "title": "3.5.5 Pipes", + "slug": "_3-5-5-pipes", + "normalizedTitle": "3.5.5 pipes", + "charIndex": 12444 + }, { "level": 2, - "title": "15.2 ByteBuf源码分析", - "slug": "_15-2-bytebuf源码分析", - "normalizedTitle": "15.2 bytebuf源码分析", - "charIndex": 2 + "title": "4.1 基础", + "slug": "_4-1-基础", + "normalizedTitle": "4.1 基础", + "charIndex": 13917 }, { "level": 2, - "title": "15.3 ByteBuffer相关辅助类", - "slug": "_15-3-bytebuffer相关辅助类", - "normalizedTitle": "15.3 bytebuffer相关辅助类", - "charIndex": 8055 + "title": "4.2 示例", + "slug": "_4-2-示例", + "normalizedTitle": "4.2 示例", + "charIndex": 14101 } ], - "headersStr": "15.2 ByteBuf源码分析 15.3 ByteBuffer相关辅助类", - "content": "# 15.2 ByteBuf源码分析\n\n继承关系 从内存分配的角度看,byteBuf分为两类:\n\n * 堆内存缓冲区:优点是内存的分配和回收快。缺点是进行IO读写时需要一次内存复制,用户空间和内核空间的复制。\n * 直接内存缓冲区:优缺点和堆内存缓冲区整好相反。 经验表明ByteBuf的最佳实践是在IO通信线程的读写缓冲区使用DirectByteBuf,后端业务的编解码模块使用HeapByteBuf,这样的组合可以达到性能最优。 从内存回收的角度看、ByteBuf分两类,基于对象池的ByteBuf和普通ByteBuf。使用内存池后的Netty在高并发和高负载环境下内存和GC更加平稳。 15.2.1 AbstractByteBuf源码分析 实现ByteBuf的一些公共属性和功能。 主要成员变量\n\n static final ResourceLeakDetector leakDetector =\n ResourceLeakDetectorFactory.instance().newResourceLeakDetector(ByteBuf.class);//用于对象是否泄漏,定义为static,意味着所有byteBuf共享\n\n int readerIndex;//读索引\n int writerIndex;//写索引\n private int markedReaderIndex;//读mark\n private int markedWriterIndex;//写mark\n private int maxCapacity;//最大容量\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\nByte数组不在这里,因为AbstractByteBuf无法确定使用直接内存还是堆内存。 readBytes(ByteBuf dst, int dstIndex, int length)\n\npublic ByteBuf readBytes(ByteBuf dst, int dstIndex, int length) {\n checkReadableBytes(length);//校验可读性\n getBytes(readerIndex, dst, dstIndex, length);//读取。从readerIndex开始读取length个字节到目标数组中\n readerIndex += length;//移动读指针\n return this;\n }\n\n\n1\n2\n3\n4\n5\n6\n\n\n再看一下checkReadableBytes():\n\n/**\n * Throws an {@link IndexOutOfBoundsException} if the current\n * {@linkplain #readableBytes() readable bytes} of this buffer is less\n * than the specified value.\n */\n protected final void checkReadableBytes(int minimumReadableBytes) {\n if (minimumReadableBytes < 0) {\n throw new IllegalArgumentException(\"minimumReadableBytes: \" + minimumReadableBytes + \" (expected: >= 0)\");\n }\n checkReadableBytes0(minimumReadableBytes);\n }\n \nprivate void checkReadableBytes0(int minimumReadableBytes) {\n ensureAccessible();\n if (readerIndex > writerIndex - minimumReadableBytes) {\n throw new IndexOutOfBoundsException(String.format(\n \"readerIndex(%d) + length(%d) exceeds writerIndex(%d): %s\",\n readerIndex, minimumReadableBytes, writerIndex, this));\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\nwriteBytes(byte[] src, int srcIndex, int length)\n\npublic ByteBuf writeBytes(byte[] src, int srcIndex, int length) {\n ensureWritable(length);//可写校验和扩容\n setBytes(writerIndex, src, srcIndex, length);//从writerIndex开始写length长度\n writerIndex += length;//移动写指针\n return this;\n }\n\n\n1\n2\n3\n4\n5\n6\n\n\nensureWritable(int minWritableBytes)\n\npublic ByteBuf ensureWritable(int minWritableBytes) {\n if (minWritableBytes < 0) {\n throw new IllegalArgumentException(String.format(\n \"minWritableBytes: %d (expected: >= 0)\", minWritableBytes));\n }\n ensureWritable0(minWritableBytes);\n return this;\n }\n \nfinal void ensureWritable0(int minWritableBytes) {\n ensureAccessible();//检查这个buf是否还有引用(如果已经没有引用那就没必要在写了)\n if (minWritableBytes <= writableBytes()) {//写入的字节小于可写字节,校验通过\n return;\n }\n\n if (minWritableBytes > maxCapacity - writerIndex) {//写入的字节大于最大可写入字节,抛异常\n throw new IndexOutOfBoundsException(String.format(\n \"writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s\",\n writerIndex, minWritableBytes, maxCapacity, this));\n }\n\n // Normalize the current capacity to the power of 2.\n int newCapacity = alloc().calculateNewCapacity(writerIndex + minWritableBytes, maxCapacity);\n\n // Adjust to the new capacity.\n capacity(newCapacity);\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n\n\n重用缓冲区\n\npublic ByteBuf discardReadBytes() {\n ensureAccessible();\n if (readerIndex == 0) {\n return this;\n }\n\n if (readerIndex != writerIndex) {\n setBytes(0, this, readerIndex, writerIndex - readerIndex);//复制缓冲区\n writerIndex -= readerIndex;//重置写指针\n adjustMarkers(readerIndex);//调整mark指针\n readerIndex = 0;//重置读指针\n } else {\n adjustMarkers(readerIndex);\n writerIndex = readerIndex = 0;\n }\n return this;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n\n\n15.2.2 AbstractReferenceCountedByteBuf源码分析\n\npublic abstract class AbstractReferenceCountedByteBuf extends AbstractByteBuf {\n\n private static final AtomicIntegerFieldUpdater refCntUpdater =\n AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, \"refCnt\");//通过原子的方式对成员变量进行更新,消除锁\n\n private volatile int refCnt;//跟踪对象的引用次数,采用CAS对其自增1,默认值为1\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n15.2.2 UnPooledHeapByteBuf源码分析 非池化的基于堆内存,频繁的大块内存分配和回收会对性能造成影响,但是相比对外内存的申请和释放,成本还是低一些。 相比HeapByteBuf, UnPooledHeapByteBuf的实现更加加单,也不容易出现内存管理方面的问题,因此在满足性能的条件下,推荐使用UnPooledHeapByteBuf。\n\npublic class UnpooledHeapByteBuf extends AbstractReferenceCountedByteBuf {\nprivate final ByteBufAllocator alloc;\n byte[] array;//这里直接使用JDK的ByteBuffer也可以,之所以使用Byte数组是因为性能和便捷的位操作\n private ByteBuffer tmpNioBuf;//用于实现将netty的byteBuf转换为JDK的ByteBuffer\n }\n\n\n1\n2\n3\n4\n5\n\n\n转换JDK Buffer netty基于byte数组实现 jdk的nio buf提供wrap方法,可直接实现 转换 看下转换方法\n\npublic ByteBuffer nioBuffer(int index, int length) {\n ensureAccessible();\n return ByteBuffer.wrap(array, index, length).slice();\n }\npublic ByteBuffer slice() {\n return new HeapByteBuffer(hb,//仍然使用的是原buffer的全局数组,只是改变了position和limit的位置,所以新buf和原buf内容是相互影响的\n -1,\n 0,\n this.remaining(),\n this.remaining(),\n this.position() + offset);\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\nslice方法的作用:copy position到limit之间的内容, 15.2.3 pooledByteBuf内存池原理分析 PoolArena是netty的内存池显现类。 为了集中管理内存,提供内存申请是释放的效率,很多框架会申请一大块内存,提供相应的接口分配和释放内存,这样就不再频繁的使用系统调用来使用内存,可以提高性能。预先申请的那块内存就叫Memory Arena。PoolArena是netty对Memory Arena的实现。 Netty的PoolArena由多个chunk组成,每个chunk由多个Page组成。 PoolArena源码:\n\nabstract class PoolArena implements PoolArenaMetric {\n static final boolean HAS_UNSAFE = PlatformDependent.hasUnsafe();\n\n enum SizeClass {\n Tiny,\n Small,\n Normal\n }\n\n static final int numTinySubpagePools = 512 >>> 4;\n\n final PooledByteBufAllocator parent;\n\n private final int maxOrder;\n final int pageSize;\n final int pageShifts;\n final int chunkSize;\n final int subpageOverflowMask;\n final int numSmallSubpagePools;\n final int directMemoryCacheAlignment;\n final int directMemoryCacheAlignmentMask;\n private final PoolSubpage[] tinySubpagePools;\n private final PoolSubpage[] smallSubpagePools;\n\n private final PoolChunkList q050;\n private final PoolChunkList q025;\n private final PoolChunkList q000;\n private final PoolChunkList qInit;\n private final PoolChunkList q075;\n private final PoolChunkList q100;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n\n\nPoolChunk的实现 PoolChunk负责多个Page的内存管理,PoolChunk将其负责的多个Page构建成一棵二叉树。 假设一个chunk由16个page组成,则其组织形式: Page大小是4字节,chunk大小是64字节。 每个节点都记录自己在整个Memory Arena中的偏移地址,一旦被分配,则该节点及其子节点在接下来的内存分配过程中会被忽略。 举例来说,我们申请16个字节空间,则第三层的某个节点会被标记为已分配,则再次分配内存的时候会从其他三个节点中分配。 分配内存时对树采用深度优先算法,但是从哪棵子树开始深度遍历是随机的。 PoolSubPage的实现 申请内存小于一个page,则内存分配在page中完成,每个page会被分为大小相等的多个块。 被分的单位块大小等于第一次申请的内存大小,例如一个Page8字节,第一次申请2字节,则该page被切分成4块,每块2字节。而且这个page以后只能用于分配2字节的内存申请,如果再来一个4字节的内存申请,只能在另一个Page中申请。 Page使用标识位来表示内存块是否可用。维护一个long数组,每个位表示一个块的使用情况。 例如page为128字节,第一次申请内存为1字节,则该page被分为128块,则long数组中有2个元素,(每个long64位,两个long可以表示128位)。0、1表示该块是否可用。 15.2.4 PooledDirectByteBuf内存池原理分析 创建字节缓冲区 由于采用内存池实现,所以创建PooledDirectByteBuf对象不能new一个实例,而是从内存池获取。然后设置引用计数器。\n\nstatic PooledDirectByteBuf newInstance(int maxCapacity) {\n PooledDirectByteBuf buf = RECYCLER.get();\n buf.reuse(maxCapacity);\n return buf;\n }\nfinal void reuse(int maxCapacity) {\n maxCapacity(maxCapacity);\n setRefCnt(1);\n setIndex0(0, 0);\n discardMarks();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n复制字节缓冲区 会从内存池中获取一个新的buffer而不是new一个。\n\n\n# 15.3 ByteBuffer相关辅助类\n\n15.3.1 ByteBufHolder 对消息体进行包装和抽象,不同的子类有不同的实现。 实现ByteBufHolder的子类可以自己实现一些实用的方法。 Netty也有一些子类继承自ByteBufHolder。 15.3.2 ByteBufAllocator 字节缓冲区分配器,其实现类有两种:基于池的和普通的。 其API: 15.3.3 CompositeByteBuf 允许将多个ByteBuf组装到一起。 使用场景:如某协议包含消息头和消息体,当对消息进行编码的时候需要进行整合。 这种场景有两种实现方式:\n\n * 将一个buf复制到另一个buf。或者创建一个新的buf将两个buf都放到新的buf。\n * 通过List或其他集合容器,将两个buf都放入容器统一维护和处理。\n\n看下源码:\n\npublic class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements Iterable {\n\n private static final ByteBuffer EMPTY_NIO_BUFFER = Unpooled.EMPTY_BUFFER.nioBuffer();\n private static final Iterator EMPTY_ITERATOR = Collections.emptyList().iterator();\n\n private final ByteBufAllocator alloc;\n private final boolean direct;\n private final ComponentList components;//维护buf的容器\n private final int maxNumComponents;\n\n private boolean freed;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n再看下ComponentList:\n\nprivate static final class ComponentList extends ArrayList {\n\n ComponentList(int initialCapacity) {\n super(initialCapacity);\n }\n\n // Expose this methods so we not need to create a new subList just to remove a range of elements.\n @Override\n public void removeRange(int fromIndex, int toIndex) {\n super.removeRange(fromIndex, toIndex);\n }\n }\n\nprivate static final class Component {\n final ByteBuf buf;\n final int length;\n int offset;//在集合中的位置偏移\n int endOffset;\n\n Component(ByteBuf buf) {\n this.buf = buf;\n length = buf.readableBytes();\n }\n\n void freeIfNecessary() {\n buf.release(); // We should not get a NPE here. If so, it must be a bug.\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n\n\n15.3.4 ByteBufUtil 几个常用的工具方法:\n\n * encodeString\n * decodeString\n * hexDump", - "normalizedContent": "# 15.2 bytebuf源码分析\n\n继承关系 从内存分配的角度看,bytebuf分为两类:\n\n * 堆内存缓冲区:优点是内存的分配和回收快。缺点是进行io读写时需要一次内存复制,用户空间和内核空间的复制。\n * 直接内存缓冲区:优缺点和堆内存缓冲区整好相反。 经验表明bytebuf的最佳实践是在io通信线程的读写缓冲区使用directbytebuf,后端业务的编解码模块使用heapbytebuf,这样的组合可以达到性能最优。 从内存回收的角度看、bytebuf分两类,基于对象池的bytebuf和普通bytebuf。使用内存池后的netty在高并发和高负载环境下内存和gc更加平稳。 15.2.1 abstractbytebuf源码分析 实现bytebuf的一些公共属性和功能。 主要成员变量\n\n static final resourceleakdetector leakdetector =\n resourceleakdetectorfactory.instance().newresourceleakdetector(bytebuf.class);//用于对象是否泄漏,定义为static,意味着所有bytebuf共享\n\n int readerindex;//读索引\n int writerindex;//写索引\n private int markedreaderindex;//读mark\n private int markedwriterindex;//写mark\n private int maxcapacity;//最大容量\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\nbyte数组不在这里,因为abstractbytebuf无法确定使用直接内存还是堆内存。 readbytes(bytebuf dst, int dstindex, int length)\n\npublic bytebuf readbytes(bytebuf dst, int dstindex, int length) {\n checkreadablebytes(length);//校验可读性\n getbytes(readerindex, dst, dstindex, length);//读取。从readerindex开始读取length个字节到目标数组中\n readerindex += length;//移动读指针\n return this;\n }\n\n\n1\n2\n3\n4\n5\n6\n\n\n再看一下checkreadablebytes():\n\n/**\n * throws an {@link indexoutofboundsexception} if the current\n * {@linkplain #readablebytes() readable bytes} of this buffer is less\n * than the specified value.\n */\n protected final void checkreadablebytes(int minimumreadablebytes) {\n if (minimumreadablebytes < 0) {\n throw new illegalargumentexception(\"minimumreadablebytes: \" + minimumreadablebytes + \" (expected: >= 0)\");\n }\n checkreadablebytes0(minimumreadablebytes);\n }\n \nprivate void checkreadablebytes0(int minimumreadablebytes) {\n ensureaccessible();\n if (readerindex > writerindex - minimumreadablebytes) {\n throw new indexoutofboundsexception(string.format(\n \"readerindex(%d) + length(%d) exceeds writerindex(%d): %s\",\n readerindex, minimumreadablebytes, writerindex, this));\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\nwritebytes(byte[] src, int srcindex, int length)\n\npublic bytebuf writebytes(byte[] src, int srcindex, int length) {\n ensurewritable(length);//可写校验和扩容\n setbytes(writerindex, src, srcindex, length);//从writerindex开始写length长度\n writerindex += length;//移动写指针\n return this;\n }\n\n\n1\n2\n3\n4\n5\n6\n\n\nensurewritable(int minwritablebytes)\n\npublic bytebuf ensurewritable(int minwritablebytes) {\n if (minwritablebytes < 0) {\n throw new illegalargumentexception(string.format(\n \"minwritablebytes: %d (expected: >= 0)\", minwritablebytes));\n }\n ensurewritable0(minwritablebytes);\n return this;\n }\n \nfinal void ensurewritable0(int minwritablebytes) {\n ensureaccessible();//检查这个buf是否还有引用(如果已经没有引用那就没必要在写了)\n if (minwritablebytes <= writablebytes()) {//写入的字节小于可写字节,校验通过\n return;\n }\n\n if (minwritablebytes > maxcapacity - writerindex) {//写入的字节大于最大可写入字节,抛异常\n throw new indexoutofboundsexception(string.format(\n \"writerindex(%d) + minwritablebytes(%d) exceeds maxcapacity(%d): %s\",\n writerindex, minwritablebytes, maxcapacity, this));\n }\n\n // normalize the current capacity to the power of 2.\n int newcapacity = alloc().calculatenewcapacity(writerindex + minwritablebytes, maxcapacity);\n\n // adjust to the new capacity.\n capacity(newcapacity);\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n\n\n重用缓冲区\n\npublic bytebuf discardreadbytes() {\n ensureaccessible();\n if (readerindex == 0) {\n return this;\n }\n\n if (readerindex != writerindex) {\n setbytes(0, this, readerindex, writerindex - readerindex);//复制缓冲区\n writerindex -= readerindex;//重置写指针\n adjustmarkers(readerindex);//调整mark指针\n readerindex = 0;//重置读指针\n } else {\n adjustmarkers(readerindex);\n writerindex = readerindex = 0;\n }\n return this;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n\n\n15.2.2 abstractreferencecountedbytebuf源码分析\n\npublic abstract class abstractreferencecountedbytebuf extends abstractbytebuf {\n\n private static final atomicintegerfieldupdater refcntupdater =\n atomicintegerfieldupdater.newupdater(abstractreferencecountedbytebuf.class, \"refcnt\");//通过原子的方式对成员变量进行更新,消除锁\n\n private volatile int refcnt;//跟踪对象的引用次数,采用cas对其自增1,默认值为1\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n15.2.2 unpooledheapbytebuf源码分析 非池化的基于堆内存,频繁的大块内存分配和回收会对性能造成影响,但是相比对外内存的申请和释放,成本还是低一些。 相比heapbytebuf, unpooledheapbytebuf的实现更加加单,也不容易出现内存管理方面的问题,因此在满足性能的条件下,推荐使用unpooledheapbytebuf。\n\npublic class unpooledheapbytebuf extends abstractreferencecountedbytebuf {\nprivate final bytebufallocator alloc;\n byte[] array;//这里直接使用jdk的bytebuffer也可以,之所以使用byte数组是因为性能和便捷的位操作\n private bytebuffer tmpniobuf;//用于实现将netty的bytebuf转换为jdk的bytebuffer\n }\n\n\n1\n2\n3\n4\n5\n\n\n转换jdk buffer netty基于byte数组实现 jdk的nio buf提供wrap方法,可直接实现 转换 看下转换方法\n\npublic bytebuffer niobuffer(int index, int length) {\n ensureaccessible();\n return bytebuffer.wrap(array, index, length).slice();\n }\npublic bytebuffer slice() {\n return new heapbytebuffer(hb,//仍然使用的是原buffer的全局数组,只是改变了position和limit的位置,所以新buf和原buf内容是相互影响的\n -1,\n 0,\n this.remaining(),\n this.remaining(),\n this.position() + offset);\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\nslice方法的作用:copy position到limit之间的内容, 15.2.3 pooledbytebuf内存池原理分析 poolarena是netty的内存池显现类。 为了集中管理内存,提供内存申请是释放的效率,很多框架会申请一大块内存,提供相应的接口分配和释放内存,这样就不再频繁的使用系统调用来使用内存,可以提高性能。预先申请的那块内存就叫memory arena。poolarena是netty对memory arena的实现。 netty的poolarena由多个chunk组成,每个chunk由多个page组成。 poolarena源码:\n\nabstract class poolarena implements poolarenametric {\n static final boolean has_unsafe = platformdependent.hasunsafe();\n\n enum sizeclass {\n tiny,\n small,\n normal\n }\n\n static final int numtinysubpagepools = 512 >>> 4;\n\n final pooledbytebufallocator parent;\n\n private final int maxorder;\n final int pagesize;\n final int pageshifts;\n final int chunksize;\n final int subpageoverflowmask;\n final int numsmallsubpagepools;\n final int directmemorycachealignment;\n final int directmemorycachealignmentmask;\n private final poolsubpage[] tinysubpagepools;\n private final poolsubpage[] smallsubpagepools;\n\n private final poolchunklist q050;\n private final poolchunklist q025;\n private final poolchunklist q000;\n private final poolchunklist qinit;\n private final poolchunklist q075;\n private final poolchunklist q100;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n\n\npoolchunk的实现 poolchunk负责多个page的内存管理,poolchunk将其负责的多个page构建成一棵二叉树。 假设一个chunk由16个page组成,则其组织形式: page大小是4字节,chunk大小是64字节。 每个节点都记录自己在整个memory arena中的偏移地址,一旦被分配,则该节点及其子节点在接下来的内存分配过程中会被忽略。 举例来说,我们申请16个字节空间,则第三层的某个节点会被标记为已分配,则再次分配内存的时候会从其他三个节点中分配。 分配内存时对树采用深度优先算法,但是从哪棵子树开始深度遍历是随机的。 poolsubpage的实现 申请内存小于一个page,则内存分配在page中完成,每个page会被分为大小相等的多个块。 被分的单位块大小等于第一次申请的内存大小,例如一个page8字节,第一次申请2字节,则该page被切分成4块,每块2字节。而且这个page以后只能用于分配2字节的内存申请,如果再来一个4字节的内存申请,只能在另一个page中申请。 page使用标识位来表示内存块是否可用。维护一个long数组,每个位表示一个块的使用情况。 例如page为128字节,第一次申请内存为1字节,则该page被分为128块,则long数组中有2个元素,(每个long64位,两个long可以表示128位)。0、1表示该块是否可用。 15.2.4 pooleddirectbytebuf内存池原理分析 创建字节缓冲区 由于采用内存池实现,所以创建pooleddirectbytebuf对象不能new一个实例,而是从内存池获取。然后设置引用计数器。\n\nstatic pooleddirectbytebuf newinstance(int maxcapacity) {\n pooleddirectbytebuf buf = recycler.get();\n buf.reuse(maxcapacity);\n return buf;\n }\nfinal void reuse(int maxcapacity) {\n maxcapacity(maxcapacity);\n setrefcnt(1);\n setindex0(0, 0);\n discardmarks();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n复制字节缓冲区 会从内存池中获取一个新的buffer而不是new一个。\n\n\n# 15.3 bytebuffer相关辅助类\n\n15.3.1 bytebufholder 对消息体进行包装和抽象,不同的子类有不同的实现。 实现bytebufholder的子类可以自己实现一些实用的方法。 netty也有一些子类继承自bytebufholder。 15.3.2 bytebufallocator 字节缓冲区分配器,其实现类有两种:基于池的和普通的。 其api: 15.3.3 compositebytebuf 允许将多个bytebuf组装到一起。 使用场景:如某协议包含消息头和消息体,当对消息进行编码的时候需要进行整合。 这种场景有两种实现方式:\n\n * 将一个buf复制到另一个buf。或者创建一个新的buf将两个buf都放到新的buf。\n * 通过list或其他集合容器,将两个buf都放入容器统一维护和处理。\n\n看下源码:\n\npublic class compositebytebuf extends abstractreferencecountedbytebuf implements iterable {\n\n private static final bytebuffer empty_nio_buffer = unpooled.empty_buffer.niobuffer();\n private static final iterator empty_iterator = collections.emptylist().iterator();\n\n private final bytebufallocator alloc;\n private final boolean direct;\n private final componentlist components;//维护buf的容器\n private final int maxnumcomponents;\n\n private boolean freed;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n再看下componentlist:\n\nprivate static final class componentlist extends arraylist {\n\n componentlist(int initialcapacity) {\n super(initialcapacity);\n }\n\n // expose this methods so we not need to create a new sublist just to remove a range of elements.\n @override\n public void removerange(int fromindex, int toindex) {\n super.removerange(fromindex, toindex);\n }\n }\n\nprivate static final class component {\n final bytebuf buf;\n final int length;\n int offset;//在集合中的位置偏移\n int endoffset;\n\n component(bytebuf buf) {\n this.buf = buf;\n length = buf.readablebytes();\n }\n\n void freeifnecessary() {\n buf.release(); // we should not get a npe here. if so, it must be a bug.\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n\n\n15.3.4 bytebufutil 几个常用的工具方法:\n\n * encodestring\n * decodestring\n * hexdump", + "headersStr": "1.1 IO概念 1.1.1 Buffer Handler 1.1.1 scatter/gather 1.1.2 虚拟内存 1.1.3 内存页 1.1.4 文件IO 1.1.5 IO流 2.1 Buffer Basic 2.1.1 Buffer的几个属性 2.1.2 Buffer API 2.1.3 访问Buffer 2.1.4 write 2.1.5 Flipping 2.1.6 Draining 2.1.7 Compacting 2.1.9 Mark 2.1.9 Comparing 2.2 创建Buffer 2.3 Duplicating Buffers 2.4 Byte Buffers 2.4.1 Byte Ordering 2.4.2 Direct Buffers 2.4.3 View Buffers 2.4.4 Data Element Views 3.1 basic 3.1.1 Open channel 3.1.2 Using Channels 3.1.3 close Channels 3.2 Scatter/Gather 3.3 File Channels 3.3.1 Accessing Files 3.3.2 File Locking 3.4 Memory-Mapped Files 3.5 Socket Channels 3.5.1 Nonblocking Mode 3.5.2 ServerSocketChannel 3.5.4 DatagramChannel 3.5.5 Pipes 4.1 基础 4.2 示例", + "content": "@TOC\n\n\n# 第一章 IO介绍\n\n\n# 1.1 IO概念\n\n\n# 1.1.1 Buffer Handler\n\n内存分为用户空间(User space),内核空间(Kernel space)。 用户空间的数据读写需要通过内存空间传递。 如下图所示,用户需要读数据,则向cpu发送一个读请求,cpu相应这个请求控制disk controller从disk读取数据到内核buffer,然后再把内核buffer的数据推送给用户空间的buffer。 为什么要区分用户空间和内核空间呢? 每个进程有独立的内存空间好处在于多个进程的内存分配互不影响。如果多个进程共享一块内存,那么内存分配的时候就得排队。\n\n为什么用户空间不能直接从disk读数据呢?\n\n * 硬件控制器只能处理固定大小的数据,用户空间需要的可能是数据块大小异常或者不对齐的数据,内核空间起作到数拆分、重组的作用。\n * 硬件设备通常不允许直接操作虚拟内存,即用户空间\n\nDMA是什么? DMA(Direct Memory Access,直接存储器访问)。 在DMA之前通过中断CPU来传输数据。CPU响应中断,控制总线传输数据。 DMA不需要CPU参与数据传输。DMA可以和CPU交互请求控制总线传输数据。\n\n\n# 1.1.1 scatter/gather\n\n分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。 聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。\n\n比如某个协议的消息固定消息头128字节,消息体1024字节,消息尾128字节。我们想要分别处理消息头,消息体,消息尾。 使用scatter示例代码:\n\nByteBuffer header = ByteBuffer.allocate(128);\nByteBuffer body = ByteBuffer.allocate(1024);\nByteBuffer tail = ByteBuffer.allocate(128);\n\nByteBuffer[] bufferArray = { header, body,tail };\nchannel.read(bufferArray);\n\n\n1\n2\n3\n4\n5\n6\n\n\n注意buffer首先被插入到数组,然后再将数组作为channel.read() 的输入参数。read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。 所以。\n\nscatter不适合处理动态消息,相反,gather适合处理动态消息。 buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与Scattering Reads相反,Gathering Writes能较好的处理动态消息。\n\n\n# 1.1.2 虚拟内存\n\n虚拟内存是指使用虚拟内存地址代替物理内存地址。\n\n * 会有多个虚拟内存指向同一个物理内存\n * 虚拟内存可能会大于物理内存 上面说disk controller不可以直接把数据读到用户空间。虚拟内存可以通过虚拟地址映射内核空间的方式做到这一点。 如下图,内核空间的buffer对用户空间也是可见的。这就是netty零拷贝的原理,这减少了buffer在用户空间和内核空间的拷贝,非常有意义。\n\n\n# 1.1.3 内存页\n\n操作系统将内存按固定字节分页。 内存读取的基本单位是页。\n\nMMU(Memory Management Unit):内存管理单元,保存虚拟内存和物理内存的映射关系,处在CPU和内存之间,起到将虚拟内存转换为物理内存的作用。\n\n\n# 1.1.4 文件IO\n\n文件系统:文件IO发生在文件系统。文件保存在磁盘,磁盘扇区类似内存分页的概念。内存文件读取通过文件系统间接操作磁盘文件。 文件零拷贝如下图。避免了文件系统内存页和用户内存页之间的拷贝。\n\n\n# 1.1.5 IO流\n\n上面说的IO都是面向块的IO。还有面向流的IO。 大多数操作系统支持将流置为非阻塞模式,额外设置一个进程用于检查流上是否有输入,因此流本身是不被阻塞的。 网络IO一般都是流IO。 多路复用:用一个进程管理多个非阻塞IO流的状态。\n\n\n# 第二章 Buffer\n\n标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。\n\n\n# 2.1 Buffer Basic\n\n\n# 2.1.1 Buffer的几个属性\n\n * capacity\n * limit\n * position\n * mark\n\n\n# 2.1.2 Buffer API\n\npackage java.nio;\npublic abstract class Buffer {\npublic final int capacity()\npublic final int position()\npublic final Buffer position (int newPosition) public final int limit()\n }\npublic final Buffer limit (int newLimit)\npublic final Buffer mark()\npublic final Buffer reset()\npublic final Buffer clear()\npublic final Buffer flip()\npublic final Buffer rewind()\npublic final int remaining()\npublic final boolean hasRemaining()\npublic abstract boolean isReadOnly();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n\n\n\n# 2.1.3 访问Buffer\n\npublic abstract class ByteBuffer\n extends Buffer implements Comparable\n {\n// This is a partial API listing\npublic abstract byte get();\npublic abstract byte get (int index);\npublic abstract ByteBuffer put (byte b);\npublic abstract ByteBuffer put (int index, byte b);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n\n# 2.1.4 write\n\nFigure 2-2的buffer经过下面这个代码会变成Figure 2-3的样子:\n\nbuffer.put((byte)'H').put((byte)'e').put((byte)'l')\n.put((byte)'l').put( (byte)'o');\n\n\n1\n2\n\n\n也可以按绝对位置写入:\n\nbuffer.put(0, (byte)'M').put((byte)'w');\n\n\n1\n\n\n\n\n\n# 2.1.5 Flipping\n\n当我们想要写出buffer的内容时,我们需要用position和limit指针指向时机内容的起始位置。类似这样:\n\nbuffer.limit(buffer.position()).position(0);\n\n\n1\n\n\n这等同于buffer.flip(); 调用flip()后2-4会变成2-5 说白了,flip()改变position和limit两个指针。 rewind()只改变position指针,等同于:\n\nbuffer.position(0);\n\n\n1\n\n\n\n# 2.1.6 Draining\n\nhasRemaining()可以判断是否到达limit指针 下面是一个实现,将buffer内容读到一个数组中\n\nfor (int i = 0; buffer.hasRemaining(), i++) {\n myByteArray [i] = buffer.get();\n}\n\n\n1\n2\n3\n\n\nremaining()会返回position到limit的数量。 将buffer内容读到一个数组中另一种实现方式:\n\nint count = buffer.remaining();\nfor (int i = 0; i < count, i++) {\n myByteArray [i] = buffer.get();\n}\n\n\n1\n2\n3\n4\n\n\n\n# 2.1.7 Compacting\n\n当你drain一部分数据想要继续fill数据的时候,你需要把还未drain的数据整体前移。 buffer.compact();可以完成这个工作。 调用buffer.compact()的效果就是2-6到2-7\n\n\n# 2.1.9 Mark\n\nmark()方法会用mark指针指向position的位置。\n\nbuffer.position(2).mark().position(4);\n\n\n1\n\n\nreset()方法会把position指针指向mark的位置。\n\nbuffer.reset();\n\n\n1\n\n\n\n\n\n# 2.1.9 Comparing\n\npublic abstract class ByteBuffer\n extends Buffer implements Comparable\n {\n // This is a partial API listing\npublic boolean equals (Object ob)\npublic int compareTo (Object ob)\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nequals()返回true需要满足三个条件:\n\n * Buffer包含相同类型的元素\n * 两个Buffer的remaining()返回值相同\n * remaining data的序列相同 下面是两个例子:\n\n\n# 2.2 创建Buffer\n\n创建新buffer的两种方式:allocation or wrapping\n\npublic abstract class CharBuffer\nextends Buffer implements CharSequence, Comparable\n{\n}\n// This is a partial API listing\npublic static CharBuffer allocate (int capacity)\npublic static CharBuffer wrap (char [] array)\npublic static CharBuffer wrap (char [] array, int offset, int length)\npublic final boolean hasArray()\npublic final char [] array()\npublic final int arrayOffset()\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n通过allocation创建的buffer是分配一块固定大小的堆内存作为buffer存储空间。 wrapping创建的buffer是创建一个buffer对象指向一个array空间。\n\n\n# 2.3 Duplicating Buffers\n\npublic abstract class CharBuffer\nextends Buffer implements CharSequence, Comparable\n {\n}\n// This is a partial API listing\npublic abstract CharBuffer duplicate();\npublic abstract CharBuffer asReadOnlyBuffer(); \npublic abstract CharBuffer slice();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\nduplicate()创建一个新的buffer,和原buffer一样。 两个buffer共享数据元素 但是他们有独立的position, limit, and mark 一个buffer更改了数据元素对另一个buffer可见 只读、直接内存这两个属性也可以通过duplicate()继承 举个例子:\n\nCharBuffer buffer = CharBuffer.allocate (8); \nbuffer.position (3).limit (6).mark().position (5); \nCharBuffer dupeBuffer = buffer.duplicate(); buffer.clear();\n\n\n1\n2\n3\n\n\nasReadOnlyBuffer()和duplicate()功能类似,只是asReadOnlyBuffer()返回一个只读的视图。 slice() 返回一个新的buffer视图,不过 new_position = original_position new_capacity = original_limit - original_position 举个例子来看slice():\n\nCharBuffer buffer = CharBuffer.allocate (8); \nbuffer.position (3).limit (5);\nCharBuffer sliceBuffer = buffer.slice();\n\n\n1\n2\n3\n\n\n\n\n\n# 2.4 Byte Buffers\n\n\n# 2.4.1 Byte Ordering\n\n原始数据类型在内存中的存储方式是多个字节的连续序列。 举个例子一个32-bit int 值为 0x037FB4C7的内存存储序列可能为2-14也可能为2-15: 这就是所谓的大端设计和小端设计。 采用大端还是小端通常由硬件的设计者决定而不是小端的设计者。 IP协议定义的字节顺序是大端。所有使用IP协议的多字节数值必须在网络字节序和本地主机字节序做转换。\n\npackage java.nio;\npublic final class ByteOrder\n{\npublic static final ByteOrder BIG_ENDIAN public static final ByteOrder LITTLE_ENDIAN\npublic static ByteOrder nativeOrder()\npublic String toString()\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n除了ByteBuffer之外,其他buffer的order()适中返回同一个值,ByteOrder.nativeOrder()。\n\n\n# 2.4.2 Direct Buffers\n\npublic abstract class ByteBuffer extends Buffer implements Comparable {\n// This is a partial API listing\npublic static ByteBuffer allocate (int capacity) ;\npublic static ByteBuffer allocateDirect (int capacity) ;\npublic abstract boolean isDirect();\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\nallocateDirect可以直接在堆外开辟一个buffer。 堆外buffer节省了用户空间和系统空间的buffer拷贝,提升效率。\n\n\n# 2.4.3 View Buffers\n\n收到数据的时候你可能要先看一下数据再决定做什么操作,这就需要用到ByteBuffer的视图API。\n\npublic abstract class ByteBuffer extends Buffer implements Comparable {\n// This is a partial API listing\npublic abstract CharBuffer asCharBuffer(); \npublic abstract ShortBuffer asShortBuffer(); \npublic abstract IntBuffer asIntBuffer();\n public abstract LongBuffer asLongBuffer();\n public abstract FloatBuffer asFloatBuffer();\n public abstract DoubleBuffer asDoubleBuffer();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n视图在直接内存buffer上操作更高效。 当本地硬件的字节顺序和buffer中的字节顺序一致时,通过低级代码就可以操作buffer中的数据,而不需要经过字节码的编码解码。\n\n\n# 2.4.4 Data Element Views\n\n当getInt() 被调用时,那么从当前位置开始的4个字节将被打包成一个int返回。 int value = buffer.getInt();将返回buffer中的1-4位,具体返回的数据值与字节序有关系。 int value = buffer.order (ByteOrder.BIG_ENDIAN).getInt(); 返回0x3BC5315E。 int value = buffer.order (ByteOrder.LITTLE_ENDIAN).getInt(); 返回0x5E31C53B。\n\n\n# 第三章 Channels\n\n\n# 3.1 basic\n\n在NIO接口中我们需要使用Channel和Buffer进行IO操作,Channel模拟了流的概念,但是又有不同。数据总是从一个Channel读到一个buffer中,或者从一个buffer中写到channel中。 channel接口的主要实现类如下:\n\n * FileChannel\n * DatagramChannel\n * SocketChannel\n * ServerSocketChannel\n\nJavaNIO Channels和流有一些相似,但是又有些不同:\n\n你可以同时读和写Channels,流Stream只支持单向的读或写(InputStream/OutputStream)\n\n * Channels可以异步的读和写,流Stream是同步的\n * Channels总是读取到buffer或者从buffer中写入\n\n下面分别介绍一下Channel最重要的一些实现类:\n\n * FileChannel : 可以读写文件中的数据\n * DatagramChannel:可以通过UDP协议读写数据\n * SocketChannel:可以通过TCP协议读写数据\n * ServerSocketChannel:允许我们像一个web服务器那样监听TCP链接请求,为每一个链接请求创建一个SocketChannel\n\n\n# 3.1.1 Open channel\n\nFileChannel只能通过getChannel()得到。 SocketChannel有工厂方法可以得到。\n\nSocketChannel sc = SocketChannel.open();\nsc.connect (new InetSocketAddress (\"somehost\", someport));\n\nServerSocketChannel ssc = ServerSocketChannel.open(); ssc.socket().bind (new InetSocketAddress (somelocalport));\nDatagramChannel dc = DatagramChannel.open();\n\nRandomAccessFile raf = new RandomAccessFile (\"somefile\", \"r\"); FileChannel fc = raf.getChannel();\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 3.1.2 Using Channels\n\npublic interface ReadableByteChannel extends Channel {\npublic int read (ByteBuffer dst) throws IOException;\n}\n\npublic interface WritableByteChannel extends Channel {\npublic int write (ByteBuffer src) throws IOException;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nChannels 可以是单向或者双向的。 如果一个class实现了上面两个接口,就是双向的。 Channels可以在阻塞模式和非阻塞模式工作。 只有socket和pipes可以工作在非阻塞模式。\n\n\n# 3.1.3 close Channels\n\n\n# 3.2 Scatter/Gather\n\nScatter:将多个buffer按照顺序合成一个 Gather:将一个字符串按照顺序拆分成多个。逐个填满buffer。\n\n\n# 3.3 File Channels\n\nFile channels 只能以阻塞模式工作。\n\n\n# 3.3.1 Accessing Files\n\npublic abstract class FileChannel extends AbstractChannel \nimplements ByteChannel, GatheringByteChannel, ScatteringByteChannel{\n// This is a partial API listing\n//获取position位置\npublic abstract long position()\n//设置position\npublic abstract void position (long newPosition)\n//从position位置开始读\npublic abstract int read (ByteBuffer dst)\n//从position位置开始读\npublic abstract int read (ByteBuffer dst, long position)\n//从position位置开始写\n public abstract int write (ByteBuffer src)\n//从position位置开始写\npublic abstract int write (ByteBuffer src, long position)\npublic abstract long size()\n//截取文件保留size大小,并移动position到size的位置\npublic abstract void truncate (long size) \n//强制刷新缓存到磁盘文件\npublic abstract void force (boolean metaData)\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n\n\n\n# 3.3.2 File Locking\n\n\n# 3.4 Memory-Mapped Files\n\n\n# 3.5 Socket Channels\n\nThe new socket channels can operate in nonblocking mode and are selectable. These two capabilities enable tremendous scalability and flexibility in large applications. ServerSocketChannel没有实现read和write接口,说明它不需要转发数据。它只负责家庭socket连接,创建SocketChannel。\n\na channel is a conduit to an I/O service and provides methods for interacting with that service\n\n每个socket channels都有一个socket与之对应,调用socket()可以获得。 但并不是所有的sockets都有channels。socket的getChannel()可能但会null;\n\n\n# 3.5.1 Nonblocking Mode\n\n调用configureBlocking (false);设置channel为非阻塞模式。 但是只有持有blockingLock() 返回的对象锁的线程才能修改阻塞模式。\n\nSocket socket = null;\nObject lockObj = serverChannel.blockingLock();\n// have a handle to the lock object, but haven't locked it yet\n// may block here until lock is acquired\nsynchronize (lockObj){\n// This thread now owns the lock; mode can't be changed boolean prevState = serverChannel.isBlocking();\nserverChannel.configureBlocking (false); socket = serverChannel.accept(); serverChannel.configureBlocking (prevState);\n}\n// lock is now released, mode is allowed to change\nif (socket != null) {\ndoSomethingWithTheSocket (socket);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n\n# 3.5.2 ServerSocketChannel\n\nThe ServerSocketChannel class is a channel-based socket listener. 它负责处理socket任务并添加channel。\n\npublic abstract class SocketChannel\n extends AbstractSelectableChannel\n implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel\n{\n//SocketChannel被创建但是并没有连接,open()只是创建一个SocketChannel,还需要调用connect()连接到一个地址\npublic static SocketChannel open() throws IOException\n//创建并连接\npublic static SocketChannel open(SocketAddress remote) throws IOException\n//支持的操作(读、写、连接)\npublic final int validOps()\n、、\npublic abstract Socket socket();\n//是否连接到远程\npublic abstract boolean isConnected();\n//Connect()方法是耗时的,因为需要数据包对话(TCP握手)。如果SocketChannel身上有并发、connect(),isConnectionPending返回true\npublic abstract boolean isConnectionPending();\n//建立连接\npublic abstract boolean connect(SocketAddress remote) throws IOException;\n//任何时候都可以安全的调用\npublic abstract boolean finishConnect() throws IOException;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\nSocket channels是线程安全的。 Socket是面向流的而不是面向数据包的,它能保证到达的顺序是发送的顺序,不能保证维护分组。发送方发送20个字节,接收方read可能直接受到7个字节,其余字节在传输中。所以,多个线程共享一个socket的一端不是好的设计。\n\n\n# 3.5.4 DatagramChannel\n\nsocketchannel为面向连接的流协议(如tcp/ip) datagramchannel为面向无连接的数据包协议(如udp/ip)\n\n\n# 3.5.5 Pipes\n\nPipe用于同一个JVM内不同线程之间的数据交换问题 不同JVM之间的数据交换问题应该使用SocketChannel Pipe类创建了一对channel对象,它们提供了一种回送机制。 Pipe实现两个进程之间的单向数据连接。 Pipe有一个SinkChannel和一个SourceChannel 数据会从SourceChannel读取、被写到SinkChannel\n\npublic abstract class Pipe {\n public static abstract class SourceChannel\n extends AbstractSelectableChannel\n implements ReadableByteChannel, ScatteringByteChannel\n {\n protected SourceChannel(SelectorProvider provider) {\n super(provider);\n }\n public final int validOps() {\n return SelectionKey.OP_READ;\n }\n }\n\n public static abstract class SinkChannel\n extends AbstractSelectableChannel\n implements WritableByteChannel, GatheringByteChannel\n {\n protected SinkChannel(SelectorProvider provider) {\n super(provider);\n }\n public final int validOps() {\n return SelectionKey.OP_WRITE;\n }\n\n }\n protected Pipe() { }\n //从管道读取数据,要访问source通道\n public abstract SourceChannel source();\n //向管道写入数据,要访问Sink通道\n public abstract SinkChannel sink();\n //管道创建\n public static Pipe open() throws IOException {\n return SelectorProvider.provider().openPipe();\n }\n}\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n\n\n\n# 第四章 Selectors\n\nSelector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。\n\n\n# 4.1 基础\n\n * Selector:一个selector可以注册多个channel,一个线程通过管理Selector实现管理多个channel。\n * SelectableChannel:提供channle selectability的通用方法。\n * SelectionKey:封装了channle和selector的注册关系,包含注册的感兴趣的事件。\n\n\n# 4.2 示例\n\n//创建Selector\nSelector selector = Selector.open();\nchannel.configureBlocking(false);\n//注册Selector\nSelectionKey key = channel.register(selector, SelectionKey.OP_READ);\nwhile(true) {\n//单线程阻塞检查Selector中就绪的事件\n int readyChannels = selector.select();\n if(readyChannels == 0) continue;\n Set selectedKeys = selector.selectedKeys();\n Iterator keyIterator = selectedKeys.iterator();\n //循环处理就绪的事件\n while(keyIterator.hasNext()) {\n SelectionKey key = keyIterator.next();\n if(key.isAcceptable()) {\n // a connection was accepted by a ServerSocketChannel.\n } else if (key.isConnectable()) {\n // a connection was established with a remote server.\n } else if (key.isReadable()) {\n // a channel is ready for reading\n } else if (key.isWritable()) {\n // a channel is ready for writing\n }\n keyIterator.remove();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n", + "normalizedContent": "@toc\n\n\n# 第一章 io介绍\n\n\n# 1.1 io概念\n\n\n# 1.1.1 buffer handler\n\n内存分为用户空间(user space),内核空间(kernel space)。 用户空间的数据读写需要通过内存空间传递。 如下图所示,用户需要读数据,则向cpu发送一个读请求,cpu相应这个请求控制disk controller从disk读取数据到内核buffer,然后再把内核buffer的数据推送给用户空间的buffer。 为什么要区分用户空间和内核空间呢? 每个进程有独立的内存空间好处在于多个进程的内存分配互不影响。如果多个进程共享一块内存,那么内存分配的时候就得排队。\n\n为什么用户空间不能直接从disk读数据呢?\n\n * 硬件控制器只能处理固定大小的数据,用户空间需要的可能是数据块大小异常或者不对齐的数据,内核空间起作到数拆分、重组的作用。\n * 硬件设备通常不允许直接操作虚拟内存,即用户空间\n\ndma是什么? dma(direct memory access,直接存储器访问)。 在dma之前通过中断cpu来传输数据。cpu响应中断,控制总线传输数据。 dma不需要cpu参与数据传输。dma可以和cpu交互请求控制总线传输数据。\n\n\n# 1.1.1 scatter/gather\n\n分散(scatter)从channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,channel将从channel中读取的数据“分散(scatter)”到多个buffer中。 聚集(gather)写入channel是指在写操作时将多个buffer的数据写入同一个channel,因此,channel 将多个buffer中的数据“聚集(gather)”后发送到channel。\n\n比如某个协议的消息固定消息头128字节,消息体1024字节,消息尾128字节。我们想要分别处理消息头,消息体,消息尾。 使用scatter示例代码:\n\nbytebuffer header = bytebuffer.allocate(128);\nbytebuffer body = bytebuffer.allocate(1024);\nbytebuffer tail = bytebuffer.allocate(128);\n\nbytebuffer[] bufferarray = { header, body,tail };\nchannel.read(bufferarray);\n\n\n1\n2\n3\n4\n5\n6\n\n\n注意buffer首先被插入到数组,然后再将数组作为channel.read() 的输入参数。read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。 所以。\n\nscatter不适合处理动态消息,相反,gather适合处理动态消息。 buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与scattering reads相反,gathering writes能较好的处理动态消息。\n\n\n# 1.1.2 虚拟内存\n\n虚拟内存是指使用虚拟内存地址代替物理内存地址。\n\n * 会有多个虚拟内存指向同一个物理内存\n * 虚拟内存可能会大于物理内存 上面说disk controller不可以直接把数据读到用户空间。虚拟内存可以通过虚拟地址映射内核空间的方式做到这一点。 如下图,内核空间的buffer对用户空间也是可见的。这就是netty零拷贝的原理,这减少了buffer在用户空间和内核空间的拷贝,非常有意义。\n\n\n# 1.1.3 内存页\n\n操作系统将内存按固定字节分页。 内存读取的基本单位是页。\n\nmmu(memory management unit):内存管理单元,保存虚拟内存和物理内存的映射关系,处在cpu和内存之间,起到将虚拟内存转换为物理内存的作用。\n\n\n# 1.1.4 文件io\n\n文件系统:文件io发生在文件系统。文件保存在磁盘,磁盘扇区类似内存分页的概念。内存文件读取通过文件系统间接操作磁盘文件。 文件零拷贝如下图。避免了文件系统内存页和用户内存页之间的拷贝。\n\n\n# 1.1.5 io流\n\n上面说的io都是面向块的io。还有面向流的io。 大多数操作系统支持将流置为非阻塞模式,额外设置一个进程用于检查流上是否有输入,因此流本身是不被阻塞的。 网络io一般都是流io。 多路复用:用一个进程管理多个非阻塞io流的状态。\n\n\n# 第二章 buffer\n\n标准的io基于字节流和字符流进行操作的,而nio是基于通道(channel)和缓冲区(buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。\n\n\n# 2.1 buffer basic\n\n\n# 2.1.1 buffer的几个属性\n\n * capacity\n * limit\n * position\n * mark\n\n\n# 2.1.2 buffer api\n\npackage java.nio;\npublic abstract class buffer {\npublic final int capacity()\npublic final int position()\npublic final buffer position (int newposition) public final int limit()\n }\npublic final buffer limit (int newlimit)\npublic final buffer mark()\npublic final buffer reset()\npublic final buffer clear()\npublic final buffer flip()\npublic final buffer rewind()\npublic final int remaining()\npublic final boolean hasremaining()\npublic abstract boolean isreadonly();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n\n\n\n# 2.1.3 访问buffer\n\npublic abstract class bytebuffer\n extends buffer implements comparable\n {\n// this is a partial api listing\npublic abstract byte get();\npublic abstract byte get (int index);\npublic abstract bytebuffer put (byte b);\npublic abstract bytebuffer put (int index, byte b);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n\n# 2.1.4 write\n\nfigure 2-2的buffer经过下面这个代码会变成figure 2-3的样子:\n\nbuffer.put((byte)'h').put((byte)'e').put((byte)'l')\n.put((byte)'l').put( (byte)'o');\n\n\n1\n2\n\n\n也可以按绝对位置写入:\n\nbuffer.put(0, (byte)'m').put((byte)'w');\n\n\n1\n\n\n\n\n\n# 2.1.5 flipping\n\n当我们想要写出buffer的内容时,我们需要用position和limit指针指向时机内容的起始位置。类似这样:\n\nbuffer.limit(buffer.position()).position(0);\n\n\n1\n\n\n这等同于buffer.flip(); 调用flip()后2-4会变成2-5 说白了,flip()改变position和limit两个指针。 rewind()只改变position指针,等同于:\n\nbuffer.position(0);\n\n\n1\n\n\n\n# 2.1.6 draining\n\nhasremaining()可以判断是否到达limit指针 下面是一个实现,将buffer内容读到一个数组中\n\nfor (int i = 0; buffer.hasremaining(), i++) {\n mybytearray [i] = buffer.get();\n}\n\n\n1\n2\n3\n\n\nremaining()会返回position到limit的数量。 将buffer内容读到一个数组中另一种实现方式:\n\nint count = buffer.remaining();\nfor (int i = 0; i < count, i++) {\n mybytearray [i] = buffer.get();\n}\n\n\n1\n2\n3\n4\n\n\n\n# 2.1.7 compacting\n\n当你drain一部分数据想要继续fill数据的时候,你需要把还未drain的数据整体前移。 buffer.compact();可以完成这个工作。 调用buffer.compact()的效果就是2-6到2-7\n\n\n# 2.1.9 mark\n\nmark()方法会用mark指针指向position的位置。\n\nbuffer.position(2).mark().position(4);\n\n\n1\n\n\nreset()方法会把position指针指向mark的位置。\n\nbuffer.reset();\n\n\n1\n\n\n\n\n\n# 2.1.9 comparing\n\npublic abstract class bytebuffer\n extends buffer implements comparable\n {\n // this is a partial api listing\npublic boolean equals (object ob)\npublic int compareto (object ob)\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nequals()返回true需要满足三个条件:\n\n * buffer包含相同类型的元素\n * 两个buffer的remaining()返回值相同\n * remaining data的序列相同 下面是两个例子:\n\n\n# 2.2 创建buffer\n\n创建新buffer的两种方式:allocation or wrapping\n\npublic abstract class charbuffer\nextends buffer implements charsequence, comparable\n{\n}\n// this is a partial api listing\npublic static charbuffer allocate (int capacity)\npublic static charbuffer wrap (char [] array)\npublic static charbuffer wrap (char [] array, int offset, int length)\npublic final boolean hasarray()\npublic final char [] array()\npublic final int arrayoffset()\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n通过allocation创建的buffer是分配一块固定大小的堆内存作为buffer存储空间。 wrapping创建的buffer是创建一个buffer对象指向一个array空间。\n\n\n# 2.3 duplicating buffers\n\npublic abstract class charbuffer\nextends buffer implements charsequence, comparable\n {\n}\n// this is a partial api listing\npublic abstract charbuffer duplicate();\npublic abstract charbuffer asreadonlybuffer(); \npublic abstract charbuffer slice();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\nduplicate()创建一个新的buffer,和原buffer一样。 两个buffer共享数据元素 但是他们有独立的position, limit, and mark 一个buffer更改了数据元素对另一个buffer可见 只读、直接内存这两个属性也可以通过duplicate()继承 举个例子:\n\ncharbuffer buffer = charbuffer.allocate (8); \nbuffer.position (3).limit (6).mark().position (5); \ncharbuffer dupebuffer = buffer.duplicate(); buffer.clear();\n\n\n1\n2\n3\n\n\nasreadonlybuffer()和duplicate()功能类似,只是asreadonlybuffer()返回一个只读的视图。 slice() 返回一个新的buffer视图,不过 new_position = original_position new_capacity = original_limit - original_position 举个例子来看slice():\n\ncharbuffer buffer = charbuffer.allocate (8); \nbuffer.position (3).limit (5);\ncharbuffer slicebuffer = buffer.slice();\n\n\n1\n2\n3\n\n\n\n\n\n# 2.4 byte buffers\n\n\n# 2.4.1 byte ordering\n\n原始数据类型在内存中的存储方式是多个字节的连续序列。 举个例子一个32-bit int 值为 0x037fb4c7的内存存储序列可能为2-14也可能为2-15: 这就是所谓的大端设计和小端设计。 采用大端还是小端通常由硬件的设计者决定而不是小端的设计者。 ip协议定义的字节顺序是大端。所有使用ip协议的多字节数值必须在网络字节序和本地主机字节序做转换。\n\npackage java.nio;\npublic final class byteorder\n{\npublic static final byteorder big_endian public static final byteorder little_endian\npublic static byteorder nativeorder()\npublic string tostring()\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n除了bytebuffer之外,其他buffer的order()适中返回同一个值,byteorder.nativeorder()。\n\n\n# 2.4.2 direct buffers\n\npublic abstract class bytebuffer extends buffer implements comparable {\n// this is a partial api listing\npublic static bytebuffer allocate (int capacity) ;\npublic static bytebuffer allocatedirect (int capacity) ;\npublic abstract boolean isdirect();\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\nallocatedirect可以直接在堆外开辟一个buffer。 堆外buffer节省了用户空间和系统空间的buffer拷贝,提升效率。\n\n\n# 2.4.3 view buffers\n\n收到数据的时候你可能要先看一下数据再决定做什么操作,这就需要用到bytebuffer的视图api。\n\npublic abstract class bytebuffer extends buffer implements comparable {\n// this is a partial api listing\npublic abstract charbuffer ascharbuffer(); \npublic abstract shortbuffer asshortbuffer(); \npublic abstract intbuffer asintbuffer();\n public abstract longbuffer aslongbuffer();\n public abstract floatbuffer asfloatbuffer();\n public abstract doublebuffer asdoublebuffer();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n视图在直接内存buffer上操作更高效。 当本地硬件的字节顺序和buffer中的字节顺序一致时,通过低级代码就可以操作buffer中的数据,而不需要经过字节码的编码解码。\n\n\n# 2.4.4 data element views\n\n当getint() 被调用时,那么从当前位置开始的4个字节将被打包成一个int返回。 int value = buffer.getint();将返回buffer中的1-4位,具体返回的数据值与字节序有关系。 int value = buffer.order (byteorder.big_endian).getint(); 返回0x3bc5315e。 int value = buffer.order (byteorder.little_endian).getint(); 返回0x5e31c53b。\n\n\n# 第三章 channels\n\n\n# 3.1 basic\n\n在nio接口中我们需要使用channel和buffer进行io操作,channel模拟了流的概念,但是又有不同。数据总是从一个channel读到一个buffer中,或者从一个buffer中写到channel中。 channel接口的主要实现类如下:\n\n * filechannel\n * datagramchannel\n * socketchannel\n * serversocketchannel\n\njavanio channels和流有一些相似,但是又有些不同:\n\n你可以同时读和写channels,流stream只支持单向的读或写(inputstream/outputstream)\n\n * channels可以异步的读和写,流stream是同步的\n * channels总是读取到buffer或者从buffer中写入\n\n下面分别介绍一下channel最重要的一些实现类:\n\n * filechannel : 可以读写文件中的数据\n * datagramchannel:可以通过udp协议读写数据\n * socketchannel:可以通过tcp协议读写数据\n * serversocketchannel:允许我们像一个web服务器那样监听tcp链接请求,为每一个链接请求创建一个socketchannel\n\n\n# 3.1.1 open channel\n\nfilechannel只能通过getchannel()得到。 socketchannel有工厂方法可以得到。\n\nsocketchannel sc = socketchannel.open();\nsc.connect (new inetsocketaddress (\"somehost\", someport));\n\nserversocketchannel ssc = serversocketchannel.open(); ssc.socket().bind (new inetsocketaddress (somelocalport));\ndatagramchannel dc = datagramchannel.open();\n\nrandomaccessfile raf = new randomaccessfile (\"somefile\", \"r\"); filechannel fc = raf.getchannel();\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 3.1.2 using channels\n\npublic interface readablebytechannel extends channel {\npublic int read (bytebuffer dst) throws ioexception;\n}\n\npublic interface writablebytechannel extends channel {\npublic int write (bytebuffer src) throws ioexception;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nchannels 可以是单向或者双向的。 如果一个class实现了上面两个接口,就是双向的。 channels可以在阻塞模式和非阻塞模式工作。 只有socket和pipes可以工作在非阻塞模式。\n\n\n# 3.1.3 close channels\n\n\n# 3.2 scatter/gather\n\nscatter:将多个buffer按照顺序合成一个 gather:将一个字符串按照顺序拆分成多个。逐个填满buffer。\n\n\n# 3.3 file channels\n\nfile channels 只能以阻塞模式工作。\n\n\n# 3.3.1 accessing files\n\npublic abstract class filechannel extends abstractchannel \nimplements bytechannel, gatheringbytechannel, scatteringbytechannel{\n// this is a partial api listing\n//获取position位置\npublic abstract long position()\n//设置position\npublic abstract void position (long newposition)\n//从position位置开始读\npublic abstract int read (bytebuffer dst)\n//从position位置开始读\npublic abstract int read (bytebuffer dst, long position)\n//从position位置开始写\n public abstract int write (bytebuffer src)\n//从position位置开始写\npublic abstract int write (bytebuffer src, long position)\npublic abstract long size()\n//截取文件保留size大小,并移动position到size的位置\npublic abstract void truncate (long size) \n//强制刷新缓存到磁盘文件\npublic abstract void force (boolean metadata)\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n\n\n\n# 3.3.2 file locking\n\n\n# 3.4 memory-mapped files\n\n\n# 3.5 socket channels\n\nthe new socket channels can operate in nonblocking mode and are selectable. these two capabilities enable tremendous scalability and flexibility in large applications. serversocketchannel没有实现read和write接口,说明它不需要转发数据。它只负责家庭socket连接,创建socketchannel。\n\na channel is a conduit to an i/o service and provides methods for interacting with that service\n\n每个socket channels都有一个socket与之对应,调用socket()可以获得。 但并不是所有的sockets都有channels。socket的getchannel()可能但会null;\n\n\n# 3.5.1 nonblocking mode\n\n调用configureblocking (false);设置channel为非阻塞模式。 但是只有持有blockinglock() 返回的对象锁的线程才能修改阻塞模式。\n\nsocket socket = null;\nobject lockobj = serverchannel.blockinglock();\n// have a handle to the lock object, but haven't locked it yet\n// may block here until lock is acquired\nsynchronize (lockobj){\n// this thread now owns the lock; mode can't be changed boolean prevstate = serverchannel.isblocking();\nserverchannel.configureblocking (false); socket = serverchannel.accept(); serverchannel.configureblocking (prevstate);\n}\n// lock is now released, mode is allowed to change\nif (socket != null) {\ndosomethingwiththesocket (socket);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n\n# 3.5.2 serversocketchannel\n\nthe serversocketchannel class is a channel-based socket listener. 它负责处理socket任务并添加channel。\n\npublic abstract class socketchannel\n extends abstractselectablechannel\n implements bytechannel, scatteringbytechannel, gatheringbytechannel, networkchannel\n{\n//socketchannel被创建但是并没有连接,open()只是创建一个socketchannel,还需要调用connect()连接到一个地址\npublic static socketchannel open() throws ioexception\n//创建并连接\npublic static socketchannel open(socketaddress remote) throws ioexception\n//支持的操作(读、写、连接)\npublic final int validops()\n、、\npublic abstract socket socket();\n//是否连接到远程\npublic abstract boolean isconnected();\n//connect()方法是耗时的,因为需要数据包对话(tcp握手)。如果socketchannel身上有并发、connect(),isconnectionpending返回true\npublic abstract boolean isconnectionpending();\n//建立连接\npublic abstract boolean connect(socketaddress remote) throws ioexception;\n//任何时候都可以安全的调用\npublic abstract boolean finishconnect() throws ioexception;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\nsocket channels是线程安全的。 socket是面向流的而不是面向数据包的,它能保证到达的顺序是发送的顺序,不能保证维护分组。发送方发送20个字节,接收方read可能直接受到7个字节,其余字节在传输中。所以,多个线程共享一个socket的一端不是好的设计。\n\n\n# 3.5.4 datagramchannel\n\nsocketchannel为面向连接的流协议(如tcp/ip) datagramchannel为面向无连接的数据包协议(如udp/ip)\n\n\n# 3.5.5 pipes\n\npipe用于同一个jvm内不同线程之间的数据交换问题 不同jvm之间的数据交换问题应该使用socketchannel pipe类创建了一对channel对象,它们提供了一种回送机制。 pipe实现两个进程之间的单向数据连接。 pipe有一个sinkchannel和一个sourcechannel 数据会从sourcechannel读取、被写到sinkchannel\n\npublic abstract class pipe {\n public static abstract class sourcechannel\n extends abstractselectablechannel\n implements readablebytechannel, scatteringbytechannel\n {\n protected sourcechannel(selectorprovider provider) {\n super(provider);\n }\n public final int validops() {\n return selectionkey.op_read;\n }\n }\n\n public static abstract class sinkchannel\n extends abstractselectablechannel\n implements writablebytechannel, gatheringbytechannel\n {\n protected sinkchannel(selectorprovider provider) {\n super(provider);\n }\n public final int validops() {\n return selectionkey.op_write;\n }\n\n }\n protected pipe() { }\n //从管道读取数据,要访问source通道\n public abstract sourcechannel source();\n //向管道写入数据,要访问sink通道\n public abstract sinkchannel sink();\n //管道创建\n public static pipe open() throws ioexception {\n return selectorprovider.provider().openpipe();\n }\n}\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n\n\n\n# 第四章 selectors\n\nselector(选择器)是java nio中能够检测一到多个nio通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,仅用单个线程来处理多个channels的好处是,只需要更少的线程来处理通道。\n\n\n# 4.1 基础\n\n * selector:一个selector可以注册多个channel,一个线程通过管理selector实现管理多个channel。\n * selectablechannel:提供channle selectability的通用方法。\n * selectionkey:封装了channle和selector的注册关系,包含注册的感兴趣的事件。\n\n\n# 4.2 示例\n\n//创建selector\nselector selector = selector.open();\nchannel.configureblocking(false);\n//注册selector\nselectionkey key = channel.register(selector, selectionkey.op_read);\nwhile(true) {\n//单线程阻塞检查selector中就绪的事件\n int readychannels = selector.select();\n if(readychannels == 0) continue;\n set selectedkeys = selector.selectedkeys();\n iterator keyiterator = selectedkeys.iterator();\n //循环处理就绪的事件\n while(keyiterator.hasnext()) {\n selectionkey key = keyiterator.next();\n if(key.isacceptable()) {\n // a connection was accepted by a serversocketchannel.\n } else if (key.isconnectable()) {\n // a connection was established with a remote server.\n } else if (key.isreadable()) {\n // a channel is ready for reading\n } else if (key.iswritable()) {\n // a channel is ready for writing\n }\n keyiterator.remove();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n", "charsets": { "cjk": true }, @@ -6110,6 +6531,33 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, + { + "title": "如何保证Kafka的可靠性", + "frontmatter": { + "title": "如何保证Kafka的可靠性", + "date": "2022-05-21T23:47:03.000Z", + "permalink": "/pages/76a94c/", + "categories": [ + "中间件", + "Kafka" + ], + "tags": [ + null + ] + }, + "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/40.Kafka/040.%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81Kafka%E7%9A%84%E5%8F%AF%E9%9D%A0%E6%80%A7.html", + "relativePath": "04.中间件/40.Kafka/040.如何保证Kafka的可靠性.md", + "key": "v-684c4815", + "path": "/pages/76a94c/", + "headersStr": null, + "content": "# 可靠性保证\n\nkafka在哪些方面做出保证呢?\n\n * 单生产者,kafka可以保证顺序消费\n * 只有当写入的消息被写入所有的副本时,才认为是“已提交”的\n * 只要有一个活跃的分区副本,那么,已提交的数据就不会丢失。\n * 消费者只能读取已经提交的消息\n\n如何保证topic元数据信息安全 replication.factor配置topic的复制系数\n\n所有的分区副本都没有正常同步,分区首领又挂掉了,如何做选举? 方案有两个\n\n * 在未正常同步的副中选举一个作为首领,缺点是存在丢失数据的风险。\n * 等待旧的首领恢复。缺点是可用性低。\n\n配置unclean.leader.election.enable=true表示允许不同步的副本称为首领,这也将面临丢失消息的风险。\n\n\n# producer如何做可靠性保证\n\n如何保证可靠投递消息?\n\n 1. ack=all表示broker要等待所有分区副本同步完消息才返回给producer response,是最可靠的投递。\n 2. 设置重试次数,注意要做业务幂等。\n 3. 有些小时不可重试错误,比如消息太大、认证错误等。根据业务来特殊处理这类消息。\n\n\n# consumer如何做可靠性保证\n\nconsumer请求的offset在broker上不存在时怎么处理? 配置auto.offset.reset指定了两种策略:\n\n * earliest:从分区的开始读取\n * latest:从分区的末尾读取", + "normalizedContent": "# 可靠性保证\n\nkafka在哪些方面做出保证呢?\n\n * 单生产者,kafka可以保证顺序消费\n * 只有当写入的消息被写入所有的副本时,才认为是“已提交”的\n * 只要有一个活跃的分区副本,那么,已提交的数据就不会丢失。\n * 消费者只能读取已经提交的消息\n\n如何保证topic元数据信息安全 replication.factor配置topic的复制系数\n\n所有的分区副本都没有正常同步,分区首领又挂掉了,如何做选举? 方案有两个\n\n * 在未正常同步的副中选举一个作为首领,缺点是存在丢失数据的风险。\n * 等待旧的首领恢复。缺点是可用性低。\n\n配置unclean.leader.election.enable=true表示允许不同步的副本称为首领,这也将面临丢失消息的风险。\n\n\n# producer如何做可靠性保证\n\n如何保证可靠投递消息?\n\n 1. ack=all表示broker要等待所有分区副本同步完消息才返回给producer response,是最可靠的投递。\n 2. 设置重试次数,注意要做业务幂等。\n 3. 有些小时不可重试错误,比如消息太大、认证错误等。根据业务来特殊处理这类消息。\n\n\n# consumer如何做可靠性保证\n\nconsumer请求的offset在broker上不存在时怎么处理? 配置auto.offset.reset指定了两种策略:\n\n * earliest:从分区的开始读取\n * latest:从分区的末尾读取", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, { "title": "什么是 CAP ?什么是最终一致性?什么是幂等操作?", "frontmatter": { @@ -6157,211 +6605,30 @@ export const siteData = { "charIndex": 784 }, { - "level": 3, - "title": "一致性的妥协——最终一致性和Base原则", - "slug": "一致性的妥协-最终一致性和base原则", - "normalizedTitle": "一致性的妥协——最终一致性和base原则", - "charIndex": 1070 - }, - { - "level": 3, - "title": "幂等性", - "slug": "幂等性", - "normalizedTitle": "幂等性", - "charIndex": 1420 - }, - { - "level": 3, - "title": "如何解决幂等性问题", - "slug": "如何解决幂等性问题", - "normalizedTitle": "如何解决幂等性问题", - "charIndex": 1560 - } - ], - "headersStr": "CAP理论 为什么这三者如此重要? CAP权衡 三种组合 一致性的妥协——最终一致性和Base原则 幂等性 如何解决幂等性问题", - "content": "# CAP理论\n\nCAP的结论非常简单:在分布式系统里,有3个属性非常重要,但只能同时满足其中的2个。\n\nConsistency:all nodes在任何时刻看到的data都是一样的(或说client的read操作总是返回最新写入的那个value) Availability:系统时刻都允许操作,并且操作总会快速被Coordinator响应,最终client很快就得到返回的结果 Partition-tolerance:尽管网路有时候会因为故障导致被分隔开,但是系统依然在正常工作(或者说在满足前述的条件下工作)\n\n\n# 为什么这三者如此重要?\n\nFor Availability:\n\n经统计,对于Google、Amazon这样的数据公司,系统增加500ms的延迟就会损失公司20%的收益,所以必须快速且可靠地进行 Read/Write。\n\nFor Consistency:\n\n比如在银行系统,任何client都必须查看到最新的updated data item,不然就会给交易造成致命的影响。\n\nFor Partition-tolerance:\n\n因为Internet可能因为某些原因随时断开(Router故障、海地线缆断开、DNS故障);即使在同一个data center里,故障都会随时随地地发生,比如 Rack switcher 宕机。\n\n\n# CAP权衡\n\n * 如今的云计算环境里,因为网络随时都会被隔离开来,这是无法避免的,P是必须满足的,那么CAP暗示一个system要在C和A中做出抉择。\n * A和C并不是一个硬币的两面,只能选择其中一个;A和C应该看成天平,系统可以选择向哪边倾斜,但另一边也应该一定程度的保留。\n * 对于A和C之间的选择,不应该粗粒度的整个系统级别进行选取,而应该针对系统中的不同子系统,针对性的采取不同的取舍策略。\n\n\n# 三种组合\n\n * CA: 保证可用性和一致性,放弃分区:除非不是分布式架构,或者应用在一个永不会通信故障的网络中(理想),只有个别场景符合,当前的互联网架构显然不符合使用\n * CP: 保证一致性和分区容忍性,放弃可用性:当节点间不可通信时,进行阻塞,直到通信恢复,期间无法再对外提供服务,用户体验不好,如A转账给B,只有A扣款成功并B收款成功,整个事务才算完成,显然耗费资源\n * AP: 保证可用性和分区容忍性,放弃强一致性(使用最终一致性):给出一个用户可以忍受的时间,时间内达成数据的最终一致性,比如跨行转账,并不是立刻到账,可能是明天,或者2小时内到账\n\n\n# 一致性的妥协——最终一致性和Base原则\n\n * BA(Basically Available)基本可用:系统在绝大部分时间应处于可用状态,允许出现故障损失部分可用性,但保证核心可用。\n * S(Soft State)软状态:数据状态不要求在任何时刻都保持一致,允许存在中间状态,而该状态不影响系统可用性。对于多副本的存储系统而言,就是允许副本之间的同步存在延时,并且在这个过程中系统依旧可以响应客户端请求。\n * E(Eventual Consistency)最终一致性:尽管软状态不要求分布式数据在任何时刻都保持一致,但经过一定时间后,这些数据最终能达到一致性状态。 BASE理论的核心思想是:把分布式系统的可用性放在首位,放弃CAP中对数据强一致性的追求,只要系统能保证数据最终一致。\n\n\n# 幂等性\n\n在微服务架构下,不同微服务间会有大量的基于http,rpc或者mq消息的网络通信,接口的重复调用以及消息的重复消费可能会经常发生。\n\n微服务架构应该具有幂等性,当接口被重复调用时,消息被重复消费时,对系统的产生的影响应该和接口被调用一次,消息被消费一次时一样。\n\n\n# 如何解决幂等性问题\n\n * 全局唯一ID。根据业务生成一个全局唯一ID,在调用接口时会传入该ID,接口提供方会从相应的存储系统比如Redis中去检索这个全局ID是否存在,如果存在则说明该操作已经执行过了,将拒绝本次服务请求;否则将相应该服务请求并将全局ID存入存储系统中,之后包含相同业务ID参数的请求将被拒绝。\n\n * 数据库表唯一键。这种方法适用于在业务中有唯一标识的插入场景。比如在支付场景中,一个订单只会支付一次,可以建立一张去重表,将订单ID作为唯一索引。把支付并且写入支付单据到去重表放入一个事务中,这样当出现重复支付时,数据库就会抛出唯一约束异常,操作就会回滚。这样保证了订单只会被支付一次。\n\n * 多版本并发控制适合对更新请求作幂等性控制,比如要更新商品的名字,这是就可以在更新的接口中增加一个版本号来做幂等性控制", - "normalizedContent": "# cap理论\n\ncap的结论非常简单:在分布式系统里,有3个属性非常重要,但只能同时满足其中的2个。\n\nconsistency:all nodes在任何时刻看到的data都是一样的(或说client的read操作总是返回最新写入的那个value) availability:系统时刻都允许操作,并且操作总会快速被coordinator响应,最终client很快就得到返回的结果 partition-tolerance:尽管网路有时候会因为故障导致被分隔开,但是系统依然在正常工作(或者说在满足前述的条件下工作)\n\n\n# 为什么这三者如此重要?\n\nfor availability:\n\n经统计,对于google、amazon这样的数据公司,系统增加500ms的延迟就会损失公司20%的收益,所以必须快速且可靠地进行 read/write。\n\nfor consistency:\n\n比如在银行系统,任何client都必须查看到最新的updated data item,不然就会给交易造成致命的影响。\n\nfor partition-tolerance:\n\n因为internet可能因为某些原因随时断开(router故障、海地线缆断开、dns故障);即使在同一个data center里,故障都会随时随地地发生,比如 rack switcher 宕机。\n\n\n# cap权衡\n\n * 如今的云计算环境里,因为网络随时都会被隔离开来,这是无法避免的,p是必须满足的,那么cap暗示一个system要在c和a中做出抉择。\n * a和c并不是一个硬币的两面,只能选择其中一个;a和c应该看成天平,系统可以选择向哪边倾斜,但另一边也应该一定程度的保留。\n * 对于a和c之间的选择,不应该粗粒度的整个系统级别进行选取,而应该针对系统中的不同子系统,针对性的采取不同的取舍策略。\n\n\n# 三种组合\n\n * ca: 保证可用性和一致性,放弃分区:除非不是分布式架构,或者应用在一个永不会通信故障的网络中(理想),只有个别场景符合,当前的互联网架构显然不符合使用\n * cp: 保证一致性和分区容忍性,放弃可用性:当节点间不可通信时,进行阻塞,直到通信恢复,期间无法再对外提供服务,用户体验不好,如a转账给b,只有a扣款成功并b收款成功,整个事务才算完成,显然耗费资源\n * ap: 保证可用性和分区容忍性,放弃强一致性(使用最终一致性):给出一个用户可以忍受的时间,时间内达成数据的最终一致性,比如跨行转账,并不是立刻到账,可能是明天,或者2小时内到账\n\n\n# 一致性的妥协——最终一致性和base原则\n\n * ba(basically available)基本可用:系统在绝大部分时间应处于可用状态,允许出现故障损失部分可用性,但保证核心可用。\n * s(soft state)软状态:数据状态不要求在任何时刻都保持一致,允许存在中间状态,而该状态不影响系统可用性。对于多副本的存储系统而言,就是允许副本之间的同步存在延时,并且在这个过程中系统依旧可以响应客户端请求。\n * e(eventual consistency)最终一致性:尽管软状态不要求分布式数据在任何时刻都保持一致,但经过一定时间后,这些数据最终能达到一致性状态。 base理论的核心思想是:把分布式系统的可用性放在首位,放弃cap中对数据强一致性的追求,只要系统能保证数据最终一致。\n\n\n# 幂等性\n\n在微服务架构下,不同微服务间会有大量的基于http,rpc或者mq消息的网络通信,接口的重复调用以及消息的重复消费可能会经常发生。\n\n微服务架构应该具有幂等性,当接口被重复调用时,消息被重复消费时,对系统的产生的影响应该和接口被调用一次,消息被消费一次时一样。\n\n\n# 如何解决幂等性问题\n\n * 全局唯一id。根据业务生成一个全局唯一id,在调用接口时会传入该id,接口提供方会从相应的存储系统比如redis中去检索这个全局id是否存在,如果存在则说明该操作已经执行过了,将拒绝本次服务请求;否则将相应该服务请求并将全局id存入存储系统中,之后包含相同业务id参数的请求将被拒绝。\n\n * 数据库表唯一键。这种方法适用于在业务中有唯一标识的插入场景。比如在支付场景中,一个订单只会支付一次,可以建立一张去重表,将订单id作为唯一索引。把支付并且写入支付单据到去重表放入一个事务中,这样当出现重复支付时,数据库就会抛出唯一约束异常,操作就会回滚。这样保证了订单只会被支付一次。\n\n * 多版本并发控制适合对更新请求作幂等性控制,比如要更新商品的名字,这是就可以在更新的接口中增加一个版本号来做幂等性控制", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "系统设计", - "frontmatter": { - "title": "系统设计", - "date": "2022-03-11T00:56:07.000Z", - "permalink": "/pages/01b9b2/", - "categories": [ - "系统设计" - ], - "tags": [ - null - ] - }, - "regularPath": "/08.%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/001.%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1.html", - "relativePath": "08.系统设计/001.系统设计.md", - "key": "v-a9558ec0", - "path": "/pages/01b9b2/", - "headers": [ - { - "level": 2, - "title": "电商系统中,如何实现秒杀功能?如何解决商品的超卖问题?", - "slug": "电商系统中-如何实现秒杀功能-如何解决商品的超卖问题", - "normalizedTitle": "电商系统中,如何实现秒杀功能?如何解决商品的超卖问题?", - "charIndex": 2 - }, - { - "level": 2, - "title": "如何解决缓存与数据库不一致的问题?", - "slug": "如何解决缓存与数据库不一致的问题", - "normalizedTitle": "如何解决缓存与数据库不一致的问题?", - "charIndex": 34 - }, - { - "level": 2, - "title": "简述中间件削峰和限流的使用场景", - "slug": "简述中间件削峰和限流的使用场景", - "normalizedTitle": "简述中间件削峰和限流的使用场景", - "charIndex": 56 - }, - { - "level": 2, - "title": "项目上有什么技术难点?", - "slug": "项目上有什么技术难点", - "normalizedTitle": "项目上有什么技术难点?", - "charIndex": 76 - }, - { - "level": 2, - "title": "简述 Zookeeper 基础原理以及使用场景", - "slug": "简述-zookeeper-基础原理以及使用场景", - "normalizedTitle": "简述 zookeeper 基础原理以及使用场景", - "charIndex": 92 - }, - { - "level": 2, - "title": "如何实现百度搜索功能?", - "slug": "如何实现百度搜索功能", - "normalizedTitle": "如何实现百度搜索功能?", - "charIndex": 120 - }, - { - "level": 2, - "title": "高并发情景下,核心线程池该如何设置参数?", - "slug": "高并发情景下-核心线程池该如何设置参数", - "normalizedTitle": "高并发情景下,核心线程池该如何设置参数?", - "charIndex": 136 - }, - { - "level": 2, - "title": "设计一个阻塞队列", - "slug": "设计一个阻塞队列", - "normalizedTitle": "设计一个阻塞队列", - "charIndex": 161 - }, - { - "level": 2, - "title": "容器化了解吗?它的基本原理是什么?", - "slug": "容器化了解吗-它的基本原理是什么", - "normalizedTitle": "容器化了解吗?它的基本原理是什么?", - "charIndex": 174 - }, - { - "level": 2, - "title": "如何实现唯一的分布式 ID", - "slug": "如何实现唯一的分布式-id", - "normalizedTitle": "如何实现唯一的分布式 id", - "charIndex": 196 - }, - { - "level": 2, - "title": "简述中间件削峰和限流的使用场景", - "slug": "简述中间件削峰和限流的使用场景-2", - "normalizedTitle": "简述中间件削峰和限流的使用场景", - "charIndex": 56 - }, - { - "level": 2, - "title": "简述一致性哈希算法的实现方式及原理", - "slug": "简述一致性哈希算法的实现方式及原理", - "normalizedTitle": "简述一致性哈希算法的实现方式及原理", - "charIndex": 234 - }, - { - "level": 2, - "title": "简述常见的负载均衡算法", - "slug": "简述常见的负载均衡算法", - "normalizedTitle": "简述常见的负载均衡算法", - "charIndex": 256 - }, - { - "level": 2, - "title": "简述 MapReduce 的原理", - "slug": "简述-mapreduce-的原理", - "normalizedTitle": "简述 mapreduce 的原理", - "charIndex": 272 - }, - { - "level": 2, - "title": "设计 Tiny URL 系统", - "slug": "设计-tiny-url-系统", - "normalizedTitle": "设计 tiny url 系统", - "charIndex": 293 - }, - { - "level": 2, - "title": "如何设计一个无锁队列", - "slug": "如何设计一个无锁队列", - "normalizedTitle": "如何设计一个无锁队列", - "charIndex": 312 - }, - { - "level": 2, - "title": "RestFul 与 RPC 的区别是什么?RestFul 的优点在哪里?", - "slug": "restful-与-rpc-的区别是什么-restful-的优点在哪里", - "normalizedTitle": "restful 与 rpc 的区别是什么?restful 的优点在哪里?", - "charIndex": 327 + "level": 3, + "title": "一致性的妥协——最终一致性和Base原则", + "slug": "一致性的妥协-最终一致性和base原则", + "normalizedTitle": "一致性的妥协——最终一致性和base原则", + "charIndex": 1070 }, { - "level": 2, - "title": "假如明天是活动高峰?QPS 预计会翻10倍,你要怎么做?", - "slug": "假如明天是活动高峰-qps-预计会翻10倍-你要怎么做", - "normalizedTitle": "假如明天是活动高峰?qps 预计会翻10倍,你要怎么做?", - "charIndex": 368 + "level": 3, + "title": "幂等性", + "slug": "幂等性", + "normalizedTitle": "幂等性", + "charIndex": 1420 + }, + { + "level": 3, + "title": "如何解决幂等性问题", + "slug": "如何解决幂等性问题", + "normalizedTitle": "如何解决幂等性问题", + "charIndex": 1560 } ], - "headersStr": "电商系统中,如何实现秒杀功能?如何解决商品的超卖问题? 如何解决缓存与数据库不一致的问题? 简述中间件削峰和限流的使用场景 项目上有什么技术难点? 简述 Zookeeper 基础原理以及使用场景 如何实现百度搜索功能? 高并发情景下,核心线程池该如何设置参数? 设计一个阻塞队列 容器化了解吗?它的基本原理是什么? 如何实现唯一的分布式 ID 简述中间件削峰和限流的使用场景 简述一致性哈希算法的实现方式及原理 简述常见的负载均衡算法 简述 MapReduce 的原理 设计 Tiny URL 系统 如何设计一个无锁队列 RestFul 与 RPC 的区别是什么?RestFul 的优点在哪里? 假如明天是活动高峰?QPS 预计会翻10倍,你要怎么做?", - "content": "# 电商系统中,如何实现秒杀功能?如何解决商品的超卖问题?\n\n\n# 如何解决缓存与数据库不一致的问题?\n\n\n# 简述中间件削峰和限流的使用场景\n\n\n# 项目上有什么技术难点?\n\n\n# 简述 Zookeeper 基础原理以及使用场景\n\n\n# 如何实现百度搜索功能?\n\n\n# 高并发情景下,核心线程池该如何设置参数?\n\n\n# 设计一个阻塞队列\n\n\n# 容器化了解吗?它的基本原理是什么?\n\n\n# 如何实现唯一的分布式 ID\n\n\n# 简述中间件削峰和限流的使用场景\n\n\n# 简述一致性哈希算法的实现方式及原理\n\n\n# 简述常见的负载均衡算法\n\n\n# 简述 MapReduce 的原理\n\n\n# 设计 Tiny URL 系统\n\n\n# 如何设计一个无锁队列\n\n\n# RestFul 与 RPC 的区别是什么?RestFul 的优点在哪里?\n\n\n# 假如明天是活动高峰?QPS 预计会翻10倍,你要怎么做?", - "normalizedContent": "# 电商系统中,如何实现秒杀功能?如何解决商品的超卖问题?\n\n\n# 如何解决缓存与数据库不一致的问题?\n\n\n# 简述中间件削峰和限流的使用场景\n\n\n# 项目上有什么技术难点?\n\n\n# 简述 zookeeper 基础原理以及使用场景\n\n\n# 如何实现百度搜索功能?\n\n\n# 高并发情景下,核心线程池该如何设置参数?\n\n\n# 设计一个阻塞队列\n\n\n# 容器化了解吗?它的基本原理是什么?\n\n\n# 如何实现唯一的分布式 id\n\n\n# 简述中间件削峰和限流的使用场景\n\n\n# 简述一致性哈希算法的实现方式及原理\n\n\n# 简述常见的负载均衡算法\n\n\n# 简述 mapreduce 的原理\n\n\n# 设计 tiny url 系统\n\n\n# 如何设计一个无锁队列\n\n\n# restful 与 rpc 的区别是什么?restful 的优点在哪里?\n\n\n# 假如明天是活动高峰?qps 预计会翻10倍,你要怎么做?", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "如何保证Kafka的可靠性", - "frontmatter": { - "title": "如何保证Kafka的可靠性", - "date": "2022-05-21T23:47:03.000Z", - "permalink": "/pages/76a94c/", - "categories": [ - "中间件", - "Kafka" - ], - "tags": [ - null - ] - }, - "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/40.Kafka/040.%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81Kafka%E7%9A%84%E5%8F%AF%E9%9D%A0%E6%80%A7.html", - "relativePath": "04.中间件/40.Kafka/040.如何保证Kafka的可靠性.md", - "key": "v-684c4815", - "path": "/pages/76a94c/", - "headersStr": null, - "content": "# 可靠性保证\n\nkafka在哪些方面做出保证呢?\n\n * 单生产者,kafka可以保证顺序消费\n * 只有当写入的消息被写入所有的副本时,才认为是“已提交”的\n * 只要有一个活跃的分区副本,那么,已提交的数据就不会丢失。\n * 消费者只能读取已经提交的消息\n\n如何保证topic元数据信息安全 replication.factor配置topic的复制系数\n\n所有的分区副本都没有正常同步,分区首领又挂掉了,如何做选举? 方案有两个\n\n * 在未正常同步的副中选举一个作为首领,缺点是存在丢失数据的风险。\n * 等待旧的首领恢复。缺点是可用性低。\n\n配置unclean.leader.election.enable=true表示允许不同步的副本称为首领,这也将面临丢失消息的风险。\n\n\n# producer如何做可靠性保证\n\n如何保证可靠投递消息?\n\n 1. ack=all表示broker要等待所有分区副本同步完消息才返回给producer response,是最可靠的投递。\n 2. 设置重试次数,注意要做业务幂等。\n 3. 有些小时不可重试错误,比如消息太大、认证错误等。根据业务来特殊处理这类消息。\n\n\n# consumer如何做可靠性保证\n\nconsumer请求的offset在broker上不存在时怎么处理? 配置auto.offset.reset指定了两种策略:\n\n * earliest:从分区的开始读取\n * latest:从分区的末尾读取", - "normalizedContent": "# 可靠性保证\n\nkafka在哪些方面做出保证呢?\n\n * 单生产者,kafka可以保证顺序消费\n * 只有当写入的消息被写入所有的副本时,才认为是“已提交”的\n * 只要有一个活跃的分区副本,那么,已提交的数据就不会丢失。\n * 消费者只能读取已经提交的消息\n\n如何保证topic元数据信息安全 replication.factor配置topic的复制系数\n\n所有的分区副本都没有正常同步,分区首领又挂掉了,如何做选举? 方案有两个\n\n * 在未正常同步的副中选举一个作为首领,缺点是存在丢失数据的风险。\n * 等待旧的首领恢复。缺点是可用性低。\n\n配置unclean.leader.election.enable=true表示允许不同步的副本称为首领,这也将面临丢失消息的风险。\n\n\n# producer如何做可靠性保证\n\n如何保证可靠投递消息?\n\n 1. ack=all表示broker要等待所有分区副本同步完消息才返回给producer response,是最可靠的投递。\n 2. 设置重试次数,注意要做业务幂等。\n 3. 有些小时不可重试错误,比如消息太大、认证错误等。根据业务来特殊处理这类消息。\n\n\n# consumer如何做可靠性保证\n\nconsumer请求的offset在broker上不存在时怎么处理? 配置auto.offset.reset指定了两种策略:\n\n * earliest:从分区的开始读取\n * latest:从分区的末尾读取", + "headersStr": "CAP理论 为什么这三者如此重要? CAP权衡 三种组合 一致性的妥协——最终一致性和Base原则 幂等性 如何解决幂等性问题", + "content": "# CAP理论\n\nCAP的结论非常简单:在分布式系统里,有3个属性非常重要,但只能同时满足其中的2个。\n\nConsistency:all nodes在任何时刻看到的data都是一样的(或说client的read操作总是返回最新写入的那个value) Availability:系统时刻都允许操作,并且操作总会快速被Coordinator响应,最终client很快就得到返回的结果 Partition-tolerance:尽管网路有时候会因为故障导致被分隔开,但是系统依然在正常工作(或者说在满足前述的条件下工作)\n\n\n# 为什么这三者如此重要?\n\nFor Availability:\n\n经统计,对于Google、Amazon这样的数据公司,系统增加500ms的延迟就会损失公司20%的收益,所以必须快速且可靠地进行 Read/Write。\n\nFor Consistency:\n\n比如在银行系统,任何client都必须查看到最新的updated data item,不然就会给交易造成致命的影响。\n\nFor Partition-tolerance:\n\n因为Internet可能因为某些原因随时断开(Router故障、海地线缆断开、DNS故障);即使在同一个data center里,故障都会随时随地地发生,比如 Rack switcher 宕机。\n\n\n# CAP权衡\n\n * 如今的云计算环境里,因为网络随时都会被隔离开来,这是无法避免的,P是必须满足的,那么CAP暗示一个system要在C和A中做出抉择。\n * A和C并不是一个硬币的两面,只能选择其中一个;A和C应该看成天平,系统可以选择向哪边倾斜,但另一边也应该一定程度的保留。\n * 对于A和C之间的选择,不应该粗粒度的整个系统级别进行选取,而应该针对系统中的不同子系统,针对性的采取不同的取舍策略。\n\n\n# 三种组合\n\n * CA: 保证可用性和一致性,放弃分区:除非不是分布式架构,或者应用在一个永不会通信故障的网络中(理想),只有个别场景符合,当前的互联网架构显然不符合使用\n * CP: 保证一致性和分区容忍性,放弃可用性:当节点间不可通信时,进行阻塞,直到通信恢复,期间无法再对外提供服务,用户体验不好,如A转账给B,只有A扣款成功并B收款成功,整个事务才算完成,显然耗费资源\n * AP: 保证可用性和分区容忍性,放弃强一致性(使用最终一致性):给出一个用户可以忍受的时间,时间内达成数据的最终一致性,比如跨行转账,并不是立刻到账,可能是明天,或者2小时内到账\n\n\n# 一致性的妥协——最终一致性和Base原则\n\n * BA(Basically Available)基本可用:系统在绝大部分时间应处于可用状态,允许出现故障损失部分可用性,但保证核心可用。\n * S(Soft State)软状态:数据状态不要求在任何时刻都保持一致,允许存在中间状态,而该状态不影响系统可用性。对于多副本的存储系统而言,就是允许副本之间的同步存在延时,并且在这个过程中系统依旧可以响应客户端请求。\n * E(Eventual Consistency)最终一致性:尽管软状态不要求分布式数据在任何时刻都保持一致,但经过一定时间后,这些数据最终能达到一致性状态。 BASE理论的核心思想是:把分布式系统的可用性放在首位,放弃CAP中对数据强一致性的追求,只要系统能保证数据最终一致。\n\n\n# 幂等性\n\n在微服务架构下,不同微服务间会有大量的基于http,rpc或者mq消息的网络通信,接口的重复调用以及消息的重复消费可能会经常发生。\n\n微服务架构应该具有幂等性,当接口被重复调用时,消息被重复消费时,对系统的产生的影响应该和接口被调用一次,消息被消费一次时一样。\n\n\n# 如何解决幂等性问题\n\n * 全局唯一ID。根据业务生成一个全局唯一ID,在调用接口时会传入该ID,接口提供方会从相应的存储系统比如Redis中去检索这个全局ID是否存在,如果存在则说明该操作已经执行过了,将拒绝本次服务请求;否则将相应该服务请求并将全局ID存入存储系统中,之后包含相同业务ID参数的请求将被拒绝。\n\n * 数据库表唯一键。这种方法适用于在业务中有唯一标识的插入场景。比如在支付场景中,一个订单只会支付一次,可以建立一张去重表,将订单ID作为唯一索引。把支付并且写入支付单据到去重表放入一个事务中,这样当出现重复支付时,数据库就会抛出唯一约束异常,操作就会回滚。这样保证了订单只会被支付一次。\n\n * 多版本并发控制适合对更新请求作幂等性控制,比如要更新商品的名字,这是就可以在更新的接口中增加一个版本号来做幂等性控制", + "normalizedContent": "# cap理论\n\ncap的结论非常简单:在分布式系统里,有3个属性非常重要,但只能同时满足其中的2个。\n\nconsistency:all nodes在任何时刻看到的data都是一样的(或说client的read操作总是返回最新写入的那个value) availability:系统时刻都允许操作,并且操作总会快速被coordinator响应,最终client很快就得到返回的结果 partition-tolerance:尽管网路有时候会因为故障导致被分隔开,但是系统依然在正常工作(或者说在满足前述的条件下工作)\n\n\n# 为什么这三者如此重要?\n\nfor availability:\n\n经统计,对于google、amazon这样的数据公司,系统增加500ms的延迟就会损失公司20%的收益,所以必须快速且可靠地进行 read/write。\n\nfor consistency:\n\n比如在银行系统,任何client都必须查看到最新的updated data item,不然就会给交易造成致命的影响。\n\nfor partition-tolerance:\n\n因为internet可能因为某些原因随时断开(router故障、海地线缆断开、dns故障);即使在同一个data center里,故障都会随时随地地发生,比如 rack switcher 宕机。\n\n\n# cap权衡\n\n * 如今的云计算环境里,因为网络随时都会被隔离开来,这是无法避免的,p是必须满足的,那么cap暗示一个system要在c和a中做出抉择。\n * a和c并不是一个硬币的两面,只能选择其中一个;a和c应该看成天平,系统可以选择向哪边倾斜,但另一边也应该一定程度的保留。\n * 对于a和c之间的选择,不应该粗粒度的整个系统级别进行选取,而应该针对系统中的不同子系统,针对性的采取不同的取舍策略。\n\n\n# 三种组合\n\n * ca: 保证可用性和一致性,放弃分区:除非不是分布式架构,或者应用在一个永不会通信故障的网络中(理想),只有个别场景符合,当前的互联网架构显然不符合使用\n * cp: 保证一致性和分区容忍性,放弃可用性:当节点间不可通信时,进行阻塞,直到通信恢复,期间无法再对外提供服务,用户体验不好,如a转账给b,只有a扣款成功并b收款成功,整个事务才算完成,显然耗费资源\n * ap: 保证可用性和分区容忍性,放弃强一致性(使用最终一致性):给出一个用户可以忍受的时间,时间内达成数据的最终一致性,比如跨行转账,并不是立刻到账,可能是明天,或者2小时内到账\n\n\n# 一致性的妥协——最终一致性和base原则\n\n * ba(basically available)基本可用:系统在绝大部分时间应处于可用状态,允许出现故障损失部分可用性,但保证核心可用。\n * s(soft state)软状态:数据状态不要求在任何时刻都保持一致,允许存在中间状态,而该状态不影响系统可用性。对于多副本的存储系统而言,就是允许副本之间的同步存在延时,并且在这个过程中系统依旧可以响应客户端请求。\n * e(eventual consistency)最终一致性:尽管软状态不要求分布式数据在任何时刻都保持一致,但经过一定时间后,这些数据最终能达到一致性状态。 base理论的核心思想是:把分布式系统的可用性放在首位,放弃cap中对数据强一致性的追求,只要系统能保证数据最终一致。\n\n\n# 幂等性\n\n在微服务架构下,不同微服务间会有大量的基于http,rpc或者mq消息的网络通信,接口的重复调用以及消息的重复消费可能会经常发生。\n\n微服务架构应该具有幂等性,当接口被重复调用时,消息被重复消费时,对系统的产生的影响应该和接口被调用一次,消息被消费一次时一样。\n\n\n# 如何解决幂等性问题\n\n * 全局唯一id。根据业务生成一个全局唯一id,在调用接口时会传入该id,接口提供方会从相应的存储系统比如redis中去检索这个全局id是否存在,如果存在则说明该操作已经执行过了,将拒绝本次服务请求;否则将相应该服务请求并将全局id存入存储系统中,之后包含相同业务id参数的请求将被拒绝。\n\n * 数据库表唯一键。这种方法适用于在业务中有唯一标识的插入场景。比如在支付场景中,一个订单只会支付一次,可以建立一张去重表,将订单id作为唯一索引。把支付并且写入支付单据到去重表放入一个事务中,这样当出现重复支付时,数据库就会抛出唯一约束异常,操作就会回滚。这样保证了订单只会被支付一次。\n\n * 多版本并发控制适合对更新请求作幂等性控制,比如要更新商品的名字,这是就可以在更新的接口中增加一个版本号来做幂等性控制", "charsets": { "cjk": true }, @@ -6395,274 +6662,344 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "关于", + "title": "非技术问题", "frontmatter": { - "title": "关于", - "date": "2019-12-25T14:27:01.000Z", - "permalink": "/about", - "sidebar": false, - "article": false + "title": "非技术问题", + "date": "2022-03-27T13:53:27.000Z", + "permalink": "/pages/5e507a/", + "categories": [ + "非技术" + ], + "tags": [ + null + ] }, - "regularPath": "/15.%E5%85%B3%E4%BA%8E/01.%E5%85%B3%E4%BA%8E.html", - "relativePath": "15.关于/01.关于.md", - "key": "v-918dfaf8", - "path": "/about/", + "regularPath": "/10.%E9%9D%9E%E6%8A%80%E6%9C%AF/01.%E9%9D%9E%E6%8A%80%E6%9C%AF%E9%97%AE%E9%A2%98.html", + "relativePath": "10.非技术/01.非技术问题.md", + "key": "v-2a52a762", + "path": "/pages/5e507a/", "headers": [ { "level": 2, - "title": "📚Blog", - "slug": "📚blog", - "normalizedTitle": "📚blog", + "title": "最有技术难度的项目是什么?遇到技术难点怎么解决?", + "slug": "最有技术难度的项目是什么-遇到技术难点怎么解决", + "normalizedTitle": "最有技术难度的项目是什么?遇到技术难点怎么解决?", "charIndex": 2 }, { "level": 2, - "title": "⚡ 反馈与交流", - "slug": "⚡-反馈与交流", - "normalizedTitle": "⚡ 反馈与交流", - "charIndex": 27 + "title": "为什么要离开现在的公司?", + "slug": "为什么要离开现在的公司", + "normalizedTitle": "为什么要离开现在的公司?", + "charIndex": 31 }, { "level": 2, - "title": "🐼Me", - "slug": "🐼me", - "normalizedTitle": "🐼me", - "charIndex": 156 + "title": "未来的职业规划是什么?", + "slug": "未来的职业规划是什么", + "normalizedTitle": "未来的职业规划是什么?", + "charIndex": 48 }, { - "level": 3, - "title": "技能", - "slug": "技能", - "normalizedTitle": "技能", - "charIndex": 177 + "level": 2, + "title": "下一份工作希望学习到什么?", + "slug": "下一份工作希望学习到什么", + "normalizedTitle": "下一份工作希望学习到什么?", + "charIndex": 64 }, { "level": 2, - "title": "Java学习", - "slug": "java学习", - "normalizedTitle": "java学习", - "charIndex": 14 + "title": "团队中对技术选型有冲突的话怎么解决?", + "slug": "团队中对技术选型有冲突的话怎么解决", + "normalizedTitle": "团队中对技术选型有冲突的话怎么解决?", + "charIndex": 82 }, { "level": 2, - "title": "✉️ 联系", - "slug": "联系", - "normalizedTitle": "✉️ 联系", - "charIndex": 345 + "title": "你的性格和技能上有什么缺点", + "slug": "你的性格和技能上有什么缺点", + "normalizedTitle": "你的性格和技能上有什么缺点", + "charIndex": 105 + }, + { + "level": 2, + "title": "简单描述一下自己是怎么样的人?", + "slug": "简单描述一下自己是怎么样的人", + "normalizedTitle": "简单描述一下自己是怎么样的人?", + "charIndex": 123 + }, + { + "level": 2, + "title": "学习中遇到的最大的困难是什么?", + "slug": "学习中遇到的最大的困难是什么", + "normalizedTitle": "学习中遇到的最大的困难是什么?", + "charIndex": 143 + }, + { + "level": 2, + "title": "最有成就感的项目是什么?", + "slug": "最有成就感的项目是什么", + "normalizedTitle": "最有成就感的项目是什么?", + "charIndex": 163 + }, + { + "level": 2, + "title": "团队合作沟通中遇到过什么问题?", + "slug": "团队合作沟通中遇到过什么问题", + "normalizedTitle": "团队合作沟通中遇到过什么问题?", + "charIndex": 180 + }, + { + "level": 2, + "title": "目前为止,坚持得最久一件事情是什么?", + "slug": "目前为止-坚持得最久一件事情是什么", + "normalizedTitle": "目前为止,坚持得最久一件事情是什么?", + "charIndex": 200 + }, + { + "level": 2, + "title": "能接受多大强度的加班?", + "slug": "能接受多大强度的加班", + "normalizedTitle": "能接受多大强度的加班?", + "charIndex": 223 + }, + { + "level": 2, + "title": "最近在看什么书吗,有没有接触过什么新技术?", + "slug": "最近在看什么书吗-有没有接触过什么新技术", + "normalizedTitle": "最近在看什么书吗,有没有接触过什么新技术?", + "charIndex": 239 } ], - "headersStr": "📚Blog ⚡ 反馈与交流 🐼Me 技能 Java学习 ✉️ 联系", - "content": "# 📚Blog\n\n这是一个Java学习网站\n\n\n# ⚡ 反馈与交流\n\n文章内容仅是我个人的小总结,资历尚浅,如有误还请指正。\n\n在使用过程中有任何问题和想法,你可以:\n\n * 给我提 Issue。\n\n你也可以在Issue查看别人提的问题和给出解决方案。\n\n * 戳JavaDriver社区\n\n更新日志\n\n\n# 🐼Me\n\n西二旗Java老司机\n\n\n# 技能\n\n * 熟悉 SpringBoot、Mybatis、Netty、Kafka 的拼写\n * 了解 Linux、windows、macOS 的开关机方式\n * 精通 Git 的 pull 和 push,并注册了 GitHub 帐号刷了一些 star\n\n\n# Java学习\n\n\n关注公众号,回复后端资源,即可获取学习资源一份。\n\n\n# ✉️ 联系\n\n * WeChat or QQ: {{ QQ }}\n * Email: 854818303@qq.com\n * GitHub: https://github.com/AdmireAn/JavaDriver", - "normalizedContent": "# 📚blog\n\n这是一个java学习网站\n\n\n# ⚡ 反馈与交流\n\n文章内容仅是我个人的小总结,资历尚浅,如有误还请指正。\n\n在使用过程中有任何问题和想法,你可以:\n\n * 给我提 issue。\n\n你也可以在issue查看别人提的问题和给出解决方案。\n\n * 戳javadriver社区\n\n更新日志\n\n\n# 🐼me\n\n西二旗java老司机\n\n\n# 技能\n\n * 熟悉 springboot、mybatis、netty、kafka 的拼写\n * 了解 linux、windows、macos 的开关机方式\n * 精通 git 的 pull 和 push,并注册了 github 帐号刷了一些 star\n\n\n# java学习\n\n\n关注公众号,回复后端资源,即可获取学习资源一份。\n\n\n# ✉️ 联系\n\n * wechat or qq: {{ qq }}\n * email: 854818303@qq.com\n * github: https://github.com/admirean/javadriver", + "headersStr": "最有技术难度的项目是什么?遇到技术难点怎么解决? 为什么要离开现在的公司? 未来的职业规划是什么? 下一份工作希望学习到什么? 团队中对技术选型有冲突的话怎么解决? 你的性格和技能上有什么缺点 简单描述一下自己是怎么样的人? 学习中遇到的最大的困难是什么? 最有成就感的项目是什么? 团队合作沟通中遇到过什么问题? 目前为止,坚持得最久一件事情是什么? 能接受多大强度的加班? 最近在看什么书吗,有没有接触过什么新技术?", + "content": "# 最有技术难度的项目是什么?遇到技术难点怎么解决?\n\n\n# 为什么要离开现在的公司?\n\n\n# 未来的职业规划是什么?\n\n\n# 下一份工作希望学习到什么?\n\n\n# 团队中对技术选型有冲突的话怎么解决?\n\n\n# 你的性格和技能上有什么缺点\n\n\n# 简单描述一下自己是怎么样的人?\n\n\n# 学习中遇到的最大的困难是什么?\n\n\n# 最有成就感的项目是什么?\n\n\n# 团队合作沟通中遇到过什么问题?\n\n\n# 目前为止,坚持得最久一件事情是什么?\n\n\n# 能接受多大强度的加班?\n\n\n# 最近在看什么书吗,有没有接触过什么新技术?", + "normalizedContent": "# 最有技术难度的项目是什么?遇到技术难点怎么解决?\n\n\n# 为什么要离开现在的公司?\n\n\n# 未来的职业规划是什么?\n\n\n# 下一份工作希望学习到什么?\n\n\n# 团队中对技术选型有冲突的话怎么解决?\n\n\n# 你的性格和技能上有什么缺点\n\n\n# 简单描述一下自己是怎么样的人?\n\n\n# 学习中遇到的最大的困难是什么?\n\n\n# 最有成就感的项目是什么?\n\n\n# 团队合作沟通中遇到过什么问题?\n\n\n# 目前为止,坚持得最久一件事情是什么?\n\n\n# 能接受多大强度的加班?\n\n\n# 最近在看什么书吗,有没有接触过什么新技术?", "charsets": { "cjk": true }, - "lastUpdated": "2022/06/20, 22:47:41", - "lastUpdatedTimestamp": 1655736461000 + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 }, { - "title": "非技术问题", + "title": "系统设计", "frontmatter": { - "title": "非技术问题", - "date": "2022-03-27T13:53:27.000Z", - "permalink": "/pages/5e507a/", + "title": "系统设计", + "date": "2022-03-11T00:56:07.000Z", + "permalink": "/pages/01b9b2/", "categories": [ - "非技术" + "系统设计" ], "tags": [ null ] }, - "regularPath": "/10.%E9%9D%9E%E6%8A%80%E6%9C%AF/01.%E9%9D%9E%E6%8A%80%E6%9C%AF%E9%97%AE%E9%A2%98.html", - "relativePath": "10.非技术/01.非技术问题.md", - "key": "v-2a52a762", - "path": "/pages/5e507a/", + "regularPath": "/08.%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/001.%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1.html", + "relativePath": "08.系统设计/001.系统设计.md", + "key": "v-a9558ec0", + "path": "/pages/01b9b2/", "headers": [ { "level": 2, - "title": "最有技术难度的项目是什么?遇到技术难点怎么解决?", - "slug": "最有技术难度的项目是什么-遇到技术难点怎么解决", - "normalizedTitle": "最有技术难度的项目是什么?遇到技术难点怎么解决?", + "title": "电商系统中,如何实现秒杀功能?如何解决商品的超卖问题?", + "slug": "电商系统中-如何实现秒杀功能-如何解决商品的超卖问题", + "normalizedTitle": "电商系统中,如何实现秒杀功能?如何解决商品的超卖问题?", "charIndex": 2 }, { "level": 2, - "title": "为什么要离开现在的公司?", - "slug": "为什么要离开现在的公司", - "normalizedTitle": "为什么要离开现在的公司?", - "charIndex": 31 + "title": "如何解决缓存与数据库不一致的问题?", + "slug": "如何解决缓存与数据库不一致的问题", + "normalizedTitle": "如何解决缓存与数据库不一致的问题?", + "charIndex": 34 + }, + { + "level": 2, + "title": "简述中间件削峰和限流的使用场景", + "slug": "简述中间件削峰和限流的使用场景", + "normalizedTitle": "简述中间件削峰和限流的使用场景", + "charIndex": 56 + }, + { + "level": 2, + "title": "项目上有什么技术难点?", + "slug": "项目上有什么技术难点", + "normalizedTitle": "项目上有什么技术难点?", + "charIndex": 76 + }, + { + "level": 2, + "title": "简述 Zookeeper 基础原理以及使用场景", + "slug": "简述-zookeeper-基础原理以及使用场景", + "normalizedTitle": "简述 zookeeper 基础原理以及使用场景", + "charIndex": 92 + }, + { + "level": 2, + "title": "如何实现百度搜索功能?", + "slug": "如何实现百度搜索功能", + "normalizedTitle": "如何实现百度搜索功能?", + "charIndex": 120 + }, + { + "level": 2, + "title": "高并发情景下,核心线程池该如何设置参数?", + "slug": "高并发情景下-核心线程池该如何设置参数", + "normalizedTitle": "高并发情景下,核心线程池该如何设置参数?", + "charIndex": 136 }, { "level": 2, - "title": "未来的职业规划是什么?", - "slug": "未来的职业规划是什么", - "normalizedTitle": "未来的职业规划是什么?", - "charIndex": 48 + "title": "设计一个阻塞队列", + "slug": "设计一个阻塞队列", + "normalizedTitle": "设计一个阻塞队列", + "charIndex": 161 }, { "level": 2, - "title": "下一份工作希望学习到什么?", - "slug": "下一份工作希望学习到什么", - "normalizedTitle": "下一份工作希望学习到什么?", - "charIndex": 64 + "title": "容器化了解吗?它的基本原理是什么?", + "slug": "容器化了解吗-它的基本原理是什么", + "normalizedTitle": "容器化了解吗?它的基本原理是什么?", + "charIndex": 174 }, { "level": 2, - "title": "团队中对技术选型有冲突的话怎么解决?", - "slug": "团队中对技术选型有冲突的话怎么解决", - "normalizedTitle": "团队中对技术选型有冲突的话怎么解决?", - "charIndex": 82 + "title": "如何实现唯一的分布式 ID", + "slug": "如何实现唯一的分布式-id", + "normalizedTitle": "如何实现唯一的分布式 id", + "charIndex": 196 }, { "level": 2, - "title": "你的性格和技能上有什么缺点", - "slug": "你的性格和技能上有什么缺点", - "normalizedTitle": "你的性格和技能上有什么缺点", - "charIndex": 105 + "title": "简述中间件削峰和限流的使用场景", + "slug": "简述中间件削峰和限流的使用场景-2", + "normalizedTitle": "简述中间件削峰和限流的使用场景", + "charIndex": 56 }, { "level": 2, - "title": "简单描述一下自己是怎么样的人?", - "slug": "简单描述一下自己是怎么样的人", - "normalizedTitle": "简单描述一下自己是怎么样的人?", - "charIndex": 123 + "title": "简述一致性哈希算法的实现方式及原理", + "slug": "简述一致性哈希算法的实现方式及原理", + "normalizedTitle": "简述一致性哈希算法的实现方式及原理", + "charIndex": 234 }, { "level": 2, - "title": "学习中遇到的最大的困难是什么?", - "slug": "学习中遇到的最大的困难是什么", - "normalizedTitle": "学习中遇到的最大的困难是什么?", - "charIndex": 143 + "title": "简述常见的负载均衡算法", + "slug": "简述常见的负载均衡算法", + "normalizedTitle": "简述常见的负载均衡算法", + "charIndex": 256 }, { "level": 2, - "title": "最有成就感的项目是什么?", - "slug": "最有成就感的项目是什么", - "normalizedTitle": "最有成就感的项目是什么?", - "charIndex": 163 + "title": "简述 MapReduce 的原理", + "slug": "简述-mapreduce-的原理", + "normalizedTitle": "简述 mapreduce 的原理", + "charIndex": 272 }, { "level": 2, - "title": "团队合作沟通中遇到过什么问题?", - "slug": "团队合作沟通中遇到过什么问题", - "normalizedTitle": "团队合作沟通中遇到过什么问题?", - "charIndex": 180 + "title": "设计 Tiny URL 系统", + "slug": "设计-tiny-url-系统", + "normalizedTitle": "设计 tiny url 系统", + "charIndex": 293 }, { "level": 2, - "title": "目前为止,坚持得最久一件事情是什么?", - "slug": "目前为止-坚持得最久一件事情是什么", - "normalizedTitle": "目前为止,坚持得最久一件事情是什么?", - "charIndex": 200 + "title": "如何设计一个无锁队列", + "slug": "如何设计一个无锁队列", + "normalizedTitle": "如何设计一个无锁队列", + "charIndex": 312 }, { "level": 2, - "title": "能接受多大强度的加班?", - "slug": "能接受多大强度的加班", - "normalizedTitle": "能接受多大强度的加班?", - "charIndex": 223 + "title": "RestFul 与 RPC 的区别是什么?RestFul 的优点在哪里?", + "slug": "restful-与-rpc-的区别是什么-restful-的优点在哪里", + "normalizedTitle": "restful 与 rpc 的区别是什么?restful 的优点在哪里?", + "charIndex": 327 }, { "level": 2, - "title": "最近在看什么书吗,有没有接触过什么新技术?", - "slug": "最近在看什么书吗-有没有接触过什么新技术", - "normalizedTitle": "最近在看什么书吗,有没有接触过什么新技术?", - "charIndex": 239 + "title": "假如明天是活动高峰?QPS 预计会翻10倍,你要怎么做?", + "slug": "假如明天是活动高峰-qps-预计会翻10倍-你要怎么做", + "normalizedTitle": "假如明天是活动高峰?qps 预计会翻10倍,你要怎么做?", + "charIndex": 368 } ], - "headersStr": "最有技术难度的项目是什么?遇到技术难点怎么解决? 为什么要离开现在的公司? 未来的职业规划是什么? 下一份工作希望学习到什么? 团队中对技术选型有冲突的话怎么解决? 你的性格和技能上有什么缺点 简单描述一下自己是怎么样的人? 学习中遇到的最大的困难是什么? 最有成就感的项目是什么? 团队合作沟通中遇到过什么问题? 目前为止,坚持得最久一件事情是什么? 能接受多大强度的加班? 最近在看什么书吗,有没有接触过什么新技术?", - "content": "# 最有技术难度的项目是什么?遇到技术难点怎么解决?\n\n\n# 为什么要离开现在的公司?\n\n\n# 未来的职业规划是什么?\n\n\n# 下一份工作希望学习到什么?\n\n\n# 团队中对技术选型有冲突的话怎么解决?\n\n\n# 你的性格和技能上有什么缺点\n\n\n# 简单描述一下自己是怎么样的人?\n\n\n# 学习中遇到的最大的困难是什么?\n\n\n# 最有成就感的项目是什么?\n\n\n# 团队合作沟通中遇到过什么问题?\n\n\n# 目前为止,坚持得最久一件事情是什么?\n\n\n# 能接受多大强度的加班?\n\n\n# 最近在看什么书吗,有没有接触过什么新技术?", - "normalizedContent": "# 最有技术难度的项目是什么?遇到技术难点怎么解决?\n\n\n# 为什么要离开现在的公司?\n\n\n# 未来的职业规划是什么?\n\n\n# 下一份工作希望学习到什么?\n\n\n# 团队中对技术选型有冲突的话怎么解决?\n\n\n# 你的性格和技能上有什么缺点\n\n\n# 简单描述一下自己是怎么样的人?\n\n\n# 学习中遇到的最大的困难是什么?\n\n\n# 最有成就感的项目是什么?\n\n\n# 团队合作沟通中遇到过什么问题?\n\n\n# 目前为止,坚持得最久一件事情是什么?\n\n\n# 能接受多大强度的加班?\n\n\n# 最近在看什么书吗,有没有接触过什么新技术?", + "headersStr": "电商系统中,如何实现秒杀功能?如何解决商品的超卖问题? 如何解决缓存与数据库不一致的问题? 简述中间件削峰和限流的使用场景 项目上有什么技术难点? 简述 Zookeeper 基础原理以及使用场景 如何实现百度搜索功能? 高并发情景下,核心线程池该如何设置参数? 设计一个阻塞队列 容器化了解吗?它的基本原理是什么? 如何实现唯一的分布式 ID 简述中间件削峰和限流的使用场景 简述一致性哈希算法的实现方式及原理 简述常见的负载均衡算法 简述 MapReduce 的原理 设计 Tiny URL 系统 如何设计一个无锁队列 RestFul 与 RPC 的区别是什么?RestFul 的优点在哪里? 假如明天是活动高峰?QPS 预计会翻10倍,你要怎么做?", + "content": "# 电商系统中,如何实现秒杀功能?如何解决商品的超卖问题?\n\n\n# 如何解决缓存与数据库不一致的问题?\n\n\n# 简述中间件削峰和限流的使用场景\n\n\n# 项目上有什么技术难点?\n\n\n# 简述 Zookeeper 基础原理以及使用场景\n\n\n# 如何实现百度搜索功能?\n\n\n# 高并发情景下,核心线程池该如何设置参数?\n\n\n# 设计一个阻塞队列\n\n\n# 容器化了解吗?它的基本原理是什么?\n\n\n# 如何实现唯一的分布式 ID\n\n\n# 简述中间件削峰和限流的使用场景\n\n\n# 简述一致性哈希算法的实现方式及原理\n\n\n# 简述常见的负载均衡算法\n\n\n# 简述 MapReduce 的原理\n\n\n# 设计 Tiny URL 系统\n\n\n# 如何设计一个无锁队列\n\n\n# RestFul 与 RPC 的区别是什么?RestFul 的优点在哪里?\n\n\n# 假如明天是活动高峰?QPS 预计会翻10倍,你要怎么做?", + "normalizedContent": "# 电商系统中,如何实现秒杀功能?如何解决商品的超卖问题?\n\n\n# 如何解决缓存与数据库不一致的问题?\n\n\n# 简述中间件削峰和限流的使用场景\n\n\n# 项目上有什么技术难点?\n\n\n# 简述 zookeeper 基础原理以及使用场景\n\n\n# 如何实现百度搜索功能?\n\n\n# 高并发情景下,核心线程池该如何设置参数?\n\n\n# 设计一个阻塞队列\n\n\n# 容器化了解吗?它的基本原理是什么?\n\n\n# 如何实现唯一的分布式 id\n\n\n# 简述中间件削峰和限流的使用场景\n\n\n# 简述一致性哈希算法的实现方式及原理\n\n\n# 简述常见的负载均衡算法\n\n\n# 简述 mapreduce 的原理\n\n\n# 设计 tiny url 系统\n\n\n# 如何设计一个无锁队列\n\n\n# restful 与 rpc 的区别是什么?restful 的优点在哪里?\n\n\n# 假如明天是活动高峰?qps 预计会翻10倍,你要怎么做?", "charsets": { "cjk": true }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 }, { - "title": "常用的前端轮子", + "title": "关于", "frontmatter": { - "title": "常用的前端轮子", - "date": "2021-10-09T19:42:02.000Z", - "permalink": "/pages/47cf96/", + "title": "关于", + "date": "2019-12-25T14:27:01.000Z", + "permalink": "/about", + "sidebar": false, "article": false }, - "regularPath": "/@pages/20.%E6%94%B6%E8%97%8F%E5%A4%B9/02.%E5%B8%B8%E7%94%A8%E7%9A%84%E5%89%8D%E7%AB%AF%E8%BD%AE%E5%AD%90.html", - "relativePath": "@pages/20.收藏夹/02.常用的前端轮子.md", - "key": "v-09c150e6", - "path": "/pages/47cf96/", + "regularPath": "/15.%E5%85%B3%E4%BA%8E/01.%E5%85%B3%E4%BA%8E.html", + "relativePath": "15.关于/01.关于.md", + "key": "v-918dfaf8", + "path": "/about/", "headers": [ { "level": 2, - "title": "React UI 组件库", - "slug": "react-ui-组件库", - "normalizedTitle": "react ui 组件库", + "title": "📚Blog", + "slug": "📚blog", + "normalizedTitle": "📚blog", "charIndex": 2 }, { "level": 2, - "title": "Vue UI组件库", - "slug": "vue-ui组件库", - "normalizedTitle": "vue ui组件库", - "charIndex": 68 + "title": "⚡ 反馈与交流", + "slug": "⚡-反馈与交流", + "normalizedTitle": "⚡ 反馈与交流", + "charIndex": 27 }, { "level": 2, - "title": "常用效果组件", - "slug": "常用效果组件", - "normalizedTitle": "常用效果组件", - "charIndex": 124 + "title": "🐼Me", + "slug": "🐼me", + "normalizedTitle": "🐼me", + "charIndex": 156 }, { - "level": 2, - "title": "工具类", - "slug": "工具类", - "normalizedTitle": "工具类", - "charIndex": 201 + "level": 3, + "title": "技能", + "slug": "技能", + "normalizedTitle": "技能", + "charIndex": 177 }, { "level": 2, - "title": "Vue工具类", - "slug": "vue工具类", - "normalizedTitle": "vue工具类", - "charIndex": 331 + "title": "Java学习", + "slug": "java学习", + "normalizedTitle": "java学习", + "charIndex": 14 }, { "level": 2, - "title": "其他", - "slug": "其他", - "normalizedTitle": "其他", - "charIndex": 469 + "title": "✉️ 联系", + "slug": "联系", + "normalizedTitle": "✉️ 联系", + "charIndex": 345 } ], - "headersStr": "React UI 组件库 Vue UI组件库 常用效果组件 工具类 Vue工具类 其他", - "content": "# React UI 组件库\n\n * Ant Design\n * React Bootstrap\n * MATERIAL-UI\n\n\n# Vue UI组件库\n\n * Element UI PC端\n * Vant 移动端\n * View UI\n\n\n# 常用效果组件\n\n * Animate.css 动画库\n * Swiper 轮播组件\n * mescroll 下拉刷新和上拉加载框架-基于原生JS\n\n\n# 工具类\n\n * Lodash.js\n * Day.js 处理日期\n * Timeago.js 相对时间,如N小时前\n * Echarts 百度图表\n * Meditor.md 开源在线 Markdown 编辑器\n * validator.js 验证库\n\n\n# Vue工具类\n\n * vue-draggable 基于Sortable.js实现的vue拖拽插件\n * vue-qr 文本转二维码\n * vue-cropper 图片裁剪插件\n * vue-lazyload 懒加载\n * vue-simple-upload 上传组件\n\n\n# 其他\n\n * H5带笔锋手写签名,支持PC端和移动端", - "normalizedContent": "# react ui 组件库\n\n * ant design\n * react bootstrap\n * material-ui\n\n\n# vue ui组件库\n\n * element ui pc端\n * vant 移动端\n * view ui\n\n\n# 常用效果组件\n\n * animate.css 动画库\n * swiper 轮播组件\n * mescroll 下拉刷新和上拉加载框架-基于原生js\n\n\n# 工具类\n\n * lodash.js\n * day.js 处理日期\n * timeago.js 相对时间,如n小时前\n * echarts 百度图表\n * meditor.md 开源在线 markdown 编辑器\n * validator.js 验证库\n\n\n# vue工具类\n\n * vue-draggable 基于sortable.js实现的vue拖拽插件\n * vue-qr 文本转二维码\n * vue-cropper 图片裁剪插件\n * vue-lazyload 懒加载\n * vue-simple-upload 上传组件\n\n\n# 其他\n\n * h5带笔锋手写签名,支持pc端和移动端", + "headersStr": "📚Blog ⚡ 反馈与交流 🐼Me 技能 Java学习 ✉️ 联系", + "content": "# 📚Blog\n\n这是一个Java学习网站\n\n\n# ⚡ 反馈与交流\n\n文章内容仅是我个人的小总结,资历尚浅,如有误还请指正。\n\n在使用过程中有任何问题和想法,你可以:\n\n * 给我提 Issue。\n\n你也可以在Issue查看别人提的问题和给出解决方案。\n\n * 戳JavaDriver社区\n\n更新日志\n\n\n# 🐼Me\n\n西二旗Java老司机\n\n\n# 技能\n\n * 熟悉 SpringBoot、Mybatis、Netty、Kafka 的拼写\n * 了解 Linux、windows、macOS 的开关机方式\n * 精通 Git 的 pull 和 push,并注册了 GitHub 帐号刷了一些 star\n\n\n# Java学习\n\n\n关注公众号,回复后端资源,即可获取学习资源一份。\n\n\n# ✉️ 联系\n\n * WeChat or QQ: {{ QQ }}\n * Email: 854818303@qq.com\n * GitHub: https://github.com/AdmireAn/JavaDriver", + "normalizedContent": "# 📚blog\n\n这是一个java学习网站\n\n\n# ⚡ 反馈与交流\n\n文章内容仅是我个人的小总结,资历尚浅,如有误还请指正。\n\n在使用过程中有任何问题和想法,你可以:\n\n * 给我提 issue。\n\n你也可以在issue查看别人提的问题和给出解决方案。\n\n * 戳javadriver社区\n\n更新日志\n\n\n# 🐼me\n\n西二旗java老司机\n\n\n# 技能\n\n * 熟悉 springboot、mybatis、netty、kafka 的拼写\n * 了解 linux、windows、macos 的开关机方式\n * 精通 git 的 pull 和 push,并注册了 github 帐号刷了一些 star\n\n\n# java学习\n\n\n关注公众号,回复后端资源,即可获取学习资源一份。\n\n\n# ✉️ 联系\n\n * wechat or qq: {{ qq }}\n * email: 854818303@qq.com\n * github: https://github.com/admirean/javadriver", "charsets": { "cjk": true }, "lastUpdated": "2022/06/20, 22:47:41", "lastUpdatedTimestamp": 1655736461000 }, - { - "title": "归档", - "frontmatter": { - "archivesPage": true, - "title": "归档", - "permalink": "/archives/", - "article": false - }, - "regularPath": "/@pages/archivesPage.html", - "relativePath": "@pages/archivesPage.md", - "key": "v-48f7eec0", - "path": "/archives/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, { "title": "网站", "frontmatter": { @@ -6922,16 +7259,138 @@ export const siteData = { "charIndex": 5169 } ], - "excerpt": "

# 个人收藏夹

\n

# 推荐

\n\n

\n\n

\n

关注公众号[有趣研究社],回复前端资源,获取 前端学习资料

\n
", - "headersStr": "推荐 文档 社区 社区互动 技巧 博客 电子书 优秀文章 视频 Github 评论系统 前端小工具 代码编辑 Emoji表情 图片工具 思维导图 CSS CDN加速 网站托管 正则 其他 设计 图库 3D 交互 有趣 生成器 元宇宙 教程 产品 实用 Talk 算法 nginx 生活", - "content": "# 个人收藏夹\n\n\n# 推荐\n\n * panjiachen by 花裤衩\n * 编程导航 by 程序员鱼皮\n * 编程自学之路 by 程序羊\n * 前端知识图谱+B站资源整合 by 技术胖\n * 大圣编程自学网 by 大圣\n * 开发者武器库\n * 工具大全\n\n\n\n关注公众号[有趣研究社],回复前端资源,获取 前端学习资料\n\n\n# 文档\n\n * MDN | MDN-JS标准内置对象 Web技术权威文档\n * DevDocs Web 开发技术文档,非常不错的学习手册!\n * 现代JavaScript教程 以最新标准为基准的JS教程\n * ES5教程 阮一峰的JS教程\n * ES6教程 阮一峰的ES6教程\n * Bash 脚本教程 阮一峰编写\n * ECMA ECMA官网\n * 菜鸟教程 涵盖多种语言的初级教程\n * 腾讯云开发者手册\n\n\n# 社区\n\n * Github 程序员同性交友社区\n * 掘金 一个帮助开发者成长的社区\n * 简书 有很多频道的创作社区\n * 思否 解决技术问题的社区\n * stack overflow 同上,外网的\n * InfoQ 促进软件开发及相关领域知识与创新的传播\n * V2EX 创意工作者们的社区\n * 鱼塘热榜 划水网站,收集了很多网站,当天热门文章\n * 码力全开资源库 很全很强大,独立开发者/设计干货/优质利器/工具资源...\n\n\n# 社区互动\n\n * gitter\n * 兔小巢\n\n\n# 技巧\n\n * Google 趋势 查看某项技术或关键字的热度趋势,可用于分析某项技术的发展前景,或对比某两项技术的热度。\n * 百度指数 同上,但百度的数据仅限国内。\n\n\n# 博客\n\n * 阮一峰的网络日志\n * samanthaming 对前端小知识点的总结,并为每个知识点制作精美的小卡片。\n\n\n# 电子书\n\n * 高教书苑 高等教育出版社的书籍,包含多种学科。\n * SoBooks 免费的电子书资源网站\n\n\n# 优秀文章\n\n * 我做系统架构的一些原则 作者对系统架构的方法论总结\n * 灵活运用CSS开发技巧\n * 防御性CSS\n\n\n# 视频\n\n * bilibili B站,上面很多免费教学视频\n * 慕课网 实战视频教程\n * 妙味课堂 比较系统的前端入门视频教程\n * 中国大学MOOC 涵盖计算机、外语、心理学等专业免费课程\n * egghead 质量还不错的短视频教程,外网\n\n\n# Github\n\n * Repobeats 生成仓库的动态数据统计图\n\n * github 短域名服务\n * shields 徽章图标\n * followers 全球排名\n * star-history 展示一个项目 Stars 增长曲线\n\n\n# 评论系统\n\n * giscus 由 GitHub Discussions 驱动的评论系统\n\n\n# 前端小工具\n\n * Can I use 查看属性和方法的兼容性\n * 30 seconds of code 收集了许多有用的代码小片段\n\n\n# 代码编辑\n\n * codepen 在线代码编辑与演示\n * codesandbox 内嵌VSCode的在线IDE\n\n\n# Emoji表情\n\n * emoji表情\n * emoji表情备忘录\n * 根据文本匹配emoji\n * gitmoji 通过 emoji 表达 git 的操作内容\n\n> 在任意输入框快速打开emoji表情方法:\n> Windows系统下按Win + .\n> Mac系统下按Control + Command + 空格\n\n\n# 图片工具\n\n * tinypng图片压缩 压缩png很有用\n * 微图 浏览器端图片压缩,不会上传图片到服务器\n * Squoosh 谷歌出品在线免费图片压缩工具(jpg、png等,压缩效果比tinypng稍好)\n * waifu2x 通过卷积网络放大图片\n * vectormagic 转换矢量图\n * vectorizer 真正的 png 转 svg 神器\n * 在线AI图片处理 黑白修复、无损放大、动漫化、铅笔画等。\n * remove AI抠图\n * backgroundremover 又一个抠图的\n\n\n# 思维导图\n\n * processon在线作图 流程图、思维导图、原型图等\n * 百度脑图 思维导图\n * plectica 绘制知识图谱\n\n\n# CSS\n\n * 各种CSS生成器和JS代码片段\n\n * CSS Tricks CSS技巧收集与演示\n * CSS生成器\n * CSS渐变生成器\n * CSS3-Box Shadow(阴影)\n * 贝塞尔曲线生成器\n * 花纹背景生成器\n * 花纹背景-pattern.css\n * 3D字体\n * css-tricks css技巧文章\n * You-need-to-know-css CSS的各种DEMO,很全\n * animista CSS动画可视化工具,复制代码就能用\n * navnav 各种炫酷的CSS动画组件\n\n\n# CDN加速\n\n * jsDelivr 国外的一家优秀的公共 CDN 服务提供商\n * unpkg cdn 服务\n\n\n# 网站托管\n\n * vercel 好用的网站托管服务\n\n\n# 正则\n\n * 正则可视化\n * iHateRegex 正则搜索,细节做得很好\n * 正则迷你书 学习正则的小手册\n\n\n# 其他\n\n * Linux命令手册\n * carbon代码图片生成器 生成好看的代码图片\n\n\n# 设计\n\n * 创造师导航\n * 设计师网址导航\n * remove AI抠图,抠图算法很厉害\n * Manypixels 插画\n * Undraw 插画\n * storytale 插画,种类丰富,包含3D插画\n * uimovement 能从这个网站找到不少动画交互的灵感\n * awwwards是一个一个专门为设计精美的网站以及富有创意的网站颁奖的网站\n * dribbble 经常能在上面找到很多有创意好看的 gif 或者图片\n * Bēhance dribbble 是设计师的微博,Bēhance 是设计师的博客\n * Logojoy 使用 ai 做 logo 的网站,做出来的 logo 质量还不错。\n * brandmark 另一个在线制作 logo 网站\n * instant 又一个 logo 制作网站\n * namecheap又一个 logo 制作网站\n * logo-maker 又一个 logo 制作网站 这个更简单点 就是选模板之后微调\n * coolors 帮你在线配色的网站 你能找到不少配色灵感\n * colorhunt 另一个配色网站\n * uigradients 渐变色网站\n * designcap 在线海报设计\n * Flat UI 色表 Flat UI 色表\n * 0to255 颜色梯度\n * Ikonate 提供免费的图标 icons\n * remixicon 又一个提供免费图标 icons\n * feather 免费的 icons\n * nord 北欧性冷淡风主题配色\n * Unsplash 提供免费的高清图片\n * Pexels 提供免费的高清图片\n * colorkitty 从你的图片中提取配色\n * design.youzan 有赞设计原则\n * iconfont 阿里巴巴矢量图标库\n * undraw 免费的矢量插画\n * icomoon 矢量图标库\n * cssicon 所有的 icon 都是纯 css 画的 缺点:icon 不够多\n * CSS triangle generator 帮你快速用 css 做出三角形\n * clippy 在线帮你使用 css clip-path 做出各种形状的图形\n * Lorem Picsum 提供免费的占位图\n * Canva 可画 生成插画、封面、海报、头像等\n * 404页 404页素材\n * collectui 按功能组件分类的设计图\n * smartmockups 产品模板生成工具\n\n\n# 图库\n\n * uigradients 渐变色生成工具\n * freepik banner 图库\n * 觅元素一天免费下载十张 psd(免抠元素)\n * 搞定设计 可以抠图\n * vectorizer 真正的 png 转 svg 神器\n * 站酷 国内优秀的设计作品展示\n * 花瓣\n * 虎克 ps 学习教程\n * beTheme\n * UI 中国\n * wallhaven 壁纸网站-\n\n\n# 3D\n\n * sketchfab 3D模型\n\n\n# 交互\n\n * 微交互 里面收集了市面上很多很好的微交互例子 值得学习\n\n * Little Big Details 同上,一个国外微交互汇集网站\n\n * cruip 登录页的各种页面设计,可以免费下载模板\n\n * Comixify 一个波兰团队做了非常好玩的工具,可以把视频自动转成漫画,上图是他们提供的 demo,效果很棒。\n\n * taiko-web 太鼓达人网页版 只能说很 6\n\n\n# 有趣\n\n * 电脑恶搞 收集了一些恶搞小网页,比如xp系统蓝屏、黑客界面等\n * neocities 上面托管了很多有趣的网站\n * 奇趣网站收藏家 收藏了很多有趣的网站\n * FC在线模拟器(小霸王游戏机) 童年回忆\n * 帮你百度一下 可以 点我测试一下-\n * 国际版 同帮我百度一下-点我测试一下-\n * URL 地址播放 Emojis 动画 在地址栏里面播放 emoji\n * Can't Unsee 强烈建议前端、客户端、UI 开发的同学玩下,检查一下自己对设计稿的敏感度怎么样\n * ggtalk 平时一直在听的一个技术博客\n * awesome-comment 里面收集了很多有趣的代码注释\n * text-img 都将图片转化为 ascii 用来写注释\n * ascii video 使用ascii编码生成视频动画\n * weird-fonts 将普通字母转化为 特殊 unicode\n * snake 在地址栏里面玩贪吃蛇\n * zero-width-lib 利用零宽度字符实现 隐形水印、加密信息分享、逃脱词匹配,很有创意\n * abbreviations 查看一个简写是什么意思的网站\n * magi ai 搜索神器,超屌\n * 在线取名 解决取名难问题,超多名字生成\n\n\n# 生成器\n\n * 卡通头像生成器 上传真人头像生成卡通头像\n\n * artbreeder 动漫图生成真人图像\n\n * 声音生成器 生成下雨、咖啡厅、海浪、火车等声音,可几种声音合成\n\n * 诺基亚短信图片生成器\n\n * 到账语音生成器 支付宝到账1亿元\n\n\n# 元宇宙\n\n符合元宇宙特征的几个网站,允许用户拥有虚拟土地,在上面构建自己的世界:\n\n * 沙盒\n * 梦境空间\n * Decentraland\n\n\n# 教程\n\n * npx 教你怎么合理的使用 npx\n * hacksplaining 网络安全学习网站\n * mobile-web-best-practice 移动 web 最佳实践\n\n\n# 产品\n\n * Product Hunt 好产品推荐\n\n * 产品大牛 什么有很多完整的产品原型可以借鉴\n\n * 磨刀 快速出 ui 原型\n\n\n# 实用\n\n * webden 在线网页编辑器,轻便快捷\n * browser-update 浏览器版本更新提示插件\n * typeform 一个国外的在线调查问卷网站\n * VideoFk VideoFk 视频在线解析下载\n * 全历史 历史内容聚合网站\n * UzerMe 云端办公工具\n * SoBooks 强大的电子书资源网站\n * 稿定设计 键式设计工具+智能抠图\n * 大力盘 百度网盘搜索\n * ENFI 下载器 不限速下载器\n * 来画视频 像做 PPT 一样做短视频\n * Arkie 海报制作工具\n * 优品 PPT\n * 比格 PPT\n * 高清免费图片\n * 高清免费图片 2\n * shapedivider 生成波浪分隔线\n * Notion 知识库、快速笔记、TaskList、日记、读书清单,各种类型,应有尽有\n\n\n# Talk\n\n * peerigon-talks 收集了不少有意思的 talks\n\n\n# 算法\n\n * leetcode 用 js 刷 leetcode\n\n\n# nginx\n\n * nginx 可视化配置工具\n\n\n# 生活\n\n * Ventusky 风雨气温图", - "normalizedContent": "# 个人收藏夹\n\n\n# 推荐\n\n * panjiachen by 花裤衩\n * 编程导航 by 程序员鱼皮\n * 编程自学之路 by 程序羊\n * 前端知识图谱+b站资源整合 by 技术胖\n * 大圣编程自学网 by 大圣\n * 开发者武器库\n * 工具大全\n\n\n\n关注公众号[有趣研究社],回复前端资源,获取 前端学习资料\n\n\n# 文档\n\n * mdn | mdn-js标准内置对象 web技术权威文档\n * devdocs web 开发技术文档,非常不错的学习手册!\n * 现代javascript教程 以最新标准为基准的js教程\n * es5教程 阮一峰的js教程\n * es6教程 阮一峰的es6教程\n * bash 脚本教程 阮一峰编写\n * ecma ecma官网\n * 菜鸟教程 涵盖多种语言的初级教程\n * 腾讯云开发者手册\n\n\n# 社区\n\n * github 程序员同性交友社区\n * 掘金 一个帮助开发者成长的社区\n * 简书 有很多频道的创作社区\n * 思否 解决技术问题的社区\n * stack overflow 同上,外网的\n * infoq 促进软件开发及相关领域知识与创新的传播\n * v2ex 创意工作者们的社区\n * 鱼塘热榜 划水网站,收集了很多网站,当天热门文章\n * 码力全开资源库 很全很强大,独立开发者/设计干货/优质利器/工具资源...\n\n\n# 社区互动\n\n * gitter\n * 兔小巢\n\n\n# 技巧\n\n * google 趋势 查看某项技术或关键字的热度趋势,可用于分析某项技术的发展前景,或对比某两项技术的热度。\n * 百度指数 同上,但百度的数据仅限国内。\n\n\n# 博客\n\n * 阮一峰的网络日志\n * samanthaming 对前端小知识点的总结,并为每个知识点制作精美的小卡片。\n\n\n# 电子书\n\n * 高教书苑 高等教育出版社的书籍,包含多种学科。\n * sobooks 免费的电子书资源网站\n\n\n# 优秀文章\n\n * 我做系统架构的一些原则 作者对系统架构的方法论总结\n * 灵活运用css开发技巧\n * 防御性css\n\n\n# 视频\n\n * bilibili b站,上面很多免费教学视频\n * 慕课网 实战视频教程\n * 妙味课堂 比较系统的前端入门视频教程\n * 中国大学mooc 涵盖计算机、外语、心理学等专业免费课程\n * egghead 质量还不错的短视频教程,外网\n\n\n# github\n\n * repobeats 生成仓库的动态数据统计图\n\n * github 短域名服务\n * shields 徽章图标\n * followers 全球排名\n * star-history 展示一个项目 stars 增长曲线\n\n\n# 评论系统\n\n * giscus 由 github discussions 驱动的评论系统\n\n\n# 前端小工具\n\n * can i use 查看属性和方法的兼容性\n * 30 seconds of code 收集了许多有用的代码小片段\n\n\n# 代码编辑\n\n * codepen 在线代码编辑与演示\n * codesandbox 内嵌vscode的在线ide\n\n\n# emoji表情\n\n * emoji表情\n * emoji表情备忘录\n * 根据文本匹配emoji\n * gitmoji 通过 emoji 表达 git 的操作内容\n\n> 在任意输入框快速打开emoji表情方法:\n> windows系统下按win + .\n> mac系统下按control + command + 空格\n\n\n# 图片工具\n\n * tinypng图片压缩 压缩png很有用\n * 微图 浏览器端图片压缩,不会上传图片到服务器\n * squoosh 谷歌出品在线免费图片压缩工具(jpg、png等,压缩效果比tinypng稍好)\n * waifu2x 通过卷积网络放大图片\n * vectormagic 转换矢量图\n * vectorizer 真正的 png 转 svg 神器\n * 在线ai图片处理 黑白修复、无损放大、动漫化、铅笔画等。\n * remove ai抠图\n * backgroundremover 又一个抠图的\n\n\n# 思维导图\n\n * processon在线作图 流程图、思维导图、原型图等\n * 百度脑图 思维导图\n * plectica 绘制知识图谱\n\n\n# css\n\n * 各种css生成器和js代码片段\n\n * css tricks css技巧收集与演示\n * css生成器\n * css渐变生成器\n * css3-box shadow(阴影)\n * 贝塞尔曲线生成器\n * 花纹背景生成器\n * 花纹背景-pattern.css\n * 3d字体\n * css-tricks css技巧文章\n * you-need-to-know-css css的各种demo,很全\n * animista css动画可视化工具,复制代码就能用\n * navnav 各种炫酷的css动画组件\n\n\n# cdn加速\n\n * jsdelivr 国外的一家优秀的公共 cdn 服务提供商\n * unpkg cdn 服务\n\n\n# 网站托管\n\n * vercel 好用的网站托管服务\n\n\n# 正则\n\n * 正则可视化\n * ihateregex 正则搜索,细节做得很好\n * 正则迷你书 学习正则的小手册\n\n\n# 其他\n\n * linux命令手册\n * carbon代码图片生成器 生成好看的代码图片\n\n\n# 设计\n\n * 创造师导航\n * 设计师网址导航\n * remove ai抠图,抠图算法很厉害\n * manypixels 插画\n * undraw 插画\n * storytale 插画,种类丰富,包含3d插画\n * uimovement 能从这个网站找到不少动画交互的灵感\n * awwwards是一个一个专门为设计精美的网站以及富有创意的网站颁奖的网站\n * dribbble 经常能在上面找到很多有创意好看的 gif 或者图片\n * behance dribbble 是设计师的微博,behance 是设计师的博客\n * logojoy 使用 ai 做 logo 的网站,做出来的 logo 质量还不错。\n * brandmark 另一个在线制作 logo 网站\n * instant 又一个 logo 制作网站\n * namecheap又一个 logo 制作网站\n * logo-maker 又一个 logo 制作网站 这个更简单点 就是选模板之后微调\n * coolors 帮你在线配色的网站 你能找到不少配色灵感\n * colorhunt 另一个配色网站\n * uigradients 渐变色网站\n * designcap 在线海报设计\n * flat ui 色表 flat ui 色表\n * 0to255 颜色梯度\n * ikonate 提供免费的图标 icons\n * remixicon 又一个提供免费图标 icons\n * feather 免费的 icons\n * nord 北欧性冷淡风主题配色\n * unsplash 提供免费的高清图片\n * pexels 提供免费的高清图片\n * colorkitty 从你的图片中提取配色\n * design.youzan 有赞设计原则\n * iconfont 阿里巴巴矢量图标库\n * undraw 免费的矢量插画\n * icomoon 矢量图标库\n * cssicon 所有的 icon 都是纯 css 画的 缺点:icon 不够多\n * css triangle generator 帮你快速用 css 做出三角形\n * clippy 在线帮你使用 css clip-path 做出各种形状的图形\n * lorem picsum 提供免费的占位图\n * canva 可画 生成插画、封面、海报、头像等\n * 404页 404页素材\n * collectui 按功能组件分类的设计图\n * smartmockups 产品模板生成工具\n\n\n# 图库\n\n * uigradients 渐变色生成工具\n * freepik banner 图库\n * 觅元素一天免费下载十张 psd(免抠元素)\n * 搞定设计 可以抠图\n * vectorizer 真正的 png 转 svg 神器\n * 站酷 国内优秀的设计作品展示\n * 花瓣\n * 虎克 ps 学习教程\n * betheme\n * ui 中国\n * wallhaven 壁纸网站-\n\n\n# 3d\n\n * sketchfab 3d模型\n\n\n# 交互\n\n * 微交互 里面收集了市面上很多很好的微交互例子 值得学习\n\n * little big details 同上,一个国外微交互汇集网站\n\n * cruip 登录页的各种页面设计,可以免费下载模板\n\n * comixify 一个波兰团队做了非常好玩的工具,可以把视频自动转成漫画,上图是他们提供的 demo,效果很棒。\n\n * taiko-web 太鼓达人网页版 只能说很 6\n\n\n# 有趣\n\n * 电脑恶搞 收集了一些恶搞小网页,比如xp系统蓝屏、黑客界面等\n * neocities 上面托管了很多有趣的网站\n * 奇趣网站收藏家 收藏了很多有趣的网站\n * fc在线模拟器(小霸王游戏机) 童年回忆\n * 帮你百度一下 可以 点我测试一下-\n * 国际版 同帮我百度一下-点我测试一下-\n * url 地址播放 emojis 动画 在地址栏里面播放 emoji\n * can't unsee 强烈建议前端、客户端、ui 开发的同学玩下,检查一下自己对设计稿的敏感度怎么样\n * ggtalk 平时一直在听的一个技术博客\n * awesome-comment 里面收集了很多有趣的代码注释\n * text-img 都将图片转化为 ascii 用来写注释\n * ascii video 使用ascii编码生成视频动画\n * weird-fonts 将普通字母转化为 特殊 unicode\n * snake 在地址栏里面玩贪吃蛇\n * zero-width-lib 利用零宽度字符实现 隐形水印、加密信息分享、逃脱词匹配,很有创意\n * abbreviations 查看一个简写是什么意思的网站\n * magi ai 搜索神器,超屌\n * 在线取名 解决取名难问题,超多名字生成\n\n\n# 生成器\n\n * 卡通头像生成器 上传真人头像生成卡通头像\n\n * artbreeder 动漫图生成真人图像\n\n * 声音生成器 生成下雨、咖啡厅、海浪、火车等声音,可几种声音合成\n\n * 诺基亚短信图片生成器\n\n * 到账语音生成器 支付宝到账1亿元\n\n\n# 元宇宙\n\n符合元宇宙特征的几个网站,允许用户拥有虚拟土地,在上面构建自己的世界:\n\n * 沙盒\n * 梦境空间\n * decentraland\n\n\n# 教程\n\n * npx 教你怎么合理的使用 npx\n * hacksplaining 网络安全学习网站\n * mobile-web-best-practice 移动 web 最佳实践\n\n\n# 产品\n\n * product hunt 好产品推荐\n\n * 产品大牛 什么有很多完整的产品原型可以借鉴\n\n * 磨刀 快速出 ui 原型\n\n\n# 实用\n\n * webden 在线网页编辑器,轻便快捷\n * browser-update 浏览器版本更新提示插件\n * typeform 一个国外的在线调查问卷网站\n * videofk videofk 视频在线解析下载\n * 全历史 历史内容聚合网站\n * uzerme 云端办公工具\n * sobooks 强大的电子书资源网站\n * 稿定设计 键式设计工具+智能抠图\n * 大力盘 百度网盘搜索\n * enfi 下载器 不限速下载器\n * 来画视频 像做 ppt 一样做短视频\n * arkie 海报制作工具\n * 优品 ppt\n * 比格 ppt\n * 高清免费图片\n * 高清免费图片 2\n * shapedivider 生成波浪分隔线\n * notion 知识库、快速笔记、tasklist、日记、读书清单,各种类型,应有尽有\n\n\n# talk\n\n * peerigon-talks 收集了不少有意思的 talks\n\n\n# 算法\n\n * leetcode 用 js 刷 leetcode\n\n\n# nginx\n\n * nginx 可视化配置工具\n\n\n# 生活\n\n * ventusky 风雨气温图", + "excerpt": "

# 个人收藏夹

\n

# 推荐

\n\n

\n\n

\n

关注公众号[有趣研究社],回复前端资源,获取 前端学习资料

\n
", + "headersStr": "推荐 文档 社区 社区互动 技巧 博客 电子书 优秀文章 视频 Github 评论系统 前端小工具 代码编辑 Emoji表情 图片工具 思维导图 CSS CDN加速 网站托管 正则 其他 设计 图库 3D 交互 有趣 生成器 元宇宙 教程 产品 实用 Talk 算法 nginx 生活", + "content": "# 个人收藏夹\n\n\n# 推荐\n\n * panjiachen by 花裤衩\n * 编程导航 by 程序员鱼皮\n * 编程自学之路 by 程序羊\n * 前端知识图谱+B站资源整合 by 技术胖\n * 大圣编程自学网 by 大圣\n * 开发者武器库\n * 工具大全\n\n\n\n关注公众号[有趣研究社],回复前端资源,获取 前端学习资料\n\n\n# 文档\n\n * MDN | MDN-JS标准内置对象 Web技术权威文档\n * DevDocs Web 开发技术文档,非常不错的学习手册!\n * 现代JavaScript教程 以最新标准为基准的JS教程\n * ES5教程 阮一峰的JS教程\n * ES6教程 阮一峰的ES6教程\n * Bash 脚本教程 阮一峰编写\n * ECMA ECMA官网\n * 菜鸟教程 涵盖多种语言的初级教程\n * 腾讯云开发者手册\n\n\n# 社区\n\n * Github 程序员同性交友社区\n * 掘金 一个帮助开发者成长的社区\n * 简书 有很多频道的创作社区\n * 思否 解决技术问题的社区\n * stack overflow 同上,外网的\n * InfoQ 促进软件开发及相关领域知识与创新的传播\n * V2EX 创意工作者们的社区\n * 鱼塘热榜 划水网站,收集了很多网站,当天热门文章\n * 码力全开资源库 很全很强大,独立开发者/设计干货/优质利器/工具资源...\n\n\n# 社区互动\n\n * gitter\n * 兔小巢\n\n\n# 技巧\n\n * Google 趋势 查看某项技术或关键字的热度趋势,可用于分析某项技术的发展前景,或对比某两项技术的热度。\n * 百度指数 同上,但百度的数据仅限国内。\n\n\n# 博客\n\n * 阮一峰的网络日志\n * samanthaming 对前端小知识点的总结,并为每个知识点制作精美的小卡片。\n\n\n# 电子书\n\n * 高教书苑 高等教育出版社的书籍,包含多种学科。\n * SoBooks 免费的电子书资源网站\n\n\n# 优秀文章\n\n * 我做系统架构的一些原则 作者对系统架构的方法论总结\n * 灵活运用CSS开发技巧\n * 防御性CSS\n\n\n# 视频\n\n * bilibili B站,上面很多免费教学视频\n * 慕课网 实战视频教程\n * 妙味课堂 比较系统的前端入门视频教程\n * 中国大学MOOC 涵盖计算机、外语、心理学等专业免费课程\n * egghead 质量还不错的短视频教程,外网\n\n\n# Github\n\n * Repobeats 生成仓库的动态数据统计图\n\n * github 短域名服务\n * shields 徽章图标\n * followers 全球排名\n * star-history 展示一个项目 Stars 增长曲线\n\n\n# 评论系统\n\n * giscus 由 GitHub Discussions 驱动的评论系统\n\n\n# 前端小工具\n\n * Can I use 查看属性和方法的兼容性\n * 30 seconds of code 收集了许多有用的代码小片段\n\n\n# 代码编辑\n\n * codepen 在线代码编辑与演示\n * codesandbox 内嵌VSCode的在线IDE\n\n\n# Emoji表情\n\n * emoji表情\n * emoji表情备忘录\n * 根据文本匹配emoji\n * gitmoji 通过 emoji 表达 git 的操作内容\n\n> 在任意输入框快速打开emoji表情方法:\n> Windows系统下按Win + .\n> Mac系统下按Control + Command + 空格\n\n\n# 图片工具\n\n * tinypng图片压缩 压缩png很有用\n * 微图 浏览器端图片压缩,不会上传图片到服务器\n * Squoosh 谷歌出品在线免费图片压缩工具(jpg、png等,压缩效果比tinypng稍好)\n * waifu2x 通过卷积网络放大图片\n * vectormagic 转换矢量图\n * vectorizer 真正的 png 转 svg 神器\n * 在线AI图片处理 黑白修复、无损放大、动漫化、铅笔画等。\n * remove AI抠图\n * backgroundremover 又一个抠图的\n\n\n# 思维导图\n\n * processon在线作图 流程图、思维导图、原型图等\n * 百度脑图 思维导图\n * plectica 绘制知识图谱\n\n\n# CSS\n\n * 各种CSS生成器和JS代码片段\n\n * CSS Tricks CSS技巧收集与演示\n * CSS生成器\n * CSS渐变生成器\n * CSS3-Box Shadow(阴影)\n * 贝塞尔曲线生成器\n * 花纹背景生成器\n * 花纹背景-pattern.css\n * 3D字体\n * css-tricks css技巧文章\n * You-need-to-know-css CSS的各种DEMO,很全\n * animista CSS动画可视化工具,复制代码就能用\n * navnav 各种炫酷的CSS动画组件\n\n\n# CDN加速\n\n * jsDelivr 国外的一家优秀的公共 CDN 服务提供商\n * unpkg cdn 服务\n\n\n# 网站托管\n\n * vercel 好用的网站托管服务\n\n\n# 正则\n\n * 正则可视化\n * iHateRegex 正则搜索,细节做得很好\n * 正则迷你书 学习正则的小手册\n\n\n# 其他\n\n * Linux命令手册\n * carbon代码图片生成器 生成好看的代码图片\n\n\n# 设计\n\n * 创造师导航\n * 设计师网址导航\n * remove AI抠图,抠图算法很厉害\n * Manypixels 插画\n * Undraw 插画\n * storytale 插画,种类丰富,包含3D插画\n * uimovement 能从这个网站找到不少动画交互的灵感\n * awwwards是一个一个专门为设计精美的网站以及富有创意的网站颁奖的网站\n * dribbble 经常能在上面找到很多有创意好看的 gif 或者图片\n * Bēhance dribbble 是设计师的微博,Bēhance 是设计师的博客\n * Logojoy 使用 ai 做 logo 的网站,做出来的 logo 质量还不错。\n * brandmark 另一个在线制作 logo 网站\n * instant 又一个 logo 制作网站\n * namecheap又一个 logo 制作网站\n * logo-maker 又一个 logo 制作网站 这个更简单点 就是选模板之后微调\n * coolors 帮你在线配色的网站 你能找到不少配色灵感\n * colorhunt 另一个配色网站\n * uigradients 渐变色网站\n * designcap 在线海报设计\n * Flat UI 色表 Flat UI 色表\n * 0to255 颜色梯度\n * Ikonate 提供免费的图标 icons\n * remixicon 又一个提供免费图标 icons\n * feather 免费的 icons\n * nord 北欧性冷淡风主题配色\n * Unsplash 提供免费的高清图片\n * Pexels 提供免费的高清图片\n * colorkitty 从你的图片中提取配色\n * design.youzan 有赞设计原则\n * iconfont 阿里巴巴矢量图标库\n * undraw 免费的矢量插画\n * icomoon 矢量图标库\n * cssicon 所有的 icon 都是纯 css 画的 缺点:icon 不够多\n * CSS triangle generator 帮你快速用 css 做出三角形\n * clippy 在线帮你使用 css clip-path 做出各种形状的图形\n * Lorem Picsum 提供免费的占位图\n * Canva 可画 生成插画、封面、海报、头像等\n * 404页 404页素材\n * collectui 按功能组件分类的设计图\n * smartmockups 产品模板生成工具\n\n\n# 图库\n\n * uigradients 渐变色生成工具\n * freepik banner 图库\n * 觅元素一天免费下载十张 psd(免抠元素)\n * 搞定设计 可以抠图\n * vectorizer 真正的 png 转 svg 神器\n * 站酷 国内优秀的设计作品展示\n * 花瓣\n * 虎克 ps 学习教程\n * beTheme\n * UI 中国\n * wallhaven 壁纸网站-\n\n\n# 3D\n\n * sketchfab 3D模型\n\n\n# 交互\n\n * 微交互 里面收集了市面上很多很好的微交互例子 值得学习\n\n * Little Big Details 同上,一个国外微交互汇集网站\n\n * cruip 登录页的各种页面设计,可以免费下载模板\n\n * Comixify 一个波兰团队做了非常好玩的工具,可以把视频自动转成漫画,上图是他们提供的 demo,效果很棒。\n\n * taiko-web 太鼓达人网页版 只能说很 6\n\n\n# 有趣\n\n * 电脑恶搞 收集了一些恶搞小网页,比如xp系统蓝屏、黑客界面等\n * neocities 上面托管了很多有趣的网站\n * 奇趣网站收藏家 收藏了很多有趣的网站\n * FC在线模拟器(小霸王游戏机) 童年回忆\n * 帮你百度一下 可以 点我测试一下-\n * 国际版 同帮我百度一下-点我测试一下-\n * URL 地址播放 Emojis 动画 在地址栏里面播放 emoji\n * Can't Unsee 强烈建议前端、客户端、UI 开发的同学玩下,检查一下自己对设计稿的敏感度怎么样\n * ggtalk 平时一直在听的一个技术博客\n * awesome-comment 里面收集了很多有趣的代码注释\n * text-img 都将图片转化为 ascii 用来写注释\n * ascii video 使用ascii编码生成视频动画\n * weird-fonts 将普通字母转化为 特殊 unicode\n * snake 在地址栏里面玩贪吃蛇\n * zero-width-lib 利用零宽度字符实现 隐形水印、加密信息分享、逃脱词匹配,很有创意\n * abbreviations 查看一个简写是什么意思的网站\n * magi ai 搜索神器,超屌\n * 在线取名 解决取名难问题,超多名字生成\n\n\n# 生成器\n\n * 卡通头像生成器 上传真人头像生成卡通头像\n\n * artbreeder 动漫图生成真人图像\n\n * 声音生成器 生成下雨、咖啡厅、海浪、火车等声音,可几种声音合成\n\n * 诺基亚短信图片生成器\n\n * 到账语音生成器 支付宝到账1亿元\n\n\n# 元宇宙\n\n符合元宇宙特征的几个网站,允许用户拥有虚拟土地,在上面构建自己的世界:\n\n * 沙盒\n * 梦境空间\n * Decentraland\n\n\n# 教程\n\n * npx 教你怎么合理的使用 npx\n * hacksplaining 网络安全学习网站\n * mobile-web-best-practice 移动 web 最佳实践\n\n\n# 产品\n\n * Product Hunt 好产品推荐\n\n * 产品大牛 什么有很多完整的产品原型可以借鉴\n\n * 磨刀 快速出 ui 原型\n\n\n# 实用\n\n * webden 在线网页编辑器,轻便快捷\n * browser-update 浏览器版本更新提示插件\n * typeform 一个国外的在线调查问卷网站\n * VideoFk VideoFk 视频在线解析下载\n * 全历史 历史内容聚合网站\n * UzerMe 云端办公工具\n * SoBooks 强大的电子书资源网站\n * 稿定设计 键式设计工具+智能抠图\n * 大力盘 百度网盘搜索\n * ENFI 下载器 不限速下载器\n * 来画视频 像做 PPT 一样做短视频\n * Arkie 海报制作工具\n * 优品 PPT\n * 比格 PPT\n * 高清免费图片\n * 高清免费图片 2\n * shapedivider 生成波浪分隔线\n * Notion 知识库、快速笔记、TaskList、日记、读书清单,各种类型,应有尽有\n\n\n# Talk\n\n * peerigon-talks 收集了不少有意思的 talks\n\n\n# 算法\n\n * leetcode 用 js 刷 leetcode\n\n\n# nginx\n\n * nginx 可视化配置工具\n\n\n# 生活\n\n * Ventusky 风雨气温图", + "normalizedContent": "# 个人收藏夹\n\n\n# 推荐\n\n * panjiachen by 花裤衩\n * 编程导航 by 程序员鱼皮\n * 编程自学之路 by 程序羊\n * 前端知识图谱+b站资源整合 by 技术胖\n * 大圣编程自学网 by 大圣\n * 开发者武器库\n * 工具大全\n\n\n\n关注公众号[有趣研究社],回复前端资源,获取 前端学习资料\n\n\n# 文档\n\n * mdn | mdn-js标准内置对象 web技术权威文档\n * devdocs web 开发技术文档,非常不错的学习手册!\n * 现代javascript教程 以最新标准为基准的js教程\n * es5教程 阮一峰的js教程\n * es6教程 阮一峰的es6教程\n * bash 脚本教程 阮一峰编写\n * ecma ecma官网\n * 菜鸟教程 涵盖多种语言的初级教程\n * 腾讯云开发者手册\n\n\n# 社区\n\n * github 程序员同性交友社区\n * 掘金 一个帮助开发者成长的社区\n * 简书 有很多频道的创作社区\n * 思否 解决技术问题的社区\n * stack overflow 同上,外网的\n * infoq 促进软件开发及相关领域知识与创新的传播\n * v2ex 创意工作者们的社区\n * 鱼塘热榜 划水网站,收集了很多网站,当天热门文章\n * 码力全开资源库 很全很强大,独立开发者/设计干货/优质利器/工具资源...\n\n\n# 社区互动\n\n * gitter\n * 兔小巢\n\n\n# 技巧\n\n * google 趋势 查看某项技术或关键字的热度趋势,可用于分析某项技术的发展前景,或对比某两项技术的热度。\n * 百度指数 同上,但百度的数据仅限国内。\n\n\n# 博客\n\n * 阮一峰的网络日志\n * samanthaming 对前端小知识点的总结,并为每个知识点制作精美的小卡片。\n\n\n# 电子书\n\n * 高教书苑 高等教育出版社的书籍,包含多种学科。\n * sobooks 免费的电子书资源网站\n\n\n# 优秀文章\n\n * 我做系统架构的一些原则 作者对系统架构的方法论总结\n * 灵活运用css开发技巧\n * 防御性css\n\n\n# 视频\n\n * bilibili b站,上面很多免费教学视频\n * 慕课网 实战视频教程\n * 妙味课堂 比较系统的前端入门视频教程\n * 中国大学mooc 涵盖计算机、外语、心理学等专业免费课程\n * egghead 质量还不错的短视频教程,外网\n\n\n# github\n\n * repobeats 生成仓库的动态数据统计图\n\n * github 短域名服务\n * shields 徽章图标\n * followers 全球排名\n * star-history 展示一个项目 stars 增长曲线\n\n\n# 评论系统\n\n * giscus 由 github discussions 驱动的评论系统\n\n\n# 前端小工具\n\n * can i use 查看属性和方法的兼容性\n * 30 seconds of code 收集了许多有用的代码小片段\n\n\n# 代码编辑\n\n * codepen 在线代码编辑与演示\n * codesandbox 内嵌vscode的在线ide\n\n\n# emoji表情\n\n * emoji表情\n * emoji表情备忘录\n * 根据文本匹配emoji\n * gitmoji 通过 emoji 表达 git 的操作内容\n\n> 在任意输入框快速打开emoji表情方法:\n> windows系统下按win + .\n> mac系统下按control + command + 空格\n\n\n# 图片工具\n\n * tinypng图片压缩 压缩png很有用\n * 微图 浏览器端图片压缩,不会上传图片到服务器\n * squoosh 谷歌出品在线免费图片压缩工具(jpg、png等,压缩效果比tinypng稍好)\n * waifu2x 通过卷积网络放大图片\n * vectormagic 转换矢量图\n * vectorizer 真正的 png 转 svg 神器\n * 在线ai图片处理 黑白修复、无损放大、动漫化、铅笔画等。\n * remove ai抠图\n * backgroundremover 又一个抠图的\n\n\n# 思维导图\n\n * processon在线作图 流程图、思维导图、原型图等\n * 百度脑图 思维导图\n * plectica 绘制知识图谱\n\n\n# css\n\n * 各种css生成器和js代码片段\n\n * css tricks css技巧收集与演示\n * css生成器\n * css渐变生成器\n * css3-box shadow(阴影)\n * 贝塞尔曲线生成器\n * 花纹背景生成器\n * 花纹背景-pattern.css\n * 3d字体\n * css-tricks css技巧文章\n * you-need-to-know-css css的各种demo,很全\n * animista css动画可视化工具,复制代码就能用\n * navnav 各种炫酷的css动画组件\n\n\n# cdn加速\n\n * jsdelivr 国外的一家优秀的公共 cdn 服务提供商\n * unpkg cdn 服务\n\n\n# 网站托管\n\n * vercel 好用的网站托管服务\n\n\n# 正则\n\n * 正则可视化\n * ihateregex 正则搜索,细节做得很好\n * 正则迷你书 学习正则的小手册\n\n\n# 其他\n\n * linux命令手册\n * carbon代码图片生成器 生成好看的代码图片\n\n\n# 设计\n\n * 创造师导航\n * 设计师网址导航\n * remove ai抠图,抠图算法很厉害\n * manypixels 插画\n * undraw 插画\n * storytale 插画,种类丰富,包含3d插画\n * uimovement 能从这个网站找到不少动画交互的灵感\n * awwwards是一个一个专门为设计精美的网站以及富有创意的网站颁奖的网站\n * dribbble 经常能在上面找到很多有创意好看的 gif 或者图片\n * behance dribbble 是设计师的微博,behance 是设计师的博客\n * logojoy 使用 ai 做 logo 的网站,做出来的 logo 质量还不错。\n * brandmark 另一个在线制作 logo 网站\n * instant 又一个 logo 制作网站\n * namecheap又一个 logo 制作网站\n * logo-maker 又一个 logo 制作网站 这个更简单点 就是选模板之后微调\n * coolors 帮你在线配色的网站 你能找到不少配色灵感\n * colorhunt 另一个配色网站\n * uigradients 渐变色网站\n * designcap 在线海报设计\n * flat ui 色表 flat ui 色表\n * 0to255 颜色梯度\n * ikonate 提供免费的图标 icons\n * remixicon 又一个提供免费图标 icons\n * feather 免费的 icons\n * nord 北欧性冷淡风主题配色\n * unsplash 提供免费的高清图片\n * pexels 提供免费的高清图片\n * colorkitty 从你的图片中提取配色\n * design.youzan 有赞设计原则\n * iconfont 阿里巴巴矢量图标库\n * undraw 免费的矢量插画\n * icomoon 矢量图标库\n * cssicon 所有的 icon 都是纯 css 画的 缺点:icon 不够多\n * css triangle generator 帮你快速用 css 做出三角形\n * clippy 在线帮你使用 css clip-path 做出各种形状的图形\n * lorem picsum 提供免费的占位图\n * canva 可画 生成插画、封面、海报、头像等\n * 404页 404页素材\n * collectui 按功能组件分类的设计图\n * smartmockups 产品模板生成工具\n\n\n# 图库\n\n * uigradients 渐变色生成工具\n * freepik banner 图库\n * 觅元素一天免费下载十张 psd(免抠元素)\n * 搞定设计 可以抠图\n * vectorizer 真正的 png 转 svg 神器\n * 站酷 国内优秀的设计作品展示\n * 花瓣\n * 虎克 ps 学习教程\n * betheme\n * ui 中国\n * wallhaven 壁纸网站-\n\n\n# 3d\n\n * sketchfab 3d模型\n\n\n# 交互\n\n * 微交互 里面收集了市面上很多很好的微交互例子 值得学习\n\n * little big details 同上,一个国外微交互汇集网站\n\n * cruip 登录页的各种页面设计,可以免费下载模板\n\n * comixify 一个波兰团队做了非常好玩的工具,可以把视频自动转成漫画,上图是他们提供的 demo,效果很棒。\n\n * taiko-web 太鼓达人网页版 只能说很 6\n\n\n# 有趣\n\n * 电脑恶搞 收集了一些恶搞小网页,比如xp系统蓝屏、黑客界面等\n * neocities 上面托管了很多有趣的网站\n * 奇趣网站收藏家 收藏了很多有趣的网站\n * fc在线模拟器(小霸王游戏机) 童年回忆\n * 帮你百度一下 可以 点我测试一下-\n * 国际版 同帮我百度一下-点我测试一下-\n * url 地址播放 emojis 动画 在地址栏里面播放 emoji\n * can't unsee 强烈建议前端、客户端、ui 开发的同学玩下,检查一下自己对设计稿的敏感度怎么样\n * ggtalk 平时一直在听的一个技术博客\n * awesome-comment 里面收集了很多有趣的代码注释\n * text-img 都将图片转化为 ascii 用来写注释\n * ascii video 使用ascii编码生成视频动画\n * weird-fonts 将普通字母转化为 特殊 unicode\n * snake 在地址栏里面玩贪吃蛇\n * zero-width-lib 利用零宽度字符实现 隐形水印、加密信息分享、逃脱词匹配,很有创意\n * abbreviations 查看一个简写是什么意思的网站\n * magi ai 搜索神器,超屌\n * 在线取名 解决取名难问题,超多名字生成\n\n\n# 生成器\n\n * 卡通头像生成器 上传真人头像生成卡通头像\n\n * artbreeder 动漫图生成真人图像\n\n * 声音生成器 生成下雨、咖啡厅、海浪、火车等声音,可几种声音合成\n\n * 诺基亚短信图片生成器\n\n * 到账语音生成器 支付宝到账1亿元\n\n\n# 元宇宙\n\n符合元宇宙特征的几个网站,允许用户拥有虚拟土地,在上面构建自己的世界:\n\n * 沙盒\n * 梦境空间\n * decentraland\n\n\n# 教程\n\n * npx 教你怎么合理的使用 npx\n * hacksplaining 网络安全学习网站\n * mobile-web-best-practice 移动 web 最佳实践\n\n\n# 产品\n\n * product hunt 好产品推荐\n\n * 产品大牛 什么有很多完整的产品原型可以借鉴\n\n * 磨刀 快速出 ui 原型\n\n\n# 实用\n\n * webden 在线网页编辑器,轻便快捷\n * browser-update 浏览器版本更新提示插件\n * typeform 一个国外的在线调查问卷网站\n * videofk videofk 视频在线解析下载\n * 全历史 历史内容聚合网站\n * uzerme 云端办公工具\n * sobooks 强大的电子书资源网站\n * 稿定设计 键式设计工具+智能抠图\n * 大力盘 百度网盘搜索\n * enfi 下载器 不限速下载器\n * 来画视频 像做 ppt 一样做短视频\n * arkie 海报制作工具\n * 优品 ppt\n * 比格 ppt\n * 高清免费图片\n * 高清免费图片 2\n * shapedivider 生成波浪分隔线\n * notion 知识库、快速笔记、tasklist、日记、读书清单,各种类型,应有尽有\n\n\n# talk\n\n * peerigon-talks 收集了不少有意思的 talks\n\n\n# 算法\n\n * leetcode 用 js 刷 leetcode\n\n\n# nginx\n\n * nginx 可视化配置工具\n\n\n# 生活\n\n * ventusky 风雨气温图", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/06/20, 22:47:41", + "lastUpdatedTimestamp": 1655736461000 + }, + { + "title": "常用的前端轮子", + "frontmatter": { + "title": "常用的前端轮子", + "date": "2021-10-09T19:42:02.000Z", + "permalink": "/pages/47cf96/", + "article": false + }, + "regularPath": "/@pages/20.%E6%94%B6%E8%97%8F%E5%A4%B9/02.%E5%B8%B8%E7%94%A8%E7%9A%84%E5%89%8D%E7%AB%AF%E8%BD%AE%E5%AD%90.html", + "relativePath": "@pages/20.收藏夹/02.常用的前端轮子.md", + "key": "v-09c150e6", + "path": "/pages/47cf96/", + "headers": [ + { + "level": 2, + "title": "React UI 组件库", + "slug": "react-ui-组件库", + "normalizedTitle": "react ui 组件库", + "charIndex": 2 + }, + { + "level": 2, + "title": "Vue UI组件库", + "slug": "vue-ui组件库", + "normalizedTitle": "vue ui组件库", + "charIndex": 68 + }, + { + "level": 2, + "title": "常用效果组件", + "slug": "常用效果组件", + "normalizedTitle": "常用效果组件", + "charIndex": 124 + }, + { + "level": 2, + "title": "工具类", + "slug": "工具类", + "normalizedTitle": "工具类", + "charIndex": 201 + }, + { + "level": 2, + "title": "Vue工具类", + "slug": "vue工具类", + "normalizedTitle": "vue工具类", + "charIndex": 331 + }, + { + "level": 2, + "title": "其他", + "slug": "其他", + "normalizedTitle": "其他", + "charIndex": 469 + } + ], + "headersStr": "React UI 组件库 Vue UI组件库 常用效果组件 工具类 Vue工具类 其他", + "content": "# React UI 组件库\n\n * Ant Design\n * React Bootstrap\n * MATERIAL-UI\n\n\n# Vue UI组件库\n\n * Element UI PC端\n * Vant 移动端\n * View UI\n\n\n# 常用效果组件\n\n * Animate.css 动画库\n * Swiper 轮播组件\n * mescroll 下拉刷新和上拉加载框架-基于原生JS\n\n\n# 工具类\n\n * Lodash.js\n * Day.js 处理日期\n * Timeago.js 相对时间,如N小时前\n * Echarts 百度图表\n * Meditor.md 开源在线 Markdown 编辑器\n * validator.js 验证库\n\n\n# Vue工具类\n\n * vue-draggable 基于Sortable.js实现的vue拖拽插件\n * vue-qr 文本转二维码\n * vue-cropper 图片裁剪插件\n * vue-lazyload 懒加载\n * vue-simple-upload 上传组件\n\n\n# 其他\n\n * H5带笔锋手写签名,支持PC端和移动端", + "normalizedContent": "# react ui 组件库\n\n * ant design\n * react bootstrap\n * material-ui\n\n\n# vue ui组件库\n\n * element ui pc端\n * vant 移动端\n * view ui\n\n\n# 常用效果组件\n\n * animate.css 动画库\n * swiper 轮播组件\n * mescroll 下拉刷新和上拉加载框架-基于原生js\n\n\n# 工具类\n\n * lodash.js\n * day.js 处理日期\n * timeago.js 相对时间,如n小时前\n * echarts 百度图表\n * meditor.md 开源在线 markdown 编辑器\n * validator.js 验证库\n\n\n# vue工具类\n\n * vue-draggable 基于sortable.js实现的vue拖拽插件\n * vue-qr 文本转二维码\n * vue-cropper 图片裁剪插件\n * vue-lazyload 懒加载\n * vue-simple-upload 上传组件\n\n\n# 其他\n\n * h5带笔锋手写签名,支持pc端和移动端", "charsets": { "cjk": true }, "lastUpdated": "2022/06/20, 22:47:41", "lastUpdatedTimestamp": 1655736461000 }, + { + "title": "归档", + "frontmatter": { + "archivesPage": true, + "title": "归档", + "permalink": "/archives/", + "article": false + }, + "regularPath": "/@pages/archivesPage.html", + "relativePath": "@pages/archivesPage.md", + "key": "v-48f7eec0", + "path": "/archives/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, + { + "title": "分类", + "frontmatter": { + "categoriesPage": true, + "title": "分类", + "permalink": "/categories/", + "article": false + }, + "regularPath": "/@pages/categoriesPage.html", + "relativePath": "@pages/categoriesPage.md", + "key": "v-5f8dfe80", + "path": "/categories/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, + { + "title": "标签", + "frontmatter": { + "tagsPage": true, + "title": "标签", + "permalink": "/tags/", + "article": false + }, + "regularPath": "/@pages/tagsPage.html", + "relativePath": "@pages/tagsPage.md", + "key": "v-64f9b920", + "path": "/tags/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, { "title": "你知道的越多,不知道的也就越多", "frontmatter": { @@ -6966,6 +7425,38 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, + { + "title": "拥抱生活,拥抱快乐", + "frontmatter": { + "title": "拥抱生活,拥抱快乐", + "date": "2020-06-26T20:40:38.000Z", + "permalink": "/pages/cd8bde/", + "sidebar": "auto", + "categories": [ + "随笔" + ], + "tags": [ + "鸡汤" + ], + "author": { + "name": "xugaoyi", + "link": "https://github.com/xugaoyi" + } + }, + "regularPath": "/_posts/%E9%9A%8F%E7%AC%94/%E6%8B%A5%E6%8A%B1%E7%94%9F%E6%B4%BB%EF%BC%8C%E6%8B%A5%E6%8A%B1%E5%BF%AB%E4%B9%90.html", + "relativePath": "_posts/随笔/拥抱生活,拥抱快乐.md", + "key": "v-7d77d69c", + "path": "/pages/cd8bde/", + "excerpt": "

生活在后现代的今天,很多人都有一种虚无感,认为人生没有意义。但是,人生不可能没有意义,因为当你认为没有意义的时候,一定有一个与之相对应的概念叫有意义。

\n", + "headersStr": null, + "content": "生活在后现代的今天,很多人都有一种虚无感,认为人生没有意义。但是,人生不可能没有意义,因为当你认为没有意义的时候,一定有一个与之相对应的概念叫有意义。\n\n当你怀疑人生没有意义时,难道怀疑本身不值得怀疑吗?\n\n不要任由你内心的虚无感蔓延,我们需要去拥抱真实的生活。\n\n所有真实的快乐,都离不开艰辛的努力,无论是金榜题名的快乐,还是事业成功的喜悦,甚至包括洞房花烛的激动。所有真实的快乐,都需要长久的铺垫与努力,没有辛勤的汗水,快乐也就不再真实。\n\n如果快乐触手可及,这种廉价的快乐也就不值得珍惜,随时都可能抛弃。因此,对于年轻人而言,一个重要的功课就是学会去节制欲望。\n\n所有通过捷径所带来的快乐,都是廉价的,以至于所有追求都变得毫无意义,人生就了无生趣。我们需要在每天真实的努力中去拥抱生活,追寻真实的快乐。\n\n\n\n> 文章摘录自:B站视频《罗翔说刑法》,链接https://b23.tv/K8ulrE", + "normalizedContent": "生活在后现代的今天,很多人都有一种虚无感,认为人生没有意义。但是,人生不可能没有意义,因为当你认为没有意义的时候,一定有一个与之相对应的概念叫有意义。\n\n当你怀疑人生没有意义时,难道怀疑本身不值得怀疑吗?\n\n不要任由你内心的虚无感蔓延,我们需要去拥抱真实的生活。\n\n所有真实的快乐,都离不开艰辛的努力,无论是金榜题名的快乐,还是事业成功的喜悦,甚至包括洞房花烛的激动。所有真实的快乐,都需要长久的铺垫与努力,没有辛勤的汗水,快乐也就不再真实。\n\n如果快乐触手可及,这种廉价的快乐也就不值得珍惜,随时都可能抛弃。因此,对于年轻人而言,一个重要的功课就是学会去节制欲望。\n\n所有通过捷径所带来的快乐,都是廉价的,以至于所有追求都变得毫无意义,人生就了无生趣。我们需要在每天真实的努力中去拥抱生活,追寻真实的快乐。\n\n\n\n> 文章摘录自:b站视频《罗翔说刑法》,链接https://b23.tv/k8ulre", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, { "title": "我做了一个手写春联小网页,祝大家虎年暴富", "frontmatter": { @@ -7025,38 +7516,6 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, - { - "title": "拥抱生活,拥抱快乐", - "frontmatter": { - "title": "拥抱生活,拥抱快乐", - "date": "2020-06-26T20:40:38.000Z", - "permalink": "/pages/cd8bde/", - "sidebar": "auto", - "categories": [ - "随笔" - ], - "tags": [ - "鸡汤" - ], - "author": { - "name": "xugaoyi", - "link": "https://github.com/xugaoyi" - } - }, - "regularPath": "/_posts/%E9%9A%8F%E7%AC%94/%E6%8B%A5%E6%8A%B1%E7%94%9F%E6%B4%BB%EF%BC%8C%E6%8B%A5%E6%8A%B1%E5%BF%AB%E4%B9%90.html", - "relativePath": "_posts/随笔/拥抱生活,拥抱快乐.md", - "key": "v-7d77d69c", - "path": "/pages/cd8bde/", - "excerpt": "

生活在后现代的今天,很多人都有一种虚无感,认为人生没有意义。但是,人生不可能没有意义,因为当你认为没有意义的时候,一定有一个与之相对应的概念叫有意义。

\n", - "headersStr": null, - "content": "生活在后现代的今天,很多人都有一种虚无感,认为人生没有意义。但是,人生不可能没有意义,因为当你认为没有意义的时候,一定有一个与之相对应的概念叫有意义。\n\n当你怀疑人生没有意义时,难道怀疑本身不值得怀疑吗?\n\n不要任由你内心的虚无感蔓延,我们需要去拥抱真实的生活。\n\n所有真实的快乐,都离不开艰辛的努力,无论是金榜题名的快乐,还是事业成功的喜悦,甚至包括洞房花烛的激动。所有真实的快乐,都需要长久的铺垫与努力,没有辛勤的汗水,快乐也就不再真实。\n\n如果快乐触手可及,这种廉价的快乐也就不值得珍惜,随时都可能抛弃。因此,对于年轻人而言,一个重要的功课就是学会去节制欲望。\n\n所有通过捷径所带来的快乐,都是廉价的,以至于所有追求都变得毫无意义,人生就了无生趣。我们需要在每天真实的努力中去拥抱生活,追寻真实的快乐。\n\n\n\n> 文章摘录自:B站视频《罗翔说刑法》,链接https://b23.tv/K8ulrE", - "normalizedContent": "生活在后现代的今天,很多人都有一种虚无感,认为人生没有意义。但是,人生不可能没有意义,因为当你认为没有意义的时候,一定有一个与之相对应的概念叫有意义。\n\n当你怀疑人生没有意义时,难道怀疑本身不值得怀疑吗?\n\n不要任由你内心的虚无感蔓延,我们需要去拥抱真实的生活。\n\n所有真实的快乐,都离不开艰辛的努力,无论是金榜题名的快乐,还是事业成功的喜悦,甚至包括洞房花烛的激动。所有真实的快乐,都需要长久的铺垫与努力,没有辛勤的汗水,快乐也就不再真实。\n\n如果快乐触手可及,这种廉价的快乐也就不值得珍惜,随时都可能抛弃。因此,对于年轻人而言,一个重要的功课就是学会去节制欲望。\n\n所有通过捷径所带来的快乐,都是廉价的,以至于所有追求都变得毫无意义,人生就了无生趣。我们需要在每天真实的努力中去拥抱生活,追寻真实的快乐。\n\n\n\n> 文章摘录自:b站视频《罗翔说刑法》,链接https://b23.tv/k8ulre", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, { "title": "Home", "frontmatter": { @@ -7125,129 +7584,6 @@ export const siteData = { }, "lastUpdated": "2022/06/20, 22:47:41", "lastUpdatedTimestamp": 1655736461000 - }, - { - "title": "标签", - "frontmatter": { - "tagsPage": true, - "title": "标签", - "permalink": "/tags/", - "article": false - }, - "regularPath": "/@pages/tagsPage.html", - "relativePath": "@pages/tagsPage.md", - "key": "v-64f9b920", - "path": "/tags/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, - { - "title": "分类", - "frontmatter": { - "categoriesPage": true, - "title": "分类", - "permalink": "/categories/", - "article": false - }, - "regularPath": "/@pages/categoriesPage.html", - "relativePath": "@pages/categoriesPage.md", - "key": "v-5f8dfe80", - "path": "/categories/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, - { - "title": "从输入 URL 到展现页面的全过程", - "frontmatter": { - "title": "从输入 URL 到展现页面的全过程", - "date": "2022-06-23T21:10:44.000Z", - "permalink": "/pages/ab48ba/", - "categories": [ - "计算机基础", - "计算机网络" - ], - "tags": [ - null - ] - }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/020.%E4%BB%8E%E8%BE%93%E5%85%A5%20URL%20%E5%88%B0%E5%B1%95%E7%8E%B0%E9%A1%B5%E9%9D%A2%E7%9A%84%E5%85%A8%E8%BF%87%E7%A8%8B.html", - "relativePath": "02.计算机基础/10.计算机网络/020.从输入 URL 到展现页面的全过程.md", - "key": "v-6602e3da", - "path": "/pages/ab48ba/", - "headers": [ - { - "level": 2, - "title": "什么是IP地址", - "slug": "什么是ip地址", - "normalizedTitle": "什么是ip地址", - "charIndex": 459 - }, - { - "level": 2, - "title": "什么是DNS", - "slug": "什么是dns", - "normalizedTitle": "什么是dns", - "charIndex": 608 - }, - { - "level": 2, - "title": "DNS解析", - "slug": "dns解析-2", - "normalizedTitle": "dns解析", - "charIndex": 57 - }, - { - "level": 2, - "title": "DNS查询的两种方式:递归查询和迭代查询", - "slug": "dns查询的两种方式-递归查询和迭代查询", - "normalizedTitle": "dns查询的两种方式:递归查询和迭代查询", - "charIndex": 752 - }, - { - "level": 2, - "title": "DNS域名称空间的组织方式", - "slug": "dns域名称空间的组织方式", - "normalizedTitle": "dns域名称空间的组织方式", - "charIndex": 788 - }, - { - "level": 2, - "title": "DNS负载均衡", - "slug": "dns负载均衡", - "normalizedTitle": "dns负载均衡", - "charIndex": 808 - }, - { - "level": 2, - "title": "”三次握手”的详解", - "slug": "三次握手-的详解", - "normalizedTitle": "”三次握手”的详解", - "charIndex": 1033 - }, - { - "level": 2, - "title": "为什么要三次握手", - "slug": "为什么要三次握手", - "normalizedTitle": "为什么要三次握手", - "charIndex": 1049 - } - ], - "headersStr": "什么是IP地址 什么是DNS DNS解析 DNS查询的两种方式:递归查询和迭代查询 DNS域名称空间的组织方式 DNS负载均衡 ”三次握手”的详解 为什么要三次握手", - "content": "从我们输入一个网址到页面展现的过程到底发生了什么呢?\n\n\n# 总体来说分为以下几个过程:\n\n * 输入网址\n * DNS解析\n * 建立TCP/IP链接\n * 发送HTTP请求\n * 服务器处理请求\n * 服务器返回HTTP响应\n * 浏览器渲染页面并展现\n * 断开连接\n\n\n# 输入网址\n\n当我们想要打开一个页面时,很简单的会想到去输入一个网址 我们以本网址为例,解释一下它各部分的组成:\n\nhttps://www.baidu.com\n\n\n1\n\n\n * https :https是协议(Protocol),它表明了浏览器使用何种协议,有http、https、ftp、mailto等,其中https是加密认证版的http(详见《图解HTTP》)\n\n * www.baidu.com :这是域名(Domain name),它表明正在请求哪个Web服务器,其中www是主机名(host)\n\n\n# DNS解析\n\n输入网址(URL)这一步我们很好理解,但是接下来所谓的域名解析是什么呢?又是为什么要进行域名解析呢?\n\n\n# 什么是IP地址\n\nIP 地址(Internet Protocol Address)是互联网协议特有的一种地址,它是 IP 协议提供的一种统一的地址格式。IP 地址为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。 像192.168.59.0就是一个主机IP地址\n\n\n# 什么是DNS\n\nDNS服务器上存储着映射<域名-IP>,DNS解析就是去DNS服务器查询IP地址。\n\n\n# DNS解析\n\nDNS解析按照下面顺序逐个查找。\n\n * 本地(hosts文件)\n * DNS服务器(本地DNS | 根DNS)\n * 域服务器\n * 解析服务器的地址\n\n\n\n\n# DNS查询的两种方式:递归查询和迭代查询\n\n递归解析 迭代解析\n\n\n# DNS域名称空间的组织方式\n\n\n\n\n# DNS负载均衡\n\n当一个网站有足够多的用户的时候,假如每次请求的资源都位于同一台机器上面,那么这台机器随时可能会蹦掉。\n\n处理办法就是用DNS负载均衡技术,它的原理是在DNS服务器中为同一个主机名配置多个IP地址 在应答DNS查询时,DNS服务器对每个查询将以DNS文件中主机记录的IP地址按顺序返回不同的解析结果,将客户端的访问引导到不同的机器上去,使得不同的客户端访问不同的服务器,从而达到负载均衡的目的。\n\n\n# 建立TCP/IP链接\n\n\n# ”三次握手”的详解\n\n\n\n\n# 为什么要三次握手\n\n * clien端 防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。 client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致连接释放以后的某个时间才到达server。 本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。 于是就向client发出确认报文段,同意建立连接。\n\n * server端 假设不采用“三次握手”。 如果只要server发出确认,新的连接就建立了。并一直等待client发来数据。 事实是client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。 但server端就会有许多这样的等待,server的很多资源就白白浪费掉了。\n\n采用“三次握手”的办法可以防止上述现象发生。主要目的防止server端一直等待,浪费资源。", - "normalizedContent": "从我们输入一个网址到页面展现的过程到底发生了什么呢?\n\n\n# 总体来说分为以下几个过程:\n\n * 输入网址\n * dns解析\n * 建立tcp/ip链接\n * 发送http请求\n * 服务器处理请求\n * 服务器返回http响应\n * 浏览器渲染页面并展现\n * 断开连接\n\n\n# 输入网址\n\n当我们想要打开一个页面时,很简单的会想到去输入一个网址 我们以本网址为例,解释一下它各部分的组成:\n\nhttps://www.baidu.com\n\n\n1\n\n\n * https :https是协议(protocol),它表明了浏览器使用何种协议,有http、https、ftp、mailto等,其中https是加密认证版的http(详见《图解http》)\n\n * www.baidu.com :这是域名(domain name),它表明正在请求哪个web服务器,其中www是主机名(host)\n\n\n# dns解析\n\n输入网址(url)这一步我们很好理解,但是接下来所谓的域名解析是什么呢?又是为什么要进行域名解析呢?\n\n\n# 什么是ip地址\n\nip 地址(internet protocol address)是互联网协议特有的一种地址,它是 ip 协议提供的一种统一的地址格式。ip 地址为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。 像192.168.59.0就是一个主机ip地址\n\n\n# 什么是dns\n\ndns服务器上存储着映射<域名-ip>,dns解析就是去dns服务器查询ip地址。\n\n\n# dns解析\n\ndns解析按照下面顺序逐个查找。\n\n * 本地(hosts文件)\n * dns服务器(本地dns | 根dns)\n * 域服务器\n * 解析服务器的地址\n\n\n\n\n# dns查询的两种方式:递归查询和迭代查询\n\n递归解析 迭代解析\n\n\n# dns域名称空间的组织方式\n\n\n\n\n# dns负载均衡\n\n当一个网站有足够多的用户的时候,假如每次请求的资源都位于同一台机器上面,那么这台机器随时可能会蹦掉。\n\n处理办法就是用dns负载均衡技术,它的原理是在dns服务器中为同一个主机名配置多个ip地址 在应答dns查询时,dns服务器对每个查询将以dns文件中主机记录的ip地址按顺序返回不同的解析结果,将客户端的访问引导到不同的机器上去,使得不同的客户端访问不同的服务器,从而达到负载均衡的目的。\n\n\n# 建立tcp/ip链接\n\n\n# ”三次握手”的详解\n\n\n\n\n# 为什么要三次握手\n\n * clien端 防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。 client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致连接释放以后的某个时间才到达server。 本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。 于是就向client发出确认报文段,同意建立连接。\n\n * server端 假设不采用“三次握手”。 如果只要server发出确认,新的连接就建立了。并一直等待client发来数据。 事实是client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。 但server端就会有许多这样的等待,server的很多资源就白白浪费掉了。\n\n采用“三次握手”的办法可以防止上述现象发生。主要目的防止server端一直等待,浪费资源。", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/09/18, 21:28:02", - "lastUpdatedTimestamp": 1663507682000 } ], "themeConfig": { @@ -7683,16 +8019,6 @@ export const siteData = { "40.设计模式/060.观察模式.md", "观察模式", "/pages/cbff3c/" - ], - [ - "40.设计模式/070.代理模式.md", - "代理模式", - "/pages/a3c735/" - ], - [ - "40.设计模式/080.建造者模式.md", - "建造者模式", - "/pages/c332ae/" ] ] } From ceb550d38d970beaf60a2a3ae6ec8b37bcd05bc2 Mon Sep 17 00:00:00 2001 From: admirean <854818303@qq.com> Date: Mon, 26 Sep 2022 09:00:38 +0800 Subject: [PATCH 14/15] =?UTF-8?q?=E8=AE=A1=E7=AE=97=E6=9C=BA=E7=BD=91?= =?UTF-8?q?=E7=BB=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...33\346\254\241\346\214\245\346\211\213.md" | 84 + ...27\346\234\272\347\275\221\347\273\234.md" | 41 - ...01\347\232\204\345\220\253\344\271\211.md" | 10 + ...04\345\205\250\350\277\207\347\250\213.md" | 24 +- ...\222\214 POST \345\214\272\345\210\253.md" | 83 + ...57\351\235\240\344\274\240\350\276\223.md" | 10 + ...14\346\213\206\345\214\205\357\274\237.md" | 91 + ...44\350\257\201\350\277\207\347\250\213.md" | 105 + ...06\347\232\204\346\246\202\345\277\265.md" | 10 + ...10\345\214\272\345\210\253\357\274\237.md" | 52 + ...05\351\227\256\351\242\230\357\274\237.md" | 10 + ...50\345\234\272\346\231\257\357\274\237.md" | 38 + ...04\345\214\272\345\210\253\357\274\237.md" | 17 + ...33\345\214\272\345\210\253\357\274\237.md" | 39 + ...11\345\223\252\344\272\233\357\274\237.md" | 10 + ...3\350\246\201\345\214\272\345\210\253 .md" | 46 + ...17\345\217\257\351\235\240\357\274\237.md" | 22 + ...10\350\247\243\345\206\263\357\274\237.md" | 11 + ...15\344\274\240\346\234\272\345\210\266.md" | 10 + ...41\351\252\214\346\234\272\345\210\266.md" | 117 + ...04\346\226\271\345\274\217\357\274\237.md" | 52 + ...6 \347\232\204\345\214\272\345\210\253.md" | 11 + ...25\351\230\262\346\255\242\357\274\237.md" | 10 + ...62\350\214\203\345\256\203\357\274\237.md" | 10 + ...77\347\224\250\345\234\272\346\231\257.md" | 10 + ...37\350\257\267\346\261\202\357\274\237.md" | 23 + ...57\344\273\200\344\271\210\357\274\237.md" | 99 + ...14\344\274\240\350\276\223\347\232\204.md" | 10 + ...57\344\273\200\344\271\210\357\274\237.md" | 10 + ...10\347\211\271\347\202\271\357\274\237.md" | 10 + ...04\346\210\220\347\273\223\346\236\204.md" | 10 + ...73\346\224\273\345\207\273\357\274\237.md" | 10 + ...15\344\275\234\347\263\273\347\273\237.md" | 32 - ...41\346\226\271\345\274\217\357\274\237.md" | 10 + ...77\347\224\250\345\234\272\346\231\257.md" | 10 + ...10\345\214\272\345\210\253\357\274\237.md" | 10 + ...54\347\232\204\345\212\237\350\203\275.md" | 10 + ...15\346\226\271\345\274\217\357\274\237.md" | 10 + ...02\344\275\225\350\275\254\346\215\242.md" | 10 + ...15\347\212\266\346\200\201\357\274\237.md" | 10 + ...45\345\217\212\345\214\272\345\210\253.md" | 10 + ...50\350\277\207\345\244\232\357\274\237.md" | 10 + ...57\344\273\200\344\271\210\357\274\237.md" | 10 + ...04\350\277\233\347\250\213\357\274\237.md" | 10 + ...04\351\227\256\351\242\230\357\274\237.md" | 10 + ...50\346\227\245\345\277\227\357\274\237.md" | 10 + ...35\347\232\204\345\216\237\347\220\206.md" | 10 + ...07\346\215\242\345\277\253\357\274\237.md" | 10 + ...13\347\232\204\346\265\201\347\250\213.md" | 10 + ...56\346\215\242\347\256\227\346\263\225.md" | 10 + ...46\347\232\204\347\256\227\346\263\225.md" | 10 + ...05\346\240\270\346\200\201\357\274\237.md" | 10 + ...13\345\215\240\347\224\250\357\274\237.md" | 10 + ...10\344\274\230\347\202\271\357\274\237.md" | 10 + ...44\347\232\204\345\216\237\347\220\206.md" | 10 + .../core/.temp/internal/app-enhancers.js | 2 +- .../core/.temp/internal/page-components.js | 131 +- .../@vuepress/core/.temp/internal/routes.js | 1694 +++-- .../@vuepress/core/.temp/internal/siteData.js | 6393 +++++++++++------ 59 files changed, 6594 insertions(+), 2973 deletions(-) create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/01.\347\256\200\350\277\260 TCP \344\270\211\346\254\241\346\217\241\346\211\213\344\273\245\345\217\212\345\233\233\346\254\241\346\214\245\346\211\213.md" delete mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/01.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/030.HTTP \344\270\255 GET \345\222\214 POST \345\214\272\345\210\253.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/040.TCP \346\200\216\344\271\210\344\277\235\350\257\201\345\217\257\351\235\240\344\274\240\350\276\223.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/050.\344\273\200\344\271\210\346\230\257 TCP \347\262\230\345\214\205\345\222\214\346\213\206\345\214\205\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/060.\347\256\200\350\277\260 HTTPS \347\232\204\345\212\240\345\257\206\344\270\216\350\256\244\350\257\201\350\277\207\347\250\213.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/070.\347\256\200\350\277\260\345\257\271\347\247\260\344\270\216\351\235\236\345\257\271\347\247\260\345\212\240\345\257\206\347\232\204\346\246\202\345\277\265.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/080.TCP \344\270\216 UDP \345\234\250\347\275\221\347\273\234\345\215\217\350\256\256\344\270\255\347\232\204\345\223\252\344\270\200\345\261\202\357\274\214\344\273\226\344\273\254\344\271\213\351\227\264\346\234\211\344\273\200\344\271\210\345\214\272\345\210\253\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/090.\345\246\202\344\275\225\350\247\243\345\206\263 TCP \344\274\240\350\276\223\344\270\242\345\214\205\351\227\256\351\242\230\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/100.TCP \351\225\277\350\277\236\346\216\245\345\222\214\347\237\255\350\277\236\346\216\245\346\234\211\351\202\243\344\271\210\344\270\215\345\220\214\347\232\204\344\275\277\347\224\250\345\234\272\346\231\257\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/110.TCP \347\232\204 keepalive \344\272\206\350\247\243\345\220\227\357\274\237\350\257\264\344\270\200\350\257\264\345\256\203\345\222\214 HTTP \347\232\204 keepalive \347\232\204\345\214\272\345\210\253\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/120.HTTP \344\270\216 HTTPS \346\234\211\345\223\252\344\272\233\345\214\272\345\210\253\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/130.TCP \344\270\255\345\270\270\350\247\201\347\232\204\346\213\245\345\241\236\346\216\247\345\210\266\347\256\227\346\263\225\346\234\211\345\223\252\344\272\233\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/140.\347\256\200\350\277\260 HTTP 1.0\357\274\2141.1\357\274\2142.0 \347\232\204\344\270\273\350\246\201\345\214\272\345\210\253 .md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/150.\344\273\216\347\263\273\347\273\237\345\261\202\351\235\242\344\270\212\357\274\214UDP\345\246\202\344\275\225\344\277\235\350\257\201\345\260\275\351\207\217\345\217\257\351\235\240\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/160.TCP \346\214\245\346\211\213\346\227\266\345\207\272\347\216\260\345\244\247\351\207\217 CLOSE_WAIT \346\210\226 TIME_WAIT \346\200\216\344\271\210\350\247\243\345\206\263\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/170.\347\256\200\350\277\260 TCP \346\273\221\345\212\250\347\252\227\345\217\243\344\273\245\345\217\212\351\207\215\344\274\240\346\234\272\345\210\266.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/180.\347\256\200\350\277\260 JWT \347\232\204\345\216\237\347\220\206\345\222\214\346\240\241\351\252\214\346\234\272\345\210\266.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/190.\344\270\272\344\273\200\344\271\210\351\234\200\350\246\201\345\272\217\345\210\227\345\214\226\357\274\237\346\234\211\344\273\200\344\271\210\345\272\217\345\210\227\345\214\226\347\232\204\346\226\271\345\274\217\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/200.\347\256\200\350\277\260 iPv4 \345\222\214 iPv6 \347\232\204\345\214\272\345\210\253.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/210.TCP \344\270\255 SYN \346\224\273\345\207\273\346\230\257\344\273\200\344\271\210\357\274\237\345\246\202\344\275\225\351\230\262\346\255\242\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/220.\347\256\200\350\277\260 DDOS \346\224\273\345\207\273\345\216\237\347\220\206\357\274\214\345\246\202\344\275\225\351\230\262\350\214\203\345\256\203\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/230.\344\273\200\344\271\210\346\230\257 ARP \345\215\217\350\256\256\357\274\237\347\256\200\350\277\260\345\205\266\344\275\277\347\224\250\345\234\272\346\231\257.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/240.\344\273\200\344\271\210\346\230\257\350\267\250\345\237\237\357\274\214\344\273\200\344\271\210\346\203\205\345\206\265\344\270\213\344\274\232\345\217\221\347\224\237\350\267\250\345\237\237\350\257\267\346\261\202\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/250.Cookie \345\222\214 Session \347\232\204\345\205\263\347\263\273\345\222\214\345\214\272\345\210\253\346\230\257\344\273\200\344\271\210\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/260.\347\256\200\350\277\260 WebSocket \346\230\257\345\246\202\344\275\225\350\277\233\350\241\214\344\274\240\350\276\223\347\232\204.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/270.DNS \346\237\245\350\257\242\346\234\215\345\212\241\345\231\250\347\232\204\345\237\272\346\234\254\346\265\201\347\250\213\346\230\257\344\273\200\344\271\210\357\274\237DNS \345\212\253\346\214\201\346\230\257\344\273\200\344\271\210\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/280.RestFul \346\230\257\344\273\200\344\271\210\357\274\237RestFul \350\257\267\346\261\202\347\232\204 URL \346\234\211\344\273\200\344\271\210\347\211\271\347\202\271\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/290.\347\256\200\350\277\260 HTTP \346\212\245\346\226\207\345\244\264\351\203\250\347\232\204\347\273\204\346\210\220\347\273\223\346\236\204.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/300.\344\273\200\344\271\210\346\230\257 SYN flood\357\274\214\345\246\202\344\275\225\351\230\262\346\255\242\350\277\231\347\261\273\346\224\273\345\207\273\357\274\237.md" delete mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/01.\346\223\215\344\275\234\347\263\273\347\273\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/01.\350\277\233\347\250\213\351\227\264\346\234\211\345\223\252\344\272\233\351\200\232\344\277\241\346\226\271\345\274\217\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/010.\347\256\200\350\277\260\350\207\252\346\227\213\351\224\201\344\270\216\344\272\222\346\226\245\351\224\201\347\232\204\344\275\277\347\224\250\345\234\272\346\231\257.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/020.\350\277\233\347\250\213\345\222\214\347\272\277\347\250\213\344\271\213\351\227\264\346\234\211\344\273\200\344\271\210\345\214\272\345\210\253\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/030.\347\256\200\350\277\260\345\207\240\344\270\252\345\270\270\347\224\250\347\232\204 Linux \345\221\275\344\273\244\344\273\245\345\217\212\344\273\226\344\273\254\347\232\204\345\212\237\350\203\275.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/040.\345\210\233\345\273\272\347\272\277\347\250\213\346\234\211\345\244\232\345\260\221\347\247\215\346\226\271\345\274\217\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/050.\347\272\277\347\250\213\346\234\211\345\244\232\345\260\221\347\247\215\347\212\266\346\200\201\357\274\214\347\212\266\346\200\201\344\271\213\351\227\264\345\246\202\344\275\225\350\275\254\346\215\242.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/060.\350\277\233\347\250\213\346\234\211\345\244\232\345\260\221\347\247\215\347\212\266\346\200\201\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/070.\347\256\200\350\277\260 select, poll, epoll \347\232\204\344\275\277\347\224\250\345\234\272\346\231\257\344\273\245\345\217\212\345\214\272\345\210\253.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/080.Linux \344\270\213\345\246\202\344\275\225\346\216\222\346\237\245 CPU \344\273\245\345\217\212 \345\206\205\345\255\230\345\215\240\347\224\250\350\277\207\345\244\232\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/090.\350\277\233\347\250\213\351\200\232\344\277\241\344\270\255\347\232\204\347\256\241\351\201\223\345\256\236\347\216\260\345\216\237\347\220\206\346\230\257\344\273\200\344\271\210\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/100.Linux \344\270\213\345\246\202\344\275\225\346\237\245\347\234\213 CPU \350\215\267\350\275\275\357\274\214\346\255\243\345\234\250\350\277\220\350\241\214\347\232\204\350\277\233\347\250\213\357\274\214\346\237\220\344\270\252\347\253\257\345\217\243\345\257\271\345\272\224\347\232\204\350\277\233\347\250\213\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/110.\345\246\202\344\275\225\350\260\203\350\257\225\346\234\215\345\212\241\345\231\250\345\206\205\345\255\230\345\215\240\347\224\250\350\277\207\351\253\230\347\232\204\351\227\256\351\242\230\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/120.Linux \345\246\202\344\275\225\346\237\245\347\234\213\345\256\236\346\227\266\347\232\204\346\273\232\345\212\250\346\227\245\345\277\227\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/130.\347\256\200\350\277\260 Linux \351\233\266\346\213\267\350\264\235\347\232\204\345\216\237\347\220\206.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/140.\344\270\272\344\273\200\344\271\210\350\277\233\347\250\213\345\210\207\346\215\242\346\205\242\357\274\214\347\272\277\347\250\213\345\210\207\346\215\242\345\277\253\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/150.\347\256\200\350\277\260\345\210\233\345\273\272\350\277\233\347\250\213\347\232\204\346\265\201\347\250\213.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/160.\347\256\200\350\277\260 Linux \350\231\232\346\213\237\345\206\205\345\255\230\347\232\204\351\241\265\351\235\242\347\275\256\346\215\242\347\256\227\346\263\225.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/170.\347\256\200\350\277\260 Linux \350\277\233\347\250\213\350\260\203\345\272\246\347\232\204\347\256\227\346\263\225.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/180.\344\273\200\344\271\210\346\227\266\345\200\231\344\274\232\347\224\261\347\224\250\346\210\267\346\200\201\351\231\267\345\205\245\345\206\205\346\240\270\346\200\201\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/190.Linux \344\270\213\345\246\202\344\275\225\346\237\245\347\234\213\347\253\257\345\217\243\350\242\253\345\223\252\344\270\252\350\277\233\347\250\213\345\215\240\347\224\250\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/200.Linux \344\270\255\350\231\232\346\213\237\345\206\205\345\255\230\345\222\214\347\211\251\347\220\206\345\206\205\345\255\230\346\234\211\344\273\200\344\271\210\345\214\272\345\210\253\357\274\237\346\234\211\344\273\200\344\271\210\344\274\230\347\202\271\357\274\237.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/210.\347\256\200\350\277\260 traceroute \345\221\275\344\273\244\347\232\204\345\216\237\347\220\206.md" diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/01.\347\256\200\350\277\260 TCP \344\270\211\346\254\241\346\217\241\346\211\213\344\273\245\345\217\212\345\233\233\346\254\241\346\214\245\346\211\213.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/01.\347\256\200\350\277\260 TCP \344\270\211\346\254\241\346\217\241\346\211\213\344\273\245\345\217\212\345\233\233\346\254\241\346\214\245\346\211\213.md" new file mode 100644 index 000000000..41baae052 --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/01.\347\256\200\350\277\260 TCP \344\270\211\346\254\241\346\217\241\346\211\213\344\273\245\345\217\212\345\233\233\346\254\241\346\214\245\346\211\213.md" @@ -0,0 +1,84 @@ +--- +title: 简述 TCP 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手? +date: 2022-03-11 00:51:01 +permalink: /pages/e00207/ +categories: + - 计算机基础 + - HTML +tags: + - +--- + +为了准确无误地把数据送达目标处,TCP 协议采用了三次握手策略。 + +## 建立连接-TCP 三次握手 +TCP 三次握手图解 + +![](https://tva1.sinaimg.cn/large/e6c9d24ely1h6entf5jifj20hs0c7aba.jpg) + +建立一个 TCP 连接需要“三次握手”,缺一不可 : + +* 一次握手:客户端发送带有 SYN(SEQ=x) 标志的数据包 -> 服务端,然后客户端进入 SYN_SEND 状态,等待服务器的确认; +* 二次握手:服务端发送带有 SYN+ACK(SEQ=y,ACK=x+1) 标志的数据包 –> 客户端,然后服务端进入 SYN_RECV 状态 +* 三次握手:客户端发送带有带有 ACK(ACK=y+1) 标志的数据包 –> 服务端,然后客户端和服务器端都进入ESTABLISHED 状态,完成TCP三次握手。 + +当建立了 3 次握手之后,客户端和服务端就可以传输数据啦! + +## 为什么要三次握手? +三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。 + +* 第一次握手 :Client 什么都不能确认;Server 确认了对方发送正常,自己接收正常 +* 第二次握手 :Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:对方发送正常,自己接收正常 +* 第三次握手 :Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送、接收正常 + +三次握手就能确认双发收发功能都正常,缺一不可。 + +更详细的解答可以看这个:[TCP 为什么是三次握手,而不是两次或四次?](https://mp.weixin.qq.com/s/NIjxgx4NPn7FC4PfkHBAAQ) 。 + +## 第2次握手传回了ACK,为什么还要传回SYN? +服务端传回发送端所发送的 ACK 是为了告诉客户端:“我接收到的信息确实就是你所发送的信号了”,这表明从客户端到服务端的通信是正常的。回传 SYN 则是为了建立并确认从服务端到客户端的通信。 + +>SYN 同步序列编号(Synchronize Sequence Numbers) 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 TCP 网络连接时,客户机首先发出一个 SYN 消息,服务器使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以 ACK(Acknowledgement)消息响应。这样在客户机和服务器之间才能建立起可靠的 TCP 连接,数据才可以在客户机和服务器之间传递。 + +## 断开连接-TCP 四次挥手 +TCP 四次挥手图解 + +![](https://tva1.sinaimg.cn/large/e6c9d24ely1h6enumk1tvj20hs0dagn7.jpg) + +断开一个 TCP 连接则需要“四次挥手”,缺一不可 : + +* 第一次挥手 :客户端发送一个 FIN(SEQ=X) 标志的数据包->服务端,用来关闭客户端到服务器的数据传送。然后,客户端进入 FIN-WAIT-1 状态。 +* 第二次挥手 :服务器收到这个 FIN(SEQ=X) 标志的数据包,它发送一个 ACK (SEQ=X+1)标志的数据包->客户端 。然后,此时服务端进入CLOSE-WAIT状态,客户端进入FIN-WAIT-2状态。 +* 第三次挥手 :服务端关闭与客户端的连接并发送一个 FIN (SEQ=y)标志的数据包->客户端请求关闭连接,然后,服务端进入LAST-ACK状态。 +* 第四次挥手 :客户端发送 ACK (SEQ=y+1)标志的数据包->服务端并且进入TIME-WAIT状态,服务端在收到 ACK (SEQ=y+1)标志的数据包后进入 CLOSE 状态。此时,如果客户端等待 2MSL 后依然没有收到回复,就证明服务端已正常关闭,随后,客户端也可以关闭连接了。 + +只要四次挥手没有结束,客户端和服务端就可以继续传输数据! + +## 为什么要四次挥手? +TCP是全双工通信,可以双向传输数据。任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了 TCP 连接。 + +举个例子:A 和 B 打电话,通话即将结束后。 + +1. 第一次挥手 : A 说“我没啥要说的了” +2. 第二次挥手 :B 回答“我知道了”,但是 B 可能还会有要说的话,A 不能要求 B 跟着自己的节奏结束通话 +3. 第三次挥手 :于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了” +4. 第四次挥手 :A 回答“知道了”,这样通话才算结束。 + +## 为什么不能把服务器发送的 ACK 和 FIN 合并起来,变成三次挥手? +因为服务器收到客户端断开连接的请求时,可能还有一些数据没有发完,这时先回复 ACK,表示接收到了断开连接的请求。等到数据发完之后再发 FIN,断开服务器到客户端的数据传送。 + +## 如果第二次挥手时服务器的 ACK 没有送达客户端,会怎样? +客户端没有收到 ACK 确认,会重新发送 FIN 请求。 + +## 为什么第四次挥手客户端需要等待 2*MSL(报文段最长寿命)时间后才进入 CLOSED 状态? +第四次挥手时,客户端发送给服务器的 ACK 有可能丢失,如果服务端没有因为某些原因而没有收到 ACK 的话,服务端就会重发 FIN,如果客户端在 2*MSL 的时间内收到了 FIN,就会重新发送 ACK 并再次等待 2MSL,防止 Server 没有收到 ACK 而不断重发 FIN。 + +MSL(Maximum Segment Lifetime) : 一个片段在网络中最大的存活时间,2MSL 就是一个发送和一个回复所需的最大时间。如果直到 2MSL,Client 都没有再次收到 FIN,那么 Client 推断 ACK 已经被成功接收,则结束 TCP 连接。 + +## 面试官可能问你这些问题 +* 为什么要三次握手? +* 第 2 次握手传回了ACK,为什么还要传回SYN? +* 为什么要四次挥手? +* 为什么不能把服务器发送的 ACK 和 FIN 合并起来,变成三次挥手? +* 如果第二次挥手时服务器的 ACK 没有送达客户端,会怎样? +* 为什么第四次挥手客户端需要等待 2*MSL(报文段最长寿命)时间后才进入 CLOSED 状态? \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/01.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/01.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" deleted file mode 100644 index b12197895..000000000 --- "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/01.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: 计算机网络 -date: 2022-03-11 00:51:01 -permalink: /pages/e00207/ -categories: - - 计算机基础 - - HTML -tags: - - ---- -## 简述 TCP 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手? - -## 从输入 URL 到展现页面的全过程 -## HTTP 中 GET 和 POST 区别 -## TCP 怎么保证可靠传输 -## 什么是 TCP 粘包和拆包? -## 简述 HTTPS 的加密与认证过程 -## 简述对称与非对称加密的概念 -## TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别? -## 如何解决 TCP 传输丢包问题? -## TCP 长连接和短连接有那么不同的使用场景? -## TCP 的 keepalive 了解吗?说一说它和 HTTP 的 keepalive 的区别? -## HTTP 与 HTTPS 有哪些区别? -## TCP 中常见的拥塞控制算法有哪些? -## 简述 HTTP 1.0,1.1,2.0 的主要区别 -## 从系统层面上,UDP如何保证尽量可靠? -## TCP 挥手时出现大量 CLOSE_WAIT 或 TIME_WAIT 怎么解决? -## 简述 TCP 滑动窗口以及重传机制 -## 简述 JWT 的原理和校验机制 -## 为什么需要序列化?有什么序列化的方式? -## 简述 iPv4 和 iPv6 的区别 -## TCP 中 SYN 攻击是什么?如何防止? -## 简述 DDOS 攻击原理,如何防范它? -## 什么是 ARP 协议?简述其使用场景 -## 什么是跨域,什么情况下会发生跨域请求? -## Cookie 和 Session 的关系和区别是什么? -## 简述 WebSocket 是如何进行传输的 -## DNS 查询服务器的基本流程是什么?DNS 劫持是什么? -## RestFul 是什么?RestFul 请求的 URL 有什么特点? -## 简述 HTTP 报文头部的组成结构 -## 什么是 SYN flood,如何防止这类攻击? \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/010.\347\256\200\350\277\260\345\270\270\350\247\201\347\232\204 HTTP \347\212\266\346\200\201\347\240\201\347\232\204\345\220\253\344\271\211.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/010.\347\256\200\350\277\260\345\270\270\350\247\201\347\232\204 HTTP \347\212\266\346\200\201\347\240\201\347\232\204\345\220\253\344\271\211.md" index 5ffd48600..ac7e8e12a 100644 --- "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/010.\347\256\200\350\277\260\345\270\270\350\247\201\347\232\204 HTTP \347\212\266\346\200\201\347\240\201\347\232\204\345\220\253\344\271\211.md" +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/010.\347\256\200\350\277\260\345\270\270\350\247\201\347\232\204 HTTP \347\212\266\346\200\201\347\240\201\347\232\204\345\220\253\344\271\211.md" @@ -11,22 +11,32 @@ tags: ## 2XX 成功 200 OK,表示从客户端发来的请求在服务器端被正确处理 + 204 No content,表示请求成功,但响应报文不含实体的主体部分 + 206 Partial Content,进行范围请求 ## 3XX 重定向 301 moved permanently,永久性重定向,表示资源已被分配了新的 URL + 302 found,临时性重定向,表示资源临时被分配了新的 URL + 303 see other,表示资源存在着另一个 URL,应使用 GET 方法定向获取资源 + 304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况 + 307 temporary redirect,临时重定向,和302含义相同 ## 4XX 客户端错误 400 bad request,请求报文存在语法错误 + 401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息 + 403 forbidden,表示对请求资源的访问被服务器拒绝 + 404 not found,表示在服务器上没有找到请求的资源 ## 5XX 服务器错误 500 internal sever error,表示服务器端在执行请求时发生了错误 + 503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求 \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/020.\344\273\216\350\276\223\345\205\245 URL \345\210\260\345\261\225\347\216\260\351\241\265\351\235\242\347\232\204\345\205\250\350\277\207\347\250\213.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/020.\344\273\216\350\276\223\345\205\245 URL \345\210\260\345\261\225\347\216\260\351\241\265\351\235\242\347\232\204\345\205\250\350\277\207\347\250\213.md" index 8b6c22e30..bc7a810b0 100644 --- "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/020.\344\273\216\350\276\223\345\205\245 URL \345\210\260\345\261\225\347\216\260\351\241\265\351\235\242\347\232\204\345\205\250\350\277\207\347\250\213.md" +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/020.\344\273\216\350\276\223\345\205\245 URL \345\210\260\345\261\225\347\216\260\351\241\265\351\235\242\347\232\204\345\205\250\350\277\207\347\250\213.md" @@ -10,7 +10,7 @@ tags: --- 从我们输入一个网址到页面展现的过程到底发生了什么呢? -# 总体来说分为以下几个过程: +## 总体来说分为以下几个过程: * 输入网址 * DNS解析 * 建立TCP/IP链接 @@ -20,7 +20,7 @@ tags: * 浏览器渲染页面并展现 * 断开连接 -# 输入网址 +## 输入网址 当我们想要打开一个页面时,很简单的会想到去输入一个网址 我们以本网址为例,解释一下它各部分的组成: ```html @@ -30,18 +30,18 @@ https://www.baidu.com * www.baidu.com :这是域名(Domain name),它表明正在请求哪个Web服务器,其中www是主机名(host) -# DNS解析 +## DNS解析 输入网址(URL)这一步我们很好理解,但是接下来所谓的域名解析是什么呢?又是为什么要进行域名解析呢? -## 什么是IP地址 +### 什么是IP地址 IP 地址(Internet Protocol Address)是互联网协议特有的一种地址,它是 IP 协议提供的一种统一的地址格式。IP 地址为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。 像192.168.59.0就是一个主机IP地址 -## 什么是DNS +### 什么是DNS DNS服务器上存储着映射<域名-IP>,DNS解析就是去DNS服务器查询IP地址。 -## DNS解析 +### DNS解析 DNS解析按照下面顺序逐个查找。 * 本地(hosts文件) * DNS服务器(本地DNS | 根DNS) @@ -50,24 +50,24 @@ DNS解析按照下面顺序逐个查找。 ![](https://tva1.sinaimg.cn/large/e6c9d24ely1h3jr49albxj20sl0jt40n.jpg) -## DNS查询的两种方式:递归查询和迭代查询 +### DNS查询的两种方式:递归查询和迭代查询 递归解析 ![](https://tva1.sinaimg.cn/large/e6c9d24ely1h3jrcpsp8tj20fn02r3yh.jpg) 迭代解析 ![](https://tva1.sinaimg.cn/large/e6c9d24ely1h3jrd9k881j20bx0983yo.jpg) -## DNS域名称空间的组织方式 +### DNS域名称空间的组织方式 ![](https://tva1.sinaimg.cn/large/e6c9d24ely1h3jrafkgijj20fi06ogm7.jpg) -## DNS负载均衡 +### DNS负载均衡 当一个网站有足够多的用户的时候,假如每次请求的资源都位于同一台机器上面,那么这台机器随时可能会蹦掉。 处理办法就是用DNS负载均衡技术,它的原理是在DNS服务器中为同一个主机名配置多个IP地址 在应答DNS查询时,DNS服务器对每个查询将以DNS文件中主机记录的IP地址按顺序返回不同的解析结果,将客户端的访问引导到不同的机器上去,使得不同的客户端访问不同的服务器,从而达到负载均衡的目的。 -# 建立TCP/IP链接 -## ”三次握手”的详解 +## 建立TCP/IP链接 +### ”三次握手”的详解 ![](https://tva1.sinaimg.cn/large/e6c9d24ely1h3jrlodi0dj20ei09daaf.jpg) -## 为什么要三次握手 +### 为什么要三次握手 * clien端 防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。 client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致连接释放以后的某个时间才到达server。 diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/030.HTTP \344\270\255 GET \345\222\214 POST \345\214\272\345\210\253.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/030.HTTP \344\270\255 GET \345\222\214 POST \345\214\272\345\210\253.md" new file mode 100644 index 000000000..0566bf79d --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/030.HTTP \344\270\255 GET \345\222\214 POST \345\214\272\345\210\253.md" @@ -0,0 +1,83 @@ +--- +title: HTTP 中 GET 和 POST 区别 +date: 2022-09-21 22:34:00 +permalink: /pages/a0e400/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- +## 什么是 HTTP ? +超文本传输协议(HTTP)的设计目的是保证客户端与服务器之间的通信。 + +HTTP 的工作方式是客户端与服务器之间的请求-应答协议。 + +web 浏览器可能是客户端,而计算机上的网络应用程序也可能作为服务器端。 + +举例:客户端(浏览器)向服务器提交 HTTP 请求;服务器向客户端返回响应。响应包含关于请求的状态信息以及可能被请求的内容。 + +## 两种 HTTP 请求方法:GET 和 POST +在客户机和服务器之间进行请求-响应时,两种最常被用到的方法是:GET 和 POST。 + +* GET - 从指定的资源请求数据。 +* POST - 向指定的资源提交要被处理的数据。 +GET 提交参数一般显示在 URL 上,POST 通过表单提交不会显示在 URL 上,POST 更具隐蔽性: + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6frpcaxdtj20eq0cu0t2.jpg) + + +## GET 方法 +请注意,查询字符串(名称/值对)是在 GET 请求的 URL 中发送的: +```xml +/test/demo_form.php?name1=value1&name2=value2 +``` +有关 GET 请求的其他一些注释: + +* GET 请求可被缓存 +* GET 请求保留在浏览器历史记录中 +* GET 请求可被收藏为书签 +* GET 请求不应在处理敏感数据时使用 +* GET 请求有长度限制 +* GET 请求只应当用于取回数据 + +## POST 方法 +请注意,查询字符串(名称/值对)是在 POST 请求的 HTTP 消息主体中发送的: + +```xml +POST /test/demo_form.php HTTP/1.1 +Host: runoob.com +name1=value1&name2=value2 +``` +有关 POST 请求的其他一些注释: + +* POST 请求不会被缓存 +* POST 请求不会保留在浏览器历史记录中 +* POST 不能被收藏为书签 +* POST 请求对数据长度没有要求 + +## 比较 GET 与 POST +下面的表格比较了两种 HTTP 方法:GET 和 POST。 + +| | GET | POST| +| --- | --- | --- | +|后退按钮/刷新| 无害| 数据会被重新提交(浏览器应该告知用户数据会被重新提交)。| +|书签 |可收藏为书签 |不可收藏为书签| +|缓存 |能被缓存 |不能缓存| +|编码类型 |application/x-www-form-urlencoded |application/x-www-form-urlencoded or multipart/form-data。为二进制数据使用多重编码。| +|历史 |参数保留在浏览器历史中。| 参数不会保存在浏览器历史中。| +|对数据长度的限制 |是的。当发送数据时,GET 方法向 URL 添加数据;URL 的长度是受限制的(URL 的最大长度是 2048 个字符)。 |无限制。| +|对数据类型的限制 |只允许 ASCII 字符。 |没有限制。也允许二进制数据。| +|安全性 |与 POST 相比,GET 的安全性较差,因为所发送的数据是 URL 的一部分。|在发送密码或其他敏感信息时绝不要使用 GET ! POST 比 GET 更安全,因为参数不会被保存在浏览器历史或 web 服务器日志中。| +|可见性 |数据在 URL 中对所有人都是可见的。| 数据不会显示在 URL 中。| + +## 其他 HTTP 请求方法 +下面的表格列出了其他一些 HTTP 请求方法: + +|方法 |描述| +| --- | --- | +|HEAD |与 GET 相同,但只返回 HTTP 报头,不返回文档主体。| +|PUT |上传指定的 URI 表示。| +|DELETE |删除指定资源。| +|OPTIONS |返回服务器支持的 HTTP 方法。| +|CONNECT |把请求连接转换到透明的 TCP/IP 通道。| \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/040.TCP \346\200\216\344\271\210\344\277\235\350\257\201\345\217\257\351\235\240\344\274\240\350\276\223.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/040.TCP \346\200\216\344\271\210\344\277\235\350\257\201\345\217\257\351\235\240\344\274\240\350\276\223.md" new file mode 100644 index 000000000..e88fbddd4 --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/040.TCP \346\200\216\344\271\210\344\277\235\350\257\201\345\217\257\351\235\240\344\274\240\350\276\223.md" @@ -0,0 +1,10 @@ +--- +title: TCP 怎么保证可靠传输 +date: 2022-09-21 22:34:41 +permalink: /pages/5fcc49/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/050.\344\273\200\344\271\210\346\230\257 TCP \347\262\230\345\214\205\345\222\214\346\213\206\345\214\205\357\274\237.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/050.\344\273\200\344\271\210\346\230\257 TCP \347\262\230\345\214\205\345\222\214\346\213\206\345\214\205\357\274\237.md" new file mode 100644 index 000000000..58001cf5a --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/050.\344\273\200\344\271\210\346\230\257 TCP \347\262\230\345\214\205\345\222\214\346\213\206\345\214\205\357\274\237.md" @@ -0,0 +1,91 @@ +--- +title: 什么是 TCP 粘包和拆包? +date: 2022-09-21 22:35:01 +permalink: /pages/2822ac/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- +## TCP粘包与拆包 +TCP是个“流”协议,没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。 + +![](https://tva1.sinaimg.cn/large/e6c9d24ely1h6il1rx9b8j21b80lcgn0.jpg) + +假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。 + +(1)服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包; + +(2)服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包; + +(3)服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包; + +(4)服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。 + +如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。 + +## 粘包和拆包原因 +(1)要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包; + +(2)接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包; + +(3)要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包; + +(4)待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。即TCP报文长度-TCP头部长度>MSS。 + +## 粘包和拆包解决策略 +由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,归纳如下: + +消息定长。发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。 + +设置消息边界。服务端从网络流中按消息边界分离出消息内容。在包尾增加回车换行符进行分割,例如FTP协议。 + +将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段。 +更复杂的应用层协议。 + +## 粘包的概念 +粘包:多个数据包被连续存储于连续的缓存中,在对数据包进行读取时由于无法确定发生方的发送边界,而采用某一估测值大小来进行数据读出,若双方的size不一致时就会使指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。 + +比如说:发送方发送了两个数据,接收方一次收了一个半数据(接收方可能不清楚一个包有多大) + +![](https://tva1.sinaimg.cn/large/e6c9d24ely1h6il36bwhrj21c00e4myr.jpg) + +## 出现粘包的原因 +出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。 + +发送方引起的粘包是由TCP协议本身造成的: + +TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。 + +![](https://tva1.sinaimg.cn/large/e6c9d24ely1h6il3mbawaj21ec09o75g.jpg) + +TCP协议规定有MSS,如果数据包过长就会被分开传输。这样接收方就收到了拆包数据。 + +![](https://tva1.sinaimg.cn/large/e6c9d24ely1h6il4nlrlmj21f80f0gna.jpg) + +接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。 + +在代码中常见体现: + +要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。 + +要发送的数据大于MSS,TCP在传输前将进行拆包。 + +要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。 + +接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。 +等等。 + +## 粘包的处理方式 +当短连接的情况下,不用考虑粘包的情况 + +如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包 + +如果双方建立长连接,需要在连接后一段时间内发送不同结构数据 + +发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。 + +发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。 + +可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。 \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/060.\347\256\200\350\277\260 HTTPS \347\232\204\345\212\240\345\257\206\344\270\216\350\256\244\350\257\201\350\277\207\347\250\213.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/060.\347\256\200\350\277\260 HTTPS \347\232\204\345\212\240\345\257\206\344\270\216\350\256\244\350\257\201\350\277\207\347\250\213.md" new file mode 100644 index 000000000..cdf67549d --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/060.\347\256\200\350\277\260 HTTPS \347\232\204\345\212\240\345\257\206\344\270\216\350\256\244\350\257\201\350\277\207\347\250\213.md" @@ -0,0 +1,105 @@ +--- +title: 简述 HTTPS 的加密与认证过程 +date: 2022-09-21 22:35:16 +permalink: /pages/12c0cc/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- +## HTTPS 协议优点 +保密性好、信任度高。 + +## HTTPS 的核心—SSL/TLS协议 +HTTPS 之所以能达到较高的安全性要求,就是结合了 SSL/TLS 和 TCP 协议,对通信数据进行加密,解决了 HTTP 数据透明的问题。接下来重点介绍一下 SSL/TLS 的工作原理。 + +## SSL 和 TLS 的区别? +SSL 和 TLS 没有太大的区别。 + +SSL 指安全套接字协议(Secure Sockets Layer),首次发布与 1996 年。SSL 的首次发布其实已经是他的 3.0 版本,SSL 1.0 从未面世,SSL 2.0 则具有较大的缺陷(DROWN 缺陷——Decrypting RSA with Obsolete and Weakened eNcryption)。很快,在 1999 年,SSL 3.0 进一步升级,新版本被命名为 TLS 1.0。因此,TLS 是基于 SSL 之上的,但由于习惯叫法,通常把 HTTPS 中的核心加密协议混成为 SSL/TLS。 + +## SSL/TLS 的工作原理 +### 非对称加密 +SSL/TLS 的核心要素是非对称加密。非对称加密采用两个密钥——一个公钥,一个私钥。在通信时,私钥仅由解密者保存,公钥由任何一个想与解密者通信的发送者(加密者)所知。可以设想一个场景, + +>在某个自助邮局,每个通信信道都是一个邮箱,每一个邮箱所有者都在旁边立了一个牌子,上面挂着一把钥匙:这是我的公钥,发送者请将信件放入我的邮箱,并用公钥锁好。 +但是公钥只能加锁,并不能解锁。解锁只能由邮箱的所有者——因为只有他保存着私钥。 +这样,通信信息就不会被其他人截获了,这依赖于私钥的保密性。 + +![](https://tva1.sinaimg.cn/large/e6c9d24ely1h6eoa5f5uoj20oa0ivq3x.jpg) + +非对称加密的公钥和私钥需要采用一种复杂的数学机制生成(密码学认为,为了较高的安全性,尽量不要自己创造加密方案)。公私钥对的生成算法依赖于单向陷门函数。 + +>单向函数:已知单向函数 f,给定任意一个输入 x,易计算输出 y=f(x);而给定一个输出 y,假设存在 f(x)=y,很难根据 f 来计算出 x。 +单向陷门函数:一个较弱的单向函数。已知单向陷门函数 f,陷门 h,给定任意一个输入 x,易计算出输出 y=f(x;h);而给定一个输出 y,假设存在 f(x;h)=y,很难根据 f 来计算出 x,但可以根据 f 和 h 来推导出 x。 + +![](https://tva1.sinaimg.cn/large/e6c9d24ely1h6eoap853dj20ue0ljdgy.jpg) + +上图就是一个单向函数(不是单项陷门函数),假设有一个绝世秘籍,任何知道了这个秘籍的人都可以把苹果汁榨成苹果,那么这个秘籍就是“陷门”了吧。 + +在这里,函数 f 的计算方法相当于公钥,陷门 h 相当于私钥。公钥 f 是公开的,任何人对已有输入,都可以用 f 加密,而要想根据加密信息还原出原信息,必须要有私钥才行。 + +### 对称加密 +使用 SSL/TLS 进行通信的双方需要使用非对称加密方案来通信,但是非对称加密设计了较为复杂的数学算法,在实际通信过程中,计算的代价较高,效率太低,因此,SSL/TLS 实际对消息的加密使用的是对称加密。 + +>对称加密:通信双方共享唯一密钥 k,加解密算法已知,加密方利用密钥 k 加密,解密方利用密钥 k 解密,保密性依赖于密钥 k 的保密性。 + +![](https://tva1.sinaimg.cn/large/e6c9d24ely1h6eob0y22kj20vd0am0tl.jpg) + +对称加密的密钥生成代价比公私钥对的生成代价低得多,那么有的人会问了,为什么 SSL/TLS 还需要使用非对称加密呢? +因为对称加密的保密性完全依赖于密钥的保密性。 +在双方通信之前,需要商量一个用于对称加密的密钥。 +我们知道网络通信的信道是不安全的,传输报文对任何人是可见的,密钥的交换肯定不能直接在网络信道中传输。 +因此,使用非对称加密,对对称加密的密钥进行加密,保护该密钥不在网络信道中被窃听。 +这样,通信双方只需要一次非对称加密,交换对称加密的密钥,在之后的信息通信中, +使用绝对安全的密钥,对信息进行对称加密,即可保证传输消息的保密性。 + +### 公钥传输的信赖性 +SSL/TLS 介绍到这里,了解信息安全的朋友又会想到一个安全隐患,设想一个下面的场景: + +>客户端 C 和服务器 S 想要使用 SSL/TLS 通信,由上述 SSL/TLS 通信原理,C 需要先知道 S 的公钥,而 S 公钥的唯一获取途径,就是把 S 公钥在网络信道中传输。要注意网络信道通信中有几个前提: + +>1.任何人都可以捕获通信包 +> +>2.通信包的保密性由发送者设计 +> +>3.保密算法设计方案默认为公开,而(解密)密钥默认是安全的 +> +>因此,假设 S 公钥不做加密,在信道中传输,那么很有可能存在一个攻击者 A,发送给 C 一个诈包,假装是 S 公钥,其实是诱饵服务器 AS 的公钥。当 C 收获了 AS 的公钥(却以为是 S 的公钥),C 后续就会使用 AS 公钥对数据进行加密,并在公开信道传输,那么 A 将捕获这些加密包,用 AS 的私钥解密,就截获了 C 本要给 S 发送的内容,而 C 和 S 二人全然不知。 + +同样的,S 公钥即使做加密,也难以避免这种信任性问题,C 被 AS 拐跑了! + +![](https://tva1.sinaimg.cn/large/e6c9d24ely1h6eobt11v5j20mj09zq3i.jpg) + +为了公钥传输的信赖性问题,第三方机构应运而生——证书颁发机构(CA,Certificate Authority)。CA 默认是受信任的第三方。CA 会给各个服务器颁发证书,证书存储在服务器上,并附有 CA 的电子签名。 + +当客户端(浏览器)向服务器发送 HTTPS 请求时,一定要先获取目标服务器的证书,并根据证书上的信息,检验证书的合法性。一旦客户端检测到证书非法,就会发生错误。客户端获取了服务器的证书后,由于证书的信任性是由第三方信赖机构认证的,而证书上又包含着服务器的公钥信息,客户端就可以放心的信任证书上的公钥就是目标服务器的公钥。 + +### 数字签名 +好,到这一小节,已经是 SSL/TLS 的尾声了。上一小节提到了数字签名,数字签名要解决的问题,是防止证书被伪造。第三方信赖机构 CA 之所以能被信赖,就是 靠数字签名技术 。 + +数字签名,是 CA 在给服务器颁发证书时,使用散列+加密的组合技术,在证书上盖个章,以此来提供验伪的功能。具体行为如下: + +>CA 知道服务器的公钥,对该公钥采用散列技术生成一个摘要。CA 使用 CA 私钥对该摘要进行加密,并附在证书下方,发送给服务器。 + +>现在服务器将该证书发送给客户端,客户端需要验证该证书的身份。客户端找到第三方机构 CA,获知 CA 的公钥,并用 CA 公钥对证书的签名进行解密,获得了 CA 生成的摘要。 + +>客户端对证书数据(也就是服务器的公钥)做相同的散列处理,得到摘要,并将该摘要与之前从签名中解码出的摘要做对比,如果相同,则身份验证成功;否则验证失败。 + +![](https://tva1.sinaimg.cn/large/e6c9d24ely1h6eodt1e5lj20hk058jri.jpg) + + +总结来说,带有证书的公钥传输机制如下: + +1. 设有服务器 S,客户端 C,和第三方信赖机构 CA。 +2. S 信任 CA,CA 是知道 S 公钥的,CA 向 S 颁发证书。并附上 CA 私钥对消息摘要的加密签名。 +3. S 获得 CA 颁发的证书,将该证书传递给 C。 +4. C 获得 S 的证书,信任 CA 并知晓 CA 公钥,使用 CA 公钥对 S 证书上的签名解密,同时对消息进行散列处理,得到摘要。比较摘要,验证 S 证书的真实性。 +5. 如果 C 验证 S 证书是真实的,则信任 S 的公钥(在 S 证书中)。 + +![](https://tva1.sinaimg.cn/large/e6c9d24ely1h6eoer805bj20fg0c2wey.jpg) + +对于数字签名,我这里讲的比较简单,如果你没有搞清楚的话,强烈推荐你看看[数字签名及数字证书原理](https://www.bilibili.com/video/BV18N411X7ty/) 这个视频,这是我看过最清晰的讲解。 + +![](https://tva1.sinaimg.cn/large/e6c9d24ely1h6eog6qutnj20m80c73za.jpg) \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/070.\347\256\200\350\277\260\345\257\271\347\247\260\344\270\216\351\235\236\345\257\271\347\247\260\345\212\240\345\257\206\347\232\204\346\246\202\345\277\265.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/070.\347\256\200\350\277\260\345\257\271\347\247\260\344\270\216\351\235\236\345\257\271\347\247\260\345\212\240\345\257\206\347\232\204\346\246\202\345\277\265.md" new file mode 100644 index 000000000..0011a3be0 --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/070.\347\256\200\350\277\260\345\257\271\347\247\260\344\270\216\351\235\236\345\257\271\347\247\260\345\212\240\345\257\206\347\232\204\346\246\202\345\277\265.md" @@ -0,0 +1,10 @@ +--- +title: 简述对称与非对称加密的概念 +date: 2022-09-21 22:35:34 +permalink: /pages/057092/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/080.TCP \344\270\216 UDP \345\234\250\347\275\221\347\273\234\345\215\217\350\256\256\344\270\255\347\232\204\345\223\252\344\270\200\345\261\202\357\274\214\344\273\226\344\273\254\344\271\213\351\227\264\346\234\211\344\273\200\344\271\210\345\214\272\345\210\253\357\274\237.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/080.TCP \344\270\216 UDP \345\234\250\347\275\221\347\273\234\345\215\217\350\256\256\344\270\255\347\232\204\345\223\252\344\270\200\345\261\202\357\274\214\344\273\226\344\273\254\344\271\213\351\227\264\346\234\211\344\273\200\344\271\210\345\214\272\345\210\253\357\274\237.md" new file mode 100644 index 000000000..00741f6d6 --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/080.TCP \344\270\216 UDP \345\234\250\347\275\221\347\273\234\345\215\217\350\256\256\344\270\255\347\232\204\345\223\252\344\270\200\345\261\202\357\274\214\344\273\226\344\273\254\344\271\213\351\227\264\346\234\211\344\273\200\344\271\210\345\214\272\345\210\253\357\274\237.md" @@ -0,0 +1,52 @@ +--- +title: TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别? +date: 2022-09-21 22:35:48 +permalink: /pages/cc4b24/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- +## TCP 与 UDP 的区别(重要) + +1. 是否面向连接 :UDP 在传送数据之前不需要先建立连接。而 TCP 提供面向连接的服务,在传送数据之前必须先建立连接,数据传送结束后要释放连接。 +2. 是否是可靠传输:远地主机在收到 UDP 报文后,不需要给出任何确认,并且不保证数据不丢失,不保证是否顺序到达。TCP 提供可靠的传输服务,TCP 在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制。通过 TCP 连接传输的数据,无差错、不丢失、不重复、并且按序到达。 +3. 是否有状态 :这个和上面的“是否可靠传输”相对应。TCP 传输是有状态的,这个有状态说的是 TCP 会去记录自己发送消息的状态比如消息是否发送了、是否被接收了等等。为此 ,TCP 需要维持复杂的连接状态表。而 UDP 是无状态服务,简单来说就是不管发出去之后的事情了(这很渣男!)。 +4. 传输效率 :由于使用 TCP 进行传输的时候多了连接、确认、重传等机制,所以 TCP 的传输效率要比 UDP 低很多。 +5. 传输形式 : TCP 是面向字节流的,UDP 是面向报文的。 +6. 首部开销 :TCP 首部开销(20 ~ 60 字节)比 UDP 首部开销(8 字节)要大。 +7. 是否提供广播或多播服务 :TCP 只支持点对点通信,UDP 支持一对一、一对多、多对一、多对多; +...... +我把上面总结的内容通过表格形式展示出来了! + +||TCP| UDP| +|---|---|---| +|是否面向连接 |是| 否| +|是否可靠 |是 |否| +|是否有状态 |是 |否| +|传输效率| 较慢 |较快| +|传输形式 |字节流| 数据报文段| +|首部开销 |20 ~ 60 bytes| 8 bytes| +|是否提供广播或多播服务 |否 |是| +## 什么时候选择 TCP,什么时候选 UDP? +* UDP 一般用于即时通信,比如: 语音、 视频 、直播等等。这些场景对传输数据的准确性要求不是特别高,比如你看视频即使少个一两帧,实际给人的感觉区别也不大。 +* TCP 用于对传输准确性要求特别高的场景,比如文件传输、发送和接收邮件、远程登录等等。 +## HTTP 基于 TCP 还是 UDP? +HTTP 协议是基于 TCP 协议的,所以发送 HTTP 请求之前首先要建立 TCP 连接也就是要经历 3 次握手。 + +## 使用 TCP 的协议有哪些?使用 UDP 的协议有哪些? +运行于 TCP 协议之上的协议 : + +1. HTTP 协议 :超文本传输协议(HTTP,HyperText Transfer Protocol)主要是为 Web 浏览器与 Web 服务器之间的通信而设计的。当我们使用浏览器浏览网页的时候,我们网页就是通过 HTTP 请求进行加载的。 +2. HTTPS 协议 :更安全的超文本传输协议(HTTPS,Hypertext Transfer Protocol Secure),身披 SSL 外衣的 HTTP 协议 +3. FTP 协议:文件传输协议 FTP(File Transfer Protocol),提供文件传输服务,基于 TCP 实现可靠的传输。使用 FTP 传输文件的好处是可以屏蔽操作系统和文件存储方式。 +4. SMTP 协议:简单邮件传输协议(SMTP,Simple Mail Transfer Protocol)的缩写,基于 TCP 协议,用来发送电子邮件。注意 ⚠️:接受邮件的协议不是 SMTP 而是 POP3 协议。 +5. POP3/IMAP 协议: POP3 和 IMAP 两者都是负责邮件接收的协议。 +6. Telent 协议:远程登陆协议,通过一个终端登陆到其他服务器。被一种称为 SSH 的非常安全的协议所取代。 +7. SSH 协议 : SSH( Secure Shell)是目前较可靠,专为远程登录会话和其他网络服务提供安全性的协议。利用 SSH 协议可以有效防止远程管理过程中的信息泄露问题。SSH 建立在可靠的传输协议 TCP 之上。 +...... +运行于 UDP 协议之上的协议 : + +1. DHCP 协议:动态主机配置协议,动态配置 IP 地址 +2. DNS : 域名系统(DNS,Domain Name System)将人类可读的域名 (例如,www.baidu.com) 转换为机器可读的 IP 地址 (例如,220.181.38.148)。 我们可以将其理解为专为互联网设计的电话薄。实际上 DNS 同时支持 UDP 和 TCP 协议。 \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/090.\345\246\202\344\275\225\350\247\243\345\206\263 TCP \344\274\240\350\276\223\344\270\242\345\214\205\351\227\256\351\242\230\357\274\237.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/090.\345\246\202\344\275\225\350\247\243\345\206\263 TCP \344\274\240\350\276\223\344\270\242\345\214\205\351\227\256\351\242\230\357\274\237.md" new file mode 100644 index 000000000..6ef34f597 --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/090.\345\246\202\344\275\225\350\247\243\345\206\263 TCP \344\274\240\350\276\223\344\270\242\345\214\205\351\227\256\351\242\230\357\274\237.md" @@ -0,0 +1,10 @@ +--- +title: 如何解决 TCP 传输丢包问题? +date: 2022-09-21 22:36:03 +permalink: /pages/a656a9/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/100.TCP \351\225\277\350\277\236\346\216\245\345\222\214\347\237\255\350\277\236\346\216\245\346\234\211\351\202\243\344\271\210\344\270\215\345\220\214\347\232\204\344\275\277\347\224\250\345\234\272\346\231\257\357\274\237.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/100.TCP \351\225\277\350\277\236\346\216\245\345\222\214\347\237\255\350\277\236\346\216\245\346\234\211\351\202\243\344\271\210\344\270\215\345\220\214\347\232\204\344\275\277\347\224\250\345\234\272\346\231\257\357\274\237.md" new file mode 100644 index 000000000..f8be97424 --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/100.TCP \351\225\277\350\277\236\346\216\245\345\222\214\347\237\255\350\277\236\346\216\245\346\234\211\351\202\243\344\271\210\344\270\215\345\220\214\347\232\204\344\275\277\347\224\250\345\234\272\346\231\257\357\274\237.md" @@ -0,0 +1,38 @@ +--- +title: TCP 长连接和短连接有那么不同的使用场景? +date: 2022-09-21 22:36:19 +permalink: /pages/dda19e/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- +## 前提: +HTTP/1.0默认使用短连接,HTTP/1.1开始默认使用长连接; + +HTTP协议的长连接和短连接,实质就是TCP协议的长连接和短连接; + +TCP协议建立连接需要3次握手,断开连接需要4次握手,这个过程会消耗网络资源和时间; + +## 定义: +* 长连接:在一个TCP连接上可以发送多个数据包,但是如果没有数据包发送时,也要双方发检测包以维持这个长连接;三次握手后连接,不断开连接,保持客户端和服务端通信,直到服务器超时自动断开连接,或者客户端主动断开连接。 +* 短连接:当双方需要数据交互的时候,就建立一个TCP连接,本次交互完之后就断开这个连接;三次握手后建立连接,发送数据包并得到服务器返回的结果后,通过客户端和服务器的四次握手后断开连接。 + +### 优缺点: +长连接可以省去较多建立连接和断开连接的操作,所以比较节省资源和时间,但是长连接如果一直存在的话,需要很多探测包的发送来维持这个连接,这对服务器将是很大的负担; +相对而言,短连接不需要服务器承担太大负担,只要存在的连接就是有用的连接,但如果客户端请求频繁,就会在TCP的建立连接和断开连接上浪费较大的资源和时间。 + +### 使用场景: +短连接:适用于网页浏览等数据刷新频度较低的场景。一般而言像及京东,淘宝这些大型网站,随时都会有成千上万的用户请求,一般使用短连接,用户量太大,服务器扛不住那么多长连接; +长连接:适用于客户端和服务端通信频繁的场景,例如:聊天室,实时游戏等场景。即时通讯(QQ)般使用的都是长连接,但并不是永久连接(比如20分钟,半个小时),因为即时通讯是频繁的发送请求,使用长连接只需要建立一次连接,同时再根据业务设置保持时间,超过这个时间就会断开连接,一定程度上保证了服务器的压力不会过大。 + +##Socket心跳包机制: +像心跳一样,每隔固定时间向服务器发送一个包,以此来告诉服务器,这个客户端还活着。为了保持长连接,一般都是很小的包(节约流量)或者只有包头的空包。 +1.客户端每隔一段时间间隔就发送一个探测包给服务器; +2.客户端发包时启动一个超时定时器; +3.服务端接收到探测包后会回应一个包; +4.如果客户端收到服务器的应答包,则说明服务器正常,删除超时定时器;如果没有收到则服务器异常。 +———————————————— +版权声明:本文为CSDN博主「测试小明」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 +原文链接:https://blog.csdn.net/Dave0002/article/details/125973214 \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/110.TCP \347\232\204 keepalive \344\272\206\350\247\243\345\220\227\357\274\237\350\257\264\344\270\200\350\257\264\345\256\203\345\222\214 HTTP \347\232\204 keepalive \347\232\204\345\214\272\345\210\253\357\274\237.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/110.TCP \347\232\204 keepalive \344\272\206\350\247\243\345\220\227\357\274\237\350\257\264\344\270\200\350\257\264\345\256\203\345\222\214 HTTP \347\232\204 keepalive \347\232\204\345\214\272\345\210\253\357\274\237.md" new file mode 100644 index 000000000..9df3e6793 --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/110.TCP \347\232\204 keepalive \344\272\206\350\247\243\345\220\227\357\274\237\350\257\264\344\270\200\350\257\264\345\256\203\345\222\214 HTTP \347\232\204 keepalive \347\232\204\345\214\272\345\210\253\357\274\237.md" @@ -0,0 +1,17 @@ +--- +title: TCP 的 keepalive 了解吗?说一说它和 HTTP 的 keepalive 的区别? +date: 2022-09-21 22:36:40 +permalink: /pages/212438/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- +HTTP Keep-Alive 也叫 HTTP 长连接,该函数由应用程序实现,允许使用相同的TCP连接发送和接收多个HTTP请求/响应,减少由HTTP短途连接产生的多个TCP连接的创建和释放成本。 + + +TCP的存活者 也叫 TCP 保活机制,这个函数由内核实现,当客户和服务端在一定时间内与数据不交互时,为了确保连接仍然有效,就会发送探测报文,检查对方是否还在网上,然后决定是否关闭连接 + +参考: +[阿里一面:TCP 的 Keepalive 和 HTTP 的 Keep-Alive 是一个东西吗](https://www.wangsu123.cn/news/27546.html) \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/120.HTTP \344\270\216 HTTPS \346\234\211\345\223\252\344\272\233\345\214\272\345\210\253\357\274\237.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/120.HTTP \344\270\216 HTTPS \346\234\211\345\223\252\344\272\233\345\214\272\345\210\253\357\274\237.md" new file mode 100644 index 000000000..bcd3b1740 --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/120.HTTP \344\270\216 HTTPS \346\234\211\345\223\252\344\272\233\345\214\272\345\210\253\357\274\237.md" @@ -0,0 +1,39 @@ +--- +title: HTTP 与 HTTPS 有哪些区别? +date: 2022-09-21 22:36:56 +permalink: /pages/253334/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- +## HTTP 协议介绍 +HTTP 协议,全称超文本传输协议(Hypertext Transfer Protocol)。顾名思义,HTTP 协议就是用来规范超文本的传输,超文本,也就是网络上的包括文本在内的各式各样的消息,具体来说,主要是来规范浏览器和服务器端的行为的。 + +并且,HTTP 是一个无状态(stateless)协议,也就是说服务器不维护任何有关客户端过去所发请求的消息。这其实是一种懒政,有状态协议会更加复杂,需要维护状态(历史信息),而且如果客户或服务器失效,会产生状态的不一致,解决这种不一致的代价更高。 + +## HTTP 协议通信过程 +HTTP 是应用层协议,它以 TCP(传输层)作为底层协议,默认端口为 80. 通信过程主要如下: + +1. 服务器在 80 端口等待客户的请求。 +2. 浏览器发起到服务器的 TCP 连接(创建套接字 Socket)。 +3. 服务器接收来自浏览器的 TCP 连接。 +4. 浏览器(HTTP 客户端)与 Web 服务器(HTTP 服务器)交换 HTTP 消息。 +4. 关闭 TCP 连接。 +## HTTP 协议优点 +扩展性强、速度快、跨平台支持性好。 + +## HTTPS 协议 +### HTTPS 协议介绍 +HTTPS 协议(Hyper Text Transfer Protocol Secure),是 HTTP 的加强安全版本。HTTPS 是基于 HTTP 的,也是用 TCP 作为底层协议,并额外使用 SSL/TLS 协议用作加密和安全认证。默认端口号是 443. + +HTTPS 协议中,SSL 通道通常使用基于密钥的加密算法,密钥长度通常是 40 比特或 128 比特。 + +### HTTPS 协议优点 +保密性好、信任度高。 + +## 总结 +端口号 :HTTP 默认是 80,HTTPS 默认是 443。 +URL 前缀 :HTTP 的 URL 前缀是 http://,HTTPS 的 URL 前缀是 https://。 +安全性和资源消耗 : HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS 是运行在 SSL/TLS 之上的 HTTP 协议,SSL/TLS 运行在 TCP 之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源。 \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/130.TCP \344\270\255\345\270\270\350\247\201\347\232\204\346\213\245\345\241\236\346\216\247\345\210\266\347\256\227\346\263\225\346\234\211\345\223\252\344\272\233\357\274\237.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/130.TCP \344\270\255\345\270\270\350\247\201\347\232\204\346\213\245\345\241\236\346\216\247\345\210\266\347\256\227\346\263\225\346\234\211\345\223\252\344\272\233\357\274\237.md" new file mode 100644 index 000000000..18615b761 --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/130.TCP \344\270\255\345\270\270\350\247\201\347\232\204\346\213\245\345\241\236\346\216\247\345\210\266\347\256\227\346\263\225\346\234\211\345\223\252\344\272\233\357\274\237.md" @@ -0,0 +1,10 @@ +--- +title: TCP 中常见的拥塞控制算法有哪些? +date: 2022-09-21 22:37:12 +permalink: /pages/9ccc31/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/140.\347\256\200\350\277\260 HTTP 1.0\357\274\2141.1\357\274\2142.0 \347\232\204\344\270\273\350\246\201\345\214\272\345\210\253 .md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/140.\347\256\200\350\277\260 HTTP 1.0\357\274\2141.1\357\274\2142.0 \347\232\204\344\270\273\350\246\201\345\214\272\345\210\253 .md" new file mode 100644 index 000000000..35b0189b7 --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/140.\347\256\200\350\277\260 HTTP 1.0\357\274\2141.1\357\274\2142.0 \347\232\204\344\270\273\350\246\201\345\214\272\345\210\253 .md" @@ -0,0 +1,46 @@ +--- +title: 简述 HTTP 1.0,1.1,2.0 的主要区别 +date: 2022-09-21 22:37:24 +permalink: /pages/f50b70/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- +## 1 HTTP1.0和HTTP1.1的区别 +### 1.1 长连接(Persistent Connection) + HTTP1.1支持长连接和请求的流水线处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟,在HTTP1.1中默认开启长连接keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点。HTTP1.0需要使用keep-alive参数来告知服务器端要建立一个长连接。 + +### 1.2 节约带宽 + HTTP1.0中存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能。HTTP1.1支持只发送header信息(不带任何body信息),如果服务器认为客户端有权限请求服务器,则返回100,客户端接收到100才开始把请求body发送到服务器;如果返回401,客户端就可以不用发送请求body了节约了带宽。 + +### 1.3 HOST域 + 在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname),HTTP1.0没有host域。随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都支持host域,且请求消息中如果没有host域会报告一个错误(400 Bad Request)。 + +### 1.4缓存处理 + 在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。 + +### 1.5错误通知的管理 + 在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。 + +## 2 HTTP1.1和HTTP2.0的区别 +### 2.1 多路复用 + HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。HTTP1.1也可以多建立几个TCP连接,来支持处理更多并发的请求,但是创建TCP连接本身也是有开销的。 + + + +### 2.2 头部数据压缩 + 在HTTP1.1中,HTTP请求和响应都是由状态行、请求/响应头部、消息主体三部分组成。一般而言,消息主体都会经过gzip压缩,或者本身传输的就是压缩过后的二进制文件,但状态行和头部却没有经过任何压缩,直接以纯文本传输。随着Web功能越来越复杂,每个页面产生的请求数也越来越多,导致消耗在头部的流量越来越多,尤其是每次都要传输UserAgent、Cookie这类不会频繁变动的内容,完全是一种浪费。 + + HTTP1.1不支持header数据的压缩,HTTP2.0使用HPACK算法对header的数据进行压缩,这样数据体积小了,在网络上传输就会更快。 + +### 2.3 服务器推送 + 服务端推送是一种在客户端请求之前发送数据的机制。网页使用了许多资源:HTML、样式表、脚本、图片等等。在HTTP1.1中这些资源每一个都必须明确地请求。这是一个很慢的过程。浏览器从获取HTML开始,然后在它解析和评估页面的时候,增量地获取更多的资源。因为服务器必须等待浏览器做每一个请求,网络经常是空闲的和未充分使用的。 + + 为了改善延迟,HTTP2.0引入了server push,它允许服务端推送资源给浏览器,在浏览器明确地请求之前,免得客户端再次创建连接发送请求到服务器端获取。这样客户端可以直接从本地加载这些资源,不用再通过网络。 + + + +参考: +[HTTP1.0、HTTP1.1 和 HTTP2.0 的区别](https://juejin.cn/post/6844903489596833800) \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/150.\344\273\216\347\263\273\347\273\237\345\261\202\351\235\242\344\270\212\357\274\214UDP\345\246\202\344\275\225\344\277\235\350\257\201\345\260\275\351\207\217\345\217\257\351\235\240\357\274\237.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/150.\344\273\216\347\263\273\347\273\237\345\261\202\351\235\242\344\270\212\357\274\214UDP\345\246\202\344\275\225\344\277\235\350\257\201\345\260\275\351\207\217\345\217\257\351\235\240\357\274\237.md" new file mode 100644 index 000000000..400eb5481 --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/150.\344\273\216\347\263\273\347\273\237\345\261\202\351\235\242\344\270\212\357\274\214UDP\345\246\202\344\275\225\344\277\235\350\257\201\345\260\275\351\207\217\345\217\257\351\235\240\357\274\237.md" @@ -0,0 +1,22 @@ +--- +title: 从系统层面上,UDP如何保证尽量可靠? +date: 2022-09-21 22:37:36 +permalink: /pages/e57cfe/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- +基本的思想:模拟tcp的可靠机制来实现,保证四个无即可(无丢失、无失序、无错误、无重复) + 可以从以下几点着手: + ①加上序号 + ②确认机制,超时重传 + ③校验机制,出错重传 + +我们都知道UDP是面向无连接,不可靠的传输方式,而TCP虽然是面向连接,可靠地连接,但是其过程太繁琐,效率太低,传输速度很慢,所以我们可以利用UDP进行底层传输数据,上层进行封装,来弥补UDP的不足。 +做到以下4点,就可以保证UDP协议可靠: +①不要TCP的三次握手和四次挥手。 +②发送方每次发出的数据进行编号,同时保持顺序的正确。 +③每次接收方接收到数据,发出应答信号。同时发送方在规定的时间检测是否接收到应答,如果没有接收到应答,重发,三次后还未收到应答直接判断发送失败。 +④发送数据时,发送方增加校验位。如果接收方校验出错,请求重发。 \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/160.TCP \346\214\245\346\211\213\346\227\266\345\207\272\347\216\260\345\244\247\351\207\217 CLOSE_WAIT \346\210\226 TIME_WAIT \346\200\216\344\271\210\350\247\243\345\206\263\357\274\237.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/160.TCP \346\214\245\346\211\213\346\227\266\345\207\272\347\216\260\345\244\247\351\207\217 CLOSE_WAIT \346\210\226 TIME_WAIT \346\200\216\344\271\210\350\247\243\345\206\263\357\274\237.md" new file mode 100644 index 000000000..7f430fa96 --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/160.TCP \346\214\245\346\211\213\346\227\266\345\207\272\347\216\260\345\244\247\351\207\217 CLOSE_WAIT \346\210\226 TIME_WAIT \346\200\216\344\271\210\350\247\243\345\206\263\357\274\237.md" @@ -0,0 +1,11 @@ +--- +title: TCP 挥手时出现大量 CLOSE_WAIT 或 TIME_WAIT 怎么解决? +date: 2022-09-21 22:37:51 +permalink: /pages/15d37a/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- + diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/170.\347\256\200\350\277\260 TCP \346\273\221\345\212\250\347\252\227\345\217\243\344\273\245\345\217\212\351\207\215\344\274\240\346\234\272\345\210\266.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/170.\347\256\200\350\277\260 TCP \346\273\221\345\212\250\347\252\227\345\217\243\344\273\245\345\217\212\351\207\215\344\274\240\346\234\272\345\210\266.md" new file mode 100644 index 000000000..fff60e5ac --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/170.\347\256\200\350\277\260 TCP \346\273\221\345\212\250\347\252\227\345\217\243\344\273\245\345\217\212\351\207\215\344\274\240\346\234\272\345\210\266.md" @@ -0,0 +1,10 @@ +--- +title: 简述 TCP 滑动窗口以及重传机制 +date: 2022-09-21 22:38:09 +permalink: /pages/dae451/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/180.\347\256\200\350\277\260 JWT \347\232\204\345\216\237\347\220\206\345\222\214\346\240\241\351\252\214\346\234\272\345\210\266.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/180.\347\256\200\350\277\260 JWT \347\232\204\345\216\237\347\220\206\345\222\214\346\240\241\351\252\214\346\234\272\345\210\266.md" new file mode 100644 index 000000000..059146943 --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/180.\347\256\200\350\277\260 JWT \347\232\204\345\216\237\347\220\206\345\222\214\346\240\241\351\252\214\346\234\272\345\210\266.md" @@ -0,0 +1,117 @@ +--- +title: 简述 JWT 的原理和校验机制 +date: 2022-09-21 22:38:22 +permalink: /pages/34d064/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- + +## 什么是 Token(令牌) +### Acesss Token +* 访问资源接口(API)时所需要的资源凭证 +* 简单 token 的组成: uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串) +* 特点: + * 服务端无状态化、可扩展性好 + * 支持移动端设备 + * 安全 + * 支持跨程序调用 +* token 的身份验证流程: + +![](https://tva1.sinaimg.cn/large/e6c9d24ely1h6j6f8asmdj20hs08amxe.jpg) + +1. 客户端使用用户名跟密码请求登录 +2. 服务端收到请求,去验证用户名与密码 +3. 验证成功后,服务端会签发一个 token 并把这个 token 发送给客户端 +4. 客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localStorage 里 +5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 token +6. 服务端收到请求,然后去验证客户端请求里面带着的 token ,如果验证成功,就向客户端返回请求的数据 +* 每一次请求都需要携带 token,需要把 token 放到 HTTP 的 Header 里 +* 基于 token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 token 数据。用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库 +* token 完全由应用管理,所以它可以避开同源策略 + +### Refresh Token +* 另外一种 token——refresh token +* refresh token 是专用于刷新 access token 的 token。如果没有 refresh token,也可以刷新 access token,但每次刷新都要用户输入登录用户名与密码,会很麻烦。有了 refresh token,可以减少这个麻烦,客户端直接用 refresh token 去更新 access token,无需用户进行额外的操作。 + +![](https://tva1.sinaimg.cn/large/e6c9d24ely1h6j6gk1l0sj20hs0bw3z5.jpg) + +* Access Token 的有效期比较短,当 Acesss Token 由于过期而失效时,使用 Refresh Token 就可以获取到新的 Token,如果 Refresh Token 也失效了,用户就只能重新登录了。 +* Refresh Token 及过期时间是存储在服务器的数据库中,只有在申请新的 Acesss Token 时才会验证,不会对业务接口响应时间造成影响,也不需要向 Session 一样一直保持在内存中以应对大量的请求。 +## Token 和 Session 的区别 +* Session 是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息。而 Token 是令牌,访问资源接口(API)时所需要的资源凭证。Token 使服务端无状态化,不会存储会话信息。 + +* Session 和 Token 并不矛盾,作为身份认证 Token 安全性比 Session 好,因为每一个请求都有签名还能防止监听以及重放攻击,而 Session 就必须依赖链路层来保障通讯安全了。如果你需要实现有状态的会话,仍然可以增加 Session 来在服务器端保存一些状态。 + +* 所谓 Session 认证只是简单的把 User 信息存储到 Session 里,因为 SessionID 的不可预测性,暂且认为是安全的。而 Token ,如果指的是 OAuth Token 或类似的机制的话,提供的是 认证 和 授权 ,认证是针对用户,授权是针对 App 。其目的是让某 App 有权利访问某用户的信息。这里的 Token 是唯一的。不可以转移到其它 App上,也不可以转到其它用户上。Session 只提供一种简单的认证,即只要有此 SessionID ,即认为有此 User 的全部权利。是需要严格保密的,这个数据应该只保存在站方,不应该共享给其它网站或者第三方 App。所以简单来说:如果你的用户数据可能需要和第三方共享,或者允许第三方调用 API 接口,用 Token 。如果永远只是自己的网站,自己的 App,用什么就无所谓了。 + +## 什么是 JWT +* JSON Web Token(简称 JWT)是目前最流行的跨域认证解决方案。 +* 是一种认证授权机制。 +* JWT 是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准(RFC 7519)。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。 +可以使用 HMAC 算法或者是 RSA 的公/私秘钥对 JWT 进行签名。因为数字签名的存在,这些传递的信息是可信的。 +阮一峰老师的 JSON Web Token 入门教程 讲的非常通俗易懂,这里就不再班门弄斧了 + +## 生成 JWT +* https://jwt.io/ +* https://www.jsonwebtoken.io/ + +## JWT 的原理 +![](https://tva1.sinaimg.cn/large/e6c9d24ely1h6j6ijg9ozj20hs0bp0t5.jpg) +### JWT 认证流程: + +* 用户输入用户名/密码登录,服务端认证成功后,会返回给客户端一个 JWT +* 客户端将 token 保存到本地(通常使用 localstorage,也可以使用 cookie) +* 当用户希望访问一个受保护的路由或者资源的时候,需要请求头的 Authorization 字段中使用Bearer 模式添加 JWT,其内容看起来是下面这样 +```java +Authorization: Bearer +``` +* 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为 +* 因为 JWT 是自包含的(内部包含了一些会话信息),因此减少了需要查询数据库的需要 +* 因为 JWT 并不使用 Cookie 的,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS) +* 因为用户的状态不再存储在服务端的内存中,所以这是一种无状态的认证机制 +## JWT 的使用方式 +客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。 +### 方式一 +* 当用户希望访问一个受保护的路由或者资源的时候,可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求头信息的 Authorization 字段里,使用 Bearer 模式添加 JWT。 +```html +GET /calendar/v1/events +Host: api.example.com +Authorization: Bearer +``` +* 用户的状态不会存储在服务端的内存中,这是一种 无状态的认证机制 +* 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为。 +* 由于 JWT 是自包含的,因此减少了需要查询数据库的需要 +* JWT 的这些特性使得我们可以完全依赖其无状态的特性提供数据 API 服务,甚至是创建一个下载流服务。 +* 因为 JWT 并不使用 Cookie ,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS) + +### 方式二 +* 跨域的时候,可以把 JWT 放在 POST 请求的数据体里。 +### 方式三 +通过 URL 传输 +```html +http://www.example.com/user?token=xxx +``` +## Token 和 JWT 的区别 +### 相同: + +* 都是访问资源的令牌 +* 都可以记录用户的信息 +* 都是使服务端无状态化 +* 都是只有验证成功后,客户端才能访问服务端上受保护的资源 + +### 区别: + +* Token:服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效。 +* JWT:将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。 + +## 常见的前后端鉴权方式 +Session-Cookie +Token 验证(包括 JWT,SSO) +OAuth2.0(开放授权) + + +参考: +[还分不清 Cookie、Session、Token、JWT?](https://zhuanlan.zhihu.com/p/164696755) \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/190.\344\270\272\344\273\200\344\271\210\351\234\200\350\246\201\345\272\217\345\210\227\345\214\226\357\274\237\346\234\211\344\273\200\344\271\210\345\272\217\345\210\227\345\214\226\347\232\204\346\226\271\345\274\217\357\274\237.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/190.\344\270\272\344\273\200\344\271\210\351\234\200\350\246\201\345\272\217\345\210\227\345\214\226\357\274\237\346\234\211\344\273\200\344\271\210\345\272\217\345\210\227\345\214\226\347\232\204\346\226\271\345\274\217\357\274\237.md" new file mode 100644 index 000000000..a496179ff --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/190.\344\270\272\344\273\200\344\271\210\351\234\200\350\246\201\345\272\217\345\210\227\345\214\226\357\274\237\346\234\211\344\273\200\344\271\210\345\272\217\345\210\227\345\214\226\347\232\204\346\226\271\345\274\217\357\274\237.md" @@ -0,0 +1,52 @@ +--- +title: 为什么需要序列化?有什么序列化的方式? +date: 2022-09-21 22:38:38 +permalink: /pages/68d6c8/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- +## 定义 +1)序列化:把对象转化为可传输的字节序列过程称为序列化。 + +2)反序列化:把字节序列还原为对象的过程称为反序列化。 + + + +## 2.为什么要序列化?实现序列化的作用是什么? +如果光看定义我想你很难一下子理解序列化的意义,那么我们可以从另一个角度来推导出什么是序列化, 那么究竟序列化的目的是什么? + +其实序列化最终的目的是为了对象可以跨平台存储,和进行网络传输。而我们进行跨平台存储和网络传输的方式就是IO,而我们的IO支持的数据格式就是字节数组。 + +因为我们单方面的只把对象转成字节数组还不行,因为没有规则的字节数组我们是没办法把对象的本来面目还原回来的,所以我们必须在把对象转成字节数组的时候就制定一种规则(序列化),那么我们从IO流里面读出数据的时候再以这种规则把对象还原回来(反序列化)。 + +如果我们要把一栋房子从一个地方运输到另一个地方去,序列化就是我把房子拆成一个个的砖块放到车子里,然后留下一张房子原来结构的图纸,反序列化就是我们把房子运输到了目的地以后,根据图纸把一块块砖头还原成房子原来面目的过程 + + + +## 3.什么情况下需要序列化? +通过上面我想你已经知道了凡是需要进行“跨平台存储”和”网络传输”的数据,都需要进行序列化。 + +本质上存储和网络传输 都需要经过 把一个对象状态保存成一种跨平台识别的字节格式,然后其他的平台才可以通过字节信息解析还原对象信息。 + +## 4.序列化的方式 +序列化只是一种拆装组装对象的规则,那么这种规则肯定也可能有多种多样,比如现在常见的序列化方式有: + +JDK(不支持跨语言)、JSON、XML、Hessian、Kryo(不支持跨语言)、Thrift、Protostuff、FST(不支持跨语言) + +## 5.序列化技术选型的几个关键点 +序列化协议各有千秋,不能简单的说一种序列化协议是最好的,只能从你的当时环境下去选择最适合你们的序列化协议,如果你要为你的公司项目进行序列化技术的选型,那么主要从以下几个因素。 + +协议是否支持跨平台 + +如果你们公司有好多种语言进行混合开发,那么就肯定不适合用有语言局限性的序列化协议,要不然你JDK序列化出来的格式,其他语言并没法支持。 + +序列化的速度 + +如果序列化的频率非常高,那么选择序列化速度快的协议会为你的系统性能提升不少。 + +序列化出来的大小 + +如果频繁的在网络中传输的数据那就需要数据越小越好,小的数据传输快,也不占带宽,也能整体提升系统的性能。 \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/200.\347\256\200\350\277\260 iPv4 \345\222\214 iPv6 \347\232\204\345\214\272\345\210\253.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/200.\347\256\200\350\277\260 iPv4 \345\222\214 iPv6 \347\232\204\345\214\272\345\210\253.md" new file mode 100644 index 000000000..ab5c96753 --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/200.\347\256\200\350\277\260 iPv4 \345\222\214 iPv6 \347\232\204\345\214\272\345\210\253.md" @@ -0,0 +1,11 @@ +--- +title: 简述 iPv4 和 iPv6 的区别 +date: 2022-09-21 22:38:54 +permalink: /pages/6a2e50/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- + diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/210.TCP \344\270\255 SYN \346\224\273\345\207\273\346\230\257\344\273\200\344\271\210\357\274\237\345\246\202\344\275\225\351\230\262\346\255\242\357\274\237.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/210.TCP \344\270\255 SYN \346\224\273\345\207\273\346\230\257\344\273\200\344\271\210\357\274\237\345\246\202\344\275\225\351\230\262\346\255\242\357\274\237.md" new file mode 100644 index 000000000..186c65373 --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/210.TCP \344\270\255 SYN \346\224\273\345\207\273\346\230\257\344\273\200\344\271\210\357\274\237\345\246\202\344\275\225\351\230\262\346\255\242\357\274\237.md" @@ -0,0 +1,10 @@ +--- +title: TCP 中 SYN 攻击是什么?如何防止? +date: 2022-09-21 22:39:09 +permalink: /pages/d0c6d3/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/220.\347\256\200\350\277\260 DDOS \346\224\273\345\207\273\345\216\237\347\220\206\357\274\214\345\246\202\344\275\225\351\230\262\350\214\203\345\256\203\357\274\237.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/220.\347\256\200\350\277\260 DDOS \346\224\273\345\207\273\345\216\237\347\220\206\357\274\214\345\246\202\344\275\225\351\230\262\350\214\203\345\256\203\357\274\237.md" new file mode 100644 index 000000000..c206ced3c --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/220.\347\256\200\350\277\260 DDOS \346\224\273\345\207\273\345\216\237\347\220\206\357\274\214\345\246\202\344\275\225\351\230\262\350\214\203\345\256\203\357\274\237.md" @@ -0,0 +1,10 @@ +--- +title: 简述 DDOS 攻击原理,如何防范它? +date: 2022-09-21 22:53:23 +permalink: /pages/056828/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/230.\344\273\200\344\271\210\346\230\257 ARP \345\215\217\350\256\256\357\274\237\347\256\200\350\277\260\345\205\266\344\275\277\347\224\250\345\234\272\346\231\257.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/230.\344\273\200\344\271\210\346\230\257 ARP \345\215\217\350\256\256\357\274\237\347\256\200\350\277\260\345\205\266\344\275\277\347\224\250\345\234\272\346\231\257.md" new file mode 100644 index 000000000..aefe4084c --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/230.\344\273\200\344\271\210\346\230\257 ARP \345\215\217\350\256\256\357\274\237\347\256\200\350\277\260\345\205\266\344\275\277\347\224\250\345\234\272\346\231\257.md" @@ -0,0 +1,10 @@ +--- +title: 什么是 ARP 协议?简述其使用场景 +date: 2022-09-21 22:53:54 +permalink: /pages/665162/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/240.\344\273\200\344\271\210\346\230\257\350\267\250\345\237\237\357\274\214\344\273\200\344\271\210\346\203\205\345\206\265\344\270\213\344\274\232\345\217\221\347\224\237\350\267\250\345\237\237\350\257\267\346\261\202\357\274\237.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/240.\344\273\200\344\271\210\346\230\257\350\267\250\345\237\237\357\274\214\344\273\200\344\271\210\346\203\205\345\206\265\344\270\213\344\274\232\345\217\221\347\224\237\350\267\250\345\237\237\350\257\267\346\261\202\357\274\237.md" new file mode 100644 index 000000000..d43535790 --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/240.\344\273\200\344\271\210\346\230\257\350\267\250\345\237\237\357\274\214\344\273\200\344\271\210\346\203\205\345\206\265\344\270\213\344\274\232\345\217\221\347\224\237\350\267\250\345\237\237\350\257\267\346\261\202\357\274\237.md" @@ -0,0 +1,23 @@ +--- +title: 什么是跨域,什么情况下会发生跨域请求? +date: 2022-09-21 22:54:11 +permalink: /pages/eadd05/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- +说起跨域请求,必须要了解浏览器的同源策略,同源策略/SOP(Same origin policy)是一种约定,由 Netscape 公司 1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到 XSS、CSFR 等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个 ip 地址,也非同源。 + +解决跨域请求的常用方法是: + +* 通过代理来避免,比如使用 Nginx 在后端转发请求,避免了前端出现跨域的问题。 +* 通过 Jsonp 跨域 +* 其它跨域解决方案 + +重点谈一下 Jsonp 跨域原理。浏览器的同源策略把跨域请求都禁止了,但是页面中的 \n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n57\n58\n59\n60\n61\n62\n63\n64\n65\n66\n67\n68\n69\n70\n71\n72\n73\n74\n75\n76\n77\n78\n79\n80\n81\n82\n83\n84\n85\n86\n87\n88\n89\n90\n91\n92\n93\n94\n95\n96\n97\n98\n99\n100\n101\n102\n103\n104\n105\n106\n107\n108\n109\n110\n111\n112\n113\n114\n115\n116\n117\n118\n119\n120\n121\n122\n123\n124\n125\n126\n127\n128\n129\n130\n131\n132\n133\n134\n135\n136\n137\n138\n139\n140\n141\n142\n143\n144\n145\n146\n147\n148\n149\n150\n151\n152\n153\n154\n155\n156\n157\n158\n159\n160\n161\n162\n163\n164\n165\n166\n167\n168\n169\n170\n171\n172\n173\n174\n175\n176\n177\n178\n179\n180\n181\n182\n183\n184\n185\n186\n187\n188\n189\n190\n191\n192\n193\n194\n195\n196\n197\n198\n199\n200\n201\n202\n203\n204\n205\n206\n207\n208\n209\n210\n211\n212\n213\n214\n215\n216\n217\n218\n219\n220\n221\n222\n223\n224\n225\n226\n227\n228\n229\n230\n231\n232\n233\n234\n235\n236\n237\n238\n239\n240\n241\n242\n243\n244\n245\n246\n247\n248\n249\n250\n251\n252\n253\n254\n255\n256\n257\n258\n259\n260\n261\n262\n263\n264\n265\n266\n267\n268\n269\n270\n271\n272\n273\n274\n275\n276\n277\n278\n279\n280\n281\n282\n283\n284\n285\n286\n287\n288\n289\n290\n291\n292\n293\n294\n295\n296\n297\n298\n299\n300\n301\n302\n303\n304\n305\n306\n307\n308\n309\n310\n311\n312\n313\n314\n315\n316\n317\n318\n319\n320\n321\n322\n323\n324\n325\n326\n327\n328\n329\n330\n331\n332\n333\n334\n335\n336\n337\n338\n339\n340\n341\n342\n343\n344\n345\n346\n347\n348\n349\n350\n351\n352\n353\n354\n355\n356\n357\n358\n359\n360\n361\n362\n363\n364\n365\n366\n367\n368\n369\n370\n371\n372\n373\n374\n375\n376\n377\n378\n379\n380\n381\n382\n383\n384\n385\n386\n387\n388\n389\n390\n391\n392\n393\n394\n395\n396\n397\n398\n399\n400\n401\n402\n403\n404\n405\n406\n407\n408\n409\n410\n411\n412\n413\n414\n415\n416\n417\n418\n419\n420\n421\n422\n423\n424\n425\n426\n427\n428\n429\n430\n431\n432\n433\n434\n435\n436\n437\n438\n439\n440\n441\n442\n443\n444\n445\n446\n447\n448\n449\n450\n451\n452\n453\n454\n455\n456\n457\n458\n459\n460\n461\n462\n463\n464\n465\n466\n467\n468\n469\n470\n471\n472\n473\n474\n475\n476\n477\n478\n479\n480\n481\n482\n483\n484\n485\n486\n487\n488\n489\n490\n491\n492\n493\n494\n495\n496\n497\n498\n499\n500\n501\n502\n503\n504\n505\n506\n507\n508\n509\n510\n511\n512\n513\n514\n515\n516\n517\n518\n519\n520\n521\n522\n523\n524\n525\n526\n527\n528\n529\n530\n531\n532\n533\n534\n535\n536\n537\n538\n539\n540\n541\n542\n543\n544\n545\n546\n547\n548\n549\n550\n551\n552\n553\n554\n555\n556\n557\n558\n559\n560\n561\n562\n563\n564\n565\n566\n567\n568\n569\n570\n571\n572\n573\n574\n575\n576\n577\n578\n579\n580\n581\n582\n583\n584\n585\n586\n587\n588\n589\n590\n591\n592\n593\n594\n595\n596\n597\n598\n599\n600\n601\n602\n603\n604\n605\n606\n607\n608\n609\n610\n611\n612\n613\n614\n615\n616\n617\n618\n619\n620\n621\n622\n623\n624\n625\n626\n627\n628\n629\n630\n631\n632\n633\n634\n635\n636\n637\n638\n639\n640\n641\n642\n643\n644\n645\n646\n647\n648\n649\n650\n651\n652\n653\n654\n655\n656\n657\n658\n659\n660\n661\n662\n663\n664\n665\n666\n667\n668\n669\n670\n671\n672\n673\n674\n675\n676\n677\n678\n679\n680\n681\n682\n683\n684\n685\n686\n687\n688\n689\n690\n691\n692\n693\n694\n695\n696\n697\n698\n699\n700\n701\n702\n703\n704\n705\n706\n707\n708\n709\n710\n711\n712\n713\n714\n715\n716\n717\n718\n719\n720\n721\n722\n723\n724\n725\n726\n727\n728\n729\n730\n731\n732\n733\n734\n735\n736\n737\n\n\n更多有趣的小网页欢迎关注公众号有趣研究社:\n\n> 手写春联\n> FC在线模拟器\n> 爱国头像生成器\n> 到账语音生成器", + "normalizedContent": "手写春联:https://cl.xugaoyi.com/\n\n\n# 前言\n\n虎年春节快到了,首先祝大家新年快乐,轻松暴富。 最近在网上经常看到生成春联的文章,不过这些小demo要么功能简陋,要么ui特别‘程序员’,满足不了我挑剔的眼光。干脆我自己做一个吧,顺便简单体验一下vite+vue3。(因为页面相对简单,vue组件风格还是使用选项式api,重点还是想把产品快速做出来。)\n\n\n\n\n# 产品构思\n\n包含手写春节和生成春联两大功能:\n\n * 手写春联\n \n * 模拟用笔写字的字迹\n * 选择画笔颜色\n * 调整画笔大小\n * 清空画布\n * 撤回笔画\n * 切换上、下联、横批、福字\n * 随机切换对联提示\n * 预览图片和下载\n * 贴春联海报和下载\n\n * 生成模式\n \n * 选择画笔颜色\n * 挑选生成的对联\n * 输入对联\n * 随机切换对联\n * 贴春联海报和下载\n\n * 其他\n \n * 快速切换模式按钮\n * 可控制的背景音乐\n * 微信分享网页\n\n\n# 设计\n\n\n\n\n# 开发\n\n * 技术栈\n * vite (打包&构建)\n * vue3 (页面开发)\n * vant(ui)\n * sass (css)\n * smooth-signature.js (带笔锋手写库)\n\n\n\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n57\n58\n59\n60\n61\n62\n63\n64\n65\n66\n67\n68\n69\n70\n71\n72\n73\n74\n75\n76\n77\n78\n79\n80\n81\n82\n83\n84\n85\n86\n87\n88\n89\n90\n91\n92\n93\n94\n95\n96\n97\n98\n99\n100\n101\n102\n103\n104\n105\n106\n107\n108\n109\n110\n111\n112\n113\n114\n115\n116\n117\n118\n119\n120\n121\n122\n123\n124\n125\n126\n127\n128\n129\n130\n131\n132\n133\n134\n135\n136\n137\n138\n139\n140\n141\n142\n143\n144\n145\n146\n147\n148\n149\n150\n151\n152\n153\n154\n155\n156\n157\n158\n159\n160\n161\n162\n163\n164\n165\n166\n167\n168\n169\n170\n171\n172\n173\n174\n175\n176\n177\n178\n179\n180\n181\n182\n183\n184\n185\n186\n187\n188\n189\n190\n191\n192\n193\n194\n195\n196\n197\n198\n199\n200\n201\n202\n203\n204\n205\n206\n207\n208\n209\n210\n211\n212\n213\n214\n215\n216\n217\n218\n219\n220\n221\n222\n223\n224\n225\n226\n227\n228\n229\n230\n231\n232\n233\n234\n235\n236\n237\n238\n239\n240\n241\n242\n243\n244\n245\n246\n247\n248\n249\n250\n251\n252\n253\n254\n255\n256\n257\n258\n259\n260\n261\n262\n263\n264\n265\n266\n267\n268\n269\n270\n271\n272\n273\n274\n275\n276\n277\n278\n279\n280\n281\n282\n283\n284\n285\n286\n287\n288\n289\n290\n291\n292\n293\n294\n295\n296\n297\n298\n299\n300\n301\n302\n303\n304\n305\n306\n307\n308\n309\n310\n311\n312\n313\n314\n315\n316\n317\n318\n319\n320\n321\n322\n323\n324\n325\n326\n327\n328\n329\n330\n331\n332\n333\n334\n335\n336\n337\n338\n339\n340\n341\n342\n343\n344\n345\n346\n347\n348\n349\n350\n351\n352\n353\n354\n355\n356\n357\n358\n359\n360\n361\n362\n363\n364\n365\n366\n367\n368\n369\n370\n371\n372\n373\n374\n375\n376\n377\n378\n379\n380\n381\n382\n383\n384\n385\n386\n387\n388\n389\n390\n391\n392\n393\n394\n395\n396\n397\n398\n399\n400\n401\n402\n403\n404\n405\n406\n407\n408\n409\n410\n411\n412\n413\n414\n415\n416\n417\n418\n419\n420\n421\n422\n423\n424\n425\n426\n427\n428\n429\n430\n431\n432\n433\n434\n435\n436\n437\n438\n439\n440\n441\n442\n443\n444\n445\n446\n447\n448\n449\n450\n451\n452\n453\n454\n455\n456\n457\n458\n459\n460\n461\n462\n463\n464\n465\n466\n467\n468\n469\n470\n471\n472\n473\n474\n475\n476\n477\n478\n479\n480\n481\n482\n483\n484\n485\n486\n487\n488\n489\n490\n491\n492\n493\n494\n495\n496\n497\n498\n499\n500\n501\n502\n503\n504\n505\n506\n507\n508\n509\n510\n511\n512\n513\n514\n515\n516\n517\n518\n519\n520\n521\n522\n523\n524\n525\n526\n527\n528\n529\n530\n531\n532\n533\n534\n535\n536\n537\n538\n539\n540\n541\n542\n543\n544\n545\n546\n547\n548\n549\n550\n551\n552\n553\n554\n555\n556\n557\n558\n559\n560\n561\n562\n563\n564\n565\n566\n567\n568\n569\n570\n571\n572\n573\n574\n575\n576\n577\n578\n579\n580\n581\n582\n583\n584\n585\n586\n587\n588\n589\n590\n591\n592\n593\n594\n595\n596\n597\n598\n599\n600\n601\n602\n603\n604\n605\n606\n607\n608\n609\n610\n611\n612\n613\n614\n615\n616\n617\n618\n619\n620\n621\n622\n623\n624\n625\n626\n627\n628\n629\n630\n631\n632\n633\n634\n635\n636\n637\n638\n639\n640\n641\n642\n643\n644\n645\n646\n647\n648\n649\n650\n651\n652\n653\n654\n655\n656\n657\n658\n659\n660\n661\n662\n663\n664\n665\n666\n667\n668\n669\n670\n671\n672\n673\n674\n675\n676\n677\n678\n679\n680\n681\n682\n683\n684\n685\n686\n687\n688\n689\n690\n691\n692\n693\n694\n695\n696\n697\n698\n699\n700\n701\n702\n703\n704\n705\n706\n707\n708\n709\n710\n711\n712\n713\n714\n715\n716\n717\n718\n719\n720\n721\n722\n723\n724\n725\n726\n727\n728\n729\n730\n731\n732\n733\n734\n735\n736\n737\n\n\n更多有趣的小网页欢迎关注公众号有趣研究社:\n\n> 手写春联\n> fc在线模拟器\n> 爱国头像生成器\n> 到账语音生成器", "charsets": { "cjk": true }, - "lastUpdated": "2022/06/20, 22:47:41", - "lastUpdatedTimestamp": 1655736461000 + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 }, { "title": "常用的前端轮子", @@ -7334,25 +8812,6 @@ export const siteData = { "lastUpdated": "2022/06/20, 22:47:41", "lastUpdatedTimestamp": 1655736461000 }, - { - "title": "归档", - "frontmatter": { - "archivesPage": true, - "title": "归档", - "permalink": "/archives/", - "article": false - }, - "regularPath": "/@pages/archivesPage.html", - "relativePath": "@pages/archivesPage.md", - "key": "v-48f7eec0", - "path": "/archives/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, { "title": "分类", "frontmatter": { @@ -7372,59 +8831,6 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, - { - "title": "标签", - "frontmatter": { - "tagsPage": true, - "title": "标签", - "permalink": "/tags/", - "article": false - }, - "regularPath": "/@pages/tagsPage.html", - "relativePath": "@pages/tagsPage.md", - "key": "v-64f9b920", - "path": "/tags/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, - { - "title": "你知道的越多,不知道的也就越多", - "frontmatter": { - "title": "你知道的越多,不知道的也就越多", - "date": "2020-05-06T15:52:40.000Z", - "permalink": "/pages/f2e63f", - "sidebar": "auto", - "categories": [ - "随笔" - ], - "tags": [ - "学习", - "知识", - "鸡汤" - ], - "author": { - "name": "xugaoyi", - "link": "https://github.com/xugaoyi" - } - }, - "regularPath": "/_posts/%E9%9A%8F%E7%AC%94/%E4%BD%A0%E7%9F%A5%E9%81%93%E7%9A%84%E8%B6%8A%E5%A4%9A%EF%BC%8C%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84%E4%B9%9F%E5%B0%B1%E8%B6%8A%E5%A4%9A.html", - "relativePath": "_posts/随笔/你知道的越多,不知道的也就越多.md", - "key": "v-2db11cb2", - "path": "/pages/f2e63f/", - "excerpt": "

不知道大家有没有发现,我们身边经常有这样的人,他们越是有能力的,越是有知识的,越是低调,越是谦逊,因为他们深知,知道的越多,不知道的也就越多。

\n", - "headersStr": null, - "content": "不知道大家有没有发现,我们身边经常有这样的人,他们越是有能力的,越是有知识的,越是低调,越是谦逊,因为他们深知,知道的越多,不知道的也就越多。\n\n你知道的越多,你不知道的也就越多,这是一句非常有哲理的话。\n\n每个人的知识面都是有限的,你有可能在某个领域会有较深的研究,成为这个领域里的专家,等到你站在高处的时候,才会发现,自己是多么的渺小,才知道自己有多少没有涉及的领域。知道的越多,疑惑、问题就会越来越多,对已知的质疑、疑虑、困惑就会越来越多。\n\n即使如此,我们也应该努力,至少可以成为某个领域的佼佼者。\n\n鸡汤1\n\n弱小的人,才习惯嘲讽与否定,而内心强大的人,从不吝啬赞美与鼓励。\n\n鸡汤2\n\n当代青年人都应该摆脱冷气,只管向上走,不必听从自暴自弃者的流言。能做事的做事,能发声的发声。有一份热,发一份光,就像萤火一般,也可以在黑暗里发一点光,不必等候炬火。", - "normalizedContent": "不知道大家有没有发现,我们身边经常有这样的人,他们越是有能力的,越是有知识的,越是低调,越是谦逊,因为他们深知,知道的越多,不知道的也就越多。\n\n你知道的越多,你不知道的也就越多,这是一句非常有哲理的话。\n\n每个人的知识面都是有限的,你有可能在某个领域会有较深的研究,成为这个领域里的专家,等到你站在高处的时候,才会发现,自己是多么的渺小,才知道自己有多少没有涉及的领域。知道的越多,疑惑、问题就会越来越多,对已知的质疑、疑虑、困惑就会越来越多。\n\n即使如此,我们也应该努力,至少可以成为某个领域的佼佼者。\n\n鸡汤1\n\n弱小的人,才习惯嘲讽与否定,而内心强大的人,从不吝啬赞美与鼓励。\n\n鸡汤2\n\n当代青年人都应该摆脱冷气,只管向上走,不必听从自暴自弃者的流言。能做事的做事,能发声的发声。有一份热,发一份光,就像萤火一般,也可以在黑暗里发一点光,不必等候炬火。", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, { "title": "拥抱生活,拥抱快乐", "frontmatter": { @@ -7458,61 +8864,21 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "我做了一个手写春联小网页,祝大家虎年暴富", + "title": "标签", "frontmatter": { - "title": "我做了一个手写春联小网页,祝大家虎年暴富", - "date": "2022-01-28T14:59:51.000Z", - "permalink": "/pages/829589/", - "titleTag": "原创", - "sidebar": "auto", - "categories": [ - "随笔" - ], - "tags": [ - null - ] - }, - "regularPath": "/_posts/%E9%9A%8F%E7%AC%94/%E6%88%91%E5%81%9A%E4%BA%86%E4%B8%80%E4%B8%AA%E6%89%8B%E5%86%99%E6%98%A5%E8%81%94%E5%B0%8F%E7%BD%91%E9%A1%B5%EF%BC%8C%E7%A5%9D%E5%A4%A7%E5%AE%B6%E8%99%8E%E5%B9%B4%E6%9A%B4%E5%AF%8C.html", - "relativePath": "_posts/随笔/我做了一个手写春联小网页,祝大家虎年暴富.md", - "key": "v-a5c5f278", - "path": "/pages/829589/", - "headers": [ - { - "level": 3, - "title": "前言", - "slug": "前言", - "normalizedTitle": "前言", - "charIndex": 33 - }, - { - "level": 3, - "title": "产品构思", - "slug": "产品构思", - "normalizedTitle": "产品构思", - "charIndex": 195 - }, - { - "level": 3, - "title": "设计", - "slug": "设计", - "normalizedTitle": "设计", - "charIndex": 478 - }, - { - "level": 3, - "title": "开发", - "slug": "开发", - "normalizedTitle": "开发", - "charIndex": 487 - } - ], - "excerpt": "

手写春联:https://cl.xugaoyi.com/

\n

# 前言

\n

虎年春节快到了,首先祝大家新年快乐,轻松暴富。\n最近在网上经常看到生成春联的文章,不过这些小demo要么功能简陋,要么UI特别‘程序员’,满足不了我挑剔的眼光。干脆我自己做一个吧,顺便简单体验一下vite+vue3。(因为页面相对简单,vue组件风格还是使用选项式api,重点还是想把产品快速做出来。)

\n", - "headersStr": "前言 产品构思 设计 开发", - "content": "手写春联:https://cl.xugaoyi.com/\n\n\n# 前言\n\n虎年春节快到了,首先祝大家新年快乐,轻松暴富。 最近在网上经常看到生成春联的文章,不过这些小demo要么功能简陋,要么UI特别‘程序员’,满足不了我挑剔的眼光。干脆我自己做一个吧,顺便简单体验一下vite+vue3。(因为页面相对简单,vue组件风格还是使用选项式api,重点还是想把产品快速做出来。)\n\n\n\n\n# 产品构思\n\n包含手写春节和生成春联两大功能:\n\n * 手写春联\n \n * 模拟用笔写字的字迹\n * 选择画笔颜色\n * 调整画笔大小\n * 清空画布\n * 撤回笔画\n * 切换上、下联、横批、福字\n * 随机切换对联提示\n * 预览图片和下载\n * 贴春联海报和下载\n\n * 生成模式\n \n * 选择画笔颜色\n * 挑选生成的对联\n * 输入对联\n * 随机切换对联\n * 贴春联海报和下载\n\n * 其他\n \n * 快速切换模式按钮\n * 可控制的背景音乐\n * 微信分享网页\n\n\n# 设计\n\n\n\n\n# 开发\n\n * 技术栈\n * vite (打包&构建)\n * vue3 (页面开发)\n * vant(ui)\n * sass (css)\n * smooth-signature.js (带笔锋手写库)\n\n\n\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n57\n58\n59\n60\n61\n62\n63\n64\n65\n66\n67\n68\n69\n70\n71\n72\n73\n74\n75\n76\n77\n78\n79\n80\n81\n82\n83\n84\n85\n86\n87\n88\n89\n90\n91\n92\n93\n94\n95\n96\n97\n98\n99\n100\n101\n102\n103\n104\n105\n106\n107\n108\n109\n110\n111\n112\n113\n114\n115\n116\n117\n118\n119\n120\n121\n122\n123\n124\n125\n126\n127\n128\n129\n130\n131\n132\n133\n134\n135\n136\n137\n138\n139\n140\n141\n142\n143\n144\n145\n146\n147\n148\n149\n150\n151\n152\n153\n154\n155\n156\n157\n158\n159\n160\n161\n162\n163\n164\n165\n166\n167\n168\n169\n170\n171\n172\n173\n174\n175\n176\n177\n178\n179\n180\n181\n182\n183\n184\n185\n186\n187\n188\n189\n190\n191\n192\n193\n194\n195\n196\n197\n198\n199\n200\n201\n202\n203\n204\n205\n206\n207\n208\n209\n210\n211\n212\n213\n214\n215\n216\n217\n218\n219\n220\n221\n222\n223\n224\n225\n226\n227\n228\n229\n230\n231\n232\n233\n234\n235\n236\n237\n238\n239\n240\n241\n242\n243\n244\n245\n246\n247\n248\n249\n250\n251\n252\n253\n254\n255\n256\n257\n258\n259\n260\n261\n262\n263\n264\n265\n266\n267\n268\n269\n270\n271\n272\n273\n274\n275\n276\n277\n278\n279\n280\n281\n282\n283\n284\n285\n286\n287\n288\n289\n290\n291\n292\n293\n294\n295\n296\n297\n298\n299\n300\n301\n302\n303\n304\n305\n306\n307\n308\n309\n310\n311\n312\n313\n314\n315\n316\n317\n318\n319\n320\n321\n322\n323\n324\n325\n326\n327\n328\n329\n330\n331\n332\n333\n334\n335\n336\n337\n338\n339\n340\n341\n342\n343\n344\n345\n346\n347\n348\n349\n350\n351\n352\n353\n354\n355\n356\n357\n358\n359\n360\n361\n362\n363\n364\n365\n366\n367\n368\n369\n370\n371\n372\n373\n374\n375\n376\n377\n378\n379\n380\n381\n382\n383\n384\n385\n386\n387\n388\n389\n390\n391\n392\n393\n394\n395\n396\n397\n398\n399\n400\n401\n402\n403\n404\n405\n406\n407\n408\n409\n410\n411\n412\n413\n414\n415\n416\n417\n418\n419\n420\n421\n422\n423\n424\n425\n426\n427\n428\n429\n430\n431\n432\n433\n434\n435\n436\n437\n438\n439\n440\n441\n442\n443\n444\n445\n446\n447\n448\n449\n450\n451\n452\n453\n454\n455\n456\n457\n458\n459\n460\n461\n462\n463\n464\n465\n466\n467\n468\n469\n470\n471\n472\n473\n474\n475\n476\n477\n478\n479\n480\n481\n482\n483\n484\n485\n486\n487\n488\n489\n490\n491\n492\n493\n494\n495\n496\n497\n498\n499\n500\n501\n502\n503\n504\n505\n506\n507\n508\n509\n510\n511\n512\n513\n514\n515\n516\n517\n518\n519\n520\n521\n522\n523\n524\n525\n526\n527\n528\n529\n530\n531\n532\n533\n534\n535\n536\n537\n538\n539\n540\n541\n542\n543\n544\n545\n546\n547\n548\n549\n550\n551\n552\n553\n554\n555\n556\n557\n558\n559\n560\n561\n562\n563\n564\n565\n566\n567\n568\n569\n570\n571\n572\n573\n574\n575\n576\n577\n578\n579\n580\n581\n582\n583\n584\n585\n586\n587\n588\n589\n590\n591\n592\n593\n594\n595\n596\n597\n598\n599\n600\n601\n602\n603\n604\n605\n606\n607\n608\n609\n610\n611\n612\n613\n614\n615\n616\n617\n618\n619\n620\n621\n622\n623\n624\n625\n626\n627\n628\n629\n630\n631\n632\n633\n634\n635\n636\n637\n638\n639\n640\n641\n642\n643\n644\n645\n646\n647\n648\n649\n650\n651\n652\n653\n654\n655\n656\n657\n658\n659\n660\n661\n662\n663\n664\n665\n666\n667\n668\n669\n670\n671\n672\n673\n674\n675\n676\n677\n678\n679\n680\n681\n682\n683\n684\n685\n686\n687\n688\n689\n690\n691\n692\n693\n694\n695\n696\n697\n698\n699\n700\n701\n702\n703\n704\n705\n706\n707\n708\n709\n710\n711\n712\n713\n714\n715\n716\n717\n718\n719\n720\n721\n722\n723\n724\n725\n726\n727\n728\n729\n730\n731\n732\n733\n734\n735\n736\n737\n\n\n更多有趣的小网页欢迎关注公众号有趣研究社:\n\n> 手写春联\n> FC在线模拟器\n> 爱国头像生成器\n> 到账语音生成器", - "normalizedContent": "手写春联:https://cl.xugaoyi.com/\n\n\n# 前言\n\n虎年春节快到了,首先祝大家新年快乐,轻松暴富。 最近在网上经常看到生成春联的文章,不过这些小demo要么功能简陋,要么ui特别‘程序员’,满足不了我挑剔的眼光。干脆我自己做一个吧,顺便简单体验一下vite+vue3。(因为页面相对简单,vue组件风格还是使用选项式api,重点还是想把产品快速做出来。)\n\n\n\n\n# 产品构思\n\n包含手写春节和生成春联两大功能:\n\n * 手写春联\n \n * 模拟用笔写字的字迹\n * 选择画笔颜色\n * 调整画笔大小\n * 清空画布\n * 撤回笔画\n * 切换上、下联、横批、福字\n * 随机切换对联提示\n * 预览图片和下载\n * 贴春联海报和下载\n\n * 生成模式\n \n * 选择画笔颜色\n * 挑选生成的对联\n * 输入对联\n * 随机切换对联\n * 贴春联海报和下载\n\n * 其他\n \n * 快速切换模式按钮\n * 可控制的背景音乐\n * 微信分享网页\n\n\n# 设计\n\n\n\n\n# 开发\n\n * 技术栈\n * vite (打包&构建)\n * vue3 (页面开发)\n * vant(ui)\n * sass (css)\n * smooth-signature.js (带笔锋手写库)\n\n\n\n\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n57\n58\n59\n60\n61\n62\n63\n64\n65\n66\n67\n68\n69\n70\n71\n72\n73\n74\n75\n76\n77\n78\n79\n80\n81\n82\n83\n84\n85\n86\n87\n88\n89\n90\n91\n92\n93\n94\n95\n96\n97\n98\n99\n100\n101\n102\n103\n104\n105\n106\n107\n108\n109\n110\n111\n112\n113\n114\n115\n116\n117\n118\n119\n120\n121\n122\n123\n124\n125\n126\n127\n128\n129\n130\n131\n132\n133\n134\n135\n136\n137\n138\n139\n140\n141\n142\n143\n144\n145\n146\n147\n148\n149\n150\n151\n152\n153\n154\n155\n156\n157\n158\n159\n160\n161\n162\n163\n164\n165\n166\n167\n168\n169\n170\n171\n172\n173\n174\n175\n176\n177\n178\n179\n180\n181\n182\n183\n184\n185\n186\n187\n188\n189\n190\n191\n192\n193\n194\n195\n196\n197\n198\n199\n200\n201\n202\n203\n204\n205\n206\n207\n208\n209\n210\n211\n212\n213\n214\n215\n216\n217\n218\n219\n220\n221\n222\n223\n224\n225\n226\n227\n228\n229\n230\n231\n232\n233\n234\n235\n236\n237\n238\n239\n240\n241\n242\n243\n244\n245\n246\n247\n248\n249\n250\n251\n252\n253\n254\n255\n256\n257\n258\n259\n260\n261\n262\n263\n264\n265\n266\n267\n268\n269\n270\n271\n272\n273\n274\n275\n276\n277\n278\n279\n280\n281\n282\n283\n284\n285\n286\n287\n288\n289\n290\n291\n292\n293\n294\n295\n296\n297\n298\n299\n300\n301\n302\n303\n304\n305\n306\n307\n308\n309\n310\n311\n312\n313\n314\n315\n316\n317\n318\n319\n320\n321\n322\n323\n324\n325\n326\n327\n328\n329\n330\n331\n332\n333\n334\n335\n336\n337\n338\n339\n340\n341\n342\n343\n344\n345\n346\n347\n348\n349\n350\n351\n352\n353\n354\n355\n356\n357\n358\n359\n360\n361\n362\n363\n364\n365\n366\n367\n368\n369\n370\n371\n372\n373\n374\n375\n376\n377\n378\n379\n380\n381\n382\n383\n384\n385\n386\n387\n388\n389\n390\n391\n392\n393\n394\n395\n396\n397\n398\n399\n400\n401\n402\n403\n404\n405\n406\n407\n408\n409\n410\n411\n412\n413\n414\n415\n416\n417\n418\n419\n420\n421\n422\n423\n424\n425\n426\n427\n428\n429\n430\n431\n432\n433\n434\n435\n436\n437\n438\n439\n440\n441\n442\n443\n444\n445\n446\n447\n448\n449\n450\n451\n452\n453\n454\n455\n456\n457\n458\n459\n460\n461\n462\n463\n464\n465\n466\n467\n468\n469\n470\n471\n472\n473\n474\n475\n476\n477\n478\n479\n480\n481\n482\n483\n484\n485\n486\n487\n488\n489\n490\n491\n492\n493\n494\n495\n496\n497\n498\n499\n500\n501\n502\n503\n504\n505\n506\n507\n508\n509\n510\n511\n512\n513\n514\n515\n516\n517\n518\n519\n520\n521\n522\n523\n524\n525\n526\n527\n528\n529\n530\n531\n532\n533\n534\n535\n536\n537\n538\n539\n540\n541\n542\n543\n544\n545\n546\n547\n548\n549\n550\n551\n552\n553\n554\n555\n556\n557\n558\n559\n560\n561\n562\n563\n564\n565\n566\n567\n568\n569\n570\n571\n572\n573\n574\n575\n576\n577\n578\n579\n580\n581\n582\n583\n584\n585\n586\n587\n588\n589\n590\n591\n592\n593\n594\n595\n596\n597\n598\n599\n600\n601\n602\n603\n604\n605\n606\n607\n608\n609\n610\n611\n612\n613\n614\n615\n616\n617\n618\n619\n620\n621\n622\n623\n624\n625\n626\n627\n628\n629\n630\n631\n632\n633\n634\n635\n636\n637\n638\n639\n640\n641\n642\n643\n644\n645\n646\n647\n648\n649\n650\n651\n652\n653\n654\n655\n656\n657\n658\n659\n660\n661\n662\n663\n664\n665\n666\n667\n668\n669\n670\n671\n672\n673\n674\n675\n676\n677\n678\n679\n680\n681\n682\n683\n684\n685\n686\n687\n688\n689\n690\n691\n692\n693\n694\n695\n696\n697\n698\n699\n700\n701\n702\n703\n704\n705\n706\n707\n708\n709\n710\n711\n712\n713\n714\n715\n716\n717\n718\n719\n720\n721\n722\n723\n724\n725\n726\n727\n728\n729\n730\n731\n732\n733\n734\n735\n736\n737\n\n\n更多有趣的小网页欢迎关注公众号有趣研究社:\n\n> 手写春联\n> fc在线模拟器\n> 爱国头像生成器\n> 到账语音生成器", - "charsets": { - "cjk": true + "tagsPage": true, + "title": "标签", + "permalink": "/tags/", + "article": false }, + "regularPath": "/@pages/tagsPage.html", + "relativePath": "@pages/tagsPage.md", + "key": "v-64f9b920", + "path": "/tags/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, @@ -8029,8 +9395,8 @@ export const siteData = { "collapsable": true, "children": [ [ - "10.计算机网络/01.计算机网络.md", - "计算机网络", + "10.计算机网络/01.简述 TCP 三次握手以及四次挥手.md", + "简述 TCP 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手?", "/pages/e00207/" ], [ @@ -8042,6 +9408,146 @@ export const siteData = { "10.计算机网络/020.从输入 URL 到展现页面的全过程.md", "从输入 URL 到展现页面的全过程", "/pages/ab48ba/" + ], + [ + "10.计算机网络/030.HTTP 中 GET 和 POST 区别.md", + "HTTP 中 GET 和 POST 区别", + "/pages/a0e400/" + ], + [ + "10.计算机网络/040.TCP 怎么保证可靠传输.md", + "TCP 怎么保证可靠传输", + "/pages/5fcc49/" + ], + [ + "10.计算机网络/050.什么是 TCP 粘包和拆包?.md", + "什么是 TCP 粘包和拆包?", + "/pages/2822ac/" + ], + [ + "10.计算机网络/060.简述 HTTPS 的加密与认证过程.md", + "简述 HTTPS 的加密与认证过程", + "/pages/12c0cc/" + ], + [ + "10.计算机网络/070.简述对称与非对称加密的概念.md", + "简述对称与非对称加密的概念", + "/pages/057092/" + ], + [ + "10.计算机网络/080.TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别?.md", + "TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别?", + "/pages/cc4b24/" + ], + [ + "10.计算机网络/090.如何解决 TCP 传输丢包问题?.md", + "如何解决 TCP 传输丢包问题?", + "/pages/a656a9/" + ], + [ + "10.计算机网络/100.TCP 长连接和短连接有那么不同的使用场景?.md", + "TCP 长连接和短连接有那么不同的使用场景?", + "/pages/dda19e/" + ], + [ + "10.计算机网络/110.TCP 的 keepalive 了解吗?说一说它和 HTTP 的 keepalive 的区别?.md", + "TCP 的 keepalive 了解吗?说一说它和 HTTP 的 keepalive 的区别?", + "/pages/212438/" + ], + [ + "10.计算机网络/120.HTTP 与 HTTPS 有哪些区别?.md", + "HTTP 与 HTTPS 有哪些区别?", + "/pages/253334/" + ], + [ + "10.计算机网络/130.TCP 中常见的拥塞控制算法有哪些?.md", + "TCP 中常见的拥塞控制算法有哪些?", + "/pages/9ccc31/" + ], + [ + "10.计算机网络/140.简述 HTTP 1.0,1.1,2.0 的主要区别 .md", + "简述 HTTP 1.0,1.1,2.0 的主要区别", + "/pages/f50b70/" + ], + [ + "10.计算机网络/150.从系统层面上,UDP如何保证尽量可靠?.md", + "从系统层面上,UDP如何保证尽量可靠?", + "/pages/e57cfe/" + ], + [ + "10.计算机网络/160.TCP 挥手时出现大量 CLOSE_WAIT 或 TIME_WAIT 怎么解决?.md", + "TCP 挥手时出现大量 CLOSE_WAIT 或 TIME_WAIT 怎么解决?", + "/pages/15d37a/" + ], + [ + "10.计算机网络/170.简述 TCP 滑动窗口以及重传机制.md", + "简述 TCP 滑动窗口以及重传机制", + "/pages/dae451/" + ], + [ + "10.计算机网络/180.简述 JWT 的原理和校验机制.md", + "简述 JWT 的原理和校验机制", + "/pages/34d064/" + ], + [ + "10.计算机网络/190.为什么需要序列化?有什么序列化的方式?.md", + "为什么需要序列化?有什么序列化的方式?", + "/pages/68d6c8/" + ], + [ + "10.计算机网络/200.简述 iPv4 和 iPv6 的区别.md", + "简述 iPv4 和 iPv6 的区别", + "/pages/6a2e50/" + ], + [ + "10.计算机网络/210.TCP 中 SYN 攻击是什么?如何防止?.md", + "TCP 中 SYN 攻击是什么?如何防止?", + "/pages/d0c6d3/" + ], + [ + "10.计算机网络/220.简述 DDOS 攻击原理,如何防范它?.md", + "简述 DDOS 攻击原理,如何防范它?", + "/pages/056828/" + ], + [ + "10.计算机网络/230.什么是 ARP 协议?简述其使用场景.md", + "什么是 ARP 协议?简述其使用场景", + "/pages/665162/" + ], + [ + "10.计算机网络/240.什么是跨域,什么情况下会发生跨域请求?.md", + "什么是跨域,什么情况下会发生跨域请求?", + "/pages/eadd05/" + ], + [ + "10.计算机网络/250.Cookie 和 Session 的关系和区别是什么?.md", + "Cookie 和 Session 的关系和区别是什么?", + "/pages/980cc7/" + ], + [ + "10.计算机网络/260.简述 WebSocket 是如何进行传输的.md", + "简述 WebSocket 是如何进行传输的", + "/pages/172129/" + ], + [ + "10.计算机网络/270.DNS 查询服务器的基本流程是什么?DNS 劫持是什么?.md", + "DNS 查询服务器的基本流程是什么?DNS 劫持是什么?", + "/pages/017113/" + ], + [ + "10.计算机网络/280.RestFul 是什么?RestFul 请求的 URL 有什么特点?.md", + "RestFul 是什么?RestFul 请求的 URL 有什么特点?", + "/pages/c08bb6/" + ], + [ + "10.计算机网络/290.简述 HTTP 报文头部的组成结构.md", + "简述 HTTP 报文头部的组成结构", + "/pages/dfb780/" + ], + [ + "10.计算机网络/300.什么是 SYN flood,如何防止这类攻击?.md", + "什么是 SYN flood,如何防止这类攻击?", + "/pages/367c4d/" ] ] }, @@ -8050,9 +9556,114 @@ export const siteData = { "collapsable": true, "children": [ [ - "20.操作系统/01.操作系统.md", - "操作系统", + "20.操作系统/01.进程间有哪些通信方式?.md", + "进程间有哪些通信方式?", "/pages/e5f2d7/" + ], + [ + "20.操作系统/010.简述自旋锁与互斥锁的使用场景.md", + "简述自旋锁与互斥锁的使用场景", + "/pages/9e6232/" + ], + [ + "20.操作系统/020.进程和线程之间有什么区别?.md", + "进程和线程之间有什么区别?", + "/pages/c2fd8a/" + ], + [ + "20.操作系统/030.简述几个常用的 Linux 命令以及他们的功能.md", + "简述几个常用的 Linux 命令以及他们的功能", + "/pages/198887/" + ], + [ + "20.操作系统/040.创建线程有多少种方式?.md", + "创建线程有多少种方式?", + "/pages/888737/" + ], + [ + "20.操作系统/050.线程有多少种状态,状态之间如何转换.md", + "线程有多少种状态,状态之间如何转换", + "/pages/7de80d/" + ], + [ + "20.操作系统/060.进程有多少种状态?.md", + "进程有多少种状态?", + "/pages/165ea8/" + ], + [ + "20.操作系统/070.简述 select, poll, epoll 的使用场景以及区别.md", + "简述 select, poll, epoll 的使用场景以及区别", + "/pages/68ea00/" + ], + [ + "20.操作系统/080.Linux 下如何排查 CPU 以及 内存占用过多?.md", + "Linux 下如何排查 CPU 以及 内存占用过多?", + "/pages/b043a6/" + ], + [ + "20.操作系统/090.进程通信中的管道实现原理是什么?.md", + "进程通信中的管道实现原理是什么?", + "/pages/8e880a/" + ], + [ + "20.操作系统/100.Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程?.md", + "Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程?", + "/pages/326cf9/" + ], + [ + "20.操作系统/110.如何调试服务器内存占用过高的问题?.md", + "如何调试服务器内存占用过高的问题?", + "/pages/3653a2/" + ], + [ + "20.操作系统/120.Linux 如何查看实时的滚动日志?.md", + "Linux 如何查看实时的滚动日志?", + "/pages/f6e4d2/" + ], + [ + "20.操作系统/130.简述 Linux 零拷贝的原理.md", + "简述 Linux 零拷贝的原理", + "/pages/b43547/" + ], + [ + "20.操作系统/140.为什么进程切换慢,线程切换快?.md", + "为什么进程切换慢,线程切换快?", + "/pages/773d64/" + ], + [ + "20.操作系统/150.简述创建进程的流程.md", + "简述创建进程的流程", + "/pages/c64d20/" + ], + [ + "20.操作系统/160.简述 Linux 虚拟内存的页面置换算法.md", + "简述 Linux 虚拟内存的页面置换算法", + "/pages/93b870/" + ], + [ + "20.操作系统/170.简述 Linux 进程调度的算法.md", + "简述 Linux 进程调度的算法", + "/pages/36fd29/" + ], + [ + "20.操作系统/180.什么时候会由用户态陷入内核态?.md", + "什么时候会由用户态陷入内核态?", + "/pages/b84950/" + ], + [ + "20.操作系统/190.Linux 下如何查看端口被哪个进程占用?.md", + "Linux 下如何查看端口被哪个进程占用?", + "/pages/4c2d8e/" + ], + [ + "20.操作系统/200.Linux 中虚拟内存和物理内存有什么区别?有什么优点?.md", + "Linux 中虚拟内存和物理内存有什么区别?有什么优点?", + "/pages/3bab8c/" + ], + [ + "20.操作系统/210.简述 traceroute 命令的原理.md", + "简述 traceroute 命令的原理", + "/pages/655f8f/" ] ] }, From 1f4f58526f6ad85e7f94621fb70f0ac6aaa04d16 Mon Sep 17 00:00:00 2001 From: admirean <854818303@qq.com> Date: Sun, 9 Oct 2022 11:12:48 +0800 Subject: [PATCH 15/15] =?UTF-8?q?=E6=93=8D=E4=BD=9C=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E7=AB=A0=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...57\351\235\240\344\274\240\350\276\223.md" | 16 + ...11\345\223\252\344\272\233\357\274\237.md" | 1 + ...17\345\217\257\351\235\240\357\274\237.md" | 21 +- ...10\350\247\243\345\206\263\357\274\237.md" | 89 + ...21\345\212\250\347\252\227\345\217\243.md" | 99 + ...15\344\274\240\346\234\272\345\210\266.md" | 10 - ...15\344\274\240\346\234\272\345\210\266.md" | 135 + ...41\351\252\214\346\234\272\345\210\266.md" | 2 +- ...6 \347\232\204\345\214\272\345\210\253.md" | 198 + ...25\351\230\262\346\255\242\357\274\237.md" | 71 + ...62\350\214\203\345\256\203\357\274\237.md" | 59 + ...77\347\224\250\345\234\272\346\231\257.md" | 104 + ...14\344\274\240\350\276\223\347\232\204.md" | 87 + ...57\344\273\200\344\271\210\357\274\237.md" | 64 + ...10\347\211\271\347\202\271\357\274\237.md" | 128 + ...04\346\210\220\347\273\223\346\236\204.md" | 4 + ...73\346\224\273\345\207\273\357\274\237.md" | 10 - ...41\346\226\271\345\274\217\357\274\237.md" | 56 + ...10\345\214\272\345\210\253\357\274\237.md" | 34 + ...15\347\212\266\346\200\201\357\274\237.md" | 10 + ...45\345\217\212\345\214\272\345\210\253.md" | 216 + .../core/.temp/internal/app-enhancers.js | 2 +- .../core/.temp/internal/page-components.js | 117 +- .../@vuepress/core/.temp/internal/routes.js | 1380 ++--- .../@vuepress/core/.temp/internal/siteData.js | 5161 ++++++++++------- 25 files changed, 5167 insertions(+), 2907 deletions(-) create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/170.\347\256\200\350\277\260 TCP \346\273\221\345\212\250\347\252\227\345\217\243.md" delete mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/170.\347\256\200\350\277\260 TCP \346\273\221\345\212\250\347\252\227\345\217\243\344\273\245\345\217\212\351\207\215\344\274\240\346\234\272\345\210\266.md" create mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/171. \347\256\200\350\277\260TCP\351\207\215\344\274\240\346\234\272\345\210\266.md" delete mode 100644 "docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/300.\344\273\200\344\271\210\346\230\257 SYN flood\357\274\214\345\246\202\344\275\225\351\230\262\346\255\242\350\277\231\347\261\273\346\224\273\345\207\273\357\274\237.md" diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/040.TCP \346\200\216\344\271\210\344\277\235\350\257\201\345\217\257\351\235\240\344\274\240\350\276\223.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/040.TCP \346\200\216\344\271\210\344\277\235\350\257\201\345\217\257\351\235\240\344\274\240\350\276\223.md" index e88fbddd4..4baa89097 100644 --- "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/040.TCP \346\200\216\344\271\210\344\277\235\350\257\201\345\217\257\351\235\240\344\274\240\350\276\223.md" +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/040.TCP \346\200\216\344\271\210\344\277\235\350\257\201\345\217\257\351\235\240\344\274\240\350\276\223.md" @@ -8,3 +8,19 @@ categories: tags: - --- +TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文段在超时时间内没有收到确认,那么就重传这个报文段。 + +一个报文段从发送再到接收到确认所经过的时间称为往返时间 RTT,加权平均往返时间 RTTs 计算如下: + +![](https://tva1.sinaimg.cn/large/006y8mN6ly1h6y8szdk43j308700jt8i.jpg) + +其中,0 ≤ a < 1,RTTs 随着 a 的增加更容易受到 RTT 的影响。 +超时时间 RTO 应该略大于 RTTs,TCP 使用的超时时间计算如下: + +![](https://tva1.sinaimg.cn/large/006y8mN6ly1h6y8vd1lo5j305p00g0si.jpg) + +其中 RTTd 为偏差的加权平均值。 + + +参考: +[TCP 可靠传输](http://www.cyc2018.xyz/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E7%BD%91%E7%BB%9C%E5%9F%BA%E7%A1%80/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%20-%20%E4%BC%A0%E8%BE%93%E5%B1%82.html#tcp-%E5%8F%AF%E9%9D%A0%E4%BC%A0%E8%BE%93) \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/130.TCP \344\270\255\345\270\270\350\247\201\347\232\204\346\213\245\345\241\236\346\216\247\345\210\266\347\256\227\346\263\225\346\234\211\345\223\252\344\272\233\357\274\237.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/130.TCP \344\270\255\345\270\270\350\247\201\347\232\204\346\213\245\345\241\236\346\216\247\345\210\266\347\256\227\346\263\225\346\234\211\345\223\252\344\272\233\357\274\237.md" index 18615b761..b22012f43 100644 --- "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/130.TCP \344\270\255\345\270\270\350\247\201\347\232\204\346\213\245\345\241\236\346\216\247\345\210\266\347\256\227\346\263\225\346\234\211\345\223\252\344\272\233\357\274\237.md" +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/130.TCP \344\270\255\345\270\270\350\247\201\347\232\204\346\213\245\345\241\236\346\216\247\345\210\266\347\256\227\346\263\225\346\234\211\345\223\252\344\272\233\357\274\237.md" @@ -8,3 +8,4 @@ categories: tags: - --- + diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/150.\344\273\216\347\263\273\347\273\237\345\261\202\351\235\242\344\270\212\357\274\214UDP\345\246\202\344\275\225\344\277\235\350\257\201\345\260\275\351\207\217\345\217\257\351\235\240\357\274\237.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/150.\344\273\216\347\263\273\347\273\237\345\261\202\351\235\242\344\270\212\357\274\214UDP\345\246\202\344\275\225\344\277\235\350\257\201\345\260\275\351\207\217\345\217\257\351\235\240\357\274\237.md" index 400eb5481..86a3e9cd9 100644 --- "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/150.\344\273\216\347\263\273\347\273\237\345\261\202\351\235\242\344\270\212\357\274\214UDP\345\246\202\344\275\225\344\277\235\350\257\201\345\260\275\351\207\217\345\217\257\351\235\240\357\274\237.md" +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/150.\344\273\216\347\263\273\347\273\237\345\261\202\351\235\242\344\270\212\357\274\214UDP\345\246\202\344\275\225\344\277\235\350\257\201\345\260\275\351\207\217\345\217\257\351\235\240\357\274\237.md" @@ -10,13 +10,20 @@ tags: --- 基本的思想:模拟tcp的可靠机制来实现,保证四个无即可(无丢失、无失序、无错误、无重复) 可以从以下几点着手: - ①加上序号 - ②确认机制,超时重传 - ③校验机制,出错重传 + +* ①加上序号 + +* ②确认机制,超时重传 + +* ③校验机制,出错重传 我们都知道UDP是面向无连接,不可靠的传输方式,而TCP虽然是面向连接,可靠地连接,但是其过程太繁琐,效率太低,传输速度很慢,所以我们可以利用UDP进行底层传输数据,上层进行封装,来弥补UDP的不足。 做到以下4点,就可以保证UDP协议可靠: -①不要TCP的三次握手和四次挥手。 -②发送方每次发出的数据进行编号,同时保持顺序的正确。 -③每次接收方接收到数据,发出应答信号。同时发送方在规定的时间检测是否接收到应答,如果没有接收到应答,重发,三次后还未收到应答直接判断发送失败。 -④发送数据时,发送方增加校验位。如果接收方校验出错,请求重发。 \ No newline at end of file + +* ①不要TCP的三次握手和四次挥手。 + +* ②发送方每次发出的数据进行编号,同时保持顺序的正确。 + +* ③每次接收方接收到数据,发出应答信号。同时发送方在规定的时间检测是否接收到应答,如果没有接收到应答,重发,三次后还未收到应答直接判断发送失败。 + +* ④发送数据时,发送方增加校验位。如果接收方校验出错,请求重发。 \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/160.TCP \346\214\245\346\211\213\346\227\266\345\207\272\347\216\260\345\244\247\351\207\217 CLOSE_WAIT \346\210\226 TIME_WAIT \346\200\216\344\271\210\350\247\243\345\206\263\357\274\237.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/160.TCP \346\214\245\346\211\213\346\227\266\345\207\272\347\216\260\345\244\247\351\207\217 CLOSE_WAIT \346\210\226 TIME_WAIT \346\200\216\344\271\210\350\247\243\345\206\263\357\274\237.md" index 7f430fa96..52a5d34e2 100644 --- "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/160.TCP \346\214\245\346\211\213\346\227\266\345\207\272\347\216\260\345\244\247\351\207\217 CLOSE_WAIT \346\210\226 TIME_WAIT \346\200\216\344\271\210\350\247\243\345\206\263\357\274\237.md" +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/160.TCP \346\214\245\346\211\213\346\227\266\345\207\272\347\216\260\345\244\247\351\207\217 CLOSE_WAIT \346\210\226 TIME_WAIT \346\200\216\344\271\210\350\247\243\345\206\263\357\274\237.md" @@ -9,3 +9,92 @@ tags: - --- +## 由于socket是全双工的工作模式,一个socket的关闭,是需要四次握手来完成的。 + +主动关闭连接的一方,调用close();协议层发送FIN包 + +被动关闭的一方收到FIN包后,协议层回复ACK;然后被动关闭的一方,进入CLOSE_WAIT状态,主动关闭的一方等待对方关闭,则进入FIN_WAIT_2状态;此时,主动关闭的一方 等待 被动关闭一方的应用程序,调用close操作。 + +被动关闭的一方在完成所有数据发送后,调用close()操作;此时,协议层发送FIN包给主动关闭的一方,等待对方的ACK,被动关闭的一方进入LAST_ACK状态; + +主动关闭的一方收到FIN包,协议层回复ACK;此时,主动关闭连接的一方,进入TIME_WAIT状态;而被动关闭的一方,进入CLOSED状态 + +等待2MSL时间,如没发现重传的FIN,主动关闭的一方,结束TIME_WAIT,进入CLOSED状态 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6ke74g96pj20pj0jijsj.jpg) + +## time_wait 产生原因(等待2MSL意义): + +假设是客户端向服务端发起断开连接 + +为了保证客户端发送的最后一个ACK报文段能够到达服务器。因为这个ACK有可能丢失,从而导致处在LAST-ACK状态的服务器收不到对FIN-ACK的确认报文。服务器会超时重传这个FIN-ACK,接着客户端再重传一次确认,重新启动时间等待计时器。最后客户端和服务器都能正常的关闭。假设客户端不等待2MSL,而是在发送完ACK之后直接释放关闭,一但这个ACK丢失的话,服务器就无法正常的进入关闭连接状态。 + +防止“已失效的连接请求报文段”出现在本连接中。 + +客户端在发送完最后一个ACK报文段后,再经过2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段。 + +## time_wait过多产生原因: + +正常的TCP客户端连接在关闭后,会进入一个TIME_WAIT的状态,持续的时间一般在1-4分钟,对于连接数不高的场景,1-4分钟其实并不长,对系统也不会有什么影响, +但如果短时间内(例如1s内)进行大量的短连接,则可能出现这样一种情况:客户端所在的操作系统的socket端口和文件描述符被用尽,系统无法再发起新的连接! + +举例来说: +  假设每秒建立了1000个短连接(Web场景下是很常见的,例如每个请求都去访问memcached),假设TIME_WAIT的时间是1分钟,则1分钟内需要建立6W个短连接,由于TIME_WAIT时间是1分钟,这些短连接1分钟内都处于TIME_WAIT状态,都不会释放,而Linux默认的本地端口范围配置是:net.ipv4.ip_local_port_range = 32768 61000不到3W,因此这种情况下新的请求由于没有本地端口就不能建立了。 + +## time_wait过多解决方法: + +1.可以改为长连接,但代价较大,长连接太多会导致服务器性能问题并且安全性也较差; +2.修改ipv4.ip_local_port_range,增大可用端口范围,但只能缓解问题,不能根本解决问题; + +3.客户端机器打开tcp_tw_reuse和tcp_timestamps选项; + +字面意思,reuse TIME_WAIT状态的连接。时刻记住一条socket连接,就是那个五元组,出现TIME_WAIT状态的连接,一定出现在主动关闭连接的一方。所以,当主动关闭连接的一方,再次向对方发起连接请求的时候(例如,客户端关闭连接,客户端再次连接服务端,此时可以复用了;负载均衡服务器,主动关闭后端的连接,当有新的HTTP请求,负载均衡服务器再次连接后端服务器,此时也可以复用),可以复用TIME_WAIT状态的连接。 + +通过字面解释,以及例子说明,你看到了,tcp_tw_reuse应用的场景:某一方,需要不断的通过“短连接”连接其他服务器,总是自己先关闭连接(TIME_WAIT在自己这方),关闭后又不断的重新连接对方。 + +那么,当连接被复用了之后,延迟或者重发的数据包到达,新的连接怎么判断,到达的数据是属于复用后的连接,还是复用前的连接呢?那就需要依赖前面提到的两个时间字段了。复用连接后,这条连接的时间被更新为当前的时间,当延迟的数据达到,延迟数据的时间是小于新连接的时间,所以,内核可以通过时间判断出,延迟的数据可以安全的丢弃掉了。 + +这个配置,依赖于连接双方,同时对timestamps的支持。同时,这个配置,仅仅影响outbound连接,即做为客户端的角色,连接服务端[connect(dest_ip, dest_port)]时复用TIME_WAIT的socket。 + +4.客户端机器打开tcp_tw_recycle和tcp_timestamps选项; + +字面意思,销毁掉 TIME_WAIT。 + +当开启了这个配置后,内核会快速的回收处于TIME_WAIT状态的socket连接。多快?不再是2MSL,而是一个RTO(retransmission timeout,数据包重传的timeout时间)的时间,这个时间根据RTT动态计算出来,但是远小于2MSL。 + + 有了这个配置,还是需要保障 丢失重传或者延迟的数据包,不会被新的连接(注意,这里不再是复用了,而是之前处于TIME_WAIT状态的连接已经被destroy掉了,新的连接,刚好是和某一个被destroy掉的连接使用了相同的五元组而已)所错误的接收。在启用该配置,当一个socket连接进入TIME_WAIT状态后,内核里会记录包括该socket连接对应的五元组中的对方IP等在内的一些统计数据,当然也包括从该对方IP所接收到的最近的一次数据包时间。当有新的数据包到达,只要时间晚于内核记录的这个时间,数据包都会被统统的丢掉。 + + 这个配置,依赖于连接双方对timestamps的支持。同时,这个配置,主要影响到了inbound的连接(对outbound的连接也有影响,但是不是复用),即做为服务端角色,客户端连进来,服务端主动关闭了连接,TIME_WAIT状态的socket处于服务端,服务端快速的回收该状态的连接。 + +## 短连接 + + +连接->传输数据->关闭连接 +HTTP是无状态的,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。 +也可以这样说:短连接是指SOCKET连接后发送后接收完数据后马上断开连接。 + + +## 长连接 + + +连接->传输数据->保持连接 -> 传输数据-> 。。。 ->关闭连接。 +长连接指建立SOCKET连接后不管是否使用都保持连接,但安全性较差。 + +## close_wait产生原因: + +比如是客户端要与服务端断开连接,先发一个FIN表示自己要主动断开连接了,服务端会先回一个ACK,这时表示客户端没数据要发了,但有可能服务端数据还没发完,所以要经历一个close_wait,等待服务端数据发送完,再回一个FIN和ACK。 + +close_wait产生太多原因: + +close_wait 按照正常操作的话应该很短暂的一个状态,接收到客户端的fin包并且回复客户端ack之后,会继续发送FIN包告知客户端关闭关闭连接,之后迁移到Last_ACK状态。但是close_wait过多只能说明没有迁移到Last_ACK,也就是服务端是否发送FIN包,只有发送FIN包才会发生迁移,所以问题定位在是否发送FIN包。FIN包的底层实现其实就是调用socket的close方法,这里的问题出在没有执行close方法。说明服务端socket忙于读写。 + +## close_wait太多解决方法: + +代码层面做到 +第一:使用完socket就调用close方法; +第二:socket读控制,当读取的长度为0时(读到结尾),立即close; +第三:如果read返回-1,出现错误,检查error返回码,有三种情况:INTR(被中断,可以继续读取),WOULDBLOCK(表示当前socket_fd文件描述符是非阻塞的,但是现在被阻塞了),AGAIN(表示现在没有数据稍后重新读取)。如果不是AGAIN,立即close +可以设置TCP的连接时长keep_alive_time还有tcp监控连接的频率以及连接没有活动多长时间被迫断开连接 +———————————————— + +参考[TCP通信过程中time_wait和close_wait产生过多的原因和解决方法](https://blog.csdn.net/weixin_43851782/article/details/116925584) \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/170.\347\256\200\350\277\260 TCP \346\273\221\345\212\250\347\252\227\345\217\243.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/170.\347\256\200\350\277\260 TCP \346\273\221\345\212\250\347\252\227\345\217\243.md" new file mode 100644 index 000000000..7100292e1 --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/170.\347\256\200\350\277\260 TCP \346\273\221\345\212\250\347\252\227\345\217\243.md" @@ -0,0 +1,99 @@ +--- +title: 简述 TCP 滑动窗口 +date: 2022-09-21 22:38:09 +permalink: /pages/dae451/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- +## 滑动窗口解决什么问题? +我们都知道 TCP 是每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了, 再发送下一个。 + +这个模式就有点像我和你面对面聊天,你一句我一句。但这种方式的缺点是效率比较低的。 + +如果你说完一句话,我在处理其他事情,没有及时回复你,那你不是要干等着我做完其他事情后,我回复你,你才能说下一句话,很显然这不现实。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6livhlkpxj20xz0u0dgf.jpg) + +所以,这样的传输方式有一个缺点:数据包的往返时间越长,通信的效率就越低。 + +## 滑动窗口是什么? +为解决这个问题,TCP 引入了窗口这个概念。即使在往返时间较长的情况下,它也不会降低网络通信的效率。 + +那么有了窗口,就可以指定窗口大小,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值。 + +窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。 + +假设窗口大小为 3个 TCP 段,那么发送方就可以「连续发送」3个 TCP 段,并且中途若有 ACK 丢失,可以通过「下一个确认应答进行确认」。如下图: + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6lix0ifhrj21760u0412.jpg) + +图中的 ACK 600 确认应答报文丢失,也没关系,因为可以通过下一个确认应答进行确认,只要发送方收到了 ACK 700 确认应答,就意味着 700 之前的所有数据「接收方」都收到了。这个模式就叫累计确认或者累计应答。 + +## 窗口大小由哪一方决定? +TCP 头里有一个字段叫 Window,也就是窗口大小。 + +这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。 + +所以,通常窗口的大小是由接收方的窗口大小来决定的。 + +发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。 + +我们先来看看发送方的窗口,下图就是发送方缓存的数据,根据处理的情况分成四个部分,其中深蓝色方框是发送窗口,紫色方框是可用窗口: + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6liy1bmtnj21cg0hg772.jpg) + +1. 井1是已发送并收到 ACK确认的数据:1~31 字节 +2. 井2是已发送但未收到 ACK确认的数据:32~45 字节 +3. 井3是未发送但总大小在接收方处理范围内(接收方还有空间):46~51字节 +4. 井4是未发送但总大小超过接收方处理范围(接收方没有空间):52字节以后 +在下图,当发送方把数据「全部」都一下发送出去后,可用窗口的大小就为 0 了,表明可用窗口耗尽,在没收到 ACK 确认之前是无法继续发送数据了。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6liyn1bdjj21ca0ectar.jpg) + +在下图,当收到之前发送的数据 32~36字节的 ACK 确认应答后,如果发送窗口的大小没有变化,则滑动窗口往右边移动 5 个字节,因为有 5 个字节的数据被应答确认,接下来 52~56字节又变成了可用窗口,那么后续也就可以发送 52~56 这 5 个字节的数据了。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6liywwmduj21bu0dc0v4.jpg) + +## 程序是如何表示发送方的四个部分的呢? +TCP 滑动窗口方案使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。其中两个指针是绝对指针(指特定的序列号),一个是相对指针(需要做偏移)。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6liz7dcxsj21c40i6jug.jpg) + +* SND.WND:表示发送窗口的大小(大小是由接收方指定的); + +* SND.UNA:是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一个字节。 + +* SND.NXT:也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号,也就是 #3 的第一个字节。 + +指向 #4 的第一个字节是个相对指针,它需要 SND.UNA 指针加上 SND.WND大小的偏移量,就可以指向 #4 的第一个字节了。 + +那么可用窗口大小的计算就可以是: + +**可用窗口大小 = SND.WND -(SND.NXT - SND.UNA)** + +## 接收方的滑动窗口 +接下来我们看看接收方的窗口,接收窗口相对简单一些,根据处理的情况划分成三个部分: + +* 井1 + 井2 是已成功接收并确认的数据(等待应用进程读取) +* 井3 是未收到数据但可以接收的数据 +* 井4 未收到数据并不可以接收的数据 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6lj1jw6x0j21bs0h2jtg.jpg) + +接收窗口 +其中三个接收部分,使用两个指针进行划分: + +* RCV.WND:表示接收窗口的大小,它会通告给发送方。 +* RCV.NXT:是一个指针,它指向期望从发送方发送来的下一个数据字节的序列号,也就是 #3 的第一个字节。 +* 指向 #4 的第一个字节是个相对指针,它需要RCV.NXT指针加上 RCV.WND大小的偏移量,就可以指向 #4 的第一个字节了。 + +## 接收窗口和发送窗口的大小是相等的吗? +并不是完全相等,接收窗口的大小是约等于发送窗口的大小的。 + +因为滑动窗口并不是一成不变的。比如,当接收方的应用进程读取数据的速度非常快的话,这样的话接收窗口可以很快的就空缺出来。那么新的接收窗口大小,是通过 TCP 报文中的 Windows 字段来告诉发送方。那么这个传输过程是存在时延的,所以接收窗口和发送窗口是约等于的关系。 + +## 参考: +[TCP滑动窗口和重传机制](https://blog.csdn.net/weixin_43207025/article/details/110387212) \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/170.\347\256\200\350\277\260 TCP \346\273\221\345\212\250\347\252\227\345\217\243\344\273\245\345\217\212\351\207\215\344\274\240\346\234\272\345\210\266.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/170.\347\256\200\350\277\260 TCP \346\273\221\345\212\250\347\252\227\345\217\243\344\273\245\345\217\212\351\207\215\344\274\240\346\234\272\345\210\266.md" deleted file mode 100644 index fff60e5ac..000000000 --- "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/170.\347\256\200\350\277\260 TCP \346\273\221\345\212\250\347\252\227\345\217\243\344\273\245\345\217\212\351\207\215\344\274\240\346\234\272\345\210\266.md" +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: 简述 TCP 滑动窗口以及重传机制 -date: 2022-09-21 22:38:09 -permalink: /pages/dae451/ -categories: - - 计算机基础 - - 计算机网络 -tags: - - ---- diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/171. \347\256\200\350\277\260TCP\351\207\215\344\274\240\346\234\272\345\210\266.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/171. \347\256\200\350\277\260TCP\351\207\215\344\274\240\346\234\272\345\210\266.md" new file mode 100644 index 000000000..a94a065e3 --- /dev/null +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/171. \347\256\200\350\277\260TCP\351\207\215\344\274\240\346\234\272\345\210\266.md" @@ -0,0 +1,135 @@ +--- +title: 简述TCP重传机制 +date: 2022-09-27 22:26:11 +permalink: /pages/a219df/ +categories: + - 计算机基础 + - 计算机网络 +tags: + - +--- +## 重传机制 +TCP 实现可靠传输的方式之一,是通过序列号与确认应答。 + +在 TCP 中,当发送端的数据到达接收主机时,接收端主机会返回一个确认应答消息,表示已收到消息。 + +常见的重传机制: + +* 超时重传 +* 快速重传 +* SACK +D-SACK + +## 超时重传 +重传机制的其中一个方式,就是在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK 确认应答报文,就会重发该数据,也就是我们常说的超时重传。 + +TCP 会在以下两种情况发生超时重传: + +* 数据包丢失 +* 确认应答丢失 +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6lj8deslnj21420u0ab4.jpg) + +**超时时间应该设置为多少呢?** + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6lj8qe3dqj219c0teaak.jpg) + +`RTT` 就是数据从网络一端传送到另一端所需的时间,也就是包的往返时间。 + +超时重传时间是以 `RTO (Retransmission Timeout 超时重传时间)`表示。 + +假设在重传的情况下,超时时间 `RTO` 「较长或较短」时,会发生什么事情呢? + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6lj984b1wj21ca0t40vg.jpg) + +上图中有两种超时时间不同的情况: + +* 当超时时间 `RTO` 较大时,重发就慢,丢了老半天才重发,没有效率,性能差; +* 当超时时间 `RTO` 较小时,会导致可能并没有丢就重发,于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。 + +根据上述的两种情况,我们可以得知,超时重传时间 **RTO 的值应该略大于报文往返 RTT 的值**。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6lj9ul0etj218i0sa3zu.jpg) + +好像就是在发送端发包时记下`t0` ,然后接收端再把这个`ack` 回来时再记一个`t1`,于是`RTT = t1 – t0`。没那么简单,这只是一个采样,不能代表普遍情况。 + +实际上「**报文往返 RTT 的值**」是经常变化的,因为我们的网络也是时常变化的。也就因为「**报文往返 RTT 的值**」 是经常波动变化的,所以「超时重传时间 RTO 的值」应该是一个动态变化的值。 + +估计往返时间,通常需要采样以下两个: + +* 需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个平滑 RTT 的值,而且这个值还是要不断变化的,因为网络状况不断地变化。 +* 除了采样 RTT,还要采样 RTT 的波动范围,这样就避免如果 RTT 有一个大的波动的话,很难被发现的情况。 +RFC6289 建议使用以下的公式计算 RTO: + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6ljaz488sj21350u0q5f.jpg) + +其中 `SRTT` 是计算平滑的RTT ,`DevRTR` 是计算平滑的RTT 与 最新 RTT 的差距。 + +在 Linux 下,`α = 0.125,β = 0.25, μ = 1,∂ = 4`。别问怎么来的,问就是大量实验中调出来的。 + +如果超时重发的数据,再次超时的时候,又需要重传的时候,TCP 的策略是超时时间隔加倍。 + +也就是每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。 + +超时触发重传存在的问题是,超时周期可能相对较长。那是不是可以有更快的方式呢? + +于是就可以用「快速重传」机制来解决超时重发的时间等待。 + +## 快速重传 +TCP 还有另外一种快速重传(Fast Retransmit)机制,它不以时间为驱动,而是以数据驱动重传。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6ljc0v3v1j21370u0gnj.jpg) + +在上图,发送方发出了 1,2,3,4,5 份数据: + +* 第一份 Seq1 先送到了,于是就 Ack 回 2; +* 结果 Seq2 因为某些原因没收到,Seq3 到达了,于是还是 Ack 回 2; +* 后面的 Seq4 和 Seq5 都到了,但还是 Ack 回 2,因为 Seq2 还是没有收到; +* 发送端收到了三个 Ack = 2 的确认,知道了 Seq2 还没有收到,就会在定时器过期之前,重传丢失的 Seq2。 +* 最后,收到了 Seq2,此时因为 Seq3,Seq4,Seq5 都收到了,于是 Ack 回 6 。 + +所以,快速重传的工作方式是当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。 + +快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传之前的一个,还是重传所有的问题。 + +比如对于上面的例子,是重传 Seq2 呢?还是重传 Seq2、Seq3、Seq4、Seq5 呢?因为发送端并不清楚这连续的三个 Ack 2 是谁传回来的。 + +为了解决不知道该重传哪些 TCP 报文,于是就有 SACK 方法。 + +## SACK 方法 +还有一种实现重传机制的方式叫:SACK( Selective Acknowledgment 选择性确认)。 + +这种方式需要在 TCP 头部「选项」字段里加一个 SACK的东西,它可以将缓存的数据发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。 + +如下图,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK 信息发现只有 200~299 这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6ljcuptppj21c40scab9.jpg) + +## Duplicate SACK +Duplicate SACK 又称 D-SACK,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。 + +下面举例两个栗子,来说明 D-SACK 的作用。 + +栗子一号:ACK 丢包: + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6ljd3gop2j21800u0ack.jpg) + +「接收方」发给「发送方」的两个 ACK 确认应答都丢失了,所以发送方超时后,重传第一个数据包(3000 ~ 3499) +于是「接收方」发现数据是重复收到的,于是回了一个 SACK = 3000~3500,告诉「发送方」 3000~3500 的数据早已被接收了,因为 ACK 都到了 4000 了,已经意味着 4000 之前的所有数据都已收到,所以这个 SACK 就代表着 D-SACK。 +这样「发送方」就知道了,数据没有丢,是「接收方」的 ACK 确认报文丢了。 +栗子二号:网络延时: + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6ljddv1qrj20qq0u2dh6.jpg) + +* 数据包(1000~1499) 被网络延迟了,导致「发送方」没有收到 Ack 1500 的确认报文。 +* 而后面报文到达的三个相同的 ACK 确认报文,就触发了快速重传机制,但是在重传后,被延迟的数据包(1000~1499)又到了「接收方」; +* 所以「接收方」回了一个 SACK=1000~1500,因为 ACK 已经到了 3000,所以这个 SACK 是 D-SACK,表示收到了重复的包。 +* 这样发送方就知道快速重传触发的原因不是发出去的包丢了,也不是因为回应的 ACK 包丢了,而是因为网络延迟了。 + +可见,D-SACK 有这么几个好处: + +1. 可以让「发送方」知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了; +2. 可以知道是不是「发送方」的数据包被网络延迟了; +3. 可以知道网络中是不是把「发送方」的数据包给复制了; + +## 参考 +[TCP滑动窗口和重传机制](https://blog.csdn.net/weixin_43207025/article/details/110387212) \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/180.\347\256\200\350\277\260 JWT \347\232\204\345\216\237\347\220\206\345\222\214\346\240\241\351\252\214\346\234\272\345\210\266.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/180.\347\256\200\350\277\260 JWT \347\232\204\345\216\237\347\220\206\345\222\214\346\240\241\351\252\214\346\234\272\345\210\266.md" index 059146943..ea8f79d65 100644 --- "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/180.\347\256\200\350\277\260 JWT \347\232\204\345\216\237\347\220\206\345\222\214\346\240\241\351\252\214\346\234\272\345\210\266.md" +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/180.\347\256\200\350\277\260 JWT \347\232\204\345\216\237\347\220\206\345\222\214\346\240\241\351\252\214\346\234\272\345\210\266.md" @@ -65,7 +65,7 @@ tags: * 用户输入用户名/密码登录,服务端认证成功后,会返回给客户端一个 JWT * 客户端将 token 保存到本地(通常使用 localstorage,也可以使用 cookie) * 当用户希望访问一个受保护的路由或者资源的时候,需要请求头的 Authorization 字段中使用Bearer 模式添加 JWT,其内容看起来是下面这样 -```java +```html Authorization: Bearer ``` * 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为 diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/200.\347\256\200\350\277\260 iPv4 \345\222\214 iPv6 \347\232\204\345\214\272\345\210\253.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/200.\347\256\200\350\277\260 iPv4 \345\222\214 iPv6 \347\232\204\345\214\272\345\210\253.md" index ab5c96753..1346cf7a6 100644 --- "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/200.\347\256\200\350\277\260 iPv4 \345\222\214 iPv6 \347\232\204\345\214\272\345\210\253.md" +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/200.\347\256\200\350\277\260 iPv4 \345\222\214 iPv6 \347\232\204\345\214\272\345\210\253.md" @@ -9,3 +9,201 @@ tags: - --- +前言 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6kebmr4prj21620n4ju7.jpg) + +Internet协议(IP)是为连接到Internet网络的每个设备分配的数字地址。 + +它类似于电话号码,是一种独特的数字组合,允许用户与他人通信。 + +IP地址主要有两个主要功能。 + +首先,有了IP,用户能够在Internet上被识别。 + +第二,IP地址允许计算机通过Internet发送和接收数据,也就是我们经常说的通信。 + +在本文中,我们将深入研究两种类型的IP地址:IPv4与IPv6。 + +我们将从以下几个方面来给大家介绍这两种类型的IP地址,让大家了解到两类IP的必备知识: + +什么是IPv4和IPv6? + +IPv4和IPv6之间的区别 + +IPv4或IPv6:使用哪个? + +IPv4与IPv6安全性如何 + +## 什么是IPv4和IPv6? + + +IPv4和IPv6是不同类型的IP地址。 + +它们的主要用途相同,标记不同的用户,并且让用户能通过IP进行通信。 + +主要区别在于IPv6是最新一代的IP地址。 + + +## IPv4地址 + +IPv4地址的概念是在1980年代初期提出的。 + +即使有新版本的IP地址,IPv4地址仍然是Internet用户使用最广泛的地址。 + +通常,IPv4地址以点分十进制表示。每个部分代表一组构成8位地址方案的8位地址。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6keeoobstj219o0ncacl.jpg) + +IPv4地址组合的数量是有限的。总体而言,可以算出40亿(256 4)个唯一地址。 + +在IPv4地址才开始时,这个数字似乎永远不会过期。但是,现在情况有所不同了。 + +2011年,全球互联网编号分配机构(IANA)分发了IPv4地址空间的最后一块。 + +2015年,IANA正式宣布美国已用完IPv4地址。 + +直到今天,IPv4地址仍然承载着最多(超过90%)的互联网流量。 + +到目前为止,即使目前存在IPv4地址耗尽的问题,也有一些方法可以继续使用IPv4地址。 + +例如,当仅需要一个唯一的IP地址来代表一组设备时,网络地址转换(NAT)是一种方法。 + +除此之外,IP地址可以重复使用。当然,我们已经有了彻底耗尽的解决方案-IPv6地址。 + +## IPv6地址 + +仔细观察,您会发现IPv6地址并不是一种全新的技术。 + +它是Internet协议的最新版本,但它是在1998年开发的,旨在替换IPv4地址。 + +IPv6地址使用以冒号分隔的十六进制数字。 + +它分为八个16位块,构成一个128位地址方案。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6keef1rflj219u0pgq5l.jpg) + +IPv6也存在数量限制。不过可用的IP数量远大于IPv4。 + +从理论上讲,可以创建大约3.4×10 38个地址。 + +这一数据听起来很高,远超于IPv4的总数40亿个,但是有一天也可能出现不够的情况。 + +但就目前而言,这些地址将可以供我们使用很长一段时间。 + + + + +## IPv4和IPv6之间的区别 + + +IPv4和IPv6用于用户标识和Internet上不同设备之间的通信。 + +IPv4是32位IP地址,而IPv6是128位IP地址。 + +IPv4是数字地址,用点分隔。IPv6是一个字母数字地址,用冒号分隔。 + +我们分别详细介绍了IPv4和IPv6类型。 + +现在,我们可以比较这些类型,并找出这两种协议之间的主要区别。 + +我们列举了IPv4和IPv6之间的八个主要区别。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6kef0x1jkj217i0awjt1.jpg) + +1. 地址类型。IPv4具有三种不同类型的地址:多播,广播和单播。IPv6还具有三种不同类型的地址:任意广播,单播和多播。 + +2. 数据包大小。对于IPv4,最小数据包大小为576字节。对于IPv6,最小数据包大小为1208字节。 + +3. header区域字段数。IPv4具有12个标头字段,而IPv6支持8个标头字段。 + +4. 可选字段。IPv4具有可选字段,而IPv6没有。但是,IPv6具有扩展header,可以在将来扩展协议而不会影响主包结构。 + +5. 配置。在IPv4中,新装的系统必须配置好才能与其他系统通信。在IPv6中,配置是可选的,它允许根据所需功能进行选择。 + +6. 安全性。在IPv4中,安全性主要取决于网站和应用程序。它不是针对安全性而开发的IP协议。而IPv6集成了Internet协议安全标准(IPSec)。IPv6的网络安全不像IPv4是可选项,IPv6里的网络安全项是强制性的。 + +7. 与移动设备的兼容性。IPv4不适合移动网络,因为正如我们前面提到的,它使用点分十进制表示法,而IPv6使用冒号,是移动设备的更好选择。 + +8. 主要功能。IPv6允许直接寻址,因为存在大量可能的地址。但是,IPv4已经广泛传播并得到许多设备的支持,这使其更易于使用。 + + + + + +## IPv4或IPv6:使用哪个? + + +对于使用IPv6还是IPv4这个问题,没有标准答案。 + +在考虑未来的网络体验时,IPv6地址就显得至关重要。 + +即使在我们已经没有网络地址的情况下仍然可以有其他办法使用IPv4地址, + +但是这些选项也可能会轻微影响到网络速度或引起其他问题。 + +不过,使用IPv6需要开发支持IPv6的新技术和产品。 + +IPv6的速度显然不比IPv4快,但是从IPv4完全更改为IPv6将为Internet提供更大的唯一IP池。 + +## 那么为什么我们仍在使用IPv4? + +问题就在于IPv4和IPv6无法相互通信。 + +这就是为什么IPv6的集成和适配很复杂。 + +大多数网站或应用程序仅支持IPv4类型的IP地址。想象一下突然更改每个设备的IP地址。 + +用户将无法访问大多数网站或应用程序,而我们在互联网上将陷入一片混乱。 + +从旧的IP类型转换为新的IP类型的过程应分步完成。 + +例如,这两个协议能够并行运行。 + +此功能称为双重堆栈。它允许用户同时访问IPv4和IPv6内容。 + +## 您需要什么才能使用IPv6? + +1. 操作系统必须与IPv6兼容。Windows Vista和Windows的较新版本,Mac OS X的现代版本以及Linux。 + +2. 大多数路由器不支持IPv6。如果您想尝试使用IPv6,请检查路由器的详细信息。 + +3. Internet服务提供商(ISP)也必须支持IPv6。即使您具有合适的操作系统和路由器,您的ISP也必须提供IPv6连接。 + + + + +## IPv4与IPv6的安全性 + + +IPv6的开发考虑了安全性。这就是将IPSec集成在IPv6中的原因,而对于IPv4,IPSec是可选的。 + + +什么是IPSec? + +IPSec(Internet协议安全性)是一种安全的网络协议,它对数据包进行身份验证和加密,以在设备之间提供安全的通信。 + +加密是只有经过确认的各方才能理解的一种秘密代码。它有助于确保通过公共网络发送的信息的安全。 + + +由于IPv4还可以选择集成IPSec,因此我们可以假设在安全性方面IPv4与IPv6几乎相同。 + +但是,如果已经集成了安全措施,则要简单得多。 + + + + + + +## 总结 + + +IPv6是IP地址的未来。 +但是,从IPv4过渡到IPv6的过程却是漫长而复杂的。 + +如果您对使用IPv6地址感到好奇,建议将其用法与IPv4结合使用,因为大多数网站和应用程序仍仅支持IPv4地址。 + +IPv4和IPv6使用的结合称为双重堆栈 + +## 参考: +[IPv4 vs. IPv6: What is the Difference?](https://oxylabs.io/blog/ipv4-vs-ipv6?utm_source=social&utm_medium=zhihu&utm_term=article&utm_content=englishpage&utm_campaign=none) \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/210.TCP \344\270\255 SYN \346\224\273\345\207\273\346\230\257\344\273\200\344\271\210\357\274\237\345\246\202\344\275\225\351\230\262\346\255\242\357\274\237.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/210.TCP \344\270\255 SYN \346\224\273\345\207\273\346\230\257\344\273\200\344\271\210\357\274\237\345\246\202\344\275\225\351\230\262\346\255\242\357\274\237.md" index 186c65373..da125f248 100644 --- "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/210.TCP \344\270\255 SYN \346\224\273\345\207\273\346\230\257\344\273\200\344\271\210\357\274\237\345\246\202\344\275\225\351\230\262\346\255\242\357\274\237.md" +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/210.TCP \344\270\255 SYN \346\224\273\345\207\273\346\230\257\344\273\200\344\271\210\357\274\237\345\246\202\344\275\225\351\230\262\346\255\242\357\274\237.md" @@ -8,3 +8,74 @@ categories: tags: - --- +## SYN Flooding简介 +拒绝服务攻击(DDoS)从1970 年出现直到今天都依然在作祟 + +并给全球范围内的各大组织带来了不可估量的损失。 + +SYN Flood是互联网上最经典的DDoS攻击方式之一, + +最早出现于 1999 年左右,雅虎是当时最著名的受害者。 + +SYN Flood攻击利用了 TCP 三次握手的缺陷,能够以较小代价使目标服务器无法响应,且难以追查。 + +SYN flood 是一种常见的 DOS(denial of service拒绝服务)和 DDos(distributed denial of serivce 分布式拒绝服务)攻击方式。 + +这是一种使用TCP协议缺陷,发送大量的伪造的 TCP 连接请求, + +使得被攻击方 CPU 或内存资源耗尽,最终导致被攻击方无法提供正常的服务。 + +## TCP SYN Flood攻击原理 +TCP SYN Flood 攻击利用的是 TCP 的三次握手(SYN -> SYN/ACK -> ACK), + +假设连接发起方是A,连接接受方是 B,即 B 在某个端口(Port)上监听A发出的连接请求, + +过程如下图所示,左边是A,右边是B。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6kes3oey0j20zy0dogmt.jpg) + +A 首先发送 SYN(Synchronization)消息给 B,要求 B 做好接收数据的准备; + +B 收到后反馈 SYN-ACK(Synchronization-Acknowledgement) 消息给A,这个消息的目的有两个: + +* 向 A 确认已做好接收数据的准备 + +* 同时要求 A 也做好接收数据的准备,此时 B 已向 A 确认好接收状态,并等待 A 的确认,连接处于半开状态(Half-Open),顾名思义只开了一半;A 收到后再次发送ACK (Acknowledgement) 消息给B,向 B 确认也做好了接收数据的准备,至此三次握手完成,「连接」就建立了, + +大家注意到没有,最关键的一点在于双方是否都按对方的要求进入了可以接收消息的状态。而这个状态的确认主要是双方将要使用的消息序号(SquenceNum),TCP 为保证消息按发送顺序抵达接收方的上层应用,需要用消息序号来标记消息的发送先后顺序的。 + +TCP是「双工」(Duplex)连接,同时支持双向通信,也就是双方同时可向对方发送消息,其中 SYN 和 SYN-ACK 消息开启了A→B的单向通信通道(B 获知了 A 的消息序号);SYN-ACK 和 ACK 消息开启了B→A单向通信通道(A获知了B的消息序号)。 + +上面讨论的是双方在诚实守信,正常情况下的通信。 + +但实际情况是,网络可能不稳定会丢包,使握手消息不能抵达对方,也可能是对方故意不按规矩来,故意延迟或不发送握手确认消息。 + +假设 B 通过某 TCP 端口提供服务,B 在收到 A 的 SYN 消息时,积极的反馈了SYN-ACK 消息, + +使连接进入半开状态,因为 B 不确定自己发给 A 的 SYN-ACK 消息或 A 反馈的 ACK 消息是否会丢在半路, + +所以会给每个待完成的半开连接都设一个Timer,如果超过时间还没有收到 A 的 ACK 消息, + +则重新发送一次 SYN-ACK 消息给A,直到重试超过一定次数时才会放弃。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6ketedovpj20fb094weo.jpg) + +B 为帮助 A 能顺利连接,需要分配内核资源维护半开连接,那么当 B 面临海量的连接 A 时,如上图所示,SYN Flood 攻击就形成了。攻击方 A 可以控制肉鸡向 B 发送大量 SYN 消息但不响应 ACK 消息,或者干脆伪造 SYN 消息中的 Source IP,使 B 反馈的 SYN-ACK 消息石沉大海,导致 B 被大量注定不能完成的半开连接占据,直到资源耗尽,停止响应正常的连接请求。 + +## 如何检测SYN攻击 +查看服务器上是否能看到大量的半连接状态时,特别时源IP地址时随机的,基本可以断定这是一次SYN攻击 +```shell script +netstat -ntp|grep SYB_RECV +``` + +## 常见的防御SYN攻击方法 + +* 缩短超时(SYN Timeout) 时间 +* 增加最大半连接数(SYN队列) +* 过滤网关防护 +* [SYN cookies技术](https://blog.csdn.net/weixin_44390164/article/details/120519073) +* 减少syn+ack重传次数 + +## 参考 + +[TCP/IP常见攻击手段](https://zhuanlan.zhihu.com/p/360918707) \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/220.\347\256\200\350\277\260 DDOS \346\224\273\345\207\273\345\216\237\347\220\206\357\274\214\345\246\202\344\275\225\351\230\262\350\214\203\345\256\203\357\274\237.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/220.\347\256\200\350\277\260 DDOS \346\224\273\345\207\273\345\216\237\347\220\206\357\274\214\345\246\202\344\275\225\351\230\262\350\214\203\345\256\203\357\274\237.md" index c206ced3c..c2794fd7f 100644 --- "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/220.\347\256\200\350\277\260 DDOS \346\224\273\345\207\273\345\216\237\347\220\206\357\274\214\345\246\202\344\275\225\351\230\262\350\214\203\345\256\203\357\274\237.md" +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/220.\347\256\200\350\277\260 DDOS \346\224\273\345\207\273\345\216\237\347\220\206\357\274\214\345\246\202\344\275\225\351\230\262\350\214\203\345\256\203\357\274\237.md" @@ -8,3 +8,62 @@ categories: tags: - --- +其实,像全球互联网各大公司,均遭受过大量的 DDoS。 + +2018年,GitHub 在一瞬间遭到高达 1.35Tbps 的带宽攻击。 + +这次 DDoS 攻击几乎可以堪称是互联网有史以来规模最大、威力最大的 DDoS 攻击了。 + +在 GitHub 遭到攻击后,仅仅一周后,DDoS 攻击又开始对 Google、亚马逊甚至 Pornhub 等网站进行了 DDoS 攻击。 + +后续的 DDoS 攻击带宽最高也达到了 1Tbps。 + +## DDoS 攻击究竟是什么? +DDos 全名 Distributed Denial of Service,翻译成中文就是分布式拒绝服务。 + +指的是处于不同位置的多个攻击者同时向一个或数个目标发动攻击,是一种分布的、协同的大规模攻击方式。 + +单一的DoS攻击一般是采用一对一方式的,它利用网络协议和操作系统的一些缺陷,采用欺骗和伪装的策略来进行网络攻击, + +使网站服务器充斥大量要求回复的信息,消耗网络带宽或系统资源,导致网络或系统不胜负荷以至于瘫痪而停止提供正常的网络服务。 + +## 举个例子 +我开了一家有五十个座位的重庆火锅店,由于用料上等,童叟无欺。 + +平时门庭若市,生意特别红火,而对面二狗家的火锅店却无人问津。 + +二狗为了对付我,想了一个办法,叫了五十个人来我的火锅店坐着却不点菜,让别的客人无法吃饭。 + +上面这个例子讲的就是典型的 DDoS 攻击,一般来说是指攻击者利用“肉鸡”对目标网站在较短的时间内发起大量请求, + +大规模消耗目标网站的主机资源,让它无法正常服务。在线游戏、互联网金融等领域是 DDoS 攻击的高发行业。 + +攻击方式很多,比如 ICMP Flood、UDP Flood、NTP Flood、SYN Flood、CC 攻击、DNS Query Flood等等。 + +## SYN Flood进行DDoS攻击的实现原理 +SYN Flood 是一种利用 TCP 协议缺陷,发送大量伪造的 TCP 连接请求,从而使得被攻击方资源耗尽(CPU满负荷或内存不足)的攻击方式。 + +一次正常的建立 TCP 连接,需要三次握手:客户端发送 SYN 报文,服务端收到请求并返回报文表示接受,客户端也返回确认,完成连接。 + +SYN Flood 就是用户向服务器发送报文后突然死机或掉线,那么服务器在发出应答报文后就无法收到客户端的确认报文(第三次握手无法完成),这时服务器端一般会重试并等待一段时间后再丢弃这个未完成的连接。 + +一个用户出现异常导致服务器的一个线程等待一会儿并不是大问题,但恶意攻击者大量模拟这种情况,服务器端为了维护数以万计的半连接而消耗非常多的资源,结果往往是无暇理睬客户的正常请求,甚至崩溃。从正常客户的角度看来,网站失去了响应,无法访问。 + +## 如何应对 DDoS 攻击? +1. 高防服务器 +还是拿开的重庆火锅店举例,高防服务器就是我给重庆火锅店增加了两名保安,这两名保安可以让保护店铺不受流氓骚扰,并且还会定期在店铺周围巡逻防止流氓骚扰。 + +高防服务器主要是指能独立硬防御 50Gbps 以上的服务器,能够帮助网站拒绝服务攻击,定期扫描网络主节点等,这东西是不错,就是贵~ + +2. 黑名单 +面对火锅店里面的流氓,我一怒之下将他们拍照入档,并禁止他们踏入店铺,但是有的时候遇到长得像的人也会禁止他进入店铺。这个就是设置黑名单,此方法秉承的就是“错杀一千,也不放一百”的原则,会封锁正常流量,影响到正常业务。 + +3. DDoS 清洗 +DDos 清洗,就是我发现客人进店几分钟以后,但是一直不点餐,我就把他踢出店里。 + +DDoS 清洗会对用户请求数据进行实时监控,及时发现 **DOS **攻击等异常流量,在不影响正常业务开展的情况下清洗掉这些异常流量。 + +4. CDN 加速 +CDN 加速,我们可以这么理解:为了减少流氓骚扰,我干脆将火锅店开到了线上,承接外卖服务,这样流氓找不到店在哪里,也耍不来流氓了。 + +在现实中,CDN 服务将网站访问流量分配到了各个节点中,这样一方面隐藏网站的真实 IP,另一方面即使遭遇 DDoS 攻击,也可以将流量分散到各个节点中,防止源站崩溃。 \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/230.\344\273\200\344\271\210\346\230\257 ARP \345\215\217\350\256\256\357\274\237\347\256\200\350\277\260\345\205\266\344\275\277\347\224\250\345\234\272\346\231\257.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/230.\344\273\200\344\271\210\346\230\257 ARP \345\215\217\350\256\256\357\274\237\347\256\200\350\277\260\345\205\266\344\275\277\347\224\250\345\234\272\346\231\257.md" index aefe4084c..3a17d7c0e 100644 --- "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/230.\344\273\200\344\271\210\346\230\257 ARP \345\215\217\350\256\256\357\274\237\347\256\200\350\277\260\345\205\266\344\275\277\347\224\250\345\234\272\346\231\257.md" +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/230.\344\273\200\344\271\210\346\230\257 ARP \345\215\217\350\256\256\357\274\237\347\256\200\350\277\260\345\205\266\344\275\277\347\224\250\345\234\272\346\231\257.md" @@ -8,3 +8,107 @@ categories: tags: - --- +## 什么是 Mac 地址? +MAC 地址的全称是 媒体访问控制地址(Media Access Control Address)。 + +如果说,互联网中每一个资源都由 IP 地址唯一标识(IP 协议内容),那么一切网络设备都由 MAC 地址唯一标识。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6kf5pm5ofj20uw0ccmz0.jpg) + +路由器的背面就会注明 MAC 位址. + +可以理解为,MAC 地址是一个网络设备真正的身份证号,IP 地址只是一种不重复的定位方式(比如说住在某省某市某街道的张三,这种逻辑定位是 IP 地址,他的身份证号才是他的 MAC 地址),也可以理解为 MAC 地址是身份证号,IP 地址是邮政地址。MAC 地址也有一些别称,如 LAN 地址、物理地址、以太网地址等。 + +还有一点要知道的是,不仅仅是网络资源才有 IP 地址,网络设备也有 IP 地址,比如路由器。但从结构上说,路由器等网络设备的作用是组成一个网络,而且通常是内网,所以它们使用的 IP 地址通常是内网 IP,内网的设备在与内网以外的设备进行通信时,需要用到 NAT 协议。 +MAC 地址的长度为 6 字节(48 比特),地址空间大小有 280 万亿之多($2^{48}$),MAC 地址由 IEEE 统一管理与分配,理论上,一个网络设备中的网卡上的 MAC 地址是永久的。不同的网卡生产商从 IEEE 那里购买自己的 MAC 地址空间(MAC 的前 24 比特),也就是前 24 比特由 IEEE 统一管理,保证不会重复。而后 24 比特,由各家生产商自己管理,同样保证生产的两块网卡的 MAC 地址不会重复。 + +MAC 地址具有可携带性、永久性,身份证号永久地标识一个人的身份,不论他到哪里都不会改变。而 IP 地址不具有这些性质,当一台设备更换了网络,它的 IP 地址也就可能发生改变,也就是它在互联网中的定位发生了变化。 + +最后,记住,MAC 地址有一个特殊地址:FF-FF-FF-FF-FF-FF(全 1 地址),该地址表示广播地址。 + +## ARP 协议解决了什么问题地位如何? +ARP 协议,全称 地址解析协议(Address Resolution Protocol),它解决的是网络层地址和链路层地址之间的转换问题。 + +因为一个 IP 数据报在物理上传输的过程中,总是需要知道下一跳(物理上的下一个目的地)该去往何处,但 IP 地址属于逻辑地址,而 MAC 地址才是物理地址. + +ARP 协议解决了 IP 地址转 MAC 地址的问题。 + +## ARP 协议工作原理 +ARP 协议工作时有一个大前提,那就是 ARP 表。 + +在一个局域网内,每个网络设备都自己维护了一个 ARP 表,ARP 表记录了某些其他网络设备的 IP 地址-MAC 地址映射关系,该映射关系以 三元组的形式存储。其中,TTL 为该映射关系的生存周期,典型值为 20 分钟,超过该时间,该条目将被丢弃。 + +ARP 的工作原理将分两种场景讨论: + +同一局域网内的 MAC 寻址; +从一个局域网到另一个局域网中的网络设备的寻址。 +### 同一局域网内的 MAC 寻址 +假设当前有如下场景:IP 地址为137.196.7.23的主机 A,想要给同一局域网内的 IP 地址为137.196.7.14主机 B,发送 IP 数据报文。 + +再次强调,当主机发送 IP 数据报文时(网络层),仅知道目的地的 IP 地址,并不清楚目的地的 MAC 地址,而 ARP 协议就是解决这一问题的。 +为了达成这一目标,主机 A 将不得不通过 ARP 协议来获取主机 B 的 MAC 地址,并将 IP 报文封装成链路层帧,发送到下一跳上。在该局域网内,关于此将按照时间顺序,依次发生如下事件: + +1. 主机 A 检索自己的 ARP 表,发现 ARP 表中并无主机 B 的 IP 地址对应的映射条目,也就无从知道主机 B 的 MAC 地址。 + +2. 主机 A 将构造一个 ARP 查询分组,并将其广播到所在的局域网中。 + +ARP 分组是一种特殊报文,ARP 分组有两类,一种是查询分组,另一种是响应分组,它们具有相同的格式,均包含了发送和接收的 IP 地址、发送和接收的 MAC 地址。当然了,查询分组中,发送的 IP 地址,即为主机 A 的 IP 地址,接收的 IP 地址即为主机 B 的 IP 地址,发送的 MAC 地址也是主机 A 的 MAC 地址,但接收的 MAC 地址绝不会是主机 B 的 MAC 地址(因为这正是我们要问询的!),而是一个特殊值——FF-FF-FF-FF-FF-FF,之前说过,该 MAC 地址是广播地址,也就是说,查询分组将广播给该局域网内的所有设备。 + +3. 主机 A 构造的查询分组将在该局域网内广播,理论上,每一个设备都会收到该分组,并检查查询分组的接收 IP 地址是否为自己的 IP 地址,如果是,说明查询分组已经到达了主机 B,否则,该查询分组对当前设备无效,丢弃之。 + +4. 主机 B 收到了查询分组之后,验证是对自己的问询,接着构造一个 ARP 响应分组,该分组的目的地只有一个——主机 A,发送给主机 A。同时,主机 B 提取查询分组中的 IP 地址和 MAC 地址信息,在自己的 ARP 表中构造一条主机 A 的 IP-MAC 映射记录。 + +ARP 响应分组具有和 ARP 查询分组相同的构造,不同的是,发送和接受的 IP 地址恰恰相反,发送的 MAC 地址为发送者本身,目标 MAC 地址为查询分组的发送者,也就是说,ARP 响应分组只有一个目的地,而非广播。 + +5. 主机 A 终将收到主机 B 的响应分组,提取出该分组中的 IP 地址和 MAC 地址后,构造映射信息,加入到自己的 ARP 表中。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6kf8uoptjj20ou0fjdgx.jpg) + +在整个过程中,有几点需要补充说明的是: + +1. 主机 A 想要给主机 B 发送 IP 数据报,如果主机 B 的 IP-MAC 映射信息已经存在于主机 A 的 ARP 表中,那么主机 A 无需广播,只需提取 MAC 地址并构造链路层帧发送即可。 + +2. ARP 表中的映射信息是有生存周期的,典型值为 20 分钟。 + +3. 目标主机接收到了问询主机构造的问询报文后,将先把问询主机的 IP-MAC 映射存进自己的 ARP 表中,这样才能获取到响应的目标 MAC 地址,顺利的发送响应分组。 + +总结来说,ARP 协议是一个广播问询,单播响应协议。 + +### 不同局域网内的 MAC 寻址 +更复杂的情况是,发送主机 A 和接收主机 B 不在同一个子网中, + +假设一个一般场景,两台主机所在的子网由一台路由器联通。这里需要注意的是, + +一般情况下,我们说网络设备都有一个 IP 地址和一个 MAC 地址,这里说的网络设备, + +更严谨的说法应该是一个接口。路由器作为互联设备,具有多个接口,每个接口同样也应该具备不重复的 IP 地址和 MAC 地址。 + +因此,在讨论 ARP 表时,路由器的多个接口都个各自维护一个 ARP 表,而非一个路由器只维护一个 ARP 表。 + + + +接下来,回顾同一子网内的 MAC 寻址,如果主机 A 发送一个广播问询分组,那么 A 所在子网内的所有设备(接口)都将不会捕获该分组, + +因为该分组的目的 IP 地址在另一个子网中,本子网内不会有设备成功接收。 + +那么,主机 A 应该发送怎样的查询分组呢?整个过程按照时间顺序发生的事件如下: + +1. 主机 A 查询 ARP 表,期望寻找到目标路由器的本子网接口的 MAC 地址。 + +目标路由器指的是,根据目的主机 B 的 IP 地址,分析出 B 所在的子网,能够把报文转发到 B 所在子网的那个路由器。 + +2. 主机 A 未能找到目标路由器的本子网接口的 MAC 地址,将采用 ARP 协议,问询到该 MAC 地址,由于目标接口与主机 A 在同一个子网内,该过程与同一局域网内的 MAC 寻址相同。 + +3. 主机 A 获取到目标接口的 MAC 地址,先构造 IP 数据报,其中源 IP 是 A 的 IP 地址,目的 IP 地址是 B 的 IP 地址,再构造链路层帧,其中源 MAC 地址是 A 的 MAC 地址,目的 MAC 地址是本子网内与路由器连接的接口的 MAC 地址。主机 A 将把这个链路层帧,以单播的方式,发送给目标接口。 + +4. 目标接口接收到了主机 A 发过来的链路层帧,解析,根据目的 IP 地址,查询转发表,将该 IP 数据报转发到与主机 B 所在子网相连的接口上。 + +到此,该帧已经从主机 A 所在的子网,转移到了主机 B 所在的子网了。 + +5. 路由器接口查询 ARP 表,期望寻找到主机 B 的 MAC 地址。 + +6. 路由器接口如未能找到主机 B 的 MAC 地址,将采用 ARP 协议,广播问询,单播响应,获取到主机 B 的 MAC 地址。 + +7. 路由器接口将对 IP 数据报重新封装成链路层帧,目标 MAC 地址为主机 B 的 MAC 地址,单播发送,直到目的地。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6kfawf6i0j20qt0frwgj.jpg) \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/260.\347\256\200\350\277\260 WebSocket \346\230\257\345\246\202\344\275\225\350\277\233\350\241\214\344\274\240\350\276\223\347\232\204.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/260.\347\256\200\350\277\260 WebSocket \346\230\257\345\246\202\344\275\225\350\277\233\350\241\214\344\274\240\350\276\223\347\232\204.md" index d08be2a78..ec259f705 100644 --- "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/260.\347\256\200\350\277\260 WebSocket \346\230\257\345\246\202\344\275\225\350\277\233\350\241\214\344\274\240\350\276\223\347\232\204.md" +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/260.\347\256\200\350\277\260 WebSocket \346\230\257\345\246\202\344\275\225\350\277\233\350\241\214\344\274\240\350\276\223\347\232\204.md" @@ -8,3 +8,90 @@ categories: tags: - --- +一、什么是 WebSocket ? +WebSocket ——一种在 2011 年被互联网工程任务组( IETF )标准化的协议。 + +WebSocket 解决了一个长期存在的问题:既然底层的协议( HTTP )是一个请求 / 响应模式的交互序列, + +那么如何实时地发布信息呢? + +AJAX 提供了一定程度上的改善,但是数据流仍然是由客户端所发送的请求驱动的。 + +还有其他的一些或多或少的取巧方式(Comet) +WebSocket 规范以及它的实现代表了对一种更加有效的解决方案的尝试。 + +简单地说,WebSocket 提供了“在一个单个的 TCP 连接上提供双向的通信……结合 WebSocket API …… + +它为网页和远程服务器之间的双向通信提供了一种替代 HTTP 轮询的方案。 + +但是最终它们仍然属于扩展性受限的变通之法。也就是说,WebSocket 在客户端和 + +服务器之间提供了真正的双向数据交换。 + +WebSocket 连接允许客户端和服务器之间进行全双工通信, + +以便任一方都可以通过建立的连接将数据推送到另一端。 + +WebSocket 只需要建立一次连接,就可以一直保持连接状态。这相比于轮询方式的不停建立连接显然效率要大大提高。 + +Web 浏览器和服务器都必须实现 WebSockets 协议来建立和维护连接。 +特点 + +1. HTML5 中的协议,实现与客户端与服务器双向,基于消息的文本或二进制数据通信 + +2. 适合于对数据的实时性要求比较强的场景,如通信、直播、共享桌面,特别适合于客户与服务频繁交互的情况下,如实时共享、多人协作等平台。 + +3. 采用新的协议,后端需要单独实现 + +4. 客户端并不是所有浏览器都支持 + +## WebSocket 协议 + +WebSocket协议是一种双向通信协议,它建立在TCP之上,同http一样通过TCP来传输数据, + +但是它和http最大的不同有两点: + +1. WebSocket是一种双向通信协议,在建立连接后,WebSocket服务器和Browser/UA都能主动的向对方发送或接收数据,就像Socket一样, + +不同的是WebSocket是一种建立在Web基础上的一种简单模拟Socket的协议; + +2. WebSocket需要通过握手连接,类似于TCP它也需要客户端和服务器端进行握手连接,连接成功后才能相互通信。 + +下面是一个简单的建立握手的时序图: + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6kfkon1mrj20e508adg0.jpg) + +这里简单说明一下WebSocket握手的过程。 + +当Web应用程序调用new WebSocket(url)接口时,Browser就开始了与地址为url的WebServer建立握手连接的过程。 + +1. Browser与WebSocket服务器通过TCP三次握手建立连接,如果这个建立连接失败,那么后面的过程就不会执行,Web应用程序将收到错误消息通知。 + +2. 在TCP建立连接成功后,Browser/UA通过http协议传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端。 + +例如: + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6kfld24nij20mi0acjsq.jpg) + +3. WebSocket服务器收到Browser/UA发送来的握手请求后,如果数据包数据和格式正确,客户端和服务器端的协议版本号匹配等等, + +就接受本次握手连接,并给出相应的数据回复,同样回复的数据包也是采用http协议传输。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6kflwuc8mj20pi06iwfe.jpg) + +4. Browser收到服务器回复的数据包后,如果数据包内容、格式都没有问题的话,就表示本次连接成功,触发onopen消息,此时Web开发者就可以在此时通过send接口想服务器发送数据。否则,握手连接失败,Web应用程序会收到onerror消息,并且能知道连接失败的原因。 + +## WebSocket与TCP、Http协议之间的关系 +WebSocket与http协议一样都是基于TCP的,所以他们都是可靠的协议, + +Web开发者调用的WebSocket的send函数在browser的实现中最终都是通过TCP的系统接口进行传输的。 + +WebSocket和Http协议一样都属于应用层的协议,那么他们之间有没有什么关系呢? + +答案是肯定的,WebSocket在建立握手连接时,数据是通过http协议传输的,正如我们之前所看到的“GET/chat HTTP/1.1”, + +这里面用到的只是http协议一些简单的字段。但是在建立连接之后,真正的数据传输阶段是不需要http协议参与的。 + +具体关系可以参考下图: + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6kfmslnlkj20f006ft8r.jpg) \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/270.DNS \346\237\245\350\257\242\346\234\215\345\212\241\345\231\250\347\232\204\345\237\272\346\234\254\346\265\201\347\250\213\346\230\257\344\273\200\344\271\210\357\274\237DNS \345\212\253\346\214\201\346\230\257\344\273\200\344\271\210\357\274\237.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/270.DNS \346\237\245\350\257\242\346\234\215\345\212\241\345\231\250\347\232\204\345\237\272\346\234\254\346\265\201\347\250\213\346\230\257\344\273\200\344\271\210\357\274\237DNS \345\212\253\346\214\201\346\230\257\344\273\200\344\271\210\357\274\237.md" index 1e3611506..21dd32a0f 100644 --- "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/270.DNS \346\237\245\350\257\242\346\234\215\345\212\241\345\231\250\347\232\204\345\237\272\346\234\254\346\265\201\347\250\213\346\230\257\344\273\200\344\271\210\357\274\237DNS \345\212\253\346\214\201\346\230\257\344\273\200\344\271\210\357\274\237.md" +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/270.DNS \346\237\245\350\257\242\346\234\215\345\212\241\345\231\250\347\232\204\345\237\272\346\234\254\346\265\201\347\250\213\346\230\257\344\273\200\344\271\210\357\274\237DNS \345\212\253\346\214\201\346\230\257\344\273\200\344\271\210\357\274\237.md" @@ -8,3 +8,67 @@ categories: tags: - --- +## 1、DNS是什么? + +域名解析系统,可以将域名与IP地址建立映射关系,通过域名得知目标IP。 + +## 2、为什么会出DNS这种东西? + +在网络中,想要访问主机资源或者服务器资源需要知道目标的IP地址,但是IP地址又长数字又很容易记错,而且全世界那么多IP地址我也记不住,怎么办?就需要域名来代表一个IP地址,进行相互之间的映射关系 + +## 3、那DNS在我们生活中怎么用的? + +比如:在我们平常访问百度在网页地址栏输入: 这样我们就会进入百度的主页。 + +![](https://tva1.sinaimg.cn/large/e6c9d24egy1h6kfpr5dpdj21i80u0ada.jpg) + + +DNS实现过程 +1. 第一步:当在浏览器上输入域名时,我们的PC机会自动提出域名解析请求,并将请求发送给我们所配置的域名服务器进行解析 + +2. 第二步:域名服务器收到我们PC机的域名解析请求值后,会先查询本地的缓存(DNS缓存),如果有对应的域名与IP地址条目,域名服务器会将结果直接发送给我们的PC机,不会进一步解析操作。 + +3. 第三步:但是如果域名服务器的本地缓存中没有该纪录,则本地域名服务器就会把请求发给根域名服务器进行解析,然后根域名服务器收到后再返回给本地域名服务器一个所查询域(根的子域) 的主域名服务器的地址给我们的PC机。 + +4. 第四步:本地服务器再向上一步返回的域名服务器发送请求,然后接受请求的域名服务器会继续查询自己的缓存,如果没有该纪录,则返回相关的下级的域名服务器的地址。 + +5. 第五步:持续进行第四步的查询解析操作,直至解析到对应的纪录。 + +6. 第六步:本地域名服务器把歇息到的纪录返回的结果保存到缓存,以备下一次使用,同时还将结果返回给客户机 + +7. 第七步:客户机收到结果后向向目标IP发出访问请求,进行访问 + + + + +## 4、DNS劫持是什么? + +当我们访问某个网页时,如输入百度的域名,但是进入页面时其他的网页,这就是DNS劫持。 + +## 如何防止DNS劫持? +通常我们使用的DNS服务器使用的是服务商域名服务器,我们出来可以向运营商反馈这种情况外,还可以将自己的DNS服务器切换为其他第三方DNS服务器,如百度DNS、阿里DNS等等都是可以的。 + +## DNS劫持具体有哪些技术手段呢? +1. DNS缓存感染 + +攻击者使用DNS请求将数据放入脆弱的DNS服务器的缓存。然后这些缓存信息在用户进行DNS访问时返回用户,将对通常的域名的访问引导到入侵者设定的钓鱼、挂马等页面,或通过伪造邮件或其他server服务取得用户密码信息会给客人带来进一步的侵害。 + +2. DNS信息劫持 + +TCP/IP系统避免以诸如序列号等各种方式插入钓鱼数据,但是入侵者可以通过截取客户端与DNS服务器的交互来推测服务器响应于客户端的DNS查询ID。每个DNS报告包括相关联的16位ID号,并且DNS服务器从该ID号获取请求源位置。攻击者在DNS服务器前向用户发送虚假应答,欺骗客户访问恶意网站。 + +3. DNS重定向 + +攻击者如果将权威DNS服务器重定向到恶意DNS服务器,那么被劫持域名的解析就完全置于攻击者的控制之下。 + +4. 盗用DNS服务器 + +在该攻击中,攻击者不仅可以破坏DNS服务器还可以改变DNS记录,将DNS请求重定向到恶意网站。 + +5. 中间人(MiTM)DNS攻击 + +在DNS劫持中还有一种类型是,攻击者执行中间人(MiTM)攻击以截断用户与DNS服务器之间的通信,并向恶意网站重定向用户,以提供不同的目标IP地址。 + +6. 流氓DNS服务器 + +在此攻击中,攻击者可以破解DNS服务器,并更改DNS记录以将DNS请求重定向到恶意站点。 \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/280.RestFul \346\230\257\344\273\200\344\271\210\357\274\237RestFul \350\257\267\346\261\202\347\232\204 URL \346\234\211\344\273\200\344\271\210\347\211\271\347\202\271\357\274\237.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/280.RestFul \346\230\257\344\273\200\344\271\210\357\274\237RestFul \350\257\267\346\261\202\347\232\204 URL \346\234\211\344\273\200\344\271\210\347\211\271\347\202\271\357\274\237.md" index d23a16d76..5c744cb2e 100644 --- "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/280.RestFul \346\230\257\344\273\200\344\271\210\357\274\237RestFul \350\257\267\346\261\202\347\232\204 URL \346\234\211\344\273\200\344\271\210\347\211\271\347\202\271\357\274\237.md" +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/280.RestFul \346\230\257\344\273\200\344\271\210\357\274\237RestFul \350\257\267\346\261\202\347\232\204 URL \346\234\211\344\273\200\344\271\210\347\211\271\347\202\271\357\274\237.md" @@ -8,3 +8,131 @@ categories: tags: - --- +## REST 设计风格 +很多人会拿 REST 与 RPC 互相比较,其实,REST 无论是在思想上、概念上,还是使用范围上,与 RPC 都不尽相同,充其量只能算是有一些相似, + +应用会有一部分重合之处,但本质上并不是同一类型的东西。 + +REST 与 RPC 在思想上差异的核心是抽象的目标不一样,即面向资源的编程思想与面向过程的编程思想两者之间的区别。 + +面向过程编程、面向对象编程大家想必听说过,但什么是面向资源编程? + +这个问题等介绍完 REST 的特征之后我们再回头细说。 + +而概念上的不同是指 REST 并不是一种远程服务调用协议,甚至可以把定语也去掉,它就不是一种协议。 + +协议都带有一定的规范性和强制性,最起码也该有个规约文档,譬如 JSON-RPC,它哪怕再简单,也要有个[《JSON-RPC Specification》](https://www.jsonrpc.org/specification) 来规定协议的格式细节、异常、响应码等信息, + +但是 REST 并没有定义这些内容,尽管有一些指导原则,但实际上并不受任何强制的约束。 + +常有人批评某个系统接口“设计得不够 RESTful”,其实这句话本身就有些争议,REST 只能说是风格而不是规范、协议, + +并且能完全达到 REST 所有指导原则的系统也是不多见的,这一点我们同样将在后文中详细讨论。 + +至于使用范围,REST 与 RPC 作为主流的两种远程调用方式,在使用上是确有重合的,但重合的区域有多大就见仁见智了。 + + +## 理解 REST +个人会有好恶偏爱,但计算机科学是务实的,有了 RPC,还会提出 REST,有了面向过程编程之后,还能产生面向资源编程,并引起广泛的关注、使用和讨论, + +说明后者一定是有一些前者没有的闪光点,或者解决、避免了一些面向过程中的缺陷。我们不妨先去理解 REST 为什么会出现,然后再来讨论评价它。 + +简单搜索就能找到 REST 源于 Roy Thomas Fielding 在 2000 年发表的博士论文:[《Architectural Styles and the Design of Network-based Software Architectures》](https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm) ,此文的确是 REST 的源头,但我们不应该忽略 Fielding 的身份和此前的工作背景,这些信息对理解 REST 的设计思想至关重要。 + +首先,Fielding 是一名很优秀的软件工程师,他是 Apache 服务器的核心开发者,后来成为了著名的[Apache 软件基金会](https://www.apache.org/) 的联合创始人; + +同时,Fielding 也是 HTTP 1.0 协议(1996 年发布)的专家组成员,后来还晋升为 HTTP 1.1 协议(1999 年发布)的负责人。 + +HTTP 1.1 协议设计得极为成功,以至于发布之后长达十年的时间里,都没有收到多少修订的意见。 + +用来指导 HTTP 1.1 协议设计的理论和思想,最初是以备忘录的形式用作专家组成员之间交流,除了 IETF、W3C 的专家外,并没有在外界广泛流传。 + +从时间上看,对 HTTP 1.1 协议的设计工作贯穿了 Fielding 的整个博士研究生涯,当起草 HTTP 1.1 协议的工作完成后,Fielding 回到了加州大学欧文分校继续攻读自己的博士学位。 + +第二年,他更为系统、严谨地阐述了这套理论框架,并且以这套理论框架导出了一种新的编程思想,他为这种程序设计风格取了一个很多人难以理解,但是今天已经广为人知的名字 REST,即“表征状态转移”的缩写。 + +哪怕对编程和网络都很熟悉的同学,只从标题中也不太可能直接弄明白什么叫“表征”、啥东西的“状态”、从哪“转移”到哪。 + +尽管在论文原文中确有论述这些概念,但写得确实相当晦涩(不想读英文的同学从此处获得中文翻译版本),笔者推荐比较容易理解 REST 思想的途径是先理解什么是 HTTP, + +再配合一些实际例子来进行类比,你会发现“REST”(Representational State Transfer)实际上是“HTT”(Hypertext Transfer)的进一步抽象,两者就如同接口与实现类的关系一般。 + +HTTP 中使用的“超文本”(Hypertext)一词是美国社会学家 Theodor Holm Nelson 在 1967 年于[《Brief Words on the Hypertext》](https://archive.org/details/SelectedPapers1977) 一文里提出的,下面引用的是他本人在 1992 年修正后的定义: + +>Hypertext + +>By now the word "hypertext" has become generally accepted for branching and responding text, but the corresponding word "hypermedia", meaning complexes of branching and responding graphics, movies and sound – as well as text – is much less used. + +>现在,"超文本 "一词已被普遍接受,它指的是能够进行分支判断和差异响应的文本,相应地, "超媒体 "一词指的是能够进行分支判断和差异响应的图形、电影和声音(也包括文本)的复合体。 + +>—— Theodor Holm Nelson Literary Machines, 1992 + +以上定义描述的“超文本(或超媒体,Hypermedia)”是一种“能够对操作进行判断和响应的文本(或声音、图像等)”,这个概念在上世纪 60 年代提出时应该还属于科幻的范畴,但是今天大众已经完全接受了它,互联网中一段文字可以点击、可以触发脚本执行、可以调用服务端,这一切已毫不稀奇。下面我们继续尝试从“超文本”或者“超媒体”的含义来理解什么是“表征”以及 REST 中其他关键概念,这里使用一个具体事例将其描述如下。 + +* **资源(Resource)**:譬如你现在正在阅读一篇名为《REST 设计风格》的文章,这篇文章的内容本身(你可以将其理解为其蕴含的信息、数据)我们称之为“资源”。无论你是购买的书籍、是在浏览器看的网页、是打印出来看的文稿、是在电脑屏幕上阅读抑或是手机上浏览,尽管呈现的样子各不相同,但其中的信息是不变的,你所阅读的仍是同一份“资源”。 + +* **表征(Representation)**:当你通过电脑浏览器阅读此文章时,浏览器向服务端发出请求“我需要这个资源的 HTML 格式”,服务端向浏览器返回的这个 HTML 就被称之为“表征”,你可能通过其他方式拿到本文的 PDF、Markdown、RSS 等其他形式的版本,它们也同样是一个资源的多种表征。可见“表征”这个概念是指信息与用户交互时的表示形式,这与我们软件分层架构中常说的“表示层”(Presentation Layer)的语义其实是一致的。 + +* **状态(State)**:当你读完了这篇文章,想看后面是什么内容时,你向服务器发出请求“给我下一篇文章”。但是“下一篇”是个相对概念,必须依赖“当前你正在阅读的文章是哪一篇”才能正确回应,这类在特定语境中才能产生的上下文信息即被称为“状态”。我们所说的有状态(Stateful)抑或是无状态(Stateless),都是只相对于服务端来说的,服务器要完成“取下一篇”的请求,要么自己记住用户的状态:这个用户现在阅读的是哪一篇文章,这称为有状态;要么客户端来记住状态,在请求的时候明确告诉服务器:我正在阅读某某文章,现在要读它的下一篇,这称为无状态。 + +* **转移(Transfer)**:无论状态是由服务端还是客户端来提供的,“取下一篇文章”这个行为逻辑必然只能由服务端来提供,因为只有服务端拥有该资源及其表征形式。服务器通过某种方式,把“用户当前阅读的文章”转变成“下一篇文章”,这就被称为“表征状态转移”。 + +通过“阅读文章”这个例子,笔者对资源等概念进行通俗的释义,你应该能够理解 REST 所说的“表征状态转移”的含义了。借着这个故事的上下文状态,笔者再继续介绍几个现在不涉及但稍后要用到的概念名词。 + +* **统一接口(Uniform Interface)**:上面说的服务器“通过某种方式”让表征状态发生转移,具体是什么方式?如果你真的是用浏览器阅读本文电子版的话,请把本文滚动到结尾处,右下角有下一篇文章的 URI 超链接地址,这是服务端渲染这篇文章时就预置好的,点击它让页面跳转到下一篇,就是所谓“某种方式”的其中一种方式。任何人都不会对点击超链接网页会出现跳转感到奇怪,但你细想一下,URI 的含义是统一资源标识符,是一个名词,如何能表达出“转移”动作的含义呢?答案是 HTTP 协议中已经提前约定好了一套“统一接口”,它包括:GET、HEAD、POST、PUT、DELETE、TRACE、OPTIONS 七种基本操作,任何一个支持 HTTP 协议的服务器都会遵守这套规定,对特定的 URI 采取这些操作,服务器就会触发相应的表征状态转移。 + +* **超文本驱动(Hypertext Driven)**:尽管表征状态转移是由浏览器主动向服务器发出请求所引发的,该请求导致了“在浏览器屏幕上显示出了下一篇文章的内容”这个结果的出现。但是,你我都清楚这不可能真的是浏览器的主动意图,浏览器是根据用户输入的 URI 地址来找到网站首页,服务器给予的首页超文本内容后,浏览器再通过超文本内部的链接来导航到了这篇文章,阅读结束时,也是通过超文本内部的链接来再导航到下一篇。浏览器作为所有网站的通用的客户端,任何网站的导航(状态转移)行为都不可能是预置于浏览器代码之中,而是由服务器发出的请求响应信息(超文本)来驱动的。这点与其他带有客户端的软件有十分本质的区别,在那些软件中,业务逻辑往往是预置于程序代码之中的,有专门的页面控制器(无论在服务端还是在客户端中)来驱动页面的状态转移。 + +* **自描述消息(Self-Descriptive Messages)**:由于资源的表征可能存在多种不同形态,在消息中应当有明确的信息来告知客户端该消息的类型以及应如何处理这条消息。一种被广泛采用的自描述方法是在名为“Content-Type”的 HTTP Header 中标识出互联网媒体类型(MIME type),譬如“Content-Type : application/json; charset=utf-8”,则说明该资源会以 JSON 的格式来返回,请使用 UTF-8 字符集进行处理。 + +除了以上列出的这些看名字不容易弄懂的概念外,在理解 REST 的过程中,还有一个常见的误区值得注意,Fielding 提出 REST 时所谈论的范围是“架构风格与网络的软件架构设计”(Architectural Styles and Design of Network-based Software Architectures),而不是现在被人们所狭义理解的一种“远程服务设计风格”,这两者的范围差别就好比本书所谈论的话题“软件架构”与本章谈论话题“访问远程服务”的关系那样,前者是后者的一个很大的超集,尽管基于本节的主题和多数人的关注点考虑,我们确实是会以“远程服务设计风格”作为讨论的重点,但至少应该说明清楚它们范围上的差别。 + +## RESTful 的系统 +如果你已经理解了上面这些概念,我们就可以开始讨论面向资源的编程思想与 Fielding 所提出的几个具体的软件架构设计原则了。Fielding 认为,一套理想的、完全满足 REST 风格的系统应该满足以下六大原则。 + +1. 服务端与客户端分离(Client-Server) +将用户界面所关注的逻辑和数据存储所关注的逻辑分离开来,有助于提高用户界面的跨平台的可移植性,这一点正越来越受到广大开发者所认可,以前完全基于服务端控制和渲染(如 JSF 这类)框架实际用户已甚少,而在服务端进行界面控制(Controller),通过服务端或者客户端的模版渲染引擎来进行界面渲染的框架(如 Struts、SpringMVC 这类)也受到了颇大的冲击。这一点主要推动力量与 REST 可能关系并不大,前端技术(从 ES 规范,到语言实现,到前端框架等)的近年来的高速发展,使得前端表达能力大幅度加强才是真正的幕后推手。由于前端的日渐强势,现在还流行起由前端代码反过来驱动服务端进行渲染的 SSR(Server-Side Rendering)技术,在 Serverless、SEO 等场景中已经占领了一块领地。 + +2. 无状态(Stateless) +无状态是 REST 的一条核心原则,部分开发者在做服务接口规划时,觉得 REST 风格的服务怎么设计都感觉别扭,很有可能的一种原因是在服务端持有着比较重的状态。REST 希望服务器不要去负责维护状态,每一次从客户端发送的请求中,应包括所有的必要的上下文信息,会话信息也由客户端负责保存维护,服务端依据客户端传递的状态来执行业务处理逻辑,驱动整个应用的状态变迁。客户端承担状态维护职责以后,会产生一些新的问题,譬如身份认证、授权等可信问题,它们都应有针对性的解决方案(这部分内容可参见“安全架构”的内容)。 +但必须承认的现状是,目前大多数的系统都达不到这个要求,往往越复杂、越大型的系统越是如此。服务端无状态可以在分布式计算中获得非常高价值的好处,但大型系统的上下文状态数量完全可能膨胀到让客户端在每次请求时提供变得不切实际的程度,在服务端的内存、会话、数据库或者缓存等地方持有一定的状态成为一种是事实上存在,并将长期存在、被广泛使用的主流的方案。 + +3. 可缓存(Cacheability) +无状态服务虽然提升了系统的可见性、可靠性和可伸缩性,但降低了系统的网络性。“降低网络性”的通俗解释是某个功能如果使用有状态的设计只需要一次(或少量)请求就能完成,使用无状态的设计则可能会需要多次请求,或者在请求中带有额外冗余的信息。为了缓解这个矛盾,REST 希望软件系统能够如同万维网一样,允许客户端和中间的通讯传递者(譬如代理)将部分服务端的应答缓存起来。当然,为了缓存能够正确地运作,服务端的应答中必须明确地或者间接地表明本身是否可以进行缓存、可以缓存多长时间,以避免客户端在将来进行请求的时候得到过时的数据。运作良好的缓存机制可以减少客户端、服务器之间的交互,甚至有些场景中可以完全避免交互,这就进一步提高了性能。 + +4. 分层系统(Layered System) +这里所指的并不是表示层、服务层、持久层这种意义上的分层。而是指客户端一般不需要知道是否直接连接到了最终的服务器,抑或连接到路径上的中间服务器。中间服务器可以通过负载均衡和共享缓存的机制提高系统的可扩展性,这样也便于缓存、伸缩和安全策略的部署。该原则的典型的应用是内容分发网络(Content Distribution Network,CDN)。如果你是通过网站浏览到这篇文章的话,你所发出的请求一般(假设你在中国国境内的话)并不是直接访问位于 GitHub Pages 的源服务器,而是访问了位于国内的 CDN 服务器,但作为用户,你完全不需要感知到这一点。我们将在“透明多级分流系统”中讨论如何构建自动的、可缓存的分层系统。 + +5. 统一接口(Uniform Interface) +这是 REST 的另一条核心原则,REST 希望开发者面向资源编程,希望软件系统设计的重点放在抽象系统该有哪些资源上,而不是抽象系统该有哪些行为(服务)上。这条原则你可以类比计算机中对文件管理的操作来理解,管理文件可能会进行创建、修改、删除、移动等操作,这些操作数量是可数的,而且对所有文件都是固定的、统一的。如果面向资源来设计系统,同样会具有类似的操作特征,由于 REST 并没有设计新的协议,所以这些操作都借用了 HTTP 协议中固有的操作命令来完成。 +统一接口也是 REST 最容易陷入争论的地方,基于网络的软件系统,到底是面向资源更好,还是面向服务更合适,这事情哪怕是很长时间里都不会有个定论,也许永远都没有。但是,已经有一个基本清晰的结论是:面向资源编程的抽象程度通常更高。抽象程度高意味着坏处是往往距离人类的思维方式更远,而好处是往往通用程度会更好。用这样的语言去诠释 REST,大概本身就挺抽象的,笔者还是举个例子来说明:譬如,几乎每个系统都有的登录和注销功能,如果你理解成登录对应于 login()服务,注销对应于 logout()服务这样两个独立服务,这是“符合人类思维”的;如果你理解成登录是 PUT Session,注销是 DELETE Session,这样你只需要设计一种“Session 资源”即可满足需求,甚至以后对 Session 的其他需求,如查询登陆用户的信息,就是 GET Session 而已,其他操作如修改用户信息等都可以被这同一套设计囊括在内,这便是“抽象程度更高”带来的好处。 +想要在架构设计中合理恰当地利用统一接口,Fielding 建议系统应能做到每次请求中都包含资源的 ID,所有操作均通过资源 ID 来进行;建议每个资源都应该是自描述的消息;建议通过超文本来驱动应用状态的转移。 + +6. 按需代码([Code-On-Demand](https://en.wikipedia.org/wiki/Code_on_demand) ) +按需代码被 Fielding 列为一条可选原则。它是指任何按照客户端(譬如浏览器)的请求,将可执行的软件程序从服务器发送到客户端的技术,按需代码赋予了客户端无需事先知道所有来自服务端的信息应该如何处理、如何运行的宽容度。举个具体例子,以前的[Java Applet](https://en.wikipedia.org/wiki/Java_applet) 技术,今天的[WebAssembly](https://en.wikipedia.org/wiki/WebAssembly) 等都属于典型的按需代码,蕴含着具体执行逻辑的代码是存放在服务端,只有当客户端请求了某个 Java Applet 之后,代码才会被传输并在客户端机器中运行,结束后通常也会随即在客户端中被销毁掉。将按需代码列为可选原则的原因并非是它特别难以达到,而更多是出于必要性和性价比的实际考虑。 + +至此,REST 中的主要概念与思想原则已经介绍完毕,我们再回过头来讨论本节开篇提出的 REST 与 RPC 在思想上的差异。REST 的基本思想是面向资源来抽象问题,它与此前流行的编程思想——面向过程的编程在抽象主体上有本质的差别。在 REST 提出以前,人们设计分布式系统服务的唯一方案就只有 RPC,RPC 是将本地的方法调用思路迁移到远程方法调用上,开发者是围绕着“远程方法”去设计两个系统间交互的,譬如 CORBA、RMI、DCOM,等等。这样做的坏处不仅是“如何在异构系统间表示一个方法”、“如何获得接口能够提供的方法清单”都成了需要专门协议去解决的问题(RPC 的三大基本问题之一),更在于服务的每个方法都是完全独立的,服务使用者必须逐个学习才能正确地使用它们。Google 在[《Google API Design Guide》](https://cloud.google.com/apis/design) 中曾经写下这样一段话: + +>Traditionally, people design RPC APIs in terms of API interfaces and methods, such as CORBA and Windows COM. As time goes by, more and more interfaces and methods are introduced. The end result can be an overwhelming number of interfaces and methods, each of them different from the others. Developers have to learn each one carefully in order to use it correctly, which can be both time consuming and error prone + +以前,人们面向方法去设计 RPC API,譬如 CORBA 和 DCOM,随着时间推移,接口与方法越来越多却又各不相同,开发人员必须了解每一个方法才能正确使用它们,这样既耗时又容易出错。 + +—— Google API Design Guide, 2017 + +REST 提出以资源为主体进行服务设计的风格,能为它带来不少好处(自然也有坏处,笔者将在下一节集中谈论 REST 的不足与争议),譬如: + +* 降低的服务接口的学习成本。统一接口(Uniform Interface)是 REST 的重要标志,将对资源的标准操作都映射到了标准的 HTTP 方法上去,这些方法对于每个资源的用法都是一致的,语义都是类似的,不需要刻意去学习,更不需要有什么 Interface Description Language 之类的协议存在。 + +* 资源天然具有集合与层次结构。以方法为中心抽象的接口,由于方法是动词,逻辑上决定了每个接口都是互相独立的;但以资源为中心抽象的接口,由于资源是名词,天然就可以产生集合与层次结构。举个具体例子,你想像一个商城用户中心的接口设计:用户资源会拥有多个不同的下级的资源,譬如若干条短消息资源、一份用户资料资源、一部购物车资源,购物车中又会有自己的下级资源,譬如多本书籍资源。很容易在程序接口中构造出这些资源的集合关系与层次关系,而且是符合人们长期在单机或网络环境中管理数据的直觉的。相信你不需要专门阅读接口说明书,也能轻易推断出获取用户icyfenix的购物车中的第2本书的 REST 接口应该表示为: +```shell script +GET /users/icyfenix/cart/2 +``` + +* REST 绑定于 HTTP 协议。面向资源编程不是必须构筑在 HTTP 之上,但 REST 是,这是缺点,也是优点。因为 HTTP 本来就是面向资源而设计的网络协议,纯粹只用 HTTP(而不是 SOAP over HTTP 那样在再构筑协议)带来的好处是 RPC 中的 Wire Protocol 问题就无需再多考虑了,REST 将复用 HTTP 协议中已经定义的概念和相关基础支持来解决问题。HTTP 协议已经有效运作了三十年,其相关的技术基础设施已是千锤百炼,无比成熟。而坏处自然是,当你想去考虑那些 HTTP 不提供的特性时,便会彻底地束手无策。 + +* …… + +以上列举了一些面向资源的优点,笔者并非要证明它比面向过程、面向对象更优秀,是否选用 REST 的 API 设计风格,需要权衡的是你的需求场景、你团队的设计和开发人员是否能够适应面向资源的思想来设计软件,来编写代码。在互联网中,面向资源来进行网络传输是这三十年来 HTTP 协议精心培养出来的用户习惯,如果开发者能够适应 REST 不太符合人类思维习惯的抽象方式,那 REST 通常能够更好地匹配在 HTTP 基础上构建的互联网,在效率与扩展性方面会有可观的收益。 + +## 参考 +[REST设计风格](https://icyfenix.cn/architect-perspective/general-architecture/api-style/rest.html#%E7%90%86%E8%A7%A3-rest) \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/290.\347\256\200\350\277\260 HTTP \346\212\245\346\226\207\345\244\264\351\203\250\347\232\204\347\273\204\346\210\220\347\273\223\346\236\204.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/290.\347\256\200\350\277\260 HTTP \346\212\245\346\226\207\345\244\264\351\203\250\347\232\204\347\273\204\346\210\220\347\273\223\346\236\204.md" index 43f7edb07..09166774e 100644 --- "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/290.\347\256\200\350\277\260 HTTP \346\212\245\346\226\207\345\244\264\351\203\250\347\232\204\347\273\204\346\210\220\347\273\223\346\236\204.md" +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/290.\347\256\200\350\277\260 HTTP \346\212\245\346\226\207\345\244\264\351\203\250\347\232\204\347\273\204\346\210\220\347\273\223\346\236\204.md" @@ -8,3 +8,7 @@ categories: tags: - --- + + +## 参考 +[HTTP 报文结构详解](https://zhuanlan.zhihu.com/p/450128753) \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/300.\344\273\200\344\271\210\346\230\257 SYN flood\357\274\214\345\246\202\344\275\225\351\230\262\346\255\242\350\277\231\347\261\273\346\224\273\345\207\273\357\274\237.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/300.\344\273\200\344\271\210\346\230\257 SYN flood\357\274\214\345\246\202\344\275\225\351\230\262\346\255\242\350\277\231\347\261\273\346\224\273\345\207\273\357\274\237.md" deleted file mode 100644 index c572996c4..000000000 --- "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/10.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234/300.\344\273\200\344\271\210\346\230\257 SYN flood\357\274\214\345\246\202\344\275\225\351\230\262\346\255\242\350\277\231\347\261\273\346\224\273\345\207\273\357\274\237.md" +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: 什么是 SYN flood,如何防止这类攻击? -date: 2022-09-21 22:55:33 -permalink: /pages/367c4d/ -categories: - - 计算机基础 - - 计算机网络 -tags: - - ---- diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/01.\350\277\233\347\250\213\351\227\264\346\234\211\345\223\252\344\272\233\351\200\232\344\277\241\346\226\271\345\274\217\357\274\237.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/01.\350\277\233\347\250\213\351\227\264\346\234\211\345\223\252\344\272\233\351\200\232\344\277\241\346\226\271\345\274\217\357\274\237.md" index 36d11d871..f76be46c9 100644 --- "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/01.\350\277\233\347\250\213\351\227\264\346\234\211\345\223\252\344\272\233\351\200\232\344\277\241\346\226\271\345\274\217\357\274\237.md" +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/01.\350\277\233\347\250\213\351\227\264\346\234\211\345\223\252\344\272\233\351\200\232\344\277\241\346\226\271\345\274\217\357\274\237.md" @@ -8,3 +8,59 @@ categories: tags: - --- +进程同步与进程通信很容易混淆,它们的区别在于: + +* 进程同步:控制多个进程按一定顺序执行; +* 进程通信:进程间传输信息。 + +进程通信是一种手段,而进程同步是一种目的。也可以说,为了能够达到进程同步的目的,需要让进程进行通信,传输一些进程同步所需要的信息。 + +## 1. 管道 +管道是通过调用 pipe 函数创建的,fd[0] 用于读,fd[1] 用于写。 + +``` +#include +int pipe(int fd[2]); +``` + +它具有以下限制: + +* 只支持半双工通信(单向交替传输); +* 只能在父子进程或者兄弟进程中使用。 + +![](https://cdn.jsdelivr.net/gh/AdmireAn/blobImage@main/img/20221008224734.png) + +## 2. FIFO +也称为命名管道,去除了管道只能在父子进程中使用的限制。 + +``` +#include +int mkfifo(const char *path, mode_t mode); +int mkfifoat(int fd, const char *path, mode_t mode); +``` + +FIFO 常用于客户-服务器应用程序中,FIFO 用作汇聚点,在客户进程和服务器进程之间传递数据。 + +![](https://cdn.jsdelivr.net/gh/AdmireAn/blobImage@main/img/20221008224910.png) + +## 3. 消息队列 +相比于 FIFO,消息队列具有以下优点: + +消息队列可以独立于读写进程存在,从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难; +避免了 FIFO 的同步阻塞问题,不需要进程自己提供同步方法; +读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收。 +## 4. 信号量 +它是一个计数器,用于为多个进程提供对共享数据对象的访问。 + +## 5. 共享存储 +允许多个进程共享一个给定的存储区。因为数据不需要在进程之间复制,所以这是最快的一种 IPC。 + +需要使用信号量用来同步对共享存储的访问。 + +多个进程可以将同一个文件映射到它们的地址空间从而实现共享内存。另外 XSI 共享内存不是使用文件,而是使用内存的匿名段。 + +## 6. 套接字 +与其它通信机制不同的是,它可用于不同机器间的进程通信。 + +## 参考 +[进程通信](http://www.cyc2018.xyz/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%9F%BA%E7%A1%80/%E8%AE%A1%E7%AE%97%E6%9C%BA%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%20-%20%E8%BF%9B%E7%A8%8B%E7%AE%A1%E7%90%86.html#_2-fifo) \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/020.\350\277\233\347\250\213\345\222\214\347\272\277\347\250\213\344\271\213\351\227\264\346\234\211\344\273\200\344\271\210\345\214\272\345\210\253\357\274\237.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/020.\350\277\233\347\250\213\345\222\214\347\272\277\347\250\213\344\271\213\351\227\264\346\234\211\344\273\200\344\271\210\345\214\272\345\210\253\357\274\237.md" index 7b498f5c2..bbd6349c6 100644 --- "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/020.\350\277\233\347\250\213\345\222\214\347\272\277\347\250\213\344\271\213\351\227\264\346\234\211\344\273\200\344\271\210\345\214\272\345\210\253\357\274\237.md" +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/020.\350\277\233\347\250\213\345\222\214\347\272\277\347\250\213\344\271\213\351\227\264\346\234\211\344\273\200\344\271\210\345\214\272\345\210\253\357\274\237.md" @@ -8,3 +8,37 @@ categories: tags: - --- +## 1. 进程 +进程是资源分配的基本单位。 + +进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。 + +下图显示了 4 个程序创建了 4 个进程,这 4 个进程可以并发地执行。 + +![](https://tva1.sinaimg.cn/large/006y8mN6ly1h6y90gdfqvj30mb084dfz.jpg) + +## 2. 线程 +线程是独立调度的基本单位。 + +一个进程中可以有多个线程,它们共享进程资源。 + +QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。 + +![](https://tva1.sinaimg.cn/large/006y8mN6ly1h6y911vsrqj30a708g748.jpg) + +## 3. 区别 +### Ⅰ 拥有资源 + +进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。 + +### Ⅱ 调度 + +线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。 + +### Ⅲ 系统开销 + +由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。 + +### Ⅳ 通信方面 + +线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。 diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/060.\350\277\233\347\250\213\346\234\211\345\244\232\345\260\221\347\247\215\347\212\266\346\200\201\357\274\237.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/060.\350\277\233\347\250\213\346\234\211\345\244\232\345\260\221\347\247\215\347\212\266\346\200\201\357\274\237.md" index 1427a1a69..e03168f32 100644 --- "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/060.\350\277\233\347\250\213\346\234\211\345\244\232\345\260\221\347\247\215\347\212\266\346\200\201\357\274\237.md" +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/060.\350\277\233\347\250\213\346\234\211\345\244\232\345\260\221\347\247\215\347\212\266\346\200\201\357\274\237.md" @@ -8,3 +8,13 @@ categories: tags: - --- +![](https://cdn.jsdelivr.net/gh/AdmireAn/blobImage@main/img/20221008225033.png) + +* 就绪状态(ready):等待被调度 +* 运行状态(running) +* 阻塞状态(waiting):等待资源 +应该注意以下内容: + +* 只有就绪态和运行态可以相互转换,其它的都是单向转换。就绪状态的进程通过调度算法从而获得 CPU 时间,转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。 + +* 阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括 CPU 时间,缺少 CPU 时间会从运行态转换为就绪态。 \ No newline at end of file diff --git "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/070.\347\256\200\350\277\260 select, poll, epoll \347\232\204\344\275\277\347\224\250\345\234\272\346\231\257\344\273\245\345\217\212\345\214\272\345\210\253.md" "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/070.\347\256\200\350\277\260 select, poll, epoll \347\232\204\344\275\277\347\224\250\345\234\272\346\231\257\344\273\245\345\217\212\345\214\272\345\210\253.md" index f5e5933bb..d584ed287 100644 --- "a/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/070.\347\256\200\350\277\260 select, poll, epoll \347\232\204\344\275\277\347\224\250\345\234\272\346\231\257\344\273\245\345\217\212\345\214\272\345\210\253.md" +++ "b/docs/02.\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/20.\346\223\215\344\275\234\347\263\273\347\273\237/070.\347\256\200\350\277\260 select, poll, epoll \347\232\204\344\275\277\347\224\250\345\234\272\346\231\257\344\273\245\345\217\212\345\214\272\345\210\253.md" @@ -8,3 +8,219 @@ categories: tags: - --- +select/poll/epoll 都是 I/O 多路复用的具体实现,select 出现的最早,之后是 poll,再是 epoll。 + +## select + +``` +int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); +``` + +select 允许应用程序监视一组文件描述符,等待一个或者多个描述符成为就绪状态,从而完成 I/O 操作。 + +* fd_set 使用数组实现,数组大小使用 FD_SETSIZE 定义,所以只能监听少于 FD_SETSIZE 数量的描述符。有三种类型的描述符类型:readset、writeset、exceptset,分别对应读、写、异常条件的描述符集合。 + +* timeout 为超时参数,调用 select 会一直阻塞直到有描述符的事件到达或者等待的时间超过 timeout。 + +* 成功调用返回结果大于 0,出错返回结果为 -1,超时返回结果为 0。 + +``` +fd_set fd_in, fd_out; +struct timeval tv; + +// Reset the sets +FD_ZERO( &fd_in ); +FD_ZERO( &fd_out ); + +// Monitor sock1 for input events +FD_SET( sock1, &fd_in ); + +// Monitor sock2 for output events +FD_SET( sock2, &fd_out ); + +// Find out which socket has the largest numeric value as select requires it +int largest_sock = sock1 > sock2 ? sock1 : sock2; + +// Wait up to 10 seconds +tv.tv_sec = 10; +tv.tv_usec = 0; + +// Call the select +int ret = select( largest_sock + 1, &fd_in, &fd_out, NULL, &tv ); + +// Check if select actually succeed +if ( ret == -1 ) + // report error and abort +else if ( ret == 0 ) + // timeout; no event detected +else +{ + if ( FD_ISSET( sock1, &fd_in ) ) + // input event on sock1 + + if ( FD_ISSET( sock2, &fd_out ) ) + // output event on sock2 +} +``` + +## poll + +``` +int poll(struct pollfd *fds, unsigned int nfds, int timeout); +``` + +poll 的功能与 select 类似,也是等待一组描述符中的一个成为就绪状态。 + +poll 中的描述符是 pollfd 类型的数组,pollfd 的定义如下: + +``` +struct pollfd { + int fd; /* file descriptor */ + short events; /* requested events */ + short revents; /* returned events */ + }; +``` + +``` +// The structure for two events +struct pollfd fds[2]; + +// Monitor sock1 for input +fds[0].fd = sock1; +fds[0].events = POLLIN; + +// Monitor sock2 for output +fds[1].fd = sock2; +fds[1].events = POLLOUT; + +// Wait 10 seconds +int ret = poll( &fds, 2, 10000 ); +// Check if poll actually succeed +if ( ret == -1 ) + // report error and abort +else if ( ret == 0 ) + // timeout; no event detected +else +{ + // If we detect the event, zero it out so we can reuse the structure + if ( fds[0].revents & POLLIN ) + fds[0].revents = 0; + // input event on sock1 + + if ( fds[1].revents & POLLOUT ) + fds[1].revents = 0; + // output event on sock2 +} +``` + +## 比较 +### 1. 功能 +select 和 poll 的功能基本相同,不过在一些实现细节上有所不同。 + +* select 会修改描述符,而 poll 不会; +* select 的描述符类型使用数组实现,FD_SETSIZE 大小默认为 1024,因此默认只能监听少于 1024 个描述符。如果要监听更多描述符的话,需要修改 FD_SETSIZE 之后重新编译;而 poll 没有描述符数量的限制; +* poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高。 +* 如果一个线程对某个描述符调用了 select 或者 poll,另一个线程关闭了该描述符,会导致调用结果不确定。 +### 2. 速度 +select 和 poll 速度都比较慢,每次调用都需要将全部描述符从应用进程缓冲区复制到内核缓冲区。 + +### 3. 可移植性 +几乎所有的系统都支持 select,但是只有比较新的系统支持 poll。 + +## epoll + +``` +int epoll_create(int size); +int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); +int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); +``` + +epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符。 + +从上面的描述可以看出,epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次,并且进程不需要通过轮询来获得事件完成的描述符。 + +epoll 仅适用于 Linux OS。 + +epoll 比 select 和 poll 更加灵活而且没有描述符数量限制。 + +epoll 对多线程编程更有友好,一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符也不会产生像 select 和 poll 的不确定情况。 + +``` +// Create the epoll descriptor. Only one is needed per app, and is used to monitor all sockets. +// The function argument is ignored (it was not before, but now it is), so put your favorite number here +int pollingfd = epoll_create( 0xCAFE ); + +if ( pollingfd < 0 ) + // report error + +// Initialize the epoll structure in case more members are added in future +struct epoll_event ev = { 0 }; + +// Associate the connection class instance with the event. You can associate anything +// you want, epoll does not use this information. We store a connection class pointer, pConnection1 +ev.data.ptr = pConnection1; + +// Monitor for input, and do not automatically rearm the descriptor after the event +ev.events = EPOLLIN | EPOLLONESHOT; +// Add the descriptor into the monitoring list. We can do it even if another thread is +// waiting in epoll_wait - the descriptor will be properly added +if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, pConnection1->getSocket(), &ev ) != 0 ) + // report error + +// Wait for up to 20 events (assuming we have added maybe 200 sockets before that it may happen) +struct epoll_event pevents[ 20 ]; + +// Wait for 10 seconds, and retrieve less than 20 epoll_event and store them into epoll_event array +int ready = epoll_wait( pollingfd, pevents, 20, 10000 ); +// Check if epoll actually succeed +if ( ret == -1 ) + // report error and abort +else if ( ret == 0 ) + // timeout; no event detected +else +{ + // Check if any events detected + for ( int i = 0; i < ready; i++ ) + { + if ( pevents[i].events & EPOLLIN ) + { + // Get back our connection pointer + Connection * c = (Connection*) pevents[i].data.ptr; + c->handleReadEvent(); + } + } +} +``` + +## 工作模式 +epoll 的描述符事件有两种触发模式:LT(level trigger)和 ET(edge trigger)。 + +### 1. LT 模式 +当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。 + +### 2. ET 模式 +和 LT 模式不同的是,通知之后进程必须立即处理事件,下次再调用 epoll_wait() 时不会再得到事件到达的通知。 + +很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。 + +## 应用场景 +很容易产生一种错觉认为只要用 epoll 就可以了,select 和 poll 都已经过时了,其实它们都有各自的使用场景。 + +### 1. select 应用场景 +select 的 timeout 参数精度为微秒,而 poll 和 epoll 为毫秒,因此 select 更加适用于实时性要求比较高的场景,比如核反应堆的控制。 + +select 可移植性更好,几乎被所有主流平台所支持。 + +### 2. poll 应用场景 +poll 没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应该使用 poll 而不是 select。 + +### 3. epoll 应用场景 +只需要运行在 Linux 平台上,有大量的描述符需要同时轮询,并且这些连接最好是长连接。 + +需要同时监控小于 1000 个描述符,就没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。 + +需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。 + +因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。 + +并且 epoll 的描述符存储在内核,不容易调试。 diff --git a/node_modules/@vuepress/core/.temp/internal/app-enhancers.js b/node_modules/@vuepress/core/.temp/internal/app-enhancers.js index 256fdff06..d8d65e0df 100644 --- a/node_modules/@vuepress/core/.temp/internal/app-enhancers.js +++ b/node_modules/@vuepress/core/.temp/internal/app-enhancers.js @@ -2,7 +2,7 @@ import m0 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_module import m1 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_modules/@vuepress/core/.temp/app-enhancers/1.js" import m2 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_modules/@vuepress/core/.temp/app-enhancers/2.js" import m3 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_modules/@vuepress/core/.temp/app-enhancers/data-block.js" -import m4 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_modules/@vuepress/core/.temp/app-enhancers/global-components-23.js" +import m4 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_modules/@vuepress/core/.temp/app-enhancers/global-components-14.js" import m5 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_modules/@vuepress/core/.temp/app-enhancers/3.js" import m6 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_modules/@vuepress/core/.temp/app-enhancers/4.js" import m7 from "/Users/yonganwang/Documents/kuaishou-java/JavaDriver/node_modules/@vuepress/core/.temp/app-enhancers/5.js" diff --git a/node_modules/@vuepress/core/.temp/internal/page-components.js b/node_modules/@vuepress/core/.temp/internal/page-components.js index 5f8e49eee..45caa94c4 100644 --- a/node_modules/@vuepress/core/.temp/internal/page-components.js +++ b/node_modules/@vuepress/core/.temp/internal/page-components.js @@ -6,48 +6,48 @@ export default { "v-a612ff08": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/00.目录页/03.数据库.md"), "v-10f5d0e5": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/00.目录页/02.计算机基础.md"), "v-7abe58c4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/00.目录页/04.中间件.md"), - "v-0734374b": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/010.HashMap 与 ConcurrentHashMap 的实现原理是怎样的?.md"), "v-8bf153e4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/001.Java 异常有哪些类型?.md"), - "v-4e1fd39c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/020.Java 中接口和抽象类的区别.md"), + "v-0734374b": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/010.HashMap 与 ConcurrentHashMap 的实现原理是怎样的?.md"), "v-57012c44": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/030.为什么重写 equals 必须重写 hashcode ?.md"), - "v-7f48b402": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/050.String,StringBuffer,StringBuilder 之间有什么区别?.md"), "v-d8a2f15c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/040.== 和 equals() 的区别?.md"), - "v-d4e20148": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/070.简述 BIO, NIO, AIO 的区别.md"), + "v-4e1fd39c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/020.Java 中接口和抽象类的区别.md"), + "v-7f48b402": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/050.String,StringBuffer,StringBuilder 之间有什么区别?.md"), "v-57b2551a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/060.简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度.md"), + "v-d4e20148": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/070.简述 BIO, NIO, AIO 的区别.md"), "v-85b06162": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/090.简述 Java 的反射机制及其应用场景.md"), "v-4c273a66": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/100.Java 类的加载流程是怎样的?.md"), + "v-d32e07ea": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/080.在一个静态方法内调用一个非静态成员为什么是非法的?.md"), "v-7665e30e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/110.什么是双亲委派机制?.md"), "v-6ff6e2f0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/120.Java 中 sleep() 与 wait() 的区别 .md"), - "v-d32e07ea": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/080.在一个静态方法内调用一个非静态成员为什么是非法的?.md"), - "v-620ebadc": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/130.简述 Java 中 final 关键字的作用.md"), "v-5290507e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/140.String 类能不能被继承?为什么?.md"), + "v-620ebadc": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/130.简述 Java 中 final 关键字的作用.md"), "v-6f5c6512": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/150.Java 缓冲流 buffer 的用途和原理是什么?.md"), "v-75844e97": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/160.简述动态代理与静态代理 .md"), + "v-1218fa85": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/170.阻塞队列都有哪几种,有什么区别?.md"), "v-b21bbc4c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/180.简述 HashSet 实现原理.md"), "v-45fc1217": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/001.线程池是如何实现的?.md"), - "v-1218fa85": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/10.基础/170.阻塞队列都有哪几种,有什么区别?.md"), - "v-35324a50": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/030.简述 Synchronized,Volatile,可重入锁的不同使用场景及优缺点.md"), "v-5390e3d4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/020.简述 CAS 原理,什么是 ABA 问题,怎么解决?.md"), + "v-35324a50": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/030.简述 Synchronized,Volatile,可重入锁的不同使用场景及优缺点.md"), "v-352f36cc": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/040.Synchronized 关键字底层是如何实现的?.md"), - "v-f5bcd180": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/060.volatile 关键字解决了什么问题,它的实现原理是什么?.md"), + "v-4927c049": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/050.重入锁是如何实现的?.md"), "v-1065f7be": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/070.简述 Java 锁升级的机制.md"), + "v-f5bcd180": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/060.volatile 关键字解决了什么问题,它的实现原理是什么?.md"), + "v-134f28dc": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/090.什么是公平锁?什么是非公平锁?.md"), "v-2dbdb772": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/080.简述 Java AQS 的原理以及使用场景.md"), - "v-4927c049": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/050.重入锁是如何实现的?.md"), "v-625fbeda": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/110.Java 是如何实现线程安全的,哪些数据结构是线程安全的?.md"), - "v-134f28dc": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/090.什么是公平锁?什么是非公平锁?.md"), "v-03e420c4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/100.Java 的线程有哪些状态,转换关系是怎么样的?.md"), "v-7b2a2ed4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/115.手写死锁.md"), - "v-083aef82": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/140.手写生产者消费者模型.md"), "v-75c00225": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/130.Java 线程有哪些常用方法?.md"), - "v-f406b4e6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/150.ThreadLocal 实现原理是什么?为什么要使用弱引用?.md"), "v-9baaad8e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/120.为什么我们不能直接调用 run() 方法?.md"), + "v-083aef82": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/140.手写生产者消费者模型.md"), "v-7cd97c82": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/001.Java 中垃圾回收机制中如何判断对象需要回收?.md"), - "v-af821fc8": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/010.常见的 GC 回收算法有哪些?.md"), + "v-f406b4e6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/20.并发/150.ThreadLocal 实现原理是什么?为什么要使用弱引用?.md"), "v-5474bec0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/020.Java 中如何进行 GC 调优?.md"), - "v-2de19204": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/040.JVM 内存是如何对应到操作系统内存的?.md"), - "v-37fcb440": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/050.JVM 是怎么去调优的?了解哪些参数和指令?.md"), "v-0970cd54": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/030.JMM 中内存模型是怎样的?什么是指令序列重排序?.md"), + "v-37fcb440": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/050.JVM 是怎么去调优的?了解哪些参数和指令?.md"), "v-15fba8f1": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/060.什么是内存泄漏,怎么确定内存泄漏?.md"), + "v-af821fc8": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/010.常见的 GC 回收算法有哪些?.md"), + "v-2de19204": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/040.JVM 内存是如何对应到操作系统内存的?.md"), "v-28c1d6d9": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/30.JVM/070.简述 CMS 与 G1 机制的区别.md"), "v-88b087a6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/003.什么是设计模式 .md"), "v-2d6ed001": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/004.工厂模式使用场景 能举个例子吗.md"), @@ -55,125 +55,126 @@ export default { "v-22a9eaef": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/005.装饰者模式.md"), "v-31509c80": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/010.适配器模式.md"), "v-0564ba27": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/020.代理模式.md"), - "v-5b0157cf": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/050.策略模式.md"), - "v-28deb9da": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/040.模板方法模式.md"), "v-34f8aaa2": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/030.责任链模式.md"), - "v-470b2fce": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/01.简述 TCP 三次握手以及四次挥手.md"), - "v-0a1d1fb4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/010.简述常见的 HTTP 状态码的含义.md"), "v-64f1031c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/060.观察模式.md"), + "v-28deb9da": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/040.模板方法模式.md"), + "v-5b0157cf": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/01.Java相关/40.设计模式/050.策略模式.md"), + "v-0a1d1fb4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/010.简述常见的 HTTP 状态码的含义.md"), + "v-470b2fce": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/01.简述 TCP 三次握手以及四次挥手.md"), + "v-12717373": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/030.HTTP 中 GET 和 POST 区别.md"), "v-6602e3da": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/020.从输入 URL 到展现页面的全过程.md"), + "v-feb79e02": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/050.什么是 TCP 粘包和拆包?.md"), "v-5047727f": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/040.TCP 怎么保证可靠传输.md"), - "v-12717373": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/030.HTTP 中 GET 和 POST 区别.md"), "v-eb417bbe": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/070.简述对称与非对称加密的概念.md"), "v-3890e0b4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/060.简述 HTTPS 的加密与认证过程.md"), - "v-feb79e02": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/050.什么是 TCP 粘包和拆包?.md"), "v-c0c0e5c8": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/080.TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别?.md"), + "v-f886ba80": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/100.TCP 长连接和短连接有那么不同的使用场景?.md"), "v-aa8887a4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/090.如何解决 TCP 传输丢包问题?.md"), "v-df71a6e0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/110.TCP 的 keepalive 了解吗?说一说它和 HTTP 的 keepalive 的区别?.md"), - "v-9aa2c788": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/120.HTTP 与 HTTPS 有哪些区别?.md"), - "v-f886ba80": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/100.TCP 长连接和短连接有那么不同的使用场景?.md"), "v-75f6731c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/130.TCP 中常见的拥塞控制算法有哪些?.md"), + "v-9aa2c788": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/120.HTTP 与 HTTPS 有哪些区别?.md"), "v-f8f98912": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/140.简述 HTTP 1.0,1.1,2.0 的主要区别 .md"), - "v-93120d3a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/160.TCP 挥手时出现大量 CLOSE_WAIT 或 TIME_WAIT 怎么解决?.md"), "v-63453344": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/150.从系统层面上,UDP如何保证尽量可靠?.md"), - "v-70347066": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/170.简述 TCP 滑动窗口以及重传机制.md"), + "v-93120d3a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/160.TCP 挥手时出现大量 CLOSE_WAIT 或 TIME_WAIT 怎么解决?.md"), + "v-3e902cd4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/170.简述 TCP 滑动窗口.md"), + "v-52edac80": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/171. 简述TCP重传机制.md"), "v-0e7ed85a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/190.为什么需要序列化?有什么序列化的方式?.md"), "v-730f9a6a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/200.简述 iPv4 和 iPv6 的区别.md"), "v-74475c54": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/180.简述 JWT 的原理和校验机制.md"), - "v-15fe5424": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/210.TCP 中 SYN 攻击是什么?如何防止?.md"), "v-7fc8ca51": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/220.简述 DDOS 攻击原理,如何防范它?.md"), + "v-15fe5424": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/210.TCP 中 SYN 攻击是什么?如何防止?.md"), "v-01df2b5d": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/230.什么是 ARP 协议?简述其使用场景.md"), - "v-15ca313a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/250.Cookie 和 Session 的关系和区别是什么?.md"), "v-7370facf": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/240.什么是跨域,什么情况下会发生跨域请求?.md"), + "v-15ca313a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/250.Cookie 和 Session 的关系和区别是什么?.md"), "v-a9abf6a0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/270.DNS 查询服务器的基本流程是什么?DNS 劫持是什么?.md"), "v-7707a260": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/260.简述 WebSocket 是如何进行传输的.md"), - "v-693fb010": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/280.RestFul 是什么?RestFul 请求的 URL 有什么特点?.md"), "v-51f24219": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/290.简述 HTTP 报文头部的组成结构.md"), + "v-693fb010": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/280.RestFul 是什么?RestFul 请求的 URL 有什么特点?.md"), "v-18f7a69e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/01.进程间有哪些通信方式?.md"), - "v-64bba194": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/10.计算机网络/300.什么是 SYN flood,如何防止这类攻击?.md"), - "v-fe1405b0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/010.简述自旋锁与互斥锁的使用场景.md"), "v-a0ab5810": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/020.进程和线程之间有什么区别?.md"), + "v-fe1405b0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/010.简述自旋锁与互斥锁的使用场景.md"), + "v-32c73af0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/030.简述几个常用的 Linux 命令以及他们的功能.md"), "v-e9b5ef86": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/040.创建线程有多少种方式?.md"), "v-bf8cc4b0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/050.线程有多少种状态,状态之间如何转换.md"), - "v-32c73af0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/030.简述几个常用的 Linux 命令以及他们的功能.md"), - "v-05845816": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/080.Linux 下如何排查 CPU 以及 内存占用过多?.md"), "v-262f2c87": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/060.进程有多少种状态?.md"), "v-5a37154c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/070.简述 select, poll, epoll 的使用场景以及区别.md"), + "v-05845816": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/080.Linux 下如何排查 CPU 以及 内存占用过多?.md"), "v-303058be": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/090.进程通信中的管道实现原理是什么?.md"), - "v-1f69ea23": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/100.Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程?.md"), - "v-f6bafd34": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/110.如何调试服务器内存占用过高的问题?.md"), "v-129dffb8": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/120.Linux 如何查看实时的滚动日志?.md"), + "v-f6bafd34": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/110.如何调试服务器内存占用过高的问题?.md"), "v-7abb1115": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/130.简述 Linux 零拷贝的原理.md"), + "v-1f69ea23": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/100.Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程?.md"), + "v-1a49912a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/160.简述 Linux 虚拟内存的页面置换算法.md"), "v-c875c5a4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/140.为什么进程切换慢,线程切换快?.md"), "v-85af5b9e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/170.简述 Linux 进程调度的算法.md"), - "v-1a49912a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/160.简述 Linux 虚拟内存的页面置换算法.md"), "v-63719191": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/150.简述创建进程的流程.md"), "v-8c3da672": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/180.什么时候会由用户态陷入内核态?.md"), - "v-db80e4da": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/200.Linux 中虚拟内存和物理内存有什么区别?有什么优点?.md"), "v-1ea2064a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/190.Linux 下如何查看端口被哪个进程占用?.md"), - "v-e048e838": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/210.简述 traceroute 命令的原理.md"), + "v-d41ba7ec": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/220.简述IO模型.md"), + "v-db80e4da": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/200.Linux 中虚拟内存和物理内存有什么区别?有什么优点?.md"), "v-49688930": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/30.数据结构/01.数据结构.md"), - "v-674454ac": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/001.MySQL中 InnoDB 和 MylSAM 的区别是什么?.md"), "v-2e6d42b4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/40.算法/01.算法.md"), + "v-e048e838": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/02.计算机基础/20.操作系统/210.简述 traceroute 命令的原理.md"), + "v-674454ac": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/001.MySQL中 InnoDB 和 MylSAM 的区别是什么?.md"), "v-f0f711c4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/006.聚簇索引和非聚簇索引有什么区别?.md"), - "v-7e02c4a1": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/010.简述Mysql的事务隔离级别.md"), - "v-5b0e09c6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/015.说一下Mysql锁.md"), "v-824b776c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/007.简述 MySQL 常见索引类型,介绍一下覆盖索引.md"), + "v-7e02c4a1": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/010.简述Mysql的事务隔离级别.md"), "v-6c681639": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/020.MySQL 为什么使用 B+ 树来作索引,对比 B 树它的优点和缺点是什么.md"), - "v-c39c4fe2": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/030.简述乐观锁以及悲观锁的区别以及使用场景.md"), + "v-5b0e09c6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/015.说一下Mysql锁.md"), "v-689e142a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/040.简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?.md"), + "v-c39c4fe2": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/030.简述乐观锁以及悲观锁的区别以及使用场景.md"), "v-4ecddbec": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/050.简述什么是最左匹配原则.md"), "v-505fbe2e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/060.MySQL 联合索引底层原理是什么?.md"), - "v-53bcbb7a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/080.简述常见的负载均衡算法.md"), "v-0dd47f09": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/070.产生死锁的必要条件有哪些?如何解决死锁?.md"), + "v-53bcbb7a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/080.简述常见的负载均衡算法.md"), "v-4cef944a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/100.如何设计数据库压测方案?.md"), + "v-ed0c93b8": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/110.简述数据库中什么情况下进行分库,什么情况下进行分表?.md"), "v-c383b96e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/090.简述 MySQL 的主从同步机制,如果同步失败会怎么样?.md"), "v-f3acaa1a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/130.简述 undo log 和 redo log 的作用.md"), - "v-ed0c93b8": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/110.简述数据库中什么情况下进行分库,什么情况下进行分表?.md"), "v-d6bc240a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/120.数据库的读写分离的作用是什么?如何实现?.md"), - "v-76f5e79a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/160.简述 MySQL MVCC 的实现原理 .md"), "v-77790024": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/150.MySQL 中 join 与 left join 的区别是什么?.md"), "v-ceed6ba6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/140.什么是 SQL 注入攻击?如何防止这类攻击?.md"), - "v-6b967a6e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/180.模糊查询是如何实现的?.md"), + "v-76f5e79a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/160.简述 MySQL MVCC 的实现原理 .md"), "v-0a7e6416": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/170.MySQL 中 varchar 和 char 的区别是什么?.md"), - "v-d804fba0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/001.Redis 有几种数据结构?Zset 是如何实现的?.md"), + "v-6b967a6e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/01.MYSQL相关/180.模糊查询是如何实现的?.md"), "v-cb742458": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/002.为什么 Redis 在单线程下能如此快?.md"), + "v-d804fba0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/001.Redis 有几种数据结构?Zset 是如何实现的?.md"), "v-3e917471": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/005.简述 Redis 字符串的底层结构.md"), - "v-c0bc7b7a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/010.Redis的缓存淘汰策略有哪些?.md"), "v-7c214d00": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/020.简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点.md"), + "v-c0bc7b7a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/010.Redis的缓存淘汰策略有哪些?.md"), "v-2c6b511c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/030.Redis 如何实现分布式锁?.md"), "v-3d2fdd6a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/040.简述 Redis 集群配置以及基础原理.md"), - "v-6a45f68d": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/060.Redis 中,sentinel 和 cluster 的区别和适用场景是什么?.md"), "v-18b427a0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/050.简述 Redis 中跳表的应用以及优缺点.md"), "v-c2fe5e1a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/070.简述 Redis 中如何防止缓存雪崩和缓存击穿.md"), - "v-35718f4e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/080.简述 Redis 的线程模型以及底层架构设计.md"), + "v-6a45f68d": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/060.Redis 中,sentinel 和 cluster 的区别和适用场景是什么?.md"), "v-57957421": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/100.简述 Redis 的哨兵机制.md"), + "v-35718f4e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/080.简述 Redis 的线程模型以及底层架构设计.md"), "v-37f158ba": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/110.简述 Redis 如何处理热点 key 访问.md"), "v-02ea33c3": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/03.数据库/02.REDIS相关/120.Redis 序列化有哪些方式?.md"), "v-be02b3c0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/001.你熟悉哪些IO模型?.md"), + "v-073f22cd": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/015.Netty中有哪些解码器?.md"), "v-5acdab4d": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/010.TCP粘包拆包问题的解决之道.md"), "v-bbcf6e5a": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/020.为什么Netty要自己写一个ByteBuf.md"), - "v-073f22cd": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/015.Netty中有哪些解码器?.md"), - "v-6b32ada0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/050.Java NIO了解吗?.md"), "v-5157927c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/030.ChannelPipeline和ChannelHandler.md"), "v-62f3fa3e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/040.EventLoop和EventLoopGroup.md"), - "v-7c0136cf": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/010.生产者.md"), + "v-6b32ada0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/10.Netty/050.Java NIO了解吗?.md"), + "v-0df39df8": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/001.Kafka包含那几部分.md"), "v-04af9e42": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/020.消费者.md"), - "v-684c4815": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/040.如何保证Kafka的可靠性.md"), + "v-7c0136cf": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/010.生产者.md"), "v-0c440466": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/030.集群下的Kafka要考虑哪些事情.md"), - "v-0df39df8": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/001.Kafka包含那几部分.md"), "v-a9558ec0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/08.系统设计/001.系统设计.md"), + "v-684c4815": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/04.中间件/40.Kafka/040.如何保证Kafka的可靠性.md"), "v-5675b06b": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/08.系统设计/010.什么是 CAP ?什么是最终一致性?什么是幂等操作?.md"), + "v-f88544f4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/08.系统设计/020.简述什么是两阶段提交?.md"), "v-2a52a762": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/10.非技术/01.非技术问题.md"), "v-918dfaf8": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/15.关于/01.关于.md"), - "v-f88544f4": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/08.系统设计/020.简述什么是两阶段提交?.md"), + "v-5f8dfe80": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/categoriesPage.md"), "v-2db11cb2": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/_posts/随笔/你知道的越多,不知道的也就越多.md"), - "v-48f7eec0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/archivesPage.md"), "v-7ab50b9f": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/20.收藏夹/01.网站.md"), "v-a5c5f278": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/_posts/随笔/我做了一个手写春联小网页,祝大家虎年暴富.md"), "v-09c150e6": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/20.收藏夹/02.常用的前端轮子.md"), - "v-5f8dfe80": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/categoriesPage.md"), "v-7d77d69c": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/_posts/随笔/拥抱生活,拥抱快乐.md"), + "v-48f7eec0": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/archivesPage.md"), "v-64f9b920": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/@pages/tagsPage.md"), "v-2cf94a0e": () => import("/Users/yonganwang/Documents/kuaishou-java/JavaDriver/docs/index.md") } \ No newline at end of file diff --git a/node_modules/@vuepress/core/.temp/internal/routes.js b/node_modules/@vuepress/core/.temp/internal/routes.js index 20f0a7f90..c86f8f501 100644 --- a/node_modules/@vuepress/core/.temp/internal/routes.js +++ b/node_modules/@vuepress/core/.temp/internal/routes.js @@ -72,22 +72,6 @@ export const routes = [ path: "/00.目录页/04.中间件.html", redirect: "/middleware/" }, - { - name: "v-0734374b", - path: "/pages/d1c564/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-0734374b").then(next) - }, - }, - { - path: "/pages/d1c564/index.html", - redirect: "/pages/d1c564/" - }, - { - path: "/01.Java相关/10.基础/010.HashMap 与 ConcurrentHashMap 的实现原理是怎样的?.html", - redirect: "/pages/d1c564/" - }, { name: "v-8bf153e4", path: "/pages/a030ff/", @@ -105,20 +89,20 @@ export const routes = [ redirect: "/pages/a030ff/" }, { - name: "v-4e1fd39c", - path: "/pages/826c8b/", + name: "v-0734374b", + path: "/pages/d1c564/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-4e1fd39c").then(next) + ensureAsyncComponentsLoaded("Layout", "v-0734374b").then(next) }, }, { - path: "/pages/826c8b/index.html", - redirect: "/pages/826c8b/" + path: "/pages/d1c564/index.html", + redirect: "/pages/d1c564/" }, { - path: "/01.Java相关/10.基础/020.Java 中接口和抽象类的区别.html", - redirect: "/pages/826c8b/" + path: "/01.Java相关/10.基础/010.HashMap 与 ConcurrentHashMap 的实现原理是怎样的?.html", + redirect: "/pages/d1c564/" }, { name: "v-57012c44", @@ -137,52 +121,52 @@ export const routes = [ redirect: "/pages/0d22ad/" }, { - name: "v-7f48b402", - path: "/pages/211d8c/", + name: "v-d8a2f15c", + path: "/pages/06b28a/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-7f48b402").then(next) + ensureAsyncComponentsLoaded("Layout", "v-d8a2f15c").then(next) }, }, { - path: "/pages/211d8c/index.html", - redirect: "/pages/211d8c/" + path: "/pages/06b28a/index.html", + redirect: "/pages/06b28a/" }, { - path: "/01.Java相关/10.基础/050.String,StringBuffer,StringBuilder 之间有什么区别?.html", - redirect: "/pages/211d8c/" + path: "/01.Java相关/10.基础/040.== 和 equals() 的区别?.html", + redirect: "/pages/06b28a/" }, { - name: "v-d8a2f15c", - path: "/pages/06b28a/", + name: "v-4e1fd39c", + path: "/pages/826c8b/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-d8a2f15c").then(next) + ensureAsyncComponentsLoaded("Layout", "v-4e1fd39c").then(next) }, }, { - path: "/pages/06b28a/index.html", - redirect: "/pages/06b28a/" + path: "/pages/826c8b/index.html", + redirect: "/pages/826c8b/" }, { - path: "/01.Java相关/10.基础/040.== 和 equals() 的区别?.html", - redirect: "/pages/06b28a/" + path: "/01.Java相关/10.基础/020.Java 中接口和抽象类的区别.html", + redirect: "/pages/826c8b/" }, { - name: "v-d4e20148", - path: "/pages/276740/", + name: "v-7f48b402", + path: "/pages/211d8c/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-d4e20148").then(next) + ensureAsyncComponentsLoaded("Layout", "v-7f48b402").then(next) }, }, { - path: "/pages/276740/index.html", - redirect: "/pages/276740/" + path: "/pages/211d8c/index.html", + redirect: "/pages/211d8c/" }, { - path: "/01.Java相关/10.基础/070.简述 BIO, NIO, AIO 的区别.html", - redirect: "/pages/276740/" + path: "/01.Java相关/10.基础/050.String,StringBuffer,StringBuilder 之间有什么区别?.html", + redirect: "/pages/211d8c/" }, { name: "v-57b2551a", @@ -200,6 +184,22 @@ export const routes = [ path: "/01.Java相关/10.基础/060.简述 ArrayList 与 LinkedList 的底层实现以及常见操作的时间复杂度.html", redirect: "/pages/305919/" }, + { + name: "v-d4e20148", + path: "/pages/276740/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-d4e20148").then(next) + }, + }, + { + path: "/pages/276740/index.html", + redirect: "/pages/276740/" + }, + { + path: "/01.Java相关/10.基础/070.简述 BIO, NIO, AIO 的区别.html", + redirect: "/pages/276740/" + }, { name: "v-85b06162", path: "/pages/fae5d5/", @@ -232,6 +232,22 @@ export const routes = [ path: "/01.Java相关/10.基础/100.Java 类的加载流程是怎样的?.html", redirect: "/pages/33c5d6/" }, + { + name: "v-d32e07ea", + path: "/pages/56314c/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-d32e07ea").then(next) + }, + }, + { + path: "/pages/56314c/index.html", + redirect: "/pages/56314c/" + }, + { + path: "/01.Java相关/10.基础/080.在一个静态方法内调用一个非静态成员为什么是非法的?.html", + redirect: "/pages/56314c/" + }, { name: "v-7665e30e", path: "/pages/38bcaf/", @@ -265,20 +281,20 @@ export const routes = [ redirect: "/pages/d69890/" }, { - name: "v-d32e07ea", - path: "/pages/56314c/", + name: "v-5290507e", + path: "/pages/73113f/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-d32e07ea").then(next) + ensureAsyncComponentsLoaded("Layout", "v-5290507e").then(next) }, }, { - path: "/pages/56314c/index.html", - redirect: "/pages/56314c/" + path: "/pages/73113f/index.html", + redirect: "/pages/73113f/" }, { - path: "/01.Java相关/10.基础/080.在一个静态方法内调用一个非静态成员为什么是非法的?.html", - redirect: "/pages/56314c/" + path: "/01.Java相关/10.基础/140.String 类能不能被继承?为什么?.html", + redirect: "/pages/73113f/" }, { name: "v-620ebadc", @@ -296,22 +312,6 @@ export const routes = [ path: "/01.Java相关/10.基础/130.简述 Java 中 final 关键字的作用.html", redirect: "/pages/2b00a3/" }, - { - name: "v-5290507e", - path: "/pages/73113f/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-5290507e").then(next) - }, - }, - { - path: "/pages/73113f/index.html", - redirect: "/pages/73113f/" - }, - { - path: "/01.Java相关/10.基础/140.String 类能不能被继承?为什么?.html", - redirect: "/pages/73113f/" - }, { name: "v-6f5c6512", path: "/pages/115772/", @@ -344,6 +344,22 @@ export const routes = [ path: "/01.Java相关/10.基础/160.简述动态代理与静态代理 .html", redirect: "/pages/37dba4/" }, + { + name: "v-1218fa85", + path: "/pages/e9ce4e/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-1218fa85").then(next) + }, + }, + { + path: "/pages/e9ce4e/index.html", + redirect: "/pages/e9ce4e/" + }, + { + path: "/01.Java相关/10.基础/170.阻塞队列都有哪几种,有什么区别?.html", + redirect: "/pages/e9ce4e/" + }, { name: "v-b21bbc4c", path: "/pages/75552c/", @@ -377,20 +393,20 @@ export const routes = [ redirect: "/pages/0da4b0/" }, { - name: "v-1218fa85", - path: "/pages/e9ce4e/", + name: "v-5390e3d4", + path: "/pages/3356c6/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-1218fa85").then(next) + ensureAsyncComponentsLoaded("Layout", "v-5390e3d4").then(next) }, }, { - path: "/pages/e9ce4e/index.html", - redirect: "/pages/e9ce4e/" + path: "/pages/3356c6/index.html", + redirect: "/pages/3356c6/" }, { - path: "/01.Java相关/10.基础/170.阻塞队列都有哪几种,有什么区别?.html", - redirect: "/pages/e9ce4e/" + path: "/01.Java相关/20.并发/020.简述 CAS 原理,什么是 ABA 问题,怎么解决?.html", + redirect: "/pages/3356c6/" }, { name: "v-35324a50", @@ -408,22 +424,6 @@ export const routes = [ path: "/01.Java相关/20.并发/030.简述 Synchronized,Volatile,可重入锁的不同使用场景及优缺点.html", redirect: "/pages/97245f/" }, - { - name: "v-5390e3d4", - path: "/pages/3356c6/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-5390e3d4").then(next) - }, - }, - { - path: "/pages/3356c6/index.html", - redirect: "/pages/3356c6/" - }, - { - path: "/01.Java相关/20.并发/020.简述 CAS 原理,什么是 ABA 问题,怎么解决?.html", - redirect: "/pages/3356c6/" - }, { name: "v-352f36cc", path: "/pages/d5d161/", @@ -441,20 +441,20 @@ export const routes = [ redirect: "/pages/d5d161/" }, { - name: "v-f5bcd180", - path: "/pages/383264/", + name: "v-4927c049", + path: "/pages/9b59d1/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-f5bcd180").then(next) + ensureAsyncComponentsLoaded("Layout", "v-4927c049").then(next) }, }, { - path: "/pages/383264/index.html", - redirect: "/pages/383264/" + path: "/pages/9b59d1/index.html", + redirect: "/pages/9b59d1/" }, { - path: "/01.Java相关/20.并发/060.volatile 关键字解决了什么问题,它的实现原理是什么?.html", - redirect: "/pages/383264/" + path: "/01.Java相关/20.并发/050.重入锁是如何实现的?.html", + redirect: "/pages/9b59d1/" }, { name: "v-1065f7be", @@ -473,68 +473,68 @@ export const routes = [ redirect: "/pages/ff0616/" }, { - name: "v-2dbdb772", - path: "/pages/52a687/", + name: "v-f5bcd180", + path: "/pages/383264/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-2dbdb772").then(next) + ensureAsyncComponentsLoaded("Layout", "v-f5bcd180").then(next) }, }, { - path: "/pages/52a687/index.html", - redirect: "/pages/52a687/" + path: "/pages/383264/index.html", + redirect: "/pages/383264/" }, { - path: "/01.Java相关/20.并发/080.简述 Java AQS 的原理以及使用场景.html", - redirect: "/pages/52a687/" + path: "/01.Java相关/20.并发/060.volatile 关键字解决了什么问题,它的实现原理是什么?.html", + redirect: "/pages/383264/" }, { - name: "v-4927c049", - path: "/pages/9b59d1/", + name: "v-134f28dc", + path: "/pages/fd6a59/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-4927c049").then(next) + ensureAsyncComponentsLoaded("Layout", "v-134f28dc").then(next) }, }, { - path: "/pages/9b59d1/index.html", - redirect: "/pages/9b59d1/" + path: "/pages/fd6a59/index.html", + redirect: "/pages/fd6a59/" }, { - path: "/01.Java相关/20.并发/050.重入锁是如何实现的?.html", - redirect: "/pages/9b59d1/" + path: "/01.Java相关/20.并发/090.什么是公平锁?什么是非公平锁?.html", + redirect: "/pages/fd6a59/" }, { - name: "v-625fbeda", - path: "/pages/c1f760/", + name: "v-2dbdb772", + path: "/pages/52a687/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-625fbeda").then(next) + ensureAsyncComponentsLoaded("Layout", "v-2dbdb772").then(next) }, }, { - path: "/pages/c1f760/index.html", - redirect: "/pages/c1f760/" + path: "/pages/52a687/index.html", + redirect: "/pages/52a687/" }, { - path: "/01.Java相关/20.并发/110.Java 是如何实现线程安全的,哪些数据结构是线程安全的?.html", - redirect: "/pages/c1f760/" + path: "/01.Java相关/20.并发/080.简述 Java AQS 的原理以及使用场景.html", + redirect: "/pages/52a687/" }, { - name: "v-134f28dc", - path: "/pages/fd6a59/", + name: "v-625fbeda", + path: "/pages/c1f760/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-134f28dc").then(next) + ensureAsyncComponentsLoaded("Layout", "v-625fbeda").then(next) }, }, { - path: "/pages/fd6a59/index.html", - redirect: "/pages/fd6a59/" + path: "/pages/c1f760/index.html", + redirect: "/pages/c1f760/" }, { - path: "/01.Java相关/20.并发/090.什么是公平锁?什么是非公平锁?.html", - redirect: "/pages/fd6a59/" + path: "/01.Java相关/20.并发/110.Java 是如何实现线程安全的,哪些数据结构是线程安全的?.html", + redirect: "/pages/c1f760/" }, { name: "v-03e420c4", @@ -569,27 +569,11 @@ export const routes = [ redirect: "/pages/ce292c/" }, { - name: "v-083aef82", - path: "/pages/2b8c6e/", + name: "v-75c00225", + path: "/pages/a6c84e/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-083aef82").then(next) - }, - }, - { - path: "/pages/2b8c6e/index.html", - redirect: "/pages/2b8c6e/" - }, - { - path: "/01.Java相关/20.并发/140.手写生产者消费者模型.html", - redirect: "/pages/2b8c6e/" - }, - { - name: "v-75c00225", - path: "/pages/a6c84e/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-75c00225").then(next) + ensureAsyncComponentsLoaded("Layout", "v-75c00225").then(next) }, }, { @@ -601,36 +585,36 @@ export const routes = [ redirect: "/pages/a6c84e/" }, { - name: "v-f406b4e6", - path: "/pages/b3e720/", + name: "v-9baaad8e", + path: "/pages/13856f/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-f406b4e6").then(next) + ensureAsyncComponentsLoaded("Layout", "v-9baaad8e").then(next) }, }, { - path: "/pages/b3e720/index.html", - redirect: "/pages/b3e720/" + path: "/pages/13856f/index.html", + redirect: "/pages/13856f/" }, { - path: "/01.Java相关/20.并发/150.ThreadLocal 实现原理是什么?为什么要使用弱引用?.html", - redirect: "/pages/b3e720/" + path: "/01.Java相关/20.并发/120.为什么我们不能直接调用 run() 方法?.html", + redirect: "/pages/13856f/" }, { - name: "v-9baaad8e", - path: "/pages/13856f/", + name: "v-083aef82", + path: "/pages/2b8c6e/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-9baaad8e").then(next) + ensureAsyncComponentsLoaded("Layout", "v-083aef82").then(next) }, }, { - path: "/pages/13856f/index.html", - redirect: "/pages/13856f/" + path: "/pages/2b8c6e/index.html", + redirect: "/pages/2b8c6e/" }, { - path: "/01.Java相关/20.并发/120.为什么我们不能直接调用 run() 方法?.html", - redirect: "/pages/13856f/" + path: "/01.Java相关/20.并发/140.手写生产者消费者模型.html", + redirect: "/pages/2b8c6e/" }, { name: "v-7cd97c82", @@ -649,20 +633,20 @@ export const routes = [ redirect: "/pages/948850/" }, { - name: "v-af821fc8", - path: "/pages/9b26e8/", + name: "v-f406b4e6", + path: "/pages/b3e720/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-af821fc8").then(next) + ensureAsyncComponentsLoaded("Layout", "v-f406b4e6").then(next) }, }, { - path: "/pages/9b26e8/index.html", - redirect: "/pages/9b26e8/" + path: "/pages/b3e720/index.html", + redirect: "/pages/b3e720/" }, { - path: "/01.Java相关/30.JVM/010.常见的 GC 回收算法有哪些?.html", - redirect: "/pages/9b26e8/" + path: "/01.Java相关/20.并发/150.ThreadLocal 实现原理是什么?为什么要使用弱引用?.html", + redirect: "/pages/b3e720/" }, { name: "v-5474bec0", @@ -681,20 +665,20 @@ export const routes = [ redirect: "/pages/07a030/" }, { - name: "v-2de19204", - path: "/pages/da65e7/", + name: "v-0970cd54", + path: "/pages/c8e6cb/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-2de19204").then(next) + ensureAsyncComponentsLoaded("Layout", "v-0970cd54").then(next) }, }, { - path: "/pages/da65e7/index.html", - redirect: "/pages/da65e7/" + path: "/pages/c8e6cb/index.html", + redirect: "/pages/c8e6cb/" }, { - path: "/01.Java相关/30.JVM/040.JVM 内存是如何对应到操作系统内存的?.html", - redirect: "/pages/da65e7/" + path: "/01.Java相关/30.JVM/030.JMM 中内存模型是怎样的?什么是指令序列重排序?.html", + redirect: "/pages/c8e6cb/" }, { name: "v-37fcb440", @@ -713,36 +697,52 @@ export const routes = [ redirect: "/pages/d730dd/" }, { - name: "v-0970cd54", - path: "/pages/c8e6cb/", + name: "v-15fba8f1", + path: "/pages/3ab5f5/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-0970cd54").then(next) + ensureAsyncComponentsLoaded("Layout", "v-15fba8f1").then(next) }, }, { - path: "/pages/c8e6cb/index.html", - redirect: "/pages/c8e6cb/" + path: "/pages/3ab5f5/index.html", + redirect: "/pages/3ab5f5/" }, { - path: "/01.Java相关/30.JVM/030.JMM 中内存模型是怎样的?什么是指令序列重排序?.html", - redirect: "/pages/c8e6cb/" + path: "/01.Java相关/30.JVM/060.什么是内存泄漏,怎么确定内存泄漏?.html", + redirect: "/pages/3ab5f5/" }, { - name: "v-15fba8f1", - path: "/pages/3ab5f5/", + name: "v-af821fc8", + path: "/pages/9b26e8/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-15fba8f1").then(next) + ensureAsyncComponentsLoaded("Layout", "v-af821fc8").then(next) }, }, { - path: "/pages/3ab5f5/index.html", - redirect: "/pages/3ab5f5/" + path: "/pages/9b26e8/index.html", + redirect: "/pages/9b26e8/" }, { - path: "/01.Java相关/30.JVM/060.什么是内存泄漏,怎么确定内存泄漏?.html", - redirect: "/pages/3ab5f5/" + path: "/01.Java相关/30.JVM/010.常见的 GC 回收算法有哪些?.html", + redirect: "/pages/9b26e8/" + }, + { + name: "v-2de19204", + path: "/pages/da65e7/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-2de19204").then(next) + }, + }, + { + path: "/pages/da65e7/index.html", + redirect: "/pages/da65e7/" + }, + { + path: "/01.Java相关/30.JVM/040.JVM 内存是如何对应到操作系统内存的?.html", + redirect: "/pages/da65e7/" }, { name: "v-28c1d6d9", @@ -857,68 +857,68 @@ export const routes = [ redirect: "/pages/dd3a6f/" }, { - name: "v-5b0157cf", - path: "/pages/78c7aa/", + name: "v-34f8aaa2", + path: "/pages/d3ff35/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-5b0157cf").then(next) + ensureAsyncComponentsLoaded("Layout", "v-34f8aaa2").then(next) }, }, { - path: "/pages/78c7aa/index.html", - redirect: "/pages/78c7aa/" + path: "/pages/d3ff35/index.html", + redirect: "/pages/d3ff35/" }, { - path: "/01.Java相关/40.设计模式/050.策略模式.html", - redirect: "/pages/78c7aa/" + path: "/01.Java相关/40.设计模式/030.责任链模式.html", + redirect: "/pages/d3ff35/" }, { - name: "v-28deb9da", - path: "/pages/43b656/", + name: "v-64f1031c", + path: "/pages/cbff3c/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-28deb9da").then(next) + ensureAsyncComponentsLoaded("Layout", "v-64f1031c").then(next) }, }, { - path: "/pages/43b656/index.html", - redirect: "/pages/43b656/" + path: "/pages/cbff3c/index.html", + redirect: "/pages/cbff3c/" }, { - path: "/01.Java相关/40.设计模式/040.模板方法模式.html", - redirect: "/pages/43b656/" + path: "/01.Java相关/40.设计模式/060.观察模式.html", + redirect: "/pages/cbff3c/" }, { - name: "v-34f8aaa2", - path: "/pages/d3ff35/", + name: "v-28deb9da", + path: "/pages/43b656/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-34f8aaa2").then(next) + ensureAsyncComponentsLoaded("Layout", "v-28deb9da").then(next) }, }, { - path: "/pages/d3ff35/index.html", - redirect: "/pages/d3ff35/" + path: "/pages/43b656/index.html", + redirect: "/pages/43b656/" }, { - path: "/01.Java相关/40.设计模式/030.责任链模式.html", - redirect: "/pages/d3ff35/" + path: "/01.Java相关/40.设计模式/040.模板方法模式.html", + redirect: "/pages/43b656/" }, { - name: "v-470b2fce", - path: "/pages/e00207/", + name: "v-5b0157cf", + path: "/pages/78c7aa/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-470b2fce").then(next) + ensureAsyncComponentsLoaded("Layout", "v-5b0157cf").then(next) }, }, { - path: "/pages/e00207/index.html", - redirect: "/pages/e00207/" + path: "/pages/78c7aa/index.html", + redirect: "/pages/78c7aa/" }, { - path: "/02.计算机基础/10.计算机网络/01.简述 TCP 三次握手以及四次挥手.html", - redirect: "/pages/e00207/" + path: "/01.Java相关/40.设计模式/050.策略模式.html", + redirect: "/pages/78c7aa/" }, { name: "v-0a1d1fb4", @@ -937,20 +937,36 @@ export const routes = [ redirect: "/pages/f33310/" }, { - name: "v-64f1031c", - path: "/pages/cbff3c/", + name: "v-470b2fce", + path: "/pages/e00207/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-64f1031c").then(next) + ensureAsyncComponentsLoaded("Layout", "v-470b2fce").then(next) }, }, { - path: "/pages/cbff3c/index.html", - redirect: "/pages/cbff3c/" + path: "/pages/e00207/index.html", + redirect: "/pages/e00207/" }, { - path: "/01.Java相关/40.设计模式/060.观察模式.html", - redirect: "/pages/cbff3c/" + path: "/02.计算机基础/10.计算机网络/01.简述 TCP 三次握手以及四次挥手.html", + redirect: "/pages/e00207/" + }, + { + name: "v-12717373", + path: "/pages/a0e400/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-12717373").then(next) + }, + }, + { + path: "/pages/a0e400/index.html", + redirect: "/pages/a0e400/" + }, + { + path: "/02.计算机基础/10.计算机网络/030.HTTP 中 GET 和 POST 区别.html", + redirect: "/pages/a0e400/" }, { name: "v-6602e3da", @@ -969,36 +985,36 @@ export const routes = [ redirect: "/pages/ab48ba/" }, { - name: "v-5047727f", - path: "/pages/5fcc49/", + name: "v-feb79e02", + path: "/pages/2822ac/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-5047727f").then(next) + ensureAsyncComponentsLoaded("Layout", "v-feb79e02").then(next) }, }, { - path: "/pages/5fcc49/index.html", - redirect: "/pages/5fcc49/" + path: "/pages/2822ac/index.html", + redirect: "/pages/2822ac/" }, { - path: "/02.计算机基础/10.计算机网络/040.TCP 怎么保证可靠传输.html", - redirect: "/pages/5fcc49/" + path: "/02.计算机基础/10.计算机网络/050.什么是 TCP 粘包和拆包?.html", + redirect: "/pages/2822ac/" }, { - name: "v-12717373", - path: "/pages/a0e400/", + name: "v-5047727f", + path: "/pages/5fcc49/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-12717373").then(next) + ensureAsyncComponentsLoaded("Layout", "v-5047727f").then(next) }, }, { - path: "/pages/a0e400/index.html", - redirect: "/pages/a0e400/" + path: "/pages/5fcc49/index.html", + redirect: "/pages/5fcc49/" }, { - path: "/02.计算机基础/10.计算机网络/030.HTTP 中 GET 和 POST 区别.html", - redirect: "/pages/a0e400/" + path: "/02.计算机基础/10.计算机网络/040.TCP 怎么保证可靠传输.html", + redirect: "/pages/5fcc49/" }, { name: "v-eb417bbe", @@ -1032,22 +1048,6 @@ export const routes = [ path: "/02.计算机基础/10.计算机网络/060.简述 HTTPS 的加密与认证过程.html", redirect: "/pages/12c0cc/" }, - { - name: "v-feb79e02", - path: "/pages/2822ac/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-feb79e02").then(next) - }, - }, - { - path: "/pages/2822ac/index.html", - redirect: "/pages/2822ac/" - }, - { - path: "/02.计算机基础/10.计算机网络/050.什么是 TCP 粘包和拆包?.html", - redirect: "/pages/2822ac/" - }, { name: "v-c0c0e5c8", path: "/pages/cc4b24/", @@ -1065,16 +1065,32 @@ export const routes = [ redirect: "/pages/cc4b24/" }, { - name: "v-aa8887a4", - path: "/pages/a656a9/", + name: "v-f886ba80", + path: "/pages/dda19e/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-aa8887a4").then(next) + ensureAsyncComponentsLoaded("Layout", "v-f886ba80").then(next) }, }, { - path: "/pages/a656a9/index.html", - redirect: "/pages/a656a9/" + path: "/pages/dda19e/index.html", + redirect: "/pages/dda19e/" + }, + { + path: "/02.计算机基础/10.计算机网络/100.TCP 长连接和短连接有那么不同的使用场景?.html", + redirect: "/pages/dda19e/" + }, + { + name: "v-aa8887a4", + path: "/pages/a656a9/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-aa8887a4").then(next) + }, + }, + { + path: "/pages/a656a9/index.html", + redirect: "/pages/a656a9/" }, { path: "/02.计算机基础/10.计算机网络/090.如何解决 TCP 传输丢包问题?.html", @@ -1097,68 +1113,68 @@ export const routes = [ redirect: "/pages/212438/" }, { - name: "v-9aa2c788", - path: "/pages/253334/", + name: "v-75f6731c", + path: "/pages/9ccc31/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-9aa2c788").then(next) + ensureAsyncComponentsLoaded("Layout", "v-75f6731c").then(next) }, }, { - path: "/pages/253334/index.html", - redirect: "/pages/253334/" + path: "/pages/9ccc31/index.html", + redirect: "/pages/9ccc31/" }, { - path: "/02.计算机基础/10.计算机网络/120.HTTP 与 HTTPS 有哪些区别?.html", - redirect: "/pages/253334/" + path: "/02.计算机基础/10.计算机网络/130.TCP 中常见的拥塞控制算法有哪些?.html", + redirect: "/pages/9ccc31/" }, { - name: "v-f886ba80", - path: "/pages/dda19e/", + name: "v-9aa2c788", + path: "/pages/253334/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-f886ba80").then(next) + ensureAsyncComponentsLoaded("Layout", "v-9aa2c788").then(next) }, }, { - path: "/pages/dda19e/index.html", - redirect: "/pages/dda19e/" + path: "/pages/253334/index.html", + redirect: "/pages/253334/" }, { - path: "/02.计算机基础/10.计算机网络/100.TCP 长连接和短连接有那么不同的使用场景?.html", - redirect: "/pages/dda19e/" + path: "/02.计算机基础/10.计算机网络/120.HTTP 与 HTTPS 有哪些区别?.html", + redirect: "/pages/253334/" }, { - name: "v-75f6731c", - path: "/pages/9ccc31/", + name: "v-f8f98912", + path: "/pages/f50b70/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-75f6731c").then(next) + ensureAsyncComponentsLoaded("Layout", "v-f8f98912").then(next) }, }, { - path: "/pages/9ccc31/index.html", - redirect: "/pages/9ccc31/" + path: "/pages/f50b70/index.html", + redirect: "/pages/f50b70/" }, { - path: "/02.计算机基础/10.计算机网络/130.TCP 中常见的拥塞控制算法有哪些?.html", - redirect: "/pages/9ccc31/" + path: "/02.计算机基础/10.计算机网络/140.简述 HTTP 1.0,1.1,2.0 的主要区别 .html", + redirect: "/pages/f50b70/" }, { - name: "v-f8f98912", - path: "/pages/f50b70/", + name: "v-63453344", + path: "/pages/e57cfe/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-f8f98912").then(next) + ensureAsyncComponentsLoaded("Layout", "v-63453344").then(next) }, }, { - path: "/pages/f50b70/index.html", - redirect: "/pages/f50b70/" + path: "/pages/e57cfe/index.html", + redirect: "/pages/e57cfe/" }, { - path: "/02.计算机基础/10.计算机网络/140.简述 HTTP 1.0,1.1,2.0 的主要区别 .html", - redirect: "/pages/f50b70/" + path: "/02.计算机基础/10.计算机网络/150.从系统层面上,UDP如何保证尽量可靠?.html", + redirect: "/pages/e57cfe/" }, { name: "v-93120d3a", @@ -1177,36 +1193,36 @@ export const routes = [ redirect: "/pages/15d37a/" }, { - name: "v-63453344", - path: "/pages/e57cfe/", + name: "v-3e902cd4", + path: "/pages/dae451/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-63453344").then(next) + ensureAsyncComponentsLoaded("Layout", "v-3e902cd4").then(next) }, }, { - path: "/pages/e57cfe/index.html", - redirect: "/pages/e57cfe/" + path: "/pages/dae451/index.html", + redirect: "/pages/dae451/" }, { - path: "/02.计算机基础/10.计算机网络/150.从系统层面上,UDP如何保证尽量可靠?.html", - redirect: "/pages/e57cfe/" + path: "/02.计算机基础/10.计算机网络/170.简述 TCP 滑动窗口.html", + redirect: "/pages/dae451/" }, { - name: "v-70347066", - path: "/pages/dae451/", + name: "v-52edac80", + path: "/pages/a219df/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-70347066").then(next) + ensureAsyncComponentsLoaded("Layout", "v-52edac80").then(next) }, }, { - path: "/pages/dae451/index.html", - redirect: "/pages/dae451/" + path: "/pages/a219df/index.html", + redirect: "/pages/a219df/" }, { - path: "/02.计算机基础/10.计算机网络/170.简述 TCP 滑动窗口以及重传机制.html", - redirect: "/pages/dae451/" + path: "/02.计算机基础/10.计算机网络/171. 简述TCP重传机制.html", + redirect: "/pages/a219df/" }, { name: "v-0e7ed85a", @@ -1257,36 +1273,36 @@ export const routes = [ redirect: "/pages/34d064/" }, { - name: "v-15fe5424", - path: "/pages/d0c6d3/", + name: "v-7fc8ca51", + path: "/pages/056828/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-15fe5424").then(next) + ensureAsyncComponentsLoaded("Layout", "v-7fc8ca51").then(next) }, }, { - path: "/pages/d0c6d3/index.html", - redirect: "/pages/d0c6d3/" + path: "/pages/056828/index.html", + redirect: "/pages/056828/" }, { - path: "/02.计算机基础/10.计算机网络/210.TCP 中 SYN 攻击是什么?如何防止?.html", - redirect: "/pages/d0c6d3/" + path: "/02.计算机基础/10.计算机网络/220.简述 DDOS 攻击原理,如何防范它?.html", + redirect: "/pages/056828/" }, { - name: "v-7fc8ca51", - path: "/pages/056828/", + name: "v-15fe5424", + path: "/pages/d0c6d3/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-7fc8ca51").then(next) + ensureAsyncComponentsLoaded("Layout", "v-15fe5424").then(next) }, }, { - path: "/pages/056828/index.html", - redirect: "/pages/056828/" + path: "/pages/d0c6d3/index.html", + redirect: "/pages/d0c6d3/" }, { - path: "/02.计算机基础/10.计算机网络/220.简述 DDOS 攻击原理,如何防范它?.html", - redirect: "/pages/056828/" + path: "/02.计算机基础/10.计算机网络/210.TCP 中 SYN 攻击是什么?如何防止?.html", + redirect: "/pages/d0c6d3/" }, { name: "v-01df2b5d", @@ -1305,36 +1321,36 @@ export const routes = [ redirect: "/pages/665162/" }, { - name: "v-15ca313a", - path: "/pages/980cc7/", + name: "v-7370facf", + path: "/pages/eadd05/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-15ca313a").then(next) + ensureAsyncComponentsLoaded("Layout", "v-7370facf").then(next) }, }, { - path: "/pages/980cc7/index.html", - redirect: "/pages/980cc7/" + path: "/pages/eadd05/index.html", + redirect: "/pages/eadd05/" }, { - path: "/02.计算机基础/10.计算机网络/250.Cookie 和 Session 的关系和区别是什么?.html", - redirect: "/pages/980cc7/" + path: "/02.计算机基础/10.计算机网络/240.什么是跨域,什么情况下会发生跨域请求?.html", + redirect: "/pages/eadd05/" }, { - name: "v-7370facf", - path: "/pages/eadd05/", + name: "v-15ca313a", + path: "/pages/980cc7/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-7370facf").then(next) + ensureAsyncComponentsLoaded("Layout", "v-15ca313a").then(next) }, }, { - path: "/pages/eadd05/index.html", - redirect: "/pages/eadd05/" + path: "/pages/980cc7/index.html", + redirect: "/pages/980cc7/" }, { - path: "/02.计算机基础/10.计算机网络/240.什么是跨域,什么情况下会发生跨域请求?.html", - redirect: "/pages/eadd05/" + path: "/02.计算机基础/10.计算机网络/250.Cookie 和 Session 的关系和区别是什么?.html", + redirect: "/pages/980cc7/" }, { name: "v-a9abf6a0", @@ -1369,36 +1385,36 @@ export const routes = [ redirect: "/pages/172129/" }, { - name: "v-693fb010", - path: "/pages/c08bb6/", + name: "v-51f24219", + path: "/pages/dfb780/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-693fb010").then(next) + ensureAsyncComponentsLoaded("Layout", "v-51f24219").then(next) }, }, { - path: "/pages/c08bb6/index.html", - redirect: "/pages/c08bb6/" + path: "/pages/dfb780/index.html", + redirect: "/pages/dfb780/" }, { - path: "/02.计算机基础/10.计算机网络/280.RestFul 是什么?RestFul 请求的 URL 有什么特点?.html", - redirect: "/pages/c08bb6/" + path: "/02.计算机基础/10.计算机网络/290.简述 HTTP 报文头部的组成结构.html", + redirect: "/pages/dfb780/" }, { - name: "v-51f24219", - path: "/pages/dfb780/", + name: "v-693fb010", + path: "/pages/c08bb6/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-51f24219").then(next) + ensureAsyncComponentsLoaded("Layout", "v-693fb010").then(next) }, }, { - path: "/pages/dfb780/index.html", - redirect: "/pages/dfb780/" + path: "/pages/c08bb6/index.html", + redirect: "/pages/c08bb6/" }, { - path: "/02.计算机基础/10.计算机网络/290.简述 HTTP 报文头部的组成结构.html", - redirect: "/pages/dfb780/" + path: "/02.计算机基础/10.计算机网络/280.RestFul 是什么?RestFul 请求的 URL 有什么特点?.html", + redirect: "/pages/c08bb6/" }, { name: "v-18f7a69e", @@ -1417,20 +1433,20 @@ export const routes = [ redirect: "/pages/e5f2d7/" }, { - name: "v-64bba194", - path: "/pages/367c4d/", + name: "v-a0ab5810", + path: "/pages/c2fd8a/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-64bba194").then(next) + ensureAsyncComponentsLoaded("Layout", "v-a0ab5810").then(next) }, }, { - path: "/pages/367c4d/index.html", - redirect: "/pages/367c4d/" + path: "/pages/c2fd8a/index.html", + redirect: "/pages/c2fd8a/" }, { - path: "/02.计算机基础/10.计算机网络/300.什么是 SYN flood,如何防止这类攻击?.html", - redirect: "/pages/367c4d/" + path: "/02.计算机基础/20.操作系统/020.进程和线程之间有什么区别?.html", + redirect: "/pages/c2fd8a/" }, { name: "v-fe1405b0", @@ -1449,20 +1465,20 @@ export const routes = [ redirect: "/pages/9e6232/" }, { - name: "v-a0ab5810", - path: "/pages/c2fd8a/", + name: "v-32c73af0", + path: "/pages/198887/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-a0ab5810").then(next) + ensureAsyncComponentsLoaded("Layout", "v-32c73af0").then(next) }, }, { - path: "/pages/c2fd8a/index.html", - redirect: "/pages/c2fd8a/" + path: "/pages/198887/index.html", + redirect: "/pages/198887/" }, { - path: "/02.计算机基础/20.操作系统/020.进程和线程之间有什么区别?.html", - redirect: "/pages/c2fd8a/" + path: "/02.计算机基础/20.操作系统/030.简述几个常用的 Linux 命令以及他们的功能.html", + redirect: "/pages/198887/" }, { name: "v-e9b5ef86", @@ -1496,38 +1512,6 @@ export const routes = [ path: "/02.计算机基础/20.操作系统/050.线程有多少种状态,状态之间如何转换.html", redirect: "/pages/7de80d/" }, - { - name: "v-32c73af0", - path: "/pages/198887/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-32c73af0").then(next) - }, - }, - { - path: "/pages/198887/index.html", - redirect: "/pages/198887/" - }, - { - path: "/02.计算机基础/20.操作系统/030.简述几个常用的 Linux 命令以及他们的功能.html", - redirect: "/pages/198887/" - }, - { - name: "v-05845816", - path: "/pages/b043a6/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-05845816").then(next) - }, - }, - { - path: "/pages/b043a6/index.html", - redirect: "/pages/b043a6/" - }, - { - path: "/02.计算机基础/20.操作系统/080.Linux 下如何排查 CPU 以及 内存占用过多?.html", - redirect: "/pages/b043a6/" - }, { name: "v-262f2c87", path: "/pages/165ea8/", @@ -1560,6 +1544,22 @@ export const routes = [ path: "/02.计算机基础/20.操作系统/070.简述 select, poll, epoll 的使用场景以及区别.html", redirect: "/pages/68ea00/" }, + { + name: "v-05845816", + path: "/pages/b043a6/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-05845816").then(next) + }, + }, + { + path: "/pages/b043a6/index.html", + redirect: "/pages/b043a6/" + }, + { + path: "/02.计算机基础/20.操作系统/080.Linux 下如何排查 CPU 以及 内存占用过多?.html", + redirect: "/pages/b043a6/" + }, { name: "v-303058be", path: "/pages/8e880a/", @@ -1577,20 +1577,20 @@ export const routes = [ redirect: "/pages/8e880a/" }, { - name: "v-1f69ea23", - path: "/pages/326cf9/", + name: "v-129dffb8", + path: "/pages/f6e4d2/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-1f69ea23").then(next) + ensureAsyncComponentsLoaded("Layout", "v-129dffb8").then(next) }, }, { - path: "/pages/326cf9/index.html", - redirect: "/pages/326cf9/" + path: "/pages/f6e4d2/index.html", + redirect: "/pages/f6e4d2/" }, { - path: "/02.计算机基础/20.操作系统/100.Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程?.html", - redirect: "/pages/326cf9/" + path: "/02.计算机基础/20.操作系统/120.Linux 如何查看实时的滚动日志?.html", + redirect: "/pages/f6e4d2/" }, { name: "v-f6bafd34", @@ -1609,36 +1609,52 @@ export const routes = [ redirect: "/pages/3653a2/" }, { - name: "v-129dffb8", - path: "/pages/f6e4d2/", + name: "v-7abb1115", + path: "/pages/b43547/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-129dffb8").then(next) + ensureAsyncComponentsLoaded("Layout", "v-7abb1115").then(next) }, }, { - path: "/pages/f6e4d2/index.html", - redirect: "/pages/f6e4d2/" + path: "/pages/b43547/index.html", + redirect: "/pages/b43547/" }, { - path: "/02.计算机基础/20.操作系统/120.Linux 如何查看实时的滚动日志?.html", - redirect: "/pages/f6e4d2/" + path: "/02.计算机基础/20.操作系统/130.简述 Linux 零拷贝的原理.html", + redirect: "/pages/b43547/" }, { - name: "v-7abb1115", - path: "/pages/b43547/", + name: "v-1f69ea23", + path: "/pages/326cf9/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-7abb1115").then(next) + ensureAsyncComponentsLoaded("Layout", "v-1f69ea23").then(next) }, }, { - path: "/pages/b43547/index.html", - redirect: "/pages/b43547/" + path: "/pages/326cf9/index.html", + redirect: "/pages/326cf9/" }, { - path: "/02.计算机基础/20.操作系统/130.简述 Linux 零拷贝的原理.html", - redirect: "/pages/b43547/" + path: "/02.计算机基础/20.操作系统/100.Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程?.html", + redirect: "/pages/326cf9/" + }, + { + name: "v-1a49912a", + path: "/pages/93b870/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-1a49912a").then(next) + }, + }, + { + path: "/pages/93b870/index.html", + redirect: "/pages/93b870/" + }, + { + path: "/02.计算机基础/20.操作系统/160.简述 Linux 虚拟内存的页面置换算法.html", + redirect: "/pages/93b870/" }, { name: "v-c875c5a4", @@ -1672,22 +1688,6 @@ export const routes = [ path: "/02.计算机基础/20.操作系统/170.简述 Linux 进程调度的算法.html", redirect: "/pages/36fd29/" }, - { - name: "v-1a49912a", - path: "/pages/93b870/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-1a49912a").then(next) - }, - }, - { - path: "/pages/93b870/index.html", - redirect: "/pages/93b870/" - }, - { - path: "/02.计算机基础/20.操作系统/160.简述 Linux 虚拟内存的页面置换算法.html", - redirect: "/pages/93b870/" - }, { name: "v-63719191", path: "/pages/c64d20/", @@ -1720,22 +1720,6 @@ export const routes = [ path: "/02.计算机基础/20.操作系统/180.什么时候会由用户态陷入内核态?.html", redirect: "/pages/b84950/" }, - { - name: "v-db80e4da", - path: "/pages/3bab8c/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-db80e4da").then(next) - }, - }, - { - path: "/pages/3bab8c/index.html", - redirect: "/pages/3bab8c/" - }, - { - path: "/02.计算机基础/20.操作系统/200.Linux 中虚拟内存和物理内存有什么区别?有什么优点?.html", - redirect: "/pages/3bab8c/" - }, { name: "v-1ea2064a", path: "/pages/4c2d8e/", @@ -1753,52 +1737,52 @@ export const routes = [ redirect: "/pages/4c2d8e/" }, { - name: "v-e048e838", - path: "/pages/655f8f/", + name: "v-d41ba7ec", + path: "/pages/782c23/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-e048e838").then(next) + ensureAsyncComponentsLoaded("Layout", "v-d41ba7ec").then(next) }, }, { - path: "/pages/655f8f/index.html", - redirect: "/pages/655f8f/" + path: "/pages/782c23/index.html", + redirect: "/pages/782c23/" }, { - path: "/02.计算机基础/20.操作系统/210.简述 traceroute 命令的原理.html", - redirect: "/pages/655f8f/" + path: "/02.计算机基础/20.操作系统/220.简述IO模型.html", + redirect: "/pages/782c23/" }, { - name: "v-49688930", - path: "/pages/a25c3a/", + name: "v-db80e4da", + path: "/pages/3bab8c/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-49688930").then(next) + ensureAsyncComponentsLoaded("Layout", "v-db80e4da").then(next) }, }, { - path: "/pages/a25c3a/index.html", - redirect: "/pages/a25c3a/" + path: "/pages/3bab8c/index.html", + redirect: "/pages/3bab8c/" }, { - path: "/02.计算机基础/30.数据结构/01.数据结构.html", - redirect: "/pages/a25c3a/" + path: "/02.计算机基础/20.操作系统/200.Linux 中虚拟内存和物理内存有什么区别?有什么优点?.html", + redirect: "/pages/3bab8c/" }, { - name: "v-674454ac", - path: "/pages/3a8659/", + name: "v-49688930", + path: "/pages/a25c3a/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-674454ac").then(next) + ensureAsyncComponentsLoaded("Layout", "v-49688930").then(next) }, }, { - path: "/pages/3a8659/index.html", - redirect: "/pages/3a8659/" + path: "/pages/a25c3a/index.html", + redirect: "/pages/a25c3a/" }, { - path: "/03.数据库/01.MYSQL相关/001.MySQL中 InnoDB 和 MylSAM 的区别是什么?.html", - redirect: "/pages/3a8659/" + path: "/02.计算机基础/30.数据结构/01.数据结构.html", + redirect: "/pages/a25c3a/" }, { name: "v-2e6d42b4", @@ -1817,52 +1801,52 @@ export const routes = [ redirect: "/pages/92288a/" }, { - name: "v-f0f711c4", - path: "/pages/a5adb0/", + name: "v-e048e838", + path: "/pages/655f8f/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-f0f711c4").then(next) + ensureAsyncComponentsLoaded("Layout", "v-e048e838").then(next) }, }, { - path: "/pages/a5adb0/index.html", - redirect: "/pages/a5adb0/" + path: "/pages/655f8f/index.html", + redirect: "/pages/655f8f/" }, { - path: "/03.数据库/01.MYSQL相关/006.聚簇索引和非聚簇索引有什么区别?.html", - redirect: "/pages/a5adb0/" + path: "/02.计算机基础/20.操作系统/210.简述 traceroute 命令的原理.html", + redirect: "/pages/655f8f/" }, { - name: "v-7e02c4a1", - path: "/pages/9f2936/", + name: "v-674454ac", + path: "/pages/3a8659/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-7e02c4a1").then(next) + ensureAsyncComponentsLoaded("Layout", "v-674454ac").then(next) }, }, { - path: "/pages/9f2936/index.html", - redirect: "/pages/9f2936/" + path: "/pages/3a8659/index.html", + redirect: "/pages/3a8659/" }, { - path: "/03.数据库/01.MYSQL相关/010.简述Mysql的事务隔离级别.html", - redirect: "/pages/9f2936/" + path: "/03.数据库/01.MYSQL相关/001.MySQL中 InnoDB 和 MylSAM 的区别是什么?.html", + redirect: "/pages/3a8659/" }, { - name: "v-5b0e09c6", - path: "/pages/b17e88/", + name: "v-f0f711c4", + path: "/pages/a5adb0/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-5b0e09c6").then(next) + ensureAsyncComponentsLoaded("Layout", "v-f0f711c4").then(next) }, }, { - path: "/pages/b17e88/index.html", - redirect: "/pages/b17e88/" + path: "/pages/a5adb0/index.html", + redirect: "/pages/a5adb0/" }, { - path: "/03.数据库/01.MYSQL相关/015.说一下Mysql锁.html", - redirect: "/pages/b17e88/" + path: "/03.数据库/01.MYSQL相关/006.聚簇索引和非聚簇索引有什么区别?.html", + redirect: "/pages/a5adb0/" }, { name: "v-824b776c", @@ -1880,6 +1864,22 @@ export const routes = [ path: "/03.数据库/01.MYSQL相关/007.简述 MySQL 常见索引类型,介绍一下覆盖索引.html", redirect: "/pages/17b014/" }, + { + name: "v-7e02c4a1", + path: "/pages/9f2936/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-7e02c4a1").then(next) + }, + }, + { + path: "/pages/9f2936/index.html", + redirect: "/pages/9f2936/" + }, + { + path: "/03.数据库/01.MYSQL相关/010.简述Mysql的事务隔离级别.html", + redirect: "/pages/9f2936/" + }, { name: "v-6c681639", path: "/pages/bc5538/", @@ -1897,20 +1897,20 @@ export const routes = [ redirect: "/pages/bc5538/" }, { - name: "v-c39c4fe2", - path: "/pages/57561d/", + name: "v-5b0e09c6", + path: "/pages/b17e88/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-c39c4fe2").then(next) + ensureAsyncComponentsLoaded("Layout", "v-5b0e09c6").then(next) }, }, { - path: "/pages/57561d/index.html", - redirect: "/pages/57561d/" + path: "/pages/b17e88/index.html", + redirect: "/pages/b17e88/" }, { - path: "/03.数据库/01.MYSQL相关/030.简述乐观锁以及悲观锁的区别以及使用场景.html", - redirect: "/pages/57561d/" + path: "/03.数据库/01.MYSQL相关/015.说一下Mysql锁.html", + redirect: "/pages/b17e88/" }, { name: "v-689e142a", @@ -1928,6 +1928,22 @@ export const routes = [ path: "/03.数据库/01.MYSQL相关/040.简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?.html", redirect: "/pages/36672c/" }, + { + name: "v-c39c4fe2", + path: "/pages/57561d/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-c39c4fe2").then(next) + }, + }, + { + path: "/pages/57561d/index.html", + redirect: "/pages/57561d/" + }, + { + path: "/03.数据库/01.MYSQL相关/030.简述乐观锁以及悲观锁的区别以及使用场景.html", + redirect: "/pages/57561d/" + }, { name: "v-4ecddbec", path: "/pages/7a1f1e/", @@ -1961,36 +1977,36 @@ export const routes = [ redirect: "/pages/52773a/" }, { - name: "v-53bcbb7a", - path: "/pages/f2dbbe/", + name: "v-0dd47f09", + path: "/pages/f4cf50/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-53bcbb7a").then(next) + ensureAsyncComponentsLoaded("Layout", "v-0dd47f09").then(next) }, }, { - path: "/pages/f2dbbe/index.html", - redirect: "/pages/f2dbbe/" + path: "/pages/f4cf50/index.html", + redirect: "/pages/f4cf50/" }, { - path: "/03.数据库/01.MYSQL相关/080.简述常见的负载均衡算法.html", - redirect: "/pages/f2dbbe/" + path: "/03.数据库/01.MYSQL相关/070.产生死锁的必要条件有哪些?如何解决死锁?.html", + redirect: "/pages/f4cf50/" }, { - name: "v-0dd47f09", - path: "/pages/f4cf50/", + name: "v-53bcbb7a", + path: "/pages/f2dbbe/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-0dd47f09").then(next) + ensureAsyncComponentsLoaded("Layout", "v-53bcbb7a").then(next) }, }, { - path: "/pages/f4cf50/index.html", - redirect: "/pages/f4cf50/" + path: "/pages/f2dbbe/index.html", + redirect: "/pages/f2dbbe/" }, { - path: "/03.数据库/01.MYSQL相关/070.产生死锁的必要条件有哪些?如何解决死锁?.html", - redirect: "/pages/f4cf50/" + path: "/03.数据库/01.MYSQL相关/080.简述常见的负载均衡算法.html", + redirect: "/pages/f2dbbe/" }, { name: "v-4cef944a", @@ -2008,6 +2024,22 @@ export const routes = [ path: "/03.数据库/01.MYSQL相关/100.如何设计数据库压测方案?.html", redirect: "/pages/a8eb47/" }, + { + name: "v-ed0c93b8", + path: "/pages/ac0bb8/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-ed0c93b8").then(next) + }, + }, + { + path: "/pages/ac0bb8/index.html", + redirect: "/pages/ac0bb8/" + }, + { + path: "/03.数据库/01.MYSQL相关/110.简述数据库中什么情况下进行分库,什么情况下进行分表?.html", + redirect: "/pages/ac0bb8/" + }, { name: "v-c383b96e", path: "/pages/1ac30e/", @@ -2040,22 +2072,6 @@ export const routes = [ path: "/03.数据库/01.MYSQL相关/130.简述 undo log 和 redo log 的作用.html", redirect: "/pages/7bbbf6/" }, - { - name: "v-ed0c93b8", - path: "/pages/ac0bb8/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-ed0c93b8").then(next) - }, - }, - { - path: "/pages/ac0bb8/index.html", - redirect: "/pages/ac0bb8/" - }, - { - path: "/03.数据库/01.MYSQL相关/110.简述数据库中什么情况下进行分库,什么情况下进行分表?.html", - redirect: "/pages/ac0bb8/" - }, { name: "v-d6bc240a", path: "/pages/f62359/", @@ -2072,22 +2088,6 @@ export const routes = [ path: "/03.数据库/01.MYSQL相关/120.数据库的读写分离的作用是什么?如何实现?.html", redirect: "/pages/f62359/" }, - { - name: "v-76f5e79a", - path: "/pages/1656d8/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-76f5e79a").then(next) - }, - }, - { - path: "/pages/1656d8/index.html", - redirect: "/pages/1656d8/" - }, - { - path: "/03.数据库/01.MYSQL相关/160.简述 MySQL MVCC 的实现原理 .html", - redirect: "/pages/1656d8/" - }, { name: "v-77790024", path: "/pages/429100/", @@ -2121,20 +2121,20 @@ export const routes = [ redirect: "/pages/b4338e/" }, { - name: "v-6b967a6e", - path: "/pages/32696d/", + name: "v-76f5e79a", + path: "/pages/1656d8/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-6b967a6e").then(next) + ensureAsyncComponentsLoaded("Layout", "v-76f5e79a").then(next) }, }, { - path: "/pages/32696d/index.html", - redirect: "/pages/32696d/" + path: "/pages/1656d8/index.html", + redirect: "/pages/1656d8/" }, { - path: "/03.数据库/01.MYSQL相关/180.模糊查询是如何实现的?.html", - redirect: "/pages/32696d/" + path: "/03.数据库/01.MYSQL相关/160.简述 MySQL MVCC 的实现原理 .html", + redirect: "/pages/1656d8/" }, { name: "v-0a7e6416", @@ -2153,20 +2153,20 @@ export const routes = [ redirect: "/pages/d97948/" }, { - name: "v-d804fba0", - path: "/pages/d70c1f/", + name: "v-6b967a6e", + path: "/pages/32696d/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-d804fba0").then(next) + ensureAsyncComponentsLoaded("Layout", "v-6b967a6e").then(next) }, }, { - path: "/pages/d70c1f/index.html", - redirect: "/pages/d70c1f/" + path: "/pages/32696d/index.html", + redirect: "/pages/32696d/" }, { - path: "/03.数据库/02.REDIS相关/001.Redis 有几种数据结构?Zset 是如何实现的?.html", - redirect: "/pages/d70c1f/" + path: "/03.数据库/01.MYSQL相关/180.模糊查询是如何实现的?.html", + redirect: "/pages/32696d/" }, { name: "v-cb742458", @@ -2185,36 +2185,36 @@ export const routes = [ redirect: "/pages/d66211/" }, { - name: "v-3e917471", - path: "/pages/34aec2/", + name: "v-d804fba0", + path: "/pages/d70c1f/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-3e917471").then(next) + ensureAsyncComponentsLoaded("Layout", "v-d804fba0").then(next) }, }, { - path: "/pages/34aec2/index.html", - redirect: "/pages/34aec2/" + path: "/pages/d70c1f/index.html", + redirect: "/pages/d70c1f/" }, { - path: "/03.数据库/02.REDIS相关/005.简述 Redis 字符串的底层结构.html", - redirect: "/pages/34aec2/" + path: "/03.数据库/02.REDIS相关/001.Redis 有几种数据结构?Zset 是如何实现的?.html", + redirect: "/pages/d70c1f/" }, { - name: "v-c0bc7b7a", - path: "/pages/9326cc/", + name: "v-3e917471", + path: "/pages/34aec2/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-c0bc7b7a").then(next) + ensureAsyncComponentsLoaded("Layout", "v-3e917471").then(next) }, }, { - path: "/pages/9326cc/index.html", - redirect: "/pages/9326cc/" + path: "/pages/34aec2/index.html", + redirect: "/pages/34aec2/" }, { - path: "/03.数据库/02.REDIS相关/010.Redis的缓存淘汰策略有哪些?.html", - redirect: "/pages/9326cc/" + path: "/03.数据库/02.REDIS相关/005.简述 Redis 字符串的底层结构.html", + redirect: "/pages/34aec2/" }, { name: "v-7c214d00", @@ -2232,6 +2232,22 @@ export const routes = [ path: "/03.数据库/02.REDIS相关/020.简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点.html", redirect: "/pages/7e708d/" }, + { + name: "v-c0bc7b7a", + path: "/pages/9326cc/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-c0bc7b7a").then(next) + }, + }, + { + path: "/pages/9326cc/index.html", + redirect: "/pages/9326cc/" + }, + { + path: "/03.数据库/02.REDIS相关/010.Redis的缓存淘汰策略有哪些?.html", + redirect: "/pages/9326cc/" + }, { name: "v-2c6b511c", path: "/pages/656f46/", @@ -2264,22 +2280,6 @@ export const routes = [ path: "/03.数据库/02.REDIS相关/040.简述 Redis 集群配置以及基础原理.html", redirect: "/pages/338e30/" }, - { - name: "v-6a45f68d", - path: "/pages/e2e910/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-6a45f68d").then(next) - }, - }, - { - path: "/pages/e2e910/index.html", - redirect: "/pages/e2e910/" - }, - { - path: "/03.数据库/02.REDIS相关/060.Redis 中,sentinel 和 cluster 的区别和适用场景是什么?.html", - redirect: "/pages/e2e910/" - }, { name: "v-18b427a0", path: "/pages/a42826/", @@ -2313,20 +2313,20 @@ export const routes = [ redirect: "/pages/03cd25/" }, { - name: "v-35718f4e", - path: "/pages/9c28c9/", + name: "v-6a45f68d", + path: "/pages/e2e910/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-35718f4e").then(next) + ensureAsyncComponentsLoaded("Layout", "v-6a45f68d").then(next) }, }, { - path: "/pages/9c28c9/index.html", - redirect: "/pages/9c28c9/" + path: "/pages/e2e910/index.html", + redirect: "/pages/e2e910/" }, { - path: "/03.数据库/02.REDIS相关/080.简述 Redis 的线程模型以及底层架构设计.html", - redirect: "/pages/9c28c9/" + path: "/03.数据库/02.REDIS相关/060.Redis 中,sentinel 和 cluster 的区别和适用场景是什么?.html", + redirect: "/pages/e2e910/" }, { name: "v-57957421", @@ -2344,6 +2344,22 @@ export const routes = [ path: "/03.数据库/02.REDIS相关/100.简述 Redis 的哨兵机制.html", redirect: "/pages/cc8507/" }, + { + name: "v-35718f4e", + path: "/pages/9c28c9/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-35718f4e").then(next) + }, + }, + { + path: "/pages/9c28c9/index.html", + redirect: "/pages/9c28c9/" + }, + { + path: "/03.数据库/02.REDIS相关/080.简述 Redis 的线程模型以及底层架构设计.html", + redirect: "/pages/9c28c9/" + }, { name: "v-37f158ba", path: "/pages/795c09/", @@ -2392,6 +2408,22 @@ export const routes = [ path: "/04.中间件/10.Netty/001.你熟悉哪些IO模型?.html", redirect: "/pages/40f3c9/" }, + { + name: "v-073f22cd", + path: "/pages/59089d/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-073f22cd").then(next) + }, + }, + { + path: "/pages/59089d/index.html", + redirect: "/pages/59089d/" + }, + { + path: "/04.中间件/10.Netty/015.Netty中有哪些解码器?.html", + redirect: "/pages/59089d/" + }, { name: "v-5acdab4d", path: "/pages/3db019/", @@ -2424,38 +2456,6 @@ export const routes = [ path: "/04.中间件/10.Netty/020.为什么Netty要自己写一个ByteBuf.html", redirect: "/pages/ef17ee/" }, - { - name: "v-073f22cd", - path: "/pages/59089d/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-073f22cd").then(next) - }, - }, - { - path: "/pages/59089d/index.html", - redirect: "/pages/59089d/" - }, - { - path: "/04.中间件/10.Netty/015.Netty中有哪些解码器?.html", - redirect: "/pages/59089d/" - }, - { - name: "v-6b32ada0", - path: "/pages/bfdcc8/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-6b32ada0").then(next) - }, - }, - { - path: "/pages/bfdcc8/index.html", - redirect: "/pages/bfdcc8/" - }, - { - path: "/04.中间件/10.Netty/050.Java NIO了解吗?.html", - redirect: "/pages/bfdcc8/" - }, { name: "v-5157927c", path: "/pages/f851b6/", @@ -2489,20 +2489,36 @@ export const routes = [ redirect: "/pages/e7aa60/" }, { - name: "v-7c0136cf", - path: "/pages/fe7ca2/", + name: "v-6b32ada0", + path: "/pages/bfdcc8/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-7c0136cf").then(next) + ensureAsyncComponentsLoaded("Layout", "v-6b32ada0").then(next) }, }, { - path: "/pages/fe7ca2/index.html", - redirect: "/pages/fe7ca2/" + path: "/pages/bfdcc8/index.html", + redirect: "/pages/bfdcc8/" }, { - path: "/04.中间件/40.Kafka/010.生产者.html", - redirect: "/pages/fe7ca2/" + path: "/04.中间件/10.Netty/050.Java NIO了解吗?.html", + redirect: "/pages/bfdcc8/" + }, + { + name: "v-0df39df8", + path: "/pages/561f34/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-0df39df8").then(next) + }, + }, + { + path: "/pages/561f34/index.html", + redirect: "/pages/561f34/" + }, + { + path: "/04.中间件/40.Kafka/001.Kafka包含那几部分.html", + redirect: "/pages/561f34/" }, { name: "v-04af9e42", @@ -2521,20 +2537,20 @@ export const routes = [ redirect: "/pages/fed167/" }, { - name: "v-684c4815", - path: "/pages/76a94c/", + name: "v-7c0136cf", + path: "/pages/fe7ca2/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-684c4815").then(next) + ensureAsyncComponentsLoaded("Layout", "v-7c0136cf").then(next) }, }, { - path: "/pages/76a94c/index.html", - redirect: "/pages/76a94c/" + path: "/pages/fe7ca2/index.html", + redirect: "/pages/fe7ca2/" }, { - path: "/04.中间件/40.Kafka/040.如何保证Kafka的可靠性.html", - redirect: "/pages/76a94c/" + path: "/04.中间件/40.Kafka/010.生产者.html", + redirect: "/pages/fe7ca2/" }, { name: "v-0c440466", @@ -2553,36 +2569,36 @@ export const routes = [ redirect: "/pages/8d1d04/" }, { - name: "v-0df39df8", - path: "/pages/561f34/", + name: "v-a9558ec0", + path: "/pages/01b9b2/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-0df39df8").then(next) + ensureAsyncComponentsLoaded("Layout", "v-a9558ec0").then(next) }, }, { - path: "/pages/561f34/index.html", - redirect: "/pages/561f34/" + path: "/pages/01b9b2/index.html", + redirect: "/pages/01b9b2/" }, { - path: "/04.中间件/40.Kafka/001.Kafka包含那几部分.html", - redirect: "/pages/561f34/" + path: "/08.系统设计/001.系统设计.html", + redirect: "/pages/01b9b2/" }, { - name: "v-a9558ec0", - path: "/pages/01b9b2/", + name: "v-684c4815", + path: "/pages/76a94c/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-a9558ec0").then(next) + ensureAsyncComponentsLoaded("Layout", "v-684c4815").then(next) }, }, { - path: "/pages/01b9b2/index.html", - redirect: "/pages/01b9b2/" + path: "/pages/76a94c/index.html", + redirect: "/pages/76a94c/" }, { - path: "/08.系统设计/001.系统设计.html", - redirect: "/pages/01b9b2/" + path: "/04.中间件/40.Kafka/040.如何保证Kafka的可靠性.html", + redirect: "/pages/76a94c/" }, { name: "v-5675b06b", @@ -2600,6 +2616,22 @@ export const routes = [ path: "/08.系统设计/010.什么是 CAP ?什么是最终一致性?什么是幂等操作?.html", redirect: "/pages/20c647/" }, + { + name: "v-f88544f4", + path: "/pages/4eae5b/", + component: GlobalLayout, + beforeEnter: (to, from, next) => { + ensureAsyncComponentsLoaded("Layout", "v-f88544f4").then(next) + }, + }, + { + path: "/pages/4eae5b/index.html", + redirect: "/pages/4eae5b/" + }, + { + path: "/08.系统设计/020.简述什么是两阶段提交?.html", + redirect: "/pages/4eae5b/" + }, { name: "v-2a52a762", path: "/pages/5e507a/", @@ -2633,20 +2665,20 @@ export const routes = [ redirect: "/about/" }, { - name: "v-f88544f4", - path: "/pages/4eae5b/", + name: "v-5f8dfe80", + path: "/categories/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-f88544f4").then(next) + ensureAsyncComponentsLoaded("Layout", "v-5f8dfe80").then(next) }, }, { - path: "/pages/4eae5b/index.html", - redirect: "/pages/4eae5b/" + path: "/categories/index.html", + redirect: "/categories/" }, { - path: "/08.系统设计/020.简述什么是两阶段提交?.html", - redirect: "/pages/4eae5b/" + path: "/@pages/categoriesPage.html", + redirect: "/categories/" }, { name: "v-2db11cb2", @@ -2664,22 +2696,6 @@ export const routes = [ path: "/_posts/随笔/你知道的越多,不知道的也就越多.html", redirect: "/pages/f2e63f/" }, - { - name: "v-48f7eec0", - path: "/archives/", - component: GlobalLayout, - beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-48f7eec0").then(next) - }, - }, - { - path: "/archives/index.html", - redirect: "/archives/" - }, - { - path: "/@pages/archivesPage.html", - redirect: "/archives/" - }, { name: "v-7ab50b9f", path: "/pages/beb6c0bd8a66cea6/", @@ -2729,36 +2745,36 @@ export const routes = [ redirect: "/pages/47cf96/" }, { - name: "v-5f8dfe80", - path: "/categories/", + name: "v-7d77d69c", + path: "/pages/cd8bde/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-5f8dfe80").then(next) + ensureAsyncComponentsLoaded("Layout", "v-7d77d69c").then(next) }, }, { - path: "/categories/index.html", - redirect: "/categories/" + path: "/pages/cd8bde/index.html", + redirect: "/pages/cd8bde/" }, { - path: "/@pages/categoriesPage.html", - redirect: "/categories/" + path: "/_posts/随笔/拥抱生活,拥抱快乐.html", + redirect: "/pages/cd8bde/" }, { - name: "v-7d77d69c", - path: "/pages/cd8bde/", + name: "v-48f7eec0", + path: "/archives/", component: GlobalLayout, beforeEnter: (to, from, next) => { - ensureAsyncComponentsLoaded("Layout", "v-7d77d69c").then(next) + ensureAsyncComponentsLoaded("Layout", "v-48f7eec0").then(next) }, }, { - path: "/pages/cd8bde/index.html", - redirect: "/pages/cd8bde/" + path: "/archives/index.html", + redirect: "/archives/" }, { - path: "/_posts/随笔/拥抱生活,拥抱快乐.html", - redirect: "/pages/cd8bde/" + path: "/@pages/archivesPage.html", + redirect: "/archives/" }, { name: "v-64f9b920", diff --git a/node_modules/@vuepress/core/.temp/internal/siteData.js b/node_modules/@vuepress/core/.temp/internal/siteData.js index a8bcc4ba8..1b02dc14a 100644 --- a/node_modules/@vuepress/core/.temp/internal/siteData.js +++ b/node_modules/@vuepress/core/.temp/internal/siteData.js @@ -156,42 +156,6 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, - { - "title": "HashMap 与 ConcurrentHashMap 的实现原理是怎样的?", - "frontmatter": { - "title": "HashMap 与 ConcurrentHashMap 的实现原理是怎样的?", - "date": "2022-04-04T14:21:18.000Z", - "permalink": "/pages/d1c564/", - "categories": [ - "Java相关", - "基础" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/010.HashMap%20%E4%B8%8E%20ConcurrentHashMap%20%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E6%98%AF%E6%80%8E%E6%A0%B7%E7%9A%84%EF%BC%9F.html", - "relativePath": "01.Java相关/10.基础/010.HashMap 与 ConcurrentHashMap 的实现原理是怎样的?.md", - "key": "v-0734374b", - "path": "/pages/d1c564/", - "headers": [ - { - "level": 2, - "title": "HashMap 与 ConcurrentHashMap 的实现原理是怎样的?ConcurrentHashMap 是如何保证线程安全的?", - "slug": "hashmap-与-concurrenthashmap-的实现原理是怎样的-concurrenthashmap-是如何保证线程安全的", - "normalizedTitle": "hashmap 与 concurrenthashmap 的实现原理是怎样的?concurrenthashmap 是如何保证线程安全的?", - "charIndex": 2 - } - ], - "headersStr": "HashMap 与 ConcurrentHashMap 的实现原理是怎样的?ConcurrentHashMap 是如何保证线程安全的?", - "content": "# HashMap 与 ConcurrentHashMap 的实现原理是怎样的?ConcurrentHashMap 是如何保证线程安全的?\n\n HASHMAP CONCURRENTHASHMAP\n是否线程安全 不安全 安全\nJDK1.7-数据结构 数组+链表 数组+链表+Segment\nJDK1.7-数据结构-图示 \n锁粒度 --- 对segment加锁,最大并发修改线程数=segment数量\n--- --- ---\nJDK1.8-数据结构 数组+链表+红黑树 数组+链表+红黑树\nJDK1.8-数据结构-图示 \n锁粒度 --- HashEntry(首节点),最大并发修改线程数=数组.size\n\n> Q1:ConcurrentHashMap 中变量使用 final 和 volatile 修饰有什么用呢?\n> A:Final域使得确保初始化安全性(initialization safety)成为可能,初始化安全性让不可变形对象不需要同步就能自由地被访问和共享。\n> Node的元素val和指针next是用 volatile 修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。\n\n> Q2:ConcurrentHashMap在JDK 7和8之间的区别\n> A:去掉了Segment这种数据结构\n> 增加了红黑树结构来优化链表,解决了长链表遍历慢的问题", - "normalizedContent": "# hashmap 与 concurrenthashmap 的实现原理是怎样的?concurrenthashmap 是如何保证线程安全的?\n\n hashmap concurrenthashmap\n是否线程安全 不安全 安全\njdk1.7-数据结构 数组+链表 数组+链表+segment\njdk1.7-数据结构-图示 \n锁粒度 --- 对segment加锁,最大并发修改线程数=segment数量\n--- --- ---\njdk1.8-数据结构 数组+链表+红黑树 数组+链表+红黑树\njdk1.8-数据结构-图示 \n锁粒度 --- hashentry(首节点),最大并发修改线程数=数组.size\n\n> q1:concurrenthashmap 中变量使用 final 和 volatile 修饰有什么用呢?\n> a:final域使得确保初始化安全性(initialization safety)成为可能,初始化安全性让不可变形对象不需要同步就能自由地被访问和共享。\n> node的元素val和指针next是用 volatile 修饰的,在多线程环境下线程a修改结点的val或者新增节点的时候是对线程b可见的。\n\n> q2:concurrenthashmap在jdk 7和8之间的区别\n> a:去掉了segment这种数据结构\n> 增加了红黑树结构来优化链表,解决了长链表遍历慢的问题", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, { "title": "Java 异常有哪些类型?", "frontmatter": { @@ -229,11 +193,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "Java 中接口和抽象类的区别", + "title": "HashMap 与 ConcurrentHashMap 的实现原理是怎样的?", "frontmatter": { - "title": "Java 中接口和抽象类的区别", - "date": "2022-04-04T14:22:16.000Z", - "permalink": "/pages/826c8b/", + "title": "HashMap 与 ConcurrentHashMap 的实现原理是怎样的?", + "date": "2022-04-04T14:21:18.000Z", + "permalink": "/pages/d1c564/", "categories": [ "Java相关", "基础" @@ -242,22 +206,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/020.Java%20%E4%B8%AD%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.html", - "relativePath": "01.Java相关/10.基础/020.Java 中接口和抽象类的区别.md", - "key": "v-4e1fd39c", - "path": "/pages/826c8b/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/010.HashMap%20%E4%B8%8E%20ConcurrentHashMap%20%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E6%98%AF%E6%80%8E%E6%A0%B7%E7%9A%84%EF%BC%9F.html", + "relativePath": "01.Java相关/10.基础/010.HashMap 与 ConcurrentHashMap 的实现原理是怎样的?.md", + "key": "v-0734374b", + "path": "/pages/d1c564/", "headers": [ { "level": 2, - "title": "Java 中接口和抽象类的区别", - "slug": "java-中接口和抽象类的区别", - "normalizedTitle": "java 中接口和抽象类的区别", + "title": "HashMap 与 ConcurrentHashMap 的实现原理是怎样的?ConcurrentHashMap 是如何保证线程安全的?", + "slug": "hashmap-与-concurrenthashmap-的实现原理是怎样的-concurrenthashmap-是如何保证线程安全的", + "normalizedTitle": "hashmap 与 concurrenthashmap 的实现原理是怎样的?concurrenthashmap 是如何保证线程安全的?", "charIndex": 2 } ], - "headersStr": "Java 中接口和抽象类的区别", - "content": "# Java 中接口和抽象类的区别\n\n 抽象类 接口\n修饰符 abstract interface\n从设计层面来说 是一种模板设计,抽象类要被子类继承 是一种行为的规范,接口要被类实现\n使用场景 在代码实现方面发挥作用,可以实现代码的重用 在系统架构设计方法发挥作用,主要用于定义模块之间的通信契约\n\n抽象类-经典使用场景: StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类\n\nabstract class AbstractStringBuilder implements Appendable, CharSequence {\n /**\n * The value is used for character storage.\n */\n char[] value;\n\n /**\n * The count is the number of characters used.\n */\n int count;\n\n AbstractStringBuilder(int capacity) {\n value = new char[capacity];\n }\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n", - "normalizedContent": "# java 中接口和抽象类的区别\n\n 抽象类 接口\n修饰符 abstract interface\n从设计层面来说 是一种模板设计,抽象类要被子类继承 是一种行为的规范,接口要被类实现\n使用场景 在代码实现方面发挥作用,可以实现代码的重用 在系统架构设计方法发挥作用,主要用于定义模块之间的通信契约\n\n抽象类-经典使用场景: stringbuilder 与 stringbuffer 都继承自 abstractstringbuilder 类\n\nabstract class abstractstringbuilder implements appendable, charsequence {\n /**\n * the value is used for character storage.\n */\n char[] value;\n\n /**\n * the count is the number of characters used.\n */\n int count;\n\n abstractstringbuilder(int capacity) {\n value = new char[capacity];\n }\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n", + "headersStr": "HashMap 与 ConcurrentHashMap 的实现原理是怎样的?ConcurrentHashMap 是如何保证线程安全的?", + "content": "# HashMap 与 ConcurrentHashMap 的实现原理是怎样的?ConcurrentHashMap 是如何保证线程安全的?\n\n HASHMAP CONCURRENTHASHMAP\n是否线程安全 不安全 安全\nJDK1.7-数据结构 数组+链表 数组+链表+Segment\nJDK1.7-数据结构-图示 \n锁粒度 --- 对segment加锁,最大并发修改线程数=segment数量\n--- --- ---\nJDK1.8-数据结构 数组+链表+红黑树 数组+链表+红黑树\nJDK1.8-数据结构-图示 \n锁粒度 --- HashEntry(首节点),最大并发修改线程数=数组.size\n\n> Q1:ConcurrentHashMap 中变量使用 final 和 volatile 修饰有什么用呢?\n> A:Final域使得确保初始化安全性(initialization safety)成为可能,初始化安全性让不可变形对象不需要同步就能自由地被访问和共享。\n> Node的元素val和指针next是用 volatile 修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。\n\n> Q2:ConcurrentHashMap在JDK 7和8之间的区别\n> A:去掉了Segment这种数据结构\n> 增加了红黑树结构来优化链表,解决了长链表遍历慢的问题", + "normalizedContent": "# hashmap 与 concurrenthashmap 的实现原理是怎样的?concurrenthashmap 是如何保证线程安全的?\n\n hashmap concurrenthashmap\n是否线程安全 不安全 安全\njdk1.7-数据结构 数组+链表 数组+链表+segment\njdk1.7-数据结构-图示 \n锁粒度 --- 对segment加锁,最大并发修改线程数=segment数量\n--- --- ---\njdk1.8-数据结构 数组+链表+红黑树 数组+链表+红黑树\njdk1.8-数据结构-图示 \n锁粒度 --- hashentry(首节点),最大并发修改线程数=数组.size\n\n> q1:concurrenthashmap 中变量使用 final 和 volatile 修饰有什么用呢?\n> a:final域使得确保初始化安全性(initialization safety)成为可能,初始化安全性让不可变形对象不需要同步就能自由地被访问和共享。\n> node的元素val和指针next是用 volatile 修饰的,在多线程环境下线程a修改结点的val或者新增节点的时候是对线程b可见的。\n\n> q2:concurrenthashmap在jdk 7和8之间的区别\n> a:去掉了segment这种数据结构\n> 增加了红黑树结构来优化链表,解决了长链表遍历慢的问题", "charsets": { "cjk": true }, @@ -301,11 +265,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "String,StringBuffer,StringBuilder 之间有什么区别?", + "title": "== 和 equals() 的区别?", "frontmatter": { - "title": "String,StringBuffer,StringBuilder 之间有什么区别?", - "date": "2022-04-04T14:24:07.000Z", - "permalink": "/pages/211d8c/", + "title": "== 和 equals() 的区别?", + "date": "2022-04-04T14:23:40.000Z", + "permalink": "/pages/06b28a/", "categories": [ "Java相关", "基础" @@ -314,22 +278,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/050.String%EF%BC%8CStringBuffer%EF%BC%8CStringBuilder%20%E4%B9%8B%E9%97%B4%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F.html", - "relativePath": "01.Java相关/10.基础/050.String,StringBuffer,StringBuilder 之间有什么区别?.md", - "key": "v-7f48b402", - "path": "/pages/211d8c/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/040.==%20%E5%92%8C%20equals()%20%E7%9A%84%E5%8C%BA%E5%88%AB%EF%BC%9F.html", + "relativePath": "01.Java相关/10.基础/040.== 和 equals() 的区别?.md", + "key": "v-d8a2f15c", + "path": "/pages/06b28a/", "headers": [ { "level": 2, - "title": "String,StringBuffer,StringBuilder 之间有什么区别?", - "slug": "string-stringbuffer-stringbuilder-之间有什么区别", - "normalizedTitle": "string,stringbuffer,stringbuilder 之间有什么区别?", + "title": "== 和 equals() 的区别?", + "slug": "和-equals-的区别", + "normalizedTitle": "== 和 equals() 的区别?", "charIndex": 2 } ], - "headersStr": "String,StringBuffer,StringBuilder 之间有什么区别?", - "content": "# String,StringBuffer,StringBuilder 之间有什么区别?\n\n * String 对象是不可变的。String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[]。\n * StringBuffer,StringBuilder是可变的\n * StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 是线程不安全的。\n\n STRING STRINGBUFFER STRINGBUILDER\n可变性 不可变 可变 可变\n线程安全性 安全 安全 不安全\n使用场景 操作少量数据 多线程操作字符串缓冲区下操作大量数据 单线程操作字符串缓冲区下操作大量数据", - "normalizedContent": "# string,stringbuffer,stringbuilder 之间有什么区别?\n\n * string 对象是不可变的。string 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[]。\n * stringbuffer,stringbuilder是可变的\n * stringbuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。stringbuilder 是线程不安全的。\n\n string stringbuffer stringbuilder\n可变性 不可变 可变 可变\n线程安全性 安全 安全 不安全\n使用场景 操作少量数据 多线程操作字符串缓冲区下操作大量数据 单线程操作字符串缓冲区下操作大量数据", + "headersStr": "== 和 equals() 的区别?", + "content": "# == 和 equals() 的区别?\n\n * == : 基本数据类型==比较的是值,引用数据类型==比较的是内存地址。\n * 类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。\n\npublic static void main(String[] args) {\n\t\tString s1 = new String(\"hello\"); // s1 为一个引用\n String s2 = new String(\"hello\"); // s2和s1内容一样\n String s3 = \"world\"; // 放在常量池中\n String s4 = \"world\"; // 从常量池中查找\n String o1 = new Object(); // o1 为一个引用\n String o2 = new Object(); // o2 为另一个引用\n if (aa == bb) // true 都是常量池的地址\n System.out.println(\"aa==bb\");\n if (s1 == s2) // false,比较地址,s1 s2是两次new出来的对象,地址肯定不同\n System.out.println(\"a==b\");\n if (s1.equals(s2)) // true String的equals方法被重写了 比较的是内容\n System.out.println(\"aEQb\");\n if (o1 == o2) // false Object没有重写equals 比较地址\n System.out.println(\"aa==bb\");\n if (o1.equals(o2)) // false 比较地址 Object没有重写equals 比较地址\n System.out.println(\"a==b\");\n if (55 == 55L) { // true 基本数据类型 比较的是数值\n System.out.println(\"true\");\n } \n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n", + "normalizedContent": "# == 和 equals() 的区别?\n\n * == : 基本数据类型==比较的是值,引用数据类型==比较的是内存地址。\n * 类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。\n\npublic static void main(string[] args) {\n\t\tstring s1 = new string(\"hello\"); // s1 为一个引用\n string s2 = new string(\"hello\"); // s2和s1内容一样\n string s3 = \"world\"; // 放在常量池中\n string s4 = \"world\"; // 从常量池中查找\n string o1 = new object(); // o1 为一个引用\n string o2 = new object(); // o2 为另一个引用\n if (aa == bb) // true 都是常量池的地址\n system.out.println(\"aa==bb\");\n if (s1 == s2) // false,比较地址,s1 s2是两次new出来的对象,地址肯定不同\n system.out.println(\"a==b\");\n if (s1.equals(s2)) // true string的equals方法被重写了 比较的是内容\n system.out.println(\"aeqb\");\n if (o1 == o2) // false object没有重写equals 比较地址\n system.out.println(\"aa==bb\");\n if (o1.equals(o2)) // false 比较地址 object没有重写equals 比较地址\n system.out.println(\"a==b\");\n if (55 == 55l) { // true 基本数据类型 比较的是数值\n system.out.println(\"true\");\n } \n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n", "charsets": { "cjk": true }, @@ -337,11 +301,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "== 和 equals() 的区别?", + "title": "Java 中接口和抽象类的区别", "frontmatter": { - "title": "== 和 equals() 的区别?", - "date": "2022-04-04T14:23:40.000Z", - "permalink": "/pages/06b28a/", + "title": "Java 中接口和抽象类的区别", + "date": "2022-04-04T14:22:16.000Z", + "permalink": "/pages/826c8b/", "categories": [ "Java相关", "基础" @@ -350,22 +314,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/040.==%20%E5%92%8C%20equals()%20%E7%9A%84%E5%8C%BA%E5%88%AB%EF%BC%9F.html", - "relativePath": "01.Java相关/10.基础/040.== 和 equals() 的区别?.md", - "key": "v-d8a2f15c", - "path": "/pages/06b28a/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/020.Java%20%E4%B8%AD%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.html", + "relativePath": "01.Java相关/10.基础/020.Java 中接口和抽象类的区别.md", + "key": "v-4e1fd39c", + "path": "/pages/826c8b/", "headers": [ { "level": 2, - "title": "== 和 equals() 的区别?", - "slug": "和-equals-的区别", - "normalizedTitle": "== 和 equals() 的区别?", + "title": "Java 中接口和抽象类的区别", + "slug": "java-中接口和抽象类的区别", + "normalizedTitle": "java 中接口和抽象类的区别", "charIndex": 2 } ], - "headersStr": "== 和 equals() 的区别?", - "content": "# == 和 equals() 的区别?\n\n * == : 基本数据类型==比较的是值,引用数据类型==比较的是内存地址。\n * 类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。\n\npublic static void main(String[] args) {\n\t\tString s1 = new String(\"hello\"); // s1 为一个引用\n String s2 = new String(\"hello\"); // s2和s1内容一样\n String s3 = \"world\"; // 放在常量池中\n String s4 = \"world\"; // 从常量池中查找\n String o1 = new Object(); // o1 为一个引用\n String o2 = new Object(); // o2 为另一个引用\n if (aa == bb) // true 都是常量池的地址\n System.out.println(\"aa==bb\");\n if (s1 == s2) // false,比较地址,s1 s2是两次new出来的对象,地址肯定不同\n System.out.println(\"a==b\");\n if (s1.equals(s2)) // true String的equals方法被重写了 比较的是内容\n System.out.println(\"aEQb\");\n if (o1 == o2) // false Object没有重写equals 比较地址\n System.out.println(\"aa==bb\");\n if (o1.equals(o2)) // false 比较地址 Object没有重写equals 比较地址\n System.out.println(\"a==b\");\n if (55 == 55L) { // true 基本数据类型 比较的是数值\n System.out.println(\"true\");\n } \n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n", - "normalizedContent": "# == 和 equals() 的区别?\n\n * == : 基本数据类型==比较的是值,引用数据类型==比较的是内存地址。\n * 类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。\n\npublic static void main(string[] args) {\n\t\tstring s1 = new string(\"hello\"); // s1 为一个引用\n string s2 = new string(\"hello\"); // s2和s1内容一样\n string s3 = \"world\"; // 放在常量池中\n string s4 = \"world\"; // 从常量池中查找\n string o1 = new object(); // o1 为一个引用\n string o2 = new object(); // o2 为另一个引用\n if (aa == bb) // true 都是常量池的地址\n system.out.println(\"aa==bb\");\n if (s1 == s2) // false,比较地址,s1 s2是两次new出来的对象,地址肯定不同\n system.out.println(\"a==b\");\n if (s1.equals(s2)) // true string的equals方法被重写了 比较的是内容\n system.out.println(\"aeqb\");\n if (o1 == o2) // false object没有重写equals 比较地址\n system.out.println(\"aa==bb\");\n if (o1.equals(o2)) // false 比较地址 object没有重写equals 比较地址\n system.out.println(\"a==b\");\n if (55 == 55l) { // true 基本数据类型 比较的是数值\n system.out.println(\"true\");\n } \n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n", + "headersStr": "Java 中接口和抽象类的区别", + "content": "# Java 中接口和抽象类的区别\n\n 抽象类 接口\n修饰符 abstract interface\n从设计层面来说 是一种模板设计,抽象类要被子类继承 是一种行为的规范,接口要被类实现\n使用场景 在代码实现方面发挥作用,可以实现代码的重用 在系统架构设计方法发挥作用,主要用于定义模块之间的通信契约\n\n抽象类-经典使用场景: StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类\n\nabstract class AbstractStringBuilder implements Appendable, CharSequence {\n /**\n * The value is used for character storage.\n */\n char[] value;\n\n /**\n * The count is the number of characters used.\n */\n int count;\n\n AbstractStringBuilder(int capacity) {\n value = new char[capacity];\n }\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n", + "normalizedContent": "# java 中接口和抽象类的区别\n\n 抽象类 接口\n修饰符 abstract interface\n从设计层面来说 是一种模板设计,抽象类要被子类继承 是一种行为的规范,接口要被类实现\n使用场景 在代码实现方面发挥作用,可以实现代码的重用 在系统架构设计方法发挥作用,主要用于定义模块之间的通信契约\n\n抽象类-经典使用场景: stringbuilder 与 stringbuffer 都继承自 abstractstringbuilder 类\n\nabstract class abstractstringbuilder implements appendable, charsequence {\n /**\n * the value is used for character storage.\n */\n char[] value;\n\n /**\n * the count is the number of characters used.\n */\n int count;\n\n abstractstringbuilder(int capacity) {\n value = new char[capacity];\n }\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n", "charsets": { "cjk": true }, @@ -373,11 +337,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "简述 BIO, NIO, AIO 的区别", + "title": "String,StringBuffer,StringBuilder 之间有什么区别?", "frontmatter": { - "title": "简述 BIO, NIO, AIO 的区别", - "date": "2022-04-04T14:25:08.000Z", - "permalink": "/pages/276740/", + "title": "String,StringBuffer,StringBuilder 之间有什么区别?", + "date": "2022-04-04T14:24:07.000Z", + "permalink": "/pages/211d8c/", "categories": [ "Java相关", "基础" @@ -386,22 +350,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/070.%E7%AE%80%E8%BF%B0%20BIO,%20NIO,%20AIO%20%E7%9A%84%E5%8C%BA%E5%88%AB.html", - "relativePath": "01.Java相关/10.基础/070.简述 BIO, NIO, AIO 的区别.md", - "key": "v-d4e20148", - "path": "/pages/276740/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/050.String%EF%BC%8CStringBuffer%EF%BC%8CStringBuilder%20%E4%B9%8B%E9%97%B4%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F.html", + "relativePath": "01.Java相关/10.基础/050.String,StringBuffer,StringBuilder 之间有什么区别?.md", + "key": "v-7f48b402", + "path": "/pages/211d8c/", "headers": [ { "level": 2, - "title": "简述 BIO, NIO, AIO 的区别", - "slug": "简述-bio-nio-aio-的区别", - "normalizedTitle": "简述 bio, nio, aio 的区别", + "title": "String,StringBuffer,StringBuilder 之间有什么区别?", + "slug": "string-stringbuffer-stringbuilder-之间有什么区别", + "normalizedTitle": "string,stringbuffer,stringbuilder 之间有什么区别?", "charIndex": 2 } ], - "headersStr": "简述 BIO, NIO, AIO 的区别", - "content": "# 简述 BIO, NIO, AIO 的区别\n\nlinux中所有的外部设备都是一个文件,socket也是一个文件,有文件描述符(fd)指向它。\n\n BIO (BLOCKING I/O) NIO (NON-BLOCKING/NEW I/O) AIO (ASYNCHRONOUS I/O)\n描述 在用户进程空间调用recvfrom,直到有数据才读取数据并返回 轮训调用recvfrom,直到有数据 通知内核数据读取操作即可。用户进程不需要等待并读取数据。\n图示 \n使用场景 活动连接数不是特别高(小于单机 1000)的情况 对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发 我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。", - "normalizedContent": "# 简述 bio, nio, aio 的区别\n\nlinux中所有的外部设备都是一个文件,socket也是一个文件,有文件描述符(fd)指向它。\n\n bio (blocking i/o) nio (non-blocking/new i/o) aio (asynchronous i/o)\n描述 在用户进程空间调用recvfrom,直到有数据才读取数据并返回 轮训调用recvfrom,直到有数据 通知内核数据读取操作即可。用户进程不需要等待并读取数据。\n图示 \n使用场景 活动连接数不是特别高(小于单机 1000)的情况 对于高负载、高并发的(网络)应用,应使用 nio 的非阻塞模式来开发 我发现就目前来说 aio 的应用还不是很广泛,netty 之前也尝试使用过 aio,不过又放弃了。", + "headersStr": "String,StringBuffer,StringBuilder 之间有什么区别?", + "content": "# String,StringBuffer,StringBuilder 之间有什么区别?\n\n * String 对象是不可变的。String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[]。\n * StringBuffer,StringBuilder是可变的\n * StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 是线程不安全的。\n\n STRING STRINGBUFFER STRINGBUILDER\n可变性 不可变 可变 可变\n线程安全性 安全 安全 不安全\n使用场景 操作少量数据 多线程操作字符串缓冲区下操作大量数据 单线程操作字符串缓冲区下操作大量数据", + "normalizedContent": "# string,stringbuffer,stringbuilder 之间有什么区别?\n\n * string 对象是不可变的。string 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[]。\n * stringbuffer,stringbuilder是可变的\n * stringbuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。stringbuilder 是线程不安全的。\n\n string stringbuffer stringbuilder\n可变性 不可变 可变 可变\n线程安全性 安全 安全 不安全\n使用场景 操作少量数据 多线程操作字符串缓冲区下操作大量数据 单线程操作字符串缓冲区下操作大量数据", "charsets": { "cjk": true }, @@ -444,6 +408,42 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, + { + "title": "简述 BIO, NIO, AIO 的区别", + "frontmatter": { + "title": "简述 BIO, NIO, AIO 的区别", + "date": "2022-04-04T14:25:08.000Z", + "permalink": "/pages/276740/", + "categories": [ + "Java相关", + "基础" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/070.%E7%AE%80%E8%BF%B0%20BIO,%20NIO,%20AIO%20%E7%9A%84%E5%8C%BA%E5%88%AB.html", + "relativePath": "01.Java相关/10.基础/070.简述 BIO, NIO, AIO 的区别.md", + "key": "v-d4e20148", + "path": "/pages/276740/", + "headers": [ + { + "level": 2, + "title": "简述 BIO, NIO, AIO 的区别", + "slug": "简述-bio-nio-aio-的区别", + "normalizedTitle": "简述 bio, nio, aio 的区别", + "charIndex": 2 + } + ], + "headersStr": "简述 BIO, NIO, AIO 的区别", + "content": "# 简述 BIO, NIO, AIO 的区别\n\nlinux中所有的外部设备都是一个文件,socket也是一个文件,有文件描述符(fd)指向它。\n\n BIO (BLOCKING I/O) NIO (NON-BLOCKING/NEW I/O) AIO (ASYNCHRONOUS I/O)\n描述 在用户进程空间调用recvfrom,直到有数据才读取数据并返回 轮训调用recvfrom,直到有数据 通知内核数据读取操作即可。用户进程不需要等待并读取数据。\n图示 \n使用场景 活动连接数不是特别高(小于单机 1000)的情况 对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发 我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。", + "normalizedContent": "# 简述 bio, nio, aio 的区别\n\nlinux中所有的外部设备都是一个文件,socket也是一个文件,有文件描述符(fd)指向它。\n\n bio (blocking i/o) nio (non-blocking/new i/o) aio (asynchronous i/o)\n描述 在用户进程空间调用recvfrom,直到有数据才读取数据并返回 轮训调用recvfrom,直到有数据 通知内核数据读取操作即可。用户进程不需要等待并读取数据。\n图示 \n使用场景 活动连接数不是特别高(小于单机 1000)的情况 对于高负载、高并发的(网络)应用,应使用 nio 的非阻塞模式来开发 我发现就目前来说 aio 的应用还不是很广泛,netty 之前也尝试使用过 aio,不过又放弃了。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, { "title": "简述 Java 的反射机制及其应用场景", "frontmatter": { @@ -516,6 +516,42 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, + { + "title": "在一个静态方法内调用一个非静态成员为什么是非法的?", + "frontmatter": { + "title": "在一个静态方法内调用一个非静态成员为什么是非法的?", + "date": "2022-04-04T14:25:35.000Z", + "permalink": "/pages/56314c/", + "categories": [ + "Java相关", + "基础" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/080.%E5%9C%A8%E4%B8%80%E4%B8%AA%E9%9D%99%E6%80%81%E6%96%B9%E6%B3%95%E5%86%85%E8%B0%83%E7%94%A8%E4%B8%80%E4%B8%AA%E9%9D%9E%E9%9D%99%E6%80%81%E6%88%90%E5%91%98%E4%B8%BA%E4%BB%80%E4%B9%88%E6%98%AF%E9%9D%9E%E6%B3%95%E7%9A%84%EF%BC%9F.html", + "relativePath": "01.Java相关/10.基础/080.在一个静态方法内调用一个非静态成员为什么是非法的?.md", + "key": "v-d32e07ea", + "path": "/pages/56314c/", + "headers": [ + { + "level": 2, + "title": "在一个静态方法内调用一个非静态成员为什么是非法的?", + "slug": "在一个静态方法内调用一个非静态成员为什么是非法的", + "normalizedTitle": "在一个静态方法内调用一个非静态成员为什么是非法的?", + "charIndex": 2 + } + ], + "headersStr": "在一个静态方法内调用一个非静态成员为什么是非法的?", + "content": "# 在一个静态方法内调用一个非静态成员为什么是非法的?\n\n从java的生命周期角度来说,静态方法的初始化早于非静态方法。\n\n 静态方法 非静态成员\n初始化阶段 准备 使用", + "normalizedContent": "# 在一个静态方法内调用一个非静态成员为什么是非法的?\n\n从java的生命周期角度来说,静态方法的初始化早于非静态方法。\n\n 静态方法 非静态成员\n初始化阶段 准备 使用", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, { "title": "什么是双亲委派机制?", "frontmatter": { @@ -589,11 +625,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "在一个静态方法内调用一个非静态成员为什么是非法的?", + "title": "String 类能不能被继承?为什么?", "frontmatter": { - "title": "在一个静态方法内调用一个非静态成员为什么是非法的?", - "date": "2022-04-04T14:25:35.000Z", - "permalink": "/pages/56314c/", + "title": "String 类能不能被继承?为什么?", + "date": "2022-04-04T14:28:11.000Z", + "permalink": "/pages/73113f/", "categories": [ "Java相关", "基础" @@ -602,22 +638,22 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/080.%E5%9C%A8%E4%B8%80%E4%B8%AA%E9%9D%99%E6%80%81%E6%96%B9%E6%B3%95%E5%86%85%E8%B0%83%E7%94%A8%E4%B8%80%E4%B8%AA%E9%9D%9E%E9%9D%99%E6%80%81%E6%88%90%E5%91%98%E4%B8%BA%E4%BB%80%E4%B9%88%E6%98%AF%E9%9D%9E%E6%B3%95%E7%9A%84%EF%BC%9F.html", - "relativePath": "01.Java相关/10.基础/080.在一个静态方法内调用一个非静态成员为什么是非法的?.md", - "key": "v-d32e07ea", - "path": "/pages/56314c/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/140.String%20%E7%B1%BB%E8%83%BD%E4%B8%8D%E8%83%BD%E8%A2%AB%E7%BB%A7%E6%89%BF%EF%BC%9F%E4%B8%BA%E4%BB%80%E4%B9%88%EF%BC%9F.html", + "relativePath": "01.Java相关/10.基础/140.String 类能不能被继承?为什么?.md", + "key": "v-5290507e", + "path": "/pages/73113f/", "headers": [ { "level": 2, - "title": "在一个静态方法内调用一个非静态成员为什么是非法的?", - "slug": "在一个静态方法内调用一个非静态成员为什么是非法的", - "normalizedTitle": "在一个静态方法内调用一个非静态成员为什么是非法的?", + "title": "String 类能不能被继承?为什么?", + "slug": "string-类能不能被继承-为什么", + "normalizedTitle": "string 类能不能被继承?为什么?", "charIndex": 2 } ], - "headersStr": "在一个静态方法内调用一个非静态成员为什么是非法的?", - "content": "# 在一个静态方法内调用一个非静态成员为什么是非法的?\n\n从java的生命周期角度来说,静态方法的初始化早于非静态方法。\n\n 静态方法 非静态成员\n初始化阶段 准备 使用", - "normalizedContent": "# 在一个静态方法内调用一个非静态成员为什么是非法的?\n\n从java的生命周期角度来说,静态方法的初始化早于非静态方法。\n\n 静态方法 非静态成员\n初始化阶段 准备 使用", + "headersStr": "String 类能不能被继承?为什么?", + "content": "# String 类能不能被继承?为什么?\n\n不可以,String 是被final修饰的。\n\n\npublic final class String implements java.io.Serializable, Comparable, CharSequence {\n // 省略... \n}\n\n\n1\n2\n3\n\n\n> Q:String 为什么是 final 类型?\n> A:被final修饰的类,是不可以被继承的,这样做的目的可以保证该类不被修改", + "normalizedContent": "# string 类能不能被继承?为什么?\n\n不可以,string 是被final修饰的。\n\n\npublic final class string implements java.io.serializable, comparable, charsequence {\n // 省略... \n}\n\n\n1\n2\n3\n\n\n> q:string 为什么是 final 类型?\n> a:被final修饰的类,是不可以被继承的,这样做的目的可以保证该类不被修改", "charsets": { "cjk": true }, @@ -661,47 +697,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "String 类能不能被继承?为什么?", + "title": "Java 缓冲流 buffer 的用途和原理是什么?", "frontmatter": { - "title": "String 类能不能被继承?为什么?", - "date": "2022-04-04T14:28:11.000Z", - "permalink": "/pages/73113f/", - "categories": [ - "Java相关", - "基础" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/140.String%20%E7%B1%BB%E8%83%BD%E4%B8%8D%E8%83%BD%E8%A2%AB%E7%BB%A7%E6%89%BF%EF%BC%9F%E4%B8%BA%E4%BB%80%E4%B9%88%EF%BC%9F.html", - "relativePath": "01.Java相关/10.基础/140.String 类能不能被继承?为什么?.md", - "key": "v-5290507e", - "path": "/pages/73113f/", - "headers": [ - { - "level": 2, - "title": "String 类能不能被继承?为什么?", - "slug": "string-类能不能被继承-为什么", - "normalizedTitle": "string 类能不能被继承?为什么?", - "charIndex": 2 - } - ], - "headersStr": "String 类能不能被继承?为什么?", - "content": "# String 类能不能被继承?为什么?\n\n不可以,String 是被final修饰的。\n\n\npublic final class String implements java.io.Serializable, Comparable, CharSequence {\n // 省略... \n}\n\n\n1\n2\n3\n\n\n> Q:String 为什么是 final 类型?\n> A:被final修饰的类,是不可以被继承的,这样做的目的可以保证该类不被修改", - "normalizedContent": "# string 类能不能被继承?为什么?\n\n不可以,string 是被final修饰的。\n\n\npublic final class string implements java.io.serializable, comparable, charsequence {\n // 省略... \n}\n\n\n1\n2\n3\n\n\n> q:string 为什么是 final 类型?\n> a:被final修饰的类,是不可以被继承的,这样做的目的可以保证该类不被修改", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, - { - "title": "Java 缓冲流 buffer 的用途和原理是什么?", - "frontmatter": { - "title": "Java 缓冲流 buffer 的用途和原理是什么?", - "date": "2022-04-04T14:29:13.000Z", - "permalink": "/pages/115772/", + "title": "Java 缓冲流 buffer 的用途和原理是什么?", + "date": "2022-04-04T14:29:13.000Z", + "permalink": "/pages/115772/", "categories": [ "Java相关", "基础" @@ -768,6 +768,42 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, + { + "title": "阻塞队列都有哪几种,有什么区别?", + "frontmatter": { + "title": "阻塞队列都有哪几种,有什么区别?", + "date": "2022-04-04T14:30:18.000Z", + "permalink": "/pages/e9ce4e/", + "categories": [ + "Java相关", + "基础" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/170.%E9%98%BB%E5%A1%9E%E9%98%9F%E5%88%97%E9%83%BD%E6%9C%89%E5%93%AA%E5%87%A0%E7%A7%8D%EF%BC%8C%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F.html", + "relativePath": "01.Java相关/10.基础/170.阻塞队列都有哪几种,有什么区别?.md", + "key": "v-1218fa85", + "path": "/pages/e9ce4e/", + "headers": [ + { + "level": 2, + "title": "阻塞队列都有哪几种,有什么区别?", + "slug": "阻塞队列都有哪几种-有什么区别", + "normalizedTitle": "阻塞队列都有哪几种,有什么区别?", + "charIndex": 2 + } + ], + "headersStr": "阻塞队列都有哪几种,有什么区别?", + "content": "# 阻塞队列都有哪几种,有什么区别?\n\njava.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:\n\n\nFIFO 队列 :LinkedBlockingQueue、ArrayBlockingQueue(固定长度)\n优先级队列 :PriorityBlockingQueue\n延时队列 : DelayQueue\n同步队列 : SynchronousQueue\n\n\njava.util.concurrent.BlockingQueue提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,直到队列有空闲位置。\n\n\n \nLinkedBlockingQueue 1.内部是用链表实现的\n 2.默认容量是 Integer.MAX_VALUE\n 3.队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能\n 4.会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响\n 5.在无界的情况下,可能会造成内存溢出等问题。\nArrayBlockingQueue 1.ArrayBlockingQueue 是最典型的有界队列\n 2.其内部是用数组存储元素的\n 3.利用 ReentrantLock 实现线程安全,添加操作和移除操作采用的同一个ReenterLock锁\n 4.在创建它的时候就需要指定它的容量,之后也不可以再扩容了\n 5.构造函数中我们同样可以指定是否是公平的\nPriorityBlockingQueue 1.无界阻塞队列\n 2. 支持优先级,可以通过自定义类实现 compareTo() 方法来指定元素排序规则\nDelayQueue 1.无界队列\n 2.放入的元素必须实现 Delayed 接口\n 3.内部使用了 PriorityQueue 的能力来进行排序\nSynchronousQueue 容量为\n 0,所以没有一个地方来暂存元素,导致每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取", + "normalizedContent": "# 阻塞队列都有哪几种,有什么区别?\n\njava.util.concurrent.blockingqueue 接口有以下阻塞队列的实现:\n\n\nfifo 队列 :linkedblockingqueue、arrayblockingqueue(固定长度)\n优先级队列 :priorityblockingqueue\n延时队列 : delayqueue\n同步队列 : synchronousqueue\n\n\njava.util.concurrent.blockingqueue提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,直到队列有空闲位置。\n\n\n \nlinkedblockingqueue 1.内部是用链表实现的\n 2.默认容量是 integer.max_value\n 3.队列中的锁是分离的,其添加采用的是putlock,移除采用的则是takelock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能\n 4.会生成一个额外的node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于gc可能存在较大影响\n 5.在无界的情况下,可能会造成内存溢出等问题。\narrayblockingqueue 1.arrayblockingqueue 是最典型的有界队列\n 2.其内部是用数组存储元素的\n 3.利用 reentrantlock 实现线程安全,添加操作和移除操作采用的同一个reenterlock锁\n 4.在创建它的时候就需要指定它的容量,之后也不可以再扩容了\n 5.构造函数中我们同样可以指定是否是公平的\npriorityblockingqueue 1.无界阻塞队列\n 2. 支持优先级,可以通过自定义类实现 compareto() 方法来指定元素排序规则\ndelayqueue 1.无界队列\n 2.放入的元素必须实现 delayed 接口\n 3.内部使用了 priorityqueue 的能力来进行排序\nsynchronousqueue 容量为\n 0,所以没有一个地方来暂存元素,导致每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, { "title": "简述 HashSet 实现原理", "frontmatter": { @@ -883,35 +919,49 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "阻塞队列都有哪几种,有什么区别?", + "title": "简述 CAS 原理,什么是 ABA 问题,怎么解决?", "frontmatter": { - "title": "阻塞队列都有哪几种,有什么区别?", - "date": "2022-04-04T14:30:18.000Z", - "permalink": "/pages/e9ce4e/", + "title": "简述 CAS 原理,什么是 ABA 问题,怎么解决?", + "date": "2022-04-04T14:32:48.000Z", + "permalink": "/pages/3356c6/", "categories": [ "Java相关", - "基础" + "并发" ], "tags": [ null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/10.%E5%9F%BA%E7%A1%80/170.%E9%98%BB%E5%A1%9E%E9%98%9F%E5%88%97%E9%83%BD%E6%9C%89%E5%93%AA%E5%87%A0%E7%A7%8D%EF%BC%8C%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F.html", - "relativePath": "01.Java相关/10.基础/170.阻塞队列都有哪几种,有什么区别?.md", - "key": "v-1218fa85", - "path": "/pages/e9ce4e/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/020.%E7%AE%80%E8%BF%B0%20CAS%20%E5%8E%9F%E7%90%86%EF%BC%8C%E4%BB%80%E4%B9%88%E6%98%AF%20ABA%20%E9%97%AE%E9%A2%98%EF%BC%8C%E6%80%8E%E4%B9%88%E8%A7%A3%E5%86%B3%EF%BC%9F.html", + "relativePath": "01.Java相关/20.并发/020.简述 CAS 原理,什么是 ABA 问题,怎么解决?.md", + "key": "v-5390e3d4", + "path": "/pages/3356c6/", "headers": [ { "level": 2, - "title": "阻塞队列都有哪几种,有什么区别?", - "slug": "阻塞队列都有哪几种-有什么区别", - "normalizedTitle": "阻塞队列都有哪几种,有什么区别?", + "title": "简述 CAS 原理,什么是 ABA 问题,怎么解决?", + "slug": "简述-cas-原理-什么是-aba-问题-怎么解决", + "normalizedTitle": "简述 cas 原理,什么是 aba 问题,怎么解决?", "charIndex": 2 + }, + { + "level": 3, + "title": "CAS实现原子操作", + "slug": "cas实现原子操作", + "normalizedTitle": "cas实现原子操作", + "charIndex": 33 + }, + { + "level": 3, + "title": "ABA问题", + "slug": "aba问题", + "normalizedTitle": "aba问题", + "charIndex": 256 } ], - "headersStr": "阻塞队列都有哪几种,有什么区别?", - "content": "# 阻塞队列都有哪几种,有什么区别?\n\njava.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:\n\n\nFIFO 队列 :LinkedBlockingQueue、ArrayBlockingQueue(固定长度)\n优先级队列 :PriorityBlockingQueue\n延时队列 : DelayQueue\n同步队列 : SynchronousQueue\n\n\njava.util.concurrent.BlockingQueue提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,直到队列有空闲位置。\n\n\n \nLinkedBlockingQueue 1.内部是用链表实现的\n 2.默认容量是 Integer.MAX_VALUE\n 3.队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能\n 4.会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响\n 5.在无界的情况下,可能会造成内存溢出等问题。\nArrayBlockingQueue 1.ArrayBlockingQueue 是最典型的有界队列\n 2.其内部是用数组存储元素的\n 3.利用 ReentrantLock 实现线程安全,添加操作和移除操作采用的同一个ReenterLock锁\n 4.在创建它的时候就需要指定它的容量,之后也不可以再扩容了\n 5.构造函数中我们同样可以指定是否是公平的\nPriorityBlockingQueue 1.无界阻塞队列\n 2. 支持优先级,可以通过自定义类实现 compareTo() 方法来指定元素排序规则\nDelayQueue 1.无界队列\n 2.放入的元素必须实现 Delayed 接口\n 3.内部使用了 PriorityQueue 的能力来进行排序\nSynchronousQueue 容量为\n 0,所以没有一个地方来暂存元素,导致每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取", - "normalizedContent": "# 阻塞队列都有哪几种,有什么区别?\n\njava.util.concurrent.blockingqueue 接口有以下阻塞队列的实现:\n\n\nfifo 队列 :linkedblockingqueue、arrayblockingqueue(固定长度)\n优先级队列 :priorityblockingqueue\n延时队列 : delayqueue\n同步队列 : synchronousqueue\n\n\njava.util.concurrent.blockingqueue提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,直到队列有空闲位置。\n\n\n \nlinkedblockingqueue 1.内部是用链表实现的\n 2.默认容量是 integer.max_value\n 3.队列中的锁是分离的,其添加采用的是putlock,移除采用的则是takelock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能\n 4.会生成一个额外的node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于gc可能存在较大影响\n 5.在无界的情况下,可能会造成内存溢出等问题。\narrayblockingqueue 1.arrayblockingqueue 是最典型的有界队列\n 2.其内部是用数组存储元素的\n 3.利用 reentrantlock 实现线程安全,添加操作和移除操作采用的同一个reenterlock锁\n 4.在创建它的时候就需要指定它的容量,之后也不可以再扩容了\n 5.构造函数中我们同样可以指定是否是公平的\npriorityblockingqueue 1.无界阻塞队列\n 2. 支持优先级,可以通过自定义类实现 compareto() 方法来指定元素排序规则\ndelayqueue 1.无界队列\n 2.放入的元素必须实现 delayed 接口\n 3.内部使用了 priorityqueue 的能力来进行排序\nsynchronousqueue 容量为\n 0,所以没有一个地方来暂存元素,导致每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取", + "headersStr": "简述 CAS 原理,什么是 ABA 问题,怎么解决? CAS实现原子操作 ABA问题", + "content": "# 简述 CAS 原理,什么是 ABA 问题,怎么解决?\n\n\n# CAS实现原子操作\n\nCAS理论是 juc 包实现的基石,在intel的CPU中,CAS 通过调用本地方法(JNI)使用cmpxchg指令来实现的非阻塞算法。对比于synchronized阻塞算法,基于 CAS 实现的 juc 在性能上有了很大的提升。\n\nCAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。\n\n\n# ABA问题\n\n * 因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。\n\n * ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。\n\n * 从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。", + "normalizedContent": "# 简述 cas 原理,什么是 aba 问题,怎么解决?\n\n\n# cas实现原子操作\n\ncas理论是 juc 包实现的基石,在intel的cpu中,cas 通过调用本地方法(jni)使用cmpxchg指令来实现的非阻塞算法。对比于synchronized阻塞算法,基于 cas 实现的 juc 在性能上有了很大的提升。\n\ncas 操作包含三个操作数 —— 内存位置(v)、预期原值(a)和新值(b)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。\n\n\n# aba问题\n\n * 因为cas需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是a,变成了b,又变成了a,那么使用cas进行检查时会发现它的值没有发生变化,但是实际上却变化了。\n\n * aba问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么a-b-a 就会变成1a-2b-3a。\n\n * 从java1.5开始jdk的atomic包里提供了一个类atomicstampedreference来解决aba问题。这个类的compareandset方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。", "charsets": { "cjk": true }, @@ -1052,56 +1102,6 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, - { - "title": "简述 CAS 原理,什么是 ABA 问题,怎么解决?", - "frontmatter": { - "title": "简述 CAS 原理,什么是 ABA 问题,怎么解决?", - "date": "2022-04-04T14:32:48.000Z", - "permalink": "/pages/3356c6/", - "categories": [ - "Java相关", - "并发" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/020.%E7%AE%80%E8%BF%B0%20CAS%20%E5%8E%9F%E7%90%86%EF%BC%8C%E4%BB%80%E4%B9%88%E6%98%AF%20ABA%20%E9%97%AE%E9%A2%98%EF%BC%8C%E6%80%8E%E4%B9%88%E8%A7%A3%E5%86%B3%EF%BC%9F.html", - "relativePath": "01.Java相关/20.并发/020.简述 CAS 原理,什么是 ABA 问题,怎么解决?.md", - "key": "v-5390e3d4", - "path": "/pages/3356c6/", - "headers": [ - { - "level": 2, - "title": "简述 CAS 原理,什么是 ABA 问题,怎么解决?", - "slug": "简述-cas-原理-什么是-aba-问题-怎么解决", - "normalizedTitle": "简述 cas 原理,什么是 aba 问题,怎么解决?", - "charIndex": 2 - }, - { - "level": 3, - "title": "CAS实现原子操作", - "slug": "cas实现原子操作", - "normalizedTitle": "cas实现原子操作", - "charIndex": 33 - }, - { - "level": 3, - "title": "ABA问题", - "slug": "aba问题", - "normalizedTitle": "aba问题", - "charIndex": 256 - } - ], - "headersStr": "简述 CAS 原理,什么是 ABA 问题,怎么解决? CAS实现原子操作 ABA问题", - "content": "# 简述 CAS 原理,什么是 ABA 问题,怎么解决?\n\n\n# CAS实现原子操作\n\nCAS理论是 juc 包实现的基石,在intel的CPU中,CAS 通过调用本地方法(JNI)使用cmpxchg指令来实现的非阻塞算法。对比于synchronized阻塞算法,基于 CAS 实现的 juc 在性能上有了很大的提升。\n\nCAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。\n\n\n# ABA问题\n\n * 因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。\n\n * ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。\n\n * 从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。", - "normalizedContent": "# 简述 cas 原理,什么是 aba 问题,怎么解决?\n\n\n# cas实现原子操作\n\ncas理论是 juc 包实现的基石,在intel的cpu中,cas 通过调用本地方法(jni)使用cmpxchg指令来实现的非阻塞算法。对比于synchronized阻塞算法,基于 cas 实现的 juc 在性能上有了很大的提升。\n\ncas 操作包含三个操作数 —— 内存位置(v)、预期原值(a)和新值(b)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。\n\n\n# aba问题\n\n * 因为cas需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是a,变成了b,又变成了a,那么使用cas进行检查时会发现它的值没有发生变化,但是实际上却变化了。\n\n * aba问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么a-b-a 就会变成1a-2b-3a。\n\n * 从java1.5开始jdk的atomic包里提供了一个类atomicstampedreference来解决aba问题。这个类的compareandset方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, { "title": "Synchronized 与 Lock 相比优缺点分别是什么?", "frontmatter": { @@ -1153,11 +1153,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "volatile 关键字解决了什么问题,它的实现原理是什么?", + "title": "重入锁是如何实现的?", "frontmatter": { - "title": "volatile 关键字解决了什么问题,它的实现原理是什么?", - "date": "2022-04-04T14:34:07.000Z", - "permalink": "/pages/383264/", + "title": "重入锁是如何实现的?", + "date": "2022-04-04T14:33:48.000Z", + "permalink": "/pages/9b59d1/", "categories": [ "Java相关", "并发" @@ -1166,29 +1166,36 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/060.volatile%20%E5%85%B3%E9%94%AE%E5%AD%97%E8%A7%A3%E5%86%B3%E4%BA%86%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98%EF%BC%8C%E5%AE%83%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", - "relativePath": "01.Java相关/20.并发/060.volatile 关键字解决了什么问题,它的实现原理是什么?.md", - "key": "v-f5bcd180", - "path": "/pages/383264/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/050.%E9%87%8D%E5%85%A5%E9%94%81%E6%98%AF%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E7%9A%84%EF%BC%9F.html", + "relativePath": "01.Java相关/20.并发/050.重入锁是如何实现的?.md", + "key": "v-4927c049", + "path": "/pages/9b59d1/", "headers": [ { "level": 2, - "title": "Java中的内存可见性", - "slug": "java中的内存可见性", - "normalizedTitle": "java中的内存可见性", - "charIndex": 129 + "title": "什么是重入锁?", + "slug": "什么是重入锁", + "normalizedTitle": "什么是重入锁?", + "charIndex": 2 }, { "level": 2, - "title": "Volatile 的实现原理", - "slug": "volatile-的实现原理", - "normalizedTitle": "volatile 的实现原理", - "charIndex": 335 + "title": "ReentrantLock - 重入锁加锁", + "slug": "reentrantlock-重入锁加锁", + "normalizedTitle": "reentrantlock - 重入锁加锁", + "charIndex": 362 + }, + { + "level": 2, + "title": "ReentrantLock - 重入锁解锁", + "slug": "reentrantlock-重入锁解锁", + "normalizedTitle": "reentrantlock - 重入锁解锁", + "charIndex": 1186 } ], - "headersStr": "Java中的内存可见性 Volatile 的实现原理", - "content": "volatile 能保证 Java 中的内存可见性。 可见性的意思是:当线程A修改共享变量的值后,线程B能立刻读到这个修改后的值。\n\n\nVolatile 不会引起线程上下文的切换和调度,如果使用的恰当,会比 Synchronized 执行成本更低。\n\n\n# Java中的内存可见性\n\n * 可见性:一个线程对共享变量值的修改,能够及时被其他线程看到。\n\n * 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那这个变量就是这几个线程的共享变量。\n\n * Java内存的规定:\n\n-线程对共享变量的所有操作都必须在自己的工作内存中进行,不可直接从主内存中读写; -不同线程之间无法直接访问其他线程工作内存中的变量,线程间的变量值的传递需要通过主内存。\n\n\n\n\n# Volatile 的实现原理\n\n如果对用 Volatile 修饰的变量写操作,JVM 会向处理器发出一条 Lock 前缀的指令,Lock 前缀的指令在多核处理器下会引发两件事情:\n\n 1. 将当前处理器缓存行的数据写会到系统内存\n\n 2. 这个写会内存的操作会使其他缓存中的该内存地址的数据无效", - "normalizedContent": "volatile 能保证 java 中的内存可见性。 可见性的意思是:当线程a修改共享变量的值后,线程b能立刻读到这个修改后的值。\n\n\nvolatile 不会引起线程上下文的切换和调度,如果使用的恰当,会比 synchronized 执行成本更低。\n\n\n# java中的内存可见性\n\n * 可见性:一个线程对共享变量值的修改,能够及时被其他线程看到。\n\n * 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那这个变量就是这几个线程的共享变量。\n\n * java内存的规定:\n\n-线程对共享变量的所有操作都必须在自己的工作内存中进行,不可直接从主内存中读写; -不同线程之间无法直接访问其他线程工作内存中的变量,线程间的变量值的传递需要通过主内存。\n\n\n\n\n# volatile 的实现原理\n\n如果对用 volatile 修饰的变量写操作,jvm 会向处理器发出一条 lock 前缀的指令,lock 前缀的指令在多核处理器下会引发两件事情:\n\n 1. 将当前处理器缓存行的数据写会到系统内存\n\n 2. 这个写会内存的操作会使其他缓存中的该内存地址的数据无效", + "headersStr": "什么是重入锁? ReentrantLock - 重入锁加锁 ReentrantLock - 重入锁解锁", + "content": "# 什么是重入锁?\n\nReentrantLock,重入锁,是JDK5中添加在并发包下的一个高性能的工具。顾名思义,ReentrantLock支持同一个线程在未释放锁的情况下重复获取锁。\n\n既然已经有了元老级的synchronized,而且synchronized也支持重入,为什么Doug Lea还要专门写一个ReentrantLock呢?\n\n这是因为:当存在大量线程竞争锁时,多数情况下ReentrantLock的性能优于synchronized。\n\n因为在JDK6中对synchronized做了优化,在锁竞争不激烈的时候,多数情况下锁会停留在偏向锁和轻量级锁阶段,这两个阶段性能是很好的。当存在大量竞争时,可能会膨胀为重量级锁,性能下降,此时的ReentrantLock应该是优于synchronized的。\n\n\n# ReentrantLock - 重入锁加锁\n\n// acquires的值是1\nfinal boolean nonfairTryAcquire(int acquires) {\n // 获取当前线程\n final Thread current = Thread.currentThread();\n // 获取state的值\n int c = getState();\n // 如果state的值等于0,表示当前没有线程持有锁\n // 尝试将state的值改为1,如果修改成功,则成功获取锁,并设置当前线程为持有锁的线程,返回true\n if (c == 0) {\n if (compareAndSetState(0, acquires)) {\n setExclusiveOwnerThread(current);\n return true;\n }\n }\n // state的值不等于0,表示已经有其他线程持有锁\n // 判断当前线程是否等于持有锁的线程,如果等于,将state的值+1,并设置到state上,获取锁成功,返回true\n // 如果不是当前线程,获取锁失败,返回false\n else if (current == getExclusiveOwnerThread()) {\n int nextc = c + acquires;\n if (nextc < 0) // overflow\n throw new Error(\"Maximum lock count exceeded\");\n setState(nextc);\n return true;\n }\n return false;\n}\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n\n\n\n# ReentrantLock - 重入锁解锁\n\npublic void unlock() {\n // 调用AQS的release方法释放资源\n sync.release(1);\n}\n\n\n1\n2\n3\n4\n\n\npublic final boolean release(int arg) {\n // tryRelease也是模板方法,在Sync中实现\n if (tryRelease(arg)) {\n Node h = head;\n if (h != null && h.waitStatus != 0)\n // 成功释放锁后,唤醒同步队列中的下一个节点,使之可以重新竞争锁\n // 注意此时不会唤醒队列第一个节点之后的节点,这些节点此时还是无法竞争锁\n unparkSuccessor(h);\n return true;\n }\n return false;\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\nprotected final boolean tryRelease(int releases) {\n // 将state的值-1,如果-1之后等于0,释放锁成功\n int c = getState() - releases;\n if (Thread.currentThread() != getExclusiveOwnerThread())\n throw new IllegalMonitorStateException();\n boolean free = false;\n if (c == 0) {\n free = true;\n setExclusiveOwnerThread(null);\n }\n setState(c);\n return free;\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n", + "normalizedContent": "# 什么是重入锁?\n\nreentrantlock,重入锁,是jdk5中添加在并发包下的一个高性能的工具。顾名思义,reentrantlock支持同一个线程在未释放锁的情况下重复获取锁。\n\n既然已经有了元老级的synchronized,而且synchronized也支持重入,为什么doug lea还要专门写一个reentrantlock呢?\n\n这是因为:当存在大量线程竞争锁时,多数情况下reentrantlock的性能优于synchronized。\n\n因为在jdk6中对synchronized做了优化,在锁竞争不激烈的时候,多数情况下锁会停留在偏向锁和轻量级锁阶段,这两个阶段性能是很好的。当存在大量竞争时,可能会膨胀为重量级锁,性能下降,此时的reentrantlock应该是优于synchronized的。\n\n\n# reentrantlock - 重入锁加锁\n\n// acquires的值是1\nfinal boolean nonfairtryacquire(int acquires) {\n // 获取当前线程\n final thread current = thread.currentthread();\n // 获取state的值\n int c = getstate();\n // 如果state的值等于0,表示当前没有线程持有锁\n // 尝试将state的值改为1,如果修改成功,则成功获取锁,并设置当前线程为持有锁的线程,返回true\n if (c == 0) {\n if (compareandsetstate(0, acquires)) {\n setexclusiveownerthread(current);\n return true;\n }\n }\n // state的值不等于0,表示已经有其他线程持有锁\n // 判断当前线程是否等于持有锁的线程,如果等于,将state的值+1,并设置到state上,获取锁成功,返回true\n // 如果不是当前线程,获取锁失败,返回false\n else if (current == getexclusiveownerthread()) {\n int nextc = c + acquires;\n if (nextc < 0) // overflow\n throw new error(\"maximum lock count exceeded\");\n setstate(nextc);\n return true;\n }\n return false;\n}\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n\n\n\n# reentrantlock - 重入锁解锁\n\npublic void unlock() {\n // 调用aqs的release方法释放资源\n sync.release(1);\n}\n\n\n1\n2\n3\n4\n\n\npublic final boolean release(int arg) {\n // tryrelease也是模板方法,在sync中实现\n if (tryrelease(arg)) {\n node h = head;\n if (h != null && h.waitstatus != 0)\n // 成功释放锁后,唤醒同步队列中的下一个节点,使之可以重新竞争锁\n // 注意此时不会唤醒队列第一个节点之后的节点,这些节点此时还是无法竞争锁\n unparksuccessor(h);\n return true;\n }\n return false;\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\nprotected final boolean tryrelease(int releases) {\n // 将state的值-1,如果-1之后等于0,释放锁成功\n int c = getstate() - releases;\n if (thread.currentthread() != getexclusiveownerthread())\n throw new illegalmonitorstateexception();\n boolean free = false;\n if (c == 0) {\n free = true;\n setexclusiveownerthread(null);\n }\n setstate(c);\n return free;\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n", "charsets": { "cjk": true }, @@ -1260,11 +1267,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "简述 Java AQS 的原理以及使用场景", + "title": "volatile 关键字解决了什么问题,它的实现原理是什么?", "frontmatter": { - "title": "简述 Java AQS 的原理以及使用场景", - "date": "2022-04-04T14:34:53.000Z", - "permalink": "/pages/52a687/", + "title": "volatile 关键字解决了什么问题,它的实现原理是什么?", + "date": "2022-04-04T14:34:07.000Z", + "permalink": "/pages/383264/", "categories": [ "Java相关", "并发" @@ -1273,38 +1280,117 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/080.%E7%AE%80%E8%BF%B0%20Java%20AQS%20%E7%9A%84%E5%8E%9F%E7%90%86%E4%BB%A5%E5%8F%8A%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF.html", - "relativePath": "01.Java相关/20.并发/080.简述 Java AQS 的原理以及使用场景.md", - "key": "v-2dbdb772", - "path": "/pages/52a687/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/060.volatile%20%E5%85%B3%E9%94%AE%E5%AD%97%E8%A7%A3%E5%86%B3%E4%BA%86%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98%EF%BC%8C%E5%AE%83%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", + "relativePath": "01.Java相关/20.并发/060.volatile 关键字解决了什么问题,它的实现原理是什么?.md", + "key": "v-f5bcd180", + "path": "/pages/383264/", "headers": [ { "level": 2, - "title": "简述 Java AQS 的原理以及使用场景", - "slug": "简述-java-aqs-的原理以及使用场景", - "normalizedTitle": "简述 java aqs 的原理以及使用场景", - "charIndex": 2 - }, - { - "level": 2, - "title": "AbstractQueuedSynchronizer 是什么", - "slug": "abstractqueuedsynchronizer-是什么", - "normalizedTitle": "abstractqueuedsynchronizer 是什么", - "charIndex": 28 - }, - { - "level": 2, - "title": "AbstractQueuedSynchronizer 是如何实现的", - "slug": "abstractqueuedsynchronizer-是如何实现的", - "normalizedTitle": "abstractqueuedsynchronizer 是如何实现的", - "charIndex": 480 - }, - { - "level": 2, - "title": "如何用 AQS 实现一个排他锁", - "slug": "如何用-aqs-实现一个排他锁", - "normalizedTitle": "如何用 aqs 实现一个排他锁", - "charIndex": 1157 + "title": "Java中的内存可见性", + "slug": "java中的内存可见性", + "normalizedTitle": "java中的内存可见性", + "charIndex": 129 + }, + { + "level": 2, + "title": "Volatile 的实现原理", + "slug": "volatile-的实现原理", + "normalizedTitle": "volatile 的实现原理", + "charIndex": 335 + } + ], + "headersStr": "Java中的内存可见性 Volatile 的实现原理", + "content": "volatile 能保证 Java 中的内存可见性。 可见性的意思是:当线程A修改共享变量的值后,线程B能立刻读到这个修改后的值。\n\n\nVolatile 不会引起线程上下文的切换和调度,如果使用的恰当,会比 Synchronized 执行成本更低。\n\n\n# Java中的内存可见性\n\n * 可见性:一个线程对共享变量值的修改,能够及时被其他线程看到。\n\n * 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那这个变量就是这几个线程的共享变量。\n\n * Java内存的规定:\n\n-线程对共享变量的所有操作都必须在自己的工作内存中进行,不可直接从主内存中读写; -不同线程之间无法直接访问其他线程工作内存中的变量,线程间的变量值的传递需要通过主内存。\n\n\n\n\n# Volatile 的实现原理\n\n如果对用 Volatile 修饰的变量写操作,JVM 会向处理器发出一条 Lock 前缀的指令,Lock 前缀的指令在多核处理器下会引发两件事情:\n\n 1. 将当前处理器缓存行的数据写会到系统内存\n\n 2. 这个写会内存的操作会使其他缓存中的该内存地址的数据无效", + "normalizedContent": "volatile 能保证 java 中的内存可见性。 可见性的意思是:当线程a修改共享变量的值后,线程b能立刻读到这个修改后的值。\n\n\nvolatile 不会引起线程上下文的切换和调度,如果使用的恰当,会比 synchronized 执行成本更低。\n\n\n# java中的内存可见性\n\n * 可见性:一个线程对共享变量值的修改,能够及时被其他线程看到。\n\n * 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那这个变量就是这几个线程的共享变量。\n\n * java内存的规定:\n\n-线程对共享变量的所有操作都必须在自己的工作内存中进行,不可直接从主内存中读写; -不同线程之间无法直接访问其他线程工作内存中的变量,线程间的变量值的传递需要通过主内存。\n\n\n\n\n# volatile 的实现原理\n\n如果对用 volatile 修饰的变量写操作,jvm 会向处理器发出一条 lock 前缀的指令,lock 前缀的指令在多核处理器下会引发两件事情:\n\n 1. 将当前处理器缓存行的数据写会到系统内存\n\n 2. 这个写会内存的操作会使其他缓存中的该内存地址的数据无效", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, + { + "title": "什么是公平锁?什么是非公平锁?", + "frontmatter": { + "title": "什么是公平锁?什么是非公平锁?", + "date": "2022-04-04T14:35:15.000Z", + "permalink": "/pages/fd6a59/", + "categories": [ + "Java相关", + "并发" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/090.%E4%BB%80%E4%B9%88%E6%98%AF%E5%85%AC%E5%B9%B3%E9%94%81%EF%BC%9F%E4%BB%80%E4%B9%88%E6%98%AF%E9%9D%9E%E5%85%AC%E5%B9%B3%E9%94%81%EF%BC%9F.html", + "relativePath": "01.Java相关/20.并发/090.什么是公平锁?什么是非公平锁?.md", + "key": "v-134f28dc", + "path": "/pages/fd6a59/", + "headers": [ + { + "level": 2, + "title": "什么是公平锁?什么是非公平锁?", + "slug": "什么是公平锁-什么是非公平锁", + "normalizedTitle": "什么是公平锁?什么是非公平锁?", + "charIndex": 2 + } + ], + "headersStr": "什么是公平锁?什么是非公平锁?", + "content": "# 什么是公平锁?什么是非公平锁?\n\n其中sync是AbstractQueuedSynchronizer的实现,我们知道,ReentrantLock支持公平锁和非公平锁,其实现类分别是FairSync和NonfairSync,我们看看公平锁和非公平锁分别是怎么实现的\n\n// FairSync 公平锁的实现\nfinal void lock() {\n acquire(1);\n}\n\n// NonfairSync 非公平锁的实现\nfinal void lock() {\n if (compareAndSetState(0, 1))\n setExclusiveOwnerThread(Thread.currentThread());\n else\n acquire(1);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n可以看到,非公平锁的实现仅仅是多了一个步骤:通过CAS的方式(compareAndSetState)尝试改变state的状态,修改成功后设置当前线程以独占的方式获取了锁,修改失败执行的逻辑和公平锁一样。\n\n从这段代码中可以看到,独占锁加锁的核心逻辑就是acquire方法,接下来就看看这个方法\n\npublic final void acquire(int arg) {\n if (!tryAcquire(arg) &&\n acquireQueued(addWaiter(Node.EXCLUSIVE), arg))\n selfInterrupt();\n}\n\n\n1\n2\n3\n4\n5\n\n\n这个方法是 AQS 的核心代码。", + "normalizedContent": "# 什么是公平锁?什么是非公平锁?\n\n其中sync是abstractqueuedsynchronizer的实现,我们知道,reentrantlock支持公平锁和非公平锁,其实现类分别是fairsync和nonfairsync,我们看看公平锁和非公平锁分别是怎么实现的\n\n// fairsync 公平锁的实现\nfinal void lock() {\n acquire(1);\n}\n\n// nonfairsync 非公平锁的实现\nfinal void lock() {\n if (compareandsetstate(0, 1))\n setexclusiveownerthread(thread.currentthread());\n else\n acquire(1);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n可以看到,非公平锁的实现仅仅是多了一个步骤:通过cas的方式(compareandsetstate)尝试改变state的状态,修改成功后设置当前线程以独占的方式获取了锁,修改失败执行的逻辑和公平锁一样。\n\n从这段代码中可以看到,独占锁加锁的核心逻辑就是acquire方法,接下来就看看这个方法\n\npublic final void acquire(int arg) {\n if (!tryacquire(arg) &&\n acquirequeued(addwaiter(node.exclusive), arg))\n selfinterrupt();\n}\n\n\n1\n2\n3\n4\n5\n\n\n这个方法是 aqs 的核心代码。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, + { + "title": "简述 Java AQS 的原理以及使用场景", + "frontmatter": { + "title": "简述 Java AQS 的原理以及使用场景", + "date": "2022-04-04T14:34:53.000Z", + "permalink": "/pages/52a687/", + "categories": [ + "Java相关", + "并发" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/080.%E7%AE%80%E8%BF%B0%20Java%20AQS%20%E7%9A%84%E5%8E%9F%E7%90%86%E4%BB%A5%E5%8F%8A%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF.html", + "relativePath": "01.Java相关/20.并发/080.简述 Java AQS 的原理以及使用场景.md", + "key": "v-2dbdb772", + "path": "/pages/52a687/", + "headers": [ + { + "level": 2, + "title": "简述 Java AQS 的原理以及使用场景", + "slug": "简述-java-aqs-的原理以及使用场景", + "normalizedTitle": "简述 java aqs 的原理以及使用场景", + "charIndex": 2 + }, + { + "level": 2, + "title": "AbstractQueuedSynchronizer 是什么", + "slug": "abstractqueuedsynchronizer-是什么", + "normalizedTitle": "abstractqueuedsynchronizer 是什么", + "charIndex": 28 + }, + { + "level": 2, + "title": "AbstractQueuedSynchronizer 是如何实现的", + "slug": "abstractqueuedsynchronizer-是如何实现的", + "normalizedTitle": "abstractqueuedsynchronizer 是如何实现的", + "charIndex": 480 + }, + { + "level": 2, + "title": "如何用 AQS 实现一个排他锁", + "slug": "如何用-aqs-实现一个排他锁", + "normalizedTitle": "如何用 aqs 实现一个排他锁", + "charIndex": 1157 }, { "level": 2, @@ -1372,56 +1458,6 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, - { - "title": "重入锁是如何实现的?", - "frontmatter": { - "title": "重入锁是如何实现的?", - "date": "2022-04-04T14:33:48.000Z", - "permalink": "/pages/9b59d1/", - "categories": [ - "Java相关", - "并发" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/050.%E9%87%8D%E5%85%A5%E9%94%81%E6%98%AF%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E7%9A%84%EF%BC%9F.html", - "relativePath": "01.Java相关/20.并发/050.重入锁是如何实现的?.md", - "key": "v-4927c049", - "path": "/pages/9b59d1/", - "headers": [ - { - "level": 2, - "title": "什么是重入锁?", - "slug": "什么是重入锁", - "normalizedTitle": "什么是重入锁?", - "charIndex": 2 - }, - { - "level": 2, - "title": "ReentrantLock - 重入锁加锁", - "slug": "reentrantlock-重入锁加锁", - "normalizedTitle": "reentrantlock - 重入锁加锁", - "charIndex": 362 - }, - { - "level": 2, - "title": "ReentrantLock - 重入锁解锁", - "slug": "reentrantlock-重入锁解锁", - "normalizedTitle": "reentrantlock - 重入锁解锁", - "charIndex": 1186 - } - ], - "headersStr": "什么是重入锁? ReentrantLock - 重入锁加锁 ReentrantLock - 重入锁解锁", - "content": "# 什么是重入锁?\n\nReentrantLock,重入锁,是JDK5中添加在并发包下的一个高性能的工具。顾名思义,ReentrantLock支持同一个线程在未释放锁的情况下重复获取锁。\n\n既然已经有了元老级的synchronized,而且synchronized也支持重入,为什么Doug Lea还要专门写一个ReentrantLock呢?\n\n这是因为:当存在大量线程竞争锁时,多数情况下ReentrantLock的性能优于synchronized。\n\n因为在JDK6中对synchronized做了优化,在锁竞争不激烈的时候,多数情况下锁会停留在偏向锁和轻量级锁阶段,这两个阶段性能是很好的。当存在大量竞争时,可能会膨胀为重量级锁,性能下降,此时的ReentrantLock应该是优于synchronized的。\n\n\n# ReentrantLock - 重入锁加锁\n\n// acquires的值是1\nfinal boolean nonfairTryAcquire(int acquires) {\n // 获取当前线程\n final Thread current = Thread.currentThread();\n // 获取state的值\n int c = getState();\n // 如果state的值等于0,表示当前没有线程持有锁\n // 尝试将state的值改为1,如果修改成功,则成功获取锁,并设置当前线程为持有锁的线程,返回true\n if (c == 0) {\n if (compareAndSetState(0, acquires)) {\n setExclusiveOwnerThread(current);\n return true;\n }\n }\n // state的值不等于0,表示已经有其他线程持有锁\n // 判断当前线程是否等于持有锁的线程,如果等于,将state的值+1,并设置到state上,获取锁成功,返回true\n // 如果不是当前线程,获取锁失败,返回false\n else if (current == getExclusiveOwnerThread()) {\n int nextc = c + acquires;\n if (nextc < 0) // overflow\n throw new Error(\"Maximum lock count exceeded\");\n setState(nextc);\n return true;\n }\n return false;\n}\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n\n\n\n# ReentrantLock - 重入锁解锁\n\npublic void unlock() {\n // 调用AQS的release方法释放资源\n sync.release(1);\n}\n\n\n1\n2\n3\n4\n\n\npublic final boolean release(int arg) {\n // tryRelease也是模板方法,在Sync中实现\n if (tryRelease(arg)) {\n Node h = head;\n if (h != null && h.waitStatus != 0)\n // 成功释放锁后,唤醒同步队列中的下一个节点,使之可以重新竞争锁\n // 注意此时不会唤醒队列第一个节点之后的节点,这些节点此时还是无法竞争锁\n unparkSuccessor(h);\n return true;\n }\n return false;\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\nprotected final boolean tryRelease(int releases) {\n // 将state的值-1,如果-1之后等于0,释放锁成功\n int c = getState() - releases;\n if (Thread.currentThread() != getExclusiveOwnerThread())\n throw new IllegalMonitorStateException();\n boolean free = false;\n if (c == 0) {\n free = true;\n setExclusiveOwnerThread(null);\n }\n setState(c);\n return free;\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n", - "normalizedContent": "# 什么是重入锁?\n\nreentrantlock,重入锁,是jdk5中添加在并发包下的一个高性能的工具。顾名思义,reentrantlock支持同一个线程在未释放锁的情况下重复获取锁。\n\n既然已经有了元老级的synchronized,而且synchronized也支持重入,为什么doug lea还要专门写一个reentrantlock呢?\n\n这是因为:当存在大量线程竞争锁时,多数情况下reentrantlock的性能优于synchronized。\n\n因为在jdk6中对synchronized做了优化,在锁竞争不激烈的时候,多数情况下锁会停留在偏向锁和轻量级锁阶段,这两个阶段性能是很好的。当存在大量竞争时,可能会膨胀为重量级锁,性能下降,此时的reentrantlock应该是优于synchronized的。\n\n\n# reentrantlock - 重入锁加锁\n\n// acquires的值是1\nfinal boolean nonfairtryacquire(int acquires) {\n // 获取当前线程\n final thread current = thread.currentthread();\n // 获取state的值\n int c = getstate();\n // 如果state的值等于0,表示当前没有线程持有锁\n // 尝试将state的值改为1,如果修改成功,则成功获取锁,并设置当前线程为持有锁的线程,返回true\n if (c == 0) {\n if (compareandsetstate(0, acquires)) {\n setexclusiveownerthread(current);\n return true;\n }\n }\n // state的值不等于0,表示已经有其他线程持有锁\n // 判断当前线程是否等于持有锁的线程,如果等于,将state的值+1,并设置到state上,获取锁成功,返回true\n // 如果不是当前线程,获取锁失败,返回false\n else if (current == getexclusiveownerthread()) {\n int nextc = c + acquires;\n if (nextc < 0) // overflow\n throw new error(\"maximum lock count exceeded\");\n setstate(nextc);\n return true;\n }\n return false;\n}\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n\n\n\n# reentrantlock - 重入锁解锁\n\npublic void unlock() {\n // 调用aqs的release方法释放资源\n sync.release(1);\n}\n\n\n1\n2\n3\n4\n\n\npublic final boolean release(int arg) {\n // tryrelease也是模板方法,在sync中实现\n if (tryrelease(arg)) {\n node h = head;\n if (h != null && h.waitstatus != 0)\n // 成功释放锁后,唤醒同步队列中的下一个节点,使之可以重新竞争锁\n // 注意此时不会唤醒队列第一个节点之后的节点,这些节点此时还是无法竞争锁\n unparksuccessor(h);\n return true;\n }\n return false;\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\nprotected final boolean tryrelease(int releases) {\n // 将state的值-1,如果-1之后等于0,释放锁成功\n int c = getstate() - releases;\n if (thread.currentthread() != getexclusiveownerthread())\n throw new illegalmonitorstateexception();\n boolean free = false;\n if (c == 0) {\n free = true;\n setexclusiveownerthread(null);\n }\n setstate(c);\n return free;\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, { "title": "Java 是如何实现线程安全的,哪些数据结构是线程安全的?", "frontmatter": { @@ -1479,42 +1515,6 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, - { - "title": "什么是公平锁?什么是非公平锁?", - "frontmatter": { - "title": "什么是公平锁?什么是非公平锁?", - "date": "2022-04-04T14:35:15.000Z", - "permalink": "/pages/fd6a59/", - "categories": [ - "Java相关", - "并发" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/090.%E4%BB%80%E4%B9%88%E6%98%AF%E5%85%AC%E5%B9%B3%E9%94%81%EF%BC%9F%E4%BB%80%E4%B9%88%E6%98%AF%E9%9D%9E%E5%85%AC%E5%B9%B3%E9%94%81%EF%BC%9F.html", - "relativePath": "01.Java相关/20.并发/090.什么是公平锁?什么是非公平锁?.md", - "key": "v-134f28dc", - "path": "/pages/fd6a59/", - "headers": [ - { - "level": 2, - "title": "什么是公平锁?什么是非公平锁?", - "slug": "什么是公平锁-什么是非公平锁", - "normalizedTitle": "什么是公平锁?什么是非公平锁?", - "charIndex": 2 - } - ], - "headersStr": "什么是公平锁?什么是非公平锁?", - "content": "# 什么是公平锁?什么是非公平锁?\n\n其中sync是AbstractQueuedSynchronizer的实现,我们知道,ReentrantLock支持公平锁和非公平锁,其实现类分别是FairSync和NonfairSync,我们看看公平锁和非公平锁分别是怎么实现的\n\n// FairSync 公平锁的实现\nfinal void lock() {\n acquire(1);\n}\n\n// NonfairSync 非公平锁的实现\nfinal void lock() {\n if (compareAndSetState(0, 1))\n setExclusiveOwnerThread(Thread.currentThread());\n else\n acquire(1);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n可以看到,非公平锁的实现仅仅是多了一个步骤:通过CAS的方式(compareAndSetState)尝试改变state的状态,修改成功后设置当前线程以独占的方式获取了锁,修改失败执行的逻辑和公平锁一样。\n\n从这段代码中可以看到,独占锁加锁的核心逻辑就是acquire方法,接下来就看看这个方法\n\npublic final void acquire(int arg) {\n if (!tryAcquire(arg) &&\n acquireQueued(addWaiter(Node.EXCLUSIVE), arg))\n selfInterrupt();\n}\n\n\n1\n2\n3\n4\n5\n\n\n这个方法是 AQS 的核心代码。", - "normalizedContent": "# 什么是公平锁?什么是非公平锁?\n\n其中sync是abstractqueuedsynchronizer的实现,我们知道,reentrantlock支持公平锁和非公平锁,其实现类分别是fairsync和nonfairsync,我们看看公平锁和非公平锁分别是怎么实现的\n\n// fairsync 公平锁的实现\nfinal void lock() {\n acquire(1);\n}\n\n// nonfairsync 非公平锁的实现\nfinal void lock() {\n if (compareandsetstate(0, 1))\n setexclusiveownerthread(thread.currentthread());\n else\n acquire(1);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n可以看到,非公平锁的实现仅仅是多了一个步骤:通过cas的方式(compareandsetstate)尝试改变state的状态,修改成功后设置当前线程以独占的方式获取了锁,修改失败执行的逻辑和公平锁一样。\n\n从这段代码中可以看到,独占锁加锁的核心逻辑就是acquire方法,接下来就看看这个方法\n\npublic final void acquire(int arg) {\n if (!tryacquire(arg) &&\n acquirequeued(addwaiter(node.exclusive), arg))\n selfinterrupt();\n}\n\n\n1\n2\n3\n4\n5\n\n\n这个方法是 aqs 的核心代码。", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, { "title": "Java 的线程有哪些状态,转换关系是怎么样的?", "frontmatter": { @@ -1616,11 +1616,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "手写生产者消费者模型", + "title": "Java 线程有哪些常用方法?", "frontmatter": { - "title": "手写生产者消费者模型", - "date": "2022-04-04T14:37:07.000Z", - "permalink": "/pages/2b8c6e/", + "title": "Java 线程有哪些常用方法?", + "date": "2022-04-04T14:36:48.000Z", + "permalink": "/pages/a6c84e/", "categories": [ "Java相关", "并发" @@ -1629,59 +1629,166 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/140.%E6%89%8B%E5%86%99%E7%94%9F%E4%BA%A7%E8%80%85%E6%B6%88%E8%B4%B9%E8%80%85%E6%A8%A1%E5%9E%8B.html", - "relativePath": "01.Java相关/20.并发/140.手写生产者消费者模型.md", - "key": "v-083aef82", - "path": "/pages/2b8c6e/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/130.Java%20%E7%BA%BF%E7%A8%8B%E6%9C%89%E5%93%AA%E4%BA%9B%E5%B8%B8%E7%94%A8%E6%96%B9%E6%B3%95%EF%BC%9F.html", + "relativePath": "01.Java相关/20.并发/130.Java 线程有哪些常用方法?.md", + "key": "v-75c00225", + "path": "/pages/a6c84e/", "headers": [ { "level": 2, - "title": "生产者消费者模式说明:", - "slug": "生产者消费者模式说明", - "normalizedTitle": "生产者消费者模式说明:", + "title": "Java 线程如何启动?如何终止?", + "slug": "java-线程如何启动-如何终止", + "normalizedTitle": "java 线程如何启动?如何终止?", "charIndex": 2 }, { "level": 2, - "title": "实现的关键:", - "slug": "实现的关键", - "normalizedTitle": "实现的关键:", - "charIndex": 78 + "title": "构造线程", + "slug": "构造线程", + "normalizedTitle": "构造线程", + "charIndex": 97 }, { "level": 2, - "title": "代码实现:", - "slug": "代码实现", - "normalizedTitle": "代码实现:", - "charIndex": 187 - }, - { - "level": 3, - "title": "仓库代码", - "slug": "仓库代码", - "normalizedTitle": "仓库代码", - "charIndex": 226 + "title": "启动线程", + "slug": "启动线程", + "normalizedTitle": "启动线程", + "charIndex": 637 }, { - "level": 3, - "title": "产品", - "slug": "产品", - "normalizedTitle": "产品", - "charIndex": 207 + "level": 2, + "title": "理解中断", + "slug": "理解中断", + "normalizedTitle": "理解中断", + "charIndex": 1259 }, { - "level": 3, - "title": "生产者", - "slug": "生产者", - "normalizedTitle": "生产者", - "charIndex": 2 + "level": 2, + "title": "已不建议使用的方法:suspend(),resume(),stop()", + "slug": "已不建议使用的方法-suspend-resume-stop", + "normalizedTitle": "已不建议使用的方法:suspend(),resume(),stop()", + "charIndex": 1581 }, { - "level": 3, - "title": "消费者", - "slug": "消费者", - "normalizedTitle": "消费者", - "charIndex": 5 + "level": 2, + "title": "安全的终止线程", + "slug": "安全的终止线程", + "normalizedTitle": "安全的终止线程", + "charIndex": 1813 + } + ], + "headersStr": "Java 线程如何启动?如何终止? 构造线程 启动线程 理解中断 已不建议使用的方法:suspend(),resume(),stop() 安全的终止线程", + "content": "# Java 线程如何启动?如何终止?\n\n线程随着start()方法的调用而启动,随着run()方法调用结束而终止。 但是这其中还会涉及到线程的构造、中断、暂停、恢复,我们来一一看下。\n\n\n# 构造线程\n\n在运行一个线程之前 肯定要先构造一个线程对象,构造线程对象需要提供所需属性:如线程所属组,线程优先级,是否deamon等。\n\nprivate void init(ThreadGroup g, Runnable target, String name,\n long stackSize, AccessControlContext acc) {\n //1.设置线程名\n if (name == null) {\n throw new NullPointerException(\"name cannot be null\");\n }\n\n this.name = name;\n //2.设置线程的线程组,如果未指定,则此线程的线程组为当前初始化线程的线程组\n Thread parent = currentThread();\n ...\n if (g == null) {\n //这里有一段注释说,安卓不支持SecurityManager,所以删除了相关代码\n g = parent.getThreadGroup();\n }\n\n //所属线程组未启动线程计数+1\n g.addUnstarted();\n\n this.group = g;\n //3.设置线程的其他属性\n //线程是否是守护线程以及优先级都继承自当前线程\n this.daemon = parent.isDaemon();\n this.priority = parent.getPriority();\n this.target = target;\n //4.调用init2 执行剩余初始化操作\n init2(parent);\n //5.设置线程的堆栈大小。但是否生效要看虚拟机。那么在art虚拟机上是否生效呢,我们后续再说\n /* Stash the specified stack size in case the VM cares */\n this.stackSize = stackSize;\n\n //6.设置当前线程id\n tid = nextThreadID();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n\n\n到这里为止,我们的thread就已经初始化完毕了。在堆内存中等待这被执行start()方法开始运行。\n\n\n# 启动线程\n\n调用start()方法开始启动线程。\n\n\n# 理解中断\n\n如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。\n\n中断可以理解为线程的一个标志位属性,调用interrupt()方法中断线程仅仅是把中断标志位置为true,线程可以通过检查自身的标志位来判断是否被中断了。\n\n另外还有两点需要注意的:\n\n * 如果线程处于终结状态,即使线程被中断过,其中断标识位依然是false\n * 许多声明抛出InterruptException的方法(如Thread.sleep(long millis)),在抛出InterruptException之前会将中断标识位置为false。\n\n\n# 已不建议使用的方法:suspend(),resume(),stop()\n\nsuspend(),resume(),stop()完成了线程的暂停、恢复、停止。但是这些方法过期了 不建议继续使用,原因是:\n\n 1. suspend()暂停线程是不会释放资源(比如锁),而是占着资源入睡 ,这样容易引发死锁\n 2. stop() 终止线程时没有给线程清理资源的机会,导致程序运行在不确定状态\n\n取而代之的是wait()和notify()/notifyAll()。\n\n\n# 安全的终止线程\n\n可以通过interrupt()或者标识位来优雅的停止线程。\n\npublic class MyRunner implements Runnable {\n private volatile boolean on = true;\n public void run() {\n while(on){\n System.out.println(\"Running...\");\n }\n System.out.println(\"Stoped!\");\n }\n\n public void cancel(){\n on = false;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n通过标识位来停止线程\n\npublic class ShutdownThread {\n\n public static void main(String[] args) {\n Thread t1 = new Thread(new MyRunner().start();\n t1.cancel();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n通过interrupt()来停止线程\n\npublic class ShutdownThread {\n\n public static void main(String[] args) {\n Thread t1 = new Thread(new MyRunner().start();\n t1.interrupt();\n }\n}\n\n1\n2\n3\n4\n5\n6\n", + "normalizedContent": "# java 线程如何启动?如何终止?\n\n线程随着start()方法的调用而启动,随着run()方法调用结束而终止。 但是这其中还会涉及到线程的构造、中断、暂停、恢复,我们来一一看下。\n\n\n# 构造线程\n\n在运行一个线程之前 肯定要先构造一个线程对象,构造线程对象需要提供所需属性:如线程所属组,线程优先级,是否deamon等。\n\nprivate void init(threadgroup g, runnable target, string name,\n long stacksize, accesscontrolcontext acc) {\n //1.设置线程名\n if (name == null) {\n throw new nullpointerexception(\"name cannot be null\");\n }\n\n this.name = name;\n //2.设置线程的线程组,如果未指定,则此线程的线程组为当前初始化线程的线程组\n thread parent = currentthread();\n ...\n if (g == null) {\n //这里有一段注释说,安卓不支持securitymanager,所以删除了相关代码\n g = parent.getthreadgroup();\n }\n\n //所属线程组未启动线程计数+1\n g.addunstarted();\n\n this.group = g;\n //3.设置线程的其他属性\n //线程是否是守护线程以及优先级都继承自当前线程\n this.daemon = parent.isdaemon();\n this.priority = parent.getpriority();\n this.target = target;\n //4.调用init2 执行剩余初始化操作\n init2(parent);\n //5.设置线程的堆栈大小。但是否生效要看虚拟机。那么在art虚拟机上是否生效呢,我们后续再说\n /* stash the specified stack size in case the vm cares */\n this.stacksize = stacksize;\n\n //6.设置当前线程id\n tid = nextthreadid();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n\n\n到这里为止,我们的thread就已经初始化完毕了。在堆内存中等待这被执行start()方法开始运行。\n\n\n# 启动线程\n\n调用start()方法开始启动线程。\n\n\n# 理解中断\n\n如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。\n\n中断可以理解为线程的一个标志位属性,调用interrupt()方法中断线程仅仅是把中断标志位置为true,线程可以通过检查自身的标志位来判断是否被中断了。\n\n另外还有两点需要注意的:\n\n * 如果线程处于终结状态,即使线程被中断过,其中断标识位依然是false\n * 许多声明抛出interruptexception的方法(如thread.sleep(long millis)),在抛出interruptexception之前会将中断标识位置为false。\n\n\n# 已不建议使用的方法:suspend(),resume(),stop()\n\nsuspend(),resume(),stop()完成了线程的暂停、恢复、停止。但是这些方法过期了 不建议继续使用,原因是:\n\n 1. suspend()暂停线程是不会释放资源(比如锁),而是占着资源入睡 ,这样容易引发死锁\n 2. stop() 终止线程时没有给线程清理资源的机会,导致程序运行在不确定状态\n\n取而代之的是wait()和notify()/notifyall()。\n\n\n# 安全的终止线程\n\n可以通过interrupt()或者标识位来优雅的停止线程。\n\npublic class myrunner implements runnable {\n private volatile boolean on = true;\n public void run() {\n while(on){\n system.out.println(\"running...\");\n }\n system.out.println(\"stoped!\");\n }\n\n public void cancel(){\n on = false;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n通过标识位来停止线程\n\npublic class shutdownthread {\n\n public static void main(string[] args) {\n thread t1 = new thread(new myrunner().start();\n t1.cancel();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n通过interrupt()来停止线程\n\npublic class shutdownthread {\n\n public static void main(string[] args) {\n thread t1 = new thread(new myrunner().start();\n t1.interrupt();\n }\n}\n\n1\n2\n3\n4\n5\n6\n", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, + { + "title": "为什么我们不能直接调用 run() 方法?", + "frontmatter": { + "title": "为什么我们不能直接调用 run() 方法?", + "date": "2022-04-04T14:36:29.000Z", + "permalink": "/pages/13856f/", + "categories": [ + "Java相关", + "并发" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/120.%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E4%BB%AC%E4%B8%8D%E8%83%BD%E7%9B%B4%E6%8E%A5%E8%B0%83%E7%94%A8%20run()%20%E6%96%B9%E6%B3%95%EF%BC%9F.html", + "relativePath": "01.Java相关/20.并发/120.为什么我们不能直接调用 run() 方法?.md", + "key": "v-9baaad8e", + "path": "/pages/13856f/", + "headers": [ + { + "level": 2, + "title": "为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?", + "slug": "为什么我们调用-start-方法时会执行-run-方法-为什么我们不能直接调用-run-方法", + "normalizedTitle": "为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?", + "charIndex": null + } + ], + "headersStr": "为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?", + "content": "# 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run()方法?\n\n如果我们直接调用 run() 方法 是不会创建新线程的。\nstart() 源代码:\n\n/**\n * Causes this thread to begin execution; the Java Virtual Machine\n * calls the run method of this thread.\n *

线程被执行,JVM调用run方法\n * The result is that two threads are running concurrently: the\n * current thread (which returns from the call to the\n * start method) and the other thread (which executes its\n * run method).\n *

\n * It is never legal to start a thread more than once.多次调用start方法启动一个线程是非法的\n * In particular, a thread may not be restarted once it has completed\n * execution.\n *\n * @exception IllegalThreadStateException if the thread was already已经启动的线程再次start,异常\n * started.\n * @see #run()\n * @see #stop()\n */\n public synchronized void start() {\n /**\n * This method is not invoked for the main method thread or \"system\"\n * group threads created/set up by the VM. Any new functionality added\n * to this method in the future may have to also be added to the VM.\n *\n * A zero status value corresponds to state \"NEW\".\n */\n if (threadStatus != 0)//状态校验 0:NEW 新建状态\n throw new IllegalThreadStateException();\n\n /* Notify the group that this thread is about to be started\n * so that it can be added to the group's list of threads\n * and the group's unstarted count can be decremented. */\n group.add(this);//添加进线程组\n\n boolean started = false;\n try {\n start0();//调用native方法执行线程run方法\n started = true;\n } finally {\n try {\n if (!started) {\n group.threadStartFailed(this);//启动失败,从线程组中移除当前前程。\n }\n } catch (Throwable ignore) {\n /* do nothing. If start0 threw a Throwable then\n it will be passed up the call stack */\n }\n }\n }\n\n private native void start0();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n\n\n通过源码我们可以看到,在start()中调用了native方法 start0(),在start0()中会创建一个新的线程调用run()。 所以,如果我们直接调用 run() 方法 是不会创建新线程的。", + "normalizedContent": "# 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run()方法?\n\n如果我们直接调用 run() 方法 是不会创建新线程的。\nstart() 源代码:\n\n/**\n * causes this thread to begin execution; the java virtual machine\n * calls the run method of this thread.\n *

线程被执行,jvm调用run方法\n * the result is that two threads are running concurrently: the\n * current thread (which returns from the call to the\n * start method) and the other thread (which executes its\n * run method).\n *

\n * it is never legal to start a thread more than once.多次调用start方法启动一个线程是非法的\n * in particular, a thread may not be restarted once it has completed\n * execution.\n *\n * @exception illegalthreadstateexception if the thread was already已经启动的线程再次start,异常\n * started.\n * @see #run()\n * @see #stop()\n */\n public synchronized void start() {\n /**\n * this method is not invoked for the main method thread or \"system\"\n * group threads created/set up by the vm. any new functionality added\n * to this method in the future may have to also be added to the vm.\n *\n * a zero status value corresponds to state \"new\".\n */\n if (threadstatus != 0)//状态校验 0:new 新建状态\n throw new illegalthreadstateexception();\n\n /* notify the group that this thread is about to be started\n * so that it can be added to the group's list of threads\n * and the group's unstarted count can be decremented. */\n group.add(this);//添加进线程组\n\n boolean started = false;\n try {\n start0();//调用native方法执行线程run方法\n started = true;\n } finally {\n try {\n if (!started) {\n group.threadstartfailed(this);//启动失败,从线程组中移除当前前程。\n }\n } catch (throwable ignore) {\n /* do nothing. if start0 threw a throwable then\n it will be passed up the call stack */\n }\n }\n }\n\n private native void start0();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n\n\n通过源码我们可以看到,在start()中调用了native方法 start0(),在start0()中会创建一个新的线程调用run()。 所以,如果我们直接调用 run() 方法 是不会创建新线程的。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, + { + "title": "手写生产者消费者模型", + "frontmatter": { + "title": "手写生产者消费者模型", + "date": "2022-04-04T14:37:07.000Z", + "permalink": "/pages/2b8c6e/", + "categories": [ + "Java相关", + "并发" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/140.%E6%89%8B%E5%86%99%E7%94%9F%E4%BA%A7%E8%80%85%E6%B6%88%E8%B4%B9%E8%80%85%E6%A8%A1%E5%9E%8B.html", + "relativePath": "01.Java相关/20.并发/140.手写生产者消费者模型.md", + "key": "v-083aef82", + "path": "/pages/2b8c6e/", + "headers": [ + { + "level": 2, + "title": "生产者消费者模式说明:", + "slug": "生产者消费者模式说明", + "normalizedTitle": "生产者消费者模式说明:", + "charIndex": 2 + }, + { + "level": 2, + "title": "实现的关键:", + "slug": "实现的关键", + "normalizedTitle": "实现的关键:", + "charIndex": 78 + }, + { + "level": 2, + "title": "代码实现:", + "slug": "代码实现", + "normalizedTitle": "代码实现:", + "charIndex": 187 + }, + { + "level": 3, + "title": "仓库代码", + "slug": "仓库代码", + "normalizedTitle": "仓库代码", + "charIndex": 226 + }, + { + "level": 3, + "title": "产品", + "slug": "产品", + "normalizedTitle": "产品", + "charIndex": 207 + }, + { + "level": 3, + "title": "生产者", + "slug": "生产者", + "normalizedTitle": "生产者", + "charIndex": 2 + }, + { + "level": 3, + "title": "消费者", + "slug": "消费者", + "normalizedTitle": "消费者", + "charIndex": 5 }, { "level": 3, @@ -1743,75 +1850,68 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "Java 线程有哪些常用方法?", + "title": "Java 中垃圾回收机制中如何判断对象需要回收?", "frontmatter": { - "title": "Java 线程有哪些常用方法?", - "date": "2022-04-04T14:36:48.000Z", - "permalink": "/pages/a6c84e/", + "title": "Java 中垃圾回收机制中如何判断对象需要回收?", + "date": "2022-05-21T17:31:41.000Z", + "permalink": "/pages/948850/", "categories": [ "Java相关", - "并发" + "JVM" ], "tags": [ null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/130.Java%20%E7%BA%BF%E7%A8%8B%E6%9C%89%E5%93%AA%E4%BA%9B%E5%B8%B8%E7%94%A8%E6%96%B9%E6%B3%95%EF%BC%9F.html", - "relativePath": "01.Java相关/20.并发/130.Java 线程有哪些常用方法?.md", - "key": "v-75c00225", - "path": "/pages/a6c84e/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/001.Java%20%E4%B8%AD%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6%E4%B8%AD%E5%A6%82%E4%BD%95%E5%88%A4%E6%96%AD%E5%AF%B9%E8%B1%A1%E9%9C%80%E8%A6%81%E5%9B%9E%E6%94%B6%EF%BC%9F.html", + "relativePath": "01.Java相关/30.JVM/001.Java 中垃圾回收机制中如何判断对象需要回收?.md", + "key": "v-7cd97c82", + "path": "/pages/948850/", "headers": [ { "level": 2, - "title": "Java 线程如何启动?如何终止?", - "slug": "java-线程如何启动-如何终止", - "normalizedTitle": "java 线程如何启动?如何终止?", - "charIndex": 2 - }, - { - "level": 2, - "title": "构造线程", - "slug": "构造线程", - "normalizedTitle": "构造线程", - "charIndex": 97 + "title": "引用计数法", + "slug": "引用计数法", + "normalizedTitle": "引用计数法", + "charIndex": 95 }, { "level": 2, - "title": "启动线程", - "slug": "启动线程", - "normalizedTitle": "启动线程", - "charIndex": 637 + "title": "可达性分析算法", + "slug": "可达性分析算法", + "normalizedTitle": "可达性分析算法", + "charIndex": 738 }, { "level": 2, - "title": "理解中断", - "slug": "理解中断", - "normalizedTitle": "理解中断", - "charIndex": 1259 + "title": "引用类型总结", + "slug": "引用类型总结", + "normalizedTitle": "引用类型总结", + "charIndex": 1442 }, { "level": 2, - "title": "已不建议使用的方法:suspend(),resume(),stop()", - "slug": "已不建议使用的方法-suspend-resume-stop", - "normalizedTitle": "已不建议使用的方法:suspend(),resume(),stop()", - "charIndex": 1581 + "title": "如何判断一个常量是废弃常量?", + "slug": "如何判断一个常量是废弃常量", + "normalizedTitle": "如何判断一个常量是废弃常量?", + "charIndex": 2760 }, { "level": 2, - "title": "安全的终止线程", - "slug": "安全的终止线程", - "normalizedTitle": "安全的终止线程", - "charIndex": 1813 + "title": "如何判断一个类是无用的类", + "slug": "如何判断一个类是无用的类", + "normalizedTitle": "如何判断一个类是无用的类", + "charIndex": 2930 } ], - "headersStr": "Java 线程如何启动?如何终止? 构造线程 启动线程 理解中断 已不建议使用的方法:suspend(),resume(),stop() 安全的终止线程", - "content": "# Java 线程如何启动?如何终止?\n\n线程随着start()方法的调用而启动,随着run()方法调用结束而终止。 但是这其中还会涉及到线程的构造、中断、暂停、恢复,我们来一一看下。\n\n\n# 构造线程\n\n在运行一个线程之前 肯定要先构造一个线程对象,构造线程对象需要提供所需属性:如线程所属组,线程优先级,是否deamon等。\n\nprivate void init(ThreadGroup g, Runnable target, String name,\n long stackSize, AccessControlContext acc) {\n //1.设置线程名\n if (name == null) {\n throw new NullPointerException(\"name cannot be null\");\n }\n\n this.name = name;\n //2.设置线程的线程组,如果未指定,则此线程的线程组为当前初始化线程的线程组\n Thread parent = currentThread();\n ...\n if (g == null) {\n //这里有一段注释说,安卓不支持SecurityManager,所以删除了相关代码\n g = parent.getThreadGroup();\n }\n\n //所属线程组未启动线程计数+1\n g.addUnstarted();\n\n this.group = g;\n //3.设置线程的其他属性\n //线程是否是守护线程以及优先级都继承自当前线程\n this.daemon = parent.isDaemon();\n this.priority = parent.getPriority();\n this.target = target;\n //4.调用init2 执行剩余初始化操作\n init2(parent);\n //5.设置线程的堆栈大小。但是否生效要看虚拟机。那么在art虚拟机上是否生效呢,我们后续再说\n /* Stash the specified stack size in case the VM cares */\n this.stackSize = stackSize;\n\n //6.设置当前线程id\n tid = nextThreadID();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n\n\n到这里为止,我们的thread就已经初始化完毕了。在堆内存中等待这被执行start()方法开始运行。\n\n\n# 启动线程\n\n调用start()方法开始启动线程。\n\n\n# 理解中断\n\n如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。\n\n中断可以理解为线程的一个标志位属性,调用interrupt()方法中断线程仅仅是把中断标志位置为true,线程可以通过检查自身的标志位来判断是否被中断了。\n\n另外还有两点需要注意的:\n\n * 如果线程处于终结状态,即使线程被中断过,其中断标识位依然是false\n * 许多声明抛出InterruptException的方法(如Thread.sleep(long millis)),在抛出InterruptException之前会将中断标识位置为false。\n\n\n# 已不建议使用的方法:suspend(),resume(),stop()\n\nsuspend(),resume(),stop()完成了线程的暂停、恢复、停止。但是这些方法过期了 不建议继续使用,原因是:\n\n 1. suspend()暂停线程是不会释放资源(比如锁),而是占着资源入睡 ,这样容易引发死锁\n 2. stop() 终止线程时没有给线程清理资源的机会,导致程序运行在不确定状态\n\n取而代之的是wait()和notify()/notifyAll()。\n\n\n# 安全的终止线程\n\n可以通过interrupt()或者标识位来优雅的停止线程。\n\npublic class MyRunner implements Runnable {\n private volatile boolean on = true;\n public void run() {\n while(on){\n System.out.println(\"Running...\");\n }\n System.out.println(\"Stoped!\");\n }\n\n public void cancel(){\n on = false;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n通过标识位来停止线程\n\npublic class ShutdownThread {\n\n public static void main(String[] args) {\n Thread t1 = new Thread(new MyRunner().start();\n t1.cancel();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n通过interrupt()来停止线程\n\npublic class ShutdownThread {\n\n public static void main(String[] args) {\n Thread t1 = new Thread(new MyRunner().start();\n t1.interrupt();\n }\n}\n\n1\n2\n3\n4\n5\n6\n", - "normalizedContent": "# java 线程如何启动?如何终止?\n\n线程随着start()方法的调用而启动,随着run()方法调用结束而终止。 但是这其中还会涉及到线程的构造、中断、暂停、恢复,我们来一一看下。\n\n\n# 构造线程\n\n在运行一个线程之前 肯定要先构造一个线程对象,构造线程对象需要提供所需属性:如线程所属组,线程优先级,是否deamon等。\n\nprivate void init(threadgroup g, runnable target, string name,\n long stacksize, accesscontrolcontext acc) {\n //1.设置线程名\n if (name == null) {\n throw new nullpointerexception(\"name cannot be null\");\n }\n\n this.name = name;\n //2.设置线程的线程组,如果未指定,则此线程的线程组为当前初始化线程的线程组\n thread parent = currentthread();\n ...\n if (g == null) {\n //这里有一段注释说,安卓不支持securitymanager,所以删除了相关代码\n g = parent.getthreadgroup();\n }\n\n //所属线程组未启动线程计数+1\n g.addunstarted();\n\n this.group = g;\n //3.设置线程的其他属性\n //线程是否是守护线程以及优先级都继承自当前线程\n this.daemon = parent.isdaemon();\n this.priority = parent.getpriority();\n this.target = target;\n //4.调用init2 执行剩余初始化操作\n init2(parent);\n //5.设置线程的堆栈大小。但是否生效要看虚拟机。那么在art虚拟机上是否生效呢,我们后续再说\n /* stash the specified stack size in case the vm cares */\n this.stacksize = stacksize;\n\n //6.设置当前线程id\n tid = nextthreadid();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n\n\n到这里为止,我们的thread就已经初始化完毕了。在堆内存中等待这被执行start()方法开始运行。\n\n\n# 启动线程\n\n调用start()方法开始启动线程。\n\n\n# 理解中断\n\n如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。\n\n中断可以理解为线程的一个标志位属性,调用interrupt()方法中断线程仅仅是把中断标志位置为true,线程可以通过检查自身的标志位来判断是否被中断了。\n\n另外还有两点需要注意的:\n\n * 如果线程处于终结状态,即使线程被中断过,其中断标识位依然是false\n * 许多声明抛出interruptexception的方法(如thread.sleep(long millis)),在抛出interruptexception之前会将中断标识位置为false。\n\n\n# 已不建议使用的方法:suspend(),resume(),stop()\n\nsuspend(),resume(),stop()完成了线程的暂停、恢复、停止。但是这些方法过期了 不建议继续使用,原因是:\n\n 1. suspend()暂停线程是不会释放资源(比如锁),而是占着资源入睡 ,这样容易引发死锁\n 2. stop() 终止线程时没有给线程清理资源的机会,导致程序运行在不确定状态\n\n取而代之的是wait()和notify()/notifyall()。\n\n\n# 安全的终止线程\n\n可以通过interrupt()或者标识位来优雅的停止线程。\n\npublic class myrunner implements runnable {\n private volatile boolean on = true;\n public void run() {\n while(on){\n system.out.println(\"running...\");\n }\n system.out.println(\"stoped!\");\n }\n\n public void cancel(){\n on = false;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n通过标识位来停止线程\n\npublic class shutdownthread {\n\n public static void main(string[] args) {\n thread t1 = new thread(new myrunner().start();\n t1.cancel();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n通过interrupt()来停止线程\n\npublic class shutdownthread {\n\n public static void main(string[] args) {\n thread t1 = new thread(new myrunner().start();\n t1.interrupt();\n }\n}\n\n1\n2\n3\n4\n5\n6\n", + "headersStr": "引用计数法 可达性分析算法 引用类型总结 如何判断一个常量是废弃常量? 如何判断一个类是无用的类", + "content": "Java 中垃圾回收机制中如何判断对象需要回收? 即死亡对象判断方法 堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。\n\n\n# 引用计数法\n\n给对象中添加一个引用计数器:\n\n每当有一个地方引用它,计数器就加 1; 当引用失效,计数器就减 1; 任何时候计数器为 0 的对象就是不可能再被使用的。 这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。\n\n所谓对象之间的相互引用问题,如下面代码所示:除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。\n\npublic class ReferenceCountingGc {\n Object instance = null;\n public static void main(String[] args) {\n ReferenceCountingGc objA = new ReferenceCountingGc();\n ReferenceCountingGc objB = new ReferenceCountingGc();\n objA.instance = objB;\n objB.instance = objA;\n objA = null;\n objB = null;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n\n# 可达性分析算法\n\n这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。\n\n下图中的 Object 6 ~ Object 10 之间虽有引用关系,但它们到 GC Roots 不可达,因此为需要被回收的对象。\n\n哪些对象可以作为 GC Roots 呢?\n\n虚拟机栈(栈帧中的本地变量表)中引用的对象 本地方法栈(Native 方法)中引用的对象 方法区中类静态属性引用的对象 方法区中常量引用的对象 所有被同步锁持有的对象 对象可以被回收,就代表一定会被回收吗?\n\n即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 'finalize' 方法。当对象没有覆盖 'finalize' 方法,或 'finalize' 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。\n\n被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。\n\n> Object 类中的 finalize 方法一直被认为是一个糟糕的设计,成为了 Java 语言的负担,影响了 Java 语言的安全和 GC 的性能。JDK9 版本及后续版本中各个类中的 finalize 方法会被逐渐弃用移除。忘掉它的存在吧!\n\n\n# 引用类型总结\n\n无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。\n\nJDK1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。\n\nJDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)\n\n1.强引用(StrongReference)\n\n以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。\n\n2.软引用(SoftReference)\n\n如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。\n\n软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。\n\n3.弱引用(WeakReference)\n\n如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。\n\n弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。\n\n4.虚引用(PhantomReference)\n\n\"虚引用\"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。\n\n虚引用主要用来跟踪对象被垃圾回收的活动。\n\n虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。\n\n特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。\n\n\n# 如何判断一个常量是废弃常量?\n\n运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?\n\n假如在字符串常量池中存在字符串 \"abc\",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 \"abc\" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,\"abc\" 就会被系统清理出常量池了。\n\n\n# 如何判断一个类是无用的类\n\n方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?\n\n判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :\n\n * 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。\n * 加载该类的 ClassLoader 已经被回收。\n * 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。", + "normalizedContent": "java 中垃圾回收机制中如何判断对象需要回收? 即死亡对象判断方法 堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。\n\n\n# 引用计数法\n\n给对象中添加一个引用计数器:\n\n每当有一个地方引用它,计数器就加 1; 当引用失效,计数器就减 1; 任何时候计数器为 0 的对象就是不可能再被使用的。 这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。\n\n所谓对象之间的相互引用问题,如下面代码所示:除了对象 obja 和 objb 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 gc 回收器回收他们。\n\npublic class referencecountinggc {\n object instance = null;\n public static void main(string[] args) {\n referencecountinggc obja = new referencecountinggc();\n referencecountinggc objb = new referencecountinggc();\n obja.instance = objb;\n objb.instance = obja;\n obja = null;\n objb = null;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n\n# 可达性分析算法\n\n这个算法的基本思想就是通过一系列的称为 “gc roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 gc roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。\n\n下图中的 object 6 ~ object 10 之间虽有引用关系,但它们到 gc roots 不可达,因此为需要被回收的对象。\n\n哪些对象可以作为 gc roots 呢?\n\n虚拟机栈(栈帧中的本地变量表)中引用的对象 本地方法栈(native 方法)中引用的对象 方法区中类静态属性引用的对象 方法区中常量引用的对象 所有被同步锁持有的对象 对象可以被回收,就代表一定会被回收吗?\n\n即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 'finalize' 方法。当对象没有覆盖 'finalize' 方法,或 'finalize' 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。\n\n被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。\n\n> object 类中的 finalize 方法一直被认为是一个糟糕的设计,成为了 java 语言的负担,影响了 java 语言的安全和 gc 的性能。jdk9 版本及后续版本中各个类中的 finalize 方法会被逐渐弃用移除。忘掉它的存在吧!\n\n\n# 引用类型总结\n\n无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。\n\njdk1.2 之前,java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。\n\njdk1.2 以后,java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)\n\n1.强引用(strongreference)\n\n以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,java 虚拟机宁愿抛出 outofmemoryerror 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。\n\n2.软引用(softreference)\n\n如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。\n\n软引用可以和一个引用队列(referencequeue)联合使用,如果软引用所引用的对象被垃圾回收,java 虚拟机就会把这个软引用加入到与之关联的引用队列中。\n\n3.弱引用(weakreference)\n\n如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。\n\n弱引用可以和一个引用队列(referencequeue)联合使用,如果弱引用所引用的对象被垃圾回收,java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。\n\n4.虚引用(phantomreference)\n\n\"虚引用\"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。\n\n虚引用主要用来跟踪对象被垃圾回收的活动。\n\n虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(referencequeue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。\n\n特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 jvm 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(outofmemory)等问题的产生。\n\n\n# 如何判断一个常量是废弃常量?\n\n运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?\n\n假如在字符串常量池中存在字符串 \"abc\",如果当前没有任何 string 对象引用该字符串常量的话,就说明常量 \"abc\" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,\"abc\" 就会被系统清理出常量池了。\n\n\n# 如何判断一个类是无用的类\n\n方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?\n\n判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :\n\n * 该类所有的实例都已经被回收,也就是 java 堆中不存在该类的任何实例。\n * 加载该类的 classloader 已经被回收。\n * 该类对应的 java.lang.class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。", "charsets": { "cjk": true }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 + "lastUpdated": "2022/09/18, 21:28:02", + "lastUpdatedTimestamp": 1663507682000 }, { "title": "ThreadLocal 实现原理是什么?为什么要使用弱引用?", @@ -1913,47 +2013,11 @@ export const siteData = { "lastUpdatedTimestamp": 1652966761000 }, { - "title": "为什么我们不能直接调用 run() 方法?", - "frontmatter": { - "title": "为什么我们不能直接调用 run() 方法?", - "date": "2022-04-04T14:36:29.000Z", - "permalink": "/pages/13856f/", - "categories": [ - "Java相关", - "并发" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/20.%E5%B9%B6%E5%8F%91/120.%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E4%BB%AC%E4%B8%8D%E8%83%BD%E7%9B%B4%E6%8E%A5%E8%B0%83%E7%94%A8%20run()%20%E6%96%B9%E6%B3%95%EF%BC%9F.html", - "relativePath": "01.Java相关/20.并发/120.为什么我们不能直接调用 run() 方法?.md", - "key": "v-9baaad8e", - "path": "/pages/13856f/", - "headers": [ - { - "level": 2, - "title": "为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?", - "slug": "为什么我们调用-start-方法时会执行-run-方法-为什么我们不能直接调用-run-方法", - "normalizedTitle": "为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?", - "charIndex": null - } - ], - "headersStr": "为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?", - "content": "# 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run()方法?\n\n如果我们直接调用 run() 方法 是不会创建新线程的。\nstart() 源代码:\n\n/**\n * Causes this thread to begin execution; the Java Virtual Machine\n * calls the run method of this thread.\n *

线程被执行,JVM调用run方法\n * The result is that two threads are running concurrently: the\n * current thread (which returns from the call to the\n * start method) and the other thread (which executes its\n * run method).\n *

\n * It is never legal to start a thread more than once.多次调用start方法启动一个线程是非法的\n * In particular, a thread may not be restarted once it has completed\n * execution.\n *\n * @exception IllegalThreadStateException if the thread was already已经启动的线程再次start,异常\n * started.\n * @see #run()\n * @see #stop()\n */\n public synchronized void start() {\n /**\n * This method is not invoked for the main method thread or \"system\"\n * group threads created/set up by the VM. Any new functionality added\n * to this method in the future may have to also be added to the VM.\n *\n * A zero status value corresponds to state \"NEW\".\n */\n if (threadStatus != 0)//状态校验 0:NEW 新建状态\n throw new IllegalThreadStateException();\n\n /* Notify the group that this thread is about to be started\n * so that it can be added to the group's list of threads\n * and the group's unstarted count can be decremented. */\n group.add(this);//添加进线程组\n\n boolean started = false;\n try {\n start0();//调用native方法执行线程run方法\n started = true;\n } finally {\n try {\n if (!started) {\n group.threadStartFailed(this);//启动失败,从线程组中移除当前前程。\n }\n } catch (Throwable ignore) {\n /* do nothing. If start0 threw a Throwable then\n it will be passed up the call stack */\n }\n }\n }\n\n private native void start0();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n\n\n通过源码我们可以看到,在start()中调用了native方法 start0(),在start0()中会创建一个新的线程调用run()。 所以,如果我们直接调用 run() 方法 是不会创建新线程的。", - "normalizedContent": "# 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run()方法?\n\n如果我们直接调用 run() 方法 是不会创建新线程的。\nstart() 源代码:\n\n/**\n * causes this thread to begin execution; the java virtual machine\n * calls the run method of this thread.\n *

线程被执行,jvm调用run方法\n * the result is that two threads are running concurrently: the\n * current thread (which returns from the call to the\n * start method) and the other thread (which executes its\n * run method).\n *

\n * it is never legal to start a thread more than once.多次调用start方法启动一个线程是非法的\n * in particular, a thread may not be restarted once it has completed\n * execution.\n *\n * @exception illegalthreadstateexception if the thread was already已经启动的线程再次start,异常\n * started.\n * @see #run()\n * @see #stop()\n */\n public synchronized void start() {\n /**\n * this method is not invoked for the main method thread or \"system\"\n * group threads created/set up by the vm. any new functionality added\n * to this method in the future may have to also be added to the vm.\n *\n * a zero status value corresponds to state \"new\".\n */\n if (threadstatus != 0)//状态校验 0:new 新建状态\n throw new illegalthreadstateexception();\n\n /* notify the group that this thread is about to be started\n * so that it can be added to the group's list of threads\n * and the group's unstarted count can be decremented. */\n group.add(this);//添加进线程组\n\n boolean started = false;\n try {\n start0();//调用native方法执行线程run方法\n started = true;\n } finally {\n try {\n if (!started) {\n group.threadstartfailed(this);//启动失败,从线程组中移除当前前程。\n }\n } catch (throwable ignore) {\n /* do nothing. if start0 threw a throwable then\n it will be passed up the call stack */\n }\n }\n }\n\n private native void start0();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n\n\n通过源码我们可以看到,在start()中调用了native方法 start0(),在start0()中会创建一个新的线程调用run()。 所以,如果我们直接调用 run() 方法 是不会创建新线程的。", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, - { - "title": "Java 中垃圾回收机制中如何判断对象需要回收?", + "title": "Java 中如何进行 GC 调优?", "frontmatter": { - "title": "Java 中垃圾回收机制中如何判断对象需要回收?", - "date": "2022-05-21T17:31:41.000Z", - "permalink": "/pages/948850/", + "title": "Java 中如何进行 GC 调优?", + "date": "2022-05-21T17:31:53.000Z", + "permalink": "/pages/07a030/", "categories": [ "Java相关", "JVM" @@ -1962,201 +2026,17 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/001.Java%20%E4%B8%AD%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6%E4%B8%AD%E5%A6%82%E4%BD%95%E5%88%A4%E6%96%AD%E5%AF%B9%E8%B1%A1%E9%9C%80%E8%A6%81%E5%9B%9E%E6%94%B6%EF%BC%9F.html", - "relativePath": "01.Java相关/30.JVM/001.Java 中垃圾回收机制中如何判断对象需要回收?.md", - "key": "v-7cd97c82", - "path": "/pages/948850/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/020.Java%20%E4%B8%AD%E5%A6%82%E4%BD%95%E8%BF%9B%E8%A1%8C%20GC%20%E8%B0%83%E4%BC%98%EF%BC%9F.html", + "relativePath": "01.Java相关/30.JVM/020.Java 中如何进行 GC 调优?.md", + "key": "v-5474bec0", + "path": "/pages/07a030/", "headers": [ { "level": 2, - "title": "引用计数法", - "slug": "引用计数法", - "normalizedTitle": "引用计数法", - "charIndex": 95 - }, - { - "level": 2, - "title": "可达性分析算法", - "slug": "可达性分析算法", - "normalizedTitle": "可达性分析算法", - "charIndex": 738 - }, - { - "level": 2, - "title": "引用类型总结", - "slug": "引用类型总结", - "normalizedTitle": "引用类型总结", - "charIndex": 1442 - }, - { - "level": 2, - "title": "如何判断一个常量是废弃常量?", - "slug": "如何判断一个常量是废弃常量", - "normalizedTitle": "如何判断一个常量是废弃常量?", - "charIndex": 2760 - }, - { - "level": 2, - "title": "如何判断一个类是无用的类", - "slug": "如何判断一个类是无用的类", - "normalizedTitle": "如何判断一个类是无用的类", - "charIndex": 2930 - } - ], - "headersStr": "引用计数法 可达性分析算法 引用类型总结 如何判断一个常量是废弃常量? 如何判断一个类是无用的类", - "content": "Java 中垃圾回收机制中如何判断对象需要回收? 即死亡对象判断方法 堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。\n\n\n# 引用计数法\n\n给对象中添加一个引用计数器:\n\n每当有一个地方引用它,计数器就加 1; 当引用失效,计数器就减 1; 任何时候计数器为 0 的对象就是不可能再被使用的。 这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。\n\n所谓对象之间的相互引用问题,如下面代码所示:除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。\n\npublic class ReferenceCountingGc {\n Object instance = null;\n public static void main(String[] args) {\n ReferenceCountingGc objA = new ReferenceCountingGc();\n ReferenceCountingGc objB = new ReferenceCountingGc();\n objA.instance = objB;\n objB.instance = objA;\n objA = null;\n objB = null;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n\n# 可达性分析算法\n\n这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。\n\n下图中的 Object 6 ~ Object 10 之间虽有引用关系,但它们到 GC Roots 不可达,因此为需要被回收的对象。\n\n哪些对象可以作为 GC Roots 呢?\n\n虚拟机栈(栈帧中的本地变量表)中引用的对象 本地方法栈(Native 方法)中引用的对象 方法区中类静态属性引用的对象 方法区中常量引用的对象 所有被同步锁持有的对象 对象可以被回收,就代表一定会被回收吗?\n\n即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 'finalize' 方法。当对象没有覆盖 'finalize' 方法,或 'finalize' 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。\n\n被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。\n\n> Object 类中的 finalize 方法一直被认为是一个糟糕的设计,成为了 Java 语言的负担,影响了 Java 语言的安全和 GC 的性能。JDK9 版本及后续版本中各个类中的 finalize 方法会被逐渐弃用移除。忘掉它的存在吧!\n\n\n# 引用类型总结\n\n无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。\n\nJDK1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。\n\nJDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)\n\n1.强引用(StrongReference)\n\n以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。\n\n2.软引用(SoftReference)\n\n如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。\n\n软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。\n\n3.弱引用(WeakReference)\n\n如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。\n\n弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。\n\n4.虚引用(PhantomReference)\n\n\"虚引用\"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。\n\n虚引用主要用来跟踪对象被垃圾回收的活动。\n\n虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。\n\n特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。\n\n\n# 如何判断一个常量是废弃常量?\n\n运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?\n\n假如在字符串常量池中存在字符串 \"abc\",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 \"abc\" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,\"abc\" 就会被系统清理出常量池了。\n\n\n# 如何判断一个类是无用的类\n\n方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?\n\n判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :\n\n * 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。\n * 加载该类的 ClassLoader 已经被回收。\n * 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。", - "normalizedContent": "java 中垃圾回收机制中如何判断对象需要回收? 即死亡对象判断方法 堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。\n\n\n# 引用计数法\n\n给对象中添加一个引用计数器:\n\n每当有一个地方引用它,计数器就加 1; 当引用失效,计数器就减 1; 任何时候计数器为 0 的对象就是不可能再被使用的。 这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。\n\n所谓对象之间的相互引用问题,如下面代码所示:除了对象 obja 和 objb 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 gc 回收器回收他们。\n\npublic class referencecountinggc {\n object instance = null;\n public static void main(string[] args) {\n referencecountinggc obja = new referencecountinggc();\n referencecountinggc objb = new referencecountinggc();\n obja.instance = objb;\n objb.instance = obja;\n obja = null;\n objb = null;\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n\n# 可达性分析算法\n\n这个算法的基本思想就是通过一系列的称为 “gc roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 gc roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。\n\n下图中的 object 6 ~ object 10 之间虽有引用关系,但它们到 gc roots 不可达,因此为需要被回收的对象。\n\n哪些对象可以作为 gc roots 呢?\n\n虚拟机栈(栈帧中的本地变量表)中引用的对象 本地方法栈(native 方法)中引用的对象 方法区中类静态属性引用的对象 方法区中常量引用的对象 所有被同步锁持有的对象 对象可以被回收,就代表一定会被回收吗?\n\n即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 'finalize' 方法。当对象没有覆盖 'finalize' 方法,或 'finalize' 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。\n\n被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。\n\n> object 类中的 finalize 方法一直被认为是一个糟糕的设计,成为了 java 语言的负担,影响了 java 语言的安全和 gc 的性能。jdk9 版本及后续版本中各个类中的 finalize 方法会被逐渐弃用移除。忘掉它的存在吧!\n\n\n# 引用类型总结\n\n无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。\n\njdk1.2 之前,java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。\n\njdk1.2 以后,java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)\n\n1.强引用(strongreference)\n\n以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,java 虚拟机宁愿抛出 outofmemoryerror 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。\n\n2.软引用(softreference)\n\n如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。\n\n软引用可以和一个引用队列(referencequeue)联合使用,如果软引用所引用的对象被垃圾回收,java 虚拟机就会把这个软引用加入到与之关联的引用队列中。\n\n3.弱引用(weakreference)\n\n如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。\n\n弱引用可以和一个引用队列(referencequeue)联合使用,如果弱引用所引用的对象被垃圾回收,java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。\n\n4.虚引用(phantomreference)\n\n\"虚引用\"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。\n\n虚引用主要用来跟踪对象被垃圾回收的活动。\n\n虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(referencequeue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。\n\n特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 jvm 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(outofmemory)等问题的产生。\n\n\n# 如何判断一个常量是废弃常量?\n\n运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?\n\n假如在字符串常量池中存在字符串 \"abc\",如果当前没有任何 string 对象引用该字符串常量的话,就说明常量 \"abc\" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,\"abc\" 就会被系统清理出常量池了。\n\n\n# 如何判断一个类是无用的类\n\n方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?\n\n判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :\n\n * 该类所有的实例都已经被回收,也就是 java 堆中不存在该类的任何实例。\n * 加载该类的 classloader 已经被回收。\n * 该类对应的 java.lang.class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/09/18, 21:28:02", - "lastUpdatedTimestamp": 1663507682000 - }, - { - "title": "常见的 GC 回收算法有哪些?", - "frontmatter": { - "title": "常见的 GC 回收算法有哪些?", - "date": "2022-09-18T19:01:29.000Z", - "permalink": "/pages/9b26e8/", - "categories": [ - "Java相关", - "JVM" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/010.%E5%B8%B8%E8%A7%81%E7%9A%84%20GC%20%E5%9B%9E%E6%94%B6%E7%AE%97%E6%B3%95%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F.html", - "relativePath": "01.Java相关/30.JVM/010.常见的 GC 回收算法有哪些?.md", - "key": "v-af821fc8", - "path": "/pages/9b26e8/", - "headers": [ - { - "level": 2, - "title": "标记-清除算法", - "slug": "标记-清除算法", - "normalizedTitle": "标记-清除算法", - "charIndex": 2 - }, - { - "level": 2, - "title": "标记-复制算法", - "slug": "标记-复制算法", - "normalizedTitle": "标记-复制算法", - "charIndex": 148 - }, - { - "level": 2, - "title": "标记-整理算法", - "slug": "标记-整理算法", - "normalizedTitle": "标记-整理算法", - "charIndex": 281 - }, - { - "level": 2, - "title": "分代收集算法", - "slug": "分代收集算法", - "normalizedTitle": "分代收集算法", - "charIndex": 382 - }, - { - "level": 2, - "title": "垃圾收集器", - "slug": "垃圾收集器", - "normalizedTitle": "垃圾收集器", - "charIndex": 682 - }, - { - "level": 2, - "title": "Serial 收集器", - "slug": "serial-收集器", - "normalizedTitle": "serial 收集器", - "charIndex": 893 - }, - { - "level": 2, - "title": "ParNew 收集器", - "slug": "parnew-收集器", - "normalizedTitle": "parnew 收集器", - "charIndex": 1324 - }, - { - "level": 2, - "title": "Parallel Scavenge 收集器", - "slug": "parallel-scavenge-收集器", - "normalizedTitle": "parallel scavenge 收集器", - "charIndex": 1678 - }, - { - "level": 2, - "title": "Serial Old 收集器", - "slug": "serial-old-收集器", - "normalizedTitle": "serial old 收集器", - "charIndex": 2694 - }, - { - "level": 2, - "title": "Parallel Old 收集器", - "slug": "parallel-old-收集器", - "normalizedTitle": "parallel old 收集器", - "charIndex": 2826 - }, - { - "level": 2, - "title": "CMS 收集器", - "slug": "cms-收集器", - "normalizedTitle": "cms 收集器", - "charIndex": 1503 - }, - { - "level": 2, - "title": "G1 收集器", - "slug": "g1-收集器", - "normalizedTitle": "g1 收集器", - "charIndex": 3638 - }, - { - "level": 2, - "title": "ZGC 收集器", - "slug": "zgc-收集器", - "normalizedTitle": "zgc 收集器", - "charIndex": 4351 - } - ], - "headersStr": "标记-清除算法 标记-复制算法 标记-整理算法 分代收集算法 垃圾收集器 Serial 收集器 ParNew 收集器 Parallel Scavenge 收集器 Serial Old 收集器 Parallel Old 收集器 CMS 收集器 G1 收集器 ZGC 收集器", - "content": "# 标记-清除算法\n\n该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:\n\n效率问题 空间问题(标记清除后会产生大量不连续的碎片)\n\n\n# 标记-复制算法\n\n为了解决效率问题,“标记-复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。\n\n\n# 标记-整理算法\n\n根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。\n\n\n# 分代收集算法\n\n当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。\n\n比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。\n\n延伸面试问题: HotSpot 为什么要分为新生代和老年代?\n\n根据上面的对分代收集算法的介绍回答。\n\n\n# 垃圾收集器\n\n如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。\n\n虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的 HotSpot 虚拟机就不会实现那么多不同的垃圾收集器了。\n\n\n# Serial 收集器\n\nSerial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( \"Stop The World\" ),直到它收集结束。\n\n新生代采用标记-复制算法,老年代采用标记-整理算法。\n\n\n\n虚拟机的设计者们当然知道 Stop The World 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。\n\n但是 Serial 收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。\n\n\n# ParNew 收集器\n\nParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。\n\n新生代采用标记-复制算法,老年代采用标记-整理算法。\n\n\n\n它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。\n\n并行和并发概念补充:\n\n并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。\n\n并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。\n\n\n# Parallel Scavenge 收集器\n\nParallel Scavenge 收集器也是使用标记-复制算法的多线程收集器,它看上去几乎和 ParNew 都一样。 那么它有什么特别之处呢?\n\n-XX:+UseParallelGC\n\n 使用 Parallel 收集器+ 老年代串行\n\n-XX:+UseParallelOldGC\n\n 使用 Parallel 收集器+ 老年代并行\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nParallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。 Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。\n\n新生代采用标记-复制算法,老年代采用标记-整理算法。\n\n\n\n这是 JDK1.8 默认收集器\n\n使用 java -XX:+PrintCommandLineFlags -version 命令查看\n\n-XX:InitialHeapSize=262921408 -XX:MaxHeapSize=4206742528 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC\njava version \"1.8.0_211\"\nJava(TM) SE Runtime Environment (build 1.8.0_211-b12)\nJava HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)\nJDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC 参数,则默认指定了-XX:+UseParallelOldGC,可以使用-XX:-UseParallelOldGC 来禁用该功能\n\n\n1\n2\n3\n4\n5\n\n\n\n# Serial Old 收集器\n\nSerial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。\n\n\n# Parallel Old 收集器\n\nParallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。\n\n\n# CMS 收集器\n\nCMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。\n\nCMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。\n\n从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:\n\n初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ; 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。\n\n从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:\n\n对 CPU 资源敏感; 无法处理浮动垃圾; 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。\n\n\n# G1 收集器\n\nG1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.\n\n被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备以下特点:\n\n并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。 空间整合:与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。 G1 收集器的运作大致分为以下几个步骤:\n\n初始标记 并发标记 最终标记 筛选回收 G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。\n\n\n# ZGC 收集器\n\n与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。\n\n在 ZGC 中出现 Stop The World 的情况会更少! 详情可以看 : 《新一代垃圾回收器 ZGC 的探索与实践》", - "normalizedContent": "# 标记-清除算法\n\n该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:\n\n效率问题 空间问题(标记清除后会产生大量不连续的碎片)\n\n\n# 标记-复制算法\n\n为了解决效率问题,“标记-复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。\n\n\n# 标记-整理算法\n\n根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。\n\n\n# 分代收集算法\n\n当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。\n\n比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。\n\n延伸面试问题: hotspot 为什么要分为新生代和老年代?\n\n根据上面的对分代收集算法的介绍回答。\n\n\n# 垃圾收集器\n\n如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。\n\n虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的 hotspot 虚拟机就不会实现那么多不同的垃圾收集器了。\n\n\n# serial 收集器\n\nserial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( \"stop the world\" ),直到它收集结束。\n\n新生代采用标记-复制算法,老年代采用标记-整理算法。\n\n\n\n虚拟机的设计者们当然知道 stop the world 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。\n\n但是 serial 收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。serial 收集器对于运行在 client 模式下的虚拟机来说是个不错的选择。\n\n\n# parnew 收集器\n\nparnew 收集器其实就是 serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 serial 收集器完全一样。\n\n新生代采用标记-复制算法,老年代采用标记-整理算法。\n\n\n\n它是许多运行在 server 模式下的虚拟机的首要选择,除了 serial 收集器外,只有它能与 cms 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。\n\n并行和并发概念补充:\n\n并行(parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。\n\n并发(concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 cpu 上。\n\n\n# parallel scavenge 收集器\n\nparallel scavenge 收集器也是使用标记-复制算法的多线程收集器,它看上去几乎和 parnew 都一样。 那么它有什么特别之处呢?\n\n-xx:+useparallelgc\n\n 使用 parallel 收集器+ 老年代串行\n\n-xx:+useparalleloldgc\n\n 使用 parallel 收集器+ 老年代并行\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nparallel scavenge 收集器关注点是吞吐量(高效率的利用 cpu)。cms 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 cpu 中用于运行用户代码的时间与 cpu 总消耗时间的比值。 parallel scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 parallel scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。\n\n新生代采用标记-复制算法,老年代采用标记-整理算法。\n\n\n\n这是 jdk1.8 默认收集器\n\n使用 java -xx:+printcommandlineflags -version 命令查看\n\n-xx:initialheapsize=262921408 -xx:maxheapsize=4206742528 -xx:+printcommandlineflags -xx:+usecompressedclasspointers -xx:+usecompressedoops -xx:+useparallelgc\njava version \"1.8.0_211\"\njava(tm) se runtime environment (build 1.8.0_211-b12)\njava hotspot(tm) 64-bit server vm (build 25.211-b12, mixed mode)\njdk1.8 默认使用的是 parallel scavenge + parallel old,如果指定了-xx:+useparallelgc 参数,则默认指定了-xx:+useparalleloldgc,可以使用-xx:-useparalleloldgc 来禁用该功能\n\n\n1\n2\n3\n4\n5\n\n\n\n# serial old 收集器\n\nserial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 jdk1.5 以及以前的版本中与 parallel scavenge 收集器搭配使用,另一种用途是作为 cms 收集器的后备方案。\n\n\n# parallel old 收集器\n\nparallel scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 cpu 资源的场合,都可以优先考虑 parallel scavenge 收集器和 parallel old 收集器。\n\n\n# cms 收集器\n\ncms(concurrent mark sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。\n\ncms(concurrent mark sweep)收集器是 hotspot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。\n\n从名字中的mark sweep这两个词可以看出,cms 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:\n\n初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ; 并发标记: 同时开启 gc 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 gc 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短 并发清除: 开启用户线程,同时 gc 线程开始对未标记的区域做清扫。\n\n从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:\n\n对 cpu 资源敏感; 无法处理浮动垃圾; 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。\n\n\n# g1 收集器\n\ng1 (garbage-first) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 gc 停顿时间要求的同时,还具备高吞吐量性能特征.\n\n被视为 jdk1.7 中 hotspot 虚拟机的一个重要进化特征。它具备以下特点:\n\n并行与并发:g1 能充分利用 cpu、多核环境下的硬件优势,使用多个 cpu(cpu 或者 cpu 核心)来缩短 stop-the-world 停顿时间。部分其他收集器原本需要停顿 java 线程执行的 gc 动作,g1 收集器仍然可以通过并发的方式让 java 程序继续执行。 分代收集:虽然 g1 可以不需要其他收集器配合就能独立管理整个 gc 堆,但是还是保留了分代的概念。 空间整合:与 cms 的“标记-清理”算法不同,g1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。 可预测的停顿:这是 g1 相对于 cms 的另一个大优势,降低停顿时间是 g1 和 cms 共同的关注点,但 g1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 m 毫秒的时间片段内。 g1 收集器的运作大致分为以下几个步骤:\n\n初始标记 并发标记 最终标记 筛选回收 g1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 region(这也就是它的名字 garbage-first 的由来) 。这种使用 region 划分内存空间以及有优先级的区域回收方式,保证了 g1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。\n\n\n# zgc 收集器\n\n与 cms 中的 parnew 和 g1 类似,zgc 也采用标记-复制算法,不过 zgc 对该算法做了重大改进。\n\n在 zgc 中出现 stop the world 的情况会更少! 详情可以看 : 《新一代垃圾回收器 zgc 的探索与实践》", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/09/18, 21:28:02", - "lastUpdatedTimestamp": 1663507682000 - }, - { - "title": "Java 中如何进行 GC 调优?", - "frontmatter": { - "title": "Java 中如何进行 GC 调优?", - "date": "2022-05-21T17:31:53.000Z", - "permalink": "/pages/07a030/", - "categories": [ - "Java相关", - "JVM" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/020.Java%20%E4%B8%AD%E5%A6%82%E4%BD%95%E8%BF%9B%E8%A1%8C%20GC%20%E8%B0%83%E4%BC%98%EF%BC%9F.html", - "relativePath": "01.Java相关/30.JVM/020.Java 中如何进行 GC 调优?.md", - "key": "v-5474bec0", - "path": "/pages/07a030/", - "headers": [ - { - "level": 2, - "title": "GC优化需知", - "slug": "gc优化需知", - "normalizedTitle": "gc优化需知", - "charIndex": 305 + "title": "GC优化需知", + "slug": "gc优化需知", + "normalizedTitle": "gc优化需知", + "charIndex": 305 }, { "level": 2, @@ -2211,11 +2091,11 @@ export const siteData = { "lastUpdatedTimestamp": 1663507682000 }, { - "title": "JVM 内存是如何对应到操作系统内存的?", + "title": "JMM 中内存模型是怎样的?什么是指令序列重排序?", "frontmatter": { - "title": "JVM 内存是如何对应到操作系统内存的?", - "date": "2022-05-21T17:33:05.000Z", - "permalink": "/pages/da65e7/", + "title": "JMM 中内存模型是怎样的?什么是指令序列重排序?", + "date": "2022-05-21T17:32:49.000Z", + "permalink": "/pages/c8e6cb/", "categories": [ "Java相关", "JVM" @@ -2224,43 +2104,50 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/040.JVM%20%E5%86%85%E5%AD%98%E6%98%AF%E5%A6%82%E4%BD%95%E5%AF%B9%E5%BA%94%E5%88%B0%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%86%85%E5%AD%98%E7%9A%84%EF%BC%9F.html", - "relativePath": "01.Java相关/30.JVM/040.JVM 内存是如何对应到操作系统内存的?.md", - "key": "v-2de19204", - "path": "/pages/da65e7/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/030.JMM%20%E4%B8%AD%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B%E6%98%AF%E6%80%8E%E6%A0%B7%E7%9A%84%EF%BC%9F%E4%BB%80%E4%B9%88%E6%98%AF%E6%8C%87%E4%BB%A4%E5%BA%8F%E5%88%97%E9%87%8D%E6%8E%92%E5%BA%8F%EF%BC%9F.html", + "relativePath": "01.Java相关/30.JVM/030.JMM 中内存模型是怎样的?什么是指令序列重排序?.md", + "key": "v-0970cd54", + "path": "/pages/c8e6cb/", "headers": [ { "level": 2, - "title": "Java 内存模型", - "slug": "java-内存模型", - "normalizedTitle": "java 内存模型", - "charIndex": 2 + "title": "为什么要有内存模型?", + "slug": "为什么要有内存模型", + "normalizedTitle": "为什么要有内存模型?", + "charIndex": 40 }, { "level": 2, - "title": "Java 线程与主内存的关系", - "slug": "java-线程与主内存的关系", - "normalizedTitle": "java 线程与主内存的关系", - "charIndex": 216 + "title": "硬件内存架构", + "slug": "硬件内存架构", + "normalizedTitle": "硬件内存架构", + "charIndex": 73 }, { "level": 2, - "title": "线程间通信", - "slug": "线程间通信", - "normalizedTitle": "线程间通信", - "charIndex": 421 + "title": "缓存一致性问题", + "slug": "缓存一致性问题", + "normalizedTitle": "缓存一致性问题", + "charIndex": 557 }, { "level": 2, - "title": "有态度的总结", - "slug": "有态度的总结", - "normalizedTitle": "有态度的总结", - "charIndex": 1043 + "title": "处理器优化和指令重排序", + "slug": "处理器优化和指令重排序", + "normalizedTitle": "处理器优化和指令重排序", + "charIndex": 943 + }, + { + "level": 2, + "title": "并发编程的问题", + "slug": "并发编程的问题", + "normalizedTitle": "并发编程的问题", + "charIndex": 1368 } ], - "headersStr": "Java 内存模型 Java 线程与主内存的关系 线程间通信 有态度的总结", - "content": "# Java 内存模型\n\n同一套内存模型规范,不同语言在实现上可能会有些差别。接下来着重讲一下 Java 内存模型实现原理。\n\nJava 运行时内存区域与硬件内存的关系\n\n了解过 JVM 的同学都知道,JVM 运行时内存区域是分片的,分为栈、堆等,其实这些都是 JVM 定义的逻辑概念。在传统的硬件内存架构中是没有栈和堆这种概念。\n\n\n\n从图中可以看出栈和堆既存在于高速缓存中又存在于主内存中,所以两者并没有很直接的关系。\n\n\n# Java 线程与主内存的关系\n\nJava 内存模型是一种规范,定义了很多东西:\n\n所有的变量都存储在主内存(Main Memory)中。 每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。 不同的线程之间无法直接访问对方本地内存中的变量。 看文字太枯燥了,我又画了一张图:\n\n\n# 线程间通信\n\n如果两个线程都对一个共享变量进行操作,共享变量初始值为 1,每个线程都变量进行加 1,预期共享变量的值为 3。在 JMM 规范下会有一系列的操作。\n\n\n\n为了更好的控制主内存和本地内存的交互,Java 内存模型定义了八种操作来实现:\n\n * lock:锁定。作用于主内存的变量,把一个变量标识为一条线程独占状态。\n * unlock:解锁。作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。\n * read:读取。作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用\n * load:载入。作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。\n * use:使用。作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。\n * assign:赋值。作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。\n * store:存储。作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。\n * write:写入。作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。 注意:工作内存也就是本地内存的意思。\n\n\n# 有态度的总结\n\n由于CPU 和主内存间存在数量级的速率差,想到了引入了多级高速缓存的传统硬件内存架构来解决,多级高速缓存作为 CPU 和主内间的缓冲提升了整体性能。解决了速率差的问题,却又带来了缓存一致性问题。\n\n数据同时存在于高速缓存和主内存中,如果不加以规范势必造成灾难,因此在传统机器上又抽象出了内存模型。\n\nJava 语言在遵循内存模型的基础上推出了 JMM 规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。\n\n为了更精准控制工作内存和主内存间的交互,JMM 还定义了八种操作:lock, unlock, read, load,use,assign, store, write。\n\n好了,今天就给大家介绍到这里,简单总结下Java内存模型的定义:Java内存模型并不是一件容易的事情,这个模型必须定义得足够严谨,才能让Java的并发操作不会产生歧义;但是,也必须得足够宽松,使得虚拟机的实现能有足够的自由空间去利用硬件的各种特性(寄存器、高速缓存等)来获取更好的执行速度。经过长时间的验证和修补,在JDK1.5发布后,Java内存模型就已经成熟和完善起来了。\n\n参考: 全面学习掌握Java内存模型", - "normalizedContent": "# java 内存模型\n\n同一套内存模型规范,不同语言在实现上可能会有些差别。接下来着重讲一下 java 内存模型实现原理。\n\njava 运行时内存区域与硬件内存的关系\n\n了解过 jvm 的同学都知道,jvm 运行时内存区域是分片的,分为栈、堆等,其实这些都是 jvm 定义的逻辑概念。在传统的硬件内存架构中是没有栈和堆这种概念。\n\n\n\n从图中可以看出栈和堆既存在于高速缓存中又存在于主内存中,所以两者并没有很直接的关系。\n\n\n# java 线程与主内存的关系\n\njava 内存模型是一种规范,定义了很多东西:\n\n所有的变量都存储在主内存(main memory)中。 每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。 不同的线程之间无法直接访问对方本地内存中的变量。 看文字太枯燥了,我又画了一张图:\n\n\n# 线程间通信\n\n如果两个线程都对一个共享变量进行操作,共享变量初始值为 1,每个线程都变量进行加 1,预期共享变量的值为 3。在 jmm 规范下会有一系列的操作。\n\n\n\n为了更好的控制主内存和本地内存的交互,java 内存模型定义了八种操作来实现:\n\n * lock:锁定。作用于主内存的变量,把一个变量标识为一条线程独占状态。\n * unlock:解锁。作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。\n * read:读取。作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用\n * load:载入。作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。\n * use:使用。作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。\n * assign:赋值。作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。\n * store:存储。作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。\n * write:写入。作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。 注意:工作内存也就是本地内存的意思。\n\n\n# 有态度的总结\n\n由于cpu 和主内存间存在数量级的速率差,想到了引入了多级高速缓存的传统硬件内存架构来解决,多级高速缓存作为 cpu 和主内间的缓冲提升了整体性能。解决了速率差的问题,却又带来了缓存一致性问题。\n\n数据同时存在于高速缓存和主内存中,如果不加以规范势必造成灾难,因此在传统机器上又抽象出了内存模型。\n\njava 语言在遵循内存模型的基础上推出了 jmm 规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。\n\n为了更精准控制工作内存和主内存间的交互,jmm 还定义了八种操作:lock, unlock, read, load,use,assign, store, write。\n\n好了,今天就给大家介绍到这里,简单总结下java内存模型的定义:java内存模型并不是一件容易的事情,这个模型必须定义得足够严谨,才能让java的并发操作不会产生歧义;但是,也必须得足够宽松,使得虚拟机的实现能有足够的自由空间去利用硬件的各种特性(寄存器、高速缓存等)来获取更好的执行速度。经过长时间的验证和修补,在jdk1.5发布后,java内存模型就已经成熟和完善起来了。\n\n参考: 全面学习掌握java内存模型", + "headersStr": "为什么要有内存模型? 硬件内存架构 缓存一致性问题 处理器优化和指令重排序 并发编程的问题", + "content": "Java内存模型(JMM)和 Java 运行时内存区域是不一样的概念。\n\n\n# 为什么要有内存模型?\n\n要想回答这个问题,我们需要先弄懂传统计算机硬件内存架构。好了,我要开始画图了。\n\n\n# 硬件内存架构\n\n\n\n(1)CPU\n\n去过机房的同学都知道,一般在大型服务器上会配置多个CPU,每个CPU还会有多个\n\n核\n\n,这就意味着多个CPU或者多个核可以同时(并发)工作。如果使用Java 起了一个多线程的任务,很有可能每个 CPU 都会跑一个线程,那么你的任务在某一刻就是真正并发执行了。\n\n(2)CPU Register\n\nCPU Register也就是 CPU 寄存器。CPU 寄存器是 CPU 内部集成的,在寄存器上执行操作的效率要比在主存上高出几个数量级。\n\n(3)CPU Cache Memory\n\nCPU Cache Memory也就是 CPU 高速缓存,相对于寄存器来说,通常也可以成为 L2 二级缓存。相对于硬盘读取速度来说内存读取的效率非常高,但是与 CPU 还是相差数量级,所以在 CPU 和主存间引入了多级缓存,目的是为了做一下缓冲。\n\n(4)Main Memory\n\nMain Memory 就是主存,主存比 L1、L2 缓存要大很多。\n\n注意:部分高端机器还有 L3 三级缓存。\n\n\n# 缓存一致性问题\n\n由于主存与 CPU 处理器的运算能力之间有数量级的差距,所以在传统计算机内存架构中会引入高速缓存来作为主存和处理器之间的缓冲,CPU 将常用的数据放在高速缓存中,运算结束后 CPU 再讲运算结果同步到主存中。\n\n使用高速缓存解决了 CPU 和主存速率不匹配的问题,但同时又引入另外一个新问题:缓存一致性问题。\n\n在多CPU的系统中(或者单CPU多核的系统),每个CPU内核都有自己的高速缓存,它们共享同一主内存(Main Memory)。当多个CPU的运算任务都涉及同一块主内存区域时,CPU 会将数据读取到缓存中进行运算,这可能会导致各自的缓存数据不一致。\n\n因此需要每个 CPU 访问缓存时遵循一定的协议,在读写数据时根据协议进行操作,共同来维护缓存的一致性。这类协议有 MSI、MESI、MOSI、和 Dragon Protocol 等。\n\n\n# 处理器优化和指令重排序\n\n为了提升性能在 CPU 和主内存之间增加了高速缓存,但在多线程并发场景可能会遇到\n\n。那还有没有办法进一步提升 CPU 的执行效率呢?答案是:处理器优化。\n\n为了使处理器内部的运算单元能够最大化被充分利用,处理器会对输入代码进行乱序执行处理,这就是处理器优化。\n\n除了处理器会对代码进行优化处理,很多现代编程语言的编译器也会做类似的优化,比如像 Java 的即时编译器(JIT)会做指令重排序。\n\n处理器优化其实也是重排序的一种类型,这里总结一下,重排序可以分为三种类型: ** 编译器优化的重排序。编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序。 ** 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 ** 内存系统的重排序。由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。\n\n\n# 并发编程的问题\n\n上面讲了一堆硬件相关的东西,有些同学可能会有点懵,绕了这么大圈,这些东西跟 Java 内存模型有啥关系吗?不要急咱们慢慢往下看。\n\n熟悉 Java 并发的同学肯定对这三个问题很熟悉:『可见性问题』、『原子性问题』、『有序性问题』。如果从更深层次看这三个问题,其实就是上面讲的『缓存一致性』、『处理器优化』、『指令重排序』造成的。\n\n缓存一致性问题其实就是可见性问题,处理器优化可能会造成原子性问题,指令重排序会造成有序性问题,你看是不是都联系上了。\n\n出了问题总是要解决的,那有什么办法呢?首先想到简单粗暴的办法,干掉缓存让 CPU 直接与主内存交互就解决了可见性问题,禁止处理器优化和指令重排序就解决了原子性和有序性问题,但这样一夜回到解放前了,显然不可取。\n\n所以技术前辈们想到了在物理机器上定义出一套内存模型, 规范内存的读写操作。内存模型解决并发问题主要采用两种方式:\n\n限制处理器优化和使用内存屏障。\n\n参考: 全面学习掌握Java内存模型", + "normalizedContent": "java内存模型(jmm)和 java 运行时内存区域是不一样的概念。\n\n\n# 为什么要有内存模型?\n\n要想回答这个问题,我们需要先弄懂传统计算机硬件内存架构。好了,我要开始画图了。\n\n\n# 硬件内存架构\n\n\n\n(1)cpu\n\n去过机房的同学都知道,一般在大型服务器上会配置多个cpu,每个cpu还会有多个\n\n核\n\n,这就意味着多个cpu或者多个核可以同时(并发)工作。如果使用java 起了一个多线程的任务,很有可能每个 cpu 都会跑一个线程,那么你的任务在某一刻就是真正并发执行了。\n\n(2)cpu register\n\ncpu register也就是 cpu 寄存器。cpu 寄存器是 cpu 内部集成的,在寄存器上执行操作的效率要比在主存上高出几个数量级。\n\n(3)cpu cache memory\n\ncpu cache memory也就是 cpu 高速缓存,相对于寄存器来说,通常也可以成为 l2 二级缓存。相对于硬盘读取速度来说内存读取的效率非常高,但是与 cpu 还是相差数量级,所以在 cpu 和主存间引入了多级缓存,目的是为了做一下缓冲。\n\n(4)main memory\n\nmain memory 就是主存,主存比 l1、l2 缓存要大很多。\n\n注意:部分高端机器还有 l3 三级缓存。\n\n\n# 缓存一致性问题\n\n由于主存与 cpu 处理器的运算能力之间有数量级的差距,所以在传统计算机内存架构中会引入高速缓存来作为主存和处理器之间的缓冲,cpu 将常用的数据放在高速缓存中,运算结束后 cpu 再讲运算结果同步到主存中。\n\n使用高速缓存解决了 cpu 和主存速率不匹配的问题,但同时又引入另外一个新问题:缓存一致性问题。\n\n在多cpu的系统中(或者单cpu多核的系统),每个cpu内核都有自己的高速缓存,它们共享同一主内存(main memory)。当多个cpu的运算任务都涉及同一块主内存区域时,cpu 会将数据读取到缓存中进行运算,这可能会导致各自的缓存数据不一致。\n\n因此需要每个 cpu 访问缓存时遵循一定的协议,在读写数据时根据协议进行操作,共同来维护缓存的一致性。这类协议有 msi、mesi、mosi、和 dragon protocol 等。\n\n\n# 处理器优化和指令重排序\n\n为了提升性能在 cpu 和主内存之间增加了高速缓存,但在多线程并发场景可能会遇到\n\n。那还有没有办法进一步提升 cpu 的执行效率呢?答案是:处理器优化。\n\n为了使处理器内部的运算单元能够最大化被充分利用,处理器会对输入代码进行乱序执行处理,这就是处理器优化。\n\n除了处理器会对代码进行优化处理,很多现代编程语言的编译器也会做类似的优化,比如像 java 的即时编译器(jit)会做指令重排序。\n\n处理器优化其实也是重排序的一种类型,这里总结一下,重排序可以分为三种类型: ** 编译器优化的重排序。编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序。 ** 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 ** 内存系统的重排序。由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。\n\n\n# 并发编程的问题\n\n上面讲了一堆硬件相关的东西,有些同学可能会有点懵,绕了这么大圈,这些东西跟 java 内存模型有啥关系吗?不要急咱们慢慢往下看。\n\n熟悉 java 并发的同学肯定对这三个问题很熟悉:『可见性问题』、『原子性问题』、『有序性问题』。如果从更深层次看这三个问题,其实就是上面讲的『缓存一致性』、『处理器优化』、『指令重排序』造成的。\n\n缓存一致性问题其实就是可见性问题,处理器优化可能会造成原子性问题,指令重排序会造成有序性问题,你看是不是都联系上了。\n\n出了问题总是要解决的,那有什么办法呢?首先想到简单粗暴的办法,干掉缓存让 cpu 直接与主内存交互就解决了可见性问题,禁止处理器优化和指令重排序就解决了原子性和有序性问题,但这样一夜回到解放前了,显然不可取。\n\n所以技术前辈们想到了在物理机器上定义出一套内存模型, 规范内存的读写操作。内存模型解决并发问题主要采用两种方式:\n\n限制处理器优化和使用内存屏障。\n\n参考: 全面学习掌握java内存模型", "charsets": { "cjk": true }, @@ -2325,11 +2212,11 @@ export const siteData = { "lastUpdatedTimestamp": 1663507682000 }, { - "title": "JMM 中内存模型是怎样的?什么是指令序列重排序?", + "title": "什么是内存泄漏,怎么确定内存泄漏?", "frontmatter": { - "title": "JMM 中内存模型是怎样的?什么是指令序列重排序?", - "date": "2022-05-21T17:32:49.000Z", - "permalink": "/pages/c8e6cb/", + "title": "什么是内存泄漏,怎么确定内存泄漏?", + "date": "2022-05-21T17:33:27.000Z", + "permalink": "/pages/3ab5f5/", "categories": [ "Java相关", "JVM" @@ -2338,50 +2225,240 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/030.JMM%20%E4%B8%AD%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B%E6%98%AF%E6%80%8E%E6%A0%B7%E7%9A%84%EF%BC%9F%E4%BB%80%E4%B9%88%E6%98%AF%E6%8C%87%E4%BB%A4%E5%BA%8F%E5%88%97%E9%87%8D%E6%8E%92%E5%BA%8F%EF%BC%9F.html", - "relativePath": "01.Java相关/30.JVM/030.JMM 中内存模型是怎样的?什么是指令序列重排序?.md", - "key": "v-0970cd54", - "path": "/pages/c8e6cb/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/060.%E4%BB%80%E4%B9%88%E6%98%AF%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%EF%BC%8C%E6%80%8E%E4%B9%88%E7%A1%AE%E5%AE%9A%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%EF%BC%9F.html", + "relativePath": "01.Java相关/30.JVM/060.什么是内存泄漏,怎么确定内存泄漏?.md", + "key": "v-15fba8f1", + "path": "/pages/3ab5f5/", "headers": [ { "level": 2, - "title": "为什么要有内存模型?", - "slug": "为什么要有内存模型", - "normalizedTitle": "为什么要有内存模型?", - "charIndex": 40 + "title": "前言", + "slug": "前言", + "normalizedTitle": "前言", + "charIndex": 2 + }, + { + "level": 2, + "title": "一、介绍", + "slug": "一、介绍", + "normalizedTitle": "一、介绍", + "charIndex": 178 + }, + { + "level": 3, + "title": "1、什么是内存泄漏", + "slug": "_1、什么是内存泄漏", + "normalizedTitle": "1、什么是内存泄漏", + "charIndex": 187 + }, + { + "level": 3, + "title": "2、内存泄漏的危害", + "slug": "_2、内存泄漏的危害", + "normalizedTitle": "2、内存泄漏的危害", + "charIndex": 404 + }, + { + "level": 2, + "title": "二、内存泄漏原因", + "slug": "二、内存泄漏原因", + "normalizedTitle": "二、内存泄漏原因", + "charIndex": 491 + }, + { + "level": 3, + "title": "1、大量使用静态变量", + "slug": "_1、大量使用静态变量", + "normalizedTitle": "1、大量使用静态变量", + "charIndex": 583 + }, + { + "level": 3, + "title": "2、连接资源未关闭", + "slug": "_2、连接资源未关闭", + "normalizedTitle": "2、连接资源未关闭", + "charIndex": 1310 + }, + { + "level": 3, + "title": "4、内部类持有外部类", + "slug": "_4、内部类持有外部类", + "normalizedTitle": "4、内部类持有外部类", + "charIndex": 2418 + }, + { + "level": 3, + "title": "5、finalize方法", + "slug": "_5、finalize方法", + "normalizedTitle": "5、finalize方法", + "charIndex": 2445 + }, + { + "level": 3, + "title": "6、ThreadLocal的错误使用", + "slug": "_6、threadlocal的错误使用", + "normalizedTitle": "6、threadlocal的错误使用", + "charIndex": 2595 + }, + { + "level": 2, + "title": "三、检测内存泄漏", + "slug": "三、检测内存泄漏", + "normalizedTitle": "三、检测内存泄漏", + "charIndex": 3062 + }, + { + "level": 3, + "title": "1、工具分析", + "slug": "_1、工具分析", + "normalizedTitle": "1、工具分析", + "charIndex": 3106 + }, + { + "level": 3, + "title": "2、垃圾回收分析", + "slug": "_2、垃圾回收分析", + "normalizedTitle": "2、垃圾回收分析", + "charIndex": 3232 + }, + { + "level": 3, + "title": "3、基准测试", + "slug": "_3、基准测试", + "normalizedTitle": "3、基准测试", + "charIndex": 3374 + }, + { + "level": 2, + "title": "四、结论", + "slug": "四、结论", + "normalizedTitle": "四、结论", + "charIndex": 3418 + } + ], + "headersStr": "前言 一、介绍 1、什么是内存泄漏 2、内存泄漏的危害 二、内存泄漏原因 1、大量使用静态变量 2、连接资源未关闭 4、内部类持有外部类 5、finalize方法 6、ThreadLocal的错误使用 三、检测内存泄漏 1、工具分析 2、垃圾回收分析 3、基准测试 四、结论", + "content": "# 前言\n\n这个问题是我之前翻看面经的时候见到的。那位小姐姐把内存泄漏当成了内存溢出问题去解答的,结果当场挂掉了。为此总结一下,之前和一位老哥也讨论过这个问题。可见不管是面试还是工作这都是一个极为重要的点。\n\n我也曾在面阿里的时候也遇到过原题,题目是写出俩内存泄漏案例,然后问如何排查?如何解决?\n\n本篇文章大体结构来自外国大佬baeldung;\n\n\n# 一、介绍\n\n\n# 1、什么是内存泄漏\n\njava的优势之一就是内置了垃圾回收器GC,它帮助我们实现了自动化内存管理。但是GC再好,也有老马失前蹄的时候,它不能保证提供一个解决内存泄漏的万无一失的解决方案。什么是内存泄漏?可以看看下面这张图,\n\n也就是一部分内存空间我明明已经使用了,却没有引用指向这部分空间。造成这片已经使用的空间无法处理的情况。\n\n正规点的理解:动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。\n\n\n# 2、内存泄漏的危害\n\n长时间运行,程序变卡,性能严重下降 程序莫名其妙挂掉 OutOfMemoryError错误 乱七八糟的错误,还不易排查 反正内存泄漏不是好事。\n\n\n# 二、内存泄漏原因\n\n内存泄漏原因太多了。说不定就是某一行代码不对就会出现这种情况,因此这里给出最常见的几种。关键的还是如何找出哪个地方出现了内存泄漏,代码好修改,错误不易查。\n\n\n# 1、大量使用静态变量\n\n静态变量的生命周期与程序一致。因此常驻内存。\n\npublic class Static Test{\n public static List list = new ArrayList<>();\n public void populateList(){\n for (int i = 0; i < 10000000; i++) {\n list.add((int)Math.random()); \n } \n System.out.println(\"running......\"); \n }\n public static void main(String[] args){ \n System.out.println(\"before......\");\n new StaticTest().populateList(); \n System.out.println(\"after......\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n\n\n现在可以使用jvisualvm运行一边,看看内存效果。\n\n * 带static关键字(使用静态变量)\n\n从上图可以看到,堆内存从一开始的135M左右飙升了到了200M。直接占据了65M的内存。\n\n * 不使用static关键字(不使用静态变量)\n\n由于全局变量与程序周期不一致,因此不使用时,就会进行回收。此时内存最高150M。\n\n总结:由于静态变量与程序生命周期一致,因此对象常驻内存,造成内存泄漏\n\n\n# 2、连接资源未关闭\n\n每当建立一个连接,jvm就会为这么资源分配内存。比如数据库连接、文件输入输出流、网络连接等等。\n\npublic class File Test{\n public static void main(String[] args)throws IOException {\n File f=new File(\"G:\\\\nginx配套资料\\\\笔记资料.zip\"); \n System.out.println(f.exists());\n System.out.println(f.isDirectory());\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n依然使用jvisualvm运行一边,看看内存效果。\n\n可以看出,在连接文件资源时,jvm会为本资源分配内存。\n\n3、equals()和hashCode()方法使用不当\n\n定义新类时,如果没有重新equals()和hashCode()方法,也有可能会造成内存泄漏。主要原因是没有这两个方法时,很容易造成重复的数据添加。看例子:\n\npublic class User{\n public String name;\n publicint age;\n public User(String name, int age){\n this.name = name;\n this.age = age;}\n }\n public class EqualTest{\n public static void main(String[] args){ \n Map map = new HashMap<>();\n for(int i=0; i<100; i++) { \n map.put(new User(\"\", 1), 1);\n } \n System.out.println(map.size() == 1);//输出为false \n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\n然后运行一下,看看内存情况:\n\n内存从150M一下子飙升到225M,可见飙升的厉害。输出为false,说明user对象被重复添加了。我们知道像HashMap在添加新的对象时,会对其hashcode进行比较,如果一样,那就不插入。如果一样那就插入。此时说明这100个User其hashcode不同。\n\n\n# 4、内部类持有外部类\n\n这个场景和上面类似。\n\n\n# 5、finalize方法\n\n这个方法之前曾经专门花过文章写过,这个问题很简单。看一张图\n\n这就是整个过程。不过在这里我们主要看的是finalize方法对垃圾回收的影响,其实就是在第三步,也就是这个对象含有finalize,进入了队列但一直没有被调用的这段时间,会一直占用内存。造成内存泄漏。\n\n\n# 6、ThreadLocal的错误使用\n\nThreadLocal主要用于创建本地线程变量,不合理的使用也有可能会造成内存泄漏。\n\n上面这张图详细的揭示了ThreadLocal和Thread以及ThreadLocalMap三者的关系。\n\n1、Thread中有一个map,就是ThreadLocalMap\n\n2、ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。\n\n3、ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收\n\n4、重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。\n\n解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。\n\n现在介绍了几种常见的内存泄漏情况,上面的知识点比较常见,最主要的是如何检测出来。\n\n\n# 三、检测内存泄漏\n\n检测的目的是定位内存泄漏出现的位置,常见的有以下几种方法:\n\n\n# 1、工具分析\n\n这个工具比较多,比如说JProfiler、YourKit、Java VisualVM和Netbeans Profiler。他可以帮助我们分析是哪一个对象或者是类内存的飙升。也可以看到内存CPU的等等各种情况。上面多次演示到了。\n\n\n# 2、垃圾回收分析\n\n这个其实也可以用工具进行分析。上面的VisualVM中,可以打印堆。也可以从外部导入dump文件进行分析。\n\n如果不用工具的话,我们可以通过IDE看到。JVM配置添加-verbose:gc。然后就会打印出相关信息。下面这张图非原创,来自Baeldung。\n\n\n# 3、基准测试\n\n也就是使用科学的方式进行分析java代码的性能。进而判断分析。\n\n\n# 四、结论\n\n内存泄漏是个很严重的问题,也比较常见。\n\n最主要的原因是动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。因此良好的代码规范,可以有效地避免这些错误。\n\n参考: 什么是内存泄漏?该如何检测?又该如何解决?", + "normalizedContent": "# 前言\n\n这个问题是我之前翻看面经的时候见到的。那位小姐姐把内存泄漏当成了内存溢出问题去解答的,结果当场挂掉了。为此总结一下,之前和一位老哥也讨论过这个问题。可见不管是面试还是工作这都是一个极为重要的点。\n\n我也曾在面阿里的时候也遇到过原题,题目是写出俩内存泄漏案例,然后问如何排查?如何解决?\n\n本篇文章大体结构来自外国大佬baeldung;\n\n\n# 一、介绍\n\n\n# 1、什么是内存泄漏\n\njava的优势之一就是内置了垃圾回收器gc,它帮助我们实现了自动化内存管理。但是gc再好,也有老马失前蹄的时候,它不能保证提供一个解决内存泄漏的万无一失的解决方案。什么是内存泄漏?可以看看下面这张图,\n\n也就是一部分内存空间我明明已经使用了,却没有引用指向这部分空间。造成这片已经使用的空间无法处理的情况。\n\n正规点的理解:动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。\n\n\n# 2、内存泄漏的危害\n\n长时间运行,程序变卡,性能严重下降 程序莫名其妙挂掉 outofmemoryerror错误 乱七八糟的错误,还不易排查 反正内存泄漏不是好事。\n\n\n# 二、内存泄漏原因\n\n内存泄漏原因太多了。说不定就是某一行代码不对就会出现这种情况,因此这里给出最常见的几种。关键的还是如何找出哪个地方出现了内存泄漏,代码好修改,错误不易查。\n\n\n# 1、大量使用静态变量\n\n静态变量的生命周期与程序一致。因此常驻内存。\n\npublic class static test{\n public static list list = new arraylist<>();\n public void populatelist(){\n for (int i = 0; i < 10000000; i++) {\n list.add((int)math.random()); \n } \n system.out.println(\"running......\"); \n }\n public static void main(string[] args){ \n system.out.println(\"before......\");\n new statictest().populatelist(); \n system.out.println(\"after......\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n\n\n现在可以使用jvisualvm运行一边,看看内存效果。\n\n * 带static关键字(使用静态变量)\n\n从上图可以看到,堆内存从一开始的135m左右飙升了到了200m。直接占据了65m的内存。\n\n * 不使用static关键字(不使用静态变量)\n\n由于全局变量与程序周期不一致,因此不使用时,就会进行回收。此时内存最高150m。\n\n总结:由于静态变量与程序生命周期一致,因此对象常驻内存,造成内存泄漏\n\n\n# 2、连接资源未关闭\n\n每当建立一个连接,jvm就会为这么资源分配内存。比如数据库连接、文件输入输出流、网络连接等等。\n\npublic class file test{\n public static void main(string[] args)throws ioexception {\n file f=new file(\"g:\\\\nginx配套资料\\\\笔记资料.zip\"); \n system.out.println(f.exists());\n system.out.println(f.isdirectory());\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n依然使用jvisualvm运行一边,看看内存效果。\n\n可以看出,在连接文件资源时,jvm会为本资源分配内存。\n\n3、equals()和hashcode()方法使用不当\n\n定义新类时,如果没有重新equals()和hashcode()方法,也有可能会造成内存泄漏。主要原因是没有这两个方法时,很容易造成重复的数据添加。看例子:\n\npublic class user{\n public string name;\n publicint age;\n public user(string name, int age){\n this.name = name;\n this.age = age;}\n }\n public class equaltest{\n public static void main(string[] args){ \n map map = new hashmap<>();\n for(int i=0; i<100; i++) { \n map.put(new user(\"\", 1), 1);\n } \n system.out.println(map.size() == 1);//输出为false \n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\n然后运行一下,看看内存情况:\n\n内存从150m一下子飙升到225m,可见飙升的厉害。输出为false,说明user对象被重复添加了。我们知道像hashmap在添加新的对象时,会对其hashcode进行比较,如果一样,那就不插入。如果一样那就插入。此时说明这100个user其hashcode不同。\n\n\n# 4、内部类持有外部类\n\n这个场景和上面类似。\n\n\n# 5、finalize方法\n\n这个方法之前曾经专门花过文章写过,这个问题很简单。看一张图\n\n这就是整个过程。不过在这里我们主要看的是finalize方法对垃圾回收的影响,其实就是在第三步,也就是这个对象含有finalize,进入了队列但一直没有被调用的这段时间,会一直占用内存。造成内存泄漏。\n\n\n# 6、threadlocal的错误使用\n\nthreadlocal主要用于创建本地线程变量,不合理的使用也有可能会造成内存泄漏。\n\n上面这张图详细的揭示了threadlocal和thread以及threadlocalmap三者的关系。\n\n1、thread中有一个map,就是threadlocalmap\n\n2、threadlocalmap的key是threadlocal,值是我们自己设定的。\n\n3、threadlocal是一个弱引用,当为null时,会被当成垃圾回收\n\n4、重点来了,突然我们threadlocal是null了,也就是要被垃圾回收器回收了,但是此时我们的threadlocalmap生命周期和thread的一样,它不会回收,这时候就出现了一个现象。那就是threadlocalmap的key没了,但是value还在,这就造成了内存泄漏。\n\n解决办法:使用完threadlocal后,执行remove操作,避免出现内存溢出情况。\n\n现在介绍了几种常见的内存泄漏情况,上面的知识点比较常见,最主要的是如何检测出来。\n\n\n# 三、检测内存泄漏\n\n检测的目的是定位内存泄漏出现的位置,常见的有以下几种方法:\n\n\n# 1、工具分析\n\n这个工具比较多,比如说jprofiler、yourkit、java visualvm和netbeans profiler。他可以帮助我们分析是哪一个对象或者是类内存的飙升。也可以看到内存cpu的等等各种情况。上面多次演示到了。\n\n\n# 2、垃圾回收分析\n\n这个其实也可以用工具进行分析。上面的visualvm中,可以打印堆。也可以从外部导入dump文件进行分析。\n\n如果不用工具的话,我们可以通过ide看到。jvm配置添加-verbose:gc。然后就会打印出相关信息。下面这张图非原创,来自baeldung。\n\n\n# 3、基准测试\n\n也就是使用科学的方式进行分析java代码的性能。进而判断分析。\n\n\n# 四、结论\n\n内存泄漏是个很严重的问题,也比较常见。\n\n最主要的原因是动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。因此良好的代码规范,可以有效地避免这些错误。\n\n参考: 什么是内存泄漏?该如何检测?又该如何解决?", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/18, 21:28:02", + "lastUpdatedTimestamp": 1663507682000 + }, + { + "title": "常见的 GC 回收算法有哪些?", + "frontmatter": { + "title": "常见的 GC 回收算法有哪些?", + "date": "2022-09-18T19:01:29.000Z", + "permalink": "/pages/9b26e8/", + "categories": [ + "Java相关", + "JVM" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/010.%E5%B8%B8%E8%A7%81%E7%9A%84%20GC%20%E5%9B%9E%E6%94%B6%E7%AE%97%E6%B3%95%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F.html", + "relativePath": "01.Java相关/30.JVM/010.常见的 GC 回收算法有哪些?.md", + "key": "v-af821fc8", + "path": "/pages/9b26e8/", + "headers": [ + { + "level": 2, + "title": "标记-清除算法", + "slug": "标记-清除算法", + "normalizedTitle": "标记-清除算法", + "charIndex": 2 + }, + { + "level": 2, + "title": "标记-复制算法", + "slug": "标记-复制算法", + "normalizedTitle": "标记-复制算法", + "charIndex": 148 + }, + { + "level": 2, + "title": "标记-整理算法", + "slug": "标记-整理算法", + "normalizedTitle": "标记-整理算法", + "charIndex": 281 + }, + { + "level": 2, + "title": "分代收集算法", + "slug": "分代收集算法", + "normalizedTitle": "分代收集算法", + "charIndex": 382 + }, + { + "level": 2, + "title": "垃圾收集器", + "slug": "垃圾收集器", + "normalizedTitle": "垃圾收集器", + "charIndex": 682 + }, + { + "level": 2, + "title": "Serial 收集器", + "slug": "serial-收集器", + "normalizedTitle": "serial 收集器", + "charIndex": 893 + }, + { + "level": 2, + "title": "ParNew 收集器", + "slug": "parnew-收集器", + "normalizedTitle": "parnew 收集器", + "charIndex": 1324 + }, + { + "level": 2, + "title": "Parallel Scavenge 收集器", + "slug": "parallel-scavenge-收集器", + "normalizedTitle": "parallel scavenge 收集器", + "charIndex": 1678 + }, + { + "level": 2, + "title": "Serial Old 收集器", + "slug": "serial-old-收集器", + "normalizedTitle": "serial old 收集器", + "charIndex": 2694 }, { "level": 2, - "title": "硬件内存架构", - "slug": "硬件内存架构", - "normalizedTitle": "硬件内存架构", - "charIndex": 73 + "title": "Parallel Old 收集器", + "slug": "parallel-old-收集器", + "normalizedTitle": "parallel old 收集器", + "charIndex": 2826 }, { "level": 2, - "title": "缓存一致性问题", - "slug": "缓存一致性问题", - "normalizedTitle": "缓存一致性问题", - "charIndex": 557 + "title": "CMS 收集器", + "slug": "cms-收集器", + "normalizedTitle": "cms 收集器", + "charIndex": 1503 }, { "level": 2, - "title": "处理器优化和指令重排序", - "slug": "处理器优化和指令重排序", - "normalizedTitle": "处理器优化和指令重排序", - "charIndex": 943 + "title": "G1 收集器", + "slug": "g1-收集器", + "normalizedTitle": "g1 收集器", + "charIndex": 3638 }, { "level": 2, - "title": "并发编程的问题", - "slug": "并发编程的问题", - "normalizedTitle": "并发编程的问题", - "charIndex": 1368 + "title": "ZGC 收集器", + "slug": "zgc-收集器", + "normalizedTitle": "zgc 收集器", + "charIndex": 4351 } ], - "headersStr": "为什么要有内存模型? 硬件内存架构 缓存一致性问题 处理器优化和指令重排序 并发编程的问题", - "content": "Java内存模型(JMM)和 Java 运行时内存区域是不一样的概念。\n\n\n# 为什么要有内存模型?\n\n要想回答这个问题,我们需要先弄懂传统计算机硬件内存架构。好了,我要开始画图了。\n\n\n# 硬件内存架构\n\n\n\n(1)CPU\n\n去过机房的同学都知道,一般在大型服务器上会配置多个CPU,每个CPU还会有多个\n\n核\n\n,这就意味着多个CPU或者多个核可以同时(并发)工作。如果使用Java 起了一个多线程的任务,很有可能每个 CPU 都会跑一个线程,那么你的任务在某一刻就是真正并发执行了。\n\n(2)CPU Register\n\nCPU Register也就是 CPU 寄存器。CPU 寄存器是 CPU 内部集成的,在寄存器上执行操作的效率要比在主存上高出几个数量级。\n\n(3)CPU Cache Memory\n\nCPU Cache Memory也就是 CPU 高速缓存,相对于寄存器来说,通常也可以成为 L2 二级缓存。相对于硬盘读取速度来说内存读取的效率非常高,但是与 CPU 还是相差数量级,所以在 CPU 和主存间引入了多级缓存,目的是为了做一下缓冲。\n\n(4)Main Memory\n\nMain Memory 就是主存,主存比 L1、L2 缓存要大很多。\n\n注意:部分高端机器还有 L3 三级缓存。\n\n\n# 缓存一致性问题\n\n由于主存与 CPU 处理器的运算能力之间有数量级的差距,所以在传统计算机内存架构中会引入高速缓存来作为主存和处理器之间的缓冲,CPU 将常用的数据放在高速缓存中,运算结束后 CPU 再讲运算结果同步到主存中。\n\n使用高速缓存解决了 CPU 和主存速率不匹配的问题,但同时又引入另外一个新问题:缓存一致性问题。\n\n在多CPU的系统中(或者单CPU多核的系统),每个CPU内核都有自己的高速缓存,它们共享同一主内存(Main Memory)。当多个CPU的运算任务都涉及同一块主内存区域时,CPU 会将数据读取到缓存中进行运算,这可能会导致各自的缓存数据不一致。\n\n因此需要每个 CPU 访问缓存时遵循一定的协议,在读写数据时根据协议进行操作,共同来维护缓存的一致性。这类协议有 MSI、MESI、MOSI、和 Dragon Protocol 等。\n\n\n# 处理器优化和指令重排序\n\n为了提升性能在 CPU 和主内存之间增加了高速缓存,但在多线程并发场景可能会遇到\n\n。那还有没有办法进一步提升 CPU 的执行效率呢?答案是:处理器优化。\n\n为了使处理器内部的运算单元能够最大化被充分利用,处理器会对输入代码进行乱序执行处理,这就是处理器优化。\n\n除了处理器会对代码进行优化处理,很多现代编程语言的编译器也会做类似的优化,比如像 Java 的即时编译器(JIT)会做指令重排序。\n\n处理器优化其实也是重排序的一种类型,这里总结一下,重排序可以分为三种类型: ** 编译器优化的重排序。编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序。 ** 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 ** 内存系统的重排序。由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。\n\n\n# 并发编程的问题\n\n上面讲了一堆硬件相关的东西,有些同学可能会有点懵,绕了这么大圈,这些东西跟 Java 内存模型有啥关系吗?不要急咱们慢慢往下看。\n\n熟悉 Java 并发的同学肯定对这三个问题很熟悉:『可见性问题』、『原子性问题』、『有序性问题』。如果从更深层次看这三个问题,其实就是上面讲的『缓存一致性』、『处理器优化』、『指令重排序』造成的。\n\n缓存一致性问题其实就是可见性问题,处理器优化可能会造成原子性问题,指令重排序会造成有序性问题,你看是不是都联系上了。\n\n出了问题总是要解决的,那有什么办法呢?首先想到简单粗暴的办法,干掉缓存让 CPU 直接与主内存交互就解决了可见性问题,禁止处理器优化和指令重排序就解决了原子性和有序性问题,但这样一夜回到解放前了,显然不可取。\n\n所以技术前辈们想到了在物理机器上定义出一套内存模型, 规范内存的读写操作。内存模型解决并发问题主要采用两种方式:\n\n限制处理器优化和使用内存屏障。\n\n参考: 全面学习掌握Java内存模型", - "normalizedContent": "java内存模型(jmm)和 java 运行时内存区域是不一样的概念。\n\n\n# 为什么要有内存模型?\n\n要想回答这个问题,我们需要先弄懂传统计算机硬件内存架构。好了,我要开始画图了。\n\n\n# 硬件内存架构\n\n\n\n(1)cpu\n\n去过机房的同学都知道,一般在大型服务器上会配置多个cpu,每个cpu还会有多个\n\n核\n\n,这就意味着多个cpu或者多个核可以同时(并发)工作。如果使用java 起了一个多线程的任务,很有可能每个 cpu 都会跑一个线程,那么你的任务在某一刻就是真正并发执行了。\n\n(2)cpu register\n\ncpu register也就是 cpu 寄存器。cpu 寄存器是 cpu 内部集成的,在寄存器上执行操作的效率要比在主存上高出几个数量级。\n\n(3)cpu cache memory\n\ncpu cache memory也就是 cpu 高速缓存,相对于寄存器来说,通常也可以成为 l2 二级缓存。相对于硬盘读取速度来说内存读取的效率非常高,但是与 cpu 还是相差数量级,所以在 cpu 和主存间引入了多级缓存,目的是为了做一下缓冲。\n\n(4)main memory\n\nmain memory 就是主存,主存比 l1、l2 缓存要大很多。\n\n注意:部分高端机器还有 l3 三级缓存。\n\n\n# 缓存一致性问题\n\n由于主存与 cpu 处理器的运算能力之间有数量级的差距,所以在传统计算机内存架构中会引入高速缓存来作为主存和处理器之间的缓冲,cpu 将常用的数据放在高速缓存中,运算结束后 cpu 再讲运算结果同步到主存中。\n\n使用高速缓存解决了 cpu 和主存速率不匹配的问题,但同时又引入另外一个新问题:缓存一致性问题。\n\n在多cpu的系统中(或者单cpu多核的系统),每个cpu内核都有自己的高速缓存,它们共享同一主内存(main memory)。当多个cpu的运算任务都涉及同一块主内存区域时,cpu 会将数据读取到缓存中进行运算,这可能会导致各自的缓存数据不一致。\n\n因此需要每个 cpu 访问缓存时遵循一定的协议,在读写数据时根据协议进行操作,共同来维护缓存的一致性。这类协议有 msi、mesi、mosi、和 dragon protocol 等。\n\n\n# 处理器优化和指令重排序\n\n为了提升性能在 cpu 和主内存之间增加了高速缓存,但在多线程并发场景可能会遇到\n\n。那还有没有办法进一步提升 cpu 的执行效率呢?答案是:处理器优化。\n\n为了使处理器内部的运算单元能够最大化被充分利用,处理器会对输入代码进行乱序执行处理,这就是处理器优化。\n\n除了处理器会对代码进行优化处理,很多现代编程语言的编译器也会做类似的优化,比如像 java 的即时编译器(jit)会做指令重排序。\n\n处理器优化其实也是重排序的一种类型,这里总结一下,重排序可以分为三种类型: ** 编译器优化的重排序。编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序。 ** 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 ** 内存系统的重排序。由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。\n\n\n# 并发编程的问题\n\n上面讲了一堆硬件相关的东西,有些同学可能会有点懵,绕了这么大圈,这些东西跟 java 内存模型有啥关系吗?不要急咱们慢慢往下看。\n\n熟悉 java 并发的同学肯定对这三个问题很熟悉:『可见性问题』、『原子性问题』、『有序性问题』。如果从更深层次看这三个问题,其实就是上面讲的『缓存一致性』、『处理器优化』、『指令重排序』造成的。\n\n缓存一致性问题其实就是可见性问题,处理器优化可能会造成原子性问题,指令重排序会造成有序性问题,你看是不是都联系上了。\n\n出了问题总是要解决的,那有什么办法呢?首先想到简单粗暴的办法,干掉缓存让 cpu 直接与主内存交互就解决了可见性问题,禁止处理器优化和指令重排序就解决了原子性和有序性问题,但这样一夜回到解放前了,显然不可取。\n\n所以技术前辈们想到了在物理机器上定义出一套内存模型, 规范内存的读写操作。内存模型解决并发问题主要采用两种方式:\n\n限制处理器优化和使用内存屏障。\n\n参考: 全面学习掌握java内存模型", + "headersStr": "标记-清除算法 标记-复制算法 标记-整理算法 分代收集算法 垃圾收集器 Serial 收集器 ParNew 收集器 Parallel Scavenge 收集器 Serial Old 收集器 Parallel Old 收集器 CMS 收集器 G1 收集器 ZGC 收集器", + "content": "# 标记-清除算法\n\n该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:\n\n效率问题 空间问题(标记清除后会产生大量不连续的碎片)\n\n\n# 标记-复制算法\n\n为了解决效率问题,“标记-复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。\n\n\n# 标记-整理算法\n\n根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。\n\n\n# 分代收集算法\n\n当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。\n\n比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。\n\n延伸面试问题: HotSpot 为什么要分为新生代和老年代?\n\n根据上面的对分代收集算法的介绍回答。\n\n\n# 垃圾收集器\n\n如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。\n\n虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的 HotSpot 虚拟机就不会实现那么多不同的垃圾收集器了。\n\n\n# Serial 收集器\n\nSerial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( \"Stop The World\" ),直到它收集结束。\n\n新生代采用标记-复制算法,老年代采用标记-整理算法。\n\n\n\n虚拟机的设计者们当然知道 Stop The World 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。\n\n但是 Serial 收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。\n\n\n# ParNew 收集器\n\nParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。\n\n新生代采用标记-复制算法,老年代采用标记-整理算法。\n\n\n\n它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。\n\n并行和并发概念补充:\n\n并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。\n\n并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。\n\n\n# Parallel Scavenge 收集器\n\nParallel Scavenge 收集器也是使用标记-复制算法的多线程收集器,它看上去几乎和 ParNew 都一样。 那么它有什么特别之处呢?\n\n-XX:+UseParallelGC\n\n 使用 Parallel 收集器+ 老年代串行\n\n-XX:+UseParallelOldGC\n\n 使用 Parallel 收集器+ 老年代并行\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nParallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。 Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。\n\n新生代采用标记-复制算法,老年代采用标记-整理算法。\n\n\n\n这是 JDK1.8 默认收集器\n\n使用 java -XX:+PrintCommandLineFlags -version 命令查看\n\n-XX:InitialHeapSize=262921408 -XX:MaxHeapSize=4206742528 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC\njava version \"1.8.0_211\"\nJava(TM) SE Runtime Environment (build 1.8.0_211-b12)\nJava HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)\nJDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC 参数,则默认指定了-XX:+UseParallelOldGC,可以使用-XX:-UseParallelOldGC 来禁用该功能\n\n\n1\n2\n3\n4\n5\n\n\n\n# Serial Old 收集器\n\nSerial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。\n\n\n# Parallel Old 收集器\n\nParallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。\n\n\n# CMS 收集器\n\nCMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。\n\nCMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。\n\n从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:\n\n初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ; 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。\n\n从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:\n\n对 CPU 资源敏感; 无法处理浮动垃圾; 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。\n\n\n# G1 收集器\n\nG1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.\n\n被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备以下特点:\n\n并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。 空间整合:与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。 G1 收集器的运作大致分为以下几个步骤:\n\n初始标记 并发标记 最终标记 筛选回收 G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。\n\n\n# ZGC 收集器\n\n与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。\n\n在 ZGC 中出现 Stop The World 的情况会更少! 详情可以看 : 《新一代垃圾回收器 ZGC 的探索与实践》", + "normalizedContent": "# 标记-清除算法\n\n该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:\n\n效率问题 空间问题(标记清除后会产生大量不连续的碎片)\n\n\n# 标记-复制算法\n\n为了解决效率问题,“标记-复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。\n\n\n# 标记-整理算法\n\n根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。\n\n\n# 分代收集算法\n\n当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。\n\n比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。\n\n延伸面试问题: hotspot 为什么要分为新生代和老年代?\n\n根据上面的对分代收集算法的介绍回答。\n\n\n# 垃圾收集器\n\n如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。\n\n虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的 hotspot 虚拟机就不会实现那么多不同的垃圾收集器了。\n\n\n# serial 收集器\n\nserial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( \"stop the world\" ),直到它收集结束。\n\n新生代采用标记-复制算法,老年代采用标记-整理算法。\n\n\n\n虚拟机的设计者们当然知道 stop the world 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。\n\n但是 serial 收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。serial 收集器对于运行在 client 模式下的虚拟机来说是个不错的选择。\n\n\n# parnew 收集器\n\nparnew 收集器其实就是 serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 serial 收集器完全一样。\n\n新生代采用标记-复制算法,老年代采用标记-整理算法。\n\n\n\n它是许多运行在 server 模式下的虚拟机的首要选择,除了 serial 收集器外,只有它能与 cms 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。\n\n并行和并发概念补充:\n\n并行(parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。\n\n并发(concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 cpu 上。\n\n\n# parallel scavenge 收集器\n\nparallel scavenge 收集器也是使用标记-复制算法的多线程收集器,它看上去几乎和 parnew 都一样。 那么它有什么特别之处呢?\n\n-xx:+useparallelgc\n\n 使用 parallel 收集器+ 老年代串行\n\n-xx:+useparalleloldgc\n\n 使用 parallel 收集器+ 老年代并行\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nparallel scavenge 收集器关注点是吞吐量(高效率的利用 cpu)。cms 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 cpu 中用于运行用户代码的时间与 cpu 总消耗时间的比值。 parallel scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 parallel scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。\n\n新生代采用标记-复制算法,老年代采用标记-整理算法。\n\n\n\n这是 jdk1.8 默认收集器\n\n使用 java -xx:+printcommandlineflags -version 命令查看\n\n-xx:initialheapsize=262921408 -xx:maxheapsize=4206742528 -xx:+printcommandlineflags -xx:+usecompressedclasspointers -xx:+usecompressedoops -xx:+useparallelgc\njava version \"1.8.0_211\"\njava(tm) se runtime environment (build 1.8.0_211-b12)\njava hotspot(tm) 64-bit server vm (build 25.211-b12, mixed mode)\njdk1.8 默认使用的是 parallel scavenge + parallel old,如果指定了-xx:+useparallelgc 参数,则默认指定了-xx:+useparalleloldgc,可以使用-xx:-useparalleloldgc 来禁用该功能\n\n\n1\n2\n3\n4\n5\n\n\n\n# serial old 收集器\n\nserial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 jdk1.5 以及以前的版本中与 parallel scavenge 收集器搭配使用,另一种用途是作为 cms 收集器的后备方案。\n\n\n# parallel old 收集器\n\nparallel scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 cpu 资源的场合,都可以优先考虑 parallel scavenge 收集器和 parallel old 收集器。\n\n\n# cms 收集器\n\ncms(concurrent mark sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。\n\ncms(concurrent mark sweep)收集器是 hotspot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。\n\n从名字中的mark sweep这两个词可以看出,cms 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:\n\n初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ; 并发标记: 同时开启 gc 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 gc 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短 并发清除: 开启用户线程,同时 gc 线程开始对未标记的区域做清扫。\n\n从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:\n\n对 cpu 资源敏感; 无法处理浮动垃圾; 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。\n\n\n# g1 收集器\n\ng1 (garbage-first) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 gc 停顿时间要求的同时,还具备高吞吐量性能特征.\n\n被视为 jdk1.7 中 hotspot 虚拟机的一个重要进化特征。它具备以下特点:\n\n并行与并发:g1 能充分利用 cpu、多核环境下的硬件优势,使用多个 cpu(cpu 或者 cpu 核心)来缩短 stop-the-world 停顿时间。部分其他收集器原本需要停顿 java 线程执行的 gc 动作,g1 收集器仍然可以通过并发的方式让 java 程序继续执行。 分代收集:虽然 g1 可以不需要其他收集器配合就能独立管理整个 gc 堆,但是还是保留了分代的概念。 空间整合:与 cms 的“标记-清理”算法不同,g1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。 可预测的停顿:这是 g1 相对于 cms 的另一个大优势,降低停顿时间是 g1 和 cms 共同的关注点,但 g1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 m 毫秒的时间片段内。 g1 收集器的运作大致分为以下几个步骤:\n\n初始标记 并发标记 最终标记 筛选回收 g1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 region(这也就是它的名字 garbage-first 的由来) 。这种使用 region 划分内存空间以及有优先级的区域回收方式,保证了 g1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。\n\n\n# zgc 收集器\n\n与 cms 中的 parnew 和 g1 类似,zgc 也采用标记-复制算法,不过 zgc 对该算法做了重大改进。\n\n在 zgc 中出现 stop the world 的情况会更少! 详情可以看 : 《新一代垃圾回收器 zgc 的探索与实践》", "charsets": { "cjk": true }, @@ -2389,11 +2466,11 @@ export const siteData = { "lastUpdatedTimestamp": 1663507682000 }, { - "title": "什么是内存泄漏,怎么确定内存泄漏?", + "title": "JVM 内存是如何对应到操作系统内存的?", "frontmatter": { - "title": "什么是内存泄漏,怎么确定内存泄漏?", - "date": "2022-05-21T17:33:27.000Z", - "permalink": "/pages/3ab5f5/", + "title": "JVM 内存是如何对应到操作系统内存的?", + "date": "2022-05-21T17:33:05.000Z", + "permalink": "/pages/da65e7/", "categories": [ "Java相关", "JVM" @@ -2402,120 +2479,43 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/060.%E4%BB%80%E4%B9%88%E6%98%AF%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%EF%BC%8C%E6%80%8E%E4%B9%88%E7%A1%AE%E5%AE%9A%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%EF%BC%9F.html", - "relativePath": "01.Java相关/30.JVM/060.什么是内存泄漏,怎么确定内存泄漏?.md", - "key": "v-15fba8f1", - "path": "/pages/3ab5f5/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/30.JVM/040.JVM%20%E5%86%85%E5%AD%98%E6%98%AF%E5%A6%82%E4%BD%95%E5%AF%B9%E5%BA%94%E5%88%B0%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%86%85%E5%AD%98%E7%9A%84%EF%BC%9F.html", + "relativePath": "01.Java相关/30.JVM/040.JVM 内存是如何对应到操作系统内存的?.md", + "key": "v-2de19204", + "path": "/pages/da65e7/", "headers": [ { "level": 2, - "title": "前言", - "slug": "前言", - "normalizedTitle": "前言", + "title": "Java 内存模型", + "slug": "java-内存模型", + "normalizedTitle": "java 内存模型", "charIndex": 2 }, { "level": 2, - "title": "一、介绍", - "slug": "一、介绍", - "normalizedTitle": "一、介绍", - "charIndex": 178 - }, - { - "level": 3, - "title": "1、什么是内存泄漏", - "slug": "_1、什么是内存泄漏", - "normalizedTitle": "1、什么是内存泄漏", - "charIndex": 187 - }, - { - "level": 3, - "title": "2、内存泄漏的危害", - "slug": "_2、内存泄漏的危害", - "normalizedTitle": "2、内存泄漏的危害", - "charIndex": 404 - }, - { - "level": 2, - "title": "二、内存泄漏原因", - "slug": "二、内存泄漏原因", - "normalizedTitle": "二、内存泄漏原因", - "charIndex": 491 - }, - { - "level": 3, - "title": "1、大量使用静态变量", - "slug": "_1、大量使用静态变量", - "normalizedTitle": "1、大量使用静态变量", - "charIndex": 583 - }, - { - "level": 3, - "title": "2、连接资源未关闭", - "slug": "_2、连接资源未关闭", - "normalizedTitle": "2、连接资源未关闭", - "charIndex": 1310 - }, - { - "level": 3, - "title": "4、内部类持有外部类", - "slug": "_4、内部类持有外部类", - "normalizedTitle": "4、内部类持有外部类", - "charIndex": 2418 - }, - { - "level": 3, - "title": "5、finalize方法", - "slug": "_5、finalize方法", - "normalizedTitle": "5、finalize方法", - "charIndex": 2445 - }, - { - "level": 3, - "title": "6、ThreadLocal的错误使用", - "slug": "_6、threadlocal的错误使用", - "normalizedTitle": "6、threadlocal的错误使用", - "charIndex": 2595 + "title": "Java 线程与主内存的关系", + "slug": "java-线程与主内存的关系", + "normalizedTitle": "java 线程与主内存的关系", + "charIndex": 216 }, { "level": 2, - "title": "三、检测内存泄漏", - "slug": "三、检测内存泄漏", - "normalizedTitle": "三、检测内存泄漏", - "charIndex": 3062 - }, - { - "level": 3, - "title": "1、工具分析", - "slug": "_1、工具分析", - "normalizedTitle": "1、工具分析", - "charIndex": 3106 - }, - { - "level": 3, - "title": "2、垃圾回收分析", - "slug": "_2、垃圾回收分析", - "normalizedTitle": "2、垃圾回收分析", - "charIndex": 3232 - }, - { - "level": 3, - "title": "3、基准测试", - "slug": "_3、基准测试", - "normalizedTitle": "3、基准测试", - "charIndex": 3374 + "title": "线程间通信", + "slug": "线程间通信", + "normalizedTitle": "线程间通信", + "charIndex": 421 }, { "level": 2, - "title": "四、结论", - "slug": "四、结论", - "normalizedTitle": "四、结论", - "charIndex": 3418 + "title": "有态度的总结", + "slug": "有态度的总结", + "normalizedTitle": "有态度的总结", + "charIndex": 1043 } ], - "headersStr": "前言 一、介绍 1、什么是内存泄漏 2、内存泄漏的危害 二、内存泄漏原因 1、大量使用静态变量 2、连接资源未关闭 4、内部类持有外部类 5、finalize方法 6、ThreadLocal的错误使用 三、检测内存泄漏 1、工具分析 2、垃圾回收分析 3、基准测试 四、结论", - "content": "# 前言\n\n这个问题是我之前翻看面经的时候见到的。那位小姐姐把内存泄漏当成了内存溢出问题去解答的,结果当场挂掉了。为此总结一下,之前和一位老哥也讨论过这个问题。可见不管是面试还是工作这都是一个极为重要的点。\n\n我也曾在面阿里的时候也遇到过原题,题目是写出俩内存泄漏案例,然后问如何排查?如何解决?\n\n本篇文章大体结构来自外国大佬baeldung;\n\n\n# 一、介绍\n\n\n# 1、什么是内存泄漏\n\njava的优势之一就是内置了垃圾回收器GC,它帮助我们实现了自动化内存管理。但是GC再好,也有老马失前蹄的时候,它不能保证提供一个解决内存泄漏的万无一失的解决方案。什么是内存泄漏?可以看看下面这张图,\n\n也就是一部分内存空间我明明已经使用了,却没有引用指向这部分空间。造成这片已经使用的空间无法处理的情况。\n\n正规点的理解:动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。\n\n\n# 2、内存泄漏的危害\n\n长时间运行,程序变卡,性能严重下降 程序莫名其妙挂掉 OutOfMemoryError错误 乱七八糟的错误,还不易排查 反正内存泄漏不是好事。\n\n\n# 二、内存泄漏原因\n\n内存泄漏原因太多了。说不定就是某一行代码不对就会出现这种情况,因此这里给出最常见的几种。关键的还是如何找出哪个地方出现了内存泄漏,代码好修改,错误不易查。\n\n\n# 1、大量使用静态变量\n\n静态变量的生命周期与程序一致。因此常驻内存。\n\npublic class Static Test{\n public static List list = new ArrayList<>();\n public void populateList(){\n for (int i = 0; i < 10000000; i++) {\n list.add((int)Math.random()); \n } \n System.out.println(\"running......\"); \n }\n public static void main(String[] args){ \n System.out.println(\"before......\");\n new StaticTest().populateList(); \n System.out.println(\"after......\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n\n\n现在可以使用jvisualvm运行一边,看看内存效果。\n\n * 带static关键字(使用静态变量)\n\n从上图可以看到,堆内存从一开始的135M左右飙升了到了200M。直接占据了65M的内存。\n\n * 不使用static关键字(不使用静态变量)\n\n由于全局变量与程序周期不一致,因此不使用时,就会进行回收。此时内存最高150M。\n\n总结:由于静态变量与程序生命周期一致,因此对象常驻内存,造成内存泄漏\n\n\n# 2、连接资源未关闭\n\n每当建立一个连接,jvm就会为这么资源分配内存。比如数据库连接、文件输入输出流、网络连接等等。\n\npublic class File Test{\n public static void main(String[] args)throws IOException {\n File f=new File(\"G:\\\\nginx配套资料\\\\笔记资料.zip\"); \n System.out.println(f.exists());\n System.out.println(f.isDirectory());\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n依然使用jvisualvm运行一边,看看内存效果。\n\n可以看出,在连接文件资源时,jvm会为本资源分配内存。\n\n3、equals()和hashCode()方法使用不当\n\n定义新类时,如果没有重新equals()和hashCode()方法,也有可能会造成内存泄漏。主要原因是没有这两个方法时,很容易造成重复的数据添加。看例子:\n\npublic class User{\n public String name;\n publicint age;\n public User(String name, int age){\n this.name = name;\n this.age = age;}\n }\n public class EqualTest{\n public static void main(String[] args){ \n Map map = new HashMap<>();\n for(int i=0; i<100; i++) { \n map.put(new User(\"\", 1), 1);\n } \n System.out.println(map.size() == 1);//输出为false \n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\n然后运行一下,看看内存情况:\n\n内存从150M一下子飙升到225M,可见飙升的厉害。输出为false,说明user对象被重复添加了。我们知道像HashMap在添加新的对象时,会对其hashcode进行比较,如果一样,那就不插入。如果一样那就插入。此时说明这100个User其hashcode不同。\n\n\n# 4、内部类持有外部类\n\n这个场景和上面类似。\n\n\n# 5、finalize方法\n\n这个方法之前曾经专门花过文章写过,这个问题很简单。看一张图\n\n这就是整个过程。不过在这里我们主要看的是finalize方法对垃圾回收的影响,其实就是在第三步,也就是这个对象含有finalize,进入了队列但一直没有被调用的这段时间,会一直占用内存。造成内存泄漏。\n\n\n# 6、ThreadLocal的错误使用\n\nThreadLocal主要用于创建本地线程变量,不合理的使用也有可能会造成内存泄漏。\n\n上面这张图详细的揭示了ThreadLocal和Thread以及ThreadLocalMap三者的关系。\n\n1、Thread中有一个map,就是ThreadLocalMap\n\n2、ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。\n\n3、ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收\n\n4、重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。\n\n解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。\n\n现在介绍了几种常见的内存泄漏情况,上面的知识点比较常见,最主要的是如何检测出来。\n\n\n# 三、检测内存泄漏\n\n检测的目的是定位内存泄漏出现的位置,常见的有以下几种方法:\n\n\n# 1、工具分析\n\n这个工具比较多,比如说JProfiler、YourKit、Java VisualVM和Netbeans Profiler。他可以帮助我们分析是哪一个对象或者是类内存的飙升。也可以看到内存CPU的等等各种情况。上面多次演示到了。\n\n\n# 2、垃圾回收分析\n\n这个其实也可以用工具进行分析。上面的VisualVM中,可以打印堆。也可以从外部导入dump文件进行分析。\n\n如果不用工具的话,我们可以通过IDE看到。JVM配置添加-verbose:gc。然后就会打印出相关信息。下面这张图非原创,来自Baeldung。\n\n\n# 3、基准测试\n\n也就是使用科学的方式进行分析java代码的性能。进而判断分析。\n\n\n# 四、结论\n\n内存泄漏是个很严重的问题,也比较常见。\n\n最主要的原因是动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。因此良好的代码规范,可以有效地避免这些错误。\n\n参考: 什么是内存泄漏?该如何检测?又该如何解决?", - "normalizedContent": "# 前言\n\n这个问题是我之前翻看面经的时候见到的。那位小姐姐把内存泄漏当成了内存溢出问题去解答的,结果当场挂掉了。为此总结一下,之前和一位老哥也讨论过这个问题。可见不管是面试还是工作这都是一个极为重要的点。\n\n我也曾在面阿里的时候也遇到过原题,题目是写出俩内存泄漏案例,然后问如何排查?如何解决?\n\n本篇文章大体结构来自外国大佬baeldung;\n\n\n# 一、介绍\n\n\n# 1、什么是内存泄漏\n\njava的优势之一就是内置了垃圾回收器gc,它帮助我们实现了自动化内存管理。但是gc再好,也有老马失前蹄的时候,它不能保证提供一个解决内存泄漏的万无一失的解决方案。什么是内存泄漏?可以看看下面这张图,\n\n也就是一部分内存空间我明明已经使用了,却没有引用指向这部分空间。造成这片已经使用的空间无法处理的情况。\n\n正规点的理解:动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。\n\n\n# 2、内存泄漏的危害\n\n长时间运行,程序变卡,性能严重下降 程序莫名其妙挂掉 outofmemoryerror错误 乱七八糟的错误,还不易排查 反正内存泄漏不是好事。\n\n\n# 二、内存泄漏原因\n\n内存泄漏原因太多了。说不定就是某一行代码不对就会出现这种情况,因此这里给出最常见的几种。关键的还是如何找出哪个地方出现了内存泄漏,代码好修改,错误不易查。\n\n\n# 1、大量使用静态变量\n\n静态变量的生命周期与程序一致。因此常驻内存。\n\npublic class static test{\n public static list list = new arraylist<>();\n public void populatelist(){\n for (int i = 0; i < 10000000; i++) {\n list.add((int)math.random()); \n } \n system.out.println(\"running......\"); \n }\n public static void main(string[] args){ \n system.out.println(\"before......\");\n new statictest().populatelist(); \n system.out.println(\"after......\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n\n\n现在可以使用jvisualvm运行一边,看看内存效果。\n\n * 带static关键字(使用静态变量)\n\n从上图可以看到,堆内存从一开始的135m左右飙升了到了200m。直接占据了65m的内存。\n\n * 不使用static关键字(不使用静态变量)\n\n由于全局变量与程序周期不一致,因此不使用时,就会进行回收。此时内存最高150m。\n\n总结:由于静态变量与程序生命周期一致,因此对象常驻内存,造成内存泄漏\n\n\n# 2、连接资源未关闭\n\n每当建立一个连接,jvm就会为这么资源分配内存。比如数据库连接、文件输入输出流、网络连接等等。\n\npublic class file test{\n public static void main(string[] args)throws ioexception {\n file f=new file(\"g:\\\\nginx配套资料\\\\笔记资料.zip\"); \n system.out.println(f.exists());\n system.out.println(f.isdirectory());\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n依然使用jvisualvm运行一边,看看内存效果。\n\n可以看出,在连接文件资源时,jvm会为本资源分配内存。\n\n3、equals()和hashcode()方法使用不当\n\n定义新类时,如果没有重新equals()和hashcode()方法,也有可能会造成内存泄漏。主要原因是没有这两个方法时,很容易造成重复的数据添加。看例子:\n\npublic class user{\n public string name;\n publicint age;\n public user(string name, int age){\n this.name = name;\n this.age = age;}\n }\n public class equaltest{\n public static void main(string[] args){ \n map map = new hashmap<>();\n for(int i=0; i<100; i++) { \n map.put(new user(\"\", 1), 1);\n } \n system.out.println(map.size() == 1);//输出为false \n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\n然后运行一下,看看内存情况:\n\n内存从150m一下子飙升到225m,可见飙升的厉害。输出为false,说明user对象被重复添加了。我们知道像hashmap在添加新的对象时,会对其hashcode进行比较,如果一样,那就不插入。如果一样那就插入。此时说明这100个user其hashcode不同。\n\n\n# 4、内部类持有外部类\n\n这个场景和上面类似。\n\n\n# 5、finalize方法\n\n这个方法之前曾经专门花过文章写过,这个问题很简单。看一张图\n\n这就是整个过程。不过在这里我们主要看的是finalize方法对垃圾回收的影响,其实就是在第三步,也就是这个对象含有finalize,进入了队列但一直没有被调用的这段时间,会一直占用内存。造成内存泄漏。\n\n\n# 6、threadlocal的错误使用\n\nthreadlocal主要用于创建本地线程变量,不合理的使用也有可能会造成内存泄漏。\n\n上面这张图详细的揭示了threadlocal和thread以及threadlocalmap三者的关系。\n\n1、thread中有一个map,就是threadlocalmap\n\n2、threadlocalmap的key是threadlocal,值是我们自己设定的。\n\n3、threadlocal是一个弱引用,当为null时,会被当成垃圾回收\n\n4、重点来了,突然我们threadlocal是null了,也就是要被垃圾回收器回收了,但是此时我们的threadlocalmap生命周期和thread的一样,它不会回收,这时候就出现了一个现象。那就是threadlocalmap的key没了,但是value还在,这就造成了内存泄漏。\n\n解决办法:使用完threadlocal后,执行remove操作,避免出现内存溢出情况。\n\n现在介绍了几种常见的内存泄漏情况,上面的知识点比较常见,最主要的是如何检测出来。\n\n\n# 三、检测内存泄漏\n\n检测的目的是定位内存泄漏出现的位置,常见的有以下几种方法:\n\n\n# 1、工具分析\n\n这个工具比较多,比如说jprofiler、yourkit、java visualvm和netbeans profiler。他可以帮助我们分析是哪一个对象或者是类内存的飙升。也可以看到内存cpu的等等各种情况。上面多次演示到了。\n\n\n# 2、垃圾回收分析\n\n这个其实也可以用工具进行分析。上面的visualvm中,可以打印堆。也可以从外部导入dump文件进行分析。\n\n如果不用工具的话,我们可以通过ide看到。jvm配置添加-verbose:gc。然后就会打印出相关信息。下面这张图非原创,来自baeldung。\n\n\n# 3、基准测试\n\n也就是使用科学的方式进行分析java代码的性能。进而判断分析。\n\n\n# 四、结论\n\n内存泄漏是个很严重的问题,也比较常见。\n\n最主要的原因是动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。因此良好的代码规范,可以有效地避免这些错误。\n\n参考: 什么是内存泄漏?该如何检测?又该如何解决?", + "headersStr": "Java 内存模型 Java 线程与主内存的关系 线程间通信 有态度的总结", + "content": "# Java 内存模型\n\n同一套内存模型规范,不同语言在实现上可能会有些差别。接下来着重讲一下 Java 内存模型实现原理。\n\nJava 运行时内存区域与硬件内存的关系\n\n了解过 JVM 的同学都知道,JVM 运行时内存区域是分片的,分为栈、堆等,其实这些都是 JVM 定义的逻辑概念。在传统的硬件内存架构中是没有栈和堆这种概念。\n\n\n\n从图中可以看出栈和堆既存在于高速缓存中又存在于主内存中,所以两者并没有很直接的关系。\n\n\n# Java 线程与主内存的关系\n\nJava 内存模型是一种规范,定义了很多东西:\n\n所有的变量都存储在主内存(Main Memory)中。 每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。 不同的线程之间无法直接访问对方本地内存中的变量。 看文字太枯燥了,我又画了一张图:\n\n\n# 线程间通信\n\n如果两个线程都对一个共享变量进行操作,共享变量初始值为 1,每个线程都变量进行加 1,预期共享变量的值为 3。在 JMM 规范下会有一系列的操作。\n\n\n\n为了更好的控制主内存和本地内存的交互,Java 内存模型定义了八种操作来实现:\n\n * lock:锁定。作用于主内存的变量,把一个变量标识为一条线程独占状态。\n * unlock:解锁。作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。\n * read:读取。作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用\n * load:载入。作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。\n * use:使用。作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。\n * assign:赋值。作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。\n * store:存储。作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。\n * write:写入。作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。 注意:工作内存也就是本地内存的意思。\n\n\n# 有态度的总结\n\n由于CPU 和主内存间存在数量级的速率差,想到了引入了多级高速缓存的传统硬件内存架构来解决,多级高速缓存作为 CPU 和主内间的缓冲提升了整体性能。解决了速率差的问题,却又带来了缓存一致性问题。\n\n数据同时存在于高速缓存和主内存中,如果不加以规范势必造成灾难,因此在传统机器上又抽象出了内存模型。\n\nJava 语言在遵循内存模型的基础上推出了 JMM 规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。\n\n为了更精准控制工作内存和主内存间的交互,JMM 还定义了八种操作:lock, unlock, read, load,use,assign, store, write。\n\n好了,今天就给大家介绍到这里,简单总结下Java内存模型的定义:Java内存模型并不是一件容易的事情,这个模型必须定义得足够严谨,才能让Java的并发操作不会产生歧义;但是,也必须得足够宽松,使得虚拟机的实现能有足够的自由空间去利用硬件的各种特性(寄存器、高速缓存等)来获取更好的执行速度。经过长时间的验证和修补,在JDK1.5发布后,Java内存模型就已经成熟和完善起来了。\n\n参考: 全面学习掌握Java内存模型", + "normalizedContent": "# java 内存模型\n\n同一套内存模型规范,不同语言在实现上可能会有些差别。接下来着重讲一下 java 内存模型实现原理。\n\njava 运行时内存区域与硬件内存的关系\n\n了解过 jvm 的同学都知道,jvm 运行时内存区域是分片的,分为栈、堆等,其实这些都是 jvm 定义的逻辑概念。在传统的硬件内存架构中是没有栈和堆这种概念。\n\n\n\n从图中可以看出栈和堆既存在于高速缓存中又存在于主内存中,所以两者并没有很直接的关系。\n\n\n# java 线程与主内存的关系\n\njava 内存模型是一种规范,定义了很多东西:\n\n所有的变量都存储在主内存(main memory)中。 每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。 不同的线程之间无法直接访问对方本地内存中的变量。 看文字太枯燥了,我又画了一张图:\n\n\n# 线程间通信\n\n如果两个线程都对一个共享变量进行操作,共享变量初始值为 1,每个线程都变量进行加 1,预期共享变量的值为 3。在 jmm 规范下会有一系列的操作。\n\n\n\n为了更好的控制主内存和本地内存的交互,java 内存模型定义了八种操作来实现:\n\n * lock:锁定。作用于主内存的变量,把一个变量标识为一条线程独占状态。\n * unlock:解锁。作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。\n * read:读取。作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用\n * load:载入。作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。\n * use:使用。作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。\n * assign:赋值。作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。\n * store:存储。作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。\n * write:写入。作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。 注意:工作内存也就是本地内存的意思。\n\n\n# 有态度的总结\n\n由于cpu 和主内存间存在数量级的速率差,想到了引入了多级高速缓存的传统硬件内存架构来解决,多级高速缓存作为 cpu 和主内间的缓冲提升了整体性能。解决了速率差的问题,却又带来了缓存一致性问题。\n\n数据同时存在于高速缓存和主内存中,如果不加以规范势必造成灾难,因此在传统机器上又抽象出了内存模型。\n\njava 语言在遵循内存模型的基础上推出了 jmm 规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。\n\n为了更精准控制工作内存和主内存间的交互,jmm 还定义了八种操作:lock, unlock, read, load,use,assign, store, write。\n\n好了,今天就给大家介绍到这里,简单总结下java内存模型的定义:java内存模型并不是一件容易的事情,这个模型必须定义得足够严谨,才能让java的并发操作不会产生歧义;但是,也必须得足够宽松,使得虚拟机的实现能有足够的自由空间去利用硬件的各种特性(寄存器、高速缓存等)来获取更好的执行速度。经过长时间的验证和修补,在jdk1.5发布后,java内存模型就已经成熟和完善起来了。\n\n参考: 全面学习掌握java内存模型", "charsets": { "cjk": true }, @@ -3371,40 +3371,132 @@ export const siteData = { "title": "Coding", "slug": "coding", "normalizedTitle": "coding", - "charIndex": 3382 + "charIndex": 3382 + }, + { + "level": 2, + "title": "cglib 代理", + "slug": "cglib-代理", + "normalizedTitle": "cglib 代理", + "charIndex": 261 + }, + { + "level": 3, + "title": "Coding", + "slug": "coding-2", + "normalizedTitle": "coding", + "charIndex": 3382 + }, + { + "level": 2, + "title": "代理模式适合应用场景", + "slug": "代理模式适合应用场景", + "normalizedTitle": "代理模式适合应用场景", + "charIndex": 7232 + }, + { + "level": 2, + "title": "AOP 中的代理模式", + "slug": "aop-中的代理模式", + "normalizedTitle": "aop 中的代理模式", + "charIndex": 7961 + } + ], + "headersStr": "基本介绍 问题 解决方案 静态代理 静态代理类优缺点 优点: 缺点: 动态代理 JDK 原生动态代理 Coding cglib 代理 Coding 代理模式适合应用场景 AOP 中的代理模式", + "content": "# 基本介绍\n\n代理模式是一种结构型设计模式。为对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象,并允许在将请求提交给对象前后进行一些处理。\n\n被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象。\n\n代理模式主要有三种不同的形式:\n\n * 静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的 .class 文件就已经存在了\n * 动态代理(JDK 代理、接口代理):在程序运行时运用反射机制动态创建而成,动态就是在程序运行时生成的,而不是编译时。\n * cglib 代理(可以在内存动态的创建对象,而不是实现接口,属于动态代理的范畴)\n\n\n# 问题\n\n为什么要控制对于某个对象的访问呢? 举个例子: 有这样一个消耗大量系统资源的巨型对象, 你只是偶尔需要使用它, 并非总是需要。\n\n\n\n你可以实现延迟初始化: 在实际有需要时再创建该对象。 对象的所有客户端都要执行延迟初始代码。 不幸的是, 这很可能会带来很多重复代码。\n\n在理想情况下, 我们希望将代码直接放入对象的类中, 但这并非总是能实现: 比如类可能是第三方封闭库的一部分。\n\n\n# 解决方案\n\n代理模式建议新建一个与原服务对象接口相同的代理类, 然后更新应用以将代理对象传递给所有原始对象客户端。 代理类接收到客户端请求后会创建实际的服务对象, 并将所有工作委派给它。\n\n\n\n代理将自己伪装成数据库对象, 可在客户端或实际数据库对象不知情的情况下处理延迟初始化和缓存查询结果的工作。\n\n这有什么好处呢? 如果需要在类的主要业务逻辑前后执行一些工作, 你无需修改类就能完成这项工作。 由于代理实现的接口与原类相同, 因此你可将其传递给任何一个使用实际服务对象的客户端。\n\n#代理模式结构\n\n1.服务接口 (Service Interface) 声明了服务接口。 代理必须遵循该接口才能伪装成服务对象。 2.服务 (Service) 类提供了一些实用的业务逻辑。 3.代理 (Proxy) 类包含一个指向服务对象的引用成员变量。 代理完成其任务 (例如延迟初始化、 记录日志、 访问控制和缓存等) 后会将请求传递给服务对象。 通常情况下, 代理会对其服务对象的整个生命周期进行管理。 4.客户端 (Client) 能通过同一接口与服务或代理进行交互, 所以你可在一切需要服务对象的代码中使用代理。 打游戏有代练、买卖房子有中介代理、再比如一般公司投互联网广告也可以找代理公司,这里的代练、中介、广告代理公司扮演的角色都是代理。\n\n这里举个更接近程序员的例子,比如有些变态的公司不允许在公司刷微博,看视频,可以通过一层代理来限制我们访问这些网站。\n\n废话不多说,先来个静态代理。\n\n\n# 静态代理\n\n1、定义网络接口\n\npublic interface Internet {\n void connectTo(String serverHost) throws Exception;\n}\n\n\n1\n2\n3\n\n\n2、真正的网络连接\n\npublic class RealInternet implements Internet{\n\n @Override\n public void connectTo(String serverHost) throws Exception {\n System.out.println(\"Connecting to \"+ serverHost);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n3、公司的网络代理\n\npublic class ProxyInternet implements Internet {\n\n //目标对象,通过接口聚合\n private Internet internet;\n\n // 通过构造方法传入目标对象\n public ProxyInternet(Internet internet){\n this.internet = internet;\n }\n //网络黑名单\n private static List bannedSites;\n\n static\n {\n bannedSites = new ArrayList();\n bannedSites.add(\"bilibili.com\");\n bannedSites.add(\"youtube.com\");\n bannedSites.add(\"weibo.com\");\n bannedSites.add(\"qq.com\");\n }\n\n @Override\n public void connectTo(String serverhost) throws Exception {\n // 添加限制功能\n if(bannedSites.contains(serverhost.toLowerCase()))\n {\n throw new Exception(\"Access Denied:\"+serverhost);\n }\n internet.connectTo(serverhost);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n\n\n4、客户端验证\n\npublic class Client {\n\n public static void main(String[] args) {\n Internet internet = new ProxyInternet(new RealInternet());\n try {\n internet.connectTo(\"so.com\");\n internet.connectTo(\"qq.com\");\n } catch (Exception e) {\n System.out.println(e.getMessage());\n }\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n5、输出\n\nConnecting to so.com\nAccess Denied:qq.com\n\n\n1\n2\n\n\n不能访问娱乐性网站,但是可以用 360 搜索,SO 靠谱,哈哈\n\n\n# 静态代理类优缺点\n\n\n# 优点:\n\n在不修改目标对象的前提下,可以通过代理对象对目标对象功能扩展\n\n代理使客户端不需要知道实现类是什么,怎么做的,而客户端只需知道代理即可(解耦合),对于如上的客户端代码,RealInterner() 可以应用工厂将它隐藏。\n\n\n# 缺点:\n\n代理类和委托类实现了相同的接口,代理类通过委托类实现了相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。\n\n代理对象只服务于一种类型的对象,如果要服务多类型的对象。势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了。\n\n\n# 动态代理\n\n静态代理会产生很多静态类,所以我们要想办法可以通过一个代理类完成全部的代理功能,这就引出了动态代理。\n\n\n# JDK 原生动态代理\n\n * 代理对象,不需要实现接口,但是目标对象要实现接口,否则不能用动态代理\n * 代理对象的生成,是通过 JDK 的 API(反射机制),动态的在内存中构建代理对象 在 Java 中要想实现动态代理机制,需要 java.lang.reflect.InvocationHandler 接口和 java.lang.reflect.Proxy 类的支持\n\n\n# Coding\n\n1、网络接口不变\n\npublic interface Internet { void connectTo(String serverHost) throws Exception; } 2、真正的网络连接,也不会改变\n\npublic class RealInternet implements Internet{\n\n@Override\npublic void connectTo(String serverHost) throws Exception {\n System.out.println(\"Connecting to \"+ serverHost);\n}\n\n\n} 3、动态代理,需要实现 InvocationHandler,我们用 Lambda 表达式简化下\n\npublic class ProxyFactory {\n\n/**\n * 维护一个目标对象\n **/\nprivate Object target;\n\n/**\n * 构造器,初始化目标对象\n **/\npublic ProxyFactory(Object target) {\n this.target = target;\n}\n\npublic Object getProxyInstance() {\n\n /**\n 被代理对象target通过参数传递进来,\n 通过target.getClass().getClassLoader()获取ClassLoader对象,\n 然后通过target.getClass().getInterfaces()获取它实现的所有接口,\n 再将target包装到实现了InvocationHandler接口的对象中。\n 通过newProxyInstance函数我们就获得了一个动态代理对象。\n */\n return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {\n @Override\n public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {\n if(bannedSites.contains(args[0].toString().toLowerCase()))\n {\n throw new Exception(\"Access Denied:\"+args[0]);\n }\n //反射机制调用目标对象的方法\n Object obj = method.invoke(target, args);\n return obj;\n }\n });\n}\n\nprivate static List bannedSites;\n\nstatic\n{\n bannedSites = new ArrayList();\n bannedSites.add(\"bilibili.com\");\n bannedSites.add(\"youtube.com\");\n bannedSites.add(\"weibo.com\");\n bannedSites.add(\"qq.com\");\n}\n\n\n} 4、客户端\n\npublic class Client {\n\npublic static void main(String[] args) {\n Internet internet = new ProxyInternet(new RealInternet());\n try {\n internet.connectTo(\"360.cn\");\n internet.connectTo(\"qq.com\");\n } catch (Exception e) {\n System.out.println(e.getMessage());\n }\n}\n\n\n} 动态代理的方式中,所有的函数调用最终都会经过 invoke 函数的转发,因此我们就可以在这里做一些自己想做的操作,比如日志系统、事务、拦截器、权限控制等。\n\n\n# cglib 代理\n\n静态代理和 JDK 代理模式都要求目标对象实现一个接口,但有时候目标对象只是一个单独的对象,并没有实现任何接口,这个时候就可以使用目标对象子类来实现代理,这就是 cglib 代理。\n\ncglib (opens new window)(Code Generation Library)是一个基于 ASM 的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。cglib 通过继承方式实现代理。它广泛的被许多 AOP 的框架使用,比如我们的 Spring AOP。\n\ncglib 包的底层是通过使用字节码处理框架 ASM 来转换字节码并生成新的类。\n\ncglib 代理也被叫做子类代理,它是在内存中构建一个子类对象从而实现目标对象功能扩展。\n\n\n# Coding\n\n添加 cglib 依赖\n\n\n cglib\n cglib\n 3.3.0\n\n\n\n1\n2\n3\n4\n5\n\n\n1、不需要接口\n\npublic class RealInternet{\n\n public void connectTo(String serverHost) {\n System.out.println(\"Connecting to \"+ serverHost);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n2、代理工厂类\n\npublic class ProxyFactory implements MethodInterceptor {\n\n private Object target;\n\n public ProxyFactory(Object target){\n this.target = target;\n }\n\n @Override\n public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {\n System.out.println(\"cglib 代理开始,可以添加逻辑\");\n Object obj = method.invoke(target,objects);\n System.out.println(\"cglib 代理结束\");\n return obj;\n }\n\n\n public Object getProxyInstance(){\n //工具类,类似于JDK动态代理的Proxy类\n Enhancer enhancer = new Enhancer();\n //设置父类\n enhancer.setSuperclass(target.getClass());\n //设置回调函数\n enhancer.setCallback(this);\n //创建子类对象,即代理对象\n return enhancer.create();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n\n\n3、客户端\n\npublic class Client {\n\n public static void main(String[] args) {\n\n //目标对象\n RealInternet target = new RealInternet();\n //获取代理对象,并且将目标对象传递给代理对象\n RealInternet internet = (RealInternet) new ProxyFactory(target).getProxyInstance();\n internet.connectTo(\"so.cn\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n4、输出\n\ncglib 代理开始,可以添加逻辑\nConnecting to so.cn\ncglib 代理结束\n\n\n1\n2\n3\n\n\n\n# 代理模式适合应用场景\n\n使用代理模式的方式多种多样, 我们来看看最常见的几种。\n\n * 延迟初始化 (虚拟代理):如果你有一个偶尔使用的重量级服务对象, 一直保持该对象运行会消耗系统资源时, 可使用代理模式。\n\n你无需在程序启动时就创建该对象, 可将对象的初始化延迟到真正有需要的时候。\n\n * 访问控制 (保护代理):如果你只希望特定客户端使用服务对象, 这里的对象可以是操作系统中非常重要的部分, 而客户端则是各种已启动的程序 (包括恶意程序), 此时可使用代理模式。\n\n代理可仅在客户端凭据满足要求时将请求传递给服务对象。\n\n * 本地执行远程服务 (远程代理):适用于服务对象位于远程服务器上的情形。\n\n在这种情形中, 代理通过网络传递客户端请求, 负责处理所有与网络相关的复杂细节。\n\n * 记录日志请求 (日志记录代理):适用于当你需要保存对于服务对象的请求历史记录时。 代理可以在向服务传递请求前进行记录。\n\n * 缓存请求结果 (缓存代理):适用于需要缓存客户请求结果并对缓存生命周期进行管理时, 特别是当返回结果的体积非常大时。\n\n代理可对重复请求所需的相同结果进行缓存, 还可使用请求参数作为索引缓存的键值。比如请求图片、文件等资源时,先到代理缓存取,如果没有就去公网取并缓存到代理服务器\n\n * 智能引用:可在没有客户端使用某个重量级对象时立即销毁该对象。\n\n代理会将所有获取了指向服务对象或其结果的客户端记录在案。 代理会时不时地遍历各个客户端, 检查它们是否仍在运行。 如果相应的客户端列表为空, 代理就会销毁该服务对象, 释放底层系统资源。\n\n代理还可以记录客户端是否修改了服务对象。 其他客户端还可以复用未修改的对象。\n\n\n# AOP 中的代理模式\n\nAOP(面向切面编程)主要的的实现技术主要有 Spring AOP 和 AspectJ\n\nAspectJ 的底层技术就是静态代理,用一种 AspectJ 支持的特定语言编写切面,通过一个命令来编译,生成一个新的代理类,该代理类增强了业务类,这是在编译时增强,相对于下面说的运行时增强,编译时增强的性能更好。(AspectJ 的静态代理,不像我们前边介绍的需要为每一个目标类手动编写一个代理类,AspectJ 框架可以在编译时就生成目标类的“代理类”,在这里加了个冒号,是因为实际上它并没有生成一个新的类,而是把代理逻辑直接编译到目标类里面了)\n\nSpring AOP 采用的是动态代理,在运行期间对业务方法进行增强,所以不会生成新类,对于动态代理技术,Spring AOP 提供了对 JDK 动态代理的支持以及 CGLib 的支持。\n\n默认情况下,Spring 对实现了接口的类使用 JDK Proxy 方式,否则的话使用 CGLib。不过可以通过配置指定 Spring AOP 都通过 CGLib 来生成代理类。\n\n具体逻辑在 org.springframework.aop.framework.DefaultAopProxyFactory 类中,使用哪种方式生成由 AopProxy 根据 AdvisedSupport 对象的配置来决定源码如下:\n\npublic class DefaultAopProxyFactory implements AopProxyFactory, Serializable {\n public DefaultAopProxyFactory() {\n }\n\n public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {\n if (!config.isOptimize() && !config.isProxyTargetClass() && !this.hasNoUserSuppliedProxyInterfaces(config)) {\n return new JdkDynamicAopProxy(config);\n } else {\n Class targetClass = config.getTargetClass();\n if (targetClass == null) {\n throw new AopConfigException(\"TargetSource cannot determine target class: Either an interface or a target is required for proxy creation.\");\n } else {\n //如果目标类是接口且是代理类, 使用JDK动态代理类,否则使用Cglib生成代理类\n return (AopProxy)(!targetClass.isInterface() && !Proxy.isProxyClass(targetClass) ? new ObjenesisCglibAopProxy(config) : new JdkDynamicAopProxy(config));\n }\n }\n }\n\n private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n\n\n参考: 代理模式", + "normalizedContent": "# 基本介绍\n\n代理模式是一种结构型设计模式。为对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象,并允许在将请求提交给对象前后进行一些处理。\n\n被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象。\n\n代理模式主要有三种不同的形式:\n\n * 静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的 .class 文件就已经存在了\n * 动态代理(jdk 代理、接口代理):在程序运行时运用反射机制动态创建而成,动态就是在程序运行时生成的,而不是编译时。\n * cglib 代理(可以在内存动态的创建对象,而不是实现接口,属于动态代理的范畴)\n\n\n# 问题\n\n为什么要控制对于某个对象的访问呢? 举个例子: 有这样一个消耗大量系统资源的巨型对象, 你只是偶尔需要使用它, 并非总是需要。\n\n\n\n你可以实现延迟初始化: 在实际有需要时再创建该对象。 对象的所有客户端都要执行延迟初始代码。 不幸的是, 这很可能会带来很多重复代码。\n\n在理想情况下, 我们希望将代码直接放入对象的类中, 但这并非总是能实现: 比如类可能是第三方封闭库的一部分。\n\n\n# 解决方案\n\n代理模式建议新建一个与原服务对象接口相同的代理类, 然后更新应用以将代理对象传递给所有原始对象客户端。 代理类接收到客户端请求后会创建实际的服务对象, 并将所有工作委派给它。\n\n\n\n代理将自己伪装成数据库对象, 可在客户端或实际数据库对象不知情的情况下处理延迟初始化和缓存查询结果的工作。\n\n这有什么好处呢? 如果需要在类的主要业务逻辑前后执行一些工作, 你无需修改类就能完成这项工作。 由于代理实现的接口与原类相同, 因此你可将其传递给任何一个使用实际服务对象的客户端。\n\n#代理模式结构\n\n1.服务接口 (service interface) 声明了服务接口。 代理必须遵循该接口才能伪装成服务对象。 2.服务 (service) 类提供了一些实用的业务逻辑。 3.代理 (proxy) 类包含一个指向服务对象的引用成员变量。 代理完成其任务 (例如延迟初始化、 记录日志、 访问控制和缓存等) 后会将请求传递给服务对象。 通常情况下, 代理会对其服务对象的整个生命周期进行管理。 4.客户端 (client) 能通过同一接口与服务或代理进行交互, 所以你可在一切需要服务对象的代码中使用代理。 打游戏有代练、买卖房子有中介代理、再比如一般公司投互联网广告也可以找代理公司,这里的代练、中介、广告代理公司扮演的角色都是代理。\n\n这里举个更接近程序员的例子,比如有些变态的公司不允许在公司刷微博,看视频,可以通过一层代理来限制我们访问这些网站。\n\n废话不多说,先来个静态代理。\n\n\n# 静态代理\n\n1、定义网络接口\n\npublic interface internet {\n void connectto(string serverhost) throws exception;\n}\n\n\n1\n2\n3\n\n\n2、真正的网络连接\n\npublic class realinternet implements internet{\n\n @override\n public void connectto(string serverhost) throws exception {\n system.out.println(\"connecting to \"+ serverhost);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n3、公司的网络代理\n\npublic class proxyinternet implements internet {\n\n //目标对象,通过接口聚合\n private internet internet;\n\n // 通过构造方法传入目标对象\n public proxyinternet(internet internet){\n this.internet = internet;\n }\n //网络黑名单\n private static list bannedsites;\n\n static\n {\n bannedsites = new arraylist();\n bannedsites.add(\"bilibili.com\");\n bannedsites.add(\"youtube.com\");\n bannedsites.add(\"weibo.com\");\n bannedsites.add(\"qq.com\");\n }\n\n @override\n public void connectto(string serverhost) throws exception {\n // 添加限制功能\n if(bannedsites.contains(serverhost.tolowercase()))\n {\n throw new exception(\"access denied:\"+serverhost);\n }\n internet.connectto(serverhost);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n\n\n4、客户端验证\n\npublic class client {\n\n public static void main(string[] args) {\n internet internet = new proxyinternet(new realinternet());\n try {\n internet.connectto(\"so.com\");\n internet.connectto(\"qq.com\");\n } catch (exception e) {\n system.out.println(e.getmessage());\n }\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n5、输出\n\nconnecting to so.com\naccess denied:qq.com\n\n\n1\n2\n\n\n不能访问娱乐性网站,但是可以用 360 搜索,so 靠谱,哈哈\n\n\n# 静态代理类优缺点\n\n\n# 优点:\n\n在不修改目标对象的前提下,可以通过代理对象对目标对象功能扩展\n\n代理使客户端不需要知道实现类是什么,怎么做的,而客户端只需知道代理即可(解耦合),对于如上的客户端代码,realinterner() 可以应用工厂将它隐藏。\n\n\n# 缺点:\n\n代理类和委托类实现了相同的接口,代理类通过委托类实现了相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。\n\n代理对象只服务于一种类型的对象,如果要服务多类型的对象。势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了。\n\n\n# 动态代理\n\n静态代理会产生很多静态类,所以我们要想办法可以通过一个代理类完成全部的代理功能,这就引出了动态代理。\n\n\n# jdk 原生动态代理\n\n * 代理对象,不需要实现接口,但是目标对象要实现接口,否则不能用动态代理\n * 代理对象的生成,是通过 jdk 的 api(反射机制),动态的在内存中构建代理对象 在 java 中要想实现动态代理机制,需要 java.lang.reflect.invocationhandler 接口和 java.lang.reflect.proxy 类的支持\n\n\n# coding\n\n1、网络接口不变\n\npublic interface internet { void connectto(string serverhost) throws exception; } 2、真正的网络连接,也不会改变\n\npublic class realinternet implements internet{\n\n@override\npublic void connectto(string serverhost) throws exception {\n system.out.println(\"connecting to \"+ serverhost);\n}\n\n\n} 3、动态代理,需要实现 invocationhandler,我们用 lambda 表达式简化下\n\npublic class proxyfactory {\n\n/**\n * 维护一个目标对象\n **/\nprivate object target;\n\n/**\n * 构造器,初始化目标对象\n **/\npublic proxyfactory(object target) {\n this.target = target;\n}\n\npublic object getproxyinstance() {\n\n /**\n 被代理对象target通过参数传递进来,\n 通过target.getclass().getclassloader()获取classloader对象,\n 然后通过target.getclass().getinterfaces()获取它实现的所有接口,\n 再将target包装到实现了invocationhandler接口的对象中。\n 通过newproxyinstance函数我们就获得了一个动态代理对象。\n */\n return proxy.newproxyinstance(target.getclass().getclassloader(), target.getclass().getinterfaces(), new invocationhandler() {\n @override\n public object invoke(object proxy, method method, object[] args) throws throwable {\n if(bannedsites.contains(args[0].tostring().tolowercase()))\n {\n throw new exception(\"access denied:\"+args[0]);\n }\n //反射机制调用目标对象的方法\n object obj = method.invoke(target, args);\n return obj;\n }\n });\n}\n\nprivate static list bannedsites;\n\nstatic\n{\n bannedsites = new arraylist();\n bannedsites.add(\"bilibili.com\");\n bannedsites.add(\"youtube.com\");\n bannedsites.add(\"weibo.com\");\n bannedsites.add(\"qq.com\");\n}\n\n\n} 4、客户端\n\npublic class client {\n\npublic static void main(string[] args) {\n internet internet = new proxyinternet(new realinternet());\n try {\n internet.connectto(\"360.cn\");\n internet.connectto(\"qq.com\");\n } catch (exception e) {\n system.out.println(e.getmessage());\n }\n}\n\n\n} 动态代理的方式中,所有的函数调用最终都会经过 invoke 函数的转发,因此我们就可以在这里做一些自己想做的操作,比如日志系统、事务、拦截器、权限控制等。\n\n\n# cglib 代理\n\n静态代理和 jdk 代理模式都要求目标对象实现一个接口,但有时候目标对象只是一个单独的对象,并没有实现任何接口,这个时候就可以使用目标对象子类来实现代理,这就是 cglib 代理。\n\ncglib (opens new window)(code generation library)是一个基于 asm 的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。cglib 通过继承方式实现代理。它广泛的被许多 aop 的框架使用,比如我们的 spring aop。\n\ncglib 包的底层是通过使用字节码处理框架 asm 来转换字节码并生成新的类。\n\ncglib 代理也被叫做子类代理,它是在内存中构建一个子类对象从而实现目标对象功能扩展。\n\n\n# coding\n\n添加 cglib 依赖\n\n\n cglib\n cglib\n 3.3.0\n\n\n\n1\n2\n3\n4\n5\n\n\n1、不需要接口\n\npublic class realinternet{\n\n public void connectto(string serverhost) {\n system.out.println(\"connecting to \"+ serverhost);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n2、代理工厂类\n\npublic class proxyfactory implements methodinterceptor {\n\n private object target;\n\n public proxyfactory(object target){\n this.target = target;\n }\n\n @override\n public object intercept(object o, method method, object[] objects, methodproxy methodproxy) throws throwable {\n system.out.println(\"cglib 代理开始,可以添加逻辑\");\n object obj = method.invoke(target,objects);\n system.out.println(\"cglib 代理结束\");\n return obj;\n }\n\n\n public object getproxyinstance(){\n //工具类,类似于jdk动态代理的proxy类\n enhancer enhancer = new enhancer();\n //设置父类\n enhancer.setsuperclass(target.getclass());\n //设置回调函数\n enhancer.setcallback(this);\n //创建子类对象,即代理对象\n return enhancer.create();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n\n\n3、客户端\n\npublic class client {\n\n public static void main(string[] args) {\n\n //目标对象\n realinternet target = new realinternet();\n //获取代理对象,并且将目标对象传递给代理对象\n realinternet internet = (realinternet) new proxyfactory(target).getproxyinstance();\n internet.connectto(\"so.cn\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n4、输出\n\ncglib 代理开始,可以添加逻辑\nconnecting to so.cn\ncglib 代理结束\n\n\n1\n2\n3\n\n\n\n# 代理模式适合应用场景\n\n使用代理模式的方式多种多样, 我们来看看最常见的几种。\n\n * 延迟初始化 (虚拟代理):如果你有一个偶尔使用的重量级服务对象, 一直保持该对象运行会消耗系统资源时, 可使用代理模式。\n\n你无需在程序启动时就创建该对象, 可将对象的初始化延迟到真正有需要的时候。\n\n * 访问控制 (保护代理):如果你只希望特定客户端使用服务对象, 这里的对象可以是操作系统中非常重要的部分, 而客户端则是各种已启动的程序 (包括恶意程序), 此时可使用代理模式。\n\n代理可仅在客户端凭据满足要求时将请求传递给服务对象。\n\n * 本地执行远程服务 (远程代理):适用于服务对象位于远程服务器上的情形。\n\n在这种情形中, 代理通过网络传递客户端请求, 负责处理所有与网络相关的复杂细节。\n\n * 记录日志请求 (日志记录代理):适用于当你需要保存对于服务对象的请求历史记录时。 代理可以在向服务传递请求前进行记录。\n\n * 缓存请求结果 (缓存代理):适用于需要缓存客户请求结果并对缓存生命周期进行管理时, 特别是当返回结果的体积非常大时。\n\n代理可对重复请求所需的相同结果进行缓存, 还可使用请求参数作为索引缓存的键值。比如请求图片、文件等资源时,先到代理缓存取,如果没有就去公网取并缓存到代理服务器\n\n * 智能引用:可在没有客户端使用某个重量级对象时立即销毁该对象。\n\n代理会将所有获取了指向服务对象或其结果的客户端记录在案。 代理会时不时地遍历各个客户端, 检查它们是否仍在运行。 如果相应的客户端列表为空, 代理就会销毁该服务对象, 释放底层系统资源。\n\n代理还可以记录客户端是否修改了服务对象。 其他客户端还可以复用未修改的对象。\n\n\n# aop 中的代理模式\n\naop(面向切面编程)主要的的实现技术主要有 spring aop 和 aspectj\n\naspectj 的底层技术就是静态代理,用一种 aspectj 支持的特定语言编写切面,通过一个命令来编译,生成一个新的代理类,该代理类增强了业务类,这是在编译时增强,相对于下面说的运行时增强,编译时增强的性能更好。(aspectj 的静态代理,不像我们前边介绍的需要为每一个目标类手动编写一个代理类,aspectj 框架可以在编译时就生成目标类的“代理类”,在这里加了个冒号,是因为实际上它并没有生成一个新的类,而是把代理逻辑直接编译到目标类里面了)\n\nspring aop 采用的是动态代理,在运行期间对业务方法进行增强,所以不会生成新类,对于动态代理技术,spring aop 提供了对 jdk 动态代理的支持以及 cglib 的支持。\n\n默认情况下,spring 对实现了接口的类使用 jdk proxy 方式,否则的话使用 cglib。不过可以通过配置指定 spring aop 都通过 cglib 来生成代理类。\n\n具体逻辑在 org.springframework.aop.framework.defaultaopproxyfactory 类中,使用哪种方式生成由 aopproxy 根据 advisedsupport 对象的配置来决定源码如下:\n\npublic class defaultaopproxyfactory implements aopproxyfactory, serializable {\n public defaultaopproxyfactory() {\n }\n\n public aopproxy createaopproxy(advisedsupport config) throws aopconfigexception {\n if (!config.isoptimize() && !config.isproxytargetclass() && !this.hasnousersuppliedproxyinterfaces(config)) {\n return new jdkdynamicaopproxy(config);\n } else {\n class targetclass = config.gettargetclass();\n if (targetclass == null) {\n throw new aopconfigexception(\"targetsource cannot determine target class: either an interface or a target is required for proxy creation.\");\n } else {\n //如果目标类是接口且是代理类, 使用jdk动态代理类,否则使用cglib生成代理类\n return (aopproxy)(!targetclass.isinterface() && !proxy.isproxyclass(targetclass) ? new objenesiscglibaopproxy(config) : new jdkdynamicaopproxy(config));\n }\n }\n }\n\n private boolean hasnousersuppliedproxyinterfaces(advisedsupport config) {\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n\n\n参考: 代理模式", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/20, 23:34:59", + "lastUpdatedTimestamp": 1663688099000 + }, + { + "title": "责任链模式", + "frontmatter": { + "title": "责任链模式", + "date": "2022-05-21T17:28:46.000Z", + "permalink": "/pages/d3ff35/", + "categories": [ + "Java相关", + "设计模式" + ], + "tags": [ + null + ] + }, + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/030.%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F.html", + "relativePath": "01.Java相关/40.设计模式/030.责任链模式.md", + "key": "v-34f8aaa2", + "path": "/pages/d3ff35/", + "headers": [ + { + "level": 2, + "title": "定义", + "slug": "定义", + "normalizedTitle": "定义", + "charIndex": 1127 + }, + { + "level": 2, + "title": "角色", + "slug": "角色", + "normalizedTitle": "角色", + "charIndex": 1246 + }, + { + "level": 2, + "title": "类图", + "slug": "类图", + "normalizedTitle": "类图", + "charIndex": 1521 + }, + { + "level": 2, + "title": "coding", + "slug": "coding", + "normalizedTitle": "coding", + "charIndex": 1530 }, { "level": 2, - "title": "cglib 代理", - "slug": "cglib-代理", - "normalizedTitle": "cglib 代理", - "charIndex": 261 + "title": "实例", + "slug": "实例", + "normalizedTitle": "实例", + "charIndex": 3309 + }, + { + "level": 2, + "title": "应用", + "slug": "应用", + "normalizedTitle": "应用", + "charIndex": 784 }, { "level": 3, - "title": "Coding", - "slug": "coding-2", - "normalizedTitle": "coding", - "charIndex": 3382 + "title": "Servlet 中的责任链", + "slug": "servlet-中的责任链", + "normalizedTitle": "servlet 中的责任链", + "charIndex": 3615 }, { - "level": 2, - "title": "代理模式适合应用场景", - "slug": "代理模式适合应用场景", - "normalizedTitle": "代理模式适合应用场景", - "charIndex": 7232 + "level": 3, + "title": "Spring MVC 中的责任链", + "slug": "spring-mvc-中的责任链", + "normalizedTitle": "spring mvc 中的责任链", + "charIndex": 5641 }, { "level": 2, - "title": "AOP 中的代理模式", - "slug": "aop-中的代理模式", - "normalizedTitle": "aop 中的代理模式", - "charIndex": 7961 + "title": "总结", + "slug": "总结", + "normalizedTitle": "总结", + "charIndex": 8299 } ], - "headersStr": "基本介绍 问题 解决方案 静态代理 静态代理类优缺点 优点: 缺点: 动态代理 JDK 原生动态代理 Coding cglib 代理 Coding 代理模式适合应用场景 AOP 中的代理模式", - "content": "# 基本介绍\n\n代理模式是一种结构型设计模式。为对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象,并允许在将请求提交给对象前后进行一些处理。\n\n被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象。\n\n代理模式主要有三种不同的形式:\n\n * 静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的 .class 文件就已经存在了\n * 动态代理(JDK 代理、接口代理):在程序运行时运用反射机制动态创建而成,动态就是在程序运行时生成的,而不是编译时。\n * cglib 代理(可以在内存动态的创建对象,而不是实现接口,属于动态代理的范畴)\n\n\n# 问题\n\n为什么要控制对于某个对象的访问呢? 举个例子: 有这样一个消耗大量系统资源的巨型对象, 你只是偶尔需要使用它, 并非总是需要。\n\n\n\n你可以实现延迟初始化: 在实际有需要时再创建该对象。 对象的所有客户端都要执行延迟初始代码。 不幸的是, 这很可能会带来很多重复代码。\n\n在理想情况下, 我们希望将代码直接放入对象的类中, 但这并非总是能实现: 比如类可能是第三方封闭库的一部分。\n\n\n# 解决方案\n\n代理模式建议新建一个与原服务对象接口相同的代理类, 然后更新应用以将代理对象传递给所有原始对象客户端。 代理类接收到客户端请求后会创建实际的服务对象, 并将所有工作委派给它。\n\n\n\n代理将自己伪装成数据库对象, 可在客户端或实际数据库对象不知情的情况下处理延迟初始化和缓存查询结果的工作。\n\n这有什么好处呢? 如果需要在类的主要业务逻辑前后执行一些工作, 你无需修改类就能完成这项工作。 由于代理实现的接口与原类相同, 因此你可将其传递给任何一个使用实际服务对象的客户端。\n\n#代理模式结构\n\n1.服务接口 (Service Interface) 声明了服务接口。 代理必须遵循该接口才能伪装成服务对象。 2.服务 (Service) 类提供了一些实用的业务逻辑。 3.代理 (Proxy) 类包含一个指向服务对象的引用成员变量。 代理完成其任务 (例如延迟初始化、 记录日志、 访问控制和缓存等) 后会将请求传递给服务对象。 通常情况下, 代理会对其服务对象的整个生命周期进行管理。 4.客户端 (Client) 能通过同一接口与服务或代理进行交互, 所以你可在一切需要服务对象的代码中使用代理。 打游戏有代练、买卖房子有中介代理、再比如一般公司投互联网广告也可以找代理公司,这里的代练、中介、广告代理公司扮演的角色都是代理。\n\n这里举个更接近程序员的例子,比如有些变态的公司不允许在公司刷微博,看视频,可以通过一层代理来限制我们访问这些网站。\n\n废话不多说,先来个静态代理。\n\n\n# 静态代理\n\n1、定义网络接口\n\npublic interface Internet {\n void connectTo(String serverHost) throws Exception;\n}\n\n\n1\n2\n3\n\n\n2、真正的网络连接\n\npublic class RealInternet implements Internet{\n\n @Override\n public void connectTo(String serverHost) throws Exception {\n System.out.println(\"Connecting to \"+ serverHost);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n3、公司的网络代理\n\npublic class ProxyInternet implements Internet {\n\n //目标对象,通过接口聚合\n private Internet internet;\n\n // 通过构造方法传入目标对象\n public ProxyInternet(Internet internet){\n this.internet = internet;\n }\n //网络黑名单\n private static List bannedSites;\n\n static\n {\n bannedSites = new ArrayList();\n bannedSites.add(\"bilibili.com\");\n bannedSites.add(\"youtube.com\");\n bannedSites.add(\"weibo.com\");\n bannedSites.add(\"qq.com\");\n }\n\n @Override\n public void connectTo(String serverhost) throws Exception {\n // 添加限制功能\n if(bannedSites.contains(serverhost.toLowerCase()))\n {\n throw new Exception(\"Access Denied:\"+serverhost);\n }\n internet.connectTo(serverhost);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n\n\n4、客户端验证\n\npublic class Client {\n\n public static void main(String[] args) {\n Internet internet = new ProxyInternet(new RealInternet());\n try {\n internet.connectTo(\"so.com\");\n internet.connectTo(\"qq.com\");\n } catch (Exception e) {\n System.out.println(e.getMessage());\n }\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n5、输出\n\nConnecting to so.com\nAccess Denied:qq.com\n\n\n1\n2\n\n\n不能访问娱乐性网站,但是可以用 360 搜索,SO 靠谱,哈哈\n\n\n# 静态代理类优缺点\n\n\n# 优点:\n\n在不修改目标对象的前提下,可以通过代理对象对目标对象功能扩展\n\n代理使客户端不需要知道实现类是什么,怎么做的,而客户端只需知道代理即可(解耦合),对于如上的客户端代码,RealInterner() 可以应用工厂将它隐藏。\n\n\n# 缺点:\n\n代理类和委托类实现了相同的接口,代理类通过委托类实现了相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。\n\n代理对象只服务于一种类型的对象,如果要服务多类型的对象。势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了。\n\n\n# 动态代理\n\n静态代理会产生很多静态类,所以我们要想办法可以通过一个代理类完成全部的代理功能,这就引出了动态代理。\n\n\n# JDK 原生动态代理\n\n * 代理对象,不需要实现接口,但是目标对象要实现接口,否则不能用动态代理\n * 代理对象的生成,是通过 JDK 的 API(反射机制),动态的在内存中构建代理对象 在 Java 中要想实现动态代理机制,需要 java.lang.reflect.InvocationHandler 接口和 java.lang.reflect.Proxy 类的支持\n\n\n# Coding\n\n1、网络接口不变\n\npublic interface Internet { void connectTo(String serverHost) throws Exception; } 2、真正的网络连接,也不会改变\n\npublic class RealInternet implements Internet{\n\n@Override\npublic void connectTo(String serverHost) throws Exception {\n System.out.println(\"Connecting to \"+ serverHost);\n}\n\n\n} 3、动态代理,需要实现 InvocationHandler,我们用 Lambda 表达式简化下\n\npublic class ProxyFactory {\n\n/**\n * 维护一个目标对象\n **/\nprivate Object target;\n\n/**\n * 构造器,初始化目标对象\n **/\npublic ProxyFactory(Object target) {\n this.target = target;\n}\n\npublic Object getProxyInstance() {\n\n /**\n 被代理对象target通过参数传递进来,\n 通过target.getClass().getClassLoader()获取ClassLoader对象,\n 然后通过target.getClass().getInterfaces()获取它实现的所有接口,\n 再将target包装到实现了InvocationHandler接口的对象中。\n 通过newProxyInstance函数我们就获得了一个动态代理对象。\n */\n return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {\n @Override\n public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {\n if(bannedSites.contains(args[0].toString().toLowerCase()))\n {\n throw new Exception(\"Access Denied:\"+args[0]);\n }\n //反射机制调用目标对象的方法\n Object obj = method.invoke(target, args);\n return obj;\n }\n });\n}\n\nprivate static List bannedSites;\n\nstatic\n{\n bannedSites = new ArrayList();\n bannedSites.add(\"bilibili.com\");\n bannedSites.add(\"youtube.com\");\n bannedSites.add(\"weibo.com\");\n bannedSites.add(\"qq.com\");\n}\n\n\n} 4、客户端\n\npublic class Client {\n\npublic static void main(String[] args) {\n Internet internet = new ProxyInternet(new RealInternet());\n try {\n internet.connectTo(\"360.cn\");\n internet.connectTo(\"qq.com\");\n } catch (Exception e) {\n System.out.println(e.getMessage());\n }\n}\n\n\n} 动态代理的方式中,所有的函数调用最终都会经过 invoke 函数的转发,因此我们就可以在这里做一些自己想做的操作,比如日志系统、事务、拦截器、权限控制等。\n\n\n# cglib 代理\n\n静态代理和 JDK 代理模式都要求目标对象实现一个接口,但有时候目标对象只是一个单独的对象,并没有实现任何接口,这个时候就可以使用目标对象子类来实现代理,这就是 cglib 代理。\n\ncglib (opens new window)(Code Generation Library)是一个基于 ASM 的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。cglib 通过继承方式实现代理。它广泛的被许多 AOP 的框架使用,比如我们的 Spring AOP。\n\ncglib 包的底层是通过使用字节码处理框架 ASM 来转换字节码并生成新的类。\n\ncglib 代理也被叫做子类代理,它是在内存中构建一个子类对象从而实现目标对象功能扩展。\n\n\n# Coding\n\n添加 cglib 依赖\n\n\n cglib\n cglib\n 3.3.0\n\n\n\n1\n2\n3\n4\n5\n\n\n1、不需要接口\n\npublic class RealInternet{\n\n public void connectTo(String serverHost) {\n System.out.println(\"Connecting to \"+ serverHost);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n2、代理工厂类\n\npublic class ProxyFactory implements MethodInterceptor {\n\n private Object target;\n\n public ProxyFactory(Object target){\n this.target = target;\n }\n\n @Override\n public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {\n System.out.println(\"cglib 代理开始,可以添加逻辑\");\n Object obj = method.invoke(target,objects);\n System.out.println(\"cglib 代理结束\");\n return obj;\n }\n\n\n public Object getProxyInstance(){\n //工具类,类似于JDK动态代理的Proxy类\n Enhancer enhancer = new Enhancer();\n //设置父类\n enhancer.setSuperclass(target.getClass());\n //设置回调函数\n enhancer.setCallback(this);\n //创建子类对象,即代理对象\n return enhancer.create();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n\n\n3、客户端\n\npublic class Client {\n\n public static void main(String[] args) {\n\n //目标对象\n RealInternet target = new RealInternet();\n //获取代理对象,并且将目标对象传递给代理对象\n RealInternet internet = (RealInternet) new ProxyFactory(target).getProxyInstance();\n internet.connectTo(\"so.cn\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n4、输出\n\ncglib 代理开始,可以添加逻辑\nConnecting to so.cn\ncglib 代理结束\n\n\n1\n2\n3\n\n\n\n# 代理模式适合应用场景\n\n使用代理模式的方式多种多样, 我们来看看最常见的几种。\n\n * 延迟初始化 (虚拟代理):如果你有一个偶尔使用的重量级服务对象, 一直保持该对象运行会消耗系统资源时, 可使用代理模式。\n\n你无需在程序启动时就创建该对象, 可将对象的初始化延迟到真正有需要的时候。\n\n * 访问控制 (保护代理):如果你只希望特定客户端使用服务对象, 这里的对象可以是操作系统中非常重要的部分, 而客户端则是各种已启动的程序 (包括恶意程序), 此时可使用代理模式。\n\n代理可仅在客户端凭据满足要求时将请求传递给服务对象。\n\n * 本地执行远程服务 (远程代理):适用于服务对象位于远程服务器上的情形。\n\n在这种情形中, 代理通过网络传递客户端请求, 负责处理所有与网络相关的复杂细节。\n\n * 记录日志请求 (日志记录代理):适用于当你需要保存对于服务对象的请求历史记录时。 代理可以在向服务传递请求前进行记录。\n\n * 缓存请求结果 (缓存代理):适用于需要缓存客户请求结果并对缓存生命周期进行管理时, 特别是当返回结果的体积非常大时。\n\n代理可对重复请求所需的相同结果进行缓存, 还可使用请求参数作为索引缓存的键值。比如请求图片、文件等资源时,先到代理缓存取,如果没有就去公网取并缓存到代理服务器\n\n * 智能引用:可在没有客户端使用某个重量级对象时立即销毁该对象。\n\n代理会将所有获取了指向服务对象或其结果的客户端记录在案。 代理会时不时地遍历各个客户端, 检查它们是否仍在运行。 如果相应的客户端列表为空, 代理就会销毁该服务对象, 释放底层系统资源。\n\n代理还可以记录客户端是否修改了服务对象。 其他客户端还可以复用未修改的对象。\n\n\n# AOP 中的代理模式\n\nAOP(面向切面编程)主要的的实现技术主要有 Spring AOP 和 AspectJ\n\nAspectJ 的底层技术就是静态代理,用一种 AspectJ 支持的特定语言编写切面,通过一个命令来编译,生成一个新的代理类,该代理类增强了业务类,这是在编译时增强,相对于下面说的运行时增强,编译时增强的性能更好。(AspectJ 的静态代理,不像我们前边介绍的需要为每一个目标类手动编写一个代理类,AspectJ 框架可以在编译时就生成目标类的“代理类”,在这里加了个冒号,是因为实际上它并没有生成一个新的类,而是把代理逻辑直接编译到目标类里面了)\n\nSpring AOP 采用的是动态代理,在运行期间对业务方法进行增强,所以不会生成新类,对于动态代理技术,Spring AOP 提供了对 JDK 动态代理的支持以及 CGLib 的支持。\n\n默认情况下,Spring 对实现了接口的类使用 JDK Proxy 方式,否则的话使用 CGLib。不过可以通过配置指定 Spring AOP 都通过 CGLib 来生成代理类。\n\n具体逻辑在 org.springframework.aop.framework.DefaultAopProxyFactory 类中,使用哪种方式生成由 AopProxy 根据 AdvisedSupport 对象的配置来决定源码如下:\n\npublic class DefaultAopProxyFactory implements AopProxyFactory, Serializable {\n public DefaultAopProxyFactory() {\n }\n\n public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {\n if (!config.isOptimize() && !config.isProxyTargetClass() && !this.hasNoUserSuppliedProxyInterfaces(config)) {\n return new JdkDynamicAopProxy(config);\n } else {\n Class targetClass = config.getTargetClass();\n if (targetClass == null) {\n throw new AopConfigException(\"TargetSource cannot determine target class: Either an interface or a target is required for proxy creation.\");\n } else {\n //如果目标类是接口且是代理类, 使用JDK动态代理类,否则使用Cglib生成代理类\n return (AopProxy)(!targetClass.isInterface() && !Proxy.isProxyClass(targetClass) ? new ObjenesisCglibAopProxy(config) : new JdkDynamicAopProxy(config));\n }\n }\n }\n\n private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n\n\n参考: 代理模式", - "normalizedContent": "# 基本介绍\n\n代理模式是一种结构型设计模式。为对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象,并允许在将请求提交给对象前后进行一些处理。\n\n被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象。\n\n代理模式主要有三种不同的形式:\n\n * 静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的 .class 文件就已经存在了\n * 动态代理(jdk 代理、接口代理):在程序运行时运用反射机制动态创建而成,动态就是在程序运行时生成的,而不是编译时。\n * cglib 代理(可以在内存动态的创建对象,而不是实现接口,属于动态代理的范畴)\n\n\n# 问题\n\n为什么要控制对于某个对象的访问呢? 举个例子: 有这样一个消耗大量系统资源的巨型对象, 你只是偶尔需要使用它, 并非总是需要。\n\n\n\n你可以实现延迟初始化: 在实际有需要时再创建该对象。 对象的所有客户端都要执行延迟初始代码。 不幸的是, 这很可能会带来很多重复代码。\n\n在理想情况下, 我们希望将代码直接放入对象的类中, 但这并非总是能实现: 比如类可能是第三方封闭库的一部分。\n\n\n# 解决方案\n\n代理模式建议新建一个与原服务对象接口相同的代理类, 然后更新应用以将代理对象传递给所有原始对象客户端。 代理类接收到客户端请求后会创建实际的服务对象, 并将所有工作委派给它。\n\n\n\n代理将自己伪装成数据库对象, 可在客户端或实际数据库对象不知情的情况下处理延迟初始化和缓存查询结果的工作。\n\n这有什么好处呢? 如果需要在类的主要业务逻辑前后执行一些工作, 你无需修改类就能完成这项工作。 由于代理实现的接口与原类相同, 因此你可将其传递给任何一个使用实际服务对象的客户端。\n\n#代理模式结构\n\n1.服务接口 (service interface) 声明了服务接口。 代理必须遵循该接口才能伪装成服务对象。 2.服务 (service) 类提供了一些实用的业务逻辑。 3.代理 (proxy) 类包含一个指向服务对象的引用成员变量。 代理完成其任务 (例如延迟初始化、 记录日志、 访问控制和缓存等) 后会将请求传递给服务对象。 通常情况下, 代理会对其服务对象的整个生命周期进行管理。 4.客户端 (client) 能通过同一接口与服务或代理进行交互, 所以你可在一切需要服务对象的代码中使用代理。 打游戏有代练、买卖房子有中介代理、再比如一般公司投互联网广告也可以找代理公司,这里的代练、中介、广告代理公司扮演的角色都是代理。\n\n这里举个更接近程序员的例子,比如有些变态的公司不允许在公司刷微博,看视频,可以通过一层代理来限制我们访问这些网站。\n\n废话不多说,先来个静态代理。\n\n\n# 静态代理\n\n1、定义网络接口\n\npublic interface internet {\n void connectto(string serverhost) throws exception;\n}\n\n\n1\n2\n3\n\n\n2、真正的网络连接\n\npublic class realinternet implements internet{\n\n @override\n public void connectto(string serverhost) throws exception {\n system.out.println(\"connecting to \"+ serverhost);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n3、公司的网络代理\n\npublic class proxyinternet implements internet {\n\n //目标对象,通过接口聚合\n private internet internet;\n\n // 通过构造方法传入目标对象\n public proxyinternet(internet internet){\n this.internet = internet;\n }\n //网络黑名单\n private static list bannedsites;\n\n static\n {\n bannedsites = new arraylist();\n bannedsites.add(\"bilibili.com\");\n bannedsites.add(\"youtube.com\");\n bannedsites.add(\"weibo.com\");\n bannedsites.add(\"qq.com\");\n }\n\n @override\n public void connectto(string serverhost) throws exception {\n // 添加限制功能\n if(bannedsites.contains(serverhost.tolowercase()))\n {\n throw new exception(\"access denied:\"+serverhost);\n }\n internet.connectto(serverhost);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n\n\n4、客户端验证\n\npublic class client {\n\n public static void main(string[] args) {\n internet internet = new proxyinternet(new realinternet());\n try {\n internet.connectto(\"so.com\");\n internet.connectto(\"qq.com\");\n } catch (exception e) {\n system.out.println(e.getmessage());\n }\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n5、输出\n\nconnecting to so.com\naccess denied:qq.com\n\n\n1\n2\n\n\n不能访问娱乐性网站,但是可以用 360 搜索,so 靠谱,哈哈\n\n\n# 静态代理类优缺点\n\n\n# 优点:\n\n在不修改目标对象的前提下,可以通过代理对象对目标对象功能扩展\n\n代理使客户端不需要知道实现类是什么,怎么做的,而客户端只需知道代理即可(解耦合),对于如上的客户端代码,realinterner() 可以应用工厂将它隐藏。\n\n\n# 缺点:\n\n代理类和委托类实现了相同的接口,代理类通过委托类实现了相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。\n\n代理对象只服务于一种类型的对象,如果要服务多类型的对象。势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了。\n\n\n# 动态代理\n\n静态代理会产生很多静态类,所以我们要想办法可以通过一个代理类完成全部的代理功能,这就引出了动态代理。\n\n\n# jdk 原生动态代理\n\n * 代理对象,不需要实现接口,但是目标对象要实现接口,否则不能用动态代理\n * 代理对象的生成,是通过 jdk 的 api(反射机制),动态的在内存中构建代理对象 在 java 中要想实现动态代理机制,需要 java.lang.reflect.invocationhandler 接口和 java.lang.reflect.proxy 类的支持\n\n\n# coding\n\n1、网络接口不变\n\npublic interface internet { void connectto(string serverhost) throws exception; } 2、真正的网络连接,也不会改变\n\npublic class realinternet implements internet{\n\n@override\npublic void connectto(string serverhost) throws exception {\n system.out.println(\"connecting to \"+ serverhost);\n}\n\n\n} 3、动态代理,需要实现 invocationhandler,我们用 lambda 表达式简化下\n\npublic class proxyfactory {\n\n/**\n * 维护一个目标对象\n **/\nprivate object target;\n\n/**\n * 构造器,初始化目标对象\n **/\npublic proxyfactory(object target) {\n this.target = target;\n}\n\npublic object getproxyinstance() {\n\n /**\n 被代理对象target通过参数传递进来,\n 通过target.getclass().getclassloader()获取classloader对象,\n 然后通过target.getclass().getinterfaces()获取它实现的所有接口,\n 再将target包装到实现了invocationhandler接口的对象中。\n 通过newproxyinstance函数我们就获得了一个动态代理对象。\n */\n return proxy.newproxyinstance(target.getclass().getclassloader(), target.getclass().getinterfaces(), new invocationhandler() {\n @override\n public object invoke(object proxy, method method, object[] args) throws throwable {\n if(bannedsites.contains(args[0].tostring().tolowercase()))\n {\n throw new exception(\"access denied:\"+args[0]);\n }\n //反射机制调用目标对象的方法\n object obj = method.invoke(target, args);\n return obj;\n }\n });\n}\n\nprivate static list bannedsites;\n\nstatic\n{\n bannedsites = new arraylist();\n bannedsites.add(\"bilibili.com\");\n bannedsites.add(\"youtube.com\");\n bannedsites.add(\"weibo.com\");\n bannedsites.add(\"qq.com\");\n}\n\n\n} 4、客户端\n\npublic class client {\n\npublic static void main(string[] args) {\n internet internet = new proxyinternet(new realinternet());\n try {\n internet.connectto(\"360.cn\");\n internet.connectto(\"qq.com\");\n } catch (exception e) {\n system.out.println(e.getmessage());\n }\n}\n\n\n} 动态代理的方式中,所有的函数调用最终都会经过 invoke 函数的转发,因此我们就可以在这里做一些自己想做的操作,比如日志系统、事务、拦截器、权限控制等。\n\n\n# cglib 代理\n\n静态代理和 jdk 代理模式都要求目标对象实现一个接口,但有时候目标对象只是一个单独的对象,并没有实现任何接口,这个时候就可以使用目标对象子类来实现代理,这就是 cglib 代理。\n\ncglib (opens new window)(code generation library)是一个基于 asm 的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。cglib 通过继承方式实现代理。它广泛的被许多 aop 的框架使用,比如我们的 spring aop。\n\ncglib 包的底层是通过使用字节码处理框架 asm 来转换字节码并生成新的类。\n\ncglib 代理也被叫做子类代理,它是在内存中构建一个子类对象从而实现目标对象功能扩展。\n\n\n# coding\n\n添加 cglib 依赖\n\n\n cglib\n cglib\n 3.3.0\n\n\n\n1\n2\n3\n4\n5\n\n\n1、不需要接口\n\npublic class realinternet{\n\n public void connectto(string serverhost) {\n system.out.println(\"connecting to \"+ serverhost);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n2、代理工厂类\n\npublic class proxyfactory implements methodinterceptor {\n\n private object target;\n\n public proxyfactory(object target){\n this.target = target;\n }\n\n @override\n public object intercept(object o, method method, object[] objects, methodproxy methodproxy) throws throwable {\n system.out.println(\"cglib 代理开始,可以添加逻辑\");\n object obj = method.invoke(target,objects);\n system.out.println(\"cglib 代理结束\");\n return obj;\n }\n\n\n public object getproxyinstance(){\n //工具类,类似于jdk动态代理的proxy类\n enhancer enhancer = new enhancer();\n //设置父类\n enhancer.setsuperclass(target.getclass());\n //设置回调函数\n enhancer.setcallback(this);\n //创建子类对象,即代理对象\n return enhancer.create();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n\n\n3、客户端\n\npublic class client {\n\n public static void main(string[] args) {\n\n //目标对象\n realinternet target = new realinternet();\n //获取代理对象,并且将目标对象传递给代理对象\n realinternet internet = (realinternet) new proxyfactory(target).getproxyinstance();\n internet.connectto(\"so.cn\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n4、输出\n\ncglib 代理开始,可以添加逻辑\nconnecting to so.cn\ncglib 代理结束\n\n\n1\n2\n3\n\n\n\n# 代理模式适合应用场景\n\n使用代理模式的方式多种多样, 我们来看看最常见的几种。\n\n * 延迟初始化 (虚拟代理):如果你有一个偶尔使用的重量级服务对象, 一直保持该对象运行会消耗系统资源时, 可使用代理模式。\n\n你无需在程序启动时就创建该对象, 可将对象的初始化延迟到真正有需要的时候。\n\n * 访问控制 (保护代理):如果你只希望特定客户端使用服务对象, 这里的对象可以是操作系统中非常重要的部分, 而客户端则是各种已启动的程序 (包括恶意程序), 此时可使用代理模式。\n\n代理可仅在客户端凭据满足要求时将请求传递给服务对象。\n\n * 本地执行远程服务 (远程代理):适用于服务对象位于远程服务器上的情形。\n\n在这种情形中, 代理通过网络传递客户端请求, 负责处理所有与网络相关的复杂细节。\n\n * 记录日志请求 (日志记录代理):适用于当你需要保存对于服务对象的请求历史记录时。 代理可以在向服务传递请求前进行记录。\n\n * 缓存请求结果 (缓存代理):适用于需要缓存客户请求结果并对缓存生命周期进行管理时, 特别是当返回结果的体积非常大时。\n\n代理可对重复请求所需的相同结果进行缓存, 还可使用请求参数作为索引缓存的键值。比如请求图片、文件等资源时,先到代理缓存取,如果没有就去公网取并缓存到代理服务器\n\n * 智能引用:可在没有客户端使用某个重量级对象时立即销毁该对象。\n\n代理会将所有获取了指向服务对象或其结果的客户端记录在案。 代理会时不时地遍历各个客户端, 检查它们是否仍在运行。 如果相应的客户端列表为空, 代理就会销毁该服务对象, 释放底层系统资源。\n\n代理还可以记录客户端是否修改了服务对象。 其他客户端还可以复用未修改的对象。\n\n\n# aop 中的代理模式\n\naop(面向切面编程)主要的的实现技术主要有 spring aop 和 aspectj\n\naspectj 的底层技术就是静态代理,用一种 aspectj 支持的特定语言编写切面,通过一个命令来编译,生成一个新的代理类,该代理类增强了业务类,这是在编译时增强,相对于下面说的运行时增强,编译时增强的性能更好。(aspectj 的静态代理,不像我们前边介绍的需要为每一个目标类手动编写一个代理类,aspectj 框架可以在编译时就生成目标类的“代理类”,在这里加了个冒号,是因为实际上它并没有生成一个新的类,而是把代理逻辑直接编译到目标类里面了)\n\nspring aop 采用的是动态代理,在运行期间对业务方法进行增强,所以不会生成新类,对于动态代理技术,spring aop 提供了对 jdk 动态代理的支持以及 cglib 的支持。\n\n默认情况下,spring 对实现了接口的类使用 jdk proxy 方式,否则的话使用 cglib。不过可以通过配置指定 spring aop 都通过 cglib 来生成代理类。\n\n具体逻辑在 org.springframework.aop.framework.defaultaopproxyfactory 类中,使用哪种方式生成由 aopproxy 根据 advisedsupport 对象的配置来决定源码如下:\n\npublic class defaultaopproxyfactory implements aopproxyfactory, serializable {\n public defaultaopproxyfactory() {\n }\n\n public aopproxy createaopproxy(advisedsupport config) throws aopconfigexception {\n if (!config.isoptimize() && !config.isproxytargetclass() && !this.hasnousersuppliedproxyinterfaces(config)) {\n return new jdkdynamicaopproxy(config);\n } else {\n class targetclass = config.gettargetclass();\n if (targetclass == null) {\n throw new aopconfigexception(\"targetsource cannot determine target class: either an interface or a target is required for proxy creation.\");\n } else {\n //如果目标类是接口且是代理类, 使用jdk动态代理类,否则使用cglib生成代理类\n return (aopproxy)(!targetclass.isinterface() && !proxy.isproxyclass(targetclass) ? new objenesiscglibaopproxy(config) : new jdkdynamicaopproxy(config));\n }\n }\n }\n\n private boolean hasnousersuppliedproxyinterfaces(advisedsupport config) {\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n\n\n参考: 代理模式", + "headersStr": "定义 角色 类图 coding 实例 应用 Servlet 中的责任链 Spring MVC 中的责任链 总结", + "content": "责任链,顾名思义,就是用来处理相关事务责任的一条执行链,执行链上有多个节点,每个节点都有机会(条件匹配)处理请求事务,如果某个节点处理完了就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。\n\n这种模式给予请求的类型,对请求的发送者和接收者进行解耦。属于行为型模式。\n\n在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。\n\n\n\n先来看一段代码\n\npublic void test(int i, Request request){\n if(i==1){\n Handler1.response(request);\n }else if(i == 2){\n Handler2.response(request);\n }else if(i == 3){\n Handler3.response(request);\n }else if(i == 4){\n Handler4.response(request);\n }else{\n Handler5.response(request);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n代码的业务逻辑是这样的,方法有两个参数:整数 i 和一个请求 request,根据 i 的值来决定由谁来处理 request,如果 i==1,由 Handler1来处理,如果 i==2,由 Handler2 来处理,以此类推。在编程中,这种处理业务的方法非常常见,所有处理请求的类由 if…else… 条件判断语句连成一条责任链来对请求进行处理,相信大家都经常用到。这种方法的优点是非常直观,简单明了,并且比较容易维护,但是这种方法也存在着几个比较令人头疼的问题:\n\n代码臃肿:实际应用中的判定条件通常不是这么简单地判断是否为1或者是否为2,也许需要复杂的计算,也许需要查询数据库等等,这就会有很多额外的代码,如果判断条件再比较多,那么这个if…else…语句基本上就没法看了。\n\n耦合度高:如果我们想继续添加处理请求的类,那么就要继续添加if…else…判定条件;另外,这个条件判定的顺序也是写死的,如果想改变顺序,那么也只能修改这个条件语句。 既然缺点我们已经清楚了,就要想办法来解决。这个场景的业务逻辑很简单:如果满足条件1,则由 Handler1 来处理,不满足则向下传递;如果满足条件2,则由 Handler2 来处理,不满足则继续向下传递,以此类推,直到条件结束。其实改进的方法也很简单,就是把判定条件的部分放到处理类中,这就是责任连模式的原理。\n\n\n# 定义\n\n责任链模式(Chain of Responsibility Pattern):使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。\n\n\n# 角色\n\nHandler: 抽象处理类,抽象处理类中主要包含一个指向下一处理类的成员变量 nextHandler 和一个处理请求的方法 handRequest,handRequest 方法的主要主要思想是,如果满足处理的条件,则由本处理类来进行处理,否则由 nextHandler 来处理\n\nConcreteHandler: 具体处理类主要是对具体的处理逻辑和处理的适用条件进行实现。具体处理者接到请求后,可以选择将请求处理掉,或者将请求传给下家。由于具体处理者持有对下家的引用,因此,如果需要,具体处理者可以访问下家\n\nClient:客户端\n\n\n# 类图\n\n\n\n\n# coding\n\npublic abstract class Handler {\n private Handler nextHandler;\n private int level;\n\n public Handler(int level) {\n this.level = level;\n }\n\n public void setNextHandler(Handler handler){\n this.nextHandler = handler;\n }\n\n public final void handlerRequest(Request request){\n if(level == request.getLevel()){\n this.response(request);\n }else{\n if (this.nextHandler != null){\n this.nextHandler.handlerRequest(request);\n }else{\n System.out.println(\"===已经没有处理器了===\");\n }\n }\n\n }\n // 抽象方法,子类实现\n public abstract void response(Request request);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n\n\nclass Request {\n int level = 0;\n public Request(int level){\n this.level = level;\n }\n public int getLevel() {\n return level;\n }\n}\npublic class ConcreteHandler1 extends Handler {\n public ConcreteHandler1(int level) {\n super(level);\n }\n\n @Override\n public void response(Request request) {\n System.out.println(\"请求由处理器1进行处理\");\n }\n}\n\npublic class ConcreteHandler2 extends Handler {\n\t//...\n}\n\npublic class ConcreteHandler2 extends Handler {\n\t//...\n}\npublic class Client {\n public static void main(String[] args) {\n ConcreteHandler1 handler1 = new ConcreteHandler1(1);\n ConcreteHandler2 handler2 = new ConcreteHandler2(2);\n ConcreteHandler3 handler3 = new ConcreteHandler3(3);\n\t\t//处理者构成一个环形\n handler1.setNextHandler(handler2);\n handler2.setNextHandler(handler3);\n\n handler1.handlerRequest(new Request(1));\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n\n\n\n# 实例\n\n当你想要让一个以上的对象有机会能够处理某个请求的时候,就是用责任链模式。\n\n通过责任链模式,你可以为某个请求创建一个对象链。每个对象依序检查此请求,并对其进行处理,或者将它传给链中的下一个对象。\n\n比如\n\n * 程序员要请 3 天以上的假期,在 OA 申请,需要直接主管、总监、HR 层层审批后才生效。类似的采购审批、报销审批。。。\n\n * 美团在外卖营销业务中资源位展示的逻辑\n\n\n# 应用\n\nJAVA 中的异常处理机制、JAVA WEB 中 Apache Tomcat 对 Encoding 的处理,Struts2 的拦截器,JSP、Servlet 的 Filter 均是责任链的典型应用。\n\n\n# Servlet 中的责任链\n\npublic final class ApplicationFilterChain implements FilterChain {\n private static final ThreadLocal lastServicedRequest;\n private static final ThreadLocal lastServicedResponse;\n public static final int INCREMENT = 10;\n private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];\n private int pos = 0; //下一个要执行的filter的位置\n private int n = 0; //filter个数\n private Servlet servlet = null;\n public ApplicationFilterChain() {\n }\n\n public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {\n if (Globals.IS_SECURITY_ENABLED) {\n final ServletRequest req = request;\n final ServletResponse res = response;\n\n try {\n AccessController.doPrivileged(new PrivilegedExceptionAction() {\n public Void run() throws ServletException, IOException {\n ApplicationFilterChain.this.internalDoFilter(req, res);\n return null;\n }\n });\n } catch (PrivilegedActionException var7) {\n Exception e = var7.getException();\n if (e instanceof ServletException) {\n throw (ServletException)e;\n }\n\n if (e instanceof IOException) {\n throw (IOException)e;\n }\n\n if (e instanceof RuntimeException) {\n throw (RuntimeException)e;\n }\n\n throw new ServletException(e.getMessage(), e);\n }\n } else {\n this.internalDoFilter(request, response);\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n\n\nFilterChain 就是一条过滤链。其中每个过滤器(Filter)都可以决定是否执行下一步。过滤分两个方向,进和出:\n\n进:在把 ServletRequest 和 ServletResponse 交给 Servlet 的 service 方法之前,需要进行过滤\n\n出:在service方法完成后,往客户端发送之前,需要进行过滤\n\n\n# Spring MVC 中的责任链\n\nSpring MVC 的 diapatcherServlet 的 doDispatch 方法中,获取与请求匹配的处理器 HandlerExecutionChain 就是用到了责任链模式。\n\nprotected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {\n HttpServletRequest processedRequest = request;\n HandlerExecutionChain mappedHandler = null; //使用到了责任链模式\n boolean multipartRequestParsed = false;\n WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);\n\n try {\n try {\n ModelAndView mv = null;\n Object dispatchException = null;\n\n try {\n processedRequest = this.checkMultipart(request);\n multipartRequestParsed = processedRequest != request;\n mappedHandler = this.getHandler(processedRequest); \n if (mappedHandler == null) {\n this.noHandlerFound(processedRequest, response);\n return;\n }\n\n HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());\n String method = request.getMethod();\n boolean isGet = \"GET\".equals(method);\n if (isGet || \"HEAD\".equals(method)) {\n long lastModified = ha.getLastModified(request, mappedHandler.getHandler());\n if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {\n return;\n }\n }\n\t\t\t\t\t\t\t\t\t\t//责任链模式执行预处理方法,其实是将请求交给注册的拦截器执行\n if (!mappedHandler.applyPreHandle(processedRequest, response)) {\n return;\n }\n\n mv = ha.handle(processedRequest, response, mappedHandler.getHandler());\n if (asyncManager.isConcurrentHandlingStarted()) {\n return;\n }\n\n this.applyDefaultViewName(processedRequest, mv);\n //责任链执行后处理方法\n mappedHandler.applyPostHandle(processedRequest, response, mv);\n } catch (Exception var22) {\n //...\n } finally {\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n\n\n * SpringMVC 请求的流程中,执行了拦截器相关方法 interceptor.preHandler 等等\n\n * 在处理 SpringMVC 请求时,使用到职责链模式还使用到适配器模式\n\n * HandlerExecutionChain 主要负责的是请求拦截器的执行和请求处理,但是他本身不处理请求,只是将请求分配给链上注册处理器执行,这是职责链实现方式,减少职责链本身与处理逻辑之间的耦合,规范了处理流程\n\n * HandlerExecutionChain 维护了 HandlerInterceptor 的集合, 可以向其中注册相应的拦截器\n\n\n# 总结\n\n责任链模式其实就是一个灵活版的 if…else…语句,它就是将这些判定条件的语句放到了各个处理类中,这样做的优点是比较灵活了,但同样也带来了风险,比如设置处理类前后关系时,一定要特别仔细,搞对处理类前后逻辑的条件判断关系,并且注意不要在链中出现循环引用的问题。\n\n优点:\n\n降低耦合度:将请求和处理分开,实现解耦,提高了系统的灵活性。\n\n简化了对象:对象不需要知道链的结构\n\n良好的扩展性:增加处理者的实现很简单,只需重写处理请求业务逻辑的方法。\n\n缺点:\n\n从链头发出,直到有处理者响应,在责任链比较长的时候会影响系统性能,一般需要在 Handler 中设置一个最大节点数。\n\n请求递归,调试排错比较麻烦。\n\n使用场景:\n\n有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。\n\n在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。\n\n可动态指定一组对象处理请求。\n\n模式的扩展:\n\n职责链模式存在以下两种情况。\n\n * 纯的职责链模式:一个请求必须被某一个处理者对象所接收,且一个具体处理者对某个请求的处理只能采用以下两种行为之一:自己处理(承担责任);把责任推给下家处理。\n\n * 不纯的职责链模式:允许出现某一个具体处理者对象在承担了请求的一部分责任后又将剩余的责任传给下家的情况,且一个请求可以最终不被任何接收端对象所接收。", + "normalizedContent": "责任链,顾名思义,就是用来处理相关事务责任的一条执行链,执行链上有多个节点,每个节点都有机会(条件匹配)处理请求事务,如果某个节点处理完了就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。\n\n这种模式给予请求的类型,对请求的发送者和接收者进行解耦。属于行为型模式。\n\n在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。\n\n\n\n先来看一段代码\n\npublic void test(int i, request request){\n if(i==1){\n handler1.response(request);\n }else if(i == 2){\n handler2.response(request);\n }else if(i == 3){\n handler3.response(request);\n }else if(i == 4){\n handler4.response(request);\n }else{\n handler5.response(request);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n代码的业务逻辑是这样的,方法有两个参数:整数 i 和一个请求 request,根据 i 的值来决定由谁来处理 request,如果 i==1,由 handler1来处理,如果 i==2,由 handler2 来处理,以此类推。在编程中,这种处理业务的方法非常常见,所有处理请求的类由 if…else… 条件判断语句连成一条责任链来对请求进行处理,相信大家都经常用到。这种方法的优点是非常直观,简单明了,并且比较容易维护,但是这种方法也存在着几个比较令人头疼的问题:\n\n代码臃肿:实际应用中的判定条件通常不是这么简单地判断是否为1或者是否为2,也许需要复杂的计算,也许需要查询数据库等等,这就会有很多额外的代码,如果判断条件再比较多,那么这个if…else…语句基本上就没法看了。\n\n耦合度高:如果我们想继续添加处理请求的类,那么就要继续添加if…else…判定条件;另外,这个条件判定的顺序也是写死的,如果想改变顺序,那么也只能修改这个条件语句。 既然缺点我们已经清楚了,就要想办法来解决。这个场景的业务逻辑很简单:如果满足条件1,则由 handler1 来处理,不满足则向下传递;如果满足条件2,则由 handler2 来处理,不满足则继续向下传递,以此类推,直到条件结束。其实改进的方法也很简单,就是把判定条件的部分放到处理类中,这就是责任连模式的原理。\n\n\n# 定义\n\n责任链模式(chain of responsibility pattern):使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。\n\n\n# 角色\n\nhandler: 抽象处理类,抽象处理类中主要包含一个指向下一处理类的成员变量 nexthandler 和一个处理请求的方法 handrequest,handrequest 方法的主要主要思想是,如果满足处理的条件,则由本处理类来进行处理,否则由 nexthandler 来处理\n\nconcretehandler: 具体处理类主要是对具体的处理逻辑和处理的适用条件进行实现。具体处理者接到请求后,可以选择将请求处理掉,或者将请求传给下家。由于具体处理者持有对下家的引用,因此,如果需要,具体处理者可以访问下家\n\nclient:客户端\n\n\n# 类图\n\n\n\n\n# coding\n\npublic abstract class handler {\n private handler nexthandler;\n private int level;\n\n public handler(int level) {\n this.level = level;\n }\n\n public void setnexthandler(handler handler){\n this.nexthandler = handler;\n }\n\n public final void handlerrequest(request request){\n if(level == request.getlevel()){\n this.response(request);\n }else{\n if (this.nexthandler != null){\n this.nexthandler.handlerrequest(request);\n }else{\n system.out.println(\"===已经没有处理器了===\");\n }\n }\n\n }\n // 抽象方法,子类实现\n public abstract void response(request request);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n\n\nclass request {\n int level = 0;\n public request(int level){\n this.level = level;\n }\n public int getlevel() {\n return level;\n }\n}\npublic class concretehandler1 extends handler {\n public concretehandler1(int level) {\n super(level);\n }\n\n @override\n public void response(request request) {\n system.out.println(\"请求由处理器1进行处理\");\n }\n}\n\npublic class concretehandler2 extends handler {\n\t//...\n}\n\npublic class concretehandler2 extends handler {\n\t//...\n}\npublic class client {\n public static void main(string[] args) {\n concretehandler1 handler1 = new concretehandler1(1);\n concretehandler2 handler2 = new concretehandler2(2);\n concretehandler3 handler3 = new concretehandler3(3);\n\t\t//处理者构成一个环形\n handler1.setnexthandler(handler2);\n handler2.setnexthandler(handler3);\n\n handler1.handlerrequest(new request(1));\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n\n\n\n# 实例\n\n当你想要让一个以上的对象有机会能够处理某个请求的时候,就是用责任链模式。\n\n通过责任链模式,你可以为某个请求创建一个对象链。每个对象依序检查此请求,并对其进行处理,或者将它传给链中的下一个对象。\n\n比如\n\n * 程序员要请 3 天以上的假期,在 oa 申请,需要直接主管、总监、hr 层层审批后才生效。类似的采购审批、报销审批。。。\n\n * 美团在外卖营销业务中资源位展示的逻辑\n\n\n# 应用\n\njava 中的异常处理机制、java web 中 apache tomcat 对 encoding 的处理,struts2 的拦截器,jsp、servlet 的 filter 均是责任链的典型应用。\n\n\n# servlet 中的责任链\n\npublic final class applicationfilterchain implements filterchain {\n private static final threadlocal lastservicedrequest;\n private static final threadlocal lastservicedresponse;\n public static final int increment = 10;\n private applicationfilterconfig[] filters = new applicationfilterconfig[0];\n private int pos = 0; //下一个要执行的filter的位置\n private int n = 0; //filter个数\n private servlet servlet = null;\n public applicationfilterchain() {\n }\n\n public void dofilter(servletrequest request, servletresponse response) throws ioexception, servletexception {\n if (globals.is_security_enabled) {\n final servletrequest req = request;\n final servletresponse res = response;\n\n try {\n accesscontroller.doprivileged(new privilegedexceptionaction() {\n public void run() throws servletexception, ioexception {\n applicationfilterchain.this.internaldofilter(req, res);\n return null;\n }\n });\n } catch (privilegedactionexception var7) {\n exception e = var7.getexception();\n if (e instanceof servletexception) {\n throw (servletexception)e;\n }\n\n if (e instanceof ioexception) {\n throw (ioexception)e;\n }\n\n if (e instanceof runtimeexception) {\n throw (runtimeexception)e;\n }\n\n throw new servletexception(e.getmessage(), e);\n }\n } else {\n this.internaldofilter(request, response);\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n\n\nfilterchain 就是一条过滤链。其中每个过滤器(filter)都可以决定是否执行下一步。过滤分两个方向,进和出:\n\n进:在把 servletrequest 和 servletresponse 交给 servlet 的 service 方法之前,需要进行过滤\n\n出:在service方法完成后,往客户端发送之前,需要进行过滤\n\n\n# spring mvc 中的责任链\n\nspring mvc 的 diapatcherservlet 的 dodispatch 方法中,获取与请求匹配的处理器 handlerexecutionchain 就是用到了责任链模式。\n\nprotected void dodispatch(httpservletrequest request, httpservletresponse response) throws exception {\n httpservletrequest processedrequest = request;\n handlerexecutionchain mappedhandler = null; //使用到了责任链模式\n boolean multipartrequestparsed = false;\n webasyncmanager asyncmanager = webasyncutils.getasyncmanager(request);\n\n try {\n try {\n modelandview mv = null;\n object dispatchexception = null;\n\n try {\n processedrequest = this.checkmultipart(request);\n multipartrequestparsed = processedrequest != request;\n mappedhandler = this.gethandler(processedrequest); \n if (mappedhandler == null) {\n this.nohandlerfound(processedrequest, response);\n return;\n }\n\n handleradapter ha = this.gethandleradapter(mappedhandler.gethandler());\n string method = request.getmethod();\n boolean isget = \"get\".equals(method);\n if (isget || \"head\".equals(method)) {\n long lastmodified = ha.getlastmodified(request, mappedhandler.gethandler());\n if ((new servletwebrequest(request, response)).checknotmodified(lastmodified) && isget) {\n return;\n }\n }\n\t\t\t\t\t\t\t\t\t\t//责任链模式执行预处理方法,其实是将请求交给注册的拦截器执行\n if (!mappedhandler.applyprehandle(processedrequest, response)) {\n return;\n }\n\n mv = ha.handle(processedrequest, response, mappedhandler.gethandler());\n if (asyncmanager.isconcurrenthandlingstarted()) {\n return;\n }\n\n this.applydefaultviewname(processedrequest, mv);\n //责任链执行后处理方法\n mappedhandler.applyposthandle(processedrequest, response, mv);\n } catch (exception var22) {\n //...\n } finally {\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n\n\n * springmvc 请求的流程中,执行了拦截器相关方法 interceptor.prehandler 等等\n\n * 在处理 springmvc 请求时,使用到职责链模式还使用到适配器模式\n\n * handlerexecutionchain 主要负责的是请求拦截器的执行和请求处理,但是他本身不处理请求,只是将请求分配给链上注册处理器执行,这是职责链实现方式,减少职责链本身与处理逻辑之间的耦合,规范了处理流程\n\n * handlerexecutionchain 维护了 handlerinterceptor 的集合, 可以向其中注册相应的拦截器\n\n\n# 总结\n\n责任链模式其实就是一个灵活版的 if…else…语句,它就是将这些判定条件的语句放到了各个处理类中,这样做的优点是比较灵活了,但同样也带来了风险,比如设置处理类前后关系时,一定要特别仔细,搞对处理类前后逻辑的条件判断关系,并且注意不要在链中出现循环引用的问题。\n\n优点:\n\n降低耦合度:将请求和处理分开,实现解耦,提高了系统的灵活性。\n\n简化了对象:对象不需要知道链的结构\n\n良好的扩展性:增加处理者的实现很简单,只需重写处理请求业务逻辑的方法。\n\n缺点:\n\n从链头发出,直到有处理者响应,在责任链比较长的时候会影响系统性能,一般需要在 handler 中设置一个最大节点数。\n\n请求递归,调试排错比较麻烦。\n\n使用场景:\n\n有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。\n\n在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。\n\n可动态指定一组对象处理请求。\n\n模式的扩展:\n\n职责链模式存在以下两种情况。\n\n * 纯的职责链模式:一个请求必须被某一个处理者对象所接收,且一个具体处理者对某个请求的处理只能采用以下两种行为之一:自己处理(承担责任);把责任推给下家处理。\n\n * 不纯的职责链模式:允许出现某一个具体处理者对象在承担了请求的一部分责任后又将剩余的责任传给下家的情况,且一个请求可以最终不被任何接收端对象所接收。", "charsets": { "cjk": true }, @@ -3412,11 +3504,11 @@ export const siteData = { "lastUpdatedTimestamp": 1663688099000 }, { - "title": "策略模式", + "title": "观察模式", "frontmatter": { - "title": "策略模式", - "date": "2022-05-21T17:29:26.000Z", - "permalink": "/pages/78c7aa/", + "title": "观察模式", + "date": "2022-05-21T17:29:45.000Z", + "permalink": "/pages/cbff3c/", "categories": [ "Java相关", "设计模式" @@ -3425,85 +3517,99 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/050.%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F.html", - "relativePath": "01.Java相关/40.设计模式/050.策略模式.md", - "key": "v-5b0157cf", - "path": "/pages/78c7aa/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/060.%E8%A7%82%E5%AF%9F%E6%A8%A1%E5%BC%8F.html", + "relativePath": "01.Java相关/40.设计模式/060.观察模式.md", + "key": "v-64f1031c", + "path": "/pages/cbff3c/", "headers": [ { "level": 2, "title": "定义", "slug": "定义", "normalizedTitle": "定义", - "charIndex": 2 + "charIndex": 344 }, { "level": 2, "title": "角色", "slug": "角色", "normalizedTitle": "角色", - "charIndex": 1075 - }, - { - "level": 2, - "title": "实例", - "slug": "实例", - "normalizedTitle": "实例", - "charIndex": 1229 + "charIndex": 594 }, { "level": 2, - "title": "解析策略模式", - "slug": "解析策略模式", - "normalizedTitle": "解析策略模式", - "charIndex": 2310 + "title": "类图", + "slug": "类图", + "normalizedTitle": "类图", + "charIndex": 630 }, { "level": 2, - "title": "谁来选择具体的策略算法", - "slug": "谁来选择具体的策略算法", - "normalizedTitle": "谁来选择具体的策略算法", - "charIndex": 2795 + "title": "实例", + "slug": "实例", + "normalizedTitle": "实例", + "charIndex": 1386 }, { "level": 2, "title": "优缺点", "slug": "优缺点", "normalizedTitle": "优缺点", - "charIndex": 2992 + "charIndex": 2856 }, { "level": 3, - "title": "优点:", + "title": "优点", "slug": "优点", - "normalizedTitle": "优点:", - "charIndex": 3000 + "normalizedTitle": "优点", + "charIndex": 2864 }, { "level": 3, - "title": "缺点:", + "title": "缺点", "slug": "缺点", - "normalizedTitle": "缺点:", - "charIndex": 3092 + "normalizedTitle": "缺点", + "charIndex": 2857 }, { "level": 2, - "title": "思考", - "slug": "思考", - "normalizedTitle": "思考", - "charIndex": 3233 + "title": "应用", + "slug": "应用", + "normalizedTitle": "应用", + "charIndex": 3010 }, { - "level": 2, - "title": "策略模式在 JDK 中的应用", - "slug": "策略模式在-jdk-中的应用", - "normalizedTitle": "策略模式在 jdk 中的应用", - "charIndex": 3512 + "level": 3, + "title": "JDK 中的观察者模式", + "slug": "jdk-中的观察者模式", + "normalizedTitle": "jdk 中的观察者模式", + "charIndex": 3017 + }, + { + "level": 3, + "title": "Spring 中的观察者模式", + "slug": "spring-中的观察者模式", + "normalizedTitle": "spring 中的观察者模式", + "charIndex": 3164 + }, + { + "level": 4, + "title": "1. Spring 中观察者模式的四个角色", + "slug": "_1-spring-中观察者模式的四个角色", + "normalizedTitle": "1. spring 中观察者模式的四个角色", + "charIndex": 3228 + }, + { + "level": 4, + "title": "2. coding~~~~~~", + "slug": "_2-coding", + "normalizedTitle": "2. coding~~~~~~", + "charIndex": 3951 } ], - "headersStr": "定义 角色 实例 解析策略模式 谁来选择具体的策略算法 优缺点 优点: 缺点: 思考 策略模式在 JDK 中的应用", - "content": "# 定义\n\n策略模式(Strategy Design Pattern):封装可以互换的行为,并使用委托来决定要使用哪一个。\n\n策略模式是一种行为设计模式, 它能让你定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换。\n\n> 用人话翻译后就是:运行时我给你这个类的方法传不同的 “key”,你这个方法就去执行不同的业务逻辑。 你品,你细品,这不就是 if else 干的事吗?\n\n先直观的看下传统的多重 if else 代码\n\npublic String getCheckResult(String type) {\n if (\"校验1\".equals(type)) {\n return \"执行业务逻辑1\";\n } else if (\"校验2\".equals(type)) {\n return \"执行业务逻辑2\";\n } else if (\"校验3\".equals(type)) {\n return \"执行业务逻辑3\";\n } else if (\"校验4\".equals(type)) {\n return \"执行业务逻辑4\";\n } else if (\"校验5\".equals(type)) {\n return \"执行业务逻辑5\";\n } else if (\"校验6\".equals(type)) {\n return \"执行业务逻辑6\";\n } else if (\"校验7\".equals(type)) {\n return \"执行业务逻辑7\";\n } else if (\"校验8\".equals(type)) {\n return \"执行业务逻辑8\";\n } else if (\"校验9\".equals(type)) {\n return \"执行业务逻辑9\";\n }\n return \"不在处理的逻辑中返回业务错误\";\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n\n\n这么看,你要是还觉得挺清晰的话,想象下这些 return 里是各种复杂的业务逻辑方法~~\n\n当然,策略模式的作用可不止是避免冗长的 if-else 或者 switch 分支,它还可以像模板方法模式那样提供框架的扩展点等。\n\n网上的示例很多,比如不同路线的规划、不同支付方式的选择 都是典型的 if-else 问题,也都是典型的策略模式问题,栗子我们待会看,先看下策略模式的类图,然后去改造多重判断~\n\n\n# 角色\n\n策略模式涉及到三个角色:\n\n 1. Strategy:策略接口或者策略抽象类,用来约束一系列的策略算法(Context 使用这个接口来调用具体的策略实现算法)\n 2. ConcreateStrategy:具体的策略类(实现策略接口或继承抽象策略类)\n 3. Context:上下文类,持有具体策略类的实例,并负责调用相关的算法 应用策略模式来解决问题的思路\n\n\n# 实例\n\n先看看最简单的策略模式 demo:\n\n1、策略接口(定义策略)\n\npublic interface Strategy {\n void operate();\n}\n\n\n1\n2\n3\n\n\n2、具体的算法实现\n\npublic class ConcreteStrategyA implements Strategy {\n @Override\n public void operate() {\n //具体的算法实现\n System.out.println(\"执行业务逻辑A\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\npublic class ConcreteStrategyB implements Strategy {\n @Override\n public void operate() {\n //具体的算法实现\n System.out.println(\"执行业务逻辑B\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n3、上下文的实现\n\npublic class Context {\n\n //持有一个具体的策略对象\n private Strategy strategy;\n\n //构造方法,传入具体的策略对象\n public Context(Strategy strategy){\n this.strategy = strategy;\n }\n\n public void doSomething(){\n //调用具体的策略对象进操作\n strategy.operate();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n\n\n4、客户端使用(策略的使用)\n\npublic static void main(String[] args) {\n Context context = new Context(new ConcreteStrategyA());\n context.doSomething();\n}\n\n\n1\n2\n3\n4\n\n\n> ps:这种策略的使用方式其实很死板,真正使用的时候如果还这么写, 和写一大推 if-else 没什么区别,所以我们一般会结合工厂类, 在运行时动态确定使用哪种策略。策略模式侧重如何选择策略、工厂模式侧重如何创建策略。\n\n\n# 解析策略模式\n\n策略模式的功能就是把具体的算法实现从具体的业务处理中独立出来,把它们实现成单独的算法类,从而形成一系列算法,并让这些算法可以互相替换。\n\n> 策略模式的重心不是如何来实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活,具有更好的维护性和扩展性。\n\n实际上,每个策略算法具体实现的功能,就是原来在 if-else 结构中的具体实现,每个 if-else 语句都是一个平等的功能结构,可以说是兄弟关系。\n\n策略模式呢,就是把各个平等的具体实现封装到单独的策略实现类了,然后通过上下文与具体的策略类进行交互。\n\n『 策略模式 = 实现策略接口(或抽象类)的每个策略类 + 上下文的逻辑分派 』\n\n> 策略模式的本质:分离算法,选择实现 ——《研磨设计模式》\n\n所以说,策略模式只是在代码结构上的一个调整,即使用了策略模式,该写的逻辑一个也少不了,到逻辑分派的时候,只是变相的 if-else。\n\n而它的优化点是抽象了出了接口,将业务逻辑封装成一个一个的实现类,任意地替换。在复杂场景(业务逻辑较多)时比直接 if-else 更好维护和扩展些。\n\n\n# 谁来选择具体的策略算法\n\n如果你手写了上边的 demo,就会发现,这玩意不及 if-else 来的顺手,尤其是在判断逻辑的时候,每个逻辑都要要构造一个上下文对象,费劲。\n\n其实,策略模式中,我们可以自己定义谁来选择具体的策略算法,有两种:\n\n * 客户端:当使用上下文时,由客户端选择,像我们上边的 demo\n * 上下文:客户端不用选,由上下文来选具体的策略算法,可以在构造器中指定\n\n\n# 优缺点\n\n\n# 优点:\n\n避免多重条件语句:也就是避免大量的 if-else 更好的扩展性(完全符合开闭原则):策略模式中扩展新的策略实现很容易,无需对上下文修改,只增加新的策略实现类就可以\n\n\n# 缺点:\n\n客户必须了解每种策略的不同(这个可以通过 IOC、依赖注入的方式解决) 增加了对象数:每个具体策略都封装成了类,可能备选的策略会很多 只适合扁平的算法结构:策略模式的一系列算法是平等的,也就是在运行时刻只有一个算法会被使用,这就限制了算法使用的层级,不能嵌套使用\n\n\n# 思考\n\n实际使用中,往往不会只是单一的某个设计模式的套用,一般都会混合使用,而且模式之间的结合也是没有定势的,要具体问题具体分析。\n\n策略模式往往会结合其他模式一起使用,比如工厂、模板等,具体使用需要结合自己的业务。\n\n切记,不要为了使用设计模式而强行模式,不要把简单问题复杂化。\n\n策略模式也不是专为消除 if-else 而生的,不要和 if-else 划等号。它体现了“对修改关闭,对扩展开放“的原则。\n\n并不是说,看到 if-else 就想着用策略模式去优化,业务逻辑简单,可能几个枚举,或者几个卫语句就搞定的场景,就不用非得硬套设计模式了。\n\n\n# 策略模式在 JDK 中的应用\n\n在 JDK 中,Comparator 比较器是一个策略接口,我们常用的 compare() 方法就是一个具体的策略实现,用于定义排序规则。\n\npublic interface Comparator {\n int compare(T o1, T o2);\n //......\n}\n\n\n1\n2\n3\n4\n\n\n当我们想自定义排序规则的时候,就可以实现 Comparator 。\n\n这时候我们重写了接口中的 compare() 方法,就是具体的策略类(只不过这里可能是内部类)。当我们在调用 Arrays 的排序方法 sort() 时,可以用默认的排序规则,也可以用自定义的规则。\n\npublic static void main(String[] args) {\n Integer[] data = {4,2,7,5,1,9};\n Comparator comparator = new Comparator() {\n @Override\n public int compare(Integer o1, Integer o2) {\n if(o1 > o2){\n return 1;\n } else {\n return -1;\n }\n }\n };\n\n Arrays.sort(data,comparator);\n System.out.println(Arrays.toString(data));\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\nArrays 的 sort() 方法,有自定义规则就按自己的方法排序,反之走源码逻辑。\n\npublic static void sort(T[] a, Comparator c) {\n if (c == null) {\n sort(a);\n } else {\n if (LegacyMergeSort.userRequested)\n legacyMergeSort(a, c);\n else\n TimSort.sort(a, 0, a.length, c, null, 0, 0);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n还有,ThreadPoolExecutor 中的拒绝策略 RejectedExecutionHandler 也是典型的策略模式,感兴趣的也可以再看看源码。", - "normalizedContent": "# 定义\n\n策略模式(strategy design pattern):封装可以互换的行为,并使用委托来决定要使用哪一个。\n\n策略模式是一种行为设计模式, 它能让你定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换。\n\n> 用人话翻译后就是:运行时我给你这个类的方法传不同的 “key”,你这个方法就去执行不同的业务逻辑。 你品,你细品,这不就是 if else 干的事吗?\n\n先直观的看下传统的多重 if else 代码\n\npublic string getcheckresult(string type) {\n if (\"校验1\".equals(type)) {\n return \"执行业务逻辑1\";\n } else if (\"校验2\".equals(type)) {\n return \"执行业务逻辑2\";\n } else if (\"校验3\".equals(type)) {\n return \"执行业务逻辑3\";\n } else if (\"校验4\".equals(type)) {\n return \"执行业务逻辑4\";\n } else if (\"校验5\".equals(type)) {\n return \"执行业务逻辑5\";\n } else if (\"校验6\".equals(type)) {\n return \"执行业务逻辑6\";\n } else if (\"校验7\".equals(type)) {\n return \"执行业务逻辑7\";\n } else if (\"校验8\".equals(type)) {\n return \"执行业务逻辑8\";\n } else if (\"校验9\".equals(type)) {\n return \"执行业务逻辑9\";\n }\n return \"不在处理的逻辑中返回业务错误\";\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n\n\n这么看,你要是还觉得挺清晰的话,想象下这些 return 里是各种复杂的业务逻辑方法~~\n\n当然,策略模式的作用可不止是避免冗长的 if-else 或者 switch 分支,它还可以像模板方法模式那样提供框架的扩展点等。\n\n网上的示例很多,比如不同路线的规划、不同支付方式的选择 都是典型的 if-else 问题,也都是典型的策略模式问题,栗子我们待会看,先看下策略模式的类图,然后去改造多重判断~\n\n\n# 角色\n\n策略模式涉及到三个角色:\n\n 1. strategy:策略接口或者策略抽象类,用来约束一系列的策略算法(context 使用这个接口来调用具体的策略实现算法)\n 2. concreatestrategy:具体的策略类(实现策略接口或继承抽象策略类)\n 3. context:上下文类,持有具体策略类的实例,并负责调用相关的算法 应用策略模式来解决问题的思路\n\n\n# 实例\n\n先看看最简单的策略模式 demo:\n\n1、策略接口(定义策略)\n\npublic interface strategy {\n void operate();\n}\n\n\n1\n2\n3\n\n\n2、具体的算法实现\n\npublic class concretestrategya implements strategy {\n @override\n public void operate() {\n //具体的算法实现\n system.out.println(\"执行业务逻辑a\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\npublic class concretestrategyb implements strategy {\n @override\n public void operate() {\n //具体的算法实现\n system.out.println(\"执行业务逻辑b\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n3、上下文的实现\n\npublic class context {\n\n //持有一个具体的策略对象\n private strategy strategy;\n\n //构造方法,传入具体的策略对象\n public context(strategy strategy){\n this.strategy = strategy;\n }\n\n public void dosomething(){\n //调用具体的策略对象进操作\n strategy.operate();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n\n\n4、客户端使用(策略的使用)\n\npublic static void main(string[] args) {\n context context = new context(new concretestrategya());\n context.dosomething();\n}\n\n\n1\n2\n3\n4\n\n\n> ps:这种策略的使用方式其实很死板,真正使用的时候如果还这么写, 和写一大推 if-else 没什么区别,所以我们一般会结合工厂类, 在运行时动态确定使用哪种策略。策略模式侧重如何选择策略、工厂模式侧重如何创建策略。\n\n\n# 解析策略模式\n\n策略模式的功能就是把具体的算法实现从具体的业务处理中独立出来,把它们实现成单独的算法类,从而形成一系列算法,并让这些算法可以互相替换。\n\n> 策略模式的重心不是如何来实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活,具有更好的维护性和扩展性。\n\n实际上,每个策略算法具体实现的功能,就是原来在 if-else 结构中的具体实现,每个 if-else 语句都是一个平等的功能结构,可以说是兄弟关系。\n\n策略模式呢,就是把各个平等的具体实现封装到单独的策略实现类了,然后通过上下文与具体的策略类进行交互。\n\n『 策略模式 = 实现策略接口(或抽象类)的每个策略类 + 上下文的逻辑分派 』\n\n> 策略模式的本质:分离算法,选择实现 ——《研磨设计模式》\n\n所以说,策略模式只是在代码结构上的一个调整,即使用了策略模式,该写的逻辑一个也少不了,到逻辑分派的时候,只是变相的 if-else。\n\n而它的优化点是抽象了出了接口,将业务逻辑封装成一个一个的实现类,任意地替换。在复杂场景(业务逻辑较多)时比直接 if-else 更好维护和扩展些。\n\n\n# 谁来选择具体的策略算法\n\n如果你手写了上边的 demo,就会发现,这玩意不及 if-else 来的顺手,尤其是在判断逻辑的时候,每个逻辑都要要构造一个上下文对象,费劲。\n\n其实,策略模式中,我们可以自己定义谁来选择具体的策略算法,有两种:\n\n * 客户端:当使用上下文时,由客户端选择,像我们上边的 demo\n * 上下文:客户端不用选,由上下文来选具体的策略算法,可以在构造器中指定\n\n\n# 优缺点\n\n\n# 优点:\n\n避免多重条件语句:也就是避免大量的 if-else 更好的扩展性(完全符合开闭原则):策略模式中扩展新的策略实现很容易,无需对上下文修改,只增加新的策略实现类就可以\n\n\n# 缺点:\n\n客户必须了解每种策略的不同(这个可以通过 ioc、依赖注入的方式解决) 增加了对象数:每个具体策略都封装成了类,可能备选的策略会很多 只适合扁平的算法结构:策略模式的一系列算法是平等的,也就是在运行时刻只有一个算法会被使用,这就限制了算法使用的层级,不能嵌套使用\n\n\n# 思考\n\n实际使用中,往往不会只是单一的某个设计模式的套用,一般都会混合使用,而且模式之间的结合也是没有定势的,要具体问题具体分析。\n\n策略模式往往会结合其他模式一起使用,比如工厂、模板等,具体使用需要结合自己的业务。\n\n切记,不要为了使用设计模式而强行模式,不要把简单问题复杂化。\n\n策略模式也不是专为消除 if-else 而生的,不要和 if-else 划等号。它体现了“对修改关闭,对扩展开放“的原则。\n\n并不是说,看到 if-else 就想着用策略模式去优化,业务逻辑简单,可能几个枚举,或者几个卫语句就搞定的场景,就不用非得硬套设计模式了。\n\n\n# 策略模式在 jdk 中的应用\n\n在 jdk 中,comparator 比较器是一个策略接口,我们常用的 compare() 方法就是一个具体的策略实现,用于定义排序规则。\n\npublic interface comparator {\n int compare(t o1, t o2);\n //......\n}\n\n\n1\n2\n3\n4\n\n\n当我们想自定义排序规则的时候,就可以实现 comparator 。\n\n这时候我们重写了接口中的 compare() 方法,就是具体的策略类(只不过这里可能是内部类)。当我们在调用 arrays 的排序方法 sort() 时,可以用默认的排序规则,也可以用自定义的规则。\n\npublic static void main(string[] args) {\n integer[] data = {4,2,7,5,1,9};\n comparator comparator = new comparator() {\n @override\n public int compare(integer o1, integer o2) {\n if(o1 > o2){\n return 1;\n } else {\n return -1;\n }\n }\n };\n\n arrays.sort(data,comparator);\n system.out.println(arrays.tostring(data));\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\narrays 的 sort() 方法,有自定义规则就按自己的方法排序,反之走源码逻辑。\n\npublic static void sort(t[] a, comparator c) {\n if (c == null) {\n sort(a);\n } else {\n if (legacymergesort.userrequested)\n legacymergesort(a, c);\n else\n timsort.sort(a, 0, a.length, c, null, 0, 0);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n还有,threadpoolexecutor 中的拒绝策略 rejectedexecutionhandler 也是典型的策略模式,感兴趣的也可以再看看源码。", + "headersStr": "定义 角色 类图 实例 优缺点 优点 缺点 应用 JDK 中的观察者模式 Spring 中的观察者模式 1. Spring 中观察者模式的四个角色 2. coding~~~~~~", + "content": "在软件系统中经常会有这样的需求:如果一个对象的状态发生改变,某些与它相关的对象也要随之做出相应的变化。 举个例子:\n\n * 微信公众号,如果一个用户订阅了某个公众号,那么便会收到公众号发来的消息,那么,公众号就是『被观察者』,而用户就是『观察者』\n * 气象站可以将每天预测到的温度、湿度、气压等以公告的形式发布给各种第三方网站,如果天气数据有更新,要能够实时的通知给第三方,这里的气象局就是『被观察者』,第三方网站就是『观察者』\n * MVC 模式中的模型与视图的关系也属于观察与被观察关系\n\n观察者模式是使用频率较高的设计模式之一。\n\n观察者模式包含观察目标和观察者两类对象,一个目标可以有任意数目的与之相依赖的观察者,一旦观察目标的状态发生改变,所有的观察者都将得到通知。\n\n\n# 定义\n\n观察者模式(Observer Pattern): 定义对象间一种一对多的依赖关系,使得当每一个对象改变状态,则所有依赖于它的对象都会得到通知并自动更新。\n\n观察者模式是一种对象行为型模式。\n\n观察者模式的别名包括发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。\n\n细究的话,发布订阅和观察者有些不同,可以理解成发布订阅模式属于广义上的观察者模式。\n\n\n\n\n# 角色\n\n * Subject(目标):被观察者,它是指被观察的对象。 从类图中可以看到,类中有一个用来存放观察者对象的Vector 容器(Vector在是安全的,而List则是不安全的),这个 Vector 容器是被观察者类的核心,另外还有三个方法:attach 方法是向这个容器中添加观察者对象;detach 方法是从容器中移除观察者对象;notify 方法是依次调用观察者对象的对应方法。这个角色可以是接口,也可以是抽象类或者具体的类,因为很多情况下会与其他的模式混用,所以使用抽象类的情况比较多。\n\n * ConcreteSubject(具体目标):具体目标是目标类的子类,通常它包含经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知。同时它还实现了在目标类中定义的抽象业务逻辑方法(如果有的话)。如果无须扩展目标类,则具体目标类可以省略。\n\n * Observer(观察者):观察者将对观察目标的改变做出反应,观察者一般定义为接口,该接口声明了更新数据的方法 update(),因此又称为抽象观察者。\n\n * ConcreteObserver(具体观察者):在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者 Observer 中定义的 update() 方法。通常在实现时,可以调用具体目标类的 attach() 方法将自己添加到目标类的集合中或通过 detach() 方法将自己从目标类的集合中删除。\n\n\n# 类图\n\n\n\n再记录下 UML 类图的注意事项,这里我的 Subject 是抽象方法,所以用***斜体***,抽象方法也要用斜体,具体的各种箭头意义,我之前也总结过《设计模式前传——学设计模式前你要知道这些》(被网上各种帖子毒害过的自己,认真记录~~~)。\n\n\n# 实例\n\n1、定义观察者接口\n\ninterface Observer {\n public void update();\n}\n\n\n1\n2\n3\n\n\n2、定义被观察者\n\nabstract class Subject {\n private Vector obs = new Vector();\n\n public void addObserver(Observer obs){\n this.obs.add(obs);\n }\n public void delObserver(Observer obs){\n this.obs.remove(obs);\n }\n protected void notifyObserver(){\n for(Observer o: obs){\n o.update();\n }\n }\n public abstract void doSomething();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\n3、具体的被观察者\n\nclass ConcreteSubject extends Subject {\n public void doSomething(){\n System.out.println(\"被观察者事件发生改变\");\n this.notifyObserver();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n4、具体的观察者\n\nclass ConcreteObserver1 implements Observer {\n public void update() {\n System.out.println(\"观察者1收到信息,并进行处理\");\n }\n}\nclass ConcreteObserver2 implements Observer {\n public void update() {\n System.out.println(\"观察者2收到信息,并进行处理\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n5、客户端\n\npublic class Client {\n public static void main(String[] args){\n Subject sub = new ConcreteSubject();\n sub.addObserver(new ConcreteObserver1()); //添加观察者1\n sub.addObserver(new ConcreteObserver2()); //添加观察者2\n sub.doSomething();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\n输出\n\n被观察者事件发生改变\n观察者1收到信息,并进行处理\n观察者2收到信息,并进行处理\n\n\n1\n2\n3\n\n\n通过运行结果可以看到,我们只调用了 Subject 的方法,但同时两个观察者的相关方法都被调用了。仔细看一下代码,其实很简单,就是在 Subject 类中关联一下 Observer 类,并且在 doSomething() 方法中遍历一下 Observer 的 update() 方法就行了。\n\n\n# 优缺点\n\n\n# 优点\n\n降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系 目标与观察者之间建立了一套触发机制 支持广播通信 符合“开闭原则”的要求\n\n\n# 缺点\n\n目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率\n\n\n# 应用\n\n\n# JDK 中的观察者模式\n\n观察者模式在 Java 语言中的地位非常重要。在 JDK 的 java.util 包中,提供了 Observable 类以及 Observer 接口,它们构成了 JDK 对观察者模式的支持(可以去查看下源码,写的比较严谨)。but,在 Java9 被弃用了。\n\n\n# Spring 中的观察者模式\n\nSpring 事件驱动模型也是观察者模式很经典的应用。就是我们项目中最常见的事件监听器。\n\n# 1. Spring 中观察者模式的四个角色\n\n * 事件:ApplicationEvent 是所有事件对象的父类。ApplicationEvent 继承自 jdk 的 EventObject,所有的事件都需要继承 ApplicationEvent,并且通过 source 得到事件源。\n\n * Spring 也为我们提供了很多内置事件,ContextRefreshedEvent、ContextStartedEvent、ContextStoppedEvent、ContextClosedEvent、RequestHandledEvent。\n\n * 事件监听:ApplicationListener,也就是观察者,继承自 jdk 的 EventListener,该类中只有一个方法 onApplicationEvent。当监听的事件发生后该方法会被执行。\n\n * 事件源:ApplicationContext,ApplicationContext 是 Spring 中的核心容器,在事件监听中 ApplicationContext 可以作为事件的发布者,也就是事件源。因为 ApplicationContext 继承自 ApplicationEventPublisher。在 ApplicationEventPublisher 中定义了事件发布的方法:publishEvent(Object event)\n\n * 事件管理:ApplicationEventMulticaster,用于事件监听器的注册和事件的广播。监听器的注册就是通过它来实现的,它的作用是把 Applicationcontext 发布的 Event 广播给它的监听器列表。\n\n# 2. coding~~~~~~\n\n1、定义事件\n\npublic class MyEvent extends ApplicationEvent {\n public MyEvent(Object source) {\n super(source);\n System.out.println(\"my Event\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n2、实现事件监听器\n\n@Component\nclass MyListenerA implements ApplicationListener {\n public void onApplicationEvent(MyEvent AyEvent) {\n System.out.println(\"ListenerA received\");\n }\n}\n\n@Component\nclass MyListenerB implements ApplicationListener {\n public void onApplicationEvent(MyEvent AyEvent) {\n System.out.println(\"ListenerB received\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n3、事件发布者\n\n@Component\npublic class MyPublisher implements ApplicationContextAware {\n private ApplicationContext applicationContext;\n \n public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {\n this.applicationContext=applicationContext;\n }\n \n public void publishEvent(ApplicationEvent event){\n System.out.println(\"publish event\");\n applicationContext.publishEvent(event);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n4、测试,先用注解方式将 MyPublisher 注入 Spring\n\n@Configuration\n@ComponentScan\npublic class AppConfig {\n\n @Bean(name = \"myPublisher\")\n public MyPublisher myPublisher(){\n return new MyPublisher();\n }\n}\npublic class Client {\n\n @Test\n public void main() {\n ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);\n MyPublisher myPublisher = (MyPublisher) context.getBean(\"myPublisher\");\n myPublisher.publishEvent(new MyEvent(this));\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n\n\n5、输出\n\nmy Event\npublish event\nListenerA received\nListenerB received\n\n\n1\n2\n3\n4\n", + "normalizedContent": "在软件系统中经常会有这样的需求:如果一个对象的状态发生改变,某些与它相关的对象也要随之做出相应的变化。 举个例子:\n\n * 微信公众号,如果一个用户订阅了某个公众号,那么便会收到公众号发来的消息,那么,公众号就是『被观察者』,而用户就是『观察者』\n * 气象站可以将每天预测到的温度、湿度、气压等以公告的形式发布给各种第三方网站,如果天气数据有更新,要能够实时的通知给第三方,这里的气象局就是『被观察者』,第三方网站就是『观察者』\n * mvc 模式中的模型与视图的关系也属于观察与被观察关系\n\n观察者模式是使用频率较高的设计模式之一。\n\n观察者模式包含观察目标和观察者两类对象,一个目标可以有任意数目的与之相依赖的观察者,一旦观察目标的状态发生改变,所有的观察者都将得到通知。\n\n\n# 定义\n\n观察者模式(observer pattern): 定义对象间一种一对多的依赖关系,使得当每一个对象改变状态,则所有依赖于它的对象都会得到通知并自动更新。\n\n观察者模式是一种对象行为型模式。\n\n观察者模式的别名包括发布-订阅(publish/subscribe)模式、模型-视图(model/view)模式、源-监听器(source/listener)模式或从属者(dependents)模式。\n\n细究的话,发布订阅和观察者有些不同,可以理解成发布订阅模式属于广义上的观察者模式。\n\n\n\n\n# 角色\n\n * subject(目标):被观察者,它是指被观察的对象。 从类图中可以看到,类中有一个用来存放观察者对象的vector 容器(vector在是安全的,而list则是不安全的),这个 vector 容器是被观察者类的核心,另外还有三个方法:attach 方法是向这个容器中添加观察者对象;detach 方法是从容器中移除观察者对象;notify 方法是依次调用观察者对象的对应方法。这个角色可以是接口,也可以是抽象类或者具体的类,因为很多情况下会与其他的模式混用,所以使用抽象类的情况比较多。\n\n * concretesubject(具体目标):具体目标是目标类的子类,通常它包含经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知。同时它还实现了在目标类中定义的抽象业务逻辑方法(如果有的话)。如果无须扩展目标类,则具体目标类可以省略。\n\n * observer(观察者):观察者将对观察目标的改变做出反应,观察者一般定义为接口,该接口声明了更新数据的方法 update(),因此又称为抽象观察者。\n\n * concreteobserver(具体观察者):在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者 observer 中定义的 update() 方法。通常在实现时,可以调用具体目标类的 attach() 方法将自己添加到目标类的集合中或通过 detach() 方法将自己从目标类的集合中删除。\n\n\n# 类图\n\n\n\n再记录下 uml 类图的注意事项,这里我的 subject 是抽象方法,所以用***斜体***,抽象方法也要用斜体,具体的各种箭头意义,我之前也总结过《设计模式前传——学设计模式前你要知道这些》(被网上各种帖子毒害过的自己,认真记录~~~)。\n\n\n# 实例\n\n1、定义观察者接口\n\ninterface observer {\n public void update();\n}\n\n\n1\n2\n3\n\n\n2、定义被观察者\n\nabstract class subject {\n private vector obs = new vector();\n\n public void addobserver(observer obs){\n this.obs.add(obs);\n }\n public void delobserver(observer obs){\n this.obs.remove(obs);\n }\n protected void notifyobserver(){\n for(observer o: obs){\n o.update();\n }\n }\n public abstract void dosomething();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\n3、具体的被观察者\n\nclass concretesubject extends subject {\n public void dosomething(){\n system.out.println(\"被观察者事件发生改变\");\n this.notifyobserver();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n4、具体的观察者\n\nclass concreteobserver1 implements observer {\n public void update() {\n system.out.println(\"观察者1收到信息,并进行处理\");\n }\n}\nclass concreteobserver2 implements observer {\n public void update() {\n system.out.println(\"观察者2收到信息,并进行处理\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n5、客户端\n\npublic class client {\n public static void main(string[] args){\n subject sub = new concretesubject();\n sub.addobserver(new concreteobserver1()); //添加观察者1\n sub.addobserver(new concreteobserver2()); //添加观察者2\n sub.dosomething();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\n输出\n\n被观察者事件发生改变\n观察者1收到信息,并进行处理\n观察者2收到信息,并进行处理\n\n\n1\n2\n3\n\n\n通过运行结果可以看到,我们只调用了 subject 的方法,但同时两个观察者的相关方法都被调用了。仔细看一下代码,其实很简单,就是在 subject 类中关联一下 observer 类,并且在 dosomething() 方法中遍历一下 observer 的 update() 方法就行了。\n\n\n# 优缺点\n\n\n# 优点\n\n降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系 目标与观察者之间建立了一套触发机制 支持广播通信 符合“开闭原则”的要求\n\n\n# 缺点\n\n目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率\n\n\n# 应用\n\n\n# jdk 中的观察者模式\n\n观察者模式在 java 语言中的地位非常重要。在 jdk 的 java.util 包中,提供了 observable 类以及 observer 接口,它们构成了 jdk 对观察者模式的支持(可以去查看下源码,写的比较严谨)。but,在 java9 被弃用了。\n\n\n# spring 中的观察者模式\n\nspring 事件驱动模型也是观察者模式很经典的应用。就是我们项目中最常见的事件监听器。\n\n# 1. spring 中观察者模式的四个角色\n\n * 事件:applicationevent 是所有事件对象的父类。applicationevent 继承自 jdk 的 eventobject,所有的事件都需要继承 applicationevent,并且通过 source 得到事件源。\n\n * spring 也为我们提供了很多内置事件,contextrefreshedevent、contextstartedevent、contextstoppedevent、contextclosedevent、requesthandledevent。\n\n * 事件监听:applicationlistener,也就是观察者,继承自 jdk 的 eventlistener,该类中只有一个方法 onapplicationevent。当监听的事件发生后该方法会被执行。\n\n * 事件源:applicationcontext,applicationcontext 是 spring 中的核心容器,在事件监听中 applicationcontext 可以作为事件的发布者,也就是事件源。因为 applicationcontext 继承自 applicationeventpublisher。在 applicationeventpublisher 中定义了事件发布的方法:publishevent(object event)\n\n * 事件管理:applicationeventmulticaster,用于事件监听器的注册和事件的广播。监听器的注册就是通过它来实现的,它的作用是把 applicationcontext 发布的 event 广播给它的监听器列表。\n\n# 2. coding~~~~~~\n\n1、定义事件\n\npublic class myevent extends applicationevent {\n public myevent(object source) {\n super(source);\n system.out.println(\"my event\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n2、实现事件监听器\n\n@component\nclass mylistenera implements applicationlistener {\n public void onapplicationevent(myevent ayevent) {\n system.out.println(\"listenera received\");\n }\n}\n\n@component\nclass mylistenerb implements applicationlistener {\n public void onapplicationevent(myevent ayevent) {\n system.out.println(\"listenerb received\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n3、事件发布者\n\n@component\npublic class mypublisher implements applicationcontextaware {\n private applicationcontext applicationcontext;\n \n public void setapplicationcontext(applicationcontext applicationcontext) throws beansexception {\n this.applicationcontext=applicationcontext;\n }\n \n public void publishevent(applicationevent event){\n system.out.println(\"publish event\");\n applicationcontext.publishevent(event);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n4、测试,先用注解方式将 mypublisher 注入 spring\n\n@configuration\n@componentscan\npublic class appconfig {\n\n @bean(name = \"mypublisher\")\n public mypublisher mypublisher(){\n return new mypublisher();\n }\n}\npublic class client {\n\n @test\n public void main() {\n applicationcontext context = new annotationconfigapplicationcontext(appconfig.class);\n mypublisher mypublisher = (mypublisher) context.getbean(\"mypublisher\");\n mypublisher.publishevent(new myevent(this));\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n\n\n5、输出\n\nmy event\npublish event\nlistenera received\nlistenerb received\n\n\n1\n2\n3\n4\n", "charsets": { "cjk": true }, @@ -3589,11 +3695,11 @@ export const siteData = { "lastUpdatedTimestamp": 1663688099000 }, { - "title": "责任链模式", + "title": "策略模式", "frontmatter": { - "title": "责任链模式", - "date": "2022-05-21T17:28:46.000Z", - "permalink": "/pages/d3ff35/", + "title": "策略模式", + "date": "2022-05-21T17:29:26.000Z", + "permalink": "/pages/78c7aa/", "categories": [ "Java相关", "设计模式" @@ -3602,83 +3708,147 @@ export const siteData = { null ] }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/030.%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F.html", - "relativePath": "01.Java相关/40.设计模式/030.责任链模式.md", - "key": "v-34f8aaa2", - "path": "/pages/d3ff35/", + "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/050.%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F.html", + "relativePath": "01.Java相关/40.设计模式/050.策略模式.md", + "key": "v-5b0157cf", + "path": "/pages/78c7aa/", "headers": [ { "level": 2, "title": "定义", "slug": "定义", "normalizedTitle": "定义", - "charIndex": 1127 + "charIndex": 2 }, { "level": 2, "title": "角色", "slug": "角色", "normalizedTitle": "角色", - "charIndex": 1246 + "charIndex": 1075 }, { "level": 2, - "title": "类图", - "slug": "类图", - "normalizedTitle": "类图", - "charIndex": 1521 + "title": "实例", + "slug": "实例", + "normalizedTitle": "实例", + "charIndex": 1229 + }, + { + "level": 2, + "title": "解析策略模式", + "slug": "解析策略模式", + "normalizedTitle": "解析策略模式", + "charIndex": 2310 + }, + { + "level": 2, + "title": "谁来选择具体的策略算法", + "slug": "谁来选择具体的策略算法", + "normalizedTitle": "谁来选择具体的策略算法", + "charIndex": 2795 + }, + { + "level": 2, + "title": "优缺点", + "slug": "优缺点", + "normalizedTitle": "优缺点", + "charIndex": 2992 + }, + { + "level": 3, + "title": "优点:", + "slug": "优点", + "normalizedTitle": "优点:", + "charIndex": 3000 }, + { + "level": 3, + "title": "缺点:", + "slug": "缺点", + "normalizedTitle": "缺点:", + "charIndex": 3092 + }, + { + "level": 2, + "title": "思考", + "slug": "思考", + "normalizedTitle": "思考", + "charIndex": 3233 + }, + { + "level": 2, + "title": "策略模式在 JDK 中的应用", + "slug": "策略模式在-jdk-中的应用", + "normalizedTitle": "策略模式在 jdk 中的应用", + "charIndex": 3512 + } + ], + "headersStr": "定义 角色 实例 解析策略模式 谁来选择具体的策略算法 优缺点 优点: 缺点: 思考 策略模式在 JDK 中的应用", + "content": "# 定义\n\n策略模式(Strategy Design Pattern):封装可以互换的行为,并使用委托来决定要使用哪一个。\n\n策略模式是一种行为设计模式, 它能让你定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换。\n\n> 用人话翻译后就是:运行时我给你这个类的方法传不同的 “key”,你这个方法就去执行不同的业务逻辑。 你品,你细品,这不就是 if else 干的事吗?\n\n先直观的看下传统的多重 if else 代码\n\npublic String getCheckResult(String type) {\n if (\"校验1\".equals(type)) {\n return \"执行业务逻辑1\";\n } else if (\"校验2\".equals(type)) {\n return \"执行业务逻辑2\";\n } else if (\"校验3\".equals(type)) {\n return \"执行业务逻辑3\";\n } else if (\"校验4\".equals(type)) {\n return \"执行业务逻辑4\";\n } else if (\"校验5\".equals(type)) {\n return \"执行业务逻辑5\";\n } else if (\"校验6\".equals(type)) {\n return \"执行业务逻辑6\";\n } else if (\"校验7\".equals(type)) {\n return \"执行业务逻辑7\";\n } else if (\"校验8\".equals(type)) {\n return \"执行业务逻辑8\";\n } else if (\"校验9\".equals(type)) {\n return \"执行业务逻辑9\";\n }\n return \"不在处理的逻辑中返回业务错误\";\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n\n\n这么看,你要是还觉得挺清晰的话,想象下这些 return 里是各种复杂的业务逻辑方法~~\n\n当然,策略模式的作用可不止是避免冗长的 if-else 或者 switch 分支,它还可以像模板方法模式那样提供框架的扩展点等。\n\n网上的示例很多,比如不同路线的规划、不同支付方式的选择 都是典型的 if-else 问题,也都是典型的策略模式问题,栗子我们待会看,先看下策略模式的类图,然后去改造多重判断~\n\n\n# 角色\n\n策略模式涉及到三个角色:\n\n 1. Strategy:策略接口或者策略抽象类,用来约束一系列的策略算法(Context 使用这个接口来调用具体的策略实现算法)\n 2. ConcreateStrategy:具体的策略类(实现策略接口或继承抽象策略类)\n 3. Context:上下文类,持有具体策略类的实例,并负责调用相关的算法 应用策略模式来解决问题的思路\n\n\n# 实例\n\n先看看最简单的策略模式 demo:\n\n1、策略接口(定义策略)\n\npublic interface Strategy {\n void operate();\n}\n\n\n1\n2\n3\n\n\n2、具体的算法实现\n\npublic class ConcreteStrategyA implements Strategy {\n @Override\n public void operate() {\n //具体的算法实现\n System.out.println(\"执行业务逻辑A\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\npublic class ConcreteStrategyB implements Strategy {\n @Override\n public void operate() {\n //具体的算法实现\n System.out.println(\"执行业务逻辑B\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n3、上下文的实现\n\npublic class Context {\n\n //持有一个具体的策略对象\n private Strategy strategy;\n\n //构造方法,传入具体的策略对象\n public Context(Strategy strategy){\n this.strategy = strategy;\n }\n\n public void doSomething(){\n //调用具体的策略对象进操作\n strategy.operate();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n\n\n4、客户端使用(策略的使用)\n\npublic static void main(String[] args) {\n Context context = new Context(new ConcreteStrategyA());\n context.doSomething();\n}\n\n\n1\n2\n3\n4\n\n\n> ps:这种策略的使用方式其实很死板,真正使用的时候如果还这么写, 和写一大推 if-else 没什么区别,所以我们一般会结合工厂类, 在运行时动态确定使用哪种策略。策略模式侧重如何选择策略、工厂模式侧重如何创建策略。\n\n\n# 解析策略模式\n\n策略模式的功能就是把具体的算法实现从具体的业务处理中独立出来,把它们实现成单独的算法类,从而形成一系列算法,并让这些算法可以互相替换。\n\n> 策略模式的重心不是如何来实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活,具有更好的维护性和扩展性。\n\n实际上,每个策略算法具体实现的功能,就是原来在 if-else 结构中的具体实现,每个 if-else 语句都是一个平等的功能结构,可以说是兄弟关系。\n\n策略模式呢,就是把各个平等的具体实现封装到单独的策略实现类了,然后通过上下文与具体的策略类进行交互。\n\n『 策略模式 = 实现策略接口(或抽象类)的每个策略类 + 上下文的逻辑分派 』\n\n> 策略模式的本质:分离算法,选择实现 ——《研磨设计模式》\n\n所以说,策略模式只是在代码结构上的一个调整,即使用了策略模式,该写的逻辑一个也少不了,到逻辑分派的时候,只是变相的 if-else。\n\n而它的优化点是抽象了出了接口,将业务逻辑封装成一个一个的实现类,任意地替换。在复杂场景(业务逻辑较多)时比直接 if-else 更好维护和扩展些。\n\n\n# 谁来选择具体的策略算法\n\n如果你手写了上边的 demo,就会发现,这玩意不及 if-else 来的顺手,尤其是在判断逻辑的时候,每个逻辑都要要构造一个上下文对象,费劲。\n\n其实,策略模式中,我们可以自己定义谁来选择具体的策略算法,有两种:\n\n * 客户端:当使用上下文时,由客户端选择,像我们上边的 demo\n * 上下文:客户端不用选,由上下文来选具体的策略算法,可以在构造器中指定\n\n\n# 优缺点\n\n\n# 优点:\n\n避免多重条件语句:也就是避免大量的 if-else 更好的扩展性(完全符合开闭原则):策略模式中扩展新的策略实现很容易,无需对上下文修改,只增加新的策略实现类就可以\n\n\n# 缺点:\n\n客户必须了解每种策略的不同(这个可以通过 IOC、依赖注入的方式解决) 增加了对象数:每个具体策略都封装成了类,可能备选的策略会很多 只适合扁平的算法结构:策略模式的一系列算法是平等的,也就是在运行时刻只有一个算法会被使用,这就限制了算法使用的层级,不能嵌套使用\n\n\n# 思考\n\n实际使用中,往往不会只是单一的某个设计模式的套用,一般都会混合使用,而且模式之间的结合也是没有定势的,要具体问题具体分析。\n\n策略模式往往会结合其他模式一起使用,比如工厂、模板等,具体使用需要结合自己的业务。\n\n切记,不要为了使用设计模式而强行模式,不要把简单问题复杂化。\n\n策略模式也不是专为消除 if-else 而生的,不要和 if-else 划等号。它体现了“对修改关闭,对扩展开放“的原则。\n\n并不是说,看到 if-else 就想着用策略模式去优化,业务逻辑简单,可能几个枚举,或者几个卫语句就搞定的场景,就不用非得硬套设计模式了。\n\n\n# 策略模式在 JDK 中的应用\n\n在 JDK 中,Comparator 比较器是一个策略接口,我们常用的 compare() 方法就是一个具体的策略实现,用于定义排序规则。\n\npublic interface Comparator {\n int compare(T o1, T o2);\n //......\n}\n\n\n1\n2\n3\n4\n\n\n当我们想自定义排序规则的时候,就可以实现 Comparator 。\n\n这时候我们重写了接口中的 compare() 方法,就是具体的策略类(只不过这里可能是内部类)。当我们在调用 Arrays 的排序方法 sort() 时,可以用默认的排序规则,也可以用自定义的规则。\n\npublic static void main(String[] args) {\n Integer[] data = {4,2,7,5,1,9};\n Comparator comparator = new Comparator() {\n @Override\n public int compare(Integer o1, Integer o2) {\n if(o1 > o2){\n return 1;\n } else {\n return -1;\n }\n }\n };\n\n Arrays.sort(data,comparator);\n System.out.println(Arrays.toString(data));\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\nArrays 的 sort() 方法,有自定义规则就按自己的方法排序,反之走源码逻辑。\n\npublic static void sort(T[] a, Comparator c) {\n if (c == null) {\n sort(a);\n } else {\n if (LegacyMergeSort.userRequested)\n legacyMergeSort(a, c);\n else\n TimSort.sort(a, 0, a.length, c, null, 0, 0);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n还有,ThreadPoolExecutor 中的拒绝策略 RejectedExecutionHandler 也是典型的策略模式,感兴趣的也可以再看看源码。", + "normalizedContent": "# 定义\n\n策略模式(strategy design pattern):封装可以互换的行为,并使用委托来决定要使用哪一个。\n\n策略模式是一种行为设计模式, 它能让你定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换。\n\n> 用人话翻译后就是:运行时我给你这个类的方法传不同的 “key”,你这个方法就去执行不同的业务逻辑。 你品,你细品,这不就是 if else 干的事吗?\n\n先直观的看下传统的多重 if else 代码\n\npublic string getcheckresult(string type) {\n if (\"校验1\".equals(type)) {\n return \"执行业务逻辑1\";\n } else if (\"校验2\".equals(type)) {\n return \"执行业务逻辑2\";\n } else if (\"校验3\".equals(type)) {\n return \"执行业务逻辑3\";\n } else if (\"校验4\".equals(type)) {\n return \"执行业务逻辑4\";\n } else if (\"校验5\".equals(type)) {\n return \"执行业务逻辑5\";\n } else if (\"校验6\".equals(type)) {\n return \"执行业务逻辑6\";\n } else if (\"校验7\".equals(type)) {\n return \"执行业务逻辑7\";\n } else if (\"校验8\".equals(type)) {\n return \"执行业务逻辑8\";\n } else if (\"校验9\".equals(type)) {\n return \"执行业务逻辑9\";\n }\n return \"不在处理的逻辑中返回业务错误\";\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n\n\n这么看,你要是还觉得挺清晰的话,想象下这些 return 里是各种复杂的业务逻辑方法~~\n\n当然,策略模式的作用可不止是避免冗长的 if-else 或者 switch 分支,它还可以像模板方法模式那样提供框架的扩展点等。\n\n网上的示例很多,比如不同路线的规划、不同支付方式的选择 都是典型的 if-else 问题,也都是典型的策略模式问题,栗子我们待会看,先看下策略模式的类图,然后去改造多重判断~\n\n\n# 角色\n\n策略模式涉及到三个角色:\n\n 1. strategy:策略接口或者策略抽象类,用来约束一系列的策略算法(context 使用这个接口来调用具体的策略实现算法)\n 2. concreatestrategy:具体的策略类(实现策略接口或继承抽象策略类)\n 3. context:上下文类,持有具体策略类的实例,并负责调用相关的算法 应用策略模式来解决问题的思路\n\n\n# 实例\n\n先看看最简单的策略模式 demo:\n\n1、策略接口(定义策略)\n\npublic interface strategy {\n void operate();\n}\n\n\n1\n2\n3\n\n\n2、具体的算法实现\n\npublic class concretestrategya implements strategy {\n @override\n public void operate() {\n //具体的算法实现\n system.out.println(\"执行业务逻辑a\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\npublic class concretestrategyb implements strategy {\n @override\n public void operate() {\n //具体的算法实现\n system.out.println(\"执行业务逻辑b\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n3、上下文的实现\n\npublic class context {\n\n //持有一个具体的策略对象\n private strategy strategy;\n\n //构造方法,传入具体的策略对象\n public context(strategy strategy){\n this.strategy = strategy;\n }\n\n public void dosomething(){\n //调用具体的策略对象进操作\n strategy.operate();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n\n\n4、客户端使用(策略的使用)\n\npublic static void main(string[] args) {\n context context = new context(new concretestrategya());\n context.dosomething();\n}\n\n\n1\n2\n3\n4\n\n\n> ps:这种策略的使用方式其实很死板,真正使用的时候如果还这么写, 和写一大推 if-else 没什么区别,所以我们一般会结合工厂类, 在运行时动态确定使用哪种策略。策略模式侧重如何选择策略、工厂模式侧重如何创建策略。\n\n\n# 解析策略模式\n\n策略模式的功能就是把具体的算法实现从具体的业务处理中独立出来,把它们实现成单独的算法类,从而形成一系列算法,并让这些算法可以互相替换。\n\n> 策略模式的重心不是如何来实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活,具有更好的维护性和扩展性。\n\n实际上,每个策略算法具体实现的功能,就是原来在 if-else 结构中的具体实现,每个 if-else 语句都是一个平等的功能结构,可以说是兄弟关系。\n\n策略模式呢,就是把各个平等的具体实现封装到单独的策略实现类了,然后通过上下文与具体的策略类进行交互。\n\n『 策略模式 = 实现策略接口(或抽象类)的每个策略类 + 上下文的逻辑分派 』\n\n> 策略模式的本质:分离算法,选择实现 ——《研磨设计模式》\n\n所以说,策略模式只是在代码结构上的一个调整,即使用了策略模式,该写的逻辑一个也少不了,到逻辑分派的时候,只是变相的 if-else。\n\n而它的优化点是抽象了出了接口,将业务逻辑封装成一个一个的实现类,任意地替换。在复杂场景(业务逻辑较多)时比直接 if-else 更好维护和扩展些。\n\n\n# 谁来选择具体的策略算法\n\n如果你手写了上边的 demo,就会发现,这玩意不及 if-else 来的顺手,尤其是在判断逻辑的时候,每个逻辑都要要构造一个上下文对象,费劲。\n\n其实,策略模式中,我们可以自己定义谁来选择具体的策略算法,有两种:\n\n * 客户端:当使用上下文时,由客户端选择,像我们上边的 demo\n * 上下文:客户端不用选,由上下文来选具体的策略算法,可以在构造器中指定\n\n\n# 优缺点\n\n\n# 优点:\n\n避免多重条件语句:也就是避免大量的 if-else 更好的扩展性(完全符合开闭原则):策略模式中扩展新的策略实现很容易,无需对上下文修改,只增加新的策略实现类就可以\n\n\n# 缺点:\n\n客户必须了解每种策略的不同(这个可以通过 ioc、依赖注入的方式解决) 增加了对象数:每个具体策略都封装成了类,可能备选的策略会很多 只适合扁平的算法结构:策略模式的一系列算法是平等的,也就是在运行时刻只有一个算法会被使用,这就限制了算法使用的层级,不能嵌套使用\n\n\n# 思考\n\n实际使用中,往往不会只是单一的某个设计模式的套用,一般都会混合使用,而且模式之间的结合也是没有定势的,要具体问题具体分析。\n\n策略模式往往会结合其他模式一起使用,比如工厂、模板等,具体使用需要结合自己的业务。\n\n切记,不要为了使用设计模式而强行模式,不要把简单问题复杂化。\n\n策略模式也不是专为消除 if-else 而生的,不要和 if-else 划等号。它体现了“对修改关闭,对扩展开放“的原则。\n\n并不是说,看到 if-else 就想着用策略模式去优化,业务逻辑简单,可能几个枚举,或者几个卫语句就搞定的场景,就不用非得硬套设计模式了。\n\n\n# 策略模式在 jdk 中的应用\n\n在 jdk 中,comparator 比较器是一个策略接口,我们常用的 compare() 方法就是一个具体的策略实现,用于定义排序规则。\n\npublic interface comparator {\n int compare(t o1, t o2);\n //......\n}\n\n\n1\n2\n3\n4\n\n\n当我们想自定义排序规则的时候,就可以实现 comparator 。\n\n这时候我们重写了接口中的 compare() 方法,就是具体的策略类(只不过这里可能是内部类)。当我们在调用 arrays 的排序方法 sort() 时,可以用默认的排序规则,也可以用自定义的规则。\n\npublic static void main(string[] args) {\n integer[] data = {4,2,7,5,1,9};\n comparator comparator = new comparator() {\n @override\n public int compare(integer o1, integer o2) {\n if(o1 > o2){\n return 1;\n } else {\n return -1;\n }\n }\n };\n\n arrays.sort(data,comparator);\n system.out.println(arrays.tostring(data));\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\narrays 的 sort() 方法,有自定义规则就按自己的方法排序,反之走源码逻辑。\n\npublic static void sort(t[] a, comparator c) {\n if (c == null) {\n sort(a);\n } else {\n if (legacymergesort.userrequested)\n legacymergesort(a, c);\n else\n timsort.sort(a, 0, a.length, c, null, 0, 0);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n还有,threadpoolexecutor 中的拒绝策略 rejectedexecutionhandler 也是典型的策略模式,感兴趣的也可以再看看源码。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/20, 23:34:59", + "lastUpdatedTimestamp": 1663688099000 + }, + { + "title": "简述常见的 HTTP 状态码的含义", + "frontmatter": { + "title": "简述常见的 HTTP 状态码的含义", + "date": "2022-06-23T21:09:29.000Z", + "permalink": "/pages/f33310/", + "categories": [ + "计算机基础", + "计算机网络" + ], + "tags": [ + null + ] + }, + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/010.%E7%AE%80%E8%BF%B0%E5%B8%B8%E8%A7%81%E7%9A%84%20HTTP%20%E7%8A%B6%E6%80%81%E7%A0%81%E7%9A%84%E5%90%AB%E4%B9%89.html", + "relativePath": "02.计算机基础/10.计算机网络/010.简述常见的 HTTP 状态码的含义.md", + "key": "v-0a1d1fb4", + "path": "/pages/f33310/", + "headers": [ { "level": 2, - "title": "coding", - "slug": "coding", - "normalizedTitle": "coding", - "charIndex": 1530 + "title": "2XX 成功", + "slug": "_2xx-成功", + "normalizedTitle": "2xx 成功", + "charIndex": 2 }, { "level": 2, - "title": "实例", - "slug": "实例", - "normalizedTitle": "实例", - "charIndex": 3309 + "title": "3XX 重定向", + "slug": "_3xx-重定向", + "normalizedTitle": "3xx 重定向", + "charIndex": 109 }, { "level": 2, - "title": "应用", - "slug": "应用", - "normalizedTitle": "应用", - "charIndex": 784 - }, - { - "level": 3, - "title": "Servlet 中的责任链", - "slug": "servlet-中的责任链", - "normalizedTitle": "servlet 中的责任链", - "charIndex": 3615 - }, - { - "level": 3, - "title": "Spring MVC 中的责任链", - "slug": "spring-mvc-中的责任链", - "normalizedTitle": "spring mvc 中的责任链", - "charIndex": 5641 + "title": "4XX 客户端错误", + "slug": "_4xx-客户端错误", + "normalizedTitle": "4xx 客户端错误", + "charIndex": 333 }, { "level": 2, - "title": "总结", - "slug": "总结", - "normalizedTitle": "总结", - "charIndex": 8299 + "title": "5XX 服务器错误", + "slug": "_5xx-服务器错误", + "normalizedTitle": "5xx 服务器错误", + "charIndex": 483 } ], - "headersStr": "定义 角色 类图 coding 实例 应用 Servlet 中的责任链 Spring MVC 中的责任链 总结", - "content": "责任链,顾名思义,就是用来处理相关事务责任的一条执行链,执行链上有多个节点,每个节点都有机会(条件匹配)处理请求事务,如果某个节点处理完了就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。\n\n这种模式给予请求的类型,对请求的发送者和接收者进行解耦。属于行为型模式。\n\n在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。\n\n\n\n先来看一段代码\n\npublic void test(int i, Request request){\n if(i==1){\n Handler1.response(request);\n }else if(i == 2){\n Handler2.response(request);\n }else if(i == 3){\n Handler3.response(request);\n }else if(i == 4){\n Handler4.response(request);\n }else{\n Handler5.response(request);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n代码的业务逻辑是这样的,方法有两个参数:整数 i 和一个请求 request,根据 i 的值来决定由谁来处理 request,如果 i==1,由 Handler1来处理,如果 i==2,由 Handler2 来处理,以此类推。在编程中,这种处理业务的方法非常常见,所有处理请求的类由 if…else… 条件判断语句连成一条责任链来对请求进行处理,相信大家都经常用到。这种方法的优点是非常直观,简单明了,并且比较容易维护,但是这种方法也存在着几个比较令人头疼的问题:\n\n代码臃肿:实际应用中的判定条件通常不是这么简单地判断是否为1或者是否为2,也许需要复杂的计算,也许需要查询数据库等等,这就会有很多额外的代码,如果判断条件再比较多,那么这个if…else…语句基本上就没法看了。\n\n耦合度高:如果我们想继续添加处理请求的类,那么就要继续添加if…else…判定条件;另外,这个条件判定的顺序也是写死的,如果想改变顺序,那么也只能修改这个条件语句。 既然缺点我们已经清楚了,就要想办法来解决。这个场景的业务逻辑很简单:如果满足条件1,则由 Handler1 来处理,不满足则向下传递;如果满足条件2,则由 Handler2 来处理,不满足则继续向下传递,以此类推,直到条件结束。其实改进的方法也很简单,就是把判定条件的部分放到处理类中,这就是责任连模式的原理。\n\n\n# 定义\n\n责任链模式(Chain of Responsibility Pattern):使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。\n\n\n# 角色\n\nHandler: 抽象处理类,抽象处理类中主要包含一个指向下一处理类的成员变量 nextHandler 和一个处理请求的方法 handRequest,handRequest 方法的主要主要思想是,如果满足处理的条件,则由本处理类来进行处理,否则由 nextHandler 来处理\n\nConcreteHandler: 具体处理类主要是对具体的处理逻辑和处理的适用条件进行实现。具体处理者接到请求后,可以选择将请求处理掉,或者将请求传给下家。由于具体处理者持有对下家的引用,因此,如果需要,具体处理者可以访问下家\n\nClient:客户端\n\n\n# 类图\n\n\n\n\n# coding\n\npublic abstract class Handler {\n private Handler nextHandler;\n private int level;\n\n public Handler(int level) {\n this.level = level;\n }\n\n public void setNextHandler(Handler handler){\n this.nextHandler = handler;\n }\n\n public final void handlerRequest(Request request){\n if(level == request.getLevel()){\n this.response(request);\n }else{\n if (this.nextHandler != null){\n this.nextHandler.handlerRequest(request);\n }else{\n System.out.println(\"===已经没有处理器了===\");\n }\n }\n\n }\n // 抽象方法,子类实现\n public abstract void response(Request request);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n\n\nclass Request {\n int level = 0;\n public Request(int level){\n this.level = level;\n }\n public int getLevel() {\n return level;\n }\n}\npublic class ConcreteHandler1 extends Handler {\n public ConcreteHandler1(int level) {\n super(level);\n }\n\n @Override\n public void response(Request request) {\n System.out.println(\"请求由处理器1进行处理\");\n }\n}\n\npublic class ConcreteHandler2 extends Handler {\n\t//...\n}\n\npublic class ConcreteHandler2 extends Handler {\n\t//...\n}\npublic class Client {\n public static void main(String[] args) {\n ConcreteHandler1 handler1 = new ConcreteHandler1(1);\n ConcreteHandler2 handler2 = new ConcreteHandler2(2);\n ConcreteHandler3 handler3 = new ConcreteHandler3(3);\n\t\t//处理者构成一个环形\n handler1.setNextHandler(handler2);\n handler2.setNextHandler(handler3);\n\n handler1.handlerRequest(new Request(1));\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n\n\n\n# 实例\n\n当你想要让一个以上的对象有机会能够处理某个请求的时候,就是用责任链模式。\n\n通过责任链模式,你可以为某个请求创建一个对象链。每个对象依序检查此请求,并对其进行处理,或者将它传给链中的下一个对象。\n\n比如\n\n * 程序员要请 3 天以上的假期,在 OA 申请,需要直接主管、总监、HR 层层审批后才生效。类似的采购审批、报销审批。。。\n\n * 美团在外卖营销业务中资源位展示的逻辑\n\n\n# 应用\n\nJAVA 中的异常处理机制、JAVA WEB 中 Apache Tomcat 对 Encoding 的处理,Struts2 的拦截器,JSP、Servlet 的 Filter 均是责任链的典型应用。\n\n\n# Servlet 中的责任链\n\npublic final class ApplicationFilterChain implements FilterChain {\n private static final ThreadLocal lastServicedRequest;\n private static final ThreadLocal lastServicedResponse;\n public static final int INCREMENT = 10;\n private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];\n private int pos = 0; //下一个要执行的filter的位置\n private int n = 0; //filter个数\n private Servlet servlet = null;\n public ApplicationFilterChain() {\n }\n\n public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {\n if (Globals.IS_SECURITY_ENABLED) {\n final ServletRequest req = request;\n final ServletResponse res = response;\n\n try {\n AccessController.doPrivileged(new PrivilegedExceptionAction() {\n public Void run() throws ServletException, IOException {\n ApplicationFilterChain.this.internalDoFilter(req, res);\n return null;\n }\n });\n } catch (PrivilegedActionException var7) {\n Exception e = var7.getException();\n if (e instanceof ServletException) {\n throw (ServletException)e;\n }\n\n if (e instanceof IOException) {\n throw (IOException)e;\n }\n\n if (e instanceof RuntimeException) {\n throw (RuntimeException)e;\n }\n\n throw new ServletException(e.getMessage(), e);\n }\n } else {\n this.internalDoFilter(request, response);\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n\n\nFilterChain 就是一条过滤链。其中每个过滤器(Filter)都可以决定是否执行下一步。过滤分两个方向,进和出:\n\n进:在把 ServletRequest 和 ServletResponse 交给 Servlet 的 service 方法之前,需要进行过滤\n\n出:在service方法完成后,往客户端发送之前,需要进行过滤\n\n\n# Spring MVC 中的责任链\n\nSpring MVC 的 diapatcherServlet 的 doDispatch 方法中,获取与请求匹配的处理器 HandlerExecutionChain 就是用到了责任链模式。\n\nprotected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {\n HttpServletRequest processedRequest = request;\n HandlerExecutionChain mappedHandler = null; //使用到了责任链模式\n boolean multipartRequestParsed = false;\n WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);\n\n try {\n try {\n ModelAndView mv = null;\n Object dispatchException = null;\n\n try {\n processedRequest = this.checkMultipart(request);\n multipartRequestParsed = processedRequest != request;\n mappedHandler = this.getHandler(processedRequest); \n if (mappedHandler == null) {\n this.noHandlerFound(processedRequest, response);\n return;\n }\n\n HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());\n String method = request.getMethod();\n boolean isGet = \"GET\".equals(method);\n if (isGet || \"HEAD\".equals(method)) {\n long lastModified = ha.getLastModified(request, mappedHandler.getHandler());\n if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {\n return;\n }\n }\n\t\t\t\t\t\t\t\t\t\t//责任链模式执行预处理方法,其实是将请求交给注册的拦截器执行\n if (!mappedHandler.applyPreHandle(processedRequest, response)) {\n return;\n }\n\n mv = ha.handle(processedRequest, response, mappedHandler.getHandler());\n if (asyncManager.isConcurrentHandlingStarted()) {\n return;\n }\n\n this.applyDefaultViewName(processedRequest, mv);\n //责任链执行后处理方法\n mappedHandler.applyPostHandle(processedRequest, response, mv);\n } catch (Exception var22) {\n //...\n } finally {\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n\n\n * SpringMVC 请求的流程中,执行了拦截器相关方法 interceptor.preHandler 等等\n\n * 在处理 SpringMVC 请求时,使用到职责链模式还使用到适配器模式\n\n * HandlerExecutionChain 主要负责的是请求拦截器的执行和请求处理,但是他本身不处理请求,只是将请求分配给链上注册处理器执行,这是职责链实现方式,减少职责链本身与处理逻辑之间的耦合,规范了处理流程\n\n * HandlerExecutionChain 维护了 HandlerInterceptor 的集合, 可以向其中注册相应的拦截器\n\n\n# 总结\n\n责任链模式其实就是一个灵活版的 if…else…语句,它就是将这些判定条件的语句放到了各个处理类中,这样做的优点是比较灵活了,但同样也带来了风险,比如设置处理类前后关系时,一定要特别仔细,搞对处理类前后逻辑的条件判断关系,并且注意不要在链中出现循环引用的问题。\n\n优点:\n\n降低耦合度:将请求和处理分开,实现解耦,提高了系统的灵活性。\n\n简化了对象:对象不需要知道链的结构\n\n良好的扩展性:增加处理者的实现很简单,只需重写处理请求业务逻辑的方法。\n\n缺点:\n\n从链头发出,直到有处理者响应,在责任链比较长的时候会影响系统性能,一般需要在 Handler 中设置一个最大节点数。\n\n请求递归,调试排错比较麻烦。\n\n使用场景:\n\n有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。\n\n在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。\n\n可动态指定一组对象处理请求。\n\n模式的扩展:\n\n职责链模式存在以下两种情况。\n\n * 纯的职责链模式:一个请求必须被某一个处理者对象所接收,且一个具体处理者对某个请求的处理只能采用以下两种行为之一:自己处理(承担责任);把责任推给下家处理。\n\n * 不纯的职责链模式:允许出现某一个具体处理者对象在承担了请求的一部分责任后又将剩余的责任传给下家的情况,且一个请求可以最终不被任何接收端对象所接收。", - "normalizedContent": "责任链,顾名思义,就是用来处理相关事务责任的一条执行链,执行链上有多个节点,每个节点都有机会(条件匹配)处理请求事务,如果某个节点处理完了就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。\n\n这种模式给予请求的类型,对请求的发送者和接收者进行解耦。属于行为型模式。\n\n在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。\n\n\n\n先来看一段代码\n\npublic void test(int i, request request){\n if(i==1){\n handler1.response(request);\n }else if(i == 2){\n handler2.response(request);\n }else if(i == 3){\n handler3.response(request);\n }else if(i == 4){\n handler4.response(request);\n }else{\n handler5.response(request);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n代码的业务逻辑是这样的,方法有两个参数:整数 i 和一个请求 request,根据 i 的值来决定由谁来处理 request,如果 i==1,由 handler1来处理,如果 i==2,由 handler2 来处理,以此类推。在编程中,这种处理业务的方法非常常见,所有处理请求的类由 if…else… 条件判断语句连成一条责任链来对请求进行处理,相信大家都经常用到。这种方法的优点是非常直观,简单明了,并且比较容易维护,但是这种方法也存在着几个比较令人头疼的问题:\n\n代码臃肿:实际应用中的判定条件通常不是这么简单地判断是否为1或者是否为2,也许需要复杂的计算,也许需要查询数据库等等,这就会有很多额外的代码,如果判断条件再比较多,那么这个if…else…语句基本上就没法看了。\n\n耦合度高:如果我们想继续添加处理请求的类,那么就要继续添加if…else…判定条件;另外,这个条件判定的顺序也是写死的,如果想改变顺序,那么也只能修改这个条件语句。 既然缺点我们已经清楚了,就要想办法来解决。这个场景的业务逻辑很简单:如果满足条件1,则由 handler1 来处理,不满足则向下传递;如果满足条件2,则由 handler2 来处理,不满足则继续向下传递,以此类推,直到条件结束。其实改进的方法也很简单,就是把判定条件的部分放到处理类中,这就是责任连模式的原理。\n\n\n# 定义\n\n责任链模式(chain of responsibility pattern):使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。\n\n\n# 角色\n\nhandler: 抽象处理类,抽象处理类中主要包含一个指向下一处理类的成员变量 nexthandler 和一个处理请求的方法 handrequest,handrequest 方法的主要主要思想是,如果满足处理的条件,则由本处理类来进行处理,否则由 nexthandler 来处理\n\nconcretehandler: 具体处理类主要是对具体的处理逻辑和处理的适用条件进行实现。具体处理者接到请求后,可以选择将请求处理掉,或者将请求传给下家。由于具体处理者持有对下家的引用,因此,如果需要,具体处理者可以访问下家\n\nclient:客户端\n\n\n# 类图\n\n\n\n\n# coding\n\npublic abstract class handler {\n private handler nexthandler;\n private int level;\n\n public handler(int level) {\n this.level = level;\n }\n\n public void setnexthandler(handler handler){\n this.nexthandler = handler;\n }\n\n public final void handlerrequest(request request){\n if(level == request.getlevel()){\n this.response(request);\n }else{\n if (this.nexthandler != null){\n this.nexthandler.handlerrequest(request);\n }else{\n system.out.println(\"===已经没有处理器了===\");\n }\n }\n\n }\n // 抽象方法,子类实现\n public abstract void response(request request);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n\n\nclass request {\n int level = 0;\n public request(int level){\n this.level = level;\n }\n public int getlevel() {\n return level;\n }\n}\npublic class concretehandler1 extends handler {\n public concretehandler1(int level) {\n super(level);\n }\n\n @override\n public void response(request request) {\n system.out.println(\"请求由处理器1进行处理\");\n }\n}\n\npublic class concretehandler2 extends handler {\n\t//...\n}\n\npublic class concretehandler2 extends handler {\n\t//...\n}\npublic class client {\n public static void main(string[] args) {\n concretehandler1 handler1 = new concretehandler1(1);\n concretehandler2 handler2 = new concretehandler2(2);\n concretehandler3 handler3 = new concretehandler3(3);\n\t\t//处理者构成一个环形\n handler1.setnexthandler(handler2);\n handler2.setnexthandler(handler3);\n\n handler1.handlerrequest(new request(1));\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n\n\n\n# 实例\n\n当你想要让一个以上的对象有机会能够处理某个请求的时候,就是用责任链模式。\n\n通过责任链模式,你可以为某个请求创建一个对象链。每个对象依序检查此请求,并对其进行处理,或者将它传给链中的下一个对象。\n\n比如\n\n * 程序员要请 3 天以上的假期,在 oa 申请,需要直接主管、总监、hr 层层审批后才生效。类似的采购审批、报销审批。。。\n\n * 美团在外卖营销业务中资源位展示的逻辑\n\n\n# 应用\n\njava 中的异常处理机制、java web 中 apache tomcat 对 encoding 的处理,struts2 的拦截器,jsp、servlet 的 filter 均是责任链的典型应用。\n\n\n# servlet 中的责任链\n\npublic final class applicationfilterchain implements filterchain {\n private static final threadlocal lastservicedrequest;\n private static final threadlocal lastservicedresponse;\n public static final int increment = 10;\n private applicationfilterconfig[] filters = new applicationfilterconfig[0];\n private int pos = 0; //下一个要执行的filter的位置\n private int n = 0; //filter个数\n private servlet servlet = null;\n public applicationfilterchain() {\n }\n\n public void dofilter(servletrequest request, servletresponse response) throws ioexception, servletexception {\n if (globals.is_security_enabled) {\n final servletrequest req = request;\n final servletresponse res = response;\n\n try {\n accesscontroller.doprivileged(new privilegedexceptionaction() {\n public void run() throws servletexception, ioexception {\n applicationfilterchain.this.internaldofilter(req, res);\n return null;\n }\n });\n } catch (privilegedactionexception var7) {\n exception e = var7.getexception();\n if (e instanceof servletexception) {\n throw (servletexception)e;\n }\n\n if (e instanceof ioexception) {\n throw (ioexception)e;\n }\n\n if (e instanceof runtimeexception) {\n throw (runtimeexception)e;\n }\n\n throw new servletexception(e.getmessage(), e);\n }\n } else {\n this.internaldofilter(request, response);\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n\n\nfilterchain 就是一条过滤链。其中每个过滤器(filter)都可以决定是否执行下一步。过滤分两个方向,进和出:\n\n进:在把 servletrequest 和 servletresponse 交给 servlet 的 service 方法之前,需要进行过滤\n\n出:在service方法完成后,往客户端发送之前,需要进行过滤\n\n\n# spring mvc 中的责任链\n\nspring mvc 的 diapatcherservlet 的 dodispatch 方法中,获取与请求匹配的处理器 handlerexecutionchain 就是用到了责任链模式。\n\nprotected void dodispatch(httpservletrequest request, httpservletresponse response) throws exception {\n httpservletrequest processedrequest = request;\n handlerexecutionchain mappedhandler = null; //使用到了责任链模式\n boolean multipartrequestparsed = false;\n webasyncmanager asyncmanager = webasyncutils.getasyncmanager(request);\n\n try {\n try {\n modelandview mv = null;\n object dispatchexception = null;\n\n try {\n processedrequest = this.checkmultipart(request);\n multipartrequestparsed = processedrequest != request;\n mappedhandler = this.gethandler(processedrequest); \n if (mappedhandler == null) {\n this.nohandlerfound(processedrequest, response);\n return;\n }\n\n handleradapter ha = this.gethandleradapter(mappedhandler.gethandler());\n string method = request.getmethod();\n boolean isget = \"get\".equals(method);\n if (isget || \"head\".equals(method)) {\n long lastmodified = ha.getlastmodified(request, mappedhandler.gethandler());\n if ((new servletwebrequest(request, response)).checknotmodified(lastmodified) && isget) {\n return;\n }\n }\n\t\t\t\t\t\t\t\t\t\t//责任链模式执行预处理方法,其实是将请求交给注册的拦截器执行\n if (!mappedhandler.applyprehandle(processedrequest, response)) {\n return;\n }\n\n mv = ha.handle(processedrequest, response, mappedhandler.gethandler());\n if (asyncmanager.isconcurrenthandlingstarted()) {\n return;\n }\n\n this.applydefaultviewname(processedrequest, mv);\n //责任链执行后处理方法\n mappedhandler.applyposthandle(processedrequest, response, mv);\n } catch (exception var22) {\n //...\n } finally {\n }\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n\n\n * springmvc 请求的流程中,执行了拦截器相关方法 interceptor.prehandler 等等\n\n * 在处理 springmvc 请求时,使用到职责链模式还使用到适配器模式\n\n * handlerexecutionchain 主要负责的是请求拦截器的执行和请求处理,但是他本身不处理请求,只是将请求分配给链上注册处理器执行,这是职责链实现方式,减少职责链本身与处理逻辑之间的耦合,规范了处理流程\n\n * handlerexecutionchain 维护了 handlerinterceptor 的集合, 可以向其中注册相应的拦截器\n\n\n# 总结\n\n责任链模式其实就是一个灵活版的 if…else…语句,它就是将这些判定条件的语句放到了各个处理类中,这样做的优点是比较灵活了,但同样也带来了风险,比如设置处理类前后关系时,一定要特别仔细,搞对处理类前后逻辑的条件判断关系,并且注意不要在链中出现循环引用的问题。\n\n优点:\n\n降低耦合度:将请求和处理分开,实现解耦,提高了系统的灵活性。\n\n简化了对象:对象不需要知道链的结构\n\n良好的扩展性:增加处理者的实现很简单,只需重写处理请求业务逻辑的方法。\n\n缺点:\n\n从链头发出,直到有处理者响应,在责任链比较长的时候会影响系统性能,一般需要在 handler 中设置一个最大节点数。\n\n请求递归,调试排错比较麻烦。\n\n使用场景:\n\n有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。\n\n在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。\n\n可动态指定一组对象处理请求。\n\n模式的扩展:\n\n职责链模式存在以下两种情况。\n\n * 纯的职责链模式:一个请求必须被某一个处理者对象所接收,且一个具体处理者对某个请求的处理只能采用以下两种行为之一:自己处理(承担责任);把责任推给下家处理。\n\n * 不纯的职责链模式:允许出现某一个具体处理者对象在承担了请求的一部分责任后又将剩余的责任传给下家的情况,且一个请求可以最终不被任何接收端对象所接收。", + "headersStr": "2XX 成功 3XX 重定向 4XX 客户端错误 5XX 服务器错误", + "content": "# 2XX 成功\n\n200 OK,表示从客户端发来的请求在服务器端被正确处理\n\n204 No content,表示请求成功,但响应报文不含实体的主体部分\n\n206 Partial Content,进行范围请求\n\n\n# 3XX 重定向\n\n301 moved permanently,永久性重定向,表示资源已被分配了新的 URL\n\n302 found,临时性重定向,表示资源临时被分配了新的 URL\n\n303 see other,表示资源存在着另一个 URL,应使用 GET 方法定向获取资源\n\n304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况\n\n307 temporary redirect,临时重定向,和302含义相同\n\n\n# 4XX 客户端错误\n\n400 bad request,请求报文存在语法错误\n\n401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息\n\n403 forbidden,表示对请求资源的访问被服务器拒绝\n\n404 not found,表示在服务器上没有找到请求的资源\n\n\n# 5XX 服务器错误\n\n500 internal sever error,表示服务器端在执行请求时发生了错误\n\n503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求", + "normalizedContent": "# 2xx 成功\n\n200 ok,表示从客户端发来的请求在服务器端被正确处理\n\n204 no content,表示请求成功,但响应报文不含实体的主体部分\n\n206 partial content,进行范围请求\n\n\n# 3xx 重定向\n\n301 moved permanently,永久性重定向,表示资源已被分配了新的 url\n\n302 found,临时性重定向,表示资源临时被分配了新的 url\n\n303 see other,表示资源存在着另一个 url,应使用 get 方法定向获取资源\n\n304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况\n\n307 temporary redirect,临时重定向,和302含义相同\n\n\n# 4xx 客户端错误\n\n400 bad request,请求报文存在语法错误\n\n401 unauthorized,表示发送的请求需要有通过 http 认证的认证信息\n\n403 forbidden,表示对请求资源的访问被服务器拒绝\n\n404 not found,表示在服务器上没有找到请求的资源\n\n\n# 5xx 服务器错误\n\n500 internal sever error,表示服务器端在执行请求时发生了错误\n\n503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求", "charsets": { "cjk": true }, - "lastUpdated": "2022/09/20, 23:34:59", - "lastUpdatedTimestamp": 1663688099000 + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "简述 TCP 三次握手以及四次挥手的流程。为什么需要三次握手以及四次挥手?", @@ -3768,14 +3938,16 @@ export const siteData = { "normalizedContent": "为了准确无误地把数据送达目标处,tcp 协议采用了三次握手策略。\n\n\n# 建立连接-tcp 三次握手\n\ntcp 三次握手图解\n\n\n\n建立一个 tcp 连接需要“三次握手”,缺一不可 :\n\n * 一次握手:客户端发送带有 syn(seq=x) 标志的数据包 -> 服务端,然后客户端进入 syn_send 状态,等待服务器的确认;\n * 二次握手:服务端发送带有 syn+ack(seq=y,ack=x+1) 标志的数据包 –> 客户端,然后服务端进入 syn_recv 状态\n * 三次握手:客户端发送带有带有 ack(ack=y+1) 标志的数据包 –> 服务端,然后客户端和服务器端都进入established 状态,完成tcp三次握手。\n\n当建立了 3 次握手之后,客户端和服务端就可以传输数据啦!\n\n\n# 为什么要三次握手?\n\n三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。\n\n * 第一次握手 :client 什么都不能确认;server 确认了对方发送正常,自己接收正常\n * 第二次握手 :client 确认了:自己发送、接收正常,对方发送、接收正常;server 确认了:对方发送正常,自己接收正常\n * 第三次握手 :client 确认了:自己发送、接收正常,对方发送、接收正常;server 确认了:自己发送、接收正常,对方发送、接收正常\n\n三次握手就能确认双发收发功能都正常,缺一不可。\n\n更详细的解答可以看这个:tcp 为什么是三次握手,而不是两次或四次? 。\n\n\n# 第2次握手传回了ack,为什么还要传回syn?\n\n服务端传回发送端所发送的 ack 是为了告诉客户端:“我接收到的信息确实就是你所发送的信号了”,这表明从客户端到服务端的通信是正常的。回传 syn 则是为了建立并确认从服务端到客户端的通信。\n\n> syn 同步序列编号(synchronize sequence numbers) 是 tcp/ip 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 tcp 网络连接时,客户机首先发出一个 syn 消息,服务器使用 syn-ack 应答表示接收到了这个消息,最后客户机再以 ack(acknowledgement)消息响应。这样在客户机和服务器之间才能建立起可靠的 tcp 连接,数据才可以在客户机和服务器之间传递。\n\n\n# 断开连接-tcp 四次挥手\n\ntcp 四次挥手图解\n\n\n\n断开一个 tcp 连接则需要“四次挥手”,缺一不可 :\n\n * 第一次挥手 :客户端发送一个 fin(seq=x) 标志的数据包->服务端,用来关闭客户端到服务器的数据传送。然后,客户端进入 fin-wait-1 状态。\n * 第二次挥手 :服务器收到这个 fin(seq=x) 标志的数据包,它发送一个 ack (seq=x+1)标志的数据包->客户端 。然后,此时服务端进入close-wait状态,客户端进入fin-wait-2状态。\n * 第三次挥手 :服务端关闭与客户端的连接并发送一个 fin (seq=y)标志的数据包->客户端请求关闭连接,然后,服务端进入last-ack状态。\n * 第四次挥手 :客户端发送 ack (seq=y+1)标志的数据包->服务端并且进入time-wait状态,服务端在收到 ack (seq=y+1)标志的数据包后进入 close 状态。此时,如果客户端等待 2msl 后依然没有收到回复,就证明服务端已正常关闭,随后,客户端也可以关闭连接了。\n\n只要四次挥手没有结束,客户端和服务端就可以继续传输数据!\n\n\n# 为什么要四次挥手?\n\ntcp是全双工通信,可以双向传输数据。任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了 tcp 连接。\n\n举个例子:a 和 b 打电话,通话即将结束后。\n\n 1. 第一次挥手 : a 说“我没啥要说的了”\n 2. 第二次挥手 :b 回答“我知道了”,但是 b 可能还会有要说的话,a 不能要求 b 跟着自己的节奏结束通话\n 3. 第三次挥手 :于是 b 可能又巴拉巴拉说了一通,最后 b 说“我说完了”\n 4. 第四次挥手 :a 回答“知道了”,这样通话才算结束。\n\n\n# 为什么不能把服务器发送的 ack 和 fin 合并起来,变成三次挥手?\n\n因为服务器收到客户端断开连接的请求时,可能还有一些数据没有发完,这时先回复 ack,表示接收到了断开连接的请求。等到数据发完之后再发 fin,断开服务器到客户端的数据传送。\n\n\n# 如果第二次挥手时服务器的 ack 没有送达客户端,会怎样?\n\n客户端没有收到 ack 确认,会重新发送 fin 请求。\n\n\n# 为什么第四次挥手客户端需要等待 2*msl(报文段最长寿命)时间后才进入 closed 状态?\n\n第四次挥手时,客户端发送给服务器的 ack 有可能丢失,如果服务端没有因为某些原因而没有收到 ack 的话,服务端就会重发 fin,如果客户端在 2*msl 的时间内收到了 fin,就会重新发送 ack 并再次等待 2msl,防止 server 没有收到 ack 而不断重发 fin。\n\nmsl(maximum segment lifetime) : 一个片段在网络中最大的存活时间,2msl 就是一个发送和一个回复所需的最大时间。如果直到 2msl,client 都没有再次收到 fin,那么 client 推断 ack 已经被成功接收,则结束 tcp 连接。\n\n\n# 面试官可能问你这些问题\n\n * 为什么要三次握手?\n * 第 2 次握手传回了ack,为什么还要传回syn?\n * 为什么要四次挥手?\n * 为什么不能把服务器发送的 ack 和 fin 合并起来,变成三次挥手?\n * 如果第二次挥手时服务器的 ack 没有送达客户端,会怎样?\n * 为什么第四次挥手客户端需要等待 2*msl(报文段最长寿命)时间后才进入 closed 状态?", "charsets": { "cjk": true - } + }, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { - "title": "简述常见的 HTTP 状态码的含义", + "title": "HTTP 中 GET 和 POST 区别", "frontmatter": { - "title": "简述常见的 HTTP 状态码的含义", - "date": "2022-06-23T21:09:29.000Z", - "permalink": "/pages/f33310/", + "title": "HTTP 中 GET 和 POST 区别", + "date": "2022-09-21T22:34:00.000Z", + "permalink": "/pages/a0e400/", "categories": [ "计算机基础", "计算机网络" @@ -3784,161 +3956,62 @@ export const siteData = { null ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/010.%E7%AE%80%E8%BF%B0%E5%B8%B8%E8%A7%81%E7%9A%84%20HTTP%20%E7%8A%B6%E6%80%81%E7%A0%81%E7%9A%84%E5%90%AB%E4%B9%89.html", - "relativePath": "02.计算机基础/10.计算机网络/010.简述常见的 HTTP 状态码的含义.md", - "key": "v-0a1d1fb4", - "path": "/pages/f33310/", + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/030.HTTP%20%E4%B8%AD%20GET%20%E5%92%8C%20POST%20%E5%8C%BA%E5%88%AB.html", + "relativePath": "02.计算机基础/10.计算机网络/030.HTTP 中 GET 和 POST 区别.md", + "key": "v-12717373", + "path": "/pages/a0e400/", "headers": [ { "level": 2, - "title": "2XX 成功", - "slug": "_2xx-成功", - "normalizedTitle": "2xx 成功", + "title": "什么是 HTTP ?", + "slug": "什么是-http", + "normalizedTitle": "什么是 http ?", "charIndex": 2 }, { "level": 2, - "title": "3XX 重定向", - "slug": "_3xx-重定向", - "normalizedTitle": "3xx 重定向", - "charIndex": 109 - }, - { - "level": 2, - "title": "4XX 客户端错误", - "slug": "_4xx-客户端错误", - "normalizedTitle": "4xx 客户端错误", - "charIndex": 333 - }, - { - "level": 2, - "title": "5XX 服务器错误", - "slug": "_5xx-服务器错误", - "normalizedTitle": "5xx 服务器错误", - "charIndex": 483 - } - ], - "headersStr": "2XX 成功 3XX 重定向 4XX 客户端错误 5XX 服务器错误", - "content": "# 2XX 成功\n\n200 OK,表示从客户端发来的请求在服务器端被正确处理\n\n204 No content,表示请求成功,但响应报文不含实体的主体部分\n\n206 Partial Content,进行范围请求\n\n\n# 3XX 重定向\n\n301 moved permanently,永久性重定向,表示资源已被分配了新的 URL\n\n302 found,临时性重定向,表示资源临时被分配了新的 URL\n\n303 see other,表示资源存在着另一个 URL,应使用 GET 方法定向获取资源\n\n304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况\n\n307 temporary redirect,临时重定向,和302含义相同\n\n\n# 4XX 客户端错误\n\n400 bad request,请求报文存在语法错误\n\n401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息\n\n403 forbidden,表示对请求资源的访问被服务器拒绝\n\n404 not found,表示在服务器上没有找到请求的资源\n\n\n# 5XX 服务器错误\n\n500 internal sever error,表示服务器端在执行请求时发生了错误\n\n503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求", - "normalizedContent": "# 2xx 成功\n\n200 ok,表示从客户端发来的请求在服务器端被正确处理\n\n204 no content,表示请求成功,但响应报文不含实体的主体部分\n\n206 partial content,进行范围请求\n\n\n# 3xx 重定向\n\n301 moved permanently,永久性重定向,表示资源已被分配了新的 url\n\n302 found,临时性重定向,表示资源临时被分配了新的 url\n\n303 see other,表示资源存在着另一个 url,应使用 get 方法定向获取资源\n\n304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况\n\n307 temporary redirect,临时重定向,和302含义相同\n\n\n# 4xx 客户端错误\n\n400 bad request,请求报文存在语法错误\n\n401 unauthorized,表示发送的请求需要有通过 http 认证的认证信息\n\n403 forbidden,表示对请求资源的访问被服务器拒绝\n\n404 not found,表示在服务器上没有找到请求的资源\n\n\n# 5xx 服务器错误\n\n500 internal sever error,表示服务器端在执行请求时发生了错误\n\n503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/09/18, 21:28:02", - "lastUpdatedTimestamp": 1663507682000 - }, - { - "title": "观察模式", - "frontmatter": { - "title": "观察模式", - "date": "2022-05-21T17:29:45.000Z", - "permalink": "/pages/cbff3c/", - "categories": [ - "Java相关", - "设计模式" - ], - "tags": [ - null - ] - }, - "regularPath": "/01.Java%E7%9B%B8%E5%85%B3/40.%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/060.%E8%A7%82%E5%AF%9F%E6%A8%A1%E5%BC%8F.html", - "relativePath": "01.Java相关/40.设计模式/060.观察模式.md", - "key": "v-64f1031c", - "path": "/pages/cbff3c/", - "headers": [ - { - "level": 2, - "title": "定义", - "slug": "定义", - "normalizedTitle": "定义", - "charIndex": 344 - }, - { - "level": 2, - "title": "角色", - "slug": "角色", - "normalizedTitle": "角色", - "charIndex": 594 + "title": "两种 HTTP 请求方法:GET 和 POST", + "slug": "两种-http-请求方法-get-和-post", + "normalizedTitle": "两种 http 请求方法:get 和 post", + "charIndex": 186 }, { "level": 2, - "title": "类图", - "slug": "类图", - "normalizedTitle": "类图", - "charIndex": 630 + "title": "GET 方法", + "slug": "get-方法", + "normalizedTitle": "get 方法", + "charIndex": 364 }, { "level": 2, - "title": "实例", - "slug": "实例", - "normalizedTitle": "实例", - "charIndex": 1386 + "title": "POST 方法", + "slug": "post-方法", + "normalizedTitle": "post 方法", + "charIndex": 593 }, { "level": 2, - "title": "优缺点", - "slug": "优缺点", - "normalizedTitle": "优缺点", - "charIndex": 2856 - }, - { - "level": 3, - "title": "优点", - "slug": "优点", - "normalizedTitle": "优点", - "charIndex": 2864 - }, - { - "level": 3, - "title": "缺点", - "slug": "缺点", - "normalizedTitle": "缺点", - "charIndex": 2857 + "title": "比较 GET 与 POST", + "slug": "比较-get-与-post", + "normalizedTitle": "比较 get 与 post", + "charIndex": 834 }, { "level": 2, - "title": "应用", - "slug": "应用", - "normalizedTitle": "应用", - "charIndex": 3010 - }, - { - "level": 3, - "title": "JDK 中的观察者模式", - "slug": "jdk-中的观察者模式", - "normalizedTitle": "jdk 中的观察者模式", - "charIndex": 3017 - }, - { - "level": 3, - "title": "Spring 中的观察者模式", - "slug": "spring-中的观察者模式", - "normalizedTitle": "spring 中的观察者模式", - "charIndex": 3164 - }, - { - "level": 4, - "title": "1. Spring 中观察者模式的四个角色", - "slug": "_1-spring-中观察者模式的四个角色", - "normalizedTitle": "1. spring 中观察者模式的四个角色", - "charIndex": 3228 - }, - { - "level": 4, - "title": "2. coding~~~~~~", - "slug": "_2-coding", - "normalizedTitle": "2. coding~~~~~~", - "charIndex": 3951 + "title": "其他 HTTP 请求方法", + "slug": "其他-http-请求方法", + "normalizedTitle": "其他 http 请求方法", + "charIndex": 1967 } ], - "headersStr": "定义 角色 类图 实例 优缺点 优点 缺点 应用 JDK 中的观察者模式 Spring 中的观察者模式 1. Spring 中观察者模式的四个角色 2. coding~~~~~~", - "content": "在软件系统中经常会有这样的需求:如果一个对象的状态发生改变,某些与它相关的对象也要随之做出相应的变化。 举个例子:\n\n * 微信公众号,如果一个用户订阅了某个公众号,那么便会收到公众号发来的消息,那么,公众号就是『被观察者』,而用户就是『观察者』\n * 气象站可以将每天预测到的温度、湿度、气压等以公告的形式发布给各种第三方网站,如果天气数据有更新,要能够实时的通知给第三方,这里的气象局就是『被观察者』,第三方网站就是『观察者』\n * MVC 模式中的模型与视图的关系也属于观察与被观察关系\n\n观察者模式是使用频率较高的设计模式之一。\n\n观察者模式包含观察目标和观察者两类对象,一个目标可以有任意数目的与之相依赖的观察者,一旦观察目标的状态发生改变,所有的观察者都将得到通知。\n\n\n# 定义\n\n观察者模式(Observer Pattern): 定义对象间一种一对多的依赖关系,使得当每一个对象改变状态,则所有依赖于它的对象都会得到通知并自动更新。\n\n观察者模式是一种对象行为型模式。\n\n观察者模式的别名包括发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。\n\n细究的话,发布订阅和观察者有些不同,可以理解成发布订阅模式属于广义上的观察者模式。\n\n\n\n\n# 角色\n\n * Subject(目标):被观察者,它是指被观察的对象。 从类图中可以看到,类中有一个用来存放观察者对象的Vector 容器(Vector在是安全的,而List则是不安全的),这个 Vector 容器是被观察者类的核心,另外还有三个方法:attach 方法是向这个容器中添加观察者对象;detach 方法是从容器中移除观察者对象;notify 方法是依次调用观察者对象的对应方法。这个角色可以是接口,也可以是抽象类或者具体的类,因为很多情况下会与其他的模式混用,所以使用抽象类的情况比较多。\n\n * ConcreteSubject(具体目标):具体目标是目标类的子类,通常它包含经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知。同时它还实现了在目标类中定义的抽象业务逻辑方法(如果有的话)。如果无须扩展目标类,则具体目标类可以省略。\n\n * Observer(观察者):观察者将对观察目标的改变做出反应,观察者一般定义为接口,该接口声明了更新数据的方法 update(),因此又称为抽象观察者。\n\n * ConcreteObserver(具体观察者):在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者 Observer 中定义的 update() 方法。通常在实现时,可以调用具体目标类的 attach() 方法将自己添加到目标类的集合中或通过 detach() 方法将自己从目标类的集合中删除。\n\n\n# 类图\n\n\n\n再记录下 UML 类图的注意事项,这里我的 Subject 是抽象方法,所以用***斜体***,抽象方法也要用斜体,具体的各种箭头意义,我之前也总结过《设计模式前传——学设计模式前你要知道这些》(被网上各种帖子毒害过的自己,认真记录~~~)。\n\n\n# 实例\n\n1、定义观察者接口\n\ninterface Observer {\n public void update();\n}\n\n\n1\n2\n3\n\n\n2、定义被观察者\n\nabstract class Subject {\n private Vector obs = new Vector();\n\n public void addObserver(Observer obs){\n this.obs.add(obs);\n }\n public void delObserver(Observer obs){\n this.obs.remove(obs);\n }\n protected void notifyObserver(){\n for(Observer o: obs){\n o.update();\n }\n }\n public abstract void doSomething();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\n3、具体的被观察者\n\nclass ConcreteSubject extends Subject {\n public void doSomething(){\n System.out.println(\"被观察者事件发生改变\");\n this.notifyObserver();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n4、具体的观察者\n\nclass ConcreteObserver1 implements Observer {\n public void update() {\n System.out.println(\"观察者1收到信息,并进行处理\");\n }\n}\nclass ConcreteObserver2 implements Observer {\n public void update() {\n System.out.println(\"观察者2收到信息,并进行处理\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n5、客户端\n\npublic class Client {\n public static void main(String[] args){\n Subject sub = new ConcreteSubject();\n sub.addObserver(new ConcreteObserver1()); //添加观察者1\n sub.addObserver(new ConcreteObserver2()); //添加观察者2\n sub.doSomething();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\n输出\n\n被观察者事件发生改变\n观察者1收到信息,并进行处理\n观察者2收到信息,并进行处理\n\n\n1\n2\n3\n\n\n通过运行结果可以看到,我们只调用了 Subject 的方法,但同时两个观察者的相关方法都被调用了。仔细看一下代码,其实很简单,就是在 Subject 类中关联一下 Observer 类,并且在 doSomething() 方法中遍历一下 Observer 的 update() 方法就行了。\n\n\n# 优缺点\n\n\n# 优点\n\n降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系 目标与观察者之间建立了一套触发机制 支持广播通信 符合“开闭原则”的要求\n\n\n# 缺点\n\n目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率\n\n\n# 应用\n\n\n# JDK 中的观察者模式\n\n观察者模式在 Java 语言中的地位非常重要。在 JDK 的 java.util 包中,提供了 Observable 类以及 Observer 接口,它们构成了 JDK 对观察者模式的支持(可以去查看下源码,写的比较严谨)。but,在 Java9 被弃用了。\n\n\n# Spring 中的观察者模式\n\nSpring 事件驱动模型也是观察者模式很经典的应用。就是我们项目中最常见的事件监听器。\n\n# 1. Spring 中观察者模式的四个角色\n\n * 事件:ApplicationEvent 是所有事件对象的父类。ApplicationEvent 继承自 jdk 的 EventObject,所有的事件都需要继承 ApplicationEvent,并且通过 source 得到事件源。\n\n * Spring 也为我们提供了很多内置事件,ContextRefreshedEvent、ContextStartedEvent、ContextStoppedEvent、ContextClosedEvent、RequestHandledEvent。\n\n * 事件监听:ApplicationListener,也就是观察者,继承自 jdk 的 EventListener,该类中只有一个方法 onApplicationEvent。当监听的事件发生后该方法会被执行。\n\n * 事件源:ApplicationContext,ApplicationContext 是 Spring 中的核心容器,在事件监听中 ApplicationContext 可以作为事件的发布者,也就是事件源。因为 ApplicationContext 继承自 ApplicationEventPublisher。在 ApplicationEventPublisher 中定义了事件发布的方法:publishEvent(Object event)\n\n * 事件管理:ApplicationEventMulticaster,用于事件监听器的注册和事件的广播。监听器的注册就是通过它来实现的,它的作用是把 Applicationcontext 发布的 Event 广播给它的监听器列表。\n\n# 2. coding~~~~~~\n\n1、定义事件\n\npublic class MyEvent extends ApplicationEvent {\n public MyEvent(Object source) {\n super(source);\n System.out.println(\"my Event\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n2、实现事件监听器\n\n@Component\nclass MyListenerA implements ApplicationListener {\n public void onApplicationEvent(MyEvent AyEvent) {\n System.out.println(\"ListenerA received\");\n }\n}\n\n@Component\nclass MyListenerB implements ApplicationListener {\n public void onApplicationEvent(MyEvent AyEvent) {\n System.out.println(\"ListenerB received\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n3、事件发布者\n\n@Component\npublic class MyPublisher implements ApplicationContextAware {\n private ApplicationContext applicationContext;\n \n public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {\n this.applicationContext=applicationContext;\n }\n \n public void publishEvent(ApplicationEvent event){\n System.out.println(\"publish event\");\n applicationContext.publishEvent(event);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n4、测试,先用注解方式将 MyPublisher 注入 Spring\n\n@Configuration\n@ComponentScan\npublic class AppConfig {\n\n @Bean(name = \"myPublisher\")\n public MyPublisher myPublisher(){\n return new MyPublisher();\n }\n}\npublic class Client {\n\n @Test\n public void main() {\n ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);\n MyPublisher myPublisher = (MyPublisher) context.getBean(\"myPublisher\");\n myPublisher.publishEvent(new MyEvent(this));\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n\n\n5、输出\n\nmy Event\npublish event\nListenerA received\nListenerB received\n\n\n1\n2\n3\n4\n", - "normalizedContent": "在软件系统中经常会有这样的需求:如果一个对象的状态发生改变,某些与它相关的对象也要随之做出相应的变化。 举个例子:\n\n * 微信公众号,如果一个用户订阅了某个公众号,那么便会收到公众号发来的消息,那么,公众号就是『被观察者』,而用户就是『观察者』\n * 气象站可以将每天预测到的温度、湿度、气压等以公告的形式发布给各种第三方网站,如果天气数据有更新,要能够实时的通知给第三方,这里的气象局就是『被观察者』,第三方网站就是『观察者』\n * mvc 模式中的模型与视图的关系也属于观察与被观察关系\n\n观察者模式是使用频率较高的设计模式之一。\n\n观察者模式包含观察目标和观察者两类对象,一个目标可以有任意数目的与之相依赖的观察者,一旦观察目标的状态发生改变,所有的观察者都将得到通知。\n\n\n# 定义\n\n观察者模式(observer pattern): 定义对象间一种一对多的依赖关系,使得当每一个对象改变状态,则所有依赖于它的对象都会得到通知并自动更新。\n\n观察者模式是一种对象行为型模式。\n\n观察者模式的别名包括发布-订阅(publish/subscribe)模式、模型-视图(model/view)模式、源-监听器(source/listener)模式或从属者(dependents)模式。\n\n细究的话,发布订阅和观察者有些不同,可以理解成发布订阅模式属于广义上的观察者模式。\n\n\n\n\n# 角色\n\n * subject(目标):被观察者,它是指被观察的对象。 从类图中可以看到,类中有一个用来存放观察者对象的vector 容器(vector在是安全的,而list则是不安全的),这个 vector 容器是被观察者类的核心,另外还有三个方法:attach 方法是向这个容器中添加观察者对象;detach 方法是从容器中移除观察者对象;notify 方法是依次调用观察者对象的对应方法。这个角色可以是接口,也可以是抽象类或者具体的类,因为很多情况下会与其他的模式混用,所以使用抽象类的情况比较多。\n\n * concretesubject(具体目标):具体目标是目标类的子类,通常它包含经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知。同时它还实现了在目标类中定义的抽象业务逻辑方法(如果有的话)。如果无须扩展目标类,则具体目标类可以省略。\n\n * observer(观察者):观察者将对观察目标的改变做出反应,观察者一般定义为接口,该接口声明了更新数据的方法 update(),因此又称为抽象观察者。\n\n * concreteobserver(具体观察者):在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者 observer 中定义的 update() 方法。通常在实现时,可以调用具体目标类的 attach() 方法将自己添加到目标类的集合中或通过 detach() 方法将自己从目标类的集合中删除。\n\n\n# 类图\n\n\n\n再记录下 uml 类图的注意事项,这里我的 subject 是抽象方法,所以用***斜体***,抽象方法也要用斜体,具体的各种箭头意义,我之前也总结过《设计模式前传——学设计模式前你要知道这些》(被网上各种帖子毒害过的自己,认真记录~~~)。\n\n\n# 实例\n\n1、定义观察者接口\n\ninterface observer {\n public void update();\n}\n\n\n1\n2\n3\n\n\n2、定义被观察者\n\nabstract class subject {\n private vector obs = new vector();\n\n public void addobserver(observer obs){\n this.obs.add(obs);\n }\n public void delobserver(observer obs){\n this.obs.remove(obs);\n }\n protected void notifyobserver(){\n for(observer o: obs){\n o.update();\n }\n }\n public abstract void dosomething();\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n\n\n3、具体的被观察者\n\nclass concretesubject extends subject {\n public void dosomething(){\n system.out.println(\"被观察者事件发生改变\");\n this.notifyobserver();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n4、具体的观察者\n\nclass concreteobserver1 implements observer {\n public void update() {\n system.out.println(\"观察者1收到信息,并进行处理\");\n }\n}\nclass concreteobserver2 implements observer {\n public void update() {\n system.out.println(\"观察者2收到信息,并进行处理\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n\n\n5、客户端\n\npublic class client {\n public static void main(string[] args){\n subject sub = new concretesubject();\n sub.addobserver(new concreteobserver1()); //添加观察者1\n sub.addobserver(new concreteobserver2()); //添加观察者2\n sub.dosomething();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\n输出\n\n被观察者事件发生改变\n观察者1收到信息,并进行处理\n观察者2收到信息,并进行处理\n\n\n1\n2\n3\n\n\n通过运行结果可以看到,我们只调用了 subject 的方法,但同时两个观察者的相关方法都被调用了。仔细看一下代码,其实很简单,就是在 subject 类中关联一下 observer 类,并且在 dosomething() 方法中遍历一下 observer 的 update() 方法就行了。\n\n\n# 优缺点\n\n\n# 优点\n\n降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系 目标与观察者之间建立了一套触发机制 支持广播通信 符合“开闭原则”的要求\n\n\n# 缺点\n\n目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率\n\n\n# 应用\n\n\n# jdk 中的观察者模式\n\n观察者模式在 java 语言中的地位非常重要。在 jdk 的 java.util 包中,提供了 observable 类以及 observer 接口,它们构成了 jdk 对观察者模式的支持(可以去查看下源码,写的比较严谨)。but,在 java9 被弃用了。\n\n\n# spring 中的观察者模式\n\nspring 事件驱动模型也是观察者模式很经典的应用。就是我们项目中最常见的事件监听器。\n\n# 1. spring 中观察者模式的四个角色\n\n * 事件:applicationevent 是所有事件对象的父类。applicationevent 继承自 jdk 的 eventobject,所有的事件都需要继承 applicationevent,并且通过 source 得到事件源。\n\n * spring 也为我们提供了很多内置事件,contextrefreshedevent、contextstartedevent、contextstoppedevent、contextclosedevent、requesthandledevent。\n\n * 事件监听:applicationlistener,也就是观察者,继承自 jdk 的 eventlistener,该类中只有一个方法 onapplicationevent。当监听的事件发生后该方法会被执行。\n\n * 事件源:applicationcontext,applicationcontext 是 spring 中的核心容器,在事件监听中 applicationcontext 可以作为事件的发布者,也就是事件源。因为 applicationcontext 继承自 applicationeventpublisher。在 applicationeventpublisher 中定义了事件发布的方法:publishevent(object event)\n\n * 事件管理:applicationeventmulticaster,用于事件监听器的注册和事件的广播。监听器的注册就是通过它来实现的,它的作用是把 applicationcontext 发布的 event 广播给它的监听器列表。\n\n# 2. coding~~~~~~\n\n1、定义事件\n\npublic class myevent extends applicationevent {\n public myevent(object source) {\n super(source);\n system.out.println(\"my event\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\n2、实现事件监听器\n\n@component\nclass mylistenera implements applicationlistener {\n public void onapplicationevent(myevent ayevent) {\n system.out.println(\"listenera received\");\n }\n}\n\n@component\nclass mylistenerb implements applicationlistener {\n public void onapplicationevent(myevent ayevent) {\n system.out.println(\"listenerb received\");\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n3、事件发布者\n\n@component\npublic class mypublisher implements applicationcontextaware {\n private applicationcontext applicationcontext;\n \n public void setapplicationcontext(applicationcontext applicationcontext) throws beansexception {\n this.applicationcontext=applicationcontext;\n }\n \n public void publishevent(applicationevent event){\n system.out.println(\"publish event\");\n applicationcontext.publishevent(event);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n\n\n4、测试,先用注解方式将 mypublisher 注入 spring\n\n@configuration\n@componentscan\npublic class appconfig {\n\n @bean(name = \"mypublisher\")\n public mypublisher mypublisher(){\n return new mypublisher();\n }\n}\npublic class client {\n\n @test\n public void main() {\n applicationcontext context = new annotationconfigapplicationcontext(appconfig.class);\n mypublisher mypublisher = (mypublisher) context.getbean(\"mypublisher\");\n mypublisher.publishevent(new myevent(this));\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n\n\n5、输出\n\nmy event\npublish event\nlistenera received\nlistenerb received\n\n\n1\n2\n3\n4\n", + "headersStr": "什么是 HTTP ? 两种 HTTP 请求方法:GET 和 POST GET 方法 POST 方法 比较 GET 与 POST 其他 HTTP 请求方法", + "content": "# 什么是 HTTP ?\n\n超文本传输协议(HTTP)的设计目的是保证客户端与服务器之间的通信。\n\nHTTP 的工作方式是客户端与服务器之间的请求-应答协议。\n\nweb 浏览器可能是客户端,而计算机上的网络应用程序也可能作为服务器端。\n\n举例:客户端(浏览器)向服务器提交 HTTP 请求;服务器向客户端返回响应。响应包含关于请求的状态信息以及可能被请求的内容。\n\n\n# 两种 HTTP 请求方法:GET 和 POST\n\n在客户机和服务器之间进行请求-响应时,两种最常被用到的方法是:GET 和 POST。\n\n * GET - 从指定的资源请求数据。\n * POST - 向指定的资源提交要被处理的数据。 GET 提交参数一般显示在 URL 上,POST 通过表单提交不会显示在 URL 上,POST 更具隐蔽性:\n\n\n\n\n# GET 方法\n\n请注意,查询字符串(名称/值对)是在 GET 请求的 URL 中发送的:\n\n/test/demo_form.php?name1=value1&name2=value2\n\n\n1\n\n\n有关 GET 请求的其他一些注释:\n\n * GET 请求可被缓存\n * GET 请求保留在浏览器历史记录中\n * GET 请求可被收藏为书签\n * GET 请求不应在处理敏感数据时使用\n * GET 请求有长度限制\n * GET 请求只应当用于取回数据\n\n\n# POST 方法\n\n请注意,查询字符串(名称/值对)是在 POST 请求的 HTTP 消息主体中发送的:\n\nPOST /test/demo_form.php HTTP/1.1\nHost: runoob.com\nname1=value1&name2=value2\n\n\n1\n2\n3\n\n\n有关 POST 请求的其他一些注释:\n\n * POST 请求不会被缓存\n * POST 请求不会保留在浏览器历史记录中\n * POST 不能被收藏为书签\n * POST 请求对数据长度没有要求\n\n\n# 比较 GET 与 POST\n\n下面的表格比较了两种 HTTP 方法:GET 和 POST。\n\n GET POST\n后退按钮/刷新 无害 数据会被重新提交(浏览器应该告知用户数据会被重新提交)。\n书签 可收藏为书签 不可收藏为书签\n缓存 能被缓存 不能缓存\n编码类型 application/x-www-form-urlencoded application/x-www-form-urlencoded or\n multipart/form-data。为二进制数据使用多重编码。\n历史 参数保留在浏览器历史中。 参数不会保存在浏览器历史中。\n对数据长度的限制 是的。当发送数据时,GET 方法向 URL 添加数据;URL 的长度是受限制的(URL 的最大长度是 2048 无限制。\n 个字符)。\n对数据类型的限制 只允许 ASCII 字符。 没有限制。也允许二进制数据。\n安全性 与 POST 相比,GET 的安全性较差,因为所发送的数据是 URL 的一部分。 在发送密码或其他敏感信息时绝不要使用 GET ! POST 比 GET 更安全,因为参数不会被保存在浏览器历史或 web\n 服务器日志中。\n可见性 数据在 URL 中对所有人都是可见的。 数据不会显示在 URL 中。\n\n\n# 其他 HTTP 请求方法\n\n下面的表格列出了其他一些 HTTP 请求方法:\n\n方法 描述\nHEAD 与 GET 相同,但只返回 HTTP 报头,不返回文档主体。\nPUT 上传指定的 URI 表示。\nDELETE 删除指定资源。\nOPTIONS 返回服务器支持的 HTTP 方法。\nCONNECT 把请求连接转换到透明的 TCP/IP 通道。", + "normalizedContent": "# 什么是 http ?\n\n超文本传输协议(http)的设计目的是保证客户端与服务器之间的通信。\n\nhttp 的工作方式是客户端与服务器之间的请求-应答协议。\n\nweb 浏览器可能是客户端,而计算机上的网络应用程序也可能作为服务器端。\n\n举例:客户端(浏览器)向服务器提交 http 请求;服务器向客户端返回响应。响应包含关于请求的状态信息以及可能被请求的内容。\n\n\n# 两种 http 请求方法:get 和 post\n\n在客户机和服务器之间进行请求-响应时,两种最常被用到的方法是:get 和 post。\n\n * get - 从指定的资源请求数据。\n * post - 向指定的资源提交要被处理的数据。 get 提交参数一般显示在 url 上,post 通过表单提交不会显示在 url 上,post 更具隐蔽性:\n\n\n\n\n# get 方法\n\n请注意,查询字符串(名称/值对)是在 get 请求的 url 中发送的:\n\n/test/demo_form.php?name1=value1&name2=value2\n\n\n1\n\n\n有关 get 请求的其他一些注释:\n\n * get 请求可被缓存\n * get 请求保留在浏览器历史记录中\n * get 请求可被收藏为书签\n * get 请求不应在处理敏感数据时使用\n * get 请求有长度限制\n * get 请求只应当用于取回数据\n\n\n# post 方法\n\n请注意,查询字符串(名称/值对)是在 post 请求的 http 消息主体中发送的:\n\npost /test/demo_form.php http/1.1\nhost: runoob.com\nname1=value1&name2=value2\n\n\n1\n2\n3\n\n\n有关 post 请求的其他一些注释:\n\n * post 请求不会被缓存\n * post 请求不会保留在浏览器历史记录中\n * post 不能被收藏为书签\n * post 请求对数据长度没有要求\n\n\n# 比较 get 与 post\n\n下面的表格比较了两种 http 方法:get 和 post。\n\n get post\n后退按钮/刷新 无害 数据会被重新提交(浏览器应该告知用户数据会被重新提交)。\n书签 可收藏为书签 不可收藏为书签\n缓存 能被缓存 不能缓存\n编码类型 application/x-www-form-urlencoded application/x-www-form-urlencoded or\n multipart/form-data。为二进制数据使用多重编码。\n历史 参数保留在浏览器历史中。 参数不会保存在浏览器历史中。\n对数据长度的限制 是的。当发送数据时,get 方法向 url 添加数据;url 的长度是受限制的(url 的最大长度是 2048 无限制。\n 个字符)。\n对数据类型的限制 只允许 ascii 字符。 没有限制。也允许二进制数据。\n安全性 与 post 相比,get 的安全性较差,因为所发送的数据是 url 的一部分。 在发送密码或其他敏感信息时绝不要使用 get ! post 比 get 更安全,因为参数不会被保存在浏览器历史或 web\n 服务器日志中。\n可见性 数据在 url 中对所有人都是可见的。 数据不会显示在 url 中。\n\n\n# 其他 http 请求方法\n\n下面的表格列出了其他一些 http 请求方法:\n\n方法 描述\nhead 与 get 相同,但只返回 http 报头,不返回文档主体。\nput 上传指定的 uri 表示。\ndelete 删除指定资源。\noptions 返回服务器支持的 http 方法。\nconnect 把请求连接转换到透明的 tcp/ip 通道。", "charsets": { "cjk": true }, - "lastUpdated": "2022/09/20, 23:34:59", - "lastUpdatedTimestamp": 1663688099000 + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "从输入 URL 到展现页面的全过程", @@ -4045,43 +4118,20 @@ export const siteData = { } ], "headersStr": "总体来说分为以下几个过程: 输入网址 DNS解析 什么是IP地址 什么是DNS DNS解析 DNS查询的两种方式:递归查询和迭代查询 DNS域名称空间的组织方式 DNS负载均衡 建立TCP/IP链接 ”三次握手”的详解 为什么要三次握手", - "content": "从我们输入一个网址到页面展现的过程到底发生了什么呢?\n\n\n# 总体来说分为以下几个过程:\n\n * 输入网址\n * DNS解析\n * 建立TCP/IP链接\n * 发送HTTP请求\n * 服务器处理请求\n * 服务器返回HTTP响应\n * 浏览器渲染页面并展现\n * 断开连接\n\n\n# 输入网址\n\n当我们想要打开一个页面时,很简单的会想到去输入一个网址 我们以本网址为例,解释一下它各部分的组成:\n\nhttps://www.baidu.com\n\n\n1\n\n\n * https :https是协议(Protocol),它表明了浏览器使用何种协议,有http、https、ftp、mailto等,其中https是加密认证版的http(详见《图解HTTP》)\n\n * www.baidu.com :这是域名(Domain name),它表明正在请求哪个Web服务器,其中www是主机名(host)\n\n\n# DNS解析\n\n输入网址(URL)这一步我们很好理解,但是接下来所谓的域名解析是什么呢?又是为什么要进行域名解析呢?\n\n\n# 什么是IP地址\n\nIP 地址(Internet Protocol Address)是互联网协议特有的一种地址,它是 IP 协议提供的一种统一的地址格式。IP 地址为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。 像192.168.59.0就是一个主机IP地址\n\n\n# 什么是DNS\n\nDNS服务器上存储着映射<域名-IP>,DNS解析就是去DNS服务器查询IP地址。\n\n\n# DNS解析\n\nDNS解析按照下面顺序逐个查找。\n\n * 本地(hosts文件)\n * DNS服务器(本地DNS | 根DNS)\n * 域服务器\n * 解析服务器的地址\n\n\n\n\n# DNS查询的两种方式:递归查询和迭代查询\n\n递归解析 迭代解析\n\n\n# DNS域名称空间的组织方式\n\n\n\n\n# DNS负载均衡\n\n当一个网站有足够多的用户的时候,假如每次请求的资源都位于同一台机器上面,那么这台机器随时可能会蹦掉。\n\n处理办法就是用DNS负载均衡技术,它的原理是在DNS服务器中为同一个主机名配置多个IP地址 在应答DNS查询时,DNS服务器对每个查询将以DNS文件中主机记录的IP地址按顺序返回不同的解析结果,将客户端的访问引导到不同的机器上去,使得不同的客户端访问不同的服务器,从而达到负载均衡的目的。\n\n\n# 建立TCP/IP链接\n\n\n# ”三次握手”的详解\n\n\n\n\n# 为什么要三次握手\n\n * clien端 防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。 client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致连接释放以后的某个时间才到达server。 本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。 于是就向client发出确认报文段,同意建立连接。\n\n * server端 假设不采用“三次握手”。 如果只要server发出确认,新的连接就建立了。并一直等待client发来数据。 事实是client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。 但server端就会有许多这样的等待,server的很多资源就白白浪费掉了。\n\n采用“三次握手”的办法可以防止上述现象发生。主要目的防止server端一直等待,浪费资源。", - "normalizedContent": "从我们输入一个网址到页面展现的过程到底发生了什么呢?\n\n\n# 总体来说分为以下几个过程:\n\n * 输入网址\n * dns解析\n * 建立tcp/ip链接\n * 发送http请求\n * 服务器处理请求\n * 服务器返回http响应\n * 浏览器渲染页面并展现\n * 断开连接\n\n\n# 输入网址\n\n当我们想要打开一个页面时,很简单的会想到去输入一个网址 我们以本网址为例,解释一下它各部分的组成:\n\nhttps://www.baidu.com\n\n\n1\n\n\n * https :https是协议(protocol),它表明了浏览器使用何种协议,有http、https、ftp、mailto等,其中https是加密认证版的http(详见《图解http》)\n\n * www.baidu.com :这是域名(domain name),它表明正在请求哪个web服务器,其中www是主机名(host)\n\n\n# dns解析\n\n输入网址(url)这一步我们很好理解,但是接下来所谓的域名解析是什么呢?又是为什么要进行域名解析呢?\n\n\n# 什么是ip地址\n\nip 地址(internet protocol address)是互联网协议特有的一种地址,它是 ip 协议提供的一种统一的地址格式。ip 地址为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。 像192.168.59.0就是一个主机ip地址\n\n\n# 什么是dns\n\ndns服务器上存储着映射<域名-ip>,dns解析就是去dns服务器查询ip地址。\n\n\n# dns解析\n\ndns解析按照下面顺序逐个查找。\n\n * 本地(hosts文件)\n * dns服务器(本地dns | 根dns)\n * 域服务器\n * 解析服务器的地址\n\n\n\n\n# dns查询的两种方式:递归查询和迭代查询\n\n递归解析 迭代解析\n\n\n# dns域名称空间的组织方式\n\n\n\n\n# dns负载均衡\n\n当一个网站有足够多的用户的时候,假如每次请求的资源都位于同一台机器上面,那么这台机器随时可能会蹦掉。\n\n处理办法就是用dns负载均衡技术,它的原理是在dns服务器中为同一个主机名配置多个ip地址 在应答dns查询时,dns服务器对每个查询将以dns文件中主机记录的ip地址按顺序返回不同的解析结果,将客户端的访问引导到不同的机器上去,使得不同的客户端访问不同的服务器,从而达到负载均衡的目的。\n\n\n# 建立tcp/ip链接\n\n\n# ”三次握手”的详解\n\n\n\n\n# 为什么要三次握手\n\n * clien端 防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。 client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致连接释放以后的某个时间才到达server。 本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。 于是就向client发出确认报文段,同意建立连接。\n\n * server端 假设不采用“三次握手”。 如果只要server发出确认,新的连接就建立了。并一直等待client发来数据。 事实是client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。 但server端就会有许多这样的等待,server的很多资源就白白浪费掉了。\n\n采用“三次握手”的办法可以防止上述现象发生。主要目的防止server端一直等待,浪费资源。", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/09/18, 21:28:02", - "lastUpdatedTimestamp": 1663507682000 - }, - { - "title": "TCP 怎么保证可靠传输", - "frontmatter": { - "title": "TCP 怎么保证可靠传输", - "date": "2022-09-21T22:34:41.000Z", - "permalink": "/pages/5fcc49/", - "categories": [ - "计算机基础", - "计算机网络" - ], - "tags": [ - null - ] - }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/040.TCP%20%E6%80%8E%E4%B9%88%E4%BF%9D%E8%AF%81%E5%8F%AF%E9%9D%A0%E4%BC%A0%E8%BE%93.html", - "relativePath": "02.计算机基础/10.计算机网络/040.TCP 怎么保证可靠传输.md", - "key": "v-5047727f", - "path": "/pages/5fcc49/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {} + "content": "从我们输入一个网址到页面展现的过程到底发生了什么呢?\n\n\n# 总体来说分为以下几个过程:\n\n * 输入网址\n * DNS解析\n * 建立TCP/IP链接\n * 发送HTTP请求\n * 服务器处理请求\n * 服务器返回HTTP响应\n * 浏览器渲染页面并展现\n * 断开连接\n\n\n# 输入网址\n\n当我们想要打开一个页面时,很简单的会想到去输入一个网址 我们以本网址为例,解释一下它各部分的组成:\n\nhttps://www.baidu.com\n\n\n1\n\n\n * https :https是协议(Protocol),它表明了浏览器使用何种协议,有http、https、ftp、mailto等,其中https是加密认证版的http(详见《图解HTTP》)\n\n * www.baidu.com :这是域名(Domain name),它表明正在请求哪个Web服务器,其中www是主机名(host)\n\n\n# DNS解析\n\n输入网址(URL)这一步我们很好理解,但是接下来所谓的域名解析是什么呢?又是为什么要进行域名解析呢?\n\n\n# 什么是IP地址\n\nIP 地址(Internet Protocol Address)是互联网协议特有的一种地址,它是 IP 协议提供的一种统一的地址格式。IP 地址为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。 像192.168.59.0就是一个主机IP地址\n\n\n# 什么是DNS\n\nDNS服务器上存储着映射<域名-IP>,DNS解析就是去DNS服务器查询IP地址。\n\n\n# DNS解析\n\nDNS解析按照下面顺序逐个查找。\n\n * 本地(hosts文件)\n * DNS服务器(本地DNS | 根DNS)\n * 域服务器\n * 解析服务器的地址\n\n\n\n\n# DNS查询的两种方式:递归查询和迭代查询\n\n递归解析 迭代解析\n\n\n# DNS域名称空间的组织方式\n\n\n\n\n# DNS负载均衡\n\n当一个网站有足够多的用户的时候,假如每次请求的资源都位于同一台机器上面,那么这台机器随时可能会蹦掉。\n\n处理办法就是用DNS负载均衡技术,它的原理是在DNS服务器中为同一个主机名配置多个IP地址 在应答DNS查询时,DNS服务器对每个查询将以DNS文件中主机记录的IP地址按顺序返回不同的解析结果,将客户端的访问引导到不同的机器上去,使得不同的客户端访问不同的服务器,从而达到负载均衡的目的。\n\n\n# 建立TCP/IP链接\n\n\n# ”三次握手”的详解\n\n\n\n\n# 为什么要三次握手\n\n * clien端 防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。 client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致连接释放以后的某个时间才到达server。 本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。 于是就向client发出确认报文段,同意建立连接。\n\n * server端 假设不采用“三次握手”。 如果只要server发出确认,新的连接就建立了。并一直等待client发来数据。 事实是client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。 但server端就会有许多这样的等待,server的很多资源就白白浪费掉了。\n\n采用“三次握手”的办法可以防止上述现象发生。主要目的防止server端一直等待,浪费资源。", + "normalizedContent": "从我们输入一个网址到页面展现的过程到底发生了什么呢?\n\n\n# 总体来说分为以下几个过程:\n\n * 输入网址\n * dns解析\n * 建立tcp/ip链接\n * 发送http请求\n * 服务器处理请求\n * 服务器返回http响应\n * 浏览器渲染页面并展现\n * 断开连接\n\n\n# 输入网址\n\n当我们想要打开一个页面时,很简单的会想到去输入一个网址 我们以本网址为例,解释一下它各部分的组成:\n\nhttps://www.baidu.com\n\n\n1\n\n\n * https :https是协议(protocol),它表明了浏览器使用何种协议,有http、https、ftp、mailto等,其中https是加密认证版的http(详见《图解http》)\n\n * www.baidu.com :这是域名(domain name),它表明正在请求哪个web服务器,其中www是主机名(host)\n\n\n# dns解析\n\n输入网址(url)这一步我们很好理解,但是接下来所谓的域名解析是什么呢?又是为什么要进行域名解析呢?\n\n\n# 什么是ip地址\n\nip 地址(internet protocol address)是互联网协议特有的一种地址,它是 ip 协议提供的一种统一的地址格式。ip 地址为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。 像192.168.59.0就是一个主机ip地址\n\n\n# 什么是dns\n\ndns服务器上存储着映射<域名-ip>,dns解析就是去dns服务器查询ip地址。\n\n\n# dns解析\n\ndns解析按照下面顺序逐个查找。\n\n * 本地(hosts文件)\n * dns服务器(本地dns | 根dns)\n * 域服务器\n * 解析服务器的地址\n\n\n\n\n# dns查询的两种方式:递归查询和迭代查询\n\n递归解析 迭代解析\n\n\n# dns域名称空间的组织方式\n\n\n\n\n# dns负载均衡\n\n当一个网站有足够多的用户的时候,假如每次请求的资源都位于同一台机器上面,那么这台机器随时可能会蹦掉。\n\n处理办法就是用dns负载均衡技术,它的原理是在dns服务器中为同一个主机名配置多个ip地址 在应答dns查询时,dns服务器对每个查询将以dns文件中主机记录的ip地址按顺序返回不同的解析结果,将客户端的访问引导到不同的机器上去,使得不同的客户端访问不同的服务器,从而达到负载均衡的目的。\n\n\n# 建立tcp/ip链接\n\n\n# ”三次握手”的详解\n\n\n\n\n# 为什么要三次握手\n\n * clien端 防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。 client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致连接释放以后的某个时间才到达server。 本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。 于是就向client发出确认报文段,同意建立连接。\n\n * server端 假设不采用“三次握手”。 如果只要server发出确认,新的连接就建立了。并一直等待client发来数据。 事实是client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。 但server端就会有许多这样的等待,server的很多资源就白白浪费掉了。\n\n采用“三次握手”的办法可以防止上述现象发生。主要目的防止server端一直等待,浪费资源。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { - "title": "HTTP 中 GET 和 POST 区别", + "title": "什么是 TCP 粘包和拆包?", "frontmatter": { - "title": "HTTP 中 GET 和 POST 区别", - "date": "2022-09-21T22:34:00.000Z", - "permalink": "/pages/a0e400/", + "title": "什么是 TCP 粘包和拆包?", + "date": "2022-09-21T22:35:01.000Z", + "permalink": "/pages/2822ac/", "categories": [ "计算机基础", "计算机网络" @@ -4090,60 +4140,89 @@ export const siteData = { null ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/030.HTTP%20%E4%B8%AD%20GET%20%E5%92%8C%20POST%20%E5%8C%BA%E5%88%AB.html", - "relativePath": "02.计算机基础/10.计算机网络/030.HTTP 中 GET 和 POST 区别.md", - "key": "v-12717373", - "path": "/pages/a0e400/", + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/050.%E4%BB%80%E4%B9%88%E6%98%AF%20TCP%20%E7%B2%98%E5%8C%85%E5%92%8C%E6%8B%86%E5%8C%85%EF%BC%9F.html", + "relativePath": "02.计算机基础/10.计算机网络/050.什么是 TCP 粘包和拆包?.md", + "key": "v-feb79e02", + "path": "/pages/2822ac/", "headers": [ { "level": 2, - "title": "什么是 HTTP ?", - "slug": "什么是-http", - "normalizedTitle": "什么是 http ?", + "title": "TCP粘包与拆包", + "slug": "tcp粘包与拆包", + "normalizedTitle": "tcp粘包与拆包", "charIndex": 2 }, { "level": 2, - "title": "两种 HTTP 请求方法:GET 和 POST", - "slug": "两种-http-请求方法-get-和-post", - "normalizedTitle": "两种 http 请求方法:get 和 post", - "charIndex": 186 + "title": "粘包和拆包原因", + "slug": "粘包和拆包原因", + "normalizedTitle": "粘包和拆包原因", + "charIndex": 510 }, { "level": 2, - "title": "GET 方法", - "slug": "get-方法", - "normalizedTitle": "get 方法", - "charIndex": 364 + "title": "粘包和拆包解决策略", + "slug": "粘包和拆包解决策略", + "normalizedTitle": "粘包和拆包解决策略", + "charIndex": 704 }, { "level": 2, - "title": "POST 方法", - "slug": "post-方法", - "normalizedTitle": "post 方法", - "charIndex": 593 + "title": "粘包的概念", + "slug": "粘包的概念", + "normalizedTitle": "粘包的概念", + "charIndex": 985 }, { "level": 2, - "title": "比较 GET 与 POST", - "slug": "比较-get-与-post", - "normalizedTitle": "比较 get 与 post", - "charIndex": 834 + "title": "出现粘包的原因", + "slug": "出现粘包的原因", + "normalizedTitle": "出现粘包的原因", + "charIndex": 1173 }, { "level": 2, - "title": "其他 HTTP 请求方法", - "slug": "其他-http-请求方法", - "normalizedTitle": "其他 http 请求方法", - "charIndex": 1967 + "title": "粘包的处理方式", + "slug": "粘包的处理方式", + "normalizedTitle": "粘包的处理方式", + "charIndex": 1711 } ], - "headersStr": "什么是 HTTP ? 两种 HTTP 请求方法:GET 和 POST GET 方法 POST 方法 比较 GET 与 POST 其他 HTTP 请求方法", - "content": "# 什么是 HTTP ?\n\n超文本传输协议(HTTP)的设计目的是保证客户端与服务器之间的通信。\n\nHTTP 的工作方式是客户端与服务器之间的请求-应答协议。\n\nweb 浏览器可能是客户端,而计算机上的网络应用程序也可能作为服务器端。\n\n举例:客户端(浏览器)向服务器提交 HTTP 请求;服务器向客户端返回响应。响应包含关于请求的状态信息以及可能被请求的内容。\n\n\n# 两种 HTTP 请求方法:GET 和 POST\n\n在客户机和服务器之间进行请求-响应时,两种最常被用到的方法是:GET 和 POST。\n\n * GET - 从指定的资源请求数据。\n * POST - 向指定的资源提交要被处理的数据。 GET 提交参数一般显示在 URL 上,POST 通过表单提交不会显示在 URL 上,POST 更具隐蔽性:\n\n\n\n\n# GET 方法\n\n请注意,查询字符串(名称/值对)是在 GET 请求的 URL 中发送的:\n\n/test/demo_form.php?name1=value1&name2=value2\n\n\n1\n\n\n有关 GET 请求的其他一些注释:\n\n * GET 请求可被缓存\n * GET 请求保留在浏览器历史记录中\n * GET 请求可被收藏为书签\n * GET 请求不应在处理敏感数据时使用\n * GET 请求有长度限制\n * GET 请求只应当用于取回数据\n\n\n# POST 方法\n\n请注意,查询字符串(名称/值对)是在 POST 请求的 HTTP 消息主体中发送的:\n\nPOST /test/demo_form.php HTTP/1.1\nHost: runoob.com\nname1=value1&name2=value2\n\n\n1\n2\n3\n\n\n有关 POST 请求的其他一些注释:\n\n * POST 请求不会被缓存\n * POST 请求不会保留在浏览器历史记录中\n * POST 不能被收藏为书签\n * POST 请求对数据长度没有要求\n\n\n# 比较 GET 与 POST\n\n下面的表格比较了两种 HTTP 方法:GET 和 POST。\n\n GET POST\n后退按钮/刷新 无害 数据会被重新提交(浏览器应该告知用户数据会被重新提交)。\n书签 可收藏为书签 不可收藏为书签\n缓存 能被缓存 不能缓存\n编码类型 application/x-www-form-urlencoded application/x-www-form-urlencoded or\n multipart/form-data。为二进制数据使用多重编码。\n历史 参数保留在浏览器历史中。 参数不会保存在浏览器历史中。\n对数据长度的限制 是的。当发送数据时,GET 方法向 URL 添加数据;URL 的长度是受限制的(URL 的最大长度是 2048 无限制。\n 个字符)。\n对数据类型的限制 只允许 ASCII 字符。 没有限制。也允许二进制数据。\n安全性 与 POST 相比,GET 的安全性较差,因为所发送的数据是 URL 的一部分。 在发送密码或其他敏感信息时绝不要使用 GET ! POST 比 GET 更安全,因为参数不会被保存在浏览器历史或 web\n 服务器日志中。\n可见性 数据在 URL 中对所有人都是可见的。 数据不会显示在 URL 中。\n\n\n# 其他 HTTP 请求方法\n\n下面的表格列出了其他一些 HTTP 请求方法:\n\n方法 描述\nHEAD 与 GET 相同,但只返回 HTTP 报头,不返回文档主体。\nPUT 上传指定的 URI 表示。\nDELETE 删除指定资源。\nOPTIONS 返回服务器支持的 HTTP 方法。\nCONNECT 把请求连接转换到透明的 TCP/IP 通道。", - "normalizedContent": "# 什么是 http ?\n\n超文本传输协议(http)的设计目的是保证客户端与服务器之间的通信。\n\nhttp 的工作方式是客户端与服务器之间的请求-应答协议。\n\nweb 浏览器可能是客户端,而计算机上的网络应用程序也可能作为服务器端。\n\n举例:客户端(浏览器)向服务器提交 http 请求;服务器向客户端返回响应。响应包含关于请求的状态信息以及可能被请求的内容。\n\n\n# 两种 http 请求方法:get 和 post\n\n在客户机和服务器之间进行请求-响应时,两种最常被用到的方法是:get 和 post。\n\n * get - 从指定的资源请求数据。\n * post - 向指定的资源提交要被处理的数据。 get 提交参数一般显示在 url 上,post 通过表单提交不会显示在 url 上,post 更具隐蔽性:\n\n\n\n\n# get 方法\n\n请注意,查询字符串(名称/值对)是在 get 请求的 url 中发送的:\n\n/test/demo_form.php?name1=value1&name2=value2\n\n\n1\n\n\n有关 get 请求的其他一些注释:\n\n * get 请求可被缓存\n * get 请求保留在浏览器历史记录中\n * get 请求可被收藏为书签\n * get 请求不应在处理敏感数据时使用\n * get 请求有长度限制\n * get 请求只应当用于取回数据\n\n\n# post 方法\n\n请注意,查询字符串(名称/值对)是在 post 请求的 http 消息主体中发送的:\n\npost /test/demo_form.php http/1.1\nhost: runoob.com\nname1=value1&name2=value2\n\n\n1\n2\n3\n\n\n有关 post 请求的其他一些注释:\n\n * post 请求不会被缓存\n * post 请求不会保留在浏览器历史记录中\n * post 不能被收藏为书签\n * post 请求对数据长度没有要求\n\n\n# 比较 get 与 post\n\n下面的表格比较了两种 http 方法:get 和 post。\n\n get post\n后退按钮/刷新 无害 数据会被重新提交(浏览器应该告知用户数据会被重新提交)。\n书签 可收藏为书签 不可收藏为书签\n缓存 能被缓存 不能缓存\n编码类型 application/x-www-form-urlencoded application/x-www-form-urlencoded or\n multipart/form-data。为二进制数据使用多重编码。\n历史 参数保留在浏览器历史中。 参数不会保存在浏览器历史中。\n对数据长度的限制 是的。当发送数据时,get 方法向 url 添加数据;url 的长度是受限制的(url 的最大长度是 2048 无限制。\n 个字符)。\n对数据类型的限制 只允许 ascii 字符。 没有限制。也允许二进制数据。\n安全性 与 post 相比,get 的安全性较差,因为所发送的数据是 url 的一部分。 在发送密码或其他敏感信息时绝不要使用 get ! post 比 get 更安全,因为参数不会被保存在浏览器历史或 web\n 服务器日志中。\n可见性 数据在 url 中对所有人都是可见的。 数据不会显示在 url 中。\n\n\n# 其他 http 请求方法\n\n下面的表格列出了其他一些 http 请求方法:\n\n方法 描述\nhead 与 get 相同,但只返回 http 报头,不返回文档主体。\nput 上传指定的 uri 表示。\ndelete 删除指定资源。\noptions 返回服务器支持的 http 方法。\nconnect 把请求连接转换到透明的 tcp/ip 通道。", + "headersStr": "TCP粘包与拆包 粘包和拆包原因 粘包和拆包解决策略 粘包的概念 出现粘包的原因 粘包的处理方式", + "content": "# TCP粘包与拆包\n\nTCP是个“流”协议,没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。\n\n\n\n假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。\n\n(1)服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;\n\n(2)服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;\n\n(3)服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;\n\n(4)服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。\n\n如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。\n\n\n# 粘包和拆包原因\n\n(1)要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包;\n\n(2)接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包;\n\n(3)要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包;\n\n(4)待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。即TCP报文长度-TCP头部长度>MSS。\n\n\n# 粘包和拆包解决策略\n\n由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,归纳如下:\n\n消息定长。发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。\n\n设置消息边界。服务端从网络流中按消息边界分离出消息内容。在包尾增加回车换行符进行分割,例如FTP协议。\n\n将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段。 更复杂的应用层协议。\n\n\n# 粘包的概念\n\n粘包:多个数据包被连续存储于连续的缓存中,在对数据包进行读取时由于无法确定发生方的发送边界,而采用某一估测值大小来进行数据读出,若双方的size不一致时就会使指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。\n\n比如说:发送方发送了两个数据,接收方一次收了一个半数据(接收方可能不清楚一个包有多大)\n\n\n\n\n# 出现粘包的原因\n\n出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。\n\n发送方引起的粘包是由TCP协议本身造成的:\n\nTCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。\n\n\n\nTCP协议规定有MSS,如果数据包过长就会被分开传输。这样接收方就收到了拆包数据。\n\n\n\n接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。\n\n在代码中常见体现:\n\n要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。\n\n要发送的数据大于MSS,TCP在传输前将进行拆包。\n\n要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。\n\n接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。 等等。\n\n\n# 粘包的处理方式\n\n当短连接的情况下,不用考虑粘包的情况\n\n如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包\n\n如果双方建立长连接,需要在连接后一段时间内发送不同结构数据\n\n发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。\n\n发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。\n\n可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。", + "normalizedContent": "# tcp粘包与拆包\n\ntcp是个“流”协议,没有界限的一串数据。tcp底层并不了解上层业务数据的具体含义,它会根据tcp缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被tcp拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的tcp粘包和拆包问题。\n\n\n\n假设客户端分别发送了两个数据包d1和d2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。\n\n(1)服务端分两次读取到了两个独立的数据包,分别是d1和d2,没有粘包和拆包;\n\n(2)服务端一次接收到了两个数据包,d1和d2粘合在一起,被称为tcp粘包;\n\n(3)服务端分两次读取到了两个数据包,第一次读取到了完整的d1包和d2包的部分内容,第二次读取到了d2包的剩余内容,这被称为tcp拆包;\n\n(4)服务端分两次读取到了两个数据包,第一次读取到了d1包的部分内容d1_1,第二次读取到了d1包的剩余内容d1_2和d2包的整包。\n\n如果此时服务端tcp接收滑窗非常小,而数据包d1和d2比较大,很有可能会发生第五种可能,即服务端分多次才能将d1和d2包接收完全,期间发生多次拆包。\n\n\n# 粘包和拆包原因\n\n(1)要发送的数据小于tcp发送缓冲区的大小,tcp将多次写入缓冲区的数据一次发送出去,将会发生粘包;\n\n(2)接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包;\n\n(3)要发送的数据大于tcp发送缓冲区剩余空间大小,将会发生拆包;\n\n(4)待发送数据大于mss(最大报文长度),tcp在传输前将进行拆包。即tcp报文长度-tcp头部长度>mss。\n\n\n# 粘包和拆包解决策略\n\n由于底层的tcp无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,归纳如下:\n\n消息定长。发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。\n\n设置消息边界。服务端从网络流中按消息边界分离出消息内容。在包尾增加回车换行符进行分割,例如ftp协议。\n\n将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段。 更复杂的应用层协议。\n\n\n# 粘包的概念\n\n粘包:多个数据包被连续存储于连续的缓存中,在对数据包进行读取时由于无法确定发生方的发送边界,而采用某一估测值大小来进行数据读出,若双方的size不一致时就会使指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。\n\n比如说:发送方发送了两个数据,接收方一次收了一个半数据(接收方可能不清楚一个包有多大)\n\n\n\n\n# 出现粘包的原因\n\n出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。\n\n发送方引起的粘包是由tcp协议本身造成的:\n\ntcp为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常tcp会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。\n\n\n\ntcp协议规定有mss,如果数据包过长就会被分开传输。这样接收方就收到了拆包数据。\n\n\n\n接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。\n\n在代码中常见体现:\n\n要发送的数据大于tcp发送缓冲区剩余空间大小,将会发生拆包。\n\n要发送的数据大于mss,tcp在传输前将进行拆包。\n\n要发送的数据小于tcp发送缓冲区的大小,tcp将多次写入缓冲区的数据一次发送出去,将会发生粘包。\n\n接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。 等等。\n\n\n# 粘包的处理方式\n\n当短连接的情况下,不用考虑粘包的情况\n\n如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包\n\n如果双方建立长连接,需要在连接后一段时间内发送不同结构数据\n\n发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。\n\n发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。\n\n可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。", "charsets": { "cjk": true - } + }, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 + }, + { + "title": "TCP 怎么保证可靠传输", + "frontmatter": { + "title": "TCP 怎么保证可靠传输", + "date": "2022-09-21T22:34:41.000Z", + "permalink": "/pages/5fcc49/", + "categories": [ + "计算机基础", + "计算机网络" + ], + "tags": [ + null + ] + }, + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/040.TCP%20%E6%80%8E%E4%B9%88%E4%BF%9D%E8%AF%81%E5%8F%AF%E9%9D%A0%E4%BC%A0%E8%BE%93.html", + "relativePath": "02.计算机基础/10.计算机网络/040.TCP 怎么保证可靠传输.md", + "key": "v-5047727f", + "path": "/pages/5fcc49/", + "headersStr": null, + "content": "TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文段在超时时间内没有收到确认,那么就重传这个报文段。\n\n一个报文段从发送再到接收到确认所经过的时间称为往返时间 RTT,加权平均往返时间 RTTs 计算如下:\n\n\n\n其中,0 ≤ a < 1,RTTs 随着 a 的增加更容易受到 RTT 的影响。 超时时间 RTO 应该略大于 RTTs,TCP 使用的超时时间计算如下:\n\n\n\n其中 RTTd 为偏差的加权平均值。\n\n参考: TCP 可靠传输", + "normalizedContent": "tcp 使用超时重传来实现可靠传输:如果一个已经发送的报文段在超时时间内没有收到确认,那么就重传这个报文段。\n\n一个报文段从发送再到接收到确认所经过的时间称为往返时间 rtt,加权平均往返时间 rtts 计算如下:\n\n\n\n其中,0 ≤ a < 1,rtts 随着 a 的增加更容易受到 rtt 的影响。 超时时间 rto 应该略大于 rtts,tcp 使用的超时时间计算如下:\n\n\n\n其中 rttd 为偏差的加权平均值。\n\n参考: tcp 可靠传输", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "简述对称与非对称加密的概念", @@ -4166,7 +4245,9 @@ export const siteData = { "headersStr": null, "content": "", "normalizedContent": "", - "charsets": {} + "charsets": {}, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "简述 HTTPS 的加密与认证过程", @@ -4249,76 +4330,9 @@ export const siteData = { "normalizedContent": "# https 协议优点\n\n保密性好、信任度高。\n\n\n# https 的核心—ssl/tls协议\n\nhttps 之所以能达到较高的安全性要求,就是结合了 ssl/tls 和 tcp 协议,对通信数据进行加密,解决了 http 数据透明的问题。接下来重点介绍一下 ssl/tls 的工作原理。\n\n\n# ssl 和 tls 的区别?\n\nssl 和 tls 没有太大的区别。\n\nssl 指安全套接字协议(secure sockets layer),首次发布与 1996 年。ssl 的首次发布其实已经是他的 3.0 版本,ssl 1.0 从未面世,ssl 2.0 则具有较大的缺陷(drown 缺陷——decrypting rsa with obsolete and weakened encryption)。很快,在 1999 年,ssl 3.0 进一步升级,新版本被命名为 tls 1.0。因此,tls 是基于 ssl 之上的,但由于习惯叫法,通常把 https 中的核心加密协议混成为 ssl/tls。\n\n\n# ssl/tls 的工作原理\n\n\n# 非对称加密\n\nssl/tls 的核心要素是非对称加密。非对称加密采用两个密钥——一个公钥,一个私钥。在通信时,私钥仅由解密者保存,公钥由任何一个想与解密者通信的发送者(加密者)所知。可以设想一个场景,\n\n> 在某个自助邮局,每个通信信道都是一个邮箱,每一个邮箱所有者都在旁边立了一个牌子,上面挂着一把钥匙:这是我的公钥,发送者请将信件放入我的邮箱,并用公钥锁好。 但是公钥只能加锁,并不能解锁。解锁只能由邮箱的所有者——因为只有他保存着私钥。 这样,通信信息就不会被其他人截获了,这依赖于私钥的保密性。\n\n\n\n非对称加密的公钥和私钥需要采用一种复杂的数学机制生成(密码学认为,为了较高的安全性,尽量不要自己创造加密方案)。公私钥对的生成算法依赖于单向陷门函数。\n\n> 单向函数:已知单向函数 f,给定任意一个输入 x,易计算输出 y=f(x);而给定一个输出 y,假设存在 f(x)=y,很难根据 f 来计算出 x。 单向陷门函数:一个较弱的单向函数。已知单向陷门函数 f,陷门 h,给定任意一个输入 x,易计算出输出 y=f(x;h);而给定一个输出 y,假设存在 f(x;h)=y,很难根据 f 来计算出 x,但可以根据 f 和 h 来推导出 x。\n\n\n\n上图就是一个单向函数(不是单项陷门函数),假设有一个绝世秘籍,任何知道了这个秘籍的人都可以把苹果汁榨成苹果,那么这个秘籍就是“陷门”了吧。\n\n在这里,函数 f 的计算方法相当于公钥,陷门 h 相当于私钥。公钥 f 是公开的,任何人对已有输入,都可以用 f 加密,而要想根据加密信息还原出原信息,必须要有私钥才行。\n\n\n# 对称加密\n\n使用 ssl/tls 进行通信的双方需要使用非对称加密方案来通信,但是非对称加密设计了较为复杂的数学算法,在实际通信过程中,计算的代价较高,效率太低,因此,ssl/tls 实际对消息的加密使用的是对称加密。\n\n> 对称加密:通信双方共享唯一密钥 k,加解密算法已知,加密方利用密钥 k 加密,解密方利用密钥 k 解密,保密性依赖于密钥 k 的保密性。\n\n\n\n对称加密的密钥生成代价比公私钥对的生成代价低得多,那么有的人会问了,为什么 ssl/tls 还需要使用非对称加密呢? 因为对称加密的保密性完全依赖于密钥的保密性。 在双方通信之前,需要商量一个用于对称加密的密钥。 我们知道网络通信的信道是不安全的,传输报文对任何人是可见的,密钥的交换肯定不能直接在网络信道中传输。 因此,使用非对称加密,对对称加密的密钥进行加密,保护该密钥不在网络信道中被窃听。 这样,通信双方只需要一次非对称加密,交换对称加密的密钥,在之后的信息通信中, 使用绝对安全的密钥,对信息进行对称加密,即可保证传输消息的保密性。\n\n\n# 公钥传输的信赖性\n\nssl/tls 介绍到这里,了解信息安全的朋友又会想到一个安全隐患,设想一个下面的场景:\n\n> 客户端 c 和服务器 s 想要使用 ssl/tls 通信,由上述 ssl/tls 通信原理,c 需要先知道 s 的公钥,而 s 公钥的唯一获取途径,就是把 s 公钥在网络信道中传输。要注意网络信道通信中有几个前提:\n\n> 1.任何人都可以捕获通信包\n> \n> 2.通信包的保密性由发送者设计\n> \n> 3.保密算法设计方案默认为公开,而(解密)密钥默认是安全的\n> \n> 因此,假设 s 公钥不做加密,在信道中传输,那么很有可能存在一个攻击者 a,发送给 c 一个诈包,假装是 s 公钥,其实是诱饵服务器 as 的公钥。当 c 收获了 as 的公钥(却以为是 s 的公钥),c 后续就会使用 as 公钥对数据进行加密,并在公开信道传输,那么 a 将捕获这些加密包,用 as 的私钥解密,就截获了 c 本要给 s 发送的内容,而 c 和 s 二人全然不知。\n\n同样的,s 公钥即使做加密,也难以避免这种信任性问题,c 被 as 拐跑了!\n\n\n\n为了公钥传输的信赖性问题,第三方机构应运而生——证书颁发机构(ca,certificate authority)。ca 默认是受信任的第三方。ca 会给各个服务器颁发证书,证书存储在服务器上,并附有 ca 的电子签名。\n\n当客户端(浏览器)向服务器发送 https 请求时,一定要先获取目标服务器的证书,并根据证书上的信息,检验证书的合法性。一旦客户端检测到证书非法,就会发生错误。客户端获取了服务器的证书后,由于证书的信任性是由第三方信赖机构认证的,而证书上又包含着服务器的公钥信息,客户端就可以放心的信任证书上的公钥就是目标服务器的公钥。\n\n\n# 数字签名\n\n好,到这一小节,已经是 ssl/tls 的尾声了。上一小节提到了数字签名,数字签名要解决的问题,是防止证书被伪造。第三方信赖机构 ca 之所以能被信赖,就是 靠数字签名技术 。\n\n数字签名,是 ca 在给服务器颁发证书时,使用散列+加密的组合技术,在证书上盖个章,以此来提供验伪的功能。具体行为如下:\n\n> ca 知道服务器的公钥,对该公钥采用散列技术生成一个摘要。ca 使用 ca 私钥对该摘要进行加密,并附在证书下方,发送给服务器。\n\n> 现在服务器将该证书发送给客户端,客户端需要验证该证书的身份。客户端找到第三方机构 ca,获知 ca 的公钥,并用 ca 公钥对证书的签名进行解密,获得了 ca 生成的摘要。\n\n> 客户端对证书数据(也就是服务器的公钥)做相同的散列处理,得到摘要,并将该摘要与之前从签名中解码出的摘要做对比,如果相同,则身份验证成功;否则验证失败。\n\n\n\n总结来说,带有证书的公钥传输机制如下:\n\n 1. 设有服务器 s,客户端 c,和第三方信赖机构 ca。\n 2. s 信任 ca,ca 是知道 s 公钥的,ca 向 s 颁发证书。并附上 ca 私钥对消息摘要的加密签名。\n 3. s 获得 ca 颁发的证书,将该证书传递给 c。\n 4. c 获得 s 的证书,信任 ca 并知晓 ca 公钥,使用 ca 公钥对 s 证书上的签名解密,同时对消息进行散列处理,得到摘要。比较摘要,验证 s 证书的真实性。\n 5. 如果 c 验证 s 证书是真实的,则信任 s 的公钥(在 s 证书中)。\n\n\n\n对于数字签名,我这里讲的比较简单,如果你没有搞清楚的话,强烈推荐你看看数字签名及数字证书原理 这个视频,这是我看过最清晰的讲解。\n\n", "charsets": { "cjk": true - } - }, - { - "title": "什么是 TCP 粘包和拆包?", - "frontmatter": { - "title": "什么是 TCP 粘包和拆包?", - "date": "2022-09-21T22:35:01.000Z", - "permalink": "/pages/2822ac/", - "categories": [ - "计算机基础", - "计算机网络" - ], - "tags": [ - null - ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/050.%E4%BB%80%E4%B9%88%E6%98%AF%20TCP%20%E7%B2%98%E5%8C%85%E5%92%8C%E6%8B%86%E5%8C%85%EF%BC%9F.html", - "relativePath": "02.计算机基础/10.计算机网络/050.什么是 TCP 粘包和拆包?.md", - "key": "v-feb79e02", - "path": "/pages/2822ac/", - "headers": [ - { - "level": 2, - "title": "TCP粘包与拆包", - "slug": "tcp粘包与拆包", - "normalizedTitle": "tcp粘包与拆包", - "charIndex": 2 - }, - { - "level": 2, - "title": "粘包和拆包原因", - "slug": "粘包和拆包原因", - "normalizedTitle": "粘包和拆包原因", - "charIndex": 510 - }, - { - "level": 2, - "title": "粘包和拆包解决策略", - "slug": "粘包和拆包解决策略", - "normalizedTitle": "粘包和拆包解决策略", - "charIndex": 704 - }, - { - "level": 2, - "title": "粘包的概念", - "slug": "粘包的概念", - "normalizedTitle": "粘包的概念", - "charIndex": 985 - }, - { - "level": 2, - "title": "出现粘包的原因", - "slug": "出现粘包的原因", - "normalizedTitle": "出现粘包的原因", - "charIndex": 1173 - }, - { - "level": 2, - "title": "粘包的处理方式", - "slug": "粘包的处理方式", - "normalizedTitle": "粘包的处理方式", - "charIndex": 1711 - } - ], - "headersStr": "TCP粘包与拆包 粘包和拆包原因 粘包和拆包解决策略 粘包的概念 出现粘包的原因 粘包的处理方式", - "content": "# TCP粘包与拆包\n\nTCP是个“流”协议,没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。\n\n\n\n假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。\n\n(1)服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;\n\n(2)服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;\n\n(3)服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;\n\n(4)服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。\n\n如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。\n\n\n# 粘包和拆包原因\n\n(1)要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包;\n\n(2)接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包;\n\n(3)要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包;\n\n(4)待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。即TCP报文长度-TCP头部长度>MSS。\n\n\n# 粘包和拆包解决策略\n\n由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,归纳如下:\n\n消息定长。发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。\n\n设置消息边界。服务端从网络流中按消息边界分离出消息内容。在包尾增加回车换行符进行分割,例如FTP协议。\n\n将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段。 更复杂的应用层协议。\n\n\n# 粘包的概念\n\n粘包:多个数据包被连续存储于连续的缓存中,在对数据包进行读取时由于无法确定发生方的发送边界,而采用某一估测值大小来进行数据读出,若双方的size不一致时就会使指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。\n\n比如说:发送方发送了两个数据,接收方一次收了一个半数据(接收方可能不清楚一个包有多大)\n\n\n\n\n# 出现粘包的原因\n\n出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。\n\n发送方引起的粘包是由TCP协议本身造成的:\n\nTCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。\n\n\n\nTCP协议规定有MSS,如果数据包过长就会被分开传输。这样接收方就收到了拆包数据。\n\n\n\n接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。\n\n在代码中常见体现:\n\n要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。\n\n要发送的数据大于MSS,TCP在传输前将进行拆包。\n\n要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。\n\n接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。 等等。\n\n\n# 粘包的处理方式\n\n当短连接的情况下,不用考虑粘包的情况\n\n如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包\n\n如果双方建立长连接,需要在连接后一段时间内发送不同结构数据\n\n发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。\n\n发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。\n\n可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。", - "normalizedContent": "# tcp粘包与拆包\n\ntcp是个“流”协议,没有界限的一串数据。tcp底层并不了解上层业务数据的具体含义,它会根据tcp缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被tcp拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的tcp粘包和拆包问题。\n\n\n\n假设客户端分别发送了两个数据包d1和d2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。\n\n(1)服务端分两次读取到了两个独立的数据包,分别是d1和d2,没有粘包和拆包;\n\n(2)服务端一次接收到了两个数据包,d1和d2粘合在一起,被称为tcp粘包;\n\n(3)服务端分两次读取到了两个数据包,第一次读取到了完整的d1包和d2包的部分内容,第二次读取到了d2包的剩余内容,这被称为tcp拆包;\n\n(4)服务端分两次读取到了两个数据包,第一次读取到了d1包的部分内容d1_1,第二次读取到了d1包的剩余内容d1_2和d2包的整包。\n\n如果此时服务端tcp接收滑窗非常小,而数据包d1和d2比较大,很有可能会发生第五种可能,即服务端分多次才能将d1和d2包接收完全,期间发生多次拆包。\n\n\n# 粘包和拆包原因\n\n(1)要发送的数据小于tcp发送缓冲区的大小,tcp将多次写入缓冲区的数据一次发送出去,将会发生粘包;\n\n(2)接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包;\n\n(3)要发送的数据大于tcp发送缓冲区剩余空间大小,将会发生拆包;\n\n(4)待发送数据大于mss(最大报文长度),tcp在传输前将进行拆包。即tcp报文长度-tcp头部长度>mss。\n\n\n# 粘包和拆包解决策略\n\n由于底层的tcp无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,归纳如下:\n\n消息定长。发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。\n\n设置消息边界。服务端从网络流中按消息边界分离出消息内容。在包尾增加回车换行符进行分割,例如ftp协议。\n\n将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段。 更复杂的应用层协议。\n\n\n# 粘包的概念\n\n粘包:多个数据包被连续存储于连续的缓存中,在对数据包进行读取时由于无法确定发生方的发送边界,而采用某一估测值大小来进行数据读出,若双方的size不一致时就会使指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。\n\n比如说:发送方发送了两个数据,接收方一次收了一个半数据(接收方可能不清楚一个包有多大)\n\n\n\n\n# 出现粘包的原因\n\n出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。\n\n发送方引起的粘包是由tcp协议本身造成的:\n\ntcp为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常tcp会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。\n\n\n\ntcp协议规定有mss,如果数据包过长就会被分开传输。这样接收方就收到了拆包数据。\n\n\n\n接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。\n\n在代码中常见体现:\n\n要发送的数据大于tcp发送缓冲区剩余空间大小,将会发生拆包。\n\n要发送的数据大于mss,tcp在传输前将进行拆包。\n\n要发送的数据小于tcp发送缓冲区的大小,tcp将多次写入缓冲区的数据一次发送出去,将会发生粘包。\n\n接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。 等等。\n\n\n# 粘包的处理方式\n\n当短连接的情况下,不用考虑粘包的情况\n\n如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包\n\n如果双方建立长连接,需要在连接后一段时间内发送不同结构数据\n\n发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。\n\n发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。\n\n可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。", - "charsets": { - "cjk": true - } + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "TCP 与 UDP 在网络协议中的哪一层,他们之间有什么区别?", @@ -4373,7 +4387,66 @@ export const siteData = { "normalizedContent": "# tcp 与 udp 的区别(重要)\n\n 1. 是否面向连接 :udp 在传送数据之前不需要先建立连接。而 tcp 提供面向连接的服务,在传送数据之前必须先建立连接,数据传送结束后要释放连接。\n 2. 是否是可靠传输:远地主机在收到 udp 报文后,不需要给出任何确认,并且不保证数据不丢失,不保证是否顺序到达。tcp 提供可靠的传输服务,tcp 在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制。通过 tcp 连接传输的数据,无差错、不丢失、不重复、并且按序到达。\n 3. 是否有状态 :这个和上面的“是否可靠传输”相对应。tcp 传输是有状态的,这个有状态说的是 tcp 会去记录自己发送消息的状态比如消息是否发送了、是否被接收了等等。为此 ,tcp 需要维持复杂的连接状态表。而 udp 是无状态服务,简单来说就是不管发出去之后的事情了(这很渣男!)。\n 4. 传输效率 :由于使用 tcp 进行传输的时候多了连接、确认、重传等机制,所以 tcp 的传输效率要比 udp 低很多。\n 5. 传输形式 : tcp 是面向字节流的,udp 是面向报文的。\n 6. 首部开销 :tcp 首部开销(20 ~ 60 字节)比 udp 首部开销(8 字节)要大。\n 7. 是否提供广播或多播服务 :tcp 只支持点对点通信,udp 支持一对一、一对多、多对一、多对多; ...... 我把上面总结的内容通过表格形式展示出来了!\n\n tcp udp\n是否面向连接 是 否\n是否可靠 是 否\n是否有状态 是 否\n传输效率 较慢 较快\n传输形式 字节流 数据报文段\n首部开销 20 ~ 60 bytes 8 bytes\n是否提供广播或多播服务 否 是\n\n\n# 什么时候选择 tcp,什么时候选 udp?\n\n * udp 一般用于即时通信,比如: 语音、 视频 、直播等等。这些场景对传输数据的准确性要求不是特别高,比如你看视频即使少个一两帧,实际给人的感觉区别也不大。\n * tcp 用于对传输准确性要求特别高的场景,比如文件传输、发送和接收邮件、远程登录等等。\n\n\n# http 基于 tcp 还是 udp?\n\nhttp 协议是基于 tcp 协议的,所以发送 http 请求之前首先要建立 tcp 连接也就是要经历 3 次握手。\n\n\n# 使用 tcp 的协议有哪些?使用 udp 的协议有哪些?\n\n运行于 tcp 协议之上的协议 :\n\n 1. http 协议 :超文本传输协议(http,hypertext transfer protocol)主要是为 web 浏览器与 web 服务器之间的通信而设计的。当我们使用浏览器浏览网页的时候,我们网页就是通过 http 请求进行加载的。\n\n 2. https 协议 :更安全的超文本传输协议(https,hypertext transfer protocol secure),身披 ssl 外衣的 http 协议\n\n 3. ftp 协议:文件传输协议 ftp(file transfer protocol),提供文件传输服务,基于 tcp 实现可靠的传输。使用 ftp 传输文件的好处是可以屏蔽操作系统和文件存储方式。\n\n 4. smtp 协议:简单邮件传输协议(smtp,simple mail transfer protocol)的缩写,基于 tcp 协议,用来发送电子邮件。注意 ⚠️:接受邮件的协议不是 smtp 而是 pop3 协议。\n\n 5. pop3/imap 协议: pop3 和 imap 两者都是负责邮件接收的协议。\n\n 6. telent 协议:远程登陆协议,通过一个终端登陆到其他服务器。被一种称为 ssh 的非常安全的协议所取代。\n\n 7. ssh 协议 : ssh( secure shell)是目前较可靠,专为远程登录会话和其他网络服务提供安全性的协议。利用 ssh 协议可以有效防止远程管理过程中的信息泄露问题。ssh 建立在可靠的传输协议 tcp 之上。 ...... 运行于 udp 协议之上的协议 :\n\n 8. dhcp 协议:动态主机配置协议,动态配置 ip 地址\n\n 9. dns : 域名系统(dns,domain name system)将人类可读的域名 (例如,www.baidu.com) 转换为机器可读的 ip 地址 (例如,220.181.38.148)。 我们可以将其理解为专为互联网设计的电话薄。实际上 dns 同时支持 udp 和 tcp 协议。", "charsets": { "cjk": true - } + }, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 + }, + { + "title": "TCP 长连接和短连接有那么不同的使用场景?", + "frontmatter": { + "title": "TCP 长连接和短连接有那么不同的使用场景?", + "date": "2022-09-21T22:36:19.000Z", + "permalink": "/pages/dda19e/", + "categories": [ + "计算机基础", + "计算机网络" + ], + "tags": [ + null + ] + }, + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/100.TCP%20%E9%95%BF%E8%BF%9E%E6%8E%A5%E5%92%8C%E7%9F%AD%E8%BF%9E%E6%8E%A5%E6%9C%89%E9%82%A3%E4%B9%88%E4%B8%8D%E5%90%8C%E7%9A%84%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF%EF%BC%9F.html", + "relativePath": "02.计算机基础/10.计算机网络/100.TCP 长连接和短连接有那么不同的使用场景?.md", + "key": "v-f886ba80", + "path": "/pages/dda19e/", + "headers": [ + { + "level": 2, + "title": "前提:", + "slug": "前提", + "normalizedTitle": "前提:", + "charIndex": 2 + }, + { + "level": 2, + "title": "定义:", + "slug": "定义", + "normalizedTitle": "定义:", + "charIndex": 125 + }, + { + "level": 3, + "title": "优缺点:", + "slug": "优缺点", + "normalizedTitle": "优缺点:", + "charIndex": 338 + }, + { + "level": 3, + "title": "使用场景:", + "slug": "使用场景", + "normalizedTitle": "使用场景:", + "charIndex": 504 + } + ], + "headersStr": "前提: 定义: 优缺点: 使用场景:", + "content": "# 前提:\n\nHTTP/1.0默认使用短连接,HTTP/1.1开始默认使用长连接;\n\nHTTP协议的长连接和短连接,实质就是TCP协议的长连接和短连接;\n\nTCP协议建立连接需要3次握手,断开连接需要4次握手,这个过程会消耗网络资源和时间;\n\n\n# 定义:\n\n * 长连接:在一个TCP连接上可以发送多个数据包,但是如果没有数据包发送时,也要双方发检测包以维持这个长连接;三次握手后连接,不断开连接,保持客户端和服务端通信,直到服务器超时自动断开连接,或者客户端主动断开连接。\n * 短连接:当双方需要数据交互的时候,就建立一个TCP连接,本次交互完之后就断开这个连接;三次握手后建立连接,发送数据包并得到服务器返回的结果后,通过客户端和服务器的四次握手后断开连接。\n\n\n# 优缺点:\n\n长连接可以省去较多建立连接和断开连接的操作,所以比较节省资源和时间,但是长连接如果一直存在的话,需要很多探测包的发送来维持这个连接,这对服务器将是很大的负担; 相对而言,短连接不需要服务器承担太大负担,只要存在的连接就是有用的连接,但如果客户端请求频繁,就会在TCP的建立连接和断开连接上浪费较大的资源和时间。\n\n\n# 使用场景:\n\n短连接:适用于网页浏览等数据刷新频度较低的场景。一般而言像及京东,淘宝这些大型网站,随时都会有成千上万的用户请求,一般使用短连接,用户量太大,服务器扛不住那么多长连接; 长连接:适用于客户端和服务端通信频繁的场景,例如:聊天室,实时游戏等场景。即时通讯(QQ)般使用的都是长连接,但并不是永久连接(比如20分钟,半个小时),因为即时通讯是频繁的发送请求,使用长连接只需要建立一次连接,同时再根据业务设置保持时间,超过这个时间就会断开连接,一定程度上保证了服务器的压力不会过大。\n\n##Socket心跳包机制: 像心跳一样,每隔固定时间向服务器发送一个包,以此来告诉服务器,这个客户端还活着。为了保持长连接,一般都是很小的包(节约流量)或者只有包头的空包。 1.客户端每隔一段时间间隔就发送一个探测包给服务器; 2.客户端发包时启动一个超时定时器; 3.服务端接收到探测包后会回应一个包; 4.如果客户端收到服务器的应答包,则说明服务器正常,删除超时定时器;如果没有收到则服务器异常。 ———————————————— 版权声明:本文为CSDN博主「测试小明」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/Dave0002/article/details/125973214", + "normalizedContent": "# 前提:\n\nhttp/1.0默认使用短连接,http/1.1开始默认使用长连接;\n\nhttp协议的长连接和短连接,实质就是tcp协议的长连接和短连接;\n\ntcp协议建立连接需要3次握手,断开连接需要4次握手,这个过程会消耗网络资源和时间;\n\n\n# 定义:\n\n * 长连接:在一个tcp连接上可以发送多个数据包,但是如果没有数据包发送时,也要双方发检测包以维持这个长连接;三次握手后连接,不断开连接,保持客户端和服务端通信,直到服务器超时自动断开连接,或者客户端主动断开连接。\n * 短连接:当双方需要数据交互的时候,就建立一个tcp连接,本次交互完之后就断开这个连接;三次握手后建立连接,发送数据包并得到服务器返回的结果后,通过客户端和服务器的四次握手后断开连接。\n\n\n# 优缺点:\n\n长连接可以省去较多建立连接和断开连接的操作,所以比较节省资源和时间,但是长连接如果一直存在的话,需要很多探测包的发送来维持这个连接,这对服务器将是很大的负担; 相对而言,短连接不需要服务器承担太大负担,只要存在的连接就是有用的连接,但如果客户端请求频繁,就会在tcp的建立连接和断开连接上浪费较大的资源和时间。\n\n\n# 使用场景:\n\n短连接:适用于网页浏览等数据刷新频度较低的场景。一般而言像及京东,淘宝这些大型网站,随时都会有成千上万的用户请求,一般使用短连接,用户量太大,服务器扛不住那么多长连接; 长连接:适用于客户端和服务端通信频繁的场景,例如:聊天室,实时游戏等场景。即时通讯(qq)般使用的都是长连接,但并不是永久连接(比如20分钟,半个小时),因为即时通讯是频繁的发送请求,使用长连接只需要建立一次连接,同时再根据业务设置保持时间,超过这个时间就会断开连接,一定程度上保证了服务器的压力不会过大。\n\n##socket心跳包机制: 像心跳一样,每隔固定时间向服务器发送一个包,以此来告诉服务器,这个客户端还活着。为了保持长连接,一般都是很小的包(节约流量)或者只有包头的空包。 1.客户端每隔一段时间间隔就发送一个探测包给服务器; 2.客户端发包时启动一个超时定时器; 3.服务端接收到探测包后会回应一个包; 4.如果客户端收到服务器的应答包,则说明服务器正常,删除超时定时器;如果没有收到则服务器异常。 ———————————————— 版权声明:本文为csdn博主「测试小明」的原创文章,遵循cc 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/dave0002/article/details/125973214", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "如何解决 TCP 传输丢包问题?", @@ -4396,7 +4469,9 @@ export const siteData = { "headersStr": null, "content": "", "normalizedContent": "", - "charsets": {} + "charsets": {}, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "TCP 的 keepalive 了解吗?说一说它和 HTTP 的 keepalive 的区别?", @@ -4421,14 +4496,16 @@ export const siteData = { "normalizedContent": "http keep-alive 也叫 http 长连接,该函数由应用程序实现,允许使用相同的tcp连接发送和接收多个http请求/响应,减少由http短途连接产生的多个tcp连接的创建和释放成本。\n\ntcp的存活者 也叫 tcp 保活机制,这个函数由内核实现,当客户和服务端在一定时间内与数据不交互时,为了确保连接仍然有效,就会发送探测报文,检查对方是否还在网上,然后决定是否关闭连接\n\n参考: 阿里一面:tcp 的 keepalive 和 http 的 keep-alive 是一个东西吗", "charsets": { "cjk": true - } + }, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { - "title": "HTTP 与 HTTPS 有哪些区别?", + "title": "TCP 中常见的拥塞控制算法有哪些?", "frontmatter": { - "title": "HTTP 与 HTTPS 有哪些区别?", - "date": "2022-09-21T22:36:56.000Z", - "permalink": "/pages/253334/", + "title": "TCP 中常见的拥塞控制算法有哪些?", + "date": "2022-09-21T22:37:12.000Z", + "permalink": "/pages/9ccc31/", "categories": [ "计算机基础", "计算机网络" @@ -4437,23 +4514,23 @@ export const siteData = { null ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/120.HTTP%20%E4%B8%8E%20HTTPS%20%E6%9C%89%E5%93%AA%E4%BA%9B%E5%8C%BA%E5%88%AB%EF%BC%9F.html", - "relativePath": "02.计算机基础/10.计算机网络/120.HTTP 与 HTTPS 有哪些区别?.md", - "key": "v-9aa2c788", - "path": "/pages/253334/", + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/130.TCP%20%E4%B8%AD%E5%B8%B8%E8%A7%81%E7%9A%84%E6%8B%A5%E5%A1%9E%E6%8E%A7%E5%88%B6%E7%AE%97%E6%B3%95%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F.html", + "relativePath": "02.计算机基础/10.计算机网络/130.TCP 中常见的拥塞控制算法有哪些?.md", + "key": "v-75f6731c", + "path": "/pages/9ccc31/", "headersStr": null, - "content": "HTTP 协议介绍 HTTP 协议,全称超文本传输协议(Hypertext Transfer Protocol)。顾名思义,HTTP 协议就是用来规范超文本的传输,超文本,也就是网络上的包括文本在内的各式各样的消息,具体来说,主要是来规范浏览器和服务器端的行为的。\n\n并且,HTTP 是一个无状态(stateless)协议,也就是说服务器不维护任何有关客户端过去所发请求的消息。这其实是一种懒政,有状态协议会更加复杂,需要维护状态(历史信息),而且如果客户或服务器失效,会产生状态的不一致,解决这种不一致的代价更高。\n\n#HTTP 协议通信过程 HTTP 是应用层协议,它以 TCP(传输层)作为底层协议,默认端口为 80. 通信过程主要如下:\n\n服务器在 80 端口等待客户的请求。 浏览器发起到服务器的 TCP 连接(创建套接字 Socket)。 服务器接收来自浏览器的 TCP 连接。 浏览器(HTTP 客户端)与 Web 服务器(HTTP 服务器)交换 HTTP 消息。 关闭 TCP 连接。 #HTTP 协议优点 扩展性强、速度快、跨平台支持性好。\n\n#HTTPS 协议 #HTTPS 协议介绍 HTTPS 协议(Hyper Text Transfer Protocol Secure),是 HTTP 的加强安全版本。HTTPS 是基于 HTTP 的,也是用 TCP 作为底层协议,并额外使用 SSL/TLS 协议用作加密和安全认证。默认端口号是 443.\n\nHTTPS 协议中,SSL 通道通常使用基于密钥的加密算法,密钥长度通常是 40 比特或 128 比特。\n\n#HTTPS 协议优点 保密性好、信任度高。\n\n#总结 端口号 :HTTP 默认是 80,HTTPS 默认是 443。 URL 前缀 :HTTP 的 URL 前缀是 http://,HTTPS 的 URL 前缀是 https://。 安全性和资源消耗 : HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS 是运行在 SSL/TLS 之上的 HTTP 协议,SSL/TLS 运行在 TCP 之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源。", - "normalizedContent": "http 协议介绍 http 协议,全称超文本传输协议(hypertext transfer protocol)。顾名思义,http 协议就是用来规范超文本的传输,超文本,也就是网络上的包括文本在内的各式各样的消息,具体来说,主要是来规范浏览器和服务器端的行为的。\n\n并且,http 是一个无状态(stateless)协议,也就是说服务器不维护任何有关客户端过去所发请求的消息。这其实是一种懒政,有状态协议会更加复杂,需要维护状态(历史信息),而且如果客户或服务器失效,会产生状态的不一致,解决这种不一致的代价更高。\n\n#http 协议通信过程 http 是应用层协议,它以 tcp(传输层)作为底层协议,默认端口为 80. 通信过程主要如下:\n\n服务器在 80 端口等待客户的请求。 浏览器发起到服务器的 tcp 连接(创建套接字 socket)。 服务器接收来自浏览器的 tcp 连接。 浏览器(http 客户端)与 web 服务器(http 服务器)交换 http 消息。 关闭 tcp 连接。 #http 协议优点 扩展性强、速度快、跨平台支持性好。\n\n#https 协议 #https 协议介绍 https 协议(hyper text transfer protocol secure),是 http 的加强安全版本。https 是基于 http 的,也是用 tcp 作为底层协议,并额外使用 ssl/tls 协议用作加密和安全认证。默认端口号是 443.\n\nhttps 协议中,ssl 通道通常使用基于密钥的加密算法,密钥长度通常是 40 比特或 128 比特。\n\n#https 协议优点 保密性好、信任度高。\n\n#总结 端口号 :http 默认是 80,https 默认是 443。 url 前缀 :http 的 url 前缀是 http://,https 的 url 前缀是 https://。 安全性和资源消耗 : http 协议运行在 tcp 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。https 是运行在 ssl/tls 之上的 http 协议,ssl/tls 运行在 tcp 之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,http 安全性没有 https 高,但是 https 比 http 耗费更多服务器资源。", - "charsets": { - "cjk": true - } + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { - "title": "TCP 长连接和短连接有那么不同的使用场景?", - "frontmatter": { - "title": "TCP 长连接和短连接有那么不同的使用场景?", - "date": "2022-09-21T22:36:19.000Z", - "permalink": "/pages/dda19e/", + "title": "HTTP 与 HTTPS 有哪些区别?", + "frontmatter": { + "title": "HTTP 与 HTTPS 有哪些区别?", + "date": "2022-09-21T22:36:56.000Z", + "permalink": "/pages/253334/", "categories": [ "计算机基础", "计算机网络" @@ -4462,69 +4539,69 @@ export const siteData = { null ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/100.TCP%20%E9%95%BF%E8%BF%9E%E6%8E%A5%E5%92%8C%E7%9F%AD%E8%BF%9E%E6%8E%A5%E6%9C%89%E9%82%A3%E4%B9%88%E4%B8%8D%E5%90%8C%E7%9A%84%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF%EF%BC%9F.html", - "relativePath": "02.计算机基础/10.计算机网络/100.TCP 长连接和短连接有那么不同的使用场景?.md", - "key": "v-f886ba80", - "path": "/pages/dda19e/", + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/120.HTTP%20%E4%B8%8E%20HTTPS%20%E6%9C%89%E5%93%AA%E4%BA%9B%E5%8C%BA%E5%88%AB%EF%BC%9F.html", + "relativePath": "02.计算机基础/10.计算机网络/120.HTTP 与 HTTPS 有哪些区别?.md", + "key": "v-9aa2c788", + "path": "/pages/253334/", "headers": [ { "level": 2, - "title": "前提:", - "slug": "前提", - "normalizedTitle": "前提:", + "title": "HTTP 协议介绍", + "slug": "http-协议介绍", + "normalizedTitle": "http 协议介绍", "charIndex": 2 }, { "level": 2, - "title": "定义:", - "slug": "定义", - "normalizedTitle": "定义:", - "charIndex": 125 + "title": "HTTP 协议通信过程", + "slug": "http-协议通信过程", + "normalizedTitle": "http 协议通信过程", + "charIndex": 266 + }, + { + "level": 2, + "title": "HTTP 协议优点", + "slug": "http-协议优点", + "normalizedTitle": "http 协议优点", + "charIndex": 481 + }, + { + "level": 2, + "title": "HTTPS 协议", + "slug": "https-协议", + "normalizedTitle": "https 协议", + "charIndex": 514 }, { "level": 3, - "title": "优缺点:", - "slug": "优缺点", - "normalizedTitle": "优缺点:", - "charIndex": 338 + "title": "HTTPS 协议介绍", + "slug": "https-协议介绍", + "normalizedTitle": "https 协议介绍", + "charIndex": 527 }, { "level": 3, - "title": "使用场景:", - "slug": "使用场景", - "normalizedTitle": "使用场景:", - "charIndex": 504 + "title": "HTTPS 协议优点", + "slug": "https-协议优点", + "normalizedTitle": "https 协议优点", + "charIndex": 728 + }, + { + "level": 2, + "title": "总结", + "slug": "总结", + "normalizedTitle": "总结", + "charIndex": 755 } ], - "headersStr": "前提: 定义: 优缺点: 使用场景:", - "content": "# 前提:\n\nHTTP/1.0默认使用短连接,HTTP/1.1开始默认使用长连接;\n\nHTTP协议的长连接和短连接,实质就是TCP协议的长连接和短连接;\n\nTCP协议建立连接需要3次握手,断开连接需要4次握手,这个过程会消耗网络资源和时间;\n\n\n# 定义:\n\n * 长连接:在一个TCP连接上可以发送多个数据包,但是如果没有数据包发送时,也要双方发检测包以维持这个长连接;三次握手后连接,不断开连接,保持客户端和服务端通信,直到服务器超时自动断开连接,或者客户端主动断开连接。\n * 短连接:当双方需要数据交互的时候,就建立一个TCP连接,本次交互完之后就断开这个连接;三次握手后建立连接,发送数据包并得到服务器返回的结果后,通过客户端和服务器的四次握手后断开连接。\n\n\n# 优缺点:\n\n长连接可以省去较多建立连接和断开连接的操作,所以比较节省资源和时间,但是长连接如果一直存在的话,需要很多探测包的发送来维持这个连接,这对服务器将是很大的负担; 相对而言,短连接不需要服务器承担太大负担,只要存在的连接就是有用的连接,但如果客户端请求频繁,就会在TCP的建立连接和断开连接上浪费较大的资源和时间。\n\n\n# 使用场景:\n\n短连接:适用于网页浏览等数据刷新频度较低的场景。一般而言像及京东,淘宝这些大型网站,随时都会有成千上万的用户请求,一般使用短连接,用户量太大,服务器扛不住那么多长连接; 长连接:适用于客户端和服务端通信频繁的场景,例如:聊天室,实时游戏等场景。即时通讯(QQ)般使用的都是长连接,但并不是永久连接(比如20分钟,半个小时),因为即时通讯是频繁的发送请求,使用长连接只需要建立一次连接,同时再根据业务设置保持时间,超过这个时间就会断开连接,一定程度上保证了服务器的压力不会过大。\n\n##Socket心跳包机制: 像心跳一样,每隔固定时间向服务器发送一个包,以此来告诉服务器,这个客户端还活着。为了保持长连接,一般都是很小的包(节约流量)或者只有包头的空包。 1.客户端每隔一段时间间隔就发送一个探测包给服务器; 2.客户端发包时启动一个超时定时器; 3.服务端接收到探测包后会回应一个包; 4.如果客户端收到服务器的应答包,则说明服务器正常,删除超时定时器;如果没有收到则服务器异常。 ———————————————— 版权声明:本文为CSDN博主「测试小明」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/Dave0002/article/details/125973214", - "normalizedContent": "# 前提:\n\nhttp/1.0默认使用短连接,http/1.1开始默认使用长连接;\n\nhttp协议的长连接和短连接,实质就是tcp协议的长连接和短连接;\n\ntcp协议建立连接需要3次握手,断开连接需要4次握手,这个过程会消耗网络资源和时间;\n\n\n# 定义:\n\n * 长连接:在一个tcp连接上可以发送多个数据包,但是如果没有数据包发送时,也要双方发检测包以维持这个长连接;三次握手后连接,不断开连接,保持客户端和服务端通信,直到服务器超时自动断开连接,或者客户端主动断开连接。\n * 短连接:当双方需要数据交互的时候,就建立一个tcp连接,本次交互完之后就断开这个连接;三次握手后建立连接,发送数据包并得到服务器返回的结果后,通过客户端和服务器的四次握手后断开连接。\n\n\n# 优缺点:\n\n长连接可以省去较多建立连接和断开连接的操作,所以比较节省资源和时间,但是长连接如果一直存在的话,需要很多探测包的发送来维持这个连接,这对服务器将是很大的负担; 相对而言,短连接不需要服务器承担太大负担,只要存在的连接就是有用的连接,但如果客户端请求频繁,就会在tcp的建立连接和断开连接上浪费较大的资源和时间。\n\n\n# 使用场景:\n\n短连接:适用于网页浏览等数据刷新频度较低的场景。一般而言像及京东,淘宝这些大型网站,随时都会有成千上万的用户请求,一般使用短连接,用户量太大,服务器扛不住那么多长连接; 长连接:适用于客户端和服务端通信频繁的场景,例如:聊天室,实时游戏等场景。即时通讯(qq)般使用的都是长连接,但并不是永久连接(比如20分钟,半个小时),因为即时通讯是频繁的发送请求,使用长连接只需要建立一次连接,同时再根据业务设置保持时间,超过这个时间就会断开连接,一定程度上保证了服务器的压力不会过大。\n\n##socket心跳包机制: 像心跳一样,每隔固定时间向服务器发送一个包,以此来告诉服务器,这个客户端还活着。为了保持长连接,一般都是很小的包(节约流量)或者只有包头的空包。 1.客户端每隔一段时间间隔就发送一个探测包给服务器; 2.客户端发包时启动一个超时定时器; 3.服务端接收到探测包后会回应一个包; 4.如果客户端收到服务器的应答包,则说明服务器正常,删除超时定时器;如果没有收到则服务器异常。 ———————————————— 版权声明:本文为csdn博主「测试小明」的原创文章,遵循cc 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/dave0002/article/details/125973214", + "headersStr": "HTTP 协议介绍 HTTP 协议通信过程 HTTP 协议优点 HTTPS 协议 HTTPS 协议介绍 HTTPS 协议优点 总结", + "content": "# HTTP 协议介绍\n\nHTTP 协议,全称超文本传输协议(Hypertext Transfer Protocol)。顾名思义,HTTP 协议就是用来规范超文本的传输,超文本,也就是网络上的包括文本在内的各式各样的消息,具体来说,主要是来规范浏览器和服务器端的行为的。\n\n并且,HTTP 是一个无状态(stateless)协议,也就是说服务器不维护任何有关客户端过去所发请求的消息。这其实是一种懒政,有状态协议会更加复杂,需要维护状态(历史信息),而且如果客户或服务器失效,会产生状态的不一致,解决这种不一致的代价更高。\n\n\n# HTTP 协议通信过程\n\nHTTP 是应用层协议,它以 TCP(传输层)作为底层协议,默认端口为 80. 通信过程主要如下:\n\n 1. 服务器在 80 端口等待客户的请求。\n 2. 浏览器发起到服务器的 TCP 连接(创建套接字 Socket)。\n 3. 服务器接收来自浏览器的 TCP 连接。\n 4. 浏览器(HTTP 客户端)与 Web 服务器(HTTP 服务器)交换 HTTP 消息。\n 5. 关闭 TCP 连接。\n\n\n# HTTP 协议优点\n\n扩展性强、速度快、跨平台支持性好。\n\n\n# HTTPS 协议\n\n\n# HTTPS 协议介绍\n\nHTTPS 协议(Hyper Text Transfer Protocol Secure),是 HTTP 的加强安全版本。HTTPS 是基于 HTTP 的,也是用 TCP 作为底层协议,并额外使用 SSL/TLS 协议用作加密和安全认证。默认端口号是 443.\n\nHTTPS 协议中,SSL 通道通常使用基于密钥的加密算法,密钥长度通常是 40 比特或 128 比特。\n\n\n# HTTPS 协议优点\n\n保密性好、信任度高。\n\n\n# 总结\n\n端口号 :HTTP 默认是 80,HTTPS 默认是 443。 URL 前缀 :HTTP 的 URL 前缀是 http://,HTTPS 的 URL 前缀是 https://。 安全性和资源消耗 : HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS 是运行在 SSL/TLS 之上的 HTTP 协议,SSL/TLS 运行在 TCP 之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源。", + "normalizedContent": "# http 协议介绍\n\nhttp 协议,全称超文本传输协议(hypertext transfer protocol)。顾名思义,http 协议就是用来规范超文本的传输,超文本,也就是网络上的包括文本在内的各式各样的消息,具体来说,主要是来规范浏览器和服务器端的行为的。\n\n并且,http 是一个无状态(stateless)协议,也就是说服务器不维护任何有关客户端过去所发请求的消息。这其实是一种懒政,有状态协议会更加复杂,需要维护状态(历史信息),而且如果客户或服务器失效,会产生状态的不一致,解决这种不一致的代价更高。\n\n\n# http 协议通信过程\n\nhttp 是应用层协议,它以 tcp(传输层)作为底层协议,默认端口为 80. 通信过程主要如下:\n\n 1. 服务器在 80 端口等待客户的请求。\n 2. 浏览器发起到服务器的 tcp 连接(创建套接字 socket)。\n 3. 服务器接收来自浏览器的 tcp 连接。\n 4. 浏览器(http 客户端)与 web 服务器(http 服务器)交换 http 消息。\n 5. 关闭 tcp 连接。\n\n\n# http 协议优点\n\n扩展性强、速度快、跨平台支持性好。\n\n\n# https 协议\n\n\n# https 协议介绍\n\nhttps 协议(hyper text transfer protocol secure),是 http 的加强安全版本。https 是基于 http 的,也是用 tcp 作为底层协议,并额外使用 ssl/tls 协议用作加密和安全认证。默认端口号是 443.\n\nhttps 协议中,ssl 通道通常使用基于密钥的加密算法,密钥长度通常是 40 比特或 128 比特。\n\n\n# https 协议优点\n\n保密性好、信任度高。\n\n\n# 总结\n\n端口号 :http 默认是 80,https 默认是 443。 url 前缀 :http 的 url 前缀是 http://,https 的 url 前缀是 https://。 安全性和资源消耗 : http 协议运行在 tcp 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。https 是运行在 ssl/tls 之上的 http 协议,ssl/tls 运行在 tcp 之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,http 安全性没有 https 高,但是 https 比 http 耗费更多服务器资源。", "charsets": { "cjk": true - } - }, - { - "title": "TCP 中常见的拥塞控制算法有哪些?", - "frontmatter": { - "title": "TCP 中常见的拥塞控制算法有哪些?", - "date": "2022-09-21T22:37:12.000Z", - "permalink": "/pages/9ccc31/", - "categories": [ - "计算机基础", - "计算机网络" - ], - "tags": [ - null - ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/130.TCP%20%E4%B8%AD%E5%B8%B8%E8%A7%81%E7%9A%84%E6%8B%A5%E5%A1%9E%E6%8E%A7%E5%88%B6%E7%AE%97%E6%B3%95%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F.html", - "relativePath": "02.计算机基础/10.计算机网络/130.TCP 中常见的拥塞控制算法有哪些?.md", - "key": "v-75f6731c", - "path": "/pages/9ccc31/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {} + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "简述 HTTP 1.0,1.1,2.0 的主要区别", @@ -4621,7 +4698,36 @@ export const siteData = { "normalizedContent": "# 1 http1.0和http1.1的区别\n\n\n# 1.1 长连接(persistent connection)\n\n http1.1支持长连接和请求的流水线处理,在一个tcp连接上可以传送多个http请求和响应,减少了建立和关闭连接的消耗和延迟,在http1.1中默认开启长连接keep-alive,一定程度上弥补了http1.0每次请求都要创建连接的缺点。http1.0需要使用keep-alive参数来告知服务器端要建立一个长连接。\n\n\n\n# 1.2 节约带宽\n\n http1.0中存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能。http1.1支持只发送header信息(不带任何body信息),如果服务器认为客户端有权限请求服务器,则返回100,客户端接收到100才开始把请求body发送到服务器;如果返回401,客户端就可以不用发送请求body了节约了带宽。\n\n\n\n# 1.3 host域\n\n 在http1.0中认为每台服务器都绑定一个唯一的ip地址,因此,请求消息中的url并没有传递主机名(hostname),http1.0没有host域。随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(multi-homed web servers),并且它们共享一个ip地址。http1.1的请求消息和响应消息都支持host域,且请求消息中如果没有host域会报告一个错误(400 bad request)。\n\n\n\n# 1.4缓存处理\n\n 在http1.0中主要使用header里的if-modified-since,expires来做为缓存判断的标准,http1.1则引入了更多的缓存控制策略例如entity tag,if-unmodified-since, if-match, if-none-match等更多可供选择的缓存头来控制缓存策略。\n\n\n\n# 1.5错误通知的管理\n\n 在http1.1中新增了24个错误状态响应码,如409(conflict)表示请求的资源与资源的当前状态发生冲突;410(gone)表示服务器上的某个资源被永久性的删除。\n\n\n\n# 2 http1.1和http2.0的区别\n\n\n# 2.1 多路复用\n\n http2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比http1.1大了好几个数量级。http1.1也可以多建立几个tcp连接,来支持处理更多并发的请求,但是创建tcp连接本身也是有开销的。\n\n\n\n# 2.2 头部数据压缩\n\n 在http1.1中,http请求和响应都是由状态行、请求/响应头部、消息主体三部分组成。一般而言,消息主体都会经过gzip压缩,或者本身传输的就是压缩过后的二进制文件,但状态行和头部却没有经过任何压缩,直接以纯文本传输。随着web功能越来越复杂,每个页面产生的请求数也越来越多,导致消耗在头部的流量越来越多,尤其是每次都要传输useragent、cookie这类不会频繁变动的内容,完全是一种浪费。\n\n http1.1不支持header数据的压缩,http2.0使用hpack算法对header的数据进行压缩,这样数据体积小了,在网络上传输就会更快。\n\n\n\n# 2.3 服务器推送\n\n 服务端推送是一种在客户端请求之前发送数据的机制。网页使用了许多资源:html、样式表、脚本、图片等等。在http1.1中这些资源每一个都必须明确地请求。这是一个很慢的过程。浏览器从获取html开始,然后在它解析和评估页面的时候,增量地获取更多的资源。因为服务器必须等待浏览器做每一个请求,网络经常是空闲的和未充分使用的。\n\n 为了改善延迟,http2.0引入了server push,它允许服务端推送资源给浏览器,在浏览器明确地请求之前,免得客户端再次创建连接发送请求到服务器端获取。这样客户端可以直接从本地加载这些资源,不用再通过网络。\n\n\n参考: http1.0、http1.1 和 http2.0 的区别", "charsets": { "cjk": true - } + }, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 + }, + { + "title": "从系统层面上,UDP如何保证尽量可靠?", + "frontmatter": { + "title": "从系统层面上,UDP如何保证尽量可靠?", + "date": "2022-09-21T22:37:36.000Z", + "permalink": "/pages/e57cfe/", + "categories": [ + "计算机基础", + "计算机网络" + ], + "tags": [ + null + ] + }, + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/150.%E4%BB%8E%E7%B3%BB%E7%BB%9F%E5%B1%82%E9%9D%A2%E4%B8%8A%EF%BC%8CUDP%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81%E5%B0%BD%E9%87%8F%E5%8F%AF%E9%9D%A0%EF%BC%9F.html", + "relativePath": "02.计算机基础/10.计算机网络/150.从系统层面上,UDP如何保证尽量可靠?.md", + "key": "v-63453344", + "path": "/pages/e57cfe/", + "headersStr": null, + "content": "基本的思想:模拟tcp的可靠机制来实现,保证四个无即可(无丢失、无失序、无错误、无重复) 可以从以下几点着手:\n\n * ①加上序号\n\n * ②确认机制,超时重传\n\n * ③校验机制,出错重传\n\n我们都知道UDP是面向无连接,不可靠的传输方式,而TCP虽然是面向连接,可靠地连接,但是其过程太繁琐,效率太低,传输速度很慢,所以我们可以利用UDP进行底层传输数据,上层进行封装,来弥补UDP的不足。 做到以下4点,就可以保证UDP协议可靠:\n\n * ①不要TCP的三次握手和四次挥手。\n\n * ②发送方每次发出的数据进行编号,同时保持顺序的正确。\n\n * ③每次接收方接收到数据,发出应答信号。同时发送方在规定的时间检测是否接收到应答,如果没有接收到应答,重发,三次后还未收到应答直接判断发送失败。\n\n * ④发送数据时,发送方增加校验位。如果接收方校验出错,请求重发。", + "normalizedContent": "基本的思想:模拟tcp的可靠机制来实现,保证四个无即可(无丢失、无失序、无错误、无重复) 可以从以下几点着手:\n\n * ①加上序号\n\n * ②确认机制,超时重传\n\n * ③校验机制,出错重传\n\n我们都知道udp是面向无连接,不可靠的传输方式,而tcp虽然是面向连接,可靠地连接,但是其过程太繁琐,效率太低,传输速度很慢,所以我们可以利用udp进行底层传输数据,上层进行封装,来弥补udp的不足。 做到以下4点,就可以保证udp协议可靠:\n\n * ①不要tcp的三次握手和四次挥手。\n\n * ②发送方每次发出的数据进行编号,同时保持顺序的正确。\n\n * ③每次接收方接收到数据,发出应答信号。同时发送方在规定的时间检测是否接收到应答,如果没有接收到应答,重发,三次后还未收到应答直接判断发送失败。\n\n * ④发送数据时,发送方增加校验位。如果接收方校验出错,请求重发。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "TCP 挥手时出现大量 CLOSEWAIT 或 TIMEWAIT 怎么解决?", @@ -4641,17 +4747,79 @@ export const siteData = { "relativePath": "02.计算机基础/10.计算机网络/160.TCP 挥手时出现大量 CLOSE_WAIT 或 TIME_WAIT 怎么解决?.md", "key": "v-93120d3a", "path": "/pages/15d37a/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {} + "headers": [ + { + "level": 2, + "title": "由于socket是全双工的工作模式,一个socket的关闭,是需要四次握手来完成的。", + "slug": "由于socket是全双工的工作模式-一个socket的关闭-是需要四次握手来完成的。", + "normalizedTitle": "由于socket是全双工的工作模式,一个socket的关闭,是需要四次握手来完成的。", + "charIndex": 2 + }, + { + "level": 2, + "title": "time_wait 产生原因(等待2MSL意义):", + "slug": "time-wait-产生原因-等待2msl意义", + "normalizedTitle": "time_wait 产生原因(等待2msl意义):", + "charIndex": 404 + }, + { + "level": 2, + "title": "time_wait过多产生原因:", + "slug": "time-wait过多产生原因", + "normalizedTitle": "time_wait过多产生原因:", + "charIndex": 762 + }, + { + "level": 2, + "title": "time_wait过多解决方法:", + "slug": "time-wait过多解决方法", + "normalizedTitle": "time_wait过多解决方法:", + "charIndex": 1178 + }, + { + "level": 2, + "title": "短连接", + "slug": "短连接", + "normalizedTitle": "短连接", + "charIndex": 882 + }, + { + "level": 2, + "title": "长连接", + "slug": "长连接", + "normalizedTitle": "长连接", + "charIndex": 1202 + }, + { + "level": 2, + "title": "close_wait产生原因:", + "slug": "close-wait产生原因", + "normalizedTitle": "close_wait产生原因:", + "charIndex": 2732 + }, + { + "level": 2, + "title": "close_wait太多解决方法:", + "slug": "close-wait太多解决方法", + "normalizedTitle": "close_wait太多解决方法:", + "charIndex": 3126 + } + ], + "headersStr": "由于socket是全双工的工作模式,一个socket的关闭,是需要四次握手来完成的。 time_wait 产生原因(等待2MSL意义): time_wait过多产生原因: time_wait过多解决方法: 短连接 长连接 close_wait产生原因: close_wait太多解决方法:", + "content": "# 由于socket是全双工的工作模式,一个socket的关闭,是需要四次握手来完成的。\n\n主动关闭连接的一方,调用close();协议层发送FIN包\n\n被动关闭的一方收到FIN包后,协议层回复ACK;然后被动关闭的一方,进入CLOSE_WAIT状态,主动关闭的一方等待对方关闭,则进入FIN_WAIT_2状态;此时,主动关闭的一方 等待 被动关闭一方的应用程序,调用close操作。\n\n被动关闭的一方在完成所有数据发送后,调用close()操作;此时,协议层发送FIN包给主动关闭的一方,等待对方的ACK,被动关闭的一方进入LAST_ACK状态;\n\n主动关闭的一方收到FIN包,协议层回复ACK;此时,主动关闭连接的一方,进入TIME_WAIT状态;而被动关闭的一方,进入CLOSED状态\n\n等待2MSL时间,如没发现重传的FIN,主动关闭的一方,结束TIME_WAIT,进入CLOSED状态\n\n\n\n\n# time_wait 产生原因(等待2MSL意义):\n\n假设是客户端向服务端发起断开连接\n\n为了保证客户端发送的最后一个ACK报文段能够到达服务器。因为这个ACK有可能丢失,从而导致处在LAST-ACK状态的服务器收不到对FIN-ACK的确认报文。服务器会超时重传这个FIN-ACK,接着客户端再重传一次确认,重新启动时间等待计时器。最后客户端和服务器都能正常的关闭。假设客户端不等待2MSL,而是在发送完ACK之后直接释放关闭,一但这个ACK丢失的话,服务器就无法正常的进入关闭连接状态。\n\n防止“已失效的连接请求报文段”出现在本连接中。\n\n客户端在发送完最后一个ACK报文段后,再经过2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段。\n\n\n# time_wait过多产生原因:\n\n正常的TCP客户端连接在关闭后,会进入一个TIME_WAIT的状态,持续的时间一般在1-4分钟,对于连接数不高的场景,1-4分钟其实并不长,对系统也不会有什么影响, 但如果短时间内(例如1s内)进行大量的短连接,则可能出现这样一种情况:客户端所在的操作系统的socket端口和文件描述符被用尽,系统无法再发起新的连接!\n\n举例来说:   假设每秒建立了1000个短连接(Web场景下是很常见的,例如每个请求都去访问memcached),假设TIME_WAIT的时间是1分钟,则1分钟内需要建立6W个短连接,由于TIME_WAIT时间是1分钟,这些短连接1分钟内都处于TIME_WAIT状态,都不会释放,而Linux默认的本地端口范围配置是:net.ipv4.ip_local_port_range = 32768 61000不到3W,因此这种情况下新的请求由于没有本地端口就不能建立了。\n\n\n# time_wait过多解决方法:\n\n1.可以改为长连接,但代价较大,长连接太多会导致服务器性能问题并且安全性也较差; 2.修改ipv4.ip_local_port_range,增大可用端口范围,但只能缓解问题,不能根本解决问题;\n\n3.客户端机器打开tcp_tw_reuse和tcp_timestamps选项;\n\n字面意思,reuse TIME_WAIT状态的连接。时刻记住一条socket连接,就是那个五元组,出现TIME_WAIT状态的连接,一定出现在主动关闭连接的一方。所以,当主动关闭连接的一方,再次向对方发起连接请求的时候(例如,客户端关闭连接,客户端再次连接服务端,此时可以复用了;负载均衡服务器,主动关闭后端的连接,当有新的HTTP请求,负载均衡服务器再次连接后端服务器,此时也可以复用),可以复用TIME_WAIT状态的连接。\n\n通过字面解释,以及例子说明,你看到了,tcp_tw_reuse应用的场景:某一方,需要不断的通过“短连接”连接其他服务器,总是自己先关闭连接(TIME_WAIT在自己这方),关闭后又不断的重新连接对方。\n\n那么,当连接被复用了之后,延迟或者重发的数据包到达,新的连接怎么判断,到达的数据是属于复用后的连接,还是复用前的连接呢?那就需要依赖前面提到的两个时间字段了。复用连接后,这条连接的时间被更新为当前的时间,当延迟的数据达到,延迟数据的时间是小于新连接的时间,所以,内核可以通过时间判断出,延迟的数据可以安全的丢弃掉了。\n\n这个配置,依赖于连接双方,同时对timestamps的支持。同时,这个配置,仅仅影响outbound连接,即做为客户端的角色,连接服务端[connect(dest_ip, dest_port)]时复用TIME_WAIT的socket。\n\n4.客户端机器打开tcp_tw_recycle和tcp_timestamps选项;\n\n字面意思,销毁掉 TIME_WAIT。\n\n当开启了这个配置后,内核会快速的回收处于TIME_WAIT状态的socket连接。多快?不再是2MSL,而是一个RTO(retransmission timeout,数据包重传的timeout时间)的时间,这个时间根据RTT动态计算出来,但是远小于2MSL。\n\n有了这个配置,还是需要保障 丢失重传或者延迟的数据包,不会被新的连接(注意,这里不再是复用了,而是之前处于TIME_WAIT状态的连接已经被destroy掉了,新的连接,刚好是和某一个被destroy掉的连接使用了相同的五元组而已)所错误的接收。在启用该配置,当一个socket连接进入TIME_WAIT状态后,内核里会记录包括该socket连接对应的五元组中的对方IP等在内的一些统计数据,当然也包括从该对方IP所接收到的最近的一次数据包时间。当有新的数据包到达,只要时间晚于内核记录的这个时间,数据包都会被统统的丢掉。\n\n\n这个配置,依赖于连接双方对timestamps的支持。同时,这个配置,主要影响到了inbound的连接(对outbound的连接也有影响,但是不是复用),即做为服务端角色,客户端连进来,服务端主动关闭了连接,TIME_WAIT状态的socket处于服务端,服务端快速的回收该状态的连接。\n\n\n# 短连接\n\n连接->传输数据->关闭连接 HTTP是无状态的,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。 也可以这样说:短连接是指SOCKET连接后发送后接收完数据后马上断开连接。\n\n\n# 长连接\n\n连接->传输数据->保持连接 -> 传输数据-> 。。。 ->关闭连接。 长连接指建立SOCKET连接后不管是否使用都保持连接,但安全性较差。\n\n\n# close_wait产生原因:\n\n比如是客户端要与服务端断开连接,先发一个FIN表示自己要主动断开连接了,服务端会先回一个ACK,这时表示客户端没数据要发了,但有可能服务端数据还没发完,所以要经历一个close_wait,等待服务端数据发送完,再回一个FIN和ACK。\n\nclose_wait产生太多原因:\n\nclose_wait 按照正常操作的话应该很短暂的一个状态,接收到客户端的fin包并且回复客户端ack之后,会继续发送FIN包告知客户端关闭关闭连接,之后迁移到Last_ACK状态。但是close_wait过多只能说明没有迁移到Last_ACK,也就是服务端是否发送FIN包,只有发送FIN包才会发生迁移,所以问题定位在是否发送FIN包。FIN包的底层实现其实就是调用socket的close方法,这里的问题出在没有执行close方法。说明服务端socket忙于读写。\n\n\n# close_wait太多解决方法:\n\n代码层面做到 第一:使用完socket就调用close方法; 第二:socket读控制,当读取的长度为0时(读到结尾),立即close; 第三:如果read返回-1,出现错误,检查error返回码,有三种情况:INTR(被中断,可以继续读取),WOULDBLOCK(表示当前socket_fd文件描述符是非阻塞的,但是现在被阻塞了),AGAIN(表示现在没有数据稍后重新读取)。如果不是AGAIN,立即close 可以设置TCP的连接时长keep_alive_time还有tcp监控连接的频率以及连接没有活动多长时间被迫断开连接 ————————————————\n\n参考TCP通信过程中time_wait和close_wait产生过多的原因和解决方法", + "normalizedContent": "# 由于socket是全双工的工作模式,一个socket的关闭,是需要四次握手来完成的。\n\n主动关闭连接的一方,调用close();协议层发送fin包\n\n被动关闭的一方收到fin包后,协议层回复ack;然后被动关闭的一方,进入close_wait状态,主动关闭的一方等待对方关闭,则进入fin_wait_2状态;此时,主动关闭的一方 等待 被动关闭一方的应用程序,调用close操作。\n\n被动关闭的一方在完成所有数据发送后,调用close()操作;此时,协议层发送fin包给主动关闭的一方,等待对方的ack,被动关闭的一方进入last_ack状态;\n\n主动关闭的一方收到fin包,协议层回复ack;此时,主动关闭连接的一方,进入time_wait状态;而被动关闭的一方,进入closed状态\n\n等待2msl时间,如没发现重传的fin,主动关闭的一方,结束time_wait,进入closed状态\n\n\n\n\n# time_wait 产生原因(等待2msl意义):\n\n假设是客户端向服务端发起断开连接\n\n为了保证客户端发送的最后一个ack报文段能够到达服务器。因为这个ack有可能丢失,从而导致处在last-ack状态的服务器收不到对fin-ack的确认报文。服务器会超时重传这个fin-ack,接着客户端再重传一次确认,重新启动时间等待计时器。最后客户端和服务器都能正常的关闭。假设客户端不等待2msl,而是在发送完ack之后直接释放关闭,一但这个ack丢失的话,服务器就无法正常的进入关闭连接状态。\n\n防止“已失效的连接请求报文段”出现在本连接中。\n\n客户端在发送完最后一个ack报文段后,再经过2msl,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段。\n\n\n# time_wait过多产生原因:\n\n正常的tcp客户端连接在关闭后,会进入一个time_wait的状态,持续的时间一般在1-4分钟,对于连接数不高的场景,1-4分钟其实并不长,对系统也不会有什么影响, 但如果短时间内(例如1s内)进行大量的短连接,则可能出现这样一种情况:客户端所在的操作系统的socket端口和文件描述符被用尽,系统无法再发起新的连接!\n\n举例来说:   假设每秒建立了1000个短连接(web场景下是很常见的,例如每个请求都去访问memcached),假设time_wait的时间是1分钟,则1分钟内需要建立6w个短连接,由于time_wait时间是1分钟,这些短连接1分钟内都处于time_wait状态,都不会释放,而linux默认的本地端口范围配置是:net.ipv4.ip_local_port_range = 32768 61000不到3w,因此这种情况下新的请求由于没有本地端口就不能建立了。\n\n\n# time_wait过多解决方法:\n\n1.可以改为长连接,但代价较大,长连接太多会导致服务器性能问题并且安全性也较差; 2.修改ipv4.ip_local_port_range,增大可用端口范围,但只能缓解问题,不能根本解决问题;\n\n3.客户端机器打开tcp_tw_reuse和tcp_timestamps选项;\n\n字面意思,reuse time_wait状态的连接。时刻记住一条socket连接,就是那个五元组,出现time_wait状态的连接,一定出现在主动关闭连接的一方。所以,当主动关闭连接的一方,再次向对方发起连接请求的时候(例如,客户端关闭连接,客户端再次连接服务端,此时可以复用了;负载均衡服务器,主动关闭后端的连接,当有新的http请求,负载均衡服务器再次连接后端服务器,此时也可以复用),可以复用time_wait状态的连接。\n\n通过字面解释,以及例子说明,你看到了,tcp_tw_reuse应用的场景:某一方,需要不断的通过“短连接”连接其他服务器,总是自己先关闭连接(time_wait在自己这方),关闭后又不断的重新连接对方。\n\n那么,当连接被复用了之后,延迟或者重发的数据包到达,新的连接怎么判断,到达的数据是属于复用后的连接,还是复用前的连接呢?那就需要依赖前面提到的两个时间字段了。复用连接后,这条连接的时间被更新为当前的时间,当延迟的数据达到,延迟数据的时间是小于新连接的时间,所以,内核可以通过时间判断出,延迟的数据可以安全的丢弃掉了。\n\n这个配置,依赖于连接双方,同时对timestamps的支持。同时,这个配置,仅仅影响outbound连接,即做为客户端的角色,连接服务端[connect(dest_ip, dest_port)]时复用time_wait的socket。\n\n4.客户端机器打开tcp_tw_recycle和tcp_timestamps选项;\n\n字面意思,销毁掉 time_wait。\n\n当开启了这个配置后,内核会快速的回收处于time_wait状态的socket连接。多快?不再是2msl,而是一个rto(retransmission timeout,数据包重传的timeout时间)的时间,这个时间根据rtt动态计算出来,但是远小于2msl。\n\n有了这个配置,还是需要保障 丢失重传或者延迟的数据包,不会被新的连接(注意,这里不再是复用了,而是之前处于time_wait状态的连接已经被destroy掉了,新的连接,刚好是和某一个被destroy掉的连接使用了相同的五元组而已)所错误的接收。在启用该配置,当一个socket连接进入time_wait状态后,内核里会记录包括该socket连接对应的五元组中的对方ip等在内的一些统计数据,当然也包括从该对方ip所接收到的最近的一次数据包时间。当有新的数据包到达,只要时间晚于内核记录的这个时间,数据包都会被统统的丢掉。\n\n\n这个配置,依赖于连接双方对timestamps的支持。同时,这个配置,主要影响到了inbound的连接(对outbound的连接也有影响,但是不是复用),即做为服务端角色,客户端连进来,服务端主动关闭了连接,time_wait状态的socket处于服务端,服务端快速的回收该状态的连接。\n\n\n# 短连接\n\n连接->传输数据->关闭连接 http是无状态的,浏览器和服务器每进行一次http操作,就建立一次连接,但任务结束就中断连接。 也可以这样说:短连接是指socket连接后发送后接收完数据后马上断开连接。\n\n\n# 长连接\n\n连接->传输数据->保持连接 -> 传输数据-> 。。。 ->关闭连接。 长连接指建立socket连接后不管是否使用都保持连接,但安全性较差。\n\n\n# close_wait产生原因:\n\n比如是客户端要与服务端断开连接,先发一个fin表示自己要主动断开连接了,服务端会先回一个ack,这时表示客户端没数据要发了,但有可能服务端数据还没发完,所以要经历一个close_wait,等待服务端数据发送完,再回一个fin和ack。\n\nclose_wait产生太多原因:\n\nclose_wait 按照正常操作的话应该很短暂的一个状态,接收到客户端的fin包并且回复客户端ack之后,会继续发送fin包告知客户端关闭关闭连接,之后迁移到last_ack状态。但是close_wait过多只能说明没有迁移到last_ack,也就是服务端是否发送fin包,只有发送fin包才会发生迁移,所以问题定位在是否发送fin包。fin包的底层实现其实就是调用socket的close方法,这里的问题出在没有执行close方法。说明服务端socket忙于读写。\n\n\n# close_wait太多解决方法:\n\n代码层面做到 第一:使用完socket就调用close方法; 第二:socket读控制,当读取的长度为0时(读到结尾),立即close; 第三:如果read返回-1,出现错误,检查error返回码,有三种情况:intr(被中断,可以继续读取),wouldblock(表示当前socket_fd文件描述符是非阻塞的,但是现在被阻塞了),again(表示现在没有数据稍后重新读取)。如果不是again,立即close 可以设置tcp的连接时长keep_alive_time还有tcp监控连接的频率以及连接没有活动多长时间被迫断开连接 ————————————————\n\n参考tcp通信过程中time_wait和close_wait产生过多的原因和解决方法", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { - "title": "从系统层面上,UDP如何保证尽量可靠?", + "title": "简述 TCP 滑动窗口", "frontmatter": { - "title": "从系统层面上,UDP如何保证尽量可靠?", - "date": "2022-09-21T22:37:36.000Z", - "permalink": "/pages/e57cfe/", + "title": "简述 TCP 滑动窗口", + "date": "2022-09-21T22:38:09.000Z", + "permalink": "/pages/dae451/", "categories": [ "计算机基础", "计算机网络" @@ -4660,23 +4828,74 @@ export const siteData = { null ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/150.%E4%BB%8E%E7%B3%BB%E7%BB%9F%E5%B1%82%E9%9D%A2%E4%B8%8A%EF%BC%8CUDP%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81%E5%B0%BD%E9%87%8F%E5%8F%AF%E9%9D%A0%EF%BC%9F.html", - "relativePath": "02.计算机基础/10.计算机网络/150.从系统层面上,UDP如何保证尽量可靠?.md", - "key": "v-63453344", - "path": "/pages/e57cfe/", - "headersStr": null, - "content": "基本的思想:模拟tcp的可靠机制来实现,保证四个无即可(无丢失、无失序、无错误、无重复) 可以从以下几点着手: ①加上序号 ②确认机制,超时重传 ③校验机制,出错重传\n\n我们都知道UDP是面向无连接,不可靠的传输方式,而TCP虽然是面向连接,可靠地连接,但是其过程太繁琐,效率太低,传输速度很慢,所以我们可以利用UDP进行底层传输数据,上层进行封装,来弥补UDP的不足。 做到以下4点,就可以保证UDP协议可靠: ①不要TCP的三次握手和四次挥手。 ②发送方每次发出的数据进行编号,同时保持顺序的正确。 ③每次接收方接收到数据,发出应答信号。同时发送方在规定的时间检测是否接收到应答,如果没有接收到应答,重发,三次后还未收到应答直接判断发送失败。 ④发送数据时,发送方增加校验位。如果接收方校验出错,请求重发。", - "normalizedContent": "基本的思想:模拟tcp的可靠机制来实现,保证四个无即可(无丢失、无失序、无错误、无重复) 可以从以下几点着手: ①加上序号 ②确认机制,超时重传 ③校验机制,出错重传\n\n我们都知道udp是面向无连接,不可靠的传输方式,而tcp虽然是面向连接,可靠地连接,但是其过程太繁琐,效率太低,传输速度很慢,所以我们可以利用udp进行底层传输数据,上层进行封装,来弥补udp的不足。 做到以下4点,就可以保证udp协议可靠: ①不要tcp的三次握手和四次挥手。 ②发送方每次发出的数据进行编号,同时保持顺序的正确。 ③每次接收方接收到数据,发出应答信号。同时发送方在规定的时间检测是否接收到应答,如果没有接收到应答,重发,三次后还未收到应答直接判断发送失败。 ④发送数据时,发送方增加校验位。如果接收方校验出错,请求重发。", + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/170.%E7%AE%80%E8%BF%B0%20TCP%20%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3.html", + "relativePath": "02.计算机基础/10.计算机网络/170.简述 TCP 滑动窗口.md", + "key": "v-3e902cd4", + "path": "/pages/dae451/", + "headers": [ + { + "level": 2, + "title": "滑动窗口解决什么问题?", + "slug": "滑动窗口解决什么问题", + "normalizedTitle": "滑动窗口解决什么问题?", + "charIndex": 2 + }, + { + "level": 2, + "title": "滑动窗口是什么?", + "slug": "滑动窗口是什么", + "normalizedTitle": "滑动窗口是什么?", + "charIndex": 221 + }, + { + "level": 2, + "title": "窗口大小由哪一方决定?", + "slug": "窗口大小由哪一方决定", + "normalizedTitle": "窗口大小由哪一方决定?", + "charIndex": 609 + }, + { + "level": 2, + "title": "程序是如何表示发送方的四个部分的呢?", + "slug": "程序是如何表示发送方的四个部分的呢", + "normalizedTitle": "程序是如何表示发送方的四个部分的呢?", + "charIndex": 1217 + }, + { + "level": 2, + "title": "接收方的滑动窗口", + "slug": "接收方的滑动窗口", + "normalizedTitle": "接收方的滑动窗口", + "charIndex": 1598 + }, + { + "level": 2, + "title": "接收窗口和发送窗口的大小是相等的吗?", + "slug": "接收窗口和发送窗口的大小是相等的吗", + "normalizedTitle": "接收窗口和发送窗口的大小是相等的吗?", + "charIndex": 1917 + }, + { + "level": 2, + "title": "参考:", + "slug": "参考", + "normalizedTitle": "参考:", + "charIndex": 2106 + } + ], + "headersStr": "滑动窗口解决什么问题? 滑动窗口是什么? 窗口大小由哪一方决定? 程序是如何表示发送方的四个部分的呢? 接收方的滑动窗口 接收窗口和发送窗口的大小是相等的吗? 参考:", + "content": "# 滑动窗口解决什么问题?\n\n我们都知道 TCP 是每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了, 再发送下一个。\n\n这个模式就有点像我和你面对面聊天,你一句我一句。但这种方式的缺点是效率比较低的。\n\n如果你说完一句话,我在处理其他事情,没有及时回复你,那你不是要干等着我做完其他事情后,我回复你,你才能说下一句话,很显然这不现实。\n\n\n\n所以,这样的传输方式有一个缺点:数据包的往返时间越长,通信的效率就越低。\n\n\n# 滑动窗口是什么?\n\n为解决这个问题,TCP 引入了窗口这个概念。即使在往返时间较长的情况下,它也不会降低网络通信的效率。\n\n那么有了窗口,就可以指定窗口大小,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值。\n\n窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。\n\n假设窗口大小为 3个 TCP 段,那么发送方就可以「连续发送」3个 TCP 段,并且中途若有 ACK 丢失,可以通过「下一个确认应答进行确认」。如下图:\n\n\n\n图中的 ACK 600 确认应答报文丢失,也没关系,因为可以通过下一个确认应答进行确认,只要发送方收到了 ACK 700 确认应答,就意味着 700 之前的所有数据「接收方」都收到了。这个模式就叫累计确认或者累计应答。\n\n\n# 窗口大小由哪一方决定?\n\nTCP 头里有一个字段叫 Window,也就是窗口大小。\n\n这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。\n\n所以,通常窗口的大小是由接收方的窗口大小来决定的。\n\n发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。\n\n我们先来看看发送方的窗口,下图就是发送方缓存的数据,根据处理的情况分成四个部分,其中深蓝色方框是发送窗口,紫色方框是可用窗口:\n\n\n\n 1. 井1是已发送并收到 ACK确认的数据:1~31 字节\n 2. 井2是已发送但未收到 ACK确认的数据:32~45 字节\n 3. 井3是未发送但总大小在接收方处理范围内(接收方还有空间):46~51字节\n 4. 井4是未发送但总大小超过接收方处理范围(接收方没有空间):52字节以后 在下图,当发送方把数据「全部」都一下发送出去后,可用窗口的大小就为 0 了,表明可用窗口耗尽,在没收到 ACK 确认之前是无法继续发送数据了。\n\n\n\n在下图,当收到之前发送的数据 32~36字节的 ACK 确认应答后,如果发送窗口的大小没有变化,则滑动窗口往右边移动 5 个字节,因为有 5 个字节的数据被应答确认,接下来 52~56字节又变成了可用窗口,那么后续也就可以发送 52~56 这 5 个字节的数据了。\n\n\n\n\n# 程序是如何表示发送方的四个部分的呢?\n\nTCP 滑动窗口方案使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。其中两个指针是绝对指针(指特定的序列号),一个是相对指针(需要做偏移)。\n\n\n\n * SND.WND:表示发送窗口的大小(大小是由接收方指定的);\n\n * SND.UNA:是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一个字节。\n\n * SND.NXT:也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号,也就是 #3 的第一个字节。\n\n指向 #4 的第一个字节是个相对指针,它需要 SND.UNA 指针加上 SND.WND大小的偏移量,就可以指向 #4 的第一个字节了。\n\n那么可用窗口大小的计算就可以是:\n\n可用窗口大小 = SND.WND -(SND.NXT - SND.UNA)\n\n\n# 接收方的滑动窗口\n\n接下来我们看看接收方的窗口,接收窗口相对简单一些,根据处理的情况划分成三个部分:\n\n * 井1 + 井2 是已成功接收并确认的数据(等待应用进程读取)\n * 井3 是未收到数据但可以接收的数据\n * 井4 未收到数据并不可以接收的数据\n\n\n\n接收窗口 其中三个接收部分,使用两个指针进行划分:\n\n * RCV.WND:表示接收窗口的大小,它会通告给发送方。\n * RCV.NXT:是一个指针,它指向期望从发送方发送来的下一个数据字节的序列号,也就是 #3 的第一个字节。\n * 指向 #4 的第一个字节是个相对指针,它需要RCV.NXT指针加上 RCV.WND大小的偏移量,就可以指向 #4 的第一个字节了。\n\n\n# 接收窗口和发送窗口的大小是相等的吗?\n\n并不是完全相等,接收窗口的大小是约等于发送窗口的大小的。\n\n因为滑动窗口并不是一成不变的。比如,当接收方的应用进程读取数据的速度非常快的话,这样的话接收窗口可以很快的就空缺出来。那么新的接收窗口大小,是通过 TCP 报文中的 Windows 字段来告诉发送方。那么这个传输过程是存在时延的,所以接收窗口和发送窗口是约等于的关系。\n\n\n# 参考:\n\nTCP滑动窗口和重传机制", + "normalizedContent": "# 滑动窗口解决什么问题?\n\n我们都知道 tcp 是每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了, 再发送下一个。\n\n这个模式就有点像我和你面对面聊天,你一句我一句。但这种方式的缺点是效率比较低的。\n\n如果你说完一句话,我在处理其他事情,没有及时回复你,那你不是要干等着我做完其他事情后,我回复你,你才能说下一句话,很显然这不现实。\n\n\n\n所以,这样的传输方式有一个缺点:数据包的往返时间越长,通信的效率就越低。\n\n\n# 滑动窗口是什么?\n\n为解决这个问题,tcp 引入了窗口这个概念。即使在往返时间较长的情况下,它也不会降低网络通信的效率。\n\n那么有了窗口,就可以指定窗口大小,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值。\n\n窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。\n\n假设窗口大小为 3个 tcp 段,那么发送方就可以「连续发送」3个 tcp 段,并且中途若有 ack 丢失,可以通过「下一个确认应答进行确认」。如下图:\n\n\n\n图中的 ack 600 确认应答报文丢失,也没关系,因为可以通过下一个确认应答进行确认,只要发送方收到了 ack 700 确认应答,就意味着 700 之前的所有数据「接收方」都收到了。这个模式就叫累计确认或者累计应答。\n\n\n# 窗口大小由哪一方决定?\n\ntcp 头里有一个字段叫 window,也就是窗口大小。\n\n这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。\n\n所以,通常窗口的大小是由接收方的窗口大小来决定的。\n\n发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。\n\n我们先来看看发送方的窗口,下图就是发送方缓存的数据,根据处理的情况分成四个部分,其中深蓝色方框是发送窗口,紫色方框是可用窗口:\n\n\n\n 1. 井1是已发送并收到 ack确认的数据:1~31 字节\n 2. 井2是已发送但未收到 ack确认的数据:32~45 字节\n 3. 井3是未发送但总大小在接收方处理范围内(接收方还有空间):46~51字节\n 4. 井4是未发送但总大小超过接收方处理范围(接收方没有空间):52字节以后 在下图,当发送方把数据「全部」都一下发送出去后,可用窗口的大小就为 0 了,表明可用窗口耗尽,在没收到 ack 确认之前是无法继续发送数据了。\n\n\n\n在下图,当收到之前发送的数据 32~36字节的 ack 确认应答后,如果发送窗口的大小没有变化,则滑动窗口往右边移动 5 个字节,因为有 5 个字节的数据被应答确认,接下来 52~56字节又变成了可用窗口,那么后续也就可以发送 52~56 这 5 个字节的数据了。\n\n\n\n\n# 程序是如何表示发送方的四个部分的呢?\n\ntcp 滑动窗口方案使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。其中两个指针是绝对指针(指特定的序列号),一个是相对指针(需要做偏移)。\n\n\n\n * snd.wnd:表示发送窗口的大小(大小是由接收方指定的);\n\n * snd.una:是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一个字节。\n\n * snd.nxt:也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号,也就是 #3 的第一个字节。\n\n指向 #4 的第一个字节是个相对指针,它需要 snd.una 指针加上 snd.wnd大小的偏移量,就可以指向 #4 的第一个字节了。\n\n那么可用窗口大小的计算就可以是:\n\n可用窗口大小 = snd.wnd -(snd.nxt - snd.una)\n\n\n# 接收方的滑动窗口\n\n接下来我们看看接收方的窗口,接收窗口相对简单一些,根据处理的情况划分成三个部分:\n\n * 井1 + 井2 是已成功接收并确认的数据(等待应用进程读取)\n * 井3 是未收到数据但可以接收的数据\n * 井4 未收到数据并不可以接收的数据\n\n\n\n接收窗口 其中三个接收部分,使用两个指针进行划分:\n\n * rcv.wnd:表示接收窗口的大小,它会通告给发送方。\n * rcv.nxt:是一个指针,它指向期望从发送方发送来的下一个数据字节的序列号,也就是 #3 的第一个字节。\n * 指向 #4 的第一个字节是个相对指针,它需要rcv.nxt指针加上 rcv.wnd大小的偏移量,就可以指向 #4 的第一个字节了。\n\n\n# 接收窗口和发送窗口的大小是相等的吗?\n\n并不是完全相等,接收窗口的大小是约等于发送窗口的大小的。\n\n因为滑动窗口并不是一成不变的。比如,当接收方的应用进程读取数据的速度非常快的话,这样的话接收窗口可以很快的就空缺出来。那么新的接收窗口大小,是通过 tcp 报文中的 windows 字段来告诉发送方。那么这个传输过程是存在时延的,所以接收窗口和发送窗口是约等于的关系。\n\n\n# 参考:\n\ntcp滑动窗口和重传机制", "charsets": { "cjk": true } }, { - "title": "简述 TCP 滑动窗口以及重传机制", + "title": "简述TCP重传机制", "frontmatter": { - "title": "简述 TCP 滑动窗口以及重传机制", - "date": "2022-09-21T22:38:09.000Z", - "permalink": "/pages/dae451/", + "title": "简述TCP重传机制", + "date": "2022-09-27T22:26:11.000Z", + "permalink": "/pages/a219df/", "categories": [ "计算机基础", "计算机网络" @@ -4685,14 +4904,60 @@ export const siteData = { null ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/170.%E7%AE%80%E8%BF%B0%20TCP%20%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E4%BB%A5%E5%8F%8A%E9%87%8D%E4%BC%A0%E6%9C%BA%E5%88%B6.html", - "relativePath": "02.计算机基础/10.计算机网络/170.简述 TCP 滑动窗口以及重传机制.md", - "key": "v-70347066", - "path": "/pages/dae451/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {} + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/171.%20%E7%AE%80%E8%BF%B0TCP%E9%87%8D%E4%BC%A0%E6%9C%BA%E5%88%B6.html", + "relativePath": "02.计算机基础/10.计算机网络/171. 简述TCP重传机制.md", + "key": "v-52edac80", + "path": "/pages/a219df/", + "headers": [ + { + "level": 2, + "title": "重传机制", + "slug": "重传机制", + "normalizedTitle": "重传机制", + "charIndex": 2 + }, + { + "level": 2, + "title": "超时重传", + "slug": "超时重传", + "normalizedTitle": "超时重传", + "charIndex": 101 + }, + { + "level": 2, + "title": "快速重传", + "slug": "快速重传", + "normalizedTitle": "快速重传", + "charIndex": 109 + }, + { + "level": 2, + "title": "SACK 方法", + "slug": "sack-方法", + "normalizedTitle": "sack 方法", + "charIndex": 1791 + }, + { + "level": 2, + "title": "Duplicate SACK", + "slug": "duplicate-sack", + "normalizedTitle": "duplicate sack", + "charIndex": 2063 + }, + { + "level": 2, + "title": "参考", + "slug": "参考", + "normalizedTitle": "参考", + "charIndex": 2811 + } + ], + "headersStr": "重传机制 超时重传 快速重传 SACK 方法 Duplicate SACK 参考", + "content": "# 重传机制\n\nTCP 实现可靠传输的方式之一,是通过序列号与确认应答。\n\n在 TCP 中,当发送端的数据到达接收主机时,接收端主机会返回一个确认应答消息,表示已收到消息。\n\n常见的重传机制:\n\n * 超时重传\n * 快速重传\n * SACK D-SACK\n\n\n# 超时重传\n\n重传机制的其中一个方式,就是在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK 确认应答报文,就会重发该数据,也就是我们常说的超时重传。\n\nTCP 会在以下两种情况发生超时重传:\n\n * 数据包丢失\n * 确认应答丢失\n\n超时时间应该设置为多少呢?\n\n\n\nRTT 就是数据从网络一端传送到另一端所需的时间,也就是包的往返时间。\n\n超时重传时间是以 RTO (Retransmission Timeout 超时重传时间)表示。\n\n假设在重传的情况下,超时时间 RTO 「较长或较短」时,会发生什么事情呢?\n\n\n\n上图中有两种超时时间不同的情况:\n\n * 当超时时间 RTO 较大时,重发就慢,丢了老半天才重发,没有效率,性能差;\n * 当超时时间 RTO 较小时,会导致可能并没有丢就重发,于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。\n\n根据上述的两种情况,我们可以得知,超时重传时间 RTO 的值应该略大于报文往返 RTT 的值。\n\n\n\n好像就是在发送端发包时记下t0 ,然后接收端再把这个ack 回来时再记一个t1,于是RTT = t1 – t0。没那么简单,这只是一个采样,不能代表普遍情况。\n\n实际上「报文往返 RTT 的值」是经常变化的,因为我们的网络也是时常变化的。也就因为「报文往返 RTT 的值」 是经常波动变化的,所以「超时重传时间 RTO 的值」应该是一个动态变化的值。\n\n估计往返时间,通常需要采样以下两个:\n\n * 需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个平滑 RTT 的值,而且这个值还是要不断变化的,因为网络状况不断地变化。\n * 除了采样 RTT,还要采样 RTT 的波动范围,这样就避免如果 RTT 有一个大的波动的话,很难被发现的情况。 RFC6289 建议使用以下的公式计算 RTO:\n\n\n\n其中 SRTT 是计算平滑的RTT ,DevRTR 是计算平滑的RTT 与 最新 RTT 的差距。\n\n在 Linux 下,α = 0.125,β = 0.25, μ = 1,∂ = 4。别问怎么来的,问就是大量实验中调出来的。\n\n如果超时重发的数据,再次超时的时候,又需要重传的时候,TCP 的策略是超时时间隔加倍。\n\n也就是每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。\n\n超时触发重传存在的问题是,超时周期可能相对较长。那是不是可以有更快的方式呢?\n\n于是就可以用「快速重传」机制来解决超时重发的时间等待。\n\n\n# 快速重传\n\nTCP 还有另外一种快速重传(Fast Retransmit)机制,它不以时间为驱动,而是以数据驱动重传。\n\n\n\n在上图,发送方发出了 1,2,3,4,5 份数据:\n\n * 第一份 Seq1 先送到了,于是就 Ack 回 2;\n * 结果 Seq2 因为某些原因没收到,Seq3 到达了,于是还是 Ack 回 2;\n * 后面的 Seq4 和 Seq5 都到了,但还是 Ack 回 2,因为 Seq2 还是没有收到;\n * 发送端收到了三个 Ack = 2 的确认,知道了 Seq2 还没有收到,就会在定时器过期之前,重传丢失的 Seq2。\n * 最后,收到了 Seq2,此时因为 Seq3,Seq4,Seq5 都收到了,于是 Ack 回 6 。\n\n所以,快速重传的工作方式是当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。\n\n快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传之前的一个,还是重传所有的问题。\n\n比如对于上面的例子,是重传 Seq2 呢?还是重传 Seq2、Seq3、Seq4、Seq5 呢?因为发送端并不清楚这连续的三个 Ack 2 是谁传回来的。\n\n为了解决不知道该重传哪些 TCP 报文,于是就有 SACK 方法。\n\n\n# SACK 方法\n\n还有一种实现重传机制的方式叫:SACK( Selective Acknowledgment 选择性确认)。\n\n这种方式需要在 TCP 头部「选项」字段里加一个 SACK的东西,它可以将缓存的数据发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。\n\n如下图,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK 信息发现只有 200~299 这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复。\n\n\n\n\n# Duplicate SACK\n\nDuplicate SACK 又称 D-SACK,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。\n\n下面举例两个栗子,来说明 D-SACK 的作用。\n\n栗子一号:ACK 丢包:\n\n\n\n「接收方」发给「发送方」的两个 ACK 确认应答都丢失了,所以发送方超时后,重传第一个数据包(3000 ~ 3499) 于是「接收方」发现数据是重复收到的,于是回了一个 SACK = 3000~3500,告诉「发送方」 3000~3500 的数据早已被接收了,因为 ACK 都到了 4000 了,已经意味着 4000 之前的所有数据都已收到,所以这个 SACK 就代表着 D-SACK。 这样「发送方」就知道了,数据没有丢,是「接收方」的 ACK 确认报文丢了。 栗子二号:网络延时:\n\n\n\n * 数据包(1000~1499) 被网络延迟了,导致「发送方」没有收到 Ack 1500 的确认报文。\n * 而后面报文到达的三个相同的 ACK 确认报文,就触发了快速重传机制,但是在重传后,被延迟的数据包(1000~1499)又到了「接收方」;\n * 所以「接收方」回了一个 SACK=1000~1500,因为 ACK 已经到了 3000,所以这个 SACK 是 D-SACK,表示收到了重复的包。\n * 这样发送方就知道快速重传触发的原因不是发出去的包丢了,也不是因为回应的 ACK 包丢了,而是因为网络延迟了。\n\n可见,D-SACK 有这么几个好处:\n\n 1. 可以让「发送方」知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了;\n 2. 可以知道是不是「发送方」的数据包被网络延迟了;\n 3. 可以知道网络中是不是把「发送方」的数据包给复制了;\n\n\n# 参考\n\nTCP滑动窗口和重传机制", + "normalizedContent": "# 重传机制\n\ntcp 实现可靠传输的方式之一,是通过序列号与确认应答。\n\n在 tcp 中,当发送端的数据到达接收主机时,接收端主机会返回一个确认应答消息,表示已收到消息。\n\n常见的重传机制:\n\n * 超时重传\n * 快速重传\n * sack d-sack\n\n\n# 超时重传\n\n重传机制的其中一个方式,就是在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ack 确认应答报文,就会重发该数据,也就是我们常说的超时重传。\n\ntcp 会在以下两种情况发生超时重传:\n\n * 数据包丢失\n * 确认应答丢失\n\n超时时间应该设置为多少呢?\n\n\n\nrtt 就是数据从网络一端传送到另一端所需的时间,也就是包的往返时间。\n\n超时重传时间是以 rto (retransmission timeout 超时重传时间)表示。\n\n假设在重传的情况下,超时时间 rto 「较长或较短」时,会发生什么事情呢?\n\n\n\n上图中有两种超时时间不同的情况:\n\n * 当超时时间 rto 较大时,重发就慢,丢了老半天才重发,没有效率,性能差;\n * 当超时时间 rto 较小时,会导致可能并没有丢就重发,于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。\n\n根据上述的两种情况,我们可以得知,超时重传时间 rto 的值应该略大于报文往返 rtt 的值。\n\n\n\n好像就是在发送端发包时记下t0 ,然后接收端再把这个ack 回来时再记一个t1,于是rtt = t1 – t0。没那么简单,这只是一个采样,不能代表普遍情况。\n\n实际上「报文往返 rtt 的值」是经常变化的,因为我们的网络也是时常变化的。也就因为「报文往返 rtt 的值」 是经常波动变化的,所以「超时重传时间 rto 的值」应该是一个动态变化的值。\n\n估计往返时间,通常需要采样以下两个:\n\n * 需要 tcp 通过采样 rtt 的时间,然后进行加权平均,算出一个平滑 rtt 的值,而且这个值还是要不断变化的,因为网络状况不断地变化。\n * 除了采样 rtt,还要采样 rtt 的波动范围,这样就避免如果 rtt 有一个大的波动的话,很难被发现的情况。 rfc6289 建议使用以下的公式计算 rto:\n\n\n\n其中 srtt 是计算平滑的rtt ,devrtr 是计算平滑的rtt 与 最新 rtt 的差距。\n\n在 linux 下,α = 0.125,β = 0.25, μ = 1,∂ = 4。别问怎么来的,问就是大量实验中调出来的。\n\n如果超时重发的数据,再次超时的时候,又需要重传的时候,tcp 的策略是超时时间隔加倍。\n\n也就是每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。\n\n超时触发重传存在的问题是,超时周期可能相对较长。那是不是可以有更快的方式呢?\n\n于是就可以用「快速重传」机制来解决超时重发的时间等待。\n\n\n# 快速重传\n\ntcp 还有另外一种快速重传(fast retransmit)机制,它不以时间为驱动,而是以数据驱动重传。\n\n\n\n在上图,发送方发出了 1,2,3,4,5 份数据:\n\n * 第一份 seq1 先送到了,于是就 ack 回 2;\n * 结果 seq2 因为某些原因没收到,seq3 到达了,于是还是 ack 回 2;\n * 后面的 seq4 和 seq5 都到了,但还是 ack 回 2,因为 seq2 还是没有收到;\n * 发送端收到了三个 ack = 2 的确认,知道了 seq2 还没有收到,就会在定时器过期之前,重传丢失的 seq2。\n * 最后,收到了 seq2,此时因为 seq3,seq4,seq5 都收到了,于是 ack 回 6 。\n\n所以,快速重传的工作方式是当收到三个相同的 ack 报文时,会在定时器过期之前,重传丢失的报文段。\n\n快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传之前的一个,还是重传所有的问题。\n\n比如对于上面的例子,是重传 seq2 呢?还是重传 seq2、seq3、seq4、seq5 呢?因为发送端并不清楚这连续的三个 ack 2 是谁传回来的。\n\n为了解决不知道该重传哪些 tcp 报文,于是就有 sack 方法。\n\n\n# sack 方法\n\n还有一种实现重传机制的方式叫:sack( selective acknowledgment 选择性确认)。\n\n这种方式需要在 tcp 头部「选项」字段里加一个 sack的东西,它可以将缓存的数据发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。\n\n如下图,发送方收到了三次同样的 ack 确认报文,于是就会触发快速重发机制,通过 sack 信息发现只有 200~299 这段数据丢失,则重发时,就只选择了这个 tcp 段进行重复。\n\n\n\n\n# duplicate sack\n\nduplicate sack 又称 d-sack,其主要使用了 sack 来告诉「发送方」有哪些数据被重复接收了。\n\n下面举例两个栗子,来说明 d-sack 的作用。\n\n栗子一号:ack 丢包:\n\n\n\n「接收方」发给「发送方」的两个 ack 确认应答都丢失了,所以发送方超时后,重传第一个数据包(3000 ~ 3499) 于是「接收方」发现数据是重复收到的,于是回了一个 sack = 3000~3500,告诉「发送方」 3000~3500 的数据早已被接收了,因为 ack 都到了 4000 了,已经意味着 4000 之前的所有数据都已收到,所以这个 sack 就代表着 d-sack。 这样「发送方」就知道了,数据没有丢,是「接收方」的 ack 确认报文丢了。 栗子二号:网络延时:\n\n\n\n * 数据包(1000~1499) 被网络延迟了,导致「发送方」没有收到 ack 1500 的确认报文。\n * 而后面报文到达的三个相同的 ack 确认报文,就触发了快速重传机制,但是在重传后,被延迟的数据包(1000~1499)又到了「接收方」;\n * 所以「接收方」回了一个 sack=1000~1500,因为 ack 已经到了 3000,所以这个 sack 是 d-sack,表示收到了重复的包。\n * 这样发送方就知道快速重传触发的原因不是发出去的包丢了,也不是因为回应的 ack 包丢了,而是因为网络延迟了。\n\n可见,d-sack 有这么几个好处:\n\n 1. 可以让「发送方」知道,是发出去的包丢了,还是接收方回应的 ack 包丢了;\n 2. 可以知道是不是「发送方」的数据包被网络延迟了;\n 3. 可以知道网络中是不是把「发送方」的数据包给复制了;\n\n\n# 参考\n\ntcp滑动窗口和重传机制", + "charsets": { + "cjk": true + } }, { "title": "为什么需要序列化?有什么序列化的方式?", @@ -4736,48 +5001,126 @@ export const siteData = { }, { "level": 2, - "title": "4.序列化的方式", - "slug": "_4-序列化的方式", - "normalizedTitle": "4.序列化的方式", - "charIndex": 599 + "title": "4.序列化的方式", + "slug": "_4-序列化的方式", + "normalizedTitle": "4.序列化的方式", + "charIndex": 599 + }, + { + "level": 2, + "title": "5.序列化技术选型的几个关键点", + "slug": "_5-序列化技术选型的几个关键点", + "normalizedTitle": "5.序列化技术选型的几个关键点", + "charIndex": 735 + } + ], + "headersStr": "定义 2.为什么要序列化?实现序列化的作用是什么? 3.什么情况下需要序列化? 4.序列化的方式 5.序列化技术选型的几个关键点", + "content": "# 定义\n\n1)序列化:把对象转化为可传输的字节序列过程称为序列化。\n\n2)反序列化:把字节序列还原为对象的过程称为反序列化。\n\n\n# 2.为什么要序列化?实现序列化的作用是什么?\n\n如果光看定义我想你很难一下子理解序列化的意义,那么我们可以从另一个角度来推导出什么是序列化, 那么究竟序列化的目的是什么?\n\n其实序列化最终的目的是为了对象可以跨平台存储,和进行网络传输。而我们进行跨平台存储和网络传输的方式就是IO,而我们的IO支持的数据格式就是字节数组。\n\n因为我们单方面的只把对象转成字节数组还不行,因为没有规则的字节数组我们是没办法把对象的本来面目还原回来的,所以我们必须在把对象转成字节数组的时候就制定一种规则(序列化),那么我们从IO流里面读出数据的时候再以这种规则把对象还原回来(反序列化)。\n\n如果我们要把一栋房子从一个地方运输到另一个地方去,序列化就是我把房子拆成一个个的砖块放到车子里,然后留下一张房子原来结构的图纸,反序列化就是我们把房子运输到了目的地以后,根据图纸把一块块砖头还原成房子原来面目的过程\n\n\n# 3.什么情况下需要序列化?\n\n通过上面我想你已经知道了凡是需要进行“跨平台存储”和”网络传输”的数据,都需要进行序列化。\n\n本质上存储和网络传输 都需要经过 把一个对象状态保存成一种跨平台识别的字节格式,然后其他的平台才可以通过字节信息解析还原对象信息。\n\n\n# 4.序列化的方式\n\n序列化只是一种拆装组装对象的规则,那么这种规则肯定也可能有多种多样,比如现在常见的序列化方式有:\n\nJDK(不支持跨语言)、JSON、XML、Hessian、Kryo(不支持跨语言)、Thrift、Protostuff、FST(不支持跨语言)\n\n\n# 5.序列化技术选型的几个关键点\n\n序列化协议各有千秋,不能简单的说一种序列化协议是最好的,只能从你的当时环境下去选择最适合你们的序列化协议,如果你要为你的公司项目进行序列化技术的选型,那么主要从以下几个因素。\n\n协议是否支持跨平台\n\n如果你们公司有好多种语言进行混合开发,那么就肯定不适合用有语言局限性的序列化协议,要不然你JDK序列化出来的格式,其他语言并没法支持。\n\n序列化的速度\n\n如果序列化的频率非常高,那么选择序列化速度快的协议会为你的系统性能提升不少。\n\n序列化出来的大小\n\n如果频繁的在网络中传输的数据那就需要数据越小越好,小的数据传输快,也不占带宽,也能整体提升系统的性能。", + "normalizedContent": "# 定义\n\n1)序列化:把对象转化为可传输的字节序列过程称为序列化。\n\n2)反序列化:把字节序列还原为对象的过程称为反序列化。\n\n\n# 2.为什么要序列化?实现序列化的作用是什么?\n\n如果光看定义我想你很难一下子理解序列化的意义,那么我们可以从另一个角度来推导出什么是序列化, 那么究竟序列化的目的是什么?\n\n其实序列化最终的目的是为了对象可以跨平台存储,和进行网络传输。而我们进行跨平台存储和网络传输的方式就是io,而我们的io支持的数据格式就是字节数组。\n\n因为我们单方面的只把对象转成字节数组还不行,因为没有规则的字节数组我们是没办法把对象的本来面目还原回来的,所以我们必须在把对象转成字节数组的时候就制定一种规则(序列化),那么我们从io流里面读出数据的时候再以这种规则把对象还原回来(反序列化)。\n\n如果我们要把一栋房子从一个地方运输到另一个地方去,序列化就是我把房子拆成一个个的砖块放到车子里,然后留下一张房子原来结构的图纸,反序列化就是我们把房子运输到了目的地以后,根据图纸把一块块砖头还原成房子原来面目的过程\n\n\n# 3.什么情况下需要序列化?\n\n通过上面我想你已经知道了凡是需要进行“跨平台存储”和”网络传输”的数据,都需要进行序列化。\n\n本质上存储和网络传输 都需要经过 把一个对象状态保存成一种跨平台识别的字节格式,然后其他的平台才可以通过字节信息解析还原对象信息。\n\n\n# 4.序列化的方式\n\n序列化只是一种拆装组装对象的规则,那么这种规则肯定也可能有多种多样,比如现在常见的序列化方式有:\n\njdk(不支持跨语言)、json、xml、hessian、kryo(不支持跨语言)、thrift、protostuff、fst(不支持跨语言)\n\n\n# 5.序列化技术选型的几个关键点\n\n序列化协议各有千秋,不能简单的说一种序列化协议是最好的,只能从你的当时环境下去选择最适合你们的序列化协议,如果你要为你的公司项目进行序列化技术的选型,那么主要从以下几个因素。\n\n协议是否支持跨平台\n\n如果你们公司有好多种语言进行混合开发,那么就肯定不适合用有语言局限性的序列化协议,要不然你jdk序列化出来的格式,其他语言并没法支持。\n\n序列化的速度\n\n如果序列化的频率非常高,那么选择序列化速度快的协议会为你的系统性能提升不少。\n\n序列化出来的大小\n\n如果频繁的在网络中传输的数据那就需要数据越小越好,小的数据传输快,也不占带宽,也能整体提升系统的性能。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 + }, + { + "title": "简述 iPv4 和 iPv6 的区别", + "frontmatter": { + "title": "简述 iPv4 和 iPv6 的区别", + "date": "2022-09-21T22:38:54.000Z", + "permalink": "/pages/6a2e50/", + "categories": [ + "计算机基础", + "计算机网络" + ], + "tags": [ + null + ] + }, + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/200.%E7%AE%80%E8%BF%B0%20iPv4%20%E5%92%8C%20iPv6%20%E7%9A%84%E5%8C%BA%E5%88%AB.html", + "relativePath": "02.计算机基础/10.计算机网络/200.简述 iPv4 和 iPv6 的区别.md", + "key": "v-730f9a6a", + "path": "/pages/6a2e50/", + "headers": [ + { + "level": 2, + "title": "什么是IPv4和IPv6?", + "slug": "什么是ipv4和ipv6", + "normalizedTitle": "什么是ipv4和ipv6?", + "charIndex": 249 + }, + { + "level": 2, + "title": "IPv4地址", + "slug": "ipv4地址", + "normalizedTitle": "ipv4地址", + "charIndex": 414 + }, + { + "level": 2, + "title": "IPv6地址", + "slug": "ipv6地址", + "normalizedTitle": "ipv6地址", + "charIndex": 844 + }, + { + "level": 2, + "title": "IPv4和IPv6之间的区别", + "slug": "ipv4和ipv6之间的区别", + "normalizedTitle": "ipv4和ipv6之间的区别", + "charIndex": 264 + }, + { + "level": 2, + "title": "IPv4或IPv6:使用哪个?", + "slug": "ipv4或ipv6-使用哪个", + "normalizedTitle": "ipv4或ipv6:使用哪个?", + "charIndex": 280 + }, + { + "level": 2, + "title": "那么为什么我们仍在使用IPv4?", + "slug": "那么为什么我们仍在使用ipv4", + "normalizedTitle": "那么为什么我们仍在使用ipv4?", + "charIndex": 2104 + }, + { + "level": 2, + "title": "您需要什么才能使用IPv6?", + "slug": "您需要什么才能使用ipv6", + "normalizedTitle": "您需要什么才能使用ipv6?", + "charIndex": 2328 + }, + { + "level": 2, + "title": "IPv4与IPv6的安全性", + "slug": "ipv4与ipv6的安全性", + "normalizedTitle": "ipv4与ipv6的安全性", + "charIndex": 2528 }, { "level": 2, - "title": "5.序列化技术选型的几个关键点", - "slug": "_5-序列化技术选型的几个关键点", - "normalizedTitle": "5.序列化技术选型的几个关键点", - "charIndex": 735 + "title": "总结", + "slug": "总结", + "normalizedTitle": "总结", + "charIndex": 2794 + }, + { + "level": 2, + "title": "参考:", + "slug": "参考", + "normalizedTitle": "参考:", + "charIndex": 2924 } ], - "headersStr": "定义 2.为什么要序列化?实现序列化的作用是什么? 3.什么情况下需要序列化? 4.序列化的方式 5.序列化技术选型的几个关键点", - "content": "# 定义\n\n1)序列化:把对象转化为可传输的字节序列过程称为序列化。\n\n2)反序列化:把字节序列还原为对象的过程称为反序列化。\n\n\n# 2.为什么要序列化?实现序列化的作用是什么?\n\n如果光看定义我想你很难一下子理解序列化的意义,那么我们可以从另一个角度来推导出什么是序列化, 那么究竟序列化的目的是什么?\n\n其实序列化最终的目的是为了对象可以跨平台存储,和进行网络传输。而我们进行跨平台存储和网络传输的方式就是IO,而我们的IO支持的数据格式就是字节数组。\n\n因为我们单方面的只把对象转成字节数组还不行,因为没有规则的字节数组我们是没办法把对象的本来面目还原回来的,所以我们必须在把对象转成字节数组的时候就制定一种规则(序列化),那么我们从IO流里面读出数据的时候再以这种规则把对象还原回来(反序列化)。\n\n如果我们要把一栋房子从一个地方运输到另一个地方去,序列化就是我把房子拆成一个个的砖块放到车子里,然后留下一张房子原来结构的图纸,反序列化就是我们把房子运输到了目的地以后,根据图纸把一块块砖头还原成房子原来面目的过程\n\n\n# 3.什么情况下需要序列化?\n\n通过上面我想你已经知道了凡是需要进行“跨平台存储”和”网络传输”的数据,都需要进行序列化。\n\n本质上存储和网络传输 都需要经过 把一个对象状态保存成一种跨平台识别的字节格式,然后其他的平台才可以通过字节信息解析还原对象信息。\n\n\n# 4.序列化的方式\n\n序列化只是一种拆装组装对象的规则,那么这种规则肯定也可能有多种多样,比如现在常见的序列化方式有:\n\nJDK(不支持跨语言)、JSON、XML、Hessian、Kryo(不支持跨语言)、Thrift、Protostuff、FST(不支持跨语言)\n\n\n# 5.序列化技术选型的几个关键点\n\n序列化协议各有千秋,不能简单的说一种序列化协议是最好的,只能从你的当时环境下去选择最适合你们的序列化协议,如果你要为你的公司项目进行序列化技术的选型,那么主要从以下几个因素。\n\n协议是否支持跨平台\n\n如果你们公司有好多种语言进行混合开发,那么就肯定不适合用有语言局限性的序列化协议,要不然你JDK序列化出来的格式,其他语言并没法支持。\n\n序列化的速度\n\n如果序列化的频率非常高,那么选择序列化速度快的协议会为你的系统性能提升不少。\n\n序列化出来的大小\n\n如果频繁的在网络中传输的数据那就需要数据越小越好,小的数据传输快,也不占带宽,也能整体提升系统的性能。", - "normalizedContent": "# 定义\n\n1)序列化:把对象转化为可传输的字节序列过程称为序列化。\n\n2)反序列化:把字节序列还原为对象的过程称为反序列化。\n\n\n# 2.为什么要序列化?实现序列化的作用是什么?\n\n如果光看定义我想你很难一下子理解序列化的意义,那么我们可以从另一个角度来推导出什么是序列化, 那么究竟序列化的目的是什么?\n\n其实序列化最终的目的是为了对象可以跨平台存储,和进行网络传输。而我们进行跨平台存储和网络传输的方式就是io,而我们的io支持的数据格式就是字节数组。\n\n因为我们单方面的只把对象转成字节数组还不行,因为没有规则的字节数组我们是没办法把对象的本来面目还原回来的,所以我们必须在把对象转成字节数组的时候就制定一种规则(序列化),那么我们从io流里面读出数据的时候再以这种规则把对象还原回来(反序列化)。\n\n如果我们要把一栋房子从一个地方运输到另一个地方去,序列化就是我把房子拆成一个个的砖块放到车子里,然后留下一张房子原来结构的图纸,反序列化就是我们把房子运输到了目的地以后,根据图纸把一块块砖头还原成房子原来面目的过程\n\n\n# 3.什么情况下需要序列化?\n\n通过上面我想你已经知道了凡是需要进行“跨平台存储”和”网络传输”的数据,都需要进行序列化。\n\n本质上存储和网络传输 都需要经过 把一个对象状态保存成一种跨平台识别的字节格式,然后其他的平台才可以通过字节信息解析还原对象信息。\n\n\n# 4.序列化的方式\n\n序列化只是一种拆装组装对象的规则,那么这种规则肯定也可能有多种多样,比如现在常见的序列化方式有:\n\njdk(不支持跨语言)、json、xml、hessian、kryo(不支持跨语言)、thrift、protostuff、fst(不支持跨语言)\n\n\n# 5.序列化技术选型的几个关键点\n\n序列化协议各有千秋,不能简单的说一种序列化协议是最好的,只能从你的当时环境下去选择最适合你们的序列化协议,如果你要为你的公司项目进行序列化技术的选型,那么主要从以下几个因素。\n\n协议是否支持跨平台\n\n如果你们公司有好多种语言进行混合开发,那么就肯定不适合用有语言局限性的序列化协议,要不然你jdk序列化出来的格式,其他语言并没法支持。\n\n序列化的速度\n\n如果序列化的频率非常高,那么选择序列化速度快的协议会为你的系统性能提升不少。\n\n序列化出来的大小\n\n如果频繁的在网络中传输的数据那就需要数据越小越好,小的数据传输快,也不占带宽,也能整体提升系统的性能。", + "headersStr": "什么是IPv4和IPv6? IPv4地址 IPv6地址 IPv4和IPv6之间的区别 IPv4或IPv6:使用哪个? 那么为什么我们仍在使用IPv4? 您需要什么才能使用IPv6? IPv4与IPv6的安全性 总结 参考:", + "content": "前言\n\n\n\nInternet协议(IP)是为连接到Internet网络的每个设备分配的数字地址。\n\n它类似于电话号码,是一种独特的数字组合,允许用户与他人通信。\n\nIP地址主要有两个主要功能。\n\n首先,有了IP,用户能够在Internet上被识别。\n\n第二,IP地址允许计算机通过Internet发送和接收数据,也就是我们经常说的通信。\n\n在本文中,我们将深入研究两种类型的IP地址:IPv4与IPv6。\n\n我们将从以下几个方面来给大家介绍这两种类型的IP地址,让大家了解到两类IP的必备知识:\n\n什么是IPv4和IPv6?\n\nIPv4和IPv6之间的区别\n\nIPv4或IPv6:使用哪个?\n\nIPv4与IPv6安全性如何\n\n\n# 什么是IPv4和IPv6?\n\nIPv4和IPv6是不同类型的IP地址。\n\n它们的主要用途相同,标记不同的用户,并且让用户能通过IP进行通信。\n\n主要区别在于IPv6是最新一代的IP地址。\n\n\n# IPv4地址\n\nIPv4地址的概念是在1980年代初期提出的。\n\n即使有新版本的IP地址,IPv4地址仍然是Internet用户使用最广泛的地址。\n\n通常,IPv4地址以点分十进制表示。每个部分代表一组构成8位地址方案的8位地址。\n\n\n\nIPv4地址组合的数量是有限的。总体而言,可以算出40亿(256 4)个唯一地址。\n\n在IPv4地址才开始时,这个数字似乎永远不会过期。但是,现在情况有所不同了。\n\n2011年,全球互联网编号分配机构(IANA)分发了IPv4地址空间的最后一块。\n\n2015年,IANA正式宣布美国已用完IPv4地址。\n\n直到今天,IPv4地址仍然承载着最多(超过90%)的互联网流量。\n\n到目前为止,即使目前存在IPv4地址耗尽的问题,也有一些方法可以继续使用IPv4地址。\n\n例如,当仅需要一个唯一的IP地址来代表一组设备时,网络地址转换(NAT)是一种方法。\n\n除此之外,IP地址可以重复使用。当然,我们已经有了彻底耗尽的解决方案-IPv6地址。\n\n\n# IPv6地址\n\n仔细观察,您会发现IPv6地址并不是一种全新的技术。\n\n它是Internet协议的最新版本,但它是在1998年开发的,旨在替换IPv4地址。\n\nIPv6地址使用以冒号分隔的十六进制数字。\n\n它分为八个16位块,构成一个128位地址方案。\n\n\n\nIPv6也存在数量限制。不过可用的IP数量远大于IPv4。\n\n从理论上讲,可以创建大约3.4×10 38个地址。\n\n这一数据听起来很高,远超于IPv4的总数40亿个,但是有一天也可能出现不够的情况。\n\n但就目前而言,这些地址将可以供我们使用很长一段时间。\n\n\n# IPv4和IPv6之间的区别\n\nIPv4和IPv6用于用户标识和Internet上不同设备之间的通信。\n\nIPv4是32位IP地址,而IPv6是128位IP地址。\n\nIPv4是数字地址,用点分隔。IPv6是一个字母数字地址,用冒号分隔。\n\n我们分别详细介绍了IPv4和IPv6类型。\n\n现在,我们可以比较这些类型,并找出这两种协议之间的主要区别。\n\n我们列举了IPv4和IPv6之间的八个主要区别。\n\n\n\n 1. 地址类型。IPv4具有三种不同类型的地址:多播,广播和单播。IPv6还具有三种不同类型的地址:任意广播,单播和多播。\n\n 2. 数据包大小。对于IPv4,最小数据包大小为576字节。对于IPv6,最小数据包大小为1208字节。\n\n 3. header区域字段数。IPv4具有12个标头字段,而IPv6支持8个标头字段。\n\n 4. 可选字段。IPv4具有可选字段,而IPv6没有。但是,IPv6具有扩展header,可以在将来扩展协议而不会影响主包结构。\n\n 5. 配置。在IPv4中,新装的系统必须配置好才能与其他系统通信。在IPv6中,配置是可选的,它允许根据所需功能进行选择。\n\n 6. 安全性。在IPv4中,安全性主要取决于网站和应用程序。它不是针对安全性而开发的IP协议。而IPv6集成了Internet协议安全标准(IPSec)。IPv6的网络安全不像IPv4是可选项,IPv6里的网络安全项是强制性的。\n\n 7. 与移动设备的兼容性。IPv4不适合移动网络,因为正如我们前面提到的,它使用点分十进制表示法,而IPv6使用冒号,是移动设备的更好选择。\n\n 8. 主要功能。IPv6允许直接寻址,因为存在大量可能的地址。但是,IPv4已经广泛传播并得到许多设备的支持,这使其更易于使用。\n\n\n# IPv4或IPv6:使用哪个?\n\n对于使用IPv6还是IPv4这个问题,没有标准答案。\n\n在考虑未来的网络体验时,IPv6地址就显得至关重要。\n\n即使在我们已经没有网络地址的情况下仍然可以有其他办法使用IPv4地址,\n\n但是这些选项也可能会轻微影响到网络速度或引起其他问题。\n\n不过,使用IPv6需要开发支持IPv6的新技术和产品。\n\nIPv6的速度显然不比IPv4快,但是从IPv4完全更改为IPv6将为Internet提供更大的唯一IP池。\n\n\n# 那么为什么我们仍在使用IPv4?\n\n问题就在于IPv4和IPv6无法相互通信。\n\n这就是为什么IPv6的集成和适配很复杂。\n\n大多数网站或应用程序仅支持IPv4类型的IP地址。想象一下突然更改每个设备的IP地址。\n\n用户将无法访问大多数网站或应用程序,而我们在互联网上将陷入一片混乱。\n\n从旧的IP类型转换为新的IP类型的过程应分步完成。\n\n例如,这两个协议能够并行运行。\n\n此功能称为双重堆栈。它允许用户同时访问IPv4和IPv6内容。\n\n\n# 您需要什么才能使用IPv6?\n\n 1. 操作系统必须与IPv6兼容。Windows Vista和Windows的较新版本,Mac OS X的现代版本以及Linux。\n\n 2. 大多数路由器不支持IPv6。如果您想尝试使用IPv6,请检查路由器的详细信息。\n\n 3. Internet服务提供商(ISP)也必须支持IPv6。即使您具有合适的操作系统和路由器,您的ISP也必须提供IPv6连接。\n\n\n# IPv4与IPv6的安全性\n\nIPv6的开发考虑了安全性。这就是将IPSec集成在IPv6中的原因,而对于IPv4,IPSec是可选的。\n\n什么是IPSec?\n\nIPSec(Internet协议安全性)是一种安全的网络协议,它对数据包进行身份验证和加密,以在设备之间提供安全的通信。\n\n加密是只有经过确认的各方才能理解的一种秘密代码。它有助于确保通过公共网络发送的信息的安全。\n\n由于IPv4还可以选择集成IPSec,因此我们可以假设在安全性方面IPv4与IPv6几乎相同。\n\n但是,如果已经集成了安全措施,则要简单得多。\n\n\n# 总结\n\nIPv6是IP地址的未来。 但是,从IPv4过渡到IPv6的过程却是漫长而复杂的。\n\n如果您对使用IPv6地址感到好奇,建议将其用法与IPv4结合使用,因为大多数网站和应用程序仍仅支持IPv4地址。\n\nIPv4和IPv6使用的结合称为双重堆栈\n\n\n# 参考:\n\nIPv4 vs. IPv6: What is the Difference?", + "normalizedContent": "前言\n\n\n\ninternet协议(ip)是为连接到internet网络的每个设备分配的数字地址。\n\n它类似于电话号码,是一种独特的数字组合,允许用户与他人通信。\n\nip地址主要有两个主要功能。\n\n首先,有了ip,用户能够在internet上被识别。\n\n第二,ip地址允许计算机通过internet发送和接收数据,也就是我们经常说的通信。\n\n在本文中,我们将深入研究两种类型的ip地址:ipv4与ipv6。\n\n我们将从以下几个方面来给大家介绍这两种类型的ip地址,让大家了解到两类ip的必备知识:\n\n什么是ipv4和ipv6?\n\nipv4和ipv6之间的区别\n\nipv4或ipv6:使用哪个?\n\nipv4与ipv6安全性如何\n\n\n# 什么是ipv4和ipv6?\n\nipv4和ipv6是不同类型的ip地址。\n\n它们的主要用途相同,标记不同的用户,并且让用户能通过ip进行通信。\n\n主要区别在于ipv6是最新一代的ip地址。\n\n\n# ipv4地址\n\nipv4地址的概念是在1980年代初期提出的。\n\n即使有新版本的ip地址,ipv4地址仍然是internet用户使用最广泛的地址。\n\n通常,ipv4地址以点分十进制表示。每个部分代表一组构成8位地址方案的8位地址。\n\n\n\nipv4地址组合的数量是有限的。总体而言,可以算出40亿(256 4)个唯一地址。\n\n在ipv4地址才开始时,这个数字似乎永远不会过期。但是,现在情况有所不同了。\n\n2011年,全球互联网编号分配机构(iana)分发了ipv4地址空间的最后一块。\n\n2015年,iana正式宣布美国已用完ipv4地址。\n\n直到今天,ipv4地址仍然承载着最多(超过90%)的互联网流量。\n\n到目前为止,即使目前存在ipv4地址耗尽的问题,也有一些方法可以继续使用ipv4地址。\n\n例如,当仅需要一个唯一的ip地址来代表一组设备时,网络地址转换(nat)是一种方法。\n\n除此之外,ip地址可以重复使用。当然,我们已经有了彻底耗尽的解决方案-ipv6地址。\n\n\n# ipv6地址\n\n仔细观察,您会发现ipv6地址并不是一种全新的技术。\n\n它是internet协议的最新版本,但它是在1998年开发的,旨在替换ipv4地址。\n\nipv6地址使用以冒号分隔的十六进制数字。\n\n它分为八个16位块,构成一个128位地址方案。\n\n\n\nipv6也存在数量限制。不过可用的ip数量远大于ipv4。\n\n从理论上讲,可以创建大约3.4×10 38个地址。\n\n这一数据听起来很高,远超于ipv4的总数40亿个,但是有一天也可能出现不够的情况。\n\n但就目前而言,这些地址将可以供我们使用很长一段时间。\n\n\n# ipv4和ipv6之间的区别\n\nipv4和ipv6用于用户标识和internet上不同设备之间的通信。\n\nipv4是32位ip地址,而ipv6是128位ip地址。\n\nipv4是数字地址,用点分隔。ipv6是一个字母数字地址,用冒号分隔。\n\n我们分别详细介绍了ipv4和ipv6类型。\n\n现在,我们可以比较这些类型,并找出这两种协议之间的主要区别。\n\n我们列举了ipv4和ipv6之间的八个主要区别。\n\n\n\n 1. 地址类型。ipv4具有三种不同类型的地址:多播,广播和单播。ipv6还具有三种不同类型的地址:任意广播,单播和多播。\n\n 2. 数据包大小。对于ipv4,最小数据包大小为576字节。对于ipv6,最小数据包大小为1208字节。\n\n 3. header区域字段数。ipv4具有12个标头字段,而ipv6支持8个标头字段。\n\n 4. 可选字段。ipv4具有可选字段,而ipv6没有。但是,ipv6具有扩展header,可以在将来扩展协议而不会影响主包结构。\n\n 5. 配置。在ipv4中,新装的系统必须配置好才能与其他系统通信。在ipv6中,配置是可选的,它允许根据所需功能进行选择。\n\n 6. 安全性。在ipv4中,安全性主要取决于网站和应用程序。它不是针对安全性而开发的ip协议。而ipv6集成了internet协议安全标准(ipsec)。ipv6的网络安全不像ipv4是可选项,ipv6里的网络安全项是强制性的。\n\n 7. 与移动设备的兼容性。ipv4不适合移动网络,因为正如我们前面提到的,它使用点分十进制表示法,而ipv6使用冒号,是移动设备的更好选择。\n\n 8. 主要功能。ipv6允许直接寻址,因为存在大量可能的地址。但是,ipv4已经广泛传播并得到许多设备的支持,这使其更易于使用。\n\n\n# ipv4或ipv6:使用哪个?\n\n对于使用ipv6还是ipv4这个问题,没有标准答案。\n\n在考虑未来的网络体验时,ipv6地址就显得至关重要。\n\n即使在我们已经没有网络地址的情况下仍然可以有其他办法使用ipv4地址,\n\n但是这些选项也可能会轻微影响到网络速度或引起其他问题。\n\n不过,使用ipv6需要开发支持ipv6的新技术和产品。\n\nipv6的速度显然不比ipv4快,但是从ipv4完全更改为ipv6将为internet提供更大的唯一ip池。\n\n\n# 那么为什么我们仍在使用ipv4?\n\n问题就在于ipv4和ipv6无法相互通信。\n\n这就是为什么ipv6的集成和适配很复杂。\n\n大多数网站或应用程序仅支持ipv4类型的ip地址。想象一下突然更改每个设备的ip地址。\n\n用户将无法访问大多数网站或应用程序,而我们在互联网上将陷入一片混乱。\n\n从旧的ip类型转换为新的ip类型的过程应分步完成。\n\n例如,这两个协议能够并行运行。\n\n此功能称为双重堆栈。它允许用户同时访问ipv4和ipv6内容。\n\n\n# 您需要什么才能使用ipv6?\n\n 1. 操作系统必须与ipv6兼容。windows vista和windows的较新版本,mac os x的现代版本以及linux。\n\n 2. 大多数路由器不支持ipv6。如果您想尝试使用ipv6,请检查路由器的详细信息。\n\n 3. internet服务提供商(isp)也必须支持ipv6。即使您具有合适的操作系统和路由器,您的isp也必须提供ipv6连接。\n\n\n# ipv4与ipv6的安全性\n\nipv6的开发考虑了安全性。这就是将ipsec集成在ipv6中的原因,而对于ipv4,ipsec是可选的。\n\n什么是ipsec?\n\nipsec(internet协议安全性)是一种安全的网络协议,它对数据包进行身份验证和加密,以在设备之间提供安全的通信。\n\n加密是只有经过确认的各方才能理解的一种秘密代码。它有助于确保通过公共网络发送的信息的安全。\n\n由于ipv4还可以选择集成ipsec,因此我们可以假设在安全性方面ipv4与ipv6几乎相同。\n\n但是,如果已经集成了安全措施,则要简单得多。\n\n\n# 总结\n\nipv6是ip地址的未来。 但是,从ipv4过渡到ipv6的过程却是漫长而复杂的。\n\n如果您对使用ipv6地址感到好奇,建议将其用法与ipv4结合使用,因为大多数网站和应用程序仍仅支持ipv4地址。\n\nipv4和ipv6使用的结合称为双重堆栈\n\n\n# 参考:\n\nipv4 vs. ipv6: what is the difference?", "charsets": { "cjk": true - } - }, - { - "title": "简述 iPv4 和 iPv6 的区别", - "frontmatter": { - "title": "简述 iPv4 和 iPv6 的区别", - "date": "2022-09-21T22:38:54.000Z", - "permalink": "/pages/6a2e50/", - "categories": [ - "计算机基础", - "计算机网络" - ], - "tags": [ - null - ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/200.%E7%AE%80%E8%BF%B0%20iPv4%20%E5%92%8C%20iPv6%20%E7%9A%84%E5%8C%BA%E5%88%AB.html", - "relativePath": "02.计算机基础/10.计算机网络/200.简述 iPv4 和 iPv6 的区别.md", - "key": "v-730f9a6a", - "path": "/pages/6a2e50/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {} + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "简述 JWT 的原理和校验机制", @@ -4912,18 +5255,20 @@ export const siteData = { } ], "headersStr": "什么是 Token(令牌) Acesss Token Refresh Token Token 和 Session 的区别 什么是 JWT 生成 JWT JWT 的原理 JWT 认证流程: JWT 的使用方式 方式一 方式二 方式三 Token 和 JWT 的区别 相同: 区别: 常见的前后端鉴权方式", - "content": "# 什么是 Token(令牌)\n\n\n# Acesss Token\n\n * 访问资源接口(API)时所需要的资源凭证\n * 简单 token 的组成: uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)\n * 特点:\n * 服务端无状态化、可扩展性好\n * 支持移动端设备\n * 安全\n * 支持跨程序调用\n * token 的身份验证流程:\n\n\n\n 1. 客户端使用用户名跟密码请求登录\n 2. 服务端收到请求,去验证用户名与密码\n 3. 验证成功后,服务端会签发一个 token 并把这个 token 发送给客户端\n 4. 客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localStorage 里\n 5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 token\n 6. 服务端收到请求,然后去验证客户端请求里面带着的 token ,如果验证成功,就向客户端返回请求的数据\n\n * 每一次请求都需要携带 token,需要把 token 放到 HTTP 的 Header 里\n * 基于 token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 token 数据。用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库\n * token 完全由应用管理,所以它可以避开同源策略\n\n\n# Refresh Token\n\n * 另外一种 token——refresh token\n * refresh token 是专用于刷新 access token 的 token。如果没有 refresh token,也可以刷新 access token,但每次刷新都要用户输入登录用户名与密码,会很麻烦。有了 refresh token,可以减少这个麻烦,客户端直接用 refresh token 去更新 access token,无需用户进行额外的操作。\n\n\n\n * Access Token 的有效期比较短,当 Acesss Token 由于过期而失效时,使用 Refresh Token 就可以获取到新的 Token,如果 Refresh Token 也失效了,用户就只能重新登录了。\n * Refresh Token 及过期时间是存储在服务器的数据库中,只有在申请新的 Acesss Token 时才会验证,不会对业务接口响应时间造成影响,也不需要向 Session 一样一直保持在内存中以应对大量的请求。\n\n\n# Token 和 Session 的区别\n\n * Session 是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息。而 Token 是令牌,访问资源接口(API)时所需要的资源凭证。Token 使服务端无状态化,不会存储会话信息。\n\n * Session 和 Token 并不矛盾,作为身份认证 Token 安全性比 Session 好,因为每一个请求都有签名还能防止监听以及重放攻击,而 Session 就必须依赖链路层来保障通讯安全了。如果你需要实现有状态的会话,仍然可以增加 Session 来在服务器端保存一些状态。\n\n * 所谓 Session 认证只是简单的把 User 信息存储到 Session 里,因为 SessionID 的不可预测性,暂且认为是安全的。而 Token ,如果指的是 OAuth Token 或类似的机制的话,提供的是 认证 和 授权 ,认证是针对用户,授权是针对 App 。其目的是让某 App 有权利访问某用户的信息。这里的 Token 是唯一的。不可以转移到其它 App上,也不可以转到其它用户上。Session 只提供一种简单的认证,即只要有此 SessionID ,即认为有此 User 的全部权利。是需要严格保密的,这个数据应该只保存在站方,不应该共享给其它网站或者第三方 App。所以简单来说:如果你的用户数据可能需要和第三方共享,或者允许第三方调用 API 接口,用 Token 。如果永远只是自己的网站,自己的 App,用什么就无所谓了。\n\n\n# 什么是 JWT\n\n * JSON Web Token(简称 JWT)是目前最流行的跨域认证解决方案。\n * 是一种认证授权机制。\n * JWT 是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准(RFC 7519)。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。 可以使用 HMAC 算法或者是 RSA 的公/私秘钥对 JWT 进行签名。因为数字签名的存在,这些传递的信息是可信的。 阮一峰老师的 JSON Web Token 入门教程 讲的非常通俗易懂,这里就不再班门弄斧了\n\n\n# 生成 JWT\n\n * https://jwt.io/\n * https://www.jsonwebtoken.io/\n\n\n# JWT 的原理\n\n\n\n\n# JWT 认证流程:\n\n * 用户输入用户名/密码登录,服务端认证成功后,会返回给客户端一个 JWT\n * 客户端将 token 保存到本地(通常使用 localstorage,也可以使用 cookie)\n * 当用户希望访问一个受保护的路由或者资源的时候,需要请求头的 Authorization 字段中使用Bearer 模式添加 JWT,其内容看起来是下面这样\n\nAuthorization: Bearer \n\n\n1\n\n * 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为\n * 因为 JWT 是自包含的(内部包含了一些会话信息),因此减少了需要查询数据库的需要\n * 因为 JWT 并不使用 Cookie 的,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)\n * 因为用户的状态不再存储在服务端的内存中,所以这是一种无状态的认证机制\n\n\n# JWT 的使用方式\n\n客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。\n\n\n# 方式一\n\n * 当用户希望访问一个受保护的路由或者资源的时候,可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求头信息的 Authorization 字段里,使用 Bearer 模式添加 JWT。\n\nGET /calendar/v1/events\nHost: api.example.com\nAuthorization: Bearer \n\n\n1\n2\n3\n\n * 用户的状态不会存储在服务端的内存中,这是一种 无状态的认证机制\n * 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为。\n * 由于 JWT 是自包含的,因此减少了需要查询数据库的需要\n * JWT 的这些特性使得我们可以完全依赖其无状态的特性提供数据 API 服务,甚至是创建一个下载流服务。\n * 因为 JWT 并不使用 Cookie ,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)\n\n\n# 方式二\n\n * 跨域的时候,可以把 JWT 放在 POST 请求的数据体里。\n\n\n# 方式三\n\n通过 URL 传输\n\nhttp://www.example.com/user?token=xxx\n\n\n1\n\n\n\n# Token 和 JWT 的区别\n\n\n# 相同:\n\n * 都是访问资源的令牌\n * 都可以记录用户的信息\n * 都是使服务端无状态化\n * 都是只有验证成功后,客户端才能访问服务端上受保护的资源\n\n\n# 区别:\n\n * Token:服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效。\n * JWT:将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。\n\n\n# 常见的前后端鉴权方式\n\nSession-Cookie Token 验证(包括 JWT,SSO) OAuth2.0(开放授权)", - "normalizedContent": "# 什么是 token(令牌)\n\n\n# acesss token\n\n * 访问资源接口(api)时所需要的资源凭证\n * 简单 token 的组成: uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)\n * 特点:\n * 服务端无状态化、可扩展性好\n * 支持移动端设备\n * 安全\n * 支持跨程序调用\n * token 的身份验证流程:\n\n\n\n 1. 客户端使用用户名跟密码请求登录\n 2. 服务端收到请求,去验证用户名与密码\n 3. 验证成功后,服务端会签发一个 token 并把这个 token 发送给客户端\n 4. 客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localstorage 里\n 5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 token\n 6. 服务端收到请求,然后去验证客户端请求里面带着的 token ,如果验证成功,就向客户端返回请求的数据\n\n * 每一次请求都需要携带 token,需要把 token 放到 http 的 header 里\n * 基于 token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 token 数据。用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库\n * token 完全由应用管理,所以它可以避开同源策略\n\n\n# refresh token\n\n * 另外一种 token——refresh token\n * refresh token 是专用于刷新 access token 的 token。如果没有 refresh token,也可以刷新 access token,但每次刷新都要用户输入登录用户名与密码,会很麻烦。有了 refresh token,可以减少这个麻烦,客户端直接用 refresh token 去更新 access token,无需用户进行额外的操作。\n\n\n\n * access token 的有效期比较短,当 acesss token 由于过期而失效时,使用 refresh token 就可以获取到新的 token,如果 refresh token 也失效了,用户就只能重新登录了。\n * refresh token 及过期时间是存储在服务器的数据库中,只有在申请新的 acesss token 时才会验证,不会对业务接口响应时间造成影响,也不需要向 session 一样一直保持在内存中以应对大量的请求。\n\n\n# token 和 session 的区别\n\n * session 是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息。而 token 是令牌,访问资源接口(api)时所需要的资源凭证。token 使服务端无状态化,不会存储会话信息。\n\n * session 和 token 并不矛盾,作为身份认证 token 安全性比 session 好,因为每一个请求都有签名还能防止监听以及重放攻击,而 session 就必须依赖链路层来保障通讯安全了。如果你需要实现有状态的会话,仍然可以增加 session 来在服务器端保存一些状态。\n\n * 所谓 session 认证只是简单的把 user 信息存储到 session 里,因为 sessionid 的不可预测性,暂且认为是安全的。而 token ,如果指的是 oauth token 或类似的机制的话,提供的是 认证 和 授权 ,认证是针对用户,授权是针对 app 。其目的是让某 app 有权利访问某用户的信息。这里的 token 是唯一的。不可以转移到其它 app上,也不可以转到其它用户上。session 只提供一种简单的认证,即只要有此 sessionid ,即认为有此 user 的全部权利。是需要严格保密的,这个数据应该只保存在站方,不应该共享给其它网站或者第三方 app。所以简单来说:如果你的用户数据可能需要和第三方共享,或者允许第三方调用 api 接口,用 token 。如果永远只是自己的网站,自己的 app,用什么就无所谓了。\n\n\n# 什么是 jwt\n\n * json web token(简称 jwt)是目前最流行的跨域认证解决方案。\n * 是一种认证授权机制。\n * jwt 是为了在网络应用环境间传递声明而执行的一种基于 json 的开放标准(rfc 7519)。jwt 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。 可以使用 hmac 算法或者是 rsa 的公/私秘钥对 jwt 进行签名。因为数字签名的存在,这些传递的信息是可信的。 阮一峰老师的 json web token 入门教程 讲的非常通俗易懂,这里就不再班门弄斧了\n\n\n# 生成 jwt\n\n * https://jwt.io/\n * https://www.jsonwebtoken.io/\n\n\n# jwt 的原理\n\n\n\n\n# jwt 认证流程:\n\n * 用户输入用户名/密码登录,服务端认证成功后,会返回给客户端一个 jwt\n * 客户端将 token 保存到本地(通常使用 localstorage,也可以使用 cookie)\n * 当用户希望访问一个受保护的路由或者资源的时候,需要请求头的 authorization 字段中使用bearer 模式添加 jwt,其内容看起来是下面这样\n\nauthorization: bearer \n\n\n1\n\n * 服务端的保护路由将会检查请求头 authorization 中的 jwt 信息,如果合法,则允许用户的行为\n * 因为 jwt 是自包含的(内部包含了一些会话信息),因此减少了需要查询数据库的需要\n * 因为 jwt 并不使用 cookie 的,所以你可以使用任何域名提供你的 api 服务而不需要担心跨域资源共享问题(cors)\n * 因为用户的状态不再存储在服务端的内存中,所以这是一种无状态的认证机制\n\n\n# jwt 的使用方式\n\n客户端收到服务器返回的 jwt,可以储存在 cookie 里面,也可以储存在 localstorage。\n\n\n# 方式一\n\n * 当用户希望访问一个受保护的路由或者资源的时候,可以把它放在 cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 http 请求头信息的 authorization 字段里,使用 bearer 模式添加 jwt。\n\nget /calendar/v1/events\nhost: api.example.com\nauthorization: bearer \n\n\n1\n2\n3\n\n * 用户的状态不会存储在服务端的内存中,这是一种 无状态的认证机制\n * 服务端的保护路由将会检查请求头 authorization 中的 jwt 信息,如果合法,则允许用户的行为。\n * 由于 jwt 是自包含的,因此减少了需要查询数据库的需要\n * jwt 的这些特性使得我们可以完全依赖其无状态的特性提供数据 api 服务,甚至是创建一个下载流服务。\n * 因为 jwt 并不使用 cookie ,所以你可以使用任何域名提供你的 api 服务而不需要担心跨域资源共享问题(cors)\n\n\n# 方式二\n\n * 跨域的时候,可以把 jwt 放在 post 请求的数据体里。\n\n\n# 方式三\n\n通过 url 传输\n\nhttp://www.example.com/user?token=xxx\n\n\n1\n\n\n\n# token 和 jwt 的区别\n\n\n# 相同:\n\n * 都是访问资源的令牌\n * 都可以记录用户的信息\n * 都是使服务端无状态化\n * 都是只有验证成功后,客户端才能访问服务端上受保护的资源\n\n\n# 区别:\n\n * token:服务端验证客户端发送过来的 token 时,还需要查询数据库获取用户信息,然后验证 token 是否有效。\n * jwt:将 token 和 payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 jwt 自己实现的)即可,不需要查询或者减少查询数据库,因为 jwt 自包含了用户信息和加密的数据。\n\n\n# 常见的前后端鉴权方式\n\nsession-cookie token 验证(包括 jwt,sso) oauth2.0(开放授权)", + "content": "# 什么是 Token(令牌)\n\n\n# Acesss Token\n\n * 访问资源接口(API)时所需要的资源凭证\n * 简单 token 的组成: uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)\n * 特点:\n * 服务端无状态化、可扩展性好\n * 支持移动端设备\n * 安全\n * 支持跨程序调用\n * token 的身份验证流程:\n\n\n\n 1. 客户端使用用户名跟密码请求登录\n 2. 服务端收到请求,去验证用户名与密码\n 3. 验证成功后,服务端会签发一个 token 并把这个 token 发送给客户端\n 4. 客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localStorage 里\n 5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 token\n 6. 服务端收到请求,然后去验证客户端请求里面带着的 token ,如果验证成功,就向客户端返回请求的数据\n\n * 每一次请求都需要携带 token,需要把 token 放到 HTTP 的 Header 里\n * 基于 token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 token 数据。用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库\n * token 完全由应用管理,所以它可以避开同源策略\n\n\n# Refresh Token\n\n * 另外一种 token——refresh token\n * refresh token 是专用于刷新 access token 的 token。如果没有 refresh token,也可以刷新 access token,但每次刷新都要用户输入登录用户名与密码,会很麻烦。有了 refresh token,可以减少这个麻烦,客户端直接用 refresh token 去更新 access token,无需用户进行额外的操作。\n\n\n\n * Access Token 的有效期比较短,当 Acesss Token 由于过期而失效时,使用 Refresh Token 就可以获取到新的 Token,如果 Refresh Token 也失效了,用户就只能重新登录了。\n * Refresh Token 及过期时间是存储在服务器的数据库中,只有在申请新的 Acesss Token 时才会验证,不会对业务接口响应时间造成影响,也不需要向 Session 一样一直保持在内存中以应对大量的请求。\n\n\n# Token 和 Session 的区别\n\n * Session 是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息。而 Token 是令牌,访问资源接口(API)时所需要的资源凭证。Token 使服务端无状态化,不会存储会话信息。\n\n * Session 和 Token 并不矛盾,作为身份认证 Token 安全性比 Session 好,因为每一个请求都有签名还能防止监听以及重放攻击,而 Session 就必须依赖链路层来保障通讯安全了。如果你需要实现有状态的会话,仍然可以增加 Session 来在服务器端保存一些状态。\n\n * 所谓 Session 认证只是简单的把 User 信息存储到 Session 里,因为 SessionID 的不可预测性,暂且认为是安全的。而 Token ,如果指的是 OAuth Token 或类似的机制的话,提供的是 认证 和 授权 ,认证是针对用户,授权是针对 App 。其目的是让某 App 有权利访问某用户的信息。这里的 Token 是唯一的。不可以转移到其它 App上,也不可以转到其它用户上。Session 只提供一种简单的认证,即只要有此 SessionID ,即认为有此 User 的全部权利。是需要严格保密的,这个数据应该只保存在站方,不应该共享给其它网站或者第三方 App。所以简单来说:如果你的用户数据可能需要和第三方共享,或者允许第三方调用 API 接口,用 Token 。如果永远只是自己的网站,自己的 App,用什么就无所谓了。\n\n\n# 什么是 JWT\n\n * JSON Web Token(简称 JWT)是目前最流行的跨域认证解决方案。\n * 是一种认证授权机制。\n * JWT 是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准(RFC 7519)。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。 可以使用 HMAC 算法或者是 RSA 的公/私秘钥对 JWT 进行签名。因为数字签名的存在,这些传递的信息是可信的。 阮一峰老师的 JSON Web Token 入门教程 讲的非常通俗易懂,这里就不再班门弄斧了\n\n\n# 生成 JWT\n\n * https://jwt.io/\n * https://www.jsonwebtoken.io/\n\n\n# JWT 的原理\n\n\n\n\n# JWT 认证流程:\n\n * 用户输入用户名/密码登录,服务端认证成功后,会返回给客户端一个 JWT\n * 客户端将 token 保存到本地(通常使用 localstorage,也可以使用 cookie)\n * 当用户希望访问一个受保护的路由或者资源的时候,需要请求头的 Authorization 字段中使用Bearer 模式添加 JWT,其内容看起来是下面这样\n\nAuthorization: Bearer \n\n\n1\n\n * 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为\n * 因为 JWT 是自包含的(内部包含了一些会话信息),因此减少了需要查询数据库的需要\n * 因为 JWT 并不使用 Cookie 的,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)\n * 因为用户的状态不再存储在服务端的内存中,所以这是一种无状态的认证机制\n\n\n# JWT 的使用方式\n\n客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。\n\n\n# 方式一\n\n * 当用户希望访问一个受保护的路由或者资源的时候,可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求头信息的 Authorization 字段里,使用 Bearer 模式添加 JWT。\n\nGET /calendar/v1/events\nHost: api.example.com\nAuthorization: Bearer \n\n\n1\n2\n3\n\n * 用户的状态不会存储在服务端的内存中,这是一种 无状态的认证机制\n * 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为。\n * 由于 JWT 是自包含的,因此减少了需要查询数据库的需要\n * JWT 的这些特性使得我们可以完全依赖其无状态的特性提供数据 API 服务,甚至是创建一个下载流服务。\n * 因为 JWT 并不使用 Cookie ,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)\n\n\n# 方式二\n\n * 跨域的时候,可以把 JWT 放在 POST 请求的数据体里。\n\n\n# 方式三\n\n通过 URL 传输\n\nhttp://www.example.com/user?token=xxx\n\n\n1\n\n\n\n# Token 和 JWT 的区别\n\n\n# 相同:\n\n * 都是访问资源的令牌\n * 都可以记录用户的信息\n * 都是使服务端无状态化\n * 都是只有验证成功后,客户端才能访问服务端上受保护的资源\n\n\n# 区别:\n\n * Token:服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效。\n * JWT:将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。\n\n\n# 常见的前后端鉴权方式\n\nSession-Cookie Token 验证(包括 JWT,SSO) OAuth2.0(开放授权)\n\n参考: 还分不清 Cookie、Session、Token、JWT?", + "normalizedContent": "# 什么是 token(令牌)\n\n\n# acesss token\n\n * 访问资源接口(api)时所需要的资源凭证\n * 简单 token 的组成: uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)\n * 特点:\n * 服务端无状态化、可扩展性好\n * 支持移动端设备\n * 安全\n * 支持跨程序调用\n * token 的身份验证流程:\n\n\n\n 1. 客户端使用用户名跟密码请求登录\n 2. 服务端收到请求,去验证用户名与密码\n 3. 验证成功后,服务端会签发一个 token 并把这个 token 发送给客户端\n 4. 客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localstorage 里\n 5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 token\n 6. 服务端收到请求,然后去验证客户端请求里面带着的 token ,如果验证成功,就向客户端返回请求的数据\n\n * 每一次请求都需要携带 token,需要把 token 放到 http 的 header 里\n * 基于 token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 token 数据。用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库\n * token 完全由应用管理,所以它可以避开同源策略\n\n\n# refresh token\n\n * 另外一种 token——refresh token\n * refresh token 是专用于刷新 access token 的 token。如果没有 refresh token,也可以刷新 access token,但每次刷新都要用户输入登录用户名与密码,会很麻烦。有了 refresh token,可以减少这个麻烦,客户端直接用 refresh token 去更新 access token,无需用户进行额外的操作。\n\n\n\n * access token 的有效期比较短,当 acesss token 由于过期而失效时,使用 refresh token 就可以获取到新的 token,如果 refresh token 也失效了,用户就只能重新登录了。\n * refresh token 及过期时间是存储在服务器的数据库中,只有在申请新的 acesss token 时才会验证,不会对业务接口响应时间造成影响,也不需要向 session 一样一直保持在内存中以应对大量的请求。\n\n\n# token 和 session 的区别\n\n * session 是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息。而 token 是令牌,访问资源接口(api)时所需要的资源凭证。token 使服务端无状态化,不会存储会话信息。\n\n * session 和 token 并不矛盾,作为身份认证 token 安全性比 session 好,因为每一个请求都有签名还能防止监听以及重放攻击,而 session 就必须依赖链路层来保障通讯安全了。如果你需要实现有状态的会话,仍然可以增加 session 来在服务器端保存一些状态。\n\n * 所谓 session 认证只是简单的把 user 信息存储到 session 里,因为 sessionid 的不可预测性,暂且认为是安全的。而 token ,如果指的是 oauth token 或类似的机制的话,提供的是 认证 和 授权 ,认证是针对用户,授权是针对 app 。其目的是让某 app 有权利访问某用户的信息。这里的 token 是唯一的。不可以转移到其它 app上,也不可以转到其它用户上。session 只提供一种简单的认证,即只要有此 sessionid ,即认为有此 user 的全部权利。是需要严格保密的,这个数据应该只保存在站方,不应该共享给其它网站或者第三方 app。所以简单来说:如果你的用户数据可能需要和第三方共享,或者允许第三方调用 api 接口,用 token 。如果永远只是自己的网站,自己的 app,用什么就无所谓了。\n\n\n# 什么是 jwt\n\n * json web token(简称 jwt)是目前最流行的跨域认证解决方案。\n * 是一种认证授权机制。\n * jwt 是为了在网络应用环境间传递声明而执行的一种基于 json 的开放标准(rfc 7519)。jwt 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。 可以使用 hmac 算法或者是 rsa 的公/私秘钥对 jwt 进行签名。因为数字签名的存在,这些传递的信息是可信的。 阮一峰老师的 json web token 入门教程 讲的非常通俗易懂,这里就不再班门弄斧了\n\n\n# 生成 jwt\n\n * https://jwt.io/\n * https://www.jsonwebtoken.io/\n\n\n# jwt 的原理\n\n\n\n\n# jwt 认证流程:\n\n * 用户输入用户名/密码登录,服务端认证成功后,会返回给客户端一个 jwt\n * 客户端将 token 保存到本地(通常使用 localstorage,也可以使用 cookie)\n * 当用户希望访问一个受保护的路由或者资源的时候,需要请求头的 authorization 字段中使用bearer 模式添加 jwt,其内容看起来是下面这样\n\nauthorization: bearer \n\n\n1\n\n * 服务端的保护路由将会检查请求头 authorization 中的 jwt 信息,如果合法,则允许用户的行为\n * 因为 jwt 是自包含的(内部包含了一些会话信息),因此减少了需要查询数据库的需要\n * 因为 jwt 并不使用 cookie 的,所以你可以使用任何域名提供你的 api 服务而不需要担心跨域资源共享问题(cors)\n * 因为用户的状态不再存储在服务端的内存中,所以这是一种无状态的认证机制\n\n\n# jwt 的使用方式\n\n客户端收到服务器返回的 jwt,可以储存在 cookie 里面,也可以储存在 localstorage。\n\n\n# 方式一\n\n * 当用户希望访问一个受保护的路由或者资源的时候,可以把它放在 cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 http 请求头信息的 authorization 字段里,使用 bearer 模式添加 jwt。\n\nget /calendar/v1/events\nhost: api.example.com\nauthorization: bearer \n\n\n1\n2\n3\n\n * 用户的状态不会存储在服务端的内存中,这是一种 无状态的认证机制\n * 服务端的保护路由将会检查请求头 authorization 中的 jwt 信息,如果合法,则允许用户的行为。\n * 由于 jwt 是自包含的,因此减少了需要查询数据库的需要\n * jwt 的这些特性使得我们可以完全依赖其无状态的特性提供数据 api 服务,甚至是创建一个下载流服务。\n * 因为 jwt 并不使用 cookie ,所以你可以使用任何域名提供你的 api 服务而不需要担心跨域资源共享问题(cors)\n\n\n# 方式二\n\n * 跨域的时候,可以把 jwt 放在 post 请求的数据体里。\n\n\n# 方式三\n\n通过 url 传输\n\nhttp://www.example.com/user?token=xxx\n\n\n1\n\n\n\n# token 和 jwt 的区别\n\n\n# 相同:\n\n * 都是访问资源的令牌\n * 都可以记录用户的信息\n * 都是使服务端无状态化\n * 都是只有验证成功后,客户端才能访问服务端上受保护的资源\n\n\n# 区别:\n\n * token:服务端验证客户端发送过来的 token 时,还需要查询数据库获取用户信息,然后验证 token 是否有效。\n * jwt:将 token 和 payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 jwt 自己实现的)即可,不需要查询或者减少查询数据库,因为 jwt 自包含了用户信息和加密的数据。\n\n\n# 常见的前后端鉴权方式\n\nsession-cookie token 验证(包括 jwt,sso) oauth2.0(开放授权)\n\n参考: 还分不清 cookie、session、token、jwt?", "charsets": { "cjk": true - } + }, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { - "title": "TCP 中 SYN 攻击是什么?如何防止?", + "title": "简述 DDOS 攻击原理,如何防范它?", "frontmatter": { - "title": "TCP 中 SYN 攻击是什么?如何防止?", - "date": "2022-09-21T22:39:09.000Z", - "permalink": "/pages/d0c6d3/", + "title": "简述 DDOS 攻击原理,如何防范它?", + "date": "2022-09-21T22:53:23.000Z", + "permalink": "/pages/056828/", "categories": [ "计算机基础", "计算机网络" @@ -4932,21 +5277,55 @@ export const siteData = { null ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/210.TCP%20%E4%B8%AD%20SYN%20%E6%94%BB%E5%87%BB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F%E5%A6%82%E4%BD%95%E9%98%B2%E6%AD%A2%EF%BC%9F.html", - "relativePath": "02.计算机基础/10.计算机网络/210.TCP 中 SYN 攻击是什么?如何防止?.md", - "key": "v-15fe5424", - "path": "/pages/d0c6d3/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {} + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/220.%E7%AE%80%E8%BF%B0%20DDOS%20%E6%94%BB%E5%87%BB%E5%8E%9F%E7%90%86%EF%BC%8C%E5%A6%82%E4%BD%95%E9%98%B2%E8%8C%83%E5%AE%83%EF%BC%9F.html", + "relativePath": "02.计算机基础/10.计算机网络/220.简述 DDOS 攻击原理,如何防范它?.md", + "key": "v-7fc8ca51", + "path": "/pages/056828/", + "headers": [ + { + "level": 2, + "title": "DDoS 攻击究竟是什么?", + "slug": "ddos-攻击究竟是什么", + "normalizedTitle": "ddos 攻击究竟是什么?", + "charIndex": 216 + }, + { + "level": 2, + "title": "举个例子", + "slug": "举个例子", + "normalizedTitle": "举个例子", + "charIndex": 458 + }, + { + "level": 2, + "title": "SYN Flood进行DDoS攻击的实现原理", + "slug": "syn-flood进行ddos攻击的实现原理", + "normalizedTitle": "syn flood进行ddos攻击的实现原理", + "charIndex": 766 + }, + { + "level": 2, + "title": "如何应对 DDoS 攻击?", + "slug": "如何应对-ddos-攻击", + "normalizedTitle": "如何应对 ddos 攻击?", + "charIndex": 1157 + } + ], + "headersStr": "DDoS 攻击究竟是什么? 举个例子 SYN Flood进行DDoS攻击的实现原理 如何应对 DDoS 攻击?", + "content": "其实,像全球互联网各大公司,均遭受过大量的 DDoS。\n\n2018年,GitHub 在一瞬间遭到高达 1.35Tbps 的带宽攻击。\n\n这次 DDoS 攻击几乎可以堪称是互联网有史以来规模最大、威力最大的 DDoS 攻击了。\n\n在 GitHub 遭到攻击后,仅仅一周后,DDoS 攻击又开始对 Google、亚马逊甚至 Pornhub 等网站进行了 DDoS 攻击。\n\n后续的 DDoS 攻击带宽最高也达到了 1Tbps。\n\n\n# DDoS 攻击究竟是什么?\n\nDDos 全名 Distributed Denial of Service,翻译成中文就是分布式拒绝服务。\n\n指的是处于不同位置的多个攻击者同时向一个或数个目标发动攻击,是一种分布的、协同的大规模攻击方式。\n\n单一的DoS攻击一般是采用一对一方式的,它利用网络协议和操作系统的一些缺陷,采用欺骗和伪装的策略来进行网络攻击,\n\n使网站服务器充斥大量要求回复的信息,消耗网络带宽或系统资源,导致网络或系统不胜负荷以至于瘫痪而停止提供正常的网络服务。\n\n\n# 举个例子\n\n我开了一家有五十个座位的重庆火锅店,由于用料上等,童叟无欺。\n\n平时门庭若市,生意特别红火,而对面二狗家的火锅店却无人问津。\n\n二狗为了对付我,想了一个办法,叫了五十个人来我的火锅店坐着却不点菜,让别的客人无法吃饭。\n\n上面这个例子讲的就是典型的 DDoS 攻击,一般来说是指攻击者利用“肉鸡”对目标网站在较短的时间内发起大量请求,\n\n大规模消耗目标网站的主机资源,让它无法正常服务。在线游戏、互联网金融等领域是 DDoS 攻击的高发行业。\n\n攻击方式很多,比如 ICMP Flood、UDP Flood、NTP Flood、SYN Flood、CC 攻击、DNS Query Flood等等。\n\n\n# SYN Flood进行DDoS攻击的实现原理\n\nSYN Flood 是一种利用 TCP 协议缺陷,发送大量伪造的 TCP 连接请求,从而使得被攻击方资源耗尽(CPU满负荷或内存不足)的攻击方式。\n\n一次正常的建立 TCP 连接,需要三次握手:客户端发送 SYN 报文,服务端收到请求并返回报文表示接受,客户端也返回确认,完成连接。\n\nSYN Flood 就是用户向服务器发送报文后突然死机或掉线,那么服务器在发出应答报文后就无法收到客户端的确认报文(第三次握手无法完成),这时服务器端一般会重试并等待一段时间后再丢弃这个未完成的连接。\n\n一个用户出现异常导致服务器的一个线程等待一会儿并不是大问题,但恶意攻击者大量模拟这种情况,服务器端为了维护数以万计的半连接而消耗非常多的资源,结果往往是无暇理睬客户的正常请求,甚至崩溃。从正常客户的角度看来,网站失去了响应,无法访问。\n\n\n# 如何应对 DDoS 攻击?\n\n 1. 高防服务器 还是拿开的重庆火锅店举例,高防服务器就是我给重庆火锅店增加了两名保安,这两名保安可以让保护店铺不受流氓骚扰,并且还会定期在店铺周围巡逻防止流氓骚扰。\n\n高防服务器主要是指能独立硬防御 50Gbps 以上的服务器,能够帮助网站拒绝服务攻击,定期扫描网络主节点等,这东西是不错,就是贵~\n\n 2. 黑名单 面对火锅店里面的流氓,我一怒之下将他们拍照入档,并禁止他们踏入店铺,但是有的时候遇到长得像的人也会禁止他进入店铺。这个就是设置黑名单,此方法秉承的就是“错杀一千,也不放一百”的原则,会封锁正常流量,影响到正常业务。\n\n 3. DDoS 清洗 DDos 清洗,就是我发现客人进店几分钟以后,但是一直不点餐,我就把他踢出店里。\n\nDDoS 清洗会对用户请求数据进行实时监控,及时发现 **DOS **攻击等异常流量,在不影响正常业务开展的情况下清洗掉这些异常流量。\n\n 4. CDN 加速 CDN 加速,我们可以这么理解:为了减少流氓骚扰,我干脆将火锅店开到了线上,承接外卖服务,这样流氓找不到店在哪里,也耍不来流氓了。\n\n在现实中,CDN 服务将网站访问流量分配到了各个节点中,这样一方面隐藏网站的真实 IP,另一方面即使遭遇 DDoS 攻击,也可以将流量分散到各个节点中,防止源站崩溃。", + "normalizedContent": "其实,像全球互联网各大公司,均遭受过大量的 ddos。\n\n2018年,github 在一瞬间遭到高达 1.35tbps 的带宽攻击。\n\n这次 ddos 攻击几乎可以堪称是互联网有史以来规模最大、威力最大的 ddos 攻击了。\n\n在 github 遭到攻击后,仅仅一周后,ddos 攻击又开始对 google、亚马逊甚至 pornhub 等网站进行了 ddos 攻击。\n\n后续的 ddos 攻击带宽最高也达到了 1tbps。\n\n\n# ddos 攻击究竟是什么?\n\nddos 全名 distributed denial of service,翻译成中文就是分布式拒绝服务。\n\n指的是处于不同位置的多个攻击者同时向一个或数个目标发动攻击,是一种分布的、协同的大规模攻击方式。\n\n单一的dos攻击一般是采用一对一方式的,它利用网络协议和操作系统的一些缺陷,采用欺骗和伪装的策略来进行网络攻击,\n\n使网站服务器充斥大量要求回复的信息,消耗网络带宽或系统资源,导致网络或系统不胜负荷以至于瘫痪而停止提供正常的网络服务。\n\n\n# 举个例子\n\n我开了一家有五十个座位的重庆火锅店,由于用料上等,童叟无欺。\n\n平时门庭若市,生意特别红火,而对面二狗家的火锅店却无人问津。\n\n二狗为了对付我,想了一个办法,叫了五十个人来我的火锅店坐着却不点菜,让别的客人无法吃饭。\n\n上面这个例子讲的就是典型的 ddos 攻击,一般来说是指攻击者利用“肉鸡”对目标网站在较短的时间内发起大量请求,\n\n大规模消耗目标网站的主机资源,让它无法正常服务。在线游戏、互联网金融等领域是 ddos 攻击的高发行业。\n\n攻击方式很多,比如 icmp flood、udp flood、ntp flood、syn flood、cc 攻击、dns query flood等等。\n\n\n# syn flood进行ddos攻击的实现原理\n\nsyn flood 是一种利用 tcp 协议缺陷,发送大量伪造的 tcp 连接请求,从而使得被攻击方资源耗尽(cpu满负荷或内存不足)的攻击方式。\n\n一次正常的建立 tcp 连接,需要三次握手:客户端发送 syn 报文,服务端收到请求并返回报文表示接受,客户端也返回确认,完成连接。\n\nsyn flood 就是用户向服务器发送报文后突然死机或掉线,那么服务器在发出应答报文后就无法收到客户端的确认报文(第三次握手无法完成),这时服务器端一般会重试并等待一段时间后再丢弃这个未完成的连接。\n\n一个用户出现异常导致服务器的一个线程等待一会儿并不是大问题,但恶意攻击者大量模拟这种情况,服务器端为了维护数以万计的半连接而消耗非常多的资源,结果往往是无暇理睬客户的正常请求,甚至崩溃。从正常客户的角度看来,网站失去了响应,无法访问。\n\n\n# 如何应对 ddos 攻击?\n\n 1. 高防服务器 还是拿开的重庆火锅店举例,高防服务器就是我给重庆火锅店增加了两名保安,这两名保安可以让保护店铺不受流氓骚扰,并且还会定期在店铺周围巡逻防止流氓骚扰。\n\n高防服务器主要是指能独立硬防御 50gbps 以上的服务器,能够帮助网站拒绝服务攻击,定期扫描网络主节点等,这东西是不错,就是贵~\n\n 2. 黑名单 面对火锅店里面的流氓,我一怒之下将他们拍照入档,并禁止他们踏入店铺,但是有的时候遇到长得像的人也会禁止他进入店铺。这个就是设置黑名单,此方法秉承的就是“错杀一千,也不放一百”的原则,会封锁正常流量,影响到正常业务。\n\n 3. ddos 清洗 ddos 清洗,就是我发现客人进店几分钟以后,但是一直不点餐,我就把他踢出店里。\n\nddos 清洗会对用户请求数据进行实时监控,及时发现 **dos **攻击等异常流量,在不影响正常业务开展的情况下清洗掉这些异常流量。\n\n 4. cdn 加速 cdn 加速,我们可以这么理解:为了减少流氓骚扰,我干脆将火锅店开到了线上,承接外卖服务,这样流氓找不到店在哪里,也耍不来流氓了。\n\n在现实中,cdn 服务将网站访问流量分配到了各个节点中,这样一方面隐藏网站的真实 ip,另一方面即使遭遇 ddos 攻击,也可以将流量分散到各个节点中,防止源站崩溃。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { - "title": "简述 DDOS 攻击原理,如何防范它?", + "title": "TCP 中 SYN 攻击是什么?如何防止?", "frontmatter": { - "title": "简述 DDOS 攻击原理,如何防范它?", - "date": "2022-09-21T22:53:23.000Z", - "permalink": "/pages/056828/", + "title": "TCP 中 SYN 攻击是什么?如何防止?", + "date": "2022-09-21T22:39:09.000Z", + "permalink": "/pages/d0c6d3/", "categories": [ "计算机基础", "计算机网络" @@ -4955,14 +5334,55 @@ export const siteData = { null ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/220.%E7%AE%80%E8%BF%B0%20DDOS%20%E6%94%BB%E5%87%BB%E5%8E%9F%E7%90%86%EF%BC%8C%E5%A6%82%E4%BD%95%E9%98%B2%E8%8C%83%E5%AE%83%EF%BC%9F.html", - "relativePath": "02.计算机基础/10.计算机网络/220.简述 DDOS 攻击原理,如何防范它?.md", - "key": "v-7fc8ca51", - "path": "/pages/056828/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {} + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/210.TCP%20%E4%B8%AD%20SYN%20%E6%94%BB%E5%87%BB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F%E5%A6%82%E4%BD%95%E9%98%B2%E6%AD%A2%EF%BC%9F.html", + "relativePath": "02.计算机基础/10.计算机网络/210.TCP 中 SYN 攻击是什么?如何防止?.md", + "key": "v-15fe5424", + "path": "/pages/d0c6d3/", + "headers": [ + { + "level": 2, + "title": "SYN Flooding简介", + "slug": "syn-flooding简介", + "normalizedTitle": "syn flooding简介", + "charIndex": 2 + }, + { + "level": 2, + "title": "TCP SYN Flood攻击原理", + "slug": "tcp-syn-flood攻击原理", + "normalizedTitle": "tcp syn flood攻击原理", + "charIndex": 361 + }, + { + "level": 2, + "title": "如何检测SYN攻击", + "slug": "如何检测syn攻击", + "normalizedTitle": "如何检测syn攻击", + "charIndex": 1553 + }, + { + "level": 2, + "title": "常见的防御SYN攻击方法", + "slug": "常见的防御syn攻击方法", + "normalizedTitle": "常见的防御syn攻击方法", + "charIndex": 1651 + }, + { + "level": 2, + "title": "参考", + "slug": "参考", + "normalizedTitle": "参考", + "charIndex": 1756 + } + ], + "headersStr": "SYN Flooding简介 TCP SYN Flood攻击原理 如何检测SYN攻击 常见的防御SYN攻击方法 参考", + "content": "# SYN Flooding简介\n\n拒绝服务攻击(DDoS)从1970 年出现直到今天都依然在作祟\n\n并给全球范围内的各大组织带来了不可估量的损失。\n\nSYN Flood是互联网上最经典的DDoS攻击方式之一,\n\n最早出现于 1999 年左右,雅虎是当时最著名的受害者。\n\nSYN Flood攻击利用了 TCP 三次握手的缺陷,能够以较小代价使目标服务器无法响应,且难以追查。\n\nSYN flood 是一种常见的 DOS(denial of service拒绝服务)和 DDos(distributed denial of serivce 分布式拒绝服务)攻击方式。\n\n这是一种使用TCP协议缺陷,发送大量的伪造的 TCP 连接请求,\n\n使得被攻击方 CPU 或内存资源耗尽,最终导致被攻击方无法提供正常的服务。\n\n\n# TCP SYN Flood攻击原理\n\nTCP SYN Flood 攻击利用的是 TCP 的三次握手(SYN -> SYN/ACK -> ACK),\n\n假设连接发起方是A,连接接受方是 B,即 B 在某个端口(Port)上监听A发出的连接请求,\n\n过程如下图所示,左边是A,右边是B。\n\n\n\nA 首先发送 SYN(Synchronization)消息给 B,要求 B 做好接收数据的准备;\n\nB 收到后反馈 SYN-ACK(Synchronization-Acknowledgement) 消息给A,这个消息的目的有两个:\n\n * 向 A 确认已做好接收数据的准备\n\n * 同时要求 A 也做好接收数据的准备,此时 B 已向 A 确认好接收状态,并等待 A 的确认,连接处于半开状态(Half-Open),顾名思义只开了一半;A 收到后再次发送ACK (Acknowledgement) 消息给B,向 B 确认也做好了接收数据的准备,至此三次握手完成,「连接」就建立了,\n\n大家注意到没有,最关键的一点在于双方是否都按对方的要求进入了可以接收消息的状态。而这个状态的确认主要是双方将要使用的消息序号(SquenceNum),TCP 为保证消息按发送顺序抵达接收方的上层应用,需要用消息序号来标记消息的发送先后顺序的。\n\nTCP是「双工」(Duplex)连接,同时支持双向通信,也就是双方同时可向对方发送消息,其中 SYN 和 SYN-ACK 消息开启了A→B的单向通信通道(B 获知了 A 的消息序号);SYN-ACK 和 ACK 消息开启了B→A单向通信通道(A获知了B的消息序号)。\n\n上面讨论的是双方在诚实守信,正常情况下的通信。\n\n但实际情况是,网络可能不稳定会丢包,使握手消息不能抵达对方,也可能是对方故意不按规矩来,故意延迟或不发送握手确认消息。\n\n假设 B 通过某 TCP 端口提供服务,B 在收到 A 的 SYN 消息时,积极的反馈了SYN-ACK 消息,\n\n使连接进入半开状态,因为 B 不确定自己发给 A 的 SYN-ACK 消息或 A 反馈的 ACK 消息是否会丢在半路,\n\n所以会给每个待完成的半开连接都设一个Timer,如果超过时间还没有收到 A 的 ACK 消息,\n\n则重新发送一次 SYN-ACK 消息给A,直到重试超过一定次数时才会放弃。\n\n\n\nB 为帮助 A 能顺利连接,需要分配内核资源维护半开连接,那么当 B 面临海量的连接 A 时,如上图所示,SYN Flood 攻击就形成了。攻击方 A 可以控制肉鸡向 B 发送大量 SYN 消息但不响应 ACK 消息,或者干脆伪造 SYN 消息中的 Source IP,使 B 反馈的 SYN-ACK 消息石沉大海,导致 B 被大量注定不能完成的半开连接占据,直到资源耗尽,停止响应正常的连接请求。\n\n\n# 如何检测SYN攻击\n\n查看服务器上是否能看到大量的半连接状态时,特别时源IP地址时随机的,基本可以断定这是一次SYN攻击\n\nnetstat -ntp|grep SYB_RECV\n\n\n1\n\n\n\n# 常见的防御SYN攻击方法\n\n * 缩短超时(SYN Timeout) 时间\n * 增加最大半连接数(SYN队列)\n * 过滤网关防护\n * SYN cookies技术\n * 减少syn+ack重传次数\n\n\n# 参考\n\nTCP/IP常见攻击手段", + "normalizedContent": "# syn flooding简介\n\n拒绝服务攻击(ddos)从1970 年出现直到今天都依然在作祟\n\n并给全球范围内的各大组织带来了不可估量的损失。\n\nsyn flood是互联网上最经典的ddos攻击方式之一,\n\n最早出现于 1999 年左右,雅虎是当时最著名的受害者。\n\nsyn flood攻击利用了 tcp 三次握手的缺陷,能够以较小代价使目标服务器无法响应,且难以追查。\n\nsyn flood 是一种常见的 dos(denial of service拒绝服务)和 ddos(distributed denial of serivce 分布式拒绝服务)攻击方式。\n\n这是一种使用tcp协议缺陷,发送大量的伪造的 tcp 连接请求,\n\n使得被攻击方 cpu 或内存资源耗尽,最终导致被攻击方无法提供正常的服务。\n\n\n# tcp syn flood攻击原理\n\ntcp syn flood 攻击利用的是 tcp 的三次握手(syn -> syn/ack -> ack),\n\n假设连接发起方是a,连接接受方是 b,即 b 在某个端口(port)上监听a发出的连接请求,\n\n过程如下图所示,左边是a,右边是b。\n\n\n\na 首先发送 syn(synchronization)消息给 b,要求 b 做好接收数据的准备;\n\nb 收到后反馈 syn-ack(synchronization-acknowledgement) 消息给a,这个消息的目的有两个:\n\n * 向 a 确认已做好接收数据的准备\n\n * 同时要求 a 也做好接收数据的准备,此时 b 已向 a 确认好接收状态,并等待 a 的确认,连接处于半开状态(half-open),顾名思义只开了一半;a 收到后再次发送ack (acknowledgement) 消息给b,向 b 确认也做好了接收数据的准备,至此三次握手完成,「连接」就建立了,\n\n大家注意到没有,最关键的一点在于双方是否都按对方的要求进入了可以接收消息的状态。而这个状态的确认主要是双方将要使用的消息序号(squencenum),tcp 为保证消息按发送顺序抵达接收方的上层应用,需要用消息序号来标记消息的发送先后顺序的。\n\ntcp是「双工」(duplex)连接,同时支持双向通信,也就是双方同时可向对方发送消息,其中 syn 和 syn-ack 消息开启了a→b的单向通信通道(b 获知了 a 的消息序号);syn-ack 和 ack 消息开启了b→a单向通信通道(a获知了b的消息序号)。\n\n上面讨论的是双方在诚实守信,正常情况下的通信。\n\n但实际情况是,网络可能不稳定会丢包,使握手消息不能抵达对方,也可能是对方故意不按规矩来,故意延迟或不发送握手确认消息。\n\n假设 b 通过某 tcp 端口提供服务,b 在收到 a 的 syn 消息时,积极的反馈了syn-ack 消息,\n\n使连接进入半开状态,因为 b 不确定自己发给 a 的 syn-ack 消息或 a 反馈的 ack 消息是否会丢在半路,\n\n所以会给每个待完成的半开连接都设一个timer,如果超过时间还没有收到 a 的 ack 消息,\n\n则重新发送一次 syn-ack 消息给a,直到重试超过一定次数时才会放弃。\n\n\n\nb 为帮助 a 能顺利连接,需要分配内核资源维护半开连接,那么当 b 面临海量的连接 a 时,如上图所示,syn flood 攻击就形成了。攻击方 a 可以控制肉鸡向 b 发送大量 syn 消息但不响应 ack 消息,或者干脆伪造 syn 消息中的 source ip,使 b 反馈的 syn-ack 消息石沉大海,导致 b 被大量注定不能完成的半开连接占据,直到资源耗尽,停止响应正常的连接请求。\n\n\n# 如何检测syn攻击\n\n查看服务器上是否能看到大量的半连接状态时,特别时源ip地址时随机的,基本可以断定这是一次syn攻击\n\nnetstat -ntp|grep syb_recv\n\n\n1\n\n\n\n# 常见的防御syn攻击方法\n\n * 缩短超时(syn timeout) 时间\n * 增加最大半连接数(syn队列)\n * 过滤网关防护\n * syn cookies技术\n * 减少syn+ack重传次数\n\n\n# 参考\n\ntcp/ip常见攻击手段", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "什么是 ARP 协议?简述其使用场景", @@ -4982,10 +5402,78 @@ export const siteData = { "relativePath": "02.计算机基础/10.计算机网络/230.什么是 ARP 协议?简述其使用场景.md", "key": "v-01df2b5d", "path": "/pages/665162/", + "headers": [ + { + "level": 2, + "title": "什么是 Mac 地址?", + "slug": "什么是-mac-地址", + "normalizedTitle": "什么是 mac 地址?", + "charIndex": 2 + }, + { + "level": 2, + "title": "ARP 协议解决了什么问题地位如何?", + "slug": "arp-协议解决了什么问题地位如何", + "normalizedTitle": "arp 协议解决了什么问题地位如何?", + "charIndex": 832 + }, + { + "level": 2, + "title": "ARP 协议工作原理", + "slug": "arp-协议工作原理", + "normalizedTitle": "arp 协议工作原理", + "charIndex": 1036 + }, + { + "level": 3, + "title": "同一局域网内的 MAC 寻址", + "slug": "同一局域网内的-mac-寻址", + "normalizedTitle": "同一局域网内的 mac 寻址", + "charIndex": 1237 + }, + { + "level": 3, + "title": "不同局域网内的 MAC 寻址", + "slug": "不同局域网内的-mac-寻址", + "normalizedTitle": "不同局域网内的 mac 寻址", + "charIndex": 2663 + } + ], + "headersStr": "什么是 Mac 地址? ARP 协议解决了什么问题地位如何? ARP 协议工作原理 同一局域网内的 MAC 寻址 不同局域网内的 MAC 寻址", + "content": "# 什么是 Mac 地址?\n\nMAC 地址的全称是 媒体访问控制地址(Media Access Control Address)。\n\n如果说,互联网中每一个资源都由 IP 地址唯一标识(IP 协议内容),那么一切网络设备都由 MAC 地址唯一标识。\n\n\n\n路由器的背面就会注明 MAC 位址.\n\n可以理解为,MAC 地址是一个网络设备真正的身份证号,IP 地址只是一种不重复的定位方式(比如说住在某省某市某街道的张三,这种逻辑定位是 IP 地址,他的身份证号才是他的 MAC 地址),也可以理解为 MAC 地址是身份证号,IP 地址是邮政地址。MAC 地址也有一些别称,如 LAN 地址、物理地址、以太网地址等。\n\n还有一点要知道的是,不仅仅是网络资源才有 IP 地址,网络设备也有 IP 地址,比如路由器。但从结构上说,路由器等网络设备的作用是组成一个网络,而且通常是内网,所以它们使用的 IP 地址通常是内网 IP,内网的设备在与内网以外的设备进行通信时,需要用到 NAT 协议。 MAC 地址的长度为 6 字节(48 比特),地址空间大小有 280 万亿之多($2^{48}$),MAC 地址由 IEEE 统一管理与分配,理论上,一个网络设备中的网卡上的 MAC 地址是永久的。不同的网卡生产商从 IEEE 那里购买自己的 MAC 地址空间(MAC 的前 24 比特),也就是前 24 比特由 IEEE 统一管理,保证不会重复。而后 24 比特,由各家生产商自己管理,同样保证生产的两块网卡的 MAC 地址不会重复。\n\nMAC 地址具有可携带性、永久性,身份证号永久地标识一个人的身份,不论他到哪里都不会改变。而 IP 地址不具有这些性质,当一台设备更换了网络,它的 IP 地址也就可能发生改变,也就是它在互联网中的定位发生了变化。\n\n最后,记住,MAC 地址有一个特殊地址:FF-FF-FF-FF-FF-FF(全 1 地址),该地址表示广播地址。\n\n\n# ARP 协议解决了什么问题地位如何?\n\nARP 协议,全称 地址解析协议(Address Resolution Protocol),它解决的是网络层地址和链路层地址之间的转换问题。\n\n因为一个 IP 数据报在物理上传输的过程中,总是需要知道下一跳(物理上的下一个目的地)该去往何处,但 IP 地址属于逻辑地址,而 MAC 地址才是物理地址.\n\nARP 协议解决了 IP 地址转 MAC 地址的问题。\n\n\n# ARP 协议工作原理\n\nARP 协议工作时有一个大前提,那就是 ARP 表。\n\n在一个局域网内,每个网络设备都自己维护了一个 ARP 表,ARP 表记录了某些其他网络设备的 IP 地址-MAC 地址映射关系,该映射关系以 三元组的形式存储。其中,TTL 为该映射关系的生存周期,典型值为 20 分钟,超过该时间,该条目将被丢弃。\n\nARP 的工作原理将分两种场景讨论:\n\n同一局域网内的 MAC 寻址; 从一个局域网到另一个局域网中的网络设备的寻址。\n\n\n# 同一局域网内的 MAC 寻址\n\n假设当前有如下场景:IP 地址为137.196.7.23的主机 A,想要给同一局域网内的 IP 地址为137.196.7.14主机 B,发送 IP 数据报文。\n\n再次强调,当主机发送 IP 数据报文时(网络层),仅知道目的地的 IP 地址,并不清楚目的地的 MAC 地址,而 ARP 协议就是解决这一问题的。 为了达成这一目标,主机 A 将不得不通过 ARP 协议来获取主机 B 的 MAC 地址,并将 IP 报文封装成链路层帧,发送到下一跳上。在该局域网内,关于此将按照时间顺序,依次发生如下事件:\n\n 1. 主机 A 检索自己的 ARP 表,发现 ARP 表中并无主机 B 的 IP 地址对应的映射条目,也就无从知道主机 B 的 MAC 地址。\n\n 2. 主机 A 将构造一个 ARP 查询分组,并将其广播到所在的局域网中。\n\nARP 分组是一种特殊报文,ARP 分组有两类,一种是查询分组,另一种是响应分组,它们具有相同的格式,均包含了发送和接收的 IP 地址、发送和接收的 MAC 地址。当然了,查询分组中,发送的 IP 地址,即为主机 A 的 IP 地址,接收的 IP 地址即为主机 B 的 IP 地址,发送的 MAC 地址也是主机 A 的 MAC 地址,但接收的 MAC 地址绝不会是主机 B 的 MAC 地址(因为这正是我们要问询的!),而是一个特殊值——FF-FF-FF-FF-FF-FF,之前说过,该 MAC 地址是广播地址,也就是说,查询分组将广播给该局域网内的所有设备。\n\n 3. 主机 A 构造的查询分组将在该局域网内广播,理论上,每一个设备都会收到该分组,并检查查询分组的接收 IP 地址是否为自己的 IP 地址,如果是,说明查询分组已经到达了主机 B,否则,该查询分组对当前设备无效,丢弃之。\n\n 4. 主机 B 收到了查询分组之后,验证是对自己的问询,接着构造一个 ARP 响应分组,该分组的目的地只有一个——主机 A,发送给主机 A。同时,主机 B 提取查询分组中的 IP 地址和 MAC 地址信息,在自己的 ARP 表中构造一条主机 A 的 IP-MAC 映射记录。\n\nARP 响应分组具有和 ARP 查询分组相同的构造,不同的是,发送和接受的 IP 地址恰恰相反,发送的 MAC 地址为发送者本身,目标 MAC 地址为查询分组的发送者,也就是说,ARP 响应分组只有一个目的地,而非广播。\n\n 5. 主机 A 终将收到主机 B 的响应分组,提取出该分组中的 IP 地址和 MAC 地址后,构造映射信息,加入到自己的 ARP 表中。\n\n\n\n在整个过程中,有几点需要补充说明的是:\n\n 1. 主机 A 想要给主机 B 发送 IP 数据报,如果主机 B 的 IP-MAC 映射信息已经存在于主机 A 的 ARP 表中,那么主机 A 无需广播,只需提取 MAC 地址并构造链路层帧发送即可。\n\n 2. ARP 表中的映射信息是有生存周期的,典型值为 20 分钟。\n\n 3. 目标主机接收到了问询主机构造的问询报文后,将先把问询主机的 IP-MAC 映射存进自己的 ARP 表中,这样才能获取到响应的目标 MAC 地址,顺利的发送响应分组。\n\n总结来说,ARP 协议是一个广播问询,单播响应协议。\n\n\n# 不同局域网内的 MAC 寻址\n\n更复杂的情况是,发送主机 A 和接收主机 B 不在同一个子网中,\n\n假设一个一般场景,两台主机所在的子网由一台路由器联通。这里需要注意的是,\n\n一般情况下,我们说网络设备都有一个 IP 地址和一个 MAC 地址,这里说的网络设备,\n\n更严谨的说法应该是一个接口。路由器作为互联设备,具有多个接口,每个接口同样也应该具备不重复的 IP 地址和 MAC 地址。\n\n因此,在讨论 ARP 表时,路由器的多个接口都个各自维护一个 ARP 表,而非一个路由器只维护一个 ARP 表。\n\n接下来,回顾同一子网内的 MAC 寻址,如果主机 A 发送一个广播问询分组,那么 A 所在子网内的所有设备(接口)都将不会捕获该分组,\n\n因为该分组的目的 IP 地址在另一个子网中,本子网内不会有设备成功接收。\n\n那么,主机 A 应该发送怎样的查询分组呢?整个过程按照时间顺序发生的事件如下:\n\n 1. 主机 A 查询 ARP 表,期望寻找到目标路由器的本子网接口的 MAC 地址。\n\n目标路由器指的是,根据目的主机 B 的 IP 地址,分析出 B 所在的子网,能够把报文转发到 B 所在子网的那个路由器。\n\n 2. 主机 A 未能找到目标路由器的本子网接口的 MAC 地址,将采用 ARP 协议,问询到该 MAC 地址,由于目标接口与主机 A 在同一个子网内,该过程与同一局域网内的 MAC 寻址相同。\n\n 3. 主机 A 获取到目标接口的 MAC 地址,先构造 IP 数据报,其中源 IP 是 A 的 IP 地址,目的 IP 地址是 B 的 IP 地址,再构造链路层帧,其中源 MAC 地址是 A 的 MAC 地址,目的 MAC 地址是本子网内与路由器连接的接口的 MAC 地址。主机 A 将把这个链路层帧,以单播的方式,发送给目标接口。\n\n 4. 目标接口接收到了主机 A 发过来的链路层帧,解析,根据目的 IP 地址,查询转发表,将该 IP 数据报转发到与主机 B 所在子网相连的接口上。\n\n到此,该帧已经从主机 A 所在的子网,转移到了主机 B 所在的子网了。\n\n 5. 路由器接口查询 ARP 表,期望寻找到主机 B 的 MAC 地址。\n\n 6. 路由器接口如未能找到主机 B 的 MAC 地址,将采用 ARP 协议,广播问询,单播响应,获取到主机 B 的 MAC 地址。\n\n 7. 路由器接口将对 IP 数据报重新封装成链路层帧,目标 MAC 地址为主机 B 的 MAC 地址,单播发送,直到目的地。\n\n", + "normalizedContent": "# 什么是 mac 地址?\n\nmac 地址的全称是 媒体访问控制地址(media access control address)。\n\n如果说,互联网中每一个资源都由 ip 地址唯一标识(ip 协议内容),那么一切网络设备都由 mac 地址唯一标识。\n\n\n\n路由器的背面就会注明 mac 位址.\n\n可以理解为,mac 地址是一个网络设备真正的身份证号,ip 地址只是一种不重复的定位方式(比如说住在某省某市某街道的张三,这种逻辑定位是 ip 地址,他的身份证号才是他的 mac 地址),也可以理解为 mac 地址是身份证号,ip 地址是邮政地址。mac 地址也有一些别称,如 lan 地址、物理地址、以太网地址等。\n\n还有一点要知道的是,不仅仅是网络资源才有 ip 地址,网络设备也有 ip 地址,比如路由器。但从结构上说,路由器等网络设备的作用是组成一个网络,而且通常是内网,所以它们使用的 ip 地址通常是内网 ip,内网的设备在与内网以外的设备进行通信时,需要用到 nat 协议。 mac 地址的长度为 6 字节(48 比特),地址空间大小有 280 万亿之多($2^{48}$),mac 地址由 ieee 统一管理与分配,理论上,一个网络设备中的网卡上的 mac 地址是永久的。不同的网卡生产商从 ieee 那里购买自己的 mac 地址空间(mac 的前 24 比特),也就是前 24 比特由 ieee 统一管理,保证不会重复。而后 24 比特,由各家生产商自己管理,同样保证生产的两块网卡的 mac 地址不会重复。\n\nmac 地址具有可携带性、永久性,身份证号永久地标识一个人的身份,不论他到哪里都不会改变。而 ip 地址不具有这些性质,当一台设备更换了网络,它的 ip 地址也就可能发生改变,也就是它在互联网中的定位发生了变化。\n\n最后,记住,mac 地址有一个特殊地址:ff-ff-ff-ff-ff-ff(全 1 地址),该地址表示广播地址。\n\n\n# arp 协议解决了什么问题地位如何?\n\narp 协议,全称 地址解析协议(address resolution protocol),它解决的是网络层地址和链路层地址之间的转换问题。\n\n因为一个 ip 数据报在物理上传输的过程中,总是需要知道下一跳(物理上的下一个目的地)该去往何处,但 ip 地址属于逻辑地址,而 mac 地址才是物理地址.\n\narp 协议解决了 ip 地址转 mac 地址的问题。\n\n\n# arp 协议工作原理\n\narp 协议工作时有一个大前提,那就是 arp 表。\n\n在一个局域网内,每个网络设备都自己维护了一个 arp 表,arp 表记录了某些其他网络设备的 ip 地址-mac 地址映射关系,该映射关系以 三元组的形式存储。其中,ttl 为该映射关系的生存周期,典型值为 20 分钟,超过该时间,该条目将被丢弃。\n\narp 的工作原理将分两种场景讨论:\n\n同一局域网内的 mac 寻址; 从一个局域网到另一个局域网中的网络设备的寻址。\n\n\n# 同一局域网内的 mac 寻址\n\n假设当前有如下场景:ip 地址为137.196.7.23的主机 a,想要给同一局域网内的 ip 地址为137.196.7.14主机 b,发送 ip 数据报文。\n\n再次强调,当主机发送 ip 数据报文时(网络层),仅知道目的地的 ip 地址,并不清楚目的地的 mac 地址,而 arp 协议就是解决这一问题的。 为了达成这一目标,主机 a 将不得不通过 arp 协议来获取主机 b 的 mac 地址,并将 ip 报文封装成链路层帧,发送到下一跳上。在该局域网内,关于此将按照时间顺序,依次发生如下事件:\n\n 1. 主机 a 检索自己的 arp 表,发现 arp 表中并无主机 b 的 ip 地址对应的映射条目,也就无从知道主机 b 的 mac 地址。\n\n 2. 主机 a 将构造一个 arp 查询分组,并将其广播到所在的局域网中。\n\narp 分组是一种特殊报文,arp 分组有两类,一种是查询分组,另一种是响应分组,它们具有相同的格式,均包含了发送和接收的 ip 地址、发送和接收的 mac 地址。当然了,查询分组中,发送的 ip 地址,即为主机 a 的 ip 地址,接收的 ip 地址即为主机 b 的 ip 地址,发送的 mac 地址也是主机 a 的 mac 地址,但接收的 mac 地址绝不会是主机 b 的 mac 地址(因为这正是我们要问询的!),而是一个特殊值——ff-ff-ff-ff-ff-ff,之前说过,该 mac 地址是广播地址,也就是说,查询分组将广播给该局域网内的所有设备。\n\n 3. 主机 a 构造的查询分组将在该局域网内广播,理论上,每一个设备都会收到该分组,并检查查询分组的接收 ip 地址是否为自己的 ip 地址,如果是,说明查询分组已经到达了主机 b,否则,该查询分组对当前设备无效,丢弃之。\n\n 4. 主机 b 收到了查询分组之后,验证是对自己的问询,接着构造一个 arp 响应分组,该分组的目的地只有一个——主机 a,发送给主机 a。同时,主机 b 提取查询分组中的 ip 地址和 mac 地址信息,在自己的 arp 表中构造一条主机 a 的 ip-mac 映射记录。\n\narp 响应分组具有和 arp 查询分组相同的构造,不同的是,发送和接受的 ip 地址恰恰相反,发送的 mac 地址为发送者本身,目标 mac 地址为查询分组的发送者,也就是说,arp 响应分组只有一个目的地,而非广播。\n\n 5. 主机 a 终将收到主机 b 的响应分组,提取出该分组中的 ip 地址和 mac 地址后,构造映射信息,加入到自己的 arp 表中。\n\n\n\n在整个过程中,有几点需要补充说明的是:\n\n 1. 主机 a 想要给主机 b 发送 ip 数据报,如果主机 b 的 ip-mac 映射信息已经存在于主机 a 的 arp 表中,那么主机 a 无需广播,只需提取 mac 地址并构造链路层帧发送即可。\n\n 2. arp 表中的映射信息是有生存周期的,典型值为 20 分钟。\n\n 3. 目标主机接收到了问询主机构造的问询报文后,将先把问询主机的 ip-mac 映射存进自己的 arp 表中,这样才能获取到响应的目标 mac 地址,顺利的发送响应分组。\n\n总结来说,arp 协议是一个广播问询,单播响应协议。\n\n\n# 不同局域网内的 mac 寻址\n\n更复杂的情况是,发送主机 a 和接收主机 b 不在同一个子网中,\n\n假设一个一般场景,两台主机所在的子网由一台路由器联通。这里需要注意的是,\n\n一般情况下,我们说网络设备都有一个 ip 地址和一个 mac 地址,这里说的网络设备,\n\n更严谨的说法应该是一个接口。路由器作为互联设备,具有多个接口,每个接口同样也应该具备不重复的 ip 地址和 mac 地址。\n\n因此,在讨论 arp 表时,路由器的多个接口都个各自维护一个 arp 表,而非一个路由器只维护一个 arp 表。\n\n接下来,回顾同一子网内的 mac 寻址,如果主机 a 发送一个广播问询分组,那么 a 所在子网内的所有设备(接口)都将不会捕获该分组,\n\n因为该分组的目的 ip 地址在另一个子网中,本子网内不会有设备成功接收。\n\n那么,主机 a 应该发送怎样的查询分组呢?整个过程按照时间顺序发生的事件如下:\n\n 1. 主机 a 查询 arp 表,期望寻找到目标路由器的本子网接口的 mac 地址。\n\n目标路由器指的是,根据目的主机 b 的 ip 地址,分析出 b 所在的子网,能够把报文转发到 b 所在子网的那个路由器。\n\n 2. 主机 a 未能找到目标路由器的本子网接口的 mac 地址,将采用 arp 协议,问询到该 mac 地址,由于目标接口与主机 a 在同一个子网内,该过程与同一局域网内的 mac 寻址相同。\n\n 3. 主机 a 获取到目标接口的 mac 地址,先构造 ip 数据报,其中源 ip 是 a 的 ip 地址,目的 ip 地址是 b 的 ip 地址,再构造链路层帧,其中源 mac 地址是 a 的 mac 地址,目的 mac 地址是本子网内与路由器连接的接口的 mac 地址。主机 a 将把这个链路层帧,以单播的方式,发送给目标接口。\n\n 4. 目标接口接收到了主机 a 发过来的链路层帧,解析,根据目的 ip 地址,查询转发表,将该 ip 数据报转发到与主机 b 所在子网相连的接口上。\n\n到此,该帧已经从主机 a 所在的子网,转移到了主机 b 所在的子网了。\n\n 5. 路由器接口查询 arp 表,期望寻找到主机 b 的 mac 地址。\n\n 6. 路由器接口如未能找到主机 b 的 mac 地址,将采用 arp 协议,广播问询,单播响应,获取到主机 b 的 mac 地址。\n\n 7. 路由器接口将对 ip 数据报重新封装成链路层帧,目标 mac 地址为主机 b 的 mac 地址,单播发送,直到目的地。\n\n", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 + }, + { + "title": "什么是跨域,什么情况下会发生跨域请求?", + "frontmatter": { + "title": "什么是跨域,什么情况下会发生跨域请求?", + "date": "2022-09-21T22:54:11.000Z", + "permalink": "/pages/eadd05/", + "categories": [ + "计算机基础", + "计算机网络" + ], + "tags": [ + null + ] + }, + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/240.%E4%BB%80%E4%B9%88%E6%98%AF%E8%B7%A8%E5%9F%9F%EF%BC%8C%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E4%BC%9A%E5%8F%91%E7%94%9F%E8%B7%A8%E5%9F%9F%E8%AF%B7%E6%B1%82%EF%BC%9F.html", + "relativePath": "02.计算机基础/10.计算机网络/240.什么是跨域,什么情况下会发生跨域请求?.md", + "key": "v-7370facf", + "path": "/pages/eadd05/", "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {} + "content": "说起跨域请求,必须要了解浏览器的同源策略,同源策略/SOP(Same origin policy)是一种约定,由 Netscape 公司 1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到 XSS、CSFR 等攻击。所谓同源是指\"协议+域名+端口\"三者相同,即便两个不同的域名指向同一个 ip 地址,也非同源。\n\n解决跨域请求的常用方法是:\n\n * 通过代理来避免,比如使用 Nginx 在后端转发请求,避免了前端出现跨域的问题。\n * 通过 Jsonp 跨域\n * 其它跨域解决方案\n\n重点谈一下 Jsonp 跨域原理。浏览器的同源策略把跨域请求都禁止了,但是页面中的", + "normalizedContent": "说起跨域请求,必须要了解浏览器的同源策略,同源策略/sop(same origin policy)是一种约定,由 netscape 公司 1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到 xss、csfr 等攻击。所谓同源是指\"协议+域名+端口\"三者相同,即便两个不同的域名指向同一个 ip 地址,也非同源。\n\n解决跨域请求的常用方法是:\n\n * 通过代理来避免,比如使用 nginx 在后端转发请求,避免了前端出现跨域的问题。\n * 通过 jsonp 跨域\n * 其它跨域解决方案\n\n重点谈一下 jsonp 跨域原理。浏览器的同源策略把跨域请求都禁止了,但是页面中的", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "Cookie 和 Session 的关系和区别是什么?", @@ -5075,32 +5563,9 @@ export const siteData = { "normalizedContent": "# 什么是认证(authentication)\n\n通俗地讲就是验证当前用户的身份,证明“你是你自己”(比如:你每天上下班打卡,都需要通过指纹打卡,当你的指纹和系统里录入的指纹相匹配时,就打卡成功) 互联网中的认证:\n\n * 用户名密码登录\n * 邮箱发送登录链接\n * 手机号接收验证码\n * 只要你能收到邮箱/验证码,就默认你是账号的主人\n\n\n# 什么是授权(authorization)\n\n用户授予第三方应用访问该用户某些资源的权限\n\n * 你在安装手机应用的时候,app 会询问是否允许授予权限(访问相册、地理位置等权限)\n * 你在访问微信小程序时,当登录时,小程序会询问是否允许授予权限(获取昵称、头像、地区、性别等个人信息)\n * 实现授权的方式有:cookie、session、token、oauth\n\n\n# 什么是凭证(credentials)\n\n实现认证和授权的前提是需要一种媒介(证书) 来标记访问者的身份\n\n * 在战国时期,商鞅变法,发明了照身帖。照身帖由官府发放,是一块打磨光滑细密的竹板,上面刻有持有人的头像和籍贯信息。国人必须持有,如若没有就被认为是黑户,或者间谍之类的。\n * 在现实生活中,每个人都会有一张专属的居民身份证,是用于证明持有人身份的一种法定证件。通过身份证,我们可以办理手机卡/银行卡/个人贷款/交通出行等等,这就是认证的凭证。\n * 在互联网应用中,一般网站(如掘金)会有两种模式,游客模式和登录模式。游客模式下,可以正常浏览网站上面的文章,一旦想要点赞/收藏/分享文章,就需要登录或者注册账号。当用户登录成功后,服务器会给该用户使用的浏览器颁发一个令牌(token),这个令牌用来表明你的身份,每次浏览器发送请求时会带上这个令牌,就可以使用游客模式下无法使用的功能。\n\n\n# 什么是 cookie\n\n * http 是无状态的协议(对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息):每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。所以服务器与浏览器为了进行会话跟踪(知道是谁在访问我),就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器。而这个状态需要通过 cookie 或者 session 去实现。\n * cookie 存储在客户端: cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。 cookie 是不可跨域的: 每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的(靠的是 domain)。\n\n\n# 什么是 session\n\n * session 是另一种记录服务器和客户端会话状态的机制\n * session 是基于 cookie 实现的,session 存储在服务器端,sessionid 会被存储到客户端的cookie 中\n\n\n\nsession 认证流程:\n\n * 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 session\n * 请求返回时将此 session 的唯一标识信息 sessionid 返回给浏览器\n * 浏览器接收到服务器返回的 sessionid 信息后,会将此信息存入到 cookie 中,同时 cookie 记录此 sessionid 属于哪个域名\n * 当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 cookie 信息,如果存在自动将 cookie 信息也发送给服务端,服务端会从 cookie 中获取 sessionid,再根据 sessionid 查找对应的 session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 session 证明用户已经登录可执行后面操作。\n\n根据以上流程可知,sessionid 是连接 cookie 和 session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。\n\n\n# 二:cookie和session的区别?\n\n存储位置不同:cookie保存在客户端浏览器,session保存在服务器端;\n\n存取方式不同:cookie只能保存ascii(是基于拉丁字母的一套电脑编码系统),session能保存任意数据类型;\n\n有效期不同:cookie中的数据可以长时间存在,session中的数据在会话结束或者浏览器关闭时失效;\n\n数据安全性不同:因为cookie是保存在浏览器中的,数据安全性相对较差;session是存储在服务器端的,安全性相对较高;\n\n存储大小不同:cookie一般保存的数据大小不会超过4k;而session理论上来说没有限制;\n\n\n# 三:为什么需要cookie和session,它们的联系是什么?\n\n因为浏览器是无状态的(http协议是无状态),浏览器并不知道是谁在跟服务器联系,这个时候就需要一个机制来告诉服务器,是谁在操作浏览器,操作人是否登录等,要实现这个机制,就需要cookie和session配合完成;\n\n当用户第一次访问服务器时,服务器会根据用户提交的信息生成session,然后给浏览器返回一个sessionid,浏览器接收到这个sessionid后,保存在cookie中,同时cookie记录此sessionid属于哪个域名;\n\n用户再次访问服务器时,请求会自动判断此域名下是否用cookie信息,如果存在cookie,则将cookie发送给服务器,服务器解析cookie拿到sessionid,然后根据sessionid查询session信息,如果能找到session信息,则可以执行后面的操作,如果没有则说明没有登录或者登录已经失效;\n\nsessionid是联系cookie和session的桥梁,大部分系统也是通过这个来判断用户是否登录;\n\n\n# 四:如果浏览器禁止使用cookie,那如何保证判断用户是否登录的机制正常执行?\n\n第一种方案,每次请求中都携带一个 sessionid 的参数,也可以 post 的方式提交,也可以在请求的地址后面拼接 xxx?sessionid=123456...。\n\n第二种方案,token 机制。token 机制多用于 app 客户端和服务器交互的模式,也可以用于 web 端做用户状态管理。\n\ntoken 的意思是“令牌”,是服务端生成的一串字符串,作为客户端进行请求的一个标识。token 机制和 cookie 和 session 的使用机制比较类似。\n\n当用户第一次登录后,服务器根据提交的用户信息生成一个 token,响应时将 token 返回给客户端,以后客户端只需带上这个 token 前来请求数据即可,无需再次登录验证。\n\n\n# 五:如何考虑分布式 session 问题?\n\n在互联网公司为了可以支撑更大的流量,后端往往需要多台服务器共同来支撑前端用户请求,那如果用户在 a 服务器登录了,第二次请求跑到服务 b 就会出现登录失效问题。\n\n分布式 session 一般会有以下几种解决方案:\n\nnginx ip_hash 策略,服务端使用 nginx 代理,每个请求按访问 ip 的 hash 分配,这样来自同一 ip 固定访问一个后台服务器,避免了在服务器 a 创建 session,第二次分发到服务器 b 的现象。\n\nsession 复制,任何一个服务器上的 session 发生改变(增删改),该节点会把这个 session 的所有内容序列化,然后广播给所有其它节点。\n\n共享 session,服务端无状态话,将用户的 session 等信息使用缓存中间件来统一管理,保障分发到每一个服务器的响应结果都一致。\n\n建议采用第三种方案。\n\n参考: 还分不清 cookie、session、token、jwt?", "charsets": { "cjk": true - } - }, - { - "title": "什么是跨域,什么情况下会发生跨域请求?", - "frontmatter": { - "title": "什么是跨域,什么情况下会发生跨域请求?", - "date": "2022-09-21T22:54:11.000Z", - "permalink": "/pages/eadd05/", - "categories": [ - "计算机基础", - "计算机网络" - ], - "tags": [ - null - ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/240.%E4%BB%80%E4%B9%88%E6%98%AF%E8%B7%A8%E5%9F%9F%EF%BC%8C%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E4%BC%9A%E5%8F%91%E7%94%9F%E8%B7%A8%E5%9F%9F%E8%AF%B7%E6%B1%82%EF%BC%9F.html", - "relativePath": "02.计算机基础/10.计算机网络/240.什么是跨域,什么情况下会发生跨域请求?.md", - "key": "v-7370facf", - "path": "/pages/eadd05/", - "headersStr": null, - "content": "说起跨域请求,必须要了解浏览器的同源策略,同源策略/SOP(Same origin policy)是一种约定,由 Netscape 公司 1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到 XSS、CSFR 等攻击。所谓同源是指\"协议+域名+端口\"三者相同,即便两个不同的域名指向同一个 ip 地址,也非同源。\n\n解决跨域请求的常用方法是:\n\n * 通过代理来避免,比如使用 Nginx 在后端转发请求,避免了前端出现跨域的问题。\n * 通过 Jsonp 跨域\n * 其它跨域解决方案\n\n重点谈一下 Jsonp 跨域原理。浏览器的同源策略把跨域请求都禁止了,但是页面中的", - "normalizedContent": "说起跨域请求,必须要了解浏览器的同源策略,同源策略/sop(same origin policy)是一种约定,由 netscape 公司 1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到 xss、csfr 等攻击。所谓同源是指\"协议+域名+端口\"三者相同,即便两个不同的域名指向同一个 ip 地址,也非同源。\n\n解决跨域请求的常用方法是:\n\n * 通过代理来避免,比如使用 nginx 在后端转发请求,避免了前端出现跨域的问题。\n * 通过 jsonp 跨域\n * 其它跨域解决方案\n\n重点谈一下 jsonp 跨域原理。浏览器的同源策略把跨域请求都禁止了,但是页面中的", - "charsets": { - "cjk": true - } + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "DNS 查询服务器的基本流程是什么?DNS 劫持是什么?", @@ -5120,10 +5585,58 @@ export const siteData = { "relativePath": "02.计算机基础/10.计算机网络/270.DNS 查询服务器的基本流程是什么?DNS 劫持是什么?.md", "key": "v-a9abf6a0", "path": "/pages/017113/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {} + "headers": [ + { + "level": 2, + "title": "1、DNS是什么?", + "slug": "_1、dns是什么", + "normalizedTitle": "1、dns是什么?", + "charIndex": 2 + }, + { + "level": 2, + "title": "2、为什么会出DNS这种东西?", + "slug": "_2、为什么会出dns这种东西", + "normalizedTitle": "2、为什么会出dns这种东西?", + "charIndex": 53 + }, + { + "level": 2, + "title": "3、那DNS在我们生活中怎么用的?", + "slug": "_3、那dns在我们生活中怎么用的", + "normalizedTitle": "3、那dns在我们生活中怎么用的?", + "charIndex": 172 + }, + { + "level": 2, + "title": "4、DNS劫持是什么?", + "slug": "_4、dns劫持是什么", + "normalizedTitle": "4、dns劫持是什么?", + "charIndex": 719 + }, + { + "level": 2, + "title": "如何防止DNS劫持?", + "slug": "如何防止dns劫持", + "normalizedTitle": "如何防止dns劫持?", + "charIndex": 779 + }, + { + "level": 2, + "title": "DNS劫持具体有哪些技术手段呢?", + "slug": "dns劫持具体有哪些技术手段呢", + "normalizedTitle": "dns劫持具体有哪些技术手段呢?", + "charIndex": 888 + } + ], + "headersStr": "1、DNS是什么? 2、为什么会出DNS这种东西? 3、那DNS在我们生活中怎么用的? 4、DNS劫持是什么? 如何防止DNS劫持? DNS劫持具体有哪些技术手段呢?", + "content": "# 1、DNS是什么?\n\n域名解析系统,可以将域名与IP地址建立映射关系,通过域名得知目标IP。\n\n\n# 2、为什么会出DNS这种东西?\n\n在网络中,想要访问主机资源或者服务器资源需要知道目标的IP地址,但是IP地址又长数字又很容易记错,而且全世界那么多IP地址我也记不住,怎么办?就需要域名来代表一个IP地址,进行相互之间的映射关系\n\n\n# 3、那DNS在我们生活中怎么用的?\n\n比如:在我们平常访问百度在网页地址栏输入: 这样我们就会进入百度的主页。\n\n\n\nDNS实现过程\n\n 1. 第一步:当在浏览器上输入域名时,我们的PC机会自动提出域名解析请求,并将请求发送给我们所配置的域名服务器进行解析\n\n 2. 第二步:域名服务器收到我们PC机的域名解析请求值后,会先查询本地的缓存(DNS缓存),如果有对应的域名与IP地址条目,域名服务器会将结果直接发送给我们的PC机,不会进一步解析操作。\n\n 3. 第三步:但是如果域名服务器的本地缓存中没有该纪录,则本地域名服务器就会把请求发给根域名服务器进行解析,然后根域名服务器收到后再返回给本地域名服务器一个所查询域(根的子域) 的主域名服务器的地址给我们的PC机。\n\n 4. 第四步:本地服务器再向上一步返回的域名服务器发送请求,然后接受请求的域名服务器会继续查询自己的缓存,如果没有该纪录,则返回相关的下级的域名服务器的地址。\n\n 5. 第五步:持续进行第四步的查询解析操作,直至解析到对应的纪录。\n\n 6. 第六步:本地域名服务器把歇息到的纪录返回的结果保存到缓存,以备下一次使用,同时还将结果返回给客户机\n\n 7. 第七步:客户机收到结果后向向目标IP发出访问请求,进行访问\n\n\n# 4、DNS劫持是什么?\n\n当我们访问某个网页时,如输入百度的域名,但是进入页面时其他的网页,这就是DNS劫持。\n\n\n# 如何防止DNS劫持?\n\n通常我们使用的DNS服务器使用的是服务商域名服务器,我们出来可以向运营商反馈这种情况外,还可以将自己的DNS服务器切换为其他第三方DNS服务器,如百度DNS、阿里DNS等等都是可以的。\n\n\n# DNS劫持具体有哪些技术手段呢?\n\n 1. DNS缓存感染\n\n攻击者使用DNS请求将数据放入脆弱的DNS服务器的缓存。然后这些缓存信息在用户进行DNS访问时返回用户,将对通常的域名的访问引导到入侵者设定的钓鱼、挂马等页面,或通过伪造邮件或其他server服务取得用户密码信息会给客人带来进一步的侵害。\n\n 2. DNS信息劫持\n\nTCP/IP系统避免以诸如序列号等各种方式插入钓鱼数据,但是入侵者可以通过截取客户端与DNS服务器的交互来推测服务器响应于客户端的DNS查询ID。每个DNS报告包括相关联的16位ID号,并且DNS服务器从该ID号获取请求源位置。攻击者在DNS服务器前向用户发送虚假应答,欺骗客户访问恶意网站。\n\n 3. DNS重定向\n\n攻击者如果将权威DNS服务器重定向到恶意DNS服务器,那么被劫持域名的解析就完全置于攻击者的控制之下。\n\n 4. 盗用DNS服务器\n\n在该攻击中,攻击者不仅可以破坏DNS服务器还可以改变DNS记录,将DNS请求重定向到恶意网站。\n\n 5. 中间人(MiTM)DNS攻击\n\n在DNS劫持中还有一种类型是,攻击者执行中间人(MiTM)攻击以截断用户与DNS服务器之间的通信,并向恶意网站重定向用户,以提供不同的目标IP地址。\n\n 6. 流氓DNS服务器\n\n在此攻击中,攻击者可以破解DNS服务器,并更改DNS记录以将DNS请求重定向到恶意站点。", + "normalizedContent": "# 1、dns是什么?\n\n域名解析系统,可以将域名与ip地址建立映射关系,通过域名得知目标ip。\n\n\n# 2、为什么会出dns这种东西?\n\n在网络中,想要访问主机资源或者服务器资源需要知道目标的ip地址,但是ip地址又长数字又很容易记错,而且全世界那么多ip地址我也记不住,怎么办?就需要域名来代表一个ip地址,进行相互之间的映射关系\n\n\n# 3、那dns在我们生活中怎么用的?\n\n比如:在我们平常访问百度在网页地址栏输入: 这样我们就会进入百度的主页。\n\n\n\ndns实现过程\n\n 1. 第一步:当在浏览器上输入域名时,我们的pc机会自动提出域名解析请求,并将请求发送给我们所配置的域名服务器进行解析\n\n 2. 第二步:域名服务器收到我们pc机的域名解析请求值后,会先查询本地的缓存(dns缓存),如果有对应的域名与ip地址条目,域名服务器会将结果直接发送给我们的pc机,不会进一步解析操作。\n\n 3. 第三步:但是如果域名服务器的本地缓存中没有该纪录,则本地域名服务器就会把请求发给根域名服务器进行解析,然后根域名服务器收到后再返回给本地域名服务器一个所查询域(根的子域) 的主域名服务器的地址给我们的pc机。\n\n 4. 第四步:本地服务器再向上一步返回的域名服务器发送请求,然后接受请求的域名服务器会继续查询自己的缓存,如果没有该纪录,则返回相关的下级的域名服务器的地址。\n\n 5. 第五步:持续进行第四步的查询解析操作,直至解析到对应的纪录。\n\n 6. 第六步:本地域名服务器把歇息到的纪录返回的结果保存到缓存,以备下一次使用,同时还将结果返回给客户机\n\n 7. 第七步:客户机收到结果后向向目标ip发出访问请求,进行访问\n\n\n# 4、dns劫持是什么?\n\n当我们访问某个网页时,如输入百度的域名,但是进入页面时其他的网页,这就是dns劫持。\n\n\n# 如何防止dns劫持?\n\n通常我们使用的dns服务器使用的是服务商域名服务器,我们出来可以向运营商反馈这种情况外,还可以将自己的dns服务器切换为其他第三方dns服务器,如百度dns、阿里dns等等都是可以的。\n\n\n# dns劫持具体有哪些技术手段呢?\n\n 1. dns缓存感染\n\n攻击者使用dns请求将数据放入脆弱的dns服务器的缓存。然后这些缓存信息在用户进行dns访问时返回用户,将对通常的域名的访问引导到入侵者设定的钓鱼、挂马等页面,或通过伪造邮件或其他server服务取得用户密码信息会给客人带来进一步的侵害。\n\n 2. dns信息劫持\n\ntcp/ip系统避免以诸如序列号等各种方式插入钓鱼数据,但是入侵者可以通过截取客户端与dns服务器的交互来推测服务器响应于客户端的dns查询id。每个dns报告包括相关联的16位id号,并且dns服务器从该id号获取请求源位置。攻击者在dns服务器前向用户发送虚假应答,欺骗客户访问恶意网站。\n\n 3. dns重定向\n\n攻击者如果将权威dns服务器重定向到恶意dns服务器,那么被劫持域名的解析就完全置于攻击者的控制之下。\n\n 4. 盗用dns服务器\n\n在该攻击中,攻击者不仅可以破坏dns服务器还可以改变dns记录,将dns请求重定向到恶意网站。\n\n 5. 中间人(mitm)dns攻击\n\n在dns劫持中还有一种类型是,攻击者执行中间人(mitm)攻击以截断用户与dns服务器之间的通信,并向恶意网站重定向用户,以提供不同的目标ip地址。\n\n 6. 流氓dns服务器\n\n在此攻击中,攻击者可以破解dns服务器,并更改dns记录以将dns请求重定向到恶意站点。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "简述 WebSocket 是如何进行传输的", @@ -5143,10 +5656,66 @@ export const siteData = { "relativePath": "02.计算机基础/10.计算机网络/260.简述 WebSocket 是如何进行传输的.md", "key": "v-7707a260", "path": "/pages/172129/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {} + "headers": [ + { + "level": 2, + "title": "WebSocket 协议", + "slug": "websocket-协议", + "normalizedTitle": "websocket 协议", + "charIndex": 729 + }, + { + "level": 2, + "title": "WebSocket与TCP、Http协议之间的关系", + "slug": "websocket与tcp、http协议之间的关系", + "normalizedTitle": "websocket与tcp、http协议之间的关系", + "charIndex": 1550 + } + ], + "headersStr": "WebSocket 协议 WebSocket与TCP、Http协议之间的关系", + "content": "一、什么是 WebSocket ? WebSocket ——一种在 2011 年被互联网工程任务组( IETF )标准化的协议。\n\nWebSocket 解决了一个长期存在的问题:既然底层的协议( HTTP )是一个请求 / 响应模式的交互序列,\n\n那么如何实时地发布信息呢?\n\nAJAX 提供了一定程度上的改善,但是数据流仍然是由客户端所发送的请求驱动的。\n\n还有其他的一些或多或少的取巧方式(Comet) WebSocket 规范以及它的实现代表了对一种更加有效的解决方案的尝试。\n\n简单地说,WebSocket 提供了“在一个单个的 TCP 连接上提供双向的通信……结合 WebSocket API ……\n\n它为网页和远程服务器之间的双向通信提供了一种替代 HTTP 轮询的方案。\n\n但是最终它们仍然属于扩展性受限的变通之法。也就是说,WebSocket 在客户端和\n\n服务器之间提供了真正的双向数据交换。\n\nWebSocket 连接允许客户端和服务器之间进行全双工通信,\n\n以便任一方都可以通过建立的连接将数据推送到另一端。\n\nWebSocket 只需要建立一次连接,就可以一直保持连接状态。这相比于轮询方式的不停建立连接显然效率要大大提高。\n\nWeb 浏览器和服务器都必须实现 WebSockets 协议来建立和维护连接。 特点\n\n 1. HTML5 中的协议,实现与客户端与服务器双向,基于消息的文本或二进制数据通信\n\n 2. 适合于对数据的实时性要求比较强的场景,如通信、直播、共享桌面,特别适合于客户与服务频繁交互的情况下,如实时共享、多人协作等平台。\n\n 3. 采用新的协议,后端需要单独实现\n\n 4. 客户端并不是所有浏览器都支持\n\n\n# WebSocket 协议\n\nWebSocket协议是一种双向通信协议,它建立在TCP之上,同http一样通过TCP来传输数据,\n\n但是它和http最大的不同有两点:\n\n 1. WebSocket是一种双向通信协议,在建立连接后,WebSocket服务器和Browser/UA都能主动的向对方发送或接收数据,就像Socket一样,\n\n不同的是WebSocket是一种建立在Web基础上的一种简单模拟Socket的协议;\n\n 2. WebSocket需要通过握手连接,类似于TCP它也需要客户端和服务器端进行握手连接,连接成功后才能相互通信。\n\n下面是一个简单的建立握手的时序图:\n\n\n\n这里简单说明一下WebSocket握手的过程。\n\n当Web应用程序调用new WebSocket(url)接口时,Browser就开始了与地址为url的WebServer建立握手连接的过程。\n\n 1. Browser与WebSocket服务器通过TCP三次握手建立连接,如果这个建立连接失败,那么后面的过程就不会执行,Web应用程序将收到错误消息通知。\n\n 2. 在TCP建立连接成功后,Browser/UA通过http协议传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端。\n\n例如:\n\n\n\n 3. WebSocket服务器收到Browser/UA发送来的握手请求后,如果数据包数据和格式正确,客户端和服务器端的协议版本号匹配等等,\n\n就接受本次握手连接,并给出相应的数据回复,同样回复的数据包也是采用http协议传输。\n\n\n\n 4. Browser收到服务器回复的数据包后,如果数据包内容、格式都没有问题的话,就表示本次连接成功,触发onopen消息,此时Web开发者就可以在此时通过send接口想服务器发送数据。否则,握手连接失败,Web应用程序会收到onerror消息,并且能知道连接失败的原因。\n\n\n# WebSocket与TCP、Http协议之间的关系\n\nWebSocket与http协议一样都是基于TCP的,所以他们都是可靠的协议,\n\nWeb开发者调用的WebSocket的send函数在browser的实现中最终都是通过TCP的系统接口进行传输的。\n\nWebSocket和Http协议一样都属于应用层的协议,那么他们之间有没有什么关系呢?\n\n答案是肯定的,WebSocket在建立握手连接时,数据是通过http协议传输的,正如我们之前所看到的“GET/chat HTTP/1.1”,\n\n这里面用到的只是http协议一些简单的字段。但是在建立连接之后,真正的数据传输阶段是不需要http协议参与的。\n\n具体关系可以参考下图:\n\n", + "normalizedContent": "一、什么是 websocket ? websocket ——一种在 2011 年被互联网工程任务组( ietf )标准化的协议。\n\nwebsocket 解决了一个长期存在的问题:既然底层的协议( http )是一个请求 / 响应模式的交互序列,\n\n那么如何实时地发布信息呢?\n\najax 提供了一定程度上的改善,但是数据流仍然是由客户端所发送的请求驱动的。\n\n还有其他的一些或多或少的取巧方式(comet) websocket 规范以及它的实现代表了对一种更加有效的解决方案的尝试。\n\n简单地说,websocket 提供了“在一个单个的 tcp 连接上提供双向的通信……结合 websocket api ……\n\n它为网页和远程服务器之间的双向通信提供了一种替代 http 轮询的方案。\n\n但是最终它们仍然属于扩展性受限的变通之法。也就是说,websocket 在客户端和\n\n服务器之间提供了真正的双向数据交换。\n\nwebsocket 连接允许客户端和服务器之间进行全双工通信,\n\n以便任一方都可以通过建立的连接将数据推送到另一端。\n\nwebsocket 只需要建立一次连接,就可以一直保持连接状态。这相比于轮询方式的不停建立连接显然效率要大大提高。\n\nweb 浏览器和服务器都必须实现 websockets 协议来建立和维护连接。 特点\n\n 1. html5 中的协议,实现与客户端与服务器双向,基于消息的文本或二进制数据通信\n\n 2. 适合于对数据的实时性要求比较强的场景,如通信、直播、共享桌面,特别适合于客户与服务频繁交互的情况下,如实时共享、多人协作等平台。\n\n 3. 采用新的协议,后端需要单独实现\n\n 4. 客户端并不是所有浏览器都支持\n\n\n# websocket 协议\n\nwebsocket协议是一种双向通信协议,它建立在tcp之上,同http一样通过tcp来传输数据,\n\n但是它和http最大的不同有两点:\n\n 1. websocket是一种双向通信协议,在建立连接后,websocket服务器和browser/ua都能主动的向对方发送或接收数据,就像socket一样,\n\n不同的是websocket是一种建立在web基础上的一种简单模拟socket的协议;\n\n 2. websocket需要通过握手连接,类似于tcp它也需要客户端和服务器端进行握手连接,连接成功后才能相互通信。\n\n下面是一个简单的建立握手的时序图:\n\n\n\n这里简单说明一下websocket握手的过程。\n\n当web应用程序调用new websocket(url)接口时,browser就开始了与地址为url的webserver建立握手连接的过程。\n\n 1. browser与websocket服务器通过tcp三次握手建立连接,如果这个建立连接失败,那么后面的过程就不会执行,web应用程序将收到错误消息通知。\n\n 2. 在tcp建立连接成功后,browser/ua通过http协议传送websocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端。\n\n例如:\n\n\n\n 3. websocket服务器收到browser/ua发送来的握手请求后,如果数据包数据和格式正确,客户端和服务器端的协议版本号匹配等等,\n\n就接受本次握手连接,并给出相应的数据回复,同样回复的数据包也是采用http协议传输。\n\n\n\n 4. browser收到服务器回复的数据包后,如果数据包内容、格式都没有问题的话,就表示本次连接成功,触发onopen消息,此时web开发者就可以在此时通过send接口想服务器发送数据。否则,握手连接失败,web应用程序会收到onerror消息,并且能知道连接失败的原因。\n\n\n# websocket与tcp、http协议之间的关系\n\nwebsocket与http协议一样都是基于tcp的,所以他们都是可靠的协议,\n\nweb开发者调用的websocket的send函数在browser的实现中最终都是通过tcp的系统接口进行传输的。\n\nwebsocket和http协议一样都属于应用层的协议,那么他们之间有没有什么关系呢?\n\n答案是肯定的,websocket在建立握手连接时,数据是通过http协议传输的,正如我们之前所看到的“get/chat http/1.1”,\n\n这里面用到的只是http协议一些简单的字段。但是在建立连接之后,真正的数据传输阶段是不需要http协议参与的。\n\n具体关系可以参考下图:\n\n", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 + }, + { + "title": "简述 HTTP 报文头部的组成结构", + "frontmatter": { + "title": "简述 HTTP 报文头部的组成结构", + "date": "2022-09-21T22:55:20.000Z", + "permalink": "/pages/dfb780/", + "categories": [ + "计算机基础", + "计算机网络" + ], + "tags": [ + null + ] + }, + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/290.%E7%AE%80%E8%BF%B0%20HTTP%20%E6%8A%A5%E6%96%87%E5%A4%B4%E9%83%A8%E7%9A%84%E7%BB%84%E6%88%90%E7%BB%93%E6%9E%84.html", + "relativePath": "02.计算机基础/10.计算机网络/290.简述 HTTP 报文头部的组成结构.md", + "key": "v-51f24219", + "path": "/pages/dfb780/", + "headers": [ + { + "level": 2, + "title": "参考", + "slug": "参考", + "normalizedTitle": "参考", + "charIndex": 2 + } + ], + "headersStr": "参考", + "content": "# 参考\n\nHTTP 报文结构详解", + "normalizedContent": "# 参考\n\nhttp 报文结构详解", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "RestFul 是什么?RestFul 请求的 URL 有什么特点?", @@ -5166,33 +5735,44 @@ export const siteData = { "relativePath": "02.计算机基础/10.计算机网络/280.RestFul 是什么?RestFul 请求的 URL 有什么特点?.md", "key": "v-693fb010", "path": "/pages/c08bb6/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {} - }, - { - "title": "简述 HTTP 报文头部的组成结构", - "frontmatter": { - "title": "简述 HTTP 报文头部的组成结构", - "date": "2022-09-21T22:55:20.000Z", - "permalink": "/pages/dfb780/", - "categories": [ - "计算机基础", - "计算机网络" - ], - "tags": [ - null - ] + "headers": [ + { + "level": 2, + "title": "REST 设计风格", + "slug": "rest-设计风格", + "normalizedTitle": "rest 设计风格", + "charIndex": 2 + }, + { + "level": 2, + "title": "理解 REST", + "slug": "理解-rest", + "normalizedTitle": "理解 rest", + "charIndex": 611 + }, + { + "level": 2, + "title": "RESTful 的系统", + "slug": "restful-的系统", + "normalizedTitle": "restful 的系统", + "charIndex": 4659 + }, + { + "level": 2, + "title": "参考", + "slug": "参考", + "normalizedTitle": "参考", + "charIndex": 9427 + } + ], + "headersStr": "REST 设计风格 理解 REST RESTful 的系统 参考", + "content": "# REST 设计风格\n\n很多人会拿 REST 与 RPC 互相比较,其实,REST 无论是在思想上、概念上,还是使用范围上,与 RPC 都不尽相同,充其量只能算是有一些相似,\n\n应用会有一部分重合之处,但本质上并不是同一类型的东西。\n\nREST 与 RPC 在思想上差异的核心是抽象的目标不一样,即面向资源的编程思想与面向过程的编程思想两者之间的区别。\n\n面向过程编程、面向对象编程大家想必听说过,但什么是面向资源编程?\n\n这个问题等介绍完 REST 的特征之后我们再回头细说。\n\n而概念上的不同是指 REST 并不是一种远程服务调用协议,甚至可以把定语也去掉,它就不是一种协议。\n\n协议都带有一定的规范性和强制性,最起码也该有个规约文档,譬如 JSON-RPC,它哪怕再简单,也要有个《JSON-RPC Specification》 来规定协议的格式细节、异常、响应码等信息,\n\n但是 REST 并没有定义这些内容,尽管有一些指导原则,但实际上并不受任何强制的约束。\n\n常有人批评某个系统接口“设计得不够 RESTful”,其实这句话本身就有些争议,REST 只能说是风格而不是规范、协议,\n\n并且能完全达到 REST 所有指导原则的系统也是不多见的,这一点我们同样将在后文中详细讨论。\n\n至于使用范围,REST 与 RPC 作为主流的两种远程调用方式,在使用上是确有重合的,但重合的区域有多大就见仁见智了。\n\n\n# 理解 REST\n\n个人会有好恶偏爱,但计算机科学是务实的,有了 RPC,还会提出 REST,有了面向过程编程之后,还能产生面向资源编程,并引起广泛的关注、使用和讨论,\n\n说明后者一定是有一些前者没有的闪光点,或者解决、避免了一些面向过程中的缺陷。我们不妨先去理解 REST 为什么会出现,然后再来讨论评价它。\n\n简单搜索就能找到 REST 源于 Roy Thomas Fielding 在 2000 年发表的博士论文:《Architectural Styles and the Design of Network-based Software Architectures》 ,此文的确是 REST 的源头,但我们不应该忽略 Fielding 的身份和此前的工作背景,这些信息对理解 REST 的设计思想至关重要。\n\n首先,Fielding 是一名很优秀的软件工程师,他是 Apache 服务器的核心开发者,后来成为了著名的Apache 软件基金会 的联合创始人;\n\n同时,Fielding 也是 HTTP 1.0 协议(1996 年发布)的专家组成员,后来还晋升为 HTTP 1.1 协议(1999 年发布)的负责人。\n\nHTTP 1.1 协议设计得极为成功,以至于发布之后长达十年的时间里,都没有收到多少修订的意见。\n\n用来指导 HTTP 1.1 协议设计的理论和思想,最初是以备忘录的形式用作专家组成员之间交流,除了 IETF、W3C 的专家外,并没有在外界广泛流传。\n\n从时间上看,对 HTTP 1.1 协议的设计工作贯穿了 Fielding 的整个博士研究生涯,当起草 HTTP 1.1 协议的工作完成后,Fielding 回到了加州大学欧文分校继续攻读自己的博士学位。\n\n第二年,他更为系统、严谨地阐述了这套理论框架,并且以这套理论框架导出了一种新的编程思想,他为这种程序设计风格取了一个很多人难以理解,但是今天已经广为人知的名字 REST,即“表征状态转移”的缩写。\n\n哪怕对编程和网络都很熟悉的同学,只从标题中也不太可能直接弄明白什么叫“表征”、啥东西的“状态”、从哪“转移”到哪。\n\n尽管在论文原文中确有论述这些概念,但写得确实相当晦涩(不想读英文的同学从此处获得中文翻译版本),笔者推荐比较容易理解 REST 思想的途径是先理解什么是 HTTP,\n\n再配合一些实际例子来进行类比,你会发现“REST”(Representational State Transfer)实际上是“HTT”(Hypertext Transfer)的进一步抽象,两者就如同接口与实现类的关系一般。\n\nHTTP 中使用的“超文本”(Hypertext)一词是美国社会学家 Theodor Holm Nelson 在 1967 年于《Brief Words on the Hypertext》 一文里提出的,下面引用的是他本人在 1992 年修正后的定义:\n\n> Hypertext\n\n> By now the word \"hypertext\" has become generally accepted for branching and responding text, but the corresponding word \"hypermedia\", meaning complexes of branching and responding graphics, movies and sound – as well as text – is much less used.\n\n> 现在,\"超文本 \"一词已被普遍接受,它指的是能够进行分支判断和差异响应的文本,相应地, \"超媒体 \"一词指的是能够进行分支判断和差异响应的图形、电影和声音(也包括文本)的复合体。\n\n> —— Theodor Holm Nelson Literary Machines, 1992\n\n以上定义描述的“超文本(或超媒体,Hypermedia)”是一种“能够对操作进行判断和响应的文本(或声音、图像等)”,这个概念在上世纪 60 年代提出时应该还属于科幻的范畴,但是今天大众已经完全接受了它,互联网中一段文字可以点击、可以触发脚本执行、可以调用服务端,这一切已毫不稀奇。下面我们继续尝试从“超文本”或者“超媒体”的含义来理解什么是“表征”以及 REST 中其他关键概念,这里使用一个具体事例将其描述如下。\n\n * 资源(Resource):譬如你现在正在阅读一篇名为《REST 设计风格》的文章,这篇文章的内容本身(你可以将其理解为其蕴含的信息、数据)我们称之为“资源”。无论你是购买的书籍、是在浏览器看的网页、是打印出来看的文稿、是在电脑屏幕上阅读抑或是手机上浏览,尽管呈现的样子各不相同,但其中的信息是不变的,你所阅读的仍是同一份“资源”。\n\n * 表征(Representation):当你通过电脑浏览器阅读此文章时,浏览器向服务端发出请求“我需要这个资源的 HTML 格式”,服务端向浏览器返回的这个 HTML 就被称之为“表征”,你可能通过其他方式拿到本文的 PDF、Markdown、RSS 等其他形式的版本,它们也同样是一个资源的多种表征。可见“表征”这个概念是指信息与用户交互时的表示形式,这与我们软件分层架构中常说的“表示层”(Presentation Layer)的语义其实是一致的。\n\n * 状态(State):当你读完了这篇文章,想看后面是什么内容时,你向服务器发出请求“给我下一篇文章”。但是“下一篇”是个相对概念,必须依赖“当前你正在阅读的文章是哪一篇”才能正确回应,这类在特定语境中才能产生的上下文信息即被称为“状态”。我们所说的有状态(Stateful)抑或是无状态(Stateless),都是只相对于服务端来说的,服务器要完成“取下一篇”的请求,要么自己记住用户的状态:这个用户现在阅读的是哪一篇文章,这称为有状态;要么客户端来记住状态,在请求的时候明确告诉服务器:我正在阅读某某文章,现在要读它的下一篇,这称为无状态。\n\n * 转移(Transfer):无论状态是由服务端还是客户端来提供的,“取下一篇文章”这个行为逻辑必然只能由服务端来提供,因为只有服务端拥有该资源及其表征形式。服务器通过某种方式,把“用户当前阅读的文章”转变成“下一篇文章”,这就被称为“表征状态转移”。\n\n通过“阅读文章”这个例子,笔者对资源等概念进行通俗的释义,你应该能够理解 REST 所说的“表征状态转移”的含义了。借着这个故事的上下文状态,笔者再继续介绍几个现在不涉及但稍后要用到的概念名词。\n\n * 统一接口(Uniform Interface):上面说的服务器“通过某种方式”让表征状态发生转移,具体是什么方式?如果你真的是用浏览器阅读本文电子版的话,请把本文滚动到结尾处,右下角有下一篇文章的 URI 超链接地址,这是服务端渲染这篇文章时就预置好的,点击它让页面跳转到下一篇,就是所谓“某种方式”的其中一种方式。任何人都不会对点击超链接网页会出现跳转感到奇怪,但你细想一下,URI 的含义是统一资源标识符,是一个名词,如何能表达出“转移”动作的含义呢?答案是 HTTP 协议中已经提前约定好了一套“统一接口”,它包括:GET、HEAD、POST、PUT、DELETE、TRACE、OPTIONS 七种基本操作,任何一个支持 HTTP 协议的服务器都会遵守这套规定,对特定的 URI 采取这些操作,服务器就会触发相应的表征状态转移。\n\n * 超文本驱动(Hypertext Driven):尽管表征状态转移是由浏览器主动向服务器发出请求所引发的,该请求导致了“在浏览器屏幕上显示出了下一篇文章的内容”这个结果的出现。但是,你我都清楚这不可能真的是浏览器的主动意图,浏览器是根据用户输入的 URI 地址来找到网站首页,服务器给予的首页超文本内容后,浏览器再通过超文本内部的链接来导航到了这篇文章,阅读结束时,也是通过超文本内部的链接来再导航到下一篇。浏览器作为所有网站的通用的客户端,任何网站的导航(状态转移)行为都不可能是预置于浏览器代码之中,而是由服务器发出的请求响应信息(超文本)来驱动的。这点与其他带有客户端的软件有十分本质的区别,在那些软件中,业务逻辑往往是预置于程序代码之中的,有专门的页面控制器(无论在服务端还是在客户端中)来驱动页面的状态转移。\n\n * 自描述消息(Self-Descriptive Messages):由于资源的表征可能存在多种不同形态,在消息中应当有明确的信息来告知客户端该消息的类型以及应如何处理这条消息。一种被广泛采用的自描述方法是在名为“Content-Type”的 HTTP Header 中标识出互联网媒体类型(MIME type),譬如“Content-Type : application/json; charset=utf-8”,则说明该资源会以 JSON 的格式来返回,请使用 UTF-8 字符集进行处理。\n\n除了以上列出的这些看名字不容易弄懂的概念外,在理解 REST 的过程中,还有一个常见的误区值得注意,Fielding 提出 REST 时所谈论的范围是“架构风格与网络的软件架构设计”(Architectural Styles and Design of Network-based Software Architectures),而不是现在被人们所狭义理解的一种“远程服务设计风格”,这两者的范围差别就好比本书所谈论的话题“软件架构”与本章谈论话题“访问远程服务”的关系那样,前者是后者的一个很大的超集,尽管基于本节的主题和多数人的关注点考虑,我们确实是会以“远程服务设计风格”作为讨论的重点,但至少应该说明清楚它们范围上的差别。\n\n\n# RESTful 的系统\n\n如果你已经理解了上面这些概念,我们就可以开始讨论面向资源的编程思想与 Fielding 所提出的几个具体的软件架构设计原则了。Fielding 认为,一套理想的、完全满足 REST 风格的系统应该满足以下六大原则。\n\n 1. 服务端与客户端分离(Client-Server) 将用户界面所关注的逻辑和数据存储所关注的逻辑分离开来,有助于提高用户界面的跨平台的可移植性,这一点正越来越受到广大开发者所认可,以前完全基于服务端控制和渲染(如 JSF 这类)框架实际用户已甚少,而在服务端进行界面控制(Controller),通过服务端或者客户端的模版渲染引擎来进行界面渲染的框架(如 Struts、SpringMVC 这类)也受到了颇大的冲击。这一点主要推动力量与 REST 可能关系并不大,前端技术(从 ES 规范,到语言实现,到前端框架等)的近年来的高速发展,使得前端表达能力大幅度加强才是真正的幕后推手。由于前端的日渐强势,现在还流行起由前端代码反过来驱动服务端进行渲染的 SSR(Server-Side Rendering)技术,在 Serverless、SEO 等场景中已经占领了一块领地。\n\n 2. 无状态(Stateless) 无状态是 REST 的一条核心原则,部分开发者在做服务接口规划时,觉得 REST 风格的服务怎么设计都感觉别扭,很有可能的一种原因是在服务端持有着比较重的状态。REST 希望服务器不要去负责维护状态,每一次从客户端发送的请求中,应包括所有的必要的上下文信息,会话信息也由客户端负责保存维护,服务端依据客户端传递的状态来执行业务处理逻辑,驱动整个应用的状态变迁。客户端承担状态维护职责以后,会产生一些新的问题,譬如身份认证、授权等可信问题,它们都应有针对性的解决方案(这部分内容可参见“安全架构”的内容)。 但必须承认的现状是,目前大多数的系统都达不到这个要求,往往越复杂、越大型的系统越是如此。服务端无状态可以在分布式计算中获得非常高价值的好处,但大型系统的上下文状态数量完全可能膨胀到让客户端在每次请求时提供变得不切实际的程度,在服务端的内存、会话、数据库或者缓存等地方持有一定的状态成为一种是事实上存在,并将长期存在、被广泛使用的主流的方案。\n\n 3. 可缓存(Cacheability) 无状态服务虽然提升了系统的可见性、可靠性和可伸缩性,但降低了系统的网络性。“降低网络性”的通俗解释是某个功能如果使用有状态的设计只需要一次(或少量)请求就能完成,使用无状态的设计则可能会需要多次请求,或者在请求中带有额外冗余的信息。为了缓解这个矛盾,REST 希望软件系统能够如同万维网一样,允许客户端和中间的通讯传递者(譬如代理)将部分服务端的应答缓存起来。当然,为了缓存能够正确地运作,服务端的应答中必须明确地或者间接地表明本身是否可以进行缓存、可以缓存多长时间,以避免客户端在将来进行请求的时候得到过时的数据。运作良好的缓存机制可以减少客户端、服务器之间的交互,甚至有些场景中可以完全避免交互,这就进一步提高了性能。\n\n 4. 分层系统(Layered System) 这里所指的并不是表示层、服务层、持久层这种意义上的分层。而是指客户端一般不需要知道是否直接连接到了最终的服务器,抑或连接到路径上的中间服务器。中间服务器可以通过负载均衡和共享缓存的机制提高系统的可扩展性,这样也便于缓存、伸缩和安全策略的部署。该原则的典型的应用是内容分发网络(Content Distribution Network,CDN)。如果你是通过网站浏览到这篇文章的话,你所发出的请求一般(假设你在中国国境内的话)并不是直接访问位于 GitHub Pages 的源服务器,而是访问了位于国内的 CDN 服务器,但作为用户,你完全不需要感知到这一点。我们将在“透明多级分流系统”中讨论如何构建自动的、可缓存的分层系统。\n\n 5. 统一接口(Uniform Interface) 这是 REST 的另一条核心原则,REST 希望开发者面向资源编程,希望软件系统设计的重点放在抽象系统该有哪些资源上,而不是抽象系统该有哪些行为(服务)上。这条原则你可以类比计算机中对文件管理的操作来理解,管理文件可能会进行创建、修改、删除、移动等操作,这些操作数量是可数的,而且对所有文件都是固定的、统一的。如果面向资源来设计系统,同样会具有类似的操作特征,由于 REST 并没有设计新的协议,所以这些操作都借用了 HTTP 协议中固有的操作命令来完成。 统一接口也是 REST 最容易陷入争论的地方,基于网络的软件系统,到底是面向资源更好,还是面向服务更合适,这事情哪怕是很长时间里都不会有个定论,也许永远都没有。但是,已经有一个基本清晰的结论是:面向资源编程的抽象程度通常更高。抽象程度高意味着坏处是往往距离人类的思维方式更远,而好处是往往通用程度会更好。用这样的语言去诠释 REST,大概本身就挺抽象的,笔者还是举个例子来说明:譬如,几乎每个系统都有的登录和注销功能,如果你理解成登录对应于 login()服务,注销对应于 logout()服务这样两个独立服务,这是“符合人类思维”的;如果你理解成登录是 PUT Session,注销是 DELETE Session,这样你只需要设计一种“Session 资源”即可满足需求,甚至以后对 Session 的其他需求,如查询登陆用户的信息,就是 GET Session 而已,其他操作如修改用户信息等都可以被这同一套设计囊括在内,这便是“抽象程度更高”带来的好处。 想要在架构设计中合理恰当地利用统一接口,Fielding 建议系统应能做到每次请求中都包含资源的 ID,所有操作均通过资源 ID 来进行;建议每个资源都应该是自描述的消息;建议通过超文本来驱动应用状态的转移。\n\n 6. 按需代码(Code-On-Demand ) 按需代码被 Fielding 列为一条可选原则。它是指任何按照客户端(譬如浏览器)的请求,将可执行的软件程序从服务器发送到客户端的技术,按需代码赋予了客户端无需事先知道所有来自服务端的信息应该如何处理、如何运行的宽容度。举个具体例子,以前的Java Applet 技术,今天的WebAssembly 等都属于典型的按需代码,蕴含着具体执行逻辑的代码是存放在服务端,只有当客户端请求了某个 Java Applet 之后,代码才会被传输并在客户端机器中运行,结束后通常也会随即在客户端中被销毁掉。将按需代码列为可选原则的原因并非是它特别难以达到,而更多是出于必要性和性价比的实际考虑。\n\n至此,REST 中的主要概念与思想原则已经介绍完毕,我们再回过头来讨论本节开篇提出的 REST 与 RPC 在思想上的差异。REST 的基本思想是面向资源来抽象问题,它与此前流行的编程思想——面向过程的编程在抽象主体上有本质的差别。在 REST 提出以前,人们设计分布式系统服务的唯一方案就只有 RPC,RPC 是将本地的方法调用思路迁移到远程方法调用上,开发者是围绕着“远程方法”去设计两个系统间交互的,譬如 CORBA、RMI、DCOM,等等。这样做的坏处不仅是“如何在异构系统间表示一个方法”、“如何获得接口能够提供的方法清单”都成了需要专门协议去解决的问题(RPC 的三大基本问题之一),更在于服务的每个方法都是完全独立的,服务使用者必须逐个学习才能正确地使用它们。Google 在《Google API Design Guide》 中曾经写下这样一段话:\n\n> Traditionally, people design RPC APIs in terms of API interfaces and methods, such as CORBA and Windows COM. As time goes by, more and more interfaces and methods are introduced. The end result can be an overwhelming number of interfaces and methods, each of them different from the others. Developers have to learn each one carefully in order to use it correctly, which can be both time consuming and error prone\n\n以前,人们面向方法去设计 RPC API,譬如 CORBA 和 DCOM,随着时间推移,接口与方法越来越多却又各不相同,开发人员必须了解每一个方法才能正确使用它们,这样既耗时又容易出错。\n\n—— Google API Design Guide, 2017\n\nREST 提出以资源为主体进行服务设计的风格,能为它带来不少好处(自然也有坏处,笔者将在下一节集中谈论 REST 的不足与争议),譬如:\n\n * 降低的服务接口的学习成本。统一接口(Uniform Interface)是 REST 的重要标志,将对资源的标准操作都映射到了标准的 HTTP 方法上去,这些方法对于每个资源的用法都是一致的,语义都是类似的,不需要刻意去学习,更不需要有什么 Interface Description Language 之类的协议存在。\n\n * 资源天然具有集合与层次结构。以方法为中心抽象的接口,由于方法是动词,逻辑上决定了每个接口都是互相独立的;但以资源为中心抽象的接口,由于资源是名词,天然就可以产生集合与层次结构。举个具体例子,你想像一个商城用户中心的接口设计:用户资源会拥有多个不同的下级的资源,譬如若干条短消息资源、一份用户资料资源、一部购物车资源,购物车中又会有自己的下级资源,譬如多本书籍资源。很容易在程序接口中构造出这些资源的集合关系与层次关系,而且是符合人们长期在单机或网络环境中管理数据的直觉的。相信你不需要专门阅读接口说明书,也能轻易推断出获取用户icyfenix的购物车中的第2本书的 REST 接口应该表示为:\n\nGET /users/icyfenix/cart/2\n\n\n1\n\n\n * REST 绑定于 HTTP 协议。面向资源编程不是必须构筑在 HTTP 之上,但 REST 是,这是缺点,也是优点。因为 HTTP 本来就是面向资源而设计的网络协议,纯粹只用 HTTP(而不是 SOAP over HTTP 那样在再构筑协议)带来的好处是 RPC 中的 Wire Protocol 问题就无需再多考虑了,REST 将复用 HTTP 协议中已经定义的概念和相关基础支持来解决问题。HTTP 协议已经有效运作了三十年,其相关的技术基础设施已是千锤百炼,无比成熟。而坏处自然是,当你想去考虑那些 HTTP 不提供的特性时,便会彻底地束手无策。\n\n * ……\n\n以上列举了一些面向资源的优点,笔者并非要证明它比面向过程、面向对象更优秀,是否选用 REST 的 API 设计风格,需要权衡的是你的需求场景、你团队的设计和开发人员是否能够适应面向资源的思想来设计软件,来编写代码。在互联网中,面向资源来进行网络传输是这三十年来 HTTP 协议精心培养出来的用户习惯,如果开发者能够适应 REST 不太符合人类思维习惯的抽象方式,那 REST 通常能够更好地匹配在 HTTP 基础上构建的互联网,在效率与扩展性方面会有可观的收益。\n\n\n# 参考\n\nREST设计风格", + "normalizedContent": "# rest 设计风格\n\n很多人会拿 rest 与 rpc 互相比较,其实,rest 无论是在思想上、概念上,还是使用范围上,与 rpc 都不尽相同,充其量只能算是有一些相似,\n\n应用会有一部分重合之处,但本质上并不是同一类型的东西。\n\nrest 与 rpc 在思想上差异的核心是抽象的目标不一样,即面向资源的编程思想与面向过程的编程思想两者之间的区别。\n\n面向过程编程、面向对象编程大家想必听说过,但什么是面向资源编程?\n\n这个问题等介绍完 rest 的特征之后我们再回头细说。\n\n而概念上的不同是指 rest 并不是一种远程服务调用协议,甚至可以把定语也去掉,它就不是一种协议。\n\n协议都带有一定的规范性和强制性,最起码也该有个规约文档,譬如 json-rpc,它哪怕再简单,也要有个《json-rpc specification》 来规定协议的格式细节、异常、响应码等信息,\n\n但是 rest 并没有定义这些内容,尽管有一些指导原则,但实际上并不受任何强制的约束。\n\n常有人批评某个系统接口“设计得不够 restful”,其实这句话本身就有些争议,rest 只能说是风格而不是规范、协议,\n\n并且能完全达到 rest 所有指导原则的系统也是不多见的,这一点我们同样将在后文中详细讨论。\n\n至于使用范围,rest 与 rpc 作为主流的两种远程调用方式,在使用上是确有重合的,但重合的区域有多大就见仁见智了。\n\n\n# 理解 rest\n\n个人会有好恶偏爱,但计算机科学是务实的,有了 rpc,还会提出 rest,有了面向过程编程之后,还能产生面向资源编程,并引起广泛的关注、使用和讨论,\n\n说明后者一定是有一些前者没有的闪光点,或者解决、避免了一些面向过程中的缺陷。我们不妨先去理解 rest 为什么会出现,然后再来讨论评价它。\n\n简单搜索就能找到 rest 源于 roy thomas fielding 在 2000 年发表的博士论文:《architectural styles and the design of network-based software architectures》 ,此文的确是 rest 的源头,但我们不应该忽略 fielding 的身份和此前的工作背景,这些信息对理解 rest 的设计思想至关重要。\n\n首先,fielding 是一名很优秀的软件工程师,他是 apache 服务器的核心开发者,后来成为了著名的apache 软件基金会 的联合创始人;\n\n同时,fielding 也是 http 1.0 协议(1996 年发布)的专家组成员,后来还晋升为 http 1.1 协议(1999 年发布)的负责人。\n\nhttp 1.1 协议设计得极为成功,以至于发布之后长达十年的时间里,都没有收到多少修订的意见。\n\n用来指导 http 1.1 协议设计的理论和思想,最初是以备忘录的形式用作专家组成员之间交流,除了 ietf、w3c 的专家外,并没有在外界广泛流传。\n\n从时间上看,对 http 1.1 协议的设计工作贯穿了 fielding 的整个博士研究生涯,当起草 http 1.1 协议的工作完成后,fielding 回到了加州大学欧文分校继续攻读自己的博士学位。\n\n第二年,他更为系统、严谨地阐述了这套理论框架,并且以这套理论框架导出了一种新的编程思想,他为这种程序设计风格取了一个很多人难以理解,但是今天已经广为人知的名字 rest,即“表征状态转移”的缩写。\n\n哪怕对编程和网络都很熟悉的同学,只从标题中也不太可能直接弄明白什么叫“表征”、啥东西的“状态”、从哪“转移”到哪。\n\n尽管在论文原文中确有论述这些概念,但写得确实相当晦涩(不想读英文的同学从此处获得中文翻译版本),笔者推荐比较容易理解 rest 思想的途径是先理解什么是 http,\n\n再配合一些实际例子来进行类比,你会发现“rest”(representational state transfer)实际上是“htt”(hypertext transfer)的进一步抽象,两者就如同接口与实现类的关系一般。\n\nhttp 中使用的“超文本”(hypertext)一词是美国社会学家 theodor holm nelson 在 1967 年于《brief words on the hypertext》 一文里提出的,下面引用的是他本人在 1992 年修正后的定义:\n\n> hypertext\n\n> by now the word \"hypertext\" has become generally accepted for branching and responding text, but the corresponding word \"hypermedia\", meaning complexes of branching and responding graphics, movies and sound – as well as text – is much less used.\n\n> 现在,\"超文本 \"一词已被普遍接受,它指的是能够进行分支判断和差异响应的文本,相应地, \"超媒体 \"一词指的是能够进行分支判断和差异响应的图形、电影和声音(也包括文本)的复合体。\n\n> —— theodor holm nelson literary machines, 1992\n\n以上定义描述的“超文本(或超媒体,hypermedia)”是一种“能够对操作进行判断和响应的文本(或声音、图像等)”,这个概念在上世纪 60 年代提出时应该还属于科幻的范畴,但是今天大众已经完全接受了它,互联网中一段文字可以点击、可以触发脚本执行、可以调用服务端,这一切已毫不稀奇。下面我们继续尝试从“超文本”或者“超媒体”的含义来理解什么是“表征”以及 rest 中其他关键概念,这里使用一个具体事例将其描述如下。\n\n * 资源(resource):譬如你现在正在阅读一篇名为《rest 设计风格》的文章,这篇文章的内容本身(你可以将其理解为其蕴含的信息、数据)我们称之为“资源”。无论你是购买的书籍、是在浏览器看的网页、是打印出来看的文稿、是在电脑屏幕上阅读抑或是手机上浏览,尽管呈现的样子各不相同,但其中的信息是不变的,你所阅读的仍是同一份“资源”。\n\n * 表征(representation):当你通过电脑浏览器阅读此文章时,浏览器向服务端发出请求“我需要这个资源的 html 格式”,服务端向浏览器返回的这个 html 就被称之为“表征”,你可能通过其他方式拿到本文的 pdf、markdown、rss 等其他形式的版本,它们也同样是一个资源的多种表征。可见“表征”这个概念是指信息与用户交互时的表示形式,这与我们软件分层架构中常说的“表示层”(presentation layer)的语义其实是一致的。\n\n * 状态(state):当你读完了这篇文章,想看后面是什么内容时,你向服务器发出请求“给我下一篇文章”。但是“下一篇”是个相对概念,必须依赖“当前你正在阅读的文章是哪一篇”才能正确回应,这类在特定语境中才能产生的上下文信息即被称为“状态”。我们所说的有状态(stateful)抑或是无状态(stateless),都是只相对于服务端来说的,服务器要完成“取下一篇”的请求,要么自己记住用户的状态:这个用户现在阅读的是哪一篇文章,这称为有状态;要么客户端来记住状态,在请求的时候明确告诉服务器:我正在阅读某某文章,现在要读它的下一篇,这称为无状态。\n\n * 转移(transfer):无论状态是由服务端还是客户端来提供的,“取下一篇文章”这个行为逻辑必然只能由服务端来提供,因为只有服务端拥有该资源及其表征形式。服务器通过某种方式,把“用户当前阅读的文章”转变成“下一篇文章”,这就被称为“表征状态转移”。\n\n通过“阅读文章”这个例子,笔者对资源等概念进行通俗的释义,你应该能够理解 rest 所说的“表征状态转移”的含义了。借着这个故事的上下文状态,笔者再继续介绍几个现在不涉及但稍后要用到的概念名词。\n\n * 统一接口(uniform interface):上面说的服务器“通过某种方式”让表征状态发生转移,具体是什么方式?如果你真的是用浏览器阅读本文电子版的话,请把本文滚动到结尾处,右下角有下一篇文章的 uri 超链接地址,这是服务端渲染这篇文章时就预置好的,点击它让页面跳转到下一篇,就是所谓“某种方式”的其中一种方式。任何人都不会对点击超链接网页会出现跳转感到奇怪,但你细想一下,uri 的含义是统一资源标识符,是一个名词,如何能表达出“转移”动作的含义呢?答案是 http 协议中已经提前约定好了一套“统一接口”,它包括:get、head、post、put、delete、trace、options 七种基本操作,任何一个支持 http 协议的服务器都会遵守这套规定,对特定的 uri 采取这些操作,服务器就会触发相应的表征状态转移。\n\n * 超文本驱动(hypertext driven):尽管表征状态转移是由浏览器主动向服务器发出请求所引发的,该请求导致了“在浏览器屏幕上显示出了下一篇文章的内容”这个结果的出现。但是,你我都清楚这不可能真的是浏览器的主动意图,浏览器是根据用户输入的 uri 地址来找到网站首页,服务器给予的首页超文本内容后,浏览器再通过超文本内部的链接来导航到了这篇文章,阅读结束时,也是通过超文本内部的链接来再导航到下一篇。浏览器作为所有网站的通用的客户端,任何网站的导航(状态转移)行为都不可能是预置于浏览器代码之中,而是由服务器发出的请求响应信息(超文本)来驱动的。这点与其他带有客户端的软件有十分本质的区别,在那些软件中,业务逻辑往往是预置于程序代码之中的,有专门的页面控制器(无论在服务端还是在客户端中)来驱动页面的状态转移。\n\n * 自描述消息(self-descriptive messages):由于资源的表征可能存在多种不同形态,在消息中应当有明确的信息来告知客户端该消息的类型以及应如何处理这条消息。一种被广泛采用的自描述方法是在名为“content-type”的 http header 中标识出互联网媒体类型(mime type),譬如“content-type : application/json; charset=utf-8”,则说明该资源会以 json 的格式来返回,请使用 utf-8 字符集进行处理。\n\n除了以上列出的这些看名字不容易弄懂的概念外,在理解 rest 的过程中,还有一个常见的误区值得注意,fielding 提出 rest 时所谈论的范围是“架构风格与网络的软件架构设计”(architectural styles and design of network-based software architectures),而不是现在被人们所狭义理解的一种“远程服务设计风格”,这两者的范围差别就好比本书所谈论的话题“软件架构”与本章谈论话题“访问远程服务”的关系那样,前者是后者的一个很大的超集,尽管基于本节的主题和多数人的关注点考虑,我们确实是会以“远程服务设计风格”作为讨论的重点,但至少应该说明清楚它们范围上的差别。\n\n\n# restful 的系统\n\n如果你已经理解了上面这些概念,我们就可以开始讨论面向资源的编程思想与 fielding 所提出的几个具体的软件架构设计原则了。fielding 认为,一套理想的、完全满足 rest 风格的系统应该满足以下六大原则。\n\n 1. 服务端与客户端分离(client-server) 将用户界面所关注的逻辑和数据存储所关注的逻辑分离开来,有助于提高用户界面的跨平台的可移植性,这一点正越来越受到广大开发者所认可,以前完全基于服务端控制和渲染(如 jsf 这类)框架实际用户已甚少,而在服务端进行界面控制(controller),通过服务端或者客户端的模版渲染引擎来进行界面渲染的框架(如 struts、springmvc 这类)也受到了颇大的冲击。这一点主要推动力量与 rest 可能关系并不大,前端技术(从 es 规范,到语言实现,到前端框架等)的近年来的高速发展,使得前端表达能力大幅度加强才是真正的幕后推手。由于前端的日渐强势,现在还流行起由前端代码反过来驱动服务端进行渲染的 ssr(server-side rendering)技术,在 serverless、seo 等场景中已经占领了一块领地。\n\n 2. 无状态(stateless) 无状态是 rest 的一条核心原则,部分开发者在做服务接口规划时,觉得 rest 风格的服务怎么设计都感觉别扭,很有可能的一种原因是在服务端持有着比较重的状态。rest 希望服务器不要去负责维护状态,每一次从客户端发送的请求中,应包括所有的必要的上下文信息,会话信息也由客户端负责保存维护,服务端依据客户端传递的状态来执行业务处理逻辑,驱动整个应用的状态变迁。客户端承担状态维护职责以后,会产生一些新的问题,譬如身份认证、授权等可信问题,它们都应有针对性的解决方案(这部分内容可参见“安全架构”的内容)。 但必须承认的现状是,目前大多数的系统都达不到这个要求,往往越复杂、越大型的系统越是如此。服务端无状态可以在分布式计算中获得非常高价值的好处,但大型系统的上下文状态数量完全可能膨胀到让客户端在每次请求时提供变得不切实际的程度,在服务端的内存、会话、数据库或者缓存等地方持有一定的状态成为一种是事实上存在,并将长期存在、被广泛使用的主流的方案。\n\n 3. 可缓存(cacheability) 无状态服务虽然提升了系统的可见性、可靠性和可伸缩性,但降低了系统的网络性。“降低网络性”的通俗解释是某个功能如果使用有状态的设计只需要一次(或少量)请求就能完成,使用无状态的设计则可能会需要多次请求,或者在请求中带有额外冗余的信息。为了缓解这个矛盾,rest 希望软件系统能够如同万维网一样,允许客户端和中间的通讯传递者(譬如代理)将部分服务端的应答缓存起来。当然,为了缓存能够正确地运作,服务端的应答中必须明确地或者间接地表明本身是否可以进行缓存、可以缓存多长时间,以避免客户端在将来进行请求的时候得到过时的数据。运作良好的缓存机制可以减少客户端、服务器之间的交互,甚至有些场景中可以完全避免交互,这就进一步提高了性能。\n\n 4. 分层系统(layered system) 这里所指的并不是表示层、服务层、持久层这种意义上的分层。而是指客户端一般不需要知道是否直接连接到了最终的服务器,抑或连接到路径上的中间服务器。中间服务器可以通过负载均衡和共享缓存的机制提高系统的可扩展性,这样也便于缓存、伸缩和安全策略的部署。该原则的典型的应用是内容分发网络(content distribution network,cdn)。如果你是通过网站浏览到这篇文章的话,你所发出的请求一般(假设你在中国国境内的话)并不是直接访问位于 github pages 的源服务器,而是访问了位于国内的 cdn 服务器,但作为用户,你完全不需要感知到这一点。我们将在“透明多级分流系统”中讨论如何构建自动的、可缓存的分层系统。\n\n 5. 统一接口(uniform interface) 这是 rest 的另一条核心原则,rest 希望开发者面向资源编程,希望软件系统设计的重点放在抽象系统该有哪些资源上,而不是抽象系统该有哪些行为(服务)上。这条原则你可以类比计算机中对文件管理的操作来理解,管理文件可能会进行创建、修改、删除、移动等操作,这些操作数量是可数的,而且对所有文件都是固定的、统一的。如果面向资源来设计系统,同样会具有类似的操作特征,由于 rest 并没有设计新的协议,所以这些操作都借用了 http 协议中固有的操作命令来完成。 统一接口也是 rest 最容易陷入争论的地方,基于网络的软件系统,到底是面向资源更好,还是面向服务更合适,这事情哪怕是很长时间里都不会有个定论,也许永远都没有。但是,已经有一个基本清晰的结论是:面向资源编程的抽象程度通常更高。抽象程度高意味着坏处是往往距离人类的思维方式更远,而好处是往往通用程度会更好。用这样的语言去诠释 rest,大概本身就挺抽象的,笔者还是举个例子来说明:譬如,几乎每个系统都有的登录和注销功能,如果你理解成登录对应于 login()服务,注销对应于 logout()服务这样两个独立服务,这是“符合人类思维”的;如果你理解成登录是 put session,注销是 delete session,这样你只需要设计一种“session 资源”即可满足需求,甚至以后对 session 的其他需求,如查询登陆用户的信息,就是 get session 而已,其他操作如修改用户信息等都可以被这同一套设计囊括在内,这便是“抽象程度更高”带来的好处。 想要在架构设计中合理恰当地利用统一接口,fielding 建议系统应能做到每次请求中都包含资源的 id,所有操作均通过资源 id 来进行;建议每个资源都应该是自描述的消息;建议通过超文本来驱动应用状态的转移。\n\n 6. 按需代码(code-on-demand ) 按需代码被 fielding 列为一条可选原则。它是指任何按照客户端(譬如浏览器)的请求,将可执行的软件程序从服务器发送到客户端的技术,按需代码赋予了客户端无需事先知道所有来自服务端的信息应该如何处理、如何运行的宽容度。举个具体例子,以前的java applet 技术,今天的webassembly 等都属于典型的按需代码,蕴含着具体执行逻辑的代码是存放在服务端,只有当客户端请求了某个 java applet 之后,代码才会被传输并在客户端机器中运行,结束后通常也会随即在客户端中被销毁掉。将按需代码列为可选原则的原因并非是它特别难以达到,而更多是出于必要性和性价比的实际考虑。\n\n至此,rest 中的主要概念与思想原则已经介绍完毕,我们再回过头来讨论本节开篇提出的 rest 与 rpc 在思想上的差异。rest 的基本思想是面向资源来抽象问题,它与此前流行的编程思想——面向过程的编程在抽象主体上有本质的差别。在 rest 提出以前,人们设计分布式系统服务的唯一方案就只有 rpc,rpc 是将本地的方法调用思路迁移到远程方法调用上,开发者是围绕着“远程方法”去设计两个系统间交互的,譬如 corba、rmi、dcom,等等。这样做的坏处不仅是“如何在异构系统间表示一个方法”、“如何获得接口能够提供的方法清单”都成了需要专门协议去解决的问题(rpc 的三大基本问题之一),更在于服务的每个方法都是完全独立的,服务使用者必须逐个学习才能正确地使用它们。google 在《google api design guide》 中曾经写下这样一段话:\n\n> traditionally, people design rpc apis in terms of api interfaces and methods, such as corba and windows com. as time goes by, more and more interfaces and methods are introduced. the end result can be an overwhelming number of interfaces and methods, each of them different from the others. developers have to learn each one carefully in order to use it correctly, which can be both time consuming and error prone\n\n以前,人们面向方法去设计 rpc api,譬如 corba 和 dcom,随着时间推移,接口与方法越来越多却又各不相同,开发人员必须了解每一个方法才能正确使用它们,这样既耗时又容易出错。\n\n—— google api design guide, 2017\n\nrest 提出以资源为主体进行服务设计的风格,能为它带来不少好处(自然也有坏处,笔者将在下一节集中谈论 rest 的不足与争议),譬如:\n\n * 降低的服务接口的学习成本。统一接口(uniform interface)是 rest 的重要标志,将对资源的标准操作都映射到了标准的 http 方法上去,这些方法对于每个资源的用法都是一致的,语义都是类似的,不需要刻意去学习,更不需要有什么 interface description language 之类的协议存在。\n\n * 资源天然具有集合与层次结构。以方法为中心抽象的接口,由于方法是动词,逻辑上决定了每个接口都是互相独立的;但以资源为中心抽象的接口,由于资源是名词,天然就可以产生集合与层次结构。举个具体例子,你想像一个商城用户中心的接口设计:用户资源会拥有多个不同的下级的资源,譬如若干条短消息资源、一份用户资料资源、一部购物车资源,购物车中又会有自己的下级资源,譬如多本书籍资源。很容易在程序接口中构造出这些资源的集合关系与层次关系,而且是符合人们长期在单机或网络环境中管理数据的直觉的。相信你不需要专门阅读接口说明书,也能轻易推断出获取用户icyfenix的购物车中的第2本书的 rest 接口应该表示为:\n\nget /users/icyfenix/cart/2\n\n\n1\n\n\n * rest 绑定于 http 协议。面向资源编程不是必须构筑在 http 之上,但 rest 是,这是缺点,也是优点。因为 http 本来就是面向资源而设计的网络协议,纯粹只用 http(而不是 soap over http 那样在再构筑协议)带来的好处是 rpc 中的 wire protocol 问题就无需再多考虑了,rest 将复用 http 协议中已经定义的概念和相关基础支持来解决问题。http 协议已经有效运作了三十年,其相关的技术基础设施已是千锤百炼,无比成熟。而坏处自然是,当你想去考虑那些 http 不提供的特性时,便会彻底地束手无策。\n\n * ……\n\n以上列举了一些面向资源的优点,笔者并非要证明它比面向过程、面向对象更优秀,是否选用 rest 的 api 设计风格,需要权衡的是你的需求场景、你团队的设计和开发人员是否能够适应面向资源的思想来设计软件,来编写代码。在互联网中,面向资源来进行网络传输是这三十年来 http 协议精心培养出来的用户习惯,如果开发者能够适应 rest 不太符合人类思维习惯的抽象方式,那 rest 通常能够更好地匹配在 http 基础上构建的互联网,在效率与扩展性方面会有可观的收益。\n\n\n# 参考\n\nrest设计风格", + "charsets": { + "cjk": true }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/290.%E7%AE%80%E8%BF%B0%20HTTP%20%E6%8A%A5%E6%96%87%E5%A4%B4%E9%83%A8%E7%9A%84%E7%BB%84%E6%88%90%E7%BB%93%E6%9E%84.html", - "relativePath": "02.计算机基础/10.计算机网络/290.简述 HTTP 报文头部的组成结构.md", - "key": "v-51f24219", - "path": "/pages/dfb780/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {} + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "进程间有哪些通信方式?", @@ -5212,33 +5792,143 @@ export const siteData = { "relativePath": "02.计算机基础/20.操作系统/01.进程间有哪些通信方式?.md", "key": "v-18f7a69e", "path": "/pages/e5f2d7/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {} + "headers": [ + { + "level": 2, + "title": "1. 管道", + "slug": "_1-管道", + "normalizedTitle": "1. 管道", + "charIndex": 136 + }, + { + "level": 2, + "title": "2. FIFO", + "slug": "_2-fifo", + "normalizedTitle": "2. fifo", + "charIndex": 291 + }, + { + "level": 2, + "title": "3. 消息队列", + "slug": "_3-消息队列", + "normalizedTitle": "3. 消息队列", + "charIndex": 513 + }, + { + "level": 2, + "title": "4. 信号量", + "slug": "_4-信号量", + "normalizedTitle": "4. 信号量", + "charIndex": 668 + }, + { + "level": 2, + "title": "5. 共享存储", + "slug": "_5-共享存储", + "normalizedTitle": "5. 共享存储", + "charIndex": 709 + }, + { + "level": 2, + "title": "6. 套接字", + "slug": "_6-套接字", + "normalizedTitle": "6. 套接字", + "charIndex": 853 + }, + { + "level": 2, + "title": "参考", + "slug": "参考", + "normalizedTitle": "参考", + "charIndex": 893 + } + ], + "headersStr": "1. 管道 2. FIFO 3. 消息队列 4. 信号量 5. 共享存储 6. 套接字 参考", + "content": "进程同步与进程通信很容易混淆,它们的区别在于:\n\n * 进程同步:控制多个进程按一定顺序执行;\n * 进程通信:进程间传输信息。\n\n进程通信是一种手段,而进程同步是一种目的。也可以说,为了能够达到进程同步的目的,需要让进程进行通信,传输一些进程同步所需要的信息。\n\n\n# 1. 管道\n\n管道是通过调用 pipe 函数创建的,fd[0] 用于读,fd[1] 用于写。\n\n#include \nint pipe(int fd[2]);\n\n\n1\n2\n\n\n它具有以下限制:\n\n * 只支持半双工通信(单向交替传输);\n * 只能在父子进程或者兄弟进程中使用。\n\n\n\n\n# 2. FIFO\n\n也称为命名管道,去除了管道只能在父子进程中使用的限制。\n\n#include \nint mkfifo(const char *path, mode_t mode);\nint mkfifoat(int fd, const char *path, mode_t mode);\n\n\n1\n2\n3\n\n\nFIFO 常用于客户-服务器应用程序中,FIFO 用作汇聚点,在客户进程和服务器进程之间传递数据。\n\n\n\n\n# 3. 消息队列\n\n相比于 FIFO,消息队列具有以下优点:\n\n消息队列可以独立于读写进程存在,从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难; 避免了 FIFO 的同步阻塞问题,不需要进程自己提供同步方法; 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收。\n\n\n# 4. 信号量\n\n它是一个计数器,用于为多个进程提供对共享数据对象的访问。\n\n\n# 5. 共享存储\n\n允许多个进程共享一个给定的存储区。因为数据不需要在进程之间复制,所以这是最快的一种 IPC。\n\n需要使用信号量用来同步对共享存储的访问。\n\n多个进程可以将同一个文件映射到它们的地址空间从而实现共享内存。另外 XSI 共享内存不是使用文件,而是使用内存的匿名段。\n\n\n# 6. 套接字\n\n与其它通信机制不同的是,它可用于不同机器间的进程通信。\n\n\n# 参考\n\n进程通信", + "normalizedContent": "进程同步与进程通信很容易混淆,它们的区别在于:\n\n * 进程同步:控制多个进程按一定顺序执行;\n * 进程通信:进程间传输信息。\n\n进程通信是一种手段,而进程同步是一种目的。也可以说,为了能够达到进程同步的目的,需要让进程进行通信,传输一些进程同步所需要的信息。\n\n\n# 1. 管道\n\n管道是通过调用 pipe 函数创建的,fd[0] 用于读,fd[1] 用于写。\n\n#include \nint pipe(int fd[2]);\n\n\n1\n2\n\n\n它具有以下限制:\n\n * 只支持半双工通信(单向交替传输);\n * 只能在父子进程或者兄弟进程中使用。\n\n\n\n\n# 2. fifo\n\n也称为命名管道,去除了管道只能在父子进程中使用的限制。\n\n#include \nint mkfifo(const char *path, mode_t mode);\nint mkfifoat(int fd, const char *path, mode_t mode);\n\n\n1\n2\n3\n\n\nfifo 常用于客户-服务器应用程序中,fifo 用作汇聚点,在客户进程和服务器进程之间传递数据。\n\n\n\n\n# 3. 消息队列\n\n相比于 fifo,消息队列具有以下优点:\n\n消息队列可以独立于读写进程存在,从而避免了 fifo 中同步管道的打开和关闭时可能产生的困难; 避免了 fifo 的同步阻塞问题,不需要进程自己提供同步方法; 读进程可以根据消息类型有选择地接收消息,而不像 fifo 那样只能默认地接收。\n\n\n# 4. 信号量\n\n它是一个计数器,用于为多个进程提供对共享数据对象的访问。\n\n\n# 5. 共享存储\n\n允许多个进程共享一个给定的存储区。因为数据不需要在进程之间复制,所以这是最快的一种 ipc。\n\n需要使用信号量用来同步对共享存储的访问。\n\n多个进程可以将同一个文件映射到它们的地址空间从而实现共享内存。另外 xsi 共享内存不是使用文件,而是使用内存的匿名段。\n\n\n# 6. 套接字\n\n与其它通信机制不同的是,它可用于不同机器间的进程通信。\n\n\n# 参考\n\n进程通信", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { - "title": "什么是 SYN flood,如何防止这类攻击?", + "title": "进程和线程之间有什么区别?", "frontmatter": { - "title": "什么是 SYN flood,如何防止这类攻击?", - "date": "2022-09-21T22:55:33.000Z", - "permalink": "/pages/367c4d/", + "title": "进程和线程之间有什么区别?", + "date": "2022-09-21T23:00:14.000Z", + "permalink": "/pages/c2fd8a/", "categories": [ "计算机基础", - "计算机网络" + "操作系统" ], "tags": [ null ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/10.%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/300.%E4%BB%80%E4%B9%88%E6%98%AF%20SYN%20flood%EF%BC%8C%E5%A6%82%E4%BD%95%E9%98%B2%E6%AD%A2%E8%BF%99%E7%B1%BB%E6%94%BB%E5%87%BB%EF%BC%9F.html", - "relativePath": "02.计算机基础/10.计算机网络/300.什么是 SYN flood,如何防止这类攻击?.md", - "key": "v-64bba194", - "path": "/pages/367c4d/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {} + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/020.%E8%BF%9B%E7%A8%8B%E5%92%8C%E7%BA%BF%E7%A8%8B%E4%B9%8B%E9%97%B4%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F.html", + "relativePath": "02.计算机基础/20.操作系统/020.进程和线程之间有什么区别?.md", + "key": "v-a0ab5810", + "path": "/pages/c2fd8a/", + "headers": [ + { + "level": 2, + "title": "1. 进程", + "slug": "_1-进程", + "normalizedTitle": "1. 进程", + "charIndex": 2 + }, + { + "level": 2, + "title": "2. 线程", + "slug": "_2-线程", + "normalizedTitle": "2. 线程", + "charIndex": 145 + }, + { + "level": 2, + "title": "3. 区别", + "slug": "_3-区别", + "normalizedTitle": "3. 区别", + "charIndex": 302 + }, + { + "level": 3, + "title": "Ⅰ 拥有资源", + "slug": "i-拥有资源", + "normalizedTitle": "ⅰ 拥有资源", + "charIndex": 312 + }, + { + "level": 3, + "title": "Ⅱ 调度", + "slug": "ii-调度", + "normalizedTitle": "ⅱ 调度", + "charIndex": 362 + }, + { + "level": 3, + "title": "Ⅲ 系统开销", + "slug": "iii-系统开销", + "normalizedTitle": "ⅲ 系统开销", + "charIndex": 438 + }, + { + "level": 3, + "title": "Ⅳ 通信方面", + "slug": "iv-通信方面", + "normalizedTitle": "ⅳ 通信方面", + "charIndex": 585 + } + ], + "headersStr": "1. 进程 2. 线程 3. 区别 Ⅰ 拥有资源 Ⅱ 调度 Ⅲ 系统开销 Ⅳ 通信方面", + "content": "# 1. 进程\n\n进程是资源分配的基本单位。\n\n进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。\n\n下图显示了 4 个程序创建了 4 个进程,这 4 个进程可以并发地执行。\n\n\n\n\n# 2. 线程\n\n线程是独立调度的基本单位。\n\n一个进程中可以有多个线程,它们共享进程资源。\n\nQQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。\n\n\n\n\n# 3. 区别\n\n\n# Ⅰ 拥有资源\n\n进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。\n\n\n# Ⅱ 调度\n\n线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。\n\n\n# Ⅲ 系统开销\n\n由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。\n\n\n# Ⅳ 通信方面\n\n线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。", + "normalizedContent": "# 1. 进程\n\n进程是资源分配的基本单位。\n\n进程控制块 (process control block, pcb) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 pcb 的操作。\n\n下图显示了 4 个程序创建了 4 个进程,这 4 个进程可以并发地执行。\n\n\n\n\n# 2. 线程\n\n线程是独立调度的基本单位。\n\n一个进程中可以有多个线程,它们共享进程资源。\n\nqq 和浏览器是两个进程,浏览器进程里面有很多线程,例如 http 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 http 请求时,浏览器还可以响应用户的其它事件。\n\n\n\n\n# 3. 区别\n\n\n# ⅰ 拥有资源\n\n进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。\n\n\n# ⅱ 调度\n\n线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。\n\n\n# ⅲ 系统开销\n\n由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、i/o 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 cpu 环境的保存及新调度进程 cpu 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。\n\n\n# ⅳ 通信方面\n\n线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 ipc。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "简述自旋锁与互斥锁的使用场景", @@ -5261,14 +5951,16 @@ export const siteData = { "headersStr": null, "content": "", "normalizedContent": "", - "charsets": {} + "charsets": {}, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { - "title": "进程和线程之间有什么区别?", + "title": "简述几个常用的 Linux 命令以及他们的功能", "frontmatter": { - "title": "进程和线程之间有什么区别?", - "date": "2022-09-21T23:00:14.000Z", - "permalink": "/pages/c2fd8a/", + "title": "简述几个常用的 Linux 命令以及他们的功能", + "date": "2022-09-21T23:00:27.000Z", + "permalink": "/pages/198887/", "categories": [ "计算机基础", "操作系统" @@ -5277,14 +5969,16 @@ export const siteData = { null ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/020.%E8%BF%9B%E7%A8%8B%E5%92%8C%E7%BA%BF%E7%A8%8B%E4%B9%8B%E9%97%B4%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F.html", - "relativePath": "02.计算机基础/20.操作系统/020.进程和线程之间有什么区别?.md", - "key": "v-a0ab5810", - "path": "/pages/c2fd8a/", + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/030.%E7%AE%80%E8%BF%B0%E5%87%A0%E4%B8%AA%E5%B8%B8%E7%94%A8%E7%9A%84%20Linux%20%E5%91%BD%E4%BB%A4%E4%BB%A5%E5%8F%8A%E4%BB%96%E4%BB%AC%E7%9A%84%E5%8A%9F%E8%83%BD.html", + "relativePath": "02.计算机基础/20.操作系统/030.简述几个常用的 Linux 命令以及他们的功能.md", + "key": "v-32c73af0", + "path": "/pages/198887/", "headersStr": null, "content": "", "normalizedContent": "", - "charsets": {} + "charsets": {}, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "创建线程有多少种方式?", @@ -5307,7 +6001,9 @@ export const siteData = { "headersStr": null, "content": "", "normalizedContent": "", - "charsets": {} + "charsets": {}, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "线程有多少种状态,状态之间如何转换", @@ -5330,53 +6026,9 @@ export const siteData = { "headersStr": null, "content": "", "normalizedContent": "", - "charsets": {} - }, - { - "title": "简述几个常用的 Linux 命令以及他们的功能", - "frontmatter": { - "title": "简述几个常用的 Linux 命令以及他们的功能", - "date": "2022-09-21T23:00:27.000Z", - "permalink": "/pages/198887/", - "categories": [ - "计算机基础", - "操作系统" - ], - "tags": [ - null - ] - }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/030.%E7%AE%80%E8%BF%B0%E5%87%A0%E4%B8%AA%E5%B8%B8%E7%94%A8%E7%9A%84%20Linux%20%E5%91%BD%E4%BB%A4%E4%BB%A5%E5%8F%8A%E4%BB%96%E4%BB%AC%E7%9A%84%E5%8A%9F%E8%83%BD.html", - "relativePath": "02.计算机基础/20.操作系统/030.简述几个常用的 Linux 命令以及他们的功能.md", - "key": "v-32c73af0", - "path": "/pages/198887/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {} - }, - { - "title": "Linux 下如何排查 CPU 以及 内存占用过多?", - "frontmatter": { - "title": "Linux 下如何排查 CPU 以及 内存占用过多?", - "date": "2022-09-21T23:01:37.000Z", - "permalink": "/pages/b043a6/", - "categories": [ - "计算机基础", - "操作系统" - ], - "tags": [ - null - ] - }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/080.Linux%20%E4%B8%8B%E5%A6%82%E4%BD%95%E6%8E%92%E6%9F%A5%20CPU%20%E4%BB%A5%E5%8F%8A%20%E5%86%85%E5%AD%98%E5%8D%A0%E7%94%A8%E8%BF%87%E5%A4%9A%EF%BC%9F.html", - "relativePath": "02.计算机基础/20.操作系统/080.Linux 下如何排查 CPU 以及 内存占用过多?.md", - "key": "v-05845816", - "path": "/pages/b043a6/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {} + "charsets": {}, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "进程有多少种状态?", @@ -5397,9 +6049,13 @@ export const siteData = { "key": "v-262f2c87", "path": "/pages/165ea8/", "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {} + "content": " * 就绪状态(ready):等待被调度\n\n * 运行状态(running)\n\n * 阻塞状态(waiting):等待资源 应该注意以下内容:\n\n * 只有就绪态和运行态可以相互转换,其它的都是单向转换。就绪状态的进程通过调度算法从而获得 CPU 时间,转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。\n\n * 阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括 CPU 时间,缺少 CPU 时间会从运行态转换为就绪态。", + "normalizedContent": " * 就绪状态(ready):等待被调度\n\n * 运行状态(running)\n\n * 阻塞状态(waiting):等待资源 应该注意以下内容:\n\n * 只有就绪态和运行态可以相互转换,其它的都是单向转换。就绪状态的进程通过调度算法从而获得 cpu 时间,转为运行状态;而运行状态的进程,在分配给它的 cpu 时间片用完之后就会转为就绪状态,等待下一次调度。\n\n * 阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括 cpu 时间,缺少 cpu 时间会从运行态转换为就绪态。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "简述 select, poll, epoll 的使用场景以及区别", @@ -5419,10 +6075,139 @@ export const siteData = { "relativePath": "02.计算机基础/20.操作系统/070.简述 select, poll, epoll 的使用场景以及区别.md", "key": "v-5a37154c", "path": "/pages/68ea00/", + "headers": [ + { + "level": 2, + "title": "select", + "slug": "select", + "normalizedTitle": "select", + "charIndex": 0 + }, + { + "level": 2, + "title": "poll", + "slug": "poll", + "normalizedTitle": "poll", + "charIndex": 7 + }, + { + "level": 2, + "title": "比较", + "slug": "比较", + "normalizedTitle": "比较", + "charIndex": 2467 + }, + { + "level": 3, + "title": "1. 功能", + "slug": "_1-功能", + "normalizedTitle": "1. 功能", + "charIndex": 2474 + }, + { + "level": 3, + "title": "2. 速度", + "slug": "_2-速度", + "normalizedTitle": "2. 速度", + "charIndex": 2775 + }, + { + "level": 3, + "title": "3. 可移植性", + "slug": "_3-可移植性", + "normalizedTitle": "3. 可移植性", + "charIndex": 2838 + }, + { + "level": 2, + "title": "epoll", + "slug": "epoll", + "normalizedTitle": "epoll", + "charIndex": 12 + }, + { + "level": 2, + "title": "工作模式", + "slug": "工作模式", + "normalizedTitle": "工作模式", + "charIndex": 5260 + }, + { + "level": 3, + "title": "1. LT 模式", + "slug": "_1-lt-模式", + "normalizedTitle": "1. lt 模式", + "charIndex": 5327 + }, + { + "level": 3, + "title": "2. ET 模式", + "slug": "_2-et-模式", + "normalizedTitle": "2. et 模式", + "charIndex": 5456 + }, + { + "level": 2, + "title": "应用场景", + "slug": "应用场景", + "normalizedTitle": "应用场景", + "charIndex": 5626 + }, + { + "level": 3, + "title": "1. select 应用场景", + "slug": "_1-select-应用场景", + "normalizedTitle": "1. select 应用场景", + "charIndex": 5698 + }, + { + "level": 3, + "title": "2. poll 应用场景", + "slug": "_2-poll-应用场景", + "normalizedTitle": "2. poll 应用场景", + "charIndex": 5829 + }, + { + "level": 3, + "title": "3. epoll 应用场景", + "slug": "_3-epoll-应用场景", + "normalizedTitle": "3. epoll 应用场景", + "charIndex": 5904 + } + ], + "headersStr": "select poll 比较 1. 功能 2. 速度 3. 可移植性 epoll 工作模式 1. LT 模式 2. ET 模式 应用场景 1. select 应用场景 2. poll 应用场景 3. epoll 应用场景", + "content": "select/poll/epoll 都是 I/O 多路复用的具体实现,select 出现的最早,之后是 poll,再是 epoll。\n\n\n# select\n\nint select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);\n\n\n1\n\n\nselect 允许应用程序监视一组文件描述符,等待一个或者多个描述符成为就绪状态,从而完成 I/O 操作。\n\n * fd_set 使用数组实现,数组大小使用 FD_SETSIZE 定义,所以只能监听少于 FD_SETSIZE 数量的描述符。有三种类型的描述符类型:readset、writeset、exceptset,分别对应读、写、异常条件的描述符集合。\n\n * timeout 为超时参数,调用 select 会一直阻塞直到有描述符的事件到达或者等待的时间超过 timeout。\n\n * 成功调用返回结果大于 0,出错返回结果为 -1,超时返回结果为 0。\n\nfd_set fd_in, fd_out;\nstruct timeval tv;\n\n// Reset the sets\nFD_ZERO( &fd_in );\nFD_ZERO( &fd_out );\n\n// Monitor sock1 for input events\nFD_SET( sock1, &fd_in );\n\n// Monitor sock2 for output events\nFD_SET( sock2, &fd_out );\n\n// Find out which socket has the largest numeric value as select requires it\nint largest_sock = sock1 > sock2 ? sock1 : sock2;\n\n// Wait up to 10 seconds\ntv.tv_sec = 10;\ntv.tv_usec = 0;\n\n// Call the select\nint ret = select( largest_sock + 1, &fd_in, &fd_out, NULL, &tv );\n\n// Check if select actually succeed\nif ( ret == -1 )\n // report error and abort\nelse if ( ret == 0 )\n // timeout; no event detected\nelse\n{\n if ( FD_ISSET( sock1, &fd_in ) )\n // input event on sock1\n\n if ( FD_ISSET( sock2, &fd_out ) )\n // output event on sock2\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n\n\n\n# poll\n\nint poll(struct pollfd *fds, unsigned int nfds, int timeout);\n\n\n1\n\n\npoll 的功能与 select 类似,也是等待一组描述符中的一个成为就绪状态。\n\npoll 中的描述符是 pollfd 类型的数组,pollfd 的定义如下:\n\nstruct pollfd {\n int fd; /* file descriptor */\n short events; /* requested events */\n short revents; /* returned events */\n };\n\n\n1\n2\n3\n4\n5\n\n\n// The structure for two events\nstruct pollfd fds[2];\n\n// Monitor sock1 for input\nfds[0].fd = sock1;\nfds[0].events = POLLIN;\n\n// Monitor sock2 for output\nfds[1].fd = sock2;\nfds[1].events = POLLOUT;\n\n// Wait 10 seconds\nint ret = poll( &fds, 2, 10000 );\n// Check if poll actually succeed\nif ( ret == -1 )\n // report error and abort\nelse if ( ret == 0 )\n // timeout; no event detected\nelse\n{\n // If we detect the event, zero it out so we can reuse the structure\n if ( fds[0].revents & POLLIN )\n fds[0].revents = 0;\n // input event on sock1\n\n if ( fds[1].revents & POLLOUT )\n fds[1].revents = 0;\n // output event on sock2\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n\n\n\n# 比较\n\n\n# 1. 功能\n\nselect 和 poll 的功能基本相同,不过在一些实现细节上有所不同。\n\n * select 会修改描述符,而 poll 不会;\n * select 的描述符类型使用数组实现,FD_SETSIZE 大小默认为 1024,因此默认只能监听少于 1024 个描述符。如果要监听更多描述符的话,需要修改 FD_SETSIZE 之后重新编译;而 poll 没有描述符数量的限制;\n * poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高。\n * 如果一个线程对某个描述符调用了 select 或者 poll,另一个线程关闭了该描述符,会导致调用结果不确定。\n\n\n# 2. 速度\n\nselect 和 poll 速度都比较慢,每次调用都需要将全部描述符从应用进程缓冲区复制到内核缓冲区。\n\n\n# 3. 可移植性\n\n几乎所有的系统都支持 select,但是只有比较新的系统支持 poll。\n\n\n# epoll\n\nint epoll_create(int size);\nint epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);\nint epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);\n\n\n1\n2\n3\n\n\nepoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符。\n\n从上面的描述可以看出,epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次,并且进程不需要通过轮询来获得事件完成的描述符。\n\nepoll 仅适用于 Linux OS。\n\nepoll 比 select 和 poll 更加灵活而且没有描述符数量限制。\n\nepoll 对多线程编程更有友好,一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符也不会产生像 select 和 poll 的不确定情况。\n\n// Create the epoll descriptor. Only one is needed per app, and is used to monitor all sockets.\n// The function argument is ignored (it was not before, but now it is), so put your favorite number here\nint pollingfd = epoll_create( 0xCAFE );\n\nif ( pollingfd < 0 )\n // report error\n\n// Initialize the epoll structure in case more members are added in future\nstruct epoll_event ev = { 0 };\n\n// Associate the connection class instance with the event. You can associate anything\n// you want, epoll does not use this information. We store a connection class pointer, pConnection1\nev.data.ptr = pConnection1;\n\n// Monitor for input, and do not automatically rearm the descriptor after the event\nev.events = EPOLLIN | EPOLLONESHOT;\n// Add the descriptor into the monitoring list. We can do it even if another thread is\n// waiting in epoll_wait - the descriptor will be properly added\nif ( epoll_ctl( epollfd, EPOLL_CTL_ADD, pConnection1->getSocket(), &ev ) != 0 )\n // report error\n\n// Wait for up to 20 events (assuming we have added maybe 200 sockets before that it may happen)\nstruct epoll_event pevents[ 20 ];\n\n// Wait for 10 seconds, and retrieve less than 20 epoll_event and store them into epoll_event array\nint ready = epoll_wait( pollingfd, pevents, 20, 10000 );\n// Check if epoll actually succeed\nif ( ret == -1 )\n // report error and abort\nelse if ( ret == 0 )\n // timeout; no event detected\nelse\n{\n // Check if any events detected\n for ( int i = 0; i < ready; i++ )\n {\n if ( pevents[i].events & EPOLLIN )\n {\n // Get back our connection pointer\n Connection * c = (Connection*) pevents[i].data.ptr;\n c->handleReadEvent();\n }\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n\n\n\n# 工作模式\n\nepoll 的描述符事件有两种触发模式:LT(level trigger)和 ET(edge trigger)。\n\n\n# 1. LT 模式\n\n当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。\n\n\n# 2. ET 模式\n\n和 LT 模式不同的是,通知之后进程必须立即处理事件,下次再调用 epoll_wait() 时不会再得到事件到达的通知。\n\n很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。\n\n\n# 应用场景\n\n很容易产生一种错觉认为只要用 epoll 就可以了,select 和 poll 都已经过时了,其实它们都有各自的使用场景。\n\n\n# 1. select 应用场景\n\nselect 的 timeout 参数精度为微秒,而 poll 和 epoll 为毫秒,因此 select 更加适用于实时性要求比较高的场景,比如核反应堆的控制。\n\nselect 可移植性更好,几乎被所有主流平台所支持。\n\n\n# 2. poll 应用场景\n\npoll 没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应该使用 poll 而不是 select。\n\n\n# 3. epoll 应用场景\n\n只需要运行在 Linux 平台上,有大量的描述符需要同时轮询,并且这些连接最好是长连接。\n\n需要同时监控小于 1000 个描述符,就没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。\n\n需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。\n\n因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。\n\n并且 epoll 的描述符存储在内核,不容易调试。", + "normalizedContent": "select/poll/epoll 都是 i/o 多路复用的具体实现,select 出现的最早,之后是 poll,再是 epoll。\n\n\n# select\n\nint select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);\n\n\n1\n\n\nselect 允许应用程序监视一组文件描述符,等待一个或者多个描述符成为就绪状态,从而完成 i/o 操作。\n\n * fd_set 使用数组实现,数组大小使用 fd_setsize 定义,所以只能监听少于 fd_setsize 数量的描述符。有三种类型的描述符类型:readset、writeset、exceptset,分别对应读、写、异常条件的描述符集合。\n\n * timeout 为超时参数,调用 select 会一直阻塞直到有描述符的事件到达或者等待的时间超过 timeout。\n\n * 成功调用返回结果大于 0,出错返回结果为 -1,超时返回结果为 0。\n\nfd_set fd_in, fd_out;\nstruct timeval tv;\n\n// reset the sets\nfd_zero( &fd_in );\nfd_zero( &fd_out );\n\n// monitor sock1 for input events\nfd_set( sock1, &fd_in );\n\n// monitor sock2 for output events\nfd_set( sock2, &fd_out );\n\n// find out which socket has the largest numeric value as select requires it\nint largest_sock = sock1 > sock2 ? sock1 : sock2;\n\n// wait up to 10 seconds\ntv.tv_sec = 10;\ntv.tv_usec = 0;\n\n// call the select\nint ret = select( largest_sock + 1, &fd_in, &fd_out, null, &tv );\n\n// check if select actually succeed\nif ( ret == -1 )\n // report error and abort\nelse if ( ret == 0 )\n // timeout; no event detected\nelse\n{\n if ( fd_isset( sock1, &fd_in ) )\n // input event on sock1\n\n if ( fd_isset( sock2, &fd_out ) )\n // output event on sock2\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n\n\n\n# poll\n\nint poll(struct pollfd *fds, unsigned int nfds, int timeout);\n\n\n1\n\n\npoll 的功能与 select 类似,也是等待一组描述符中的一个成为就绪状态。\n\npoll 中的描述符是 pollfd 类型的数组,pollfd 的定义如下:\n\nstruct pollfd {\n int fd; /* file descriptor */\n short events; /* requested events */\n short revents; /* returned events */\n };\n\n\n1\n2\n3\n4\n5\n\n\n// the structure for two events\nstruct pollfd fds[2];\n\n// monitor sock1 for input\nfds[0].fd = sock1;\nfds[0].events = pollin;\n\n// monitor sock2 for output\nfds[1].fd = sock2;\nfds[1].events = pollout;\n\n// wait 10 seconds\nint ret = poll( &fds, 2, 10000 );\n// check if poll actually succeed\nif ( ret == -1 )\n // report error and abort\nelse if ( ret == 0 )\n // timeout; no event detected\nelse\n{\n // if we detect the event, zero it out so we can reuse the structure\n if ( fds[0].revents & pollin )\n fds[0].revents = 0;\n // input event on sock1\n\n if ( fds[1].revents & pollout )\n fds[1].revents = 0;\n // output event on sock2\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n\n\n\n# 比较\n\n\n# 1. 功能\n\nselect 和 poll 的功能基本相同,不过在一些实现细节上有所不同。\n\n * select 会修改描述符,而 poll 不会;\n * select 的描述符类型使用数组实现,fd_setsize 大小默认为 1024,因此默认只能监听少于 1024 个描述符。如果要监听更多描述符的话,需要修改 fd_setsize 之后重新编译;而 poll 没有描述符数量的限制;\n * poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高。\n * 如果一个线程对某个描述符调用了 select 或者 poll,另一个线程关闭了该描述符,会导致调用结果不确定。\n\n\n# 2. 速度\n\nselect 和 poll 速度都比较慢,每次调用都需要将全部描述符从应用进程缓冲区复制到内核缓冲区。\n\n\n# 3. 可移植性\n\n几乎所有的系统都支持 select,但是只有比较新的系统支持 poll。\n\n\n# epoll\n\nint epoll_create(int size);\nint epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);\nint epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);\n\n\n1\n2\n3\n\n\nepoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 i/o 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符。\n\n从上面的描述可以看出,epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次,并且进程不需要通过轮询来获得事件完成的描述符。\n\nepoll 仅适用于 linux os。\n\nepoll 比 select 和 poll 更加灵活而且没有描述符数量限制。\n\nepoll 对多线程编程更有友好,一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符也不会产生像 select 和 poll 的不确定情况。\n\n// create the epoll descriptor. only one is needed per app, and is used to monitor all sockets.\n// the function argument is ignored (it was not before, but now it is), so put your favorite number here\nint pollingfd = epoll_create( 0xcafe );\n\nif ( pollingfd < 0 )\n // report error\n\n// initialize the epoll structure in case more members are added in future\nstruct epoll_event ev = { 0 };\n\n// associate the connection class instance with the event. you can associate anything\n// you want, epoll does not use this information. we store a connection class pointer, pconnection1\nev.data.ptr = pconnection1;\n\n// monitor for input, and do not automatically rearm the descriptor after the event\nev.events = epollin | epolloneshot;\n// add the descriptor into the monitoring list. we can do it even if another thread is\n// waiting in epoll_wait - the descriptor will be properly added\nif ( epoll_ctl( epollfd, epoll_ctl_add, pconnection1->getsocket(), &ev ) != 0 )\n // report error\n\n// wait for up to 20 events (assuming we have added maybe 200 sockets before that it may happen)\nstruct epoll_event pevents[ 20 ];\n\n// wait for 10 seconds, and retrieve less than 20 epoll_event and store them into epoll_event array\nint ready = epoll_wait( pollingfd, pevents, 20, 10000 );\n// check if epoll actually succeed\nif ( ret == -1 )\n // report error and abort\nelse if ( ret == 0 )\n // timeout; no event detected\nelse\n{\n // check if any events detected\n for ( int i = 0; i < ready; i++ )\n {\n if ( pevents[i].events & epollin )\n {\n // get back our connection pointer\n connection * c = (connection*) pevents[i].data.ptr;\n c->handlereadevent();\n }\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n\n\n\n# 工作模式\n\nepoll 的描述符事件有两种触发模式:lt(level trigger)和 et(edge trigger)。\n\n\n# 1. lt 模式\n\n当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 blocking 和 no-blocking。\n\n\n# 2. et 模式\n\n和 lt 模式不同的是,通知之后进程必须立即处理事件,下次再调用 epoll_wait() 时不会再得到事件到达的通知。\n\n很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 lt 模式高。只支持 no-blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。\n\n\n# 应用场景\n\n很容易产生一种错觉认为只要用 epoll 就可以了,select 和 poll 都已经过时了,其实它们都有各自的使用场景。\n\n\n# 1. select 应用场景\n\nselect 的 timeout 参数精度为微秒,而 poll 和 epoll 为毫秒,因此 select 更加适用于实时性要求比较高的场景,比如核反应堆的控制。\n\nselect 可移植性更好,几乎被所有主流平台所支持。\n\n\n# 2. poll 应用场景\n\npoll 没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应该使用 poll 而不是 select。\n\n\n# 3. epoll 应用场景\n\n只需要运行在 linux 平台上,有大量的描述符需要同时轮询,并且这些连接最好是长连接。\n\n需要同时监控小于 1000 个描述符,就没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。\n\n需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。\n\n因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。\n\n并且 epoll 的描述符存储在内核,不容易调试。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 + }, + { + "title": "Linux 下如何排查 CPU 以及 内存占用过多?", + "frontmatter": { + "title": "Linux 下如何排查 CPU 以及 内存占用过多?", + "date": "2022-09-21T23:01:37.000Z", + "permalink": "/pages/b043a6/", + "categories": [ + "计算机基础", + "操作系统" + ], + "tags": [ + null + ] + }, + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/080.Linux%20%E4%B8%8B%E5%A6%82%E4%BD%95%E6%8E%92%E6%9F%A5%20CPU%20%E4%BB%A5%E5%8F%8A%20%E5%86%85%E5%AD%98%E5%8D%A0%E7%94%A8%E8%BF%87%E5%A4%9A%EF%BC%9F.html", + "relativePath": "02.计算机基础/20.操作系统/080.Linux 下如何排查 CPU 以及 内存占用过多?.md", + "key": "v-05845816", + "path": "/pages/b043a6/", "headersStr": null, "content": "", "normalizedContent": "", - "charsets": {} + "charsets": {}, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "进程通信中的管道实现原理是什么?", @@ -5445,14 +6230,16 @@ export const siteData = { "headersStr": null, "content": "", "normalizedContent": "", - "charsets": {} + "charsets": {}, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { - "title": "Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程?", + "title": "Linux 如何查看实时的滚动日志?", "frontmatter": { - "title": "Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程?", - "date": "2022-09-21T23:02:10.000Z", - "permalink": "/pages/326cf9/", + "title": "Linux 如何查看实时的滚动日志?", + "date": "2022-09-21T23:02:33.000Z", + "permalink": "/pages/f6e4d2/", "categories": [ "计算机基础", "操作系统" @@ -5461,14 +6248,16 @@ export const siteData = { null ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/100.Linux%20%E4%B8%8B%E5%A6%82%E4%BD%95%E6%9F%A5%E7%9C%8B%20CPU%20%E8%8D%B7%E8%BD%BD%EF%BC%8C%E6%AD%A3%E5%9C%A8%E8%BF%90%E8%A1%8C%E7%9A%84%E8%BF%9B%E7%A8%8B%EF%BC%8C%E6%9F%90%E4%B8%AA%E7%AB%AF%E5%8F%A3%E5%AF%B9%E5%BA%94%E7%9A%84%E8%BF%9B%E7%A8%8B%EF%BC%9F.html", - "relativePath": "02.计算机基础/20.操作系统/100.Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程?.md", - "key": "v-1f69ea23", - "path": "/pages/326cf9/", + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/120.Linux%20%E5%A6%82%E4%BD%95%E6%9F%A5%E7%9C%8B%E5%AE%9E%E6%97%B6%E7%9A%84%E6%BB%9A%E5%8A%A8%E6%97%A5%E5%BF%97%EF%BC%9F.html", + "relativePath": "02.计算机基础/20.操作系统/120.Linux 如何查看实时的滚动日志?.md", + "key": "v-129dffb8", + "path": "/pages/f6e4d2/", "headersStr": null, "content": "", "normalizedContent": "", - "charsets": {} + "charsets": {}, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "如何调试服务器内存占用过高的问题?", @@ -5491,14 +6280,16 @@ export const siteData = { "headersStr": null, "content": "", "normalizedContent": "", - "charsets": {} + "charsets": {}, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { - "title": "Linux 如何查看实时的滚动日志?", + "title": "简述 Linux 零拷贝的原理", "frontmatter": { - "title": "Linux 如何查看实时的滚动日志?", - "date": "2022-09-21T23:02:33.000Z", - "permalink": "/pages/f6e4d2/", + "title": "简述 Linux 零拷贝的原理", + "date": "2022-09-21T23:02:44.000Z", + "permalink": "/pages/b43547/", "categories": [ "计算机基础", "操作系统" @@ -5507,21 +6298,23 @@ export const siteData = { null ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/120.Linux%20%E5%A6%82%E4%BD%95%E6%9F%A5%E7%9C%8B%E5%AE%9E%E6%97%B6%E7%9A%84%E6%BB%9A%E5%8A%A8%E6%97%A5%E5%BF%97%EF%BC%9F.html", - "relativePath": "02.计算机基础/20.操作系统/120.Linux 如何查看实时的滚动日志?.md", - "key": "v-129dffb8", - "path": "/pages/f6e4d2/", + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/130.%E7%AE%80%E8%BF%B0%20Linux%20%E9%9B%B6%E6%8B%B7%E8%B4%9D%E7%9A%84%E5%8E%9F%E7%90%86.html", + "relativePath": "02.计算机基础/20.操作系统/130.简述 Linux 零拷贝的原理.md", + "key": "v-7abb1115", + "path": "/pages/b43547/", "headersStr": null, "content": "", "normalizedContent": "", - "charsets": {} + "charsets": {}, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { - "title": "简述 Linux 零拷贝的原理", + "title": "Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程?", "frontmatter": { - "title": "简述 Linux 零拷贝的原理", - "date": "2022-09-21T23:02:44.000Z", - "permalink": "/pages/b43547/", + "title": "Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程?", + "date": "2022-09-21T23:02:10.000Z", + "permalink": "/pages/326cf9/", "categories": [ "计算机基础", "操作系统" @@ -5530,14 +6323,41 @@ export const siteData = { null ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/130.%E7%AE%80%E8%BF%B0%20Linux%20%E9%9B%B6%E6%8B%B7%E8%B4%9D%E7%9A%84%E5%8E%9F%E7%90%86.html", - "relativePath": "02.计算机基础/20.操作系统/130.简述 Linux 零拷贝的原理.md", - "key": "v-7abb1115", - "path": "/pages/b43547/", + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/100.Linux%20%E4%B8%8B%E5%A6%82%E4%BD%95%E6%9F%A5%E7%9C%8B%20CPU%20%E8%8D%B7%E8%BD%BD%EF%BC%8C%E6%AD%A3%E5%9C%A8%E8%BF%90%E8%A1%8C%E7%9A%84%E8%BF%9B%E7%A8%8B%EF%BC%8C%E6%9F%90%E4%B8%AA%E7%AB%AF%E5%8F%A3%E5%AF%B9%E5%BA%94%E7%9A%84%E8%BF%9B%E7%A8%8B%EF%BC%9F.html", + "relativePath": "02.计算机基础/20.操作系统/100.Linux 下如何查看 CPU 荷载,正在运行的进程,某个端口对应的进程?.md", + "key": "v-1f69ea23", + "path": "/pages/326cf9/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 + }, + { + "title": "简述 Linux 虚拟内存的页面置换算法", + "frontmatter": { + "title": "简述 Linux 虚拟内存的页面置换算法", + "date": "2022-09-21T23:03:19.000Z", + "permalink": "/pages/93b870/", + "categories": [ + "计算机基础", + "操作系统" + ], + "tags": [ + null + ] + }, + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/160.%E7%AE%80%E8%BF%B0%20Linux%20%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98%E7%9A%84%E9%A1%B5%E9%9D%A2%E7%BD%AE%E6%8D%A2%E7%AE%97%E6%B3%95.html", + "relativePath": "02.计算机基础/20.操作系统/160.简述 Linux 虚拟内存的页面置换算法.md", + "key": "v-1a49912a", + "path": "/pages/93b870/", "headersStr": null, "content": "", "normalizedContent": "", - "charsets": {} + "charsets": {}, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "为什么进程切换慢,线程切换快?", @@ -5560,7 +6380,9 @@ export const siteData = { "headersStr": null, "content": "", "normalizedContent": "", - "charsets": {} + "charsets": {}, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "简述 Linux 进程调度的算法", @@ -5583,30 +6405,9 @@ export const siteData = { "headersStr": null, "content": "", "normalizedContent": "", - "charsets": {} - }, - { - "title": "简述 Linux 虚拟内存的页面置换算法", - "frontmatter": { - "title": "简述 Linux 虚拟内存的页面置换算法", - "date": "2022-09-21T23:03:19.000Z", - "permalink": "/pages/93b870/", - "categories": [ - "计算机基础", - "操作系统" - ], - "tags": [ - null - ] - }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/160.%E7%AE%80%E8%BF%B0%20Linux%20%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98%E7%9A%84%E9%A1%B5%E9%9D%A2%E7%BD%AE%E6%8D%A2%E7%AE%97%E6%B3%95.html", - "relativePath": "02.计算机基础/20.操作系统/160.简述 Linux 虚拟内存的页面置换算法.md", - "key": "v-1a49912a", - "path": "/pages/93b870/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {} + "charsets": {}, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "简述创建进程的流程", @@ -5629,7 +6430,9 @@ export const siteData = { "headersStr": null, "content": "", "normalizedContent": "", - "charsets": {} + "charsets": {}, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "什么时候会由用户态陷入内核态?", @@ -5652,14 +6455,16 @@ export const siteData = { "headersStr": null, "content": "", "normalizedContent": "", - "charsets": {} + "charsets": {}, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { - "title": "Linux 中虚拟内存和物理内存有什么区别?有什么优点?", + "title": "Linux 下如何查看端口被哪个进程占用?", "frontmatter": { - "title": "Linux 中虚拟内存和物理内存有什么区别?有什么优点?", - "date": "2022-09-21T23:04:03.000Z", - "permalink": "/pages/3bab8c/", + "title": "Linux 下如何查看端口被哪个进程占用?", + "date": "2022-09-21T23:03:52.000Z", + "permalink": "/pages/4c2d8e/", "categories": [ "计算机基础", "操作系统" @@ -5668,21 +6473,23 @@ export const siteData = { null ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/200.Linux%20%E4%B8%AD%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98%E5%92%8C%E7%89%A9%E7%90%86%E5%86%85%E5%AD%98%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F%E6%9C%89%E4%BB%80%E4%B9%88%E4%BC%98%E7%82%B9%EF%BC%9F.html", - "relativePath": "02.计算机基础/20.操作系统/200.Linux 中虚拟内存和物理内存有什么区别?有什么优点?.md", - "key": "v-db80e4da", - "path": "/pages/3bab8c/", + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/190.Linux%20%E4%B8%8B%E5%A6%82%E4%BD%95%E6%9F%A5%E7%9C%8B%E7%AB%AF%E5%8F%A3%E8%A2%AB%E5%93%AA%E4%B8%AA%E8%BF%9B%E7%A8%8B%E5%8D%A0%E7%94%A8%EF%BC%9F.html", + "relativePath": "02.计算机基础/20.操作系统/190.Linux 下如何查看端口被哪个进程占用?.md", + "key": "v-1ea2064a", + "path": "/pages/4c2d8e/", "headersStr": null, "content": "", "normalizedContent": "", - "charsets": {} + "charsets": {}, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { - "title": "Linux 下如何查看端口被哪个进程占用?", + "title": "Untitled", "frontmatter": { - "title": "Linux 下如何查看端口被哪个进程占用?", - "date": "2022-09-21T23:03:52.000Z", - "permalink": "/pages/4c2d8e/", + "title": "Untitled", + "date": "2022-10-09T11:07:13.000Z", + "permalink": "/pages/782c23/", "categories": [ "计算机基础", "操作系统" @@ -5691,21 +6498,67 @@ export const siteData = { null ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/190.Linux%20%E4%B8%8B%E5%A6%82%E4%BD%95%E6%9F%A5%E7%9C%8B%E7%AB%AF%E5%8F%A3%E8%A2%AB%E5%93%AA%E4%B8%AA%E8%BF%9B%E7%A8%8B%E5%8D%A0%E7%94%A8%EF%BC%9F.html", - "relativePath": "02.计算机基础/20.操作系统/190.Linux 下如何查看端口被哪个进程占用?.md", - "key": "v-1ea2064a", - "path": "/pages/4c2d8e/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {} + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/220.%E7%AE%80%E8%BF%B0IO%E6%A8%A1%E5%9E%8B.html", + "relativePath": "02.计算机基础/20.操作系统/220.简述IO模型.md", + "key": "v-d41ba7ec", + "path": "/pages/782c23/", + "headers": [ + { + "level": 3, + "title": "## 阻塞式 I/O", + "slug": "阻塞式-i-o", + "normalizedTitle": "## 阻塞式 i/o", + "charIndex": 232 + }, + { + "level": 3, + "title": "## 非阻塞式 I/O", + "slug": "非阻塞式-i-o", + "normalizedTitle": "## 非阻塞式 i/o", + "charIndex": 569 + }, + { + "level": 3, + "title": "## I/O 复用", + "slug": "i-o-复用", + "normalizedTitle": "## i/o 复用", + "charIndex": 706 + }, + { + "level": 3, + "title": "## 信号驱动 I/O", + "slug": "信号驱动-i-o", + "normalizedTitle": "## 信号驱动 i/o", + "charIndex": 998 + }, + { + "level": 3, + "title": "[#](http://www.cyc2018.xyz/计算机基础/Socket/Socket.html#异步-i-o)异步 I/O", + "slug": "异步-i-o", + "normalizedTitle": "#异步 i/o", + "charIndex": null + }, + { + "level": 3, + "title": "## 五大 I/O 模型比较", + "slug": "五大-i-o-模型比较", + "normalizedTitle": "## 五大 i/o 模型比较", + "charIndex": 1347 + } + ], + "headersStr": "## 阻塞式 I/O ## 非阻塞式 I/O ## I/O 复用 ## 信号驱动 I/O [#](http://www.cyc2018.xyz/计算机基础/Socket/Socket.html#异步-i-o)异步 I/O ## 五大 I/O 模型比较", + "content": "一个输入操作通常包括两个阶段:\n\n * 等待数据准备好\n * 从内核向进程复制数据\n\n对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待数据到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。\n\nUnix 有五种 I/O 模型:\n\n * 阻塞式 I/O\n * 非阻塞式 I/O\n * I/O 复用(select 和 poll)\n * 信号驱动式 I/O(SIGIO)\n * 异步 I/O(AIO)\n\n\n# ## 阻塞式 I/O\n\n应用进程被阻塞,直到数据从内核缓冲区复制到应用进程缓冲区中才返回。\n\n应该注意到,在阻塞的过程中,其它应用进程还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其它应用进程还可以执行,所以不消耗 CPU 时间,这种模型的 CPU 利用率会比较高。\n\n下图中,recvfrom() 用于接收 Socket 传来的数据,并复制到应用进程的缓冲区 buf 中。这里把 recvfrom() 当成系统调用。\n\nssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);\n\n\n1\n\n\n\n\n\n# ## 非阻塞式 I/O\n\n应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询(polling)。\n\n由于 CPU 要处理更多的系统调用,因此这种模型的 CPU 利用率比较低。\n\n\n\n\n# ## I/O 复用\n\n使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读。这一过程会被阻塞,当某一个套接字可读时返回,之后再使用 recvfrom 把数据从内核复制到进程中。\n\n它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O,即事件驱动 I/O。\n\n如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都需要创建一个线程去处理。如果同时有几万个连接,那么就需要创建相同数量的线程。相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销更小。\n\n\n\n\n# ## 信号驱动 I/O\n\n应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。\n\n相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高。\n\n\n\n\n# #异步 I/O\n\n应用进程执行 aio_read 系统调用会立即返回,应用进程可以继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。\n\n异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。\n\n\n\n\n# ## 五大 I/O 模型比较\n\n * 同步 I/O:将数据从内核缓冲区复制到应用进程缓冲区的阶段(第二阶段),应用进程会阻塞。\n * 异步 I/O:第二阶段应用进程不会阻塞。\n\n同步 I/O 包括阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O ,它们的主要区别在第一个阶段。\n\n非阻塞式 I/O 、信号驱动 I/O 和异步 I/O 在第一阶段不会阻塞。", + "normalizedContent": "一个输入操作通常包括两个阶段:\n\n * 等待数据准备好\n * 从内核向进程复制数据\n\n对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待数据到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。\n\nunix 有五种 i/o 模型:\n\n * 阻塞式 i/o\n * 非阻塞式 i/o\n * i/o 复用(select 和 poll)\n * 信号驱动式 i/o(sigio)\n * 异步 i/o(aio)\n\n\n# ## 阻塞式 i/o\n\n应用进程被阻塞,直到数据从内核缓冲区复制到应用进程缓冲区中才返回。\n\n应该注意到,在阻塞的过程中,其它应用进程还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其它应用进程还可以执行,所以不消耗 cpu 时间,这种模型的 cpu 利用率会比较高。\n\n下图中,recvfrom() 用于接收 socket 传来的数据,并复制到应用进程的缓冲区 buf 中。这里把 recvfrom() 当成系统调用。\n\nssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);\n\n\n1\n\n\n\n\n\n# ## 非阻塞式 i/o\n\n应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 i/o 是否完成,这种方式称为轮询(polling)。\n\n由于 cpu 要处理更多的系统调用,因此这种模型的 cpu 利用率比较低。\n\n\n\n\n# ## i/o 复用\n\n使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读。这一过程会被阻塞,当某一个套接字可读时返回,之后再使用 recvfrom 把数据从内核复制到进程中。\n\n它可以让单个进程具有处理多个 i/o 事件的能力。又被称为 event driven i/o,即事件驱动 i/o。\n\n如果一个 web 服务器没有 i/o 复用,那么每一个 socket 连接都需要创建一个线程去处理。如果同时有几万个连接,那么就需要创建相同数量的线程。相比于多进程和多线程技术,i/o 复用不需要进程线程创建和切换的开销,系统开销更小。\n\n\n\n\n# ## 信号驱动 i/o\n\n应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 sigio 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。\n\n相比于非阻塞式 i/o 的轮询方式,信号驱动 i/o 的 cpu 利用率更高。\n\n\n\n\n# #异步 i/o\n\n应用进程执行 aio_read 系统调用会立即返回,应用进程可以继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。\n\n异步 i/o 与信号驱动 i/o 的区别在于,异步 i/o 的信号是通知应用进程 i/o 完成,而信号驱动 i/o 的信号是通知应用进程可以开始 i/o。\n\n\n\n\n# ## 五大 i/o 模型比较\n\n * 同步 i/o:将数据从内核缓冲区复制到应用进程缓冲区的阶段(第二阶段),应用进程会阻塞。\n * 异步 i/o:第二阶段应用进程不会阻塞。\n\n同步 i/o 包括阻塞式 i/o、非阻塞式 i/o、i/o 复用和信号驱动 i/o ,它们的主要区别在第一个阶段。\n\n非阻塞式 i/o 、信号驱动 i/o 和异步 i/o 在第一阶段不会阻塞。", + "charsets": { + "cjk": true + } }, { - "title": "简述 traceroute 命令的原理", + "title": "Linux 中虚拟内存和物理内存有什么区别?有什么优点?", "frontmatter": { - "title": "简述 traceroute 命令的原理", - "date": "2022-09-21T23:04:14.000Z", - "permalink": "/pages/655f8f/", + "title": "Linux 中虚拟内存和物理内存有什么区别?有什么优点?", + "date": "2022-09-21T23:04:03.000Z", + "permalink": "/pages/3bab8c/", "categories": [ "计算机基础", "操作系统" @@ -5714,14 +6567,16 @@ export const siteData = { null ] }, - "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/210.%E7%AE%80%E8%BF%B0%20traceroute%20%E5%91%BD%E4%BB%A4%E7%9A%84%E5%8E%9F%E7%90%86.html", - "relativePath": "02.计算机基础/20.操作系统/210.简述 traceroute 命令的原理.md", - "key": "v-e048e838", - "path": "/pages/655f8f/", + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/200.Linux%20%E4%B8%AD%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98%E5%92%8C%E7%89%A9%E7%90%86%E5%86%85%E5%AD%98%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F%E6%9C%89%E4%BB%80%E4%B9%88%E4%BC%98%E7%82%B9%EF%BC%9F.html", + "relativePath": "02.计算机基础/20.操作系统/200.Linux 中虚拟内存和物理内存有什么区别?有什么优点?.md", + "key": "v-db80e4da", + "path": "/pages/3bab8c/", "headersStr": null, "content": "", "normalizedContent": "", - "charsets": {} + "charsets": {}, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 }, { "title": "数据结构", @@ -5808,49 +6663,6 @@ export const siteData = { "lastUpdated": "2022/06/20, 22:47:41", "lastUpdatedTimestamp": 1655736461000 }, - { - "title": "MySQL中 InnoDB 和 MylSAM 的区别是什么?", - "frontmatter": { - "title": "MySQL中 InnoDB 和 MylSAM 的区别是什么?", - "date": "2022-05-21T17:07:47.000Z", - "permalink": "/pages/3a8659/", - "categories": [ - "数据库", - "MYSQL相关" - ], - "tags": [ - null - ] - }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/001.MySQL%E4%B8%AD%20InnoDB%20%E5%92%8C%20MylSAM%20%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/001.MySQL中 InnoDB 和 MylSAM 的区别是什么?.md", - "key": "v-674454ac", - "path": "/pages/3a8659/", - "headers": [ - { - "level": 2, - "title": "区别", - "slug": "区别", - "normalizedTitle": "区别", - "charIndex": 37 - }, - { - "level": 2, - "title": "场景选择", - "slug": "场景选择", - "normalizedTitle": "场景选择", - "charIndex": 253 - } - ], - "headersStr": "区别 场景选择", - "content": "MySQL5.5版本开始Innodb已经成为Mysql的默认引擎\n\n\n# 区别\n\n * InnoDB 支持事务,MyISAM 不支持事务。 这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一\n * MyISAM锁的粒度是表级的,而InnoDB支持行级锁\n * MyISAM支持全文类型索引,而InnoDB不支持全文索引\n * MyISAM相对简单,效率上要优于InnoDB,小型应用可以考虑使用MyISAM\n * MyISAM表保存成文件形式,跨平台使用更加方便\n\n\n# 场景选择\n\nMyISAM管理非事务表,提供高速存储和检索以及全文搜索能力,如果再应用中执行大量select操作,应该选择MyISAM InnoDB用于事务处理,具有ACID事务支持等特性,如果在应用中执行大量insert和update操作,应该选择InnoDB", - "normalizedContent": "mysql5.5版本开始innodb已经成为mysql的默认引擎\n\n\n# 区别\n\n * innodb 支持事务,myisam 不支持事务。 这是 mysql 将默认存储引擎从 myisam 变成 innodb 的重要原因之一\n * myisam锁的粒度是表级的,而innodb支持行级锁\n * myisam支持全文类型索引,而innodb不支持全文索引\n * myisam相对简单,效率上要优于innodb,小型应用可以考虑使用myisam\n * myisam表保存成文件形式,跨平台使用更加方便\n\n\n# 场景选择\n\nmyisam管理非事务表,提供高速存储和检索以及全文搜索能力,如果再应用中执行大量select操作,应该选择myisam innodb用于事务处理,具有acid事务支持等特性,如果在应用中执行大量insert和update操作,应该选择innodb", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, { "title": "算法", "frontmatter": { @@ -5900,27 +6712,95 @@ export const siteData = { }, { "level": 2, - "title": "hash算法", - "slug": "hash算法", - "normalizedTitle": "hash算法", - "charIndex": 43 + "title": "hash算法", + "slug": "hash算法", + "normalizedTitle": "hash算法", + "charIndex": 43 + }, + { + "level": 2, + "title": "海量数据处理", + "slug": "海量数据处理", + "normalizedTitle": "海量数据处理", + "charIndex": 54 + } + ], + "headersStr": "数据结构与算法知识 全排列 贪心算法 KMP算法 hash算法 海量数据处理", + "content": "# 数据结构与算法知识\n\n\n# 全排列\n\n\n# 贪心算法\n\n\n# KMP算法\n\n\n# hash算法\n\n\n# 海量数据处理\n\n* 分治\n* hash映射\n* 堆排序\n* 双层桶划分\n* Bloom Filter\n* bitmap\n* 数据库索引\n* mapreduce等。", + "normalizedContent": "# 数据结构与算法知识\n\n\n# 全排列\n\n\n# 贪心算法\n\n\n# kmp算法\n\n\n# hash算法\n\n\n# 海量数据处理\n\n* 分治\n* hash映射\n* 堆排序\n* 双层桶划分\n* bloom filter\n* bitmap\n* 数据库索引\n* mapreduce等。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/06/20, 22:47:41", + "lastUpdatedTimestamp": 1655736461000 + }, + { + "title": "简述 traceroute 命令的原理", + "frontmatter": { + "title": "简述 traceroute 命令的原理", + "date": "2022-09-21T23:04:14.000Z", + "permalink": "/pages/655f8f/", + "categories": [ + "计算机基础", + "操作系统" + ], + "tags": [ + null + ] + }, + "regularPath": "/02.%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/20.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/210.%E7%AE%80%E8%BF%B0%20traceroute%20%E5%91%BD%E4%BB%A4%E7%9A%84%E5%8E%9F%E7%90%86.html", + "relativePath": "02.计算机基础/20.操作系统/210.简述 traceroute 命令的原理.md", + "key": "v-e048e838", + "path": "/pages/655f8f/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/09/26, 09:00:38", + "lastUpdatedTimestamp": 1664154038000 + }, + { + "title": "MySQL中 InnoDB 和 MylSAM 的区别是什么?", + "frontmatter": { + "title": "MySQL中 InnoDB 和 MylSAM 的区别是什么?", + "date": "2022-05-21T17:07:47.000Z", + "permalink": "/pages/3a8659/", + "categories": [ + "数据库", + "MYSQL相关" + ], + "tags": [ + null + ] + }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/001.MySQL%E4%B8%AD%20InnoDB%20%E5%92%8C%20MylSAM%20%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/001.MySQL中 InnoDB 和 MylSAM 的区别是什么?.md", + "key": "v-674454ac", + "path": "/pages/3a8659/", + "headers": [ + { + "level": 2, + "title": "区别", + "slug": "区别", + "normalizedTitle": "区别", + "charIndex": 37 }, { "level": 2, - "title": "海量数据处理", - "slug": "海量数据处理", - "normalizedTitle": "海量数据处理", - "charIndex": 54 + "title": "场景选择", + "slug": "场景选择", + "normalizedTitle": "场景选择", + "charIndex": 253 } ], - "headersStr": "数据结构与算法知识 全排列 贪心算法 KMP算法 hash算法 海量数据处理", - "content": "# 数据结构与算法知识\n\n\n# 全排列\n\n\n# 贪心算法\n\n\n# KMP算法\n\n\n# hash算法\n\n\n# 海量数据处理\n\n* 分治\n* hash映射\n* 堆排序\n* 双层桶划分\n* Bloom Filter\n* bitmap\n* 数据库索引\n* mapreduce等。", - "normalizedContent": "# 数据结构与算法知识\n\n\n# 全排列\n\n\n# 贪心算法\n\n\n# kmp算法\n\n\n# hash算法\n\n\n# 海量数据处理\n\n* 分治\n* hash映射\n* 堆排序\n* 双层桶划分\n* bloom filter\n* bitmap\n* 数据库索引\n* mapreduce等。", + "headersStr": "区别 场景选择", + "content": "MySQL5.5版本开始Innodb已经成为Mysql的默认引擎\n\n\n# 区别\n\n * InnoDB 支持事务,MyISAM 不支持事务。 这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一\n * MyISAM锁的粒度是表级的,而InnoDB支持行级锁\n * MyISAM支持全文类型索引,而InnoDB不支持全文索引\n * MyISAM相对简单,效率上要优于InnoDB,小型应用可以考虑使用MyISAM\n * MyISAM表保存成文件形式,跨平台使用更加方便\n\n\n# 场景选择\n\nMyISAM管理非事务表,提供高速存储和检索以及全文搜索能力,如果再应用中执行大量select操作,应该选择MyISAM InnoDB用于事务处理,具有ACID事务支持等特性,如果在应用中执行大量insert和update操作,应该选择InnoDB", + "normalizedContent": "mysql5.5版本开始innodb已经成为mysql的默认引擎\n\n\n# 区别\n\n * innodb 支持事务,myisam 不支持事务。 这是 mysql 将默认存储引擎从 myisam 变成 innodb 的重要原因之一\n * myisam锁的粒度是表级的,而innodb支持行级锁\n * myisam支持全文类型索引,而innodb不支持全文索引\n * myisam相对简单,效率上要优于innodb,小型应用可以考虑使用myisam\n * myisam表保存成文件形式,跨平台使用更加方便\n\n\n# 场景选择\n\nmyisam管理非事务表,提供高速存储和检索以及全文搜索能力,如果再应用中执行大量select操作,应该选择myisam innodb用于事务处理,具有acid事务支持等特性,如果在应用中执行大量insert和update操作,应该选择innodb", "charsets": { "cjk": true }, - "lastUpdated": "2022/06/20, 22:47:41", - "lastUpdatedTimestamp": 1655736461000 + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 }, { "title": "聚簇索引和非聚簇索引有什么区别?", @@ -5965,6 +6845,31 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, + { + "title": "简述 MySQL 常见索引类型,介绍一下覆盖索引", + "frontmatter": { + "title": "简述 MySQL 常见索引类型,介绍一下覆盖索引", + "date": "2022-05-21T17:08:24.000Z", + "permalink": "/pages/17b014/", + "categories": [ + "数据库", + "MYSQL相关" + ], + "tags": [ + null + ] + }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/007.%E7%AE%80%E8%BF%B0%20MySQL%20%E5%B8%B8%E8%A7%81%E7%B4%A2%E5%BC%95%E7%B1%BB%E5%9E%8B%EF%BC%8C%E4%BB%8B%E7%BB%8D%E4%B8%80%E4%B8%8B%E8%A6%86%E7%9B%96%E7%B4%A2%E5%BC%95.html", + "relativePath": "03.数据库/01.MYSQL相关/007.简述 MySQL 常见索引类型,介绍一下覆盖索引.md", + "key": "v-824b776c", + "path": "/pages/17b014/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, { "title": "简述Mysql的事务隔离级别", "frontmatter": { @@ -6022,6 +6927,49 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, + { + "title": "MySQL 为什么使用 B+ 树来作索引,对比 B 树它的优点和缺点是什么", + "frontmatter": { + "title": "MySQL 为什么使用 B+ 树来作索引,对比 B 树它的优点和缺点是什么", + "date": "2022-05-21T17:05:11.000Z", + "permalink": "/pages/bc5538/", + "categories": [ + "数据库", + "MYSQL相关" + ], + "tags": [ + null + ] + }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/020.MySQL%20%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BD%BF%E7%94%A8%20B+%20%E6%A0%91%E6%9D%A5%E4%BD%9C%E7%B4%A2%E5%BC%95%EF%BC%8C%E5%AF%B9%E6%AF%94%20B%20%E6%A0%91%E5%AE%83%E7%9A%84%E4%BC%98%E7%82%B9%E5%92%8C%E7%BC%BA%E7%82%B9%E6%98%AF%E4%BB%80%E4%B9%88.html", + "relativePath": "03.数据库/01.MYSQL相关/020.MySQL 为什么使用 B+ 树来作索引,对比 B 树它的优点和缺点是什么.md", + "key": "v-6c681639", + "path": "/pages/bc5538/", + "headers": [ + { + "level": 2, + "title": "磁盘存取原理", + "slug": "磁盘存取原理", + "normalizedTitle": "磁盘存取原理", + "charIndex": 59 + }, + { + "level": 2, + "title": "B-/+Tree索引的性能分析", + "slug": "b-tree索引的性能分析", + "normalizedTitle": "b-/+tree索引的性能分析", + "charIndex": 246 + } + ], + "headersStr": "磁盘存取原理 B-/+Tree索引的性能分析", + "content": "> 索引本身也是以文件的形式存储在磁盘上,索引查找过程中存在I/O消耗,采用B tree结构可以减少I/O。\n\n\n# 磁盘存取原理\n\n> 局部性原理与磁盘预读:当一个数据被用到时,其附近的数据也很快会被用到。 预读的长度一般为页的整数倍,主存和磁盘以页为单位交换数据。\n\n磁盘和磁头构成了存取物理结构。同心圆环是磁道,磁道被划分成一个个段,叫扇区,扇区是最小存储单元。 i/o读取数据的过程:磁盘旋转到指定扇区,磁头移动到指定磁道。 所以减少磁盘旋转或者减少磁头移动就能减少i/o。\n\n\n# B-/+Tree索引的性能分析\n\nB Tree利用局部性原理可减少i/o次数。 根据B Tree定义可知,检索一次最多需要访问h个节点,h-1次i/o,h是树高,mysql设计者巧妙的将一个节点大小设置为等于一个页大小,每次新建节点都申请一个页的空间,这样就保证一个节点物理上也存储在一个页,这样每个节点只需要一次i/o就可以完全载入。", + "normalizedContent": "> 索引本身也是以文件的形式存储在磁盘上,索引查找过程中存在i/o消耗,采用b tree结构可以减少i/o。\n\n\n# 磁盘存取原理\n\n> 局部性原理与磁盘预读:当一个数据被用到时,其附近的数据也很快会被用到。 预读的长度一般为页的整数倍,主存和磁盘以页为单位交换数据。\n\n磁盘和磁头构成了存取物理结构。同心圆环是磁道,磁道被划分成一个个段,叫扇区,扇区是最小存储单元。 i/o读取数据的过程:磁盘旋转到指定扇区,磁头移动到指定磁道。 所以减少磁盘旋转或者减少磁头移动就能减少i/o。\n\n\n# b-/+tree索引的性能分析\n\nb tree利用局部性原理可减少i/o次数。 根据b tree定义可知,检索一次最多需要访问h个节点,h-1次i/o,h是树高,mysql设计者巧妙的将一个节点大小设置为等于一个页大小,每次新建节点都申请一个页的空间,这样就保证一个节点物理上也存储在一个页,这样每个节点只需要一次i/o就可以完全载入。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, { "title": "说一下Mysql锁", "frontmatter": { @@ -6080,11 +7028,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述 MySQL 常见索引类型,介绍一下覆盖索引", + "title": "简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?", "frontmatter": { - "title": "简述 MySQL 常见索引类型,介绍一下覆盖索引", - "date": "2022-05-21T17:08:24.000Z", - "permalink": "/pages/17b014/", + "title": "简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?", + "date": "2022-05-21T17:05:55.000Z", + "permalink": "/pages/36672c/", "categories": [ "数据库", "MYSQL相关" @@ -6093,10 +7041,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/007.%E7%AE%80%E8%BF%B0%20MySQL%20%E5%B8%B8%E8%A7%81%E7%B4%A2%E5%BC%95%E7%B1%BB%E5%9E%8B%EF%BC%8C%E4%BB%8B%E7%BB%8D%E4%B8%80%E4%B8%8B%E8%A6%86%E7%9B%96%E7%B4%A2%E5%BC%95.html", - "relativePath": "03.数据库/01.MYSQL相关/007.简述 MySQL 常见索引类型,介绍一下覆盖索引.md", - "key": "v-824b776c", - "path": "/pages/17b014/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/040.%E7%AE%80%E8%BF%B0%E8%84%8F%E8%AF%BB%E5%92%8C%E5%B9%BB%E8%AF%BB%E7%9A%84%E5%8F%91%E7%94%9F%E5%9C%BA%E6%99%AF%EF%BC%8CInnoDB%20%E6%98%AF%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E5%B9%BB%E8%AF%BB%E7%9A%84%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/040.简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?.md", + "key": "v-689e142a", + "path": "/pages/36672c/", "headersStr": null, "content": "", "normalizedContent": "", @@ -6104,49 +7052,6 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, - { - "title": "MySQL 为什么使用 B+ 树来作索引,对比 B 树它的优点和缺点是什么", - "frontmatter": { - "title": "MySQL 为什么使用 B+ 树来作索引,对比 B 树它的优点和缺点是什么", - "date": "2022-05-21T17:05:11.000Z", - "permalink": "/pages/bc5538/", - "categories": [ - "数据库", - "MYSQL相关" - ], - "tags": [ - null - ] - }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/020.MySQL%20%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BD%BF%E7%94%A8%20B+%20%E6%A0%91%E6%9D%A5%E4%BD%9C%E7%B4%A2%E5%BC%95%EF%BC%8C%E5%AF%B9%E6%AF%94%20B%20%E6%A0%91%E5%AE%83%E7%9A%84%E4%BC%98%E7%82%B9%E5%92%8C%E7%BC%BA%E7%82%B9%E6%98%AF%E4%BB%80%E4%B9%88.html", - "relativePath": "03.数据库/01.MYSQL相关/020.MySQL 为什么使用 B+ 树来作索引,对比 B 树它的优点和缺点是什么.md", - "key": "v-6c681639", - "path": "/pages/bc5538/", - "headers": [ - { - "level": 2, - "title": "磁盘存取原理", - "slug": "磁盘存取原理", - "normalizedTitle": "磁盘存取原理", - "charIndex": 59 - }, - { - "level": 2, - "title": "B-/+Tree索引的性能分析", - "slug": "b-tree索引的性能分析", - "normalizedTitle": "b-/+tree索引的性能分析", - "charIndex": 246 - } - ], - "headersStr": "磁盘存取原理 B-/+Tree索引的性能分析", - "content": "> 索引本身也是以文件的形式存储在磁盘上,索引查找过程中存在I/O消耗,采用B tree结构可以减少I/O。\n\n\n# 磁盘存取原理\n\n> 局部性原理与磁盘预读:当一个数据被用到时,其附近的数据也很快会被用到。 预读的长度一般为页的整数倍,主存和磁盘以页为单位交换数据。\n\n磁盘和磁头构成了存取物理结构。同心圆环是磁道,磁道被划分成一个个段,叫扇区,扇区是最小存储单元。 i/o读取数据的过程:磁盘旋转到指定扇区,磁头移动到指定磁道。 所以减少磁盘旋转或者减少磁头移动就能减少i/o。\n\n\n# B-/+Tree索引的性能分析\n\nB Tree利用局部性原理可减少i/o次数。 根据B Tree定义可知,检索一次最多需要访问h个节点,h-1次i/o,h是树高,mysql设计者巧妙的将一个节点大小设置为等于一个页大小,每次新建节点都申请一个页的空间,这样就保证一个节点物理上也存储在一个页,这样每个节点只需要一次i/o就可以完全载入。", - "normalizedContent": "> 索引本身也是以文件的形式存储在磁盘上,索引查找过程中存在i/o消耗,采用b tree结构可以减少i/o。\n\n\n# 磁盘存取原理\n\n> 局部性原理与磁盘预读:当一个数据被用到时,其附近的数据也很快会被用到。 预读的长度一般为页的整数倍,主存和磁盘以页为单位交换数据。\n\n磁盘和磁头构成了存取物理结构。同心圆环是磁道,磁道被划分成一个个段,叫扇区,扇区是最小存储单元。 i/o读取数据的过程:磁盘旋转到指定扇区,磁头移动到指定磁道。 所以减少磁盘旋转或者减少磁头移动就能减少i/o。\n\n\n# b-/+tree索引的性能分析\n\nb tree利用局部性原理可减少i/o次数。 根据b tree定义可知,检索一次最多需要访问h个节点,h-1次i/o,h是树高,mysql设计者巧妙的将一个节点大小设置为等于一个页大小,每次新建节点都申请一个页的空间,这样就保证一个节点物理上也存储在一个页,这样每个节点只需要一次i/o就可以完全载入。", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, { "title": "简述乐观锁以及悲观锁的区别以及使用场景", "frontmatter": { @@ -6173,11 +7078,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?", + "title": "简述什么是最左匹配原则", "frontmatter": { - "title": "简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?", - "date": "2022-05-21T17:05:55.000Z", - "permalink": "/pages/36672c/", + "title": "简述什么是最左匹配原则", + "date": "2022-05-21T17:06:10.000Z", + "permalink": "/pages/7a1f1e/", "categories": [ "数据库", "MYSQL相关" @@ -6186,10 +7091,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/040.%E7%AE%80%E8%BF%B0%E8%84%8F%E8%AF%BB%E5%92%8C%E5%B9%BB%E8%AF%BB%E7%9A%84%E5%8F%91%E7%94%9F%E5%9C%BA%E6%99%AF%EF%BC%8CInnoDB%20%E6%98%AF%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E5%B9%BB%E8%AF%BB%E7%9A%84%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/040.简述脏读和幻读的发生场景,InnoDB 是如何解决幻读的?.md", - "key": "v-689e142a", - "path": "/pages/36672c/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/050.%E7%AE%80%E8%BF%B0%E4%BB%80%E4%B9%88%E6%98%AF%E6%9C%80%E5%B7%A6%E5%8C%B9%E9%85%8D%E5%8E%9F%E5%88%99.html", + "relativePath": "03.数据库/01.MYSQL相关/050.简述什么是最左匹配原则.md", + "key": "v-4ecddbec", + "path": "/pages/7a1f1e/", "headersStr": null, "content": "", "normalizedContent": "", @@ -6198,11 +7103,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述什么是最左匹配原则", + "title": "MySQL 联合索引底层原理是什么?", "frontmatter": { - "title": "简述什么是最左匹配原则", - "date": "2022-05-21T17:06:10.000Z", - "permalink": "/pages/7a1f1e/", + "title": "MySQL 联合索引底层原理是什么?", + "date": "2022-05-21T17:06:26.000Z", + "permalink": "/pages/52773a/", "categories": [ "数据库", "MYSQL相关" @@ -6211,10 +7116,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/050.%E7%AE%80%E8%BF%B0%E4%BB%80%E4%B9%88%E6%98%AF%E6%9C%80%E5%B7%A6%E5%8C%B9%E9%85%8D%E5%8E%9F%E5%88%99.html", - "relativePath": "03.数据库/01.MYSQL相关/050.简述什么是最左匹配原则.md", - "key": "v-4ecddbec", - "path": "/pages/7a1f1e/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/060.MySQL%20%E8%81%94%E5%90%88%E7%B4%A2%E5%BC%95%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/060.MySQL 联合索引底层原理是什么?.md", + "key": "v-505fbe2e", + "path": "/pages/52773a/", "headersStr": null, "content": "", "normalizedContent": "", @@ -6223,11 +7128,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "MySQL 联合索引底层原理是什么?", + "title": "产生死锁的必要条件有哪些?如何解决死锁?", "frontmatter": { - "title": "MySQL 联合索引底层原理是什么?", - "date": "2022-05-21T17:06:26.000Z", - "permalink": "/pages/52773a/", + "title": "产生死锁的必要条件有哪些?如何解决死锁?", + "date": "2022-05-21T17:06:48.000Z", + "permalink": "/pages/f4cf50/", "categories": [ "数据库", "MYSQL相关" @@ -6236,10 +7141,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/060.MySQL%20%E8%81%94%E5%90%88%E7%B4%A2%E5%BC%95%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/060.MySQL 联合索引底层原理是什么?.md", - "key": "v-505fbe2e", - "path": "/pages/52773a/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/070.%E4%BA%A7%E7%94%9F%E6%AD%BB%E9%94%81%E7%9A%84%E5%BF%85%E8%A6%81%E6%9D%A1%E4%BB%B6%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E6%AD%BB%E9%94%81%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/070.产生死锁的必要条件有哪些?如何解决死锁?.md", + "key": "v-0dd47f09", + "path": "/pages/f4cf50/", "headersStr": null, "content": "", "normalizedContent": "", @@ -6273,11 +7178,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "产生死锁的必要条件有哪些?如何解决死锁?", + "title": "如何设计数据库压测方案?", "frontmatter": { - "title": "产生死锁的必要条件有哪些?如何解决死锁?", - "date": "2022-05-21T17:06:48.000Z", - "permalink": "/pages/f4cf50/", + "title": "如何设计数据库压测方案?", + "date": "2022-05-21T17:08:40.000Z", + "permalink": "/pages/a8eb47/", "categories": [ "数据库", "MYSQL相关" @@ -6286,10 +7191,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/070.%E4%BA%A7%E7%94%9F%E6%AD%BB%E9%94%81%E7%9A%84%E5%BF%85%E8%A6%81%E6%9D%A1%E4%BB%B6%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E6%AD%BB%E9%94%81%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/070.产生死锁的必要条件有哪些?如何解决死锁?.md", - "key": "v-0dd47f09", - "path": "/pages/f4cf50/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/100.%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%8B%E6%B5%8B%E6%96%B9%E6%A1%88%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/100.如何设计数据库压测方案?.md", + "key": "v-4cef944a", + "path": "/pages/a8eb47/", "headersStr": null, "content": "", "normalizedContent": "", @@ -6298,11 +7203,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "如何设计数据库压测方案?", + "title": "简述数据库中什么情况下进行分库,什么情况下进行分表?", "frontmatter": { - "title": "如何设计数据库压测方案?", - "date": "2022-05-21T17:08:40.000Z", - "permalink": "/pages/a8eb47/", + "title": "简述数据库中什么情况下进行分库,什么情况下进行分表?", + "date": "2022-05-21T17:08:52.000Z", + "permalink": "/pages/ac0bb8/", "categories": [ "数据库", "MYSQL相关" @@ -6311,10 +7216,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/100.%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%8B%E6%B5%8B%E6%96%B9%E6%A1%88%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/100.如何设计数据库压测方案?.md", - "key": "v-4cef944a", - "path": "/pages/a8eb47/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/110.%E7%AE%80%E8%BF%B0%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B8%AD%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E8%BF%9B%E8%A1%8C%E5%88%86%E5%BA%93%EF%BC%8C%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E8%BF%9B%E8%A1%8C%E5%88%86%E8%A1%A8%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/110.简述数据库中什么情况下进行分库,什么情况下进行分表?.md", + "key": "v-ed0c93b8", + "path": "/pages/ac0bb8/", "headersStr": null, "content": "", "normalizedContent": "", @@ -6399,11 +7304,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述数据库中什么情况下进行分库,什么情况下进行分表?", + "title": "数据库的读写分离的作用是什么?如何实现?", "frontmatter": { - "title": "简述数据库中什么情况下进行分库,什么情况下进行分表?", - "date": "2022-05-21T17:08:52.000Z", - "permalink": "/pages/ac0bb8/", + "title": "数据库的读写分离的作用是什么?如何实现?", + "date": "2022-05-21T17:09:05.000Z", + "permalink": "/pages/f62359/", "categories": [ "数据库", "MYSQL相关" @@ -6412,10 +7317,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/110.%E7%AE%80%E8%BF%B0%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B8%AD%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E8%BF%9B%E8%A1%8C%E5%88%86%E5%BA%93%EF%BC%8C%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E8%BF%9B%E8%A1%8C%E5%88%86%E8%A1%A8%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/110.简述数据库中什么情况下进行分库,什么情况下进行分表?.md", - "key": "v-ed0c93b8", - "path": "/pages/ac0bb8/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/120.%E6%95%B0%E6%8D%AE%E5%BA%93%E7%9A%84%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB%E7%9A%84%E4%BD%9C%E7%94%A8%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/120.数据库的读写分离的作用是什么?如何实现?.md", + "key": "v-d6bc240a", + "path": "/pages/f62359/", "headersStr": null, "content": "", "normalizedContent": "", @@ -6424,11 +7329,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "数据库的读写分离的作用是什么?如何实现?", + "title": "MySQL 中 join 与 left join 的区别是什么?", "frontmatter": { - "title": "数据库的读写分离的作用是什么?如何实现?", - "date": "2022-05-21T17:09:05.000Z", - "permalink": "/pages/f62359/", + "title": "MySQL 中 join 与 left join 的区别是什么?", + "date": "2022-05-21T17:09:52.000Z", + "permalink": "/pages/429100/", "categories": [ "数据库", "MYSQL相关" @@ -6437,10 +7342,35 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/120.%E6%95%B0%E6%8D%AE%E5%BA%93%E7%9A%84%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB%E7%9A%84%E4%BD%9C%E7%94%A8%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/120.数据库的读写分离的作用是什么?如何实现?.md", - "key": "v-d6bc240a", - "path": "/pages/f62359/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/150.MySQL%20%E4%B8%AD%20join%20%E4%B8%8E%20left%20join%20%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/150.MySQL 中 join 与 left join 的区别是什么?.md", + "key": "v-77790024", + "path": "/pages/429100/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "什么是 SQL 注入攻击?如何防止这类攻击?", + "frontmatter": { + "title": "什么是 SQL 注入攻击?如何防止这类攻击?", + "date": "2022-05-21T17:09:41.000Z", + "permalink": "/pages/b4338e/", + "categories": [ + "数据库", + "MYSQL相关" + ], + "tags": [ + null + ] + }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/140.%E4%BB%80%E4%B9%88%E6%98%AF%20SQL%20%E6%B3%A8%E5%85%A5%E6%94%BB%E5%87%BB%EF%BC%9F%E5%A6%82%E4%BD%95%E9%98%B2%E6%AD%A2%E8%BF%99%E7%B1%BB%E6%94%BB%E5%87%BB%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/140.什么是 SQL 注入攻击?如何防止这类攻击?.md", + "key": "v-ceed6ba6", + "path": "/pages/b4338e/", "headersStr": null, "content": "", "normalizedContent": "", @@ -6562,36 +7492,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "MySQL 中 join 与 left join 的区别是什么?", - "frontmatter": { - "title": "MySQL 中 join 与 left join 的区别是什么?", - "date": "2022-05-21T17:09:52.000Z", - "permalink": "/pages/429100/", - "categories": [ - "数据库", - "MYSQL相关" - ], - "tags": [ - null - ] - }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/150.MySQL%20%E4%B8%AD%20join%20%E4%B8%8E%20left%20join%20%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/150.MySQL 中 join 与 left join 的区别是什么?.md", - "key": "v-77790024", - "path": "/pages/429100/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "什么是 SQL 注入攻击?如何防止这类攻击?", + "title": "MySQL 中 varchar 和 char 的区别是什么?", "frontmatter": { - "title": "什么是 SQL 注入攻击?如何防止这类攻击?", - "date": "2022-05-21T17:09:41.000Z", - "permalink": "/pages/b4338e/", + "title": "MySQL 中 varchar 和 char 的区别是什么?", + "date": "2022-05-21T17:10:21.000Z", + "permalink": "/pages/d97948/", "categories": [ "数据库", "MYSQL相关" @@ -6600,10 +7505,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/140.%E4%BB%80%E4%B9%88%E6%98%AF%20SQL%20%E6%B3%A8%E5%85%A5%E6%94%BB%E5%87%BB%EF%BC%9F%E5%A6%82%E4%BD%95%E9%98%B2%E6%AD%A2%E8%BF%99%E7%B1%BB%E6%94%BB%E5%87%BB%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/140.什么是 SQL 注入攻击?如何防止这类攻击?.md", - "key": "v-ceed6ba6", - "path": "/pages/b4338e/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/170.MySQL%20%E4%B8%AD%20varchar%20%E5%92%8C%20char%20%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", + "relativePath": "03.数据库/01.MYSQL相关/170.MySQL 中 varchar 和 char 的区别是什么?.md", + "key": "v-0a7e6416", + "path": "/pages/d97948/", "headersStr": null, "content": "", "normalizedContent": "", @@ -6637,23 +7542,23 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "MySQL 中 varchar 和 char 的区别是什么?", + "title": "为什么 Redis 在单线程下能如此快?", "frontmatter": { - "title": "MySQL 中 varchar 和 char 的区别是什么?", - "date": "2022-05-21T17:10:21.000Z", - "permalink": "/pages/d97948/", + "title": "为什么 Redis 在单线程下能如此快?", + "date": "2022-05-21T17:20:12.000Z", + "permalink": "/pages/d66211/", "categories": [ "数据库", - "MYSQL相关" + "REDIS相关" ], "tags": [ null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/01.MYSQL%E7%9B%B8%E5%85%B3/170.MySQL%20%E4%B8%AD%20varchar%20%E5%92%8C%20char%20%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", - "relativePath": "03.数据库/01.MYSQL相关/170.MySQL 中 varchar 和 char 的区别是什么?.md", - "key": "v-0a7e6416", - "path": "/pages/d97948/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/002.%E4%B8%BA%E4%BB%80%E4%B9%88%20Redis%20%E5%9C%A8%E5%8D%95%E7%BA%BF%E7%A8%8B%E4%B8%8B%E8%83%BD%E5%A6%82%E6%AD%A4%E5%BF%AB%EF%BC%9F.html", + "relativePath": "03.数据库/02.REDIS相关/002.为什么 Redis 在单线程下能如此快?.md", + "key": "v-cb742458", + "path": "/pages/d66211/", "headersStr": null, "content": "", "normalizedContent": "", @@ -6739,31 +7644,6 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, - { - "title": "为什么 Redis 在单线程下能如此快?", - "frontmatter": { - "title": "为什么 Redis 在单线程下能如此快?", - "date": "2022-05-21T17:20:12.000Z", - "permalink": "/pages/d66211/", - "categories": [ - "数据库", - "REDIS相关" - ], - "tags": [ - null - ] - }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/002.%E4%B8%BA%E4%BB%80%E4%B9%88%20Redis%20%E5%9C%A8%E5%8D%95%E7%BA%BF%E7%A8%8B%E4%B8%8B%E8%83%BD%E5%A6%82%E6%AD%A4%E5%BF%AB%EF%BC%9F.html", - "relativePath": "03.数据库/02.REDIS相关/002.为什么 Redis 在单线程下能如此快?.md", - "key": "v-cb742458", - "path": "/pages/d66211/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, { "title": "简述 Redis 字符串的底层结构", "frontmatter": { @@ -6790,11 +7670,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "Redis的缓存淘汰策略有哪些?", + "title": "简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点", "frontmatter": { - "title": "Redis的缓存淘汰策略有哪些?", - "date": "2022-05-21T17:20:33.000Z", - "permalink": "/pages/9326cc/", + "title": "简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点", + "date": "2022-05-21T17:21:43.000Z", + "permalink": "/pages/7e708d/", "categories": [ "数据库", "REDIS相关" @@ -6803,29 +7683,29 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/010.Redis%E7%9A%84%E7%BC%93%E5%AD%98%E6%B7%98%E6%B1%B0%E7%AD%96%E7%95%A5%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F.html", - "relativePath": "03.数据库/02.REDIS相关/010.Redis的缓存淘汰策略有哪些?.md", - "key": "v-c0bc7b7a", - "path": "/pages/9326cc/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/020.%E7%AE%80%E8%BF%B0%20Redis%20%E6%8C%81%E4%B9%85%E5%8C%96%E4%B8%AD%20RDB%20%E4%BB%A5%E5%8F%8A%20AOF%20%E6%96%B9%E6%A1%88%E7%9A%84%E4%BC%98%E7%BC%BA%E7%82%B9.html", + "relativePath": "03.数据库/02.REDIS相关/020.简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点.md", + "key": "v-7c214d00", + "path": "/pages/7e708d/", "headers": [ { "level": 2, - "title": "过期键", - "slug": "过期键", - "normalizedTitle": "过期键", + "title": "RDB持久化", + "slug": "rdb持久化", + "normalizedTitle": "rdb持久化", "charIndex": 2 }, { "level": 2, - "title": "内存淘汰策略(内存不足时)", - "slug": "内存淘汰策略-内存不足时", - "normalizedTitle": "内存淘汰策略(内存不足时)", - "charIndex": 235 + "title": "AOF持久化", + "slug": "aof持久化", + "normalizedTitle": "aof持久化", + "charIndex": 1345 } ], - "headersStr": "过期键 内存淘汰策略(内存不足时)", - "content": "# 过期键\n\n * 删除过期键的策略\n \n * 定时:对内存友好,对cpu不友好\n * 惰性:对内存不友好,对cpu友好\n * 定期:是定时和惰性的折中方案。执行时长和频率是衡量一个定期策略好坏的标准。\n\n * rdb再说生成和加载过程中过滤掉过期的键\n\n * aof模式下,过期的键只有真正del的时候才会记录在aof文件。重写aof文件不会包含过期的键。\n\n * 主从复制:从服务器遇到过期键不会del,接收到主服务器的del命令才会删除。\n\n\n# 内存淘汰策略(内存不足时)\n\n * noeviction:抛异常\n * allkeys-lru:在所有的key中按lru淘汰\n * allkeys-random:在所有的key中随机淘汰\n * volatile-lru:在设置了过期时间的key中lru淘汰\n * volatile-random:在设置了过期时间的key中随机淘汰\n * volatile-ttl:删除快过期的key", - "normalizedContent": "# 过期键\n\n * 删除过期键的策略\n \n * 定时:对内存友好,对cpu不友好\n * 惰性:对内存不友好,对cpu友好\n * 定期:是定时和惰性的折中方案。执行时长和频率是衡量一个定期策略好坏的标准。\n\n * rdb再说生成和加载过程中过滤掉过期的键\n\n * aof模式下,过期的键只有真正del的时候才会记录在aof文件。重写aof文件不会包含过期的键。\n\n * 主从复制:从服务器遇到过期键不会del,接收到主服务器的del命令才会删除。\n\n\n# 内存淘汰策略(内存不足时)\n\n * noeviction:抛异常\n * allkeys-lru:在所有的key中按lru淘汰\n * allkeys-random:在所有的key中随机淘汰\n * volatile-lru:在设置了过期时间的key中lru淘汰\n * volatile-random:在设置了过期时间的key中随机淘汰\n * volatile-ttl:删除快过期的key", + "headersStr": "RDB持久化 AOF持久化", + "content": "# RDB持久化\n\n 1. save和bgsave源码\n\nvoid saveCommand(redisClient *c) {\n\n // BGSAVE 已经在执行中,不能再执行 SAVE\n // 否则将产生竞争条件\n if (server.rdb_child_pid != -1) {\n addReplyError(c,\"Background save already in progress\");\n return;\n }\n\n // 执行 \n if (rdbSave(server.rdb_filename) == REDIS_OK) {\n addReply(c,shared.ok);\n } else {\n addReply(c,shared.err);\n }\n}\n\nvoid bgsaveCommand(redisClient *c) {\n\n // 不能重复执行 BGSAVE\n if (server.rdb_child_pid != -1) {\n addReplyError(c,\"Background save already in progress\");\n\n // 不能在 BGREWRITEAOF 正在运行时执行\n } else if (server.aof_child_pid != -1) {\n addReplyError(c,\"Can't BGSAVE while AOF log rewriting is in progress\");\n\n // 执行 BGSAVE\n } else if (rdbSaveBackground(server.rdb_filename) == REDIS_OK) {\n addReplyStatus(c,\"Background saving started\");\n\n } else {\n addReply(c,shared.err);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n\n 2. 自动间隔性保存\n\n> 服务器间隔性检车saveparams中的任意一个条件是否得到满足,如果得到满足,执行bgsave.saveparams[0]表示距离上次bgsave900s内,至少有一次数据库变化。\n\n[外链图片转存中...(img-SwAqAX8z-1653145947293)]\n\n 3. rdb文件结构 [外链图片转存中...(img-Prs54pGE-1653145947294)] 其中database结构如下: [外链图片转存中...(img-rn1gUv6w-1653145947294)] 其中ksy_value_pairs结构如下 [外链图片转存中...(img-B62S2gV5-1653145947295)]\n\n> type指定了value的类型。value的编码方式不同期存储结构也不同。\n\n\n# AOF持久化\n\n 1. aof的实现 命令追加:所有客户端命令都会被存到redis_server的aof_buf缓冲区。 文件写入:操作系统在写入文件的时候,先写入缓存,缓存满了才写入文件。 文件同步:flushAppendOnlyFile()负责将aof_buf写入文件aof。\n\n> flushAppendOnlyFile中配置的appendfsync决定同步策略。appendfsync有三个取值:always(每次写入都同步)、everysec(每次事件都写入但不同步,每秒同步)、 no(每次事件都写入但不同步,操作系统决定同步);\n\n 2. aof的载入与还原 [外链图片转存中...(img-pNMyXRLJ-1653145947295)]\n 3. AOF重写 [外链图片转存中...(img-i1nhwgNs-1653145947295)]", + "normalizedContent": "# rdb持久化\n\n 1. save和bgsave源码\n\nvoid savecommand(redisclient *c) {\n\n // bgsave 已经在执行中,不能再执行 save\n // 否则将产生竞争条件\n if (server.rdb_child_pid != -1) {\n addreplyerror(c,\"background save already in progress\");\n return;\n }\n\n // 执行 \n if (rdbsave(server.rdb_filename) == redis_ok) {\n addreply(c,shared.ok);\n } else {\n addreply(c,shared.err);\n }\n}\n\nvoid bgsavecommand(redisclient *c) {\n\n // 不能重复执行 bgsave\n if (server.rdb_child_pid != -1) {\n addreplyerror(c,\"background save already in progress\");\n\n // 不能在 bgrewriteaof 正在运行时执行\n } else if (server.aof_child_pid != -1) {\n addreplyerror(c,\"can't bgsave while aof log rewriting is in progress\");\n\n // 执行 bgsave\n } else if (rdbsavebackground(server.rdb_filename) == redis_ok) {\n addreplystatus(c,\"background saving started\");\n\n } else {\n addreply(c,shared.err);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n\n 2. 自动间隔性保存\n\n> 服务器间隔性检车saveparams中的任意一个条件是否得到满足,如果得到满足,执行bgsave.saveparams[0]表示距离上次bgsave900s内,至少有一次数据库变化。\n\n[外链图片转存中...(img-swaqax8z-1653145947293)]\n\n 3. rdb文件结构 [外链图片转存中...(img-prs54pge-1653145947294)] 其中database结构如下: [外链图片转存中...(img-rn1guv6w-1653145947294)] 其中ksy_value_pairs结构如下 [外链图片转存中...(img-b62s2gv5-1653145947295)]\n\n> type指定了value的类型。value的编码方式不同期存储结构也不同。\n\n\n# aof持久化\n\n 1. aof的实现 命令追加:所有客户端命令都会被存到redis_server的aof_buf缓冲区。 文件写入:操作系统在写入文件的时候,先写入缓存,缓存满了才写入文件。 文件同步:flushappendonlyfile()负责将aof_buf写入文件aof。\n\n> flushappendonlyfile中配置的appendfsync决定同步策略。appendfsync有三个取值:always(每次写入都同步)、everysec(每次事件都写入但不同步,每秒同步)、 no(每次事件都写入但不同步,操作系统决定同步);\n\n 2. aof的载入与还原 [外链图片转存中...(img-pnmyxrlj-1653145947295)]\n 3. aof重写 [外链图片转存中...(img-i1nhwgns-1653145947295)]", "charsets": { "cjk": true }, @@ -6833,11 +7713,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点", + "title": "Redis的缓存淘汰策略有哪些?", "frontmatter": { - "title": "简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点", - "date": "2022-05-21T17:21:43.000Z", - "permalink": "/pages/7e708d/", + "title": "Redis的缓存淘汰策略有哪些?", + "date": "2022-05-21T17:20:33.000Z", + "permalink": "/pages/9326cc/", "categories": [ "数据库", "REDIS相关" @@ -6846,29 +7726,29 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/020.%E7%AE%80%E8%BF%B0%20Redis%20%E6%8C%81%E4%B9%85%E5%8C%96%E4%B8%AD%20RDB%20%E4%BB%A5%E5%8F%8A%20AOF%20%E6%96%B9%E6%A1%88%E7%9A%84%E4%BC%98%E7%BC%BA%E7%82%B9.html", - "relativePath": "03.数据库/02.REDIS相关/020.简述 Redis 持久化中 RDB 以及 AOF 方案的优缺点.md", - "key": "v-7c214d00", - "path": "/pages/7e708d/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/010.Redis%E7%9A%84%E7%BC%93%E5%AD%98%E6%B7%98%E6%B1%B0%E7%AD%96%E7%95%A5%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F.html", + "relativePath": "03.数据库/02.REDIS相关/010.Redis的缓存淘汰策略有哪些?.md", + "key": "v-c0bc7b7a", + "path": "/pages/9326cc/", "headers": [ { "level": 2, - "title": "RDB持久化", - "slug": "rdb持久化", - "normalizedTitle": "rdb持久化", + "title": "过期键", + "slug": "过期键", + "normalizedTitle": "过期键", "charIndex": 2 }, { "level": 2, - "title": "AOF持久化", - "slug": "aof持久化", - "normalizedTitle": "aof持久化", - "charIndex": 1345 + "title": "内存淘汰策略(内存不足时)", + "slug": "内存淘汰策略-内存不足时", + "normalizedTitle": "内存淘汰策略(内存不足时)", + "charIndex": 235 } ], - "headersStr": "RDB持久化 AOF持久化", - "content": "# RDB持久化\n\n 1. save和bgsave源码\n\nvoid saveCommand(redisClient *c) {\n\n // BGSAVE 已经在执行中,不能再执行 SAVE\n // 否则将产生竞争条件\n if (server.rdb_child_pid != -1) {\n addReplyError(c,\"Background save already in progress\");\n return;\n }\n\n // 执行 \n if (rdbSave(server.rdb_filename) == REDIS_OK) {\n addReply(c,shared.ok);\n } else {\n addReply(c,shared.err);\n }\n}\n\nvoid bgsaveCommand(redisClient *c) {\n\n // 不能重复执行 BGSAVE\n if (server.rdb_child_pid != -1) {\n addReplyError(c,\"Background save already in progress\");\n\n // 不能在 BGREWRITEAOF 正在运行时执行\n } else if (server.aof_child_pid != -1) {\n addReplyError(c,\"Can't BGSAVE while AOF log rewriting is in progress\");\n\n // 执行 BGSAVE\n } else if (rdbSaveBackground(server.rdb_filename) == REDIS_OK) {\n addReplyStatus(c,\"Background saving started\");\n\n } else {\n addReply(c,shared.err);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n\n 2. 自动间隔性保存\n\n> 服务器间隔性检车saveparams中的任意一个条件是否得到满足,如果得到满足,执行bgsave.saveparams[0]表示距离上次bgsave900s内,至少有一次数据库变化。\n\n[外链图片转存中...(img-SwAqAX8z-1653145947293)]\n\n 3. rdb文件结构 [外链图片转存中...(img-Prs54pGE-1653145947294)] 其中database结构如下: [外链图片转存中...(img-rn1gUv6w-1653145947294)] 其中ksy_value_pairs结构如下 [外链图片转存中...(img-B62S2gV5-1653145947295)]\n\n> type指定了value的类型。value的编码方式不同期存储结构也不同。\n\n\n# AOF持久化\n\n 1. aof的实现 命令追加:所有客户端命令都会被存到redis_server的aof_buf缓冲区。 文件写入:操作系统在写入文件的时候,先写入缓存,缓存满了才写入文件。 文件同步:flushAppendOnlyFile()负责将aof_buf写入文件aof。\n\n> flushAppendOnlyFile中配置的appendfsync决定同步策略。appendfsync有三个取值:always(每次写入都同步)、everysec(每次事件都写入但不同步,每秒同步)、 no(每次事件都写入但不同步,操作系统决定同步);\n\n 2. aof的载入与还原 [外链图片转存中...(img-pNMyXRLJ-1653145947295)]\n 3. AOF重写 [外链图片转存中...(img-i1nhwgNs-1653145947295)]", - "normalizedContent": "# rdb持久化\n\n 1. save和bgsave源码\n\nvoid savecommand(redisclient *c) {\n\n // bgsave 已经在执行中,不能再执行 save\n // 否则将产生竞争条件\n if (server.rdb_child_pid != -1) {\n addreplyerror(c,\"background save already in progress\");\n return;\n }\n\n // 执行 \n if (rdbsave(server.rdb_filename) == redis_ok) {\n addreply(c,shared.ok);\n } else {\n addreply(c,shared.err);\n }\n}\n\nvoid bgsavecommand(redisclient *c) {\n\n // 不能重复执行 bgsave\n if (server.rdb_child_pid != -1) {\n addreplyerror(c,\"background save already in progress\");\n\n // 不能在 bgrewriteaof 正在运行时执行\n } else if (server.aof_child_pid != -1) {\n addreplyerror(c,\"can't bgsave while aof log rewriting is in progress\");\n\n // 执行 bgsave\n } else if (rdbsavebackground(server.rdb_filename) == redis_ok) {\n addreplystatus(c,\"background saving started\");\n\n } else {\n addreply(c,shared.err);\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n\n 2. 自动间隔性保存\n\n> 服务器间隔性检车saveparams中的任意一个条件是否得到满足,如果得到满足,执行bgsave.saveparams[0]表示距离上次bgsave900s内,至少有一次数据库变化。\n\n[外链图片转存中...(img-swaqax8z-1653145947293)]\n\n 3. rdb文件结构 [外链图片转存中...(img-prs54pge-1653145947294)] 其中database结构如下: [外链图片转存中...(img-rn1guv6w-1653145947294)] 其中ksy_value_pairs结构如下 [外链图片转存中...(img-b62s2gv5-1653145947295)]\n\n> type指定了value的类型。value的编码方式不同期存储结构也不同。\n\n\n# aof持久化\n\n 1. aof的实现 命令追加:所有客户端命令都会被存到redis_server的aof_buf缓冲区。 文件写入:操作系统在写入文件的时候,先写入缓存,缓存满了才写入文件。 文件同步:flushappendonlyfile()负责将aof_buf写入文件aof。\n\n> flushappendonlyfile中配置的appendfsync决定同步策略。appendfsync有三个取值:always(每次写入都同步)、everysec(每次事件都写入但不同步,每秒同步)、 no(每次事件都写入但不同步,操作系统决定同步);\n\n 2. aof的载入与还原 [外链图片转存中...(img-pnmyxrlj-1653145947295)]\n 3. aof重写 [外链图片转存中...(img-i1nhwgns-1653145947295)]", + "headersStr": "过期键 内存淘汰策略(内存不足时)", + "content": "# 过期键\n\n * 删除过期键的策略\n \n * 定时:对内存友好,对cpu不友好\n * 惰性:对内存不友好,对cpu友好\n * 定期:是定时和惰性的折中方案。执行时长和频率是衡量一个定期策略好坏的标准。\n\n * rdb再说生成和加载过程中过滤掉过期的键\n\n * aof模式下,过期的键只有真正del的时候才会记录在aof文件。重写aof文件不会包含过期的键。\n\n * 主从复制:从服务器遇到过期键不会del,接收到主服务器的del命令才会删除。\n\n\n# 内存淘汰策略(内存不足时)\n\n * noeviction:抛异常\n * allkeys-lru:在所有的key中按lru淘汰\n * allkeys-random:在所有的key中随机淘汰\n * volatile-lru:在设置了过期时间的key中lru淘汰\n * volatile-random:在设置了过期时间的key中随机淘汰\n * volatile-ttl:删除快过期的key", + "normalizedContent": "# 过期键\n\n * 删除过期键的策略\n \n * 定时:对内存友好,对cpu不友好\n * 惰性:对内存不友好,对cpu友好\n * 定期:是定时和惰性的折中方案。执行时长和频率是衡量一个定期策略好坏的标准。\n\n * rdb再说生成和加载过程中过滤掉过期的键\n\n * aof模式下,过期的键只有真正del的时候才会记录在aof文件。重写aof文件不会包含过期的键。\n\n * 主从复制:从服务器遇到过期键不会del,接收到主服务器的del命令才会删除。\n\n\n# 内存淘汰策略(内存不足时)\n\n * noeviction:抛异常\n * allkeys-lru:在所有的key中按lru淘汰\n * allkeys-random:在所有的key中随机淘汰\n * volatile-lru:在设置了过期时间的key中lru淘汰\n * volatile-random:在设置了过期时间的key中随机淘汰\n * volatile-ttl:删除快过期的key", "charsets": { "cjk": true }, @@ -6936,31 +7816,6 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, - { - "title": "Redis 中,sentinel 和 cluster 的区别和适用场景是什么?", - "frontmatter": { - "title": "Redis 中,sentinel 和 cluster 的区别和适用场景是什么?", - "date": "2022-05-21T17:22:27.000Z", - "permalink": "/pages/e2e910/", - "categories": [ - "数据库", - "REDIS相关" - ], - "tags": [ - null - ] - }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/060.Redis%20%E4%B8%AD%EF%BC%8Csentinel%20%E5%92%8C%20cluster%20%E7%9A%84%E5%8C%BA%E5%88%AB%E5%92%8C%E9%80%82%E7%94%A8%E5%9C%BA%E6%99%AF%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", - "relativePath": "03.数据库/02.REDIS相关/060.Redis 中,sentinel 和 cluster 的区别和适用场景是什么?.md", - "key": "v-6a45f68d", - "path": "/pages/e2e910/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, { "title": "简述 Redis 中跳表的应用以及优缺点", "frontmatter": { @@ -7012,11 +7867,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "简述 Redis 的线程模型以及底层架构设计", + "title": "Redis 中,sentinel 和 cluster 的区别和适用场景是什么?", "frontmatter": { - "title": "简述 Redis 的线程模型以及底层架构设计", - "date": "2022-05-21T17:23:08.000Z", - "permalink": "/pages/9c28c9/", + "title": "Redis 中,sentinel 和 cluster 的区别和适用场景是什么?", + "date": "2022-05-21T17:22:27.000Z", + "permalink": "/pages/e2e910/", "categories": [ "数据库", "REDIS相关" @@ -7025,10 +7880,10 @@ export const siteData = { null ] }, - "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/080.%E7%AE%80%E8%BF%B0%20Redis%20%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B%E4%BB%A5%E5%8F%8A%E5%BA%95%E5%B1%82%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1.html", - "relativePath": "03.数据库/02.REDIS相关/080.简述 Redis 的线程模型以及底层架构设计.md", - "key": "v-35718f4e", - "path": "/pages/9c28c9/", + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/060.Redis%20%E4%B8%AD%EF%BC%8Csentinel%20%E5%92%8C%20cluster%20%E7%9A%84%E5%8C%BA%E5%88%AB%E5%92%8C%E9%80%82%E7%94%A8%E5%9C%BA%E6%99%AF%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F.html", + "relativePath": "03.数据库/02.REDIS相关/060.Redis 中,sentinel 和 cluster 的区别和适用场景是什么?.md", + "key": "v-6a45f68d", + "path": "/pages/e2e910/", "headersStr": null, "content": "", "normalizedContent": "", @@ -7072,6 +7927,31 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, + { + "title": "简述 Redis 的线程模型以及底层架构设计", + "frontmatter": { + "title": "简述 Redis 的线程模型以及底层架构设计", + "date": "2022-05-21T17:23:08.000Z", + "permalink": "/pages/9c28c9/", + "categories": [ + "数据库", + "REDIS相关" + ], + "tags": [ + null + ] + }, + "regularPath": "/03.%E6%95%B0%E6%8D%AE%E5%BA%93/02.REDIS%E7%9B%B8%E5%85%B3/080.%E7%AE%80%E8%BF%B0%20Redis%20%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B%E4%BB%A5%E5%8F%8A%E5%BA%95%E5%B1%82%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1.html", + "relativePath": "03.数据库/02.REDIS相关/080.简述 Redis 的线程模型以及底层架构设计.md", + "key": "v-35718f4e", + "path": "/pages/9c28c9/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, { "title": "简述 Redis 如何处理热点 key 访问", "frontmatter": { @@ -7228,6 +8108,49 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, + { + "title": "Netty中有哪些解码器?", + "frontmatter": { + "title": "Netty中有哪些解码器?", + "date": "2022-05-21T23:33:34.000Z", + "permalink": "/pages/59089d/", + "categories": [ + "中间件", + "Netty" + ], + "tags": [ + null + ] + }, + "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/10.Netty/015.Netty%E4%B8%AD%E6%9C%89%E5%93%AA%E4%BA%9B%E8%A7%A3%E7%A0%81%E5%99%A8%EF%BC%9F.html", + "relativePath": "04.中间件/10.Netty/015.Netty中有哪些解码器?.md", + "key": "v-073f22cd", + "path": "/pages/59089d/", + "headers": [ + { + "level": 2, + "title": "5.1 DelimiterBasedFrameDecoder", + "slug": "_5-1-delimiterbasedframedecoder", + "normalizedTitle": "5.1 delimiterbasedframedecoder", + "charIndex": 2 + }, + { + "level": 2, + "title": "5.2 FixedLengthFrameDecoder", + "slug": "_5-2-fixedlengthframedecoder", + "normalizedTitle": "5.2 fixedlengthframedecoder", + "charIndex": 83 + } + ], + "headersStr": "5.1 DelimiterBasedFrameDecoder 5.2 FixedLengthFrameDecoder", + "content": "# 5.1 DelimiterBasedFrameDecoder\n\n支持任意字符为分隔符 支持设置单条消息最大长度,如果找了最大长度还没找到分隔符就抛出异常\n\n\n# 5.2 FixedLengthFrameDecoder\n\n使用简单,指定包长渡就ok。", + "normalizedContent": "# 5.1 delimiterbasedframedecoder\n\n支持任意字符为分隔符 支持设置单条消息最大长度,如果找了最大长度还没找到分隔符就抛出异常\n\n\n# 5.2 fixedlengthframedecoder\n\n使用简单,指定包长渡就ok。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, { "title": "TCP粘包拆包问题的解决之道", "frontmatter": { @@ -7336,11 +8259,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "Netty中有哪些解码器?", + "title": "ChannelPipeline和ChannelHandler", "frontmatter": { - "title": "Netty中有哪些解码器?", - "date": "2022-05-21T23:33:34.000Z", - "permalink": "/pages/59089d/", + "title": "ChannelPipeline和ChannelHandler", + "date": "2022-05-21T23:36:46.000Z", + "permalink": "/pages/f851b6/", "categories": [ "中间件", "Netty" @@ -7349,29 +8272,72 @@ export const siteData = { null ] }, - "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/10.Netty/015.Netty%E4%B8%AD%E6%9C%89%E5%93%AA%E4%BA%9B%E8%A7%A3%E7%A0%81%E5%99%A8%EF%BC%9F.html", - "relativePath": "04.中间件/10.Netty/015.Netty中有哪些解码器?.md", - "key": "v-073f22cd", - "path": "/pages/59089d/", + "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/10.Netty/030.ChannelPipeline%E5%92%8CChannelHandler.html", + "relativePath": "04.中间件/10.Netty/030.ChannelPipeline和ChannelHandler.md", + "key": "v-5157927c", + "path": "/pages/f851b6/", "headers": [ { "level": 2, - "title": "5.1 DelimiterBasedFrameDecoder", - "slug": "_5-1-delimiterbasedframedecoder", - "normalizedTitle": "5.1 delimiterbasedframedecoder", + "title": "ChannelPipeline和ChannelHandler", + "slug": "channelpipeline和channelhandler", + "normalizedTitle": "channelpipeline和channelhandler", "charIndex": 2 }, { "level": 2, - "title": "5.2 FixedLengthFrameDecoder", - "slug": "_5-2-fixedlengthframedecoder", - "normalizedTitle": "5.2 fixedlengthframedecoder", - "charIndex": 83 + "title": "17.1 ChannelPipeline的功能说明", + "slug": "_17-1-channelpipeline的功能说明", + "normalizedTitle": "17.1 channelpipeline的功能说明", + "charIndex": 170 + }, + { + "level": 2, + "title": "17.1 ChannelPipeline源码", + "slug": "_17-1-channelpipeline源码", + "normalizedTitle": "17.1 channelpipeline源码", + "charIndex": 641 } ], - "headersStr": "5.1 DelimiterBasedFrameDecoder 5.2 FixedLengthFrameDecoder", - "content": "# 5.1 DelimiterBasedFrameDecoder\n\n支持任意字符为分隔符 支持设置单条消息最大长度,如果找了最大长度还没找到分隔符就抛出异常\n\n\n# 5.2 FixedLengthFrameDecoder\n\n使用简单,指定包长渡就ok。", - "normalizedContent": "# 5.1 delimiterbasedframedecoder\n\n支持任意字符为分隔符 支持设置单条消息最大长度,如果找了最大长度还没找到分隔符就抛出异常\n\n\n# 5.2 fixedlengthframedecoder\n\n使用简单,指定包长渡就ok。", + "headersStr": "ChannelPipeline和ChannelHandler 17.1 ChannelPipeline的功能说明 17.1 ChannelPipeline源码", + "content": "# ChannelPipeline和ChannelHandler\n\nChannelPipeline是对Channel的封装,ChannelPipeline持有时间拦截器ChannelHandler的链表,由ChannelHandler对I/O事件拦截和处理,可以通过新增和删除ChannelHandler来实现不同业务的逻辑定制。\n\n\n# 17.1 ChannelPipeline的功能说明\n\nChannelPipeline是ChannelHandler的容器,负责ChannelHandler的管理和事件拦截 17.1.1 ChannelPipeline的事件处理 Netty中的事件分为inbound和outbound事件 inbound事件通常由IO线程触发,如tcp连接建立、断开、异常通知等 outbound事件通常是由用户主动发起的网络IO操作,如用户发起的绑定本地地址,发送消息等 17.1.2 自定义拦截器 通常ChannelHandler只需要继承ChannelHandlerAdapter类覆盖自己关心的方法即可。 17.1.3 ChannelPipeline的主要特性 ChannelPipeline支持动态添加或者删除ChannelHandler。使用场景(业务高峰期加入拥塞保护ChannelHandler,高峰过后删掉ChannelHandler)。 ChannelPipeline是线程安全的,但是ChannelHandler不是线程安全的。\n\n\n# 17.1 ChannelPipeline源码\n\n实际上是ChannelHandler 的容器,内部维护了一个ChannelHandler链表和迭代器。", + "normalizedContent": "# channelpipeline和channelhandler\n\nchannelpipeline是对channel的封装,channelpipeline持有时间拦截器channelhandler的链表,由channelhandler对i/o事件拦截和处理,可以通过新增和删除channelhandler来实现不同业务的逻辑定制。\n\n\n# 17.1 channelpipeline的功能说明\n\nchannelpipeline是channelhandler的容器,负责channelhandler的管理和事件拦截 17.1.1 channelpipeline的事件处理 netty中的事件分为inbound和outbound事件 inbound事件通常由io线程触发,如tcp连接建立、断开、异常通知等 outbound事件通常是由用户主动发起的网络io操作,如用户发起的绑定本地地址,发送消息等 17.1.2 自定义拦截器 通常channelhandler只需要继承channelhandleradapter类覆盖自己关心的方法即可。 17.1.3 channelpipeline的主要特性 channelpipeline支持动态添加或者删除channelhandler。使用场景(业务高峰期加入拥塞保护channelhandler,高峰过后删掉channelhandler)。 channelpipeline是线程安全的,但是channelhandler不是线程安全的。\n\n\n# 17.1 channelpipeline源码\n\n实际上是channelhandler 的容器,内部维护了一个channelhandler链表和迭代器。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, + { + "title": "EventLoop和EventLoopGroup", + "frontmatter": { + "title": "EventLoop和EventLoopGroup", + "date": "2022-05-21T23:37:23.000Z", + "permalink": "/pages/e7aa60/", + "categories": [ + "中间件", + "Netty" + ], + "tags": [ + null + ] + }, + "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/10.Netty/040.EventLoop%E5%92%8CEventLoopGroup.html", + "relativePath": "04.中间件/10.Netty/040.EventLoop和EventLoopGroup.md", + "key": "v-62f3fa3e", + "path": "/pages/e7aa60/", + "headers": [ + { + "level": 2, + "title": "EventLoop和EventLoopGroup", + "slug": "eventloop和eventloopgroup", + "normalizedTitle": "eventloop和eventloopgroup", + "charIndex": 2 + } + ], + "headersStr": "EventLoop和EventLoopGroup", + "content": "# EventLoop和EventLoopGroup\n\nEventLoop负责处理连接中的事件, 和EvenvLoop有关的几个概念是:Channel, EventLoopGroup。\n\n * 一个EventLoopGroup包含一个或多个EventLoop。\n * 一个EventLoop对应于一个线程,所有EventLoop处理的I/O事件都在这个线程中完成。\n * 一个Channel对应唯一个一个EventLoop。\n * 一个EventLoop可以对应多个Channel。\n\n对于基于Netty的网络服务,Client端启动需要一个EventLoopGroup, Server端启动需要两个EventLoopGroup, 因为Server端需要两种Channel, 一种是ServerChannel, 只有一个,负责接受连接,另一种是用于处理连接的一组Channel。\n\nEventLoop继承了concurrent包里的 ScheduledExecutorService,这使得它可以接受Callable或者Runnable并执行。EventLoop中的parent()方法返回包含这个EventLoop的EventLoopGroup", + "normalizedContent": "# eventloop和eventloopgroup\n\neventloop负责处理连接中的事件, 和evenvloop有关的几个概念是:channel, eventloopgroup。\n\n * 一个eventloopgroup包含一个或多个eventloop。\n * 一个eventloop对应于一个线程,所有eventloop处理的i/o事件都在这个线程中完成。\n * 一个channel对应唯一个一个eventloop。\n * 一个eventloop可以对应多个channel。\n\n对于基于netty的网络服务,client端启动需要一个eventloopgroup, server端启动需要两个eventloopgroup, 因为server端需要两种channel, 一种是serverchannel, 只有一个,负责接受连接,另一种是用于处理连接的一组channel。\n\neventloop继承了concurrent包里的 scheduledexecutorservice,这使得它可以接受callable或者runnable并执行。eventloop中的parent()方法返回包含这个eventloop的eventloopgroup", "charsets": { "cjk": true }, @@ -7646,127 +8612,41 @@ export const siteData = { "level": 3, "title": "3.5.2 ServerSocketChannel", "slug": "_3-5-2-serversocketchannel", - "normalizedTitle": "3.5.2 serversocketchannel", - "charIndex": 11237 - }, - { - "level": 3, - "title": "3.5.4 DatagramChannel", - "slug": "_3-5-4-datagramchannel", - "normalizedTitle": "3.5.4 datagramchannel", - "charIndex": 12348 - }, - { - "level": 3, - "title": "3.5.5 Pipes", - "slug": "_3-5-5-pipes", - "normalizedTitle": "3.5.5 pipes", - "charIndex": 12444 - }, - { - "level": 2, - "title": "4.1 基础", - "slug": "_4-1-基础", - "normalizedTitle": "4.1 基础", - "charIndex": 13917 - }, - { - "level": 2, - "title": "4.2 示例", - "slug": "_4-2-示例", - "normalizedTitle": "4.2 示例", - "charIndex": 14101 - } - ], - "headersStr": "1.1 IO概念 1.1.1 Buffer Handler 1.1.1 scatter/gather 1.1.2 虚拟内存 1.1.3 内存页 1.1.4 文件IO 1.1.5 IO流 2.1 Buffer Basic 2.1.1 Buffer的几个属性 2.1.2 Buffer API 2.1.3 访问Buffer 2.1.4 write 2.1.5 Flipping 2.1.6 Draining 2.1.7 Compacting 2.1.9 Mark 2.1.9 Comparing 2.2 创建Buffer 2.3 Duplicating Buffers 2.4 Byte Buffers 2.4.1 Byte Ordering 2.4.2 Direct Buffers 2.4.3 View Buffers 2.4.4 Data Element Views 3.1 basic 3.1.1 Open channel 3.1.2 Using Channels 3.1.3 close Channels 3.2 Scatter/Gather 3.3 File Channels 3.3.1 Accessing Files 3.3.2 File Locking 3.4 Memory-Mapped Files 3.5 Socket Channels 3.5.1 Nonblocking Mode 3.5.2 ServerSocketChannel 3.5.4 DatagramChannel 3.5.5 Pipes 4.1 基础 4.2 示例", - "content": "@TOC\n\n\n# 第一章 IO介绍\n\n\n# 1.1 IO概念\n\n\n# 1.1.1 Buffer Handler\n\n内存分为用户空间(User space),内核空间(Kernel space)。 用户空间的数据读写需要通过内存空间传递。 如下图所示,用户需要读数据,则向cpu发送一个读请求,cpu相应这个请求控制disk controller从disk读取数据到内核buffer,然后再把内核buffer的数据推送给用户空间的buffer。 为什么要区分用户空间和内核空间呢? 每个进程有独立的内存空间好处在于多个进程的内存分配互不影响。如果多个进程共享一块内存,那么内存分配的时候就得排队。\n\n为什么用户空间不能直接从disk读数据呢?\n\n * 硬件控制器只能处理固定大小的数据,用户空间需要的可能是数据块大小异常或者不对齐的数据,内核空间起作到数拆分、重组的作用。\n * 硬件设备通常不允许直接操作虚拟内存,即用户空间\n\nDMA是什么? DMA(Direct Memory Access,直接存储器访问)。 在DMA之前通过中断CPU来传输数据。CPU响应中断,控制总线传输数据。 DMA不需要CPU参与数据传输。DMA可以和CPU交互请求控制总线传输数据。\n\n\n# 1.1.1 scatter/gather\n\n分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。 聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。\n\n比如某个协议的消息固定消息头128字节,消息体1024字节,消息尾128字节。我们想要分别处理消息头,消息体,消息尾。 使用scatter示例代码:\n\nByteBuffer header = ByteBuffer.allocate(128);\nByteBuffer body = ByteBuffer.allocate(1024);\nByteBuffer tail = ByteBuffer.allocate(128);\n\nByteBuffer[] bufferArray = { header, body,tail };\nchannel.read(bufferArray);\n\n\n1\n2\n3\n4\n5\n6\n\n\n注意buffer首先被插入到数组,然后再将数组作为channel.read() 的输入参数。read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。 所以。\n\nscatter不适合处理动态消息,相反,gather适合处理动态消息。 buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与Scattering Reads相反,Gathering Writes能较好的处理动态消息。\n\n\n# 1.1.2 虚拟内存\n\n虚拟内存是指使用虚拟内存地址代替物理内存地址。\n\n * 会有多个虚拟内存指向同一个物理内存\n * 虚拟内存可能会大于物理内存 上面说disk controller不可以直接把数据读到用户空间。虚拟内存可以通过虚拟地址映射内核空间的方式做到这一点。 如下图,内核空间的buffer对用户空间也是可见的。这就是netty零拷贝的原理,这减少了buffer在用户空间和内核空间的拷贝,非常有意义。\n\n\n# 1.1.3 内存页\n\n操作系统将内存按固定字节分页。 内存读取的基本单位是页。\n\nMMU(Memory Management Unit):内存管理单元,保存虚拟内存和物理内存的映射关系,处在CPU和内存之间,起到将虚拟内存转换为物理内存的作用。\n\n\n# 1.1.4 文件IO\n\n文件系统:文件IO发生在文件系统。文件保存在磁盘,磁盘扇区类似内存分页的概念。内存文件读取通过文件系统间接操作磁盘文件。 文件零拷贝如下图。避免了文件系统内存页和用户内存页之间的拷贝。\n\n\n# 1.1.5 IO流\n\n上面说的IO都是面向块的IO。还有面向流的IO。 大多数操作系统支持将流置为非阻塞模式,额外设置一个进程用于检查流上是否有输入,因此流本身是不被阻塞的。 网络IO一般都是流IO。 多路复用:用一个进程管理多个非阻塞IO流的状态。\n\n\n# 第二章 Buffer\n\n标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。\n\n\n# 2.1 Buffer Basic\n\n\n# 2.1.1 Buffer的几个属性\n\n * capacity\n * limit\n * position\n * mark\n\n\n# 2.1.2 Buffer API\n\npackage java.nio;\npublic abstract class Buffer {\npublic final int capacity()\npublic final int position()\npublic final Buffer position (int newPosition) public final int limit()\n }\npublic final Buffer limit (int newLimit)\npublic final Buffer mark()\npublic final Buffer reset()\npublic final Buffer clear()\npublic final Buffer flip()\npublic final Buffer rewind()\npublic final int remaining()\npublic final boolean hasRemaining()\npublic abstract boolean isReadOnly();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n\n\n\n# 2.1.3 访问Buffer\n\npublic abstract class ByteBuffer\n extends Buffer implements Comparable\n {\n// This is a partial API listing\npublic abstract byte get();\npublic abstract byte get (int index);\npublic abstract ByteBuffer put (byte b);\npublic abstract ByteBuffer put (int index, byte b);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n\n# 2.1.4 write\n\nFigure 2-2的buffer经过下面这个代码会变成Figure 2-3的样子:\n\nbuffer.put((byte)'H').put((byte)'e').put((byte)'l')\n.put((byte)'l').put( (byte)'o');\n\n\n1\n2\n\n\n也可以按绝对位置写入:\n\nbuffer.put(0, (byte)'M').put((byte)'w');\n\n\n1\n\n\n\n\n\n# 2.1.5 Flipping\n\n当我们想要写出buffer的内容时,我们需要用position和limit指针指向时机内容的起始位置。类似这样:\n\nbuffer.limit(buffer.position()).position(0);\n\n\n1\n\n\n这等同于buffer.flip(); 调用flip()后2-4会变成2-5 说白了,flip()改变position和limit两个指针。 rewind()只改变position指针,等同于:\n\nbuffer.position(0);\n\n\n1\n\n\n\n# 2.1.6 Draining\n\nhasRemaining()可以判断是否到达limit指针 下面是一个实现,将buffer内容读到一个数组中\n\nfor (int i = 0; buffer.hasRemaining(), i++) {\n myByteArray [i] = buffer.get();\n}\n\n\n1\n2\n3\n\n\nremaining()会返回position到limit的数量。 将buffer内容读到一个数组中另一种实现方式:\n\nint count = buffer.remaining();\nfor (int i = 0; i < count, i++) {\n myByteArray [i] = buffer.get();\n}\n\n\n1\n2\n3\n4\n\n\n\n# 2.1.7 Compacting\n\n当你drain一部分数据想要继续fill数据的时候,你需要把还未drain的数据整体前移。 buffer.compact();可以完成这个工作。 调用buffer.compact()的效果就是2-6到2-7\n\n\n# 2.1.9 Mark\n\nmark()方法会用mark指针指向position的位置。\n\nbuffer.position(2).mark().position(4);\n\n\n1\n\n\nreset()方法会把position指针指向mark的位置。\n\nbuffer.reset();\n\n\n1\n\n\n\n\n\n# 2.1.9 Comparing\n\npublic abstract class ByteBuffer\n extends Buffer implements Comparable\n {\n // This is a partial API listing\npublic boolean equals (Object ob)\npublic int compareTo (Object ob)\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nequals()返回true需要满足三个条件:\n\n * Buffer包含相同类型的元素\n * 两个Buffer的remaining()返回值相同\n * remaining data的序列相同 下面是两个例子:\n\n\n# 2.2 创建Buffer\n\n创建新buffer的两种方式:allocation or wrapping\n\npublic abstract class CharBuffer\nextends Buffer implements CharSequence, Comparable\n{\n}\n// This is a partial API listing\npublic static CharBuffer allocate (int capacity)\npublic static CharBuffer wrap (char [] array)\npublic static CharBuffer wrap (char [] array, int offset, int length)\npublic final boolean hasArray()\npublic final char [] array()\npublic final int arrayOffset()\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n通过allocation创建的buffer是分配一块固定大小的堆内存作为buffer存储空间。 wrapping创建的buffer是创建一个buffer对象指向一个array空间。\n\n\n# 2.3 Duplicating Buffers\n\npublic abstract class CharBuffer\nextends Buffer implements CharSequence, Comparable\n {\n}\n// This is a partial API listing\npublic abstract CharBuffer duplicate();\npublic abstract CharBuffer asReadOnlyBuffer(); \npublic abstract CharBuffer slice();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\nduplicate()创建一个新的buffer,和原buffer一样。 两个buffer共享数据元素 但是他们有独立的position, limit, and mark 一个buffer更改了数据元素对另一个buffer可见 只读、直接内存这两个属性也可以通过duplicate()继承 举个例子:\n\nCharBuffer buffer = CharBuffer.allocate (8); \nbuffer.position (3).limit (6).mark().position (5); \nCharBuffer dupeBuffer = buffer.duplicate(); buffer.clear();\n\n\n1\n2\n3\n\n\nasReadOnlyBuffer()和duplicate()功能类似,只是asReadOnlyBuffer()返回一个只读的视图。 slice() 返回一个新的buffer视图,不过 new_position = original_position new_capacity = original_limit - original_position 举个例子来看slice():\n\nCharBuffer buffer = CharBuffer.allocate (8); \nbuffer.position (3).limit (5);\nCharBuffer sliceBuffer = buffer.slice();\n\n\n1\n2\n3\n\n\n\n\n\n# 2.4 Byte Buffers\n\n\n# 2.4.1 Byte Ordering\n\n原始数据类型在内存中的存储方式是多个字节的连续序列。 举个例子一个32-bit int 值为 0x037FB4C7的内存存储序列可能为2-14也可能为2-15: 这就是所谓的大端设计和小端设计。 采用大端还是小端通常由硬件的设计者决定而不是小端的设计者。 IP协议定义的字节顺序是大端。所有使用IP协议的多字节数值必须在网络字节序和本地主机字节序做转换。\n\npackage java.nio;\npublic final class ByteOrder\n{\npublic static final ByteOrder BIG_ENDIAN public static final ByteOrder LITTLE_ENDIAN\npublic static ByteOrder nativeOrder()\npublic String toString()\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n除了ByteBuffer之外,其他buffer的order()适中返回同一个值,ByteOrder.nativeOrder()。\n\n\n# 2.4.2 Direct Buffers\n\npublic abstract class ByteBuffer extends Buffer implements Comparable {\n// This is a partial API listing\npublic static ByteBuffer allocate (int capacity) ;\npublic static ByteBuffer allocateDirect (int capacity) ;\npublic abstract boolean isDirect();\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\nallocateDirect可以直接在堆外开辟一个buffer。 堆外buffer节省了用户空间和系统空间的buffer拷贝,提升效率。\n\n\n# 2.4.3 View Buffers\n\n收到数据的时候你可能要先看一下数据再决定做什么操作,这就需要用到ByteBuffer的视图API。\n\npublic abstract class ByteBuffer extends Buffer implements Comparable {\n// This is a partial API listing\npublic abstract CharBuffer asCharBuffer(); \npublic abstract ShortBuffer asShortBuffer(); \npublic abstract IntBuffer asIntBuffer();\n public abstract LongBuffer asLongBuffer();\n public abstract FloatBuffer asFloatBuffer();\n public abstract DoubleBuffer asDoubleBuffer();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n视图在直接内存buffer上操作更高效。 当本地硬件的字节顺序和buffer中的字节顺序一致时,通过低级代码就可以操作buffer中的数据,而不需要经过字节码的编码解码。\n\n\n# 2.4.4 Data Element Views\n\n当getInt() 被调用时,那么从当前位置开始的4个字节将被打包成一个int返回。 int value = buffer.getInt();将返回buffer中的1-4位,具体返回的数据值与字节序有关系。 int value = buffer.order (ByteOrder.BIG_ENDIAN).getInt(); 返回0x3BC5315E。 int value = buffer.order (ByteOrder.LITTLE_ENDIAN).getInt(); 返回0x5E31C53B。\n\n\n# 第三章 Channels\n\n\n# 3.1 basic\n\n在NIO接口中我们需要使用Channel和Buffer进行IO操作,Channel模拟了流的概念,但是又有不同。数据总是从一个Channel读到一个buffer中,或者从一个buffer中写到channel中。 channel接口的主要实现类如下:\n\n * FileChannel\n * DatagramChannel\n * SocketChannel\n * ServerSocketChannel\n\nJavaNIO Channels和流有一些相似,但是又有些不同:\n\n你可以同时读和写Channels,流Stream只支持单向的读或写(InputStream/OutputStream)\n\n * Channels可以异步的读和写,流Stream是同步的\n * Channels总是读取到buffer或者从buffer中写入\n\n下面分别介绍一下Channel最重要的一些实现类:\n\n * FileChannel : 可以读写文件中的数据\n * DatagramChannel:可以通过UDP协议读写数据\n * SocketChannel:可以通过TCP协议读写数据\n * ServerSocketChannel:允许我们像一个web服务器那样监听TCP链接请求,为每一个链接请求创建一个SocketChannel\n\n\n# 3.1.1 Open channel\n\nFileChannel只能通过getChannel()得到。 SocketChannel有工厂方法可以得到。\n\nSocketChannel sc = SocketChannel.open();\nsc.connect (new InetSocketAddress (\"somehost\", someport));\n\nServerSocketChannel ssc = ServerSocketChannel.open(); ssc.socket().bind (new InetSocketAddress (somelocalport));\nDatagramChannel dc = DatagramChannel.open();\n\nRandomAccessFile raf = new RandomAccessFile (\"somefile\", \"r\"); FileChannel fc = raf.getChannel();\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 3.1.2 Using Channels\n\npublic interface ReadableByteChannel extends Channel {\npublic int read (ByteBuffer dst) throws IOException;\n}\n\npublic interface WritableByteChannel extends Channel {\npublic int write (ByteBuffer src) throws IOException;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nChannels 可以是单向或者双向的。 如果一个class实现了上面两个接口,就是双向的。 Channels可以在阻塞模式和非阻塞模式工作。 只有socket和pipes可以工作在非阻塞模式。\n\n\n# 3.1.3 close Channels\n\n\n# 3.2 Scatter/Gather\n\nScatter:将多个buffer按照顺序合成一个 Gather:将一个字符串按照顺序拆分成多个。逐个填满buffer。\n\n\n# 3.3 File Channels\n\nFile channels 只能以阻塞模式工作。\n\n\n# 3.3.1 Accessing Files\n\npublic abstract class FileChannel extends AbstractChannel \nimplements ByteChannel, GatheringByteChannel, ScatteringByteChannel{\n// This is a partial API listing\n//获取position位置\npublic abstract long position()\n//设置position\npublic abstract void position (long newPosition)\n//从position位置开始读\npublic abstract int read (ByteBuffer dst)\n//从position位置开始读\npublic abstract int read (ByteBuffer dst, long position)\n//从position位置开始写\n public abstract int write (ByteBuffer src)\n//从position位置开始写\npublic abstract int write (ByteBuffer src, long position)\npublic abstract long size()\n//截取文件保留size大小,并移动position到size的位置\npublic abstract void truncate (long size) \n//强制刷新缓存到磁盘文件\npublic abstract void force (boolean metaData)\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n\n\n\n# 3.3.2 File Locking\n\n\n# 3.4 Memory-Mapped Files\n\n\n# 3.5 Socket Channels\n\nThe new socket channels can operate in nonblocking mode and are selectable. These two capabilities enable tremendous scalability and flexibility in large applications. ServerSocketChannel没有实现read和write接口,说明它不需要转发数据。它只负责家庭socket连接,创建SocketChannel。\n\na channel is a conduit to an I/O service and provides methods for interacting with that service\n\n每个socket channels都有一个socket与之对应,调用socket()可以获得。 但并不是所有的sockets都有channels。socket的getChannel()可能但会null;\n\n\n# 3.5.1 Nonblocking Mode\n\n调用configureBlocking (false);设置channel为非阻塞模式。 但是只有持有blockingLock() 返回的对象锁的线程才能修改阻塞模式。\n\nSocket socket = null;\nObject lockObj = serverChannel.blockingLock();\n// have a handle to the lock object, but haven't locked it yet\n// may block here until lock is acquired\nsynchronize (lockObj){\n// This thread now owns the lock; mode can't be changed boolean prevState = serverChannel.isBlocking();\nserverChannel.configureBlocking (false); socket = serverChannel.accept(); serverChannel.configureBlocking (prevState);\n}\n// lock is now released, mode is allowed to change\nif (socket != null) {\ndoSomethingWithTheSocket (socket);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n\n# 3.5.2 ServerSocketChannel\n\nThe ServerSocketChannel class is a channel-based socket listener. 它负责处理socket任务并添加channel。\n\npublic abstract class SocketChannel\n extends AbstractSelectableChannel\n implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel\n{\n//SocketChannel被创建但是并没有连接,open()只是创建一个SocketChannel,还需要调用connect()连接到一个地址\npublic static SocketChannel open() throws IOException\n//创建并连接\npublic static SocketChannel open(SocketAddress remote) throws IOException\n//支持的操作(读、写、连接)\npublic final int validOps()\n、、\npublic abstract Socket socket();\n//是否连接到远程\npublic abstract boolean isConnected();\n//Connect()方法是耗时的,因为需要数据包对话(TCP握手)。如果SocketChannel身上有并发、connect(),isConnectionPending返回true\npublic abstract boolean isConnectionPending();\n//建立连接\npublic abstract boolean connect(SocketAddress remote) throws IOException;\n//任何时候都可以安全的调用\npublic abstract boolean finishConnect() throws IOException;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\nSocket channels是线程安全的。 Socket是面向流的而不是面向数据包的,它能保证到达的顺序是发送的顺序,不能保证维护分组。发送方发送20个字节,接收方read可能直接受到7个字节,其余字节在传输中。所以,多个线程共享一个socket的一端不是好的设计。\n\n\n# 3.5.4 DatagramChannel\n\nsocketchannel为面向连接的流协议(如tcp/ip) datagramchannel为面向无连接的数据包协议(如udp/ip)\n\n\n# 3.5.5 Pipes\n\nPipe用于同一个JVM内不同线程之间的数据交换问题 不同JVM之间的数据交换问题应该使用SocketChannel Pipe类创建了一对channel对象,它们提供了一种回送机制。 Pipe实现两个进程之间的单向数据连接。 Pipe有一个SinkChannel和一个SourceChannel 数据会从SourceChannel读取、被写到SinkChannel\n\npublic abstract class Pipe {\n public static abstract class SourceChannel\n extends AbstractSelectableChannel\n implements ReadableByteChannel, ScatteringByteChannel\n {\n protected SourceChannel(SelectorProvider provider) {\n super(provider);\n }\n public final int validOps() {\n return SelectionKey.OP_READ;\n }\n }\n\n public static abstract class SinkChannel\n extends AbstractSelectableChannel\n implements WritableByteChannel, GatheringByteChannel\n {\n protected SinkChannel(SelectorProvider provider) {\n super(provider);\n }\n public final int validOps() {\n return SelectionKey.OP_WRITE;\n }\n\n }\n protected Pipe() { }\n //从管道读取数据,要访问source通道\n public abstract SourceChannel source();\n //向管道写入数据,要访问Sink通道\n public abstract SinkChannel sink();\n //管道创建\n public static Pipe open() throws IOException {\n return SelectorProvider.provider().openPipe();\n }\n}\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n\n\n\n# 第四章 Selectors\n\nSelector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。\n\n\n# 4.1 基础\n\n * Selector:一个selector可以注册多个channel,一个线程通过管理Selector实现管理多个channel。\n * SelectableChannel:提供channle selectability的通用方法。\n * SelectionKey:封装了channle和selector的注册关系,包含注册的感兴趣的事件。\n\n\n# 4.2 示例\n\n//创建Selector\nSelector selector = Selector.open();\nchannel.configureBlocking(false);\n//注册Selector\nSelectionKey key = channel.register(selector, SelectionKey.OP_READ);\nwhile(true) {\n//单线程阻塞检查Selector中就绪的事件\n int readyChannels = selector.select();\n if(readyChannels == 0) continue;\n Set selectedKeys = selector.selectedKeys();\n Iterator keyIterator = selectedKeys.iterator();\n //循环处理就绪的事件\n while(keyIterator.hasNext()) {\n SelectionKey key = keyIterator.next();\n if(key.isAcceptable()) {\n // a connection was accepted by a ServerSocketChannel.\n } else if (key.isConnectable()) {\n // a connection was established with a remote server.\n } else if (key.isReadable()) {\n // a channel is ready for reading\n } else if (key.isWritable()) {\n // a channel is ready for writing\n }\n keyIterator.remove();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n", - "normalizedContent": "@toc\n\n\n# 第一章 io介绍\n\n\n# 1.1 io概念\n\n\n# 1.1.1 buffer handler\n\n内存分为用户空间(user space),内核空间(kernel space)。 用户空间的数据读写需要通过内存空间传递。 如下图所示,用户需要读数据,则向cpu发送一个读请求,cpu相应这个请求控制disk controller从disk读取数据到内核buffer,然后再把内核buffer的数据推送给用户空间的buffer。 为什么要区分用户空间和内核空间呢? 每个进程有独立的内存空间好处在于多个进程的内存分配互不影响。如果多个进程共享一块内存,那么内存分配的时候就得排队。\n\n为什么用户空间不能直接从disk读数据呢?\n\n * 硬件控制器只能处理固定大小的数据,用户空间需要的可能是数据块大小异常或者不对齐的数据,内核空间起作到数拆分、重组的作用。\n * 硬件设备通常不允许直接操作虚拟内存,即用户空间\n\ndma是什么? dma(direct memory access,直接存储器访问)。 在dma之前通过中断cpu来传输数据。cpu响应中断,控制总线传输数据。 dma不需要cpu参与数据传输。dma可以和cpu交互请求控制总线传输数据。\n\n\n# 1.1.1 scatter/gather\n\n分散(scatter)从channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,channel将从channel中读取的数据“分散(scatter)”到多个buffer中。 聚集(gather)写入channel是指在写操作时将多个buffer的数据写入同一个channel,因此,channel 将多个buffer中的数据“聚集(gather)”后发送到channel。\n\n比如某个协议的消息固定消息头128字节,消息体1024字节,消息尾128字节。我们想要分别处理消息头,消息体,消息尾。 使用scatter示例代码:\n\nbytebuffer header = bytebuffer.allocate(128);\nbytebuffer body = bytebuffer.allocate(1024);\nbytebuffer tail = bytebuffer.allocate(128);\n\nbytebuffer[] bufferarray = { header, body,tail };\nchannel.read(bufferarray);\n\n\n1\n2\n3\n4\n5\n6\n\n\n注意buffer首先被插入到数组,然后再将数组作为channel.read() 的输入参数。read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。 所以。\n\nscatter不适合处理动态消息,相反,gather适合处理动态消息。 buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与scattering reads相反,gathering writes能较好的处理动态消息。\n\n\n# 1.1.2 虚拟内存\n\n虚拟内存是指使用虚拟内存地址代替物理内存地址。\n\n * 会有多个虚拟内存指向同一个物理内存\n * 虚拟内存可能会大于物理内存 上面说disk controller不可以直接把数据读到用户空间。虚拟内存可以通过虚拟地址映射内核空间的方式做到这一点。 如下图,内核空间的buffer对用户空间也是可见的。这就是netty零拷贝的原理,这减少了buffer在用户空间和内核空间的拷贝,非常有意义。\n\n\n# 1.1.3 内存页\n\n操作系统将内存按固定字节分页。 内存读取的基本单位是页。\n\nmmu(memory management unit):内存管理单元,保存虚拟内存和物理内存的映射关系,处在cpu和内存之间,起到将虚拟内存转换为物理内存的作用。\n\n\n# 1.1.4 文件io\n\n文件系统:文件io发生在文件系统。文件保存在磁盘,磁盘扇区类似内存分页的概念。内存文件读取通过文件系统间接操作磁盘文件。 文件零拷贝如下图。避免了文件系统内存页和用户内存页之间的拷贝。\n\n\n# 1.1.5 io流\n\n上面说的io都是面向块的io。还有面向流的io。 大多数操作系统支持将流置为非阻塞模式,额外设置一个进程用于检查流上是否有输入,因此流本身是不被阻塞的。 网络io一般都是流io。 多路复用:用一个进程管理多个非阻塞io流的状态。\n\n\n# 第二章 buffer\n\n标准的io基于字节流和字符流进行操作的,而nio是基于通道(channel)和缓冲区(buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。\n\n\n# 2.1 buffer basic\n\n\n# 2.1.1 buffer的几个属性\n\n * capacity\n * limit\n * position\n * mark\n\n\n# 2.1.2 buffer api\n\npackage java.nio;\npublic abstract class buffer {\npublic final int capacity()\npublic final int position()\npublic final buffer position (int newposition) public final int limit()\n }\npublic final buffer limit (int newlimit)\npublic final buffer mark()\npublic final buffer reset()\npublic final buffer clear()\npublic final buffer flip()\npublic final buffer rewind()\npublic final int remaining()\npublic final boolean hasremaining()\npublic abstract boolean isreadonly();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n\n\n\n# 2.1.3 访问buffer\n\npublic abstract class bytebuffer\n extends buffer implements comparable\n {\n// this is a partial api listing\npublic abstract byte get();\npublic abstract byte get (int index);\npublic abstract bytebuffer put (byte b);\npublic abstract bytebuffer put (int index, byte b);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n\n# 2.1.4 write\n\nfigure 2-2的buffer经过下面这个代码会变成figure 2-3的样子:\n\nbuffer.put((byte)'h').put((byte)'e').put((byte)'l')\n.put((byte)'l').put( (byte)'o');\n\n\n1\n2\n\n\n也可以按绝对位置写入:\n\nbuffer.put(0, (byte)'m').put((byte)'w');\n\n\n1\n\n\n\n\n\n# 2.1.5 flipping\n\n当我们想要写出buffer的内容时,我们需要用position和limit指针指向时机内容的起始位置。类似这样:\n\nbuffer.limit(buffer.position()).position(0);\n\n\n1\n\n\n这等同于buffer.flip(); 调用flip()后2-4会变成2-5 说白了,flip()改变position和limit两个指针。 rewind()只改变position指针,等同于:\n\nbuffer.position(0);\n\n\n1\n\n\n\n# 2.1.6 draining\n\nhasremaining()可以判断是否到达limit指针 下面是一个实现,将buffer内容读到一个数组中\n\nfor (int i = 0; buffer.hasremaining(), i++) {\n mybytearray [i] = buffer.get();\n}\n\n\n1\n2\n3\n\n\nremaining()会返回position到limit的数量。 将buffer内容读到一个数组中另一种实现方式:\n\nint count = buffer.remaining();\nfor (int i = 0; i < count, i++) {\n mybytearray [i] = buffer.get();\n}\n\n\n1\n2\n3\n4\n\n\n\n# 2.1.7 compacting\n\n当你drain一部分数据想要继续fill数据的时候,你需要把还未drain的数据整体前移。 buffer.compact();可以完成这个工作。 调用buffer.compact()的效果就是2-6到2-7\n\n\n# 2.1.9 mark\n\nmark()方法会用mark指针指向position的位置。\n\nbuffer.position(2).mark().position(4);\n\n\n1\n\n\nreset()方法会把position指针指向mark的位置。\n\nbuffer.reset();\n\n\n1\n\n\n\n\n\n# 2.1.9 comparing\n\npublic abstract class bytebuffer\n extends buffer implements comparable\n {\n // this is a partial api listing\npublic boolean equals (object ob)\npublic int compareto (object ob)\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nequals()返回true需要满足三个条件:\n\n * buffer包含相同类型的元素\n * 两个buffer的remaining()返回值相同\n * remaining data的序列相同 下面是两个例子:\n\n\n# 2.2 创建buffer\n\n创建新buffer的两种方式:allocation or wrapping\n\npublic abstract class charbuffer\nextends buffer implements charsequence, comparable\n{\n}\n// this is a partial api listing\npublic static charbuffer allocate (int capacity)\npublic static charbuffer wrap (char [] array)\npublic static charbuffer wrap (char [] array, int offset, int length)\npublic final boolean hasarray()\npublic final char [] array()\npublic final int arrayoffset()\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n通过allocation创建的buffer是分配一块固定大小的堆内存作为buffer存储空间。 wrapping创建的buffer是创建一个buffer对象指向一个array空间。\n\n\n# 2.3 duplicating buffers\n\npublic abstract class charbuffer\nextends buffer implements charsequence, comparable\n {\n}\n// this is a partial api listing\npublic abstract charbuffer duplicate();\npublic abstract charbuffer asreadonlybuffer(); \npublic abstract charbuffer slice();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\nduplicate()创建一个新的buffer,和原buffer一样。 两个buffer共享数据元素 但是他们有独立的position, limit, and mark 一个buffer更改了数据元素对另一个buffer可见 只读、直接内存这两个属性也可以通过duplicate()继承 举个例子:\n\ncharbuffer buffer = charbuffer.allocate (8); \nbuffer.position (3).limit (6).mark().position (5); \ncharbuffer dupebuffer = buffer.duplicate(); buffer.clear();\n\n\n1\n2\n3\n\n\nasreadonlybuffer()和duplicate()功能类似,只是asreadonlybuffer()返回一个只读的视图。 slice() 返回一个新的buffer视图,不过 new_position = original_position new_capacity = original_limit - original_position 举个例子来看slice():\n\ncharbuffer buffer = charbuffer.allocate (8); \nbuffer.position (3).limit (5);\ncharbuffer slicebuffer = buffer.slice();\n\n\n1\n2\n3\n\n\n\n\n\n# 2.4 byte buffers\n\n\n# 2.4.1 byte ordering\n\n原始数据类型在内存中的存储方式是多个字节的连续序列。 举个例子一个32-bit int 值为 0x037fb4c7的内存存储序列可能为2-14也可能为2-15: 这就是所谓的大端设计和小端设计。 采用大端还是小端通常由硬件的设计者决定而不是小端的设计者。 ip协议定义的字节顺序是大端。所有使用ip协议的多字节数值必须在网络字节序和本地主机字节序做转换。\n\npackage java.nio;\npublic final class byteorder\n{\npublic static final byteorder big_endian public static final byteorder little_endian\npublic static byteorder nativeorder()\npublic string tostring()\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n除了bytebuffer之外,其他buffer的order()适中返回同一个值,byteorder.nativeorder()。\n\n\n# 2.4.2 direct buffers\n\npublic abstract class bytebuffer extends buffer implements comparable {\n// this is a partial api listing\npublic static bytebuffer allocate (int capacity) ;\npublic static bytebuffer allocatedirect (int capacity) ;\npublic abstract boolean isdirect();\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\nallocatedirect可以直接在堆外开辟一个buffer。 堆外buffer节省了用户空间和系统空间的buffer拷贝,提升效率。\n\n\n# 2.4.3 view buffers\n\n收到数据的时候你可能要先看一下数据再决定做什么操作,这就需要用到bytebuffer的视图api。\n\npublic abstract class bytebuffer extends buffer implements comparable {\n// this is a partial api listing\npublic abstract charbuffer ascharbuffer(); \npublic abstract shortbuffer asshortbuffer(); \npublic abstract intbuffer asintbuffer();\n public abstract longbuffer aslongbuffer();\n public abstract floatbuffer asfloatbuffer();\n public abstract doublebuffer asdoublebuffer();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n视图在直接内存buffer上操作更高效。 当本地硬件的字节顺序和buffer中的字节顺序一致时,通过低级代码就可以操作buffer中的数据,而不需要经过字节码的编码解码。\n\n\n# 2.4.4 data element views\n\n当getint() 被调用时,那么从当前位置开始的4个字节将被打包成一个int返回。 int value = buffer.getint();将返回buffer中的1-4位,具体返回的数据值与字节序有关系。 int value = buffer.order (byteorder.big_endian).getint(); 返回0x3bc5315e。 int value = buffer.order (byteorder.little_endian).getint(); 返回0x5e31c53b。\n\n\n# 第三章 channels\n\n\n# 3.1 basic\n\n在nio接口中我们需要使用channel和buffer进行io操作,channel模拟了流的概念,但是又有不同。数据总是从一个channel读到一个buffer中,或者从一个buffer中写到channel中。 channel接口的主要实现类如下:\n\n * filechannel\n * datagramchannel\n * socketchannel\n * serversocketchannel\n\njavanio channels和流有一些相似,但是又有些不同:\n\n你可以同时读和写channels,流stream只支持单向的读或写(inputstream/outputstream)\n\n * channels可以异步的读和写,流stream是同步的\n * channels总是读取到buffer或者从buffer中写入\n\n下面分别介绍一下channel最重要的一些实现类:\n\n * filechannel : 可以读写文件中的数据\n * datagramchannel:可以通过udp协议读写数据\n * socketchannel:可以通过tcp协议读写数据\n * serversocketchannel:允许我们像一个web服务器那样监听tcp链接请求,为每一个链接请求创建一个socketchannel\n\n\n# 3.1.1 open channel\n\nfilechannel只能通过getchannel()得到。 socketchannel有工厂方法可以得到。\n\nsocketchannel sc = socketchannel.open();\nsc.connect (new inetsocketaddress (\"somehost\", someport));\n\nserversocketchannel ssc = serversocketchannel.open(); ssc.socket().bind (new inetsocketaddress (somelocalport));\ndatagramchannel dc = datagramchannel.open();\n\nrandomaccessfile raf = new randomaccessfile (\"somefile\", \"r\"); filechannel fc = raf.getchannel();\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 3.1.2 using channels\n\npublic interface readablebytechannel extends channel {\npublic int read (bytebuffer dst) throws ioexception;\n}\n\npublic interface writablebytechannel extends channel {\npublic int write (bytebuffer src) throws ioexception;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nchannels 可以是单向或者双向的。 如果一个class实现了上面两个接口,就是双向的。 channels可以在阻塞模式和非阻塞模式工作。 只有socket和pipes可以工作在非阻塞模式。\n\n\n# 3.1.3 close channels\n\n\n# 3.2 scatter/gather\n\nscatter:将多个buffer按照顺序合成一个 gather:将一个字符串按照顺序拆分成多个。逐个填满buffer。\n\n\n# 3.3 file channels\n\nfile channels 只能以阻塞模式工作。\n\n\n# 3.3.1 accessing files\n\npublic abstract class filechannel extends abstractchannel \nimplements bytechannel, gatheringbytechannel, scatteringbytechannel{\n// this is a partial api listing\n//获取position位置\npublic abstract long position()\n//设置position\npublic abstract void position (long newposition)\n//从position位置开始读\npublic abstract int read (bytebuffer dst)\n//从position位置开始读\npublic abstract int read (bytebuffer dst, long position)\n//从position位置开始写\n public abstract int write (bytebuffer src)\n//从position位置开始写\npublic abstract int write (bytebuffer src, long position)\npublic abstract long size()\n//截取文件保留size大小,并移动position到size的位置\npublic abstract void truncate (long size) \n//强制刷新缓存到磁盘文件\npublic abstract void force (boolean metadata)\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n\n\n\n# 3.3.2 file locking\n\n\n# 3.4 memory-mapped files\n\n\n# 3.5 socket channels\n\nthe new socket channels can operate in nonblocking mode and are selectable. these two capabilities enable tremendous scalability and flexibility in large applications. serversocketchannel没有实现read和write接口,说明它不需要转发数据。它只负责家庭socket连接,创建socketchannel。\n\na channel is a conduit to an i/o service and provides methods for interacting with that service\n\n每个socket channels都有一个socket与之对应,调用socket()可以获得。 但并不是所有的sockets都有channels。socket的getchannel()可能但会null;\n\n\n# 3.5.1 nonblocking mode\n\n调用configureblocking (false);设置channel为非阻塞模式。 但是只有持有blockinglock() 返回的对象锁的线程才能修改阻塞模式。\n\nsocket socket = null;\nobject lockobj = serverchannel.blockinglock();\n// have a handle to the lock object, but haven't locked it yet\n// may block here until lock is acquired\nsynchronize (lockobj){\n// this thread now owns the lock; mode can't be changed boolean prevstate = serverchannel.isblocking();\nserverchannel.configureblocking (false); socket = serverchannel.accept(); serverchannel.configureblocking (prevstate);\n}\n// lock is now released, mode is allowed to change\nif (socket != null) {\ndosomethingwiththesocket (socket);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n\n# 3.5.2 serversocketchannel\n\nthe serversocketchannel class is a channel-based socket listener. 它负责处理socket任务并添加channel。\n\npublic abstract class socketchannel\n extends abstractselectablechannel\n implements bytechannel, scatteringbytechannel, gatheringbytechannel, networkchannel\n{\n//socketchannel被创建但是并没有连接,open()只是创建一个socketchannel,还需要调用connect()连接到一个地址\npublic static socketchannel open() throws ioexception\n//创建并连接\npublic static socketchannel open(socketaddress remote) throws ioexception\n//支持的操作(读、写、连接)\npublic final int validops()\n、、\npublic abstract socket socket();\n//是否连接到远程\npublic abstract boolean isconnected();\n//connect()方法是耗时的,因为需要数据包对话(tcp握手)。如果socketchannel身上有并发、connect(),isconnectionpending返回true\npublic abstract boolean isconnectionpending();\n//建立连接\npublic abstract boolean connect(socketaddress remote) throws ioexception;\n//任何时候都可以安全的调用\npublic abstract boolean finishconnect() throws ioexception;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\nsocket channels是线程安全的。 socket是面向流的而不是面向数据包的,它能保证到达的顺序是发送的顺序,不能保证维护分组。发送方发送20个字节,接收方read可能直接受到7个字节,其余字节在传输中。所以,多个线程共享一个socket的一端不是好的设计。\n\n\n# 3.5.4 datagramchannel\n\nsocketchannel为面向连接的流协议(如tcp/ip) datagramchannel为面向无连接的数据包协议(如udp/ip)\n\n\n# 3.5.5 pipes\n\npipe用于同一个jvm内不同线程之间的数据交换问题 不同jvm之间的数据交换问题应该使用socketchannel pipe类创建了一对channel对象,它们提供了一种回送机制。 pipe实现两个进程之间的单向数据连接。 pipe有一个sinkchannel和一个sourcechannel 数据会从sourcechannel读取、被写到sinkchannel\n\npublic abstract class pipe {\n public static abstract class sourcechannel\n extends abstractselectablechannel\n implements readablebytechannel, scatteringbytechannel\n {\n protected sourcechannel(selectorprovider provider) {\n super(provider);\n }\n public final int validops() {\n return selectionkey.op_read;\n }\n }\n\n public static abstract class sinkchannel\n extends abstractselectablechannel\n implements writablebytechannel, gatheringbytechannel\n {\n protected sinkchannel(selectorprovider provider) {\n super(provider);\n }\n public final int validops() {\n return selectionkey.op_write;\n }\n\n }\n protected pipe() { }\n //从管道读取数据,要访问source通道\n public abstract sourcechannel source();\n //向管道写入数据,要访问sink通道\n public abstract sinkchannel sink();\n //管道创建\n public static pipe open() throws ioexception {\n return selectorprovider.provider().openpipe();\n }\n}\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n\n\n\n# 第四章 selectors\n\nselector(选择器)是java nio中能够检测一到多个nio通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,仅用单个线程来处理多个channels的好处是,只需要更少的线程来处理通道。\n\n\n# 4.1 基础\n\n * selector:一个selector可以注册多个channel,一个线程通过管理selector实现管理多个channel。\n * selectablechannel:提供channle selectability的通用方法。\n * selectionkey:封装了channle和selector的注册关系,包含注册的感兴趣的事件。\n\n\n# 4.2 示例\n\n//创建selector\nselector selector = selector.open();\nchannel.configureblocking(false);\n//注册selector\nselectionkey key = channel.register(selector, selectionkey.op_read);\nwhile(true) {\n//单线程阻塞检查selector中就绪的事件\n int readychannels = selector.select();\n if(readychannels == 0) continue;\n set selectedkeys = selector.selectedkeys();\n iterator keyiterator = selectedkeys.iterator();\n //循环处理就绪的事件\n while(keyiterator.hasnext()) {\n selectionkey key = keyiterator.next();\n if(key.isacceptable()) {\n // a connection was accepted by a serversocketchannel.\n } else if (key.isconnectable()) {\n // a connection was established with a remote server.\n } else if (key.isreadable()) {\n // a channel is ready for reading\n } else if (key.iswritable()) {\n // a channel is ready for writing\n }\n keyiterator.remove();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "ChannelPipeline和ChannelHandler", - "frontmatter": { - "title": "ChannelPipeline和ChannelHandler", - "date": "2022-05-21T23:36:46.000Z", - "permalink": "/pages/f851b6/", - "categories": [ - "中间件", - "Netty" - ], - "tags": [ - null - ] - }, - "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/10.Netty/030.ChannelPipeline%E5%92%8CChannelHandler.html", - "relativePath": "04.中间件/10.Netty/030.ChannelPipeline和ChannelHandler.md", - "key": "v-5157927c", - "path": "/pages/f851b6/", - "headers": [ - { - "level": 2, - "title": "ChannelPipeline和ChannelHandler", - "slug": "channelpipeline和channelhandler", - "normalizedTitle": "channelpipeline和channelhandler", - "charIndex": 2 - }, - { - "level": 2, - "title": "17.1 ChannelPipeline的功能说明", - "slug": "_17-1-channelpipeline的功能说明", - "normalizedTitle": "17.1 channelpipeline的功能说明", - "charIndex": 170 - }, - { - "level": 2, - "title": "17.1 ChannelPipeline源码", - "slug": "_17-1-channelpipeline源码", - "normalizedTitle": "17.1 channelpipeline源码", - "charIndex": 641 - } - ], - "headersStr": "ChannelPipeline和ChannelHandler 17.1 ChannelPipeline的功能说明 17.1 ChannelPipeline源码", - "content": "# ChannelPipeline和ChannelHandler\n\nChannelPipeline是对Channel的封装,ChannelPipeline持有时间拦截器ChannelHandler的链表,由ChannelHandler对I/O事件拦截和处理,可以通过新增和删除ChannelHandler来实现不同业务的逻辑定制。\n\n\n# 17.1 ChannelPipeline的功能说明\n\nChannelPipeline是ChannelHandler的容器,负责ChannelHandler的管理和事件拦截 17.1.1 ChannelPipeline的事件处理 Netty中的事件分为inbound和outbound事件 inbound事件通常由IO线程触发,如tcp连接建立、断开、异常通知等 outbound事件通常是由用户主动发起的网络IO操作,如用户发起的绑定本地地址,发送消息等 17.1.2 自定义拦截器 通常ChannelHandler只需要继承ChannelHandlerAdapter类覆盖自己关心的方法即可。 17.1.3 ChannelPipeline的主要特性 ChannelPipeline支持动态添加或者删除ChannelHandler。使用场景(业务高峰期加入拥塞保护ChannelHandler,高峰过后删掉ChannelHandler)。 ChannelPipeline是线程安全的,但是ChannelHandler不是线程安全的。\n\n\n# 17.1 ChannelPipeline源码\n\n实际上是ChannelHandler 的容器,内部维护了一个ChannelHandler链表和迭代器。", - "normalizedContent": "# channelpipeline和channelhandler\n\nchannelpipeline是对channel的封装,channelpipeline持有时间拦截器channelhandler的链表,由channelhandler对i/o事件拦截和处理,可以通过新增和删除channelhandler来实现不同业务的逻辑定制。\n\n\n# 17.1 channelpipeline的功能说明\n\nchannelpipeline是channelhandler的容器,负责channelhandler的管理和事件拦截 17.1.1 channelpipeline的事件处理 netty中的事件分为inbound和outbound事件 inbound事件通常由io线程触发,如tcp连接建立、断开、异常通知等 outbound事件通常是由用户主动发起的网络io操作,如用户发起的绑定本地地址,发送消息等 17.1.2 自定义拦截器 通常channelhandler只需要继承channelhandleradapter类覆盖自己关心的方法即可。 17.1.3 channelpipeline的主要特性 channelpipeline支持动态添加或者删除channelhandler。使用场景(业务高峰期加入拥塞保护channelhandler,高峰过后删掉channelhandler)。 channelpipeline是线程安全的,但是channelhandler不是线程安全的。\n\n\n# 17.1 channelpipeline源码\n\n实际上是channelhandler 的容器,内部维护了一个channelhandler链表和迭代器。", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, - { - "title": "EventLoop和EventLoopGroup", - "frontmatter": { - "title": "EventLoop和EventLoopGroup", - "date": "2022-05-21T23:37:23.000Z", - "permalink": "/pages/e7aa60/", - "categories": [ - "中间件", - "Netty" - ], - "tags": [ - null - ] - }, - "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/10.Netty/040.EventLoop%E5%92%8CEventLoopGroup.html", - "relativePath": "04.中间件/10.Netty/040.EventLoop和EventLoopGroup.md", - "key": "v-62f3fa3e", - "path": "/pages/e7aa60/", - "headers": [ + "normalizedTitle": "3.5.2 serversocketchannel", + "charIndex": 11237 + }, + { + "level": 3, + "title": "3.5.4 DatagramChannel", + "slug": "_3-5-4-datagramchannel", + "normalizedTitle": "3.5.4 datagramchannel", + "charIndex": 12348 + }, + { + "level": 3, + "title": "3.5.5 Pipes", + "slug": "_3-5-5-pipes", + "normalizedTitle": "3.5.5 pipes", + "charIndex": 12444 + }, { "level": 2, - "title": "EventLoop和EventLoopGroup", - "slug": "eventloop和eventloopgroup", - "normalizedTitle": "eventloop和eventloopgroup", - "charIndex": 2 + "title": "4.1 基础", + "slug": "_4-1-基础", + "normalizedTitle": "4.1 基础", + "charIndex": 13917 + }, + { + "level": 2, + "title": "4.2 示例", + "slug": "_4-2-示例", + "normalizedTitle": "4.2 示例", + "charIndex": 14101 } ], - "headersStr": "EventLoop和EventLoopGroup", - "content": "# EventLoop和EventLoopGroup\n\nEventLoop负责处理连接中的事件, 和EvenvLoop有关的几个概念是:Channel, EventLoopGroup。\n\n * 一个EventLoopGroup包含一个或多个EventLoop。\n * 一个EventLoop对应于一个线程,所有EventLoop处理的I/O事件都在这个线程中完成。\n * 一个Channel对应唯一个一个EventLoop。\n * 一个EventLoop可以对应多个Channel。\n\n对于基于Netty的网络服务,Client端启动需要一个EventLoopGroup, Server端启动需要两个EventLoopGroup, 因为Server端需要两种Channel, 一种是ServerChannel, 只有一个,负责接受连接,另一种是用于处理连接的一组Channel。\n\nEventLoop继承了concurrent包里的 ScheduledExecutorService,这使得它可以接受Callable或者Runnable并执行。EventLoop中的parent()方法返回包含这个EventLoop的EventLoopGroup", - "normalizedContent": "# eventloop和eventloopgroup\n\neventloop负责处理连接中的事件, 和evenvloop有关的几个概念是:channel, eventloopgroup。\n\n * 一个eventloopgroup包含一个或多个eventloop。\n * 一个eventloop对应于一个线程,所有eventloop处理的i/o事件都在这个线程中完成。\n * 一个channel对应唯一个一个eventloop。\n * 一个eventloop可以对应多个channel。\n\n对于基于netty的网络服务,client端启动需要一个eventloopgroup, server端启动需要两个eventloopgroup, 因为server端需要两种channel, 一种是serverchannel, 只有一个,负责接受连接,另一种是用于处理连接的一组channel。\n\neventloop继承了concurrent包里的 scheduledexecutorservice,这使得它可以接受callable或者runnable并执行。eventloop中的parent()方法返回包含这个eventloop的eventloopgroup", + "headersStr": "1.1 IO概念 1.1.1 Buffer Handler 1.1.1 scatter/gather 1.1.2 虚拟内存 1.1.3 内存页 1.1.4 文件IO 1.1.5 IO流 2.1 Buffer Basic 2.1.1 Buffer的几个属性 2.1.2 Buffer API 2.1.3 访问Buffer 2.1.4 write 2.1.5 Flipping 2.1.6 Draining 2.1.7 Compacting 2.1.9 Mark 2.1.9 Comparing 2.2 创建Buffer 2.3 Duplicating Buffers 2.4 Byte Buffers 2.4.1 Byte Ordering 2.4.2 Direct Buffers 2.4.3 View Buffers 2.4.4 Data Element Views 3.1 basic 3.1.1 Open channel 3.1.2 Using Channels 3.1.3 close Channels 3.2 Scatter/Gather 3.3 File Channels 3.3.1 Accessing Files 3.3.2 File Locking 3.4 Memory-Mapped Files 3.5 Socket Channels 3.5.1 Nonblocking Mode 3.5.2 ServerSocketChannel 3.5.4 DatagramChannel 3.5.5 Pipes 4.1 基础 4.2 示例", + "content": "@TOC\n\n\n# 第一章 IO介绍\n\n\n# 1.1 IO概念\n\n\n# 1.1.1 Buffer Handler\n\n内存分为用户空间(User space),内核空间(Kernel space)。 用户空间的数据读写需要通过内存空间传递。 如下图所示,用户需要读数据,则向cpu发送一个读请求,cpu相应这个请求控制disk controller从disk读取数据到内核buffer,然后再把内核buffer的数据推送给用户空间的buffer。 为什么要区分用户空间和内核空间呢? 每个进程有独立的内存空间好处在于多个进程的内存分配互不影响。如果多个进程共享一块内存,那么内存分配的时候就得排队。\n\n为什么用户空间不能直接从disk读数据呢?\n\n * 硬件控制器只能处理固定大小的数据,用户空间需要的可能是数据块大小异常或者不对齐的数据,内核空间起作到数拆分、重组的作用。\n * 硬件设备通常不允许直接操作虚拟内存,即用户空间\n\nDMA是什么? DMA(Direct Memory Access,直接存储器访问)。 在DMA之前通过中断CPU来传输数据。CPU响应中断,控制总线传输数据。 DMA不需要CPU参与数据传输。DMA可以和CPU交互请求控制总线传输数据。\n\n\n# 1.1.1 scatter/gather\n\n分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。 聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。\n\n比如某个协议的消息固定消息头128字节,消息体1024字节,消息尾128字节。我们想要分别处理消息头,消息体,消息尾。 使用scatter示例代码:\n\nByteBuffer header = ByteBuffer.allocate(128);\nByteBuffer body = ByteBuffer.allocate(1024);\nByteBuffer tail = ByteBuffer.allocate(128);\n\nByteBuffer[] bufferArray = { header, body,tail };\nchannel.read(bufferArray);\n\n\n1\n2\n3\n4\n5\n6\n\n\n注意buffer首先被插入到数组,然后再将数组作为channel.read() 的输入参数。read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。 所以。\n\nscatter不适合处理动态消息,相反,gather适合处理动态消息。 buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与Scattering Reads相反,Gathering Writes能较好的处理动态消息。\n\n\n# 1.1.2 虚拟内存\n\n虚拟内存是指使用虚拟内存地址代替物理内存地址。\n\n * 会有多个虚拟内存指向同一个物理内存\n * 虚拟内存可能会大于物理内存 上面说disk controller不可以直接把数据读到用户空间。虚拟内存可以通过虚拟地址映射内核空间的方式做到这一点。 如下图,内核空间的buffer对用户空间也是可见的。这就是netty零拷贝的原理,这减少了buffer在用户空间和内核空间的拷贝,非常有意义。\n\n\n# 1.1.3 内存页\n\n操作系统将内存按固定字节分页。 内存读取的基本单位是页。\n\nMMU(Memory Management Unit):内存管理单元,保存虚拟内存和物理内存的映射关系,处在CPU和内存之间,起到将虚拟内存转换为物理内存的作用。\n\n\n# 1.1.4 文件IO\n\n文件系统:文件IO发生在文件系统。文件保存在磁盘,磁盘扇区类似内存分页的概念。内存文件读取通过文件系统间接操作磁盘文件。 文件零拷贝如下图。避免了文件系统内存页和用户内存页之间的拷贝。\n\n\n# 1.1.5 IO流\n\n上面说的IO都是面向块的IO。还有面向流的IO。 大多数操作系统支持将流置为非阻塞模式,额外设置一个进程用于检查流上是否有输入,因此流本身是不被阻塞的。 网络IO一般都是流IO。 多路复用:用一个进程管理多个非阻塞IO流的状态。\n\n\n# 第二章 Buffer\n\n标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。\n\n\n# 2.1 Buffer Basic\n\n\n# 2.1.1 Buffer的几个属性\n\n * capacity\n * limit\n * position\n * mark\n\n\n# 2.1.2 Buffer API\n\npackage java.nio;\npublic abstract class Buffer {\npublic final int capacity()\npublic final int position()\npublic final Buffer position (int newPosition) public final int limit()\n }\npublic final Buffer limit (int newLimit)\npublic final Buffer mark()\npublic final Buffer reset()\npublic final Buffer clear()\npublic final Buffer flip()\npublic final Buffer rewind()\npublic final int remaining()\npublic final boolean hasRemaining()\npublic abstract boolean isReadOnly();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n\n\n\n# 2.1.3 访问Buffer\n\npublic abstract class ByteBuffer\n extends Buffer implements Comparable\n {\n// This is a partial API listing\npublic abstract byte get();\npublic abstract byte get (int index);\npublic abstract ByteBuffer put (byte b);\npublic abstract ByteBuffer put (int index, byte b);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n\n# 2.1.4 write\n\nFigure 2-2的buffer经过下面这个代码会变成Figure 2-3的样子:\n\nbuffer.put((byte)'H').put((byte)'e').put((byte)'l')\n.put((byte)'l').put( (byte)'o');\n\n\n1\n2\n\n\n也可以按绝对位置写入:\n\nbuffer.put(0, (byte)'M').put((byte)'w');\n\n\n1\n\n\n\n\n\n# 2.1.5 Flipping\n\n当我们想要写出buffer的内容时,我们需要用position和limit指针指向时机内容的起始位置。类似这样:\n\nbuffer.limit(buffer.position()).position(0);\n\n\n1\n\n\n这等同于buffer.flip(); 调用flip()后2-4会变成2-5 说白了,flip()改变position和limit两个指针。 rewind()只改变position指针,等同于:\n\nbuffer.position(0);\n\n\n1\n\n\n\n# 2.1.6 Draining\n\nhasRemaining()可以判断是否到达limit指针 下面是一个实现,将buffer内容读到一个数组中\n\nfor (int i = 0; buffer.hasRemaining(), i++) {\n myByteArray [i] = buffer.get();\n}\n\n\n1\n2\n3\n\n\nremaining()会返回position到limit的数量。 将buffer内容读到一个数组中另一种实现方式:\n\nint count = buffer.remaining();\nfor (int i = 0; i < count, i++) {\n myByteArray [i] = buffer.get();\n}\n\n\n1\n2\n3\n4\n\n\n\n# 2.1.7 Compacting\n\n当你drain一部分数据想要继续fill数据的时候,你需要把还未drain的数据整体前移。 buffer.compact();可以完成这个工作。 调用buffer.compact()的效果就是2-6到2-7\n\n\n# 2.1.9 Mark\n\nmark()方法会用mark指针指向position的位置。\n\nbuffer.position(2).mark().position(4);\n\n\n1\n\n\nreset()方法会把position指针指向mark的位置。\n\nbuffer.reset();\n\n\n1\n\n\n\n\n\n# 2.1.9 Comparing\n\npublic abstract class ByteBuffer\n extends Buffer implements Comparable\n {\n // This is a partial API listing\npublic boolean equals (Object ob)\npublic int compareTo (Object ob)\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nequals()返回true需要满足三个条件:\n\n * Buffer包含相同类型的元素\n * 两个Buffer的remaining()返回值相同\n * remaining data的序列相同 下面是两个例子:\n\n\n# 2.2 创建Buffer\n\n创建新buffer的两种方式:allocation or wrapping\n\npublic abstract class CharBuffer\nextends Buffer implements CharSequence, Comparable\n{\n}\n// This is a partial API listing\npublic static CharBuffer allocate (int capacity)\npublic static CharBuffer wrap (char [] array)\npublic static CharBuffer wrap (char [] array, int offset, int length)\npublic final boolean hasArray()\npublic final char [] array()\npublic final int arrayOffset()\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n通过allocation创建的buffer是分配一块固定大小的堆内存作为buffer存储空间。 wrapping创建的buffer是创建一个buffer对象指向一个array空间。\n\n\n# 2.3 Duplicating Buffers\n\npublic abstract class CharBuffer\nextends Buffer implements CharSequence, Comparable\n {\n}\n// This is a partial API listing\npublic abstract CharBuffer duplicate();\npublic abstract CharBuffer asReadOnlyBuffer(); \npublic abstract CharBuffer slice();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\nduplicate()创建一个新的buffer,和原buffer一样。 两个buffer共享数据元素 但是他们有独立的position, limit, and mark 一个buffer更改了数据元素对另一个buffer可见 只读、直接内存这两个属性也可以通过duplicate()继承 举个例子:\n\nCharBuffer buffer = CharBuffer.allocate (8); \nbuffer.position (3).limit (6).mark().position (5); \nCharBuffer dupeBuffer = buffer.duplicate(); buffer.clear();\n\n\n1\n2\n3\n\n\nasReadOnlyBuffer()和duplicate()功能类似,只是asReadOnlyBuffer()返回一个只读的视图。 slice() 返回一个新的buffer视图,不过 new_position = original_position new_capacity = original_limit - original_position 举个例子来看slice():\n\nCharBuffer buffer = CharBuffer.allocate (8); \nbuffer.position (3).limit (5);\nCharBuffer sliceBuffer = buffer.slice();\n\n\n1\n2\n3\n\n\n\n\n\n# 2.4 Byte Buffers\n\n\n# 2.4.1 Byte Ordering\n\n原始数据类型在内存中的存储方式是多个字节的连续序列。 举个例子一个32-bit int 值为 0x037FB4C7的内存存储序列可能为2-14也可能为2-15: 这就是所谓的大端设计和小端设计。 采用大端还是小端通常由硬件的设计者决定而不是小端的设计者。 IP协议定义的字节顺序是大端。所有使用IP协议的多字节数值必须在网络字节序和本地主机字节序做转换。\n\npackage java.nio;\npublic final class ByteOrder\n{\npublic static final ByteOrder BIG_ENDIAN public static final ByteOrder LITTLE_ENDIAN\npublic static ByteOrder nativeOrder()\npublic String toString()\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n除了ByteBuffer之外,其他buffer的order()适中返回同一个值,ByteOrder.nativeOrder()。\n\n\n# 2.4.2 Direct Buffers\n\npublic abstract class ByteBuffer extends Buffer implements Comparable {\n// This is a partial API listing\npublic static ByteBuffer allocate (int capacity) ;\npublic static ByteBuffer allocateDirect (int capacity) ;\npublic abstract boolean isDirect();\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\nallocateDirect可以直接在堆外开辟一个buffer。 堆外buffer节省了用户空间和系统空间的buffer拷贝,提升效率。\n\n\n# 2.4.3 View Buffers\n\n收到数据的时候你可能要先看一下数据再决定做什么操作,这就需要用到ByteBuffer的视图API。\n\npublic abstract class ByteBuffer extends Buffer implements Comparable {\n// This is a partial API listing\npublic abstract CharBuffer asCharBuffer(); \npublic abstract ShortBuffer asShortBuffer(); \npublic abstract IntBuffer asIntBuffer();\n public abstract LongBuffer asLongBuffer();\n public abstract FloatBuffer asFloatBuffer();\n public abstract DoubleBuffer asDoubleBuffer();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n视图在直接内存buffer上操作更高效。 当本地硬件的字节顺序和buffer中的字节顺序一致时,通过低级代码就可以操作buffer中的数据,而不需要经过字节码的编码解码。\n\n\n# 2.4.4 Data Element Views\n\n当getInt() 被调用时,那么从当前位置开始的4个字节将被打包成一个int返回。 int value = buffer.getInt();将返回buffer中的1-4位,具体返回的数据值与字节序有关系。 int value = buffer.order (ByteOrder.BIG_ENDIAN).getInt(); 返回0x3BC5315E。 int value = buffer.order (ByteOrder.LITTLE_ENDIAN).getInt(); 返回0x5E31C53B。\n\n\n# 第三章 Channels\n\n\n# 3.1 basic\n\n在NIO接口中我们需要使用Channel和Buffer进行IO操作,Channel模拟了流的概念,但是又有不同。数据总是从一个Channel读到一个buffer中,或者从一个buffer中写到channel中。 channel接口的主要实现类如下:\n\n * FileChannel\n * DatagramChannel\n * SocketChannel\n * ServerSocketChannel\n\nJavaNIO Channels和流有一些相似,但是又有些不同:\n\n你可以同时读和写Channels,流Stream只支持单向的读或写(InputStream/OutputStream)\n\n * Channels可以异步的读和写,流Stream是同步的\n * Channels总是读取到buffer或者从buffer中写入\n\n下面分别介绍一下Channel最重要的一些实现类:\n\n * FileChannel : 可以读写文件中的数据\n * DatagramChannel:可以通过UDP协议读写数据\n * SocketChannel:可以通过TCP协议读写数据\n * ServerSocketChannel:允许我们像一个web服务器那样监听TCP链接请求,为每一个链接请求创建一个SocketChannel\n\n\n# 3.1.1 Open channel\n\nFileChannel只能通过getChannel()得到。 SocketChannel有工厂方法可以得到。\n\nSocketChannel sc = SocketChannel.open();\nsc.connect (new InetSocketAddress (\"somehost\", someport));\n\nServerSocketChannel ssc = ServerSocketChannel.open(); ssc.socket().bind (new InetSocketAddress (somelocalport));\nDatagramChannel dc = DatagramChannel.open();\n\nRandomAccessFile raf = new RandomAccessFile (\"somefile\", \"r\"); FileChannel fc = raf.getChannel();\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 3.1.2 Using Channels\n\npublic interface ReadableByteChannel extends Channel {\npublic int read (ByteBuffer dst) throws IOException;\n}\n\npublic interface WritableByteChannel extends Channel {\npublic int write (ByteBuffer src) throws IOException;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nChannels 可以是单向或者双向的。 如果一个class实现了上面两个接口,就是双向的。 Channels可以在阻塞模式和非阻塞模式工作。 只有socket和pipes可以工作在非阻塞模式。\n\n\n# 3.1.3 close Channels\n\n\n# 3.2 Scatter/Gather\n\nScatter:将多个buffer按照顺序合成一个 Gather:将一个字符串按照顺序拆分成多个。逐个填满buffer。\n\n\n# 3.3 File Channels\n\nFile channels 只能以阻塞模式工作。\n\n\n# 3.3.1 Accessing Files\n\npublic abstract class FileChannel extends AbstractChannel \nimplements ByteChannel, GatheringByteChannel, ScatteringByteChannel{\n// This is a partial API listing\n//获取position位置\npublic abstract long position()\n//设置position\npublic abstract void position (long newPosition)\n//从position位置开始读\npublic abstract int read (ByteBuffer dst)\n//从position位置开始读\npublic abstract int read (ByteBuffer dst, long position)\n//从position位置开始写\n public abstract int write (ByteBuffer src)\n//从position位置开始写\npublic abstract int write (ByteBuffer src, long position)\npublic abstract long size()\n//截取文件保留size大小,并移动position到size的位置\npublic abstract void truncate (long size) \n//强制刷新缓存到磁盘文件\npublic abstract void force (boolean metaData)\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n\n\n\n# 3.3.2 File Locking\n\n\n# 3.4 Memory-Mapped Files\n\n\n# 3.5 Socket Channels\n\nThe new socket channels can operate in nonblocking mode and are selectable. These two capabilities enable tremendous scalability and flexibility in large applications. ServerSocketChannel没有实现read和write接口,说明它不需要转发数据。它只负责家庭socket连接,创建SocketChannel。\n\na channel is a conduit to an I/O service and provides methods for interacting with that service\n\n每个socket channels都有一个socket与之对应,调用socket()可以获得。 但并不是所有的sockets都有channels。socket的getChannel()可能但会null;\n\n\n# 3.5.1 Nonblocking Mode\n\n调用configureBlocking (false);设置channel为非阻塞模式。 但是只有持有blockingLock() 返回的对象锁的线程才能修改阻塞模式。\n\nSocket socket = null;\nObject lockObj = serverChannel.blockingLock();\n// have a handle to the lock object, but haven't locked it yet\n// may block here until lock is acquired\nsynchronize (lockObj){\n// This thread now owns the lock; mode can't be changed boolean prevState = serverChannel.isBlocking();\nserverChannel.configureBlocking (false); socket = serverChannel.accept(); serverChannel.configureBlocking (prevState);\n}\n// lock is now released, mode is allowed to change\nif (socket != null) {\ndoSomethingWithTheSocket (socket);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n\n# 3.5.2 ServerSocketChannel\n\nThe ServerSocketChannel class is a channel-based socket listener. 它负责处理socket任务并添加channel。\n\npublic abstract class SocketChannel\n extends AbstractSelectableChannel\n implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel\n{\n//SocketChannel被创建但是并没有连接,open()只是创建一个SocketChannel,还需要调用connect()连接到一个地址\npublic static SocketChannel open() throws IOException\n//创建并连接\npublic static SocketChannel open(SocketAddress remote) throws IOException\n//支持的操作(读、写、连接)\npublic final int validOps()\n、、\npublic abstract Socket socket();\n//是否连接到远程\npublic abstract boolean isConnected();\n//Connect()方法是耗时的,因为需要数据包对话(TCP握手)。如果SocketChannel身上有并发、connect(),isConnectionPending返回true\npublic abstract boolean isConnectionPending();\n//建立连接\npublic abstract boolean connect(SocketAddress remote) throws IOException;\n//任何时候都可以安全的调用\npublic abstract boolean finishConnect() throws IOException;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\nSocket channels是线程安全的。 Socket是面向流的而不是面向数据包的,它能保证到达的顺序是发送的顺序,不能保证维护分组。发送方发送20个字节,接收方read可能直接受到7个字节,其余字节在传输中。所以,多个线程共享一个socket的一端不是好的设计。\n\n\n# 3.5.4 DatagramChannel\n\nsocketchannel为面向连接的流协议(如tcp/ip) datagramchannel为面向无连接的数据包协议(如udp/ip)\n\n\n# 3.5.5 Pipes\n\nPipe用于同一个JVM内不同线程之间的数据交换问题 不同JVM之间的数据交换问题应该使用SocketChannel Pipe类创建了一对channel对象,它们提供了一种回送机制。 Pipe实现两个进程之间的单向数据连接。 Pipe有一个SinkChannel和一个SourceChannel 数据会从SourceChannel读取、被写到SinkChannel\n\npublic abstract class Pipe {\n public static abstract class SourceChannel\n extends AbstractSelectableChannel\n implements ReadableByteChannel, ScatteringByteChannel\n {\n protected SourceChannel(SelectorProvider provider) {\n super(provider);\n }\n public final int validOps() {\n return SelectionKey.OP_READ;\n }\n }\n\n public static abstract class SinkChannel\n extends AbstractSelectableChannel\n implements WritableByteChannel, GatheringByteChannel\n {\n protected SinkChannel(SelectorProvider provider) {\n super(provider);\n }\n public final int validOps() {\n return SelectionKey.OP_WRITE;\n }\n\n }\n protected Pipe() { }\n //从管道读取数据,要访问source通道\n public abstract SourceChannel source();\n //向管道写入数据,要访问Sink通道\n public abstract SinkChannel sink();\n //管道创建\n public static Pipe open() throws IOException {\n return SelectorProvider.provider().openPipe();\n }\n}\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n\n\n\n# 第四章 Selectors\n\nSelector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。\n\n\n# 4.1 基础\n\n * Selector:一个selector可以注册多个channel,一个线程通过管理Selector实现管理多个channel。\n * SelectableChannel:提供channle selectability的通用方法。\n * SelectionKey:封装了channle和selector的注册关系,包含注册的感兴趣的事件。\n\n\n# 4.2 示例\n\n//创建Selector\nSelector selector = Selector.open();\nchannel.configureBlocking(false);\n//注册Selector\nSelectionKey key = channel.register(selector, SelectionKey.OP_READ);\nwhile(true) {\n//单线程阻塞检查Selector中就绪的事件\n int readyChannels = selector.select();\n if(readyChannels == 0) continue;\n Set selectedKeys = selector.selectedKeys();\n Iterator keyIterator = selectedKeys.iterator();\n //循环处理就绪的事件\n while(keyIterator.hasNext()) {\n SelectionKey key = keyIterator.next();\n if(key.isAcceptable()) {\n // a connection was accepted by a ServerSocketChannel.\n } else if (key.isConnectable()) {\n // a connection was established with a remote server.\n } else if (key.isReadable()) {\n // a channel is ready for reading\n } else if (key.isWritable()) {\n // a channel is ready for writing\n }\n keyIterator.remove();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n", + "normalizedContent": "@toc\n\n\n# 第一章 io介绍\n\n\n# 1.1 io概念\n\n\n# 1.1.1 buffer handler\n\n内存分为用户空间(user space),内核空间(kernel space)。 用户空间的数据读写需要通过内存空间传递。 如下图所示,用户需要读数据,则向cpu发送一个读请求,cpu相应这个请求控制disk controller从disk读取数据到内核buffer,然后再把内核buffer的数据推送给用户空间的buffer。 为什么要区分用户空间和内核空间呢? 每个进程有独立的内存空间好处在于多个进程的内存分配互不影响。如果多个进程共享一块内存,那么内存分配的时候就得排队。\n\n为什么用户空间不能直接从disk读数据呢?\n\n * 硬件控制器只能处理固定大小的数据,用户空间需要的可能是数据块大小异常或者不对齐的数据,内核空间起作到数拆分、重组的作用。\n * 硬件设备通常不允许直接操作虚拟内存,即用户空间\n\ndma是什么? dma(direct memory access,直接存储器访问)。 在dma之前通过中断cpu来传输数据。cpu响应中断,控制总线传输数据。 dma不需要cpu参与数据传输。dma可以和cpu交互请求控制总线传输数据。\n\n\n# 1.1.1 scatter/gather\n\n分散(scatter)从channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,channel将从channel中读取的数据“分散(scatter)”到多个buffer中。 聚集(gather)写入channel是指在写操作时将多个buffer的数据写入同一个channel,因此,channel 将多个buffer中的数据“聚集(gather)”后发送到channel。\n\n比如某个协议的消息固定消息头128字节,消息体1024字节,消息尾128字节。我们想要分别处理消息头,消息体,消息尾。 使用scatter示例代码:\n\nbytebuffer header = bytebuffer.allocate(128);\nbytebuffer body = bytebuffer.allocate(1024);\nbytebuffer tail = bytebuffer.allocate(128);\n\nbytebuffer[] bufferarray = { header, body,tail };\nchannel.read(bufferarray);\n\n\n1\n2\n3\n4\n5\n6\n\n\n注意buffer首先被插入到数组,然后再将数组作为channel.read() 的输入参数。read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。 所以。\n\nscatter不适合处理动态消息,相反,gather适合处理动态消息。 buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与scattering reads相反,gathering writes能较好的处理动态消息。\n\n\n# 1.1.2 虚拟内存\n\n虚拟内存是指使用虚拟内存地址代替物理内存地址。\n\n * 会有多个虚拟内存指向同一个物理内存\n * 虚拟内存可能会大于物理内存 上面说disk controller不可以直接把数据读到用户空间。虚拟内存可以通过虚拟地址映射内核空间的方式做到这一点。 如下图,内核空间的buffer对用户空间也是可见的。这就是netty零拷贝的原理,这减少了buffer在用户空间和内核空间的拷贝,非常有意义。\n\n\n# 1.1.3 内存页\n\n操作系统将内存按固定字节分页。 内存读取的基本单位是页。\n\nmmu(memory management unit):内存管理单元,保存虚拟内存和物理内存的映射关系,处在cpu和内存之间,起到将虚拟内存转换为物理内存的作用。\n\n\n# 1.1.4 文件io\n\n文件系统:文件io发生在文件系统。文件保存在磁盘,磁盘扇区类似内存分页的概念。内存文件读取通过文件系统间接操作磁盘文件。 文件零拷贝如下图。避免了文件系统内存页和用户内存页之间的拷贝。\n\n\n# 1.1.5 io流\n\n上面说的io都是面向块的io。还有面向流的io。 大多数操作系统支持将流置为非阻塞模式,额外设置一个进程用于检查流上是否有输入,因此流本身是不被阻塞的。 网络io一般都是流io。 多路复用:用一个进程管理多个非阻塞io流的状态。\n\n\n# 第二章 buffer\n\n标准的io基于字节流和字符流进行操作的,而nio是基于通道(channel)和缓冲区(buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。\n\n\n# 2.1 buffer basic\n\n\n# 2.1.1 buffer的几个属性\n\n * capacity\n * limit\n * position\n * mark\n\n\n# 2.1.2 buffer api\n\npackage java.nio;\npublic abstract class buffer {\npublic final int capacity()\npublic final int position()\npublic final buffer position (int newposition) public final int limit()\n }\npublic final buffer limit (int newlimit)\npublic final buffer mark()\npublic final buffer reset()\npublic final buffer clear()\npublic final buffer flip()\npublic final buffer rewind()\npublic final int remaining()\npublic final boolean hasremaining()\npublic abstract boolean isreadonly();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n\n\n\n# 2.1.3 访问buffer\n\npublic abstract class bytebuffer\n extends buffer implements comparable\n {\n// this is a partial api listing\npublic abstract byte get();\npublic abstract byte get (int index);\npublic abstract bytebuffer put (byte b);\npublic abstract bytebuffer put (int index, byte b);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n\n# 2.1.4 write\n\nfigure 2-2的buffer经过下面这个代码会变成figure 2-3的样子:\n\nbuffer.put((byte)'h').put((byte)'e').put((byte)'l')\n.put((byte)'l').put( (byte)'o');\n\n\n1\n2\n\n\n也可以按绝对位置写入:\n\nbuffer.put(0, (byte)'m').put((byte)'w');\n\n\n1\n\n\n\n\n\n# 2.1.5 flipping\n\n当我们想要写出buffer的内容时,我们需要用position和limit指针指向时机内容的起始位置。类似这样:\n\nbuffer.limit(buffer.position()).position(0);\n\n\n1\n\n\n这等同于buffer.flip(); 调用flip()后2-4会变成2-5 说白了,flip()改变position和limit两个指针。 rewind()只改变position指针,等同于:\n\nbuffer.position(0);\n\n\n1\n\n\n\n# 2.1.6 draining\n\nhasremaining()可以判断是否到达limit指针 下面是一个实现,将buffer内容读到一个数组中\n\nfor (int i = 0; buffer.hasremaining(), i++) {\n mybytearray [i] = buffer.get();\n}\n\n\n1\n2\n3\n\n\nremaining()会返回position到limit的数量。 将buffer内容读到一个数组中另一种实现方式:\n\nint count = buffer.remaining();\nfor (int i = 0; i < count, i++) {\n mybytearray [i] = buffer.get();\n}\n\n\n1\n2\n3\n4\n\n\n\n# 2.1.7 compacting\n\n当你drain一部分数据想要继续fill数据的时候,你需要把还未drain的数据整体前移。 buffer.compact();可以完成这个工作。 调用buffer.compact()的效果就是2-6到2-7\n\n\n# 2.1.9 mark\n\nmark()方法会用mark指针指向position的位置。\n\nbuffer.position(2).mark().position(4);\n\n\n1\n\n\nreset()方法会把position指针指向mark的位置。\n\nbuffer.reset();\n\n\n1\n\n\n\n\n\n# 2.1.9 comparing\n\npublic abstract class bytebuffer\n extends buffer implements comparable\n {\n // this is a partial api listing\npublic boolean equals (object ob)\npublic int compareto (object ob)\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nequals()返回true需要满足三个条件:\n\n * buffer包含相同类型的元素\n * 两个buffer的remaining()返回值相同\n * remaining data的序列相同 下面是两个例子:\n\n\n# 2.2 创建buffer\n\n创建新buffer的两种方式:allocation or wrapping\n\npublic abstract class charbuffer\nextends buffer implements charsequence, comparable\n{\n}\n// this is a partial api listing\npublic static charbuffer allocate (int capacity)\npublic static charbuffer wrap (char [] array)\npublic static charbuffer wrap (char [] array, int offset, int length)\npublic final boolean hasarray()\npublic final char [] array()\npublic final int arrayoffset()\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n\n\n通过allocation创建的buffer是分配一块固定大小的堆内存作为buffer存储空间。 wrapping创建的buffer是创建一个buffer对象指向一个array空间。\n\n\n# 2.3 duplicating buffers\n\npublic abstract class charbuffer\nextends buffer implements charsequence, comparable\n {\n}\n// this is a partial api listing\npublic abstract charbuffer duplicate();\npublic abstract charbuffer asreadonlybuffer(); \npublic abstract charbuffer slice();\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n\n\nduplicate()创建一个新的buffer,和原buffer一样。 两个buffer共享数据元素 但是他们有独立的position, limit, and mark 一个buffer更改了数据元素对另一个buffer可见 只读、直接内存这两个属性也可以通过duplicate()继承 举个例子:\n\ncharbuffer buffer = charbuffer.allocate (8); \nbuffer.position (3).limit (6).mark().position (5); \ncharbuffer dupebuffer = buffer.duplicate(); buffer.clear();\n\n\n1\n2\n3\n\n\nasreadonlybuffer()和duplicate()功能类似,只是asreadonlybuffer()返回一个只读的视图。 slice() 返回一个新的buffer视图,不过 new_position = original_position new_capacity = original_limit - original_position 举个例子来看slice():\n\ncharbuffer buffer = charbuffer.allocate (8); \nbuffer.position (3).limit (5);\ncharbuffer slicebuffer = buffer.slice();\n\n\n1\n2\n3\n\n\n\n\n\n# 2.4 byte buffers\n\n\n# 2.4.1 byte ordering\n\n原始数据类型在内存中的存储方式是多个字节的连续序列。 举个例子一个32-bit int 值为 0x037fb4c7的内存存储序列可能为2-14也可能为2-15: 这就是所谓的大端设计和小端设计。 采用大端还是小端通常由硬件的设计者决定而不是小端的设计者。 ip协议定义的字节顺序是大端。所有使用ip协议的多字节数值必须在网络字节序和本地主机字节序做转换。\n\npackage java.nio;\npublic final class byteorder\n{\npublic static final byteorder big_endian public static final byteorder little_endian\npublic static byteorder nativeorder()\npublic string tostring()\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n除了bytebuffer之外,其他buffer的order()适中返回同一个值,byteorder.nativeorder()。\n\n\n# 2.4.2 direct buffers\n\npublic abstract class bytebuffer extends buffer implements comparable {\n// this is a partial api listing\npublic static bytebuffer allocate (int capacity) ;\npublic static bytebuffer allocatedirect (int capacity) ;\npublic abstract boolean isdirect();\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\nallocatedirect可以直接在堆外开辟一个buffer。 堆外buffer节省了用户空间和系统空间的buffer拷贝,提升效率。\n\n\n# 2.4.3 view buffers\n\n收到数据的时候你可能要先看一下数据再决定做什么操作,这就需要用到bytebuffer的视图api。\n\npublic abstract class bytebuffer extends buffer implements comparable {\n// this is a partial api listing\npublic abstract charbuffer ascharbuffer(); \npublic abstract shortbuffer asshortbuffer(); \npublic abstract intbuffer asintbuffer();\n public abstract longbuffer aslongbuffer();\n public abstract floatbuffer asfloatbuffer();\n public abstract doublebuffer asdoublebuffer();\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\n\n视图在直接内存buffer上操作更高效。 当本地硬件的字节顺序和buffer中的字节顺序一致时,通过低级代码就可以操作buffer中的数据,而不需要经过字节码的编码解码。\n\n\n# 2.4.4 data element views\n\n当getint() 被调用时,那么从当前位置开始的4个字节将被打包成一个int返回。 int value = buffer.getint();将返回buffer中的1-4位,具体返回的数据值与字节序有关系。 int value = buffer.order (byteorder.big_endian).getint(); 返回0x3bc5315e。 int value = buffer.order (byteorder.little_endian).getint(); 返回0x5e31c53b。\n\n\n# 第三章 channels\n\n\n# 3.1 basic\n\n在nio接口中我们需要使用channel和buffer进行io操作,channel模拟了流的概念,但是又有不同。数据总是从一个channel读到一个buffer中,或者从一个buffer中写到channel中。 channel接口的主要实现类如下:\n\n * filechannel\n * datagramchannel\n * socketchannel\n * serversocketchannel\n\njavanio channels和流有一些相似,但是又有些不同:\n\n你可以同时读和写channels,流stream只支持单向的读或写(inputstream/outputstream)\n\n * channels可以异步的读和写,流stream是同步的\n * channels总是读取到buffer或者从buffer中写入\n\n下面分别介绍一下channel最重要的一些实现类:\n\n * filechannel : 可以读写文件中的数据\n * datagramchannel:可以通过udp协议读写数据\n * socketchannel:可以通过tcp协议读写数据\n * serversocketchannel:允许我们像一个web服务器那样监听tcp链接请求,为每一个链接请求创建一个socketchannel\n\n\n# 3.1.1 open channel\n\nfilechannel只能通过getchannel()得到。 socketchannel有工厂方法可以得到。\n\nsocketchannel sc = socketchannel.open();\nsc.connect (new inetsocketaddress (\"somehost\", someport));\n\nserversocketchannel ssc = serversocketchannel.open(); ssc.socket().bind (new inetsocketaddress (somelocalport));\ndatagramchannel dc = datagramchannel.open();\n\nrandomaccessfile raf = new randomaccessfile (\"somefile\", \"r\"); filechannel fc = raf.getchannel();\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 3.1.2 using channels\n\npublic interface readablebytechannel extends channel {\npublic int read (bytebuffer dst) throws ioexception;\n}\n\npublic interface writablebytechannel extends channel {\npublic int write (bytebuffer src) throws ioexception;\n }\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\nchannels 可以是单向或者双向的。 如果一个class实现了上面两个接口,就是双向的。 channels可以在阻塞模式和非阻塞模式工作。 只有socket和pipes可以工作在非阻塞模式。\n\n\n# 3.1.3 close channels\n\n\n# 3.2 scatter/gather\n\nscatter:将多个buffer按照顺序合成一个 gather:将一个字符串按照顺序拆分成多个。逐个填满buffer。\n\n\n# 3.3 file channels\n\nfile channels 只能以阻塞模式工作。\n\n\n# 3.3.1 accessing files\n\npublic abstract class filechannel extends abstractchannel \nimplements bytechannel, gatheringbytechannel, scatteringbytechannel{\n// this is a partial api listing\n//获取position位置\npublic abstract long position()\n//设置position\npublic abstract void position (long newposition)\n//从position位置开始读\npublic abstract int read (bytebuffer dst)\n//从position位置开始读\npublic abstract int read (bytebuffer dst, long position)\n//从position位置开始写\n public abstract int write (bytebuffer src)\n//从position位置开始写\npublic abstract int write (bytebuffer src, long position)\npublic abstract long size()\n//截取文件保留size大小,并移动position到size的位置\npublic abstract void truncate (long size) \n//强制刷新缓存到磁盘文件\npublic abstract void force (boolean metadata)\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n\n\n\n# 3.3.2 file locking\n\n\n# 3.4 memory-mapped files\n\n\n# 3.5 socket channels\n\nthe new socket channels can operate in nonblocking mode and are selectable. these two capabilities enable tremendous scalability and flexibility in large applications. serversocketchannel没有实现read和write接口,说明它不需要转发数据。它只负责家庭socket连接,创建socketchannel。\n\na channel is a conduit to an i/o service and provides methods for interacting with that service\n\n每个socket channels都有一个socket与之对应,调用socket()可以获得。 但并不是所有的sockets都有channels。socket的getchannel()可能但会null;\n\n\n# 3.5.1 nonblocking mode\n\n调用configureblocking (false);设置channel为非阻塞模式。 但是只有持有blockinglock() 返回的对象锁的线程才能修改阻塞模式。\n\nsocket socket = null;\nobject lockobj = serverchannel.blockinglock();\n// have a handle to the lock object, but haven't locked it yet\n// may block here until lock is acquired\nsynchronize (lockobj){\n// this thread now owns the lock; mode can't be changed boolean prevstate = serverchannel.isblocking();\nserverchannel.configureblocking (false); socket = serverchannel.accept(); serverchannel.configureblocking (prevstate);\n}\n// lock is now released, mode is allowed to change\nif (socket != null) {\ndosomethingwiththesocket (socket);\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n\n\n\n# 3.5.2 serversocketchannel\n\nthe serversocketchannel class is a channel-based socket listener. 它负责处理socket任务并添加channel。\n\npublic abstract class socketchannel\n extends abstractselectablechannel\n implements bytechannel, scatteringbytechannel, gatheringbytechannel, networkchannel\n{\n//socketchannel被创建但是并没有连接,open()只是创建一个socketchannel,还需要调用connect()连接到一个地址\npublic static socketchannel open() throws ioexception\n//创建并连接\npublic static socketchannel open(socketaddress remote) throws ioexception\n//支持的操作(读、写、连接)\npublic final int validops()\n、、\npublic abstract socket socket();\n//是否连接到远程\npublic abstract boolean isconnected();\n//connect()方法是耗时的,因为需要数据包对话(tcp握手)。如果socketchannel身上有并发、connect(),isconnectionpending返回true\npublic abstract boolean isconnectionpending();\n//建立连接\npublic abstract boolean connect(socketaddress remote) throws ioexception;\n//任何时候都可以安全的调用\npublic abstract boolean finishconnect() throws ioexception;\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n\n\nsocket channels是线程安全的。 socket是面向流的而不是面向数据包的,它能保证到达的顺序是发送的顺序,不能保证维护分组。发送方发送20个字节,接收方read可能直接受到7个字节,其余字节在传输中。所以,多个线程共享一个socket的一端不是好的设计。\n\n\n# 3.5.4 datagramchannel\n\nsocketchannel为面向连接的流协议(如tcp/ip) datagramchannel为面向无连接的数据包协议(如udp/ip)\n\n\n# 3.5.5 pipes\n\npipe用于同一个jvm内不同线程之间的数据交换问题 不同jvm之间的数据交换问题应该使用socketchannel pipe类创建了一对channel对象,它们提供了一种回送机制。 pipe实现两个进程之间的单向数据连接。 pipe有一个sinkchannel和一个sourcechannel 数据会从sourcechannel读取、被写到sinkchannel\n\npublic abstract class pipe {\n public static abstract class sourcechannel\n extends abstractselectablechannel\n implements readablebytechannel, scatteringbytechannel\n {\n protected sourcechannel(selectorprovider provider) {\n super(provider);\n }\n public final int validops() {\n return selectionkey.op_read;\n }\n }\n\n public static abstract class sinkchannel\n extends abstractselectablechannel\n implements writablebytechannel, gatheringbytechannel\n {\n protected sinkchannel(selectorprovider provider) {\n super(provider);\n }\n public final int validops() {\n return selectionkey.op_write;\n }\n\n }\n protected pipe() { }\n //从管道读取数据,要访问source通道\n public abstract sourcechannel source();\n //向管道写入数据,要访问sink通道\n public abstract sinkchannel sink();\n //管道创建\n public static pipe open() throws ioexception {\n return selectorprovider.provider().openpipe();\n }\n}\n\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n\n\n\n# 第四章 selectors\n\nselector(选择器)是java nio中能够检测一到多个nio通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,仅用单个线程来处理多个channels的好处是,只需要更少的线程来处理通道。\n\n\n# 4.1 基础\n\n * selector:一个selector可以注册多个channel,一个线程通过管理selector实现管理多个channel。\n * selectablechannel:提供channle selectability的通用方法。\n * selectionkey:封装了channle和selector的注册关系,包含注册的感兴趣的事件。\n\n\n# 4.2 示例\n\n//创建selector\nselector selector = selector.open();\nchannel.configureblocking(false);\n//注册selector\nselectionkey key = channel.register(selector, selectionkey.op_read);\nwhile(true) {\n//单线程阻塞检查selector中就绪的事件\n int readychannels = selector.select();\n if(readychannels == 0) continue;\n set selectedkeys = selector.selectedkeys();\n iterator keyiterator = selectedkeys.iterator();\n //循环处理就绪的事件\n while(keyiterator.hasnext()) {\n selectionkey key = keyiterator.next();\n if(key.isacceptable()) {\n // a connection was accepted by a serversocketchannel.\n } else if (key.isconnectable()) {\n // a connection was established with a remote server.\n } else if (key.isreadable()) {\n // a channel is ready for reading\n } else if (key.iswritable()) {\n // a channel is ready for writing\n }\n keyiterator.remove();\n }\n}\n\n\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n", "charsets": { "cjk": true }, @@ -7774,11 +8654,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "生产者", + "title": "Kafka", "frontmatter": { - "title": "生产者", - "date": "2022-05-21T23:43:06.000Z", - "permalink": "/pages/fe7ca2/", + "title": "Kafka", + "date": "2020-05-21T21:50:26.000Z", + "permalink": "/pages/561f34/", "categories": [ "中间件", "Kafka" @@ -7787,13 +8667,13 @@ export const siteData = { null ] }, - "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/40.Kafka/010.%E7%94%9F%E4%BA%A7%E8%80%85.html", - "relativePath": "04.中间件/40.Kafka/010.生产者.md", - "key": "v-7c0136cf", - "path": "/pages/fe7ca2/", + "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/40.Kafka/001.Kafka%E5%8C%85%E5%90%AB%E9%82%A3%E5%87%A0%E9%83%A8%E5%88%86.html", + "relativePath": "04.中间件/40.Kafka/001.Kafka包含那几部分.md", + "key": "v-0df39df8", + "path": "/pages/561f34/", "headersStr": null, - "content": "# 创建生产者\n\n创建生产者有三个属性是必选的 bootstrap.servers 指定broker集群的地址,格式为host:port key.serializer kafka broker希望收到消息的键和值都是字节数组,但是为了代码的可读性,允许producer把java对象发送给broker,但是需要指定一个实现了org.apache.kafka.common.serialization.Serializer接口的序列化器。 value.serializer 与key.serializer一样的道理。\n\n\t\tProperties props = new Properties();\n props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, \"172.16.1.1,172.16.1.2\");\n props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, \"org.apache.kafka.common.serialization.StringSerializer\");\n props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, \"org.apache.kafka.common.serialization.StringSerializer\");\n producer = new KafkaProducer<>(props);\n\n\n1\n2\n3\n4\n5\n\n\n\n# 发送消息\n\n发送消息有三种方式:\n\n * 发送并忘记(fire-and-forget)\n * 同步发送\n * 异步发送\n\n发送并忘记 不关心消息是否到达\n\nProducerRecord record = new ProducerRecord<>(topicName, key,value);\nproducer.send(record);\n\n\n1\n2\n\n\n同步发送\n\nProducerRecord record = new ProducerRecord<>(topicName, key,value);\ntry{\n\tproducer.send(record).get();//producer.send(record)返回一个Future对象,调用get()阻塞等待返回结果\n} catch (Exception e){\n\te.printStackTrace();\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\nkafkaProducer会发送两类错误\n\n * 可重试的:比如连接错误、无主(no leader)错误\n * 不可重试的:比如消息太大 异步发送 异步发送的异常需要记录下拉,producer支持回调。\n\nProducerRecord record = new ProducerRecord<>(topicNameTrade, key,value);\n producer.send(record, new Callback() {\n @Override\n public void onCompletion(RecordMetadata metadata, Exception exception) {\n exception.printStackTrace();\n }\n });\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 生产者的配置\n\nacks 指定要多少个分区副本收到消息,生产者才认为消息写入是成功的。其值有0、1、all.\n\n * 0:不等待任何服务器的相应\n * 1: 只要集群的首领节点收到\n * all: 等待所有副本都收到\n\nbuffer.memory 设置生产者内存缓冲区大小。 程序发送消息的速度 > 发送到broker的速度会导致这个缓冲区空间不足。 空间不足时程序阻塞还是抛异常取决于配置max.block.ms compression.type 默认,消息是不会被压缩的。 producer向broker发送消息不是逐条发送的,是一批一批发送的。 发送批次有可选的压缩方式,有三种:\n\n * snappy\n * gzip\n * lz4\n\nretries 重试次数 默认重试之间会等待100ms retry.backoff.ms可设置这个等待时间 batch.size 有多个消息要发送到一个分区的时候,producer会把他们放到一个批次。 该配置指定是批次大小(字节)。 批次装满的时候会被发送出去,\n\n\n# 序列化器\n\n\n# 分区\n\nkafka消息的recode包含topic、key、value。 broker用key分配分区。key相同的一定会分配到同一个分区。 如果key为null,那么将使用默认分区,默认的分区器是使用Round Robin算法。 也可以实现partitioner实现自定义分区器。", - "normalizedContent": "# 创建生产者\n\n创建生产者有三个属性是必选的 bootstrap.servers 指定broker集群的地址,格式为host:port key.serializer kafka broker希望收到消息的键和值都是字节数组,但是为了代码的可读性,允许producer把java对象发送给broker,但是需要指定一个实现了org.apache.kafka.common.serialization.serializer接口的序列化器。 value.serializer 与key.serializer一样的道理。\n\n\t\tproperties props = new properties();\n props.put(producerconfig.bootstrap_servers_config, \"172.16.1.1,172.16.1.2\");\n props.put(producerconfig.key_serializer_class_config, \"org.apache.kafka.common.serialization.stringserializer\");\n props.put(producerconfig.value_serializer_class_config, \"org.apache.kafka.common.serialization.stringserializer\");\n producer = new kafkaproducer<>(props);\n\n\n1\n2\n3\n4\n5\n\n\n\n# 发送消息\n\n发送消息有三种方式:\n\n * 发送并忘记(fire-and-forget)\n * 同步发送\n * 异步发送\n\n发送并忘记 不关心消息是否到达\n\nproducerrecord record = new producerrecord<>(topicname, key,value);\nproducer.send(record);\n\n\n1\n2\n\n\n同步发送\n\nproducerrecord record = new producerrecord<>(topicname, key,value);\ntry{\n\tproducer.send(record).get();//producer.send(record)返回一个future对象,调用get()阻塞等待返回结果\n} catch (exception e){\n\te.printstacktrace();\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\nkafkaproducer会发送两类错误\n\n * 可重试的:比如连接错误、无主(no leader)错误\n * 不可重试的:比如消息太大 异步发送 异步发送的异常需要记录下拉,producer支持回调。\n\nproducerrecord record = new producerrecord<>(topicnametrade, key,value);\n producer.send(record, new callback() {\n @override\n public void oncompletion(recordmetadata metadata, exception exception) {\n exception.printstacktrace();\n }\n });\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 生产者的配置\n\nacks 指定要多少个分区副本收到消息,生产者才认为消息写入是成功的。其值有0、1、all.\n\n * 0:不等待任何服务器的相应\n * 1: 只要集群的首领节点收到\n * all: 等待所有副本都收到\n\nbuffer.memory 设置生产者内存缓冲区大小。 程序发送消息的速度 > 发送到broker的速度会导致这个缓冲区空间不足。 空间不足时程序阻塞还是抛异常取决于配置max.block.ms compression.type 默认,消息是不会被压缩的。 producer向broker发送消息不是逐条发送的,是一批一批发送的。 发送批次有可选的压缩方式,有三种:\n\n * snappy\n * gzip\n * lz4\n\nretries 重试次数 默认重试之间会等待100ms retry.backoff.ms可设置这个等待时间 batch.size 有多个消息要发送到一个分区的时候,producer会把他们放到一个批次。 该配置指定是批次大小(字节)。 批次装满的时候会被发送出去,\n\n\n# 序列化器\n\n\n# 分区\n\nkafka消息的recode包含topic、key、value。 broker用key分配分区。key相同的一定会分配到同一个分区。 如果key为null,那么将使用默认分区,默认的分区器是使用round robin算法。 也可以实现partitioner实现自定义分区器。", + "content": "# 主题和分区\n\nkafka通过主题对消息分类。 kafka通过分区实现数据冗余和伸缩性。 一个主题包含多个分区,所以在topic内消息可能是无需的,但是在单个partition内部是有序的。\n\n\n# 生产者和消费者\n\n生产者创建消息,发布到一个特定的主题上,消息会均衡在所有分区上。 消费者订阅一个或者多个主题,按照生产顺序消费消息。 偏移量是一种元数据,是一个不断递增的整数值,消费者通过偏移量来区分已经消费的消息。每个分区有唯一的偏移量,消费者把最后消费的消息的偏移量发送到broker或者zookeeper保存。 消费者群组多个消费者共同读取一个主题,有唯一的groupId,群组保证,每个分区只能被一个消费者使用。\n\n\n# broker和集群\n\nbroker是一个独立的kafka 服务器。负责接收生产者的消息,为消息设置偏移量,提交消息到磁盘保存。是集群的组成部分。 集群控制器是集群中的一个broker。负责分区分配和broker监控。 分区首领。一个分区会分配给多个broker,这种复制机制是为了提供消息冗余。在这多个broker中需要有一个分区首领,消费者只消费分区首领的消息,分区首领负责把消费的offset同步到冗余分区中。", + "normalizedContent": "# 主题和分区\n\nkafka通过主题对消息分类。 kafka通过分区实现数据冗余和伸缩性。 一个主题包含多个分区,所以在topic内消息可能是无需的,但是在单个partition内部是有序的。\n\n\n# 生产者和消费者\n\n生产者创建消息,发布到一个特定的主题上,消息会均衡在所有分区上。 消费者订阅一个或者多个主题,按照生产顺序消费消息。 偏移量是一种元数据,是一个不断递增的整数值,消费者通过偏移量来区分已经消费的消息。每个分区有唯一的偏移量,消费者把最后消费的消息的偏移量发送到broker或者zookeeper保存。 消费者群组多个消费者共同读取一个主题,有唯一的groupid,群组保证,每个分区只能被一个消费者使用。\n\n\n# broker和集群\n\nbroker是一个独立的kafka 服务器。负责接收生产者的消息,为消息设置偏移量,提交消息到磁盘保存。是集群的组成部分。 集群控制器是集群中的一个broker。负责分区分配和broker监控。 分区首领。一个分区会分配给多个broker,这种复制机制是为了提供消息冗余。在这多个broker中需要有一个分区首领,消费者只消费分区首领的消息,分区首领负责把消费的offset同步到冗余分区中。", "charsets": { "cjk": true }, @@ -7844,11 +8724,11 @@ export const siteData = { "lastUpdatedTimestamp": 1653148861000 }, { - "title": "如何保证Kafka的可靠性", + "title": "生产者", "frontmatter": { - "title": "如何保证Kafka的可靠性", - "date": "2022-05-21T23:47:03.000Z", - "permalink": "/pages/76a94c/", + "title": "生产者", + "date": "2022-05-21T23:43:06.000Z", + "permalink": "/pages/fe7ca2/", "categories": [ "中间件", "Kafka" @@ -7857,13 +8737,13 @@ export const siteData = { null ] }, - "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/40.Kafka/040.%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81Kafka%E7%9A%84%E5%8F%AF%E9%9D%A0%E6%80%A7.html", - "relativePath": "04.中间件/40.Kafka/040.如何保证Kafka的可靠性.md", - "key": "v-684c4815", - "path": "/pages/76a94c/", + "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/40.Kafka/010.%E7%94%9F%E4%BA%A7%E8%80%85.html", + "relativePath": "04.中间件/40.Kafka/010.生产者.md", + "key": "v-7c0136cf", + "path": "/pages/fe7ca2/", "headersStr": null, - "content": "# 可靠性保证\n\nkafka在哪些方面做出保证呢?\n\n * 单生产者,kafka可以保证顺序消费\n * 只有当写入的消息被写入所有的副本时,才认为是“已提交”的\n * 只要有一个活跃的分区副本,那么,已提交的数据就不会丢失。\n * 消费者只能读取已经提交的消息\n\n如何保证topic元数据信息安全 replication.factor配置topic的复制系数\n\n所有的分区副本都没有正常同步,分区首领又挂掉了,如何做选举? 方案有两个\n\n * 在未正常同步的副中选举一个作为首领,缺点是存在丢失数据的风险。\n * 等待旧的首领恢复。缺点是可用性低。\n\n配置unclean.leader.election.enable=true表示允许不同步的副本称为首领,这也将面临丢失消息的风险。\n\n\n# producer如何做可靠性保证\n\n如何保证可靠投递消息?\n\n 1. ack=all表示broker要等待所有分区副本同步完消息才返回给producer response,是最可靠的投递。\n 2. 设置重试次数,注意要做业务幂等。\n 3. 有些小时不可重试错误,比如消息太大、认证错误等。根据业务来特殊处理这类消息。\n\n\n# consumer如何做可靠性保证\n\nconsumer请求的offset在broker上不存在时怎么处理? 配置auto.offset.reset指定了两种策略:\n\n * earliest:从分区的开始读取\n * latest:从分区的末尾读取", - "normalizedContent": "# 可靠性保证\n\nkafka在哪些方面做出保证呢?\n\n * 单生产者,kafka可以保证顺序消费\n * 只有当写入的消息被写入所有的副本时,才认为是“已提交”的\n * 只要有一个活跃的分区副本,那么,已提交的数据就不会丢失。\n * 消费者只能读取已经提交的消息\n\n如何保证topic元数据信息安全 replication.factor配置topic的复制系数\n\n所有的分区副本都没有正常同步,分区首领又挂掉了,如何做选举? 方案有两个\n\n * 在未正常同步的副中选举一个作为首领,缺点是存在丢失数据的风险。\n * 等待旧的首领恢复。缺点是可用性低。\n\n配置unclean.leader.election.enable=true表示允许不同步的副本称为首领,这也将面临丢失消息的风险。\n\n\n# producer如何做可靠性保证\n\n如何保证可靠投递消息?\n\n 1. ack=all表示broker要等待所有分区副本同步完消息才返回给producer response,是最可靠的投递。\n 2. 设置重试次数,注意要做业务幂等。\n 3. 有些小时不可重试错误,比如消息太大、认证错误等。根据业务来特殊处理这类消息。\n\n\n# consumer如何做可靠性保证\n\nconsumer请求的offset在broker上不存在时怎么处理? 配置auto.offset.reset指定了两种策略:\n\n * earliest:从分区的开始读取\n * latest:从分区的末尾读取", + "content": "# 创建生产者\n\n创建生产者有三个属性是必选的 bootstrap.servers 指定broker集群的地址,格式为host:port key.serializer kafka broker希望收到消息的键和值都是字节数组,但是为了代码的可读性,允许producer把java对象发送给broker,但是需要指定一个实现了org.apache.kafka.common.serialization.Serializer接口的序列化器。 value.serializer 与key.serializer一样的道理。\n\n\t\tProperties props = new Properties();\n props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, \"172.16.1.1,172.16.1.2\");\n props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, \"org.apache.kafka.common.serialization.StringSerializer\");\n props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, \"org.apache.kafka.common.serialization.StringSerializer\");\n producer = new KafkaProducer<>(props);\n\n\n1\n2\n3\n4\n5\n\n\n\n# 发送消息\n\n发送消息有三种方式:\n\n * 发送并忘记(fire-and-forget)\n * 同步发送\n * 异步发送\n\n发送并忘记 不关心消息是否到达\n\nProducerRecord record = new ProducerRecord<>(topicName, key,value);\nproducer.send(record);\n\n\n1\n2\n\n\n同步发送\n\nProducerRecord record = new ProducerRecord<>(topicName, key,value);\ntry{\n\tproducer.send(record).get();//producer.send(record)返回一个Future对象,调用get()阻塞等待返回结果\n} catch (Exception e){\n\te.printStackTrace();\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\nkafkaProducer会发送两类错误\n\n * 可重试的:比如连接错误、无主(no leader)错误\n * 不可重试的:比如消息太大 异步发送 异步发送的异常需要记录下拉,producer支持回调。\n\nProducerRecord record = new ProducerRecord<>(topicNameTrade, key,value);\n producer.send(record, new Callback() {\n @Override\n public void onCompletion(RecordMetadata metadata, Exception exception) {\n exception.printStackTrace();\n }\n });\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 生产者的配置\n\nacks 指定要多少个分区副本收到消息,生产者才认为消息写入是成功的。其值有0、1、all.\n\n * 0:不等待任何服务器的相应\n * 1: 只要集群的首领节点收到\n * all: 等待所有副本都收到\n\nbuffer.memory 设置生产者内存缓冲区大小。 程序发送消息的速度 > 发送到broker的速度会导致这个缓冲区空间不足。 空间不足时程序阻塞还是抛异常取决于配置max.block.ms compression.type 默认,消息是不会被压缩的。 producer向broker发送消息不是逐条发送的,是一批一批发送的。 发送批次有可选的压缩方式,有三种:\n\n * snappy\n * gzip\n * lz4\n\nretries 重试次数 默认重试之间会等待100ms retry.backoff.ms可设置这个等待时间 batch.size 有多个消息要发送到一个分区的时候,producer会把他们放到一个批次。 该配置指定是批次大小(字节)。 批次装满的时候会被发送出去,\n\n\n# 序列化器\n\n\n# 分区\n\nkafka消息的recode包含topic、key、value。 broker用key分配分区。key相同的一定会分配到同一个分区。 如果key为null,那么将使用默认分区,默认的分区器是使用Round Robin算法。 也可以实现partitioner实现自定义分区器。", + "normalizedContent": "# 创建生产者\n\n创建生产者有三个属性是必选的 bootstrap.servers 指定broker集群的地址,格式为host:port key.serializer kafka broker希望收到消息的键和值都是字节数组,但是为了代码的可读性,允许producer把java对象发送给broker,但是需要指定一个实现了org.apache.kafka.common.serialization.serializer接口的序列化器。 value.serializer 与key.serializer一样的道理。\n\n\t\tproperties props = new properties();\n props.put(producerconfig.bootstrap_servers_config, \"172.16.1.1,172.16.1.2\");\n props.put(producerconfig.key_serializer_class_config, \"org.apache.kafka.common.serialization.stringserializer\");\n props.put(producerconfig.value_serializer_class_config, \"org.apache.kafka.common.serialization.stringserializer\");\n producer = new kafkaproducer<>(props);\n\n\n1\n2\n3\n4\n5\n\n\n\n# 发送消息\n\n发送消息有三种方式:\n\n * 发送并忘记(fire-and-forget)\n * 同步发送\n * 异步发送\n\n发送并忘记 不关心消息是否到达\n\nproducerrecord record = new producerrecord<>(topicname, key,value);\nproducer.send(record);\n\n\n1\n2\n\n\n同步发送\n\nproducerrecord record = new producerrecord<>(topicname, key,value);\ntry{\n\tproducer.send(record).get();//producer.send(record)返回一个future对象,调用get()阻塞等待返回结果\n} catch (exception e){\n\te.printstacktrace();\n}\n\n\n1\n2\n3\n4\n5\n6\n\n\nkafkaproducer会发送两类错误\n\n * 可重试的:比如连接错误、无主(no leader)错误\n * 不可重试的:比如消息太大 异步发送 异步发送的异常需要记录下拉,producer支持回调。\n\nproducerrecord record = new producerrecord<>(topicnametrade, key,value);\n producer.send(record, new callback() {\n @override\n public void oncompletion(recordmetadata metadata, exception exception) {\n exception.printstacktrace();\n }\n });\n\n\n1\n2\n3\n4\n5\n6\n7\n\n\n\n# 生产者的配置\n\nacks 指定要多少个分区副本收到消息,生产者才认为消息写入是成功的。其值有0、1、all.\n\n * 0:不等待任何服务器的相应\n * 1: 只要集群的首领节点收到\n * all: 等待所有副本都收到\n\nbuffer.memory 设置生产者内存缓冲区大小。 程序发送消息的速度 > 发送到broker的速度会导致这个缓冲区空间不足。 空间不足时程序阻塞还是抛异常取决于配置max.block.ms compression.type 默认,消息是不会被压缩的。 producer向broker发送消息不是逐条发送的,是一批一批发送的。 发送批次有可选的压缩方式,有三种:\n\n * snappy\n * gzip\n * lz4\n\nretries 重试次数 默认重试之间会等待100ms retry.backoff.ms可设置这个等待时间 batch.size 有多个消息要发送到一个分区的时候,producer会把他们放到一个批次。 该配置指定是批次大小(字节)。 批次装满的时候会被发送出去,\n\n\n# 序列化器\n\n\n# 分区\n\nkafka消息的recode包含topic、key、value。 broker用key分配分区。key相同的一定会分配到同一个分区。 如果key为null,那么将使用默认分区,默认的分区器是使用round robin算法。 也可以实现partitioner实现自定义分区器。", "charsets": { "cjk": true }, @@ -7897,33 +8777,6 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, - { - "title": "Kafka", - "frontmatter": { - "title": "Kafka", - "date": "2020-05-21T21:50:26.000Z", - "permalink": "/pages/561f34/", - "categories": [ - "中间件", - "Kafka" - ], - "tags": [ - null - ] - }, - "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/40.Kafka/001.Kafka%E5%8C%85%E5%90%AB%E9%82%A3%E5%87%A0%E9%83%A8%E5%88%86.html", - "relativePath": "04.中间件/40.Kafka/001.Kafka包含那几部分.md", - "key": "v-0df39df8", - "path": "/pages/561f34/", - "headersStr": null, - "content": "# 主题和分区\n\nkafka通过主题对消息分类。 kafka通过分区实现数据冗余和伸缩性。 一个主题包含多个分区,所以在topic内消息可能是无需的,但是在单个partition内部是有序的。\n\n\n# 生产者和消费者\n\n生产者创建消息,发布到一个特定的主题上,消息会均衡在所有分区上。 消费者订阅一个或者多个主题,按照生产顺序消费消息。 偏移量是一种元数据,是一个不断递增的整数值,消费者通过偏移量来区分已经消费的消息。每个分区有唯一的偏移量,消费者把最后消费的消息的偏移量发送到broker或者zookeeper保存。 消费者群组多个消费者共同读取一个主题,有唯一的groupId,群组保证,每个分区只能被一个消费者使用。\n\n\n# broker和集群\n\nbroker是一个独立的kafka 服务器。负责接收生产者的消息,为消息设置偏移量,提交消息到磁盘保存。是集群的组成部分。 集群控制器是集群中的一个broker。负责分区分配和broker监控。 分区首领。一个分区会分配给多个broker,这种复制机制是为了提供消息冗余。在这多个broker中需要有一个分区首领,消费者只消费分区首领的消息,分区首领负责把消费的offset同步到冗余分区中。", - "normalizedContent": "# 主题和分区\n\nkafka通过主题对消息分类。 kafka通过分区实现数据冗余和伸缩性。 一个主题包含多个分区,所以在topic内消息可能是无需的,但是在单个partition内部是有序的。\n\n\n# 生产者和消费者\n\n生产者创建消息,发布到一个特定的主题上,消息会均衡在所有分区上。 消费者订阅一个或者多个主题,按照生产顺序消费消息。 偏移量是一种元数据,是一个不断递增的整数值,消费者通过偏移量来区分已经消费的消息。每个分区有唯一的偏移量,消费者把最后消费的消息的偏移量发送到broker或者zookeeper保存。 消费者群组多个消费者共同读取一个主题,有唯一的groupid,群组保证,每个分区只能被一个消费者使用。\n\n\n# broker和集群\n\nbroker是一个独立的kafka 服务器。负责接收生产者的消息,为消息设置偏移量,提交消息到磁盘保存。是集群的组成部分。 集群控制器是集群中的一个broker。负责分区分配和broker监控。 分区首领。一个分区会分配给多个broker,这种复制机制是为了提供消息冗余。在这多个broker中需要有一个分区首领,消费者只消费分区首领的消息,分区首领负责把消费的offset同步到冗余分区中。", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 - }, { "title": "系统设计", "frontmatter": { @@ -8078,6 +8931,33 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, + { + "title": "如何保证Kafka的可靠性", + "frontmatter": { + "title": "如何保证Kafka的可靠性", + "date": "2022-05-21T23:47:03.000Z", + "permalink": "/pages/76a94c/", + "categories": [ + "中间件", + "Kafka" + ], + "tags": [ + null + ] + }, + "regularPath": "/04.%E4%B8%AD%E9%97%B4%E4%BB%B6/40.Kafka/040.%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81Kafka%E7%9A%84%E5%8F%AF%E9%9D%A0%E6%80%A7.html", + "relativePath": "04.中间件/40.Kafka/040.如何保证Kafka的可靠性.md", + "key": "v-684c4815", + "path": "/pages/76a94c/", + "headersStr": null, + "content": "# 可靠性保证\n\nkafka在哪些方面做出保证呢?\n\n * 单生产者,kafka可以保证顺序消费\n * 只有当写入的消息被写入所有的副本时,才认为是“已提交”的\n * 只要有一个活跃的分区副本,那么,已提交的数据就不会丢失。\n * 消费者只能读取已经提交的消息\n\n如何保证topic元数据信息安全 replication.factor配置topic的复制系数\n\n所有的分区副本都没有正常同步,分区首领又挂掉了,如何做选举? 方案有两个\n\n * 在未正常同步的副中选举一个作为首领,缺点是存在丢失数据的风险。\n * 等待旧的首领恢复。缺点是可用性低。\n\n配置unclean.leader.election.enable=true表示允许不同步的副本称为首领,这也将面临丢失消息的风险。\n\n\n# producer如何做可靠性保证\n\n如何保证可靠投递消息?\n\n 1. ack=all表示broker要等待所有分区副本同步完消息才返回给producer response,是最可靠的投递。\n 2. 设置重试次数,注意要做业务幂等。\n 3. 有些小时不可重试错误,比如消息太大、认证错误等。根据业务来特殊处理这类消息。\n\n\n# consumer如何做可靠性保证\n\nconsumer请求的offset在broker上不存在时怎么处理? 配置auto.offset.reset指定了两种策略:\n\n * earliest:从分区的开始读取\n * latest:从分区的末尾读取", + "normalizedContent": "# 可靠性保证\n\nkafka在哪些方面做出保证呢?\n\n * 单生产者,kafka可以保证顺序消费\n * 只有当写入的消息被写入所有的副本时,才认为是“已提交”的\n * 只要有一个活跃的分区副本,那么,已提交的数据就不会丢失。\n * 消费者只能读取已经提交的消息\n\n如何保证topic元数据信息安全 replication.factor配置topic的复制系数\n\n所有的分区副本都没有正常同步,分区首领又挂掉了,如何做选举? 方案有两个\n\n * 在未正常同步的副中选举一个作为首领,缺点是存在丢失数据的风险。\n * 等待旧的首领恢复。缺点是可用性低。\n\n配置unclean.leader.election.enable=true表示允许不同步的副本称为首领,这也将面临丢失消息的风险。\n\n\n# producer如何做可靠性保证\n\n如何保证可靠投递消息?\n\n 1. ack=all表示broker要等待所有分区副本同步完消息才返回给producer response,是最可靠的投递。\n 2. 设置重试次数,注意要做业务幂等。\n 3. 有些小时不可重试错误,比如消息太大、认证错误等。根据业务来特殊处理这类消息。\n\n\n# consumer如何做可靠性保证\n\nconsumer请求的offset在broker上不存在时怎么处理? 配置auto.offset.reset指定了两种策略:\n\n * earliest:从分区的开始读取\n * latest:从分区的末尾读取", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, { "title": "什么是 CAP ?什么是最终一致性?什么是幂等操作?", "frontmatter": { @@ -8155,6 +9035,32 @@ export const siteData = { "lastUpdated": "2022/05/22, 00:01:01", "lastUpdatedTimestamp": 1653148861000 }, + { + "title": "简述什么是两阶段提交?", + "frontmatter": { + "title": "简述什么是两阶段提交?", + "date": "2022-05-21T17:12:23.000Z", + "permalink": "/pages/4eae5b/", + "categories": [ + "系统设计" + ], + "tags": [ + null + ] + }, + "regularPath": "/08.%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/020.%E7%AE%80%E8%BF%B0%E4%BB%80%E4%B9%88%E6%98%AF%E4%B8%A4%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4%EF%BC%9F.html", + "relativePath": "08.系统设计/020.简述什么是两阶段提交?.md", + "key": "v-f88544f4", + "path": "/pages/4eae5b/", + "headersStr": null, + "content": "TCC/Try Confirm Cancel模式TCC模式只有在所有的服务的第一阶段(try)都成功的时候才进行第二阶段确认(Confirm)操作,否则进行补偿(Cancel)操作,而在try阶段是不会进行真正的业务处理的。\n\n\nTCC模式TCC模式的具体流程为两个阶段:Try,业务服务完成所有的业务检查,预留必需的业务资源如果Try在所有服务中都成功,那么执行Confirm操作,Confirm操作不做任何的业务检查(因为try中已经做过),只是用Try阶段预留的业务资源进行业务处理;否则进行Cancel操作,Cancel操作释放Try阶段预留的业务资源。\n\n\n这么说可能比较模糊,下面我举一个具体的例子,小明在线从招商银行转账100元到广发银行。\n\n\n这个操作可看作两个服务,服务a从小明的招行账户转出100元,服务b从小明的广发银行帐户汇入100元。\n\n\n服务a(小明从招行转出100元):try: update cmb_account set balance=balance-100, freeze=freeze+100 where\nacc_id=1 and balance>100;\n\nconfirm: update cmb_account set freeze=freeze-100 where acc_id=1;\n\ncancel: update cmb_account set balance=balance+100, freeze=freeze-100 where\nacc_id=1;\n\n\n服务b(小明往广发银行汇入100元):try: update cgb_account set freeze=freeze+100 where acc_id=1;\n\nconfirm: update cgb_account set balance=balance+100, freeze=freeze-100 where acc_id=1;\n\ncancel: update cgb_account set freeze=freeze-100 where acc_id=1;\n\n\n具体说明:a的try阶段,服务做了两件事,1:业务检查,这里是检查小明的帐户里的钱是否多余100元;2:预留资源,将100元从余额中划入冻结资金。\na的confirm阶段,这里不再进行业务检查,因为try阶段已经做过了,同时由于转账已经成功,将冻结资金扣除。a的cancel阶段,释放预留资源,既100元冻结资金,并恢复到余额。\n\n\nb的try阶段进行,预留资源,将100元冻结。b的confirm阶段,使用try阶段预留的资源,将100元冻结资金划入余额。\nb的cancel阶段,释放try阶段的预留资源,将100元从冻结资金中减去。从上面的简单例子可以看出,TCC模式比纯业务补偿模式更加复杂,所以在实现上每个服务都需要实现Cofirm和Cancel两个接口。", + "normalizedContent": "tcc/try confirm cancel模式tcc模式只有在所有的服务的第一阶段(try)都成功的时候才进行第二阶段确认(confirm)操作,否则进行补偿(cancel)操作,而在try阶段是不会进行真正的业务处理的。\n\n\ntcc模式tcc模式的具体流程为两个阶段:try,业务服务完成所有的业务检查,预留必需的业务资源如果try在所有服务中都成功,那么执行confirm操作,confirm操作不做任何的业务检查(因为try中已经做过),只是用try阶段预留的业务资源进行业务处理;否则进行cancel操作,cancel操作释放try阶段预留的业务资源。\n\n\n这么说可能比较模糊,下面我举一个具体的例子,小明在线从招商银行转账100元到广发银行。\n\n\n这个操作可看作两个服务,服务a从小明的招行账户转出100元,服务b从小明的广发银行帐户汇入100元。\n\n\n服务a(小明从招行转出100元):try: update cmb_account set balance=balance-100, freeze=freeze+100 where\nacc_id=1 and balance>100;\n\nconfirm: update cmb_account set freeze=freeze-100 where acc_id=1;\n\ncancel: update cmb_account set balance=balance+100, freeze=freeze-100 where\nacc_id=1;\n\n\n服务b(小明往广发银行汇入100元):try: update cgb_account set freeze=freeze+100 where acc_id=1;\n\nconfirm: update cgb_account set balance=balance+100, freeze=freeze-100 where acc_id=1;\n\ncancel: update cgb_account set freeze=freeze-100 where acc_id=1;\n\n\n具体说明:a的try阶段,服务做了两件事,1:业务检查,这里是检查小明的帐户里的钱是否多余100元;2:预留资源,将100元从余额中划入冻结资金。\na的confirm阶段,这里不再进行业务检查,因为try阶段已经做过了,同时由于转账已经成功,将冻结资金扣除。a的cancel阶段,释放预留资源,既100元冻结资金,并恢复到余额。\n\n\nb的try阶段进行,预留资源,将100元冻结。b的confirm阶段,使用try阶段预留的资源,将100元冻结资金划入余额。\nb的cancel阶段,释放try阶段的预留资源,将100元从冻结资金中减去。从上面的简单例子可以看出,tcc模式比纯业务补偿模式更加复杂,所以在实现上每个服务都需要实现cofirm和cancel两个接口。", + "charsets": { + "cjk": true + }, + "lastUpdated": "2022/05/22, 00:01:01", + "lastUpdatedTimestamp": 1653148861000 + }, { "title": "非技术问题", "frontmatter": { @@ -8341,30 +9247,23 @@ export const siteData = { "lastUpdatedTimestamp": 1655736461000 }, { - "title": "简述什么是两阶段提交?", + "title": "分类", "frontmatter": { - "title": "简述什么是两阶段提交?", - "date": "2022-05-21T17:12:23.000Z", - "permalink": "/pages/4eae5b/", - "categories": [ - "系统设计" - ], - "tags": [ - null - ] + "categoriesPage": true, + "title": "分类", + "permalink": "/categories/", + "article": false }, - "regularPath": "/08.%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/020.%E7%AE%80%E8%BF%B0%E4%BB%80%E4%B9%88%E6%98%AF%E4%B8%A4%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4%EF%BC%9F.html", - "relativePath": "08.系统设计/020.简述什么是两阶段提交?.md", - "key": "v-f88544f4", - "path": "/pages/4eae5b/", + "regularPath": "/@pages/categoriesPage.html", + "relativePath": "@pages/categoriesPage.md", + "key": "v-5f8dfe80", + "path": "/categories/", "headersStr": null, - "content": "TCC/Try Confirm Cancel模式TCC模式只有在所有的服务的第一阶段(try)都成功的时候才进行第二阶段确认(Confirm)操作,否则进行补偿(Cancel)操作,而在try阶段是不会进行真正的业务处理的。\n\n\nTCC模式TCC模式的具体流程为两个阶段:Try,业务服务完成所有的业务检查,预留必需的业务资源如果Try在所有服务中都成功,那么执行Confirm操作,Confirm操作不做任何的业务检查(因为try中已经做过),只是用Try阶段预留的业务资源进行业务处理;否则进行Cancel操作,Cancel操作释放Try阶段预留的业务资源。\n\n\n这么说可能比较模糊,下面我举一个具体的例子,小明在线从招商银行转账100元到广发银行。\n\n\n这个操作可看作两个服务,服务a从小明的招行账户转出100元,服务b从小明的广发银行帐户汇入100元。\n\n\n服务a(小明从招行转出100元):try: update cmb_account set balance=balance-100, freeze=freeze+100 where\nacc_id=1 and balance>100;\n\nconfirm: update cmb_account set freeze=freeze-100 where acc_id=1;\n\ncancel: update cmb_account set balance=balance+100, freeze=freeze-100 where\nacc_id=1;\n\n\n服务b(小明往广发银行汇入100元):try: update cgb_account set freeze=freeze+100 where acc_id=1;\n\nconfirm: update cgb_account set balance=balance+100, freeze=freeze-100 where acc_id=1;\n\ncancel: update cgb_account set freeze=freeze-100 where acc_id=1;\n\n\n具体说明:a的try阶段,服务做了两件事,1:业务检查,这里是检查小明的帐户里的钱是否多余100元;2:预留资源,将100元从余额中划入冻结资金。\na的confirm阶段,这里不再进行业务检查,因为try阶段已经做过了,同时由于转账已经成功,将冻结资金扣除。a的cancel阶段,释放预留资源,既100元冻结资金,并恢复到余额。\n\n\nb的try阶段进行,预留资源,将100元冻结。b的confirm阶段,使用try阶段预留的资源,将100元冻结资金划入余额。\nb的cancel阶段,释放try阶段的预留资源,将100元从冻结资金中减去。从上面的简单例子可以看出,TCC模式比纯业务补偿模式更加复杂,所以在实现上每个服务都需要实现Cofirm和Cancel两个接口。", - "normalizedContent": "tcc/try confirm cancel模式tcc模式只有在所有的服务的第一阶段(try)都成功的时候才进行第二阶段确认(confirm)操作,否则进行补偿(cancel)操作,而在try阶段是不会进行真正的业务处理的。\n\n\ntcc模式tcc模式的具体流程为两个阶段:try,业务服务完成所有的业务检查,预留必需的业务资源如果try在所有服务中都成功,那么执行confirm操作,confirm操作不做任何的业务检查(因为try中已经做过),只是用try阶段预留的业务资源进行业务处理;否则进行cancel操作,cancel操作释放try阶段预留的业务资源。\n\n\n这么说可能比较模糊,下面我举一个具体的例子,小明在线从招商银行转账100元到广发银行。\n\n\n这个操作可看作两个服务,服务a从小明的招行账户转出100元,服务b从小明的广发银行帐户汇入100元。\n\n\n服务a(小明从招行转出100元):try: update cmb_account set balance=balance-100, freeze=freeze+100 where\nacc_id=1 and balance>100;\n\nconfirm: update cmb_account set freeze=freeze-100 where acc_id=1;\n\ncancel: update cmb_account set balance=balance+100, freeze=freeze-100 where\nacc_id=1;\n\n\n服务b(小明往广发银行汇入100元):try: update cgb_account set freeze=freeze+100 where acc_id=1;\n\nconfirm: update cgb_account set balance=balance+100, freeze=freeze-100 where acc_id=1;\n\ncancel: update cgb_account set freeze=freeze-100 where acc_id=1;\n\n\n具体说明:a的try阶段,服务做了两件事,1:业务检查,这里是检查小明的帐户里的钱是否多余100元;2:预留资源,将100元从余额中划入冻结资金。\na的confirm阶段,这里不再进行业务检查,因为try阶段已经做过了,同时由于转账已经成功,将冻结资金扣除。a的cancel阶段,释放预留资源,既100元冻结资金,并恢复到余额。\n\n\nb的try阶段进行,预留资源,将100元冻结。b的confirm阶段,使用try阶段预留的资源,将100元冻结资金划入余额。\nb的cancel阶段,释放try阶段的预留资源,将100元从冻结资金中减去。从上面的简单例子可以看出,tcc模式比纯业务补偿模式更加复杂,所以在实现上每个服务都需要实现cofirm和cancel两个接口。", - "charsets": { - "cjk": true - }, - "lastUpdated": "2022/05/22, 00:01:01", - "lastUpdatedTimestamp": 1653148861000 + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 }, { "title": "你知道的越多,不知道的也就越多", @@ -8400,25 +9299,6 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, - { - "title": "归档", - "frontmatter": { - "archivesPage": true, - "title": "归档", - "permalink": "/archives/", - "article": false - }, - "regularPath": "/@pages/archivesPage.html", - "relativePath": "@pages/archivesPage.md", - "key": "v-48f7eec0", - "path": "/archives/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, { "title": "网站", "frontmatter": { @@ -8812,25 +9692,6 @@ export const siteData = { "lastUpdated": "2022/06/20, 22:47:41", "lastUpdatedTimestamp": 1655736461000 }, - { - "title": "分类", - "frontmatter": { - "categoriesPage": true, - "title": "分类", - "permalink": "/categories/", - "article": false - }, - "regularPath": "/@pages/categoriesPage.html", - "relativePath": "@pages/categoriesPage.md", - "key": "v-5f8dfe80", - "path": "/categories/", - "headersStr": null, - "content": "", - "normalizedContent": "", - "charsets": {}, - "lastUpdated": "2022/05/19, 21:26:01", - "lastUpdatedTimestamp": 1652966761000 - }, { "title": "拥抱生活,拥抱快乐", "frontmatter": { @@ -8863,6 +9724,25 @@ export const siteData = { "lastUpdated": "2022/05/19, 21:26:01", "lastUpdatedTimestamp": 1652966761000 }, + { + "title": "归档", + "frontmatter": { + "archivesPage": true, + "title": "归档", + "permalink": "/archives/", + "article": false + }, + "regularPath": "/@pages/archivesPage.html", + "relativePath": "@pages/archivesPage.md", + "key": "v-48f7eec0", + "path": "/archives/", + "headersStr": null, + "content": "", + "normalizedContent": "", + "charsets": {}, + "lastUpdated": "2022/05/19, 21:26:01", + "lastUpdatedTimestamp": 1652966761000 + }, { "title": "标签", "frontmatter": { @@ -9480,10 +10360,15 @@ export const siteData = { "/pages/15d37a/" ], [ - "10.计算机网络/170.简述 TCP 滑动窗口以及重传机制.md", - "简述 TCP 滑动窗口以及重传机制", + "10.计算机网络/170.简述 TCP 滑动窗口.md", + "简述 TCP 滑动窗口", "/pages/dae451/" ], + [ + "10.计算机网络/171. 简述TCP重传机制.md", + "简述TCP重传机制", + "/pages/a219df/" + ], [ "10.计算机网络/180.简述 JWT 的原理和校验机制.md", "简述 JWT 的原理和校验机制", @@ -9543,11 +10428,6 @@ export const siteData = { "10.计算机网络/290.简述 HTTP 报文头部的组成结构.md", "简述 HTTP 报文头部的组成结构", "/pages/dfb780/" - ], - [ - "10.计算机网络/300.什么是 SYN flood,如何防止这类攻击?.md", - "什么是 SYN flood,如何防止这类攻击?", - "/pages/367c4d/" ] ] }, @@ -9664,6 +10544,11 @@ export const siteData = { "20.操作系统/210.简述 traceroute 命令的原理.md", "简述 traceroute 命令的原理", "/pages/655f8f/" + ], + [ + "20.操作系统/220.简述IO模型.md", + "Untitled", + "/pages/782c23/" ] ] },