-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathatom.xml
575 lines (284 loc) · 769 KB
/
atom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>panzun blog</title>
<link href="https://zunpan.github.io/atom.xml" rel="self"/>
<link href="https://zunpan.github.io/"/>
<updated>2024-08-12T02:39:24.899Z</updated>
<id>https://zunpan.github.io/</id>
<author>
<name>panzun</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>大话设计模式学习笔记</title>
<link href="https://zunpan.github.io/2024/08/12/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
<id>https://zunpan.github.io/2024/08/12/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/</id>
<published>2024-08-12T02:37:14.000Z</published>
<updated>2024-08-12T02:39:24.899Z</updated>
<content type="html"><![CDATA[<h1 id="设计模式"><a class="markdownIt-Anchor" href="#设计模式"></a> 设计模式</h1><h2 id="简单工厂模式"><a class="markdownIt-Anchor" href="#简单工厂模式"></a> 简单工厂模式</h2><p>用单独的一个类完成创建所有实例,这就是工厂。</p><pre class="highlight"><code class="java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">OperationFactory</span>{ <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Operation <span class="hljs-title function_">createOperate</span><span class="hljs-params">(String op)</span>{ <span class="hljs-type">Operation</span> <span class="hljs-variable">operation</span> <span class="hljs-operator">=</span> <span class="hljs-literal">null</span>; <span class="hljs-keyword">switch</span>(op){ <span class="hljs-keyword">case</span> <span class="hljs-string">"+"</span>:{ operation = <span class="hljs-keyword">new</span> <span class="hljs-title class_">OperationAdd</span>(); } <span class="hljs-keyword">case</span> <span class="hljs-string">"-"</span>:{ operation = <span class="hljs-keyword">new</span> <span class="hljs-title class_">OperationSub</span>(); } <span class="hljs-comment">// ...</span> } }}</code></pre><p>优点:工厂对象类之间解耦<br />缺点:工厂类和工厂对象类之间存在耦合,新增工厂对象类需要修改工厂类</p><h2 id="策略模式"><a class="markdownIt-Anchor" href="#策略模式"></a> 策略模式</h2><p>策略模式定义了算法家族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化,不会影响到使用算法的客户</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20231215-153639291.png" alt="图 0" /></p><p>单独使用策略模式,实例化对象的过程会落在客户端(switch-case-new过程)。因此可以将策略模式和简单工厂模式结合,将实例化对象的过程放在Context类中</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20231215-155304930.png" alt="图 1" /></p><p>策略模式和简单工厂模式结合使用的好处是,客户端只需要知道Context类</p><p>该模式不足的地方在于新增策略仍然要修改Context类中的switch代码块</p><h2 id="单一职责原则"><a class="markdownIt-Anchor" href="#单一职责原则"></a> 单一职责原则</h2><p>单一职责原则:就一个类而言,应该仅有一个引起它变化的原因</p><p>例如,游戏逻辑和界面应该分离</p><h2 id="开闭原则"><a class="markdownIt-Anchor" href="#开闭原则"></a> 开闭原则</h2><p>开闭原则:软件实体(类、模块、函数等等)应该可以扩展,但是不可以修改</p><p>开发人员应该对程序中频繁变化的那些部分做抽象</p><h2 id="依赖倒转原则"><a class="markdownIt-Anchor" href="#依赖倒转原则"></a> 依赖倒转原则</h2><p>依赖倒转原则:</p><ol><li>高层模块不应该依赖低层模块。两个都应该依赖抽象</li><li>抽象不应该依赖细节,细节应该依赖抽象。换句话说,要针对接口编程,不要对实现编程</li></ol><p>针对原则一,子类型必须能够替换掉它们的父类型而不影响软件工程,这样才能真正复用父类,因此高层和低层模块都应该依赖抽象。</p><p>针对原则二,例如,电脑主板、CPU、内存、硬盘都是针对接口设计的,如果针对实现来设计,内存就要对应具体的某个品牌的主板,那就会出现换内存要把主板一起换了的尴尬</p><h2 id="装饰模式"><a class="markdownIt-Anchor" href="#装饰模式"></a> 装饰模式</h2><p>装饰模式:动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更加灵活</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20231219-152604094.png" alt="图 2" /></p><h2 id="代理模式"><a class="markdownIt-Anchor" href="#代理模式"></a> 代理模式</h2><p>代理模式:为其它对象提供一种代理以控制对这个对象的访问</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20231219-152725523.png" alt="图 3" /></p><p>使用场景:</p><ol><li>远程代理。为一个对象在不同的地址空间提供局部代表,这样可以隐藏一个对象存在于不同地址空间的事实。例子:客户端调用代理实现远程访问</li><li>虚拟代理。使用代理来存放创建开销很大的对象。例子:HTML中的未下载完成的图片框就是虚拟代理,它替代了真实图片</li><li>安全代理。用来控制真实对象的访问权限</li><li>智能指引。指调用真实对象时,代理处理另外一些事</li></ol><h2 id="工厂方法模式"><a class="markdownIt-Anchor" href="#工厂方法模式"></a> 工厂方法模式</h2><p>简单工厂模式违背了开闭原则,于是有了工厂方法模式</p><p>工厂方法模式:定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20240803-091825631.png" alt="图 4" /></p><h2 id="原型模式"><a class="markdownIt-Anchor" href="#原型模式"></a> 原型模式</h2><p>原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20240803-093010713.png" alt="图 6" /></p><h2 id="模板方法模式"><a class="markdownIt-Anchor" href="#模板方法模式"></a> 模板方法模式</h2><p>魔棒方法模式:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20240803-101147090.png" alt="图 7" /></p><h2 id="迪米特法则"><a class="markdownIt-Anchor" href="#迪米特法则"></a> 迪米特法则</h2><p>迪米特法则(最少知识原则):如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。</p><p>迪米特法则的根本思想是类之间的松耦合。</p><h2 id="外观模式"><a class="markdownIt-Anchor" href="#外观模式"></a> 外观模式</h2><p>外观模式:为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20240803-105223749.png" alt="图 8" /></p><p>使用场景:</p><ol><li>在设计阶段,分层架构之间增加Facade,例如表示层和业务逻辑层之间、业务逻辑层和数据访问层之间</li><li>在开发阶段,随着类越来越多,可以增加Facade降低类调用复杂性</li><li>在系统重构阶段,可以在系统中增加Facade,Facade调用旧系统代码保证平滑过渡</li></ol><h2 id="建造者模式"><a class="markdownIt-Anchor" href="#建造者模式"></a> 建造者模式</h2><p>建造者模式:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20240803-111734445.png" alt="图 9" /></p><h2 id="观察者模式"><a class="markdownIt-Anchor" href="#观察者模式"></a> 观察者模式</h2><p>观察者模式(发布-订阅模式):定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20240803-112904485.png" alt="图 10" /></p><p>使用场景:当一个对象的改变需要同时改变其它对象,且不知道有多少对象需要改变时使用观察者模式</p><h2 id="抽象工厂模式"><a class="markdownIt-Anchor" href="#抽象工厂模式"></a> 抽象工厂模式</h2><p>抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20240803-193834095.png" alt="图 11" /></p><p>抽象工厂的优点是只需要更换工厂实例对象,就可以更换一系列产品。缺点是增加产品时需要需改大量类</p><p>反射+配置文件+简单工厂可以避免大量修改</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20240803-200045582.png" alt="图 12" /></p><h2 id="状态模式"><a class="markdownIt-Anchor" href="#状态模式"></a> 状态模式</h2><p>状态模式:当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20240803-204440872.png" alt="图 13" /></p><p>使用场景:当一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为时,可以解决if-else过长的问题</p><h2 id="适配器模式"><a class="markdownIt-Anchor" href="#适配器模式"></a> 适配器模式</h2><p>适配器模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20240804-073711819.png" alt="图 14" /></p><h2 id="备忘录模式"><a class="markdownIt-Anchor" href="#备忘录模式"></a> 备忘录模式</h2><p>备忘录模式:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先保存的状态</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20240808-082715600.png" alt="图 15" /></p><p>使用场景:适用于功能复杂、需要维护或记录属性历史的类,或者需要保存的属性只是众多属性中的一小部分</p><h2 id="组合模式"><a class="markdownIt-Anchor" href="#组合模式"></a> 组合模式</h2><p>组合模式:将对象组合成树形结构以表示‘部分-整体’的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20240808-084757390.png" alt="图 16" /></p><p>使用场景:需求中体现部分与整体层次的结构时,以及希望用户可以忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象时</p><h2 id="迭代器模式"><a class="markdownIt-Anchor" href="#迭代器模式"></a> 迭代器模式</h2><p>迭代器模式:提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示(foreach语法糖)</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20240808-091431901.png" alt="图 17" /></p><h2 id="单例模式"><a class="markdownIt-Anchor" href="#单例模式"></a> 单例模式</h2><p>单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20240808-094937128.png" alt="图 18" /></p><h2 id="桥接模式"><a class="markdownIt-Anchor" href="#桥接模式"></a> 桥接模式</h2><p>桥接模式:将抽象部分与它的实现部分分离,使它们都可以独立的变化</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20240808-120851150.png" alt="图 19" /></p><p>使用场景:实现系统可能有多角度分类,每一种分类都有可能变化,那么就把这种多角度分离出来让它们独立变化,减少它们之间的耦合</p><h2 id="命令模式"><a class="markdownIt-Anchor" href="#命令模式"></a> 命令模式</h2><p>命令模式:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20240810-093211853.png" alt="图 20" /></p><h2 id="职责链模式"><a class="markdownIt-Anchor" href="#职责链模式"></a> 职责链模式</h2><p>职责链模式:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20240810-095301089.png" alt="图 21" /></p><h2 id="中介者模式"><a class="markdownIt-Anchor" href="#中介者模式"></a> 中介者模式</h2><p>中介者模式:用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间地交互。</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20240810-195623991.png" alt="图 22" /></p><p>使用场景:一组对象以定义良好但是复杂地方式进行通信的场合;想定制一个分布在多个类中的行为,而又不想生成太多子类的场合</p><h2 id="享元模式"><a class="markdownIt-Anchor" href="#享元模式"></a> 享元模式</h2><p>享元模式:运用共享技术有效地支持大量细粒度的对象</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20240810-200407455.png" alt="图 23" /></p><p>使用场景:如果一个应用程序使用了大量的对象,而大量的这些对象造成了很大的存储开销时就应该考虑使用</p><h2 id="解释器模式"><a class="markdownIt-Anchor" href="#解释器模式"></a> 解释器模式</h2><p>解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20240812-084507634.png" alt="图 24" /></p><p>使用场景:当有一个语言需要解释执行,并且可以将该语言中的句子表示为一个抽象语法树时,可使用解释器模式(例如正则表达式,浏览器)</p><p>优势:容易地改变和扩展文法<br />劣势:解释器模式为文法中的每一条规则至少定义了一个类,因此包含许多规则的文法可能难以管理和维护。建议当文法非常复杂时,使用其它技术如语法分析程序或编译器生成器来处理</p><h2 id="访问者模式"><a class="markdownIt-Anchor" href="#访问者模式"></a> 访问者模式</h2><p>访问者模式:表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%A4%A7%E8%AF%9D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20240812-091222555.png" alt="图 25" /></p><p>使用场景:适用于数据结构稳定,算法易于变化</p><h2 id="模式总结"><a class="markdownIt-Anchor" href="#模式总结"></a> 模式总结</h2><ol><li>创建型模式:单例、工厂方法、抽象工厂、建造者、原型</li><li>结构型模式:适配器、装饰、桥接、组合、享元、代理、外观</li><li>行为型模式:观察者、模板方法、命令、状态、职责链、解释器、终结者、访问者、策略、备忘录、迭代器</li></ol>]]></content>
<summary type="html"><h1 id="设计模式"><a class="markdownIt-Anchor" href="#设计模式"></a> 设计模式</h1>
<h2 id="简单工厂模式"><a class="markdownIt-Anchor" href="#简单工厂模式"></a> 简单工厂</summary>
<category term="Java" scheme="https://zunpan.github.io/categories/Java/"/>
<category term="设计模式" scheme="https://zunpan.github.io/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"/>
</entry>
<entry>
<title>深入理解Java虚拟机学习笔记</title>
<link href="https://zunpan.github.io/2023/06/10/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
<id>https://zunpan.github.io/2023/06/10/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/</id>
<published>2023-06-10T15:30:14.000Z</published>
<updated>2023-09-24T04:27:40.285Z</updated>
<content type="html"><![CDATA[<h2 id="第一部分-走近java"><a class="markdownIt-Anchor" href="#第一部分-走近java"></a> 第一部分 走近Java</h2><h3 id="1-走近java"><a class="markdownIt-Anchor" href="#1-走近java"></a> 1. 走近Java</h3><h4 id="11-概述"><a class="markdownIt-Anchor" href="#11-概述"></a> 1.1 概述</h4><p>Java不仅仅是一门编程语言,还是一个由一系列计算机软件和规范组成的技术体系</p><p>Java优点:1. 一次编写,到处运行;2. 避免了绝大部分内存泄露和指针越界问题;3. 实现了热点代码检测和运行时编译及优化,越运行性能越好;4. 完善的类库</p><h4 id="12-java技术体系"><a class="markdownIt-Anchor" href="#12-java技术体系"></a> 1.2 Java技术体系</h4><p>广义上,Kotlin等运行在JVM上的编程语言都属于Java技术体系<br />传统上,JCP定义的Java技术体系包含:1. Java程序设计语言;2. 各种硬件平台上的Java虚拟机实现;3. Class文件格式;4 Java类库API;5. 来自商业机构和开源社区的第三方Java类库</p><p>JDK:Java程序设计语言+Java虚拟机+Java类库<br />JRE:Java类库API中的Java SE API子集和Java虚拟机<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230610-225849195.png" alt="图 1" /></p><h4 id="13-java发展史"><a class="markdownIt-Anchor" href="#13-java发展史"></a> 1.3 Java发展史</h4><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230610-231416313.png" alt="图 2" /></p><h4 id="14-java虚拟机家族"><a class="markdownIt-Anchor" href="#14-java虚拟机家族"></a> 1.4 Java虚拟机家族</h4><ul><li>Sun Classic/Exact VM。Sun Classic是世界上第一款商用Java虚拟机,纯解释执行代码,如果外挂即时编译器会完全接管执行,两者不能混合工作。Exact VM解决了许多问题但是碰上了引进的HotSpot,生命周期很短</li><li>HotSpot VM:使用最广泛的Java虚拟机。HotSpot继承了前者的优点,也有许多新特性,例如热点代码探测技术。JDK 8中的HotSpot融合了BEA JRockit优秀特性</li><li>Mobile/Embedded VM:针对移动和嵌入式市场的虚拟机</li><li>BEA JRockit/IBM J9 VM:JRockit专注服务端应用,不关注程序启动速度,全靠编译器编译后执行。J9定位类似HotSpot</li><li>BEA Liquid VM/Azul VM:和专用硬件绑定,更高性能的虚拟机</li><li>Apache Harmony/Google Android Dalvik VM:Harmony被吸收进IBM的JDK 7以及Android SDK,Dalvik被ART虚拟机取代</li><li>Microsoft JVM:Windows系统下性能最好的Java虚拟机,因侵权被抹去</li></ul><h4 id="15-展望java技术的未来"><a class="markdownIt-Anchor" href="#15-展望java技术的未来"></a> 1.5 展望Java技术的未来</h4><ul><li>无语言倾向:Graal VM可以作为“任何语言”的运行平台</li><li>新一代即时编译器:Graal编译器,取代C2(HotSpot中编译耗时长但代码优化质量高的即时编译器)</li><li>向Native迈进:Substrate VM提前编译代码,显著降低内存和启动时间</li><li>灵活的胖子:经过一系列重构与开放,提高开放性和扩展性</li><li>语言语法持续增强:增加语法糖和语言功能</li></ul><h2 id="第二部分-自动内存管理"><a class="markdownIt-Anchor" href="#第二部分-自动内存管理"></a> 第二部分 自动内存管理</h2><h3 id="2-java内存区域与内存溢出异常"><a class="markdownIt-Anchor" href="#2-java内存区域与内存溢出异常"></a> 2. Java内存区域与内存溢出异常</h3><h4 id="21-概述"><a class="markdownIt-Anchor" href="#21-概述"></a> 2.1 概述</h4><p>C、C++程序员需要自己管理内存,Java程序员在虚拟机自动内存管理机制下不需要为每一个new操作写对应的delete/free代码,但是一旦出现内存泄露和溢出,不了解虚拟机就很难排错</p><h4 id="22-运行时数据区域"><a class="markdownIt-Anchor" href="#22-运行时数据区域"></a> 2.2 运行时数据区域</h4><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230615-210628087.png" alt="图 3" /></p><ul><li>程序计数器:当前线程执行的下一条指令的地址;线程私有;不会OOM</li><li>虚拟机栈:Java方法执行的线程内存模型,每个方法执行时,JVM都会创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息;线程私有;会栈溢出和OOM</li><li>局部变量表:存放的变量的类型有8种基本数据类型、对象引用和returnAddress类型(指向字节码指令的地址),这些变量除了64位的long和double占两个变量槽,其它占1个。局部变量表的大小在编译器确定</li><li>本地方法栈:和虚拟机栈作用类似,区别在于只是为本地(Native)方法服务,HotSpot将两者合二为一</li><li>堆:“几乎”所有对象实例都在此分配内存;可分代,也不分代;逻辑上连续,物理上可以不连续;会OOM</li><li>方法区:也叫“非堆”,存储已加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据;实现上,之前HotSpot使用永久代实现方法区,目的是方便垃圾回收,但是这样有OOM问题,JDK8废弃永久代改为使用本地内存的元空间,主要存放类型信息,其它移到堆;内存回收目标主要是常量池和类型信息</li><li>运行时常量池:Class文件中的常量池表存放编译期生成的各种字面量和符号引用,这部分内容在类加载后放到方法区的运行时常量池;具有动态性,运行时产生的常量也可以放入运行时常量池,String类的intern()利用了这个特性;会OOM</li><li>直接内存:不是运行时数据区的一部分,也不是Java虚拟机规范里定义的内存区域;NIO使用Native函数库直接分配堆外内存;会OOM</li></ul><h4 id="23-hotspot虚拟机对象探秘"><a class="markdownIt-Anchor" href="#23-hotspot虚拟机对象探秘"></a> 2.3 HotSpot虚拟机对象探秘</h4><h5 id="231-对象的创建"><a class="markdownIt-Anchor" href="#231-对象的创建"></a> 2.3.1 对象的创建</h5><ol><li>当JVM碰到new指令,首先检查指令参数能否在常量池中定位到一个类的符号引用,并且检查该类是否已被加载、解析和初始化,如果没有就进行类加载过程</li><li>JVM为新生对象分配内存。<ul><li>对象所需内存大小在类加载后可确定,分配方法有两种:当垃圾收集器(Serial、ParNew)能空间压缩整理时,堆是规整的,分配内存就是把指针向空闲空间方向移动对象大小的距离,这种叫“指针碰撞”,使用CMS收集器的堆是不规整的,需要维护空闲列表来分配内存。</li><li>内存分配可能线程不安全,例如线程在给A分配内存,指针来没来得及修改,另一线程创建对象B又同时使用了原来的指针来分配内存。解决方法有两个:1.对分配内存空间的动作进行同步处理,JVM采用CAS+失败重试的方式保证原子性;2.预先给每个线程分配一小块内存,称为本地线程分配缓冲(TLAB),线程分配内存先在TLAB上分配,TLAB用完了再同步分配新的缓存区</li></ul></li><li>JVM将分配到的内存空间(不包括对象头)初始化为零值,这步保证对象的实例字段可以不赋初始值就直接使用</li><li>JVM对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息存放在对象的对象头中</li><li>执行构造函数,即Class文件中的<code><init>()</code></li></ol><h5 id="232-对象的内存布局"><a class="markdownIt-Anchor" href="#232-对象的内存布局"></a> 2.3.2 对象的内存布局</h5><ul><li><p>对象头</p><ul><li>用于存储对象自身的运行时数据,如HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳,称为“Mark Word”。这部分数据长度32比特或64比特,Bitmap存储,为了在极小空间存储更多数据,不同状态的对象用不同标志位表示不同存储内容<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230618-192458680.png" alt="图 4" /></li><li>类型指针,即对象指向它的类型元数据的指针,JVM通过该指针来确定对象是哪个类的实例。若对象是数组,还必须记录数组长度</li></ul></li><li><p>实例数据,包括父类继承下来的,和子类中定义的字段</p></li><li><p>对齐填充,HotSpot要求对象大小必须是8字节的整数倍,不够就对齐填充</p></li></ul><h5 id="233-对象的访问定位"><a class="markdownIt-Anchor" href="#233-对象的访问定位"></a> 2.3.3 对象的访问定位</h5><p>通过栈上的reference数据来访问堆上的具体对象,访问方式由虚拟机实现决定,主流有两种</p><ul><li>句柄访问。Java堆会划分出一块内存来作为句柄池,reference中存储的是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自具体的地址信息。优点是对象被移动时只需要改变句柄中的实例数据指针<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230618-194355057.png" alt="图 5" /></li><li>直接指针访问。reference存储的就是对象地址,类型数据指针在对象中。优点是节省一次指针定位的时间开销,HotSpot使用此方式来访问对象<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230618-194608417.png" alt="图 6" /></li></ul><h4 id="24-实现outofmemoryerror异常"><a class="markdownIt-Anchor" href="#24-实现outofmemoryerror异常"></a> 2.4 实现:OutOfMemoryError异常</h4><h5 id="241-java堆溢出"><a class="markdownIt-Anchor" href="#241-java堆溢出"></a> 2.4.1 Java堆溢出</h5><pre class="highlight"><code class="java"><span class="hljs-comment">/** * VM options:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError * <span class="hljs-doctag">@author</span> [email protected] * <span class="hljs-doctag">@date</span> 2023/6/18 19:54 */</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">HeapOOM</span> { <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">OOMObject</span>{ } <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> { ArrayList<OOMObject> list = <span class="hljs-keyword">new</span> <span class="hljs-title class_">ArrayList</span><>(); <span class="hljs-keyword">while</span>(<span class="hljs-literal">true</span>){ list.add(<span class="hljs-keyword">new</span> <span class="hljs-title class_">OOMObject</span>()); } }}</code></pre><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230618-200529059.png" alt="图 7" /></p><p>排查思路:首先确认导致OOM的对象是否是必要的,也就是是分清楚是内存泄露了还是内存溢出了</p><p>如果是内存泄露了,可以通过工具查看泄露对象到GC Roots的引用链,定位到产生内存泄露的代码的具体位置</p><p>如果是内存溢出了,可以调大堆空间,优化生命周期过长的对象</p><h5 id="242-虚拟机栈和本地方法栈溢出"><a class="markdownIt-Anchor" href="#242-虚拟机栈和本地方法栈溢出"></a> 2.4.2 虚拟机栈和本地方法栈溢出</h5><p>HotSpot不区分虚拟机栈和本地方法栈,所以-Xoss(本地方法栈大小)参数没效果,栈容量由-Xss参数设定。</p><p>当线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常;当扩展栈容量无法申请到足够内存,将抛出OutOfMemoryError异常。HotSpot不支持扩展栈容量</p><h5 id="243-方法区和运行时常量池溢出"><a class="markdownIt-Anchor" href="#243-方法区和运行时常量池溢出"></a> 2.4.3 方法区和运行时常量池溢出</h5><p>JDK8使用元空间取代了永久代,运行时常量池移动到了堆中,所以可能会产生堆内存的OOM</p><p>方法区的主要职责是存放类型信息,如类名、访问修饰符、常量池、字段描述、方法描述等。用CGLib不断生成增强类,可能产生元空间的OOM</p><pre class="highlight"><code class="java"><span class="hljs-comment">/** * VM Args:-XX:MaxMetaspaceSize=10M * <span class="hljs-doctag">@author</span> [email protected] * <span class="hljs-doctag">@date</span> 2023/6/18 22:13 */</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">JavaMethodAreaOOM</span> { <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> { <span class="hljs-keyword">while</span>(<span class="hljs-literal">true</span>){ <span class="hljs-type">Enhancer</span> <span class="hljs-variable">enhancer</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Enhancer</span>(); enhancer.setSuperclass(HeapOOM.OOMObject.class); enhancer.setUseCache(<span class="hljs-literal">false</span>); enhancer.setCallback(<span class="hljs-keyword">new</span> <span class="hljs-title class_">MethodInterceptor</span>() { <span class="hljs-meta">@Override</span> <span class="hljs-keyword">public</span> Object <span class="hljs-title function_">intercept</span><span class="hljs-params">(Object obj, Method method, Object[] objects, MethodProxy proxy)</span> <span class="hljs-keyword">throws</span> Throwable { <span class="hljs-keyword">return</span> proxy.invokeSuper(obj,args); } }); enhancer.create(); } } <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">OOMObject</span>{ }}</code></pre><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230618-222215679.png" alt="图 8" /></p><h5 id="244-本机直接内存溢出"><a class="markdownIt-Anchor" href="#244-本机直接内存溢出"></a> 2.4.4 本机直接内存溢出</h5><p>直接内存通过-XX:MaxDirectMemorySize参数来指定,默认与Java堆最大值一致</p><p>虽然使用DirectByteBuffer分配内存会OOM,但它抛出异常时并没有真正向操作系统申请内存,而是通过计算得知无法分配就手动抛异常,真正申请内存的方法是Unsafe::allocateMemory()</p><pre class="highlight"><code class="java"><span class="hljs-comment">/** * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M * <span class="hljs-doctag">@author</span> [email protected] * <span class="hljs-doctag">@date</span> 2023/6/19 20:38 */</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">DirectMemoryOOM</span> { <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> <span class="hljs-variable">_1MB</span> <span class="hljs-operator">=</span> <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>; <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> IllegalAccessException { <span class="hljs-type">Field</span> <span class="hljs-variable">unsafeField</span> <span class="hljs-operator">=</span> Unsafe.class.getDeclaredFields()[<span class="hljs-number">0</span>]; unsafeField.setAccessible(<span class="hljs-literal">true</span>); <span class="hljs-type">Unsafe</span> <span class="hljs-variable">unsafe</span> <span class="hljs-operator">=</span> (Unsafe) unsafeField.get(<span class="hljs-literal">null</span>); <span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) { unsafe.allocateMemory(_1MB); } }}</code></pre><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230619-205928722.png" alt="图 9" /></p><p>由直接内存导致的内存溢出,一个明显的特征是Heap Dump文件不会看到有什么明显的异常情况。如果内存溢出后产生的Dump文件很小,而程序中又直接或间接使用了DirectMemory(NIO),可以重点检查直接内存</p><h3 id="3-垃圾收集器与内存分配策略"><a class="markdownIt-Anchor" href="#3-垃圾收集器与内存分配策略"></a> 3. 垃圾收集器与内存分配策略</h3><h4 id="31-概述"><a class="markdownIt-Anchor" href="#31-概述"></a> 3.1 概述</h4><p>线程独占的程序计数器、虚拟机栈、本地方法栈3个区域的内存分配和回收都具备确定性。</p><p>而Java堆和方法区有显著的不确定性:一个接口的多个实现类需要的内存可能不一样,一个方法不同分支需要的内存也不同,只有处于运行期间,才知道程序会创建哪些对象,这部分内存的分配和回收是动态的</p><h4 id="32-判断对象是否存活的算法"><a class="markdownIt-Anchor" href="#32-判断对象是否存活的算法"></a> 3.2 判断对象是否存活的算法</h4><ul><li>引用计数法。看似简单,但必须配合大量额外处理才能正确工作,譬如简单的引用计数法无法解决对象循环引用</li><li>可达性分析算法。从GC Roots对象开始,根据引用关系向下搜索,走过的路径称为“引用链”,引用链上对象仍然存活,不在引用链上的对象可回收。<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230619-220636571.png" alt="图 10" /></li></ul><p><strong>GC Roots对象包括以下几种:</strong></p><ul><li>虚拟机栈中引用的对象。例如方法参数、局部变量等</li><li>方法区中类静态属性引用的对象。例如Java类的引用类型静态变量</li><li>方法区中常量引用的对象。例如字符串常量池里的引用</li><li>本地方法栈JNI引用的对象</li><li>Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointException、OutOfMemoryError)、还有系统类加载器</li><li>所有被同步锁(synchronized)持有的对象</li><li>反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等</li><li>根据用户选用的垃圾收集器以及回收区域,临时加入其它对象。目的是当某个区域垃圾回收时,该区域的对象也可能被别的区域的对象引用</li></ul><p><strong>引用包含以下几种类型:</strong></p><ul><li>强引用:被强引用引用的对象不会被回收</li><li>软引用:被软引用引用的对象在OOM前会被回收</li><li>弱引用:被弱引用引用的对象在下一次垃圾回收时被回收</li><li>虚引用:虚引用不会影响对象的生存时间,唯一目的是能在对象被回收时收到一个系统通知</li></ul><p>即便对象已经不可达,也不是立即标记为可回收,对象真正死亡要经历两次标记过程:可达性分析发现不可达就第一次标记;如果对象重写了finalize()方法且没过JVM调用过,那么该对象会被放到队列中,由Finalizer线程去执行finalize()方法,这是对象自救的最后一次机会,只要重新与引用链上任意对象建立关联就行,譬如把this赋给某个对象的成员变量,第二次标记时就会被移除“即将回收”集合</p><pre class="highlight"><code class="java"><span class="hljs-comment">/** * 此代码演示了两点: * 1.对象可以在被GC时自我拯救。 * 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次 * <span class="hljs-doctag">@author</span> [email protected] * <span class="hljs-doctag">@date</span> 2023/6/20 21:52 */</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">FinalizeEscapeGC</span> { <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-type">FinalizeEscapeGC</span> <span class="hljs-variable">SAVE_HOOK</span> <span class="hljs-operator">=</span> <span class="hljs-literal">null</span>; <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">isAlive</span><span class="hljs-params">()</span>{ System.out.println(<span class="hljs-string">"yes, i am still alive"</span>); } <span class="hljs-meta">@Override</span> <span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">finalize</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> Throwable { <span class="hljs-built_in">super</span>.finalize(); System.out.println(<span class="hljs-string">"finalize method executed! :)"</span>); FinalizeEscapeGC.SAVE_HOOK = <span class="hljs-built_in">this</span>; } <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> InterruptedException { SAVE_HOOK = <span class="hljs-keyword">new</span> <span class="hljs-title class_">FinalizeEscapeGC</span>(); SAVE_HOOK = <span class="hljs-literal">null</span>; System.gc(); Thread.sleep(<span class="hljs-number">500</span>); <span class="hljs-keyword">if</span> (SAVE_HOOK != <span class="hljs-literal">null</span>) { SAVE_HOOK.isAlive(); } <span class="hljs-keyword">else</span> { System.out.println(<span class="hljs-string">"no ,i am dead! :("</span>); } <span class="hljs-comment">// 自救失败</span> SAVE_HOOK = <span class="hljs-literal">null</span>; System.gc(); Thread.sleep(<span class="hljs-number">500</span>); <span class="hljs-keyword">if</span> (SAVE_HOOK != <span class="hljs-literal">null</span>) { SAVE_HOOK.isAlive(); } <span class="hljs-keyword">else</span> { System.out.println(<span class="hljs-string">"no ,i am dead! :("</span>); } }}</code></pre><p>方法区没有强制要垃圾回收,例如JDK11的ZGC不支持类卸载。</p><p>方法区主要回收两部分:废弃的常量和不再使用的类型。</p><p>回收废弃常量和回收堆中的对象非常类似<br />回收“不再使用的类”需要满足三个条件:</p><ul><li>该类所有实例已被回收,包括子类实例</li><li>加载该类的类加载器已被回收,这个条件很难</li><li>该类对象的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法</li></ul><h4 id="33-垃圾收集算法"><a class="markdownIt-Anchor" href="#33-垃圾收集算法"></a> 3.3 垃圾收集算法</h4><p>从如何判定对象消亡的角度出发,GC算法可以划分为“引用计数式”和“追踪式”,这两类也被称作“直接垃圾收集”和“间接垃圾收集”。本节介绍追踪式垃圾收集</p><h5 id="331-分代收集理论"><a class="markdownIt-Anchor" href="#331-分代收集理论"></a> 3.3.1 分代收集理论</h5><p>分代收集理论建立在两个假说上:</p><ul><li>弱分代假说:绝大多数对象都是朝生夕灭的</li><li>强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡</li></ul><p>分代收集在遇到对象之间存在跨代引用时需要遍历其它代的所有对象来确定是否还存在跨代引用,性能负担大,所以给分代收集理论添加第三个假说:</p><ul><li>跨代引用假说:跨代引用相对于同代引用来说占极少数</li></ul><p>分代收集的名词定义:</p><ul><li><p>部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:</p><ul><li>新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集</li><li>老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS会有单独收集老年代的行为</li><li>混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1会有这种行为</li></ul></li><li><p>整堆收集(Full GC):收集整个Java堆和方法区的垃圾</p></li></ul><h5 id="332-标记-清除算法"><a class="markdownIt-Anchor" href="#332-标记-清除算法"></a> 3.3.2 标记-清除算法</h5><p>标记所有需要回收的对象,标记完成后,统一回收<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230621-005225729.png" alt="图 11" /></p><p>缺点:执行效率不稳定;内存空间碎片化</p><h5 id="333-标记-复制算法"><a class="markdownIt-Anchor" href="#333-标记-复制算法"></a> 3.3.3 标记-复制算法</h5><p>将内存划分为大小相等的两块,每次只使用一块。当这一块的内存用完了,就将还存活着的对象复制到另一外上面,然后再把已使用过的内存空间一次性清理掉</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230621-005601185.png" alt="图 12" /></p><p>优点:对于多数对象都是可回收的情况,算法复制开销小;没有碎片<br />缺点:可用内存小了一半;需要空间担保</p><p>1:1划分新生代太浪费空间,HotSpot将新生代划分成Eden:Survivor0:Survivor0 = 8:1:1,每次可以用Eden和一块Survivor,垃圾回收时把存活对象写到另一块Survivor,然后清理掉Eden和已用过的Survivor,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖老年代进行分配担保</p><h5 id="334-标记-整理算法"><a class="markdownIt-Anchor" href="#334-标记-整理算法"></a> 3.3.4 标记-整理算法</h5><p>标记所有存活的对象,然后移动到内存空间一端,清理掉边界以外的内存<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230621-011120062.png" alt="图 13" /></p><p>优点:解决了标记-清除算法造成的空间碎片化问题<br />缺点:整理期间,用户应用程序暂停,这段时间被称为“Stop The World”</p><p>整理即移动对象,移动则内存回收时会更复杂,不移动则内存分配会更复杂。从GC停顿时间来看,不移动对象停顿时间短;从吞吐量来看,移动对象更划算。</p><p>HotSpot里关注吞吐量的Parallel Scavenge收集器采用标记-整理算法,关注延迟的CMS收集器采用标记-清除算法</p><h4 id="34-hotspot的算法细节实现"><a class="markdownIt-Anchor" href="#34-hotspot的算法细节实现"></a> 3.4 HotSpot的算法细节实现</h4><h5 id="341-根节点枚举"><a class="markdownIt-Anchor" href="#341-根节点枚举"></a> 3.4.1 根节点枚举</h5><p>固定作为GC Root的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表),Java程序越来越庞大,逐个作为起点进行可达性分析会消耗大量时间</p><p>目前,所有收集器在根节点枚举时和整理内存碎片一样必须暂停用户线程,可达性分析算法可以和用户线程一起并发。为了降低STW时间,在不扫描全部的GC Root节点情况下,得知哪些地方存在对象引用,HotSpot提供了OopMap的数据结构保存引用的位置信息</p><h5 id="342-安全点"><a class="markdownIt-Anchor" href="#342-安全点"></a> 3.4.2 安全点</h5><p>OopMap可以帮助HotSpot快速完成GC Root枚举,但是如果为每条改变OomMap内容的指令都生成对应的OopMap,会需要大量额外存储空间</p><p>因此HotSpot没有为每条指令都生成OopMap,只在特定位置生成,这些位置称为安全点。用户程序只有在执行到安全点才能停顿下来垃圾回收。</p><p>安全点的选取考虑:不能太少以至于让收集器等待时间太长,也不能太频繁以至于增大内存负担</p><p>如何在垃圾回收时让所有线程(不包括执行JNI调用的线程)都跑到最近的安全点,然后停顿下来。有两种方案:</p><ul><li>抢先式中断:先把用户线程全部中断,如果发现有用户线程不在安全点上就恢复这个线程,过一会再中断直至它跑到安全点。现在几乎不使用</li><li>主动式中断:设置一个标志位,各个线程执行时主动轮询这个标志,一旦为真主动中断挂起。HotSpot使用内存保护陷阱的方式将轮询操作精简至只有一条汇编指令</li></ul><h5 id="343-安全区域"><a class="markdownIt-Anchor" href="#343-安全区域"></a> 3.4.3 安全区域</h5><p>当用户线程处于Sleep或者Blocked状态时不能执行到安全点。针对这种情况,引入安全区域。</p><p>安全区域是指在某一段代码片段中,引用关系不会发生变化,在这个区域中任意地方开始GC都是安全的。</p><p>当用户线程执行到安全区域时,首先标识自己进入了安全区域,这样在GC时,虚拟机就不会去管这些已标识自己进入安全区域的线程。当线程离开安全区域时,它检查虚拟机是否完成了根节点枚举(或者其它需要暂停用户线程的阶段),如果完成了就继续执行,否则等待直到收到可以离开安全区域的信号为止。</p><h5 id="344-记忆集与卡表"><a class="markdownIt-Anchor" href="#344-记忆集与卡表"></a> 3.4.4 记忆集与卡表</h5><p>记忆集是一种记录从非收集区域指向收集区域的指针集合的抽象数据结构,用于解决对象跨代引用带来的问题</p><p>记忆集最简单的实现是非收集区域中所有含跨代引用的对象数组,这种结构浪费太多空间,可以粗化记录粒度:</p><ul><li>字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如32或64),该字包含跨代指针</li><li>卡精度:每个记录精确到一块内存区域,该区域有对象含有跨代指针</li></ul><p>卡精度使用卡表实现,卡表的实现是一个字节数组,字节数组每个元素都对应着一块特定大小的内存块,称为卡页。只要卡页内有一个或更多对象的字段存在跨代引用,卡表中对应的数组元素的值标识为1,称为变脏。垃圾收集时,只要筛选出卡表中变脏的元素就可以知道哪些卡页内存块有跨代引用,把它们放入GC Roots中一起扫描</p><h5 id="345-写屏障"><a class="markdownIt-Anchor" href="#345-写屏障"></a> 3.4.5 写屏障</h5><p>卡表元素变脏的时间点是引用类型字段赋值那一刻,HotSpot通过写屏障来维护卡表状态。写屏障可以看作JVM层面对“引用类型字段赋值”这个动作的AOP切面。</p><p>写屏障会导致伪共享问题,伪共享是指,现代CPU的缓存系统是以缓存行为单位存储的,当多线程修改相互独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低</p><p>伪共享的一种简单解决方法是不采用无条件的写屏障,而是先检查卡表标记,只有卡表元素未被标记时才将其变脏。HotSpot参数-XX:+UseCondCardMark决定是否开启卡表更新,开启会多一次判断开销,但能够避免伪共享带来的性能损耗</p><h5 id="346-并发的可达性分析"><a class="markdownIt-Anchor" href="#346-并发的可达性分析"></a> 3.4.6 并发的可达性分析</h5><p>可达性分析在标记阶段会暂停用户线程以在一致性的快照上进行对象图的遍历,不一致情况下会出现“对象消失”问题。原因可以由三色标记方法推导</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230623-183852237.png" alt="图 14" /></p><p>白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚开始时,所有对象都是白的,若在分析结束阶段,仍然是白色的对象是不可达<br />黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色对象代表已经扫描过,是安全存活的,如果有其它对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象<br />灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过</p><p>当且仅当以下两个条件同时满足时,会产生“对象消失”问题,即原本应该黑色的对象被误标为白色:</p><ul><li>赋值器插入了一条或多条从黑色对象到白色对象的新引用</li><li>赋值器删除了全部从灰色对象到该白色对象的直接或间接引用</li></ul><p>解决并发扫描时的对象消失问题,只需破坏两个条件之一即可,因此有两种方案:</p><ul><li>增量更新。增量更新破坏条件一,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束后,再以这个黑色对象为根,重新扫描一次。可以理解为,黑色对象一旦新插入指向白色对象的引用之后,它就变回灰色对象</li><li>原始快照。当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。可以理解为,无论引用关系删除与否,都会按照刚开始扫描那一刻的对象图快照进行搜索</li></ul><p>对引用关系记录的插入和删除都是通过写屏障实现。</p><h4 id="35-经典垃圾收集器"><a class="markdownIt-Anchor" href="#35-经典垃圾收集器"></a> 3.5 经典垃圾收集器</h4><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230628-201447667.png" alt="图 15" /></p><p>图中连线表示可以搭配使用</p><h5 id="351-serial收集器"><a class="markdownIt-Anchor" href="#351-serial收集器"></a> 3.5.1 Serial收集器</h5><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230628-202324451.png" alt="图 16" /></p><p>适合资源(cpu和内存)受限的场景,新生代一两百兆以内的垃圾收集停顿时间最多一百多毫秒以内</p><h5 id="352-parnew收集器"><a class="markdownIt-Anchor" href="#352-parnew收集器"></a> 3.5.2 ParNew收集器</h5><p>实质是多线程版的Serial</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230628-203214503.png" alt="图 17" /></p><h5 id="353-parallel-scavenge收集器"><a class="markdownIt-Anchor" href="#353-parallel-scavenge收集器"></a> 3.5.3 Parallel Scavenge收集器</h5><p>与ParNew类似,不同点是其它收集器关注停顿时间,Parallel Scavenge收集器目标是可控制的吞吐量</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230628-204146049.png" alt="图 18" /></p><p>-XX:MaxGCPauseMillis控制最大垃圾收集停顿时间,-XX:GCTimeRatio直接设置吞吐量,-XX:+UseAdaptiveSizePolicy会根据系统运行情况动态调整虚拟机参数以获得最合适的停顿时间或者最大的吞吐量</p><h5 id="354-serial-old收集器"><a class="markdownIt-Anchor" href="#354-serial-old收集器"></a> 3.5.4 Serial Old收集器</h5><p>Serial收集器的老年代版本</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230628-210623950.png" alt="图 19" /></p><h5 id="355-parallel-old收集器"><a class="markdownIt-Anchor" href="#355-parallel-old收集器"></a> 3.5.5 Parallel Old收集器</h5><p>Parallel Scavenge的老年代版本,在注重吞吐量或者处理器资源稀缺场合,可以考虑使用Parallel Scavenge+Parallel Old</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230628-211020700.png" alt="图 20" /></p><h5 id="356-cms收集器"><a class="markdownIt-Anchor" href="#356-cms收集器"></a> 3.5.6 CMS收集器</h5><p>四步骤:<br />1)初始标记<br />2)并发标记<br />3)重新标记<br />4)并发清理</p><p>初始标记和重新标记仍然要暂停用户线程。初始标记仅仅标记GC Roots能直接关联的对象,速度很快;并发标记从关联对象开始遍历整个对象图,不需要暂停用户线程;重新标记用来修正并发标记期间的变动(增量更新)</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230628-213003554.png" alt="图 21" /></p><p>优点:并发收集、低停顿<br />缺点:</p><ul><li>对处理器资源敏感。并发会占用处理器,降低吞吐量</li><li>无法处理“浮动垃圾”。“浮动垃圾”指并发标记和清理期间用户线程产生的垃圾在下一次GC时清理,预留空间不足会,预留空间不足会导致“Concurrent Mode Failure”进而导致Full GC或者Serial Old重新回收老年代。</li><li>内存空间碎片化</li></ul><h5 id="357-garbage-firstg1收集器"><a class="markdownIt-Anchor" href="#357-garbage-firstg1收集器"></a> 3.5.7 Garbage First(G1)收集器</h5><p>开创面向局部收集的设计思路和基于Region的内存布局形式</p><p>局部收集只收集范围不是新生代或老年代,而是堆中任意部分。</p><p>基于Region的堆内存布局:G1不再坚持固定大小和数量的分代区域划分,而是把连续的堆划分为多个大小相等的独立区域Region,每个Region都可以根据需要扮演新生代或者老年代。超过Region容量一半的对象会被存到Humongous区域中,超过整个Region容量的对象会被存到N个连续的Humongous Region中,G1大多数行为把Humongous Region当老年代处理<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230628-225424687.png" alt="图 22" /></p><p>四个步骤:</p><ul><li>初始标记:标记GC Roots能直接关联的对象,修改TAMS指针的值,让下一阶段用户并发运行时能正确在可用的Region中分配新对象</li><li>并发标记:递归扫描对象图。扫描完成后重新处理SATB记录下的在并发时有引用变动的对象</li><li>最终标记:处理并发阶段遗留的少量SATB记录</li><li>筛选回收:负责更新Region统计数据,制定回收计划</li></ul><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230701-163141458.png" alt="图 23" /></p><h4 id="36-低延迟垃圾收集器"><a class="markdownIt-Anchor" href="#36-低延迟垃圾收集器"></a> 3.6 低延迟垃圾收集器</h4><p>垃圾收集器“不可能三角”:内存占用、吞吐量、延迟。延迟是最重要指标</p><p>下图,浅色表示挂起用户线程,深色表示gc线程和用户线程并发</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230701-164655072.png" alt="图 24" /></p><p>Shenandoah和ZGC在可管理的堆容量下,停顿时间基本是固定的,两者被命名为低延迟垃圾收集器</p><h5 id="361-shenandoah收集器"><a class="markdownIt-Anchor" href="#361-shenandoah收集器"></a> 3.6.1 Shenandoah收集器</h5><p>目标:能在任何堆内存大小下都可以把GC停顿时间限制在10ms以内</p><p>Shenandoah和G1有相似的堆内存布局,在初始标记、并发标记等阶段的处理思路高度一致。<br />有三个明显的不同:</p><ul><li>支持并发的整理算法</li><li>默认不分代,即不使用专门的新生代Region和老年代Region</li><li>放弃G1中耗费大量内存和计算资源去维护的记忆集,改为“连接矩阵”记录跨Region的引用关系</li></ul><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230701-210502176.png" alt="图 25" /></p><p>收集器的工作分为九个阶段:</p><ul><li>初始标记:与G1一样,首先标记GC Roots直接关联的对象,停顿时间与堆大小无关,与GC Roots数量有关</li><li>并发标记:与G1一样,遍历对象图,与用户线程并发,时间取决于对象数量和对象图的结构复杂程度</li><li>最终标记:与G1一样,处理剩余的SATB扫描,统计价值最高的Region组成回收集,有一小段停顿时间</li><li>并发清理:清理一个存活对象都没有的Region</li><li>并发回收:将回收集中的存活对象复制到未被使用的Region中,通过读屏障和“Brooks Pointers”转发指针解决和用户线程并发产生的问题</li><li>初始引用更新:一个非常短暂的停顿,用于确保所有并发回收阶段中收集器线程都已完成分配对象移动任务</li><li>并发引用更新:真正开始进行引用更新,与用户线程并发</li><li>最终引用更新:修正存在于GC Roots中的引用,停顿时间与GC Roots的数量有关</li><li>并发清理:经过并发回收和引用更新后,整个回收集的Region已经没有存货对象,回收这些Region的内存空间</li></ul><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230701-213506046.png" alt="图 26" /></p><p>对象移动和用户程序并发,原来的解决方案是在被移动对象原有的内存上设置保护陷阱,一旦用户程序访问到原有的地址就产生自陷中断,进入预设的异常处理器,将访问转发到新对象。这个方案会导致用户态频繁切换到核心态</p><p>Brooks Pointers是在原有对象布局结构的最前面加一个新的引用字段,移动前指向自己,移动后指向新对象。这里需要用CAS解决对象访问的并发问题</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230701-221938700.png" alt="图 27" /></p><h5 id="362-zgc收集器"><a class="markdownIt-Anchor" href="#362-zgc收集器"></a> 3.6.2 ZGC收集器</h5><p>ZGC收集器是一款基于Region内存布局的,不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。收集过程全程可并发,短暂停顿时间只与GC Roots大小相关而与堆内存大小无关</p><ol><li><p>ZGC的Region是动态的:动态创建和销毁、动态大小</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230716-221512875.png" alt="图 28" /></p></li><li><p>染色指针:把标记信息记在引用对象的指针上,标记信息有4个bit,虚拟机可以直接从指针中看到其引用对 象的三色标记状态、是否进入了重分配集(即被移动过)、是否只能通过finalize()方法才能被访问到</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230716-222826226.png" alt="图 29" /></p><p>三大优势:</p><ul><li>染色指针可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用 掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理</li><li>染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,设置内存屏障,尤其是写屏障的 目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作</li><li>染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以 便日后进一步提高性能</li></ul><p>标志位影响了寻址地址,ZGC通过多重映射,将不同标志位的指针映射到同一内存区域</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230716-223643829.png" alt="图 30" /></p></li><li><p>ZGC的收集过程分为四大阶段</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230716-223855413.png" alt="图 31" /></p><ul><li>并发标记:与G1、Shenandoah类似,区别是标记在指针上而不是对象上,标记会更新染色指针的Marked 0、Marked 1标志位</li><li>并发预备重分配:根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集</li><li>并发重分配:把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表,记录从旧对象到新对象的转向关系</li><li>并发重映射:修正整个堆中指向重分配集中旧对象的所 有引用</li></ul></li></ol><h4 id="37-选择合适的垃圾收集器"><a class="markdownIt-Anchor" href="#37-选择合适的垃圾收集器"></a> 3.7 选择合适的垃圾收集器</h4><h5 id="371-epsilon收集器"><a class="markdownIt-Anchor" href="#371-epsilon收集器"></a> 3.7.1 Epsilon收集器</h5><p>不收集垃圾,负责堆的管理与布局、对象的分配、与解释器的协作、与编译器的协作、与监控子系统协作等职责</p><p>响应微服务而生,在堆内存耗尽前就退出,不收集垃圾就非常合适</p><h5 id="372-收集器的权衡"><a class="markdownIt-Anchor" href="#372-收集器的权衡"></a> 3.7.2 收集器的权衡</h5><p>三个因素:</p><ul><li>应用程序关注点是什么?吞吐量、延时还是内存占用</li><li>基础设施如何?处理器的数量、内存大小、操作系统</li><li>JDK的发行商是谁?版本号是多少</li></ul><p>例如直接面向用户的B/S系统,延迟是主要关注点。</p><ul><li>预算充足就用商业的</li><li>预算不足但能掌控基础设施,可以尝试ZGC</li><li>对ZGC的稳定性有疑虑就考虑Shenandoah</li><li>软硬件和JDK都老的考虑CMS</li></ul>]]></content>
<summary type="html"><h2 id="第一部分-走近java"><a class="markdownIt-Anchor" href="#第一部分-走近java"></a> 第一部分 走近Java</h2>
<h3 id="1-走近java"><a class="markdownIt-Anchor" h</summary>
<category term="Java" scheme="https://zunpan.github.io/categories/Java/"/>
<category term="深入理解Java虚拟机" scheme="https://zunpan.github.io/tags/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA/"/>
</entry>
<entry>
<title>Excel比对与合并系统</title>
<link href="https://zunpan.github.io/2023/03/03/Excel%E6%AF%94%E5%AF%B9%E4%B8%8E%E5%90%88%E5%B9%B6%E7%B3%BB%E7%BB%9F/"/>
<id>https://zunpan.github.io/2023/03/03/Excel%E6%AF%94%E5%AF%B9%E4%B8%8E%E5%90%88%E5%B9%B6%E7%B3%BB%E7%BB%9F/</id>
<published>2023-03-03T07:00:03.000Z</published>
<updated>2023-09-24T04:27:40.287Z</updated>
<content type="html"><![CDATA[<h2 id="背景"><a class="markdownIt-Anchor" href="#背景"></a> 背景</h2><p>许多游戏策划使用Excel来配置数值。策划需要保存所有版本的数值表,有时需要查看两个数值表有何差异,有时想把差异或者叫修改应用到另一张数值表中,这非常类似于版本控制,但是市面上的版本控制系统svn和git都是针对文本文件,不能用于Excel的版本控制</p><h2 id="excel比对算法"><a class="markdownIt-Anchor" href="#excel比对算法"></a> Excel比对算法</h2><h3 id="一维数据比对算法"><a class="markdownIt-Anchor" href="#一维数据比对算法"></a> 一维数据比对算法</h3><p>假设有两个序列<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>A</mi><mn>1</mn></msub><mi mathvariant="normal">.</mi><mi mathvariant="normal">.</mi><mi mathvariant="normal">.</mi><msub><mi>A</mi><mi>m</mi></msub></mrow><annotation encoding="application/x-tex">A_1...A_m</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.83333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal">A</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.30110799999999993em;"><span style="top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord">.</span><span class="mord">.</span><span class="mord">.</span><span class="mord"><span class="mord mathnormal">A</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.151392em;"><span style="top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">m</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span>和<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>B</mi><mn>1</mn></msub><mi mathvariant="normal">.</mi><mi mathvariant="normal">.</mi><mi mathvariant="normal">.</mi><msub><mi>B</mi><mi>n</mi></msub></mrow><annotation encoding="application/x-tex">B_1...B_n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.83333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.05017em;">B</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.30110799999999993em;"><span style="top:-2.5500000000000003em;margin-left:-0.05017em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord">.</span><span class="mord">.</span><span class="mord">.</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.05017em;">B</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.151392em;"><span style="top:-2.5500000000000003em;margin-left:-0.05017em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">n</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span>,我们可以通过对A序列进行一些列操作,使之变为B序列。对每种操作都定义个Cost,如何找到总Cost最小的使A变为B的操作序列,可以通过动态规划解决。这是一个已经被广为研究的算法问题,本文就不再整述,读者可以在网上搜索Edit编辑距离获取更多信息。</p><p>操作集合的定义有多种方式,一种较为常见的操作集合定义如下(Cost均为1) :</p><ul><li>在序列中插入一个元素:</li><li>在序列中删除一个元素;</li></ul><p>比如,将字符串kiten变换为sitting,需要删除k,插入s,删除e,插入i,在尾部插入g。如果在原序列和目标序列中去掉删除和插入的元素,那么原序列和目标序列就是完全相同的了(比如上面的例子两者都变为itn了),因此这种编辑距离被命名为LCS (Longest Common Subsequence) 编辑距离。<a href="https://leetcode.cn/problems/longest-common-subsequence/">LeetCode 1143. 最长公共子序列</a></p><p>再回到本文要讨论的差异比较问题,要比较两个序列的差异,实际上就是要找到二者之间尽量多的公共部分,剩下的就是差异部分,所以这和最短编辑距离问题是完全等价的。</p><p>此外,除了LCS编辑距离之外,还有一种常用的编辑距离,允许插入、删除和修改操作,叫做Levenshtein编组距离。另外,还可以定义一种广义的Levenshtein编辑距离,删除元素<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>A</mi><mi>i</mi></msub></mrow><annotation encoding="application/x-tex">A_i</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.83333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal">A</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.31166399999999994em;"><span style="top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span>和插入元素<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>B</mi><mi>j</mi></msub></mrow><annotation encoding="application/x-tex">B_j</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.969438em;vertical-align:-0.286108em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.05017em;">B</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.311664em;"><span style="top:-2.5500000000000003em;margin-left:-0.05017em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.05724em;">j</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.286108em;"><span></span></span></span></span></span></span></span></span></span>;的Cost由一个单参数函数决定,记为Cost(<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>A</mi><mi>i</mi></msub></mrow><annotation encoding="application/x-tex">A_i</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.83333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal">A</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.31166399999999994em;"><span style="top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span>)或Cost(<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>B</mi><mi>j</mi></msub></mrow><annotation encoding="application/x-tex">B_j</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.969438em;vertical-align:-0.286108em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.05017em;">B</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.311664em;"><span style="top:-2.5500000000000003em;margin-left:-0.05017em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.05724em;">j</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.286108em;"><span></span></span></span></span></span></span></span></span></span>); 将<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>A</mi><mi>i</mi></msub></mrow><annotation encoding="application/x-tex">A_i</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.83333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal">A</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.31166399999999994em;"><span style="top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span>修改为<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>B</mi><mi>j</mi></msub></mrow><annotation encoding="application/x-tex">B_j</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.969438em;vertical-align:-0.286108em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.05017em;">B</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.311664em;"><span style="top:-2.5500000000000003em;margin-left:-0.05017em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.05724em;">j</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.286108em;"><span></span></span></span></span></span></span></span></span></span>;的操作的Cost由一个双参数函数决定,记为Cost2(<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>A</mi><mi>i</mi></msub><mo separator="true">,</mo><msub><mi>B</mi><mi>j</mi></msub></mrow><annotation encoding="application/x-tex">A_i, B_j</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.969438em;vertical-align:-0.286108em;"></span><span class="mord"><span class="mord mathnormal">A</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.31166399999999994em;"><span style="top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.05017em;">B</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.311664em;"><span style="top:-2.5500000000000003em;margin-left:-0.05017em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.05724em;">j</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.286108em;"><span></span></span></span></span></span></span></span></span></span>)。</p><pre class="highlight"><code class="Java"><span class="hljs-comment">/** * 比对的基本单位是单个字符 * <span class="hljs-doctag">@param</span> text1 字符串1 * <span class="hljs-doctag">@param</span> text2 字符串2 * <span class="hljs-doctag">@return</span> levenshteinDP数组 */</span> <span class="hljs-keyword">static</span> <span class="hljs-type">int</span>[][] levenshteinDP(String text1, String text2) { <span class="hljs-type">int</span> <span class="hljs-variable">len1</span> <span class="hljs-operator">=</span> text1.length(); <span class="hljs-type">int</span> <span class="hljs-variable">len2</span> <span class="hljs-operator">=</span> text2.length(); <span class="hljs-comment">// dp[i][j]表示从text1[0...i-1]到text2[0...j-1]的最小编辑距离(cost)</span> dp = <span class="hljs-keyword">new</span> <span class="hljs-title class_">int</span>[len1 + <span class="hljs-number">1</span>][len2 + <span class="hljs-number">1</span>]; <span class="hljs-comment">// path记录此方格的来源是多个此类枚举值的布尔或值</span> path = <span class="hljs-keyword">new</span> <span class="hljs-title class_">int</span>[len1 + <span class="hljs-number">1</span>][len2 + <span class="hljs-number">1</span>]; <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> <span class="hljs-variable">i</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; i < len1 + <span class="hljs-number">1</span>; i++) { dp[i][<span class="hljs-number">0</span>] = i; path[i][<span class="hljs-number">0</span>] = FROM_INIT; } <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> <span class="hljs-variable">j</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; j < len2 + <span class="hljs-number">1</span>; j++) { dp[<span class="hljs-number">0</span>][j] = j; path[<span class="hljs-number">0</span>][j] = FROM_INIT; } <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> <span class="hljs-variable">i</span> <span class="hljs-operator">=</span> <span class="hljs-number">1</span>; i < len1 + <span class="hljs-number">1</span>; i++) { <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> <span class="hljs-variable">j</span> <span class="hljs-operator">=</span> <span class="hljs-number">1</span>; j < len2 + <span class="hljs-number">1</span>; j++) { path[i][j] = FROM_INIT; <span class="hljs-type">int</span> <span class="hljs-variable">left</span> <span class="hljs-operator">=</span> dp[i][j - <span class="hljs-number">1</span>] + <span class="hljs-number">1</span>; <span class="hljs-type">int</span> <span class="hljs-variable">up</span> <span class="hljs-operator">=</span> dp[i - <span class="hljs-number">1</span>][j] + <span class="hljs-number">1</span>; <span class="hljs-type">int</span> leftUp; <span class="hljs-type">boolean</span> replace; <span class="hljs-keyword">if</span> (text1.charAt(i - <span class="hljs-number">1</span>) == text2.charAt(j - <span class="hljs-number">1</span>)) { leftUp = dp[i - <span class="hljs-number">1</span>][j - <span class="hljs-number">1</span>]; replace = <span class="hljs-literal">false</span>; } <span class="hljs-keyword">else</span> { leftUp = dp[i - <span class="hljs-number">1</span>][j - <span class="hljs-number">1</span>] + <span class="hljs-number">1</span>; replace = <span class="hljs-literal">true</span>; } dp[i][j] = Math.min(Math.min(left, up), leftUp); <span class="hljs-keyword">if</span> (dp[i][j] == left) { path[i][j] |= FROM_LEFT; } <span class="hljs-keyword">if</span> (dp[i][j] == up) { path[i][j] |= FROM_UP; } <span class="hljs-comment">// 对应:两字符完全一样或者可以修改成一样</span> <span class="hljs-keyword">if</span> (dp[i][j] == leftUp) { <span class="hljs-keyword">if</span> (replace) { path[i][j] |= FROM_LEFT_UP_REPLACE; } <span class="hljs-keyword">else</span> { path[i][j] |= FROM_LEFT_UP_COPY; } } } } <span class="hljs-keyword">return</span> dp; }</code></pre><p>同样的,对于广义Levenshtein编辑距离,如果去掉删除和插入的元素,那么两个序列中剩下的元素即为一一对应的关系,每组对应的两个元素,要么是完全相同的,要么前者是被后者修改掉的。从这部分论述中我们不难看出,比较算法的核心思路实际上就是找到元素与元素之间的一一对应关系</p><h3 id="二维数据比对算法"><a class="markdownIt-Anchor" href="#二维数据比对算法"></a> 二维数据比对算法</h3><p>Excel的一个分页可以认为是一个二维数据阵列,阵列中的每个元素是对应单元格内容的字符串值。根据前面的论述,比较两个二维阵列的核心就是找到他们公共的行/列,或者说原阵列和目标阵列的行/列对应关系。比如,对于下面两张表:<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/Excel%E6%AF%94%E5%AF%B9%E4%B8%8E%E5%90%88%E5%B9%B6%E7%B3%BB%E7%BB%9F/IMG_20230303-153631780.png" alt="图 2" /></p><p>甲表的第1、2、3列对应乙表的1、2、4列,甲表的1、3行对应乙表的1、2行。那么这两张表的差异通过下列方式描述:</p><ul><li>在第2列的位置插入新列</li><li>删除第2行</li><li>将(原表中) 第3行第3列的元素从9修改为0;</li></ul><p>如何计算两张表对应的行/列,一个比较容易想到的方案是将其拆分为两个独立求解的问题,计算对应的行和计算对应的列。对于前者,我们可以把阵列的每一行当成一个元素,所有行组成一个序列,然后对这个序列进行比较:后者亦然。这样我们就把二维比较问题转化成了一维比较的问题。关于编辑距离的定义,可以采用广义Levenshtein编辑距离,定义删除、插入元素的Cost为该行(列)的元素数,定义修改元素的Cost为这两行(列)之间的LCS编辑距离.于是两个二维阵列的比较过程如下:<br />找到二者对应的 (或者叫公共的) 行/列,非公共的行/列记为删除、插入行/列操作;两张表只保留公共的行/列,此时他们尺寸完全相同,对应位置的单元格逐一比较,如果值不相同,则记为单元格修改操作;<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/Excel%E6%AF%94%E5%AF%B9%E4%B8%8E%E5%90%88%E5%B9%B6%E7%B3%BB%E7%BB%9F/IMG_20230303-153658521.png" alt="图 3" /></p><h4 id="算法优化"><a class="markdownIt-Anchor" href="#算法优化"></a> 算法优化</h4><p>上一个部分介绍的二维阵列比较方案只是一个理论上可行的方案,在实际应用中,存在以下问题:</p><ul><li>删除、插入行/列的操作都是对于整个行/列的,而计算两行/列之间的LCS编辑距离是独立计算的,因此算法本身有一定不合理性;</li><li>计算修改Cost里又包含了LCS编辑距离的计算,二层嵌套,性能开销比较大;</li></ul><p>针对上述问题,从游戏开发的实际应用场景出发,做了如下优化:<br />首先计算列之间的对应关系,只取表头前h行(不同项目表头行数h可能不同,可以通过参数配置) ,这样就把对整列的LCS编辑距离计算优化为h个单元格逐已比较,大幅优化了效率,而且对准确度基本不会有什么影响;</p><p>根据上一步的计算结果,去掉非公共的列(即删除、添加的列),这样,剩下的列都是两边一一对应的了,此时再计算行的对应关系,修改操作的Cost定义就可以从LCS编辑距离改为单元格的逐一比较了,这样又大幅优化了性能</p><p>在上面所述基础之上,还可以再做优化,因为在实际应用中,绝大多数情况下,绝大多数行都不会有修改,因此可以先用LCS编辑距离对所有行的对应关系进行计算,即只有当两行内容完全相同时才会被当做是对应的; 然后再把剩下的未匹配的行分组用广义Levenshtein编辑距离进行对应关系匹配。这样么做的原因是LCS编辑距离比广义Levenshtein编辑距离的求解速度要快很多。</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/Excel%E6%AF%94%E5%AF%B9%E4%B8%8E%E5%90%88%E5%B9%B6%E7%B3%BB%E7%BB%9F/IMG_20230303-164334559.png" alt="图 6" /></p><h4 id="功能扩展"><a class="markdownIt-Anchor" href="#功能扩展"></a> 功能扩展</h4><p>在开发过程中,我们经常会将单行或者连续若干行单元格上移或下移到另一位置,按照目前的比较逻辑,该操作会被认为是删除这些行,然后在新的位置重新插入这些行。这样的结果和不合理的。为此,我们可以引入一种新的操作: 移动行到另一位置。加入了这个新的操作之后,我们依然可以建立之前所述的行对应关系,只不过两边行的顺序可以是乱序的。这种不保序对应关系可以通过多个轮次的编辑距离匹配计算,每次匹配之后去掉匹配上的行,剩下未匹配的行组成一个新的序列进行下一轮的匹配。每轮匹配是采用LCS编辑距离还是广义Levenshtein编指距离可以灵活决定,比如前若干轮或者行数较多时采用LCS编辑距离,后面的轮次再用广义Levenshtein编辑距离。</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/Excel%E6%AF%94%E5%AF%B9%E4%B8%8E%E5%90%88%E5%B9%B6%E7%B3%BB%E7%BB%9F/IMG_20230303-164400538.png" alt="图 7" /></p>]]></content>
<summary type="html"><h2 id="背景"><a class="markdownIt-Anchor" href="#背景"></a> 背景</h2>
<p>许多游戏策划使用Excel来配置数值。策划需要保存所有版本的数值表,有时需要查看两个数值表有何差异,有时想把差异或者叫修改应用到另一张数值表中,</summary>
<category term="Java" scheme="https://zunpan.github.io/categories/Java/"/>
<category term="LCS" scheme="https://zunpan.github.io/tags/LCS/"/>
<category term="Levenshtein" scheme="https://zunpan.github.io/tags/Levenshtein/"/>
<category term="编辑距离" scheme="https://zunpan.github.io/tags/%E7%BC%96%E8%BE%91%E8%B7%9D%E7%A6%BB/"/>
<category term="diff" scheme="https://zunpan.github.io/tags/diff/"/>
<category term="merge" scheme="https://zunpan.github.io/tags/merge/"/>
</entry>
<entry>
<title>MySQL实战45讲学习笔记</title>
<link href="https://zunpan.github.io/2023/02/01/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
<id>https://zunpan.github.io/2023/02/01/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/</id>
<published>2023-02-01T10:57:05.000Z</published>
<updated>2023-09-24T04:27:40.280Z</updated>
<content type="html"><![CDATA[<h2 id="基础架构一条sql查询语句是如何执行的"><a class="markdownIt-Anchor" href="#基础架构一条sql查询语句是如何执行的"></a> 基础架构:一条SQL查询语句是如何执行的</h2><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230123-203930857.png" alt="图 1" /></p><p>MySQL可以分为Server层和存储引擎层两部分</p><p>Server层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖MySQL的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。</p><p>而存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎</p><h3 id="连接器"><a class="markdownIt-Anchor" href="#连接器"></a> 连接器</h3><p>连接器负责跟客户端建立连接、获取权限、维持和管理连接。建立连接后,权限修改不会影响已经存在的连接的权限</p><p>长连接:连接成功后,如果客户度持续有请求,一直使用同一个连接<br />短连接:每次执行很少的几次查询就断开连接,下次查询再重新建立</p><p>建立连接比较耗时,尽量使用长连接,但是全部使用长连接会导致OOM,因为MySQL在执行过程中临时使用的内存是管理在连接对象里面,连接不断开内存不会释放</p><p>解决方案:</p><ol><li>定期断开长连接</li><li>执行mysql_reset_connection重新初始化连接资源</li></ol><h3 id="查询缓存"><a class="markdownIt-Anchor" href="#查询缓存"></a> 查询缓存</h3><p>执行过的语句及其结果可能会以key-value对的形式,直接缓存在内存中</p><p>查询缓存的失效非常频繁,只要有对一个表的更新,这个表上的所有查询缓存都会被清空,因此不要使用查询缓存,MySQL 8.0删掉了此功能</p><h3 id="分析器"><a class="markdownIt-Anchor" href="#分析器"></a> 分析器</h3><p>分析器先做“词法分析”,识别出SQL语句中的字符串分别是什么,例如,识别“select”是查询语句,“T”识别成表名T</p><p>然后做“语法分析”,判断输入的SQL语句是否满足MySQL语法,如果语句不对,会收到“You have an error in your SQL syntax”的错误提醒</p><h3 id="优化器"><a class="markdownIt-Anchor" href="#优化器"></a> 优化器</h3><p>优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序</p><p>例如下面的语句执行两个表的join:</p><pre class="highlight"><code class="SQL">mysql<span class="hljs-operator">></span> <span class="hljs-keyword">select</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> t1 <span class="hljs-keyword">join</span> t2 <span class="hljs-keyword">using</span>(ID) <span class="hljs-keyword">where</span> t1.c<span class="hljs-operator">=</span><span class="hljs-number">10</span> <span class="hljs-keyword">and</span> t2.d<span class="hljs-operator">=</span><span class="hljs-number">20</span>;</code></pre><ul><li>既可以先从表t1里面取出c=10的记录的ID值,再根据ID值关联到表t2,再判断t2里面d的值是否等于20。</li><li>也可以先从表t2里面取出d=20的记录的ID值,再根据ID值关联到t1,再判断t1里面c的值是否等于10。</li></ul><p>这两种执行方法的结果一样但效率不同,优化器决定使用哪个方法</p><h3 id="执行器"><a class="markdownIt-Anchor" href="#执行器"></a> 执行器</h3><ol><li>判断有没有表的执行权限</li><li>根据表的引擎定义调用引擎提供的接口,例如“取满足条件的第一行”,“满足条件的下一行”,数据库的慢查询日志rows_examined字段表示语句在执行过程中扫描了多少行,引擎扫描行数跟rows_examined并不是完全相同的</li></ol><h2 id="日志系统一条sql更新语句是如何执行的"><a class="markdownIt-Anchor" href="#日志系统一条sql更新语句是如何执行的"></a> 日志系统:一条SQL更新语句是如何执行的</h2><p>一条查询语句的执行过程一般是经过连接器、分析器、优化器、执行器等功能模块,最后到达存储引擎。更新语句还涉及 redo log(重做日志)和 binlog(归档日志)</p><h3 id="redo-log"><a class="markdownIt-Anchor" href="#redo-log"></a> redo log</h3><p>WAL(Write-Ahead Logging):更新记录时,InnoDB引擎会先把记录写到redo log里面并更新内存,然后在适当的时候将操作记录更新到磁盘里面</p><p>InnoDB的redo log是固定大小和循环写的,write pos是当前记录的位置,checkpoint是当前要擦除的位置,擦除记录前要把记录更新到数据文件</p><p>redo log保证即使数据库异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe</p><h3 id="binlog"><a class="markdownIt-Anchor" href="#binlog"></a> binlog</h3><p>redo log是InnoDB引擎特有的日志,Server层特有的引擎是binlog(归档日志)</p><p>两者有三点不同</p><ol><li>redo log是InnoDB引擎特有的;binlog是MySQL的Server层实现的,所有引擎都可以使用</li><li>redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这一行的c字段加1”</li><li>redo log是循环写的,空间固定会用完;binlog是可以追加写入的。“追加写”是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志</li></ol><h3 id="执行器和innodb引擎在执行update语句时的内部流程"><a class="markdownIt-Anchor" href="#执行器和innodb引擎在执行update语句时的内部流程"></a> 执行器和InnoDB引擎在执行update语句时的内部流程</h3><ol><li>执行器先找引擎取ID=2这一行。ID是主键,引擎直接用树搜索找到这一行。如果ID=2这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。</li><li>执行器拿到引擎给的行数据,把这个值加上1,比如原来是N,现在就是N+1,得到新的一行数据,再调用引擎接口写入这行新数据。</li><li>引擎将这行新数据更新到内存中,同时将这个更新操作记录到redo log里面,此时redo log处于prepare状态。然后告知执行器执行完成了,随时可以提交事务。</li><li>执行器生成这个操作的binlog,并把binlog写入磁盘。</li><li>执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交(commit)状态,更新完成。</li></ol><p>下图的浅色框表示在InnoDB内部执行,深色框表示在执行器中执行</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230125-191218672.png" alt="图 2" /></p><h4 id="两阶段提交"><a class="markdownIt-Anchor" href="#两阶段提交"></a> 两阶段提交</h4><p>redo log的写入分为两步骤:prepare和commit,也就是”两阶段提交“,目的是为了让两份的日志之间的逻辑一致</p><p>当数据库需要恢复到指定的某一秒时,可以先在临时库上这样做:</p><ol><li>找到最近的一次全量备份</li><li>从备份的时间点开始,将备份的binlog依次取出来重放到指定时间</li></ol><p>如果redo log不是两阶段提交</p><ol><li>先写redo log后写binlog。假设在redo log写完,binlog还没写完,MySQL异常重启,数据可以恢复,但是binlog没有记录这个语句。之后用binlog恢复临时库时会缺少更新</li><li>先写binlog后写redo log。假设binlog写完,redo log还没写,MySQL异常重启之后,这个事务无效,数据没有恢复。但是binlog里面已经有这个语句,所以之后用binlog恢复临时库会多一个事务</li></ol><p>innodb_flush_log_at_trx_commit这个参数设置成1的时候,表示每次事务的redo log都直接持久化到磁盘。这个参数建议设置成1,这样可以保证MySQL异常重启之后数据不丢失。</p><p>sync_binlog这个参数设置成1的时候,表示每次事务的binlog都持久化到磁盘。建议设置成1,这样可以保证MySQL异常重启之后binlog不丢失。</p><p>如果redo log处于prepare状态且binlog写入完成,MySQL异常重启会commit掉这个事务</p><h2 id="事务隔离"><a class="markdownIt-Anchor" href="#事务隔离"></a> 事务隔离</h2><p>事务保证一组数据库操作要么全部成功,要么全部失败</p><h3 id="隔离性与隔离级别"><a class="markdownIt-Anchor" href="#隔离性与隔离级别"></a> 隔离性与隔离级别</h3><p>ACID中的I指的是隔离性(Isolation)</p><p>当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。</p><p>事务隔离级别包括:</p><ul><li>读未提交:一个事务还没提交时,它做的变更就能被别的事务看到</li><li>读提交:一个事务提交之后,它做的变更才会被其他事务看到</li><li>可重复读:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其它事务也是不可见的</li><li>串行化:对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成才能继续执行</li></ul><p>数据库使用视图来实现隔离级别。在“可重复读”隔离级别下,视图是在事务开启时创建的。在“读提交”隔离级别下,视图是在每个SQL语句开始执行的时候创建的。“读未提交”直接返回记录的最新值,没有视图概念。“串行化”直接用加锁的方式</p><h3 id="事务隔离的实现"><a class="markdownIt-Anchor" href="#事务隔离的实现"></a> 事务隔离的实现</h3><p>这里展开说明“可重复读”</p><p>在MySQL中,每条记录在更新时都会同时记录一条回滚操作。假设一个值从1按顺序改成了2、3、4,在回滚日志里有类似下面的记录</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230126-160002573.png" alt="图 3" /></p><p>当前值是4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的read-view。视图A、B、C对应的值分别是1、2、4,同一条记录在系统中可以存在多个版本,这就是数据库的多版本并发控制(MVCC)。对于视图A,要得到1,就必须将当前值依次执行图中所有的回滚操作。即使现在有另一个事务正在将4改成5,这个事务跟视图A、B、C对应的事务是不会冲突的。</p><p>系统会将没有比回滚日志更早的read-view时删掉这个回滚日志。因此尽量不要使用长事务,系统里面会存在很老的事务视图</p><h3 id="事务的启动方式"><a class="markdownIt-Anchor" href="#事务的启动方式"></a> 事务的启动方式</h3><p>MySQL的事务启动方式有如下几种:</p><ol><li>显式启动事务,begin 或 start transaction。配套的提交语句是commit,回滚语句是rollback</li><li>隐式启动事务,一条SQL语句会自动开启一个事务。需要设置autocommit = 1 才会自动提交</li></ol><p>set autocommit=0,会将这个线程的自动提交关掉。事务持续存在直到你主动执行commit 或 rollback语句,或者断开连接</p><p>建议使用set autocommit=1,并显示启动事务</p><p>在autocommit为1的情况下,用begin显式启动的事务,如果执行commit则提交事务。如果执行 commit work and chain,则是提交事务并自动启动下一个事务,这样也省去了再次执行begin语句的开销</p><p>可以在information_schema库的innodb_trx这个表中查询长事务,比如下面这个语句,用于查找持续时间超过60s的事务。</p><pre class="highlight"><code class="SQL"><span class="hljs-keyword">select</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> information_schema.innodb_trx <span class="hljs-keyword">where</span> TIME_TO_SEC(timediff(now(),trx_started))<span class="hljs-operator">></span><span class="hljs-number">60</span></code></pre><p>如何避免长事务对业务的影响?</p><p>从应用端来看</p><ol><li>通过MySQL的general_log确保autocommit=1</li><li>包含多个select语句的只读事务,没有一致性要求就拆分</li><li>通过SET MAX_EXECUTION_TIME控制每个语句的最长执行时间</li></ol><p>从数据库端来看</p><ol><li>监控 information_schema.Innodb_trx表,设置长事务阈值,超过就kill</li><li>推荐使用Percona的pt-kill</li><li>业务测试阶段就输出所有general_log,分析日志提前发现问题</li><li>innodb_undo_tablespaces设置成2或更大的值</li></ol><h2 id="深入浅出索引上"><a class="markdownIt-Anchor" href="#深入浅出索引上"></a> 深入浅出索引(上)</h2><p>索引的出现是为了提高数据查询的效率</p><h3 id="索引的常见模型"><a class="markdownIt-Anchor" href="#索引的常见模型"></a> 索引的常见模型</h3><p>哈希表,只适用于只有等值查询的场景,不适用于范围查询</p><p>有序数组在等值查询和范围查询场景中都非常优秀,但更新数据需要挪动数组元素,成本太高。只适用于静态存储引擎(数据不再变化)</p><p>平衡二叉查找树的时间复杂度是O(log(N)),但是算上每次访问节点的磁盘IO开销,查询非常慢。为了减少磁盘IO次数,出现了N叉树</p><h3 id="innodb的索引模型"><a class="markdownIt-Anchor" href="#innodb的索引模型"></a> InnoDB的索引模型</h3><p>根据叶子节点内容,索引类型分为主键索引和非主键索引</p><p>主键索引(聚簇索引):叶子节点存的是整行数据<br />普通索引(二级索引):叶子结点存的是主键的值</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230126-190532664.png" alt="图 4" /></p><p>基于主键索引和普通索引的查询的区别:</p><ul><li>如果语句是select * from T where ID=500,即主键查询方式,则只需要搜索ID这棵B+树;</li><li>如果语句是select * from T where k=5,即普通索引查询方式,则需要先搜索k索引树,得到ID的值为500,再到ID索引树搜索一次。这个过程称为回表。</li></ul><h3 id="索引维护"><a class="markdownIt-Anchor" href="#索引维护"></a> 索引维护</h3><p>在插入新记录时,B+树为了维护有序性会进行页分裂和页合并</p><p>自增主键 VS 业务字段主键</p><p>性能上:自增主键按序插入,不会触发叶子节点的分裂,而业务字段做主键往往不是有序插入,导致页分裂和页合并,性能差<br />存储空间上:主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。业务字段主键是身份证号(20字节)不如自增主键(4字节或8字节)</p><p>业务字段做主键的场景是:</p><ol><li>只有一个索引</li><li>该索引必须是唯一索引</li></ol><p>这就是典型的KV场景,直接将这个字段设置为主键</p><h2 id="深入浅出索引下"><a class="markdownIt-Anchor" href="#深入浅出索引下"></a> 深入浅出索引(下)</h2><h3 id="覆盖索引"><a class="markdownIt-Anchor" href="#覆盖索引"></a> 覆盖索引</h3><p>覆盖索引:索引的叶子节点可以直接提供查询结果,不需要回表</p><p>可以为高频请求建立联合索引起到覆盖索引的作用</p><h3 id="最左前缀原则"><a class="markdownIt-Anchor" href="#最左前缀原则"></a> 最左前缀原则</h3><p>索引项是按照索引定义里面出现的字段的顺序排序的。满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左N个字段,也可以是字符串索引的最左M个字符</p><p>索引内的字段顺序评估标准:</p><ol><li>复用能力,如果该顺序的联合索引能少维护一个索引,那么该顺序优先使用</li><li>空间,如果必须维护联合索引和单独索引,那么给小字段单独索引,联合索引的顺序是(大字段,小字段)</li></ol><h3 id="索引下推"><a class="markdownIt-Anchor" href="#索引下推"></a> 索引下推</h3><p>在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足的条件的记录,减少回表次数(联合索引在按最左匹配时碰到范围查询停止,索引下推可以对后面的索引字段做条件判断后再返回结果集)</p><h2 id="全局锁和表锁"><a class="markdownIt-Anchor" href="#全局锁和表锁"></a> 全局锁和表锁</h2><p>根据加锁的范围,MySQL里面的锁大致可以分成全局锁、表级锁和行锁三类</p><h3 id="全局锁"><a class="markdownIt-Anchor" href="#全局锁"></a> 全局锁</h3><p>全局锁是对整个数据库实例加锁,让整个库处于只读状态</p><pre class="highlight"><code class="SQL">Flush tables <span class="hljs-keyword">with</span> read lock</code></pre><p>全局锁的典型使用场景是不支持MVCC的引擎(MyISAM)的全库逻辑备份,如果所有表的引擎支持MVCC,可以在备份时开启事务确保拿到一致性视图(mysqldump加上参数-single-transaction)</p><p>让全库只读,另外一种方式是<code>set global readonly = true</code>,但仍然建议使用FTWRL,因为:</p><ol><li>readonly的值可能会用来做其它逻辑,比如判断是主库还是备库</li><li>FTWRL在客户端发生异常断开时,MySQL会自动释放全局锁,而readonly会一直保持</li></ol><h3 id="表级锁"><a class="markdownIt-Anchor" href="#表级锁"></a> 表级锁</h3><p>表级锁有两种:表锁,元数据锁(meta data lock,MDL)</p><h4 id="表锁"><a class="markdownIt-Anchor" href="#表锁"></a> 表锁</h4><p>语法:lock tables … read/write</p><p>表锁会限制其它线程的读写,也会限定本线程的操作对象</p><p>例如,线程A执行<code>lock tables t1 read, t2 write;</code>,其它线程写t1、读写t2都会被阻塞,线程A只能执行读t1、读写t2,不能访问其它表</p><p>如果支持行锁,一般不使用表锁</p><h4 id="元数据锁"><a class="markdownIt-Anchor" href="#元数据锁"></a> 元数据锁</h4><p>MDL不需要显示使用,在访问表时会被自动加上,事务提交才释放,作用是保证读写的正确性</p><p>当对表做增删改查操作时,加MDL读锁;当对表结构变更时,加MDL写锁</p><ul><li>读锁之间不互斥,因此可以有多个线程同时对一张表增删查改</li><li>读写锁之间、写锁之间是互斥的。因此如果有两个线程要同时给一个表加字段,其中一个要等另一执行完再执行</li></ul><p>给表加字段的方式:</p><ol><li>kill掉长事务,事务不提交会一直占着MDL</li><li>在alter table语句设置等待时间,如果在等待时间内能拿到MDL写锁最好,拿不到也不要阻塞后面的业务语句。后面重试这个过程</li></ol><h2 id="行锁"><a class="markdownIt-Anchor" href="#行锁"></a> 行锁</h2><p>行锁是针对表中行记录的锁</p><h3 id="两阶段锁"><a class="markdownIt-Anchor" href="#两阶段锁"></a> 两阶段锁</h3><p>两阶段锁协议:在InnoDB事务中,行锁是在需要的时候加上的,但并不是不需要了就立刻释放,而是要等到事务结束才释放</p><p>如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放</p><h3 id="死锁和死锁检测"><a class="markdownIt-Anchor" href="#死锁和死锁检测"></a> 死锁和死锁检测</h3><p>死锁:并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源,导致这几个线程无限等待</p><p>死锁出现后有两种策略:</p><ul><li>设置等待时间,修改innodb_lock_wait_timeout</li><li>发起死锁检测,主动回滚死锁链条中的某一个事务,让其他事务可以继续执行。innodb_deadlock_detect设置为on表示开启</li></ul><p>第一种策略,等待时间太长,业务的用户接受不了,等待时间太短会出现误伤。所以一般用死锁检测</p><p>死锁检测有性能问题,解决思路有几种:</p><ul><li>如果能确保业务一定不会出现死锁,可以临时把死锁检测关掉。这种方法存在业务有损的风险,业务逻辑碰到死锁会回滚重试,但是没有死锁检测会超时导致业务有损</li><li>控制并发程度。数据库Server层实现,对相同行的更新,在进入引擎之前排队</li><li>将一行改成逻辑上的多行。例如账户余额等于10行之和,扣钱时随机扣一行,这种方案需要根据业务逻辑做详细设计</li></ul><h2 id="详解事务隔离"><a class="markdownIt-Anchor" href="#详解事务隔离"></a> 详解事务隔离</h2><p>假如有如下表和事务A、B、C</p><pre class="highlight"><code class="SQL">mysql<span class="hljs-operator">></span> <span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> `t` ( `id` <span class="hljs-type">int</span>(<span class="hljs-number">11</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">NULL</span>, `k` <span class="hljs-type">int</span>(<span class="hljs-number">11</span>) <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">NULL</span>, <span class="hljs-keyword">PRIMARY</span> KEY (`id`)) ENGINE<span class="hljs-operator">=</span>InnoDB;<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> t(id, k) <span class="hljs-keyword">values</span>(<span class="hljs-number">1</span>,<span class="hljs-number">1</span>),(<span class="hljs-number">2</span>,<span class="hljs-number">2</span>);</code></pre><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230127-213705932.png" alt="图 5" /></p><h3 id="快照在mvcc里是怎么工作的"><a class="markdownIt-Anchor" href="#快照在mvcc里是怎么工作的"></a> "快照"在MVCC里是怎么工作的</h3><p>快照不是整个数据库的拷贝。</p><p>InnoDB里每个事务都有一个唯一的transaction id,是事务开始时申请的,严格递增的。每行数据有多个版本,每次事务更新数据时,都会生成一个新的数据版本,并把transaction id赋给这个数据版本的事务id,记为row trx_id。某个版本的数据可以通过当前版本和undo log计算出来</p><p>在实现上,InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务ID。“活跃”指的是启动了但还没提交</p><p>数组里面事务ID的最小值记为低水位,当前系统里面已经创建过的事务ID的最大值加1记为高水位</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230127-215120184.png" alt="图 6" /></p><p>对于当前事务的启动瞬间来说,一个数据版本的row trx_id,有以下几种可能:</p><ol><li>如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;</li><li>如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;</li><li>如果落在黄色部分,那就包括两种情况<br />a. 若 row trx_id在数组中,表示这个版本是由还没提交的事务生成的,不可见;<br />b. 若 row trx_id不在数组中,表示这个版本是已经提交了的事务生成的,可见。</li></ol><p>InnoDB利用了“所有数据都有多个版本”的特性,实现了“秒级创建快照”的能力</p><p>可以用时间顺序来理解版本的可见性。</p><p>一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:</p><ol><li>版本未提交,不可见;</li><li>版本已提交,但是是在视图创建后提交的,不可见</li><li>版本已提交,而且是在视图创建前提交的,可见</li></ol><h3 id="更新逻辑"><a class="markdownIt-Anchor" href="#更新逻辑"></a> 更新逻辑</h3><p>更新数据都是先读后写,读是当前值,称为“当前读”(current read)。所以事务B是在(1,2)上进行修改</p><p>select如果加锁,也是当前读,不加就是一致读,下面两个select语句分别加了读锁(S锁,共享锁)和写锁(X锁,排它锁)。行锁包括共享锁和排它锁</p><pre class="highlight"><code class="SQL">mysql<span class="hljs-operator">></span> <span class="hljs-keyword">select</span> k <span class="hljs-keyword">from</span> t <span class="hljs-keyword">where</span> id<span class="hljs-operator">=</span><span class="hljs-number">1</span> lock <span class="hljs-keyword">in</span> share mode;mysql<span class="hljs-operator">></span> <span class="hljs-keyword">select</span> k <span class="hljs-keyword">from</span> t <span class="hljs-keyword">where</span> id<span class="hljs-operator">=</span><span class="hljs-number">1</span> <span class="hljs-keyword">for</span> <span class="hljs-keyword">update</span>;</code></pre><p>假设事务C变成了事务C’</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230127-220337299.png" alt="图 7" /></p><p>事务C’还没提交,但是生成了最新版本(1,2),根据“两阶段协议”,(1,2)这个版本上的写锁还没释放,事务B的更新是当前读,需要加锁,所以被阻塞</p><p>可重复读的核心就是一致性读(consistent read);而事务更新数据时只能用当前读,如果当前的记录的行锁被其他事务占用的话,就进入锁等待。</p><p>读提交的逻辑和可重复读的逻辑类似,主要区别是:</p><ul><li>在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;</li><li>在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。</li></ul><h2 id="普通索引和唯一索引之间的选择"><a class="markdownIt-Anchor" href="#普通索引和唯一索引之间的选择"></a> 普通索引和唯一索引之间的选择</h2><p>普通索引VS唯一索引:两者类似,区别是唯一索引的列值不能重复,允许一个为空</p><p>下面从这两种索引对查询语句和更新语句的性能来分析,前提是业务保证记录的唯一性,如果业务不能保证唯一性又有唯一需求,就必须用唯一索引</p><h3 id="查询过程"><a class="markdownIt-Anchor" href="#查询过程"></a> 查询过程</h3><p>普通索引:查找到满足条件的第一个记录后,需要查找下一个记录,直到碰到第一个不满足条件的记录</p><p>唯一索引:由于唯一性,查找到满足条件的第一个记录后就停止</p><p>由于InnoDB的数据是按数据页为单位来读写,所以两者性能差距微乎其微</p><h3 id="更新过程"><a class="markdownIt-Anchor" href="#更新过程"></a> 更新过程</h3><p>change buffer:当需要更新一个数据页时,如果数据页在内存中就直接更新,如果不在,在不影响数据一致性的前提下,InnoDB会将这些更新操作缓存在change buffer中。在下次查询需要访问这个数据页时,读入内存,然后执行change buffer中与这个页有关的操作,这个过程称为merge</p><p>唯一索引的更新不能用change buffer,因为需要先将数据页读入内存判断操作是否违反唯一性约束</p><p>假如现在有个插入新记录的操作,如果要更新的目标页在内存中,普通索引和唯一索引性能差距不大。如果目标页不在内存中,对于唯一索引来说,需要将数据页读入内存,判断到没有冲突,插入这个值;对于普通索引来说,将更新记录在change buffer,此时普通索引的性能好(主键索引的数据页是一定要加载进内存做更新操作,普通索引的数据页不用进内存)</p><h3 id="change-buffer的使用场景"><a class="markdownIt-Anchor" href="#change-buffer的使用场景"></a> change buffer的使用场景</h3><p>因为merge的时候是真正做数据更新的时候,在merge之前,change buffer记录的变更越多,收益越大</p><p>对于写多读少的业务,change buffer的效果最好,比如账单类、日志类的系统</p><h3 id="索引的选择和实践"><a class="markdownIt-Anchor" href="#索引的选择和实践"></a> 索引的选择和实践</h3><p>尽量使用普通索引</p><p>如果更新完马上查询,就关闭change buffer。否则开着能提升更新性能</p><h3 id="change-buffer-和-redo-log"><a class="markdownIt-Anchor" href="#change-buffer-和-redo-log"></a> change buffer 和 redo log</h3><p>redo log 主要节省的是随机写磁盘的IO消耗(转成顺序写),而change buffer主要节省的则是随机读磁盘的IO消耗。</p><h2 id="mysql为什么有时候会选错索引"><a class="markdownIt-Anchor" href="#mysql为什么有时候会选错索引"></a> MySQL为什么有时候会选错索引</h2><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230128-145832228.png" alt="图 8" /></p><p>session B 先删除了所有数据然后调用idata存储过程插入了10万行数据。</p><p>然后session B 执行三条SQL:</p><pre class="highlight"><code class="SQL"><span class="hljs-operator">/</span><span class="hljs-operator">/</span> 将慢查询日志的阈值设置为<span class="hljs-number">0</span>,表示这个线程接下来的语句都会被记录慢查询日志中<span class="hljs-keyword">set</span> long_query_time<span class="hljs-operator">=</span><span class="hljs-number">0</span>;<span class="hljs-keyword">select</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> t <span class="hljs-keyword">where</span> a <span class="hljs-keyword">between</span> <span class="hljs-number">10000</span> <span class="hljs-keyword">and</span> <span class="hljs-number">20000</span>; <span class="hljs-comment">/*Q1*/</span><span class="hljs-keyword">select</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> t force index(a) <span class="hljs-keyword">where</span> a <span class="hljs-keyword">between</span> <span class="hljs-number">10000</span> <span class="hljs-keyword">and</span> <span class="hljs-number">20000</span>;<span class="hljs-comment">/*Q2*/</span></code></pre><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230128-150139525.png" alt="图 9" /></p><p>Q1走了全表扫描,Q2使用了正确的索引</p><h3 id="优化器的逻辑"><a class="markdownIt-Anchor" href="#优化器的逻辑"></a> 优化器的逻辑</h3><p>选择索引是优化器的工作,目的是寻找最优方案执行语句,判断标准包括扫描行数、是否使用临时表、是否排序等因素</p><p>上面查询语句没有涉及临时表和排序,说明扫描行数判断错误了</p><h4 id="mysql是怎么判断扫描行数的"><a class="markdownIt-Anchor" href="#mysql是怎么判断扫描行数的"></a> MySQL是怎么判断扫描行数的</h4><p>MySQL在真正开始执行语句之前,并不能精确知道有多少行,而只能用统计信息估算。这个统计信息就是索引的“区分度”,索引上不同的值称为“基数”,基数越大,区分度越好。基数由采样统计得出。</p><p>如果统计信息不对导致行数和实际情况差距较大,可以使用<code>analyze table 表名</code> 来重新统计索引信息</p><h3 id="索引选择异常和处理"><a class="markdownIt-Anchor" href="#索引选择异常和处理"></a> 索引选择异常和处理</h3><p>由于索引统计信息不准确导致的问题,可以用analyze table来解决,其它优化器误判的解决方法如下:</p><ul><li>使用force index强行选择索引。缺点是变更不及时,开发通常不写force index,当生产环境出现问题,再修改需要重新测试和发布</li><li>修改语句,引导MySQL使用我们期望的索引。缺点是需要根据数据特征进行修改,不具备通用性</li><li>新建更合适的索引或删掉误用的索引。缺点是找到更合适的索引比较困难</li></ul><h2 id="怎么给字符串字段加索引"><a class="markdownIt-Anchor" href="#怎么给字符串字段加索引"></a> 怎么给字符串字段加索引</h2><p>可以给字符串字段建立一个普通索引,也可以给字符串前缀建立普通索引。使用前缀索引,定义好长度,就可以既节省空间,又不用额外增加太多查询成本</p><p>可以通过统计索引上有多少个不同的值来判断使用多长的前缀,不同值越多,区分度越高,查询性能越好</p><p>首先算出这列有多少不同值</p><pre class="highlight"><code class="SQL">mysql<span class="hljs-operator">></span> <span class="hljs-keyword">select</span> <span class="hljs-built_in">count</span>(<span class="hljs-keyword">distinct</span> email) <span class="hljs-keyword">as</span> L <span class="hljs-keyword">from</span> SUser;</code></pre><p>然后选取不同长度的前缀来看这个值</p><pre class="highlight"><code class="SQL">mysql<span class="hljs-operator">></span> <span class="hljs-keyword">select</span> <span class="hljs-built_in">count</span>(<span class="hljs-keyword">distinct</span> <span class="hljs-keyword">left</span>(email,<span class="hljs-number">4</span>))<span class="hljs-keyword">as</span> L4, <span class="hljs-built_in">count</span>(<span class="hljs-keyword">distinct</span> <span class="hljs-keyword">left</span>(email,<span class="hljs-number">5</span>))<span class="hljs-keyword">as</span> L5, <span class="hljs-built_in">count</span>(<span class="hljs-keyword">distinct</span> <span class="hljs-keyword">left</span>(email,<span class="hljs-number">6</span>))<span class="hljs-keyword">as</span> L6, <span class="hljs-built_in">count</span>(<span class="hljs-keyword">distinct</span> <span class="hljs-keyword">left</span>(email,<span class="hljs-number">7</span>))<span class="hljs-keyword">as</span> L7,<span class="hljs-keyword">from</span> SUser;</code></pre><h3 id="前缀索引对覆盖索引的影响"><a class="markdownIt-Anchor" href="#前缀索引对覆盖索引的影响"></a> 前缀索引对覆盖索引的影响</h3><p>前缀索引可能会增加扫描行数,导致影响性能,还可能导致用不上覆盖索引对查询的优化。</p><p>前缀索引的叶子节点只包含主键,如果查询字段不仅仅有主键,那必须回表。而用完整字符串做索引,如果查询字段只有主键和索引字段,那就不用回表</p><h3 id="其它方式"><a class="markdownIt-Anchor" href="#其它方式"></a> 其它方式</h3><p>对于邮箱来说,前缀索引的效果不错。<br />但是对于身份证来说,可能需要长度12以上的前缀索引,才能满足区分度要求,但是这样又太占空间了</p><p>有一些占用空间更小但是查询效率相同的方法:</p><ol><li>倒序存储身份证号,建立长度为6的前缀索引,身份证后6位可以提供足够的区分度</li><li>加个身份证的整型hash字段,给这个字段加索引</li></ol><p>这两种方法的相同点是都不支持范围查询,区别在于:</p><ol><li>倒序存储不占额外空间</li><li>倒序每次写和读都需要额外调用一次reverse函数,hash字段需要额外调用一次crc32函数,reverse稍优</li><li>hash字段的查询性能更稳定一些</li></ol><h2 id="为什么mysql为抖一下"><a class="markdownIt-Anchor" href="#为什么mysql为抖一下"></a> 为什么MySQL为“抖”一下</h2><p>“抖”:SQL语句偶尔会执行特别慢,且随机出现,持续时间很短</p><p>当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。</p><p>平时执行很快的更新操作,其实就是在写内存和日志,“抖”可能是在刷脏页(flush),情况有以下几种:</p><ol><li>redo log满了,系统会停止所有更新操作,把checkpoint往前推进,原位置和新位置之间的所有脏页都flush到磁盘上。尽量避免这种情况,会阻塞更新操作</li><li>系统内存不足,淘汰脏页。尽量避免一个查询要淘汰的脏页太多</li><li>系统空闲</li><li>正常关闭</li></ol><h3 id="innodb刷脏页的控制策略"><a class="markdownIt-Anchor" href="#innodb刷脏页的控制策略"></a> InnoDB刷脏页的控制策略</h3><p>使用fio测试磁盘的IOPS,并把innodb_io_capacity设置成这个值,告诉InnoDB全力刷盘可以刷多快</p><pre class="highlight"><code class="SQL">fio <span class="hljs-operator">-</span>filename<span class="hljs-operator">=</span>$filename <span class="hljs-operator">-</span>direct<span class="hljs-operator">=</span><span class="hljs-number">1</span> <span class="hljs-operator">-</span>iodepth <span class="hljs-number">1</span> <span class="hljs-operator">-</span>thread <span class="hljs-operator">-</span>rw<span class="hljs-operator">=</span>randrw <span class="hljs-operator">-</span>ioengine<span class="hljs-operator">=</span>psync <span class="hljs-operator">-</span>bs<span class="hljs-operator">=</span><span class="hljs-number">16</span>k <span class="hljs-operator">-</span>size<span class="hljs-operator">=</span><span class="hljs-number">500</span>M <span class="hljs-operator">-</span>numjobs<span class="hljs-operator">=</span><span class="hljs-number">10</span> <span class="hljs-operator">-</span>runtime<span class="hljs-operator">=</span><span class="hljs-number">10</span> <span class="hljs-operator">-</span>group_reporting <span class="hljs-operator">-</span>name<span class="hljs-operator">=</span>mytest</code></pre><p>不能总是全力刷盘,InnoDB刷盘速度还要参考内存脏页比例和redo log写盘速度</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230129-105732893.png" alt="图 10" /></p><p>脏页比例不要经常接近75%,查看命令如下:</p><pre class="highlight"><code class="SQL">mysql<span class="hljs-operator">></span> <span class="hljs-keyword">select</span> VARIABLE_VALUE <span class="hljs-keyword">into</span> <span class="hljs-variable">@a</span> <span class="hljs-keyword">from</span> global_status <span class="hljs-keyword">where</span> VARIABLE_NAME <span class="hljs-operator">=</span> <span class="hljs-string">'Innodb_buffer_pool_pages_dirty'</span>;<span class="hljs-keyword">select</span> VARIABLE_VALUE <span class="hljs-keyword">into</span> <span class="hljs-variable">@b</span> <span class="hljs-keyword">from</span> global_status <span class="hljs-keyword">where</span> VARIABLE_NAME <span class="hljs-operator">=</span> <span class="hljs-string">'Innodb_buffer_pool_pages_total'</span>;<span class="hljs-keyword">select</span> <span class="hljs-variable">@a</span><span class="hljs-operator">/</span><span class="hljs-variable">@b</span>;</code></pre><p>还有个策略是刷盘的“连坐”机制:脏页的邻居如果是脏页会一起被刷盘。这种策略对机械硬盘有大幅度性能提升,但是SSD的IOPS已不是瓶颈,推荐innodb_flush_neighbors设置成0,只刷自己</p><h2 id="为什么表数据删掉一半表文件大小不变"><a class="markdownIt-Anchor" href="#为什么表数据删掉一半表文件大小不变"></a> 为什么表数据删掉一半,表文件大小不变</h2><p>InnoDB表包含两部分:表结构定义和数据。MySQL 8.0 之前,表结构是存在以.frm为后缀的文件,MySQL 8.0 允许表结构定义放在系统数据表中</p><h3 id="参数innodb_file_per_table"><a class="markdownIt-Anchor" href="#参数innodb_file_per_table"></a> 参数innodb_file_per_table</h3><p>设置成OFF表示,表的数据放在系统共享表空间,也就是跟数据字典放在一起;<br />设置成ON表示,每个InnoDB表数据存储在一个以.ibd为后缀的文件中</p><p>从MySQL 5.6.6开始默认就是ON</p><h3 id="数据删除流程"><a class="markdownIt-Anchor" href="#数据删除流程"></a> 数据删除流程</h3><p>InnoDB里的数据都是用B+数的结构组织的。</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230129-152037596.png" alt="图 11" /></p><p>记录的复用:删除R4记录时,InnoDB会把记录标记为删除,插入ID在300到600之间的记录时可能会复用这个位置,磁盘文件大小不会缩小</p><p>数据页的复用:InnoDB的数据是按页存储的。如果将page A上所有记录删除以后,page A会被标记为可复用,这时候插入ID=50的记录需要使用新页时,page A会被复用。因此,delete整个表会把所有数据页都标记为可复用,但是磁盘文件不会变小</p><p>可以复用,而没被使用的空间,看起来就像是“空洞”,不只是删除数据会造成空洞,随机插入数据会引发索引的数据页分裂,导致空洞。更新索引上的值,可以理解为删除旧值和插入新值,也会造成空洞。解决空洞的方法是重建表</p><h3 id="重建表"><a class="markdownIt-Anchor" href="#重建表"></a> 重建表</h3><p>可以使用alter table A engine=InnoDB命令来重建表。MySQL 5.6是离线重建,重建期间更新会丢失。</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230215-161007040.png" alt="图 39" /><br />MySQL 5.6 引入了Online DDL,重建表的流程:</p><ol><li>建立一个临时文件,扫描表A主键的所有数据页</li><li>用数据页中表A的记录生成B+数,存储到临时文件中</li><li>生成临时文件的过程中,将所有对A的操作记录在一个日志文件(row log)中,对应图中state2状态</li><li>临时文件生成后,将日志文件中的操作应用到临时文件,得到一个逻辑数据上与表A相同的数据文件,对应图中state3状态</li><li>用临时文件替换表A的数据文件</li></ol><p>alter语句在启动时需要获取MDL写锁,这个写锁在真正拷贝数据之前就退化成读锁了,目的是实现Online,MDL读锁不会阻塞记录的增删改操作(DML)</p><p>推荐使用gh-ost做大表的重建</p><h3 id="online-和-inplace"><a class="markdownIt-Anchor" href="#online-和-inplace"></a> Online 和 inplace</h3><p>inplace是指整个DDL过程在 InnoDB 内部完成,对于 Server层来说,没有把数据挪动到临时表,这是一个“原地”操作,这就是inplace名称由来</p><p>和inplace对应的是copy,也就是前面离线重建</p><p>DDL过程如果是 Online 的,就一定是inplace的;反过来未必,全文索引和空间索引是 inplace 的,但不是 Online 的</p><p>optimize table、analyze table和alter table三种方式重建表的区别:</p><ul><li>从MySQL 5.6开始,alter table t engine=InnoDB(也就是recreate)默认就是上面引入Online DDL后的重建过程</li><li>analyze table t 不是重建表,只是对表的索引信息做重新统计,没有修改数据,这个过程中加了MDL读锁</li><li>optimize table t 等于recreate+analyze</li></ul><h2 id="count慢该怎么办"><a class="markdownIt-Anchor" href="#count慢该怎么办"></a> count(*)慢该怎么办</h2><h3 id="count的实现方式"><a class="markdownIt-Anchor" href="#count的实现方式"></a> count(*)的实现方式</h3><p>InnoDB count(*)会遍历全表,优化器会找到最小的索引数进行计数,结果准确但有性能问题。show table status命令显示的行数是采样估算的,不准确</p><h3 id="用缓存系统保存计数"><a class="markdownIt-Anchor" href="#用缓存系统保存计数"></a> 用缓存系统保存计数</h3><p>可以用Redis来保存记录数,但是会出现逻辑上不精确的问题。根本原因是这两个不同的存储构成的系统,不支持分布式事务,无法拿到精确一致的视图</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230129-164341919.png" alt="图 12" /><br />这种情况是Redis的计数不精确</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230129-164422649.png" alt="图 13" /><br />这种情况是查询结果不精确</p><h3 id="在数据库保存计数"><a class="markdownIt-Anchor" href="#在数据库保存计数"></a> 在数据库保存计数</h3><p>将计数放在数据库里单独的一张计数表中,可以利用事务解决计数不精确的问题</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230129-165101821.png" alt="图 14" /></p><p>在会话B读操作期间,会话A还没提交事务,因此B没有看到计数值加1的操作,因此计数值和“最近100条记录”的结果在逻辑上是一致的</p><h3 id="不同的count用法"><a class="markdownIt-Anchor" href="#不同的count用法"></a> 不同的count用法</h3><p>count(*)、count(主键id)和count(1) 都表示返回满足条件的结果集的总行数。count(字段),则表示返回满足条件的数据行里面,参数“字段”不为NULL的总个数</p><p>对于count(主键id)来说,InnoDB引擎会遍历整张表,把每一行的id值都取出来,返回给server层。server层拿到id后,判断是不可能为空的,就按行累加。</p><p>对于count(1)来说,InnoDB引擎遍历整张表,但不取值。server层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加。</p><p>count(1)执行得要比count(主键id)快。因为从引擎返回id会涉及到解析数据行,以及拷贝字段值的操作。</p><p>对于count(字段)来说:</p><p>如果这个“字段”是定义为not null的话,一行行地从记录里面读出这个字段,判断不能为null,按行累加;</p><p>如果这个“字段”定义允许为null,那么执行的时候,判断到有可能是null,还要把值取出来再判断一下,不是null才累加。</p><p>count(*) 是例外,并不会把全部字段取出来,而是专门做了优化,不取值。count(*)肯定不是null,按行累加。</p><p>结论是:按照效率排序的话,<br />count(字段) < count(主键id) < count(1) ≈ count(*),所以尽量使用count(*)。</p><h2 id="orderby是怎么工作的"><a class="markdownIt-Anchor" href="#orderby是怎么工作的"></a> orderby是怎么工作的</h2><p>假设有SQL语句</p><pre class="highlight"><code class="SQL"><span class="hljs-keyword">select</span> city,name,age <span class="hljs-keyword">from</span> t <span class="hljs-keyword">where</span> city<span class="hljs-operator">=</span><span class="hljs-string">'杭州'</span> <span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> name limit <span class="hljs-number">1000</span>;</code></pre><h3 id="全字段排序"><a class="markdownIt-Anchor" href="#全字段排序"></a> 全字段排序</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230130-103507466.png" alt="图 15" /></p><p>如果要排序的数据量小于sort_buffer_size,排序就在内存中完成,否则外部排序(归并)</p><h3 id="rowid-排序"><a class="markdownIt-Anchor" href="#rowid-排序"></a> rowid 排序</h3><p>max_length_for_sort_data 是MySQL中专门控制用于排序的行数据的长度的参数,超过这个值就不会全字段排序,用rowid排序</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230130-104412769.png" alt="图 16" /></p><h3 id="全字段排序-vs-rowid排序"><a class="markdownIt-Anchor" href="#全字段排序-vs-rowid排序"></a> 全字段排序 VS rowid排序</h3><p>如果内存够就用全字段排序,rowid排序回表多造成磁盘读,性能较差</p><p>并不是所有的order by语句都要排序的,如果建索引时就是有序的就不排</p><p>创建一个city和name的联合索引,查询过程如下:</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230130-110648453.png" alt="图 17" /></p><p>还可以创建city、name和age的联合索引,这样就不用回表了</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230130-132603879.png" alt="图 18" /></p><h2 id="如何正确地显示随机消息"><a class="markdownIt-Anchor" href="#如何正确地显示随机消息"></a> 如何正确地显示随机消息</h2><p>10000行记录如何随机选择3个</p><h3 id="内存临时表"><a class="markdownIt-Anchor" href="#内存临时表"></a> 内存临时表</h3><p>用order by rand()来实现这个逻辑</p><pre class="highlight"><code class="SQL">mysql<span class="hljs-operator">></span> <span class="hljs-keyword">select</span> word <span class="hljs-keyword">from</span> words <span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> rand() limit <span class="hljs-number">3</span>;</code></pre><p>R:随机数,W:单词,pos:rowid,对于有主键的表,rowid就是主键ID,没有主键就由系统生成</p><p>原表->内存临时表:扫描10000行<br />内存临时表->sort_buffer:扫描10000行<br />内存临时表->结果集:访问3行</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230130-164151861.png" alt="图 19" /></p><p>order by rand()使用了内存临时表,内存临时表排序的时候使用了rowid排序方法</p><h3 id="磁盘临时表"><a class="markdownIt-Anchor" href="#磁盘临时表"></a> 磁盘临时表</h3><p>当内存临时表大小超过了tmp_table_size时,如果使用归并排序,内存临时表会转为磁盘临时表,如果使用优先队列排序(排序+limit操作),且维护的堆大小不超过sort_buffer_size,则不会转为磁盘临时表</p><h3 id="随机排序方法"><a class="markdownIt-Anchor" href="#随机排序方法"></a> 随机排序方法</h3><ol><li>取得整个表的行数,记为C</li><li>取得 Y = floor(C * rand())</li><li>再用 limit Y,1 取得一行</li></ol><p>取多个随机行就重复多次这个算法</p><pre class="highlight"><code class="SQL">mysql<span class="hljs-operator">></span> <span class="hljs-keyword">select</span> <span class="hljs-built_in">count</span>(<span class="hljs-operator">*</span>) <span class="hljs-keyword">into</span> <span class="hljs-variable">@C</span> <span class="hljs-keyword">from</span> t;<span class="hljs-keyword">set</span> <span class="hljs-variable">@Y1</span> <span class="hljs-operator">=</span> <span class="hljs-built_in">floor</span>(<span class="hljs-variable">@C</span> <span class="hljs-operator">*</span> rand());<span class="hljs-keyword">set</span> <span class="hljs-variable">@Y2</span> <span class="hljs-operator">=</span> <span class="hljs-built_in">floor</span>(<span class="hljs-variable">@C</span> <span class="hljs-operator">*</span> rand());<span class="hljs-keyword">set</span> <span class="hljs-variable">@Y3</span> <span class="hljs-operator">=</span> <span class="hljs-built_in">floor</span>(<span class="hljs-variable">@C</span> <span class="hljs-operator">*</span> rand());<span class="hljs-keyword">select</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> t limit <span class="hljs-variable">@Y1</span>,<span class="hljs-number">1</span>; <span class="hljs-operator">/</span><span class="hljs-operator">/</span>在应用代码里面取Y1、Y2、Y3值,拼出<span class="hljs-keyword">SQL</span>后执行<span class="hljs-keyword">select</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> t limit <span class="hljs-variable">@Y2</span>,<span class="hljs-number">1</span>;<span class="hljs-keyword">select</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> t limit <span class="hljs-variable">@Y3</span>,<span class="hljs-number">1</span>;</code></pre><p>或者优化一下,Y1,Y2,Y3从小到大排序,这样扫描的行数就是Y3</p><pre class="highlight"><code class="SQL">id1 <span class="hljs-operator">=</span> <span class="hljs-keyword">select</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> t limit <span class="hljs-variable">@Y1</span>,<span class="hljs-number">1</span>;id2 <span class="hljs-operator">=</span> <span class="hljs-keyword">select</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> t <span class="hljs-keyword">where</span> id <span class="hljs-operator">></span> id1 limit <span class="hljs-variable">@Y2</span><span class="hljs-operator">-</span><span class="hljs-variable">@Y1</span>,<span class="hljs-number">1</span>;id3 <span class="hljs-operator">=</span> <span class="hljs-keyword">select</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> t <span class="hljs-keyword">where</span> id <span class="hljs-operator">></span> id2 limit <span class="hljs-variable">@Y3</span></code></pre><h2 id="为什么逻辑相同的sql语句性能差异巨大"><a class="markdownIt-Anchor" href="#为什么逻辑相同的sql语句性能差异巨大"></a> 为什么逻辑相同的SQL语句性能差异巨大</h2><ol><li>对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器决定放弃走树搜索功能,但不是放弃索引,优化器可以选择遍历索引</li><li>隐式类型转换可能会触发上面的规则1</li><li>隐式字符编码转换也可能触发上面的规则1</li></ol><h2 id="为什么只查一行的语句也执行这么慢"><a class="markdownIt-Anchor" href="#为什么只查一行的语句也执行这么慢"></a> 为什么只查一行的语句也执行这么慢</h2><h3 id="查询长时间不返回"><a class="markdownIt-Anchor" href="#查询长时间不返回"></a> 查询长时间不返回</h3><ol><li>等MDL锁。通过查询sys.schema_table_lock_waits,可以找出造成阻塞的process id,把这个连接用kill杀掉</li><li>等flush。可能情况是有一个flush tables命令被别的语句堵住了,然后它又堵住了查询语句,可以用show processlist 查出并杀掉阻塞flush的连接</li><li>等行锁。通过查询sys.innodb_lock_waits 杀掉对应连接</li></ol><h3 id="查询慢"><a class="markdownIt-Anchor" href="#查询慢"></a> 查询慢</h3><ol><li>查询字段没有索引,走了全表扫描</li><li>事务隔离级别为可重复读,当前事务看不到别的事务的修改,但是别的事务执行了多次修改,当前事务在查询时要根据undo log查询到应该看到的值</li></ol><h2 id="幻读"><a class="markdownIt-Anchor" href="#幻读"></a> 幻读</h2><p>幻读:一个事务在前后两次查询同一个范围时,后一次查询看到了前一次查询没有看到的行</p><ol><li>在可重复读隔离级别下,普通的查询时快照读,是不会看到别的事务插入的数据的。因此,幻读在“当前读”下才会出现</li><li>幻读专指“新插入的行”</li></ol><h3 id="幻读的问题"><a class="markdownIt-Anchor" href="#幻读的问题"></a> 幻读的问题</h3><ul><li><p>语义被破坏</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230201-140557484.png" alt="图 20" /></p><p>session A在T1时刻声明了,“我要把所有d=5的行锁住,不准别的事务进行读写操作”。session B和C破坏了这个语义</p></li><li><p>数据不一致。根据binlog克隆的库与主库不一致,原因是即使给所有记录都加上锁,新记录还是没上锁</p></li></ul><h3 id="解决幻读"><a class="markdownIt-Anchor" href="#解决幻读"></a> 解决幻读</h3><p>间隙锁:锁住两行之间的间隙</p><p>在行扫描过程中,不仅给行加行锁,还给行间的间隙上锁</p><p>跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作。间隙锁之间都不存在冲突关系。</p><p>间隙锁和行锁合称next-key lock,左开右闭</p><p>间隙锁的引入,可能会导致同样的语句锁住更大的范围,影响并发度。</p><p>间隙锁只在可重复读隔离级别下才会生效</p><h2 id="为什么只改一行的语句锁这么多"><a class="markdownIt-Anchor" href="#为什么只改一行的语句锁这么多"></a> 为什么只改一行的语句,锁这么多</h2><p>加锁规则(可重复读隔离级别):</p><ol><li>原则1:加锁的基本单位是next-key lock</li><li>原则2:查找过程中访问到的对象才会加锁</li><li>优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁</li><li>优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁</li><li>一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止</li></ol><p>假设有如下SQL语句</p><pre class="highlight"><code class="SQL"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> `t` ( `id` <span class="hljs-type">int</span>(<span class="hljs-number">11</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">NULL</span>, `c` <span class="hljs-type">int</span>(<span class="hljs-number">11</span>) <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">NULL</span>, `d` <span class="hljs-type">int</span>(<span class="hljs-number">11</span>) <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">NULL</span>, <span class="hljs-keyword">PRIMARY</span> KEY (`id`), KEY `c` (`c`)) ENGINE<span class="hljs-operator">=</span>InnoDB;<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> t <span class="hljs-keyword">values</span>(<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>),(<span class="hljs-number">5</span>,<span class="hljs-number">5</span>,<span class="hljs-number">5</span>),(<span class="hljs-number">10</span>,<span class="hljs-number">10</span>,<span class="hljs-number">10</span>),(<span class="hljs-number">15</span>,<span class="hljs-number">15</span>,<span class="hljs-number">15</span>),(<span class="hljs-number">20</span>,<span class="hljs-number">20</span>,<span class="hljs-number">20</span>),(<span class="hljs-number">25</span>,<span class="hljs-number">25</span>,<span class="hljs-number">25</span>);</code></pre><h3 id="案例一等值查询间隙锁"><a class="markdownIt-Anchor" href="#案例一等值查询间隙锁"></a> 案例一:等值查询间隙锁</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230201-152349258.png" alt="图 21" /></p><p>由于表t中没有id=7的记录</p><ol><li>根据原则1,加锁单位是next-key lock,session A加锁范围就是(5,10];</li><li>同时根据优化2,这是一个等值查询(id=7),而id=10不满足查询条件,next-key lock退化成间隙锁,因此最终加锁的范围是(5,10)。</li></ol><p>所以,session B要往这个间隙里面插入id=8的记录会被锁住,但是session C修改id=10这行是可以的。</p><h3 id="案例二非唯一索引等值锁"><a class="markdownIt-Anchor" href="#案例二非唯一索引等值锁"></a> 案例二:非唯一索引等值锁</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230201-153351121.png" alt="图 22" /></p><p>这里session A要给索引c上c=5的这一行加上读锁。</p><ol><li>根据原则1,加锁单位是next-key lock,因此会给(0,5]加上next-key lock</li><li>要注意c是普通索引,因此仅访问c=5这一条记录是不能马上停下来的,需要向右遍历,查到c=10才放弃。根据原则2,访问到的都要加锁,因此要给(5,10]加next-key lock。</li><li>但是同时这个符合优化2:等值判断,向右遍历,最后一个值不满足c=5这个等值条件,因此退化成间隙锁(5,10)</li><li>根据原则2 ,只有访问到的对象才会加锁,这个查询使用覆盖索引,并不需要访问主键索引,所以主键索引上没有加任何锁,这就是为什么session B的update语句可以执行完成。</li></ol><p>但session C要插入一个(7,7,7)的记录,就会被session A的间隙锁(5,10)锁住。</p><p>需要注意,在这个例子中,lock in share mode只锁覆盖索引,但是如果是for update就不一样了。 执行 for update时,系统会认为你接下来要更新数据,因此会顺便给主键索引上满足条件的行加上行锁。</p><p>这个例子说明,锁是加在索引上的;同时,它给我们的指导是,如果你要用lock in share mode来给行加读锁避免数据被更新的话,就必须得绕过覆盖索引的优化,在查询字段中加入索引中不存在的字段。比如,将session A的查询语句改成select d from t where c=5 lock in share mode</p><h3 id="案例三主键索引范围锁"><a class="markdownIt-Anchor" href="#案例三主键索引范围锁"></a> 案例三:主键索引范围锁</h3><pre class="highlight"><code class="SQL">mysql<span class="hljs-operator">></span> <span class="hljs-keyword">select</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> t <span class="hljs-keyword">where</span> id<span class="hljs-operator">=</span><span class="hljs-number">10</span> <span class="hljs-keyword">for</span> <span class="hljs-keyword">update</span>;mysql<span class="hljs-operator">></span> <span class="hljs-keyword">select</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> t <span class="hljs-keyword">where</span> id<span class="hljs-operator">>=</span><span class="hljs-number">10</span> <span class="hljs-keyword">and</span> id<span class="hljs-operator"><</span><span class="hljs-number">11</span> <span class="hljs-keyword">for</span> <span class="hljs-keyword">update</span>;</code></pre><p>这两条语句在逻辑上是等价的,但是加锁规则不一样</p><ol><li>开始执行的时候,要找到第一个id=10的行,因此本该是next-key lock(5,10]。 根据优化1, 主键id上的等值条件,退化成行锁,只加了id=10这一行的行锁</li><li>范围查找就往后继续找,找到id=15这一行停下来,因此需要加next-key lock(10,15]</li></ol><p>所以,session A这时候锁的范围就是主键索引上,行锁id=10和next-key lock(10,15]</p><p>需要注意一点,首次session A定位查找id=10的行的时候,是当做等值查询来判断的,而向右扫描到id=15的时候,用的是范围查询判断</p><h3 id="案例四非唯一索引范围锁"><a class="markdownIt-Anchor" href="#案例四非唯一索引范围锁"></a> 案例四:非唯一索引范围锁</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230201-162644013.png" alt="图 23" /></p><p>这次session A用字段c来判断,加锁规则跟主键索引范围锁的唯一不同是:在第一次用c=10定位记录的时候,索引c上加了(5,10]这个next-key lock后,由于索引c是非唯一索引,没有优化规则,也就是说不会蜕变为行锁,因此最终sesion A加的锁是,索引c上的(5,10] 和(10,15] 这两个next-key lock</p><h3 id="案例五唯一索引范围锁bug"><a class="markdownIt-Anchor" href="#案例五唯一索引范围锁bug"></a> 案例五:唯一索引范围锁bug</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230201-163246731.png" alt="图 24" /></p><p>session A是一个范围查询,按照原则1的话,应该是索引id上只加(10,15]这个next-key lock,并且因为id是唯一键,所以循环判断到id=15这一行就应该停止了。</p><p>但是实现上,InnoDB会往前扫描到第一个不满足条件的行为止,也就是id=20。而且由于这是个范围扫描,因此索引id上的(15,20]这个next-key lock也会被锁上。</p><h3 id="案例六非唯一索引上存在等值的例子"><a class="markdownIt-Anchor" href="#案例六非唯一索引上存在等值的例子"></a> 案例六:非唯一索引上存在“等值”的例子</h3><p>现在插入一条新记录</p><pre class="highlight"><code class="SQL">mysql<span class="hljs-operator">></span> <span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> t <span class="hljs-keyword">values</span>(<span class="hljs-number">30</span>,<span class="hljs-number">10</span>,<span class="hljs-number">30</span>);</code></pre><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230201-163733565.png" alt="图 25" /></p><p>delete语句加锁的逻辑和 select … for update是类似的,session A在遍历的时候,先访问第一个c=10的记录。同样地,根据原则1,这里加的是(c=5,id=5)到(c=10,id=10)这个next-key lock。</p><p>然后,session A向右查找,直到碰到(c=15,id=15)这一行,循环才结束。根据优化2,这是一个等值查询,向右查找到了不满足条件的行,所以会退化成(c=10,id=10) 到 (c=15,id=15)的间隙锁。</p><p>也就是说,这个delete语句在索引c上的加锁范围,就是下图中蓝色区域覆盖的部分。蓝色区域左右两边都是虚线,表示开区间,即(c=5,id=5)和(c=15,id=15)这两行上都没有锁。</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230201-164216676.png" alt="图 26" /></p><h3 id="案例七limit-语句加锁"><a class="markdownIt-Anchor" href="#案例七limit-语句加锁"></a> 案例七:limit 语句加锁</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230201-164703623.png" alt="图 27" /></p><p>session A的delete语句加了 limit 2。你知道表t里c=10的记录其实只有两条,因此加不加limit 2,删除的效果都是一样的,但是加锁的效果却不同。可以看到,session B的insert语句执行通过了,跟案例六的结果不同。这是因为,案例七里的delete语句明确加了limit 2的限制,因此在遍历到(c=10, id=30)这一行之后,满足条件的语句已经有两条,循环就结束了。</p><p>因此,索引c上的加锁范围就变成了从(c=5,id=5)到(c=10,id=30)这个前开后闭区间,如下图所示:<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230201-164931487.png" alt="图 28" /></p><p>这个例子对我们实践的指导意义就是,在删除数据的时候尽量加limit。这样不仅可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围。</p><h3 id="案例八一个死锁的例子"><a class="markdownIt-Anchor" href="#案例八一个死锁的例子"></a> 案例八:一个死锁的例子</h3><p>本案例目的是说明:next-key lock 实际上是间隙锁和行锁加起来的结果</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230201-171117714.png" alt="图 29" /></p><ol><li>session A 启动事务后执行查询语句加lock in share mode,在索引c上加了next-key lock(5,10] 和间隙锁(10,15);</li><li>session B 的update语句也要在索引c上加next-key lock(5,10] ,进入锁等待;</li><li>然后session A要再插入(8,8,8)这一行,被session B的间隙锁锁住。由于出现了死锁,InnoDB让session B回滚</li></ol><p>你可能会问,session B的next-key lock不是还没申请成功吗?</p><p>其实是这样的,session B的“加next-key lock(5,10] ”操作,实际上分成了两步,先是加(5,10)的间隙锁,加锁成功;然后加c=10的行锁,这时候才被锁住的。</p><p>也就是说,我们在分析加锁规则的时候可以用next-key lock来分析。但是要知道,具体执行的时候,是要分成间隙锁和行锁两段来执行的。</p><h2 id="饮鸩止渴提高性能的方法"><a class="markdownIt-Anchor" href="#饮鸩止渴提高性能的方法"></a> “饮鸩止渴”提高性能的方法</h2><h3 id="短连接风暴"><a class="markdownIt-Anchor" href="#短连接风暴"></a> 短连接风暴</h3><p>短连接模式就是连接到数据库后,执行很少的SQL语句就断开,下次需要的时候再重连,在业务高峰期,会出现连接数暴涨的情况</p><p>两种有损业务的解决方法:</p><ol><li>处理掉占着连接但是不工作的线程。优先断开事务外空闲太久的连接</li><li>减少连接过程的损耗。关闭权限验证</li></ol><h3 id="慢查询性能问题"><a class="markdownIt-Anchor" href="#慢查询性能问题"></a> 慢查询性能问题</h3><p>引发慢查询的情况有三种:</p><ol><li>索引没有设计好。最高效的解决方法是直接alter table建索引</li><li>SQL语句没有写好,导致没用上索引。解决方法是使用query_rewrite重写SQL语句</li><li>MySQL选错了索引。应急方案是给语句加上force index或者使用query_rewrite重写语句加上force index</li></ol><p>出现情况最多的是前两种,通过下面过程可以预先发现和避免</p><ol><li>上线前,在测试环境,把慢查询日志(slow log)打开,并且把long_query_time设置成0,确保每个语句都会被记录入慢查询日志</li><li>在测试表里插入模拟线上的数据,做一遍回归测试</li><li>观察慢查询日志里每类语句的输出,特别留意Rows_examined字段是否与预期一致</li></ol><h3 id="qps突增问题"><a class="markdownIt-Anchor" href="#qps突增问题"></a> QPS突增问题</h3><ol><li>业务bug导致。可以把这个功能的SQL从白名单去掉</li><li>如果新功能使用的是单独的数据库用户,可以用管理员账号把这个用户删掉,然后断开连接</li><li>用query_rewrite把压力最大的SQL语句直接重写成"select 1"返回</li></ol><p>方法3存在两个副作用:</p><ol><li>如果别的功能也用到了这个SQL语句就会误伤</li><li>该语句可能是业务逻辑的一部分,导致业务逻辑一起失败</li></ol><p>方法3是优先级最低的方法。方法1和2依赖于规范的运维体系:虚拟化、白名单机制、业务账号分离</p><h2 id="mysql是怎么保证数据不丢的"><a class="markdownIt-Anchor" href="#mysql是怎么保证数据不丢的"></a> MySQL是怎么保证数据不丢的</h2><p>只要redo log和binlog保证持久化到磁盘,就能确保MySQL异常重启后,数据可以恢复</p><h3 id="binlog的写入机制"><a class="markdownIt-Anchor" href="#binlog的写入机制"></a> binlog的写入机制</h3><p>binlog的写入逻辑:事务执行过程中,先把日志写到binlog cache,事务提交的时候,再把binlog cache写到binlog文件中</p><p>一个事务的binlog是不能被拆开的,因此不论这个事务多大,也要确保一次性写入</p><p>系统给binlog cache分配了一片内存,每个线程一个,参数binlog_cache_size用于控制单个线程内binlog cache所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘</p><p>事务提交的时候,执行器把binlog cache里的完整事务写入到binlog中,并清空binlog cache<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230202-144644384.png" alt="图 1" /></p><ul><li>图中的write,指的是把日志写入到文件系统的page cache,并没有把数据持久化到磁盘,所以速度比较快</li><li>图中的fsync,才是将数据持久化到磁盘的操作。一般情况下,我们认为fsync才占磁盘的IOPS</li></ul><p>write和fsync的时机由参数sync_binlog控制:</p><ol><li>sync_binlog=0,表示每次提交事务都只write,不fsync</li><li>sync_binlog=1,表示每次提交事务都会fsync</li><li>sync_binlog=N(N>1),表示每次提交事务都write,但累积N个事务后才fsync</li></ol><p>sync_binlog设置成N可以改善IO瓶颈场景的性能,但对应的风险是:如果主机发生异常重启,会丢失最近N个事务的binlog</p><h3 id="redo-log的写入机制"><a class="markdownIt-Anchor" href="#redo-log的写入机制"></a> redo log的写入机制</h3><p>事务执行过程中,生成的redo log要先写到redo log buffer,但不是每次生成后都要直接持久化到磁盘,因为事务没提交,日志丢了也不会有损失。<br />但是也有可能事务没有提交,redo log buffer 中的部分日志持久化到了磁盘。下图是redo log的三种状态</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230202-152531407.png" alt="图 2" /></p><p>日志写到redo log buffer是很快的,write到page cache也快,但是持久化到磁盘就很慢。</p><p>InnoDB提供了innodb_flush_log_at_trx_commit参数来控制redo log的写入策略:</p><ol><li>设置为0表示每次事务提交时都只是把redo log 留在redo log buffer中</li><li>设置为1表示每次事务提交时都将redo log直接持久化到磁盘</li><li>设置为2表示每次事务提交时都只是把redo log写到page cache</li></ol><p>InnoDB有个后台线程,每隔1秒,就会把redo log buffer中的日志,调用write写到文件系统的page cache,然后调用fsync持久化到磁盘</p><p>注意,事务执行过程中的redo log也是直接写在redo log buffer中的,这些redo log也会被后台线程一起持久化到磁盘。也就是说,一个没有提交的事务的redo log,也可能已经持久化到磁盘</p><p>除了后台线程每秒一次的轮询操作外,还有两个场景会让没提交的事务的redo log写入到磁盘</p><ol><li>redo log buffer占用的空间即将达到 innodb_log_buffer_size一半的时候,后台线程会主动write到page cache</li><li>并行的事务提交的时候,顺带将这个事务的redo log buffer持久化到磁盘</li></ol><p>两阶段提交的过程,时序上redo log先prepare,再写binlog,最后再把redo log commit</p><p>如果innodb_flush_log_at_trx_commit设置为1,那么redo log在prepare阶段就要持久化一次,因为有一个崩溃恢复逻辑是prepare的redo log + 完整的binlog</p><p>每秒一次后台轮询刷盘,再加上崩溃恢复这个逻辑,InnoDB就认为redo log在commit的时候就不需要fsync了,只会write到文件系统的page cache中就够了</p><p>通常我们说MySQL的“双1”配置,指的就是sync_binlog和innodb_flush_log_at_trx_commit都设置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是redo log(prepare 阶段),一次是binlog</p><h3 id="redo-log组提交"><a class="markdownIt-Anchor" href="#redo-log组提交"></a> redo log组提交</h3><p>日志逻辑序列号LSN:LSN单调递增,用来对应redo log的一个个写入点。每次写入长度为length的redo log,LSN的值就会加上length</p><p>如下图所示,是三个并发事务在prepare阶段,都写完redo log buffer,持久化到磁盘的过程中<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230202-161454115.png" alt="图 3" /></p><p>从图中可以看到,</p><ol><li>trx1是第一个到达的,会被选为这组的leader</li><li>等trx1要开始写盘的时候,这个组里面已经有3个事务了,这时候LSN也变成了160</li><li>trx1去写盘的时候,带的就是LSN=160,因此等trx1返回时,所有LSN小于等于160的redo log,都已经被持久化到磁盘</li><li>这时候trx2和trx3就可以直接返回了</li></ol><p>所以,一次组提交里面,组员越多,节约磁盘IOPS的效果越好。但如果只有单线程压测,那就只能老老实实地一个事务对应一次持久化操作了。</p><p>在并发更新场景下,第一个事务写完redo log buffer以后,接下来这个fsync越晚调用,组员可能越多,节约IOPS的效果就越好。</p><p>MySQL为了让组提交的效果更好,细化了两阶段提及的顺序,让redo log的fsync往后拖</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230202-162042962.png" alt="图 4" /></p><p>上图的顺序说明binlog也可以组提交,但是通常情况下步骤3会执行得很快,所以能集合到一起持久化的binlog比较少。可以通过设置binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count来提升binlog组提交的效果</p><h3 id="性能瓶颈在io的提升方法"><a class="markdownIt-Anchor" href="#性能瓶颈在io的提升方法"></a> 性能瓶颈在IO的提升方法</h3><ol><li>设置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count参数,减少binlog的写盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但没有丢失数据的风险。</li><li>将sync_binlog 设置为大于1的值(比较常见是100~1000)。这样做的风险是,主机掉电时会丢binlog日志。</li><li>将innodb_flush_log_at_trx_commit设置为2。这样做的风险是,主机掉电的时候会丢数据。</li></ol><h2 id="mysql是怎么保证主备一致的"><a class="markdownIt-Anchor" href="#mysql是怎么保证主备一致的"></a> MySQL是怎么保证主备一致的</h2><p>MySQL的主备一致依赖于binlog</p><h3 id="mysql主备的基本原理"><a class="markdownIt-Anchor" href="#mysql主备的基本原理"></a> MySQL主备的基本原理</h3><h4 id="主备切换流程"><a class="markdownIt-Anchor" href="#主备切换流程"></a> 主备切换流程</h4><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230202-183322091.png" alt="图 5" /></p><p>客户端的读写是直接访问主库,备库同步主库的更新,与主库保持一致。虽然备库不会被客户端访问,但仍推荐设置成只读模式,因为:</p><ol><li>有时候一些运营类的查询语句会放到备库上去查,设置为只读可以防止误操作</li><li>防止切换逻辑有bug,比如切换过程中出现双写,造成主备不一致</li><li>可以用readonly状态来判断节点的角色</li></ol><p>备库的只读对超级权限用户是无效的,用于同步更新的线程拥有超级权限</p><h4 id="同步流程"><a class="markdownIt-Anchor" href="#同步流程"></a> 同步流程</h4><p>主库的更新语句同步到备库的完成流程图如下</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230202-184958956.png" alt="图 6" /></p><p>备库B跟主库A之间维持了一个长连接。主库A内部有一个线程,专门用于服务备库B的这个长连接。一个事务日志同步的完整过程如下:</p><ol><li>在备库B上通过change master命令,设置主库A的IP、端口、用户名、密码,以及要从哪个位置开始请求binlog,这个位置包含文件名和日志偏移量</li><li>在备库B上执行start slave命令,这时候备库会启动两个线程,就是图中的io_thread和sql_thread。其中io_thread负责与主库建立连接。</li><li>主库A校验完用户名、密码后,开始按照备库B传过来的位置,从本地读取binlog,发给B</li><li>备库B拿到binlog后,写到本地文件,称为中转日志(relay log)</li><li>sql_thread读取中转日志,解析出日志里的命令,并执行</li></ol><h4 id="binlog的三种格式对比"><a class="markdownIt-Anchor" href="#binlog的三种格式对比"></a> binlog的三种格式对比</h4><p>binlog有三种格式,statement、row以及前两种格式的混合mixed</p><p>假设有如下表:</p><pre class="highlight"><code class="SQL">mysql<span class="hljs-operator">></span> <span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> `t` ( `id` <span class="hljs-type">int</span>(<span class="hljs-number">11</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">NULL</span>, `a` <span class="hljs-type">int</span>(<span class="hljs-number">11</span>) <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">NULL</span>, `t_modified` <span class="hljs-type">timestamp</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">NULL</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-built_in">CURRENT_TIMESTAMP</span>, <span class="hljs-keyword">PRIMARY</span> KEY (`id`), KEY `a` (`a`), KEY `t_modified`(`t_modified`)) ENGINE<span class="hljs-operator">=</span>InnoDB;<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> t <span class="hljs-keyword">values</span>(<span class="hljs-number">1</span>,<span class="hljs-number">1</span>,<span class="hljs-string">'2018-11-13'</span>);<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> t <span class="hljs-keyword">values</span>(<span class="hljs-number">2</span>,<span class="hljs-number">2</span>,<span class="hljs-string">'2018-11-12'</span>);<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> t <span class="hljs-keyword">values</span>(<span class="hljs-number">3</span>,<span class="hljs-number">3</span>,<span class="hljs-string">'2018-11-11'</span>);<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> t <span class="hljs-keyword">values</span>(<span class="hljs-number">4</span>,<span class="hljs-number">4</span>,<span class="hljs-string">'2018-11-10'</span>);<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> t <span class="hljs-keyword">values</span>(<span class="hljs-number">5</span>,<span class="hljs-number">5</span>,<span class="hljs-string">'2018-11-09'</span>);</code></pre><p>statement格式就是SQL语句原文<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230202-191035361.png" alt="图 7" /></p><p>下图是该语句执行效果<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230202-191456396.png" alt="图 8" /></p><p>statement格式下,delete 带 limit,很可能出现主备数据不一致的情况,比如上面的例子:</p><ol><li>如果delete语句使用的是索引a,那么会根据索引a找到第一个满足条件的行,也就是说删除的是a=4这一行</li><li>但如果使用的是索引t_modified,那么删除的就是 t_modified='2018-11-09’ 也就是a=5这一行。</li></ol><p>row格式binlog如下<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230202-192933464.png" alt="图 9" /></p><p>row格式的binlog把SQL语句替换成了两个event:Table_map和Delete_rows</p><ol><li>Table_map event,用于说明接下来要操作的表是test库的表t;</li><li>Delete_rows event,用于定义删除的行为。</li></ol><p>借助mysqlbinlog工具查看详细的binlog</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230202-193454354.png" alt="图 10" /></p><p>当binlog_format使用row格式的时候,binlog里面记录了真实删除行的主键id,这样binlog传到备库去的时候,就肯定会删除id=4的行,不会有主备删除不同行的问题。</p><p>mixed格式吸收了statement和row格式的优点,占用空间小,避免了数据不一致</p><p>但是现在binlog设置成row的场景更多,理由有很多,其中之一是恢复数据。</p><p>如果执行的是delete语句,row格式的binlog也会把被删掉的行的整行信息保存起来。所以,如果你在执行完一条delete语句以后,发现删错数据了,可以直接把binlog中记录的delete语句转成insert,把被错删的数据插入回去就可以恢复了</p><p>如果你是执行错了insert语句呢?那就更直接了。row格式下,insert语句的binlog里会记录所有的字段信息,这些信息可以用来精确定位刚刚被插入的那一行。这时,你直接把insert语句转成delete语句,删除掉这被误插入的一行数据就可以了。</p><p>如果执行的是update语句的话,binlog里面会记录修改前整行的数据和修改后的整行数据。所以,如果你误执行了update语句的话,只需要把这个event前后的两行信息对调一下,再去数据库里面执行,就能恢复这个更新操作了</p><h3 id="循环复制问题"><a class="markdownIt-Anchor" href="#循环复制问题"></a> 循环复制问题</h3><p>binlog的特性确保了主备一致性。实际生产上使用比较多的是双M结构<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230202-201447162.png" alt="图 11" /></p><p>双M结构中,节点A和B之间总是互为主备关系,在切换的时候就不用再修改主备关系</p><p>循环复制指的是A节点更新完,把binlog发给B,B更新完又生成binlog发给了A,解决循环复制的方法如下:</p><ol><li>规定两个库的server id必须不同,如果相同,则它们之间不能设定为主备关系</li><li>一个备库接到binlog并在重放的过程中,生成与原binlog的server id相同的新的binlog</li><li>每个库在收到从自己的主库发过来的日志后,先判断server id,如果跟自己的相同,表示这个日志是自己生成的,直接丢弃</li></ol><p>因此,双M结构的日志执行流会变成这样:</p><ol><li>从节点A更新的事务,binlog里面记得都是A的server id</li><li>传到节点B执行一次以后,节点B生成的binlog的server id也是A的server id</li><li>再传回给节点A,A判断到这个server id与自己的相同,就不会再处理这个日志。所以,死循环这里就断掉了</li></ol><h2 id="mysql是怎么保证高可用的"><a class="markdownIt-Anchor" href="#mysql是怎么保证高可用的"></a> MySQL是怎么保证高可用的</h2><p>正常情况下,只要主库执行更新生成的所有binlog,都可以传到备库并被正确地执行,备库就能达到跟主库一致的状态,这就是最终一致性。但是MySQL要提供高可用,只有最终一致性是不够的</p><h3 id="主备延迟"><a class="markdownIt-Anchor" href="#主备延迟"></a> 主备延迟</h3><p>与数据同步有关的时间点主要包括以下三个:</p><p>T1:主库A执行完成一个事务,写入binlog<br />T2:备库B接收完这个binlog<br />T3:备库B执行完成这个事务</p><p>主备延迟:同一个事务,在备库执行完成的时间和主库执行完成的时间之间的差值,也就是T3-T1</p><p>seconds_behind_master表示备库延迟了多少秒</p><p>网络正常情况下,主备延迟的主要因素是T3-T2,直接表现是备库消费中转日志(relay log)的速度比主库生产binlog的速度慢</p><h4 id="主备延迟的来源"><a class="markdownIt-Anchor" href="#主备延迟的来源"></a> 主备延迟的来源</h4><ol><li>备库的机器性能差。解决方法是对称部署</li><li>备库的压力大。有些统计查询语句只在备库上跑,导致备库压力大,解决方法是一主多从分担读的压力或者把binlog输送到Hadoop来提供统计查询能力</li><li>大事务。比如一次性用delete删除太多数据或者大表DDL</li><li>备库的并行复制能力</li></ol><p>由于主备延迟的存在,所以在主备切换的时候,有不同的策略</p><h3 id="可靠性优先策略"><a class="markdownIt-Anchor" href="#可靠性优先策略"></a> 可靠性优先策略</h3><p>在双M结构下,主备切换流程如下:</p><ol><li>判断备库B现在的seconds_behind_master,如果小于某个值(比如5秒)继续下一步,否则持续重试这一步;</li><li>把主库A改成只读状态,即把readonly设置为true;</li><li>判断备库B的seconds_behind_master的值,直到这个值变成0为止;</li><li>把备库B改成可读写状态,也就是把readonly 设置为false;</li><li>把业务请求切到备库B。</li></ol><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230207-195231199.png" alt="图 12" /></p><p>步骤2直到步骤5,主库A和备库B都处于readonly状态,系统不可用(不可写)</p><h3 id="可用性优先策略"><a class="markdownIt-Anchor" href="#可用性优先策略"></a> 可用性优先策略</h3><p>把上面策略里的步骤4和5放到最开始执行,代价是可能出现数据不一致的情况</p><p>一般情况下,可靠性优于可用性。在满足数据可靠性的前提下,MySQL高可用系统的可用性,是依赖于主备延迟的。延迟的时间越小,在主库故障的时候,服务恢复需要的时间就越短,可用性就越高。</p><h2 id="备库为什么会延迟好几个小时"><a class="markdownIt-Anchor" href="#备库为什么会延迟好几个小时"></a> 备库为什么会延迟好几个小时</h2><p>当备库执行日志的速度持续低于主库生成日志的速度,那这个延迟就有可能成了小时级别</p><p>MySQL 5.6之前,备库应用日志更新数据只能使用单线程,在主库并发高、TPS高时会出现严重的主备延迟问题</p><h3 id="按表分发策略"><a class="markdownIt-Anchor" href="#按表分发策略"></a> 按表分发策略</h3><p>按表分发事务的基本思路是,如果两个事务更新不同的表,它们就可以并行</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230208-142404167.png" alt="图 13" /></p><p>worker线程维护一张执行队列里的事务涉及的表,key是“库名.表名”,value表示队列中有多少个事务修改这个表</p><p>事务在分发的时候,和所有worker的冲突关系有3种:</p><ol><li>如果跟所有worker都不冲突,coordinator线程就会把这个事务分配给最空闲的woker;</li><li>如果跟多于一个worker冲突,coordinator线程就进入等待状态,直到和这个事务存在冲突关系的worker只剩下1个;</li><li>如果只跟一个worker冲突,coordinator线程就会把这个事务分配给这个存在冲突关系的worker。</li></ol><p>按表分发方案在多个表负载均衡的场景效果很好。但是碰到热点表会退化成单线程复制</p><h3 id="按行分发策略"><a class="markdownIt-Anchor" href="#按行分发策略"></a> 按行分发策略</h3><p>要解决热点表的并行复制问题,就需要一个按行并行复制方案。核心思路是:如果两个事务没有更新相同的行,它们在备库上可以并行执行。这个模式要求binlog格式必须是row</p><p>判断事务和worker是否冲突,用的规则不是“修改同一个表”,而是“修改同一行”。worker维护的hash表的key是“库名+表名+唯一索引的名字+唯一索引的值”</p><p>按行分发策略比按表分发策略需要消耗更多的计算资源,这两种方案都有一样的约束条件:</p><ol><li>要能够从binlog里面解析出表名、主键值和唯一索引的值。也就是说,主库的binlog格式必须是row;</li><li>表必须有主键;</li><li>不能有外键。表上如果有外键,级联更新的行不会记录在binlog中,这样冲突检测就不准确。</li></ol><h3 id="mysql-56版本的并行复制策略"><a class="markdownIt-Anchor" href="#mysql-56版本的并行复制策略"></a> MySQL 5.6版本的并行复制策略</h3><p>官方MySQL 5.6版本支持的并行复制的力度是按库并行。hash表的key是数据库名</p><p>相比于按表和按行分发,有两个优势:</p><ol><li>构造hash值的时候很快,只需要库名;而且一个实例上DB数也不会很多,不会出现需要构造100万个项这种情况</li><li>不要求binlog的格式。因为statement格式的binlog也可以很容易拿到库名</li></ol><h3 id="mariadb的并行复制策略"><a class="markdownIt-Anchor" href="#mariadb的并行复制策略"></a> MariaDB的并行复制策略</h3><p>MariaDB的并行复制策略利用了redo log组提交优化的特性:</p><ol><li>能够在同一组里提交的事务,一定不会修改同一行</li><li>主库上可以并行执行的事务,备库上也一定可以并行执行</li></ol><p>这个策略的目标是“模拟主库的并行模式”,但它没有实现“真正的模拟主库并发度”这个目标。在主库上,一组事务在commit的时候,下一组事务是同时处于“执行中”状态的</p><h3 id="mysql-57的并行复制策略"><a class="markdownIt-Anchor" href="#mysql-57的并行复制策略"></a> MySQL 5.7的并行复制策略</h3><p>由参数slave-parallel-type来控制并行复制策略:</p><ol><li>配置为DATABASE,表示使用MySQL 5.6版本的按库并行策略;</li><li>配置为 LOGICAL_CLOCK,表示的就是优化过的类似MariaDB的策略</li></ol><p>该优化的策略的思想是:</p><ol><li>同时处于prepare状态的事务,在备库执行时是可以并行的;</li><li>处于prepare状态的事务,与处于commit状态的事务之间,在备库执行时也是可以并行的。</li></ol><h3 id="mysql-5722的并行复制策略"><a class="markdownIt-Anchor" href="#mysql-5722的并行复制策略"></a> MySQL 5.7.22的并行复制策略</h3><p>新增了一个并行复制策略,基于WRITESET的并行复制。</p><p>新增参数binlog-transaction-dependency-tracking,用来控制是否启用这个新策略:</p><ol><li>COMMIT_ORDER,表示的就是前面介绍的,根据同时进入prepare和commit来判断是否可以并行的策略。</li><li>WRITESET,表示的是对于事务涉及更新的每一行,计算出这一行的hash值,组成集合writeset。如果两个事务没有操作相同的行,也就是说它们的writeset没有交集,就可以并行</li><li>WRITESET_SESSION,是在WRITESET的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序。</li></ol><p>该策略类似按行分发,但是有很大优势:</p><ol><li>writeset是在主库生成后直接写入到binlog里面的,这样在备库执行的时候,不需要解析binlog内容(event里的行数据),节省了很多计算量;</li><li>不需要把整个事务的binlog都扫一遍才能决定分发到哪个worker,更省内存;</li><li>由于备库的分发策略不依赖于binlog内容,所以binlog是statement格式也是可以的。</li></ol><p>该方案对于“表上没主键”和“外键约束”的场景,也会暂时退化为单线程模型。</p><h2 id="主库出问题了从库怎么办"><a class="markdownIt-Anchor" href="#主库出问题了从库怎么办"></a> 主库出问题了,从库怎么办</h2><p>大多数互联网应用场景都是读多写少,要解决读性能问题,就要涉及:一主多从</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230208-152230195.png" alt="图 14" /></p><p>图中,虚线箭头表示的是主备关系,也就是A和A’互为主备, 从库B、C、D指向的是主库A。一主多从的设置,一般用于读写分离,主库负责所有的写入和一部分读,其他的读请求则由从库分担。</p><p>下面讨论,在一主多从架构下,主库故障后的主备切换问题</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230208-152724067.png" alt="图 15" /></p><p>相比于一主一备的切换流程,一主多从结构在切换完成后,A’会成为新的主库,从库B、C、D也要改接到A’。正是由于多了从库B、C、D重新指向的这个过程,所以主备切换的复杂性也相应增加了</p><h3 id="基于位点的主备切换"><a class="markdownIt-Anchor" href="#基于位点的主备切换"></a> 基于位点的主备切换</h3><p>节点B设置成节点A’ 的从库的时候,需要执行change master命令,必须设置主库的日志文件名和偏移量。A和A’的位点是不同的,从库B切换时需要先经过“找同步位点”这个逻辑</p><p>同步位点很难精确取到</p><p>取同步位点的方法如下:</p><ol><li>等待新主库A’ 把中转日志(relay log)全部同步完成</li><li>在A’ 上执行show master status命令,得到当前A’ 上最新的 File 和 Position</li><li>取原主库A故障的时刻T</li><li>用mysqlbinlog工具解析A’的File,得到T时刻的位点</li></ol><pre class="highlight"><code class="SQL">mysqlbinlog File <span class="hljs-comment">--stop-datetime=T --start-datetime=T</span></code></pre><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230208-153915174.png" alt="图 16" /></p><p>图中,end_log_pos后面的值“123”,表示的就是A’这个实例,在T时刻写入新的binlog的位置,可以把这个值作为$master_log_pos ,用在节点B的change master命令里</p><p>这个值并不精确,从库B的同步线程可能会出错,解决方法如下:</p><ol><li>通过sql_slave_skip_counter跳过出错事务</li><li>设置slave_skip_errors,跳过指定错误,通常设置成1032,1062,对应的错误是删除数据找不到行,插入数据唯一键冲突</li></ol><h3 id="gtid"><a class="markdownIt-Anchor" href="#gtid"></a> GTID</h3><p>前两种方式操作复杂,容易出错,MySQL 5.6 引入了GITD。</p><p>GTID全称是Global Transaction Identifier,也就是全局事务ID,是一个事务在提交的时候生成的,是这个事务的唯一标识,格式是:</p><pre class="highlight"><code class="SQL">GTID<span class="hljs-operator">=</span>server_uuid:gno</code></pre><ul><li>server_uuid是一个实例第一次启动时自动生成的,是一个全局唯一的值</li><li>gno是一个整数,初始值是1,每次提交事务的时候分配给这个事务,并加1</li></ul><p>GTID有两种生成方式:</p><ol><li><p>如果gtid_next=automatic,代表使用默认值。这时,MySQL就会把server_uuid:gno分配给这个事务。<br />a. 记录binlog的时候,先记录一行 SET @@SESSION.GTID_NEXT=‘server_uuid:gno’;<br />b. 把这个GTID加入本实例的GTID集合</p></li><li><p>如果gtid_next是一个指定的GTID的值,比如通过set gtid_next='current_gtid’指定为current_gtid,那么就有两种可能:<br />a. 如果current_gtid已经存在于实例的GTID集合中,接下来执行的这个事务会直接被系统忽略;<br />b. 如果current_gtid没有存在于实例的GTID集合中,就将这个current_gtid分配给接下来要执行的事务,也就是说系统不需要给这个事务生成新的GTID,因此gno也不用加1</p></li></ol><p>每个MySQL实例都维护了一个GTID集合,用来对应“这个实例执行过的所有事务”</p><p>当从库需要跳过某个事务时,在主库上查出GTID,在从库上提交空事务,把这个GTID加入到从库的GTID集合中</p><pre class="highlight"><code class="SQL"><span class="hljs-keyword">set</span> gtid_next<span class="hljs-operator">=</span><span class="hljs-string">'aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10'</span>;<span class="hljs-keyword">begin</span>;<span class="hljs-keyword">commit</span>;<span class="hljs-keyword">set</span> gtid_next<span class="hljs-operator">=</span>automatic;<span class="hljs-keyword">start</span> slave;</code></pre><h3 id="基于gtid的主备切换"><a class="markdownIt-Anchor" href="#基于gtid的主备切换"></a> 基于GTID的主备切换</h3><p>切换命令指定master_auto_position=1表示这个主备关系使用的是GTID协议,不需要指定主库日志文件和偏移量</p><p>我们把A’ 的GTID集合记为set_a,实例B的GTID集合记为set_b,切换流程如下:</p><ol><li>实例B指定主库A’,基于主备协议建立连接。</li><li>实例B把set_b发给主库A’</li><li>实例A’算出set_a与set_b的差集,也就是所有存在于set_a,但是不存在于set_b的GITD的集合,判断A’本地是否包含了这个差集需要的所有binlog事务。<br />a. 如果不包含,表示A’已经把实例B需要的binlog给删掉了,直接返回错误;<br />b. 如果确认全部包含,A’从自己的binlog文件里面,找出第一个不在set_b的事务,发给B;</li><li>之后就从这个事务开始,往后读文件,按顺序取binlog发给B去执行</li></ol><h3 id="gtid和在线ddl"><a class="markdownIt-Anchor" href="#gtid和在线ddl"></a> GTID和在线DDL</h3><p>假设,这两个互为主备关系的库还是实例X和实例Y,且当前主库是X,并且都打开了GTID模式。这时的主备切换流程可以变成下面这样:</p><ul><li><p>在实例X上执行stop slave。</p></li><li><p>在实例Y上执行DDL语句。注意,这里并不需要关闭binlog。</p></li><li><p>执行完成后,查出这个DDL语句对应的GTID,并记为 server_uuid_of_Y:gno。</p></li><li><p>到实例X上执行以下语句序列:</p><pre class="highlight"><code class="SQL"><span class="hljs-keyword">set</span> GTID_NEXT<span class="hljs-operator">=</span>"server_uuid_of_Y:gno";<span class="hljs-keyword">begin</span>;<span class="hljs-keyword">commit</span>;<span class="hljs-keyword">set</span> gtid_next<span class="hljs-operator">=</span>automatic;<span class="hljs-keyword">start</span> slave;</code></pre><p>这样做的目的在于,既可以让实例Y的更新有binlog记录,同时也可以确保不会在实例X上执行这条更新。</p></li><li><p>接下来,执行完主备切换,然后照着上述流程再执行一遍即可</p></li></ul><h2 id="读写分离有哪些坑"><a class="markdownIt-Anchor" href="#读写分离有哪些坑"></a> 读写分离有哪些坑</h2><p>由于主从可能存在延迟,客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,就有可能读到刚刚的事务更新之前的状态,这种现象称为“过期读”</p><p>过期读处理方案包括:</p><ul><li>强制走主库方案</li><li>sleep方案</li><li>判断主备无延迟方案</li><li>配合semi-sync方案</li><li>等主库位点方案</li><li>等GTID方案</li></ul><h3 id="强制走主库方案"><a class="markdownIt-Anchor" href="#强制走主库方案"></a> 强制走主库方案</h3><p>该方案将查询请求分为两类:</p><ol><li>对于必须要拿到最新结果的请求,强制将其发到主库上。比如,在一个交易平台上,卖家发布商品以后,马上要返回主页面,看商品是否发布成功。那么,这个请求需要拿到最新的结果,就必须走主库</li><li>对于可以读到旧数据的请求,才将其发到从库上。在这个交易平台上,买家来逛商铺页面,就算晚几秒看到最新发布的商品,也是可以接受的。那么,这类请求就可以走从库</li></ol><p>这个方案用的最多,但是问题在于存在“所有查询都不能是过期读”的需求,比如金融类业务,那就必须放弃读写分离,所有读写压力都在主库</p><p>下面讨论的是:可以支持读写分离的场景下,有哪些解决过期读的方案</p><h3 id="sleep方案"><a class="markdownIt-Anchor" href="#sleep方案"></a> Sleep方案</h3><p>主库更新后,读从库之前先sleep一下。这个方案假设,大多数情况下主备延迟在1s之内</p><p>该方案可以解决类似Ajax场景下的过期读问题。例如卖家发布商品,直接将卖家输入的内容作为新商品显示出来,并不查从库。等待卖家刷新页面,相当于sleep了一段时间,解决了过期读问题</p><p>该方案存在的问题是不精确:</p><ol><li>如果查询请求本来0.5s就可以在从库上拿到正确结果,也会等到1s</li><li>如果延迟超过1s,还是会出现过期读</li></ol><h3 id="判断主备无延迟方案"><a class="markdownIt-Anchor" href="#判断主备无延迟方案"></a> 判断主备无延迟方案</h3><p>有三种方法:</p><ol><li>每次从库执行查询请求前,先判断seconds_behind_master是否已经等于0。如果还不等于0 ,那就必须等到这个参数变为0才能执行查询请求。</li><li>对比位点。如果Master_Log_File和Relay_Master_Log_File、Read_Master_Log_Pos和Exec_Master_Log_Pos这两组值完全相同表示主备无延迟</li><li>对比GITD。Retrieved_Gtid_Set和Executed_Gtid_Set相同表示是主备无延迟</li></ol><p>该方案比Sleep更准确,方法2和3比1准确,但是不能说精确。因为存在客户端已经收到提交确认,而备库还没收到日志的状态,因此备库认为主备无延迟,从而发生过期读</p><h3 id="配合semi-sync"><a class="markdownIt-Anchor" href="#配合semi-sync"></a> 配合semi-sync</h3><p>为解决上面的问题,引入semi-sync replication:</p><p>semi-sync做了这样的设计:</p><ol><li>事务提交的时候,主库把binlog发给从库</li><li>从库收到binlog以后,发回给主库一个ack</li><li>主库收到ack以后,才能给客户端返回“事务完成”的确认</li></ol><p>开启semi-sync,就表示所有给客户端发送过确认的事务,都确保备库已经收到了这个日志</p><p>semi-sync+判断主备无延迟方案存在两个问题:</p><ol><li>一主多从情况下,因为主库只要收到一个从库的ack就给客户端返回确认,其它未响应ack的从库可能会发生过期读问题</li><li>在业务高峰期,主库的位点或者GITD集合更新很快,这种情况下,可能出现从库一直存在主备延迟导致客户端查询一直等待</li></ol><h3 id="等主库位点方案"><a class="markdownIt-Anchor" href="#等主库位点方案"></a> 等主库位点方案</h3><p>该方案解决了前面两个问题</p><p>命令:</p><pre class="highlight"><code class="SQL"><span class="hljs-keyword">select</span> master_pos_wait(file, pos[, timeout]);</code></pre><p>这条命令的逻辑如下:</p><ol><li>它是在从库执行的</li><li>参数file和pos指的是主库上的文件名和位置</li><li>timeout可选,设置为正整数N表示这个函数最多等待N秒</li></ol><p>为了解决前面两个问题,流程如下:</p><ol><li>trx1事务更新完成后,马上执行show master status得到当前主库执行到的File和Position;</li><li>选定一个从库执行查询语句;</li><li>在从库上执行select master_pos_wait(File, Position, 1);</li><li>如果返回值是>=0的整数,则在这个从库执行查询语句;</li><li>否则,到主库执行查询语句。<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230209-164909954.png" alt="图 17" /></li></ol><h3 id="gtid方案"><a class="markdownIt-Anchor" href="#gtid方案"></a> GTID方案</h3><p>等GTID也可以解决前面两个问题</p><p>流程如下:</p><ol><li>trx1事务更新完成后,从返回包直接获取这个事务的GTID,记为gtid1;</li><li>选定一个从库执行查询语句;</li><li>在从库上执行 select wait_for_executed_gtid_set(gtid1, 1);</li><li>如果返回值是0,则在这个从库执行查询语句;</li><li>否则,到主库执行查询语句。<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230209-165315678.png" alt="图 18" /></li></ol><h2 id="如何判断一个数据库是不是出问题了"><a class="markdownIt-Anchor" href="#如何判断一个数据库是不是出问题了"></a> 如何判断一个数据库是不是出问题了</h2><h3 id="select-1-判断"><a class="markdownIt-Anchor" href="#select-1-判断"></a> select 1 判断</h3><p>select 1 成功返回只能说明数据库进程还在,不能说明没问题</p><p>并发连接:通过show precesslist查询连接数,连接数可以远大于并发查询数量<br />并发查询:“当前正在执行”的语句的数量</p><p>线程进入锁等待后,并发线程的计数会减一,即进入锁等待的线程不吃CPU</p><p>假如设置并发线程数是3,下面的情况是A、B、C在并发查询,D先select 1不占并发线程数所以能正常返回,但实际上已经不能正常查询了<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230210-151018093.png" alt="图 19" /></p><h3 id="查表判断"><a class="markdownIt-Anchor" href="#查表判断"></a> 查表判断</h3><p>为了能够检测InnoDB并发线程数过多导致的系统不可用情况,我们需要找一个访问InnoDB的场景。一般的做法是,在系统库(mysql库)里创建一个表,比如命名为health_check,里面只放一行数据,然后定期执行:</p><pre class="highlight"><code class="SQL">mysql<span class="hljs-operator">></span> <span class="hljs-keyword">select</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> mysql.health_check;</code></pre><p>这种方法在磁盘空间满了就无效。因为更新事务要写binlog,而一旦binlog所在磁盘满了,那么所有更新语句都会堵住,但是系统仍然可以读数据</p><h3 id="更新判断"><a class="markdownIt-Anchor" href="#更新判断"></a> 更新判断</h3><p>我们把查询换成更新来作为监控语句。常见做法是放一个timestamp字段表示最后一次检测时间,这条更新语句类似于:</p><pre class="highlight"><code class="SQL">mysql<span class="hljs-operator">></span> <span class="hljs-keyword">update</span> mysql.health_check <span class="hljs-keyword">set</span> t_modified<span class="hljs-operator">=</span>now();</code></pre><p>主库和备库用同样的更新语句可能会出现行冲突,导致主备同步停止,所以mysql.health_check表不能只有一行数据</p><pre class="highlight"><code class="SQL">mysql<span class="hljs-operator">></span> <span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> `health_check` ( `id` <span class="hljs-type">int</span>(<span class="hljs-number">11</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">NULL</span>, `t_modified` <span class="hljs-type">timestamp</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">NULL</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-built_in">CURRENT_TIMESTAMP</span>, <span class="hljs-keyword">PRIMARY</span> KEY (`id`)) ENGINE<span class="hljs-operator">=</span>InnoDB;<span class="hljs-comment">/* 检测命令 */</span><span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> mysql.health_check(id, t_modified) <span class="hljs-keyword">values</span> (@<span class="hljs-variable">@server</span>_id, now()) <span class="hljs-keyword">on</span> duplicate key <span class="hljs-keyword">update</span> t_modified<span class="hljs-operator">=</span>now();</code></pre><p>MySQL规定主备的server_id必须不同,这样主备各自的检测命令就不会冲突</p><p>更新判断存在的问题是“判定慢”。因为更新语句在IO负载已经100%时仍然可能在超时前完成。检测系统看到update命令没有超时,就认为“系统正常”,但实际上正常SQL语句已经很慢了</p><h3 id="内部统计"><a class="markdownIt-Anchor" href="#内部统计"></a> 内部统计</h3><p>前面几种方法都是外部检测,外部检测都需要定时轮询,所以系统可能已经出问题了,但是却需要等到下一个检测发起执行语句的时候,才有可能发现问题,导致主备切换慢</p><p>针对磁盘利用率问题,MySQL 5.6 在file_summary_by_event_name表里统计了每次IO请求的时间,可以设置阈值作为检测逻辑</p><h2 id="误删数据怎么办"><a class="markdownIt-Anchor" href="#误删数据怎么办"></a> 误删数据怎么办?</h2><p>误删分为以下几类:</p><ol><li>使用delete误删数据行</li><li>使用drop table或者truncate table误删数据表</li><li>使用drop database误删数据库</li><li>使用rm误删整个MySQL实例</li></ol><h3 id="误删行"><a class="markdownIt-Anchor" href="#误删行"></a> 误删行</h3><p>可以使用Flashback恢复,原理是修改binlog的内容,拿回原库重放。使用这个方案的前提是确保binlog_format=row 和 binlog_row_image=FULL</p><p>建议在备库上执行,再恢复回主库</p><h3 id="误删库表"><a class="markdownIt-Anchor" href="#误删库表"></a> 误删库/表</h3><p>这种情况要求线上有定期的全量备份,并且实时备份binlog</p><p>假如有人中午12点误删了一个库,恢复数据的流程如下:</p><ol><li>取最近一次全量备份,假设这个库是一天一备,上次备份是当天0点;</li><li>用备份恢复出一个临时库;</li><li>从日志备份里面,取出凌晨0点之后的日志;</li><li>把这些日志,除了误删除数据的语句外,全部应用到临时库</li></ol><p>如果临时库有多个数据库,在使用mysqlbinlog时可以加上-database指定误删表所在库,加速数据恢复</p><p>在应用日志的时候,需要跳过12点误操作的那个语句的binlog:</p><ul><li>如果原实例没有使用GTID模式,只能在应用到包含12点的binlog文件的时候,先用–stop-position参数执行到误操作之前的日志,然后再用–start-position从误操作之后的日志继续执行;</li><li>如果实例使用了GTID模式,就方便多了。假设误操作命令的GTID是gtid1,那么只需要执行set gtid_next=gtid1;begin;commit; 先把这个GTID加到临时实例的GTID集合,之后按顺序执行binlog的时候,就会自动跳过误操作的语句</li></ul><p>即使这样,使用mysqlbinlog方法恢复数据仍然不快,因为:</p><ol><li>mysqlbinlog并不能指定只解析一个表的日志</li><li>用mysqlbinlog解析出日志应用,应用日志的过程就只能是单线程</li></ol><p>一种加速方法是,在用备份恢复出临时实例之后,将这个临时实例设置成线上备库的从库,这样:</p><ol><li>在start slave之前,先通过执行<br />change replication filter replicate_do_table = (tbl_name) 命令,就可以让临时库只同步误操作的表;</li><li>这样做也可以用上并行复制技术,来加速整个数据恢复过程。</li></ol><h3 id="延迟复制备库"><a class="markdownIt-Anchor" href="#延迟复制备库"></a> 延迟复制备库</h3><p>上面的方案存在“恢复时间不可控问题”,比如一周一备份,第6天误操作,那就需要恢复6天的日志,这个恢复时间可能按天计算</p><p>一般的主备复制结构存在的问题是,如果主库上有个表被误删了,这个命令很快也会被发给所有从库,进而导致所有从库的数据表也都一起被误删了。</p><p>延迟复制的备库是一种特殊的备库,通过 CHANGE MASTER TO MASTER_DELAY = N命令,可以指定这个备库持续保持跟主库有N秒的延迟。</p><p>比如你把N设置为3600,这就代表了如果主库上有数据被误删了,并且在1小时内发现了这个误操作命令,这个命令就还没有在这个延迟复制的备库执行。这时候到这个备库上执行stop slave,再通过之前介绍的方法,跳过误操作命令,就可以恢复出需要的数据。</p><p>这样的话,你就随时可以得到一个,只需要最多再追1小时,就可以恢复出数据的临时实例,也就缩短了整个数据恢复需要的时间</p><h3 id="预防误删库表的方法"><a class="markdownIt-Anchor" href="#预防误删库表的方法"></a> 预防误删库/表的方法</h3><ol><li><p>账号分离,避免写错命令</p><ul><li>只给业务开发同学DML权限,而不给truncate/drop权限。而如果业务开发人员有DDL需求的话,也可以通过开发管理系统得到支持</li><li>即使是DBA团队成员,日常也都规定只使用只读账号,必要的时候才使用有更新权限的账号</li></ul></li><li><p>指定操作规范,避免写错要删除的表名</p><ul><li>删除数据表之前,必须先对表做改名操作。然后,观察一段时间,确保对业务无影响以后再删除这张表。</li><li>改表名的时候,要求给表名加固定的后缀(比如加_to_be_deleted),然后删除表的动作必须通过管理系统执行。并且,管理系删除表的时候,只能删除固定后缀的表。</li></ul></li></ol><h3 id="rm删除数据"><a class="markdownIt-Anchor" href="#rm删除数据"></a> rm删除数据</h3><p>对于有高可用机制的MySQL集群,最不怕rm。只要整个集群没被删掉,HA系统会选出新主库,保证整个集群正常工作。因此备库尽量跨机房、跨城市</p><h2 id="为什么还有kill不掉的语句"><a class="markdownIt-Anchor" href="#为什么还有kill不掉的语句"></a> 为什么还有kill不掉的语句</h2><p>MySQL有两个kill命令:</p><ul><li>kill query+线程id,表示终止这个线程正在执行的语句</li><li>kill connection+线程id,connection可缺省,表示断开这个线程的连接,如果有语句正在执行,先停止语句</li></ul><h3 id="收到kill后线程做什么"><a class="markdownIt-Anchor" href="#收到kill后线程做什么"></a> 收到kill后,线程做什么</h3><p>kill并不是马上停止,而是告诉线程,这条语句已经不需要继续执行了,可以开始“执行停止的逻辑了”</p><p>处理kill query命令的线程做了两件事:</p><ol><li>把目标线程的运行状态改成THD::KILL_QUERY(将变量killed赋值为THD::KILL_QUERY);</li><li>给目标线程发一个信号,通知目标线程处理THD::KILL_QUERY状态。如果目标线程处于等待状态,必须是一个可以被唤醒的等待,否则不会执行到判断线程状态的“埋点”</li></ol><p>处理kill connection命令的线程做了两件事:</p><ol><li>把目标线程状态设置为KILL_CONNECTION</li><li>关闭目标线程的网络连接</li></ol><p>kill无效的两类情况:</p><ol><li>线程没有执行到判断线程状态的逻辑。这种情况有innodb_thread_concurrency 不够用,IO压力过大</li><li>终止逻辑耗时较长。这种情况有kill超大事务、回滚大查询、kill最后阶段的DDL命令</li></ol><p>处于Killed状态的线程,你可以通过影响系统环境来让状态尽早结束。比如并发度不够导致线程没有执行到判断线程状态的逻辑,就增大innodb_thread_concurrency。除此之外,做不了什么,只能等流程自己结束</p><h2 id="大查询会不会打爆内存"><a class="markdownIt-Anchor" href="#大查询会不会打爆内存"></a> 大查询会不会打爆内存</h2><p>主机内存小于表的大小,全表扫描不会用光主机内存,否则逻辑备份早就挂了</p><h3 id="全表扫描对server层的影响"><a class="markdownIt-Anchor" href="#全表扫描对server层的影响"></a> 全表扫描对server层的影响</h3><p>假设对200G的表 db1.t 全表扫描,需要保留结果到客户端,会使用类似命令:</p><pre class="highlight"><code class="SQL">mysql <span class="hljs-operator">-</span>h$host <span class="hljs-operator">-</span>P$port <span class="hljs-operator">-</span>u$<span class="hljs-keyword">user</span> <span class="hljs-operator">-</span>p$pwd <span class="hljs-operator">-</span>e "select * from db1.t" <span class="hljs-operator">></span> $target_file</code></pre><p>服务端不保存完整的查询结果集,取数据和发数据的流程是这样的:</p><ol><li>获取一行,写到net_buffer中</li><li>重复获取行,直到net_buffer写满,调用网络接口发出去</li><li>如果发送成功,就清空net_buffer,然后继续取下一行,并写入net_buffer</li><li>如果发送函数返回EAGAIN或WSAEWOULDBLOCK,就表示本地网络栈(socket send buffer)写满了,进入等待。直到网络栈重新可写,再继续发送</li></ol><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230211-155742705.png" alt="图 21" /></p><p>从这个流程可以看出:</p><ol><li>一个查询在发送过程中,占用的MySQL内部的内存最大就是net_buffer_length这么大,并不会达到200G;</li><li>socket send buffer 也不可能达到200G(默认定义/proc/sys/net/core/wmem_default),如果socket send buffer被写满,就会暂停读数据的流程。</li></ol><h3 id="全表扫描对innodb层的影响"><a class="markdownIt-Anchor" href="#全表扫描对innodb层的影响"></a> 全表扫描对InnoDB层的影响</h3><p>数据页在Buffer Pool(BP)中管理,BP可以起到加速查询的作用,作用效果依赖于一个重要指标:内存命中率</p><p>BP的大小由参数 innodb_buffer_pool_size 确定,一般设置成可用物理内存的60%~80%</p><p>如果BP满了,要从磁盘读入一个数据页,就要淘汰一个旧数据页,InnoDB内存管理用的是改进后的最近最少使用(LRU)算法</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230211-163000032.png" alt="图 23" /></p><p>上图head指向刚刚被访问过的数据页</p><p>基本的LRU算法在遇到全表扫描历史数据表时,会出现内存命中率急剧下降,磁盘压力增加,SQL响应变慢的情况</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230211-162929098.png" alt="图 22" /></p><p>InnoDB按照 5:3 将LRU链表分成young区和old区,LRU_old指向old区域第一个位置,即整个链表的5/8处</p><p>改进后的LRU算法如下:</p><ol><li>访问young区域的数据页,和之前的算法一样,移动到链表头</li><li>访问不在链表中的数据页,淘汰tail指向的最后一页,在LRU_old处插入新数据页</li><li>访问old区域的数据页,若这个数据页在LRU链表中存在时间超过1s,就移动到链表头部,否则不动,1s由参数innodb_old_blocks_time控制</li></ol><p>这个策略在扫描大表时不会对young区域造成影响,保证BP响应正常业务的查询命中率</p><h2 id="可不可以使用join"><a class="markdownIt-Anchor" href="#可不可以使用join"></a> 可不可以使用join</h2><p>先创建两个DDL一样的表</p><pre class="highlight"><code class="SQL"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> `t2` ( `id` <span class="hljs-type">int</span>(<span class="hljs-number">11</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">NULL</span>, `a` <span class="hljs-type">int</span>(<span class="hljs-number">11</span>) <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">NULL</span>, `b` <span class="hljs-type">int</span>(<span class="hljs-number">11</span>) <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">NULL</span>, <span class="hljs-keyword">PRIMARY</span> KEY (`id`), KEY `a` (`a`)) ENGINE<span class="hljs-operator">=</span>InnoDB;<span class="hljs-comment">/*省略给t2插入1000行数据*/</span><span class="hljs-keyword">create</span> <span class="hljs-keyword">table</span> t1 <span class="hljs-keyword">like</span> t2;<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> t1 (<span class="hljs-keyword">select</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> t2 <span class="hljs-keyword">where</span> id<span class="hljs-operator"><=</span><span class="hljs-number">100</span>)</code></pre><h3 id="index-nested-loop-join"><a class="markdownIt-Anchor" href="#index-nested-loop-join"></a> Index Nested-Loop Join</h3><p>有如下语句:</p><pre class="highlight"><code class="SQL"><span class="hljs-keyword">select</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> t1 straight_join t2 <span class="hljs-keyword">on</span> (t1.a<span class="hljs-operator">=</span>t2.a);</code></pre><p>straight_join让MySQL使用固定的连接方式执行查询,这里t1是驱动表,t2是被驱动表</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230211-165839494.png" alt="图 24" /></p><p>这个语句的执行流程如下:</p><ol><li>从表t1中读入一行数据R</li><li>从数据行R中,取出a字段到表t2里去查</li><li>取出表t2中满足条件的行,跟R组成一行,作为结果集的一部分</li><li>重复执行步骤1到3,直到表t1的末尾循环结束</li></ol><p>在形式上,这个过程和我们写程序时的嵌套查询类似,并且可以用上被驱动表的索引,所以我们称之为“Index Nested-Loop Join”,简称NLJ</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230211-170323544.png" alt="图 25" /></p><p>在流程里:</p><ol><li>对驱动表t1做了全表扫描,这个过程需要扫描100行</li><li>对于每一行R,根据a字段去表t2查找,走的是树搜索过程。由于我们构造的数据是一一对应的,因此每次搜索都只扫描一行,也就是总共扫描100行</li><li>所以,整个执行流程,总扫描行数是200</li></ol><p>如果不用join,上面的连接需求,用单表查询实现的话,扫描行数一样,但是交互次数多,而且客户端要自己拼接SQL语句和结果,因此不如直接join</p><p>假设驱动表行数是N。被驱动表行数是M,被驱动表查一行数据要先走索引a,再走主键索引,因此时间复杂度是<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>2</mn><mo>∗</mo><mi>l</mi><mi>o</mi><msub><mi>g</mi><mn>2</mn></msub><mi>M</mi></mrow><annotation encoding="application/x-tex">2*log_2 M</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.64444em;vertical-align:0em;"></span><span class="mord">2</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mbin">∗</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span></span><span class="base"><span class="strut" style="height:0.8888799999999999em;vertical-align:-0.19444em;"></span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">o</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.30110799999999993em;"><span style="top:-2.5500000000000003em;margin-left:-0.03588em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord mathnormal" style="margin-right:0.10903em;">M</span></span></span></span>。驱动表要扫描N行,然后每行都要去被驱动表上匹配,所以整个执行过程复杂度是 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>N</mi><mo>+</mo><mi>N</mi><mo>∗</mo><mn>2</mn><mo>∗</mo><mi>l</mi><mi>o</mi><msub><mi>g</mi><mn>2</mn></msub><mi>M</mi></mrow><annotation encoding="application/x-tex">N+N*2*log_2 M</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.76666em;vertical-align:-0.08333em;"></span><span class="mord mathnormal" style="margin-right:0.10903em;">N</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span></span><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord mathnormal" style="margin-right:0.10903em;">N</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mbin">∗</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span></span><span class="base"><span class="strut" style="height:0.64444em;vertical-align:0em;"></span><span class="mord">2</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mbin">∗</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span></span><span class="base"><span class="strut" style="height:0.8888799999999999em;vertical-align:-0.19444em;"></span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">o</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.30110799999999993em;"><span style="top:-2.5500000000000003em;margin-left:-0.03588em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord mathnormal" style="margin-right:0.10903em;">M</span></span></span></span>。显然N影响更大,因此让小表做驱动表</p><h3 id="simple-nested-loop-join"><a class="markdownIt-Anchor" href="#simple-nested-loop-join"></a> Simple Nested-Loop Join</h3><p>现在语句改成如下:</p><pre class="highlight"><code class="SQL"><span class="hljs-keyword">select</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> t1 straight_join t2 <span class="hljs-keyword">on</span> (t1.a<span class="hljs-operator">=</span>t2.b);</code></pre><p>由于t2的字段b没有索引,每次到t2去匹配都要做全表扫描,因此这个查询要扫描100*1000=10万行。</p><h3 id="block-nested-loop-join"><a class="markdownIt-Anchor" href="#block-nested-loop-join"></a> Block Nested-Loop Join</h3><p>当被驱动表上没有可用索引,MySQL使用的算法流程如下:</p><ol><li>把表t1的数据读入线程内存join_buffer中,由于我们这个语句中写的是select *,因此是把整个表t1放入了内存;</li><li>扫描表t2,把表t2中的每一行取出来,跟join_buffer中的数据做对比,满足join条件的,作为结果集的一部分返回。</li></ol><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230211-172216486.png" alt="图 26" /></p><p>该算法和Simple Nested-Loop Join算法扫描的行数一样多,但该算法是内存操作,速度更快。碰到大表不能放入join_buffer的情况就分多次放</p><p>总结一下:</p><p>第一个问题:能不能使用join语句?</p><ol><li>如果可以使用Index Nested-Loop Join算法,也就是说可以用上被驱动表上的索引,其实是没问题的;</li><li>如果使用Block Nested-Loop Join算法,扫描行数就会过多。尤其是在大表上的join操作,这样可能要扫描被驱动表很多次,会占用大量的系统资源。所以这种join尽量不要用</li></ol><p>所以在判断要不要使用join语句时,就是看explain结果里面,Extra字段里面有没有出现“Block Nested Loop”字样</p><p>第二个问题:如果要使用join,应该选择大表做驱动表还是选择小表做驱动表?</p><p>总是使用小表做驱动表。更准确地说,在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与join的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表</p><h2 id="join语句怎么优化"><a class="markdownIt-Anchor" href="#join语句怎么优化"></a> join语句怎么优化</h2><p>创建两个表t1、t2(id int primary key, a int, b int, index(a))。给表t1插入1000行数据,每一行a=1001-id,即字段a是逆序的。给表t2插入100万行数据</p><h3 id="multi-range-read优化"><a class="markdownIt-Anchor" href="#multi-range-read优化"></a> Multi-Range Read优化</h3><p>现在有SQL语句:</p><pre class="highlight"><code class="SQL"><span class="hljs-keyword">select</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> t1 <span class="hljs-keyword">where</span> a<span class="hljs-operator">>=</span><span class="hljs-number">1</span> <span class="hljs-keyword">and</span> a<span class="hljs-operator"><=</span><span class="hljs-number">100</span>;</code></pre><p>MRR优化的设计思路是:大多数的数据都是按照主键递增顺序插入的,所以按照主键的递增顺序查询的话,对磁盘的读比较接近顺序读,能够提升读性能。使用MRR的语句的执行流程如下:</p><ol><li>根据索引a,定位到满足条件的记录,将id值放入read_rnd_buffer中;</li><li>将read_rnd_buffer中的id进行递增排序</li><li>排序后的id数组,依次到主键id索引中查记录,并作为结果返回。</li></ol><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230211-185805403.png" alt="图 27" /></p><p>MRR能够提升性能的核心在于,这条查询语句在索引a上做的是一个范围查询(也就是说,这是一个多值查询),可以得到足够多的主键id。这样通过排序以后,再去主键索引查数据,才能体现出“顺序性”的优势</p><h3 id="batched-key-access"><a class="markdownIt-Anchor" href="#batched-key-access"></a> Batched Key Access</h3><p>MySQL 5.6 引入Batched Key Acess(BKA)算法,这个算法是对NLJ算法的优化</p><p>NLJ算法执行的逻辑是:从驱动表t1,一行行地取出a的值,再到被驱动表t2去做join。也就是说,对于表t2来说,每次都是匹配一个值。这时,MRR的优势就用不上了</p><p>优化思路就是,从表t1里一次性多拿出些行,一起传给表t2。取出的数据先放到join_buffer</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230211-190649703.png" alt="图 28" /></p><h3 id="bnl算法的性能问题"><a class="markdownIt-Anchor" href="#bnl算法的性能问题"></a> BNL算法的性能问题</h3><ol><li>可能会多次扫描被驱动表,占用磁盘IO资源;</li><li>判断join条件需要执行M*N次对比(M、N分别是两张表的行数),如果是大表就会占用非常多的CPU资源;</li><li>可能会导致Buffer Pool的热数据被淘汰,影响内存命中率。</li></ol><p>如果explain命令发现优化器使用BNL算法。我们就需要优化,常见做法是,给被驱动表的join字段加上索引,把BNL算法转成BKA算法</p><h3 id="bnl转bka"><a class="markdownIt-Anchor" href="#bnl转bka"></a> BNL转BKA</h3><pre class="highlight"><code class="SQL"><span class="hljs-keyword">select</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> t1 <span class="hljs-keyword">join</span> t2 <span class="hljs-keyword">on</span> (t1.b<span class="hljs-operator">=</span>t2.b) <span class="hljs-keyword">where</span> t2.b<span class="hljs-operator">>=</span><span class="hljs-number">1</span> <span class="hljs-keyword">and</span> t2.b<span class="hljs-operator"><=</span><span class="hljs-number">2000</span>;</code></pre><p>在索引创建资源开销大情况下,可以考虑使用临时表:</p><ol><li>把表t2中满足条件的数据放在临时表tmp_t中;</li><li>为了让join使用BKA算法,给临时表tmp_t的字段b加上索引;</li><li>让表t1和tmp_t做join操作</li></ol><p>对应的SQL语句:</p><pre class="highlight"><code class="SQL"><span class="hljs-keyword">create</span> temporary <span class="hljs-keyword">table</span> temp_t(id <span class="hljs-type">int</span> <span class="hljs-keyword">primary</span> key, a <span class="hljs-type">int</span>, b <span class="hljs-type">int</span>, index(b))engine<span class="hljs-operator">=</span>innodb;<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> temp_t <span class="hljs-keyword">select</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> t2 <span class="hljs-keyword">where</span> b<span class="hljs-operator">>=</span><span class="hljs-number">1</span> <span class="hljs-keyword">and</span> b<span class="hljs-operator"><=</span><span class="hljs-number">2000</span>;<span class="hljs-keyword">select</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">from</span> t1 <span class="hljs-keyword">join</span> temp_t <span class="hljs-keyword">on</span> (t1.b<span class="hljs-operator">=</span>temp_t.b);</code></pre><h3 id="扩展-hash-join"><a class="markdownIt-Anchor" href="#扩展-hash-join"></a> 扩展-hash join</h3><p>BNL的问题是join_buffer里面维护的是一个无序数组,如果是一个hash表,可以大幅减少判断次数。可以在业务端实现这个优化:</p><ol><li>select * from t1;取得表t1的全部1000行数据,在业务端存入一个hash结构</li><li>select * from t2 where b>=1 and b<=2000; 获取表t2中满足条件的2000行数据。</li><li>把这2000行数据,一行一行地取到业务端,到hash结构的数据表中寻找匹配的数据。满足匹配的条件的这行数据,就作为结果集的一行</li></ol><h2 id="为什么临时表可以重名"><a class="markdownIt-Anchor" href="#为什么临时表可以重名"></a> 为什么临时表可以重名</h2><p>内存表和临时表的区别:</p><ul><li>内存表,指的是使用Memory引擎的表,建表语法是create table … engine=memory。这种表的数据都保存在内存里,系统重启的时候会被清空,但是表结构还在。除了这两个特性看上去比较“奇怪”外,从其他的特征上看,它就是一个正常的表</li><li>临时表,可以使用各种引擎类型 。如果是使用InnoDB引擎或者MyISAM引擎的临时表,写数据的时候是写到磁盘上的。当然,临时表也可以使用Memory引擎</li></ul><h3 id="临时表的特性"><a class="markdownIt-Anchor" href="#临时表的特性"></a> 临时表的特性</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230211-193554870.png" alt="图 29" /></p><p>临时表在使用上有以下几个特点:</p><ol><li>建表语法是create temporary table …。</li><li>一个临时表只能被创建它的session访问,对其他线程不可见。所以,图中session A创建的临时表t,对于session B就是不可见的。</li><li>临时表可以与普通表同名</li><li>session A内有同名的临时表和普通表的时候,show create语句,以及增删改查语句访问的是临时表</li><li>show tables命令不显示临时表</li></ol><h3 id="临时表的应用"><a class="markdownIt-Anchor" href="#临时表的应用"></a> 临时表的应用</h3><p>由于不用担心线程之间的重名冲突,临时表经常会被用在复杂查询的优化过程中。其中,分库分表系统的跨库查询就是一个典型的使用场景。</p><p>一般分库分表的场景,就是要把一个逻辑上的大表分散到不同的数据库实例上。比如。将一个大表ht,按照字段f,拆分成1024个分表,然后分布到32个数据库实例上。如下图所示:</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230211-195543281.png" alt="图 30" /></p><p>分区key的选择是以“减少跨库和跨表查询”为依据的。如果大部分的语句都会包含f的等值条件,那么就要用f做分区键</p><p>比如:</p><pre class="highlight"><code class="SQL"><span class="hljs-keyword">select</span> v <span class="hljs-keyword">from</span> ht <span class="hljs-keyword">where</span> f<span class="hljs-operator">=</span>N;</code></pre><p>可以通过分表规则(比如,N%1024)来确认需要的数据被放在了哪个分表上</p><p>但是,如果这个表上还有另外一个索引k,并且查询语句是这样的:</p><pre class="highlight"><code class="SQL"><span class="hljs-keyword">select</span> v <span class="hljs-keyword">from</span> ht <span class="hljs-keyword">where</span> k <span class="hljs-operator">>=</span> M <span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> t_modified <span class="hljs-keyword">desc</span> limit <span class="hljs-number">100</span>;</code></pre><p>由于查询条件里面没有用到分区字段f,只能到所有的分区中去查找满足条件的所有行,然后统一做order by 的操作。这种情况有两种思路:</p><ul><li>在proxy层的进程代码中实现排序。优势是快,缺点是工作量大,proxy端压力大</li><li>把分库数据汇总到一个表中,再在汇总上操作。如下图所示</li></ul><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230211-200341781.png" alt="图 31" /></p><h3 id="为什么临时表可以重名-2"><a class="markdownIt-Anchor" href="#为什么临时表可以重名-2"></a> 为什么临时表可以重名?</h3><pre class="highlight"><code class="SQL"><span class="hljs-keyword">create</span> temporary <span class="hljs-keyword">table</span> temp_t(id <span class="hljs-type">int</span> <span class="hljs-keyword">primary</span> key)engine<span class="hljs-operator">=</span>innodb;</code></pre><p>执行该语句,MySQL会创建一个frm文件保存表结构定义。该文件放在临时文件目录下,文件名的后缀是.frm,前缀是“#sql{进程id}_{线程id}_序列号”</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230211-201716828.png" alt="图 32" /></p><p>除了文件名不同,内存里面也有一套机制区别不同的表,每个表都对应一个table_def_key</p><ul><li>一个普通表的table_def_key的值是由“库名+表名”得到的,所以如果你要在同一个库下创建两个同名的普通表,创建第二个表的过程中就会发现table_def_key已经存在了。</li><li>而对于临时表,table_def_key在“库名+表名”基础上,又加入了“server_id+thread_id”</li></ul><h3 id="临时表和主备复制"><a class="markdownIt-Anchor" href="#临时表和主备复制"></a> 临时表和主备复制</h3><p>如果当前的binlog_format=row,那么跟临时表有关的语句,就不会记录到binlog里</p><p>如果binlog_format=statment/mixed,创建临时表的语句会传到备库,由备库的同步线程执行。因为主库的线程退出时会自动删除临时表,但是备库同步线程是持续运行的,所以还需要在主库上再写一个DROP TEMPORARY TABLE传给备库执行</p><p>主库上不同线程创建同名的临时表是没关系的,但是传到备库怎么处理?</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230211-203432563.png" alt="图 33" /></p><p>MySQL在记录binlog的时候,会把主库执行这个语句的线程id写到binlog中。这样,在备库的应用线程就能够知道执行每个语句的主库线程id,并利用这个线程id来构造临时表的table_def_key:</p><ol><li>session A的临时表t1,在备库的table_def_key就是:库名+t1+“M的serverid”+“session A的thread_id”;</li><li>session B的临时表t1,在备库的table_def_key就是 :库名+t1+“M的serverid”+“session B的thread_id”</li></ol><h2 id="为什么会使用内部临时表"><a class="markdownIt-Anchor" href="#为什么会使用内部临时表"></a> 为什么会使用内部临时表</h2><h3 id="union-执行流程"><a class="markdownIt-Anchor" href="#union-执行流程"></a> union 执行流程</h3><p>假设有表t1:</p><pre class="highlight"><code class="SQL"><span class="hljs-keyword">create</span> <span class="hljs-keyword">table</span> t1(id <span class="hljs-type">int</span> <span class="hljs-keyword">primary</span> key, a <span class="hljs-type">int</span>, b <span class="hljs-type">int</span>, index(a));delimiter ;;<span class="hljs-keyword">create</span> <span class="hljs-keyword">procedure</span> idata()<span class="hljs-keyword">begin</span> <span class="hljs-keyword">declare</span> i <span class="hljs-type">int</span>; <span class="hljs-keyword">set</span> i<span class="hljs-operator">=</span><span class="hljs-number">1</span>; while(i<span class="hljs-operator"><=</span><span class="hljs-number">1000</span>)do <span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> t1 <span class="hljs-keyword">values</span>(i, i, i); <span class="hljs-keyword">set</span> i<span class="hljs-operator">=</span>i<span class="hljs-operator">+</span><span class="hljs-number">1</span>; <span class="hljs-keyword">end</span> while;<span class="hljs-keyword">end</span>;;delimiter ;<span class="hljs-keyword">call</span> idata();</code></pre><p>然后执行:</p><pre class="highlight"><code class="SQL">(<span class="hljs-keyword">select</span> <span class="hljs-number">1000</span> <span class="hljs-keyword">as</span> f) <span class="hljs-keyword">union</span> (<span class="hljs-keyword">select</span> id <span class="hljs-keyword">from</span> t1 <span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> id <span class="hljs-keyword">desc</span> limit <span class="hljs-number">2</span>);</code></pre><p>这个语句的执行流程是这样的:</p><ol><li><p>创建一个内存临时表,这个临时表只有一个整型字段f,并且f是主键字段</p></li><li><p>执行第一个子查询,得到1000这个值,并存入临时表中</p></li><li><p>执行第二个子查询:</p><ul><li>拿到第一行id=1000,试图插入临时表中。但由于1000这个值已经存在于临时表了,违反了唯一性约束,所以插入失败,然后继续执行;</li><li>取到第二行id=999,插入临时表成功</li></ul></li><li><p>从临时表中按行取出数据,返回结果,并删除临时表,结果中包含两行数据分别是1000和999</p></li></ol><p>如果使用union all,就没有去重,执行的时候是依次执行子查询,得到的结果直接作为结果集的一部分,不需要临时表</p><h3 id="group-by-执行流程"><a class="markdownIt-Anchor" href="#group-by-执行流程"></a> group by 执行流程</h3><pre class="highlight"><code class="SQL"><span class="hljs-keyword">select</span> id<span class="hljs-operator">%</span><span class="hljs-number">10</span> <span class="hljs-keyword">as</span> m, <span class="hljs-built_in">count</span>(<span class="hljs-operator">*</span>) <span class="hljs-keyword">as</span> c <span class="hljs-keyword">from</span> t1 <span class="hljs-keyword">group</span> <span class="hljs-keyword">by</span> m;</code></pre><p>这个语句的执行流程如下:</p><ol><li><p>创建内存临时表,表里有两个字段m和c,主键是m</p></li><li><p>扫描表t1的索引a,依次取出叶子节点上的id值,计算id%10的结果,记为x;</p><ul><li>如果临时表中没有主键为x的行,就插入一个记录(x,1);</li><li>如果表中有主键为x的行,就将x这一行的c值加1</li></ul></li><li><p>遍历完成后,再根据字段m做排序,得到结果集返回给客户端</p></li></ol><p>如果不需要排序,在语句末尾加上order by null</p><p>当内存临时表大小达到上限时,会转成磁盘临时表,磁盘临时表默认使用的引擎是InnoDB</p><h3 id="group-by-优化方法-索引"><a class="markdownIt-Anchor" href="#group-by-优化方法-索引"></a> group by 优化方法 --索引</h3><p>新增一列,给这列加索引</p><pre class="highlight"><code class="SQL"><span class="hljs-keyword">alter</span> <span class="hljs-keyword">table</span> t1 <span class="hljs-keyword">add</span> <span class="hljs-keyword">column</span> z <span class="hljs-type">int</span> generated always <span class="hljs-keyword">as</span>(id <span class="hljs-operator">%</span> <span class="hljs-number">100</span>), <span class="hljs-keyword">add</span> index(z);</code></pre><p>对这列group by:</p><pre class="highlight"><code class="SQL"><span class="hljs-keyword">select</span> z, <span class="hljs-built_in">count</span>(<span class="hljs-operator">*</span>) <span class="hljs-keyword">as</span> c <span class="hljs-keyword">from</span> t1 <span class="hljs-keyword">group</span> <span class="hljs-keyword">by</span> z;</code></pre><h3 id="group-by-优化方法-直接排序"><a class="markdownIt-Anchor" href="#group-by-优化方法-直接排序"></a> group by 优化方法 --直接排序</h3><p>碰到不能加索引的场景就得老老实实做排序</p><p>在group by语句中加入SQL_BIG_RESULT这个提示(hint),就可以告诉优化器:这个语句涉及的数据量很大,请直接用磁盘临时表</p><pre class="highlight"><code class="SQL"><span class="hljs-keyword">select</span> SQL_BIG_RESULT id<span class="hljs-operator">%</span><span class="hljs-number">100</span> <span class="hljs-keyword">as</span> m, <span class="hljs-built_in">count</span>(<span class="hljs-operator">*</span>) <span class="hljs-keyword">as</span> c <span class="hljs-keyword">from</span> t1 <span class="hljs-keyword">group</span> <span class="hljs-keyword">by</span> m;</code></pre><p>这个语句的执行流程如下:</p><ol><li>初始化sort_buffer,确定放入一个整型字段,记为m</li><li>扫描表t1的索引a,依次取出里面的id值, 将 id%100的值存入sort_buffer中</li><li>扫描完成后,对sort_buffer的字段m做排序(如果sort_buffer内存不够用,就会利用磁盘临时文件辅助排序)</li><li>排序完成后,就得到了一个有序数组。顺序扫描一遍就可以得到结果</li></ol><p>基于上面的union、union all和group by语句的执行过程的分析,我们来回答文章开头的问题:MySQL什么时候会使用内部临时表?</p><ol><li>如果语句执行过程可以一边读数据,一边直接得到结果,是不需要额外内存的,否则就需要额外的内存,来保存中间结果</li><li>join_buffer是无序数组,sort_buffer是有序数组,临时表是二维表结构</li><li>如果执行逻辑需要用到二维表特性,就会优先考虑使用临时表。比如我们的例子中,union需要用到唯一索引约束, group by还需要用到另外一个字段来存累积计数。</li></ol><h2 id="都说innodb好那还要不要使用memory引擎"><a class="markdownIt-Anchor" href="#都说innodb好那还要不要使用memory引擎"></a> 都说InnoDB好,那还要不要使用Memory引擎</h2><h3 id="内存表的数据组织结构"><a class="markdownIt-Anchor" href="#内存表的数据组织结构"></a> 内存表的数据组织结构</h3><p>假设有两张表t1,t2,t1使用Memory引擎,t2使用InnoDB引擎</p><pre class="highlight"><code class="SQL"><span class="hljs-keyword">create</span> <span class="hljs-keyword">table</span> t1(id <span class="hljs-type">int</span> <span class="hljs-keyword">primary</span> key, c <span class="hljs-type">int</span>) engine<span class="hljs-operator">=</span>Memory;<span class="hljs-keyword">create</span> <span class="hljs-keyword">table</span> t2(id <span class="hljs-type">int</span> <span class="hljs-keyword">primary</span> key, c <span class="hljs-type">int</span>) engine<span class="hljs-operator">=</span>innodb;<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> t1 <span class="hljs-keyword">values</span>(<span class="hljs-number">1</span>,<span class="hljs-number">1</span>),(<span class="hljs-number">2</span>,<span class="hljs-number">2</span>),(<span class="hljs-number">3</span>,<span class="hljs-number">3</span>),(<span class="hljs-number">4</span>,<span class="hljs-number">4</span>),(<span class="hljs-number">5</span>,<span class="hljs-number">5</span>),(<span class="hljs-number">6</span>,<span class="hljs-number">6</span>),(<span class="hljs-number">7</span>,<span class="hljs-number">7</span>),(<span class="hljs-number">8</span>,<span class="hljs-number">8</span>),(<span class="hljs-number">9</span>,<span class="hljs-number">9</span>),(<span class="hljs-number">0</span>,<span class="hljs-number">0</span>);<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> t2 <span class="hljs-keyword">values</span>(<span class="hljs-number">1</span>,<span class="hljs-number">1</span>),(<span class="hljs-number">2</span>,<span class="hljs-number">2</span>),(<span class="hljs-number">3</span>,<span class="hljs-number">3</span>),(<span class="hljs-number">4</span>,<span class="hljs-number">4</span>),(<span class="hljs-number">5</span>,<span class="hljs-number">5</span>),(<span class="hljs-number">6</span>,<span class="hljs-number">6</span>),(<span class="hljs-number">7</span>,<span class="hljs-number">7</span>),(<span class="hljs-number">8</span>,<span class="hljs-number">8</span>),(<span class="hljs-number">9</span>,<span class="hljs-number">9</span>),(<span class="hljs-number">0</span>,<span class="hljs-number">0</span>);</code></pre><p>然后,分别执行<code>select *from t1</code>和<code>select* from t2</code>。t2表的(0,0)出现在第一行,t1表出现在最后一行</p><p>这是因为InnoDB引擎的数据就存在主键索引上,而主键索引是有序存储的,在执行select *的时候,就会按照叶子节点从左到右扫描,所以得到的结果里,0就出现在第一行</p><p>而Memory引擎的数据和索引是分开的。主键索引存的是每个数据的位置。执行select *走的是全表扫描数据数组<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230212-161412973.png" alt="图 34" /></p><p>InnoDB和Memory引擎的数据组织方式是不同的:</p><ul><li>InnoDB引擎把数据放在主键索引上,其他索引上保存的是主键id。这种方式,我们称之为索引组织表</li><li>Memory引擎采用的是把数据单独存放,索引上保存数据位置的数据组织形式,我们称之为堆组织表</li></ul><p>两个引擎的一些典型不同:</p><ol><li>InnoDB表的数据总是有序存放的,而内存表的数据就是按照写入顺序存放的</li><li>当数据文件有空洞的时候,InnoDB表在插入新数据的时候,为了保证数据有序性,只能在固定的位置写入新值,而内存表找到空位就可以插入新值;</li><li>数据位置发生变化的时候,InnoDB表只需要修改主键索引,而内存表需要修改所有索引</li><li>InnoDB表用主键索引查询时需要走一次索引查找,用普通索引查询的时候,需要走两次索引查找。而内存表没有这个区别,所有索引的“地位”都是相同的</li><li>InnoDB支持变长数据类型,不同记录的长度可能不同;内存表不支持Blob 和 Text字段,并且即使定义了varchar(N),实际也当作char(N),也就是固定长度字符串来存储,因此内存表的每行数据长度相同。</li></ol><h3 id="hash索引和b-tree索引"><a class="markdownIt-Anchor" href="#hash索引和b-tree索引"></a> hash索引和B-Tree索引</h3><p>内存表的范围查询不能走主键索引,但是可以加一个B-Tree索引,B-Tree索引类似于InnoDB的B+树索引</p><pre class="highlight"><code class="SQL"><span class="hljs-keyword">alter</span> <span class="hljs-keyword">table</span> t1 <span class="hljs-keyword">add</span> index a_btree_index <span class="hljs-keyword">using</span> btree (id);</code></pre><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230212-162250512.png" alt="图 35" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230212-162257282.png" alt="图 36" /></p><p>不建议在生产环境使用内存表,原因有两方面:</p><ol><li>锁粒度问题。内存表不支持行锁,只支持表锁</li><li>数据持久化问题</li></ol><h2 id="自增主键为什么不是连续的"><a class="markdownIt-Anchor" href="#自增主键为什么不是连续的"></a> 自增主键为什么不是连续的</h2><p>自增主键可以让主键索引尽量保持递增顺序插入,避免页分裂,因此索引更紧凑,但自增主键不能保证连续递增</p><h3 id="自增值保存在哪"><a class="markdownIt-Anchor" href="#自增值保存在哪"></a> 自增值保存在哪?</h3><p>InnoDB的自增值保存在内存中。每次重启MySQL都会计算max(id)+1作为自增值。8.0版本,重启的时候依靠redo log恢复自增值</p><h3 id="自增值修改机制"><a class="markdownIt-Anchor" href="#自增值修改机制"></a> 自增值修改机制</h3><p>假设,某次插入的值是X,当前的自增值是Y</p><ol><li>如果X < Y,那么自增值不变</li><li>如果X >= Y,将当前自增值修改为新的自增值 Z = auto_increment_offset+k*auto_increment_increment。Z > X,auto_increment_offset是自增初始值,auto_increment_increment是自增步长,k是自然数</li></ol><h3 id="自增值的修改时机"><a class="markdownIt-Anchor" href="#自增值的修改时机"></a> 自增值的修改时机</h3><p>自增值在真正执行插入数据的操作之前修改。如果因为唯一键冲突导致插入失败会出现id不连续,事务回滚也是类似现象</p><h3 id="自增锁的优化"><a class="markdownIt-Anchor" href="#自增锁的优化"></a> 自增锁的优化</h3><p>自增id锁并不是一个事务锁,而是每次申请完就马上释放,以便允许别的事务再申请。建议innodb_autoinc_lock_mode设置成2,即前面的策略,同时binlog_format=row,避免insert … select造成主备数据不一致</p><h2 id="insert语句的锁为什么这么多"><a class="markdownIt-Anchor" href="#insert语句的锁为什么这么多"></a> insert语句的锁为什么这么多</h2><h3 id="insert-select-语句"><a class="markdownIt-Anchor" href="#insert-select-语句"></a> insert … select 语句</h3><p>在可重复读隔离级别下,binlog_format=statement时,执行 insert … select 语句会对select表的需要访问的资源加锁。加锁是为了避免主备不一致</p><h3 id="insert-循环写入"><a class="markdownIt-Anchor" href="#insert-循环写入"></a> insert 循环写入</h3><p>如果把select表的结果insert到select表中,会对select表全表扫描,创建一个临时表,再将select结果insert回表。这么做的原因是:这类一边遍历数据,一边更新数据的情况,如果读出来的数据直接写回原表,就可能在遍历过程中,读到刚刚插入的记录,新插入的记录如果参与计算逻辑,就跟语义不符</p><p>优化方法是:手动创建内存临时表,先 insert临时表select目标表,再 insert目标表select临时表,这样就不会对目标表全表扫描</p><h3 id="insert-唯一键冲突"><a class="markdownIt-Anchor" href="#insert-唯一键冲突"></a> insert 唯一键冲突</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230213-132127456.png" alt="图 37" /></p><p>在session A执行rollback语句回滚的时候,session C几乎同时发现死锁并返回</p><p>这个死锁产生的逻辑是这样的:</p><ol><li>在T1时刻,启动session A,并执行insert语句,此时在索引c的c=5上加了记录锁。注意,这个索引是唯一索引,因此退化为记录锁</li><li>在T2时刻,session B要执行相同的insert语句,发现了唯一键冲突,加上读锁;同样地,session C也在索引c上,c=5这一个记录上,加了读锁(共享next-key lock)</li><li>T3时刻,session A回滚。这时候,session B和session C都试图继续执行插入操作,都要加上写锁(排它next-key lock)。两个session都要等待对方的行锁,所以就出现了死锁</li></ol><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/IMG_20230213-132326390.png" alt="图 38" /></p><h3 id="insert-into-on-duplicate-key-update"><a class="markdownIt-Anchor" href="#insert-into-on-duplicate-key-update"></a> insert into … on duplicate key update</h3><p>这个语义的逻辑是,插入一行数据,如果碰到唯一键约束,就执行后面的更新语句。它给唯一索引加排它的next-key lock(写锁)</p><h2 id="怎么最快地复制一张表"><a class="markdownIt-Anchor" href="#怎么最快地复制一张表"></a> 怎么最快地复制一张表</h2><p>如果可以控制对原表的扫描行数和加锁范围很小的话,可以直接用insert … select。否则先将数据写到外部文件,再写回目标表,方法有三种:</p><ol><li><p>物理拷贝的方式速度最快,尤其对于大表拷贝来说是最快的方法。如果出现误删表的情况,用备份恢复出误删之前的临时库,然后再把临时库中的表拷贝到生产库上,是恢复数据最快的方法。但是,这种方法的使用也有一定的局限性:</p><ul><li>必须是全表拷贝,不能只拷贝部分数据;</li><li>需要到服务器上拷贝数据,在用户无法登录数据库主机的场景下无法使用;</li><li>由于是通过拷贝物理文件实现的,源表和目标表都是使用InnoDB引擎时才能使用。</li></ul></li><li><p>用mysqldump生成包含INSERT语句文件的方法,可以在where参数增加过滤条件,来实现只导出部分数据。这个方式的不足之一是,不能使用join这种比较复杂的where条件写法</p></li><li><p>用select … into outfile的方法是最灵活的,支持所有的SQL写法。但,这个方法的缺点之一就是,每次只能导出一张表的数据,而且表结构也需要另外的语句单独备份</p></li></ol><h2 id="grant之后要跟着flushprivileges吗"><a class="markdownIt-Anchor" href="#grant之后要跟着flushprivileges吗"></a> grant之后要跟着flushprivileges吗</h2><p>grant语句会同时修改数据表和内存,判断权限的时候使用的是内存数据。因此,规范地使用grant和revoke语句,是不需要随后加上flush privileges语句的。</p><p>flush privileges语句本身会用数据表的数据重建一份内存权限数据,所以在权限数据可能存在不一致的情况下再使用。而这种不一致往往是由于直接用DML语句操作系统权限表导致的,所以我们尽量不要使用这类语句。</p><h2 id="要不要使用分区表"><a class="markdownIt-Anchor" href="#要不要使用分区表"></a> 要不要使用分区表</h2><p>相对于用户分表:</p><p>优势:对业务透明,使用分区表的业务代码更简洁,且可以很方便的清理历史数据<br />劣势:第一次访问的时候需要访问所有分区;共用MDL锁</p>]]></content>
<summary type="html"><h2 id="基础架构一条sql查询语句是如何执行的"><a class="markdownIt-Anchor" href="#基础架构一条sql查询语句是如何执行的"></a> 基础架构:一条SQL查询语句是如何执行的</h2>
<p><img src="https://cd</summary>
<category term="MySQL" scheme="https://zunpan.github.io/categories/MySQL/"/>
<category term="MySQL" scheme="https://zunpan.github.io/tags/MySQL/"/>
<category term="数据库" scheme="https://zunpan.github.io/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
</entry>
<entry>
<title>Java并发编程实战学习笔记</title>
<link href="https://zunpan.github.io/2023/01/13/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E5%AE%9E%E6%88%98%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
<id>https://zunpan.github.io/2023/01/13/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E5%AE%9E%E6%88%98%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/</id>
<published>2023-01-13T09:02:46.000Z</published>
<updated>2023-09-24T04:27:40.278Z</updated>
<content type="html"><![CDATA[<p>本书分成四部分,<br />第一部分是基础,主要内容是并发的基础概念和线程安全,以及如何用java类库提供的并发构建块组成线程安全的类(2-5章节)</p><p>第二部分是构建并发应用程序,主要内容是如何利用线程来提高并发应用的吞吐量和响应能力(6-9章节)</p><p>第三部分是活跃性、性能和测试,主要内容是如何确保并发程序能够按照你的要求执行,并且具有可接受的性能(10-12章节)</p><p>第四部分是高级主题,涵盖了可能只有有经验的开发人员才会感兴趣的主题:显式锁、原子变量、非阻塞算法和开发自定义同步器(13-16章节)</p><h2 id="part-1-基础"><a class="markdownIt-Anchor" href="#part-1-基础"></a> Part 1 基础</h2><h2 id="chapter-1-简介"><a class="markdownIt-Anchor" href="#chapter-1-简介"></a> Chapter 1 简介</h2><h3 id="11-并发简史"><a class="markdownIt-Anchor" href="#11-并发简史"></a> 1.1 并发简史</h3><p>早期计算机没有操作系统,程序独占所有资源,一次只能有一个程序运行,效率低下</p><p>操作系统出现后,程序(进程)可以并发运行,由操作系统分配资源,进程互相隔离,必要时依靠粗粒度的通信机制:sockets、signal handlers、shared memory等机制通信</p><p>进程提高了系统吞吐量和资源利用率,线程的出现也是这个原因,线程有时被称为轻量级进程,大多数现代操作系统将线程而不是进程视为调度的基本单位,同一程序的多个线程可以同时在多个CPU上调度,如果没有同步机制协调对共享数据的访问,一个线程可能会修改另一个线程正在使用的变量,导致不可预测的结果</p><h3 id="12-线程优势"><a class="markdownIt-Anchor" href="#12-线程优势"></a> 1.2 线程优势</h3><ul><li>减小开发和维护开销</li><li>提高复杂应用的性能</li><li>提高GUI的响应能力</li><li>简化JVM实现</li></ul><h3 id="13-线程风险"><a class="markdownIt-Anchor" href="#13-线程风险"></a> 1.3 线程风险</h3><p>竞争(多个线程以未知顺序访问资源)<br />活跃性(死锁,饥饿,活锁)<br />性能(频繁切换导致开销过大)</p><h3 id="14-无处不在的线程"><a class="markdownIt-Anchor" href="#14-无处不在的线程"></a> 1.4 无处不在的线程</h3><ul><li>框架通过在框架线程中调用应用程序代码将并发性引入到应用程序中,在代码中将不可避免的访问应用程序状态,因此所有访问这些状态的代码路径都必须是线程安全的</li><li>Timer类,TimerTask在Timer管理的线程中执行</li><li>Servlet(每个请求使用一个线程同时执行Servlet)</li><li>RMI(由RMI负责打包拆包远程对象)</li><li>Swing(具有异步性)</li></ul><h2 id="chapter-2-线程安全"><a class="markdownIt-Anchor" href="#chapter-2-线程安全"></a> Chapter 2 线程安全</h2><p>多个线程访问同一个可变的状态变量时没有使用合适的同步机制,可以用以下方法修复:</p><ul><li>不在线程间共享该变量</li><li>将变量变为常量</li><li>访问时候使用同步</li></ul><h3 id="21-什么是线程安全"><a class="markdownIt-Anchor" href="#21-什么是线程安全"></a> 2.1 什么是线程安全?</h3><p>如果一个类被多线程访问,不管线程调度或交叉执行顺序如何,类的表现都是正确的,那么类是线程安全的</p><p>线程安全类封装任何需要的同步,因此客户端不需要提供自己的同步。</p><p>无状态的对象永远是线程安全的</p><h3 id="22-原子性"><a class="markdownIt-Anchor" href="#22-原子性"></a> 2.2 原子性</h3><p>竞争情况(race condition):由于不恰当的执行顺序导致出现不正确的结果,常发生在以下情况中:</p><ul><li>读取-修改-写入,例子: 自增操作</li><li>先检查后执行,例子:延迟初始化,不安全的单例模式,懒汉模式</li></ul><p>第一种情况解决方式:使用juc里面的类,比如count可以用AtomicLong类型操作保证原子性<br />第二种情况解决方式:加锁保证原子性</p><h3 id="23-加锁"><a class="markdownIt-Anchor" href="#23-加锁"></a> 2.3 加锁</h3><p>如果类有多个变量需要更新,即使它们的各自操作都是原子性的,也要把他们放在同一个原子操作中,方式是加锁。Java 提供了锁机制来增强原子性:synchronized</p><p>内置锁: synchronized 实例方法会将被调用方法的对象作为内置锁或监视锁,内置锁是互斥的,同一时刻最多只有一个线程拿到这个锁</p><p>可重入: 内置锁是可重入的,已经拿到锁的线程可以再次获取锁,实现方式是锁会(就是lock对象)关联一个持有者和计数值,持有者再次进入次数加一,退出减一,减到0会释放锁</p><h3 id="24-用锁来保护状态"><a class="markdownIt-Anchor" href="#24-用锁来保护状态"></a> 2.4 用锁来保护状态</h3><p>混合操作比如说读取-修改-写入和先检查后执行,需要保证原子性来避免竞争情况。</p><p>常见错误: 只有写入共享变量才需要同步<br />原因:读取也需要同步,不然可能会看到过期值</p><p>每个共享的可变变量应该由同一个锁保护,常见的加锁习惯是将可变变量封装到一个对象中</p><p>对于不变性条件(invariant)中涉及的多个变量,这多个变量都需要用同一个锁保护,例如Servlet缓存了请求次数和请求数据(数组),不变性条件是请求数据的长度等于次数,这通过加锁来保证</p><h3 id="25-活跃性和性能"><a class="markdownIt-Anchor" href="#25-活跃性和性能"></a> 2.5 活跃性和性能</h3><p>给Servlet的方法声明syncronized极大降低了并发性,我们可以通过缩小同步块的范围,在保证线程安全的情况下提高并发性。合理的做法是将不影响共享状态的操作从同步块中排除</p><h2 id="chapter-3-共享对象"><a class="markdownIt-Anchor" href="#chapter-3-共享对象"></a> Chapter 3 共享对象</h2><p>synchronized 块和方法可以确保操作的原子性执行,它还有另一个重要作用:内存可见性。我们不仅想要防止一个线程在另一个线程使用一个对象时修改它的状态,还想要确保当一个线程修改一个对象的状态时,其他线程可以看到最新更改</p><h3 id="31-可见性"><a class="markdownIt-Anchor" href="#31-可见性"></a> 3.1 可见性</h3><ol><li><p>过期数据(当一个线程修改数据,但其他线程不能立马看到)。 读取操作如果不同步,仍然能看到一个过期数据,这叫做最低安全性(过期数据至少是由之前的线程设置的,而不是随机值)</p></li><li><p>大多数变量都满足最低安全性,除了非volatile修饰的64位变量(double和long),jvm允许将64位操作拆解为2个 32位操作,读取这样的变量可能会出现过期值的高32位+新值的低32位的结果</p></li><li><p>内置锁保证可见性</p></li><li><p>volatile: 保证可见性,禁止指令重排,不保证原子性(使用场合:保证自身可见性,引用对象状态可见性,标识重要的生命周期事件)</p><p>当且仅当满足以下所有条件时,才应该使用volatile变量:</p><ul><li>对变量的写入不依赖于它的当前值,或者可以确保只有一个线程更新该值;</li><li>该变量不会与其他状态变量一起纳入不变性条件</li><li>在访问变量时,由于任何其他原因不需要加锁。</li></ul></li></ol><h3 id="32-发布与逃逸"><a class="markdownIt-Anchor" href="#32-发布与逃逸"></a> 3.2 发布与逃逸</h3><p>发布是指让对象在外部可见,常见方式是对象引用声明为 public static。发布对象的同时,任何通过非私有字段引用和方法调用链从发布对象中访问的对象也被发布了</p><p>逃逸是指对象的私有信息也对外可见了,比如发布一个对象包含一个私有数组,同时提供一个返回引用的get方法,外部可以通过引用修改内部私有数组</p><h3 id="33-线程封闭"><a class="markdownIt-Anchor" href="#33-线程封闭"></a> 3.3 线程封闭</h3><p>如果对象限制在一个线程中使用,即使对象不是线程安全的,也会自动线程安全</p><p>例子:Swing: 将组件和数据对象放到事件分发线程,其它线程访问不了这些对象;JDBC.Connection对象: 应用线程从数据库连接池中获取一个连接对象,连接对象由该线程独自使用</p><p>Java 提供 ThreadLocal 来实现线程封闭,程序员做的是阻止对象从线程中逃逸</p><p>线程封闭通常用来实现一个子系统,例如GUI,它是单线程的</p><ol><li><p>Ad-hoc封闭: 核线程封闭性的职责完全由程序实现来承担(脆弱,少用)</p></li><li><p>栈封闭: 只能通过局部变量访问对象(Java基本类型或者局部变量)</p></li><li><p>ThreadLocal类: 提供getter和setter,每个使用该变量的线程存有一份独立的副本</p></li></ol><h3 id="34-不可变"><a class="markdownIt-Anchor" href="#34-不可变"></a> 3.4 不可变</h3><p>不可变对象永远是线程安全的</p><p>满足以下条件,对象才是不可变的:</p><ul><li>构造函数之后状态不可修改</li><li>所有域都是final</li><li>对象正确创建(this引用没有在构造期间逃逸)</li></ul><p>多个状态的对象需要保证线程安全,可以将状态封装到一个不可变类中,用volatile修饰不可变对象引用</p><h3 id="35-安全发布"><a class="markdownIt-Anchor" href="#35-安全发布"></a> 3.5 安全发布</h3><ol><li><p>不正确的发布对象会出现两个问题:其它线程会看到null或旧值;最糟糕的是其它线程看到最新的引用但是被引用的对象还是旧的</p></li><li><p>由于不可变对象很重要,Java内存模型为不可变对象的共享提供一种特殊的初始化安全性保证,不用同步也能安全发布</p></li><li><p>一个正确构造的对象可以通过以下方式安全发布:</p><ul><li>静态初始化函数中初始化一个对象引用</li><li>引用保存到volatile域或者AtomicReference对象中</li><li>引用保存到某个正确构造对象的final域</li><li>引用保存到锁保护的域(容器也可)</li></ul></li><li><p>不可变对象,可以放宽到事实不可变对象(对象在发布之后不会改变状态)</p></li><li><p>可变对象必须通过安全方式发布,并且必须是线程安全的或者锁保护起来</p></li><li><p>并发程序共享对象实用策略</p><ul><li>线程封闭</li><li>只读共享</li><li>线程安全共享:对象内部实现同步</li><li>保护对象:锁机制</li></ul></li></ol><h2 id="chapter-4-组合对象"><a class="markdownIt-Anchor" href="#chapter-4-组合对象"></a> Chapter 4 组合对象</h2><p>本章讨论如何将线程安全的组件组合成更大的组件或程序</p><h3 id="41-设计一个线程安全的类"><a class="markdownIt-Anchor" href="#41-设计一个线程安全的类"></a> 4.1 设计一个线程安全的类</h3><ol><li><p>在设计线程安全类的过程中,常会包含以下三个基本要素:</p><ul><li>找出构成对象状态的所有变量。</li><li>找出约束状态变量的不变性条件和后验条件。</li><li>建立对象状态的并发访问管理策略。</li></ul><p>如果不了解对象的不变性条件和后验条件,就无法确保线程安全</p></li><li><p>依赖状态的方法需要先满足某种状态才能运行,即先验条件。java提供了 wait and notify 机制来等待先验条件成立,它依赖内置锁。更简单的实现方法是用java类库的阻塞队列或者信号量</p></li><li><p>一般情况下,状态所属权是封装状态的类,除非类公开可变对象的引用,这时候类只有共享权</p></li></ol><h3 id="42-实例封闭"><a class="markdownIt-Anchor" href="#42-实例封闭"></a> 4.2 实例封闭</h3><p>在对象中封装数据,通过使用对象方法访问数据,从而更容易确保始终在持有适当锁的情况下访问数据。</p><ol><li>Java监视器模式:封装可变状态到对象中,使用对象的内置锁保护状态,使用私有锁对象更有优势</li></ol><h3 id="43-线程安全的委托"><a class="markdownIt-Anchor" href="#43-线程安全的委托"></a> 4.3 线程安全的委托</h3><ol><li>将线程安全的职责委托给线程安全的类,例如计数器类不做同步处理,依赖AtomicLong类型达到线程安全</li><li>可以将线程安全委托给多个基础状态变量,只要它们是独立的</li><li>委托失效:多个变量间有不变性条件,比如大小关系等,需要加锁,除非复合操作也可以委托给变量</li><li>如果一个状态变量是线程安全的,不参与任何限制其值的不变性条件,并且在任何操作中都没有禁止的状态转换,那么它就可以安全地发布。</li></ol><h3 id="44-给现有的线程安全类加功能"><a class="markdownIt-Anchor" href="#44-给现有的线程安全类加功能"></a> 4.4 给现有的线程安全类加功能</h3><p>继承方式(可能会因为子父类加的锁不一样线程不安全)</p><ol><li>客户端加锁,使用辅助类,若类的加锁依赖其它类,那么辅助类容易错误加锁</li><li>组合方式,加锁策略完全由组合类提供</li></ol><h3 id="45-文档化同步策略"><a class="markdownIt-Anchor" href="#45-文档化同步策略"></a> 4.5 文档化同步策略</h3><p>为类的客户端记录线程安全保证;为其维护者记录其同步策略</p><h2 id="chapter-5-基础构建模块"><a class="markdownIt-Anchor" href="#chapter-5-基础构建模块"></a> Chapter 5 基础构建模块</h2><p>在实际应用中,委托是创建线程安全类最有效的策略之一,本章介绍平台库的并发构建模块,例如线程安全的集合和各种可以协调线程控制流的同步器</p><h3 id="51-同步集合"><a class="markdownIt-Anchor" href="#51-同步集合"></a> 5.1 同步集合</h3><p>Vector、Hashtable,以及JDK1.2增加了 Collections.synchronizedXxx 创建同步包装类</p><ol><li>复合线程安全的类的方法可能不是线程安全的,例如复合方法调用size和get方法,中间可能被删掉元素导致size结果不对</li><li>迭代器或者for-each不会锁定集合,在迭代过程中检测到集合变化时会抛出ConcurrentModificationException异常,检测是通过检测count值,但是没有同步,可能看到过期值</li><li>隐藏的迭代器(某些操作底层隐藏着调用迭代器,比如集合的toString)</li></ol><h3 id="52-并发集合"><a class="markdownIt-Anchor" href="#52-并发集合"></a> 5.2 并发集合</h3><p>同步集合通过序列化对集合状态的所有访问来实现线程安全,性能低。Java 5增加了并发集合</p><ol><li>ConcurrentHashMap,使用分段锁,具有弱一致性,同时size和isEmpty是估计并不精确,只有需要独占Map,才不建议使用该Map</li><li>CopyOnWriteArrayList,每次修改都是返回副本,建议迭代多修改少的时候使用</li></ol><h3 id="53-阻塞队列和生产者-消费者模式"><a class="markdownIt-Anchor" href="#53-阻塞队列和生产者-消费者模式"></a> 5.3 阻塞队列和生产者-消费者模式</h3><p>BlockingQueue,常用来实现生产者和消费者,有一个特殊的实现SynchronousQueue,它不是一个实际的队列,当生产者生产数据时直接交给消费者,适用于消费者多的场景</p><p>Deque,常用来实现工作窃取模式。生产者和消费者模式中,消费者共享一个队列,工作窃取模式中,消费者有独自的队列,当消费完后会偷其他人的工作。工作窃取模式可以减少对于共享队列的竞争</p><h3 id="54-阻塞方法与中断方法"><a class="markdownIt-Anchor" href="#54-阻塞方法与中断方法"></a> 5.4 阻塞方法与中断方法</h3><ul><li>当某个方法抛出InterruptedException,说明该方法是阻塞方法,可以被中断</li><li>代码中调用一个阻塞方法(阻塞方法和线程状态没有必然关系,方法可能是个长时间方法所以声明抛出InterruptedException,也有可能是会导致线程状态改变的sleep方法),必须处理中断响应.<ul><li>捕获/抛出异常</li><li>恢复中断.调用当前线程的interrupt</li></ul></li></ul><h3 id="55-同步器"><a class="markdownIt-Anchor" href="#55-同步器"></a> 5.5 同步器</h3><ol><li>阻塞队列</li><li>闭锁(Latch): 延迟线程进度,直到条件满足,FutureTask也可以做闭锁</li><li>信号量:类似发布凭证,但是任意线程都可以发布和返还</li><li>栅栏: 阻塞一组线程,直到某个条件满足;如果有某个线程await期间中断或者超时,所有阻塞的调用都会终止并抛出BrokenBarrierException</li></ol><h3 id="56-构建高效且可伸缩的缓存"><a class="markdownIt-Anchor" href="#56-构建高效且可伸缩的缓存"></a> 5.6 构建高效且可伸缩的缓存</h3><ol><li>使用hashMap+synchronized,性能差</li><li>ConcurrentHashMap代替hashMap+synchronized,有重复计算问题</li><li>ConcurrentHashMap的值用FutureTask包起来,只要键已经存在,从FutureTask获取结果,因为check-then-act模式,仍然存在重复计算问题</li><li>使用putIfAbsent设置缓存</li></ol><h2 id="part-2-构建并发应用程序"><a class="markdownIt-Anchor" href="#part-2-构建并发应用程序"></a> Part 2 构建并发应用程序</h2><h2 id="chapter-6-任务执行"><a class="markdownIt-Anchor" href="#chapter-6-任务执行"></a> Chapter 6 任务执行</h2><h3 id="61-在线程中执行任务"><a class="markdownIt-Anchor" href="#61-在线程中执行任务"></a> 6.1 在线程中执行任务</h3><ol><li><p>串行执行任务(响应会慢,服务器资源利用率低)</p></li><li><p>显式为每个请求申请一个线程</p><ul><li>任务处理线程从主线程分离,提高响应速度</li><li>任务可以并行处理,提高吞吐量</li><li>任务处理代码必须是线程安全的,多个线程会并发执行</li></ul></li><li><p>无限制创建线程的不足</p><ul><li>创建销毁浪费时间</li><li>浪费资源</li><li>稳定性差</li></ul></li></ol><h3 id="62-executor框架"><a class="markdownIt-Anchor" href="#62-executor框架"></a> 6.2 Executor框架</h3><ol><li><p>Executor基于生产-消费模式,提交任务相当于生产者,执行任务的线程相当于消费者.</p></li><li><p>执行策略</p><ul><li>What: 在什么线程中执行任务,按什么顺序执行,任务执行前后要执行什么操作</li><li>How Many: 多少任务并发,多少等待</li><li>Which: 系统过载时选择拒绝什么任务</li><li>How: 怎么通知任务成功/失败</li></ul></li><li><p>线程池,管理一组同构工作线程的资源池,跟工作队列密切相关</p></li><li><p>Executor生命周期</p><ul><li>运行 : 对象新建时就是运行状态</li><li>关闭 : 不接受新任务,同时等待已有任务完成,包括未执行的任务,关闭后任务再提交由 “被拒绝的执行处理器” 处理或者直接抛异常</li><li>终止 : 关闭后任务完成</li></ul></li><li><p>延迟任务和周期任务</p><p>Timer类可以负责,但是存在缺陷,应该考虑ScheduledThreadPoolExecutor代替它</p><p>Timer: 只用一个线程执行定时任务,假如某个任务耗时过长,会影响其他任务的定时准确性。除此之外,不支持抛出异常,发生异常将终止线程(已调度(scheduled)未执行的任务,线程不会执行,新任务不会调度,称为线程泄露)</p><p>DelayQueue: 阻塞队列的一种实现,为ScheduledThreadPoolExecutor提供调度策略</p></li></ol><h3 id="63-寻找可利用的并行性"><a class="markdownIt-Anchor" href="#63-寻找可利用的并行性"></a> 6.3 寻找可利用的并行性</h3><ol><li><p>将耗时的IO使用别的线程获取;而不是简单的串行执行</p></li><li><p>Future 表示一个任务的生命周期,并提供相应的方法判断完成/取消,get会阻塞或抛异常</p></li><li><p>使用Callable和Future并行化下载和渲染</p></li><li><p>异构任务并行化获取重大性能提升很困难.</p><ul><li>任务大小不同</li><li>负载均衡问题</li><li>协调开销</li></ul></li><li><p>CompletionService 将 Executor 和BlockingQueue结合在一起,Executor是生产者,CompletionService是消费者</p></li><li><p>使用 CompletionService 并行化下载和渲染</p></li><li><p>为任务设置时限</p></li><li><p>需要获取多个设置了时限的任务的结果可以用带上时间的 invokeAll 提交多个任务</p></li></ol><h2 id="chapter-7-取消和关闭"><a class="markdownIt-Anchor" href="#chapter-7-取消和关闭"></a> Chapter 7 取消和关闭</h2><p>本章讲解如何停止任务和线程,Java没有安全强制线程停止的方法,只有一种协作机制,中断</p><h3 id="71-任务取消"><a class="markdownIt-Anchor" href="#71-任务取消"></a> 7.1 任务取消</h3><p>有一种协作机制是在任务中设置取消位,任务定期查看该标识,假如置位就结束任务(假如线程阻塞了,就看不到取消位,那么就停不下来了)</p><ol><li>中断: 在取消任务或线程之外的其他操作中使用中断是不合适的<ul><li>每个线程都有一个中断标志,interrupt中断目标线程,isInterrupted返回目标线程的中断状态,interrupted(<em>糟糕的命名</em>)清除当前线程中断;</li><li>Thread.sleep和Object.wait都会检查线程什么时候中断,发现时提前返回(不会立即响应,只是传递请求而已)</li></ul></li><li>中断策略:尽快推迟执行流程,传递给上层代码;由于每个线程拥有各自的中断策略,除非知道中断对这个线程的含义,否则不应该中断该线程</li><li>中断响应<br />当调用会抛出InterruptedException的阻塞方法时,有两种处理策略<ul><li>传播异常,让你的方法也变成会抛出异常的阻塞方法(中断标志一直为true)</li><li>恢复中断状态,以便调用堆栈上较高级的代码处理它(try-catch之后中断标志为false,可以调用当前线程的interrupt方法恢复成中断状态)。</li></ul></li><li>在中断线程之前,要了解线程的中断策略</li><li>通过Future取消任务</li><li>处理不可中断的阻塞<ul><li>java.io中的同步Socket I/O.通过关闭Socket可以使阻塞线程抛出异常</li><li>java.io中的同步 I/O.终端一个InterruptibleChannel会抛出异常并关闭链路</li><li>获取某个锁. Lock提供lockInterruptibly</li></ul></li><li>通过 newTaskFor 方法进一步优化</li></ol><h3 id="72-停止基于线程的服务"><a class="markdownIt-Anchor" href="#72-停止基于线程的服务"></a> 7.2 停止基于线程的服务</h3><p>基于线程的服务:拥有线程的服务,例如线程池</p><p>只要拥有线程的服务的生命周期比创建它的方法的生命周期长,就提供生命周期方法。例如线程池 ExecutorService 提供了shutdown</p><ol><li>日志服务:多生产者写入消息到阻塞队列,单消费者从阻塞队列中取消息,停止日志服务需要正确关闭线程。需要对结束标志位和队列剩余消息数同步访问(书有错误,LoggerThread 应该 synchronized (LogService.this))</li><li>毒丸,生产者将毒丸放在队列上,消费者拿到毒丸就结束</li><li>shutdownNow 取消正在执行的任务,返回已提交未开始的任务,可以用个集合保存执行中被取消的任务</li></ol><h3 id="73-处理非正常的线程终止"><a class="markdownIt-Anchor" href="#73-处理非正常的线程终止"></a> 7.3 处理非正常的线程终止</h3><p>通常是因为抛出运行时异常导致线程终止</p><p>处理方法:</p><ol><li>try-catch 捕获任务异常,如果不能恢复,在finally块中通知线程拥有者</li><li>当线程因未捕获异常而退出时,JVM会将事件报告给线程拥有者提供的UncaughtExceptionHandler,如果没有处理程序就将堆栈打印到System.err</li><li>通过execute提交的任务的异常由UncaughtExceptionHandler处理,submit提交的任务,通过调用Future.get方法,包装在ExecutionException里面</li></ol><h3 id="74-jvm关闭"><a class="markdownIt-Anchor" href="#74-jvm关闭"></a> 7.4 JVM关闭</h3><p>有序关闭:最后一个非守护线程终止(可能是调用了System.exit,或者发送SIGINT或按Ctrl-C)后终止<br />突然关闭:通过操作系统终止JVM进程,例如发送SIGKIll</p><ol><li>有序关闭中,JVM首先启动所有已注册的关闭钩子(通过Runtime.addShutdownHook注册的未启动线程)。如果应用程序线程在关闭时仍在运行,将与关闭线程并行执行。当所有关闭钩子都完成时,如果runFinalizersOnExit为true,那么jvm可能运行终结器,然后停止</li><li>守护线程:执行辅助功能的线程,不会阻止JVM关闭。当JVM关闭时,守护线程直接关闭,不执行 finally 块,栈不会展开。守护线程适合做“内务”任务,例如清缓存</li><li>终结器:GC在回收对象后会执行 finalize 方法释放持久资源。终结器在JVM管理的线程中运行,需要同步访问。终结器难写且性能低,除非要关闭 native 方法获取的资源,否则在 finally中显示关闭就够了</li></ol><h2 id="chapter-8-使用线程池"><a class="markdownIt-Anchor" href="#chapter-8-使用线程池"></a> Chapter 8 使用线程池</h2><p>本章将介绍配置和调优线程池的高级选项,描述使用任务执行框架时需要注意的危险</p><h3 id="81-任务和执行策略之间的隐式耦合"><a class="markdownIt-Anchor" href="#81-任务和执行策略之间的隐式耦合"></a> 8.1 任务和执行策略之间的隐式耦合</h3><p>Executor 框架在任务提交和执行之间仍存在一些耦合:</p><ol><li>依赖其它任务的任务,相互依赖可能导致活跃性问题</li><li>利用线程封闭的任务,这类任务不做同步,依赖单线程执行</li><li>响应时间敏感的任务,可能需要多线程执行</li><li>使用 ThreadLocal 的任务,ThreadLocal不应该用于线程池中任务之间的通信</li></ol><h4 id="811-线程饥饿死锁"><a class="markdownIt-Anchor" href="#811-线程饥饿死锁"></a> 8.1.1 线程饥饿死锁</h4><p>把相互依赖的任务提交到一个单线程的Executor一定会发生死锁。增大线程池,如果被依赖的任务在等待队列中,也会发生死锁</p><h4 id="812-运行耗时长的任务"><a class="markdownIt-Anchor" href="#812-运行耗时长的任务"></a> 8.1.2 运行耗时长的任务</h4><p>即使不出现死锁,也会降低性能,通过限制执行时间可以缓解</p><h3 id="82-设置线程池大小"><a class="markdownIt-Anchor" href="#82-设置线程池大小"></a> 8.2 设置线程池大小</h3><p>cpu数可以调用 Runtime.availableProcessors得出</p><ol><li>计算密集型场景,线程池大小等于cpu数+1</li><li>IO密集型场景,线程池大小等于cpu数 * cpu利用率 * (1+等待/计算时间比)</li></ol><h3 id="83-配置-threadpoolexecutor"><a class="markdownIt-Anchor" href="#83-配置-threadpoolexecutor"></a> 8.3 配置 ThreadPoolExecutor</h3><h4 id="831-线程创建和销毁"><a class="markdownIt-Anchor" href="#831-线程创建和销毁"></a> 8.3.1 线程创建和销毁</h4><ol><li>corePoolSize:线程池大小,只有工作队列满了才会创建超出这个数量的线程</li><li>maximumPoolSize:最大线程数量</li><li>keepAliveTime:空闲时间超过keepAliveTime的线程会成为回收的候选线程,如果线程池的大小超过了核心的大小,线程就会被终止</li></ol><h4 id="832-管理工作队列"><a class="markdownIt-Anchor" href="#832-管理工作队列"></a> 8.3.2 管理工作队列</h4><p>可以分成三类:无界队列、有界队列和同步移交。队列的选择和线程池大小、内存大小的有关</p><p>无界队列可能会耗尽资源,有界队列会带来队列满时新任务的处理问题,同步移交只适合用在无界线程池或饱和策略可以接受</p><h4 id="833-饱和策略"><a class="markdownIt-Anchor" href="#833-饱和策略"></a> 8.3.3 饱和策略</h4><p>当任务提交给已经满的有界队列或已经关闭的Executor,饱和策略开始工作</p><ol><li>Abort,默认策略,execute方法会抛RejectedExecutionException</li><li>Discard:丢弃原本下个执行的任务,并重新提交新任务</li><li>Caller-Runs:将任务给调用execute 的线程执行</li><li>无界队列可以使用信号量进行饱和策略</li></ol><h4 id="834-线程工厂"><a class="markdownIt-Anchor" href="#834-线程工厂"></a> 8.3.4 线程工厂</h4><p>通过ThreadFactory.newThread创建线程,自定义线程工厂可以在创建线程时设置线程名、自定义异常</p><h4 id="835-调用构造函数后再定制threadpoolexecutor"><a class="markdownIt-Anchor" href="#835-调用构造函数后再定制threadpoolexecutor"></a> 8.3.5 调用构造函数后再定制ThreadPoolExecutor</h4><p>线程池的各项配置可以通过set方法配置,如果不想被修改,可以调用Executors.unconfigurableExecutorService<br />将其包装成不可修改的线程池</p><h3 id="84-扩展-threadpoolexecutor"><a class="markdownIt-Anchor" href="#84-扩展-threadpoolexecutor"></a> 8.4 扩展 ThreadPoolExecutor</h3><p>ThreadPoolExecutor给子类提供了钩子方法,beforeExecute、afterExecute和terminated</p><p>beforeExecute和afterExecute钩子在执行任务的线程中调用,可用于添加日志记录、计时、监控或统计信息收集。无论任务从run正常返回还是抛出异常,afterExecute钩子都会被调用。如果beforeExecute抛出一个RuntimeException,任务就不会执行,afterExecute也不会被调用</p><p>terminated钩子在任务都完成且所有工作线程都关闭后调用,用来释放资源、执行通知或日志记录</p><h3 id="85-递归算法并行化"><a class="markdownIt-Anchor" href="#85-递归算法并行化"></a> 8.5 递归算法并行化</h3><ol><li>如果迭代操作之间是独立的,适合并行化</li><li>递归不依赖于后续递归的返回值</li></ol><h2 id="chapter-9-gui应用"><a class="markdownIt-Anchor" href="#chapter-9-gui应用"></a> Chapter 9 GUI应用</h2><h3 id="91-为什么gui是单线程的"><a class="markdownIt-Anchor" href="#91-为什么gui是单线程的"></a> 9.1 为什么GUI是单线程的</h3><p>由于竞争情况和死锁,多线程GUI框架最终都变成了单线程</p><h4 id="911-串行事件处理"><a class="markdownIt-Anchor" href="#911-串行事件处理"></a> 9.1.1 串行事件处理</h4><p>优点:代码简单<br />缺点:耗时长的任务会发生无响应(委派给其它线程执行)</p><h4 id="912-swing的线程封闭"><a class="markdownIt-Anchor" href="#912-swing的线程封闭"></a> 9.1.2 Swing的线程封闭</h4><p>所有Swing组件和数据模型对象都封闭在事件线程中,任何访问它们的代码必须在事件线程里</p><h3 id="92-短时间的gui任务"><a class="markdownIt-Anchor" href="#92-短时间的gui任务"></a> 9.2 短时间的GUI任务</h3><p>事件在事件线程中产生,并冒泡到应用程序提供的监听器</p><p>Swing将大多数可视化组件分为两个对象(模型对象和视图对象),模型对象保存数据,可以通过引发事件表示模型发生变化,视图对象通过订阅接收事件</p><h3 id="93-长时间的gui任务"><a class="markdownIt-Anchor" href="#93-长时间的gui任务"></a> 9.3 长时间的GUI任务</h3><p>对于长时间的任务可以使用线程池</p><ol><li>取消 使用Future</li><li>进度标识</li></ol><h3 id="94-共享数据模型"><a class="markdownIt-Anchor" href="#94-共享数据模型"></a> 9.4 共享数据模型</h3><ol><li>只要阻塞操作不会过度影响响应性,那么事件线程和后台线程就可以共享该模型</li><li>分解数据模型.将共享的模型通过快照共享</li></ol><h3 id="95-其它形式单线程"><a class="markdownIt-Anchor" href="#95-其它形式单线程"></a> 9.5 其它形式单线程</h3><p>为了避免同步或死锁使用单线程,例如访问native方法使用单线程</p><h2 id="part-3-活跃性-性能和测试"><a class="markdownIt-Anchor" href="#part-3-活跃性-性能和测试"></a> Part 3 活跃性、性能和测试</h2><h2 id="chapter-10-避免活跃性危险"><a class="markdownIt-Anchor" href="#chapter-10-避免活跃性危险"></a> Chapter 10. 避免活跃性危险</h2><p>Java程序不能从死锁中恢复,本章讨论活跃性失效的一些原因以及预防措施</p><h3 id="101-死锁"><a class="markdownIt-Anchor" href="#101-死锁"></a> 10.1 死锁</h3><p>哲学家进餐问题:每个人都有另一个人需要的资源,并且等待另一个人持有的资源,在获得自己需要的资源前不会释放自己持有的资源,产生死锁</p><h4 id="1011-lock-ordering死锁"><a class="markdownIt-Anchor" href="#1011-lock-ordering死锁"></a> 10.1.1 Lock-ordering死锁</h4><p>线程之间获取锁的顺序不同导致死锁。<br />解决方法:如果所有线程以一个固定的顺序获取锁就不会出现Lock-ordering死锁</p><h4 id="1012-动态lock-order死锁"><a class="markdownIt-Anchor" href="#1012-动态lock-order死锁"></a> 10.1.2 动态Lock Order死锁</h4><p>获取锁的顺序依赖参数可能导致死锁。<br />解决方法:对参数进行排序,统一线程获取锁的顺序</p><h4 id="1013-协作对象的死锁"><a class="markdownIt-Anchor" href="#1013-协作对象的死锁"></a> 10.1.3 协作对象的死锁</h4><p>如果在持有锁时调用外部方法,将会出现活跃性问题,这个外部方法可能阻塞,加锁等导致其他线程无法获得当前被持有的锁<br />解决方法:开放调用</p><h4 id="1014-开放调用"><a class="markdownIt-Anchor" href="#1014-开放调用"></a> 10.1.4 开放调用</h4><p>如果在方法中调用外部方法时不需要持有锁(比如调用者this),那么这种调用称为开放调用。实现方式是将调用者的方法的同步范围从方法缩小到块</p><h4 id="1015-资源死锁"><a class="markdownIt-Anchor" href="#1015-资源死锁"></a> 10.1.5 资源死锁</h4><p>和循环依赖锁导致死锁类似。例如线程持有数据库连接且等待另一个线程释放,另一个线程也是这样</p><h3 id="102-避免和诊断死锁"><a class="markdownIt-Anchor" href="#102-避免和诊断死锁"></a> 10.2 避免和诊断死锁</h3><p>使用两部分策略来审计代码以确保无死锁:首先,确定哪些地方可以获得多个锁(尽量使其成为一个小集合),然后对所有这些实例进行全局分析,以确保锁的顺序在整个程序中是一致的,尽可能使用开放调用简化分析</p><h4 id="1021-定时锁"><a class="markdownIt-Anchor" href="#1021-定时锁"></a> 10.2.1 定时锁</h4><p>另一种检测死锁并从死锁中恢复的技术是使用显示锁中的Lock.tryLock()代替内置锁</p><h4 id="1022-用thread-dumps进行死锁分析"><a class="markdownIt-Anchor" href="#1022-用thread-dumps进行死锁分析"></a> 10.2.2 用Thread Dumps进行死锁分析</h4><p>线程转储包含每个运行线程的堆栈信息,锁信息(持有哪些锁,从哪个栈帧中获得)以及阻塞的线程正在等待获得哪些锁</p><h3 id="103-其它活跃性危险"><a class="markdownIt-Anchor" href="#103-其它活跃性危险"></a> 10.3 其它活跃性危险</h3><h4 id="1031-饥饿"><a class="markdownIt-Anchor" href="#1031-饥饿"></a> 10.3.1 饥饿</h4><p>线程由于无法获得它所需要的资源而不能继续执行,最常见的资源是CPU</p><p>避免使用线程优先级,可能导致饥饿</p><h4 id="1032-糟糕的响应性"><a class="markdownIt-Anchor" href="#1032-糟糕的响应性"></a> 10.3.2 糟糕的响应性</h4><p>计算密集型任务会影响响应性,通过降低执行计算密集型任务的线程的优先级可以提高前台任务的响应性</p><h4 id="1033-活锁"><a class="markdownIt-Anchor" href="#1033-活锁"></a> 10.3.3 活锁</h4><p>线程执行任务失败后,任务回滚,又添加到队列头部,导致线程没有阻塞但永远不会有进展。多个相互合作的线程为了响应其它线程而改变状态也会导致活锁<br />解决方法:在重试机制中引入一些随机性</p><h2 id="chapter-11-性能和可伸缩性"><a class="markdownIt-Anchor" href="#chapter-11-性能和可伸缩性"></a> Chapter 11. 性能和可伸缩性</h2><h3 id="111-对性能的思考"><a class="markdownIt-Anchor" href="#111-对性能的思考"></a> 11.1 对性能的思考</h3><h4 id="1111-性能和可伸缩性"><a class="markdownIt-Anchor" href="#1111-性能和可伸缩性"></a> 11.1.1 性能和可伸缩性</h4><p>性能: 可以用任务完成快慢或者数量来衡量,具体指标包括服务时间、延迟、<br />吞吐量、可伸缩性等</p><p>可伸缩性: 增加计算资源时提供程序吞吐量的能力</p><h3 id="1112-评估性能权衡"><a class="markdownIt-Anchor" href="#1112-评估性能权衡"></a> 11.1.2 评估性能权衡</h3><p>许多性能优化牺牲可读性和可维护性,比如违反面向对象设计原则,需要权衡</p><h3 id="112-amdahl定律"><a class="markdownIt-Anchor" href="#112-amdahl定律"></a> 11.2 Amdahl定律</h3><p>N:处理器数量<br />F:必须串行执行的计算部分<br />Speedup:加速比</p><p>$\text { Speedup } \leq \frac{1}{F+\frac{1-F}{N}} $</p><p>串行执行的计算部分需要仔细考虑,即使任务之间互不影响可以并行,但是线程从任务队列中需要同步,使用ConcurrentLinkedQueue比同步的LinkedList性能好</p><h3 id="113-线程引入的开销"><a class="markdownIt-Anchor" href="#113-线程引入的开销"></a> 11.3 线程引入的开销</h3><ol><li>上线文切换</li><li>内存同步(同步的性能开销包括可见性保证,即内存屏障,可以用jvm逃逸分析和编译器锁粒度粗化进行优化)</li><li>阻塞(非竞争的同步可以在JVM处理,竞争的同步需要操作系统介入,竞争失败的线程必定阻塞,JVM可以自旋等待(反复尝试获取锁,直到成功)或者被操作系统挂起进入阻塞态,短时间等待选择自旋等待,长时间等待选择挂起)</li></ol><h3 id="114-减少锁的竞争"><a class="markdownIt-Anchor" href="#114-减少锁的竞争"></a> 11.4 减少锁的竞争</h3><p>并发程序中,对伸缩性最主要的威胁就是独占方式的资源锁</p><p>三种减少锁争用的方法:</p><ul><li>减少持有锁的时间</li><li>减少请求锁的频率</li><li>用允许更大并发的协调机制替换互斥锁</li></ul><h4 id="1141-减小锁的范围"><a class="markdownIt-Anchor" href="#1141-减小锁的范围"></a> 11.4.1 减小锁的范围</h4><p>锁的范围即持有锁的时间</p><h4 id="1142-降低锁的力度"><a class="markdownIt-Anchor" href="#1142-降低锁的力度"></a> 11.4.2 降低锁的力度</h4><p>分割锁:将保护多个独立的变量的锁分割成单独的锁,这样锁的请求频率就可以降低</p><h4 id="1143-分段锁"><a class="markdownIt-Anchor" href="#1143-分段锁"></a> 11.4.3 分段锁</h4><p>分割锁可以扩展到可变大小的独立对象上的分段锁。例如ConcurrentHashMap使用了一个包含16个锁的数组,每个锁保护1/16的哈希桶</p><p>分段锁缺点:独占访问集合开销大</p><h4 id="1144-避免热点字段"><a class="markdownIt-Anchor" href="#1144-避免热点字段"></a> 11.4.4 避免热点字段</h4><p>热点字段:缓存<br />热点字段会限制可伸缩性,例如,为了缓存Map中的元素数量,添加一个计数器,每次增删时修改计数器,size操作的开销就是O(1)。单线程没问题,多线程下又需要同步访问计数器,ConcurrentHashMap每个哈希桶一个计数器</p><h4 id="1145-互斥锁的替代品"><a class="markdownIt-Anchor" href="#1145-互斥锁的替代品"></a> 11.4.5 互斥锁的替代品</h4><p>考虑使用并发集合、读写锁、不可变对象和原子变量</p><p>读写锁:只要没有一个写者想要修改共享资源,多个读者可以并发访问,但写者必须独占地获得锁<br />原子变量:提供细粒度的原子操作,可以降低更新热点字段的开销</p><h4 id="1146-监测cpu利用率"><a class="markdownIt-Anchor" href="#1146-监测cpu利用率"></a> 11.4.6 监测CPU利用率</h4><p>cpu利用率低可能是以下原因:</p><ol><li>负载不够,可以对程序加压</li><li>IO密集,可以通过iostat判断,还可以通过监测网络上的流量水平判断</li><li>外部约束,可能在等待数据库或web服务的响应</li><li>锁竞争,可以用分析工具分析哪些是“热”锁</li></ol><h4 id="1147-不要用对象池"><a class="markdownIt-Anchor" href="#1147-不要用对象池"></a> 11.4.7 不要用对象池</h4><p>现在JVM分配和回收对象已经很快了,不要用对象池</p><h3 id="115-例子比较map的性能"><a class="markdownIt-Anchor" href="#115-例子比较map的性能"></a> 11.5 例子:比较Map的性能</h3><p>ConcurrentHashMap单线程性能略好于同步的HashMap,并发时性能超好。ConcurrentHashMap对大多数成功的读操作不加锁,对写操作和少数读操作加分段锁</p><h3 id="116-减少上下文切换"><a class="markdownIt-Anchor" href="#116-减少上下文切换"></a> 11.6 减少上下文切换</h3><ol><li>日志记录由专门的线程负责</li><li>请求服务时间不应该过长</li><li>将IO移动到单个线程</li></ol><h2 id="chapter-12-并发程序的测试"><a class="markdownIt-Anchor" href="#chapter-12-并发程序的测试"></a> Chapter 12. 并发程序的测试</h2><p>大多数并发程序测试安全性和活跃性。安全性可以理解为“永远不会发生坏事”,活跃性可以理解为“最终会有好事发生”</p><h3 id="121-正确性测试"><a class="markdownIt-Anchor" href="#121-正确性测试"></a> 12.1 正确性测试</h3><h4 id="1211-基础单元测试"><a class="markdownIt-Anchor" href="#1211-基础单元测试"></a> 12.1.1 基础单元测试</h4><p>和顺序程序的测试类似,调用方法,验证程序的后置条件和不变量</p><h4 id="1212-阻塞操作测试"><a class="markdownIt-Anchor" href="#1212-阻塞操作测试"></a> 12.1.2 阻塞操作测试</h4><p>在单独的一个线程中启动阻塞活动,等待线程阻塞,中断它,然后断言阻塞操作完成。</p><p>Thread.getState不可靠,因为线程阻塞不一定进入WAITING或TIMED_WAITING状态,JVM可以通过自旋等待实现阻塞。类似地,Object.wait和Condition.wait存在伪唤醒情况,处于WAITING或TIMED_WAITING状态的线程可以暂时过渡到RUNNABLE。</p><h4 id="1213-安全性测试"><a class="markdownIt-Anchor" href="#1213-安全性测试"></a> 12.1.3 安全性测试</h4><p>给并发程序编写高效的安全测试的挑战在于识别出容易检查的属性,这些属性在程序错误时出错,同时不能让检查限制并发性,最好检查属性时不需要同步。</p><p>生产者和消费者模式中的一种方法是校验和,单生产者单消费者可以使用顺序敏感的校验和计算入队和出队元素的校验和,多生产者多消费者要用顺序不敏感的校验和</p><h4 id="1214-资源管理测试"><a class="markdownIt-Anchor" href="#1214-资源管理测试"></a> 12.1.4 资源管理测试</h4><p>任何保存或管理其他对象的对象都不应该在不必要的时间内继续维护对这些对象的引用。可以用堆检查工具测试内存使用情况</p><h4 id="1215-使用回调"><a class="markdownIt-Anchor" href="#1215-使用回调"></a> 12.1.5 使用回调</h4><p>回调函数通常是在对象生命周期的已知时刻发出的,这是断言不变量的好机会。例如自定义线程池可以在创建销毁线程时记录线程数</p><h4 id="1216-产生更多的交替操作"><a class="markdownIt-Anchor" href="#1216-产生更多的交替操作"></a> 12.1.6 产生更多的交替操作</h4><p>Thread.yield放弃cpu,保持RUNNABLE状态,重新竞争cpu<br />Thread.sleep放弃cpu进入TIME_WAITING状态,不竞争cpu,sleep较小时间比yield更稳定产生交替操作</p><p>tips:Java 线程的RUNNABLE 状态对应了操作系统的 ready 和 running 状态,TIME_WAITING(调用Thread.sleep) 和 WAITING(调用Object.wait) 和 BLOCKED(没有竞争到锁) 对应 waiting 状态。interrupt是种干预手段,如果interrupt一个RUNNABLE线程(可能在执行长时间方法需要终止),如果方法声明抛出InterruptedException,就表示可中断,方法会循环检查isInterrupted状态来响应interrupt,一般情况线程状态变成TERMINATED。如果interrupt一个 waiting 线程(可能是由sleep、wait方法导致,这些方法会抛出InterruptedException),线程重新进入 RUNNALBE 状态,处理InterruptedException</p><h3 id="122-性能测试"><a class="markdownIt-Anchor" href="#122-性能测试"></a> 12.2 性能测试</h3><ol><li>增加计时功能(CyclicBarrier)</li><li>多种算法比较(LinkedBlockingQueue在多线程情况下比ArrayBlockingQueue性能好)</li><li>衡量响应性</li></ol><h3 id="123-避免性能测试的陷阱"><a class="markdownIt-Anchor" href="#123-避免性能测试的陷阱"></a> 12.3 避免性能测试的陷阱</h3><h4 id="1231-垃圾回收"><a class="markdownIt-Anchor" href="#1231-垃圾回收"></a> 12.3.1 垃圾回收</h4><p>垃圾回收不可预测,会导致测试误差,需要长时间测试,多次垃圾回收,得到更准确结果</p><h4 id="1232-动态编译"><a class="markdownIt-Anchor" href="#1232-动态编译"></a> 12.3.2 动态编译</h4><p>动态编译会影响运行时间,需要运行足够长时间或者与完成动态编译后再开始计时</p><h4 id="1233-对代码路径的不真实采样"><a class="markdownIt-Anchor" href="#1233-对代码路径的不真实采样"></a> 12.3.3 对代码路径的不真实采样</h4><p>动态编译器会对单线程测试程序进行优化,最好多线程测试和单线程测试混合使用(测试用例至少用两个线程)</p><h4 id="1234-不真实的竞争情况"><a class="markdownIt-Anchor" href="#1234-不真实的竞争情况"></a> 12.3.4 不真实的竞争情况</h4><p>并发性能测试程序应该尽量接近真实应用程序的线程本地计算,并考虑并发协作。例如,多线程访问同步Map,如果本地计算过长,那么锁竞争情况就较弱,可能得出错误的性能瓶颈结果</p><h4 id="1235-无用代码的删除"><a class="markdownIt-Anchor" href="#1235-无用代码的删除"></a> 12.3.5 无用代码的删除</h4><p>无用代码:对结果没有影响的代码<br />由于基准测试通常不计算任何东西,很容易被优化器删除,这样测试的执行时间就会变短<br />解决方法是计算某个派生类的散列值,与任意值比较,加入相等就输出一个无用且可被忽略的消息</p><h3 id="124-补充的测试方法"><a class="markdownIt-Anchor" href="#124-补充的测试方法"></a> 12.4 补充的测试方法</h3><ol><li>代码审查</li><li>静态代码分析</li><li>面向切面的测试工具</li><li>分析与检测工具</li></ol><h2 id="part-4-高级主题"><a class="markdownIt-Anchor" href="#part-4-高级主题"></a> Part 4 高级主题</h2><h2 id="chapter-13-显示锁"><a class="markdownIt-Anchor" href="#chapter-13-显示锁"></a> Chapter 13 显示锁</h2><p>Java 5 之前,对共享数据的协调访问机制只有 synchronized 和 volatile,Java 5 增加了 ReentrantLock。</p><h3 id="131-lock和reentrantlock"><a class="markdownIt-Anchor" href="#131-lock和reentrantlock"></a> 13.1 Lock和ReentrantLock</h3><p>Lock接口定义加锁和解锁的操作。<br />ReentrantLock还提供了可重入特性</p><p>显示锁和内置锁很像,显示锁出现的原因是内置锁有一些功能限制</p><ol><li>不能中断等待锁的线程</li><li>必须在获得锁的地方释放锁</li></ol><h4 id="1311-轮询和定时获得锁"><a class="markdownIt-Anchor" href="#1311-轮询和定时获得锁"></a> 13.1.1 轮询和定时获得锁</h4><p>tryLock:轮询和定时获得锁<br />内置锁碰到死锁是致命的,唯一恢复方法是重启,唯一防御方法是统一锁获取顺序,tryLock可以概率避免死锁</p><h4 id="1312-可中断的获得锁"><a class="markdownIt-Anchor" href="#1312-可中断的获得锁"></a> 13.1.2 可中断的获得锁</h4><p>lockInterruptibly,调用后一直阻塞直至获得锁,但是接受中断信号</p><h4 id="1313-非块结构加锁"><a class="markdownIt-Anchor" href="#1313-非块结构加锁"></a> 13.1.3 非块结构加锁</h4><p>内置锁是块结构的加锁和自动释放锁,有时需要更大的灵活性,例如基于hash的集合可以使用分段锁</p><h3 id="132-性能考虑"><a class="markdownIt-Anchor" href="#132-性能考虑"></a> 13.2 性能考虑</h3><p>从Java 6 开始,内置锁已经不比显式锁性能差</p><h3 id="133-公平性"><a class="markdownIt-Anchor" href="#133-公平性"></a> 13.3 公平性</h3><p>内置锁不保证公平,ReentrantLock默认也不保证公平,非公平锁可以插队(不提倡,但是不阻止),性能相比公平锁会好一些</p><h3 id="134-在-synchronized-和-reentrantlock-中选择"><a class="markdownIt-Anchor" href="#134-在-synchronized-和-reentrantlock-中选择"></a> 13.4 在 Synchronized 和 ReentrantLock 中选择</h3><p>当你需要用到轮询和定时加锁、可中断的加锁、公平等待锁和非块结构加锁,使用 ReentrantLock,否则使用 synchronized</p><h3 id="135-读写锁"><a class="markdownIt-Anchor" href="#135-读写锁"></a> 13.5 读写锁</h3><p>读写锁:资源可以被多个读者同时访问或者单个写者访问</p><p>ReadWriteLock 定义读锁和写锁方法,和 Lock 类似,实现类在性能、调度、获得锁的优先条件、公平等方面可以不同</p><h2 id="chapter-14-构建自定义的同步工具"><a class="markdownIt-Anchor" href="#chapter-14-构建自定义的同步工具"></a> Chapter 14 构建自定义的同步工具</h2><p>最简单的方式使用已有类进行构造,例如LinkedBlockingQueue、CountDown-Latch、Semaphore和FutureTask等</p><h3 id="141-状态依赖性的管理"><a class="markdownIt-Anchor" href="#141-状态依赖性的管理"></a> 14.1 状态依赖性的管理</h3><p>单线程中,基于状态的前置条件不满足就失败。但是多线程中,状态会被其它线程修改,所以多线程程序在不满足前置条件时可以等待直至满足前置条件</p><h4 id="1411-将前置条件的失败传播给调用者"><a class="markdownIt-Anchor" href="#1411-将前置条件的失败传播给调用者"></a> 14.1.1 将前置条件的失败传播给调用者</h4><p>不满足前置条件就抛异常是滥用异常。调用者可以自旋等待(RUNNABLE态,占用cpu)或者阻塞(waiting态,不占cpu),即需要调用者编写前置条件管理的代码</p><h4 id="1412-通过轮询和睡眠粗鲁的阻塞"><a class="markdownIt-Anchor" href="#1412-通过轮询和睡眠粗鲁的阻塞"></a> 14.1.2 通过轮询和睡眠粗鲁的阻塞</h4><p>通过轮询和睡眠完成前置条件管理,不满足是就阻塞,调用者不需要管理前置条件,但需要处理 InterruptedException</p><h4 id="1413-条件队列"><a class="markdownIt-Anchor" href="#1413-条件队列"></a> 14.1.3 条件队列</h4><p>Object的wait,notify 和 notifyAll构成内置条件队列的API,wait会释放锁(本质和轮询与休眠是一样的,注意sleep前要释放锁)</p><h3 id="142-使用条件队列"><a class="markdownIt-Anchor" href="#142-使用条件队列"></a> 14.2 使用条件队列</h3><h4 id="1421-条件谓词"><a class="markdownIt-Anchor" href="#1421-条件谓词"></a> 14.2.1 条件谓词</h4><p>条件谓词:由类的状态变量构造的表达式,例如缓冲区非空即count>0</p><p>给条件队列相关的条件谓词以及等待它成立的操作写Javadoc</p><p>条件谓词涉及状态变量,状态变量由锁保护,所以在测试条件谓词之前,需要先获得锁。锁对象和条件队列对象(调用wait和notify的对象)必须是同一个对象</p><h4 id="1422-过早唤醒"><a class="markdownIt-Anchor" href="#1422-过早唤醒"></a> 14.2.2 过早唤醒</h4><p>一个线程由于其它线程调用notifyAll醒来,不意味着它的等待条件谓词一定为真。每当线程醒来必须再次测试条件谓词(使用循环)</p><h4 id="1423-丢失的信号"><a class="markdownIt-Anchor" href="#1423-丢失的信号"></a> 14.2.3 丢失的信号</h4><p>线程必须等待一个已经为真的条件,但是在开始等待之前没有检查条件谓词,发生的原因是编码错误,正确写法是循环测试条件谓词,false就继续wait</p><h4 id="1424-通知"><a class="markdownIt-Anchor" href="#1424-通知"></a> 14.2.4 通知</h4><p>优先使用 notifyAll,notify 可能会出现“hijacked signal”问题,唤醒了一个条件还未真的线程,本应被唤醒的线程还在等待。只有所有线程都在等同一个条件谓词且通知最多允许一个线程继续执行才使用notify</p><h4 id="1425-例子门"><a class="markdownIt-Anchor" href="#1425-例子门"></a> 14.2.5 例子:门</h4><p>用条件队列实现一个可以重复开关的线程门</p><h4 id="1426-子类的安全问题"><a class="markdownIt-Anchor" href="#1426-子类的安全问题"></a> 14.2.6 子类的安全问题</h4><p>一个依赖状态的类应该完全向子类暴露它的等待和通知协议,或者禁止继承</p><h4 id="1427-封装条件队列"><a class="markdownIt-Anchor" href="#1427-封装条件队列"></a> 14.2.7 封装条件队列</h4><p>最好将条件队列封装起来,在使用它的类的外面无法访问</p><h4 id="1428-进入和退出协议"><a class="markdownIt-Anchor" href="#1428-进入和退出协议"></a> 14.2.8 进入和退出协议</h4><p>进入协议:操作的条件谓词<br />退出协议:检查该操作修改的所有状态变量,确认他们是否使某个条件谓词成真,若是,通知相关队列</p><h3 id="143-显示-condition"><a class="markdownIt-Anchor" href="#143-显示-condition"></a> 14.3 显示 Condition</h3><p>显示锁在一些情况下比内置锁更灵活。类似地,Condition 比内置条件队列更灵活</p><p>内置条件队列有几个缺点:</p><ol><li>每个内置锁只能关联一个条件队列,即多个线程可能会在同一个条件队列上等待不同的条件谓词</li><li>最常见的加锁模式会暴露条件队列</li></ol><p>一个 Condition 关联一个 Lock,就像内置条件队列关联一个内置锁,使用 Lock.newCondition 来创建 Condition,Condition比内置条件队列功能丰富:每个锁有多个等待集(即一个Lock可以创建多个Condition),可中断和不可中断的条件等待,基于截止时间的等待,以及公平或不公平排队的选择。</p><p>Condition中和wait,notify,notifyAll 对应的方法是 await,signal,signalAll</p><h3 id="144-synchronizer剖析"><a class="markdownIt-Anchor" href="#144-synchronizer剖析"></a> 14.4 Synchronizer剖析</h3><p>ReentrantLock 和 Semaphore 有很多相似的地方。ReentrantLock可以作为只许一个线程进入的 Semaphore,Semaphore 可以用 ReentrantLock 实现</p><p>它们和其它同步器一样依赖基类 AbstractQueuedSynchronizer(AQS)。AQS是一个用于构建锁和同步器的框架,使用它可以轻松有效地构建范围广泛的同步器。</p><h3 id="145-abstractqueuedsynchronizer"><a class="markdownIt-Anchor" href="#145-abstractqueuedsynchronizer"></a> 14.5 AbstractQueuedSynchronizer</h3><p>依赖AQS的同步器的基本操作是获取和释放的一些变体。获取是阻塞操作,调用者获取不到会进入WAITING或失败。对于锁或信号量,获取的意思是获取锁或许可。对于CountDownLatch,获取的意思是等待门闩到达终点。对于FutureTask,获取的意思是等待任务完成。释放不是阻塞操作。同步器还会根据各自的语义维护状态信息</p><h3 id="146-juc同步器类中的aqs"><a class="markdownIt-Anchor" href="#146-juc同步器类中的aqs"></a> 14.6 JUC同步器类中的AQS</h3><p>JUC许多类使用AQS,例如 ReentrantLock, Semaphore, ReentrantReadWriteLock, CountDownLatch, SynchronousQueue, FutureTask</p><h2 id="chapter-15-原子变量与非阻塞同步机制"><a class="markdownIt-Anchor" href="#chapter-15-原子变量与非阻塞同步机制"></a> Chapter 15 原子变量与非阻塞同步机制</h2><p>非阻塞算法使用原子机器指令,例如 compare-and-swap 取代锁来实现并发下的数据完成性。它的设计比基于锁的算法复杂但可以提供更好的伸缩性和活跃性,非阻塞算法不会出现阻塞、死锁或其它活跃性问题,不会受到单个线程故障的影响</p><h3 id="151-锁的劣势"><a class="markdownIt-Anchor" href="#151-锁的劣势"></a> 15.1 锁的劣势</h3><p>JVM对非竞争锁进行优化,但是如果多个线程同时请求锁,就要借助操作系统挂起或者JVM自旋,开销很大。相比之下volatile是更轻量的同步机制,不涉及上下文切换和线程调度,然后volatile相较于锁,它不能构造原子性的复合操作,例如自增</p><p>锁还会出现优先级反转(阻塞线程优先级高,但是后执行),死锁等问题</p><h3 id="152-并发操作的硬件支持"><a class="markdownIt-Anchor" href="#152-并发操作的硬件支持"></a> 15.2 并发操作的硬件支持</h3><p>排它锁是悲观锁,总是假设最坏情况,只有确保其它线程不会干扰才会执行</p><p>乐观方法依赖碰撞检测来确定在更新过程中是否有其它线程的干扰,若有则操作失败并可以选择重试</p><p>为多处理器设计的cpu提供了对共享变量并发访问的特殊指令,例如 compare‐and‐swap,load-linked</p><h4 id="1521-compare-and-swap"><a class="markdownIt-Anchor" href="#1521-compare-and-swap"></a> 15.2.1 Compare and Swap</h4><p>CAS 有三个操作数:内存地址V,期待的旧值A,新值B。CAS在V的旧值是A的情况下原子更新值为B,否则什么都不做。CAS是一种乐观方法:它满怀希望更新变量,如果检测到其它线程更新了变量,它就会失败。CAS失败不会阻塞,允许重试(一般不重试,失败可能意味着别的线程已经完成该工作)</p><h4 id="1522-非阻塞的计数器"><a class="markdownIt-Anchor" href="#1522-非阻塞的计数器"></a> 15.2.2 非阻塞的计数器</h4><p>在竞争不激烈的情况下,性能比锁优秀。缺点是强制调用者处理竞争问题(重试、后退或放弃),而锁通过阻塞自动处理争用,直到锁可用</p><h4 id="1523-jvm对cas的支持"><a class="markdownIt-Anchor" href="#1523-jvm对cas的支持"></a> 15.2.3 JVM对CAS的支持</h4><p>JVM将CAS编译成底层硬件提供的方法,加入底层硬件不支持CAS,JVM会使用自旋锁。原子变量类使用了CAS</p><h3 id="153-原子变量类"><a class="markdownIt-Anchor" href="#153-原子变量类"></a> 15.3 原子变量类</h3><p>原子变量比锁的粒度更细,重量更轻,能提供volatile不支持的原子性</p><ol><li>原子变量可以作为更好的 volatile</li><li>在高度竞争情况下,锁性能更好,正常情况下,原子变量性能更好</li></ol><h3 id="154-非阻塞的算法"><a class="markdownIt-Anchor" href="#154-非阻塞的算法"></a> 15.4 非阻塞的算法</h3><p>如果一个线程的故障或挂起不会导致另一个线程的故障或挂起,则该算法称为非阻塞算法;如果一个算法在每个执行步骤中都有线程能够执行,那么这个算法被称为无锁算法。如果构造正确,只使用CAS进行线程间协调的算法可以是无阻塞和无锁的</p><h4 id="1541-非阻塞的栈"><a class="markdownIt-Anchor" href="#1541-非阻塞的栈"></a> 15.4.1 非阻塞的栈</h4><p>创建非阻塞算法的关键是如何在保持数据一致性的同时,将原子性更改的范围限制在单个变量内。</p><p>非阻塞的栈使用CAS来修改顶部元素</p><h4 id="1542-非阻塞的链表"><a class="markdownIt-Anchor" href="#1542-非阻塞的链表"></a> 15.4.2 非阻塞的链表</h4><p>Michale-scott算法</p><h4 id="1543-原子字段更新器"><a class="markdownIt-Anchor" href="#1543-原子字段更新器"></a> 15.4.3 原子字段更新器</h4><p>原子字段更新器代表了现有 volatile 字段的基于反射的“视图”,以便可以在现有的 volatile 字段上使用CAS</p><h4 id="1544-aba问题"><a class="markdownIt-Anchor" href="#1544-aba问题"></a> 15.4.4 ABA问题</h4><p>大部分情况下,CAS会询问“V的值还是A吗?”,是A就更新。但是有时候,我们需要知道“从我上次观察到V是A以来,它的值有没有改变?”。对于某些算法,将V的值从A->B,再从B->A,是一种更改,需要重新执行算法。解决方法是使用版本号,即使值从A变成B再变回A,版本号也会不同</p><h2 id="chapter-16-java-内存模型"><a class="markdownIt-Anchor" href="#chapter-16-java-内存模型"></a> Chapter 16 Java 内存模型</h2><h3 id="161-内存模型是什么为什么我需要一个内存模型"><a class="markdownIt-Anchor" href="#161-内存模型是什么为什么我需要一个内存模型"></a> 16.1 内存模型是什么,为什么我需要一个内存模型?</h3><p>在并发没有同步的情况下,有许多原因导致一个线程不能立即或永远不能再另一个线程中看到操作的结果</p><ol><li>编译器生成的指令顺序与源码不同</li><li>变量存在寄存器中而不是内存中</li><li>处理器可以并行或乱序执行指令</li><li>缓存可能会改变写入变量到主内存的顺序</li><li>存储在处理器本地缓存中的值可能对其它处理器不可见</li></ol><h4 id="1611-平台的内存模型"><a class="markdownIt-Anchor" href="#1611-平台的内存模型"></a> 16.1.1 平台的内存模型</h4><p>在共享存储的多处理器体系结构中,每个处理器都有自己的高速缓存,这些告诉缓存周期性地与主存协调。</p><p>体系结构的内存模型告诉程序可以从内存系统得到什么一致性保证,并制定所需的特殊指令(内存屏障或栅栏),以在共享数据时获得所需的额外内存协调保证。为了不受跨体系结构的影响,Java提供了自己的内存模型,JVM通过在适当的位置插入内存屏障来处理JMM(Java内存模型)和和底层平台的内存模型之间的差异</p><p>顺序一致性:程序中所有操作都有一个单一的顺序,而不管他们在什么处理器上执行,并且每次读取变量都会看到任何处理器按执行顺序对该变量的最后一次写入。</p><p>现代处理器没有提供顺序一致性,JMM也没有</p><h4 id="1612-重排"><a class="markdownIt-Anchor" href="#1612-重排"></a> 16.1.2 重排</h4><p>指令重排会使程序的行为出乎意料。同步限制了编译器、运行时和硬件在重排序时不会破坏JMM提供的可见性保证</p><h4 id="1613-java-内存模型"><a class="markdownIt-Anchor" href="#1613-java-内存模型"></a> 16.1.3 Java 内存模型</h4><p>Java 内存模型由一些操作指定,包括对变量的读写、监视器的锁定和解锁。JMM对所有操作定义了一个称为 happens before 的偏序规则:</p><ul><li>程序顺序规则:线程按程序定义的顺序执行操作</li><li>监视器锁规则:监视器锁的解锁必须发生在后续的加锁之前</li><li>volatile 变量规则:对 volatile 字段的写入操作必须发生在后续的读取之前</li><li>线程启动规则:对线程调用Thread.start会在该线程所有操作之前执行</li><li>线程结束规则:线程中任何操作必须在其它线程检测到该线程已经结束之前执行或者从Thread.join返回或者Thread.isAlive返回false</li><li>中断规则:一个线程对另一个线程调用 interrupt 发生在被中断线程检测到中断之前</li><li>终结器规则:对象的构造函数必须在启动该对象的终结器之前执行完成</li><li>传递性:A发生在B之前,B发生在C之前,那么A发生在C之前</li></ul><h4 id="1614-借用同步"><a class="markdownIt-Anchor" href="#1614-借用同步"></a> 16.1.4 借用同步</h4><p>通过类库保证 happens-before顺序:</p><ul><li>将元素放入线程安全的容器发生在另一个线程从集合中检索元素之前</li><li>在CountDownLatch上进行倒数发生在该线程从门闩的await返回之前</li><li>释放信号量的许可发生在获取之前</li><li>Future代表的任务执行的操作发生在另一个线程从Future.get返回之前</li><li>提交Runnable或者Callable任务给执行器发生在任务开始之前</li><li>线程到达CyclicBarrier或Exchanger发生在其它线程释放相同的barrier或者exchange point之前</li></ul><h3 id="162-发布"><a class="markdownIt-Anchor" href="#162-发布"></a> 16.2 发布</h3><h4 id="1621-不安全的发布"><a class="markdownIt-Anchor" href="#1621-不安全的发布"></a> 16.2.1 不安全的发布</h4><p>当缺少happens-before关系时候,就可能出现重排序问题,这就解释了为什么在没有同步情况下发布一个对象会导致另一个线程看到一个只被部分构造的对象</p><p>除了不可变对象之外,使用由其他线程初始化的对象都是不安全的,除非对象的发布发生在消费线程使用它之前</p><h4 id="1622-安全发布"><a class="markdownIt-Anchor" href="#1622-安全发布"></a> 16.2.2 安全发布</h4><p>使用锁或者volatile变量可以确保读写操作按照 happens-before 排序</p><h4 id="1623-安全初始化"><a class="markdownIt-Anchor" href="#1623-安全初始化"></a> 16.2.3 安全初始化</h4><p>静态字段在声明时就初始化由JVM提供线程安全保证。<br />延迟初始化,可以写在同步方法里面,或者使用辅助类,在辅助类中声明并初始化</p><h4 id="1624-双重检查锁"><a class="markdownIt-Anchor" href="#1624-双重检查锁"></a> 16.2.4 双重检查锁</h4><p>Java 5之前的双重检查锁会出现引用是新值但是对象是旧值,这意味着可以看到对象不正确的状态,Java5之后给引用声明加上 volatile 可以起到线程安全地延迟初始化作用,但是不如使用辅助类,效果一样且更容易懂</p><h3 id="163-初始化的安全性"><a class="markdownIt-Anchor" href="#163-初始化的安全性"></a> 16.3 初始化的安全性</h3><p>初始化安全性只能保证通过final字段可达的值从构造过程完成时开始的可见性(事实不可变对象以任何形式发布都是安全的)。对于通过非final字段可达的值,或者构成完成之后可能改变的值,必须采用同步确保可见性。</p>]]></content>
<summary type="html"><p>本书分成四部分,<br />
第一部分是基础,主要内容是并发的基础概念和线程安全,以及如何用java类库提供的并发构建块组成线程安全的类(2-5章节)</p>
<p>第二部分是构建并发应用程序,主要内容是如何利用线程来提高并发应用的吞吐量和响应能力(6-9章节)</p>
<</summary>
<category term="Java" scheme="https://zunpan.github.io/categories/Java/"/>
<category term="Java并发编程实战" scheme="https://zunpan.github.io/tags/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E5%AE%9E%E6%88%98/"/>
<category term="Java Concurrency In Practice" scheme="https://zunpan.github.io/tags/Java-Concurrency-In-Practice/"/>
</entry>
<entry>
<title>2022.06-2022.11实习总结</title>
<link href="https://zunpan.github.io/2022/12/03/2022.06-2022.11%E5%AE%9E%E4%B9%A0%E6%80%BB%E7%BB%93/"/>
<id>https://zunpan.github.io/2022/12/03/2022.06-2022.11%E5%AE%9E%E4%B9%A0%E6%80%BB%E7%BB%93/</id>
<published>2022-12-03T11:29:40.000Z</published>
<updated>2023-09-24T04:27:40.272Z</updated>
<content type="html"><![CDATA[<h2 id="基于json-schema的配置平台"><a class="markdownIt-Anchor" href="#基于json-schema的配置平台"></a> 基于JSON Schema的配置平台</h2><p>背景:这是公司的新人入职练手项目,总共两周时间,实现一个基于JSON Schema的表单配置平台。<br />需求:表单配置指的是通过可视化界面配置 表单的每个字段,包括名称、输入类型、输入限制等。mentor给了一个方向叫JSON Schema<br />调研:JSON Schema是描述json数据的元数据,本身也是json字符串,一般两个作用,1. 后端用 JSON Schema 对前端传的json传进行格式校验;2. 前端通过 JSON Schema生成表单</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/2022.06~2022.11%E5%AE%9E%E4%B9%A0%E6%80%BB%E7%BB%93/IMG_20221203-201430967.png" alt="图 3" /></p><p>开发:前端使用Vue、后端使用Spring Boot<br />难点:JSON Schema和表单的双向转换。用户可以手动编辑JSON Schema生成表单项,也可以通过可视化界面增加表单项来修改JSON Schema。<br />解决方案:尝试过写一套解析方案,但是dom操作太复杂作罢。调研了一些开源方案,最终选用vue-json-schema-form</p><h2 id="excel比对与合并系统"><a class="markdownIt-Anchor" href="#excel比对与合并系统"></a> Excel比对与合并系统</h2><p>背景:接手的第一个项目,关于Excel比对与合并,主要参与系统的优化与维护工作<br />主要工作:</p><ol><li>批量文件比对的多线程优化(比对方法涉及对象完全栈封闭)</li><li>批量文件合并OOM排查(合并需要先反序列化比对结果,若有多个版本的比对结果需要合并到另一分支上的同名文件,需要循环处理,每个比对结果合并之后需要置空,否则内存无法释放,排查工具:visualvm,发现调用合并方法时,Minor GC非常快,内存居高不下导致OOM;根本原因:自己开发的Excel解析工具+Java自带的序列化导致序列化产物非常大)</li></ol><p>算法介绍可以参看<a href="https://zunpan.github.io/2023/03/03/Excel%E6%AF%94%E5%AF%B9%E4%B8%8E%E5%90%88%E5%B9%B6%E7%B3%BB%E7%BB%9F/">《Excel比对与合并系统》</a></p><h2 id="助理系统"><a class="markdownIt-Anchor" href="#助理系统"></a> 助理系统</h2><p>需求:公司员工反馈行政问题都是在公司的聊天软件里反馈,反馈途径包括事业群、私聊行政助理、以及服务号反馈(类似微信公众号),行政助理需要在多个系统进行处理,助理系统为助理统一了消息来源,助理可以在助理系统中回复所有渠道的问题反馈。<br />调研:spring-boot-starter-websocket,实现了客户端与服务器全双工通信<br />难点:助理系统需要给每个反馈问题的员工生成唯一的对话,初次反馈消息时,快速发送消息会创建多个对话。这是因为后端多线程处理消息,每个线程都先去数据库查询此条消息的员工是否存在对话,如果不存在就创建。这里出现了并发经典错误 check-then-act。<br />解决方案:给会话表的员工id字段建唯一索引,插入新会话使用insert ignore。</p>]]></content>
<summary type="html"><h2 id="基于json-schema的配置平台"><a class="markdownIt-Anchor" href="#基于json-schema的配置平台"></a> 基于JSON Schema的配置平台</h2>
<p>背景:这是公司的新人入职练手项目,总共两周时间,</summary>
<category term="杂项" scheme="https://zunpan.github.io/categories/%E6%9D%82%E9%A1%B9/"/>
<category term="实习" scheme="https://zunpan.github.io/tags/%E5%AE%9E%E4%B9%A0/"/>
</entry>
<entry>
<title>Effective-Java学习笔记(九)</title>
<link href="https://zunpan.github.io/2022/08/13/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B9%9D%EF%BC%89/"/>
<id>https://zunpan.github.io/2022/08/13/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B9%9D%EF%BC%89/</id>
<published>2022-08-13T08:11:11.000Z</published>
<updated>2023-09-24T04:27:40.274Z</updated>
<content type="html"><![CDATA[<h2 id="第十章-异常"><a class="markdownIt-Anchor" href="#第十章-异常"></a> 第十章 异常</h2><h3 id="69-仅在确有异常条件下使用异常"><a class="markdownIt-Anchor" href="#69-仅在确有异常条件下使用异常"></a> 69. 仅在确有异常条件下使用异常</h3><p>有人认为用 try-catch 和 while(true) 遍历数组比用 for-each 性能更好,因为 for-each 由编译器隐藏了边界检查,而 try-catch 代码中不包含检查</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Horrible abuse of exceptions. Don't ever do this!</span><span class="hljs-keyword">try</span> { <span class="hljs-type">int</span> <span class="hljs-variable">i</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; <span class="hljs-keyword">while</span>(<span class="hljs-literal">true</span>){ range[i++].climb(); } <span class="hljs-keyword">catch</span> (ArrayIndexOutOfBoundsException e) {}}</code></pre><p>这个想法有三个误区:</p><ol><li>因为异常是为特殊情况设计的,所以 JVM 实现几乎不会让它们像显式测试一样快。</li><li>将代码放在 try-catch 块中会抑制 JVM 可能执行的某些优化。</li><li>遍历数组的标准习惯用法不一定会导致冗余检查。许多 JVM 实现对它们进行了优化。</li></ol><p>基于异常的循环除了不能提高性能外,还容易被异常隐藏循环中的 bug。因此,异常只适用于确有异常的情况;它们不应该用于一般的控制流程。</p><p>一个设计良好的 API 不能迫使其客户端为一般的控制流程使用异常。调用具有「状态依赖」方法的类,通常应该有一个单独的「状态测试」方法,表明是否适合调用「状态依赖」方法。例如,Iterator 接口具有「状态依赖」的 next 方法和对应的「状态测试」方法 hasNext。</p><pre class="highlight"><code class="java"><span class="hljs-keyword">for</span> (Iterator<Foo> i = collection.iterator(); i.hasNext(); ) { <span class="hljs-type">Foo</span> <span class="hljs-variable">foo</span> <span class="hljs-operator">=</span> i.next(); ...}</code></pre><p>如果 Iterator 缺少 hasNext 方法,客户端将被迫这样做:</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Do not use this hideous code for iteration over a collection!</span><span class="hljs-keyword">try</span> { Iterator<Foo> i = collection.iterator(); <span class="hljs-keyword">while</span>(<span class="hljs-literal">true</span>) { <span class="hljs-type">Foo</span> <span class="hljs-variable">foo</span> <span class="hljs-operator">=</span> i.next(); ... }}<span class="hljs-keyword">catch</span> (NoSuchElementException e) {}</code></pre><p>这与一开始举例的对数组进行迭代的例子非常相似,除了冗长和误导之外,基于异常的循环执行效果可能很差,并且会掩盖系统中不相关部分的 bug。</p><p>提供单独的「状态测试」方法的另一种方式,就是让「状态依赖」方法返回一个空的 Optional 对象(Item-55),或者在它不能执行所需的计算时返回一个可识别的值,比如 null。</p><p>状态测试方法、Optional、可识别的返回值之间的选择如下:</p><ol><li>如果要在没有外部同步的情况下并发地访问对象,或者受制于外部条件的状态转换,则必须使用 Optional 或可识别的返回值,因为对象的状态可能在调用「状态测试」方法与「状态依赖」方法的间隔中发生变化。</li><li>如果一个单独的「状态测试」方法重复「状态依赖」方法的工作,从性能问题考虑,可能要求使用 Optional 或可识别的返回值</li><li>在所有其他条件相同的情况下,「状态测试」方法略优于可识别的返回值。它提供了较好的可读性,而且不正确的使用可能更容易被检测:如果你忘记调用「状态测试」方法,「状态依赖」方法将抛出异常,使错误显而易见;</li><li>如果你忘记检查一个可识别的返回值,那么这个 bug 可能很难发现。但是这对于返回 Optional 对象的方式来说不是问题。</li></ol><p>总之,异常是为确有异常的情况设计的。不要将它们用于一般的控制流程,也不要编写强制其他人这样做的 API。</p><h3 id="70-对可恢复情况使用-checked-异常对编程错误使用运行时异常"><a class="markdownIt-Anchor" href="#70-对可恢复情况使用-checked-异常对编程错误使用运行时异常"></a> 70. 对可恢复情况使用 checked 异常,对编程错误使用运行时异常</h3><p>Java 提供了三种可抛出项:checked 异常、运行时异常和错误。决定是使用 checked 异常还是 unchecked 异常的基本规则是:使用 checked 异常的情况是为了合理地期望调用者能够从中恢复。</p><p>有两种 unchecked 的可抛出项:运行时异常和错误。它们在行为上是一样的:都是可抛出的,通常不需要也不应该被捕获。如果程序抛出 unchecked 异常或错误,通常情况下是不可能恢复的,如果继续执行,弊大于利。如果程序没有捕获到这样的可抛出项,它将导致当前线程停止,并发出适当的错误消息。</p><h4 id="运行时异常"><a class="markdownIt-Anchor" href="#运行时异常"></a> 运行时异常</h4><p>运行时异常用来指示编程错误。大多数运行时异常都表示操作违反了先决条件。违反先决条件是指使用 API 的客户端未能遵守 API 规范所建立的约定。例如,数组访问约定指定数组索引必须大于等于 0 并且小于等于 length-1 (length:数组长度)。ArrayIndexOutOfBoundsException 表示违反了此先决条件</p><p>这个建议存在的问题是:并不总能清楚是在处理可恢复的条件还是编程错误。例如,考虑资源耗尽的情况,这可能是由编程错误(如分配一个不合理的大数组)或真正的资源短缺造成的。如果资源枯竭是由于暂时短缺或暂时需求增加造成的,这种情况很可能是可以恢复的。对于 API 设计人员来说,判断给定的资源耗尽实例是否允许恢复是一个问题。如果你认为某个条件可能允许恢复,请使用 checked 异常;如果没有,则使用运行时异常。如果不清楚是否可以恢复,最好使用 unchecked 异常</p><h4 id="错误"><a class="markdownIt-Anchor" href="#错误"></a> 错误</h4><p>虽然 Java 语言规范没有要求,但有一个约定俗成的约定,即错误保留给 JVM 使用,以指示:资源不足、不可恢复故障或其他导致无法继续执行的条件。考虑到这种约定被大众认可,所以最好不要实现任何新的 Error 子类。因此,你实现的所有 unchecked 异常都应该继承 RuntimeException(直接或间接)。不仅不应该定义 Error 子类,而且除了 AssertionError 之外,不应该抛出它们。</p><h4 id="自定义异常"><a class="markdownIt-Anchor" href="#自定义异常"></a> 自定义异常</h4><p>自定义异常继承 Throwable 类,Java 语言规范把它们当做普通 checked 异常(普通 checked 异常是 Exception 的子类,但不是 RuntimeException的子类)。不要使用自定义异常,它会让 API 的用户困惑</p><h4 id="异常附加信息"><a class="markdownIt-Anchor" href="#异常附加信息"></a> 异常附加信息</h4><p>API 设计人员常常忘记异常是成熟对象,可以为其定义任意方法。此类方法的主要用途是提供捕获异常的代码,并提供有关引发异常的附加信息。如果缺乏此类方法,程序员需要自行解析异常的字符串表示以获取更多信息。这是极坏的做法</p><p>因为 checked 异常通常表示可恢复的条件,所以这类异常来说,设计能够提供信息的方法来帮助调用者从异常条件中恢复尤为重要。例如,假设当使用礼品卡购物由于资金不足而失败时,抛出一个 checked 异常。该异常应提供一个访问器方法来查询差额。这将使调用者能够将金额传递给购物者。</p><p>总而言之,为可恢复条件抛出 checked 异常,为编程错误抛出 unchecked 异常。当有疑问时,抛出 unchecked 异常。不要定义任何既不是 checked 异常也不是运行时异常的自定义异常。应该为 checked 异常设计相关的方法,如提供异常信息,以帮助恢复。</p><h3 id="71-避免不必要地使用-checked-异常"><a class="markdownIt-Anchor" href="#71-避免不必要地使用-checked-异常"></a> 71. 避免不必要地使用 checked 异常</h3><p>合理抛出 checked 异常可以提高程序可靠性。过度使用会使得调用它的方法多次 try-catch 或抛出,给 API 用户带来负担,尤其是 Java8 中,抛出checked 异常的方法不能直接在流中使用。</p><p>只有在正确使用 API 也无法避免异常且使用 API 的程序员在遇到异常时可以采取一些有用的操作才能使用 checked 异常,否则抛出 unchecked 异常</p><p>如果 checked 异常是方法抛出的唯一 checked 异常,那么 checked 异常给程序员带来的额外负担就会大得多。如果还有其他 checked 异常,则该方法一定已经在 try 块中了,因此该异常最多需要另一个 catch 块而已。如果一个方法抛出单个 checked 异常,那么这个异常就是该方法必须出现在 try 块中而不能直接在流中使用的唯一原因。在这种情况下,有必要问问自己是否有办法避免 checked 异常。</p><p>消除 checked 异常的最简单方法是返回所需结果类型的 Optional 对象(Item-55)。该方法只返回一个空的 Optional 对象,而不是抛出一个 checked 异常。这种技术的缺点是,该方法不能返回任何详细说明其无法执行所需计算的附加信息。相反,异常具有描述性类型,并且可以导出方法来提供附加信息(Item-70)</p><p>总之,如果谨慎使用,checked 异常可以提高程序的可靠性;当过度使用时,它们会使 API 难以使用。如果调用者不应从失败中恢复,则抛出 unchecked 异常。如果恢复是可能的,并且你希望强制调用者处理异常情况,那么首先考虑返回一个 Optional 对象。只有当在失败的情况下,提供的信息不充分时,你才应该抛出一个 checked 异常。</p><h3 id="72-鼓励复用标准异常"><a class="markdownIt-Anchor" href="#72-鼓励复用标准异常"></a> 72. 鼓励复用标准异常</h3><p>Java 库提供了一组异常,涵盖了大多数 API 的大多数异常抛出需求。</p><p>复用标准异常有几个好处:</p><ol><li>使你的 API 更容易学习和使用,因为它符合程序员已经熟悉的既定约定</li><li>使用你的 API 的程序更容易阅读,因为它们不会因为不熟悉的异常而混乱</li><li>更少的异常类意味着更小的内存占用和更少的加载类的时间</li></ol><p>常见的被复用的异常:</p><ol><li><p>IllegalArgumentException。通常是调用者传入不合适的参数时抛出的异常</p></li><li><p>IllegalStateException。如果因为接收对象的状态导致调用非法,则通常会抛出此异常。例如,调用者试图在对象被正确初始化之前使用它</p><p>可以说,每个错误的方法调用都归结为参数非法或状态非法,但是有一些异常通常用于某些特定的参数非法和状态非法。如果调用者在禁止空值的参数中传递 null,那么按照惯例,抛出 NullPointerException 而不是 IllegalArgumentException。类似地,如果调用者将表示索引的参数中的超出范围的值传递给序列,则应该抛出 IndexOutOfBoundsException,而不是 IllegalArgumentException</p></li><li><p>ConcurrentModificationException。如果一个对象被设计为由单个线程使用(或与外部同步),并且检测到它正在被并发地修改,则应该抛出该异常。因为不可能可靠地检测并发修改,所以该异常充其量只是一个提示。</p></li><li><p>UnsupportedOperationException。如果对象不支持尝试的操作,则抛出此异常。它很少使用,因为大多数对象都支持它们的所有方法。此异常用于一个类没有实现由其实现的接口定义的一个或多个可选操作。例如,对于只支持追加操作的 List 实现,试图从中删除元素时就会抛出这个异常</p></li></ol><p>不要直接复用 Exception、RuntimeException、Throwable 或 Error</p><p>此表总结了最常见的可复用异常:</p><table><thead><tr><th style="text-align:center">Exception</th><th style="text-align:center">Occasion for Use</th></tr></thead><tbody><tr><td style="text-align:center">IllegalArgumentException</td><td style="text-align:center">Non-null parameter value is inappropriate(非空参数值不合适)</td></tr><tr><td style="text-align:center">IllegalStateException</td><td style="text-align:center">Object state is inappropriate for method invocation(对象状态不适用于方法调用)</td></tr><tr><td style="text-align:center">NullPointerException</td><td style="text-align:center">Parameter value is null where prohibited(禁止参数为空时仍传入 null)</td></tr><tr><td style="text-align:center">IndexOutOfBoundsException</td><td style="text-align:center">Index parameter value is out of range(索引参数值超出范围)</td></tr><tr><td style="text-align:center">ConcurrentModificationException</td><td style="text-align:center">Concurrent modification of an object has been detected where it is prohibited(在禁止并发修改对象的地方检测到该动作)</td></tr><tr><td style="text-align:center">UnsupportedOperationException</td><td style="text-align:center">Object does not support method(对象不支持该方法调用)</td></tr></tbody></table><p>其它异常如果有合适的复用场景也可以复用,例如,如果你正在实现诸如复数或有理数之类的算术对象,那么复用 ArithmeticException 和 NumberFormatException 是合适的</p><h3 id="73-抛出适合底层抽象异常的高层异常"><a class="markdownIt-Anchor" href="#73-抛出适合底层抽象异常的高层异常"></a> 73. 抛出适合底层抽象异常的高层异常</h3><p>当方法抛出一个与它所执行的任务没有明显关联的异常时,这是令人不安的。这种情况经常发生在由方法传播自低层抽象抛出的异常。它不仅令人不安,而且让实现细节污染了上层的 API。</p><p>为了避免这个问题,高层应该捕获低层异常,并确保抛出的异常可以用高层抽象解释。 这个习惯用法称为异常转换:</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Exception Translation</span><span class="hljs-keyword">try</span> { ... <span class="hljs-comment">// Use lower-level abstraction to do our bidding</span>} <span class="hljs-keyword">catch</span> (LowerLevelException e) { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">HigherLevelException</span>(...);}</code></pre><p>下面是来自 AbstractSequentialList 类的异常转换示例,该类是 List 接口的一个框架实现(<a href="/Chapter-4/Chapter-4-Item-20-Prefer-interfaces-to-abstract-classes.md">Item-20</a>)。在本例中,异常转换是由 <code>List<E></code> 接口中的 get 方法规范强制执行的:</p><pre class="highlight"><code class="java"><span class="hljs-comment">/*** Returns the element at the specified position in this list.* <span class="hljs-doctag">@throws</span> IndexOutOfBoundsException if the index is out of range* ({<span class="hljs-doctag">@code</span> index < 0 || index >= size()}).*/</span><span class="hljs-keyword">public</span> E <span class="hljs-title function_">get</span><span class="hljs-params">(<span class="hljs-type">int</span> index)</span> { ListIterator<E> i = listIterator(index); <span class="hljs-keyword">try</span> { <span class="hljs-keyword">return</span> i.next(); } <span class="hljs-keyword">catch</span> (NoSuchElementException e) { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">IndexOutOfBoundsException</span>(<span class="hljs-string">"Index: "</span> + index); }}</code></pre><p>如果低层异常可能有助于调试高层异常的问题,则需要一种称为链式异常的特殊异常转换形式。低层异常(作为原因)传递给高层异常,高层异常提供一个访问器方法(Throwable 的 getCause 方法)来检索低层异常:</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Exception Chaining</span><span class="hljs-keyword">try</span> { ... <span class="hljs-comment">// Use lower-level abstraction to do our bidding</span>}<span class="hljs-keyword">catch</span> (LowerLevelException cause) { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">HigherLevelException</span>(cause);}</code></pre><p>高层异常的构造函数将原因传递给能够接收链式异常的父类构造函数,因此它最终被传递给 Throwable 的一个接收链式异常的构造函数,比如 Throwable(Throwable):</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Exception with chaining-aware constructor</span><span class="hljs-keyword">class</span> <span class="hljs-title class_">HigherLevelException</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_">Exception</span> { HigherLevelException(Throwable cause) { <span class="hljs-built_in">super</span>(cause); }}</code></pre><p>大多数标准异常都有接收链式异常的构造函数。对于不支持链式异常的异常,可以使用 Throwable 的 initCause 方法设置原因。异常链不仅允许你以编程方式访问原因(使用 getCause),而且还将原因的堆栈跟踪集成到更高层异常的堆栈跟踪中。</p><p>虽然异常转换优于底层异常的盲目传播,但它不应该被过度使用。在可能的情况下,处理底层异常的最佳方法是确保底层方法避免异常。有时,你可以在将高层方法的参数传递到底层之前检查它们的有效性。</p><p>如果不可能从底层防止异常,那么下一个最好的方法就是让高层静默处理这些异常,使较高层方法的调用者免受底层问题的影响。在这种情况下,可以使用一些适当的日志工具(如 <code>java.util.logging</code>)来记录异常。这允许程序员研究问题,同时将客户端代码和用户与之隔离。</p><p>总之,如果无法防止或处理来自底层的异常,则使用异常转换,但要保证底层方法的所有异常都适用于较高层。链式异常提供了兼顾两方面的最佳服务:允许抛出适当的高层异常,同时捕获并分析失败的潜在原因</p><h3 id="74-为每个方法记录会抛出的所有异常"><a class="markdownIt-Anchor" href="#74-为每个方法记录会抛出的所有异常"></a> 74. 为每个方法记录会抛出的所有异常</h3><ol><li>始终单独声明 checked 异常,并使用 Javadoc 的 @throw 标记精确记录每次抛出异常的条件。如果一个方法抛出多个异常,不要使用快捷方式声明这些异常的父类。作为一个极端的例子,即不要在公共方法声明 throws Exception,除了只被 JVM 调用的 main方法</li><li>unchecked 异常不要声明 throws,但应该像 checked 异常一样用 Javadoc 记录他们。特别是接口中的方法要记录可能抛出的 unchecked 异常。</li></ol><p>如果一个类中的许多方法都因为相同的原因抛出异常,你可以在类的文档注释中记录异常, 而不是为每个方法单独记录异常。一个常见的例子是 NullPointerException。类的文档注释可以这样描述:「如果在任何参数中传递了 null 对象引用,该类中的所有方法都会抛出 NullPointerException」</p><p>总之,记录你所编写的每个方法可能引发的每个异常。对于 unchecked 异常、checked 异常、抽象方法、实例方法都是如此。应该在文档注释中采用 @throw 标记的形式。在方法的 throws 子句中分别声明每个 checked 异常,但不要声明 unchecked 异常。如果你不记录方法可能抛出的异常,其他人将很难或不可能有效地使用你的类和接口。</p><h3 id="75-异常详细消息中应包含捕获失败的信息"><a class="markdownIt-Anchor" href="#75-异常详细消息中应包含捕获失败的信息"></a> 75. 异常详细消息中应包含捕获失败的信息</h3><p>程序因为未捕获异常而失败时,系统会自动调用异常的 toString 方法打印堆栈信息,堆栈信息包含异常的类名及详细信息。异常的详细消息应该包含导致异常的所有参数和字段的值。例如,IndexOutOfBoundsException 的详细消息应该包含下界、上界和未能位于下界之间的索引值。</p><p>异常详细信息不用过于冗长,程序失败时可以通过阅读文档和源代码收集信息,确保异常包含足够的信息的一种方法是在构造函数中配置异常信息,例如,IndexOutOfBoundsException 构造函数不包含 String 参数,而是像这样:</p><pre class="highlight"><code class="java"><span class="hljs-comment">/*** Constructs an IndexOutOfBoundsException.**<span class="hljs-doctag">@param</span> lowerBound the lowest legal index value* <span class="hljs-doctag">@param</span> upperBound the highest legal index value plus one* <span class="hljs-doctag">@param</span> index the actual index value*/</span><span class="hljs-keyword">public</span> <span class="hljs-title function_">IndexOutOfBoundsException</span><span class="hljs-params">(<span class="hljs-type">int</span> lowerBound, <span class="hljs-type">int</span> upperBound, <span class="hljs-type">int</span> index)</span> { <span class="hljs-comment">// Generate a detail message that captures the failure</span> <span class="hljs-built_in">super</span>(String.format(<span class="hljs-string">"Lower bound: %d, Upper bound: %d, Index: %d"</span>,lowerBound, upperBound, index)); <span class="hljs-comment">// Save failure information for programmatic access</span> <span class="hljs-built_in">this</span>.lowerBound = lowerBound; <span class="hljs-built_in">this</span>.upperBound = upperBound; <span class="hljs-built_in">this</span>.index = index;}</code></pre><h3 id="76-尽力保证故障原子性"><a class="markdownIt-Anchor" href="#76-尽力保证故障原子性"></a> 76. 尽力保证故障原子性</h3><p>失败的方法调用应该使对象处于调用之前的状态。 具有此属性的方法称为具备故障原子性。</p><p>有几种实现故障原子性的方式:</p><p>关于不可变对象</p><ol><li>不可变对象在创建后永远处于一致状态</li></ol><p>关于可变对象:</p><ol><li>在修改状态前,先执行可能抛出异常的操作,例如检查状态,不合法就抛出异常</li><li>在临时副本上操作,成功后替换原来的对象,不成功不影响原来的对象。例如,一些排序函数会将入参 list 复制到数组中,对数组进行排序,再转换成 list</li><li>编写回滚代码,主要用于持久化的数据</li></ol><p>有些情况是不能保证故障原子性的,例如,多线程不同步修改对象,对象可能处于不一致状态,当捕获到 ConcurrentModificationException 后对象不可恢复</p><p>总之,作为方法规范的一部分,生成的任何异常都应该使对象保持在方法调用之前的状态。如果违反了这条规则,API 文档应该清楚地指出对象将处于什么状态。</p><h3 id="77-不要忽略异常"><a class="markdownIt-Anchor" href="#77-不要忽略异常"></a> 77. 不要忽略异常</h3><p>异常要么 try-catch 要么抛出,不要写空的 catch 块,如果这样做,写上注释</p>]]></content>
<summary type="html"><h2 id="第十章-异常"><a class="markdownIt-Anchor" href="#第十章-异常"></a> 第十章 异常</h2>
<h3 id="69-仅在确有异常条件下使用异常"><a class="markdownIt-Anchor" href="#6</summary>
<category term="Java" scheme="https://zunpan.github.io/categories/Java/"/>
<category term="Effective-Java" scheme="https://zunpan.github.io/tags/Effective-Java/"/>
<category term="异常" scheme="https://zunpan.github.io/tags/%E5%BC%82%E5%B8%B8/"/>
</entry>
<entry>
<title>Effective-Java学习笔记(八)</title>
<link href="https://zunpan.github.io/2022/08/06/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E5%85%AB%EF%BC%89/"/>
<id>https://zunpan.github.io/2022/08/06/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E5%85%AB%EF%BC%89/</id>
<published>2022-08-06T07:30:09.000Z</published>
<updated>2023-09-24T04:27:40.275Z</updated>
<content type="html"><![CDATA[<h2 id="第九章-通用程序设计"><a class="markdownIt-Anchor" href="#第九章-通用程序设计"></a> 第九章 通用程序设计</h2><h3 id="57-将局部变量的作用域最小化"><a class="markdownIt-Anchor" href="#57-将局部变量的作用域最小化"></a> 57. 将局部变量的作用域最小化</h3><p>本条目在性质上类似于Item-15,即「最小化类和成员的可访问性」。通过最小化局部变量的范围,可以提高代码的可读性和可维护性,并降低出错的可能性。</p><ol><li>将局部变量的作用域最小化,最具说服力的方式就是在第一次使用它的地方声明</li><li>每个局部变量声明都应该包含一个初始化表达式。 如果你还没有足够的信息来合理地初始化一个变量,你应该推迟声明,直到条件满足</li></ol><h3 id="58-for-each循环优于for循环"><a class="markdownIt-Anchor" href="#58-for-each循环优于for循环"></a> 58. for-each循环优于for循环</h3><p>for-each 更简洁更不容易出错,且没有性能损失,只有三种情况不能用for-each</p><ol><li>破坏性过滤。如果需要遍历一个集合并删除选定元素,则需要使用显式的迭代器,以便调用其 remove 方法。通过使用 Collection 在 Java 8 中添加的 removeIf 方法,通常可以避免显式遍历。</li><li>转换。如果需要遍历一个 List 或数组并替换其中部分或全部元素的值,那么需要 List 迭代器或数组索引来替换元素的值。</li><li>并行迭代。如果需要并行遍历多个集合,那么需要显式地控制迭代器或索引变量,以便所有迭代器或索引变量都可以同步执行</li></ol><h3 id="59-了解并使用库"><a class="markdownIt-Anchor" href="#59-了解并使用库"></a> 59. 了解并使用库</h3><p>假设你想要生成 0 到某个上界之间的随机整数,有些程序员会写出如下代码:</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Common but deeply flawed!</span><span class="hljs-keyword">static</span> <span class="hljs-type">Random</span> <span class="hljs-variable">rnd</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Random</span>();<span class="hljs-keyword">static</span> <span class="hljs-type">int</span> <span class="hljs-title function_">random</span><span class="hljs-params">(<span class="hljs-type">int</span> n)</span> { <span class="hljs-keyword">return</span> Math.abs(rnd.nextInt()) % n;}</code></pre><p>这个方法有三个缺点:</p><ol><li>如果 n 是比较小的 2 的幂,随机数序列会在相当短的时间内重复</li><li>如果 n 不是 2 的幂,一些数字出现的频率会更高,当 n 很大时效果明显</li><li>会返回超出指定范围的数字,当nextInt返回Integer.MIN_VALUE时,abs方法也会返回Integer.MIN_VALUE,假设 n 不是 2 幂,那么Integer.MIN_VALUE % n 将返回负数</li></ol><p>我们不需要为这个需求自己编写方法,已经存在经过专家设计测试的标准库Random 的 nextInt(int)</p><p>从 Java 7 开始,就不应该再使用 Random。在大多数情况下,选择的随机数生成器现在是 ThreadLocalRandom。 它能产生更高质量的随机数,而且速度非常快。</p><p>总而言之,不要白费力气重新发明轮子。如果你需要做一些看起来相当常见的事情,那么库中可能已经有一个工具可以做你想做的事情。如果有,使用它;如果你不知道,查一下。一般来说,库代码可能比你自己编写的代码更好,并且随着时间的推移可能会得到改进。</p><h3 id="60-若需要精确答案就应避免使用-float-和-double-类型"><a class="markdownIt-Anchor" href="#60-若需要精确答案就应避免使用-float-和-double-类型"></a> 60. 若需要精确答案就应避免使用 float 和 double 类型</h3><p>float 和 double 类型主要用于科学计算和工程计算。它们执行二进制浮点运算,该算法经过精心设计,能够在很大范围内快速提供精确的近似值。但是,它们不能提供准确的结果。float 和 double 类型特别不适合进行货币计算,因为不可能将 0.1(或 10 的任意负次幂)精确地表示为 float 或 double</p><p>正确做法是使用 BigDecimal、int 或 long 进行货币计算。还要注意 BigDecimal 的构造函数要使用String参数而不是double,避免初始化时就用了不精确的值。int和long可以存储较小单位的值,将小数转换成整数存储</p><p>总之,对于任何需要精确答案的计算,不要使用 float 或 double 类型。如果希望系统来处理十进制小数点,并且不介意不使用基本类型带来的不便和成本,请使用 BigDecimal。使用 BigDecimal 的另一个好处是,它可以完全控制舍入,当执行需要舍入的操作时,可以从八种舍入模式中进行选择。如果你使用合法的舍入行为执行业务计算,这将非常方便。如果性能是最重要的,那么你不介意自己处理十进制小数点,而且数值不是太大,可以使用 int 或 long。如果数值不超过 9 位小数,可以使用 int;如果不超过 18 位,可以使用 long。如果数量可能超过 18 位,则使用 BigDecimal。</p><h3 id="61-基本数据类型优于包装类"><a class="markdownIt-Anchor" href="#61-基本数据类型优于包装类"></a> 61. 基本数据类型优于包装类</h3><p>Java 的类型系统有两部分,基本类型(如 int、double 和 boolean)和引用类型(如String和List)。每个基本类型都有一个对应的引用类型,称为包装类。如Integer、Double 和 Boolean</p><p>基本类型和包装类型之间的区别如下</p><ol><li>基本类型只有它们的值,而包装类型具有与其值不同的标识。换句话说,两个包装类型实例可以具有相同的值和不同的标识。</li><li>基本类型只有全部功能值,而每个包装类型除了对应的基本类型的所有功能值外,还有一个非功能值,即 null</li><li>基本类型比包装类型更节省时间和空间</li><li>用 == 来比较包装类型几乎都是错的</li><li>在操作中混用基本类型和包装类型时,包装类型会自动拆箱,可能导致 NPE,如果操作结果保存到包装类型的变量中,还会发生自动装箱,导致性能问题</li></ol><p>包装类型的用途如下:</p><ol><li>作为集合的元素、键和值</li><li>泛型和泛型方法中的类型参数</li><li>反射调用</li></ol><p>总之,只要有选择,就应该优先使用基本类型,而不是包装类型。基本类型更简单、更快。如果必须使用包装类型,请小心!自动装箱减少了使用包装类型的冗长,但没有减少危险。 当你的程序使用 == 操作符比较两个包装类型时,它会执行标识比较,这几乎肯定不是你想要的。当你的程序执行包含包装类型和基本类型的混合类型计算时,它将进行拆箱,当你的程序执行拆箱时,将抛出 NullPointerException。 最后,当你的程序将基本类型装箱时,可能会导致代价高昂且不必要的对象创建。</p><h3 id="62-其它类型更合适时应避免使用字符串"><a class="markdownIt-Anchor" href="#62-其它类型更合适时应避免使用字符串"></a> 62. 其它类型更合适时应避免使用字符串</h3><p>本条目讨论一些不应该使用字符串的场景</p><ol><li>字符串是枚举类型的糟糕替代品</li><li>字符串是聚合类型的糟糕替代品。如果一个对象有多个字段,将其连接成一个字符串会出现许多问题,更好的方法是用私有静态成员类表示聚合</li><li>字符串是功能的糟糕替代品。例如全局缓存池要求 key 不能重复,如果用字符串做 key 可能在不同线程中出现问题</li></ol><p>总之,当存在或可以编写更好的数据类型时,应避免将字符串用来表示对象。如果使用不当,字符串比其他类型更麻烦、灵活性更差、速度更慢、更容易出错。字符串经常被误用的类型包括基本类型、枚举和聚合类型。</p><h3 id="63-当心字符串连接引起的性能问题"><a class="markdownIt-Anchor" href="#63-当心字符串连接引起的性能问题"></a> 63. 当心字符串连接引起的性能问题</h3><p>字符串连接符(+)连接 n 个字符串的时间复杂度是 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mi>n</mi><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">n^2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8141079999999999em;vertical-align:0em;"></span><span class="mord"><span class="mord mathnormal">n</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141079999999999em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span>。这是字符串不可变导致的</p><p>如果要连接的字符串数量较多,可以使用 StringBuilder 代替 String</p><h3 id="64-通过接口引用对象"><a class="markdownIt-Anchor" href="#64-通过接口引用对象"></a> 64. 通过接口引用对象</h3><ol><li><p>如果存在合适的接口类型,那么应该使用接口类型声明参数、返回值、变量和字段。好处是代码可以非常方便切换性能更好或功能更丰富的实现,例如 HashMap 替换成 LinkedHashMap 可以保证迭代顺序和插入一致</p></li><li><p>如果没有合适的接口存在,那么用类引用对象是完全合适的。</p><ul><li>值类,如 String 和 BigInteger。值类很少在编写时考虑到多个实现。它们通常是 final 的,很少有相应的接口。使用这样的值类作为参数、变量、字段或返回类型非常合适。</li><li>如果一个对象属于一个基于类的框架,那么就用基类引用它</li><li>接口的实现类有接口没有的方法,例如 PriorityQueue 类有 Queue 接口没有的比较器方法</li></ul></li></ol><h3 id="65-接口优于反射"><a class="markdownIt-Anchor" href="#65-接口优于反射"></a> 65. 接口优于反射</h3><p>反射有几个缺点:</p><ol><li>失去了编译时类型检查的所有好处,包括异常检查。如果一个程序试图反射性地调用一个不存在的或不可访问的方法,它将在运行时失败,除非你采取了特殊的预防措施(大量 try-catch)</li><li>反射代码既笨拙又冗长。写起来很乏味,读起来也很困难</li><li>反射调用方法比普通调用方法更慢</li></ol><p>反射优点:</p><p>对于许多程序,它们必须用到在编译时无法获取的类,在编译时存在一个适当的接口或父类来引用该类(Item-64)。如果是这种情况,可以用反射方式创建实例,并通过它们的接口或父类正常地访问它们。</p><p>例如,这是一个创建 <code>Set<String></code> 实例的程序,类由第一个命令行参数指定。程序将剩余的命令行参数插入到集合中并打印出来。不管第一个参数是什么,程序都会打印剩余的参数,并去掉重复项。然而,打印这些参数的顺序取决于第一个参数中指定的类。如果你指定 java.util.HashSet,它们显然是随机排列的;如果你指定 java.util.TreeSet,它们是按字母顺序打印的,因为 TreeSet 中的元素是有序的</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Reflective instantiation with interface access</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> { <span class="hljs-comment">// Translate the class name into a Class object</span> Class<? <span class="hljs-keyword">extends</span> <span class="hljs-title class_">Set</span><String>> cl = <span class="hljs-literal">null</span>; <span class="hljs-keyword">try</span> { cl = (Class<? <span class="hljs-keyword">extends</span> <span class="hljs-title class_">Set</span><String>>) <span class="hljs-comment">// Unchecked cast!</span> Class.forName(args[<span class="hljs-number">0</span>]); } <span class="hljs-keyword">catch</span> (ClassNotFoundException e) { fatalError(<span class="hljs-string">"Class not found."</span>); } <span class="hljs-comment">// Get the constructor</span> Constructor<? <span class="hljs-keyword">extends</span> <span class="hljs-title class_">Set</span><String>> cons = <span class="hljs-literal">null</span>; <span class="hljs-keyword">try</span> { cons = cl.getDeclaredConstructor(); } <span class="hljs-keyword">catch</span> (NoSuchMethodException e) { fatalError(<span class="hljs-string">"No parameterless constructor"</span>); } <span class="hljs-comment">// Instantiate the set</span> Set<String> s = <span class="hljs-literal">null</span>; <span class="hljs-keyword">try</span> { s = cons.newInstance(); } <span class="hljs-keyword">catch</span> (IllegalAccessException e) { fatalError(<span class="hljs-string">"Constructor not accessible"</span>); } <span class="hljs-keyword">catch</span> (InstantiationException e) { fatalError(<span class="hljs-string">"Class not instantiable."</span>); } <span class="hljs-keyword">catch</span> (InvocationTargetException e) { fatalError(<span class="hljs-string">"Constructor threw "</span> + e.getCause()); } <span class="hljs-keyword">catch</span> (ClassCastException e) { fatalError(<span class="hljs-string">"Class doesn't implement Set"</span>); } <span class="hljs-comment">// Exercise the set</span> s.addAll(Arrays.asList(args).subList(<span class="hljs-number">1</span>, args.length)); System.out.println(s);}<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">fatalError</span><span class="hljs-params">(String msg)</span> { System.err.println(msg); System.exit(<span class="hljs-number">1</span>);}</code></pre><p>反射的合法用途(很少)是管理类对运行时可能不存在的其他类、方法或字段的依赖关系。如果你正在编写一个包,并且必须针对其他包的多个版本运行,此时反射将非常有用。该技术是根据支持包所需的最小环境(通常是最老的版本)编译包,并反射性地访问任何较新的类或方法。如果你试图访问的新类或方法在运行时不存在,要使此工作正常进行,则必须采取适当的操作。适当的操作可能包括使用一些替代方法来完成相同的目标,或者使用简化的功能进行操作</p><p>总之,反射是一种功能强大的工具,对于某些复杂的系统编程任务是必需的,但是它有很多缺点。如果编写的程序必须在编译时处理未知的类,则应该尽可能只使用反射实例化对象,并使用在编译时已知的接口或父类访问对象。</p><h3 id="66-明智地使用本地方法"><a class="markdownIt-Anchor" href="#66-明智地使用本地方法"></a> 66. 明智地使用本地方法</h3><p>JNI 允许 Java 调用本地方法,这些方法是用 C 或 C++ 等本地编程语言编写的。</p><p>历史上,本地方法主要有三个用途:</p><ol><li>提供对特定于平台的设施(如注册中心)的访问</li><li>提供对现有本地代码库的访问,包括提供对遗留数据访问</li><li>通过本地语言编写应用程序中注重性能的部分,以提高性能</li></ol><p>关于用途一:随着 Java 平台的成熟,它提供了对许多以前只能在宿主平台中上找到的特性。例如,Java 9 中添加的流 API 提供了对 OS 进程的访问。在 Java 中没有等效库时,使用本地方法来使用本地库也是合法的。</p><p>关于用途3:在早期版本(Java 3 之前),这通常是必要的,但是从那时起 JVM 变得更快了。对于大多数任务,现在可以在 Java 中获得类似的性能</p><p>本地方法的缺点:</p><ol><li>会受到内存损坏错误的影响</li><li>垃圾收集器无法自动追踪本地方法的内存使用情况,导致性能下降</li></ol><p>总之,在使用本地方法之前要三思。一般很少需要使用它们来提高性能。如果必须使用本地方法来访问底层资源或本地库,请尽可能少地使用本地代码,并对其进行彻底的测试。本地代码中的一个错误就可以破坏整个应用程序。</p><h3 id="67-明智地进行优化"><a class="markdownIt-Anchor" href="#67-明智地进行优化"></a> 67. 明智地进行优化</h3><p>关于优化的三条名言</p><ol><li>以效率的名义(不一定能达到效率)犯下的计算错误比任何其他原因都要多——包括盲目的愚蠢</li><li>不要去计较效率上的一些小小的得失,在 97% 的情况下,不成熟的优化才是一切问题的根源。</li><li>在优化方面,我们应该遵守两条规则:规则 1:不要进行优化。规则 2 (仅针对专家):还是不要进行优化,也就是说,在你还没有绝对清晰的未优化方案之前,请不要进行优化。</li></ol><p>在设计系统时,我们要仔细考虑架构,好的架构允许它在后面优化,不良的架构导致很难优化。设计中最难更改的是组件之间以及组件与外部世界交互的组件,主要是 API、线路层协议和数据持久化格式,尽量避免做限制性能的设计</p><p>JMH 是一个微基准测试框架,主要是基于方法层面的基准测试,精度可以达到纳秒级</p><p>总而言之,不要努力写快的程序,要努力写好程序;速度自然会提高。但是在设计系统时一定要考虑性能,特别是在设计API、线路层协议和持久数据格式时。当你完成了系统的构建之后,请度量它的性能。如果足够快,就完成了。如果没有,利用分析器找到问题的根源,并对系统的相关部分进行优化。第一步是检查算法的选择:再多的底层优化也不能弥补算法选择的不足。根据需要重复这个过程,在每次更改之后测量性能,直到你满意为止。</p><h3 id="68-遵守被广泛认可的命名约定"><a class="markdownIt-Anchor" href="#68-遵守被广泛认可的命名约定"></a> 68. 遵守被广泛认可的命名约定</h3><p>Java 平台有一组完善的命名约定,其中许多约定包含在《The Java Language Specification》。不严格地讲,命名约定分为两类:排版和语法。</p><h4 id="排版"><a class="markdownIt-Anchor" href="#排版"></a> 排版</h4><p>和排版有关的命名约定,包括包、类、接口、方法、字段和类型变量</p><ol><li>包名和模块名应该是分层的,组件之间用句点分隔。组件应该由小写字母组成,很少使用数字。任何在你的组织外部使用的包,名称都应该以你的组织的 Internet 域名开头,并将组件颠倒过来,例如,edu.cmu、com.google、org.eff。以 java 和 javax 开头的标准库和可选包是这个规则的例外。用户不能创建名称以 java 或 javax 开头的包或模块。<br />包名的其余部分应该由描述包的一个或多个组件组成。组件应该很短,通常为 8 个或更少的字符。鼓励使用有意义的缩写,例如 util 而不是 utilities。缩写词是可以接受的,例如 awt。组件通常应该由一个单词或缩写组成。</li><li>类和接口名称,包括枚举和注释类型名称,应该由一个或多个单词组成,每个单词的首字母大写,例如 List 或 FutureTask</li><li>方法和字段名遵循与类和接口名相同的排版约定,除了方法或字段名的第一个字母应该是小写,例如 remove 或 ensureCapacity</li><li>前面规则的唯一例外是「常量字段」,它的名称应该由一个或多个大写单词组成,由下划线分隔,例如 VALUES 或 NEGATIVE_INFINITY</li><li>局部变量名与成员名具有类似的排版命名约定,但允许使用缩写,也允许使用单个字符和短字符序列,它们的含义取决于它们出现的上下文,例如 i、denom、houseNum。输入参数是一种特殊的局部变量。它们的命名应该比普通的局部变量谨慎得多,因为它们的名称是方法文档的组成部分。</li><li>类型参数名通常由单个字母组成。最常见的是以下五种类型之一:T 表示任意类型,E 表示集合的元素类型,K 和 V 表示 Map 的键和值类型,X 表示异常。函数的返回类型通常为 R。任意类型的序列可以是 T、U、V 或 T1、T2、T3。</li></ol><p>为了快速参考,下表显示了排版约定的示例。</p><table><thead><tr><th style="text-align:center">Identifier Type</th><th style="text-align:center">Example</th></tr></thead><tbody><tr><td style="text-align:center">Package or module</td><td style="text-align:center"><code>org.junit.jupiter.api</code>, <code>com.google.common.collect</code></td></tr><tr><td style="text-align:center">Class or Interface</td><td style="text-align:center">Stream, FutureTask, LinkedHashMap,HttpClient</td></tr><tr><td style="text-align:center">Method or Field</td><td style="text-align:center">remove, groupingBy, getCrc</td></tr><tr><td style="text-align:center">Constant Field</td><td style="text-align:center">MIN_VALUE, NEGATIVE_INFINITY</td></tr><tr><td style="text-align:center">Local Variable</td><td style="text-align:center">i, denom, houseNum</td></tr><tr><td style="text-align:center">Type Parameter</td><td style="text-align:center">T, E, K, V, X, R, U, V, T1, T2</td></tr></tbody></table><h4 id="语法"><a class="markdownIt-Anchor" href="#语法"></a> 语法</h4><p>语法命名约定比排版约定更灵活,也更有争议</p><ol><li>可实例化的类,包括枚举类型,通常使用一个或多个名词来命名,例如 Thread、PriorityQueue 或 ChessPiece</li><li>不可实例化的工具类通常用复数名词来命名,例如 Collectors 和 Collections</li><li>接口的名称类似于类,例如 Collection 或 Comparator,或者以 able 或 ible 结尾的形容词,例如 Runnable、Iterable 或 Accessible</li><li>因为注解类型有很多的用途,所以没有哪部分占主导地位。名词、动词、介词和形容词都很常见,例如,BindingAnnotation、Inject、ImplementedBy 或 Singleton。</li><li>执行某些操作的方法通常用动词或动词短语(包括对象)命名,例如,append 或 drawImage。</li><li>返回布尔值的方法的名称通常以单词 is 或 has(通常很少用)开头,后面跟一个名词、一个名词短语,或者任何用作形容词的单词或短语,例如 isDigit、isProbablePrime、isEmpty、isEnabled 或 hasSiblings。</li><li>返回被调用对象的非布尔函数或属性的方法通常使用以 get 开头的名词、名词短语或动词短语来命名,例如 size、hashCode 或 getTime。有一种说法是,只有第三种形式(以 get 开头)才是可接受的,但这种说法几乎没有根据。前两种形式的代码通常可读性更强 。以 get 开头的形式起源于基本过时的 Java bean 规范,该规范构成了早期可复用组件体系结构的基础。有一些现代工具仍然依赖于 bean 命名约定,你应该可以在任何与这些工具一起使用的代码中随意使用它。如果类同时包含相同属性的 setter 和 getter,则遵循这种命名约定也有很好的先例。在本例中,这两个方法通常被命名为 getAttribute 和 setAttribute。</li><li>转换对象类型(返回不同类型的独立对象)的实例方法通常称为 toType,例如 toString 或 toArray。</li><li>返回与接收对象类型不同的视图(Item-6)的方法通常称为 asType,例如 asList</li><li>返回与调用它们的对象具有相同值的基本类型的方法通常称为类型值,例如 intValue</li><li>静态工厂的常见名称包括 from、of、valueOf、instance、getInstance、newInstance、getType 和 newType</li><li>字段名的语法约定没有类、接口和方法名的语法约定建立得好,也不那么重要,因为设计良好的 API 包含很少的公开字段。类型为 boolean 的字段的名称通常类似于 boolean 访问器方法,省略了开头「is」,例如 initialized、composite。其他类型的字段通常用名词或名词短语来命名,如 height、digits 和 bodyStyle。局部变量的语法约定类似于字段的语法约定,但要求更少。</li></ol>]]></content>
<summary type="html"><h2 id="第九章-通用程序设计"><a class="markdownIt-Anchor" href="#第九章-通用程序设计"></a> 第九章 通用程序设计</h2>
<h3 id="57-将局部变量的作用域最小化"><a class="markdownIt-Ancho</summary>
<category term="Java" scheme="https://zunpan.github.io/categories/Java/"/>
<category term="Effective-Java" scheme="https://zunpan.github.io/tags/Effective-Java/"/>
<category term="通用程序设计" scheme="https://zunpan.github.io/tags/%E9%80%9A%E7%94%A8%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1/"/>
</entry>
<entry>
<title>Effective-Java学习笔记(七)</title>
<link href="https://zunpan.github.io/2022/08/01/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%83%EF%BC%89/"/>
<id>https://zunpan.github.io/2022/08/01/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%83%EF%BC%89/</id>
<published>2022-08-01T11:27:09.000Z</published>
<updated>2023-09-24T04:27:40.273Z</updated>
<content type="html"><![CDATA[<h2 id="第八章-方法"><a class="markdownIt-Anchor" href="#第八章-方法"></a> 第八章 方法</h2><h3 id="49-检查参数的有效性"><a class="markdownIt-Anchor" href="#49-检查参数的有效性"></a> 49. 检查参数的有效性</h3><p>大多数方法和构造函数都对入参有一些限制,例如非空非负,你应该在方法主体的开头检查参数并在Javadoc中用@throws记录参数限制</p><p>Java7 添加了 Objects.requireNonNull 方法执行空检查</p><p>Java9 在 Objects 中添加了范围检查:checkFromIndexSize、checkFromToIndex 和 checkIndex</p><p>对于未导出的方法,作为包的作者,你应该定制方法调用的环境,确保只传递有效的参数值。因此,非 public 方法可以使用断言检查参数</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Private helper function for a recursive sort</span><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">sort</span><span class="hljs-params">(<span class="hljs-type">long</span> a[], <span class="hljs-type">int</span> offset, <span class="hljs-type">int</span> length)</span> { <span class="hljs-keyword">assert</span> a != <span class="hljs-literal">null</span>; <span class="hljs-keyword">assert</span> offset >= <span class="hljs-number">0</span> && offset <= a.length; <span class="hljs-keyword">assert</span> length >= <span class="hljs-number">0</span> && length <= a.length - offset; ... <span class="hljs-comment">// Do the computation</span>}</code></pre><p>断言只适合用来调试,可以关闭可以打开,public 方法一定要显示检查参数并抛出异常,断言失败只会抛出AssertionError,不利于定位错误</p><p>总而言之,每次编写方法或构造函数时,都应该考虑参数存在哪些限制。你应该在文档中记录这些限制,并在方法主体的开头显式地检查。</p><h3 id="50-在需要时制作防御性副本"><a class="markdownIt-Anchor" href="#50-在需要时制作防御性副本"></a> 50. 在需要时制作防御性副本</h3><p>Java 是一种安全的语言,这是它的一大优点。这意味着在没有 native 方法的情况下,它不受缓冲区溢出、数组溢出、非法指针和其他内存损坏错误的影响,这些错误困扰着 C 和 C++ 等不安全语言。在一种安全的语言中,可以编写一个类并确定它们的不变量将保持不变,而不管在系统的任何其他部分发生了什么。在将所有内存视为一个巨大数组的语言中,这是不可能的。</p><p>即使使用一种安全的语言,如果你不付出一些努力,也无法与其他类隔离。你必须进行防御性的设计,并假定你的类的客户端会尽最大努力破坏它的不变量。</p><h4 id="通过可变参数破坏类的不变量"><a class="markdownIt-Anchor" href="#通过可变参数破坏类的不变量"></a> 通过可变参数破坏类的不变量</h4><p>虽然如果没有对象的帮助,另一个类是不可能修改对象的内部状态的,但是要提供这样的帮助却出奇地容易。例如,考虑下面的类,它表示一个不可变的时间段:</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Broken "immutable" time period class</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">Period</span> { <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> Date start; <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> Date end; <span class="hljs-comment">/** * <span class="hljs-doctag">@param</span> start the beginning of the period * <span class="hljs-doctag">@param</span> end the end of the period; must not precede start * <span class="hljs-doctag">@throws</span> IllegalArgumentException if start is after end * <span class="hljs-doctag">@throws</span> NullPointerException if start or end is null */</span> <span class="hljs-keyword">public</span> <span class="hljs-title function_">Period</span><span class="hljs-params">(Date start, Date end)</span> { <span class="hljs-keyword">if</span> (start.compareTo(end) > <span class="hljs-number">0</span>) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">IllegalArgumentException</span>(start + <span class="hljs-string">" after "</span> + end); <span class="hljs-built_in">this</span>.start = start; <span class="hljs-built_in">this</span>.end = end; } <span class="hljs-keyword">public</span> Date <span class="hljs-title function_">start</span><span class="hljs-params">()</span> { <span class="hljs-keyword">return</span> start; } <span class="hljs-keyword">public</span> Date <span class="hljs-title function_">end</span><span class="hljs-params">()</span> { <span class="hljs-keyword">return</span> end; } ... <span class="hljs-comment">// Remainder omitted</span>}</code></pre><p>乍一看,这个类似乎是不可变的,并且要求一个时间段的开始时间不能在结束时间之后。然而,利用 Date 是可变的这一事实很容易绕过这个约束:</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Attack the internals of a Period instance</span><span class="hljs-type">Date</span> <span class="hljs-variable">start</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>();<span class="hljs-type">Date</span> <span class="hljs-variable">end</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>();<span class="hljs-type">Period</span> <span class="hljs-variable">p</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Period</span>(start, end);end.setYear(<span class="hljs-number">78</span>); <span class="hljs-comment">// Modifies internals of p!</span></code></pre><p>从 Java8 开始,解决这个问题可以用 Instant 或 LocalDateTime 或 ZonedDateTime 来代替 Date,因为它们是不可变类。</p><h4 id="防御性副本"><a class="markdownIt-Anchor" href="#防御性副本"></a> 防御性副本</h4><p>但是有时必须在 API 和内部表示中使用可变值类型,这时候可以对可变参数进行防御性副本而不是使用原始可变参数。对上面的 Period 类改进如下</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Repaired constructor - makes defensive copies of parameters</span><span class="hljs-keyword">public</span> <span class="hljs-title function_">Period</span><span class="hljs-params">(Date start, Date end)</span> { <span class="hljs-built_in">this</span>.start = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>(start.getTime()); <span class="hljs-built_in">this</span>.end = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>(end.getTime()); <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.start.compareTo(<span class="hljs-built_in">this</span>.end) > <span class="hljs-number">0</span>) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">IllegalArgumentException</span>(<span class="hljs-built_in">this</span>.start + <span class="hljs-string">" after "</span> + <span class="hljs-built_in">this</span>.end);}</code></pre><ol><li>先进行防御性复制,再在副本上检查参数,保证在检查参数和复制参数之间的空窗期,类不受其他线程更改参数的影响,这个攻击也叫time-of-check/time-of-use 或 TOCTOU 攻击</li><li>对可被不受信任方子类化的参数类型,不要使用 clone 方法进行防御性复制。</li><li>访问器也要对可变字段进行防御性复制</li><li>如果类信任它的调用者不会破坏不变量,比如类和调用者都是同一个包下,那么应该避免防御性复制</li><li>当类的作用就是修改可变参数时不用防御性复制,客户端承诺不直接修改对象</li><li>破坏不变量只会对损害客户端时不用防御性复制,例如包装类模式,客户端在包装对象之后可以直接访问对象,破坏类的不变量,但这通常只会损害客户端</li></ol><p>总而言之,如果一个类具有从客户端获取或返回给客户端的可变组件,则该类必须防御性地复制这些组件。如果复制的成本过高,并且类信任它的客户端不会不适当地修改组件,那么可以不进行防御性的复制,取而代之的是在文档中指明客户端的职责是不得修改受到影响的组件。</p><h3 id="51-仔细设计方法签名"><a class="markdownIt-Anchor" href="#51-仔细设计方法签名"></a> 51. 仔细设计方法签名</h3><ol><li>仔细选择方法名称。目标是选择可理解的、与同一包中其它名称风格一致的名称;选择广泛认可的名字;避免长方法名</li><li>不要提供过于便利的方法。每种方法都应该各司其职。太多的方法使得类难以学习、使用、记录、测试和维护。对于接口来说更是如此,在接口中,太多的方法使实现者和用户的工作变得复杂。对于类或接口支持的每个操作,请提供一个功能齐全的方法。</li><li>避免长参数列表。可以通过分解方法减少参数数量;也可以通过静态成员类 helper 类来存参数;也可以从对象构建到方法调用都采用建造者模式</li><li>参数类型优先选择接口而不是类</li><li>双元素枚举类型优于 boolean 参数。枚举比 boolean 可读性强且可以添加更多选项</li></ol><h3 id="52-明智地使用重载"><a class="markdownIt-Anchor" href="#52-明智地使用重载"></a> 52. 明智地使用重载</h3><p>考虑下面使用了重载的代码:</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Broken! - What does this program print?</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">CollectionClassifier</span> { <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> String <span class="hljs-title function_">classify</span><span class="hljs-params">(Set<?> s)</span> { <span class="hljs-keyword">return</span> <span class="hljs-string">"Set"</span>; } <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> String <span class="hljs-title function_">classify</span><span class="hljs-params">(List<?> lst)</span> { <span class="hljs-keyword">return</span> <span class="hljs-string">"List"</span>; } <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> String <span class="hljs-title function_">classify</span><span class="hljs-params">(Collection<?> c)</span> { <span class="hljs-keyword">return</span> <span class="hljs-string">"Unknown Collection"</span>; } <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> { Collection<?>[] collections = { <span class="hljs-keyword">new</span> <span class="hljs-title class_">HashSet</span><String>(),<span class="hljs-keyword">new</span> <span class="hljs-title class_">ArrayList</span><BigInteger>(),<span class="hljs-keyword">new</span> <span class="hljs-title class_">HashMap</span><String, String>().values() }; <span class="hljs-keyword">for</span> (Collection<?> c : collections) System.out.println(classify(c)); }}</code></pre><p>这段代码打印了三次 Unknown Collection。因为 classify 方法被重载,并且在编译时就决定了要调用哪个重载,编译时是 Collections<?> 类型,所以调用的就是第三个重载方法</p><h3 id="重载vs覆盖"><a class="markdownIt-Anchor" href="#重载vs覆盖"></a> 重载VS覆盖</h3><p>重载方法的选择是静态的,在编译时决定要调用哪个重载方法。而覆盖方法的选择是动态的, 在运行时根据调用方法的对象的运行时类型选择覆盖方法的正确版本</p><p>因为覆盖是常态,而重载是例外,所以覆盖满足了人们对方法调用行为的期望,重载很容易混淆这些期望。</p><h4 id="重载"><a class="markdownIt-Anchor" href="#重载"></a> 重载</h4><ol><li>安全、保守的策略是永远不导出具有相同数量参数的两个重载。你可以为方法提供不同的名称而不是重载它们。</li><li>构造函数只能重载,我们可以用静态工厂代替构造函数</li><li>不要在重载方法的相同参数位置上使用不同的函数式接口。不同的函数式接口并没有本质的不同</li></ol><p>总而言之,方法可以重载,但并不意味着就应该这样做。通常,最好避免重载具有相同数量参数的多个签名的方法。在某些情况下,特别是涉及构造函数的情况下,可能难以遵循这个建议。在这些情况下,你至少应该避免同一组参数只需经过类型转换就可以被传递给不同的重载方法。如果这是无法避免的,例如,因为要对现有类进行改造以实现新接口,那么应该确保在传递相同的参数时,所有重载的行为都是相同的。如果你做不到这一点,程序员将很难有效地使用重载方法或构造函数,他们将无法理解为什么它不能工作。</p><h3 id="53-明智地使用可变参数"><a class="markdownIt-Anchor" href="#53-明智地使用可变参数"></a> 53. 明智地使用可变参数</h3><p>可变参数首先创建一个数组,其大小是在调用点上传递的参数数量,然后将参数值放入数组,最后将数组传递给方法。</p><p>当你需要定义具有不确定数量参数的方法时,可变参数是非常有用的。在可变参数之前加上任何必需的参数,并注意使用可变参数可能会引发的性能后果。</p><h3 id="54-返回空集合或数组而不是-null"><a class="markdownIt-Anchor" href="#54-返回空集合或数组而不是-null"></a> 54. 返回空集合或数组,而不是 null</h3><p>在方法中用空集合或空数组代替 null 返回可以让客户端不用显示判空</p><h3 id="55-明智地返回-optional"><a class="markdownIt-Anchor" href="#55-明智地返回-optional"></a> 55. 明智地返回 Optional</h3><p>在 Java8 之前,方法可能无法 return 时有两种处理方法,一种是抛异常,一种是 返回 null。抛异常代价高,返回 null 需要客户端显示判空</p><p>Java8 添加了第三种方法来处理可能无法返回值的方法。<code>Optional<T></code> 类表示一个不可变的容器,它可以包含一个非空的 T 引用,也可以什么都不包含。不包含任何内容的 Optional 被称为空。一个值被认为存在于一个非空的 Optional 中。Optional 的本质上是一个不可变的集合,它最多可以容纳一个元素。</p><p>理论上应返回 T,但在某些情况下可能无法返回 T 的方法可以将返回值声明为 <code>Optional<T></code>。这允许该方法返回一个空结果来表明它不能返回有效的结果。具备 Optional 返回值的方法比抛出异常的方法更灵活、更容易使用,并且比返回 null 的方法更不容易出错。</p><p>Item-30 有一个求集合最大值方法</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Returns maximum value in collection - throws exception if empty</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <E <span class="hljs-keyword">extends</span> <span class="hljs-title class_">Comparable</span><E>> E <span class="hljs-title function_">max</span><span class="hljs-params">(Collection<E> c)</span> { <span class="hljs-keyword">if</span> (c.isEmpty()) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">IllegalArgumentException</span>(<span class="hljs-string">"Empty collection"</span>); <span class="hljs-type">E</span> <span class="hljs-variable">result</span> <span class="hljs-operator">=</span> <span class="hljs-literal">null</span>; <span class="hljs-keyword">for</span> (E e : c) <span class="hljs-keyword">if</span> (result == <span class="hljs-literal">null</span> || e.compareTo(result) > <span class="hljs-number">0</span>) result = Objects.requireNonNull(e); <span class="hljs-keyword">return</span> result;}</code></pre><p>当入参集合为空时,这个方法会抛出 IllegalArgumentException。更好的方式是返回 Optional</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Returns maximum value in collection as an Optional<E></span><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <E <span class="hljs-keyword">extends</span> <span class="hljs-title class_">Comparable</span><E>> Optional<E> <span class="hljs-title function_">max</span><span class="hljs-params">(Collection<E> c)</span> { <span class="hljs-keyword">if</span> (c.isEmpty()) <span class="hljs-keyword">return</span> Optional.empty(); <span class="hljs-type">E</span> <span class="hljs-variable">result</span> <span class="hljs-operator">=</span> <span class="hljs-literal">null</span>; <span class="hljs-keyword">for</span> (E e : c) <span class="hljs-keyword">if</span> (result == <span class="hljs-literal">null</span> || e.compareTo(result) > <span class="hljs-number">0</span>) result = Objects.requireNonNull(e); <span class="hljs-keyword">return</span> Optional.of(result);}</code></pre><p><code>Optional.empty()</code> 返回一个空的 Optional,<code>Optional.of(value)</code> 返回一个包含给定非空值的 Optional。将 null 传递给 <code>Optional.of(value)</code> 是一个编程错误。如果你这样做,该方法将通过抛出 NullPointerException 来响应。<code>Optional.ofNullable(value)</code> 方法接受一个可能为空的值,如果传入 null,则返回一个空的 Optional。<strong>永远不要让返回 optional 的方法返回 null :</strong> 它违背了这个功能的设计初衷。</p><p>为什么选择返回 Optional 而不是返回 null 或抛出异常?Optional 在本质上类似于受检查异常(Item-71),因为它们迫使 API 的用户面对可能没有返回值的事实。抛出不受检查的异常或返回 null 允许用户忽略这种可能性,抛出受检查异常会让客户端添加额外代码</p><p>如果一个方法返回一个 Optional,客户端可以选择如果该方法不能返回值该采取什么操作。你可以使用 orElse 方法指定一个默认值,或者使用 orElseGet 方法在必要时生成默认值;也可以使用 orElseThrow 方法抛出异常</p><p>关于 Optional 用法的一些Tips:</p><ol><li>isPresent 方法可以判断 Optional中有没有值,谨慎使用 isPrensent 方法,它的许多用途可以用上面的方法代替</li><li>并不是所有的返回类型都能从 Optional 处理中获益。容器类型,包括集合、Map、流、数组和 Optional,不应该封装在 Optional 中。 你应该简单的返回一个空的 <code>List<T></code>,而不是一个空的 <code>Optional<List<T>></code></li><li>永远不应该返回包装类的 Optional,除了「次基本数据类型」,如 Boolean、Byte、Character、Short 和 Float 之外</li><li>在集合或数组中使用 Optional 作为键、值或元素几乎都是不合适的。</li></ol><p>总之,如果你发现自己编写的方法不能总是返回确定值,并且你认为该方法的用户在每次调用时应该考虑这种可能性,那么你可能应该让方法返回一个 Optional。但是,你应该意识到,返回 Optional 会带来实际的性能后果;对于性能关键的方法,最好返回 null 或抛出异常。最后,除了作为返回值之外,你几乎不应该以任何其他方式使用 Optional。</p><h3 id="56-为所有公开的-api-元素编写文档注释"><a class="markdownIt-Anchor" href="#56-为所有公开的-api-元素编写文档注释"></a> 56. 为所有公开的 API 元素编写文档注释</h3>]]></content>
<summary type="html"><h2 id="第八章-方法"><a class="markdownIt-Anchor" href="#第八章-方法"></a> 第八章 方法</h2>
<h3 id="49-检查参数的有效性"><a class="markdownIt-Anchor" href="#49-检查参</summary>
<category term="Java" scheme="https://zunpan.github.io/categories/Java/"/>
<category term="Effective-Java" scheme="https://zunpan.github.io/tags/Effective-Java/"/>
<category term="方法" scheme="https://zunpan.github.io/tags/%E6%96%B9%E6%B3%95/"/>
</entry>
<entry>
<title>Effective-Java学习笔记(六)</title>
<link href="https://zunpan.github.io/2022/07/30/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E5%85%AD%EF%BC%89/"/>
<id>https://zunpan.github.io/2022/07/30/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E5%85%AD%EF%BC%89/</id>
<published>2022-07-30T06:30:09.000Z</published>
<updated>2023-09-24T04:27:40.276Z</updated>
<content type="html"><![CDATA[<h2 id="第七章-λ-表达式和流"><a class="markdownIt-Anchor" href="#第七章-λ-表达式和流"></a> 第七章 λ 表达式和流</h2><h3 id="42-λ-表达式优于匿名类"><a class="markdownIt-Anchor" href="#42-λ-表达式优于匿名类"></a> 42. λ 表达式优于匿名类</h3><h4 id="函数对象"><a class="markdownIt-Anchor" href="#函数对象"></a> 函数对象</h4><p>在历史上,带有单个抽象方法的接口(或者抽象类,但这种情况很少)被用作函数类型。它们的实例(称为函数对象)表示函数或操作。自从 JDK 1.1 在 1997 年发布以来,创建函数对象的主要方法就是匿名类(Item-24)。下面是一个按长度对字符串列表进行排序的代码片段,使用一个匿名类来创建排序的比较函数(它强制执行排序顺序):</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Anonymous class instance as a function object - obsolete!</span>Collections.sort(words, <span class="hljs-keyword">new</span> <span class="hljs-title class_">Comparator</span><String>() { <span class="hljs-keyword">public</span> <span class="hljs-type">int</span> <span class="hljs-title function_">compare</span><span class="hljs-params">(String s1, String s2)</span> { <span class="hljs-keyword">return</span> Integer.compare(s1.length(), s2.length()); }});</code></pre><h4 id="函数式接口"><a class="markdownIt-Anchor" href="#函数式接口"></a> 函数式接口</h4><p>在 Java 8 中官方化了一个概念,即具有单个抽象方法的接口是特殊的,应该得到特殊处理。这些接口现在被称为函数式接口</p><h4 id="lambda-表达式"><a class="markdownIt-Anchor" href="#lambda-表达式"></a> lambda 表达式</h4><p>可以使用 lambda 表达式为函数式接口创建实例,lambda 表达式在功能上类似于匿名类,但更简洁</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Lambda expression as function object (replaces anonymous class)</span>Collections.sort(words,(s1, s2) -> Integer.compare(s1.length(), s2.length()));</code></pre><ol><li>lambda 表达式的入参和返回值类型一般不写,由编译器做类型推断,类型能不写就不写。<br />Item26告诉你不要用原始类型,Item29,30告诉你要用泛型和泛型方法,编译器从泛型中获得了大部分类型推断所需的类型信息</li><li>lambda 表达式缺少名称和文档;如果一个算法较复杂,或者有很多行代码,不要把它放在 lambda 表达式中。 一行是理想的,三行是合理的最大值</li><li>lambda 表达式仅限于函数式接口。如果想创建抽象类实例或者多个抽象方法的接口,只能用匿名类</li><li>lambda 表达式编译后会成为外部类的一个私有方法,而匿名类会生成单独的class文件,所以 lambda 表达式的this是外部类实例,匿名类的this是匿名类实例</li><li>lambda表达式和匿名类都不能可靠的序列化,如果想序列化函数对象,可以用私有静态嵌套类</li></ol><h3 id="43-方法引用优于-λ-表达式"><a class="markdownIt-Anchor" href="#43-方法引用优于-λ-表达式"></a> 43. 方法引用优于 λ 表达式</h3><p>Java 提供了一种比 lambda 表达式更简洁的方法来生成函数对象:方法引用。下面这段代码的功能是,如果数字 1 不在映射中,则将其与键关联,如果键已经存在,则将关联值递增:</p><pre class="highlight"><code class="java">map.merge(key, <span class="hljs-number">1</span>, (count, incr) -> count + incr);</code></pre><p>上面代码的 lambda 表达式作用是返回两个入参的和,在 Java 8 中,Integer(和所有其它基本类型的包装类)提供了一个静态方法 sum,它的作用完全相同,我们可以传入一个方法引用,并得到相同结果,同时减少视觉混乱:</p><pre class="highlight"><code class="java">map.merge(key, <span class="hljs-number">1</span>, Integer::sum);</code></pre><p>函数对象的参数越多,方法引用就显得越简洁,但是 lambda 表达式指明了参数名,使得 lambda表达式比方法引用更容易阅读和维护,没有什么是方法引用能做而 lambda 表达式做不了的</p><p>许多方法引用引用静态方法,但是有四种方法不引用静态方法。其中两个是绑定和非绑定实例方法引用。在绑定引用中,接收对象在方法引用中指定。绑定引用在本质上与静态引用相似:函数对象接受与引用方法相同的参数。在未绑定引用中,在应用函数对象时通过方法声明参数之前的附加参数指定接收对象。在流管道中,未绑定引用通常用作映射和筛选函数(Item-45)。最后,对于类和数组,有两种构造函数引用。构造函数引用用作工厂对象。五种方法参考文献汇总如下表:</p><table><thead><tr><th style="text-align:center">Method Ref Type</th><th style="text-align:center">Example</th><th style="text-align:center">Lambda Equivalent</th></tr></thead><tbody><tr><td style="text-align:center">Static</td><td style="text-align:center"><code>Integer::parseInt</code></td><td style="text-align:center"><code>str -></code></td></tr><tr><td style="text-align:center">Bound</td><td style="text-align:center"><code>Instant.now()::isAfter</code></td><td style="text-align:center"><code>Instant then =Instant.now(); t ->then.isAfter(t)</code></td></tr><tr><td style="text-align:center">Unbound</td><td style="text-align:center"><code>String::toLowerCase</code></td><td style="text-align:center"><code>str ->str.toLowerCase()</code></td></tr><tr><td style="text-align:center">Class Constructor</td><td style="text-align:center"><code>TreeMap<K,V>::new</code></td><td style="text-align:center"><code>() -> new TreeMap<K,V></code></td></tr><tr><td style="text-align:center">Array Constructor</td><td style="text-align:center"><code>int[]::new</code></td><td style="text-align:center"><code>len -> new int[len]</code></td></tr></tbody></table><p>总之,方法引用通常为 lambda 表达式提供了一种更简洁的选择。如果方法引用更短、更清晰,则使用它们;如果没有,仍然使用 lambda 表达式。</p><h3 id="44-优先使用标准函数式接口"><a class="markdownIt-Anchor" href="#44-优先使用标准函数式接口"></a> 44. 优先使用标准函数式接口</h3><p>java.util.function 包提供了大量的标准函数接口。优先使用标准函数式接口而不是自己写。 通过减少 API ,使得你的 API 更容易学习,并将提供显著的互操作性优势,因为许多标准函数式接口提供了有用的默认方法</p><p>java.util.function 中有 43 个接口。不能期望你记住所有的接口,但是如果你记住了 6 个基本接口,那么你可以在需要时派生出其余的接口。基本接口操作对象引用类型。Operator 接口表示结果和参数类型相同的函数。Predicate 接口表示接受参数并返回布尔值的函数。Function 接口表示参数和返回类型不同的函数。Supplier 接口表示一个无参并返回(或「供应」)值的函数。最后,Consumer 表示一个函数,该函数接受一个参数,但不返回任何内容,本质上是使用它的参数。六个基本的函数式接口总结如下:</p><table><thead><tr><th style="text-align:center">Interface</th><th style="text-align:center">Function Signature</th><th style="text-align:center">Example</th></tr></thead><tbody><tr><td style="text-align:center"><code>UnaryOperator<T></code></td><td style="text-align:center"><code>T apply(T t)</code></td><td style="text-align:center"><code>String::toLowerCase</code></td></tr><tr><td style="text-align:center"><code>BinaryOperator<T></code></td><td style="text-align:center"><code>T apply(T t1, T t2)</code></td><td style="text-align:center"><code>BigInteger::add</code></td></tr><tr><td style="text-align:center"><code>Predicate<T></code></td><td style="text-align:center"><code>boolean test(T t)</code></td><td style="text-align:center"><code>Collection::isEmpty</code></td></tr><tr><td style="text-align:center"><code>Function<T,R></code></td><td style="text-align:center"><code>R apply(T t)</code></td><td style="text-align:center"><code>Arrays::asList</code></td></tr><tr><td style="text-align:center"><code>Supplier<T></code></td><td style="text-align:center"><code>T get()</code></td><td style="text-align:center"><code>Instant::now</code></td></tr><tr><td style="text-align:center"><code>Consumer<T></code></td><td style="text-align:center"><code>void accept(T t)</code></td><td style="text-align:center"><code>System.out::println</code></td></tr></tbody></table><p>还有 6 个基本接口的 3 个变体,用于操作基本类型 int、long 和 double。它们的名称是通过在基本接口前面加上基本类型前缀而派生出来的。例如,一个接受 int 的 Predicate 就是一个 IntPredicate,一个接受两个 long 值并返回一个 long 的二元操作符就是一个 LongBinaryOperator。没有变体类型是参数化的除了由返回类型参数化的 Function 变体外。例如,<code>LongFunction<int[]></code> 使用 long 并返回一个 int[]。</p><p>Function 接口还有 9 个额外的变体,在结果类型为基本数据类型时使用。源类型和结果类型总是不同的,因为入参只有一个且与出参类型相同的函数本身都是 UnaryOperator。如果源类型和结果类型都是基本数据类型,则使用带有 SrcToResult 的前缀函数,例如 LongToIntFunction(六个变体)。如果源是一个基本数据类型,而结果是一个对象引用,则使用带前缀 SrcToObj 的 Function 接口,例如 DoubleToObjFunction(三个变体)。</p><p>三个基本函数式接口有两个参数版本,使用它们是有意义的:<code>BiPredicate<T,U></code>、<code>BiFunction<T,U,R></code>、<code>BiConsumer<T,U></code>。也有 BiFunction 变体返回三个相关的基本类型:<code>ToIntBiFunction<T,U></code>、 <code>ToLongBiFunction<T,U></code>、<code>ToDoubleBiFunction<T,U></code>。Consumer 有两个参数变体,它们接受一个对象引用和一个基本类型:<code>ObjDoubleConsumer<T></code>、<code>ObjIntConsumer<T></code>、<code>ObjLongConsumer<T></code>。总共有9个基本接口的双参数版本。</p><p>最后是 BooleanSupplier 接口,它是 Supplier 的一个变体,返回布尔值。这是在任何标准函数接口名称中唯一显式提到布尔类型的地方,但是通过 Predicate 及其四种变体形式支持布尔返回值。前面描述的 BooleanSupplier 接口和 42 个接口占了全部 43 个标准函数式接口。</p><p>总之,既然 Java 已经有了 lambda 表达式,你必须在设计 API 时考虑 lambda 表达式。在输入时接受函数式接口类型,在输出时返回它们。一般情况下,最好使用 java.util.function 中提供的标准函数式接口,但请注意比较少见的一些情况,在这种情况下,你最好编写自己的函数式接口。</p><h3 id="45-明智地使用流"><a class="markdownIt-Anchor" href="#45-明智地使用流"></a> 45. 明智地使用流</h3><p>Java8添加了流API,用来简化序列或并行执行批量操作,API有两个关键的抽象:流(表示有限或无限的数据元素序列)和流管道(表示对这些元素的多阶段计算)。</p><h4 id="流"><a class="markdownIt-Anchor" href="#流"></a> 流</h4><p>流中的元素可以来自任何地方。常见的源包括集合、数组、文件、正则表达式的 Pattern 匹配器、伪随机数生成器和其他流。流中的数据元素可以是对象的引用或基本数据类型。支持三种基本数据类型:int、long 和 double。</p><h4 id="流管道"><a class="markdownIt-Anchor" href="#流管道"></a> 流管道</h4><p>流管道由源流跟着零个或多个中间操作和一个终止操作组成。每个中间操作以某种方式转换流,例如将每个元素映射到该元素的一个函数,或者过滤掉不满足某些条件的所有元素。中间操作都将一个流转换为另一个流,其元素类型可能与输入流相同,也可能与输入流不同。终止操作对最后一次中间操作所产生的流进行最终计算,例如将其元素存储到集合中、返回特定元素、或打印其所有元素。</p><ol><li>流管道的计算是惰性的:直到调用终止操作时才开始计算,并且对完成终止操作不需要的数据元素永远不会计算。这种惰性的求值机制使得处理无限流成为可能。</li><li>流 API 是流畅的:它被设计成允许使用链式调用将组成管道的所有调用写到单个表达式中。</li><li>谨慎使用流,全部都是流操作的代码可读性不高</li><li>谨慎命名 lambda 表达式的参数名以提高可读性,复杂表达式使用 helper 方法</li></ol><h4 id="流-vs-迭代"><a class="markdownIt-Anchor" href="#流-vs-迭代"></a> 流 VS 迭代</h4><p>迭代代码使用代码块表示重复计算,流管道使用函数对象(通常是 lambda 表达式或方法引用)表示重复计算。</p><p>迭代优势:</p><ol><li>代码块可以读取修改局部变量,lambda表达式只能读取final变量或实际上final变量(初始化后不再修改,编译器会帮我们声明为final)。</li><li>代码块可以控制迭代,包括return,break,continue,throw,但是lambda不行</li></ol><p>流适合用在以下操作:</p><ol><li>元素序列的一致变换</li><li>过滤元素序列</li><li>组合元素序列(例如添加它们,连接它们或计算它们的最小值)</li><li>聚合元素序列到一个集合中,例如按属性分组</li><li>在元素序列中搜索满足某些条件的元素</li></ol><p>总之,有些任务最好使用流来完成,有些任务最好使用迭代来完成。许多任务最好通过结合这两种方法来完成。</p><h3 id="46-在流中使用无副作用的函数"><a class="markdownIt-Anchor" href="#46-在流中使用无副作用的函数"></a> 46. 在流中使用无副作用的函数</h3><p>考虑以下代码,它用于构建文本文件中单词的频率表</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Uses the streams API but not the paradigm--Don't do this!</span>Map<String, Long> freq = <span class="hljs-keyword">new</span> <span class="hljs-title class_">HashMap</span><>();<span class="hljs-keyword">try</span> (Stream<String> words = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Scanner</span>(file).tokens()) { words.forEach(word -> { freq.merge(word.toLowerCase(), <span class="hljs-number">1L</span>, Long::sum); });}</code></pre><p>这段代码可以得出正确答案,但它不是流代码,而是伪装成流代码的迭代代码。它比迭代代码更长,更难阅读,更难维护,问题出在:forEach修改了外部状态。正确使用的流代码如下:</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Proper use of streams to initialize a frequency table</span>Map<String, Long> freq;<span class="hljs-keyword">try</span> (Stream<String> words = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Scanner</span>(file).tokens()) { freq = words.collect(groupingBy(String::toLowerCase, counting()));}</code></pre><p>forEach 操作应该只用于报告流计算的结果,而不是执行计算</p><p>正确的流代码使用了 Collectors 的生成收集器的方法,将元素收集到集合中的方法有三种:<code>toList()</code>、<code>toSet()</code> 和 <code>toCollection(collectionFactory)</code>。它们分别返回 List、Set 和程序员指定的集合类型</p><p>Collectors其它方法大部分是将流收集到Map中。最简单的Map收集器是<code>toMap(keyMapper, valueMapper)</code>,它接受两个函数,一个将流元素映射到键,另一个将流元素映射到值,Item34用到了这个收集器</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Using a toMap collector to make a map from string to enum</span><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> Map<String, Operation> stringToEnum =Stream.of(values()).collect(toMap(Object::toString, e -> e));</code></pre><p>这个收集器不能处理重复键的问题,我们可以加入第三个参数,即merge函数,处理重复键,它的参数类型是Map的值类型。我们还可以加入第四个参数用来指定特定的Map实现(如EnumMap或TreeMap)</p><p>除了toMap方法,groupingBy方法也将元素收集到Map中,键是类别,值是这个类别的所有元素的列表;groupingBy方法第二个参数是一个下游收集器,例如 counting() 作为下游收集器,最终的Map的键是类别,值是这个类别所有元素的数量;groupingBy也支持指定特定的Map实现</p><p>minBy和maxBy,它们接受一个Comparator并返回最小或最大元素;join方法将字符序列(如字符串)连接起来</p><p>总之,流管道编程的本质是无副作用的函数对象。这适用于传递给流和相关对象的所有函数对象。中间操作 forEach 只应用于报告由流执行的计算结果,而不应用于执行计算。为了正确使用流,你必须了解 collector。最重要的 collector 工厂是 toList、toSet、toMap、groupingBy 和 join。</p><h3 id="47-优先使用-collection-而不是-stream-作为返回类型"><a class="markdownIt-Anchor" href="#47-优先使用-collection-而不是-stream-作为返回类型"></a> 47. 优先使用 Collection 而不是 Stream 作为返回类型</h3><p>Stream 没有继承 Iterable,不能用 for-each 循环遍历。Collection 接口继承了 Iterable 而且提供了转换为流的方法,因此,Collection 或其适当的子类通常是公有返回序列的方法的最佳返回类型。</p><h3 id="48-谨慎使用并行流"><a class="markdownIt-Anchor" href="#48-谨慎使用并行流"></a> 48. 谨慎使用并行流</h3><p>如果流来自 Stream.iterate 或者中间操作 limit,并行化管道也不太可能提高其性能</p><p>通常,并行性带来的性能提升在 ArrayList、HashMap、HashSet 和 ConcurrentHashMap 实例上的流效果最好;int 数组和 long 数组也在其中。 这些数据结构的共同之处在于,它们都可以被精确且廉价地分割成任意大小的子程序,这使得在并行线程之间划分工作变得很容易。另一个共同点是,当按顺序处理时,它们提供了极好的引用位置:顺序元素引用一起存储在内存中,这些引用的引用对象在内存中可能彼此不太接近,这减少了引用的位置。引用位置对于并行化批量操作非常重要,如果没有它,线程将花费大量时间空闲,等待数据从内存传输到处理器的缓存中。具有最佳引用位置的数据结构是基本数组,因为数据本身是连续存储在内存中的。</p><p>如果在终止操作中完成大量工作,并且该操作本质上是顺序的,那么管道的并行化效果有限。并行化的最佳终止操作是 reduce 方法或者预先写好的reduce 方法,包括min、max、count、and。anyMatch、allMatch 和 noneMatch 的短路操作也适用于并行性。流的 collect 方法执行的操作称为可变缩减,它们不是并行化的好候选,因为组合集合的开销是昂贵的。</p><p>并行化流不仅会导致糟糕的性能,包括活动失败;它会导致不正确的结果和不可预知的行为(安全故障)。</p><p>总之,不要尝试并行化流管道,除非你有充分的理由相信它将保持计算的正确性以及提高速度。不适当地并行化流的代价可能是程序失败或性能灾难。</p>]]></content>
<summary type="html"><h2 id="第七章-λ-表达式和流"><a class="markdownIt-Anchor" href="#第七章-λ-表达式和流"></a> 第七章 λ 表达式和流</h2>
<h3 id="42-λ-表达式优于匿名类"><a class="markdownIt-Anch</summary>
<category term="Java" scheme="https://zunpan.github.io/categories/Java/"/>
<category term="Effective-Java" scheme="https://zunpan.github.io/tags/Effective-Java/"/>
<category term="lambda表达式" scheme="https://zunpan.github.io/tags/lambda%E8%A1%A8%E8%BE%BE%E5%BC%8F/"/>
<category term="Stream" scheme="https://zunpan.github.io/tags/Stream/"/>
</entry>
<entry>
<title>Effective-Java学习笔记(五)</title>
<link href="https://zunpan.github.io/2022/07/23/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%BA%94%EF%BC%89/"/>
<id>https://zunpan.github.io/2022/07/23/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%BA%94%EF%BC%89/</id>
<published>2022-07-22T16:44:09.000Z</published>
<updated>2023-09-24T04:27:40.275Z</updated>
<content type="html"><![CDATA[<h2 id="第六章-枚举和注解"><a class="markdownIt-Anchor" href="#第六章-枚举和注解"></a> 第六章 枚举和注解</h2><h3 id="34-用枚举类型代替-int-常量"><a class="markdownIt-Anchor" href="#34-用枚举类型代替-int-常量"></a> 34. 用枚举类型代替 int 常量</h3><h4 id="int枚举"><a class="markdownIt-Anchor" href="#int枚举"></a> int枚举</h4><p>int枚举类型中有一系列public static final int常量,每个常量表示一个类型。<br />它有许多缺点:</p><ol><li>不提供类型安全性,容易误用(入参是Apple常量,但是传任何int都可以)</li><li>常量值修改,客户端也必须修改</li><li>调试麻烦</li></ol><h4 id="枚举类型"><a class="markdownIt-Anchor" href="#枚举类型"></a> 枚举类型</h4><p>Java 的枚举类型是成熟的类,其他语言中的枚举类型本质上是 int 值。</p><p>枚举优势:</p><ol><li><p>实例受控<br />Java 枚举类型通过 public static final 修饰的字段为每个枚举常量导出一个实例。枚举类型实际上是 final 类型,因为构造函数是私有的。客户端既不能创建枚举类型的实例,也不能继承它,所以除了声明的枚举常量之外,不能有任何实例。换句话说,枚举类型是实例受控的类(Item-1)。它们是单例(Item-3)的推广应用,单例本质上是单元素的枚举。</p></li><li><p>提供编译时类型安全性。<br />如果将参数声明为 Apple 枚举类型,则可以保证传递给该参数的任何非空对象引用都是 Apple 枚举值之一。尝试传递错误类型的值将导致编译时错误,将一个枚举类型的表达式赋值给另一个枚举类型的变量,或者使用 == 运算符比较不同枚举类型的值同样会导致错误。</p></li><li><p>名称相同的枚举类型常量能共存<br />名称相同的枚举类型常量能和平共存,因为每种类型都有自己的名称空间。你可以在枚举类型中添加或重新排序常量,而无需重新编译其客户端,因为导出常量的字段在枚举类型及其客户端之间提供了一层隔离:常量值不会像在 int 枚举模式中那样编译到客户端中。最后,你可以通过调用枚举的 toString 方法将其转换为可打印的字符串。</p></li><li><p>允许添加任意方法和字段并实现任意接口<br />枚举类型允许添加任意方法和字段并实现任意接口。它们提供了所有 Object 方法的高质量实现(参阅 Chapter 3),还实现了 Comparable(Item-14)和 Serializable(参阅 Chapter 12),并且它们的序列化形式被设计成能够适应枚举类型的可变性。如果方法是常量特有的,可以在枚举类型中添加抽象方法,在声明枚举实例时覆盖抽象方法</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Enum type with constant-specific class bodies and data</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">enum</span> <span class="hljs-title class_">Operation</span> { PLUS(<span class="hljs-string">"+"</span>) { <span class="hljs-keyword">public</span> <span class="hljs-type">double</span> <span class="hljs-title function_">apply</span><span class="hljs-params">(<span class="hljs-type">double</span> x, <span class="hljs-type">double</span> y)</span> { <span class="hljs-keyword">return</span> x + y; } }, MINUS(<span class="hljs-string">"-"</span>) { <span class="hljs-keyword">public</span> <span class="hljs-type">double</span> <span class="hljs-title function_">apply</span><span class="hljs-params">(<span class="hljs-type">double</span> x, <span class="hljs-type">double</span> y)</span> { <span class="hljs-keyword">return</span> x - y; } }, TIMES(<span class="hljs-string">"*"</span>) { <span class="hljs-keyword">public</span> <span class="hljs-type">double</span> <span class="hljs-title function_">apply</span><span class="hljs-params">(<span class="hljs-type">double</span> x, <span class="hljs-type">double</span> y)</span> { <span class="hljs-keyword">return</span> x * y; } }, DIVIDE(<span class="hljs-string">"/"</span>) { <span class="hljs-keyword">public</span> <span class="hljs-type">double</span> <span class="hljs-title function_">apply</span><span class="hljs-params">(<span class="hljs-type">double</span> x, <span class="hljs-type">double</span> y)</span> { <span class="hljs-keyword">return</span> x / y; } }; <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> String symbol; Operation(String symbol) { <span class="hljs-built_in">this</span>.symbol = symbol; } <span class="hljs-meta">@Override</span> <span class="hljs-keyword">public</span> String <span class="hljs-title function_">toString</span><span class="hljs-params">()</span> { <span class="hljs-keyword">return</span> symbol; } <span class="hljs-comment">// Implementing a fromString method on an enum type</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> Map<String, Operation> stringToEnum =Stream.of(values()).collect(toMap(Object::toString, e -> e)); <span class="hljs-comment">// Returns Operation for string, if any</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Optional<Operation> <span class="hljs-title function_">fromString</span><span class="hljs-params">(String symbol)</span> { <span class="hljs-keyword">return</span> Optional.ofNullable(stringToEnum.get(symbol)); } <span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-type">double</span> <span class="hljs-title function_">apply</span><span class="hljs-params">(<span class="hljs-type">double</span> x, <span class="hljs-type">double</span> y)</span>;}</code></pre><p>上述实现的缺点是如果常量特有的方法有可以复用的代码,那么会造成许多冗余。你可以把冗余代码抽出成方法,常量覆盖抽象方法时调用抽出的方法,这种实现同样有许多冗余代码;你也可以把抽象方法改成具体方法,里面是可以复用的代码,添加常量时如果不覆盖那就用默认的,这种实现问题在于添加常量时如果忘记覆盖那就用默认方法了。</p><p>当常量特有的方法有可以复用的代码时,我们采用策略枚举模式,将可复用代码移到私有嵌套枚举中。当常量调用方法时,调用私有嵌套枚举常量的方法。</p><p>在枚举上实现特定常量的行为时 switch 语句不是一个好的选择,只有在枚举不在你的控制之下,你希望它有一个实例方法来返回每个常量的特定行为,这时候才用 switch</p></li></ol><p>总之,枚举类型相对于 int 常量的优势是毋庸置疑的。枚举更易于阅读、更安全、更强大。许多枚举不需要显式构造函数或成员,但有些枚举则受益于将数据与每个常量关联,并提供行为受数据影响的方法。将多个行为与一个方法关联起来,这样的枚举更少。在这种相对少见的情况下,相对于使用 switch 的枚举,特定常量方法更好。如果枚举常量有一些(但不是全部)共享公共行为,请考虑策略枚举模式。</p><h3 id="35-使用实例字段替代序数"><a class="markdownIt-Anchor" href="#35-使用实例字段替代序数"></a> 35. 使用实例字段替代序数</h3><p>每个枚举常量都有一个 ordinal 方法,返回枚举常量在枚举类中的位置。不要用这个方法返回与枚举常量关联的值,一旦常量位置变动,值就是错的,所以不要用序数生成与枚举常量关联的值,而是将其存在实例字段中</p><h3 id="36-用-enumset-替代位字段"><a class="markdownIt-Anchor" href="#36-用-enumset-替代位字段"></a> 36. 用 EnumSet 替代位字段</h3><h4 id="位字段"><a class="markdownIt-Anchor" href="#位字段"></a> 位字段</h4><p>如果枚举类型的元素主要在 Set 中使用,传统上使用 int 枚举模式(Item34),通过不同的 2 的幂次为每个常量赋值:</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Bit field enumeration constants - OBSOLETE!</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">Text</span> { <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> <span class="hljs-variable">STYLE_BOLD</span> <span class="hljs-operator">=</span> <span class="hljs-number">1</span> << <span class="hljs-number">0</span>; <span class="hljs-comment">// 1</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> <span class="hljs-variable">STYLE_ITALIC</span> <span class="hljs-operator">=</span> <span class="hljs-number">1</span> << <span class="hljs-number">1</span>; <span class="hljs-comment">// 2</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> <span class="hljs-variable">STYLE_UNDERLINE</span> <span class="hljs-operator">=</span> <span class="hljs-number">1</span> << <span class="hljs-number">2</span>; <span class="hljs-comment">// 4</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> <span class="hljs-variable">STYLE_STRIKETHROUGH</span> <span class="hljs-operator">=</span> <span class="hljs-number">1</span> << <span class="hljs-number">3</span>; <span class="hljs-comment">// 8</span> <span class="hljs-comment">// Parameter is bitwise OR of zero or more STYLE_ constants</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">applyStyles</span><span class="hljs-params">(<span class="hljs-type">int</span> styles)</span> { ... }}</code></pre><p>使用位运算的 OR 操作将几个常量组合成一个集合,这个集合叫做位字段:</p><pre class="highlight"><code class="java">text.applyStyles(STYLE_BOLD | STYLE_ITALIC);</code></pre><p>位字段表示方式允许使用位运算高效地执行集合操作,如并集和交集。但是位字段具有 int 枚举常量所有缺点,甚至更多。当位字段被打印为数字时,它比简单的 int 枚举常量更难理解。没有一种简单的方法可以遍历由位字段表示的所有元素。最后,你必须预测在编写 API 时需要的最大位数,并相应地为位字段(通常是 int 或 long)选择一种类型。一旦选择了一种类型,在不更改 API 的情况下,不能超过它的宽度(32 或 64 位)。</p><h5 id="enumset"><a class="markdownIt-Anchor" href="#enumset"></a> EnumSet</h5><p>一些使用枚举而不是 int 常量的程序员在需要传递常量集合时仍然坚持使用位字段。没有理由这样做,因为存在更好的选择。<code>java.util</code> 包提供 EnumSet 类来有效地表示从单个枚举类型中提取的值集。这个类实现了 Set 接口,提供了所有其他 Set 实现所具有的丰富性、类型安全性和互操作性。但在内部,每个 EnumSet 都表示为一个位向量。如果底层枚举类型有 64 个或更少的元素(大多数都是),则整个 EnumSet 用一个 long 表示,因此其性能与位字段的性能相当。批量操作(如 removeAll 和 retainAll)是使用逐位算法实现的,就像手动处理位字段一样。但是,你可以避免因手工修改导致产生不良代码和潜在错误:EnumSet 为你完成了这些繁重的工作。</p><p>当之前的示例修改为使用枚举和 EnumSet 而不是位字段时。它更短,更清晰,更安全:</p><pre class="highlight"><code class="java"><span class="hljs-comment">// EnumSet - a modern replacement for bit fields</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">Text</span> { <span class="hljs-keyword">public</span> <span class="hljs-keyword">enum</span> <span class="hljs-title class_">Style</span> { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH } <span class="hljs-comment">// Any Set could be passed in, but EnumSet is clearly best</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">applyStyles</span><span class="hljs-params">(Set<Style> styles)</span> { ... }}</code></pre><p>下面是将 EnumSet 实例传递给 applyStyles 方法的客户端代码。EnumSet 类提供了一组丰富的静态工厂,可以方便地创建集合,下面的代码演示了其中的一个:</p><pre class="highlight"><code class="java">text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));</code></pre><p>总之,<strong>如果枚举类型将在 Set 中使用,不要用位字段表示它。</strong> EnumSet 类结合了位字段的简洁性和性能,以及 (Item-34) 中描述的枚举类型的许多优点。EnumSet 的一个真正的缺点是,从 Java 9 开始,它不能创建不可变的 EnumSet,在未来发布的版本中可能会纠正这一点。同时,可以用 <code>Collections.unmodifiableSet</code> 包装 EnumSet,但简洁性和性能将受到影响</p><h3 id="37-使用-enummap-替换序数索引"><a class="markdownIt-Anchor" href="#37-使用-enummap-替换序数索引"></a> 37. 使用 EnumMap 替换序数索引</h3><h4 id="序数索引"><a class="markdownIt-Anchor" href="#序数索引"></a> 序数索引</h4><p>当需要将枚举常量映射到其它值时,有一种方法是序数索引,使用 ordinal 方法返回的序数表示key。这种方式存在Item35提到的问题</p><h4 id="enummap"><a class="markdownIt-Anchor" href="#enummap"></a> EnumMap</h4><p>EnumMap使用枚举常量作为key,功能丰富、类型安全</p><h3 id="38-使用接口模拟可扩展枚举"><a class="markdownIt-Anchor" href="#38-使用接口模拟可扩展枚举"></a> 38. 使用接口模拟可扩展枚举</h3><p>枚举不可以扩展(继承)另一个枚举,但可以实现接口。可以用接口模拟可扩展的枚举。下面的例子将Item34的枚举类型的Operation改成了接口类型。通过实现接口来模拟继承枚举。</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Emulated extensible enum using an interface</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title class_">Operation</span> { <span class="hljs-type">double</span> <span class="hljs-title function_">apply</span><span class="hljs-params">(<span class="hljs-type">double</span> x, <span class="hljs-type">double</span> y)</span>;}<span class="hljs-keyword">public</span> <span class="hljs-keyword">enum</span> <span class="hljs-title class_">BasicOperation</span> <span class="hljs-keyword">implements</span> <span class="hljs-title class_">Operation</span> { PLUS(<span class="hljs-string">"+"</span>) { <span class="hljs-keyword">public</span> <span class="hljs-type">double</span> <span class="hljs-title function_">apply</span><span class="hljs-params">(<span class="hljs-type">double</span> x, <span class="hljs-type">double</span> y)</span> { <span class="hljs-keyword">return</span> x + y; } }, MINUS(<span class="hljs-string">"-"</span>) { <span class="hljs-keyword">public</span> <span class="hljs-type">double</span> <span class="hljs-title function_">apply</span><span class="hljs-params">(<span class="hljs-type">double</span> x, <span class="hljs-type">double</span> y)</span> { <span class="hljs-keyword">return</span> x - y; } }, TIMES(<span class="hljs-string">"*"</span>) { <span class="hljs-keyword">public</span> <span class="hljs-type">double</span> <span class="hljs-title function_">apply</span><span class="hljs-params">(<span class="hljs-type">double</span> x, <span class="hljs-type">double</span> y)</span> { <span class="hljs-keyword">return</span> x * y; } }, DIVIDE(<span class="hljs-string">"/"</span>) { <span class="hljs-keyword">public</span> <span class="hljs-type">double</span> <span class="hljs-title function_">apply</span><span class="hljs-params">(<span class="hljs-type">double</span> x, <span class="hljs-type">double</span> y)</span> { <span class="hljs-keyword">return</span> x / y; } }; <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> String symbol; BasicOperation(String symbol) { <span class="hljs-built_in">this</span>.symbol = symbol; } <span class="hljs-meta">@Override</span> <span class="hljs-keyword">public</span> String <span class="hljs-title function_">toString</span><span class="hljs-params">()</span> { <span class="hljs-keyword">return</span> symbol; }}</code></pre><pre class="highlight"><code class="java"><span class="hljs-comment">// Emulated extension enum</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">enum</span> <span class="hljs-title class_">ExtendedOperation</span> <span class="hljs-keyword">implements</span> <span class="hljs-title class_">Operation</span> { EXP(<span class="hljs-string">"^"</span>) { <span class="hljs-keyword">public</span> <span class="hljs-type">double</span> <span class="hljs-title function_">apply</span><span class="hljs-params">(<span class="hljs-type">double</span> x, <span class="hljs-type">double</span> y)</span> { <span class="hljs-keyword">return</span> Math.pow(x, y); } }, REMAINDER(<span class="hljs-string">"%"</span>) { <span class="hljs-keyword">public</span> <span class="hljs-type">double</span> <span class="hljs-title function_">apply</span><span class="hljs-params">(<span class="hljs-type">double</span> x, <span class="hljs-type">double</span> y)</span> { <span class="hljs-keyword">return</span> x % y; } }; <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> String symbol; ExtendedOperation(String symbol) { <span class="hljs-built_in">this</span>.symbol = symbol; } <span class="hljs-meta">@Override</span> <span class="hljs-keyword">public</span> String <span class="hljs-title function_">toString</span><span class="hljs-params">()</span> { <span class="hljs-keyword">return</span> symbol; }}</code></pre><p>可以用接口类型引用指向不同子枚举类型实例。</p><pre class="highlight"><code class="java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> { <span class="hljs-type">Operation</span> <span class="hljs-variable">op</span> <span class="hljs-operator">=</span> BasicOperation.DIVIDE; System.out.println(op.apply(<span class="hljs-number">15</span>, <span class="hljs-number">3</span>)); op=ExtendedOperation.EXP; System.out.println(op.apply(<span class="hljs-number">2</span>,<span class="hljs-number">5</span>));}</code></pre><p><code>java.nio.file.LinkOption</code> 使用了这个Item描述的方法,它实现了 CopyOption 和 OpenOption 接口。</p><p>总之,枚举不可以扩展或被扩展,但是你可以通过接口模拟枚举之间的层级关系</p><h3 id="39-注解优于命名模式"><a class="markdownIt-Anchor" href="#39-注解优于命名模式"></a> 39. 注解优于命名模式</h3><h4 id="命名模式"><a class="markdownIt-Anchor" href="#命名模式"></a> 命名模式</h4><p>用来标明某些程序元素需要工具或框架特殊处理。<br />例如JUnit3及之前,用户必须写test开头的测试方法,不然测试都不能运行。<br />缺点:1.写错方法名会导致测试通过,但是实际上根本没有运行;2.不能将参数值和程序元素关联,例如测试方法只有在抛出特定异常时才成功,虽然可以精心设计命名模式,将异常名称写在测试方法名上,但是编译器无法检查异常类是否存在</p><h4 id="注解"><a class="markdownIt-Anchor" href="#注解"></a> 注解</h4><p>Junit4 开始使用注解,框架会根据注解执行测试方法,也可以将异常和测试方法绑定。注解本身不修改代码语义,而是通过反射(框架所用的技术)对其特殊处理</p><h3 id="40-坚持使用-override-注解"><a class="markdownIt-Anchor" href="#40-坚持使用-override-注解"></a> 40. 坚持使用 @Override 注解</h3><p>请在要覆盖父类声明的每个方法声明上使用 @Override 注解。只有一个例外,那就是具体类覆盖父类的抽象方法,因为具体类必须要实现父类的抽象方法,所以不必加 @Override 注解</p><h3 id="41-使用标记接口定义类型"><a class="markdownIt-Anchor" href="#41-使用标记接口定义类型"></a> 41. 使用标记接口定义类型</h3><h4 id="标记接口"><a class="markdownIt-Anchor" href="#标记接口"></a> 标记接口</h4><p>标记接口是一个空接口,它的作用是标记实现类具有某些属性。例如,实现 Serializable 接口表示类的实例可以写入 ObjectOutputStream(序列化)</p><h5 id="标记接口vs标记注解"><a class="markdownIt-Anchor" href="#标记接口vs标记注解"></a> 标记接口VS标记注解</h5><p>与标记注解(Item39)相比,标记接口有两个优点:</p><ol><li>标记接口定义的类型由标记类的实例实现;标记注解不会。因此标记接口类型的存在允许你在编译时捕获错误,标记注解只能在运行时捕获。ObjectOutputStream.writeObject方法入参必须是实现了Serializable的实例,否则会报编译错误(JDK设计缺陷,writeObject入参是Object类型)</li><li>标记接口相对于标记注解的另一个优点是可以更精确地定位子类型。例如 Set 是一个标记接口,它继承 Collection接口,Set标记了所有它的实现类都具有Collection功能。Set相较于标记接口的特殊之处在于它还细化了Collection方法 add、equals 和 hashCode 的约定</li></ol><p>标记注解的优势:与基于使用注解的框架保持一致性</p><h5 id="使用规则"><a class="markdownIt-Anchor" href="#使用规则"></a> 使用规则</h5><p>如果标记用在类或接口之外的任何程序元素,必须用标记注解;如果框架使用注解,那就用标记注解;其它情况都用标记接口</p>]]></content>
<summary type="html"><h2 id="第六章-枚举和注解"><a class="markdownIt-Anchor" href="#第六章-枚举和注解"></a> 第六章 枚举和注解</h2>
<h3 id="34-用枚举类型代替-int-常量"><a class="markdownIt-Anchor</summary>
<category term="Java" scheme="https://zunpan.github.io/categories/Java/"/>
<category term="Effective-Java" scheme="https://zunpan.github.io/tags/Effective-Java/"/>
<category term="枚举" scheme="https://zunpan.github.io/tags/%E6%9E%9A%E4%B8%BE/"/>
<category term="注解" scheme="https://zunpan.github.io/tags/%E6%B3%A8%E8%A7%A3/"/>
</entry>
<entry>
<title>Effective-Java学习笔记(四)</title>
<link href="https://zunpan.github.io/2022/07/14/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E5%9B%9B%EF%BC%89/"/>
<id>https://zunpan.github.io/2022/07/14/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E5%9B%9B%EF%BC%89/</id>
<published>2022-07-14T12:17:09.000Z</published>
<updated>2023-09-24T04:27:40.276Z</updated>
<content type="html"><![CDATA[<h2 id="第五章-泛型"><a class="markdownIt-Anchor" href="#第五章-泛型"></a> 第五章 泛型</h2><h3 id="26-不要使用原始类型"><a class="markdownIt-Anchor" href="#26-不要使用原始类型"></a> 26. 不要使用原始类型</h3><h4 id="泛型和原始类型"><a class="markdownIt-Anchor" href="#泛型和原始类型"></a> 泛型和原始类型</h4><p>声明中具有一个或多个类型参数的类或接口就是泛型类或泛型接口。泛型类和泛型接口统称为泛型。</p><p>每个泛型都定义了一个原始类型,它是没有任何相关类型参数的泛型的名称。例如,<code>List<E></code> 对应的原始类型是 List。原始类型的行为就好像所有泛型信息从类型声明中删除了一样。它们的存在主要是为了与之前的代码兼容。</p><h4 id="泛型优势"><a class="markdownIt-Anchor" href="#泛型优势"></a> 泛型优势</h4><p>泛型可以帮助编译器在编译过程中发现潜在的类型转换异常,而原始类型不行。</p><h4 id="泛型的子类型规则"><a class="markdownIt-Anchor" href="#泛型的子类型规则"></a> 泛型的子类型规则</h4><p>原始类型 List 和 参数化类型 <code>List<Object></code> 都可以保存任何类型的对象 ,但是不能把其它泛型对象 , 例如<code>List<String></code>对象赋给<code>List<Object></code>引用而可以赋给 List 引用。泛型有子类型规则,<code>List<String></code> 是原始类型 List 的子类型,而不是参数化类型 <code>List<Object></code> 的子类型(Item-28)。假如<code>List<String></code>可以是<code>List<Object></code>的子类型,那么<code>List<String></code>的对象赋给<code>List<Object></code>,通过<code>List<Object></code>插入非String元素,违反了<code>List<String></code>只放String的约定</p><h4 id="无界通配符"><a class="markdownIt-Anchor" href="#无界通配符"></a> 无界通配符</h4><p>如果你想使用泛型,但不知道或不关心实际的类型参数是什么,那么可以使用无界通配符 ? 代替。例如,泛型集合 <code>Set<E></code> 的无界通配符类型是 <code>Set<?></code>。它是最通用的参数化集合类型,能够容纳任何集合</p><p>无界通配符类型 <code>Set<?></code> 和原始类型 Set 之间的区别在于通配符类型是安全的,而原始类型不是。将任何元素放入具有原始类型的集合中,很容易破坏集合的类型一致性;而无界通配符类型不能放入元素(除了null)</p><pre class="highlight"><code class="java">Set<Integer> integerSet = <span class="hljs-keyword">new</span> <span class="hljs-title class_">HashSet</span><>();<span class="hljs-comment">// 无界通配符类型Set<?>可以引用任何Set<E>和Set,但是不能往里面放除了null的元素</span>Set<?> set1 = integerSet;<span class="hljs-comment">// 原始类型Set也可以引用任何Set<E>和Set,但是可以往里面添加元素,会存在类型转换异常</span><span class="hljs-type">Set</span> <span class="hljs-variable">set2</span> <span class="hljs-operator">=</span> integerSet;</code></pre><h4 id="使用泛型而不用原始类型的例外"><a class="markdownIt-Anchor" href="#使用泛型而不用原始类型的例外"></a> 使用泛型而不用原始类型的例外</h4><ol><li>类字面量。该规范不允许使用参数化类型(尽管它允许数组类型和基本类型)。换句话说,<code>List.class</code>,<code>String[].class</code> 和 <code>int.class</code> 都是合法的,但是 <code>List<String>.class</code> 和 <code>List<?>.class</code> 不是。</li><li>instanceof 运算符。由于泛型信息在运行时被删除,因此在不是无界通配符类型之外的参数化类型使用 instanceof 操作符是非法的。使用无界通配符类型代替原始类型不会以任何方式影响 instanceof 运算符的行为。在这种情况下,尖括号和问号只是多余的。下面的例子是使用通用类型 instanceof 运算符的首选方法:</li></ol><pre class="highlight"><code class="java"><span class="hljs-comment">// Legitimate use of raw type - instanceof operator</span><span class="hljs-keyword">if</span> (o <span class="hljs-keyword">instanceof</span> Set) { <span class="hljs-comment">// Raw type</span> Set<?> s = (Set<?>) o; <span class="hljs-comment">// Wildcard type</span> ...}</code></pre><p>总之,使用原始类型可能会在运行时导致异常,所以不要轻易使用它们。它们仅用于与引入泛型之前的遗留代码进行兼容。快速回顾一下,<code>Set<Object></code> 是一个参数化类型,表示可以包含任何类型的对象的集合,<code>Set<?></code> 是一个无界通配符类型,表示只能包含某种未知类型的对象的集合,Set 是一个原始类型,它没有使用泛型。前两个是安全的,后一个不安全</p><p>为便于参考,本条目中介绍的术语(以及后面将要介绍的一些术语)总结如下:</p><table><thead><tr><th>Term</th><th>Example</th><th>Item</th></tr></thead><tbody><tr><td>Parameterized type</td><td>List<String></td><td>Item-26</td></tr><tr><td>Actual type parameter</td><td>String</td><td>Item-26</td></tr><tr><td>Generic type</td><td>List<E></td><td>Item-26, Item-29</td></tr><tr><td>Formal type parameter</td><td>E</td><td>Item-26</td></tr><tr><td>Unbounded wildcard type</td><td>List<?></td><td>Item-26</td></tr><tr><td>Raw type</td><td>List</td><td>Item-26</td></tr><tr><td>Bounded type parameter</td><td><E extends Number></td><td>Item-29</td></tr><tr><td>Recursive type bound</td><td><T extends Comparable<T>></td><td>Item-30</td></tr><tr><td>Bounded wildcard type</td><td>List<? extends Number></td><td>Item-31</td></tr><tr><td>Generic method</td><td>static <E> List<E> asList(E[] a)</td><td>Item-30</td></tr><tr><td>Type token</td><td>String.class</td><td>Item-33</td></tr></tbody></table><h3 id="27-消除-unchecked-警告"><a class="markdownIt-Anchor" href="#27-消除-unchecked-警告"></a> 27. 消除 unchecked 警告</h3><p>消除所有 unchecked 警告可以确保代码是类型安全的,运行时不会出现 ClassCastException。如果不能消除警告,但是可以证明引发警告的代码是类型安全的,那么可以使用 SuppressWarnings(“unchecked”) 注解来抑制警告。</p><h4 id="suppresswarnings注解"><a class="markdownIt-Anchor" href="#suppresswarnings注解"></a> SuppressWarnings注解</h4><p>SuppressWarnings 注解可以用于任何声明中,从单个局部变量声明到整个类。请总是在尽可能小的范围上使用 SuppressWarnings 注解。通常用在一个变量声明或一个非常短的方法或构造函数。不要在整个类中使用 SuppressWarnings。这样做可能会掩盖关键警告。</p><p>如果你发现自己在一个超过一行的方法或构造函数上使用 SuppressWarnings 注解,那么你可以将其移动到局部变量声明中</p><p>将 SuppressWarnings 注释放在 return 语句上是非法的,因为它不是声明。你可能想把注释放在整个方法上,但是不要这样做。相反,应该声明一个局部变量来保存返回值并添加注解</p><p>每次使用 SuppressWarnings(“unchecked”) 注解时,要添加一条注释,说明这样做是安全的。这将帮助他人理解代码,更重要的是,它将降低其他人修改代码而产生不安全事件的几率。如果你觉得写这样的注释很难,那就继续思考合适的方式,你最终可能会发现,unchecked 操作毕竟是不安全的。</p><p>总之,unchecked 警告很重要。不要忽视他们。每个 unchecked 警告都代表了在运行时发生 ClassCastException 的可能性。尽最大努力消除这些警告。如果不能消除 unchecked 警告,但是可以证明引发该警告的代码是类型安全的,那么可以在尽可能狭窄的范围内使用 @SuppressWarnings(“unchecked”) 注释来抑制警告。在注释中记录你决定抑制警告的理由。</p><h3 id="28-list-优于数组"><a class="markdownIt-Anchor" href="#28-list-优于数组"></a> 28. list 优于数组</h3><h4 id="数组与泛型的区别"><a class="markdownIt-Anchor" href="#数组与泛型的区别"></a> 数组与泛型的区别</h4><ol><li><p>数组是协变的, 如果 Sub 是 Super 的一个子类型,那么数组类型 Sub[] 就是数组类型 Super[] 的一个子类型。相比之下,泛型是不变的:对于任何两个不同类型 Type1 和 Type2,<code>List<Type1></code> 既不是 <code>List<Type2></code> 的子类型,也不是 <code>List<Type2></code> 的父类型。</p></li><li><p>数组是具体化的。这意味着数组在运行时知道并强制执行他们的元素类型。如前所述,如果试图将 String 元素放入一个 Long 类型的数组中,就会得到 ArrayStoreException。相比之下,泛型是通过擦除来实现的,这意味着它们只在编译时执行类型约束,并在运行时丢弃(或擦除)元素类型信息。擦除允许泛型与不使用泛型的遗留代码自由交互操作(Item-26),确保在 Java 5 中平稳过渡</p></li></ol><h4 id="泛型数组的创建是非法的"><a class="markdownIt-Anchor" href="#泛型数组的创建是非法的"></a> 泛型数组的创建是非法的</h4><p>由于这些基本差异,数组和泛型不能很好地混合。例如,创建泛型、参数化类型或类型参数的数组是非法的。因此,这些数组创建表达式都不是合法的:<code>new List<E>[]、new List<String>[]、new E[]</code>。所有这些都会在编译时导致泛型数组创建错误。</p><p>考虑以下代码片段:</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Why generic array creation is illegal - won't compile!</span>List<String>[] stringLists = <span class="hljs-keyword">new</span> <span class="hljs-title class_">List</span><String>[<span class="hljs-number">1</span>]; <span class="hljs-comment">// (1)</span>List<Integer> intList = List.of(<span class="hljs-number">42</span>); <span class="hljs-comment">// (2)</span>Object[] objects = stringLists; <span class="hljs-comment">// (3)</span>objects[<span class="hljs-number">0</span>] = intList; <span class="hljs-comment">// (4)</span><span class="hljs-type">String</span> <span class="hljs-variable">s</span> <span class="hljs-operator">=</span> stringLists[<span class="hljs-number">0</span>].get(<span class="hljs-number">0</span>); <span class="hljs-comment">// (5)</span></code></pre><p>假设创建泛型数组的第 1 行是合法的。第 2 行创建并初始化一个包含单个元素的 <code>List<Integer></code>。第 3 行将 <code>List<String></code> 数组存储到 Object 类型的数组变量中,这是合法的,因为数组是协变的。第 4 行将 <code>List<Integer></code> 存储到 Object 类型的数组的唯一元素中,这是成功的,因为泛型是由擦除实现的:<code>List<Integer></code> 实例的运行时类型是 List,<code>List<String>[]</code> 实例的运行时类型是 <code>List[]</code>,因此这个赋值不会生成 ArrayStoreException。现在我们有麻烦了。我们将一个 <code>List<Integer></code> 实例存储到一个数组中,该数组声明只保存 <code>List<String></code> 实例。在第 5 行,我们从这个数组的唯一列表中检索唯一元素。编译器自动将检索到的元素转换为 String 类型,但它是一个 Integer 类型的元素,因此我们在运行时得到一个 ClassCastException。为了防止这种情况发生,第 1 行(创建泛型数组)必须生成编译时错误。</p><h4 id="用list替代数组"><a class="markdownIt-Anchor" href="#用list替代数组"></a> 用List替代数组</h4><p>当你在转换为数组类型时遇到泛型数组创建错误或 unchecked 强制转换警告时,通常最好的解决方案是使用集合类型 <code>List<E></code>,而不是数组类型 <code>E[]</code>。你可能会牺牲一些简洁性和性能,但作为交换,你可以获得更好地类型安全性和互操作性。</p><p>总之,数组和泛型有非常不同的类型规则。数组是协变的、具体化的;泛型是不可变的和可被擦除的。因此,数组提供了运行时类型安全性,而不是编译时类型安全性,对于泛型来说相反。一般来说,数组和泛型不能很好的混合,如果你发现将它们混合在一起并得到编译时错误或警告,那么你的第一个反应应该是将数组替换为 list。</p><h3 id="29-优先使用泛型"><a class="markdownIt-Anchor" href="#29-优先使用泛型"></a> 29. 优先使用泛型</h3><h4 id="编写泛型"><a class="markdownIt-Anchor" href="#编写泛型"></a> 编写泛型</h4><p>在将原始类型修改成泛型时, 可能会遇到不能创建泛型数组的问题,有两种解决方法</p><ol><li>创建 Object 数组并将其强制转换为 E[] 类型(字段的类型是 E[] ,将Object数组强转成 E[] 可以成功,但是方法返回 E[]给客户端,由编译器添加隐式强转就会失败,因为隐式强转是将Object数组转成声明的具体类型数组)。现在,编译器将发出一个警告来代替错误。这种用法是合法的,但(一般而言)不是类型安全的。编译器可能无法证明你的程序是类型安全的,但你可以。你必须说服自己,unchecked 的转换不会损害程序的类型安全性。所涉及的数组(元素)存储在私有字段中,从未返回给客户端或传递给任何其它方法(传到外面就会发生隐式强转)。添加元素时,元素也是 E 类型,因此 unchecked 的转换不会造成任何损害。一旦你证明了 unchecked 的转换是安全的,就将警告限制在尽可能小的范围内(Item-27)。</li><li>将字段的类型从 E[] 更改为 Object[]。编译器会产生类似的错误和警告,处理方法也和上面类似</li></ol><p>方法1优势:可读性更好,因为数组声明为 E[] 类型,这清楚地表明它只包含 E 的实例。它也更简洁,只需要在创建数组的地方做一次强转,其它地方读取数组元素不用强转成 E 类型<br />方法1劣势:会造成堆污染(Item-32):数组的运行时类型与其编译时类型不匹配(除非 E 恰好是 Object)</p><h4 id="为啥要创建泛型数组"><a class="markdownIt-Anchor" href="#为啥要创建泛型数组"></a> 为啥要创建泛型数组</h4><p>Item-28 鼓励优先使用列表而不是数组。但是在泛型中使用列表并不总是可能或可取的。Java 本身不支持列表,因此一些泛型(如ArrayList)必须在数组之上实现。其它泛型(如HashMap)用数组实现来提高性能</p><p>总之,泛型比需要在客户端代码中转换的类型更安全、更容易使用。在设计新类型时,请确保客户端可以在不使用类型转换的情况下使用它们。这通常意味着使类型具有通用性。如果你有任何应该是泛型但不是泛型的现有类型,请对它们进行泛化。这将使这些类型的新用户在不破坏现有客户端的情况下更容易使用。</p><h3 id="30-优先使用泛型方法"><a class="markdownIt-Anchor" href="#30-优先使用泛型方法"></a> 30. 优先使用泛型方法</h3><p>类可以是泛型的,方法也可以是泛型的</p><h4 id="编写泛型方法"><a class="markdownIt-Anchor" href="#编写泛型方法"></a> 编写泛型方法</h4><p>编写泛型方法类似于编写泛型。类型参数列表声明类型参数,它位于方法的修饰符与其返回类型之间。例如,类型参数列表为 <code><E></code>,返回类型为 <code>Set<E></code></p><pre class="highlight"><code class="java"><span class="hljs-comment">// Generic method</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <E> Set<E> <span class="hljs-title function_">union</span><span class="hljs-params">(Set<E> s1, Set<E> s2)</span> { Set<E> result = <span class="hljs-keyword">new</span> <span class="hljs-title class_">HashSet</span><>(s1); result.addAll(s2); <span class="hljs-keyword">return</span> result;}</code></pre><p>至少对于简单的泛型方法,这就是(要注意细节的)全部。该方法编译时不生成任何警告,并且提供了类型安全性和易用性。这里有一个简单的程序来演示。这个程序不包含转换,编译时没有错误或警告:</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Simple program to exercise generic method</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> { Set<String> guys = Set.of(<span class="hljs-string">"Tom"</span>, <span class="hljs-string">"Dick"</span>, <span class="hljs-string">"Harry"</span>); Set<String> stooges = Set.of(<span class="hljs-string">"Larry"</span>, <span class="hljs-string">"Moe"</span>, <span class="hljs-string">"Curly"</span>); Set<String> aflCio = union(guys, stooges); System.out.println(aflCio);}</code></pre><p>当你运行程序时,它会打印出 [Moe, Tom, Harry, Larry, Curly, Dick]。(输出元素的顺序可能不同)。</p><p>union 方法的一个限制是,所有三个集合(输入参数和返回值)的类型必须完全相同。你可以通过使用有界通配符类型(Item-31)使方法更加灵活。</p><h3 id="31-使用有界通配符增加-api-的灵活性"><a class="markdownIt-Anchor" href="#31-使用有界通配符增加-api-的灵活性"></a> 31. 使用有界通配符增加 API 的灵活性</h3><h4 id="pecs"><a class="markdownIt-Anchor" href="#pecs"></a> PECS</h4><p>为了获得满足里氏代换原则,应对表示生产者或消费者入参使用通配符类型。如果输入参数既是生产者优势消费者,使用精确的类型</p><p>PECS助记符:PECS 表示生产者应使用 extends,消费者应使用 super。换句话说,如果参数化类型表示 T 生产者,则使用 <code><? extends T></code>;如果它表示一个 T 消费者,则使用 <code><? super T></code>。<strong>不要使用有界通配符类型作为返回类型。</strong> 它将强制用户在客户端代码中使用通配符类型,而不是为用户提供额外的灵活性</p><h4 id="一个例子"><a class="markdownIt-Anchor" href="#一个例子"></a> 一个例子</h4><p>接下来让我们将注意力转移到 Item-30 中的 max 方法,以下是原始声明:</p><pre class="highlight"><code class="java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <T <span class="hljs-keyword">extends</span> <span class="hljs-title class_">Comparable</span><T>> T <span class="hljs-title function_">max</span><span class="hljs-params">(List<T> list)</span></code></pre><p>下面是使用通配符类型的修正声明:</p><pre class="highlight"><code class="java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <T <span class="hljs-keyword">extends</span> <span class="hljs-title class_">Comparable</span><? <span class="hljs-built_in">super</span> T>> T <span class="hljs-title function_">max</span><span class="hljs-params">(List<? extends T> list)</span></code></pre><p>这里使用了两次 PECS。第一次是参数列表,list作为生产者生成 T 的实例,所以我们将类型从 <code>List<T></code> 更改为 <code>List<? extends T></code>(个人认为这里没有必要改,调用一次max方法只能传入一个类型的List,如果是两个参数,第一个参数的类型决定了T,第二个参数的类型如果是List<? extends T>,那么类型参数必须是T或T的子类。但是如果是泛型类声明的T,那这个改动是有意义的)。第二次是是类型参数 T。这是我们第一次看到通配符应用于类型参数。最初,T 被指定为继承 <code>Comparable<T></code>,但是 Comparable<T> 消费 T 实例。因此,将参数化类型 <code>Comparable<T></code> 替换为有界通配符类型 <code>Comparable<? super T></code>,Comparable 始终是消费者,所以一般应优先使用 <code>Comparable<? super T></code> 而不是 <code>Comparable<T></code>,比较器也是如此;因此,通常应该优先使用 <code>Comparator<? super T></code> 而不是 <code>Comparator<T></code>。</p><p>修订后的 max 声明可能是本书中最复杂的方法声明。增加的复杂性真的能给你带来什么好处吗?是的。下面是一个简单的列表案例,它在原来的声明中不允许使用,但经修改的声明允许:</p><pre class="highlight"><code class="java">List<ScheduledFuture<?>> scheduledFutures = ... ;</code></pre><p>不能将原始方法声明应用于此列表的原因是 ScheduledFuture 没有实现 <code>Comparable<ScheduledFuture></code>。相反,它是 Delayed 的一个子接口,继承了 <code>Comparable<Delayed></code>。换句话说,ScheduledFuture 的实例不仅仅可以与其它 ScheduledFuture 实例进行比较,还可以与任何 Delayed 实例比较,但是原始方法只能和 ScheduledFuture 实例比较。更通俗来说,通配符用于支持不直接实现 Comparable(或 Comparator)但继承了实现 Comparable(或 Comparator)的类型的类型。</p><h4 id="类型参数和通配符的对偶性"><a class="markdownIt-Anchor" href="#类型参数和通配符的对偶性"></a> 类型参数和通配符的对偶性</h4><p>类型参数和通配符之间存在对偶性,对偶性指实现方式不同但效果一样。例如,下面是swap方法的两种可能声明,第一个使用无界类型参数(Item-30),第二个使用无界通配符:</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Two possible declarations for the swap method</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <E> <span class="hljs-keyword">void</span> <span class="hljs-title function_">swap</span><span class="hljs-params">(List<E> list, <span class="hljs-type">int</span> i, <span class="hljs-type">int</span> j)</span>;<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">swap</span><span class="hljs-params">(List<?> list, <span class="hljs-type">int</span> i, <span class="hljs-type">int</span> j)</span>;</code></pre><p>这两个声明哪个更好?在公共API中第二个更好,因为它更简单(客户端可以传入原始类型)。传入一个任意列表,该方法交换索引元素,不需要担心类型参数。通常,如果类型参数在方法声明中只出现一次,则用通配符替换它。如果它是一个无界类型参数,用一个无界通配符替换它;如果它是有界类型参数,则用有界通配符替换它。</p><p>交换的第二个声明有一个问题。下面的实现无法编译, list 的类型是 <code>List<?></code>,你不能在 <code>List<?></code> 中放入除 null 以外的任何值。</p><pre class="highlight"><code class="java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">swap</span><span class="hljs-params">(List<?> list, <span class="hljs-type">int</span> i, <span class="hljs-type">int</span> j)</span> { list.set(i, list.set(j, list.get(i)));}</code></pre><p>幸运的是,有一种方法可以实现,而无需求助于不安全的强制类型转换或原始类型。其思想是编写一个私有助手方法来捕获通配符类型。为了捕获类型,helper 方法必须是泛型方法。它看起来是这样的:</p><pre class="highlight"><code class="java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">swap</span><span class="hljs-params">(List<?> list, <span class="hljs-type">int</span> i, <span class="hljs-type">int</span> j)</span> { swapHelper(list, i, j);}<span class="hljs-comment">// Private helper method for wildcard capture</span><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <E> <span class="hljs-keyword">void</span> <span class="hljs-title function_">swapHelper</span><span class="hljs-params">(List<E> list, <span class="hljs-type">int</span> i, <span class="hljs-type">int</span> j)</span> { list.set(i, list.set(j, list.get(i)));}</code></pre><p>swapHelper 方法指导 list 是一个 <code>List<E></code>。因此,它指导它从这个列表中得到的任何值都是 E 类型的,并且将 E 类型的任何值放入这个列表中都是安全的。这个稍微复杂的实现可以正确编译。它允许我们导出基于通配符的声明,同时在内部利用更复杂的泛型方法。swap 方法的客户端不必面对更复杂的 swapHelper 声明,但它们确实从中受益。值得注意的是,helper 方法具有我们认为对于公共方法过于复杂而忽略的签名。</p><p>总之,在 API 中使用通配符类型虽然很棘手,但可以使其更加灵活。如果你编写的库将被广泛使用,则必须考虑通配符类型的正确使用。记住基本规则:生产者使用 extends,消费者使用 super(PECS)。还要记住,所有的 comparable 和 comparator 都是消费者</p><h3 id="32-明智地合用泛型和可变参数"><a class="markdownIt-Anchor" href="#32-明智地合用泛型和可变参数"></a> 32. 明智地合用泛型和可变参数</h3><h4 id="抽象泄露"><a class="markdownIt-Anchor" href="#抽象泄露"></a> 抽象泄露</h4><p>可变参数方法(Item-53)和泛型都是在 Java 5 中添加,因此你可能认为它们能够优雅地交互;可悲的是,它们并不能。可变参数的目的是允许客户端向方法传递可变数量的参数,但这是一个抽象泄漏:当你调用可变参数方法时,将创建一个数组来保存参数;该数组本应是实现细节,却是可见的。因此,当可变参数具有泛型或参数化类型时,会出现令人困惑的编译器警告。</p><h4 id="不可具体化"><a class="markdownIt-Anchor" href="#不可具体化"></a> 不可具体化</h4><p>回想一下 Item-28,不可具体化类型是指其运行时表示的信息少于其编译时表示的信息,并且几乎所有泛型和参数化类型都是不可具体化的。如果方法声明其可变参数为不可具体化类型,编译器将在声明生生成警告。如果方法是在其推断类型不可具体化的可变参数上调用的,编译器也会在调用时生成警告。生成的警告就像这样:</p><pre class="highlight"><code class="java">warning: [unchecked] Possible heap pollution from parameterized vararg type List<String></code></pre><h4 id="堆污染"><a class="markdownIt-Anchor" href="#堆污染"></a> 堆污染</h4><p>当参数化类型的变量引用不属于该类型的对象时,就会发生堆污染。它会导致编译器自动生成的强制类型转换失败,违反泛型系统的基本保证。</p><p>例如,考虑这个方法,它来自 Item-26,但做了些修改:</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Mixing generics and varargs can violate type safety!</span><span class="hljs-comment">// 泛型和可变参数混合使用可能违反类型安全原则!</span><span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">dangerous</span><span class="hljs-params">(List<String>... stringLists)</span> { List<Integer> intList = List.of(<span class="hljs-number">42</span>); Object[] objects = stringLists; objects[<span class="hljs-number">0</span>] = intList; <span class="hljs-comment">// Heap pollution</span> <span class="hljs-type">String</span> <span class="hljs-variable">s</span> <span class="hljs-operator">=</span> stringLists[<span class="hljs-number">0</span>].get(<span class="hljs-number">0</span>); <span class="hljs-comment">// ClassCastException</span>}</code></pre><h4 id="方法声明使用泛型可变参数是合法的"><a class="markdownIt-Anchor" href="#方法声明使用泛型可变参数是合法的"></a> 方法声明使用泛型可变参数是合法的</h4><p>这个例子提出了一个有趣的问题:为什么方法声明中使用泛型可变参数是合法的,而显式创建泛型数组是非法的?答案是,带有泛型或参数化类型的可变参数的方法在实际开发中非常有用,因此语言设计人员选择忍受这种不一致性。事实上,Java 库导出了几个这样的方法,包括 <code>Arrays.asList(T... a)</code>、<code>Collections.addAll(Collection<? super T> c, T... elements)</code> 以及 <code>EnumSet.of(E first, E... rest)</code>。它们与前面显示的危险方法不同,这些库方法是类型安全的。</p><p>在 Java 7 之前,使用泛型可变参数的方法的作者对调用点上产生的警告无能为力。使得这些 API 难以使用。用户必须忍受这些警告,或者在每个调用点(Item-27)使用 @SuppressWarnings(“unchecked”) 注释消除这些警告。这种做法乏善可陈,既损害了可读性,也忽略了标记实际问题的警告。</p><p>在 Java 7 中添加了 SafeVarargs 注释,以允许使用泛型可变参数的方法的作者自动抑制客户端警告。本质上,SafeVarargs 注释构成了方法作者的一个承诺,即该方法是类型安全的。 作为这个承诺的交换条件,编译器同意不对调用可能不安全的方法的用户发出警告。</p><h4 id="使用safevarargs的条件"><a class="markdownIt-Anchor" href="#使用safevarargs的条件"></a> 使用SafeVarargs的条件</h4><ol><li>方法没有修改数组元素</li><li>数组的引用没有逃逸(这会使不受信任的代码能够访问数组)</li></ol><p>换句话说,如果可变参数数组仅用于将可变数量的参数从调用方传输到方法(毕竟这是可变参数的目的),那么该方法是安全的。</p><h5 id="一个反例"><a class="markdownIt-Anchor" href="#一个反例"></a> 一个反例</h5><pre class="highlight"><code class="java"><span class="hljs-comment">// UNSAFE - Exposes a reference to its generic parameter array!</span><span class="hljs-keyword">static</span> <T> T[] toArray(T... args) { <span class="hljs-keyword">return</span> args;}</code></pre><p>这个方法直接返回了泛型可变参数数组引用,违反了上面的条件2,它可以将堆污染传播到调用堆栈上。</p><p>考虑下面的泛型方法,该方法接受三个类型为 T 的参数,并返回一个包含随机选择的两个参数的数组:</p><pre class="highlight"><code class="java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> { String[] attributes = pickTwo(<span class="hljs-string">"Good"</span>, <span class="hljs-string">"Fast"</span>, <span class="hljs-string">"Cheap"</span>);}<span class="hljs-keyword">static</span> <T> T[] pickTwo(T a, T b, T c) { <span class="hljs-keyword">switch</span>(ThreadLocalRandom.current().nextInt(<span class="hljs-number">3</span>)) { <span class="hljs-keyword">case</span> <span class="hljs-number">0</span>: <span class="hljs-keyword">return</span> toArray(a, b); <span class="hljs-keyword">case</span> <span class="hljs-number">1</span>: <span class="hljs-keyword">return</span> toArray(a, c); <span class="hljs-keyword">case</span> <span class="hljs-number">2</span>: <span class="hljs-keyword">return</span> toArray(b, c); } <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">AssertionError</span>(); <span class="hljs-comment">// Can't get here</span>}</code></pre><p>这段代码编译时不会生成任何警告,运行时会抛出 ClassCastException,尽管它不包含可见的强制类型转换。你没有看到的是,编译器在 pickTwo 返回的值上生成了一个隐藏的 String[] 转换。转换失败,因为 Object[] 实际指向的数组是Object类型的不是String,强转失败。</p><p>这个示例的目的是让人明白,让另一个方法访问泛型可变参数数组是不安全的,只有两个例外:将数组传递给另一个使用 @SafeVarargs 正确注释的可变参数方法是安全的,将数组传递给仅计算数组内容的某个函数的非可变方法也是安全的。</p><h6 id="扩展"><a class="markdownIt-Anchor" href="#扩展"></a> 扩展</h6><p>如果 main 方法直接调用 toArray 方法,不会出现 ClassCastException,为什么?泛型不是被擦除了吗,args在运行时不是一个Object[] 吗?非也,运行时args的类型是Object[],但是指向的数组是 String 类型的, 所以编译器添加的强转是可以成功的。下面的例子里,toArray1方法在字节码层面和toArray3方法是一样的,在调用toArray1方法前会先生成 String[],将引用传递给toArray1,</p><pre class="highlight"><code class="java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> { String [] arr1 = toArray1(<span class="hljs-string">"Alice"</span>, <span class="hljs-string">"Bob"</span>, <span class="hljs-string">"Cat"</span>); <span class="hljs-comment">// ClassCastException</span> String [] arr2 = toArray2(<span class="hljs-string">"Alice"</span>, <span class="hljs-string">"Bob"</span>, <span class="hljs-string">"Cat"</span>); String [] arr3 = toArray3(<span class="hljs-keyword">new</span> <span class="hljs-title class_">String</span>[]{<span class="hljs-string">"Alice"</span>, <span class="hljs-string">"Bob"</span>, <span class="hljs-string">"Cat"</span>}); <span class="hljs-comment">// ClassCastException,多层调用泛型可变参数数组会导致编译器没有足够信息创建真实类型的对象数组(存疑,从字节码来看也是创建了String[])</span> String[] arr4 = toArray1Crust1(<span class="hljs-string">"Alice"</span>, <span class="hljs-string">"Bob"</span>, <span class="hljs-string">"Cat"</span>); String[] arr5 = toArray1Crust2(<span class="hljs-keyword">new</span> <span class="hljs-title class_">String</span>[]{<span class="hljs-string">"Alice"</span>, <span class="hljs-string">"Bob"</span>, <span class="hljs-string">"Cat"</span>}); String[] arr6 = toArray1Crust3(<span class="hljs-keyword">new</span> <span class="hljs-title class_">String</span>[]{<span class="hljs-string">"Alice"</span>, <span class="hljs-string">"Bob"</span>, <span class="hljs-string">"Cat"</span>}); <span class="hljs-comment">// 泛型方法嵌套没问题</span> <span class="hljs-type">String</span> <span class="hljs-variable">s</span> <span class="hljs-operator">=</span> toString1(<span class="hljs-string">"Alice"</span>);}<span class="hljs-keyword">static</span> <T> T[] toArray1(T... args) { <span class="hljs-keyword">return</span> args;}<span class="hljs-keyword">static</span> <T> T[] toArray2(T a, T b, T c) { System.out.println(a.getClass()); <span class="hljs-comment">// 泛型数组实际类型是Object</span> T[] result = (T[]) <span class="hljs-keyword">new</span> <span class="hljs-title class_">Object</span>[<span class="hljs-number">3</span>]; result[<span class="hljs-number">0</span>] = a; result[<span class="hljs-number">1</span>] = b; result[<span class="hljs-number">2</span>] = c; <span class="hljs-keyword">return</span> result;}<span class="hljs-keyword">static</span> <T> T[] toArray3(T[] arr){ <span class="hljs-keyword">return</span> arr;}<span class="hljs-keyword">static</span> <T> T[] toArray1Crust1(T...args){ <span class="hljs-keyword">return</span> toArray1(args);}<span class="hljs-keyword">static</span> <T> T[] toArray1Crust2(T[] args){ <span class="hljs-keyword">return</span> toArray1(args);}<span class="hljs-keyword">static</span> String[] toArray1Crust3(String[] args){ <span class="hljs-keyword">return</span> toArray1(args);}<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <T> T <span class="hljs-title function_">toString1</span><span class="hljs-params">(T s)</span> { <span class="hljs-keyword">return</span> toString2(s);}<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <T> T <span class="hljs-title function_">toString2</span><span class="hljs-params">(T s)</span>{ <span class="hljs-keyword">return</span> s;}</code></pre><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E7%AC%AC%E4%BA%94%E7%AB%A0%20%E6%B3%9B%E5%9E%8B/IMG_20221209-134128538.png" alt="图 1" /></p><h5 id="一个正确例子"><a class="markdownIt-Anchor" href="#一个正确例子"></a> 一个正确例子</h5><p>下面是一个安全使用泛型可变参数的典型示例。该方法接受任意数量的列表作为参数,并返回一个包含所有输入列表的元素的序列列表。因为该方法是用 @SafeVarargs 注释的,所以它不会在声明或调用点上生成任何警告:</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Safe method with a generic varargs parameter</span><span class="hljs-meta">@SafeVarargs</span><span class="hljs-keyword">static</span> <T> List<T> <span class="hljs-title function_">flatten</span><span class="hljs-params">(List<? extends T>... lists)</span> { List<T> result = <span class="hljs-keyword">new</span> <span class="hljs-title class_">ArrayList</span><>(); <span class="hljs-keyword">for</span> (List<? <span class="hljs-keyword">extends</span> <span class="hljs-title class_">T</span>> list : lists) result.addAll(list); <span class="hljs-keyword">return</span> result;}</code></pre><h4 id="何时使用safevarargs"><a class="markdownIt-Anchor" href="#何时使用safevarargs"></a> 何时使用SafeVarargs</h4><p>决定何时使用 SafeVarargs 注释的规则很简单:在每个带有泛型或参数化类型的可变参数的方法上使用 @SafeVarargs,这样它的用户就不会被不必要的和令人困惑的编译器警告所困扰。</p><p>请注意,SafeVarargs 注释只能出现在不能覆盖的方法上,因为不可能保证所有可能覆盖的方法都是安全的。在 Java 8 中,注释只能出现在静态方法和final实例方法;在 Java 9 中,它在私有实例方法上也是合法的。</p><p>使用 SafeVarargs 注释的另一种选择是接受 Item-28 的建议,并用 List 参数替换可变参数(它是一个伪装的数组)。下面是将这种方法应用到我们的 flatten 方法时的效果。注意,只有参数声明发生了更改:</p><pre class="highlight"><code class="java"><span class="hljs-comment">// List as a typesafe alternative to a generic varargs parameter</span><span class="hljs-keyword">static</span> <T> List<T> <span class="hljs-title function_">flatten</span><span class="hljs-params">(List<List<? extends T>> lists)</span> { List<T> result = <span class="hljs-keyword">new</span> <span class="hljs-title class_">ArrayList</span><>(); <span class="hljs-keyword">for</span> (List<? <span class="hljs-keyword">extends</span> <span class="hljs-title class_">T</span>> list : lists) result.addAll(list); <span class="hljs-keyword">return</span> result;}</code></pre><p>然后可以将此方法与静态工厂方法 List.of 一起使用,以允许可变数量的参数。注意,这种方法依赖于 List.of 声明是用 @SafeVarargs 注释的:</p><pre class="highlight"><code class="java">audience = flatten(List.of(friends, romans, countrymen));</code></pre><p>这种方法的优点是编译器可以证明该方法是类型安全的。你不必使用 SafeVarargs 注释来保证它的安全性,也不必担心在确定它的安全性时可能出错。主要的缺点是客户端代码比较冗长,可能会比较慢。</p><p>这种技巧也可用于无法编写安全的可变参数方法的情况,如第 147 页中的 toArray 方法。它的列表类似于 List.of 方法,我们甚至不用写;Java 库的作者为我们做了这些工作。pickTwo 方法变成这样:</p><pre class="highlight"><code class="java"><span class="hljs-keyword">static</span> <T> List<T> <span class="hljs-title function_">pickTwo</span><span class="hljs-params">(T a, T b, T c)</span> { <span class="hljs-keyword">switch</span>(rnd.nextInt(<span class="hljs-number">3</span>)) { <span class="hljs-keyword">case</span> <span class="hljs-number">0</span>: <span class="hljs-keyword">return</span> List.of(a, b); <span class="hljs-keyword">case</span> <span class="hljs-number">1</span>: <span class="hljs-keyword">return</span> List.of(a, c); <span class="hljs-keyword">case</span> <span class="hljs-number">2</span>: <span class="hljs-keyword">return</span> List.of(b, c); } <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">AssertionError</span>();}</code></pre><p>main 方法是这样的:</p><pre class="highlight"><code class="java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> { List<String> attributes = pickTwo(<span class="hljs-string">"Good"</span>, <span class="hljs-string">"Fast"</span>, <span class="hljs-string">"Cheap"</span>);}</code></pre><p>生成的代码是类型安全的,因为它只使用泛型,而不使用数组(List在运行时没有强转,数组会强转)。</p><p>总之,可变参数方法和泛型不能很好地交互,并且数组具有与泛型不同的类型规则。虽然泛型可变参数不是类型安全的,但它们是合法的。如果选择使用泛型(或参数化)可变参数编写方法,首先要确保该方法是类型安全的,然后使用 @SafeVarargs 对其进行注释。</p><h3 id="33-考虑类型安全的异构容器"><a class="markdownIt-Anchor" href="#33-考虑类型安全的异构容器"></a> 33. 考虑类型安全的异构容器</h3><h4 id="参数化容器"><a class="markdownIt-Anchor" href="#参数化容器"></a> 参数化容器</h4><p>如果你需要存储某种类型的集合,例如存储String的Set,那么<code>Set<String></code>就足够了,又比如存储 String-Value 键值对,那么<code>Map<String,Integer></code>就足够了</p><h4 id="参数化容器的键"><a class="markdownIt-Anchor" href="#参数化容器的键"></a> 参数化容器的键</h4><p>如果你要存储任意类型的集合。例如,一个数据库行可以有任意多列,我们希望用一个Map保存该行每列元素。那么我们可以使用参数化容器的键</p><h4 id="例子"><a class="markdownIt-Anchor" href="#例子"></a> 例子</h4><p>Favorites 类,允许客户端存储和检索任意多种类型对象。Class 类的对象将扮演参数化键的角色。Class 对象被称为类型标记</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Typesafe heterogeneous container pattern - client</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> { <span class="hljs-type">Favorites</span> <span class="hljs-variable">f</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Favorites</span>(); f.putFavorite(String.class, <span class="hljs-string">"Java"</span>); f.putFavorite(Integer.class, <span class="hljs-number">0xcafebabe</span>); f.putFavorite(Class.class, Favorites.class); <span class="hljs-type">String</span> <span class="hljs-variable">favoriteString</span> <span class="hljs-operator">=</span> f.getFavorite(String.class); <span class="hljs-type">int</span> <span class="hljs-variable">favoriteInteger</span> <span class="hljs-operator">=</span> f.getFavorite(Integer.class); Class<?> favoriteClass = f.getFavorite(Class.class); System.out.printf(<span class="hljs-string">"%s %x %s%n"</span>, favoriteString,favoriteInteger, favoriteClass.getName());}</code></pre><p>Favorites实例是类型安全的:当你向它请求一个 String 类型时,它永远不会返回一个 Integer 类型。<br />Favorites实例也是异构的:所有键都是不同类型的,普通 Map 的键是固定一个类型,因此,我们将 Favorites 称为一个<strong>类型安全异构容器</strong>。</p><p>Favorites 的实现非常简短:</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Typesafe heterogeneous container pattern - implementation</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">Favorites</span> { <span class="hljs-keyword">private</span> Map<Class<?>, Object> favorites = <span class="hljs-keyword">new</span> <span class="hljs-title class_">HashMap</span><>(); <span class="hljs-keyword">public</span> <T> <span class="hljs-keyword">void</span> <span class="hljs-title function_">putFavorite</span><span class="hljs-params">(Class<T> type, T instance)</span> { favorites.put(Objects.requireNonNull(type), instance); } <span class="hljs-keyword">public</span> <T> T <span class="hljs-title function_">getFavorite</span><span class="hljs-params">(Class<T> type)</span> { <span class="hljs-keyword">return</span> type.cast(favorites.get(type)); }}</code></pre><p>通配符类型的键意味着每个键都可以有不同的参数化类型:一个可以是 <code>Class<String></code>,下一个是 <code>Class<Integer></code>,等等。这就是异构的原理。</p><p>favorites 的值类型仅仅是 Object。换句话说,Map 不保证键和值之间的类型关系</p><p>putFavorite 的实现很简单:它只是将从给定 Class 对象到给定对象的映射关系放入 favorites 中。如前所述,这将丢弃键和值之间的「类型关联」;将无法确定值是键的实例。但这没关系,因为 getFavorites 方法可以重新建立这个关联。</p><p>getFavorite 的实现比 putFavorite 的实现更复杂。首先,它从 favorites 中获取与给定 Class 对象对应的值。这是正确的对象引用返回,但它有错误的编译时类型:它是 Object(favorites 的值类型),我们需要返回一个 T。因此,getFavorite 的实现通过使用 Class 的 cast 方法,将对象引用类型动态转化为所代表的 Class 对象。</p><p>cast 方法是 Java 的 cast 运算符的动态模拟。它只是检查它的参数是否是类对象表示的类型的实例。如果是,则返回参数;否则它将抛出 ClassCastException。我们知道 getFavorite 中的强制转换调用不会抛出 ClassCastException。也就是说,我们知道 favorites 中的值总是与其键的类型匹配。</p><p>如果 cast 方法只是返回它的参数,那么它会为我们做什么呢?cast 方法的签名充分利用了 Class 类是泛型的这一事实。其返回类型为 Class 对象的类型参数:</p><pre class="highlight"><code class="java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">Class</span><T> { T <span class="hljs-title function_">cast</span><span class="hljs-params">(Object obj)</span>;}</code></pre><p>这正是 getFavorite 方法所需要的。它使我们能够使 Favorites 类型安全,而不需要对 T 进行 unchecked 的转换。</p><p>Favorites 类有两个值得注意的限制。</p><ul><li><p>恶意客户端很容易通过使用原始形式的类对象破坏 Favorites 实例的类型安全。通过使用原始类型 HashSet(Item-26),可以轻松地将 String 类型放入 <code>HashSet<Integer></code> 中。为了获得运行时的类型安全,让 putFavorite 方法检查实例是否是 type 表示的类型的实例,使用动态转换:</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Achieving runtime type safety with a dynamic cast</span><span class="hljs-keyword">public</span> <T> <span class="hljs-keyword">void</span> <span class="hljs-title function_">putFavorite</span><span class="hljs-params">(Class<T> type, T instance)</span> { favorites.put(type, type.cast(instance));}</code></pre></li><li><p>Favorites 类不能用于不可具体化的类型(Item-28)。换句话说,你可以存储的 Favorites 实例类型为 String 或 String[],但不能存储 <code>List<String></code>。原因是你不能为 <code>List<String></code> 获取 Class 对象,<code>List<String>.class</code> 是一个语法错误</p></li></ul><p>Favorites 使用的类型标记是无界的:getFavorite 和 put-Favorite 接受任何 Class 对象。有时你可能需要限制可以传递给方法的类型。这可以通过有界类型标记来实现,它只是一个类型标记,使用有界类型参数(Item-30)或有界通配符(Item-31)对可以表示的类型进行绑定。</p><p>annotation API(Item-39)广泛使用了有界类型标记。例如,下面是在运行时读取注释的方法。这个方法来自 AnnotatedElement 接口,它是由表示类、方法、字段和其他程序元素的反射类型实现的:</p><pre class="highlight"><code class="java"><span class="hljs-keyword">public</span> <T <span class="hljs-keyword">extends</span> <span class="hljs-title class_">Annotation</span>> T <span class="hljs-title function_">getAnnotation</span><span class="hljs-params">(Class<T> annotationType)</span>;</code></pre><p>参数 annotationType 是表示注释类型的有界类型标记。该方法返回该类型的元素注释(如果有的话),或者返回 null(如果没有的话)。本质上,带注释的元素是一个类型安全的异构容器,其键是注释类型。</p><p>假设你有一个 <code>Class<?></code> 类型的对象,并且希望将其传递给一个需要有界类型标记(例如 getAnnotation)的方法。你可以将对象强制转换为 <code>Class<? extends Annotation></code>,但是这个强制转换是 unchecked 的,因此它将生成一个编译时警告(Item-27)。幸运的是,Class 类提供了一个实例方法,可以安全地(动态地)执行这种类型的强制转换。该方法叫做 asSubclass,它将 Class 对象强制转换为它所调用的类对象,以表示由其参数表示的类的子类。如果转换成功,则该方法返回其参数;如果失败,则抛出 ClassCastException。</p><p>下面是如何使用 asSubclass 方法读取在编译时类型未知的注释。这个方法编译没有错误或警告:</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Use of asSubclass to safely cast to a bounded type token</span><span class="hljs-keyword">static</span> Annotation <span class="hljs-title function_">getAnnotation</span><span class="hljs-params">(AnnotatedElement element,String annotationTypeName)</span> { Class<?> annotationType = <span class="hljs-literal">null</span>; <span class="hljs-comment">// Unbounded type token</span> <span class="hljs-keyword">try</span> { annotationType = Class.forName(annotationTypeName); } <span class="hljs-keyword">catch</span> (Exception ex) { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">IllegalArgumentException</span>(ex); } <span class="hljs-keyword">return</span> element.getAnnotation(annotationType.asSubclass(Annotation.class));}</code></pre><p>总之,以集合的 API 为例的泛型在正常使用时将每个容器的类型参数限制为固定数量。你可以通过将类型参数放置在键上而不是容器上来绕过这个限制。你可以使用 Class 对象作为此类类型安全异构容器的键。以这种方式使用的 Class 对象称为类型标记。还可以使用自定义键类型。例如,可以使用 DatabaseRow 类型表示数据库行(容器),并使用泛型类型 <code>Column<T></code> 作为它的键</p>]]></content>
<summary type="html"><h2 id="第五章-泛型"><a class="markdownIt-Anchor" href="#第五章-泛型"></a> 第五章 泛型</h2>
<h3 id="26-不要使用原始类型"><a class="markdownIt-Anchor" href="#26-不要使</summary>
<category term="Java" scheme="https://zunpan.github.io/categories/Java/"/>
<category term="Effective-Java" scheme="https://zunpan.github.io/tags/Effective-Java/"/>
<category term="泛型" scheme="https://zunpan.github.io/tags/%E6%B3%9B%E5%9E%8B/"/>
</entry>
<entry>
<title>Effective-Java学习笔记(三)</title>
<link href="https://zunpan.github.io/2022/07/07/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%89%EF%BC%89/"/>
<id>https://zunpan.github.io/2022/07/07/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%89%EF%BC%89/</id>
<published>2022-07-07T14:31:09.000Z</published>
<updated>2023-09-24T04:27:40.274Z</updated>
<content type="html"><![CDATA[<h2 id="第四章-类和接口"><a class="markdownIt-Anchor" href="#第四章-类和接口"></a> 第四章 类和接口</h2><h3 id="15-尽量减少类和成员的可访问性"><a class="markdownIt-Anchor" href="#15-尽量减少类和成员的可访问性"></a> 15. 尽量减少类和成员的可访问性</h3><h4 id="信息隐藏的作用"><a class="markdownIt-Anchor" href="#信息隐藏的作用"></a> 信息隐藏的作用</h4><ol><li>将API与实现完全分离。组件之间只能通过它们的API进行通信,而不知道彼此的内部工作方式。</li><li>解耦系统的组件,允许它们被独立开发、测试、优化、使用、理解和修改。</li><li>增加了软件的复用性,降低了构建大型系统的风险,即使系统没有成功,单个组件也可能被证明是成功的</li></ol><h4 id="访问控制机制"><a class="markdownIt-Anchor" href="#访问控制机制"></a> 访问控制机制</h4><p>访问控制机制指定了类、接口和成员的可访问性。</p><ol><li><p>对于顶级(非嵌套)类和接口,只有两个可能的访问级别:包私有和公共。如果用 public 修饰符声明,它将是公共的,即API的一部分,修改会损害客户端;否则,它将是包私有的。</p></li><li><p>如果包私有顶级类或接口只被一个类使用,那么可以考虑变成这个类的私有静态嵌套类(Item-24)</p></li><li><p>对于成员(字段、方法、嵌套类和嵌套接口),有四个访问级别,这里按可访问性依次递增的顺序列出:</p><ul><li>私有,成员只能从声明它的顶级类内部访问</li><li>包私有,成员可以从声明它的类的包中访问,包私有就是默认访问,即如果没有指定访问修饰符(接口的成员除外,默认情况下,接口的成员是公共的),就是这个访问级别</li><li>保护,成员可以从声明它的类的子类和声明它的类的包中访问</li><li>公共,成员可以从任何地方访问</li></ul></li></ol><h4 id="设计类和成员的可访问性"><a class="markdownIt-Anchor" href="#设计类和成员的可访问性"></a> 设计类和成员的可访问性</h4><p>总体规则:让每个类或成员尽可能不可访问。换句话说,在不影响软件正常功能时,使用尽可能低的访问级别。</p><ol><li><p>仔细设计类的公共API,所有成员声明为私有。私有成员和包私有成员都是类实现的一部分,通常不会影响其导出的API。但是,如果类实现了Serializable(Item-86和Item-87),这些字段可能会「泄漏」到导出的 API 中</p></li><li><p>少用保护成员。保护成员是类导出API的一部分,必须永远支持</p></li><li><p>子类覆盖父类的方法,可访问性不能缩小</p></li><li><p>为了测试,将公共类的成员由私有变为包私有是可以接受的,但是进一步提高可访问性是不可接受的</p></li><li><p>带有公共可变字段的类通常不是线程安全的</p></li><li><p>为了得到不可变数组,不要公开或通过访问器返回长度非零的数组的引用,客户端将能够修改数组的内容。可以将公共数组设置为私有,并添加一个公共不可变 List;或者,将数组设置为私有,并添加一个返回私有数组副本的公共方法:</p></li></ol><p>总之,你应该尽可能减少程序元素的可访问性(在合理的范围内)。在仔细设计了一个最小的公共 API 之后,你应该防止任何游离的类、接口或成员成为 API 的一部分。除了作为常量的公共静态 final 字段外,公共类应该没有公共字段。确保public static final 字段引用的对象是不可变的。</p><h3 id="16-在公共类中使用访问器方法而不是公共字段"><a class="markdownIt-Anchor" href="#16-在公共类中使用访问器方法而不是公共字段"></a> 16. 在公共类中,使用访问器方法,而不是公共字段</h3><h4 id="访问器方法的作用"><a class="markdownIt-Anchor" href="#访问器方法的作用"></a> 访问器方法的作用</h4><p>如果类是公共的,即可以在包外访问,提供访问器方法可以维持类内部数据表示形式的灵活性</p><h4 id="一些建议"><a class="markdownIt-Anchor" href="#一些建议"></a> 一些建议</h4><ol><li>如果类是包私有或者是私有嵌套类,公开数据字段比提供访问器方法更清晰</li><li>公共类公开不可变字段的危害会小一点,但仍不建议公开字段</li></ol><h3 id="17-减少可变性"><a class="markdownIt-Anchor" href="#17-减少可变性"></a> 17. 减少可变性</h3><p>不可变类是实例创建后不能被修改的类。Java库包含许多不可变的类,包括 String、基本类型的包装类、BigInteger和BigDecimal。</p><h4 id="创建不可变类的规则"><a class="markdownIt-Anchor" href="#创建不可变类的规则"></a> 创建不可变类的规则</h4><p>要使类不可变,请遵守以下5条规则:</p><ol><li>不要提供修改对象状态的方法</li><li>确保类不能被继承。这可以防止可变子类损害父类的不可变。防止子类化通常用 final 修饰父类</li><li>所有字段用 final 修饰。</li><li>所有字段设为私有</li><li>确保对任何可变组件的独占访问。如果你的类有任何引用可变对象的字段,请确保该类的客户端无法获得对这些对象的引用。</li></ol><h4 id="不可变类的优点"><a class="markdownIt-Anchor" href="#不可变类的优点"></a> 不可变类的优点</h4><ol><li>不可变对象线程安全,复用程度高开销小,不需要防御性拷贝</li><li>不仅可以复用不可变对象,还可以复用它们的内部实现</li><li>不可变对象很适合作为其它对象的构建模块,例如 Map 的键和 Set 的元素</li><li>不可变对象自带提供故障原子性</li></ol><h4 id="不可变类的缺点"><a class="markdownIt-Anchor" href="#不可变类的缺点"></a> 不可变类的缺点</h4><ol><li><p>不可变类的主要缺点是每个不同的值都需要一个单独的对象。不可变类BigInteger的flipBit方法会创建一个和原来对象只有一个bit不同的新对象。可变类BigSet可以在固定时间内修改对象单个bit</p></li><li><p>如果执行多步操作,在每一步生成一个新对象,最终丢弃除最终结果之外的所有对象,那么性能问题就会被放大。解决方法是使用伴随类,如果能预测客户端希望在不可变类上执行哪些复杂操作就可以使用包私有可变伴随可变类(BigInteger和内部的伴随类);否则提供一个公共可变伴随类,例如String和它的伴随类StringBuilder</p></li></ol><h4 id="设计不可变类"><a class="markdownIt-Anchor" href="#设计不可变类"></a> 设计不可变类</h4><ol><li>为了保证类不被继承,可以用final修饰类,也可以将构造函数变为私有或包私有,通过静态工厂提供对象</li><li>BigInteger 和 BigDecimal没有声明为final,为了确保你的类依赖是不可变的BigInteger或BigDecimal,你需要检查对象类型,如果是BigInteger或BigDecimal的子类,那必须防御性复制</li><li>适当放松不可变类所有字段必须是 final 的限制可以提供性能,例如,用可变字段缓存计算结果,例如 hashCode 方法在第一次调用时缓存了hash</li><li>如果你选择让不可变类实现 Serializable,并且该类包含一个或多个引用可变对象的字段,那么你必须提供一个显式的 readObject 或 readResolve 方法,或者使用 ObjectOutputStream.writeUnshared 或 ObjectInputStream.readUnshared 方法</li></ol><h3 id="18-优先选择组合而不是继承"><a class="markdownIt-Anchor" href="#18-优先选择组合而不是继承"></a> 18. 优先选择组合而不是继承</h3><p>在同一个包中使用继承是安全的,因为子类和父类的实现都由相同程序员控制。在对专为继承而设计和有文档的类时使用继承也是安全的(Item-19)。然而,对普通的非抽象类进行跨包继承是危险的。与方法调用不同,继承破坏了封装性。换句话说,子类的功能正确与否依赖于它的父类的实现细节。父类的实现可能在版本之间发生变化,如果发生了变化,子类可能会崩溃,即使子类的代码没有被修改过。因此,子类必须与其父类同步更新,除非父类是专门为继承的目的而设计的,并具有很明确的文档说明。</p><h4 id="继承的风险"><a class="markdownIt-Anchor" href="#继承的风险"></a> 继承的风险</h4><ol><li>子类覆盖父类的多个方法,父类的多个方法之间有调用关系,因为多态,父类方法在调用其它父类方法时会调用到子类的方法</li><li>父类可以添加新方法,新方法没有确保在添加的元素满足断言,子类没有覆盖这个方法,导致调用这个方法时添加了非法元素</li><li>父类添加了新方法,但是子类继承原来的父类时也添加了相同签名和不同返回类型的方法,这时子类不能编译,如果签名和返回类型都相同,必须声明覆盖</li></ol><h4 id="组合"><a class="markdownIt-Anchor" href="#组合"></a> 组合</h4><p>为新类提供一个引用现有类实例的私有字段,这种设计称为组合,因为现有的类是新类的一个组件。新类中的每个实例方法调用现有类实例的对应方法,并返回结果,这称为转发。比较好的写法是包装类+转发类。</p><h4 id="总结"><a class="markdownIt-Anchor" href="#总结"></a> 总结</h4><p>只有子类确实是父类的子类型的情况下,继承才合适。换句话说,两个类 A、B 之间只有 B 满足「is-a」关系时才应该扩展 A。如果你想让 B 扩展 A,那就问问自己:每个 B 都是 A 吗?如果不能对这个问题给出肯定回答,B 不应该扩展 A;如果答案是否定的,通常情况下,B 应该包含 A 的私有实例并暴露不同的 API:A 不是 B 的基本组成部分,而仅仅是其实现的一个细节。</p><h3 id="19-继承要设计良好并且有文档否则禁止使用"><a class="markdownIt-Anchor" href="#19-继承要设计良好并且有文档否则禁止使用"></a> 19. 继承要设计良好并且有文档,否则禁止使用</h3><p>必须精确地在文档中描述覆盖任何方法的效果。文档必须指出方法调用了哪些可覆盖方法、调用顺序以及每次调用的结果如何影响后续处理过程。描述由 Javadoc 标签 @implSpec 生成</p><h3 id="20-接口优于抽象类"><a class="markdownIt-Anchor" href="#20-接口优于抽象类"></a> 20. 接口优于抽象类</h3><p>Java 有两种机制来定义允许多重实现的类型:接口和抽象类。</p><h4 id="接口优势"><a class="markdownIt-Anchor" href="#接口优势"></a> 接口优势</h4><ol><li><p>可以定义 mixin(混合类型)<br />接口是定义 mixin(混合类型)的理想工具。粗略的说,mixin 是类除了「基本类型」之外还可以实现的类型,用于声明它提供了一些可选的行为。例如,Comparable 是一个 mixin 接口,它允许类的实例可以与其他的可相互比较的对象进行排序。这样的接口称为 mixin,因为它允许可选功能「混合」到基本类型中。抽象类不能用于定义 mixin,原因是:一个类不能有多个父类,而且在类层次结构中没有插入 mixin 的合理位置。‘</p></li><li><p>允许构造非层次化类型框架<br />如果系统中有 n 个属性(例如唱、跳、rap),如果每个属性组合都封装成一个抽象类,组成一个层次化的类型框架,总共有<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mn>2</mn><mi>n</mi></msup></mrow><annotation encoding="application/x-tex">2^n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.664392em;vertical-align:0em;"></span><span class="mord"><span class="mord">2</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.664392em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">n</span></span></span></span></span></span></span></span></span></span></span>个类,而接口只需要 n 个</p></li></ol><h4 id="接口劣势"><a class="markdownIt-Anchor" href="#接口劣势"></a> 接口劣势</h4><ol><li>接口不能给 equals 和 hashCode 方法提供默认实现</li><li>接口不允许包含实例字段或者非公共静态成员(私有静态方法除外)</li></ol><h4 id="结合接口和抽象类的优势"><a class="markdownIt-Anchor" href="#结合接口和抽象类的优势"></a> 结合接口和抽象类的优势</h4><p>模板方法模式:接口定义类型,提供默认方法,抽象类(骨架实现类)实现接口其余方法。继承骨架实现类已经完成直接实现接口的大部分工作。</p><p>按照惯例,骨架实现类称为 AbstractInterface,其中 Interface 是它们实现的接口的名称。例如 Collections Framework 提供了一个骨架实现来配合每个主要的集合接口:AbstractCollection、AbstractSet、AbstractList 和 AbstractMap。可以说,把它们叫做 SkeletalCollection、SkeletalSet、SkeletalList 和 SkeletalMap 更合理,但 Abstract 的用法现在已经根深蒂固。</p><h5 id="编写骨架实现类的过程"><a class="markdownIt-Anchor" href="#编写骨架实现类的过程"></a> 编写骨架实现类的过程</h5><p>研究接口有哪些方法,哪些方法可以提供默认实现,如果都可以提供默认实现就不需要骨架实现类,否则,声明一个实现接口的骨架实现类,实现所有剩余的接口方法</p><h3 id="21-为后代设计接口"><a class="markdownIt-Anchor" href="#21-为后代设计接口"></a> 21. 为后代设计接口</h3><p>Java 8 之前,往接口添加方法会导致实现它的类缺少方法,编译错误。Java 8 添加了默认方法,目的是允许向现有接口添加方法,但是向现有接口添加新方法有风险。</p><h4 id="默认方法的风险"><a class="markdownIt-Anchor" href="#默认方法的风险"></a> 默认方法的风险</h4><ol><li>接口实现类需要同步调用每个方法,但是没有覆盖接口新加入的默认方法,导致调用默认方法时出现 ConcurrentModificationException</li><li>接口的现有实现类可以在没有错误或警告的情况下通过编译,但是运行时会出错</li></ol><h3 id="22-接口只用于定义类型"><a class="markdownIt-Anchor" href="#22-接口只用于定义类型"></a> 22. 接口只用于定义类型</h3><p>接口只用于定义类型。类实现接口表明客户端可以用类的实例做什么。将接口定义为任何其他目的都是不合适的。</p><p>有一个反例是常量接口,接口内只包含 public static final 字段,它的问题在于类使用什么常量是实现细节,而实现常量接口会导致实现细节泄露到 API 中。</p><p>导出常量,有几个合理的选择。</p><ol><li>如果这些常量与现有的类或接口紧密绑定,则应该将它们添加到类或接口。例如,所有数值包装类,比如 Integer 和 Double,都导出 MIN_VALUE 和 MAX_VALUE 常量。</li><li>枚举或者不可实例化的工具类,使用工具类的常量推荐静态导入</li></ol><h3 id="23-类层次结构优于带标签的类"><a class="markdownIt-Anchor" href="#23-类层次结构优于带标签的类"></a> 23. 类层次结构优于带标签的类</h3><h4 id="标签类"><a class="markdownIt-Anchor" href="#标签类"></a> 标签类</h4><p>类的实例有两种或两种以上的样式,并且包含一个标签字段来表示实例的样式。标签类有许多缺点,可读性差、内存占用多、容易出错、添加新样式复杂</p><h4 id="类层次结构"><a class="markdownIt-Anchor" href="#类层次结构"></a> 类层次结构</h4><p>标签类只是类层次结构的简易模仿。要将已标签的类转换为类层次结构,</p><ol><li>抽取标签类都有的方法、字段到一个抽象类中</li><li>继承抽象类实现都有的方法和子类特有的方法</li></ol><h3 id="24-静态成员类优于非静态成员类"><a class="markdownIt-Anchor" href="#24-静态成员类优于非静态成员类"></a> 24. 静态成员类优于非静态成员类</h3><p>嵌套类是在另一个类中定义的类。嵌套类应该只为它的外部类服务。如果嵌套类在其它环境中有用,那么它应该是顶级类。有四种嵌套类:静态成员类、非静态成员类、匿名类和局部类。除了静态成员类,其它嵌套类被称为内部类。</p><h4 id="静态成员类"><a class="markdownIt-Anchor" href="#静态成员类"></a> 静态成员类</h4><p>静态成员类是最简单的嵌套类。最好把它看作是一个普通的类,只是碰巧在另一个类中声明而已,并且可以访问外部类的所有成员,甚至是那些声明为 private 的成员。静态成员类是其外部类的静态成员,并且遵守与其它静态成员相同的可访问性规则。如果声明为私有,则只能在外部类中访问,等等。</p><h5 id="静态成员类作用"><a class="markdownIt-Anchor" href="#静态成员类作用"></a> 静态成员类作用</h5><ol><li>作为公共的辅助类,只有与它的外部类一起使用时才有意义,例如 Calculator 类和公有静态成员类 Operation 枚举</li><li>表示由其外部类表示的组件。例如,Map实现类内部的Entry类。Entry对象不需要访问 Map 。因此,使用非静态成员类来表示 entry 是浪费,私有静态成员类是最好的。</li></ol><h4 id="非静态成员类"><a class="markdownIt-Anchor" href="#非静态成员类"></a> 非静态成员类</h4><p>从语法上讲,静态成员类和非静态成员类之间的唯一区别是静态成员类在其声明中具有修饰符 static。尽管语法相似,但这两种嵌套类有很大不同。非静态成员类的每个实例都隐式地与外部类的实例相关联,非静态成员类的实例方法可以调用外部实例上的方法,或者使用受限制的 this (父类.this)构造获得对外部实例的引用。如果嵌套类的实例可以独立于外部类的实例存在,那么嵌套类必须是静态成员类;非静态成员类的实例依赖外部类的实例</p><p>非静态成员类实例与外部类实例之间的关联是在创建成员类实例时建立的,之后无法修改。通常,关联是通过从外部类的实例方法中调用非静态成员类构造函数自动建立的。使用 <code>enclosingInstance.new MemberClass(args)</code> 表达式手动建立关联是可能的,尽管这种情况很少见。正如你所期望的那样,关联占用了非静态成员类实例中的空间,并增加了构造时间。</p><p>如果声明的成员类不需要访问外部类的实例,那么应始终在声明中添加 static 修饰符,如果省略这个修饰符,每个实例都有一个隐藏的对其外部实例的额外引用。存储此引用需要时间和空间,更糟糕的是,外部类可能不能被垃圾回收。</p><h5 id="非静态成员类作用"><a class="markdownIt-Anchor" href="#非静态成员类作用"></a> 非静态成员类作用</h5><p>非静态成员类的一个常见用法是定义一个 Adapter,它允许外部类的实例被视为某个不相关类的实例。例如,Map 接口的实现类通常使用非静态成员类来实现它们的集合视图, Set 和 List,通常使用非静态成员类来实现它们的迭代器</p><h4 id="匿名类"><a class="markdownIt-Anchor" href="#匿名类"></a> 匿名类</h4><p>匿名类没有名称。它不是外部类的成员。它不是与其它成员一起声明的,而是在使用时同时声明和实例化。匿名类可以在代码中用在任何一个可以用表达式的地方。当且仅当它们出现在非静态环境(没有写在静态方法里面)时,匿名类才持有外部类实例。但是,即使它们出现在静态环境中,它们也不能有除常量以外的任何静态成员。</p><p>匿名类的使用有很多限制。你只能在声明它们的时候实例化,你不能执行 instanceof 测试,也不能执行任何其它需要命名类的操作。你不能声明一个匿名类来实现多个接口或继承一个类并同时实现一个接口。匿名类的使用者除了从父类继承的成员外,不能调用任何成员。因为匿名类出现在表达式中,所以它们必须保持简短——大约10行或更短,否则会影响可读性。</p><h4 id="匿名类作用"><a class="markdownIt-Anchor" href="#匿名类作用"></a> 匿名类作用</h4><p>在 lambda 表达式被添加的 Java 之前,匿名类是动态创建小型函数对象和进程对象的首选方法,但 lambda 表达式现在是首选方法(Item-42)。匿名类的另一个常见用法是实现静态工厂方法(参见 Item-20 中的 intArrayAsList 类)</p><h4 id="局部类"><a class="markdownIt-Anchor" href="#局部类"></a> 局部类</h4><p>局部类是四种嵌套类中最不常用的。局部类几乎可以在任何能够声明局部变量的地方使用,并且遵守相同的作用域规则。局部类具有与其它嵌套类相同属性。与成员类一样,它们有名称,可以重复使用呢。与匿名类一样,他们只有在非静态环境中定义的情况下才具有外部类实例,而且它们不能包含静态静态成员。和匿名类一样,它们应该保持简短,以免损害可读性。</p><h4 id="嵌套类总结"><a class="markdownIt-Anchor" href="#嵌套类总结"></a> 嵌套类总结</h4><p>简单回顾一下,有四种不同类型的嵌套类,每一种都有自己的用途。如果嵌套的类需要在单个方法之外可见,或者太长,不适合放入方法中,则使用成员类。除非成员类的每个实例都需要引用其外部类实例,否则让它保持静态。假设嵌套类属于方法内部,如果你只需要从一个位置创建实例,并且存在一个能够描述类的现有类型,那么将其设置为匿名类;否则,将其设置为局部类。</p><h3 id="25-源文件仅限有单个顶层类"><a class="markdownIt-Anchor" href="#25-源文件仅限有单个顶层类"></a> 25. 源文件仅限有单个顶层类</h3><p>虽然 Java 编译器允许你在单个源文件中定义多个顶层类,但这样做没有任何好处,而且存在重大风险。这种风险源于这样一个事实:在源文件中定义多个顶层类使得为一个类提供多个定义成为可能。所使用的定义受源文件传给编译器的顺序的影响。</p>]]></content>
<summary type="html"><h2 id="第四章-类和接口"><a class="markdownIt-Anchor" href="#第四章-类和接口"></a> 第四章 类和接口</h2>
<h3 id="15-尽量减少类和成员的可访问性"><a class="markdownIt-Anchor" hr</summary>
<category term="Java" scheme="https://zunpan.github.io/categories/Java/"/>
<category term="Effective-Java" scheme="https://zunpan.github.io/tags/Effective-Java/"/>
<category term="类和接口" scheme="https://zunpan.github.io/tags/%E7%B1%BB%E5%92%8C%E6%8E%A5%E5%8F%A3/"/>
</entry>
<entry>
<title>Effective-Java学习笔记(二)</title>
<link href="https://zunpan.github.io/2022/06/30/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%BA%8C%EF%BC%89/"/>
<id>https://zunpan.github.io/2022/06/30/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%BA%8C%EF%BC%89/</id>
<published>2022-06-30T11:50:05.000Z</published>
<updated>2023-09-24T04:27:40.274Z</updated>
<content type="html"><![CDATA[<h2 id="第三章-对象的通用方法"><a class="markdownIt-Anchor" href="#第三章-对象的通用方法"></a> 第三章 对象的通用方法</h2><h3 id="10-覆盖-equals-方法时应遵守的约定"><a class="markdownIt-Anchor" href="#10-覆盖-equals-方法时应遵守的约定"></a> 10. 覆盖 equals 方法时应遵守的约定</h3><h4 id="不覆盖equals方法的情况"><a class="markdownIt-Anchor" href="#不覆盖equals方法的情况"></a> 不覆盖equals方法的情况</h4><ul><li><p>类的每个实例本质上都是唯一的。例如Thread类,它是活动实体类而不是值类</p></li><li><p>该类不需要提供逻辑相等测试。例如<code>java.util.regex.Pattern</code>可以覆盖equals方法来检查两个Pattern实例是否表示完全相同的正则表达式,但是这个类的设计人员认为客户端不需要这个功能,所以没有覆盖</p></li><li><p>父类已经覆盖了equals方法,父类的行为也适合于这个类。例如大多数Set的equals从AbstractSet继承,List从AbstractList继承,Map从AbstractMap继承</p></li><li><p>类是私有的并且你确信它的equals方法永远不会被调用。保险起见,你可以按如下方式覆盖equals方法,以确保它不会被意外调用</p><pre class="highlight"><code class="java"><span class="hljs-meta">@Override</span><span class="hljs-keyword">public</span> <span class="hljs-type">boolean</span> <span class="hljs-title function_">equals</span><span class="hljs-params">(Object o)</span> { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">AssertionError</span>(); <span class="hljs-comment">// Method is never called</span>}</code></pre></li></ul><h4 id="覆盖equals方法的时机"><a class="markdownIt-Anchor" href="#覆盖equals方法的时机"></a> 覆盖equals方法的时机</h4><p>当一个类有一个逻辑相等的概念,而这个概念不同于仅判断对象的同一性(相同对象的引用),并且父类没有覆盖 equals。对于值类通常是这样。值类只是表示值的类,例如 Integer 或 String。程序员希望发现它们在逻辑上是否等价,而不是它们是否引用相同的对象。覆盖 equals 方法不仅是为了满足程序员的期望,它还使实例能够作为 Map 的键或 Set 元素时,具有可预测的、理想的行为。</p><p>单例模式的值类不需要覆盖equals方法。例如,枚举类型就是单例值类。逻辑相等就是引用相等。</p><h4 id="覆盖equals方法的规范"><a class="markdownIt-Anchor" href="#覆盖equals方法的规范"></a> 覆盖equals方法的规范</h4><ul><li><p>反身性:对于任何非空的参考值x,<code>x.equals(x)</code>必须返回true</p></li><li><p>对称性:<code>x.equals(y)</code>与<code>y.equals(x)</code>的值要么都为true要么为false</p></li><li><p>传递性:对于非空引用x,y,z,如果<code>x.equals(y)</code>返回true,<code>y.equals(z)</code>返回true,那么<code>x.equals(z)</code>也返回true</p></li><li><p>一致性:对于任何非空的引用值 x 和 y,<code>x.equals(y)</code> 的多次调用必须一致地返回 true 或一致地返回 false,前提是不修改 equals 中使用的信息。</p></li><li><p>非空性:对于非空引用x,<code>x.equals(null)</code>返回false,不需要显示判断是否为null,因为equals方法需要将参数转为成相同类型,转换之前会使用instanceof运算符来检查类型是否正确,如果为null,也会返回false</p><pre class="highlight"><code class="java"><span class="hljs-meta">@Override</span><span class="hljs-keyword">public</span> <span class="hljs-type">boolean</span> <span class="hljs-title function_">equals</span><span class="hljs-params">(Object o)</span> { <span class="hljs-keyword">if</span> (!(o <span class="hljs-keyword">instanceof</span> MyType)) <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>; <span class="hljs-type">MyType</span> <span class="hljs-variable">mt</span> <span class="hljs-operator">=</span> (MyType) o; ...}</code></pre></li></ul><h4 id="高质量构建equals方法的步骤"><a class="markdownIt-Anchor" href="#高质量构建equals方法的步骤"></a> 高质量构建equals方法的步骤</h4><p>1、使用==检查参数是否是this对象的引用,如果是,返回true,这是一种性能优化,如果比较的开销很大,这种优化很有必要<br />2、使用instanceof运算符检查参数是否有正确类型。<br />3、将参数转换为正确的类型。因为在转换前进行了instanceof判断,所以肯定可以强转成功<br />4、对类中的每个有意义的字段检查是否和参数的相应字段匹配</p><p>对于不是float和double的基本类型字段,使用==比较;对应对象引用字段,递归调用equals方法;对于float字段,使用静态方法<code>Float.compare(float,float)</code>方法;对于double字段,使用<code>Double.compare(double,double)</code>。float和double字段的特殊处理是由于<code>Float.NaN</code>,-0.0f 和类似的双重值的存在;对于数组字段,使用<code>Arrays.equals</code>方法。</p><p>一些对象引用字段可能允许null出现。为了避免可能出现NullPointerException,请使用<code>Objects.equals(Object,Object)</code>来检查对象的字段是否相等。</p><p>对于某些类,字段比较非常复杂,如果是这样,可以存储字段的规范形式,以便equals方法进行开销较小的比较。这种技术最适合于不可变类;如果对象可以修改,则必须使规范形式保持最新</p><p>equals 方法的性能可能会受到字段比较顺序的影响。为了获得最佳性能,你应该首先比较那些更可能不同、比较成本更低的字段。不能比较不属于对象逻辑状态的字段,例如用于同步操作的锁字段。派生字段可以不比较,这样可以提高性能,但是如果派生字段包括了对象的所有信息,比如说多边形面积,可以从边和顶点计算得出,那么先比较面积,面积不一样就肯定不是同一个对象,这样可以减小开销</p><h4 id="一些警告"><a class="markdownIt-Anchor" href="#一些警告"></a> 一些警告</h4><ul><li>覆盖equals方法时,也要覆盖hashCode方法</li><li>考虑任何形式的别名都不是一个好主意。例如,File类不应该尝试将引用同一文件的符号链接等同起来</li><li>不要用别的类型替换equals方法的Object类型</li></ul><h3 id="11-当覆盖equals方法时总是覆盖hashcode方法"><a class="markdownIt-Anchor" href="#11-当覆盖equals方法时总是覆盖hashcode方法"></a> 11. 当覆盖equals方法时,总是覆盖hashCode方法</h3><h4 id="object类中hashcode方法的规范"><a class="markdownIt-Anchor" href="#object类中hashcode方法的规范"></a> Object类中hashCode方法的规范</h4><ul><li>应用程序执行期间对对象重复调用hashCode方法,它必须返回相同的值,前提是不修改equals方法中用于比较的信息。这个值不需要在应用程序的不同执行之间保持一致</li><li>如果<code>equals(Object)</code>方法返回true,那么在这两个对象上调用hashCode方法必须产生相同的整数结果</li><li>如果<code>equals(Object)</code>方法返回false,hashCode方法的值不需要一定不同,但是,不同对象的hashCode不一样可以提高散列表性能</li></ul><p>当没有覆盖hashCode方法时,将违反第二条规范:逻辑相等的对象必须有相等的散列码。两个不同的对象在逻辑上是相等的,但它们的hashCode一般不相等。例如用Item-10中的PhoneNumber类实例作为HashMap的键</p><pre class="highlight"><code class="java">Map<PhoneNumber, String> m = <span class="hljs-keyword">new</span> <span class="hljs-title class_">HashMap</span><>();m.put(<span class="hljs-keyword">new</span> <span class="hljs-title class_">PhoneNumber</span>(<span class="hljs-number">707</span>, <span class="hljs-number">867</span>, <span class="hljs-number">5309</span>), <span class="hljs-string">"Jenny"</span>);</code></pre><p>此时,你可能期望<code>m.get(new PhoneNumber(707, 867,5309))</code> 返回「Jenny」,但是它返回 null。因为PhoneNumber类没有覆盖hashCode方法,插入到HashMap和从HashMap中获取的实例具有不相同的散列码,这违法了hashCode方法规范。因此,get方法查找电话号码的散列桶与put方法存储电话号码的散列桶不同。</p><p>解决这个问题有一个最简单但很糟糕的实现</p><pre class="highlight"><code class="java"><span class="hljs-comment">// The worst possible legal hashCode implementation - never use!</span><span class="hljs-meta">@Override</span><span class="hljs-keyword">public</span> <span class="hljs-type">int</span> <span class="hljs-title function_">hashCode</span><span class="hljs-params">()</span> { <span class="hljs-keyword">return</span> <span class="hljs-number">42</span>; }</code></pre><p>它确保了逻辑相等的对象具有相同的散列码。同时它也很糟糕,因为每个对象的散列码都相同了。每个对象都分配到一个存储桶中,散列表退化成链表。</p><h4 id="散列算法设计步骤"><a class="markdownIt-Anchor" href="#散列算法设计步骤"></a> 散列算法设计步骤</h4><p>一个好的散列算法大概率为逻辑不相等对象生成不相同的散列码。理想情况下,一个散列算法应该在所有int值上均匀合理分布所有不相等对象。实现理想情况很困难,但实现一个类似的并不难,这里有一个简单的方式:<br />1、声明一个名为 result 的int变量,并将其初始化为对象中第一个重要字段的散列码c,如步骤2.a中计算的那样<br />2、对象中剩余的重要字段f,执行以下操作:<br />a. 为字段计算一个整数散列码c : 如果字段是基本数据类型,计算<code>Type.hashCode(f)</code>,其中type是 f 类型对应的包装类 ; 如果字段是对象引用,并且该类的equals方法通过递归调用equals方法来比较字段,则递归调用字段上的hashCode方法。如果需要更复杂的比较,则为该字段计算一个【canonical representation】,并在canonical representation上调用hashCode方法。如果字段的值为空,则使用0(或其它常数,但0是惯用的);如果字段是一个数组,则对数组中每个重要元素都计算散列码,并用2.b步骤逐个组合。如果数组中没有重要元素,则使用常量,最好不是0。如果所有元素都很重要,那么使用<code>Arrays.hashCode</code></p><p>b. 将2.a步骤中计算的散列码合并到result变量,如下所示</p><pre class="highlight"><code class="java">result = <span class="hljs-number">31</span> * result + c;</code></pre><p>3、返回result变量</p><p>步骤2.b中的乘法说明result依赖字段的顺序,如果类具有多个类似字段,那么乘法会产生更好的hash性能。例如字符串hash算法中如果省略乘法,那么不同顺序的字符串都会有相同的散列码。</p><p>选择31是因为它是奇素数。如果是偶数,乘法运算就会溢出,信息就会丢失,因为乘法运算等同于移位。使用素数的好处不太明显,但它是传统用法。31有一个很好的特性,可以用移位和减法来代替乘法,从而在某些体系结构上获得更好的性能:<code>31 * i == (i <<5) – i</code>。现代虚拟机自动进行这种优化。</p><p>根据前面的步骤,给PhoneNumber类写一个hashCode方法</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Typical hashCode method</span><span class="hljs-meta">@Override</span><span class="hljs-keyword">public</span> <span class="hljs-type">int</span> <span class="hljs-title function_">hashCode</span><span class="hljs-params">()</span> { <span class="hljs-type">int</span> <span class="hljs-variable">result</span> <span class="hljs-operator">=</span> Short.hashCode(areaCode); result = <span class="hljs-number">31</span> * result + Short.hashCode(prefix); result = <span class="hljs-number">31</span> * result + Short.hashCode(lineNum); <span class="hljs-keyword">return</span> result;}</code></pre><p>因为这个方法返回一个简单的确定的计算结果,它的唯一输入是 PhoneNumber 实例中的三个重要字段,所以很明显,相等的 PhoneNumber 实例具有相等的散列码。实际上,这个方法是 PhoneNumber 的一个非常好的 hashCode 方法实现,与 Java 库中的 hashCode 方法实现相当。它很简单,速度也相当快,并且合理地将不相等的电话号码分散到不同的散列桶中。</p><p>虽然这个Item里的方法可以提供一个相当不错的散列算法,但它不是最先进的,对于大多数用途是足够的,如果需要不太可能产生冲突的散列算法。请参阅 Guava 的 com.google.common.hash.Hashing</p><p>Objects类有一个静态方法,它接受任意数量的对象并返回它们的散列码。这个名为<code>hash</code>的方法允许你编写只有一行代码的hashCode方法,它的质量可以与本Item提供的编写方法媲美。但不幸的是它们运行得很慢,因为它需要创建数组来传递可变数量的参数,如果有参数是原始类型的,则需要进行装箱和拆箱。推荐只在性能不重要的情况下使用这种散列算法。下面是使用这个静态方法编写的PhoneNumber的散列算法</p><pre class="highlight"><code class="java"><span class="hljs-comment">// One-line hashCode method - mediocre performance</span><span class="hljs-meta">@Override</span><span class="hljs-keyword">public</span> <span class="hljs-type">int</span> <span class="hljs-title function_">hashCode</span><span class="hljs-params">()</span> { <span class="hljs-keyword">return</span> Objects.hash(lineNum, prefix, areaCode);}</code></pre><h4 id="缓存散列值"><a class="markdownIt-Anchor" href="#缓存散列值"></a> 缓存散列值</h4><p>如果类是不可变的,并且计算散列码的成本非常高,那么可以考虑在对象中缓存散列码,而不是每次调用重新计算。如果这个类的对象会被用作散列键,那么应该在创建对象时就计算散列码。要不然就在第一次调用时计算散列码</p><h3 id="12-始终覆盖tostring方法"><a class="markdownIt-Anchor" href="#12-始终覆盖tostring方法"></a> 12. 始终覆盖toString方法</h3><p>虽然 Object 提供 toString 方法的实现,但它返回的字符串通常不是类的用户希望看到的。它由后跟「at」符号(@)的类名和散列码的无符号十六进制表示(例如 PhoneNumber@163b91)组成。toString 的通用约定是这么描述的,返回的字符串应该是「简洁但信息丰富的表示,易于阅读」。虽然有人认为 PhoneNumber@163b91 简洁易懂,但与 707-867-5309 相比,它的信息量并不大。toString 约定接着描述,「建议所有子类覆盖此方法。」好建议,确实!</p><p>虽然它不如遵守euals和hashCode约定(Item10和Item11)那么重要,但是提供一个好的toString方法更便于调试。当对象被传递给 println、printf、字符串连接操作符或断言或由调试器打印时,将自动调用 toString 方法。即使你从来没有调用 toString 对象,其他人也可能使用。例如,使用该对象的组件可以在日志错误消息中包含对象的字符串表示。如果你不覆盖 toString,该消息可能完全无用。</p><h3 id="13-明智地覆盖clone方法"><a class="markdownIt-Anchor" href="#13-明智地覆盖clone方法"></a> 13. 明智地覆盖clone方法</h3><h4 id="cloneable接口的作用"><a class="markdownIt-Anchor" href="#cloneable接口的作用"></a> Cloneable接口的作用</h4><p>Cloneable接口的作用是声明类可克隆,但是接口不包含任何方法,类的clone方法继承自Object,并且Object类的clone方法是受保护的,无法跨包调用,虽然可以通过反射调用,但也不能保证对象具有可访问的 clone 方法(如果类没有覆盖clone方法可以通过获取父类Object调用clone方法,但是如果类没实现Cloneable接口调用会抛出CloneNotSupportedException)。</p><p>既然 Cloneable 接口不包含任何方法,用它来做什么呢?它决定了 Object 类受保护的 clone 实现的行为:如果一个类实现了 Cloneable 接口,Object 类的 clone 方法则返回该类实例的逐字段拷贝;没实现 Cloneable 接口调用 clone 方法会抛出 CloneNotSupportedException。这是接口非常不典型的一种使用方式,不应该效仿。通常,类实现接口可以表明类能够为其客户端做些什么。在本例中,它修改了父类上受保护的方法的行为。</p><h4 id="clone-方法规范"><a class="markdownIt-Anchor" href="#clone-方法规范"></a> clone 方法规范</h4><p>虽然规范没有说明,但是在实践中,实现 Cloneable 接口的类应该提供一个功能正常的 public clone 方法。</p><p>clone方法的一般约定很薄弱。下面的内容是从Object规范复制过来的</p><pre class="highlight"><code class="text">Creates and returns a copy of this object. The precise meaning of “copy” may depend on the class of the object. The general intent is that, for any object x,the expressionx.clone() != xwill be true, and the expressionx.clone().getClass() == x.getClass()will be true, but these are not absolute requirements. While it is typically the case thatx.clone().equals(x)will be true, this is not an absolute requirement.</code></pre><p>clone方法创建并返回对象的副本。「副本」的确切含义可能取决于对象的类。通常,对于任何对象 x,表达式 x.clone() != x、x.clone().getClass() == x.getClass() 以及 x.clone().equals(x) 的值都将为 true,但都不是绝对的。(equals方法应覆盖为比较对象中的字段才能得到true,默认实现是比较对象地址,结果永远为false)</p><p>按照约定,clone方法返回的对象应该通过调用super.clone() 来获得。如果一个类和它的所有父类(Object类除外)都遵守这个约定,表达式 <code>x.clone().getClass() == x.getClass()</code> 则为 true</p><p>按照约定,返回的对象应该独立于被克隆的对象。为了实现这种独立性,可能需要在super.clone() 前修改对象的一个或多个字段</p><p>这种机制有点类似于构造方法链,只是没有强制执行:</p><ul><li>如果一个类的clone方法返回的实例不是通过调用 super.clone() 而是通过调用构造函数获得的,编译器不会报错,但是如果这个类的子类调用super.clone(),由此产生的对象将是错误的,影响子类clone方法正常工作</li><li>如果覆盖clone方法的类是final修饰的,那么可以忽略这个约定,因为不会有子类</li><li>如果一个final修饰的类的clone方法不调用super.clone()。该类没有理由实现Cloneable接口,因为它不依赖于Object的clone方法</li></ul><h4 id="覆盖clone方法"><a class="markdownIt-Anchor" href="#覆盖clone方法"></a> 覆盖clone方法</h4><ol><li>如果类是不可变的,不要提供clone方法</li><li>如果类的字段都是基本类型或不可变对象的引用,那么直接这个类的clone方法直接调用super.clone()即可</li><li>如果类的字段包含可变对象的引用,需要递归调用可变对象的深拷贝方法</li></ol><h4 id="一些细节"><a class="markdownIt-Anchor" href="#一些细节"></a> 一些细节</h4><ol><li><p>和构造函数一样,不要在clone方法中调用可覆盖方法。如果clone方法调用一个在子类中被覆盖的方法,这个方法将在子类修复其在克隆中的状态之前执行,很可能导致克隆和原始对象的破坏。</p></li><li><p>Object的clone方法被声明为抛出CloneNotSupportedException,但是覆盖方法时 try-catch 异常就行,不抛出受检查异常的方法更容易使用</p></li><li><p>设计可继承的类时不要实现 Cloneable接口(如果实现了,子类就必须对外提供clone方法)。你可以选择通过实现一个功能正常的受保护克隆方法来模拟 Object 的行为,该方法声明为抛出 CloneNotSupportedException。这给子类实现 Cloneable 或不实现 Cloneable 的自由。或者,你可以选择不实现一个有效的克隆方法,并通过提供以下退化的克隆实现来防止子类实现它:</p><pre class="highlight"><code class="java"><span class="hljs-comment">// clone method for extendable class not supporting Cloneable</span><span class="hljs-meta">@Override</span><span class="hljs-keyword">protected</span> <span class="hljs-keyword">final</span> Object <span class="hljs-title function_">clone</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> CloneNotSupportedException { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">CloneNotSupportedException</span>();}</code></pre></li><li><p>如果你编写了一个实现了 Cloneable 接口的线程安全类,请记住它的 clone 方法必须正确同步,就像其他任何方法一样。</p></li></ol><h4 id="总结"><a class="markdownIt-Anchor" href="#总结"></a> 总结</h4><p>回顾一下,所有实现 Cloneable 接口的类都应该使用一个返回类型为类本身的公有方法覆盖 clone。这个方法应该首先调用 super.clone(),然后「修复」任何需要「修复」的字段。通常,这意味着复制任何包含对象内部「深层结构」的可变对象,并将克隆对象对这些对象的引用替换为对其副本的引用。虽然这些内部副本通常可以通过递归调用 clone 来实现,但这并不总是最好的方法。如果类只包含基本数据类型的字段或对不可变对象的引用,那么很可能不需要修复任何字段。这条规则也有例外。例如,表示序列号或其他唯一 ID 的字段需要修复,即使它是基本数据类型或不可变的。</p><p>搞这么复杂真的有必要吗?答案是否定的。如果你扩展了一个已经实现了 Cloneable 接口的类,那么除了实现行为良好的 clone 方法之外,你别无选择。否则,最好提供对象复制的替代方法。一个更好的对象复制方法是提供一个复制构造函数或复制工厂。复制构造函数是一个简单的构造函数,它接受单个参数,其类型是包含构造函数的类,例如</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Copy constructor</span><span class="hljs-keyword">public</span> <span class="hljs-title function_">Yum</span><span class="hljs-params">(Yum yum)</span> { ... };</code></pre><p>复制工厂与复制构造函数的静态工厂(Item-1)类似:</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Copy factory</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Yum <span class="hljs-title function_">newInstance</span><span class="hljs-params">(Yum yum)</span> { ... };</code></pre><p>复制构造函数方法及其静态工厂变体与克隆方法相比有许多优点:</p><ol><li>它们不依赖于易发生风险的语言外对象创建机制(Object的clone方法是native的);</li><li>他们不要求无法强制执行的约定(clone方法一定要先调用super.clone());</li><li>它们与 final 字段不冲突(clone方法不能修改final字段);</li><li>它们不会抛出不必要的 checked 异常;</li><li>而且不需要强制类型转换。</li><li>可以提供类型转换构造函数</li></ol><p>考虑到与 Cloneable 相关的所有问题,新的接口不应该继承它,新的可扩展类不应该实现它。通常,复制功能最好由构造函数或工厂提供。这个规则的一个明显的例外是数组,最好使用 clone 方法来复制数组。</p><h3 id="14-考虑实现-comparable-接口"><a class="markdownIt-Anchor" href="#14-考虑实现-comparable-接口"></a> 14. 考虑实现 Comparable 接口</h3><p>与本章讨论的其它方法不同,compareTo 方法不是 Object 中声明的,而是 Comparable 接口中的唯一方法。一个类实现Comparable,表明实例具有自然顺序(字母或数字或时间顺序)。Java 库中的所有值类以及所有枚举类型(Item-34)都实现了 Comparable接口</p><h4 id="compareto方法约定"><a class="markdownIt-Anchor" href="#compareto方法约定"></a> compareTo方法约定</h4><p>compareTo 方法的一般约定类似于 equals 方法:<br />将一个对象与指定对象进行顺序比较。当该对象小于、等于或大于指定对象时,对应返回一个负整数、零或正整数。如果指定对象的类型阻止它与该对象进行比较,则抛出 ClassCastException</p><p>在下面的描述中, <code>sgn(expression)</code> 表示数学中的符号函数,它被定义为:根据传入表达式的值是负数、零或正数,对应返回-1、0或1。</p><ul><li>实现类必须确保所有 x 和 y 满足 <code>sgn(x.compareTo(y)) == -sgn(y.compareTo(x))</code>(这意味着 x.compareTo(y) 当且仅当 y.compareTo(x) 抛出异常时才抛出异常)</li><li>实现类还必须确保关系是可传递的:<code>(x.compareTo(y) > 0 && y.compareTo(z) > 0)</code> 意味着 <code>x.compareTo(z) > 0</code></li><li>最后,实现类必须确保 <code>x.compareTo(y) == 0</code> 时,所有的 z 满足 <code>sgn(x.compareTo(z)) == sgn(y.compareTo(z))</code></li><li>强烈建议 <code>(x.compareTo(y)== 0) == (x.equals(y))</code> 成立,但这不是必需的。一般来说,任何实现 Comparable 接口并违法此条件的类都应该清除地注明这一事实。推荐的表述是“Note: This class has a natural ordering that is inconsistent with equals.”</li></ul><p>与equals方法不同,equals方法入参是Object类型,而compareTo方法不需要和不同类型的对象比较:当遇到不同类型的对象时,允许抛出ClassCastException</p><p>就像违反 hashCode 约定的类可以破坏依赖 hash 的其他类一样,违反 compareTo 约定的类也可以破坏依赖 Comparable 的其他类。依赖 Comparable 的类包括排序集合 TreeSet 和 TreeMap,以及实用工具类 Collections 和 Arrays,它们都包含搜索和排序算法。</p><p>compareTo的约定和equals约定有相同的限制:反身性、对称性和传递性。如果要向实现 Comparable 的类中添加值组件,不要继承它;编写一个不相关的类,其中包含第一个类的实例。然后提供返回所包含实例的「视图」方法。这使你可以自由地在包含类上实现你喜欢的任何 compareTo 方法,同时允许它的客户端在需要时将包含类的实例视为被包含类的实例。</p><p>compareTo 约定的最后一段是一个强烈建议而不是要求,它只是简单地说明了 compareTo 方法所施加地同等性检验通常应该与 equals 方法返回相同的结果。如果一个类的 compareTo 方法强加了一个与 equals 不一致的顺序,那么这个类仍然可以工作,但是包含该类元素的有序集合可能无法遵守集合接口(Collection、Set 或 Map)的一般约定。这是因为这些接口的一般约定是根据 equals 方法定义的,但是有序集合使用 compareTo 代替了 equals 实施同等性建议,这是需要注意的地方</p><p>例如,考虑 BigDecimal 类,它的 compareTo 方法与 equals 不一致。如果你创建一个空的 HashSet 实例,然后添加 new BigDecimal(“1.0”) 和 new BigDecimal(“1.00”),那么该 HashSet 将包含两个元素,因为添加到该集合的两个 BigDecimal 实例在使用 equals 方法进行比较时结果是不相等的。但是,如果你使用 TreeSet 而不是 HashSet 执行相同的过程,那么该集合将只包含一个元素,因为使用 compareTo 方法比较两个 BigDecimal 实例时结果是相等的。(有关详细信息,请参阅 BigDecimal 文档。)</p><h4 id="编写-compareto-方法"><a class="markdownIt-Anchor" href="#编写-compareto-方法"></a> 编写 compareTo 方法</h4><ol><li><p>递归调用引用字段的 compareTo 方法。如果引用字段没有实现 Comparable,或者需要一个非标准的排序,那么应使用比较器</p></li><li><p>基本字段类型使用包装类的静态 compare 方法比较</p></li><li><p>从最重要字段开始比较</p></li><li><p>考虑使用 <code>java.util.Comparator</code>接口的比较器构造方法</p></li></ol>]]></content>
<summary type="html"><h2 id="第三章-对象的通用方法"><a class="markdownIt-Anchor" href="#第三章-对象的通用方法"></a> 第三章 对象的通用方法</h2>
<h3 id="10-覆盖-equals-方法时应遵守的约定"><a class="markdo</summary>
<category term="Java" scheme="https://zunpan.github.io/categories/Java/"/>
<category term="Effective-Java" scheme="https://zunpan.github.io/tags/Effective-Java/"/>
<category term="对象的通用方法" scheme="https://zunpan.github.io/tags/%E5%AF%B9%E8%B1%A1%E7%9A%84%E9%80%9A%E7%94%A8%E6%96%B9%E6%B3%95/"/>
</entry>
<entry>
<title>Effective-Java学习笔记(一)</title>
<link href="https://zunpan.github.io/2022/06/23/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%80%EF%BC%89/"/>
<id>https://zunpan.github.io/2022/06/23/Effective-Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%80%EF%BC%89/</id>
<published>2022-06-23T10:11:15.000Z</published>
<updated>2023-09-24T04:27:40.273Z</updated>
<content type="html"><![CDATA[<h2 id="第二章-创建和销毁对象"><a class="markdownIt-Anchor" href="#第二章-创建和销毁对象"></a> 第二章 创建和销毁对象</h2><h3 id="1-考虑用静态工厂方法代替构造函数"><a class="markdownIt-Anchor" href="#1-考虑用静态工厂方法代替构造函数"></a> 1. 考虑用静态工厂方法代替构造函数</h3><p>静态工厂方法是一个返回该类实例的 public static 方法。例如,Boolean类的valueOf方法</p><pre class="highlight"><code class="java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Boolean <span class="hljs-title function_">valueOf</span><span class="hljs-params">(<span class="hljs-type">boolean</span> b)</span> { <span class="hljs-keyword">return</span> b ? Boolean.TRUE : Boolean.FALSE;}</code></pre><p>要注意静态工厂方法与设计模式中的工厂方法不同。</p><p><strong>静态工厂方法优点:</strong></p><ol><li>静态工厂方法有确切名字,客户端实例化对象时代码更易懂。例如,<code>BigInteger</code>类中返回可能为素数的<code>BigInteger</code>对象静态工厂方法叫<code>BigInteger.probablePrime</code>。此外,每个类的构造函数签名是唯一的,但是程序员可以通过调整参数类型、个数或顺序修改构造函数的签名,这样会给客户端实例化对象带来困惑,因为静态工厂方法有确切名称所以不会出现这个问题</li><li>静态工厂方法不需要在每次调用时创建新对象。例如<code>Boolean.valueOf(boolean)</code>,true和false会返回预先创建好的对应Boolean对象。这种能力允许类在任何时候都能严格控制存在的实例,常用来实现单例</li><li>静态工厂方法可以获取任何子类的对象。这种能力的一个应用是API可以不公开子类或者实现类的情况下返回对象。例如<code>Collections</code>类提供静态工厂生成不是public的子类对象,不可修改集合和同步集合等</li><li>静态工厂方法返回对象的类型可以根据输入参数变换。例如,<code>EnumSet</code>类的<code>noneOf</code>方法,当enum的元素个数小于等于64,<code>noneOf</code>方法返回<code>RegularEnumSet</code>类型的对象,否则返回<code>JumboEnumSet</code>类型的对象</li><li>静态工厂方法的返回对象的类不需要存在。这种灵活的静态工厂方法构成了服务提供者框架的基础。service provider框架有三个必要的组件:代表实现的service interface;provider registration API,提供者用来注册实现;service access API,客户端使用它来获取服务的实例,服务访问API允许客户端选择不同实现,是一个灵活的静态工厂方法。service provider第四个可选的组件是service provider interface,它描述了产生service interface实例的工厂对象。在JDBC中,Connection扮演service interface角色,DriverManager.registerDriver是provider registration API,DriverManager.getConnection是service access API,Driver是service provider interface</li></ol><p><strong>静态工厂方法的缺点:</strong></p><ol><li><p>只提供静态工厂方法而没有public或者protected的构造方法就不能被继承,例如Collections类就不能被继承</p></li><li><p>静态工厂方法没有构造函数那么显眼,常见的静态工厂方法名字如下:<br />from,一种类型转换方法,该方法接受单个参数并返回该类型的相应实例,例如:</p><pre class="highlight"><code class="java"><span class="hljs-type">Date</span> <span class="hljs-variable">d</span> <span class="hljs-operator">=</span> Date.from(instant);</code></pre><p>of,一个聚合方法,它接受多个参数并返回一个包含这些参数的实例,例如:</p><pre class="highlight"><code class="java">Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);</code></pre><p>valueOf,一种替代 from 和 of 但更冗长的方法</p><pre class="highlight"><code class="java"><span class="hljs-type">BigInteger</span> <span class="hljs-variable">prime</span> <span class="hljs-operator">=</span> BigInteger.valueOf(Integer.MAX_VALUE);</code></pre><p>instance 或 getInstance,返回一个实例,该实例由其参数(如果有的话)描述,但不具有相同的值,例如:</p><pre class="highlight"><code class="java"><span class="hljs-type">StackWalker</span> <span class="hljs-variable">luke</span> <span class="hljs-operator">=</span> StackWalker.getInstance(options);</code></pre><p>create 或 newInstance,与 instance 或 getInstance 类似,只是该方法保证每个调用都返回一个新实例,例如:</p><pre class="highlight"><code class="java"><span class="hljs-type">Object</span> <span class="hljs-variable">newArray</span> <span class="hljs-operator">=</span> Array.newInstance(classObject, arrayLen);</code></pre><p>getType,类似于 getInstance,但如果工厂方法位于不同的类中,则使用此方法。其类型是工厂方法返回的对象类型,例如:</p><pre class="highlight"><code class="java"><span class="hljs-type">FileStore</span> <span class="hljs-variable">fs</span> <span class="hljs-operator">=</span> Files.getFileStore(path);</code></pre><p>newType,与 newInstance 类似,但是如果工厂方法在不同的类中使用。类型是工厂方法返回的对象类型,例如:</p><pre class="highlight"><code class="java"><span class="hljs-type">BufferedReader</span> <span class="hljs-variable">br</span> <span class="hljs-operator">=</span> Files.newBufferedReader(path);`</code></pre><p>type,一个用来替代 getType 和 newType 的比较简单的方式,例如:</p><pre class="highlight"><code class="java">List<Complaint> litany = Collections.list(legacyLitany);</code></pre></li></ol><h3 id="2-当构造函数有多个参数时考虑改用builder"><a class="markdownIt-Anchor" href="#2-当构造函数有多个参数时考虑改用builder"></a> 2. 当构造函数有多个参数时,考虑改用Builder</h3><p>静态工厂和构造函数都有一个局限:不能对大量可选参数做很好扩展。例如,一个表示食品营养标签的类,必选字段有净含量、热量,另外有超过20个可选字段,比如反式脂肪、钠等。</p><p>为这种类编写构造函数,通常是使用可伸缩构造函数,这里展示四个可选字段的情况</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Telescoping constructor pattern - does not scale well!</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">NutritionFacts</span> { <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> servingSize; <span class="hljs-comment">// (mL) required</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> servings; <span class="hljs-comment">// (per container) required</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> calories; <span class="hljs-comment">// (per serving) optional</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> fat; <span class="hljs-comment">// (g/serving) optional</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> sodium; <span class="hljs-comment">// (mg/serving) optional</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> carbohydrate; <span class="hljs-comment">// (g/serving) optional</span> <span class="hljs-keyword">public</span> <span class="hljs-title function_">NutritionFacts</span><span class="hljs-params">(<span class="hljs-type">int</span> servingSize, <span class="hljs-type">int</span> servings)</span> { <span class="hljs-built_in">this</span>(servingSize, servings, <span class="hljs-number">0</span>); } <span class="hljs-keyword">public</span> <span class="hljs-title function_">NutritionFacts</span><span class="hljs-params">(<span class="hljs-type">int</span> servingSize, <span class="hljs-type">int</span> servings, <span class="hljs-type">int</span> calories)</span> { <span class="hljs-built_in">this</span>(servingSize, servings, calories, <span class="hljs-number">0</span>); } <span class="hljs-keyword">public</span> <span class="hljs-title function_">NutritionFacts</span><span class="hljs-params">(<span class="hljs-type">int</span> servingSize, <span class="hljs-type">int</span> servings, <span class="hljs-type">int</span> calories, <span class="hljs-type">int</span> fat)</span> { <span class="hljs-built_in">this</span>(servingSize, servings, calories, fat, <span class="hljs-number">0</span>); } <span class="hljs-keyword">public</span> <span class="hljs-title function_">NutritionFacts</span><span class="hljs-params">(<span class="hljs-type">int</span> servingSize, <span class="hljs-type">int</span> servings, <span class="hljs-type">int</span> calories, <span class="hljs-type">int</span> fat, <span class="hljs-type">int</span> sodium)</span> { <span class="hljs-built_in">this</span>(servingSize, servings, calories, fat, sodium, <span class="hljs-number">0</span>); } <span class="hljs-keyword">public</span> <span class="hljs-title function_">NutritionFacts</span><span class="hljs-params">(<span class="hljs-type">int</span> servingSize, <span class="hljs-type">int</span> servings, <span class="hljs-type">int</span> calories, <span class="hljs-type">int</span> fat, <span class="hljs-type">int</span> sodium, <span class="hljs-type">int</span> carbohydrate)</span> { <span class="hljs-built_in">this</span>.servingSize = servingSize; <span class="hljs-built_in">this</span>.servings = servings; <span class="hljs-built_in">this</span>.calories = calories; <span class="hljs-built_in">this</span>.fat = fat; <span class="hljs-built_in">this</span>.sodium = sodium; <span class="hljs-built_in">this</span>.carbohydrate = carbohydrate; }}</code></pre><p>当你想创建指定carbohydrate的实例,就必须调用最后一个构造函数,这样就必须给出calories、fat、sodium的值。当有很多可选参数时,这种模式可读性很差。</p><p>当遇到许多可选参数时,另一种选择是使用JavaBean模式,在这种模式中,调用一个无参数的构造函数来创建对象,然后调用setter方法来设置参数值</p><pre class="highlight"><code class="java"><span class="hljs-comment">// JavaBeans Pattern - allows inconsistency, mandates mutability</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">NutritionFacts</span> { <span class="hljs-comment">// Parameters initialized to default values (if any)</span> <span class="hljs-keyword">private</span> <span class="hljs-type">int</span> <span class="hljs-variable">servingSize</span> <span class="hljs-operator">=</span> -<span class="hljs-number">1</span>; <span class="hljs-comment">// Required; no default value</span> <span class="hljs-keyword">private</span> <span class="hljs-type">int</span> <span class="hljs-variable">servings</span> <span class="hljs-operator">=</span> -<span class="hljs-number">1</span>; <span class="hljs-comment">// Required; no default value</span> <span class="hljs-keyword">private</span> <span class="hljs-type">int</span> <span class="hljs-variable">calories</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; <span class="hljs-keyword">private</span> <span class="hljs-type">int</span> <span class="hljs-variable">fat</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; <span class="hljs-keyword">private</span> <span class="hljs-type">int</span> <span class="hljs-variable">sodium</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; <span class="hljs-keyword">private</span> <span class="hljs-type">int</span> <span class="hljs-variable">carbohydrate</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; <span class="hljs-keyword">public</span> <span class="hljs-title function_">NutritionFacts</span><span class="hljs-params">()</span> { } <span class="hljs-comment">// Setters</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">setServingSize</span><span class="hljs-params">(<span class="hljs-type">int</span> val)</span> { servingSize = val; } <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">setServings</span><span class="hljs-params">(<span class="hljs-type">int</span> val)</span> { servings = val; } <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">setCalories</span><span class="hljs-params">(<span class="hljs-type">int</span> val)</span> { calories = val; } <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">setFat</span><span class="hljs-params">(<span class="hljs-type">int</span> val)</span> { fat = val; } <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">setSodium</span><span class="hljs-params">(<span class="hljs-type">int</span> val)</span> { sodium = val; } <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">setCarbohydrate</span><span class="hljs-params">(<span class="hljs-type">int</span> val)</span> { carbohydrate = val; }}</code></pre><p>这种模式比可伸缩构造函数模式更易读,但有严重的缺点。因为对象的构建要调用多个set方法,所以对象可能会在构建过程中处于不一致状态(多线程下,其它线程使用了未构建完成的对象)</p><p>第三种选择是建造者模式,它结合了可伸缩构造函数模式的安全性和JavaBean模式的可读性。客户端不直接生成所需的对象,而是使用所有必需的参数调用构造函数或静态工厂方法生成一个builder对象。然后,客户端在builder对象上调用像set一样的方法来设置可选参数,最后调用build方法生成所需对象。Builder通常是它构建的类的静态成员类</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Builder Pattern</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">NutritionFacts</span> { <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> servingSize; <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> servings; <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> calories; <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> fat; <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> sodium; <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> carbohydrate; <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">Builder</span> { <span class="hljs-comment">// Required parameters</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> servingSize; <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> servings; <span class="hljs-comment">// Optional parameters - initialized to default values</span> <span class="hljs-keyword">private</span> <span class="hljs-type">int</span> <span class="hljs-variable">calories</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; <span class="hljs-keyword">private</span> <span class="hljs-type">int</span> <span class="hljs-variable">fat</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; <span class="hljs-keyword">private</span> <span class="hljs-type">int</span> <span class="hljs-variable">sodium</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; <span class="hljs-keyword">private</span> <span class="hljs-type">int</span> <span class="hljs-variable">carbohydrate</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; <span class="hljs-keyword">public</span> <span class="hljs-title function_">Builder</span><span class="hljs-params">(<span class="hljs-type">int</span> servingSize, <span class="hljs-type">int</span> servings)</span> { <span class="hljs-built_in">this</span>.servingSize = servingSize; <span class="hljs-built_in">this</span>.servings = servings; } <span class="hljs-keyword">public</span> Builder <span class="hljs-title function_">calories</span><span class="hljs-params">(<span class="hljs-type">int</span> val)</span> { calories = val; <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>; } <span class="hljs-keyword">public</span> Builder <span class="hljs-title function_">fat</span><span class="hljs-params">(<span class="hljs-type">int</span> val)</span> { fat = val; <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>; } <span class="hljs-keyword">public</span> Builder <span class="hljs-title function_">sodium</span><span class="hljs-params">(<span class="hljs-type">int</span> val)</span> { sodium = val; <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>; } <span class="hljs-keyword">public</span> Builder <span class="hljs-title function_">carbohydrate</span><span class="hljs-params">(<span class="hljs-type">int</span> val)</span> { carbohydrate = val; <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>; } <span class="hljs-keyword">public</span> NutritionFacts <span class="hljs-title function_">build</span><span class="hljs-params">()</span> { <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">NutritionFacts</span>(<span class="hljs-built_in">this</span>); } } <span class="hljs-keyword">private</span> <span class="hljs-title function_">NutritionFacts</span><span class="hljs-params">(Builder builder)</span> { servingSize = builder.servingSize; servings = builder.servings; calories = builder.calories; fat = builder.fat; sodium = builder.sodium; carbohydrate = builder.carbohydrate; }}</code></pre><p>NutritionFacts类是不可变的。Builder的set方法返回builder对象本身,这样就可以链式调用。下面是客户端代码的样子:</p><pre class="highlight"><code class="java"><span class="hljs-type">NutritionFacts</span> <span class="hljs-variable">cocaCola</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">NutritionFacts</span>.Builder(<span class="hljs-number">240</span>, <span class="hljs-number">8</span>).calories(<span class="hljs-number">100</span>).sodium(<span class="hljs-number">35</span>).carbohydrate(<span class="hljs-number">27</span>).build();</code></pre><p>为了简介,这里省略的参数校验。参数校验在Builder的构造函数和方法中。多参数校验在build方法中</p><p>建造者模式也适用于抽象类,抽象类有抽象Builder</p><pre class="highlight"><code class="java"><span class="hljs-keyword">import</span> java.util.EnumSet;<span class="hljs-keyword">import</span> java.util.Objects;<span class="hljs-keyword">import</span> java.util.Set;<span class="hljs-comment">// Builder pattern for class hierarchies</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">Pizza</span> { <span class="hljs-keyword">public</span> <span class="hljs-keyword">enum</span> <span class="hljs-title class_">Topping</span> {HAM, MUSHROOM, ONION, PEPPER, SAUSAGE} <span class="hljs-keyword">final</span> Set<Topping> toppings; <span class="hljs-keyword">abstract</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">Builder</span><T <span class="hljs-keyword">extends</span> <span class="hljs-title class_">Builder</span><T>> { EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class); <span class="hljs-keyword">public</span> T <span class="hljs-title function_">addTopping</span><span class="hljs-params">(Topping topping)</span> { toppings.add(Objects.requireNonNull(topping)); <span class="hljs-keyword">return</span> self(); } <span class="hljs-keyword">abstract</span> Pizza <span class="hljs-title function_">build</span><span class="hljs-params">()</span>; <span class="hljs-comment">// Subclasses must override this method to return "this"</span> <span class="hljs-keyword">protected</span> <span class="hljs-keyword">abstract</span> T <span class="hljs-title function_">self</span><span class="hljs-params">()</span>; } Pizza(Builder<?> builder) { toppings = builder.toppings.clone(); <span class="hljs-comment">// See Item 50</span> }}</code></pre><pre class="highlight"><code class="java"><span class="hljs-keyword">import</span> java.util.Objects;<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">NyPizza</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_">Pizza</span> { <span class="hljs-keyword">public</span> <span class="hljs-keyword">enum</span> <span class="hljs-title class_">Size</span> {SMALL, MEDIUM, LARGE} <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> Size size; <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">Builder</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_">Pizza</span>.Builder<Builder> { <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> Size size; <span class="hljs-keyword">public</span> <span class="hljs-title function_">Builder</span><span class="hljs-params">(Size size)</span> { <span class="hljs-built_in">this</span>.size = Objects.requireNonNull(size); } <span class="hljs-meta">@Override</span> <span class="hljs-keyword">public</span> NyPizza <span class="hljs-title function_">build</span><span class="hljs-params">()</span> { <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">NyPizza</span>(<span class="hljs-built_in">this</span>); } <span class="hljs-meta">@Override</span> <span class="hljs-keyword">protected</span> Builder <span class="hljs-title function_">self</span><span class="hljs-params">()</span> { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>; } } <span class="hljs-keyword">private</span> <span class="hljs-title function_">NyPizza</span><span class="hljs-params">(Builder builder)</span> { <span class="hljs-built_in">super</span>(builder); size = builder.size; }}<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">Calzone</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_">Pizza</span> { <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-type">boolean</span> sauceInside; <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">Builder</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_">Pizza</span>.Builder<Builder> { <span class="hljs-keyword">private</span> <span class="hljs-type">boolean</span> <span class="hljs-variable">sauceInside</span> <span class="hljs-operator">=</span> <span class="hljs-literal">false</span>; <span class="hljs-comment">// Default</span> <span class="hljs-keyword">public</span> Builder <span class="hljs-title function_">sauceInside</span><span class="hljs-params">()</span> { sauceInside = <span class="hljs-literal">true</span>; <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>; } <span class="hljs-meta">@Override</span> <span class="hljs-keyword">public</span> Calzone <span class="hljs-title function_">build</span><span class="hljs-params">()</span> { <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Calzone</span>(<span class="hljs-built_in">this</span>); } <span class="hljs-meta">@Override</span> <span class="hljs-keyword">protected</span> Builder <span class="hljs-title function_">self</span><span class="hljs-params">()</span> { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>; } } <span class="hljs-keyword">private</span> <span class="hljs-title function_">Calzone</span><span class="hljs-params">(Builder builder)</span> { <span class="hljs-built_in">super</span>(builder); sauceInside = builder.sauceInside; }}</code></pre><p>客户端实例化对象代码如下:</p><pre class="highlight"><code class="java"><span class="hljs-type">NyPizza</span> <span class="hljs-variable">pizza</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">NyPizza</span>.Builder(SMALL).addTopping(SAUSAGE).addTopping(ONION).build();<span class="hljs-type">Calzone</span> <span class="hljs-variable">calzone</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Calzone</span>.Builder().addTopping(HAM).sauceInside().build();</code></pre><p>建造者模式非常灵活,一个builder对象可以反复构建多个对象,可以通过builder中的方法调用生成不同的对象。建造者模式的缺点就是生成一个对象前要先创建它的builder对象,性能会稍差一些,但为了未来字段更好扩展,建议还是用建造者模式</p><h3 id="3-使用私有构造函数或枚举类型创建单例"><a class="markdownIt-Anchor" href="#3-使用私有构造函数或枚举类型创建单例"></a> 3. 使用私有构造函数或枚举类型创建单例</h3><p>单例是只实例化一次的类。当类不保存状态或状态都一致,那么它的对象本质上都是一样的,可以用单例模式创建</p><p>实现单例有两种方法。两者都基于私有化构造函数和对外提供 public static 成员,在第一个方法中,该成员是个用final修饰的字段</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Singleton with public final field</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">Elvis</span> { <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">Elvis</span> <span class="hljs-variable">INSTANCE</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Elvis</span>(); <span class="hljs-keyword">private</span> <span class="hljs-title function_">Elvis</span><span class="hljs-params">()</span> { ... } <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">leaveTheBuilding</span><span class="hljs-params">()</span> { ... }}</code></pre><p>私有构造函数只调用一次,用于初始化public static final 修饰的Elvis类型字段INSTANCE。一旦初始化Elvis类,就只会存在一个Elvis实例。客户端不能再创建别的实例,但是要注意的是拥有特殊权限的客户端可以利用反射调用私有构造函数生成实例</p><pre class="highlight"><code class="java">Constructor<?>[] constructors = Elvis.class.getDeclaredConstructors();AccessibleObject.setAccessible(constructors, <span class="hljs-literal">true</span>);Arrays.stream(constructors).forEach(name -> { <span class="hljs-keyword">if</span> (name.toString().contains(<span class="hljs-string">"Elvis"</span>)) { <span class="hljs-type">Elvis</span> <span class="hljs-variable">instance</span> <span class="hljs-operator">=</span> (Elvis) name.newInstance(); instance.leaveTheBuilding(); }});</code></pre><p>第二种方法,对外提供的 public static 成员是个静态工厂方法</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Singleton with static factory</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">Elvis</span> { <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">Elvis</span> <span class="hljs-variable">INSTANCE</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Elvis</span>(); <span class="hljs-keyword">private</span> <span class="hljs-title function_">Elvis</span><span class="hljs-params">()</span> { ... } <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Elvis <span class="hljs-title function_">getInstance</span><span class="hljs-params">()</span> { <span class="hljs-keyword">return</span> INSTANCE; } <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">leaveTheBuilding</span><span class="hljs-params">()</span> { ... }}</code></pre><p>所有对getInsance()方法的调用都返回相同的对象,但是同样可以通过反射调用私有构造函数创建对象。<br />这两种方法要实现可序列化,仅仅在声明中添加implements Serializable是不够的。还要声明所有实例字段未transient,并提供readResolve方法,否则,每次反序列化都会创建一个新实例。JVM在反序列化时会自动调用readResolve方法</p><pre class="highlight"><code class="java"><span class="hljs-comment">// readResolve method to preserve singleton property</span><span class="hljs-keyword">private</span> Object <span class="hljs-title function_">readResolve</span><span class="hljs-params">()</span> { <span class="hljs-comment">// Return the one true Elvis and let the garbage collector</span> <span class="hljs-comment">// take care of the Elvis impersonator.</span> <span class="hljs-keyword">return</span> INSTANCE;}</code></pre><p>实现单例的第三种方法时声明一个单元素枚举</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Enum singleton - the preferred approach</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">enum</span> <span class="hljs-title class_">Elvis</span> { INSTANCE; <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">leaveTheBuilding</span><span class="hljs-params">()</span> { ... }}</code></pre><p>这种方法类似于 public 字段方法,但是它更简洁,默认提供了序列化机制,提供了对多个实例化的严格保证,即使面对复杂的序列化或反射攻击也是如此。这种方法可能有点不自然,但是单元素枚举类型通常是实现单例的最佳方法。但是,如果你的单例要继承父类,那么就不能用这种方法</p><h3 id="4-用私有构造函数实施不可实例化"><a class="markdownIt-Anchor" href="#4-用私有构造函数实施不可实例化"></a> 4. 用私有构造函数实施不可实例化</h3><p>工具类不需要实例化,它里面的方法都是public static的,可以通过私有化构造函数使类不可实例化</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Noninstantiable utility class</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">UtilityClass</span> { <span class="hljs-comment">// Suppress default constructor for noninstantiability</span> <span class="hljs-keyword">private</span> <span class="hljs-title function_">UtilityClass</span><span class="hljs-params">()</span> { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">AssertionError</span>(); } ... <span class="hljs-comment">// Remainder omitted</span>}</code></pre><p>AssertionError不是必须有的,但可以防止构造函数被意外调用(反射)</p><p>这种用法也防止了类被继承。因为所有子类构造函数都必须显示或者隐式调用父类构造函数,但父类构造函数私有化后就无法调用</p><h3 id="5-依赖注入优于硬连接资源"><a class="markdownIt-Anchor" href="#5-依赖注入优于硬连接资源"></a> 5. 依赖注入优于硬连接资源</h3><p>有些类依赖于一个或多个资源。例如拼写检查程序依赖于字典。错误实现如下:</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Inappropriate use of static utility - inflexible & untestable!</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">SpellChecker</span> { <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">Lexicon</span> <span class="hljs-variable">dictionary</span> <span class="hljs-operator">=</span> ...; <span class="hljs-keyword">private</span> <span class="hljs-title function_">SpellChecker</span><span class="hljs-params">()</span> {} <span class="hljs-comment">// Noninstantiable</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-type">boolean</span> <span class="hljs-title function_">isValid</span><span class="hljs-params">(String word)</span> { ... } <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> List<String> <span class="hljs-title function_">suggestions</span><span class="hljs-params">(String typo)</span> { ... }}</code></pre><pre class="highlight"><code class="java"><span class="hljs-comment">// Inappropriate use of singleton - inflexible & untestable!</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">SpellChecker</span> { <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-type">Lexicon</span> <span class="hljs-variable">dictionary</span> <span class="hljs-operator">=</span> ...; <span class="hljs-keyword">private</span> <span class="hljs-title function_">SpellChecker</span><span class="hljs-params">(...)</span> {} <span class="hljs-keyword">public</span> <span class="hljs-type">static</span> <span class="hljs-variable">INSTANCE</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">SpellChecker</span>(...); <span class="hljs-keyword">public</span> <span class="hljs-type">boolean</span> <span class="hljs-title function_">isValid</span><span class="hljs-params">(String word)</span> { ... } <span class="hljs-keyword">public</span> List<String> <span class="hljs-title function_">suggestions</span><span class="hljs-params">(String typo)</span> { ... }}</code></pre><p>这两种写法分别是工具类和单例,都假定使用同一个字典,实际情况是不同拼写检查程序依赖不同的字典。</p><p>你可能会想取消dictionary的final修饰,并让SpellChecker类添加更改dictionary的方法。但这种方法在并发环境下会出错。工具类和单例不适用于需要参数化依赖对象</p><p>参数化依赖对象的一种简单模式是,创建对象时将被依赖的对象传递给构造函数。这是依赖注入的一种形式。</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Dependency injection provides flexibility and testability</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">SpellChecker</span> { <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> Lexicon dictionary; <span class="hljs-keyword">public</span> <span class="hljs-title function_">SpellChecker</span><span class="hljs-params">(Lexicon dictionary)</span> { <span class="hljs-built_in">this</span>.dictionary = Objects.requireNonNull(dictionary); } <span class="hljs-keyword">public</span> <span class="hljs-type">boolean</span> <span class="hljs-title function_">isValid</span><span class="hljs-params">(String word)</span> { ... } <span class="hljs-keyword">public</span> List<String> <span class="hljs-title function_">suggestions</span><span class="hljs-params">(String typo)</span> { ... }}</code></pre><p>依赖注入适用于构造函数、静态工厂方法、建造者模式。</p><p>依赖注入的一个有用变体是将工厂传递给构造函数,这样可以反复创建被依赖的对象。Java8中引入的Supplier<T>非常适合作为工厂。下面是一个生产瓷砖的方法</p><pre class="highlight"><code class="java">Mosaic <span class="hljs-title function_">create</span><span class="hljs-params">(Supplier<? extends Tile> tileFactory)</span> { ... }</code></pre><p>尽管依赖注入极大提高了灵活性和可测试性,但它可能会使大型项目变得混乱。通过使用依赖注入框架(如Spring、Dagger、Guice)可以消除这种混乱</p><h3 id="6-避免创建不必要的对象"><a class="markdownIt-Anchor" href="#6-避免创建不必要的对象"></a> 6. 避免创建不必要的对象</h3><p>复用对象可以加快程序运行速度,如果对象是不可变的,那么它总是可以被复用的。</p><p>一个不复用对象的极端例子如下</p><pre class="highlight"><code class="java"><span class="hljs-type">String</span> <span class="hljs-variable">s</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">String</span>(<span class="hljs-string">"bikini"</span>); <span class="hljs-comment">// DON'T DO THIS!</span></code></pre><p>该语句每次执行都会创建一个新的String实例。"bikini"本身就是一个String实例,改进的代码如下</p><pre class="highlight"><code class="java"><span class="hljs-type">String</span> <span class="hljs-variable">s</span> <span class="hljs-operator">=</span> <span class="hljs-string">"bikini"</span>;</code></pre><p>这个版本使用单个String实例,而不是每次执行时都会创建一个新的实例。此外,可以保证在同一虚拟机中运行的其它代码都可以复用该对象,只要它们都包含相同的字符串字面量</p><p>通常可以使用静态工厂方法来避免创建不必要的对象。例如,Boolean.valueOf(String) 比构造函数Boolean(String) 更可取,后者在Java9中被废弃了。</p><p>有些对象的创建代价很高,如果要重复使用,最好做缓存。例如,使用正则表达式确定字符串是否为有效的罗马数字</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Performance can be greatly improved!</span><span class="hljs-keyword">static</span> <span class="hljs-type">boolean</span> <span class="hljs-title function_">isRomanNumeral</span><span class="hljs-params">(String s)</span> { <span class="hljs-keyword">return</span> s.matches(<span class="hljs-string">"^(?=.)M*(C[MD]|D?C{0,3})"</span> + <span class="hljs-string">"(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$"</span>);}</code></pre><p>这个方法的问题在于它依赖String.matches方法。虽然 String.matches 是检查字符串是否与正则表达式匹配的最简单方法,但它不适合要求高性能的情况下重复使用,因为它在内部为正则表达式创建了一个Pattern实例,并且只使用了一次就垃圾回收了,创建一个Pattern实例代价很大,因为它需要将正则表达式编译成有限的状态机</p><p>为了提高性能,将正则表达式显示编译成Pattern实例,作为类初始化的一部分,每次调用匹配的方法时都使用这个实例</p><pre class="highlight"><code class="java"><span class="hljs-comment">// Reusing expensive object for improved performance</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">RomanNumerals</span> { <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">Pattern</span> <span class="hljs-variable">ROMAN</span> <span class="hljs-operator">=</span> Pattern.compile(<span class="hljs-string">"^(?=.)M*(C[MD]|D?C{0,3})"</span> + <span class="hljs-string">"(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$"</span>); <span class="hljs-keyword">static</span> <span class="hljs-type">boolean</span> <span class="hljs-title function_">isRomanNumeral</span><span class="hljs-params">(String s)</span> { <span class="hljs-keyword">return</span> ROMAN.matcher(s).matches(); }}</code></pre><p>当对象是不可变的时候,我们很容易会想到复用。但是有些情况不那么容易想到复用。例如适配器模式中的适配器,因为适配器中没有它适配对象的状态,所以不需要创建多个适配器。</p><p>一个具体的例子是,KeySet是Map中的适配器,使得Map不仅能提供返回键值对的方法,也能返回所有键的Set,KeySet不需要重复创建,对Map的修改会同步到KeySet实例</p><h3 id="7-排除过时的对象引用"><a class="markdownIt-Anchor" href="#7-排除过时的对象引用"></a> 7. 排除过时的对象引用</h3><p>Java具体垃圾回收机制,会让程序员的工作轻松很多,但是并不意味着需要考虑内存管理,考虑以下简单的堆栈实现</p><pre class="highlight"><code class="java"><span class="hljs-keyword">import</span> java.util.Arrays;<span class="hljs-keyword">import</span> java.util.EmptyStackException;<span class="hljs-comment">// Can you spot the "memory leak"?</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">Stack</span> { <span class="hljs-keyword">private</span> Object[] elements; <span class="hljs-keyword">private</span> <span class="hljs-type">int</span> <span class="hljs-variable">size</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> <span class="hljs-variable">DEFAULT_INITIAL_CAPACITY</span> <span class="hljs-operator">=</span> <span class="hljs-number">16</span>; <span class="hljs-keyword">public</span> <span class="hljs-title function_">Stack</span><span class="hljs-params">()</span> { elements = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Object</span>[DEFAULT_INITIAL_CAPACITY]; } <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">push</span><span class="hljs-params">(Object e)</span> { ensureCapacity(); elements[size++] = e; } <span class="hljs-keyword">public</span> Object <span class="hljs-title function_">pop</span><span class="hljs-params">()</span> { <span class="hljs-keyword">if</span> (size == <span class="hljs-number">0</span>) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">EmptyStackException</span>(); <span class="hljs-keyword">return</span> elements[--size]; } <span class="hljs-comment">/** * Ensure space for at least one more element, roughly * doubling the capacity each time the array needs to grow. */</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">ensureCapacity</span><span class="hljs-params">()</span> { <span class="hljs-keyword">if</span> (elements.length == size) elements = Arrays.copyOf(elements, <span class="hljs-number">2</span> * size + <span class="hljs-number">1</span>); }}</code></pre><p>这段代码有一个潜在的内存泄露问题。当栈的长度增加,再收缩时,从栈中pop的对象不会被回收,因为引用仍然还在elements中。解决方法很简单,一旦pop就置空</p><pre class="highlight"><code class="java"><span class="hljs-keyword">public</span> Object <span class="hljs-title function_">pop</span><span class="hljs-params">()</span> { <span class="hljs-keyword">if</span> (size == <span class="hljs-number">0</span>) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">EmptyStackException</span>(); <span class="hljs-type">Object</span> <span class="hljs-variable">result</span> <span class="hljs-operator">=</span> elements[--size]; elements[size] = <span class="hljs-literal">null</span>; <span class="hljs-comment">// Eliminate obsolete reference</span> <span class="hljs-keyword">return</span> result;}</code></pre><p>用null处理过时引用的另一个好处是,如果被意外引用的话会立刻抛NullPointerException</p><p>另外一个常见的内存泄漏是缓存。HashMap的key是对实际对象的强引用,不会被GC回收。WeakHashMap的key如果只有WeakHashMap本身使用,外部没有使用,那么会被GC回收</p><p>内存泄露第三种常见来源是监听器和其它回调。如果客户端注册了回调但是没有显式地取消它们,它们就会一直在内存中。确保回调被及时回收的一种方法是仅存储它们的弱引用,例如,将他它们作为键存储在WeakHashMap中</p><h3 id="8-避免使用终结器和清除器"><a class="markdownIt-Anchor" href="#8-避免使用终结器和清除器"></a> 8. 避免使用终结器和清除器</h3><p>如标题所说</p><h3 id="9-使用-try-with-resources-优于-try-finally"><a class="markdownIt-Anchor" href="#9-使用-try-with-resources-优于-try-finally"></a> 9. 使用 try-with-resources 优于 try-finally</h3><p>Java库中有许多必须通过调用close方法手动关闭的资源,比如InputStream、OutputStream和java.sql.Connection。</p><p>从历史上看,try-finally语句是确保正确关闭资源的最佳方法,即便出现异常或返回</p><pre class="highlight"><code class="java"><span class="hljs-comment">// try-finally - No longer the best way to close resources!</span><span class="hljs-keyword">static</span> String <span class="hljs-title function_">firstLineOfFile</span><span class="hljs-params">(String path)</span> <span class="hljs-keyword">throws</span> IOException { <span class="hljs-type">BufferedReader</span> <span class="hljs-variable">br</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">BufferedReader</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">FileReader</span>(path)); <span class="hljs-keyword">try</span> { <span class="hljs-keyword">return</span> br.readLine(); } <span class="hljs-keyword">finally</span> { br.close(); }}</code></pre><p>这种方式在资源变多时就很糟糕</p><pre class="highlight"><code class="java"><span class="hljs-comment">// try-finally is ugly when used with more than one resource!</span><span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">copy</span><span class="hljs-params">(String src, String dst)</span> <span class="hljs-keyword">throws</span> IOException { <span class="hljs-type">InputStream</span> <span class="hljs-variable">in</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">FileInputStream</span>(src); <span class="hljs-keyword">try</span> { <span class="hljs-type">OutputStream</span> <span class="hljs-variable">out</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">FileOutputStream</span>(dst); <span class="hljs-keyword">try</span> { <span class="hljs-type">byte</span>[] buf = <span class="hljs-keyword">new</span> <span class="hljs-title class_">byte</span>[BUFFER_SIZE]; <span class="hljs-type">int</span> n; <span class="hljs-keyword">while</span> ((n = in.read(buf)) >= <span class="hljs-number">0</span>) out.write(buf, <span class="hljs-number">0</span>, n); } <span class="hljs-keyword">finally</span> { out.close(); } } <span class="hljs-keyword">finally</span> { in.close(); }}</code></pre><p>使用 try-finally 语句关闭资源的正确代码(如前两个代码示例所示)也有一个细微的缺陷。try 块和 finally 块中的代码都能够抛出异常。例如,在 firstLineOfFile 方法中,由于底层物理设备发生故障,对 readLine 的调用可能会抛出异常,而关闭的调用也可能出于同样的原因而失败。在这种情况下,第二个异常将完全覆盖第一个异常。异常堆栈跟踪中没有第一个异常的记录,这可能会使实际系统中的调试变得非常复杂(而这可能是希望出现的第一个异常,以便诊断问题)</p><p>Java7 引入 try-with-resources语句解决了这个问题。要使用这个结构,资源必须实现AutoCloseable接口,这个接口只有一个void close方法,下面是前两个例子的try-with-resources形式</p><pre class="highlight"><code class="java"><span class="hljs-comment">// try-with-resources - the the best way to close resources!</span><span class="hljs-keyword">static</span> String <span class="hljs-title function_">firstLineOfFile</span><span class="hljs-params">(String path)</span> <span class="hljs-keyword">throws</span> IOException { <span class="hljs-keyword">try</span> (<span class="hljs-type">BufferedReader</span> <span class="hljs-variable">br</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">BufferedReader</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">FileReader</span>(path))) { <span class="hljs-keyword">return</span> br.readLine(); }}</code></pre><pre class="highlight"><code class="java"><span class="hljs-comment">// try-with-resources on multiple resources - short and sweet</span><span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">copy</span><span class="hljs-params">(String src, String dst)</span> <span class="hljs-keyword">throws</span> IOException { <span class="hljs-keyword">try</span> (<span class="hljs-type">InputStream</span> <span class="hljs-variable">in</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">FileInputStream</span>(src);<span class="hljs-type">OutputStream</span> <span class="hljs-variable">out</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">FileOutputStream</span>(dst)) { <span class="hljs-type">byte</span>[] buf = <span class="hljs-keyword">new</span> <span class="hljs-title class_">byte</span>[BUFFER_SIZE]; <span class="hljs-type">int</span> n; <span class="hljs-keyword">while</span> ((n = in.read(buf)) >= <span class="hljs-number">0</span>) out.write(buf, <span class="hljs-number">0</span>, n); }}</code></pre><p>try-with-resources为开发者提供了更好的异常排查方式。考虑firstLineOfFile方法,如果异常由readLine和不可见close抛出,那么后者异常会被抑制。被抑制的异常不会被抛弃,它们会被打印在堆栈中并标记被抑制。可以通过getSuppressed方法访问它们,该方法是Java7中添加到Throwable中的</p><p>像try-catch-finally一样,try-with-resources也可以写catch语句。下面是firstLineOfFile方法的一个版本,它不抛出异常,但如果无法打开文件或从中读取文件,会返回一个默认值</p><pre class="highlight"><code class="java"><span class="hljs-comment">// try-with-resources with a catch clause</span><span class="hljs-keyword">static</span> String <span class="hljs-title function_">firstLineOfFile</span><span class="hljs-params">(String path, String defaultVal)</span> { <span class="hljs-keyword">try</span> (<span class="hljs-type">BufferedReader</span> <span class="hljs-variable">br</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">BufferedReader</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">FileReader</span>(path))) { <span class="hljs-keyword">return</span> br.readLine(); } <span class="hljs-keyword">catch</span> (IOException e) { <span class="hljs-keyword">return</span> defaultVal; }}</code></pre>]]></content>
<summary type="html"><h2 id="第二章-创建和销毁对象"><a class="markdownIt-Anchor" href="#第二章-创建和销毁对象"></a> 第二章 创建和销毁对象</h2>
<h3 id="1-考虑用静态工厂方法代替构造函数"><a class="markdownIt-</summary>
<category term="Java" scheme="https://zunpan.github.io/categories/Java/"/>
<category term="Effective-Java" scheme="https://zunpan.github.io/tags/Effective-Java/"/>
<category term="创建和销毁对象" scheme="https://zunpan.github.io/tags/%E5%88%9B%E5%BB%BA%E5%92%8C%E9%94%80%E6%AF%81%E5%AF%B9%E8%B1%A1/"/>
</entry>
<entry>
<title>跨域问题</title>
<link href="https://zunpan.github.io/2022/06/22/%E8%B7%A8%E5%9F%9F%E9%97%AE%E9%A2%98/"/>
<id>https://zunpan.github.io/2022/06/22/%E8%B7%A8%E5%9F%9F%E9%97%AE%E9%A2%98/</id>
<published>2022-06-22T09:12:37.000Z</published>
<updated>2023-09-24T04:27:40.285Z</updated>
<content type="html"><![CDATA[<h2 id="同源策略"><a class="markdownIt-Anchor" href="#同源策略"></a> 同源策略</h2><p>同源策略是浏览器的重要安全策略。大致来说,不同域(协议+域名/ip+端口)生成的cookie只能给这个域使用</p><h2 id="跨域出现与解决"><a class="markdownIt-Anchor" href="#跨域出现与解决"></a> 跨域出现与解决</h2><p>下面的演示是在hosts文件中添加以下配置</p><pre class="highlight"><code class="text">127.0.0.1 zhufeng-test.163.com</code></pre><p>不添加的话,把zhufeng-test.163.com换成localhost或者127.0.0.1是一样的</p><p>假如我们只有后端,它的域是<a href="http://zhufeng-test.163.com:8080">http://zhufeng-test.163.com:8080</a>,它有一个get方法是setCookie,那么访问<a href="http://zhufeng-test.163.com:8080/setCookie">http://zhufeng-test.163.com:8080/setCookie</a>,此时可以在Response Cookies中找到这个cookie,对这个域下的接口访问时会自动带上这个域所有可见的cookie(springboot开启allowCredentials,前端axios需要自己开启withCredentials: true)<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/%E8%B7%A8%E5%9F%9F%E9%97%AE%E9%A2%98/IMG_20220618-105411813.png" alt="图 2" /></p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/%E8%B7%A8%E5%9F%9F%E9%97%AE%E9%A2%98/IMG_20220618-105444278.png" alt="图 3" /></p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/%E8%B7%A8%E5%9F%9F%E9%97%AE%E9%A2%98/IMG_20220618-105349152.png" alt="图 1" /></p><p>现在我们有前端了,前后端分离运行,前端运行在<a href="http://zhufeng-test.163.com:8081">http://zhufeng-test.163.com:8081</a></p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/%E8%B7%A8%E5%9F%9F%E9%97%AE%E9%A2%98/IMG_20220618-111058801.png" alt="图 6" /></p><p>因为后端生成的cookie的domain是 <a href="http://zhufeng-test.163.com">zhufeng-test.163.com</a>,所以浏览器访问相同域名的前端也可以读取到这个cookie。<br />但是前端不能访问后端的其它接口,因为它们端口不同,发生了跨域,浏览器不会带上cookie<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/%E8%B7%A8%E5%9F%9F%E9%97%AE%E9%A2%98/IMG_20220618-111419228.png" alt="图 7" /></p><p>给后端配置一下跨域,.allowedOrigins(“<a href="http://zhufeng-test.163.com:8081/">http://zhufeng-test.163.com:8081/</a>”)表示允许前端<a href="http://zhufeng-test.163.com:8081">http://zhufeng-test.163.com:8081</a>访问后端,不开会返回状态码200的跨域错误,.allowCredentials(true)不开时前端访问后端不会带上后端域名可见的cookie</p><pre class="highlight"><code class="Java"><span class="hljs-meta">@Configuration</span><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">CorsConfig</span> <span class="hljs-keyword">implements</span> <span class="hljs-title class_">WebMvcConfigurer</span> { <span class="hljs-meta">@Override</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">addCorsMappings</span><span class="hljs-params">(CorsRegistry registry)</span> { <span class="hljs-comment">//项目中的所有接口都支持跨域</span> registry.addMapping(<span class="hljs-string">"/**"</span>) <span class="hljs-comment">// 所有地址都可以访问,也可以配置具体地址</span> <span class="hljs-comment">// .allowedOrigins("http://zhufeng-test.163.com:8081/")</span> .allowedOriginPatterns(<span class="hljs-string">"*://*:*/"</span>) <span class="hljs-comment">// "GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"</span> .allowedMethods(<span class="hljs-string">"*"</span>) <span class="hljs-comment">// 允许前端携带后端域名可见的cookie</span> .allowCredentials(<span class="hljs-literal">true</span>) .maxAge(<span class="hljs-number">3600</span>); }}</code></pre><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/%E8%B7%A8%E5%9F%9F%E9%97%AE%E9%A2%98/IMG_20220618-111742704.png" alt="图 8" /></p>]]></content>
<summary type="html"><h2 id="同源策略"><a class="markdownIt-Anchor" href="#同源策略"></a> 同源策略</h2>
<p>同源策略是浏览器的重要安全策略。大致来说,不同域(协议+域名/ip+端口)生成的cookie只能给这个域使用</p>
<h2 id=</summary>
<category term="基础知识" scheme="https://zunpan.github.io/categories/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/"/>
<category term="CORS" scheme="https://zunpan.github.io/tags/CORS/"/>
<category term="Java" scheme="https://zunpan.github.io/tags/Java/"/>
</entry>
<entry>
<title>CryptographyⅠ笔记</title>
<link href="https://zunpan.github.io/2022/06/04/Cryptography%E2%85%A0%E7%AC%94%E8%AE%B0/"/>
<id>https://zunpan.github.io/2022/06/04/Cryptography%E2%85%A0%E7%AC%94%E8%AE%B0/</id>
<published>2022-06-04T06:51:30.000Z</published>
<updated>2023-09-24T04:27:40.272Z</updated>
<content type="html"><![CDATA[<p>斯坦福教授Dan Boneh的密码学课程《Cryptography》<br />课程链接:<a href="https://www.bilibili.com/video/BV1Ht411w7Re">https://www.bilibili.com/video/BV1Ht411w7Re</a><br />讲义:<a href="https://www.cs.virginia.edu/~evans/courses/crypto-notes.pdf">https://www.cs.virginia.edu/~evans/courses/crypto-notes.pdf</a></p><p>目标:课程结束后可以推导加密机制的安全性,可以破解不安全的加密机制</p><h2 id="绪论"><a class="markdownIt-Anchor" href="#绪论"></a> 绪论</h2><h3 id="密码学用途"><a class="markdownIt-Anchor" href="#密码学用途"></a> 密码学用途</h3><ol><li>安全通信,例如https,Bluetooth</li><li>加密磁盘文件,例如EFS</li><li>内容保护,例如DVD使用CSS</li><li>用户认证</li></ol><p>高阶用途:</p><ol><li>外包计算(同态加密)</li><li>安全多方计算</li><li>零知识证明</li></ol><h4 id="安全通信"><a class="markdownIt-Anchor" href="#安全通信"></a> 安全通信</h4><p>在web服务中使用https协议进行通信,起到安全通信作用的是TLS协议,TLS协议主要包括两部分:</p><ol><li>握手协议:使用公钥密码体制建立共享密钥</li><li>Record层:使用共享密钥传输数据,保证数据的机密性和完整性<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220523-212615502.png" alt="图 1" /></li></ol><h4 id="加密磁盘文件"><a class="markdownIt-Anchor" href="#加密磁盘文件"></a> 加密磁盘文件</h4><p>加密磁盘文件从哲学角度来看就是今天的Alice和明天的Alice进行安全通信<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220523-212850304.png" alt="图 2" /></p><h3 id="对称加密"><a class="markdownIt-Anchor" href="#对称加密"></a> 对称加密</h3><p>Alice和Bob双方共享密钥k,攻击者不知道密钥k,他们使用两个算法进行通信,分别是加密算法E,解密算法D。加密算法以原信息和密钥为输入产生相应密文;解密算法正好相反,以密文和密钥为输入,输出原信息。加密解密算法是公开的,只有密钥k是保密的。我们<strong>应当使用公开的算法,因为它的安全性经过业内人士的审查</strong>。<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220523-213531700.png" alt="图 3" /></p><h3 id="密钥使用次数"><a class="markdownIt-Anchor" href="#密钥使用次数"></a> 密钥使用次数</h3><p>可以根据密钥使用次数分为一次使用的密钥,多次使用的密钥<br />一次使用的密钥只用来加密一个信息,例如加密邮件,为每一封邮件都生成一个新的密钥。<br />多次使用的密钥可以用来加密多个消息,例如用同一密钥加密文件系统的许多文件,<strong>多次使用的密钥需要更多机制来确保加密系统是安全的</strong><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220523-214054329.png" alt="图 4" /></p><h3 id="一些结论"><a class="markdownIt-Anchor" href="#一些结论"></a> 一些结论</h3><p>任何依赖可信第三方的计算都可以不借助可信第三方完成<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220523-223110733.png" alt="图 5" /></p><h3 id="密码算法提出三步骤"><a class="markdownIt-Anchor" href="#密码算法提出三步骤"></a> 密码算法提出三步骤</h3><ol><li>明确描述威胁模型。例如数字签名算法中,攻击者如何攻击数字签名?伪造签名目的何在?</li><li>提出算法构成</li><li>证明在该威胁模型下,攻击者破解了算法等同于破解了根本性难题。意思就是算法的安全性由根本性难题保证,例如RSA依赖大整数质因数分解困难问题</li></ol><h3 id="密码历史"><a class="markdownIt-Anchor" href="#密码历史"></a> 密码历史</h3><h4 id="对称密码历史"><a class="markdownIt-Anchor" href="#对称密码历史"></a> 对称密码历史</h4><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220523-225328295.png" alt="图 6" /></p><h5 id="替换式密码"><a class="markdownIt-Anchor" href="#替换式密码"></a> 替换式密码</h5><p>替换式密码的密钥是一张替换表<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220524-093954856.png" alt="图 1" /></p><p>例如:凯撒密码</p><p>凯撒密码是没有密钥或者只有一个固定密钥的替换式密码<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220524-101212632.png" alt="图 3" /></p><p>假设替换式密码的密钥只能使用26个英文字母,它的密钥空间有多大?是<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>26</mn><mo stretchy="false">!</mo></mrow><annotation encoding="application/x-tex">26!</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.69444em;vertical-align:0em;"></span><span class="mord">2</span><span class="mord">6</span><span class="mclose">!</span></span></span></span>,约等于<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mn>2</mn><mn>88</mn></msup></mrow><annotation encoding="application/x-tex">2^{88}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8141079999999999em;vertical-align:0em;"></span><span class="mord"><span class="mord">2</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141079999999999em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">8</span><span class="mord mtight">8</span></span></span></span></span></span></span></span></span></span></span></span>,也就是说可以用88位比特表示密钥,这个密钥空间是足够大的,但是这个加密算法不安全<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220524-102931163.png" alt="图 4" /></p><p>如何破解这个加密算法?</p><ol><li>使用字母频率来破解。在英文文献中,出现频次最高的字母是e,那么我们统计密文中出现频次最高的那个字母,例如c,那么这个替换式密码的密钥k中很可能包含了e->c;同样的道理,出现频次第二高的是t,第三高的是a,分别找到密文中对应的字母进行还原</li><li>使用字母配对(组合)。在英语中,最常见的二元配对有“he”,“an”,“in”,“th”。第一种方法还原出了eta,根据配对规则可以还原配对中的另一个字母。</li></ol><p>这种攻击方式叫唯密文攻击(CT only attack),替换式密码抵御不了这种攻击</p><h5 id="vigener密码"><a class="markdownIt-Anchor" href="#vigener密码"></a> Vigener密码</h5><p>Vigener密码的密钥是一个单词,加密算法是将密钥重复直到和明文一样长,与明文逐位求和模26得到密文,解密算法是对密文逐位减去密钥<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220524-105137000.png" alt="图 5" /></p><p>如何破解Vigener密码?</p><p>首先假设攻击者已知密钥长度(未知也不影响破解,枚举长度即可),例如密钥长度为6,那么攻击者可以将密文按照每6个字母一组进行分组。然后看每组的第一个字母,他们都是由同一个字母加密的,例如上图中k的字母C。我们同样可以使用唯密文攻击,在每组的第一个字母中找到出现频次最高的字母,它们对应的明文位为E,然后对应的密钥为为密文位-E = 密钥位。同样的方法对密钥第2位直到最后一位执行。这种破解方法的实质是多次使用唯密文攻击<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220524-112208851.png" alt="图 6" /></p><h5 id="轮轴机"><a class="markdownIt-Anchor" href="#轮轴机"></a> 轮轴机</h5><p>最早的轮轴机:Hebern machine(一个轮轴)</p><p>本质上是一个替换式密码。按下A,假如A加密成了T,此时轮轴机转动一格,再次按下A,A就加密成了S。轮轴机一经提出很快就被破解了,同样是唯密文攻击。<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220524-114310705.png" alt="图 7" /></p><p>最著名的轮轴机:Enigma(3-5轮轴)<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220524-115017057.png" alt="图 8" /></p><h5 id="数字时代的密码"><a class="markdownIt-Anchor" href="#数字时代的密码"></a> 数字时代的密码</h5><p>1974:DES(keys = <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mn>2</mn><mn>56</mn></msup></mrow><annotation encoding="application/x-tex">2^{56}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8141079999999999em;vertical-align:0em;"></span><span class="mord"><span class="mord">2</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141079999999999em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">5</span><span class="mord mtight">6</span></span></span></span></span></span></span></span></span></span></span></span>, block size = 64 bits)</p><p>Today: AES(2001), Salsa20(2008) (and others)</p><h3 id="离散概率"><a class="markdownIt-Anchor" href="#离散概率"></a> 离散概率</h3><h4 id="有限集合与概率分布"><a class="markdownIt-Anchor" href="#有限集合与概率分布"></a> 有限集合与概率分布</h4><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220524-152421875.png" alt="图 9" /></p><h4 id="事件"><a class="markdownIt-Anchor" href="#事件"></a> 事件</h4><p>事件是有限集合的子集</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220524-153036875.png" alt="图 12" /></p><p>事件的并集发生的概率存在上界<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220524-153726038.png" alt="图 13" /></p><h4 id="随机变量"><a class="markdownIt-Anchor" href="#随机变量"></a> 随机变量</h4><p>随机变量是一个函数 X:U(有限集合)->V(某个集合)<br />集合V是随机变量取值的地方。例如定义随机变量<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>X</mi><mo>:</mo><mo stretchy="false">{</mo><mn>0</mn><mo separator="true">,</mo><mn>1</mn><msup><mo stretchy="false">}</mo><mi>n</mi></msup><mo>⟶</mo><mo stretchy="false">{</mo><mn>0</mn><mo separator="true">,</mo><mn>1</mn><mo stretchy="false">}</mo></mrow><annotation encoding="application/x-tex">X:\{0,1\}^n\longrightarrow \{0,1\}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord mathnormal" style="margin-right:0.07847em;">X</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">:</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">{</span><span class="mord">0</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord">1</span><span class="mclose"><span class="mclose">}</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.664392em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">n</span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">⟶</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">{</span><span class="mord">0</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord">1</span><span class="mclose">}</span></span></span></span>;<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>X</mi><mo stretchy="false">(</mo><mi>y</mi><mo stretchy="false">)</mo><mo>=</mo><mi>l</mi><mi>s</mi><mi>b</mi><mo stretchy="false">(</mo><mi>y</mi><mo stretchy="false">)</mo><mo>∈</mo><mo stretchy="false">{</mo><mn>0</mn><mo separator="true">,</mo><mn>1</mn><mo stretchy="false">}</mo></mrow><annotation encoding="application/x-tex">X(y)=lsb(y) \in \{0,1\}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.07847em;">X</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">s</span><span class="mord mathnormal">b</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">∈</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">{</span><span class="mord">0</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord">1</span><span class="mclose">}</span></span></span></span><br />这个随机变量将n位长度的字符串集合映射成了只有0和1的两个元素的集合,具体做法是取最低位。任意一个n位长度的字符串会被映射出0或者1<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220524-160000158.png" alt="图 14" /></p><h4 id="均匀随机变量"><a class="markdownIt-Anchor" href="#均匀随机变量"></a> 均匀随机变量</h4><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220524-160509461.png" alt="图 15" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220524-160825434.png" alt="图 16" /></p><h4 id="随机化算法"><a class="markdownIt-Anchor" href="#随机化算法"></a> 随机化算法</h4><p>随机化算法相比于确定的算法,在原像中加入了随机因子,所以对同一条消息进行随机化,结果一般是不同的<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220524-161633389.png" alt="图 17" /></p><h4 id="独立性"><a class="markdownIt-Anchor" href="#独立性"></a> 独立性</h4><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220524-164450153.png" alt="图 18" /></p><h4 id="异或"><a class="markdownIt-Anchor" href="#异或"></a> 异或</h4><p>异或就是逐位模2和<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220524-183810231.png" alt="图 20" /></p><p>异或在密码学中非常重要,在一个有限集合上有一个随机变量Y,有一个独立的均匀随机变量X,那么<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>Z</mi><mo>:</mo><mo>=</mo><mi>Y</mi><mo>⨁</mo><mi>X</mi></mrow><annotation encoding="application/x-tex">Z:=Y\bigoplus X</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord mathnormal" style="margin-right:0.07153em;">Z</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">:</span></span><span class="base"><span class="strut" style="height:0.36687em;vertical-align:0em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1.00001em;vertical-align:-0.25001em;"></span><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mop op-symbol small-op" style="position:relative;top:-0.0000050000000000050004em;">⨁</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord mathnormal" style="margin-right:0.07847em;">X</span></span></span></span> 仍然是这个集合上的均匀随机变量<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220524-185136340.png" alt="图 21" /></p><h4 id="生日悖论"><a class="markdownIt-Anchor" href="#生日悖论"></a> 生日悖论</h4><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220524-191101592.png" alt="图 22" /></p><p>r指的是随机变量的值</p><p>独立同分布举例:假设U = {00,01,10,11}表示抛硬币两次的结果,令随机变量<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>X</mi><mtext>:</mtext><mi>U</mi><mo>→</mo><mo stretchy="false">{</mo><mn>0</mn><mo separator="true">,</mo><mn>1</mn><mo stretchy="false">}</mo></mrow><annotation encoding="application/x-tex">X:U\rightarrow\{0,1\}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord mathnormal" style="margin-right:0.07847em;">X</span><span class="mord cjk_fallback">:</span><span class="mord mathnormal" style="margin-right:0.10903em;">U</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">→</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">{</span><span class="mord">0</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord">1</span><span class="mclose">}</span></span></span></span>表示第一次抛硬币的结果,<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>Y</mi><mtext>:</mtext><mi>U</mi><mo>→</mo><mo stretchy="false">{</mo><mn>0</mn><mo separator="true">,</mo><mn>1</mn><mo stretchy="false">}</mo></mrow><annotation encoding="application/x-tex">Y:U\rightarrow\{0,1\}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span><span class="mord cjk_fallback">:</span><span class="mord mathnormal" style="margin-right:0.10903em;">U</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">→</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">{</span><span class="mord">0</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord">1</span><span class="mclose">}</span></span></span></span>表示第二次抛硬币的结果。X和Y是相互独立的且概率一样,所以这两个随机变量是独立同分布的</p><h2 id="流密码"><a class="markdownIt-Anchor" href="#流密码"></a> 流密码</h2><p>流密码是一种对称密码。<br />对称密码的严格定义:定义在<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="script">K</mi><mo separator="true">,</mo><mi mathvariant="script">M</mi><mo separator="true">,</mo><mi mathvariant="script">C</mi></mrow><annotation encoding="application/x-tex">\mathcal{K,M,C}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8777699999999999em;vertical-align:-0.19444em;"></span><span class="mord"><span class="mord mathcal" style="margin-right:0.01445em;">K</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord mathcal">M</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord mathcal" style="margin-right:0.05834em;">C</span></span></span></span></span>上的一对“有效”加解密算法<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false">(</mo><mi>E</mi><mo separator="true">,</mo><mi>D</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">(E,D)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord mathnormal" style="margin-right:0.02778em;">D</span><span class="mclose">)</span></span></span></span><br /><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="script">K</mi></mrow><annotation encoding="application/x-tex">\mathcal{K}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord"><span class="mord mathcal" style="margin-right:0.01445em;">K</span></span></span></span></span>指密钥空间,<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="script">M</mi></mrow><annotation encoding="application/x-tex">\mathcal{M}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord"><span class="mord mathcal">M</span></span></span></span></span>指明文空间,<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="script">E</mi></mrow><annotation encoding="application/x-tex">\mathcal{E}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord"><span class="mord mathcal" style="margin-right:0.08944em;">E</span></span></span></span></span>指密文空间<br /><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>E</mi><mo>:</mo><mrow><mi mathvariant="script">K</mi><mo>×</mo><mi mathvariant="script">M</mi><mo>→</mo><mi mathvariant="script">C</mi></mrow></mrow><annotation encoding="application/x-tex">E: \mathcal{K\times M\rightarrow C}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">:</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.76666em;vertical-align:-0.08333em;"></span><span class="mord"><span class="mord mathcal" style="margin-right:0.01445em;">K</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mbin">×</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mord mathcal">M</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">→</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mord mathcal" style="margin-right:0.05834em;">C</span></span></span></span></span><br /><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>D</mi><mo>:</mo><mrow><mi mathvariant="script">K</mi><mo>×</mo><mi mathvariant="script">C</mi><mo>→</mo><mi mathvariant="script">M</mi></mrow></mrow><annotation encoding="application/x-tex">D:\mathcal{K\times C\rightarrow M}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord mathnormal" style="margin-right:0.02778em;">D</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">:</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.76666em;vertical-align:-0.08333em;"></span><span class="mord"><span class="mord mathcal" style="margin-right:0.01445em;">K</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mbin">×</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mord mathcal" style="margin-right:0.05834em;">C</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">→</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mord mathcal">M</span></span></span></span></span><br />即 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">∀</mi><mi>m</mi><mo>∈</mo><mi mathvariant="script">M</mi><mo separator="true">,</mo><mi>k</mi><mo>∈</mo><mi mathvariant="script">K</mi><mo>:</mo><mi>D</mi><mo stretchy="false">(</mo><mi>k</mi><mo separator="true">,</mo><mi>E</mi><mo stretchy="false">(</mo><mi>k</mi><mo separator="true">,</mo><mi>m</mi><mo stretchy="false">)</mo><mo stretchy="false">)</mo><mo>=</mo><mi>m</mi></mrow><annotation encoding="application/x-tex">\forall m \in \mathcal{M}, k \in \mathcal{K}: D(k, E(k, m))=m</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.73354em;vertical-align:-0.0391em;"></span><span class="mord">∀</span><span class="mord mathnormal">m</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">∈</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.8888799999999999em;vertical-align:-0.19444em;"></span><span class="mord"><span class="mord mathcal">M</span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">∈</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.68333em;vertical-align:0em;"></span><span class="mord"><span class="mord mathcal" style="margin-right:0.01445em;">K</span></span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">:</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.02778em;">D</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord mathnormal">m</span><span class="mclose">)</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.43056em;vertical-align:0em;"></span><span class="mord mathnormal">m</span></span></span></span><br />"有效"这个词在理论密码学家看来,时间复杂度是多项式时间内的就是有效的,真正运行时间取决于输入规模。在应用密码学家看来,可能是加解密1GB数据要在10s内就算有效。<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220524-200714636.png" alt="图 23" /><br />特别注意,加密算法是随机化算法,解密算法是确定性的算法</p><h3 id="一次性密码本"><a class="markdownIt-Anchor" href="#一次性密码本"></a> 一次性密码本</h3><p>一次性密码本的明文空间、密文空间、密钥空间均相同<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220527-141642953.png" alt="图 24" /><br />一次性密码本的加密算法是将明文与密钥做异或运算,解密也是将密文与密钥做异或运算,根据异或运算的结合律可以知道加解密算法是正确的<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220527-142200584.png" alt="图 25" /></p><p>如何证明这个密码的安全性?</p><p>信息论的创始人香农提出一个观点:不能从密文得出关于明文的任何信息<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220527-143439609.png" alt="图 26" /><br />攻击者截获一段密文,如果满足下面的等式,即不同明文经过同一个密钥加密后等于这个密文的概率是相同的,那么攻击者就无法得知真正的明文,这种密码是完美保密性的<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220527-144022478.png" alt="图 28" /></p><p>一次性密码本是完美保密性的<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220527-145717066.png" alt="图 29" /><br /><strong>完美保密性只是意味着没有唯密文攻击</strong>,并不意味着在实际使用中是安全的<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220527-150144232.png" alt="图 30" /></p><p>香农在给出完美保密性的证明后又给出了一个定理,想要完美保密性,密钥长度要大于等于明文长度,所以完美保密性的密码是不实用的<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220527-150552535.png" alt="图 31" /></p><h3 id="流密码stream-ciphers"><a class="markdownIt-Anchor" href="#流密码stream-ciphers"></a> 流密码(Stream ciphers)</h3><p>流密码是使用的一次性密码本,它的思想是使用伪随机密钥代替随机密钥</p><p>伪随机数生成器是一个函数,它可以将s位的01比特串扩展成n位的01比特串,n>>s。注意,伪随机数生成器算法是不具备随机性的,具备随机性的是种子<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220528-142732857.png" alt="图 32" /></p><p>流密码使用伪随机数生成器(PRG),将密钥当做种子,生成真正用于加密的比特串<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220528-143032362.png" alt="图 33" /><br />因为密钥长度远小于明文长度,所以流密码并不是完美保密性的<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220528-143402189.png" alt="图 34" /><br />流密码的安全性不同于完美保密性,它需要另外一种安全性定义,这种安全性依赖具体的伪随机数生成器<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220528-143635119.png" alt="图 36" /><br />安全的伪随机数生成器必须是不可预测的。<br />假设伪随机数生成器是可预测的,那么存在某种算法可以根据PRG的前i位推测出后面的n-i位。在SMTP协议中,明文头是“from:” , 攻击者可以用这段已知的明文与截获的密文做异或得出PRG输出的前i位,再根据预测算法得出完整的密钥,再与密文异或得出明文<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220528-144530101.png" alt="图 37" /></p><p>严格定义PRG是可预测的:<br />存在“有效”算法A,对PRG的前i位做计算,输出的值与PRG的第i+1位相同的概率大于<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>1</mn><mi mathvariant="normal">/</mi><mn>2</mn><mo>+</mo><mi>ϵ</mi><mo separator="true">,</mo><mo stretchy="false">(</mo><mi>ϵ</mi><mo>≥</mo><mn>1</mn><mi mathvariant="normal">/</mi><msup><mn>2</mn><mn>30</mn></msup><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">1/2+\epsilon, (\epsilon \ge 1/2^{30})</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord">1</span><span class="mord">/</span><span class="mord">2</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal">ϵ</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mopen">(</span><span class="mord mathnormal">ϵ</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">≥</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1.064108em;vertical-align:-0.25em;"></span><span class="mord">1</span><span class="mord">/</span><span class="mord"><span class="mord">2</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141079999999999em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">3</span><span class="mord mtight">0</span></span></span></span></span></span></span></span></span><span class="mclose">)</span></span></span></span>, <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>ϵ</mi></mrow><annotation encoding="application/x-tex">\epsilon</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.43056em;vertical-align:0em;"></span><span class="mord mathnormal">ϵ</span></span></span></span>大于<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>1</mn><mi mathvariant="normal">/</mi><msup><mn>2</mn><mn>30</mn></msup></mrow><annotation encoding="application/x-tex">1/2^{30}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.064108em;vertical-align:-0.25em;"></span><span class="mord">1</span><span class="mord">/</span><span class="mord"><span class="mord">2</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141079999999999em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">3</span><span class="mord mtight">0</span></span></span></span></span></span></span></span></span></span></span></span>时是不可忽略的<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220528-150128101.png" alt="图 38" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220528-150346003.png" alt="图 39" /></p><p>不安全的PRG例子</p><ul><li>线性同余法的PRG<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220528-150804955.png" alt="图 40" /></li></ul><p>可忽略和不可忽略<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220528-151839276.png" alt="图 42" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220528-152238425.png" alt="图 43" /></p><h3 id="针对一次性密码本和流密码的攻击"><a class="markdownIt-Anchor" href="#针对一次性密码本和流密码的攻击"></a> 针对一次性密码本和流密码的攻击</h3><h4 id="两次密码本攻击"><a class="markdownIt-Anchor" href="#两次密码本攻击"></a> 两次密码本攻击</h4><p>流密码的密钥一旦使用两次就不安全<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220528-153358103.png" alt="图 44" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220528-154154282.png" alt="图 45" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220528-154548720.png" alt="图 46" /></p><p>WEP还有一个问题就是它的PRG使用的是RC4,它的不可预测性是较弱的,存在有效算法可以从四万帧中恢复PRG的输出<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220530-153712372.png" alt="图 1" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220530-154742338.png" alt="图 2" /></p><h4 id="完整性攻击"><a class="markdownIt-Anchor" href="#完整性攻击"></a> 完整性攻击</h4><p>一次性密码本和流密码都只保护数据的机密性,但不保证完整性,攻击者可以修改将密文与攻击者的置换密钥做异或从而定向影响明文,这种攻击无法被检测出来<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220530-155405962.png" alt="图 3" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220530-155642831.png" alt="图 4" /></p><h3 id="流密码实际应用"><a class="markdownIt-Anchor" href="#流密码实际应用"></a> 流密码实际应用</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220530-160327596.png" alt="图 5" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220530-161324759.png" alt="图 6" /></p><h4 id="现代流密码estream"><a class="markdownIt-Anchor" href="#现代流密码estream"></a> 现代流密码:eStream</h4><p>eStream支持5种PRG,这里讲其中一种,这种PRG的输入除了种子(密钥)还有一个随机数,好处是不用每次更换密钥,因为输入还包括随机数,整体是唯一的<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220530-163427804.png" alt="图 7" /></p><h5 id="estream中同时支持软硬件的流密码salsa20"><a class="markdownIt-Anchor" href="#estream中同时支持软硬件的流密码salsa20"></a> eStream中同时支持软硬件的流密码Salsa20</h5><p>图中的 || 并不是简单的拼接。这个PRG首先构造一个64kb的字符串,里面包含k,r,i(从0开始),通过多次一一映射h,最后与原字符串进行加法(不是异或)得出输出<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220530-165558524.png" alt="图 8" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220530-172759216.png" alt="图 9" /></p><h3 id="prg的安全定义"><a class="markdownIt-Anchor" href="#prg的安全定义"></a> PRG的安全定义</h3><p>PRG的输出与均匀随机变量的输出不可区分<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220531-142720553.png" alt="图 10" /></p><h4 id="统计测试"><a class="markdownIt-Anchor" href="#统计测试"></a> 统计测试</h4><p>为了定义不可区分,首先引入统计测试,统计测试是一个算法,输入值是“随机数”,输出0表示不是真随机数,1表示是真随机数<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220601-182904801.png" alt="图 11" /></p><p>上图的例子1,这个统计测试输出1的情况当且仅当随机数中0和1的个数差小于等于随机数的长度开根号乘以10</p><p>统计测试算法有好有坏,可能并不随机的字符串也认为是随机的。所以我们需要评估统计测试算法的好坏</p><p>我们定义一个变量叫做优势,优势是统计测试算法相对于伪随机数生成器的,优势越接近1,统计测试算法越能区分伪随机数和真随机数</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220601-184726785.png" alt="图 13" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220601-191544102.png" alt="图 14" /></p><h4 id="prg的密码学安全定义"><a class="markdownIt-Anchor" href="#prg的密码学安全定义"></a> PRG的密码学安全定义</h4><p>PRG是安全的当且仅当不存在有效的统计算法,它的优势是不可忽略的。即所有统计算法都认为PRG的输出是真随机数</p><p>但是,我们不能构造一个PRG并证明PRG是安全的,即不存在有效的统计算法。但是我们还是有大量的PRG候选方案</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220601-195519893.png" alt="图 15" /></p><p>我们可以证明当PRG是可预测时,PRG是不安全的<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220601-200341365.png" alt="图 16" /><br />当存在一个好的预测算法和好的统计测试算法,如下图。那么统计测试算法可以以一个不可忽略的 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>ϵ</mi></mrow><annotation encoding="application/x-tex">\epsilon</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.43056em;vertical-align:0em;"></span><span class="mord mathnormal">ϵ</span></span></span></span> 分辨出伪随机数和真随机数<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220601-203608396.png" alt="图 17" /><br />姚期智证明了上面命题的逆命题也成立,即当PRG是不可预测时,PRG是安全的<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220601-205232532.png" alt="图 18" /></p><p>不可区分的更通用的定义如下<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220601-211002051.png" alt="图 19" /></p><h3 id="语义安全"><a class="markdownIt-Anchor" href="#语义安全"></a> 语义安全</h3><p>什么是安全的密码?</p><ol><li>攻击者不能从密文中恢复密钥</li><li>攻击者不能从密文中恢复明文</li></ol><p>香农认为不能从密文中获得任何关于明文的信息才是安全的密码</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220601-213800816.png" alt="图 20" /></p><p>香农的完美保密性定义约束太强,可以用计算不可区分代替<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220601-214330687.png" alt="图 21" /></p><h4 id="一次性密码本的语义安全"><a class="markdownIt-Anchor" href="#一次性密码本的语义安全"></a> 一次性密码本的语义安全</h4><p>定义语义安全的的方式是通过两个实验,攻击者发送两个明文信息,挑战者(应该是被挑战者)随机选取密钥,做两次实验,第一次实验加密第一个信息,第二次实验加密第二个信息,攻击者判断密文对应的明文是哪个<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220602-145225594.png" alt="图 22" /><br />上图定义了语义安全的优势,等于实验0中攻击者输出1的概率和实验1中输出1的概率之差的绝对值。简单理解一下,假如攻击者不能区分两次实验,那么实验0和实验1的输出1的概率是一样的,那么攻击者的优势为0,不能区分两次实验,意味着加密算法是语义安全的;如果攻击者能区分实验0和实验1,那么概率差不可忽略,攻击者有一定的优势区分两次实验<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220602-150218580.png" alt="图 23" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220602-151338847.png" alt="图 24" /></p><p>事实上,一次性密码本不仅是语义安全的,而且是完美保密性的<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220602-152312589.png" alt="图 25" /></p><h3 id="安全的prg可以构成语义安全的流密码"><a class="markdownIt-Anchor" href="#安全的prg可以构成语义安全的流密码"></a> 安全的PRG可以构成语义安全的流密码</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220602-154213091.png" alt="图 26" /></p><p>我们做两次实验证明流密码的语义安全,第一次使用伪随机数生成器,第二次使用真随机数<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220602-155424176.png" alt="图 27" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220602-160424306.png" alt="图 28" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220602-160925721.png" alt="图 29" /></p><h2 id="分组密码"><a class="markdownIt-Anchor" href="#分组密码"></a> 分组密码</h2><p>分组密码也属于对称密码,将明文分解成固定大小的分组,使用密钥加密成同样大小的密文<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220602-202158199.png" alt="图 30" /><br />分组密码的加密过程是将密钥扩展成多个,使用轮函数多次计算分组得出密文<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220602-202909873.png" alt="图 31" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220602-203009017.png" alt="图 32" /></p><h3 id="prps和prfs"><a class="markdownIt-Anchor" href="#prps和prfs"></a> PRPs和PRFs</h3><p>K表示密钥空间,X表示明文空间,Y表示密文空间<br />给定密钥后,伪随机置换的加密函数是一个一一映射,也就意味着存在解密函数。伪随机置换和分组密码十分相似,有时候会混用术语<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220602-210139994.png" alt="图 33" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220602-210814515.png" alt="图 34" /></p><h4 id="安全的prfs"><a class="markdownIt-Anchor" href="#安全的prfs"></a> 安全的PRFs</h4><p>Funs[X,Y]表示所有从明文空间到密文空间的真随机函数的集合,易知这个集合大小等于明文空间大小的密文空间大小次,而伪随机函数的集合大小由密钥决定,一个密钥决定了一个伪随机函数,一个安全的PRF与真随机函数不可区分<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220602-212125363.png" alt="图 35" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220602-213044674.png" alt="图 36" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220602-213621864.png" alt="图 37" /><br />上图的G问题在于x=0时,输出固定了,攻击者可以通过x=0时的输出是否为0来判断他在和真随机函数交互还是伪随机函数,因为x=0,输出为0的概率实在太低了,等于密文空间的倒数<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220603-130807264.png" alt="图 38" /><br />可以使用安全的PRF来构造安全的PRG</p><h3 id="des"><a class="markdownIt-Anchor" href="#des"></a> DES</h3><p>DES的轮(回合)函数使用的是Feistel网络,核心思想是每个分组2n bits,右边n bits原封不动变成下一层分组左边n bits,左边n bits经过伪随机函数转换再和右边n bits异或变成下一层右边n bits<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220603-132028415.png" alt="图 39" /><br />易知这个网络是可逆的,注意,不要求伪随机函数是可逆的<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220603-132536803.png" alt="图 40" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220603-132903902.png" alt="图 41" /></p><p>定理:如果伪随机函数使用的密钥是相互独立的,那么Feistel网络是一个安全的PRP<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220603-140815958.png" alt="图 42" /><br />回合函数f由F根据回合密钥推导出来,回合密钥由主密钥推导得来,IP和IP逆是伪随机置换,和DES安全性无关,仅仅是标准要求<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220603-141228790.png" alt="图 43" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220603-142323580.png" alt="图 44" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220603-142425811.png" alt="图 45" /></p><p><strong>线性函数</strong>:函数可以表示成矩阵乘以入参<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220603-143047299.png" alt="图 46" /><br />如果所有的置换盒子都是线性的,那么整个DES就是线性的,因为只有DES算法中只有置换盒子可能是非线性的,其它就是异或、位移等线性运算。如果是线性DES,那么存在一个矩阵B,DES可以写成B乘以一个包含明文和回合密钥的向量<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220603-143613711.png" alt="图 47" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220603-143903499.png" alt="图 48" /></p><h4 id="针对des的攻击"><a class="markdownIt-Anchor" href="#针对des的攻击"></a> 针对DES的攻击</h4><h5 id="密钥穷举攻击"><a class="markdownIt-Anchor" href="#密钥穷举攻击"></a> 密钥穷举攻击</h5><p>给定一些明文密文对,找到一个密钥使得明文密文配对,这个密文极大概率是唯一的<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220603-145406296.png" alt="图 49" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220603-150020772.png" alt="图 50" /></p><p>为了抵抗穷举攻击,衍生出了3DES<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220603-154159645.png" alt="图 51" /></p><p>2DES存在安全性问题<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220603-154834197.png" alt="图 52" /></p><h3 id="针对分组密码的攻击"><a class="markdownIt-Anchor" href="#针对分组密码的攻击"></a> 针对分组密码的攻击</h3><h4 id="旁道攻击"><a class="markdownIt-Anchor" href="#旁道攻击"></a> 旁道攻击</h4><p>通过测试加解密的时间或者功耗来推测密钥</p><h4 id="错误攻击"><a class="markdownIt-Anchor" href="#错误攻击"></a> 错误攻击</h4><p>通过外部手段影响加解密硬件,比如提高时钟频率、加热芯片,使得加密的最后一回合发生错误,根据错误信息可以推测出密钥<br />这两种攻击需要先窃取到硬件,比如上图的IC卡</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220604-133952378.png" alt="图 53" /></p><h4 id="线性和差分攻击"><a class="markdownIt-Anchor" href="#线性和差分攻击"></a> 线性和差分攻击</h4><p>同样是需要给定明文密文对,推测密钥,比穷举攻击效率更高</p><p>加密算法中使用了线性函数导致下面等式以一个不可忽略的概率成立<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220604-135347890.png" alt="图 54" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220604-140513218.png" alt="图 55" /><br />图中的MAJ表示majority<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220604-141531593.png" alt="图 56" /></p><h4 id="量子攻击"><a class="markdownIt-Anchor" href="#量子攻击"></a> 量子攻击</h4><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220604-143036480.png" alt="图 58" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220604-143500926.png" alt="图 59" /></p><h3 id="aes"><a class="markdownIt-Anchor" href="#aes"></a> AES</h3><p>AES基于代换置换网络构建,和Feistel最大的区别在于,在这个网络的每一回合函数会影响每一位bit<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220604-144154191.png" alt="图 60" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220604-145019414.png" alt="图 61" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220604-145359707.png" alt="图 62" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220604-145800932.png" alt="图 63" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220604-150057554.png" alt="图 64" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220604-150716892.png" alt="图 65" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220604-160945714.png" alt="图 66" /></p><h3 id="使用prgs构造分组密码"><a class="markdownIt-Anchor" href="#使用prgs构造分组密码"></a> 使用PRGs构造分组密码</h3><p>分组密码实质是PRP,首先考虑能不能使用PRG构造PRF<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220604-162522719.png" alt="图 67" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220604-163105752.png" alt="图 68" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220604-163949129.png" alt="图 69" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220604-164309712.png" alt="图 70" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220604-164722368.png" alt="图 71" /><br />使用安全的PRG可以构造一个安全的PRF,但是并不实用<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220604-165244703.png" alt="图 72" /><br />有了安全的PRF,我们可以使用Luby-Rackoff定理转换成PRP,因此可以使用PRG来构造分组密码,但是不如AES启发性PRF实用</p><h2 id="使用分组密码"><a class="markdownIt-Anchor" href="#使用分组密码"></a> 使用分组密码</h2><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220606-204128932.png" alt="图 1" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220606-204240418.png" alt="图 2" /><br />很显然,真随机置换空间和伪随机置换空间大小一样,所以它是安全的PRP<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220606-205725499.png" alt="图 3" /><br />这个PRP不是安全的PRF,因为明文空间太小了<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220606-210323795.png" alt="图 4" /></p><h3 id="使用一次性密钥的分组密码"><a class="markdownIt-Anchor" href="#使用一次性密钥的分组密码"></a> 使用一次性密钥的分组密码</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220606-211025425.png" alt="图 6" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220606-212614884.png" alt="图 7" /></p><p>ECB的问题在于相同的明文会加密成相同的密文,攻击者可能不知道明文内容,但也会从中学到明文的一些信息<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220606-212805998.png" alt="图 8" /><br />攻击者来挑战算法,本来不应该知道两张加密图片的区别,但是ECB将头发加密成了很多1,头发又重复出现,这样密文就会出现很多1,攻击者就能根据这个区别分辨两张图片</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220606-213559871.png" alt="图 9" /></p><p>ECB被用来加密长于一个分组的消息时不是语义安全的<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220607-192824462.png" alt="图 1" /></p><p>安全的电子密码本是为每个分组生成一个随机密钥进行加密,类似于AES的密钥扩展<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220607-194239837.png" alt="图 3" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220607-194520160.png" alt="图 4" /></p><h3 id="使用密钥多次利用的分组密码"><a class="markdownIt-Anchor" href="#使用密钥多次利用的分组密码"></a> 使用密钥多次利用的分组密码</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220607-195209668.png" alt="图 5" /></p><p>密钥多次利用的分组密码的选择明文攻击就是进行多次的语义安全实验(CPA安全)<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220607-200041027.png" alt="图 6" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220607-200817236.png" alt="图 7" /></p><p>确定的加密对于选择明文攻击不可能是语义安全的,所以多次使用一个密钥加密时,相同的明文,应该产生不同的输出,有两种方法。</p><p>第一种:随机化算法<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220607-201557973.png" alt="图 8" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220607-202156136.png" alt="图 9" /></p><p>第二种:基于新鲜值的加密</p><p>新鲜值不必随机但是不能重复<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220607-203239394.png" alt="图 10" /><br />随机化的新鲜值和上面的随机化算法是一样的</p><p>基于新鲜值的加密的选择明文攻击下的安全性<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220611-201556815.png" alt="图 1" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220611-201832751.png" alt="图 2" /></p><h3 id="密钥多次利用的运行方式cbc"><a class="markdownIt-Anchor" href="#密钥多次利用的运行方式cbc"></a> 密钥多次利用的运行方式(CBC)</h3><p>CBC:密码分组链接模式<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220611-205233935.png" alt="图 3" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220611-205613503.png" alt="图 4" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220611-210022725.png" alt="图 5" /><br />L是加密的明文长度,单位是分组。q是在CPA攻击下,攻击者获得的密文数,现实意义是使用某个密钥加密的明文数量<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220611-210655019.png" alt="图 6" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220611-212159977.png" alt="图 8" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220611-212541309.png" alt="图 9" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220611-212757060.png" alt="图 10" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220611-213005393.png" alt="图 11" /></p><h3 id="密钥多次利用的运行方式ctr"><a class="markdownIt-Anchor" href="#密钥多次利用的运行方式ctr"></a> 密钥多次利用的运行方式(CTR)</h3><p>CTR:计数器模式</p><p>CTR不使用分组密码,使用PRF足够<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220611-213630832.png" alt="图 12" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220611-214208043.png" alt="图 13" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220611-214354627.png" alt="图 14" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220611-214618681.png" alt="图 15" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220611-214928902.png" alt="图 16" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220611-215209157.png" alt="图 17" /></p><h2 id="信息完整性"><a class="markdownIt-Anchor" href="#信息完整性"></a> 信息完整性</h2><p>本节先考虑信息完整性,不考虑机密性。信息完整可以保证公开信息没有被篡改,例如广告投放商不在乎广告的机密性,但在乎广告有没有被篡改</p><h3 id="macs"><a class="markdownIt-Anchor" href="#macs"></a> MACs</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-155838486.png" alt="图 9" /></p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-160135893.png" alt="图 10" /></p><p>CRC是循环冗余校验算法,为检测信息中的随机发生的错误而设计,并非针对恶意错误</p><h4 id="安全的macs"><a class="markdownIt-Anchor" href="#安全的macs"></a> 安全的MACs</h4><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-160819619.png" alt="图 11" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-161047466.png" alt="图 12" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-161336121.png" alt="图 13" /></p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-161529938.png" alt="图 15" /></p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-162015174.png" alt="图 16" /></p><p>MAC可以帮助抵御数据篡改,但是无法抵御认证消息的交换</p><h4 id="构造安全的mac"><a class="markdownIt-Anchor" href="#构造安全的mac"></a> 构造安全的MAC</h4><p>可以用安全的PRF来构造安全的MAC<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-162541758.png" alt="图 17" /></p><p>但是,PRF的输出不能太短,不然攻击者能以一个不可忽略的概率猜出消息认证码</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-162935605.png" alt="图 18" /></p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-163058370.png" alt="图 19" /></p><p>不等式右边 1/|Y| 是因为攻击者可以猜</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-163354770.png" alt="图 20" /></p><p>PRF的输出不能太短,同时为了增大输入空间,需要有一些新的构造,将小输入空间的PRF转换成大输入空间的PRF<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-164153404.png" alt="图 21" /></p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-164521447.png" alt="图 22" /></p><p>如果PRF是安全的,那么截断PRF的输出,依然是安全的,当然,如果要用PRF构造MAC,不能截到太短</p><h3 id="cbc-mac和nmac"><a class="markdownIt-Anchor" href="#cbc-mac和nmac"></a> CBC-MAC和NMAC</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-172446102.png" alt="图 23" /></p><p>n是底层PRF的分组大小</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-172823600.png" alt="图 24" /></p><p>L是分组大小</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-173239812.png" alt="图 25" /></p><p>这两种构造的最后一步都至关重要,没有最后一步,攻击者可以实施存在性伪造(扩展攻击)。<br />第二种构造的原因很简单,攻击者询问m的函数结果,将结果与w分别作为函数的密钥和明文(F公开),计算函数结果</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-191611230.png" alt="图 26" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-191629225.png" alt="图 27" /></p><p>第一种构造的原因<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-192446556.png" alt="图 29" /></p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-192433084.png" alt="图 28" /></p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-192743722.png" alt="图 30" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-193128450.png" alt="图 31" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-193412039.png" alt="图 32" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-193838235.png" alt="图 33" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-194007648.png" alt="图 34" /></p><h3 id="mac-padding"><a class="markdownIt-Anchor" href="#mac-padding"></a> MAC padding</h3><p>当数据的长度不是分组长度的倍数时,需要填充数据</p><p>全部补0会有出现存在性伪造<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-194355454.png" alt="图 35" /></p><p>填充函数必须是一一映射的<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-194848433.png" alt="图 36" /></p><p>ISO的这种填充方法,不管是不是分组长度的倍数都要进行填充</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-195545079.png" alt="图 37" /></p><h3 id="并行的mac"><a class="markdownIt-Anchor" href="#并行的mac"></a> 并行的MAC</h3><p>CBC-MAC和NMAC将一个处理短信息的PRF转换成一个处理长信息的PRF,这两种算法是串行的</p><p>P是某个有限域上的乘法<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-200719106.png" alt="图 38" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-200909314.png" alt="图 39" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-201424596.png" alt="图 40" /></p><h3 id="一次性mac"><a class="markdownIt-Anchor" href="#一次性mac"></a> 一次性MAC</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-201740008.png" alt="图 41" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-202107447.png" alt="图 42" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-202352108.png" alt="图 43" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220618-202527559.png" alt="图 44" /></p><h3 id="hmac略"><a class="markdownIt-Anchor" href="#hmac略"></a> HMAC(略)</h3><p>抗碰撞章节细说</p><h2 id="抗碰撞"><a class="markdownIt-Anchor" href="#抗碰撞"></a> 抗碰撞</h2><p>抗碰撞在信息完整性中扮演着重要角色。我们说MAC系统是安全的,如果它在选择信息攻击下,是不可被存在性伪造的。前面4种MAC构造是通过PRF或随机数来构造的,现在通过抗碰撞的哈希函数来构造MAC<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-143231888.png" alt="图 45" /></p><p>抗碰撞:没有有效算法A,能以一个不可忽略的概率找到hash函数的碰撞值</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-143618752.png" alt="图 46" /></p><p>可以用抗碰撞哈希函数和处理短信息的安全MAC组合成处理长信息的安全MAC<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-144918454.png" alt="图 47" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-145405558.png" alt="图 48" /></p><p>只用抗碰撞哈希函数也可以构造安全的MAC,而且不像之前的MAC需要密钥,但是需要一个只读空间用来存信息的hash值。这种方式非常流行<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-150316230.png" alt="图 49" /></p><h3 id="针对抗碰撞哈希函数的通用攻击生日攻击"><a class="markdownIt-Anchor" href="#针对抗碰撞哈希函数的通用攻击生日攻击"></a> 针对抗碰撞哈希函数的通用攻击(生日攻击)</h3><p>针对分组密码的通用攻击是穷举攻击,抵御穷举攻击的方法是增大密钥空间;为了抵御生日攻击,哈希函数的输出也必须大于某个下界</p><p>下面这种攻击算法通常几轮就能找到碰撞值<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-160641656.png" alt="图 50" /></p><h4 id="生日悖论的证明"><a class="markdownIt-Anchor" href="#生日悖论的证明"></a> 生日悖论的证明</h4><p>下面的证明在非均匀分布时,n的下界更低<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-170555579.png" alt="图 51" /></p><h4 id="通用攻击"><a class="markdownIt-Anchor" href="#通用攻击"></a> 通用攻击</h4><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-171409561.png" alt="图 52" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-171802425.png" alt="图 53" /></p><h3 id="使用merkle-damgard机制组建抗碰撞的哈希函数"><a class="markdownIt-Anchor" href="#使用merkle-damgard机制组建抗碰撞的哈希函数"></a> 使用Merkle-Damgard机制组建抗碰撞的哈希函数</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-172037266.png" alt="图 54" /></p><p>下面的IV是永远固定的,写在代码和标准里的值,填充函数在信息长度是分组长度倍数的时候也会填充一个哑分组<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-172758521.png" alt="图 55" /></p><p>这种机制流行的原因是只要小hash函数(即上面的压缩函数)是抗碰撞的,那么大hash函数也是抗碰撞的</p><h3 id="构建抗碰撞的压缩函数"><a class="markdownIt-Anchor" href="#构建抗碰撞的压缩函数"></a> 构建抗碰撞的压缩函数</h3><p>使用分组密码来构建压缩函数,将信息作为密钥。SHA函数都使用了Davies-Mayer压缩函数<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-191704497.png" alt="图 56" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-192558677.png" alt="图 57" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-192809330.png" alt="图 58" /></p><p>另外一类压缩函数是由数论里的困难问题构建的,这类压缩函数的抗碰撞性规约于数论难题,也就是说破解了压缩函数的抗碰撞性也就破解了数论难题。但是这类压缩函数很少使用,因为分组密码更快<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-193641416.png" alt="图 60" /></p><h3 id="sha256"><a class="markdownIt-Anchor" href="#sha256"></a> SHA256</h3><p>使用了Merkle-Damgard机制和Davies-Mayer压缩函数,底层分组密码用的是SHACAL-2<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-193056269.png" alt="图 59" /></p><h3 id="hmac"><a class="markdownIt-Anchor" href="#hmac"></a> HMAC</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-194153629.png" alt="图 61" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-194226537.png" alt="图 62" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-194533785.png" alt="图 63" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-194839726.png" alt="图 64" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-195112016.png" alt="图 65" /></p><h3 id="针对mac验证时的计时攻击"><a class="markdownIt-Anchor" href="#针对mac验证时的计时攻击"></a> 针对MAC验证时的计时攻击</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-195520857.png" alt="图 66" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-195942956.png" alt="图 67" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-200204640.png" alt="图 68" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220619-200501930.png" alt="图 69" /></p><h2 id="认证加密"><a class="markdownIt-Anchor" href="#认证加密"></a> 认证加密</h2><p>到目前为止,我们了解了机密性和信息完整性, 我们将构建同时满足这两种性质的密码<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220622-172043490.png" alt="图 70" /></p><p>机密性: 在选择明文攻击下满足语义安全<br />机密性只能抵抗攻击者窃听而不能抵抗篡改数据包</p><p>客户端加密数据后通过网络发送给服务器, 服务器解密后分发至对应端口<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220622-174028009.png" alt="图 71" /></p><p>如果没有保证完整性, 那么攻击者可以拦截客户端的数据包, 把端口改成自己能控制的服务器的端口<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220622-174516306.png" alt="图 72" /></p><p>可以抵抗CPA攻击的CBC分组密码不能抵抗篡改数据包, 攻击者可以简单地修改IV从而修改加密后的端口<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220622-174957308.png" alt="图 73" /></p><p>攻击者甚至不用进入服务器, 直接利用网络来攻击。在CTR模式下,攻击者截获数据包,将加密的校验和与t异或,将加密的数据与s异或。CTR模式的特点是对密文异或,解密后等于对明文异或。攻击者重复很多次攻击,直到得到足够数量的合法的t、s对,从而恢复数据D(插值法?)<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220623-192627520.png" alt="图 1" /></p><p>这种攻击叫做选择密文攻击。攻击者提交他选择的密文,是由他想解密的密文所推出的,然后看服务器响应,攻击者可以从中学到明文的一些信息。重复这个操作,用许多不同t、s值,攻击者可以还原明文</p><p>选择明文攻击下的安全不能保证主动攻击(前面两种攻击)下的安全<br />如果要保证信息完整性但不需要机密性,使用MAC<br />如果同时保证信息完整性和机密性,使用认证加密模式<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220623-193312944.png" alt="图 2" /></p><h3 id="认证加密定义"><a class="markdownIt-Anchor" href="#认证加密定义"></a> 认证加密定义</h3><p>目标:提供选择明文攻击下的语义安全和密文完整性<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220623-194146377.png" alt="图 3" /></p><p>密文完整性:攻击者不能造出合法的密文<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220623-194455032.png" alt="图 4" /></p><p>CBC是选择明文攻击(CPA)下的安全密码,它的解密算法从不输出bottom符号,所以它不能直接被用作认证加密<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220623-195036426.png" alt="图 5" /></p><p>认证加密不能抵抗重放攻击<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220623-195445357.png" alt="图 6" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220623-195557207.png" alt="图 7" /></p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220623-200150144.png" alt="图 8" /></p><h3 id="选择密文攻击下的安全"><a class="markdownIt-Anchor" href="#选择密文攻击下的安全"></a> 选择密文攻击下的安全</h3><p>攻击者的能力是既能选择明文攻击也能选择密文攻击,也就是说既能拿到想要的明文的加密结果,也能拿到想要的密文的解密结果<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220623-200734697.png" alt="图 9" /></p><p>攻击者选择的密文不能是CPA的返回<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220623-201248346.png" alt="图 10" /></p><p>CBC密码不是选择密文安全的,因为在选择密文时,通过对IV异或修改CPA的返回,同时由于CBC的特性,CCA的返回时对明文的异或,这样攻击者可以以1的优势赢下语义安全实验<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220623-202130682.png" alt="图 11" /></p><p>如果密码能够提供认证加密,那么它就是选择密文安全的<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220623-203032869.png" alt="图 12" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220623-205911033.png" alt="图 13" /></p><p>所以认证加密能够选择密文攻击,但是不能防止重放攻击和旁道攻击<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220623-210305986.png" alt="图 1" /></p><p>认证加密直到2000年才被正式提出,在这之前,已经有CPA安全的密码和安全的MAC,当时的工程师想将两者组合,但不是所有的组合可以作为认证加密<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-113519498.png" alt="图 1" /></p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-114038881.png" alt="图 2" /></p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-115508806.png" alt="图 3" /></p><p>SSH的MAC签名算法的输出会泄漏明文中的一些位;<br />SSL的加密和MAC算法之间会有一些不好的互动导致选择密文攻击。IPsec无论怎么组合CPA安全的密码和安全的MAC都可以作为认证加密。SSL的特点是MAC-than-ENC,IPsec的特点是ENC-than-MAC<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-151326514.png" alt="图 4" /></p><p>这几种认证加密都支持AEAD(认证加密与关联数据,例如ip报文的报文头是关联数据不加密,报文体用加密,整个报文需要认证)。MAC对整个报文使用,加密只对报文体使用</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-153855884.png" alt="图 5" /></p><p>aad是需要认证、但不需要加密的相关数据,data是需要认证和加密的数据<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-154419682.png" alt="图 6" /></p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-155007653.png" alt="图 7" /></p><h3 id="直接从prp构造认证加密"><a class="markdownIt-Anchor" href="#直接从prp构造认证加密"></a> 直接从PRP构造认证加密</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-155759786.png" alt="图 8" /></p><p>OCB比前面几种认证加密快的多,但没有被广泛使用因为各种各样的专利<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-161428575.png" alt="图 10" /></p><h3 id="认证加密的例子"><a class="markdownIt-Anchor" href="#认证加密的例子"></a> 认证加密的例子</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-163019765.png" alt="图 11" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-164131927.png" alt="图 12" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-170829122.png" alt="图 13" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-171159393.png" alt="图 15" /></p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-172014992.png" alt="图 16" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-172813589.png" alt="图 17" /></p><h3 id="针对认证加密的攻击"><a class="markdownIt-Anchor" href="#针对认证加密的攻击"></a> 针对认证加密的攻击</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-173555313.png" alt="图 18" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-174308207.png" alt="图 19" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-174853114.png" alt="图 20" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-175037140.png" alt="图 21" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-175249572.png" alt="图 22" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-191230262.png" alt="图 23" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-191502109.png" alt="图 24" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-192743896.png" alt="图 25" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-192928293.png" alt="图 26" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-193014499.png" alt="图 27" /></p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-193600429.png" alt="图 28" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-194210971.png" alt="图 29" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220625-194414840.png" alt="图 30" /></p><h2 id="零碎"><a class="markdownIt-Anchor" href="#零碎"></a> 零碎</h2><p>本章讲对称密码的一些零碎</p><h3 id="密钥推导"><a class="markdownIt-Anchor" href="#密钥推导"></a> 密钥推导</h3><p>KDF:key drivation function<br />源密钥由硬件随机数生成器生成或者密钥交换协议生成<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220626-150725572.png" alt="图 31" /></p><p>CTX:参数上下文,每个进程的ctx不同<br />当源密钥服从均匀分布,我们使用PRF来作为密钥推导函数(其实就是用PRF作为伪随机数生成器)<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220626-152027306.png" alt="图 32" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220626-152139887.png" alt="图 33" /></p><p>当源密钥不服从均匀分布,那么伪随机函数的输出看起来就不随机了,源密钥不服从均匀分布的原因可能是密钥交换协议的密钥空间的子集是均匀分布,或者伪随机数生成器有偏差<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220626-152720454.png" alt="图 34" /></p><h3 id="构建kdf的机制"><a class="markdownIt-Anchor" href="#构建kdf的机制"></a> 构建KDF的机制</h3><h4 id="先提取再扩展"><a class="markdownIt-Anchor" href="#先提取再扩展"></a> 先提取再扩展</h4><p>因为源密钥可能不是均匀的,我们使用一个提取器和一个随机选择的固定的但可以不保密的盐值将源密钥转换为服从均匀分布的密钥k,然后再使用PRF扩展密钥<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220626-154142752.png" alt="图 35" /></p><p>HMAC既用于PRF进行扩展,又用于提取器<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220626-181622755.png" alt="图 36" /></p><h4 id="基于密码的kdf"><a class="markdownIt-Anchor" href="#基于密码的kdf"></a> 基于密码的KDF</h4><p>PBKDF通过多次迭代哈希函数推导密钥<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220626-182513565.png" alt="图 37" /></p><h3 id="确定性加密"><a class="markdownIt-Anchor" href="#确定性加密"></a> 确定性加密</h3><p>确定性加密总是把给定明文映射到同一个密文。</p><p>为什么需要确定性加密?假设有个加密数据库和服务器,服务器用k1加密索引,用k2加密数据,如果加密是确定的,服务器请求数据时可以直接使用加密后的索引作为查询条件请求数据<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220626-184047714.png" alt="图 38" /></p><p>确定性加密有致命缺点就是不能抵御选择明文攻击,攻击者看到相同的密文就知道他们的明文是相同的<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220626-185452336.png" alt="图 39" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220626-185741013.png" alt="图 40" /></p><p>解决方法是不要用同一个密钥加密同一个消息两次,要么密钥从一个很大空间随机选择,要么明文就是唯一的,比如说用户id<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220626-190638496.png" alt="图 41" /></p><h4 id="确定性加密的cpa安全"><a class="markdownIt-Anchor" href="#确定性加密的cpa安全"></a> 确定性加密的CPA安全</h4><p>在标准的选择明文攻击实验基础上,Chal不会给相同的m0加密,不会给相同的m1加密。注意上面的图40不是标准的确定性加密的CPA实验,因为攻击者两次查询的m0都是同一个<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220626-192122286.png" alt="图 42" /></p><p>一个常见的错误是,固定IV的CBC不是确定性CPA安全的,<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mn>0</mn><mi>n</mi></msup><msup><mn>1</mn><mi>n</mi></msup></mrow><annotation encoding="application/x-tex">0^n1^n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.664392em;vertical-align:0em;"></span><span class="mord"><span class="mord">0</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.664392em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">n</span></span></span></span></span></span></span></span><span class="mord"><span class="mord">1</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.664392em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">n</span></span></span></span></span></span></span></span></span></span></span>表示消息有两个分组,第一个分组全0,第二个分组全1<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220627-192553855.png" alt="图 1" /></p><p>固定IV的CTR也是不安全的<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220627-193317316.png" alt="图 2" /></p><h4 id="可以抵御确定性cpa的确定性加密"><a class="markdownIt-Anchor" href="#可以抵御确定性cpa的确定性加密"></a> 可以抵御确定性CPA的确定性加密</h4><p>确定性加密是需要的,但是不能抵御选择明文攻击,因为攻击者看到两个相同的密文就知道了对应的明文是一样的。我们对确定性加密降低选择明文攻击的能力,加密者不使用一个密钥多次加密同样的明文,这样叫确定性CPA。<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220627-195212096.png" alt="图 3" /></p><h5 id="构造1合成的ivsiv"><a class="markdownIt-Anchor" href="#构造1合成的ivsiv"></a> 构造1:合成的IV(SIV)</h5><p>CPA安全的密码会有一个随机值,我们用PRF生成这个随机值<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220627-200744635.png" alt="图 4" /></p><p>SIV天然提供密文完整性,不需要MAC就能作为DAE(确定性认证加密),例如SIV-CTR<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220627-201343821.png" alt="图 5" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220627-201748042.png" alt="图 6" /></p><p>当需要确定性加密,特别是明文很长时,适合用SIV,如果明文很短,比如说少于16个字节,可以用构造2</p><h5 id="构造2仅仅使用一个prp"><a class="markdownIt-Anchor" href="#构造2仅仅使用一个prp"></a> 构造2:仅仅使用一个PRP</h5><p>实验0,攻击者看到q个随机值,实验1中,攻击者也看到q个随机值,两次实验的结果的概率分布是一样的,攻击者无法区分。这种构造不能保证密文完整性。同时只能加密16个字节<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220627-204410329.png" alt="图 7" /></p><p>我们先考虑如何将PRP扩展成一个大的PRP<br />EME有两个密钥K,L,L是由K推出的。先为每个分组用L推导出一个密码本作异或,然后用PRP加密得到PPP,将所有PPP异或得到MP,再用PRP加密MP得到MC。然后计算MP异或MC,得到另外一个密钥M用于推导更多密码本,分别对PPP异或得到CCC,然后把所有这些CCC异或得到CCCO,再用PRP加密再异或密码本<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220628-195220052.png" alt="图 1" /></p><p>现在考虑增加完整性<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220628-195844114.png" alt="图 2" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220628-201621008.png" alt="图 3" /></p><h4 id="微调加密tweakable-encryption"><a class="markdownIt-Anchor" href="#微调加密tweakable-encryption"></a> 微调加密(Tweakable encryption)</h4><p>先以硬盘加密问题引入微调加密,<br />硬盘扇区大小是固定的,明文和密文空间必须一致,我们最多可以使用确定性加密,因为随机性加密需要额外空间来放随机数,完整性需要额外空间放认证码<br />定理:如果确定性CPA安全的密码的明文空间和密文空间一样,那么这个密码一定是个PRP<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220628-202439968.png" alt="图 4" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220628-202824954.png" alt="图 5" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220628-203150808.png" alt="图 6" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220628-205028627.png" alt="图 7" /></p><p>这个微调分组密码的安全实验与常规的分组密码安全实验区别在于,在常规分组密码中,攻击者只能与一个置换进行互动,目标是分辨自己在和伪随机置换交互还是在和一个真随机置换交互。而在微调分组密码的安全实验中,攻击者与|T|个随机置换交互,目标是区分这|T|个随机置换是真是伪<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220628-210701941.png" alt="图 8" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220629-190320329.png" alt="图 1" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220629-190658847.png" alt="图 2" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220629-190825000.png" alt="图 3" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220629-190959108.png" alt="图 4" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220629-191211210.png" alt="图 5" /></p><h4 id="保格式加密format-preserving-encryption"><a class="markdownIt-Anchor" href="#保格式加密format-preserving-encryption"></a> 保格式加密(Format Preserving encryption)</h4><p>pos机刷卡时,我们希望卡号只在终端和银行可见,但是中间又有些服务商也想得到"卡号",我们可以用将卡号加密成卡号格式的密文。<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220629-192146688.png" alt="图 6" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220629-192737921.png" alt="图 7" /></p><p>我们截断使用PRF,明文后面补0(例如AES就补到128位),密文截断,然后带入Luby-Rackoff构造PRP<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220629-193441868.png" alt="图 9" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220629-193842147.png" alt="图 10" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220629-194041068.png" alt="图 11" /></p><h2 id="密钥交换"><a class="markdownIt-Anchor" href="#密钥交换"></a> 密钥交换</h2><p>现在我们知道两个用户可以通过共享一个密钥来保护通信数据,问题是,这两个用户如何产生共享密钥,这个问题将把我们带入公钥密码的世界。我们先看一些玩具性质的密钥交换协议。<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220629-194923799.png" alt="图 12" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220629-195035646.png" alt="图 13" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220629-195959725.png" alt="图 14" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220630-193139128.png" alt="图 17" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220630-193511226.png" alt="图 18" /></p><p>能否设计出可以抵御窃听和主动攻击的没有可信第三方的密钥交换协议?可以的,这就是公钥密钥的出发点<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220630-193921365.png" alt="图 19" /></p><h3 id="不需要ttp的密钥交换"><a class="markdownIt-Anchor" href="#不需要ttp的密钥交换"></a> 不需要TTP的密钥交换</h3><p>首先考虑攻击者只能窃听不能篡改消息,能不能只使用对称密码体系的算法来实现不需要TTP的密钥交换?<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220630-201847676.png" alt="图 20" /></p><p>可以的,首先给出puzzle定义:需要花一些功夫解决的问题。例如已经给出AES密钥的前96位,明文固定,那么枚举<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mn>2</mn><mn>32</mn></msup></mrow><annotation encoding="application/x-tex">2^{32}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8141079999999999em;vertical-align:0em;"></span><span class="mord"><span class="mord">2</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141079999999999em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">3</span><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span>个可能的后32位密钥可以找到能正确解密<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220630-202532587.png" alt="图 21" /></p><p>Alice准备<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mn>2</mn><mn>32</mn></msup></mrow><annotation encoding="application/x-tex">2^{32}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8141079999999999em;vertical-align:0em;"></span><span class="mord"><span class="mord">2</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141079999999999em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">3</span><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span>个puzzle,全部发给Bob,Bob选择一个然后开始枚举,只要解密出的原文开头包含"Puzzle", 对应的k作为共享密钥,x发送给Alice,Alice就知道Bob选择了哪个<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220630-204237565.png" alt="图 22" /></p><p>这个协议不实用但是有一个很好的想法,参与者花费线性的时间,而攻击者必须花费平方的时间,当攻击者想破解这个协议,有一个“平方鸿沟”横亘在参与者与攻击者的工作之间。<br />只用对称密码体系,我们不能建立一个更大的“鸿沟”<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220630-204751472.png" alt="图 23" /></p><p>我们需要具备非常特殊性质的函数,为了构建这些函数,我们必须依赖某些代数</p><h3 id="diffie-hellman协议"><a class="markdownIt-Anchor" href="#diffie-hellman协议"></a> Diffie-Hellman协议</h3><p>这是第一个实用的密钥交换机制。<br />同样,我们考虑攻击者只能窃听不能篡改。我们尝试建立参与者与攻击者之间的指数级鸿沟</p><p>Diffie-Hellman协议开创了密码学的新纪元,现在不仅仅是关于开发分组密码,而且是关于设计基于代数的协议<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220630-210521216.png" alt="图 24" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220630-211053388.png" alt="图 25" /></p><p>下面有张表是不同密钥长度的分组密码安全性等价于对应模数的DH函数安全性,如果用椭圆曲线,模数可以更小<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220630-211835142.png" alt="图 26" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220701-193533042.png" alt="图 1" /></p><p>上面的协议当存在主动攻击(中间人攻击)时就不安全<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220701-194012434.png" alt="图 2" /></p><p>事实上,上面的协议也可以改成无交互的<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220701-194305591.png" alt="图 3" /></p><p>一个开放的问题,两个参与方的密钥交换使用DH即可,三个参与方的密钥交换使用Joux提出的方法,四个及以上还没有有效方法<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220701-194701716.png" alt="图 4" /></p><h3 id="公钥加密"><a class="markdownIt-Anchor" href="#公钥加密"></a> 公钥加密</h3><p>这是另外一种密钥交换的方法<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220701-194938023.png" alt="图 5" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220701-195130689.png" alt="图 6" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220701-195343776.png" alt="图 7" /></p><p>在公钥加密中,没有必要赋予攻击者实施选择明文攻击的能力。因为在对称密钥系统中,攻击者必须请求他选择的明文的加密,而在公钥系统中,攻击者拥有公钥,所以他可以自己加密任何他想加密的明文,他不需要Chal的帮助来计算他选择的明文的加密。因此在公钥的设定中,选择明文攻击是与生俱来的,没有理由给攻击者多余的能力去实施选择明文攻击(公钥加密也是随机性的,每次密文不同)<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220701-200431505.png" alt="图 8" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220701-201448159.png" alt="图 9" /></p><p>可以抵抗窃听但也不能抵抗中间人攻击<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220701-201815764.png" alt="图 10" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220701-202110833.png" alt="图 11" /></p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220701-202209686.png" alt="图 12" /></p><h2 id="数论简介"><a class="markdownIt-Anchor" href="#数论简介"></a> 数论简介</h2><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220701-202835680.png" alt="图 13" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220701-203101241.png" alt="图 14" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220701-203326182.png" alt="图 15" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220701-203851862.png" alt="图 16" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220701-204410071.png" alt="图 17" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220701-205118470.png" alt="图 19" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220702-150813000.png" alt="图 20" /></p><p>扩展欧几里得算法是已知最有效的求元素模逆的方法(也给了我们求模线性方程的方法)<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220702-151004779.png" alt="图 21" /></p><p>注意这里是n,不是N,n=logN<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220702-151731143.png" alt="图 22" /></p><h3 id="费马小定理和欧拉定理"><a class="markdownIt-Anchor" href="#费马小定理和欧拉定理"></a> 费马小定理和欧拉定理</h3><p>费马小定理给了我们另一个计算模质数逆的方法,但是与扩展欧几里得算法有两个不足,首先它只能用在质数模上,其次算法效率更低<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220702-152638578.png" alt="图 24" /></p><p>我们可以用费马小定理以极大概率生成一个随机质数(期望是几百次迭代),这是一个简单但不是最好的方法<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220702-153504928.png" alt="图 25" /></p><p>欧拉证明了<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msubsup><mi>Z</mi><mi>p</mi><mo>∗</mo></msubsup></mrow><annotation encoding="application/x-tex">Z_p^*</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.071804em;vertical-align:-0.383108em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.07153em;">Z</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.688696em;"><span style="top:-2.4530000000000003em;margin-left:-0.07153em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">p</span></span></span><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mbin mtight">∗</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.383108em;"><span></span></span></span></span></span></span></span></span></span>是一个循环群(p是素数),也就是<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">∃</mi><mi>g</mi><mo>∈</mo><mo stretchy="false">(</mo><msub><mi>Z</mi><mi>p</mi></msub><msup><mo stretchy="false">)</mo><mo>∗</mo></msup></mrow><annotation encoding="application/x-tex">\exist g\in (Z_p)^*</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8888799999999999em;vertical-align:-0.19444em;"></span><span class="mord">∃</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">∈</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1.036108em;vertical-align:-0.286108em;"></span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.07153em;">Z</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.15139200000000003em;"><span style="top:-2.5500000000000003em;margin-left:-0.07153em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">p</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.286108em;"><span></span></span></span></span></span></span><span class="mclose"><span class="mclose">)</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.688696em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mbin mtight">∗</span></span></span></span></span></span></span></span></span></span></span> 使得 $ {1,g,g<sup>2,g</sup>3,…,g<sup>{p-2}}=(Z_p)</sup>*$<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220702-160026044.png" alt="图 26" /></p><p>有限子群的阶必然整除有限群的阶<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220702-161053533.png" alt="图 27" /></p><p>欧拉定理,费马小定理的直接推广,适用于合数<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220702-162225022.png" alt="图 28" /></p><h3 id="模高次方程"><a class="markdownIt-Anchor" href="#模高次方程"></a> 模高次方程</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220702-162937731.png" alt="图 29" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220703-154738406.png" alt="图 30" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220703-160025710.png" alt="图 31" /></p><p>0也是二次剩余,所以有(p-1)/2+1<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220703-163327125.png" alt="图 32" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220703-172010446.png" alt="图 33" /></p><p>《信息安全数学基础》P146,证明当p是形如4k+3的素数时,解的形式如下。这里Dan讲了p不是这种形式的素数时仍然是有有效的随机算法来求解<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220703-172801792.png" alt="图 34" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220703-173747813.png" alt="图 35" /></p><p>当模数是合数时且指数大于1时,同余式的解并不好找<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220703-174113937.png" alt="图 36" /></p><h3 id="一些算法"><a class="markdownIt-Anchor" href="#一些算法"></a> 一些算法</h3><p>分组用32位表示是为了乘法不溢出<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220703-193849678.png" alt="图 37" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220703-194401484.png" alt="图 38" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220703-195005236.png" alt="图 39" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220703-200105825.png" alt="图 40" /></p><p>指数运算非常慢<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220703-201825479.png" alt="图 41" /></p><h3 id="模运算的一些难题"><a class="markdownIt-Anchor" href="#模运算的一些难题"></a> 模运算的一些难题</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220703-204200982.png" alt="图 42" /></p><h4 id="质数模的难题"><a class="markdownIt-Anchor" href="#质数模的难题"></a> 质数模的难题</h4><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220703-204558694.png" alt="图 43" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220703-205155540.png" alt="图 44" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220703-205703783.png" alt="图 45" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220703-210404888.png" alt="图 46" /></p><h4 id="合数模的难题"><a class="markdownIt-Anchor" href="#合数模的难题"></a> 合数模的难题</h4><p><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>Z</mi><mrow><mo stretchy="false">(</mo><mn>2</mn><mo stretchy="false">)</mo></mrow></msub><mo stretchy="false">(</mo><mi>n</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">Z_{(2)}(n)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.1052em;vertical-align:-0.3551999999999999em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.07153em;">Z</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.34480000000000005em;"><span style="top:-2.5198em;margin-left:-0.07153em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mopen mtight">(</span><span class="mord mtight">2</span><span class="mclose mtight">)</span></span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.3551999999999999em;"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal">n</span><span class="mclose">)</span></span></span></span>表示两个位数相同的质数乘积的集合<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220703-211017230.png" alt="图 47" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220703-211343194.png" alt="图 48" /></p><h2 id="基于陷门置换的公钥加密"><a class="markdownIt-Anchor" href="#基于陷门置换的公钥加密"></a> 基于陷门置换的公钥加密</h2><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220704-194707574.png" alt="图 1" /><br />公钥密码两个作用,一是会话建立(即对称密钥交换),二是非交互式应用<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220704-195133486.png" alt="图 2" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220704-195733748.png" alt="图 3" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220704-200013927.png" alt="图 4" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220704-200324912.png" alt="图 5" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220704-201150509.png" alt="图 6" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220704-201528143.png" alt="图 7" /></p><p>选择密文攻击(CCA)下的安全,有时可以缩写成选择密文攻击下的不可区分性(IND-CCA)</p><p>当攻击者可以篡改密文时,将以优势1赢下这个CCA游戏<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220704-202234082.png" alt="图 8" /></p><h3 id="构建cca安全的公钥加密系统"><a class="markdownIt-Anchor" href="#构建cca安全的公钥加密系统"></a> 构建CCA安全的公钥加密系统</h3><p>陷门函数特点是单向的,只有私钥持有人才能做逆向计算<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220704-203706752.png" alt="图 9" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220704-204306144.png" alt="图 10" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220704-204529922.png" alt="图 11" /></p><p>单向陷门函数只加密一个随机值,随机值用来生成对称密钥,单向陷门函数的私钥持有人可以恢复随机值进而恢复密钥<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220704-205210678.png" alt="图 12" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220704-210051189.png" alt="图 13" /></p><p>不能直接用单向陷门函数加解密明文,因为算法是确定的,就不可能是语义安全的,也会存在许多类型的攻击<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220704-210448185.png" alt="图 14" /></p><h3 id="构建一个陷门函数"><a class="markdownIt-Anchor" href="#构建一个陷门函数"></a> 构建一个陷门函数</h3><p>本节构建一个经典的陷门函数叫做RSA<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220706-113913049.png" alt="图 1" /></p><p>随机选取<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>Z</mi><mi>N</mi></msub></mrow><annotation encoding="application/x-tex">Z_N</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.83333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.07153em;">Z</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.32833099999999993em;"><span style="top:-2.5500000000000003em;margin-left:-0.07153em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.10903em;">N</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span>中的随机元素,这个元素很可能也在<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msubsup><mi>Z</mi><mi>N</mi><mo>∗</mo></msubsup></mrow><annotation encoding="application/x-tex">Z_N^*</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.964027em;vertical-align:-0.275331em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.07153em;">Z</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.688696em;"><span style="top:-2.424669em;margin-left:-0.07153em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.10903em;">N</span></span></span><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mbin mtight">∗</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.275331em;"><span></span></span></span></span></span></span></span></span></span>中,即该元素很可能是可逆的<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220706-114553462.png" alt="图 2" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-134728666.png" alt="图 1" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-135243531.png" alt="图 2" /><br />(虽然x大概率是可逆的,但是如果不可逆怎么办?)<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-140518861.png" alt="图 3" /></p><p>单向陷门函数是安全的,对称密码可以提供认证加密,H是random oracle,即H是某个从<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>Z</mi><mi>N</mi></msub></mrow><annotation encoding="application/x-tex">Z_N</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.83333em;vertical-align:-0.15em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.07153em;">Z</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.32833099999999993em;"><span style="top:-2.5500000000000003em;margin-left:-0.07153em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.10903em;">N</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span></span></span></span>映射到密钥空间的随机函数。那么这个公钥系统就可以抵抗选择密文攻击<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-141337895.png" alt="图 4" /></p><p>不要直接用RSA来加密明文!!!因为RSA是确定性的函数,因此不可能是语义安全的<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-142219617.png" alt="图 5" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-144845276.png" alt="图 6" /><br /><s>(直接用RSA加密密钥会被破解,但是ISO标准是用RSA加密密钥的哈希函数原项,破解出原项再hash一下就得到了密钥,这不是一样的吗?)</s> 对称密钥的空间大小远远小于RSA的明文空间</p><h3 id="pkcs1"><a class="markdownIt-Anchor" href="#pkcs1"></a> PKCS1</h3><p>ISO标准不是RSA在实际中的应用。实际使用是将对称密钥扩展然后用RSA加密<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-155325253.png" alt="图 7" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-155745663.png" alt="图 8" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-161016404.png" alt="图 9" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-161510702.png" alt="图 10" /></p><p>解决方法是,服务器解密后发现开头不是02,就认为明文只是个随机值而不是包含密钥的明文,继续协议就会发现密钥不一致从而结束会话(最常用的PKCS1)<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-161857606.png" alt="图 11" /></p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-164403209.png" alt="图 12" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-164738247.png" alt="图 13" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-165102605.png" alt="图 14" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-165350832.png" alt="图 15" /></p><h3 id="rsa的安全性"><a class="markdownIt-Anchor" href="#rsa的安全性"></a> RSA的安全性</h3><p>如果已经知道N的因式分解,那么可以用中国剩余定理求解x<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-165742673.png" alt="图 16" /></p><p>计算e次根一定要因式分解吗?如果没有其它方法就说明了一个reduction(规约):<br />任何有效的计算模N的e次根的算法都是有效的因式分解算法<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-170402836.png" alt="图 17" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-170457930.png" alt="图 18" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-170921035.png" alt="图 19" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-191109302.png" alt="图 20" /></p><h3 id="实际应用中的rsa"><a class="markdownIt-Anchor" href="#实际应用中的rsa"></a> 实际应用中的RSA</h3><p>最小的公钥e是3,可以但推荐还是65537。<br />RSA-CRT(带中国剩余定理的RSA)。RSA的加密很快但是解密很慢<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-191659680.png" alt="图 21" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-191734351.png" alt="图 22" /></p><p>RSA数学上是正确的,但是如果没有较好实现,会出现各种旁道攻击<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-192043649.png" alt="图 23" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-192502270.png" alt="图 24" /></p><p>防火墙刚启动时种子数量少导致伪随机数生成器重复生成p,导致网络上许多设备的p相同<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-192829037.png" alt="图 25" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220709-193044727.png" alt="图 26" /></p><h2 id="elgamal"><a class="markdownIt-Anchor" href="#elgamal"></a> ElGamal</h2><p>前一节讲了基于陷门置换函数的公钥加密系统,这节讲基于Diffle-Hellman协议的公钥加密系统<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220710-135815612.png" alt="图 27" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220710-140120802.png" alt="图 28" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220710-140204783.png" alt="图 29" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220710-140605666.png" alt="图 30" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220710-140755950.png" alt="图 31" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220710-141253387.png" alt="图 32" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220710-143232412.png" alt="图 33" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220710-144258200.png" alt="图 34" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220710-144810746.png" alt="图 35" /></p><p>ElGamal如果不做预计算,加密会比解密慢,但是因为g是固定的,意味着加密可以做预计算,当内存足够时加密是比解密快的。但是内存不够,不能预计算时,RSA更快,因为只做一次指数运算<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220710-145356269.png" alt="图 36" /></p><h3 id="elgamal安全性"><a class="markdownIt-Anchor" href="#elgamal安全性"></a> ElGamal安全性</h3><p>这个计算Diffle-Hellman假设对于分析ElGamal系统的安全性并不理想<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220710-151321371.png" alt="图 37" /><br />我们引入更强的哈希Diffle-Hellman假设,更强假设的意思是,攻击者的能力更强,但是我们提出的某个论断仍然成立<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220710-152637859.png" alt="图 38" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220710-153019687.png" alt="图 39" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220710-153645991.png" alt="图 40" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220710-154322338.png" alt="图 41" /><br />语义安全是不够的,我们真正想要的是选择密文安全。<br />为了证明选择密文安全,我们引入一个更强的假设叫做交互Diffle-Hellman假设<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220710-155808287.png" alt="图 42" /></p><p>交互Diffle-Hellman假设是CCA安全的,现在问题是在CDH假设上能否实现CCA安全,没有random oracle能否实现CCA安全<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220710-155938390.png" alt="图 43" /></p><h3 id="有更好安全性分析的elgamal变种"><a class="markdownIt-Anchor" href="#有更好安全性分析的elgamal变种"></a> 有更好安全性分析的ElGamal变种</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220710-164519323.png" alt="图 44" /></p><p>我们想在CDH假设上实现CCA安全,有两种办法,第一种是使用双线性群,这种群CDH和IDH是等价的;第二种是修改ElGamal系统<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220710-165537469.png" alt="图 45" /></p><p>第二种方法有一个ElGamal的变种满足CDH假设上的CCA安全<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220711-194457506.png" alt="图 1" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220711-194911790.png" alt="图 2" /></p><p>如果没有random oracle,CCA安全还成立吗?<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220711-195444146.png" alt="图 3" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220711-195636782.png" alt="图 4" /></p><h3 id="elgamal和rsa两种公钥系统共同遵循的原理"><a class="markdownIt-Anchor" href="#elgamal和rsa两种公钥系统共同遵循的原理"></a> ElGamal和RSA两种公钥系统共同遵循的原理</h3><p>这里没有形式化给出单向函数的定义,因为要证明单向函数是否存在,也就是要证明P不等于NP(若P=NP,则公钥密码学将有根基危机)<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220711-200506299.png" alt="图 5" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220711-201221299.png" alt="图 6" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220711-201533888.png" alt="图 7" /><br />RSA有乘法性质和陷门,陷门意味着有私钥就可以逆向计算<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220711-201825603.png" alt="图 8" /></p><p>总结:公钥加密依赖具有同态性质的单向函数和陷门<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220711-202057961.png" alt="图 9" /></p><h2 id="课程总结"><a class="markdownIt-Anchor" href="#课程总结"></a> 课程总结</h2><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220711-202552874.png" alt="图 10" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220711-202649423.png" alt="图 11" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220711-202751944.png" alt="图 13" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/Cryptography/IMG_20220711-202902739.png" alt="图 14" /></p>]]></content>
<summary type="html"><p>斯坦福教授Dan Boneh的密码学课程《Cryptography》<br />
课程链接:<a href="https://www.bilibili.com/video/BV1Ht411w7Re">https://www.bilibili.com/video/BV1Ht4</summary>
<category term="密码学" scheme="https://zunpan.github.io/categories/%E5%AF%86%E7%A0%81%E5%AD%A6/"/>
<category term="密码学" scheme="https://zunpan.github.io/tags/%E5%AF%86%E7%A0%81%E5%AD%A6/"/>
<category term="Cryptography" scheme="https://zunpan.github.io/tags/Cryptography/"/>
<category term="Dan Boneh" scheme="https://zunpan.github.io/tags/Dan-Boneh/"/>
</entry>
<entry>
<title>2022.05.23 某区块链公司面试记录</title>
<link href="https://zunpan.github.io/2022/05/23/2022-5-23-%E6%9F%90%E5%8C%BA%E5%9D%97%E9%93%BE%E5%85%AC%E5%8F%B8%E9%9D%A2%E8%AF%95%E8%AE%B0%E5%BD%95/"/>
<id>https://zunpan.github.io/2022/05/23/2022-5-23-%E6%9F%90%E5%8C%BA%E5%9D%97%E9%93%BE%E5%85%AC%E5%8F%B8%E9%9D%A2%E8%AF%95%E8%AE%B0%E5%BD%95/</id>
<published>2022-05-23T11:47:58.000Z</published>
<updated>2023-09-24T04:27:40.271Z</updated>
<content type="html"><![CDATA[<p>投的后端开发实习生岗,一天面了两次技术面</p><h2 id="一面"><a class="markdownIt-Anchor" href="#一面"></a> 一面</h2><ol><li>针对简历项目提问,为什么要paillier?为什么要秘密分享?</li><li>mysql的主从备份,主从节点的事务需不需要分开执行?从节点事务执行失败,主节点如何回滚?binlog的同步和事务在主从节点的执行,两者按时间顺序如何排列?</li><li>手写代码:求字典序第k大的数</li></ol><h2 id="二面"><a class="markdownIt-Anchor" href="#二面"></a> 二面</h2><ol><li>区块链共识算法有哪些?每个算法容错数量是多少?双花问题?默克尔树?默克尔证明?</li><li>Java基本数据类型和引用类型的区别</li><li>为什么每个基本数据类型都有包装类,包装类有什么用?</li><li>针对简历提问,秘密分享时间复杂度多少?paillier用在哪里?</li><li>手写代码:多线程卖票,要求每个线程同时工作,不能超卖</li><li>两个大文件,文件内容是字符串集合,内存略大于一个文件,如何对两个文件进行字符串去重?如何优化时间复杂度?</li></ol>]]></content>
<summary type="html"><p>投的后端开发实习生岗,一天面了两次技术面</p>
<h2 id="一面"><a class="markdownIt-Anchor" href="#一面"></a> 一面</h2>
<ol>
<li>针对简历项目提问,为什么要paillier?为什么要秘密分享?</li>
<</summary>
<category term="杂项" scheme="https://zunpan.github.io/categories/%E6%9D%82%E9%A1%B9/"/>
<category term="面试" scheme="https://zunpan.github.io/tags/%E9%9D%A2%E8%AF%95/"/>
</entry>
<entry>
<title>基于区块链的安全电子选举系统</title>
<link href="https://zunpan.github.io/2022/05/13/%E5%9F%BA%E4%BA%8E%E5%8C%BA%E5%9D%97%E9%93%BE%E7%9A%84%E5%AE%89%E5%85%A8%E7%94%B5%E5%AD%90%E9%80%89%E4%B8%BE%E7%B3%BB%E7%BB%9F/"/>
<id>https://zunpan.github.io/2022/05/13/%E5%9F%BA%E4%BA%8E%E5%8C%BA%E5%9D%97%E9%93%BE%E7%9A%84%E5%AE%89%E5%85%A8%E7%94%B5%E5%AD%90%E9%80%89%E4%B8%BE%E7%B3%BB%E7%BB%9F/</id>
<published>2022-05-13T13:10:09.000Z</published>
<updated>2023-09-24T04:27:40.282Z</updated>
<content type="html"><![CDATA[<p>背景:浙江大学软件学院实训课题,本组选择的课题是关于区块链和隐私计算的融合应用场景,具体方向是电子选举<br />需求:投票人不暴露身份隐私的情况下完成投票,计票机构在选票合法的情况下完成计票</p><hr /><h2 id="技术方案总结"><a class="markdownIt-Anchor" href="#技术方案总结"></a> 技术方案总结</h2><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/IMG_20230228-150306454.png" alt="图 1" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%9F%BA%E4%BA%8E%E5%8C%BA%E5%9D%97%E9%93%BE%E7%9A%84%E5%AE%89%E5%85%A8%E7%94%B5%E5%AD%90%E9%80%89%E4%B8%BE%E7%B3%BB%E7%BB%9F/IMG_20230228-150429261.png" alt="图 2" /></p><h2 id="项目总体情况"><a class="markdownIt-Anchor" href="#项目总体情况"></a> 项目总体情况</h2><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%9F%BA%E4%BA%8E%E5%8C%BA%E5%9D%97%E9%93%BE%E7%9A%84%E5%AE%89%E5%85%A8%E7%94%B5%E5%AD%90%E9%80%89%E4%B8%BE%E7%B3%BB%E7%BB%9F/IMG_20230228-150729861.png" alt="图 4" /></p><h2 id="核心功能性能测试"><a class="markdownIt-Anchor" href="#核心功能性能测试"></a> 核心功能性能测试</h2><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%9F%BA%E4%BA%8E%E5%8C%BA%E5%9D%97%E9%93%BE%E7%9A%84%E5%AE%89%E5%85%A8%E7%94%B5%E5%AD%90%E9%80%89%E4%B8%BE%E7%B3%BB%E7%BB%9F/IMG_20230228-150810364.png" alt="图 5" /></p><p>测试环境:x86_64,Intel® Xeon® Gold 6133 CPU @ 2.50GHz,Linux 5.4.0-96-generic<br />测试结果:大规模选举时延较高,适用于小规模选举活动,但因为选举本身的非实时性,也可用于更大规模</p><h2 id="亮点"><a class="markdownIt-Anchor" href="#亮点"></a> 亮点</h2><p>在隐匿链上选票来自谁的情况下选民只需要通过零知识证明证明自己的合法选民身份。<br />即使是对一个合法证明的微小改变,都无法通过链上以及链下的零知识证明验证,攻击者难以冒充合法选民。</p><h2 id="缺陷"><a class="markdownIt-Anchor" href="#缺陷"></a> 缺陷</h2><ul><li>监管机构(后端)无法证明没有存储选民的隐私信息,例如该选民的选票号,私有盐。这里存在漏洞可以使得作恶的监管机构只需执行两次解密算法就可以获得选民的选票内容</li><li>零知识证明以匿名的形式证明了选民身份,选民通过秘密ID和监管方产生的PK生成证明并公布。秘密输入应当是选民秘密持有的,因此生成证明的过程最好是在本地做。<br />当前选民需要通过可信的后端API生成证明,无法便捷地通过WASM在本地生成(原计划下libsnark难以迁移,且曲线计算迁移到wasm慢几十倍),因此使用起来较为不便。</li></ul><h2 id="系统展示"><a class="markdownIt-Anchor" href="#系统展示"></a> 系统展示</h2><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%9F%BA%E4%BA%8E%E5%8C%BA%E5%9D%97%E9%93%BE%E7%9A%84%E5%AE%89%E5%85%A8%E7%94%B5%E5%AD%90%E9%80%89%E4%B8%BE%E7%B3%BB%E7%BB%9F/IMG_20230228-151056681.png" alt="图 6" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%9F%BA%E4%BA%8E%E5%8C%BA%E5%9D%97%E9%93%BE%E7%9A%84%E5%AE%89%E5%85%A8%E7%94%B5%E5%AD%90%E9%80%89%E4%B8%BE%E7%B3%BB%E7%BB%9F/IMG_20230228-151117820.png" alt="图 7" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%9F%BA%E4%BA%8E%E5%8C%BA%E5%9D%97%E9%93%BE%E7%9A%84%E5%AE%89%E5%85%A8%E7%94%B5%E5%AD%90%E9%80%89%E4%B8%BE%E7%B3%BB%E7%BB%9F/IMG_20230228-151126913.png" alt="图 8" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%9F%BA%E4%BA%8E%E5%8C%BA%E5%9D%97%E9%93%BE%E7%9A%84%E5%AE%89%E5%85%A8%E7%94%B5%E5%AD%90%E9%80%89%E4%B8%BE%E7%B3%BB%E7%BB%9F/IMG_20230228-151144198.png" alt="图 9" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%9F%BA%E4%BA%8E%E5%8C%BA%E5%9D%97%E9%93%BE%E7%9A%84%E5%AE%89%E5%85%A8%E7%94%B5%E5%AD%90%E9%80%89%E4%B8%BE%E7%B3%BB%E7%BB%9F/IMG_20230228-151154950.png" alt="图 10" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%9F%BA%E4%BA%8E%E5%8C%BA%E5%9D%97%E9%93%BE%E7%9A%84%E5%AE%89%E5%85%A8%E7%94%B5%E5%AD%90%E9%80%89%E4%B8%BE%E7%B3%BB%E7%BB%9F/IMG_20230228-151204577.png" alt="图 11" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%9F%BA%E4%BA%8E%E5%8C%BA%E5%9D%97%E9%93%BE%E7%9A%84%E5%AE%89%E5%85%A8%E7%94%B5%E5%AD%90%E9%80%89%E4%B8%BE%E7%B3%BB%E7%BB%9F/IMG_20230228-151219293.png" alt="图 12" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%9F%BA%E4%BA%8E%E5%8C%BA%E5%9D%97%E9%93%BE%E7%9A%84%E5%AE%89%E5%85%A8%E7%94%B5%E5%AD%90%E9%80%89%E4%B8%BE%E7%B3%BB%E7%BB%9F/IMG_20230228-151228270.png" alt="图 13" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%9F%BA%E4%BA%8E%E5%8C%BA%E5%9D%97%E9%93%BE%E7%9A%84%E5%AE%89%E5%85%A8%E7%94%B5%E5%AD%90%E9%80%89%E4%B8%BE%E7%B3%BB%E7%BB%9F/IMG_20230228-151237647.png" alt="图 14" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%9F%BA%E4%BA%8E%E5%8C%BA%E5%9D%97%E9%93%BE%E7%9A%84%E5%AE%89%E5%85%A8%E7%94%B5%E5%AD%90%E9%80%89%E4%B8%BE%E7%B3%BB%E7%BB%9F/IMG_20230228-151251769.png" alt="图 15" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur@main/%E5%9F%BA%E4%BA%8E%E5%8C%BA%E5%9D%97%E9%93%BE%E7%9A%84%E5%AE%89%E5%85%A8%E7%94%B5%E5%AD%90%E9%80%89%E4%B8%BE%E7%B3%BB%E7%BB%9F/IMG_20230228-151307320.png" alt="图 16" /></p>]]></content>
<summary type="html"><p>背景:浙江大学软件学院实训课题,本组选择的课题是关于区块链和隐私计算的融合应用场景,具体方向是电子选举<br />
需求:投票人不暴露身份隐私的情况下完成投票,计票机构在选票合法的情况下完成计票</p>
<hr />
<h2 id="技术方案总结"><a class="ma</summary>
<category term="区块链" scheme="https://zunpan.github.io/categories/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
<category term="密码学" scheme="https://zunpan.github.io/tags/%E5%AF%86%E7%A0%81%E5%AD%A6/"/>
<category term="区块链" scheme="https://zunpan.github.io/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
<category term="电子选举" scheme="https://zunpan.github.io/tags/%E7%94%B5%E5%AD%90%E9%80%89%E4%B8%BE/"/>
<category term="隐私计算" scheme="https://zunpan.github.io/tags/%E9%9A%90%E7%A7%81%E8%AE%A1%E7%AE%97/"/>
</entry>
<entry>
<title>MIT-Missing-Semester学习笔记</title>
<link href="https://zunpan.github.io/2022/05/07/MIT-Missing-Semester%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
<id>https://zunpan.github.io/2022/05/07/MIT-Missing-Semester%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/</id>
<published>2022-05-07T11:46:29.000Z</published>
<updated>2023-09-24T04:27:40.278Z</updated>
<content type="html"><![CDATA[<h2 id="course-overview-the-shell"><a class="markdownIt-Anchor" href="#course-overview-the-shell"></a> Course overview + the shell</h2><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/MIT-Missing-Semester/IMG_20220507-194044416.png" alt="图 19" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/MIT-Missing-Semester/IMG_20220507-194100184.png" alt="图 20" /></p><h2 id="shell-tools-and-scripting"><a class="markdownIt-Anchor" href="#shell-tools-and-scripting"></a> Shell Tools and Scripting</h2><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/MIT-Missing-Semester/IMG_20220507-194224160.png" alt="图 21" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/MIT-Missing-Semester/IMG_20220507-194230990.png" alt="图 22" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/MIT-Missing-Semester/IMG_20220507-194304983.png" alt="图 23" /></p><h3 id="tldr"><a class="markdownIt-Anchor" href="#tldr"></a> TLDR</h3><p>TLDR可以代替man命令,查找命令说明书,TLDR可以给出详细的例子<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/MIT-Missing-Semester/IMG_20220507-194359602.png" alt="图 24" /></p><h3 id="find"><a class="markdownIt-Anchor" href="#find"></a> find</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/MIT-Missing-Semester/IMG_20220507-194433128.png" alt="图 25" /><br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/MIT-Missing-Semester/IMG_20220507-194437955.png" alt="图 26" /></p><h3 id="ripgrep"><a class="markdownIt-Anchor" href="#ripgrep"></a> ripgrep</h3><p>可以代替grep查找文件内容符合条件的文件<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/MIT-Missing-Semester/IMG_20220507-194518243.png" alt="图 27" /></p><p>Ctrl+R 可以进入用子串查找历史命令的模式</p><p>xargs 命令,它可以使用标准输入中的内容作为参数。 例如 ls | xargs rm 会删除当前目录中的所有文件。</p><h2 id="vim"><a class="markdownIt-Anchor" href="#vim"></a> Vim</h2><p>略</p><h2 id="data-wrangling"><a class="markdownIt-Anchor" href="#data-wrangling"></a> Data Wrangling</h2><p>数据处理常用工具有grep</p><p>另外还有一些常用的数据处理工具</p><ul><li>sed,文件(输入流)处理工具</li></ul><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/MIT-Missing-Semester/IMG_20220507-194918105.png" alt="图 28" /></p><ul><li>sort,排序工具</li></ul><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/MIT-Missing-Semester/IMG_20220507-194959858.png" alt="图 29" /></p><ul><li>awk,作用于文件的多功能编程语言</li></ul><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/MIT-Missing-Semester/IMG_20220507-195028810.png" alt="图 30" /></p><h2 id="command-line-enviroment"><a class="markdownIt-Anchor" href="#command-line-enviroment"></a> Command-line Enviroment</h2><h3 id="job-control作业控制"><a class="markdownIt-Anchor" href="#job-control作业控制"></a> Job Control(作业控制)</h3><p>结束一个正在执行的程序可以按ctrl+c,实质是终端向程序发送了一个SIGINT的signal<br />SIGINT可以在程序中捕获处理,因此ctrl+c也就是SIGINT信号有时候没用。</p><p>SIGQUIT信号可以结束程序并不会被捕获,按ctrl+\</p><p>SIGSTOP信号可以暂停程序到后台,按ctrl+z。命令行输入jobs查看所有程序运行状态,使用fg %程序id 或 bg %程序id 将程序从暂停状态转到前端执行或后端执行</p><p>尽管程序可以在后端执行,但是一旦终端关闭,作为子进程的程序会收到一个SIGHUP信号导致被挂起,可以使用nohup 程序 &来使得程序在后端执行并忽略SIGHUP信号</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/MIT-Missing-Semester/IMG_20220507-195256131.png" alt="图 31" /></p><h3 id="terminal-multiplexers终端复用器"><a class="markdownIt-Anchor" href="#terminal-multiplexers终端复用器"></a> Terminal Multiplexers(终端复用器)</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/MIT-Missing-Semester/IMG_20220507-195325317.png" alt="图 32" /></p><h3 id="aliases别名"><a class="markdownIt-Anchor" href="#aliases别名"></a> Aliases(别名)</h3><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/MIT-Missing-Semester/IMG_20220507-195357888.png" alt="图 33" /></p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/MIT-Missing-Semester/IMG_20220507-195404041.png" alt="图 34" /></p><p>直接在命令行定义别名,关闭终端后就没了,需要在.bashrc或者.zshrc这样的终端启动文件中做持久化</p><h3 id="dotfiles点文件或者配置文件"><a class="markdownIt-Anchor" href="#dotfiles点文件或者配置文件"></a> Dotfiles(点文件或者配置文件)</h3><p>许多配置文件以’.‘开头,所以叫dotfile,默认情况下ls看不到dotfile<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/MIT-Missing-Semester/IMG_20220507-195451639.png" alt="图 35" /></p><p>配置文件最好用版本控制统一管理,然后将原来的配置文件路径软链接到版本控制下的配置文件路径,github上有许多dotfile仓库</p><h3 id="remote-machines远程机器"><a class="markdownIt-Anchor" href="#remote-machines远程机器"></a> Remote Machines(远程机器)</h3><p>使用ssh可以登录远程终端<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/MIT-Missing-Semester/IMG_20220507-195546703.png" alt="图 36" /></p><p>foo是用户名,bar.mit.edu是域名,也可以直接是ip地址</p><p>也可以直接使用ssh执行命令,只要在上面的命令后面加上需要执行的命令即可</p><h2 id="version-controlgit"><a class="markdownIt-Anchor" href="#version-controlgit"></a> Version Control(Git)</h2><p>略</p><h2 id="debugging-and-profiling"><a class="markdownIt-Anchor" href="#debugging-and-profiling"></a> Debugging and Profiling</h2><h3 id="debugging"><a class="markdownIt-Anchor" href="#debugging"></a> Debugging</h3><h4 id="printf-debugging-and-logging"><a class="markdownIt-Anchor" href="#printf-debugging-and-logging"></a> Printf debugging and Logging</h4><p>“The most effective debugging tool is still careful thought, coupled with judiciously placed print statements” — Brian Kernighan, Unix for Beginners.<br />在程序中使用logging而不用printf的好处在于</p><ul><li>日志可以输出到文件、sockets、甚至是远程服务器而不一定是标准输出</li><li>日志支持几种输出级别(例如INFO,DEBUG,WARN,ERROR)</li><li>对于新出现的问题,日志有足够多的信息来排查</li></ul><p>tips:输出可以按级别用不同颜色表示,例如ERROR用红色<br /><code>echo -e "\e[38;2;255;0;0mThis is red\e[0m"</code>会打印红色的“This is red”到终端上</p><h4 id="third-party-logs"><a class="markdownIt-Anchor" href="#third-party-logs"></a> Third Party logs</h4><p>许多第三方程序会将日志写到系统的某一处,一般是/var/log. NGINX服务器会将日志放在/var/log/nginx下. 许多linux系统使用systemd, 一个系统守护进程来控制许多事情例如某些服务的开启和运行, systemd将日志放在/var/log/journal下, 可以使用journalctl查看</p><p><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/MIT-Missing-Semester/IMG_20220507-195758978.png" alt="图 37" /></p><h4 id="debuggers"><a class="markdownIt-Anchor" href="#debuggers"></a> Debuggers</h4><p>略</p><h4 id="specialized-tools"><a class="markdownIt-Anchor" href="#specialized-tools"></a> Specialized Tools</h4><p>Linux下可以使用strace查看程序系统调用情况<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/MIT-Missing-Semester/IMG_20220507-195910639.png" alt="图 38" /></p><p>tcpdump和Wireshark是网络包分析器<br />web开发中,Chrome和Firefox的开发者工具十分方便</p><h3 id="profiling分析"><a class="markdownIt-Anchor" href="#profiling分析"></a> Profiling(分析)</h3><p>学习分析和检测工具可以帮助理解程序中那一部分花费最多时间或资源以便优化这部分<br />Timing<br />三种不同的运行时间</p><ul><li>真实时间:程序开始到结束的时间,包括阻塞时间、等待IO、网络</li><li>用户态时间:CPU执行程序中用户态代码的时间</li><li>系统态时间:CPU执行程序中核心态代码的时间<br />正确的程序执行时间=用户态时间+系统态时间</li></ul><p>time命令可以测试程序的三种时间<br /><img src="https://cdn.jsdelivr.net/gh/zunpan/note-imgur/MIT-Missing-Semester/IMG_20220507-200158574.png" alt="图 39" /></p><h2 id="metaprogramming"><a class="markdownIt-Anchor" href="#metaprogramming"></a> Metaprogramming</h2><p>这节主要讲系统构建工具make、持续集成等</p><p>略</p><h2 id="security-and-cryptography"><a class="markdownIt-Anchor" href="#security-and-cryptography"></a> Security and Cryptography</h2><h3 id="entropy熵"><a class="markdownIt-Anchor" href="#entropy熵"></a> Entropy(熵)</h3><p>熵度量了不确定性并可以用于决定密码的强度<br />熵的单位是比特,对于一个均匀分布的离散随机变量,熵等于<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>l</mi><mi>o</mi><msub><mi>g</mi><mn>2</mn></msub><mo stretchy="false">(</mo><mtext>所有可能的个数,即</mtext><mi>n</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">log_2(所有可能的个数,即n)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">o</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.30110799999999993em;"><span style="top:-2.5500000000000003em;margin-left:-0.03588em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord cjk_fallback">所</span><span class="mord cjk_fallback">有</span><span class="mord cjk_fallback">可</span><span class="mord cjk_fallback">能</span><span class="mord cjk_fallback">的</span><span class="mord cjk_fallback">个</span><span class="mord cjk_fallback">数</span><span class="mord cjk_fallback">,</span><span class="mord cjk_fallback">即</span><span class="mord mathnormal">n</span><span class="mclose">)</span></span></span></span><br />扔一次硬币的熵是1比特。掷一次六面骰子的熵大约为2.58比特。一般我们认为攻击者了解密码的模型(最小长度,最大长度,可能包含的字符种类等),但是不了解某个密码是如何选择的</p><p><a href="https://xkcd.com/936/">https://xkcd.com/936/</a> 例子里面,“correcthorsebatterystaple”这个密码比“Tr0ub4dor&3”更安全,因为前者熵更大,大约40比特的熵足以对抗在线穷举攻击(受限于网络速度和应用认证机制);而对于离线穷举攻击(主要受限于计算速度),一般需要更强的密码(比如80比特)</p><h2 id="potpourri"><a class="markdownIt-Anchor" href="#potpourri"></a> Potpourri</h2><p>略</p>]]></content>
<summary type="html"><h2 id="course-overview-the-shell"><a class="markdownIt-Anchor" href="#course-overview-the-shell"></a> Course overview + the shell</h2>
<p><</summary>
<category term="Linux" scheme="https://zunpan.github.io/categories/Linux/"/>
<category term="Linux" scheme="https://zunpan.github.io/tags/Linux/"/>
<category term="Shell" scheme="https://zunpan.github.io/tags/Shell/"/>
</entry>
</feed>