-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch.xml
1073 lines (501 loc) · 572 KB
/
search.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
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>tensorflow1&2</title>
<link href="/22368.html"/>
<url>/22368.html</url>
<content type="html"><![CDATA[<h4 id="tensorflow基本概念"><a href="#tensorflow基本概念" class="headerlink" title="tensorflow基本概念"></a>tensorflow基本概念</h4><h5 id="tensor"><a href="#tensor" class="headerlink" title="tensor"></a>tensor</h5><h5 id="session"><a href="#session" class="headerlink" title="session"></a>session</h5><h5 id="变量和占位符"><a href="#变量和占位符" class="headerlink" title="变量和占位符"></a>变量和占位符</h5><h5 id="计算图"><a href="#计算图" class="headerlink" title="计算图"></a>计算图</h5><h4 id="常用的packag"><a href="#常用的packag" class="headerlink" title="常用的packag"></a>常用的packag</h4><h4 id="常用的函数"><a href="#常用的函数" class="headerlink" title="常用的函数"></a>常用的函数</h4><h4 id="tensorflow1与tensorflow2-的区别以及迁移"><a href="#tensorflow1与tensorflow2-的区别以及迁移" class="headerlink" title="tensorflow1与tensorflow2 的区别以及迁移"></a>tensorflow1与tensorflow2 的区别以及迁移</h4>]]></content>
</entry>
<entry>
<title>RegExp</title>
<link href="/65388.html"/>
<url>/65388.html</url>
<content type="html"><![CDATA[<h4 id="常用正则表达式:"><a href="#常用正则表达式:" class="headerlink" title="常用正则表达式:"></a>常用正则表达式:</h4><h5 id="身份证:"><a href="#身份证:" class="headerlink" title="身份证:"></a>身份证:</h5><pre><code class="python">IDCards_pattern = r'^([1-9]\d{5}[12]\d{3}(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])\d{3}[0-9xX])$'IDs = re.findall(IDCards_pattern, text, flags=0)</code></pre><h5 id="邮箱:"><a href="#邮箱:" class="headerlink" title="邮箱:"></a>邮箱:</h5><pre><code class="python">email_pattern = '^[*#\u4e00-\u9fa5 a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$'emails = re.findall(email_pattern, text, flags=0)</code></pre><h5 id="phone-number"><a href="#phone-number" class="headerlink" title="phone number:"></a>phone number:</h5><pre><code class="python">cellphone_pattern = '^((13[0-9])|(14[0-9])|(15[0-9])|(17[0-9])|(18[0-9]))\d{8}$'phoneNumbers = re.findall(cellphone_pattern, text, flags=0)</code></pre>]]></content>
</entry>
<entry>
<title>NLper_algorithm_design</title>
<link href="/38621.html"/>
<url>/38621.html</url>
<content type="html"><![CDATA[<p>数组、链表、栈、队列、树、图</p><p>二分法、分治法、双指针、递归、动态规划</p>]]></content>
</entry>
<entry>
<title>算法工程师</title>
<link href="/22896.html"/>
<url>/22896.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title>网络编程</title>
<link href="/52560.html"/>
<url>/52560.html</url>
<content type="html"><![CDATA[<h2 id="相关书籍"><a href="#相关书籍" class="headerlink" title="相关书籍"></a>相关书籍</h2><p>Unix 网络编程</p><h2 id="Netty框架"><a href="#Netty框架" class="headerlink" title="Netty框架"></a>Netty框架</h2><p>短连接和长连接的区别</p>]]></content>
</entry>
<entry>
<title>Java_Web应用优化</title>
<link href="/56023.html"/>
<url>/56023.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title>Spring-detail</title>
<link href="/54664.html"/>
<url>/54664.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title>concurrency</title>
<link href="/35296.html"/>
<url>/35296.html</url>
<content type="html"><![CDATA[<p>底层的并发功能与上层应用程序的并发语义之间并不存在一种简单的而只管的映射关系。为了解决在Java 底层机制与设计级策略之间的不匹配问题,我们给出了一组简化的并发程序编写规则。</p><p>设计规则和思维模式</p><h2 id="并发编程基础理论"><a href="#并发编程基础理论" class="headerlink" title="并发编程基础理论"></a>并发编程基础理论</h2><h3 id="并发简史"><a href="#并发简史" class="headerlink" title="并发简史"></a>并发简史</h3><h3 id="线程的优势"><a href="#线程的优势" class="headerlink" title="线程的优势"></a>线程的优势</h3><h3 id="线程带来的风险"><a href="#线程带来的风险" class="headerlink" title="线程带来的风险"></a>线程带来的风险</h3><h4 id="安全性问题"><a href="#安全性问题" class="headerlink" title="安全性问题"></a>安全性问题</h4><h4 id="活跃性问题"><a href="#活跃性问题" class="headerlink" title="活跃性问题"></a>活跃性问题</h4><p> 死锁、饥饿、活锁</p><h4 id="性能问题"><a href="#性能问题" class="headerlink" title="性能问题"></a>性能问题</h4><h3 id="线程无处不在"><a href="#线程无处不在" class="headerlink" title="线程无处不在"></a>线程无处不在</h3><p>待开发的程序有或者没有,开发时使用的框架会有使用多线程,基本上所有的java应用程序都会有。</p><hr><h3 id="线程安全性"><a href="#线程安全性" class="headerlink" title="线程安全性"></a>线程安全性</h3><p> 编写线程安全的代码,核心在于要对状态访问操作进行管理,特别是对共现的和可变的状态的访问。</p><h4 id="什么是线程安全性"><a href="#什么是线程安全性" class="headerlink" title="什么是线程安全性"></a>什么是线程安全性</h4><p>当多个线程访问某个类时,这个类始终都能表现出正确的行为。</p><p>无状态对象一定是线程安全的。</p><h4 id="原子性"><a href="#原子性" class="headerlink" title="原子性"></a>原子性</h4><h5 id="竞态条件"><a href="#竞态条件" class="headerlink" title="竞态条件"></a>竞态条件</h5><p> 由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,也就是竞态条件。</p><p> 竞态条件和数据竞争的区别</p><h5 id="延迟初始化中的竞态条件"><a href="#延迟初始化中的竞态条件" class="headerlink" title="延迟初始化中的竞态条件"></a>延迟初始化中的竞态条件</h5><h5 id="复合操作"><a href="#复合操作" class="headerlink" title="复合操作"></a>复合操作</h5><p> java.util.concurrent.atomic包中含有原子变量类,用于实现在数值和对象引用上的原子状态转换。</p><h4 id="加锁机制"><a href="#加锁机制" class="headerlink" title="加锁机制"></a>加锁机制</h4><h5 id="内置锁"><a href="#内置锁" class="headerlink" title="内置锁"></a>内置锁</h5><h5 id="重入"><a href="#重入" class="headerlink" title="重入"></a>重入</h5><p>内置锁是可重入的</p><h4 id="用锁来保护状态"><a href="#用锁来保护状态" class="headerlink" title="用锁来保护状态"></a>用锁来保护状态</h4><h4 id="活跃性与性能"><a href="#活跃性与性能" class="headerlink" title="活跃性与性能"></a>活跃性与性能</h4><h3 id="对象的共享"><a href="#对象的共享" class="headerlink" title="对象的共享"></a>对象的共享</h3><h4 id="可见性"><a href="#可见性" class="headerlink" title="可见性"></a>可见性</h4><p>访问某个共享且可变得变量时要求所有相册给在同一个锁上同步。</p><h5 id="失效数据"><a href="#失效数据" class="headerlink" title="失效数据"></a>失效数据</h5><h5 id="非原子的64位操作"><a href="#非原子的64位操作" class="headerlink" title="非原子的64位操作"></a>非原子的64位操作</h5><h5 id="加锁与可见性"><a href="#加锁与可见性" class="headerlink" title="加锁与可见性"></a>加锁与可见性</h5><h5 id="Volatile变量"><a href="#Volatile变量" class="headerlink" title="Volatile变量"></a>Volatile变量</h5><p> volatile变量是一种比sychronized关键字更轻量级的同步机制</p><p>当且仅当满足以下所有条件时,才应该使用volatile变量</p><h4 id="发布与逸出"><a href="#发布与逸出" class="headerlink" title="发布与逸出"></a>发布与逸出</h4><p>发布一个对象是指,使对象能够在当前作用域之外的代码中使用。</p><p>当某个不应该发布的对象被发布时,这种情况就被称为逸出</p><h5 id="安全的对象构造过程"><a href="#安全的对象构造过程" class="headerlink" title="安全的对象构造过程"></a>安全的对象构造过程</h5><p>在构造函数中注册一个事件监听器或启动线程,使用一个室友的构造函数和一个公共的工厂方法来防止this引用在构造过程中逸出</p><h4 id="线程封闭"><a href="#线程封闭" class="headerlink" title="线程封闭"></a>线程封闭</h4><p>如果仅在单线程类范围数据,就不需要同步,这个技术被成为相册线程封闭。封闭技术应用于Swingy以及JDBC的Connection对象。</p><h5 id="Ad-hoc线程封闭"><a href="#Ad-hoc线程封闭" class="headerlink" title="Ad-hoc线程封闭"></a>Ad-hoc线程封闭</h5><p>维护线程封闭性的职责完全由程序实现来承担</p><p>使用单线程子系统的另一个原因是为了避免死锁</p><h5 id="栈封闭"><a href="#栈封闭" class="headerlink" title="栈封闭"></a>栈封闭</h5><h5 id="ThreadLocal类"><a href="#ThreadLocal类" class="headerlink" title="ThreadLocal类"></a>ThreadLocal类</h5><p>当某个频执行的操作西药一个临时对象,例如一个缓冲区,而同时又洗碗避免在每次执行时重新分配该临时对象,就可以使用这项技术。</p><p>在实现用用程序框架是大量使用了ThreadLocal.在EJB调用期间</p><h4 id="不变性"><a href="#不变性" class="headerlink" title="不变性"></a>不变性</h4><p>不可变对象一定是线程安全的</p><h5 id="Final域"><a href="#Final域" class="headerlink" title="Final域"></a>Final域</h5><h5 id="使用Volatile类型来发布不可变对象"><a href="#使用Volatile类型来发布不可变对象" class="headerlink" title="使用Volatile类型来发布不可变对象"></a>使用Volatile类型来发布不可变对象</h5><h4 id="安全发布"><a href="#安全发布" class="headerlink" title="安全发布"></a>安全发布</h4><h5 id="不正确的发布:正确的对象被破坏"><a href="#不正确的发布:正确的对象被破坏" class="headerlink" title="不正确的发布:正确的对象被破坏"></a>不正确的发布:正确的对象被破坏</h5><h5 id="不可变对象于初始化安全性"><a href="#不可变对象于初始化安全性" class="headerlink" title="不可变对象于初始化安全性"></a>不可变对象于初始化安全性</h5><h5 id="安全发布的常用模式"><a href="#安全发布的常用模式" class="headerlink" title="安全发布的常用模式"></a>安全发布的常用模式</h5><p> 要安全地阿发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全地发布:</p><p> 在静态初始化函数中初始化一个对象引用;</p><p> 将对象地引用保存到volatile类型地域或者AtomicReferance对象中。</p><p> 将对象的引用保存到某个正确构造对象的final类型域中。</p><p> 将对象的引用保存到一个由锁保护的域中。</p><p> 线程安全库中的容器类提供了以下的安全发布保证:</p><p> 类库中的其他数据传递机制同样能实现安全发布。</p><h5 id="事实不可变对象"><a href="#事实不可变对象" class="headerlink" title="事实不可变对象"></a>事实不可变对象</h5><h5 id="可变对象"><a href="#可变对象" class="headerlink" title="可变对象"></a>可变对象</h5><h6 id="对象的发布需求取决于它的可变性"><a href="#对象的发布需求取决于它的可变性" class="headerlink" title="对象的发布需求取决于它的可变性"></a>对象的发布需求取决于它的可变性</h6><p>不可变对象可以通过任意机制来发布;</p><p>事实不可变对象必须通过安全方式来发布;</p><p>可边对象必须通过安全方式来发布,并且必须时是线程安全的或者由某个锁保护起来。</p><p> </p><h5 id="安全地共享对象"><a href="#安全地共享对象" class="headerlink" title="安全地共享对象"></a>安全地共享对象</h5><p>在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:</p><ul><li><strong>线程封闭</strong>。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。</li><li><strong>只读共享</strong>。在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。</li><li><strong>线程安全共享</strong>。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。</li><li><strong>保护对象</strong>。被保护的对象只能通过持有特定的锁来访问。保护对象包括封闭在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。</li></ul><h3 id="对象的组合"><a href="#对象的组合" class="headerlink" title="对象的组合"></a>对象的组合</h3><h4 id="设计线程安全的类"><a href="#设计线程安全的类" class="headerlink" title="设计线程安全的类"></a>设计线程安全的类</h4><p>通过使用封装技术,可以使得在不对整个程序进行分析的情况下就可以判断一个类是否是线程安全的。</p><p>同步策略定义了如何在不违背对象的不变性条件或后验条件的情况下对其状态的访问操作进行协同。同步策略规定了如何将不可变性、线程安全性与加锁机制等结合起来以维护线程的安全性,并且还规定了哪些变量由哪些锁来保护。</p><p>在设计线程安全的类的过程中,需要包含以下三个基本要素:</p><ul><li>找出构成对象状态的所有变量</li><li>找出约束状态变量的不变性条件</li><li>建立对象状态的并发访问管理策略</li></ul><h5 id="收集同步需求"><a href="#收集同步需求" class="headerlink" title="收集同步需求"></a>收集同步需求</h5><p> 要确保类的线程安全性,就需要确保它的不变性条件不会在并发访问的情况下被破坏,这就需要对其状态进行推断。</p><p> 如果不了解对象的不变性条件与后验条件,那么就不能确保线程安全性。要满足在状态变量的有效值或状态转换上的各种约束条件,就需要借助于原子性于与封装性。</p><h5 id="依赖状态的操作"><a href="#依赖状态的操作" class="headerlink" title="依赖状态的操作"></a>依赖状态的操作</h5><h5 id="状态的所有权"><a href="#状态的所有权" class="headerlink" title="状态的所有权"></a>状态的所有权</h5><p>垃圾回收机制使我们避免了如何处理所有权的问题。</p><p>为了防止多个线程在并发访问同一个对象时产生的相互干扰,这些对象应该要么时线程安全的对象,要么是事实不可变的对象,或者由锁来保护的对象。</p><h4 id="实例封闭"><a href="#实例封闭" class="headerlink" title="实例封闭"></a>实例封闭</h4><p>封闭机制更易于构造线程安全的类,因为当封闭类的状态时,在分析类的线程安全性时就无须检查整个程序。</p><p>Java平台的类库中由很多线程封闭的示例,其中有些类的唯一用途就是将非线程安全的类转化为线程安全的类。</p><h5 id="Java监视器模式"><a href="#Java监视器模式" class="headerlink" title="Java监视器模式"></a>Java监视器模式</h5><p>私有锁和对象的内置锁</p><h5 id="示例:车辆追踪"><a href="#示例:车辆追踪" class="headerlink" title="示例:车辆追踪"></a>示例:车辆追踪</h5><h4 id="线程安全性委托"><a href="#线程安全性委托" class="headerlink" title="线程安全性委托"></a>线程安全性委托</h4><h5 id="示例:基于委托的车辆追踪"><a href="#示例:基于委托的车辆追踪" class="headerlink" title="示例:基于委托的车辆追踪"></a>示例:基于委托的车辆追踪</h5><h5 id="独立的状态变量"><a href="#独立的状态变量" class="headerlink" title="独立的状态变量"></a>独立的状态变量</h5><h5 id="当委托失效时"><a href="#当委托失效时" class="headerlink" title="当委托失效时"></a>当委托失效时</h5><h5 id="发布底层的状态变量"><a href="#发布底层的状态变量" class="headerlink" title="发布底层的状态变量"></a>发布底层的状态变量</h5><h5 id="示例:发布状态的车辆追踪器"><a href="#示例:发布状态的车辆追踪器" class="headerlink" title="示例:发布状态的车辆追踪器"></a>示例:发布状态的车辆追踪器</h5><h4 id="在现有的线程安全类中添加功能"><a href="#在现有的线程安全类中添加功能" class="headerlink" title="在现有的线程安全类中添加功能"></a>在现有的线程安全类中添加功能</h4><p>Java类库包含许多有用的“基础模块”类。通常,我们应该优先选择重用这些现有的类而不是创建新的类:重用能降低开发工作量、开发风险(因为现有的类都已经通过测试)以及维护成本。</p><h5 id="客户端加锁机制"><a href="#客户端加锁机制" class="headerlink" title="客户端加锁机制"></a>客户端加锁机制</h5><p>客户端加锁是指,对于使用某个对象X的客户端代码,使用X本身用于保护其状态的锁来保护客户代码。要使用客户端加锁,你必须知道对象X使用的是哪一个锁。</p><h5 id="组合"><a href="#组合" class="headerlink" title="组合"></a>组合</h5><h5 id="将同步策略文档化"><a href="#将同步策略文档化" class="headerlink" title="将同步策略文档化"></a>将同步策略文档化</h5><h3 id="基础构建模块"><a href="#基础构建模块" class="headerlink" title="基础构建模块"></a>基础构建模块</h3><h4 id="同步容器类"><a href="#同步容器类" class="headerlink" title="同步容器类"></a>同步容器类</h4><p>同步容器类有Vector和Hashtable,由Collections.synchronizedXxx等工厂方法创建。这些类实现线程安全的方式是:将它们的状态封装起来,并对每个共有方法都进行同步,使得每次只有一个线程能访问容器的状态。</p><h5 id="同步容器类的问题"><a href="#同步容器类的问题" class="headerlink" title="同步容器类的问题"></a>同步容器类的问题</h5><p>通过客户端加锁来解决不可靠迭代问题,但要牺牲一些伸缩性。</p><h5 id="迭代器与ConcurrentModificationException"><a href="#迭代器与ConcurrentModificationException" class="headerlink" title="迭代器与ConcurrentModificationException"></a>迭代器与ConcurrentModificationException</h5><h5 id="隐藏迭代器"><a href="#隐藏迭代器" class="headerlink" title="隐藏迭代器"></a>隐藏迭代器</h5><p>容器的hashCode和equals等方法也会间接地执行迭代操作,当容器作为另一个容器的元素或键值时,就会出现这种情况。同样,containsAll、removeAll和retainAll等方法,以及把容器作为参数的构造函数,都会对容器进行迭代。所有这些间接的迭代操作都可能抛出ConcurrentModificationException。</p><h4 id="并发容器"><a href="#并发容器" class="headerlink" title="并发容器"></a>并发容器</h4><p>Java5.0提供了许多并发容器类来改进同步容器的性能。同步容器将所有对容器状态的访问都串行化,以实现它们的线程安全性。这种哈哈的代价是杨中国降低并发性,当多个线程竞争容器的锁时,吞吐量将严重降低。</p><h5 id="ConcurrentHashMap"><a href="#ConcurrentHashMap" class="headerlink" title="ConcurrentHashMap"></a>ConcurrentHashMap</h5><h5 id="额外的原子Map操作"><a href="#额外的原子Map操作" class="headerlink" title="额外的原子Map操作"></a>额外的原子Map操作</h5><h5 id="CopyOnWriteArrayList"><a href="#CopyOnWriteArrayList" class="headerlink" title="CopyOnWriteArrayList"></a>CopyOnWriteArrayList</h5><h4 id="阻塞队列和生产者—消费者模式"><a href="#阻塞队列和生产者—消费者模式" class="headerlink" title="阻塞队列和生产者—消费者模式"></a>阻塞队列和生产者—消费者模式</h4><p>一种常见的生产者-消费者设计模式就是线程池与工作队列的组合,在Executor任务执行框架中就体现呢这种模式。</p><p><strong>在构建高可靠的应用程序时,有界队列是一种强大的资源管理工具:它们能抑制并防止产生过多的工作项,使应用程序在负荷过载的情况下变得更加健壮。</strong></p><h5 id="示例:桌面搜索"><a href="#示例:桌面搜索" class="headerlink" title="示例:桌面搜索"></a>示例:桌面搜索</h5><h5 id="串行线程封闭"><a href="#串行线程封闭" class="headerlink" title="串行线程封闭"></a>串行线程封闭</h5><h5 id="双端队列与工作密取"><a href="#双端队列与工作密取" class="headerlink" title="双端队列与工作密取"></a>双端队列与工作密取</h5><h4 id="阻塞方法与中断方法"><a href="#阻塞方法与中断方法" class="headerlink" title="阻塞方法与中断方法"></a>阻塞方法与中断方法</h4><h4 id="同步工具类"><a href="#同步工具类" class="headerlink" title="同步工具类"></a>同步工具类</h4><h5 id="闭锁-Latch"><a href="#闭锁-Latch" class="headerlink" title="闭锁-Latch"></a>闭锁-Latch</h5><p>闭锁是一种同步工具类,可以延迟线程的进度直到其到达终止状态。闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程能通过,当到达结束状态时,这扇门会打开并允许所有的线程通过。当闭锁到达结束状态后,将不会在改变状态,因此这扇门将永远保持打开的状态。</p><h5 id="Future-Task"><a href="#Future-Task" class="headerlink" title="Future Task"></a>Future Task</h5><h5 id="信号量-Semaphore"><a href="#信号量-Semaphore" class="headerlink" title="信号量-Semaphore"></a>信号量-Semaphore</h5><p>semaphore可以用于实现资源池,例如数据库的连接池。</p><h5 id="栅栏-Barrier"><a href="#栅栏-Barrier" class="headerlink" title="栅栏-Barrier"></a>栅栏-Barrier</h5><p>栅栏与闭锁的关键区别在于,所有线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。栅栏用于实现一些协议。</p><h4 id="构建高效且可伸缩的结果缓存"><a href="#构建高效且可伸缩的结果缓存" class="headerlink" title="构建高效且可伸缩的结果缓存"></a>构建高效且可伸缩的结果缓存</h4><p>简单的缓存可能会将性能瓶颈转变成可伸缩性瓶颈,即使缓存是用于提升单线程的性能。</p><h4 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h4><p><strong>并发技巧</strong></p><ul><li><p> 可变状态是至关重要的。所有的并发问题都可以归结为如何协调对并发状态的访问。可边状态越少,就越容易确保线程安全性。</p></li><li><p> 尽量将域声明为final类型,除非需要它们是可变的。</p></li><li><p> 不可变对象一定是线程安全的。不可变对象能极大地降低并发编程的复杂性。它们更为简单而且安全,可以任意共享而无须使用加锁或保护性复制等机制。</p></li><li><p> 封装有助于管理复杂性。在编写线程安全程序时,虽然可以将所有数据都保存至全局变量中,但是为什么要这样做。将数据封装在对象中,更易于维持不变性条件:将同步机制封装在对象中,更易于遵循同步策略。</p></li><li><p> 用锁来保护每个可变变量</p></li><li><p> 当保护同一个不变性条件的所有变量时,要使用一个锁。</p></li><li><p> 在执行复合操作期间,要持有锁</p></li><li><p> 如果虫多个线程中访问同一个可变变量时没有同步机制,那么程序会出现问题。</p></li><li><p> 不要故作聪明地推断出不需要使用同步。</p></li><li><p> 在设计过程中考虑线程安全,或者在文档中明 确地指出它不是线程安全的。</p></li><li><p> 将同步策略文档化。</p></li></ul><h2 id="并发应用程序的构造理论"><a href="#并发应用程序的构造理论" class="headerlink" title="并发应用程序的构造理论"></a>并发应用程序的构造理论</h2><p>大多数并发应用程序都市围绕“任务执行”来构造的:任务通常是一些抽象的且离散的工作单元。通过把应用程序的工作分解到多个任务中,可以简化程序的组织结构,提供一种自然的十五边界来优化错误恢复过程,以及提供一种自然的并行工作结构来提升并发性。</p><h3 id="任务执行"><a href="#任务执行" class="headerlink" title="任务执行"></a>任务执行</h3><h4 id="在线程中执行任务"><a href="#在线程中执行任务" class="headerlink" title="在线程中执行任务"></a>在线程中执行任务</h4><h5 id="串行地执行任务"><a href="#串行地执行任务" class="headerlink" title="串行地执行任务"></a>串行地执行任务</h5><h5 id="显式地为任务创建线程"><a href="#显式地为任务创建线程" class="headerlink" title="显式地为任务创建线程"></a>显式地为任务创建线程</h5><h5 id="无限制创建线程的不足"><a href="#无限制创建线程的不足" class="headerlink" title="无限制创建线程的不足"></a>无限制创建线程的不足</h5><h4 id="Executor框架"><a href="#Executor框架" class="headerlink" title="Executor框架"></a><strong>Executor框架</strong></h4><h5 id="示例:基于Excutor的Web服务器"><a href="#示例:基于Excutor的Web服务器" class="headerlink" title="示例:基于Excutor的Web服务器"></a>示例:基于Excutor的Web服务器</h5><h5 id="执行策略"><a href="#执行策略" class="headerlink" title="执行策略"></a>执行策略</h5><h5 id="线程池"><a href="#线程池" class="headerlink" title="线程池"></a>线程池</h5><h5 id="Executor的生命周期"><a href="#Executor的生命周期" class="headerlink" title="Executor的生命周期"></a>Executor的生命周期</h5><h5 id="延迟任务与周期任务"><a href="#延迟任务与周期任务" class="headerlink" title="延迟任务与周期任务"></a>延迟任务与周期任务</h5><h4 id="找出可利用的并行性"><a href="#找出可利用的并行性" class="headerlink" title="找出可利用的并行性"></a>找出可利用的并行性</h4><h5 id="示例:串行的页面渲染器"><a href="#示例:串行的页面渲染器" class="headerlink" title="示例:串行的页面渲染器"></a>示例:串行的页面渲染器</h5><h5 id="携带结果的任务Callable与Future"><a href="#携带结果的任务Callable与Future" class="headerlink" title="携带结果的任务Callable与Future"></a>携带结果的任务Callable与Future</h5><h5 id="示例:使用Futuren实现页面渲染器"><a href="#示例:使用Futuren实现页面渲染器" class="headerlink" title="示例:使用Futuren实现页面渲染器"></a>示例:使用Futuren实现页面渲染器</h5><h5 id="在异构任务并行化中存在的局限"><a href="#在异构任务并行化中存在的局限" class="headerlink" title="在异构任务并行化中存在的局限"></a>在异构任务并行化中存在的局限</h5><h5 id="CompletionService-Executor与BlockingQueue"><a href="#CompletionService-Executor与BlockingQueue" class="headerlink" title="CompletionService:Executor与BlockingQueue"></a>CompletionService:Executor与BlockingQueue</h5><h5 id="示例:使用CompletionService实现页面渲染器"><a href="#示例:使用CompletionService实现页面渲染器" class="headerlink" title="示例:使用CompletionService实现页面渲染器"></a>示例:使用CompletionService实现页面渲染器</h5><h5 id="为任务设置时限"><a href="#为任务设置时限" class="headerlink" title="为任务设置时限"></a>为任务设置时限</h5><h5 id="示例:旅行预订门户网站"><a href="#示例:旅行预订门户网站" class="headerlink" title="示例:旅行预订门户网站"></a>示例:旅行预订门户网站</h5><h4 id="小结-1"><a href="#小结-1" class="headerlink" title="小结"></a>小结</h4><p>通过围绕任务执行设计应用程序,可以简化开发过程,并有助于实现并发。Executor框架将任务提交与执行策略解耦开来,同时还支持多种不同类型的执行策略。当需要创建线程来执行任务时,可以考虑使用Executor。要想在将应用程序分解为不同的任务时获得最大的好处,必须定义清晰的任务边界。某些应用程序中存在着比较明显的任务边界,而在其他一些程序中则需要进一步分析才能揭示出粒度更细的并行性。</p><h3 id="取消与关闭"><a href="#取消与关闭" class="headerlink" title="取消与关闭"></a>取消与关闭</h3><h4 id="任务取消"><a href="#任务取消" class="headerlink" title="任务取消"></a>任务取消</h4><h5 id="中断"><a href="#中断" class="headerlink" title="中断"></a>中断</h5><h5 id="中断策略"><a href="#中断策略" class="headerlink" title="中断策略"></a>中断策略</h5><h5 id="响应中断"><a href="#响应中断" class="headerlink" title="响应中断"></a>响应中断</h5><h5 id="示例:计时运行"><a href="#示例:计时运行" class="headerlink" title="示例:计时运行"></a>示例:计时运行</h5><h5 id="通过Future来实现取消"><a href="#通过Future来实现取消" class="headerlink" title="通过Future来实现取消"></a>通过Future来实现取消</h5><h5 id="处理不可中断的阻塞"><a href="#处理不可中断的阻塞" class="headerlink" title="处理不可中断的阻塞"></a>处理不可中断的阻塞</h5><h5 id="采用newTaskFor来封装非标准的取消"><a href="#采用newTaskFor来封装非标准的取消" class="headerlink" title="采用newTaskFor来封装非标准的取消"></a>采用newTaskFor来封装非标准的取消</h5><h4 id="停止基于线程的服务"><a href="#停止基于线程的服务" class="headerlink" title="停止基于线程的服务"></a>停止基于线程的服务</h4><h5 id="示例:日志服务"><a href="#示例:日志服务" class="headerlink" title="示例:日志服务"></a>示例:日志服务</h5><h5 id="关闭ExecutorService"><a href="#关闭ExecutorService" class="headerlink" title="关闭ExecutorService"></a>关闭ExecutorService</h5><h5 id="“毒丸”对象"><a href="#“毒丸”对象" class="headerlink" title="“毒丸”对象"></a>“毒丸”对象</h5><h5 id="示例-只执行一次的服务"><a href="#示例-只执行一次的服务" class="headerlink" title="示例:只执行一次的服务"></a>示例:只执行一次的服务</h5><h5 id="shutdownNow的局限性"><a href="#shutdownNow的局限性" class="headerlink" title="shutdownNow的局限性"></a>shutdownNow的局限性</h5><h4 id="处理非正常的线程终止"><a href="#处理非正常的线程终止" class="headerlink" title="处理非正常的线程终止"></a>处理非正常的线程终止</h4><h5 id="未捕获异常的处理"><a href="#未捕获异常的处理" class="headerlink" title="未捕获异常的处理"></a>未捕获异常的处理</h5><h4 id="JVM关闭"><a href="#JVM关闭" class="headerlink" title="JVM关闭"></a>JVM关闭</h4><h5 id="关闭钩子"><a href="#关闭钩子" class="headerlink" title="关闭钩子"></a>关闭钩子</h5><h5 id="守护线程"><a href="#守护线程" class="headerlink" title="守护线程"></a>守护线程</h5><h5 id="终结器"><a href="#终结器" class="headerlink" title="终结器"></a>终结器</h5><h4 id="小结-2"><a href="#小结-2" class="headerlink" title="小结"></a>小结</h4><p>在任务、线程、服务以及应用程序等模块中的生命周期结束问题,可能会增加它们在设计和实现时的复杂性。Java并没有提供某种抢占式的机制来取消或者总结线程。相反,它提供了一种协作式的中断机制来实现取消操作,但这要依赖于如何构建取消操作的协议,以及能否始终遵循这些协议。通过使用FutureTask和Executor框架,可以帮助我们构建可取消任务和服务。</p><h3 id="线程池的使用"><a href="#线程池的使用" class="headerlink" title="线程池的使用"></a>线程池的使用</h3><h4 id="在任务与执行策略之间的隐性耦合"><a href="#在任务与执行策略之间的隐性耦合" class="headerlink" title="在任务与执行策略之间的隐性耦合"></a>在任务与执行策略之间的隐性耦合</h4><h5 id="线程饥饿死锁"><a href="#线程饥饿死锁" class="headerlink" title="线程饥饿死锁"></a>线程饥饿死锁</h5><h5 id="运行时间较长的任务"><a href="#运行时间较长的任务" class="headerlink" title="运行时间较长的任务"></a>运行时间较长的任务</h5><h4 id="设置线程池的大小"><a href="#设置线程池的大小" class="headerlink" title="设置线程池的大小"></a>设置线程池的大小</h4><h4 id="配置TheadPoolExecutor"><a href="#配置TheadPoolExecutor" class="headerlink" title="配置TheadPoolExecutor"></a>配置TheadPoolExecutor</h4><h5 id="线程的创建与销毁"><a href="#线程的创建与销毁" class="headerlink" title="线程的创建与销毁"></a>线程的创建与销毁</h5><h5 id="管理队列任务"><a href="#管理队列任务" class="headerlink" title="管理队列任务"></a>管理队列任务</h5><h5 id="饱和策略"><a href="#饱和策略" class="headerlink" title="饱和策略"></a>饱和策略</h5><h5 id="线程工厂"><a href="#线程工厂" class="headerlink" title="线程工厂"></a>线程工厂</h5><h5 id="在调用构造函数后再定制ThreadPoolExecutork"><a href="#在调用构造函数后再定制ThreadPoolExecutork" class="headerlink" title="在调用构造函数后再定制ThreadPoolExecutork"></a>在调用构造函数后再定制ThreadPoolExecutork</h5><h4 id="扩展ThreadPoolExecutor"><a href="#扩展ThreadPoolExecutor" class="headerlink" title="扩展ThreadPoolExecutor"></a>扩展ThreadPoolExecutor</h4><h5 id="给线程池添加统计信息"><a href="#给线程池添加统计信息" class="headerlink" title="给线程池添加统计信息"></a>给线程池添加统计信息</h5><h5 id="递归算法的并行化"><a href="#递归算法的并行化" class="headerlink" title="递归算法的并行化"></a>递归算法的并行化</h5><h5 id="谜题框架"><a href="#谜题框架" class="headerlink" title="谜题框架"></a>谜题框架</h5><h4 id="小结-3"><a href="#小结-3" class="headerlink" title="小结"></a>小结</h4><p>对于并发执行的任务,Executor框架是一种强大且灵活的框架。它提供了大量可调节的选项,例如创建线程和关闭线程的策略,处理队列任务的策略,处理过多任务的策略,并且提供了几个钩子方法来扩展它的行为。然而,与大多数功能强大的框架一样,其中有些设置参数并不能很好地工作,某些类型地任务需要特定的执行策略,而一些参数组合则可能产生奇怪的结果。</p><h2 id="并发编程的性能调优"><a href="#并发编程的性能调优" class="headerlink" title="并发编程的性能调优"></a>并发编程的性能调优</h2><h3 id="避免活跃性危险"><a href="#避免活跃性危险" class="headerlink" title="避免活跃性危险"></a>避免活跃性危险</h3><p>安全性与活跃性之间通常存在着某种制衡。我们使用加锁机制来确保线程安全,但是如果过度地使用加锁,则可能导致锁顺序死锁。同样,我们使用线程池和信号量来限制对资源的使用,但这些被限制地行为可能会导致资源死锁。Java应用程序无法从死锁诉说中恢复过来,因此在设计时一定要排那些些导致死锁出现的条件。</p><h4 id="死锁"><a href="#死锁" class="headerlink" title="死锁"></a>死锁</h4><h5 id="锁顺序死锁"><a href="#锁顺序死锁" class="headerlink" title="锁顺序死锁"></a>锁顺序死锁</h5><h5 id="动态的锁顺序死锁"><a href="#动态的锁顺序死锁" class="headerlink" title="动态的锁顺序死锁"></a>动态的锁顺序死锁</h5><h5 id="在协作对象之间发生的死锁"><a href="#在协作对象之间发生的死锁" class="headerlink" title="在协作对象之间发生的死锁"></a>在协作对象之间发生的死锁</h5><h5 id="开放调用"><a href="#开放调用" class="headerlink" title="开放调用"></a>开放调用</h5><h5 id="资源死锁"><a href="#资源死锁" class="headerlink" title="资源死锁"></a>资源死锁</h5><h4 id="死锁的避免与诊断"><a href="#死锁的避免与诊断" class="headerlink" title="死锁的避免与诊断"></a>死锁的避免与诊断</h4><h5 id="支持定时的锁"><a href="#支持定时的锁" class="headerlink" title="支持定时的锁"></a>支持定时的锁</h5><h5 id="通过线程转储信息来分析死锁"><a href="#通过线程转储信息来分析死锁" class="headerlink" title="通过线程转储信息来分析死锁"></a>通过线程转储信息来分析死锁</h5><h4 id="其他活跃性危险"><a href="#其他活跃性危险" class="headerlink" title="其他活跃性危险"></a>其他活跃性危险</h4><h5 id="饥饿"><a href="#饥饿" class="headerlink" title="饥饿"></a>饥饿</h5><h5 id="糟糕的响应"><a href="#糟糕的响应" class="headerlink" title="糟糕的响应"></a>糟糕的响应</h5><h5 id="活锁"><a href="#活锁" class="headerlink" title="活锁"></a>活锁</h5><h4 id="小结-4"><a href="#小结-4" class="headerlink" title="小结"></a>小结</h4><p>活跃性故障是一个非常严重的问题,因为当出现活跃性故障时,除了中止应用程序之外没有其他任何机制可以帮助从这种故障时恢复过来。最常见的活跃性故障就是锁顺序死锁。在设计时应该避免产生锁顺序死锁:确保线程在获取多个锁时采用一致的顺序。最好的解决方法是在程序中始终使用开放调用。这将大大减少需要同时持有多个锁的地方,也更容易发现这些地方。</p><h3 id="性能与可伸缩性"><a href="#性能与可伸缩性" class="headerlink" title="性能与可伸缩性"></a>性能与可伸缩性</h3><p>线程的最主要目的是提高程序的运行性能。</p><h4 id="对性能的思考"><a href="#对性能的思考" class="headerlink" title="对性能的思考"></a>对性能的思考</h4><p>更有效地利用现有处理资源,以及在出现新的处理资源时使程序尽可能地利用这些新资源。</p><h5 id="性能与可伸缩性-1"><a href="#性能与可伸缩性-1" class="headerlink" title="性能与可伸缩性"></a>性能与可伸缩性</h5><p>可伸缩性:当增加计算资源时(例如CPU、内存、存储容量或I/O带宽),程序的吞吐量或者处理能力相应地增加。</p><h5 id="评估各种性能权衡因素"><a href="#评估各种性能权衡因素" class="headerlink" title="评估各种性能权衡因素"></a>评估各种性能权衡因素</h5><h4 id="Amdahl定律"><a href="#Amdahl定律" class="headerlink" title="Amdahl定律"></a>Amdahl定律</h4><p>在所有并发程序中都包含一些串行部分</p><h5 id="示例:在各种框架中隐藏的串行部分"><a href="#示例:在各种框架中隐藏的串行部分" class="headerlink" title="示例:在各种框架中隐藏的串行部分"></a>示例:在各种框架中隐藏的串行部分</h5><h5 id="Amdahl定律的应用"><a href="#Amdahl定律的应用" class="headerlink" title="Amdahl定律的应用"></a>Amdahl定律的应用</h5><h4 id="线程引入的开销"><a href="#线程引入的开销" class="headerlink" title="线程引入的开销"></a>线程引入的开销</h4><h5 id="上下文切换"><a href="#上下文切换" class="headerlink" title="上下文切换"></a>上下文切换</h5><h5 id="内存同步"><a href="#内存同步" class="headerlink" title="内存同步"></a>内存同步</h5><h5 id="阻塞"><a href="#阻塞" class="headerlink" title="阻塞"></a>阻塞</h5><h4 id="减少锁的竞争"><a href="#减少锁的竞争" class="headerlink" title="减少锁的竞争"></a>减少锁的竞争</h4><p>在并发程序中,对可伸缩性的最主要威胁就是独占方式的资源锁。减少锁的竞争能够提高性能和可伸缩性。</p><p>有两个因素将影响在锁上发生竞争的可能性:锁的请求频率,以及每次持有该锁的时间。</p><p>有三种方式可以降低锁的竞争程度:减少锁的持有时间;降低锁的请求频率;使用带有协调机制的独占锁,这些机制允许更高的并发性。</p><h5 id="缩小锁的范围"><a href="#缩小锁的范围" class="headerlink" title="缩小锁的范围"></a>缩小锁的范围</h5><h5 id="减少锁的粒度"><a href="#减少锁的粒度" class="headerlink" title="减少锁的粒度"></a>减少锁的粒度</h5><p>降低线程请求锁地频率(从而减少发生竞争的可能性)。这可以通过锁分段和锁分解等技术来实现。</p><p>对竞争适中的锁进行分解时,实际上是把这些锁转变未非竞争锁,从而有效地提高性能和可伸缩性。</p><h5 id="锁分段"><a href="#锁分段" class="headerlink" title="锁分段"></a>锁分段</h5><h5 id="避免热点域"><a href="#避免热点域" class="headerlink" title="避免热点域"></a>避免热点域</h5><h5 id="一些替代独占锁的方法"><a href="#一些替代独占锁的方法" class="headerlink" title="一些替代独占锁的方法"></a>一些替代独占锁的方法</h5><p>并发容器、读写锁、不可变对象以及原子变量</p><h5 id="检测CPU利用率"><a href="#检测CPU利用率" class="headerlink" title="检测CPU利用率"></a>检测CPU利用率</h5><h5 id="向对象池说“不”"><a href="#向对象池说“不”" class="headerlink" title="向对象池说“不”"></a>向对象池说“不”</h5><h4 id="示例:比较Map的性能"><a href="#示例:比较Map的性能" class="headerlink" title="示例:比较Map的性能"></a>示例:比较Map的性能</h4><h5 id="减少上下文切换的开销"><a href="#减少上下文切换的开销" class="headerlink" title="减少上下文切换的开销"></a>减少上下文切换的开销</h5><h4 id="小结-5"><a href="#小结-5" class="headerlink" title="小结"></a>小结</h4><p>由于使用线程常常是为了充分利用多个处理器的计算能力,因此在并发程序性能的讨论中,通常更多地将侧重点放在吞吐量和可伸缩性上,而不是服务时间。Amdahl定律告诉我们,程序的可伸缩性取决于在所有代码中必须被串行执行的代码的比例。因为Java程序中串行操作的主要来源是独占方式的资源锁,因此通常可以通过一i啊方式来提升可伸缩性:减少锁的持有时间,降低锁的粒度,以及采用非独占的锁或非阻塞锁来代替独占锁。</p><h4 id="小结-6"><a href="#小结-6" class="headerlink" title="小结"></a>小结</h4><p>由于使用线程常常是为了充分利用多个处理器的计算能力,因此在并发程序性能的讨论中,通常更多地将侧重店放在吞吐量和可伸缩性上,而不是服务时间。Amdahl定律告诉我们,程序地可伸缩性取决于在所有代码中必须被串行执行地代码比例。因为Java程序中串行操作地主要来源是独占方式地资源锁,因此通常可以通过以下方式来提升可伸缩性:减少锁的持有时间,降低锁的粒度,以及采用非独占的锁或非阻塞锁来代替独占锁。</p><h3 id="并发程序的测试"><a href="#并发程序的测试" class="headerlink" title="并发程序的测试"></a>并发程序的测试</h3><h2 id="并发编程的高级主题"><a href="#并发编程的高级主题" class="headerlink" title="并发编程的高级主题"></a>并发编程的高级主题</h2><h4 id="Lock和ReentrantLock"><a href="#Lock和ReentrantLock" class="headerlink" title="Lock和ReentrantLock"></a>Lock和ReentrantLock</h4><h5 id="轮询锁与定时锁"><a href="#轮询锁与定时锁" class="headerlink" title="轮询锁与定时锁"></a>轮询锁与定时锁</h5><h5 id="可中断的锁获取操作"><a href="#可中断的锁获取操作" class="headerlink" title="可中断的锁获取操作"></a>可中断的锁获取操作</h5><h5 id="非块结构的加锁"><a href="#非块结构的加锁" class="headerlink" title="非块结构的加锁"></a>非块结构的加锁</h5><h4 id="性能考虑因素"><a href="#性能考虑因素" class="headerlink" title="性能考虑因素"></a>性能考虑因素</h4><h4 id="公平性"><a href="#公平性" class="headerlink" title="公平性"></a>公平性</h4><h5 id="在synchronized和ReentrantLock之间进行选择"><a href="#在synchronized和ReentrantLock之间进行选择" class="headerlink" title="在synchronized和ReentrantLock之间进行选择"></a>在synchronized和ReentrantLock之间进行选择</h5><p>在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用ReentranLock,这些功包括:可定时的、可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是应该优先使用synchronized。</p><h4 id="读-写锁"><a href="#读-写锁" class="headerlink" title="读-写锁"></a>读-写锁</h4><h4 id="小结-7"><a href="#小结-7" class="headerlink" title="小结"></a>小结</h4><p>与内置锁相比,显示的Lock提供了一些扩展功能,在处理锁的不可用性方面有着更高的灵活性,并且对队列有着更好的额控制。但ReentranLock不能完全替代synchronized,只有在synchronized无法忙着需求时,才应该使用它。</p><p>读-写锁允许多个读线程并发地访问被保护地对象,当范围以读取操作为主地数据结构时,它能提高程序地可伸缩性。</p><h3 id="构建自定义的同步工具"><a href="#构建自定义的同步工具" class="headerlink" title="构建自定义的同步工具"></a>构建自定义的同步工具</h3><h3 id="原子变量与非阻塞同步机制"><a href="#原子变量与非阻塞同步机制" class="headerlink" title="原子变量与非阻塞同步机制"></a>原子变量与非阻塞同步机制</h3><p>非阻塞算法在可伸缩性和活跃性上拥有巨大的优势。由于非主赛算法可以使读个线程咋竞争相同的数据时不会发生阻塞,因此他们在粒度更细的层次上进行协调,并且极大地减少调度开销。而且,在非阻塞算法中不存在死锁和其他活跃性问题</p><h4 id="锁的劣势"><a href="#锁的劣势" class="headerlink" title="锁的劣势"></a>锁的劣势</h4><h4 id="硬件对并发的支持"><a href="#硬件对并发的支持" class="headerlink" title="硬件对并发的支持"></a>硬件对并发的支持</h4><h5 id="比较并交换"><a href="#比较并交换" class="headerlink" title="比较并交换"></a>比较并交换</h5><h5 id="非阻塞的计数器"><a href="#非阻塞的计数器" class="headerlink" title="非阻塞的计数器"></a>非阻塞的计数器</h5><h5 id="JVM对CAS的支持"><a href="#JVM对CAS的支持" class="headerlink" title="JVM对CAS的支持"></a>JVM对CAS的支持</h5><h4 id="原子变量类"><a href="#原子变量类" class="headerlink" title="原子变量类"></a>原子变量类</h4><h5 id="原子变量是一种“更好的volatile”"><a href="#原子变量是一种“更好的volatile”" class="headerlink" title="原子变量是一种“更好的volatile”"></a>原子变量是一种“更好的volatile”</h5><h5 id="性能比较:锁与原子变量"><a href="#性能比较:锁与原子变量" class="headerlink" title="性能比较:锁与原子变量"></a>性能比较:锁与原子变量</h5><h4 id="非阻塞算法"><a href="#非阻塞算法" class="headerlink" title="非阻塞算法"></a>非阻塞算法</h4><p>如果在某种算法中,一个线程的失败或挂起不会导致其他线程也失败或挂起,那么这种算法就被称为非阻塞算法。</p><h5 id="非阻塞的栈"><a href="#非阻塞的栈" class="headerlink" title="非阻塞的栈"></a>非阻塞的栈</h5><h5 id="非阻塞的链表"><a href="#非阻塞的链表" class="headerlink" title="非阻塞的链表"></a>非阻塞的链表</h5><h5 id="原子的域更新器"><a href="#原子的域更新器" class="headerlink" title="原子的域更新器"></a>原子的域更新器</h5><h5 id="ABA问题"><a href="#ABA问题" class="headerlink" title="ABA问题"></a>ABA问题</h5><h4 id="小结-8"><a href="#小结-8" class="headerlink" title="小结"></a>小结</h4><p>非阻塞算法通过底层的并发原语(例如比较交换而不是锁)来维持线程的安全性。这些底层的原语通过原子变量类向外公开,这些类也用作一种“更好的volatile”,从而为整数和对象引用提供原子的更新操作。</p><p>非阻塞算法在设计和实现时非常困难,但通常能够提供更高的可伸缩性,并能更好地防止活跃性故障的发生。在JVM从一个版本升级到下一个版本的过程中,并发性能的主要提升都来自于(在JVM内部以及平台类库中)对非阻塞算法的使用。</p><h3 id="Java内存模型"><a href="#Java内存模型" class="headerlink" title="Java内存模型"></a>Java内存模型</h3><h4 id="什么是内存模型,为什么需要它"><a href="#什么是内存模型,为什么需要它" class="headerlink" title="什么是内存模型,为什么需要它"></a>什么是内存模型,为什么需要它</h4><h5 id="平台的内存模型"><a href="#平台的内存模型" class="headerlink" title="平台的内存模型"></a>平台的内存模型</h5><h5 id="重排序"><a href="#重排序" class="headerlink" title="重排序"></a>重排序</h5><h5 id="Java内存模型简介"><a href="#Java内存模型简介" class="headerlink" title="Java内存模型简介"></a>Java内存模型简介</h5><h5 id="借助同步"><a href="#借助同步" class="headerlink" title="借助同步"></a>借助同步</h5><h4 id="发布"><a href="#发布" class="headerlink" title="发布"></a>发布</h4><h5 id="不安全的发布"><a href="#不安全的发布" class="headerlink" title="不安全的发布"></a>不安全的发布</h5><h5 id="安全的发布"><a href="#安全的发布" class="headerlink" title="安全的发布"></a>安全的发布</h5><h5 id="安全初始化模式"><a href="#安全初始化模式" class="headerlink" title="安全初始化模式"></a>安全初始化模式</h5><h5 id="双重检测加锁"><a href="#双重检测加锁" class="headerlink" title="双重检测加锁"></a>双重检测加锁</h5><h4 id="初始化过程中的安全性"><a href="#初始化过程中的安全性" class="headerlink" title="初始化过程中的安全性"></a>初始化过程中的安全性</h4><h4 id="小结-9"><a href="#小结-9" class="headerlink" title="小结"></a>小结</h4><p>Java内存模型说明了某个线程的内存操作在哪些情况下对于其他线程是可见的。其中包括确保这些操作是按照一种Happens-before的偏序关系进行排序,而这种关系是基于内存操作和同步操作等级别来定义的。如果缺少重组的同步,那么当相册给访问共享数据时,会发生一些非常器官的额问题。然而,如果使用第2章与第3章介绍的更高级规则,例如@GuardedBy和安全发布,那么即使不考虑Hapens-Before的底层细节,也能确保线程安全性。</p><h2 id="线程安全类"><a href="#线程安全类" class="headerlink" title="线程安全类"></a>线程安全类</h2><p>Vector</p><p>其他同步集合类</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p>《Java 并发编程实践》机械工业出版社 2012.2</p>]]></content>
</entry>
<entry>
<title>sites-to-fork</title>
<link href="/2018.html"/>
<url>/2018.html</url>
<content type="html"><![CDATA[<h1 id="技术博客"><a href="#技术博客" class="headerlink" title="技术博客"></a>技术博客</h1><p><a href="https://haldir65.github.io/tags/linux/" target="_blank" rel="noopener">Haldir</a>:前端|开发|系统|工具</p><p><a href="https://tech.meituan.com/" target="_blank" rel="noopener">美团技术团队</a></p><p><a href="http://alibaba-middleware.mysxl.cn/" target="_blank" rel="noopener">阿里中间件团队</a></p><p><a href="http://gityuan.com/" target="_blank" rel="noopener">Gityuan</a>:Android相关的网址资料</p><p><a href="https://www.cnblogs.com/renpingsheng/p/7851141.html" target="_blank" rel="noopener">renpingsheng</a>: Redis详解</p><p><a href="https://honeypps.com/" target="_blank" rel="noopener">朱小斯</a></p><p><a href="http://www.tianxiaobo.com/" target="_blank" rel="noopener">田小波</a></p><p><a href="http://www.liangsonghua.com/" target="_blank" rel="noopener">梁松华-京东高工</a></p><p><a href="https://newsn.net/about.html" target="_blank" rel="noopener">苏南-electron</a>:electron、php</p><p><a href="https://smilenicky.blog.csdn.net/" target="_blank" rel="noopener">nicky’s blog</a>:CSDN博客(java 框架、sql调优)</p><p><a href="https://cshihong.github.io/" target="_blank" rel="noopener">曹世宏</a>:有关网络协议的总结的蛮好的</p><h1 id="工具"><a href="#工具" class="headerlink" title="工具"></a>工具</h1><p><a href="https://www.cs.usfca.edu/~galles/visualization/Algorithms.html" target="_blank" rel="noopener">数据结构可视化学习</a></p>]]></content>
</entry>
<entry>
<title>hexo文章插图显示问题</title>
<link href="/17877.html"/>
<url>/17877.html</url>
<content type="html"><![CDATA[<h2 id="方法一-GitHub路径"><a href="#方法一-GitHub路径" class="headerlink" title="方法一 GitHub路径"></a>方法一 GitHub路径</h2><p> (1)在Hexo下的.config.yml中设置post_asset_folder选项为true</p><p> (2)在git bash命令窗口下 使用hexo n “你的待插入图片的新建文章名称” 来生成新的md文件,在这个新建文件的同级目录下,会产生同名的文件夹。</p><p> (3)需要在新建md文件插入图片时,先把图片复制到(2)中的同名文件夹中</p><p> (4)按照花括号中的格式来插入图片<code>![图片描述](待插入图片github中的路径)</code>。若没有路径,可先在git bash 上使用hexo相关命令提交文章来产生。例如,我上传文章的图片的路径为{<code>![JVM 虚拟机运行时数据区](/17374/JVM.png)</code>},如下图:</p><p> <img src="/7663/github_path.png" alt="图片在github中的路径"></p><p> <img src="/7663/Hexo_pic.png" alt="图片的在代码中显示的路径"></p><p> 实际上,图片通过这种方法能正常显示是因为,图片通过这种上传方式已经存在于github上,再通过这种引用图片的格式<code>![图片描述](待插入图片github中的路径)</code>,来显示图片。</p><h2 id="方法二-图床"><a href="#方法二-图床" class="headerlink" title="方法二 图床"></a>方法二 图床</h2><p> 使用图床获取url链接来显示图片</p><p> 参考方法:<a href="https://www.dazhuanlan.com/2019/09/26/5d8cbd5d55654/" target="_blank" rel="noopener">https://www.dazhuanlan.com/2019/09/26/5d8cbd5d55654/</a></p>]]></content>
</entry>
<entry>
<title>software</title>
<link href="/7119.html"/>
<url>/7119.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title>Java-tuning</title>
<link href="/59252.html"/>
<url>/59252.html</url>
<content type="html"><![CDATA[<h2 id="Coding"><a href="#Coding" class="headerlink" title="Coding"></a>Coding</h2><h2 id="Database"><a href="#Database" class="headerlink" title="Database"></a>Database</h2><h2 id="JVM"><a href="#JVM" class="headerlink" title="JVM"></a>JVM</h2>]]></content>
</entry>
<entry>
<title>Nginx</title>
<link href="/9355.html"/>
<url>/9355.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title>Docker</title>
<link href="/40991.html"/>
<url>/40991.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title>config</title>
<link href="/55118.html"/>
<url>/55118.html</url>
<content type="html"><![CDATA[<h2 id="服务配置Config"><a href="#服务配置Config" class="headerlink" title="服务配置Config"></a>服务配置Config</h2>]]></content>
</entry>
<entry>
<title>Sleuth</title>
<link href="/37557.html"/>
<url>/37557.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title>zuul</title>
<link href="/32558.html"/>
<url>/32558.html</url>
<content type="html"><![CDATA[<h2 id="路由器和过滤器"><a href="#路由器和过滤器" class="headerlink" title="路由器和过滤器"></a>路由器和过滤器</h2><h2 id="源码分析"><a href="#源码分析" class="headerlink" title="源码分析"></a>源码分析</h2>]]></content>
</entry>
<entry>
<title>Hystrix</title>
<link href="/65363.html"/>
<url>/65363.html</url>
<content type="html"><![CDATA[<h2 id="断路器Hystrix"><a href="#断路器Hystrix" class="headerlink" title="断路器Hystrix"></a>断路器Hystrix</h2><h2 id="源码分析"><a href="#源码分析" class="headerlink" title="源码分析"></a>源码分析</h2>]]></content>
</entry>
<entry>
<title>Ribbon_Feign</title>
<link href="/20179.html"/>
<url>/20179.html</url>
<content type="html"><![CDATA[<h2 id="Ribbon-客户端负载平衡器"><a href="#Ribbon-客户端负载平衡器" class="headerlink" title="Ribbon 客户端负载平衡器"></a>Ribbon 客户端负载平衡器</h2><h2 id="Feign-声明性REST客户端"><a href="#Feign-声明性REST客户端" class="headerlink" title="Feign 声明性REST客户端"></a>Feign 声明性REST客户端</h2><h2 id="源码分析"><a href="#源码分析" class="headerlink" title="源码分析"></a>源码分析</h2><h3 id="Ribbon"><a href="#Ribbon" class="headerlink" title="Ribbon"></a>Ribbon</h3><h3 id="Feign"><a href="#Feign" class="headerlink" title="Feign"></a>Feign</h3>]]></content>
</entry>
<entry>
<title>Eureka</title>
<link href="/57702.html"/>
<url>/57702.html</url>
<content type="html"><![CDATA[<h2 id="总览"><a href="#总览" class="headerlink" title="总览"></a>总览</h2><p>Eureka服务发现与注册</p><h2 id="Eureka-Server"><a href="#Eureka-Server" class="headerlink" title="Eureka Server"></a>Eureka Server</h2><h2 id="Eureka-client"><a href="#Eureka-client" class="headerlink" title="Eureka client"></a>Eureka client</h2><h2 id="源码分析"><a href="#源码分析" class="headerlink" title="源码分析"></a>源码分析</h2><h3 id="服务端"><a href="#服务端" class="headerlink" title="服务端"></a>服务端</h3><h3 id="客户端"><a href="#客户端" class="headerlink" title="客户端"></a>客户端</h3><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p>[1] <a href="https://juejin.im/post/6844904001444511758#heading-2" target="_blank" rel="noopener">深入浅出 Spring Cloud 之 Eureka</a></p><p>[2] <a href="https://cloud.spring.io/spring-cloud-netflix/reference/html/" target="_blank" rel="noopener">Eureka 官方文档</a></p>]]></content>
</entry>
<entry>
<title>Spring-cloud</title>
<link href="/38742.html"/>
<url>/38742.html</url>
<content type="html"><![CDATA[<h2 id="微服务技术栈"><a href="#微服务技术栈" class="headerlink" title="微服务技术栈"></a>微服务技术栈</h2><p><img src="/38742/Spring_cloud.png" alt=""></p><p><img src="/38742/Spring_cloud1.png" alt=""></p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p>[1] <a href="http://springcloud.cn/" target="_blank" rel="noopener">Spring Cloud中国社区</a></p><p>[2] <a href="https://www.springcloud.cc/spring-cloud-dalston.html#_features" target="_blank" rel="noopener">与Spring-cloud搭配的组件</a></p>]]></content>
</entry>
<entry>
<title>Mongo-DB</title>
<link href="/50521.html"/>
<url>/50521.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title>sharding-sphere</title>
<link href="/9893.html"/>
<url>/9893.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title>Kafka</title>
<link href="/13749.html"/>
<url>/13749.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title>RocketMQ</title>
<link href="/50077.html"/>
<url>/50077.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title>Zookeeper</title>
<link href="/31915.html"/>
<url>/31915.html</url>
<content type="html"><![CDATA[<h2 id="ZK"><a href="#ZK" class="headerlink" title="ZK"></a>ZK</h2><p><a href="https://zhuanlan.zhihu.com/p/59669985" target="_blank" rel="noopener">zookeeper应用场景</a></p><h2 id="源码分析"><a href="#源码分析" class="headerlink" title="源码分析"></a>源码分析</h2><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p>[1] <a href="https://zookeeper.apache.org/" target="_blank" rel="noopener">Zookeeper官网</a></p><p>[2] <a href="https://github.com/apache/zookeeper">Zookeeper源码</a></p>]]></content>
</entry>
<entry>
<title>工作效率</title>
<link href="/30749.html"/>
<url>/30749.html</url>
<content type="html"><![CDATA[<p><a href="https://book.douban.com/subject/24868904/" target="_blank" rel="noopener">高效能程序员的修炼</a></p><p><a href="https://book.douban.com/subject/26657426/" target="_blank" rel="noopener">高效能人士的7个习惯</a></p>]]></content>
</entry>
<entry>
<title>系统设计</title>
<link href="/20341.html"/>
<url>/20341.html</url>
<content type="html"><![CDATA[<h2 id="单体web"><a href="#单体web" class="headerlink" title="单体web"></a>单体web</h2><p>SSH:Spring+SpringMVC+Hibernate</p><p>SSM:Spring+SpringMVC+MyBatis</p><h2 id="分布式(垂直)"><a href="#分布式(垂直)" class="headerlink" title="分布式(垂直)"></a>分布式(垂直)</h2><h2 id="SOA"><a href="#SOA" class="headerlink" title="SOA"></a>SOA</h2><h2 id="微服务"><a href="#微服务" class="headerlink" title="微服务"></a>微服务</h2><h3 id="案例"><a href="#案例" class="headerlink" title="案例"></a>案例</h3><p><a href="https://znyj.ofweek.com/news/2019-11/ART-12300-8500-30418050.html" target="_blank" rel="noopener">DMP系统设计</a></p><p>参考</p><p>[1] <a href="https://zhuanlan.zhihu.com/p/98392801" target="_blank" rel="noopener">软件架构的演变-单体架构,垂直架构,分布式架构,SOA架构和微服务架构的演变历程</a></p><p>[2] <a href="https://www.cnblogs.com/ZanderZhao/p/12033949.html" target="_blank" rel="noopener">软件体系结构</a></p><p>[3] <a href="https://book.douban.com/subject/5406042/" target="_blank" rel="noopener">The Design of Design: Essays from a Computer Scientist</a></p><p>[4] <a href="https://book.douban.com/subject/25741382/" target="_blank" rel="noopener">面向模式的软件架构 第1卷</a></p><p>[5] <a href="https://book.douban.com/subject/21624776/" target="_blank" rel="noopener">分布式系统</a></p><p>[6] <a href="https://www.zhihu.com/question/23645117" target="_blank" rel="noopener">学习分布式系统需要怎样的知识?</a></p><p>[7] <a href="https://www.cnblogs.com/xybaby/p/7787034.html" target="_blank" rel="noopener">什么是分布式系统,如何学习分布式系统</a></p>]]></content>
</entry>
<entry>
<title>Cache</title>
<link href="/50757.html"/>
<url>/50757.html</url>
<content type="html"><![CDATA[<p><a href="https://tech.meituan.com/2017/03/17/cache-about.html" target="_blank" rel="noopener">缓存那些事</a></p>]]></content>
</entry>
<entry>
<title>面经</title>
<link href="/65115.html"/>
<url>/65115.html</url>
<content type="html"><![CDATA[<h2 id="面经"><a href="#面经" class="headerlink" title="面经"></a>面经</h2><h3 id="美团"><a href="#美团" class="headerlink" title="美团"></a>美团</h3><p><a href="https://bbs.huaweicloud.com/blogs/201277" target="_blank" rel="noopener">美团点评面经</a></p>]]></content>
</entry>
<entry>
<title>ES</title>
<link href="/44402.html"/>
<url>/44402.html</url>
<content type="html"><![CDATA[<h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><p><a href="https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.html" target="_blank" rel="noopener">ElasticSearch权威指南</a></p><h2 id="ES常见问题"><a href="#ES常见问题" class="headerlink" title="ES常见问题"></a>ES常见问题</h2><p><a href="https://juejin.im/post/6844903914047815693" target="_blank" rel="noopener">ES和Lucene的关系</a> </p><p><a href="https://www.cnblogs.com/LBSer/p/4068864.html" target="_blank" rel="noopener">Lucene的空间压缩</a></p>]]></content>
</entry>
<entry>
<title>离散数学</title>
<link href="/46708.html"/>
<url>/46708.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title>计算机体系结构</title>
<link href="/60393.html"/>
<url>/60393.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title>middleware</title>
<link href="/36505.html"/>
<url>/36505.html</url>
<content type="html"><![CDATA[<h1 id="什么叫做“中间件”"><a href="#什么叫做“中间件”" class="headerlink" title="什么叫做“中间件”"></a>什么叫做“中间件”</h1><p><a href="https://www.coderxing.com/what-is-middleware-for-architecture.html" target="_blank" rel="noopener">参考1</a></p><p><a href="https://developer.aliyun.com/article/62776" target="_blank" rel="noopener">基于中间件/构件的开发</a></p>]]></content>
</entry>
<entry>
<title>关于编程中的i字符与编码</title>
<link href="/19124.html"/>
<url>/19124.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title>Spring Boot</title>
<link href="/10937.html"/>
<url>/10937.html</url>
<content type="html"><![CDATA[<h3 id="Spring-Boot-便于开发微服务web应用"><a href="#Spring-Boot-便于开发微服务web应用" class="headerlink" title="Spring Boot 便于开发微服务web应用"></a>Spring Boot 便于开发微服务web应用</h3><h3 id="使用Spring-Boot快速开始一个简单的mvc项目(hello-word)"><a href="#使用Spring-Boot快速开始一个简单的mvc项目(hello-word)" class="headerlink" title="使用Spring Boot快速开始一个简单的mvc项目(hello word)"></a>使用Spring Boot快速开始一个简单的mvc项目(hello word)</h3><h4 id="基于maven"><a href="#基于maven" class="headerlink" title="基于maven"></a>基于maven</h4><h3 id="多版本控制"><a href="#多版本控制" class="headerlink" title="多版本控制"></a>多版本控制</h3><h3 id="SpringBoot项目打包"><a href="#SpringBoot项目打包" class="headerlink" title="SpringBoot项目打包"></a>SpringBoot项目打包</h3><h3 id="静态资源处理"><a href="#静态资源处理" class="headerlink" title="静态资源处理"></a>静态资源处理</h3><h3 id="扩展Springmvc配置"><a href="#扩展Springmvc配置" class="headerlink" title="扩展Springmvc配置"></a>扩展Springmvc配置</h3><h4 id="增加拦截器"><a href="#增加拦截器" class="headerlink" title="增加拦截器"></a>增加拦截器</h4><h4 id="增加过滤器"><a href="#增加过滤器" class="headerlink" title="增加过滤器"></a>增加过滤器</h4><h4 id="增加一个servlet"><a href="#增加一个servlet" class="headerlink" title="增加一个servlet"></a>增加一个servlet</h4><h2 id="自动装配原理"><a href="#自动装配原理" class="headerlink" title="自动装配原理"></a>自动装配原理</h2><h2 id="SpringBoot-启动原理"><a href="#SpringBoot-启动原理" class="headerlink" title="SpringBoot 启动原理"></a>SpringBoot 启动原理</h2><h3 id="Spring-boot错误处理机制原理以及自定义错误处理"><a href="#Spring-boot错误处理机制原理以及自定义错误处理" class="headerlink" title="Spring-boot错误处理机制原理以及自定义错误处理"></a>Spring-boot错误处理机制原理以及自定义错误处理</h3><h1 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h1><p><a href="https://www.jianshu.com/p/ffe5ebe17c3a" target="_blank" rel="noopener">Spring 和Spring boot的关系</a></p><p>参考</p><p>[1] <a href="https://spring.io/projects/spring-boot#overview" target="_blank" rel="noopener">Spring Boot官网</a></p><p>[2] <a href="https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-documentation" target="_blank" rel="noopener">Spring Boot Documentation</a></p>]]></content>
</entry>
<entry>
<title>Servlet</title>
<link href="/55715.html"/>
<url>/55715.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title>笔试题</title>
<link href="/53879.html"/>
<url>/53879.html</url>
<content type="html"><![CDATA[<h1 id="算法与数据结构"><a href="#算法与数据结构" class="headerlink" title="算法与数据结构"></a>算法与数据结构</h1><h2 id="笔试题"><a href="#笔试题" class="headerlink" title="笔试题"></a>笔试题</h2><h2 id="面试题"><a href="#面试题" class="headerlink" title="面试题"></a>面试题</h2><h1 id="计算机网络"><a href="#计算机网络" class="headerlink" title="计算机网络"></a>计算机网络</h1><h2 id="笔试题-1"><a href="#笔试题-1" class="headerlink" title="笔试题"></a>笔试题</h2><h2 id="面试题-1"><a href="#面试题-1" class="headerlink" title="面试题"></a>面试题</h2><p>TCP/IP协议族</p><p>http协议(应用层协议)</p><p><a href="https://zhuanlan.zhihu.com/p/60305452" target="_blank" rel="noopener">常见面试题1</a></p><p><a href="https://bbs.huaweicloud.com/blogs/224939" target="_blank" rel="noopener">TCP 为什么三次握手而不是两次握手</a></p><p><a href="https://segmentfault.com/a/1190000020610336" target="_blank" rel="noopener">为何是四次挥手</a></p><h1 id="操作系统"><a href="#操作系统" class="headerlink" title="操作系统"></a>操作系统</h1><h2 id="笔试题-2"><a href="#笔试题-2" class="headerlink" title="笔试题"></a>笔试题</h2><h2 id="面试题-2"><a href="#面试题-2" class="headerlink" title="面试题"></a>面试题</h2><h1 id="Linux系统"><a href="#Linux系统" class="headerlink" title="Linux系统"></a>Linux系统</h1><h2 id="笔试题-3"><a href="#笔试题-3" class="headerlink" title="笔试题"></a>笔试题</h2><h2 id="面试题-3"><a href="#面试题-3" class="headerlink" title="面试题"></a>面试题</h2><h1 id="数据库"><a href="#数据库" class="headerlink" title="数据库"></a>数据库</h1><h2 id="笔试题-4"><a href="#笔试题-4" class="headerlink" title="笔试题"></a>笔试题</h2><h2 id="面试题-4"><a href="#面试题-4" class="headerlink" title="面试题"></a>面试题</h2><h3 id="写sql语句"><a href="#写sql语句" class="headerlink" title="写sql语句"></a>写sql语句</h3><p> <a href="https://www.cnblogs.com/rainman/archive/2013/05/01/3053703.html" target="_blank" rel="noopener">order by的用法</a></p><p> <a href="https://mp.weixin.qq.com/s?__biz=MzI4Njc5NjM1NQ==&mid=2247483693&idx=1&sn=9fa301b0076778cd854a924e96cc356e&chksm=ebd63e01dca1b71745dca1f7e1c2aa2b7c80a393185db690b4fdfba22bb10ca87ea2cd6fa774&scene=21#wechat_redirect" target="_blank" rel="noopener">sql练习题1</a></p><p> <a href="https://mp.weixin.qq.com/s?__biz=MzI4Njc5NjM1NQ==&mid=2247483696&idx=1&sn=5f472ce7720aede89e2e15ea64bed1bc&chksm=ebd63e1cdca1b70ad18dec268c9903b2cbe11f9ce7b0633980c78a28bd5b1b57c4efbe7a3411&scene=21#wechat_redirect" target="_blank" rel="noopener">sql练习题2</a></p><h3 id="事物传播机制"><a href="#事物传播机制" class="headerlink" title="事物传播机制"></a>事物传播机制</h3><p> <a href="https://zhuanlan.zhihu.com/p/148504094" target="_blank" rel="noopener">七种传播类型</a></p><h3 id="ORM框架"><a href="#ORM框架" class="headerlink" title="ORM框架"></a>ORM框架</h3><p> <a href="https://cloud.tencent.com/developer/article/1478393" target="_blank" rel="noopener">MyBatis和Hibernate的区别</a></p><p> MyBatis</p><p> Hibernate</p><p> JPA</p><p> </p><h1 id="Java语言"><a href="#Java语言" class="headerlink" title="Java语言"></a>Java语言</h1><h2 id="基础特性"><a href="#基础特性" class="headerlink" title="基础特性"></a>基础特性</h2><h3 id="笔试题-5"><a href="#笔试题-5" class="headerlink" title="笔试题"></a>笔试题</h3><p>构造函数:构造函数和继承联系在一起;</p><p>封装</p><p> 修改属性的可见性</p><p>继承</p><p>多态</p><p> 多态的实现方式:重写;接口;抽象类和抽象方法;</p><p>内部类</p><p>函数修饰符:访问权限;static;</p><p>基本数据类型:范围;<a href="https://blog.csdn.net/ToughCY/article/details/108585013" target="_blank" rel="noopener">自动类型转换与ASSIC码表</a>;</p><p><a href="https://blog.csdn.net/Coding_Zhu/article/details/53096178" target="_blank" rel="noopener">基本数据类型与引用数据类型</a></p><p>操作符:<a href="https://www.cnblogs.com/yif0118/p/10425013.html" target="_blank" rel="noopener">自增自减</a>;运算符优先级;左移位、右移位;</p><p><a href="https://www.cnblogs.com/dolphin0520/p/3780005.html" target="_blank" rel="noopener">装箱和拆箱</a>;<a href="https://zhuanlan.zhihu.com/p/64004059" target="_blank" rel="noopener">装箱和拆箱参考2-要注意的问题</a></p><p><a href="https://leer.moe/2018/12/14/java-fun-object-method/" target="_blank" rel="noopener">Object的11个方法(3+2+5+1)</a></p><p><a href="https://zhuanlan.zhihu.com/p/58337357" target="_blank" rel="noopener">hashCode和equals</a></p><p><a href="https://www.cnblogs.com/qianguyihao/p/3929585.html" target="_blank" rel="noopener">equal和==的区别</a></p><p><a href="https://juejin.cn/post/6844903913116680199" target="_blank" rel="noopener">String.intern方法</a></p><p><a href="https://www.jianshu.com/p/8c724dd28fa4" target="_blank" rel="noopener">StringBuffer和StringBuilder的区别</a></p><p><a href="https://somelou.xyz/p/3958123573/" target="_blank" rel="noopener">ArrayList和linkedList的区别</a></p><p><a href="https://juejin.cn/post/6844903925460500487#heading-6" target="_blank" rel="noopener">HashMap和HashTable的区别</a></p><p><a href="https://juejin.cn/post/6844903555149594638" target="_blank" rel="noopener">Hashmap不同版本详解(主要为1.7与1.8之间)</a></p><p><a href="https://www.jianshu.com/p/1197e4717194" target="_blank" rel="noopener">ConcurrentHashMap 和 HashMap 对比介绍,1.7 和 1.8 版本区别</a></p><p><a href="https://juejin.cn/post/6844903924911046663" target="_blank" rel="noopener">Java并发容器</a></p><p><a href="https://juejin.cn/post/6844903665241686029" target="_blank" rel="noopener">Java的四种引用,强弱软虚</a></p><p><a href="https://www.jianshu.com/p/68ddb5484ca2" target="_blank" rel="noopener">什么是多态,实现多态的机制是什么?</a></p><p>Java创建对象有几种方式?</p><p>有没有可能两个不相等的对象有相同的hashcode</p><p>final 修饰符</p><p>static 修饰符</p><p>异常:异常分类;常见异常类型;</p><p><a href="https://blog.csdn.net/ThinkWon/article/details/104588551" target="_blank" rel="noopener">集合框架</a></p><p> 线程安全性</p><p>Object类</p><p> 深拷贝与浅拷贝</p><p><a href="https://www.cnblogs.com/Qian123/p/5715402.html#_label1" target="_blank" rel="noopener">异常</a></p><p>泛型</p><p><a href="https://www.cnblogs.com/happyone/p/12663145.html" target="_blank" rel="noopener">IO流</a></p><p>序列化</p><p>网络编程</p><p> socket</p><p> BIO、NIO、AIO</p><p> 状态码</p><p>多线程编程</p><p> <a href="https://blog.csdn.net/ThinkWon/article/details/102021274" target="_blank" rel="noopener">线程和进程的区别</a></p><p> <a href="https://segmentfault.com/a/1190000037589073" target="_blank" rel="noopener">线程创建</a>和<a href="https://juejin.cn/post/6844903699546898445" target="_blank" rel="noopener">结束</a></p><p> <a href="https://segmentfault.com/a/1190000030688673?utm_source=sf-similar-article" target="_blank" rel="noopener">线程的状态</a></p><p> 线程池</p><p> 锁(JVM)</p><p> <a href="https://blog.csdn.net/zqz_zqz/article/details/70233767" target="_blank" rel="noopener">关于锁0</a></p><p> <a href="https://blog.csdn.net/renwei289443/article/details/79540809" target="_blank" rel="noopener">关于锁1</a></p><p> <a href="https://www.cnblogs.com/jyroy/p/11365935.html" target="_blank" rel="noopener">关于锁2</a></p><p> <a href="https://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&mid=2651749434&idx=3&sn=5ffa63ad47fe166f2f1a9f604ed10091&chksm=bd12a5778a652c61509d9e718ab086ff27ad8768586ea9b38c3dcf9e017a8e49bcae3df9bcc8&scene=38#wechat_redirect" target="_blank" rel="noopener">关于锁3</a></p><p> <a href="https://juejin.cn/post/6844903726268809224" target="_blank" rel="noopener">分布式锁实现方式</a></p><p> 数据库乐观锁</p><p> <a href="https://mp.weixin.qq.com/s/qJK61ew0kCExvXrqb7-RSg" target="_blank" rel="noopener">redis 分布式锁 setnx</a></p><p> 基于Zookeeper的分布式锁</p><p> <a href="https://juejin.cn/post/6844903688088059912" target="_blank" rel="noopener">分布式锁总结</a></p><p> <a href="https://segmentfault.com/a/1190000015558984" target="_blank" rel="noopener">JUC包</a> </p><h3 id="面试题-5"><a href="#面试题-5" class="headerlink" title="面试题"></a>面试题</h3><h1 id="JSP"><a href="#JSP" class="headerlink" title="JSP"></a><a href="https://www.runoob.com/jsp/jsp-tutorial.html" target="_blank" rel="noopener">JSP</a></h1><pre><code class="jsp"><%@ page%>的作用</code></pre><h1 id="JavaScript"><a href="#JavaScript" class="headerlink" title="JavaScript"></a><a href="https://www.runoob.com/js/js-tutorial.html" target="_blank" rel="noopener">JavaScript</a></h1><h1 id="并发编程"><a href="#并发编程" class="headerlink" title="并发编程"></a>并发编程</h1><p><a href="https://crossoverjie.top/2018/01/25/ReentrantLock/" target="_blank" rel="noopener">ReentrantLock 实现原理</a></p><p><a href="https://www.javazhiyin.com/42563.html" target="_blank" rel="noopener">如何保证线程的执行顺序</a></p><h2 id="高级特性"><a href="#高级特性" class="headerlink" title="高级特性"></a>高级特性</h2><h3 id="笔试题-6"><a href="#笔试题-6" class="headerlink" title="笔试题"></a>笔试题</h3><h3 id="面试题-6"><a href="#面试题-6" class="headerlink" title="面试题"></a>面试题</h3><h1 id="计算机组成原理"><a href="#计算机组成原理" class="headerlink" title="计算机组成原理"></a>计算机组成原理</h1><h2 id="笔试题-7"><a href="#笔试题-7" class="headerlink" title="笔试题"></a>笔试题</h2><h2 id="面试题-7"><a href="#面试题-7" class="headerlink" title="面试题"></a>面试题</h2><h1 id="编译原理"><a href="#编译原理" class="headerlink" title="编译原理"></a>编译原理</h1><h2 id="笔试题-8"><a href="#笔试题-8" class="headerlink" title="笔试题"></a>笔试题</h2><h2 id="面试题-8"><a href="#面试题-8" class="headerlink" title="面试题"></a>面试题</h2><h1 id="网络安全"><a href="#网络安全" class="headerlink" title="网络安全"></a>网络安全</h1><h2 id="笔试题-9"><a href="#笔试题-9" class="headerlink" title="笔试题"></a>笔试题</h2><p><a href="https://juejin.cn/post/6844903638117122056#heading-13" target="_blank" rel="noopener">常见的对称加密算法</a></p><p> <a href="https://www.cnblogs.com/zhuyeshen/p/11424734.html" target="_blank" rel="noopener">MD5非对称加密算法</a></p><h2 id="面试题-9"><a href="#面试题-9" class="headerlink" title="面试题"></a>面试题</h2><h1 id="Html、css、JS"><a href="#Html、css、JS" class="headerlink" title="Html、css、JS"></a>Html、css、JS</h1><h2 id="笔试题-10"><a href="#笔试题-10" class="headerlink" title="笔试题"></a>笔试题</h2><h2 id="面试题-10"><a href="#面试题-10" class="headerlink" title="面试题"></a>面试题</h2><h2 id="Tomcat"><a href="#Tomcat" class="headerlink" title="Tomcat"></a>Tomcat</h2><p><a href="https://blog.csdn.net/lifetragedy/article/details/7708724" target="_blank" rel="noopener">tomcat调优参数</a></p><h1 id="Spring"><a href="#Spring" class="headerlink" title="Spring"></a>Spring</h1><h2 id="笔试题-11"><a href="#笔试题-11" class="headerlink" title="笔试题"></a>笔试题</h2><h2 id="面试题-11"><a href="#面试题-11" class="headerlink" title="面试题"></a>面试题</h2><h1 id="Spring-Boot"><a href="#Spring-Boot" class="headerlink" title="Spring Boot"></a>Spring Boot</h1><h2 id="笔试题-12"><a href="#笔试题-12" class="headerlink" title="笔试题"></a>笔试题</h2><h2 id="面试题-12"><a href="#面试题-12" class="headerlink" title="面试题"></a>面试题</h2><h1 id="Spring-Cloud"><a href="#Spring-Cloud" class="headerlink" title="Spring Cloud"></a>Spring Cloud</h1><h2 id="笔试题-13"><a href="#笔试题-13" class="headerlink" title="笔试题"></a>笔试题</h2><h2 id="面试题-13"><a href="#面试题-13" class="headerlink" title="面试题"></a>面试题</h2><p><a href="https://www.redhat.com/zh/topics/microservices/what-are-microservices" target="_blank" rel="noopener">微服务</a>的理解,微服务的<a href="https://www.infoq.cn/article/evt4qkropmzqphx1xzfa" target="_blank" rel="noopener">特点,优缺点</a></p><h1 id="中间件"><a href="#中间件" class="headerlink" title="中间件"></a>中间件</h1><h2 id="Zookeeper"><a href="#Zookeeper" class="headerlink" title="Zookeeper"></a>Zookeeper</h2><h3 id="笔试题-14"><a href="#笔试题-14" class="headerlink" title="笔试题"></a>笔试题</h3><h3 id="面试题-14"><a href="#面试题-14" class="headerlink" title="面试题"></a>面试题</h3><h2 id="Dubble"><a href="#Dubble" class="headerlink" title="Dubble"></a>Dubble</h2><h3 id="笔试题-15"><a href="#笔试题-15" class="headerlink" title="笔试题"></a>笔试题</h3><h3 id="面试题-15"><a href="#面试题-15" class="headerlink" title="面试题"></a>面试题</h3><h1 id="系统设计"><a href="#系统设计" class="headerlink" title="系统设计"></a>系统设计</h1><h2 id="笔试题-16"><a href="#笔试题-16" class="headerlink" title="笔试题"></a>笔试题</h2><h2 id="面试题-16"><a href="#面试题-16" class="headerlink" title="面试题"></a>面试题</h2><h1 id="分布式"><a href="#分布式" class="headerlink" title="分布式"></a>分布式</h1><h2 id="笔试题-17"><a href="#笔试题-17" class="headerlink" title="笔试题"></a>笔试题</h2><p>库存超卖</p><p> redis原子操作+sql乐观锁</p><p><a href="https://tech.meituan.com/2016/09/29/distributed-system-mutually-exclusive-idempotence-cerberus-gtis.html" target="_blank" rel="noopener">接口幂等性</a></p><p> redis 分布式锁</p><p>促销活动高并发问题 </p><h2 id="面试题-17"><a href="#面试题-17" class="headerlink" title="面试题"></a>面试题</h2>]]></content>
</entry>
<entry>
<title>win-10-移动热点</title>
<link href="/30259.html"/>
<url>/30259.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title>编码规范</title>
<link href="/5411.html"/>
<url>/5411.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title>计算机组成原理</title>
<link href="/51917.html"/>
<url>/51917.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title>编译原理</title>
<link href="/39825.html"/>
<url>/39825.html</url>
<content type="html"><![CDATA[]]></content>
<categories>
<category> Basic </category>
</categories>
</entry>
<entry>
<title>UML</title>
<link href="/46372.html"/>
<url>/46372.html</url>
<content type="html"><![CDATA[<h1 id="UML简介"><a href="#UML简介" class="headerlink" title="UML简介"></a>UML简介</h1><p>UML类图用于描述系统中的对象本身的组成和类对象之间的各种静态关系。</p><p>类之间的关系:依赖、泛化(继承)、实现、关联、聚合与组合,如下图。</p><p><img src="C:%5CUsers%5CAdministrator%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20200910104720249.png" alt="image-20200910104720249"></p><h1 id="UML-常见问题"><a href="#UML-常见问题" class="headerlink" title="UML 常见问题"></a>UML 常见问题</h1>]]></content>
<categories>
<category> Basic </category>
</categories>
</entry>
<entry>
<title>Git</title>
<link href="/25246.html"/>
<url>/25246.html</url>
<content type="html"><![CDATA[<h2 id="Git原理"><a href="#Git原理" class="headerlink" title="Git原理"></a>Git原理</h2><p><img src="/25246/git_principle.png" alt="Git原理"></p><p>参考:</p><p><a href="https://juejin.im/post/6844904199189184525" target="_blank" rel="noopener">https://juejin.im/post/6844904199189184525</a></p><h2 id="本地版本"><a href="#本地版本" class="headerlink" title="本地版本"></a>本地版本</h2><h2 id="分支管理"><a href="#分支管理" class="headerlink" title="分支管理"></a>分支管理</h2><h2 id="标签管理"><a href="#标签管理" class="headerlink" title="标签管理"></a>标签管理</h2><h2 id="远程仓库GitHub"><a href="#远程仓库GitHub" class="headerlink" title="远程仓库GitHub"></a>远程仓库GitHub</h2>]]></content>
<categories>
<category> Basic </category>
</categories>
</entry>
<entry>
<title>面试题</title>
<link href="/27055.html"/>
<url>/27055.html</url>
<content type="html"><![CDATA[]]></content>
<categories>
<category> 面试 </category>
</categories>
</entry>
<entry>
<title>Spark</title>
<link href="/23978.html"/>
<url>/23978.html</url>
<content type="html"><![CDATA[]]></content>
<categories>
<category> Big Data </category>
</categories>
</entry>
<entry>
<title>Hadoop</title>
<link href="/31153.html"/>
<url>/31153.html</url>
<content type="html"><![CDATA[]]></content>
<categories>
<category> Big Data </category>
</categories>
</entry>
<entry>
<title>MQ</title>
<link href="/44276.html"/>
<url>/44276.html</url>
<content type="html"><![CDATA[<p><a href="https://juejin.im/post/6844903635046924296#heading-46" target="_blank" rel="noopener">浅谈消息队列及常见的消息中间件</a></p><p><a href="https://developer.aliyun.com/article/684866" target="_blank" rel="noopener">RPC远程调用和消息队列MQ的区别</a></p>]]></content>
<categories>
<category> DS </category>
</categories>
</entry>
<entry>
<title>Dubbo</title>
<link href="/37154.html"/>
<url>/37154.html</url>
<content type="html"><![CDATA[<h2 id="源码分析"><a href="#源码分析" class="headerlink" title="源码分析"></a>源码分析</h2><h3 id="协议模块源码"><a href="#协议模块源码" class="headerlink" title="协议模块源码"></a>协议模块源码</h3><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><p><a href="https://segmentfault.com/a/1190000019896723" target="_blank" rel="noopener">为什么需要dubbo</a></p><p><a href="https://segmentfault.com/a/1190000019896723" target="_blank" rel="noopener">Dubbo与zookeeper的关系</a></p><p><a href="https://developer.aliyun.com/article/684866" target="_blank" rel="noopener">RPC和MQ的区别与联系</a></p>]]></content>
<categories>
<category> DS </category>
</categories>
</entry>
<entry>
<title>分布式</title>
<link href="/41895.html"/>
<url>/41895.html</url>
<content type="html"><![CDATA[<h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><p><a href="https://juejin.im/post/6844903967701336078#heading-2" target="_blank" rel="noopener">分布式架构浅析</a></p>]]></content>
</entry>
<entry>
<title>Spring</title>
<link href="/18155.html"/>
<url>/18155.html</url>
<content type="html"><![CDATA[<h2 id="Spring-Framework"><a href="#Spring-Framework" class="headerlink" title="Spring Framework"></a>Spring Framework</h2><h2 id="Spring-体系结构"><a href="#Spring-体系结构" class="headerlink" title="Spring 体系结构"></a>Spring 体系结构</h2><p><img src="C:%5CUsers%5CAdministrator%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20200910144056583.png" alt="image-20200910144056583"></p><h2 id="IOC"><a href="#IOC" class="headerlink" title="IOC"></a>IOC</h2><p>什么是IOC容器</p><p> 一个HashMap</p><p>如何创建IOC容器</p><p> ***contextapplication</p><p>IOC容器如何初始化Bean实例</p><h2 id="基于XML的IOC"><a href="#基于XML的IOC" class="headerlink" title="基于XML的IOC"></a>基于XML的IOC</h2><h2 id="基于XML的DI"><a href="#基于XML的DI" class="headerlink" title="基于XML的DI"></a>基于XML的DI</h2><p>依赖注入,</p><h2 id="基于注解的IOC"><a href="#基于注解的IOC" class="headerlink" title="基于注解的IOC"></a>基于注解的IOC</h2><h2 id="Spring纯注解实现方式"><a href="#Spring纯注解实现方式" class="headerlink" title="Spring纯注解实现方式"></a>Spring纯注解实现方式</h2><h2 id="Spring-与Junit"><a href="#Spring-与Junit" class="headerlink" title="Spring 与Junit"></a>Spring 与Junit</h2><h2 id="Spring-分模块开发"><a href="#Spring-分模块开发" class="headerlink" title="Spring 分模块开发"></a>Spring 分模块开发</h2><h2 id="Spring-AOP-原理"><a href="#Spring-AOP-原理" class="headerlink" title="Spring AOP 原理"></a>Spring AOP 原理</h2><p>代理分为静态代理与动态代理,动态代理有JDK动态代理、CGLIB动态代理</p><p>JDK动态代理与CGLIB动态代理的区别</p><h2 id="Spring-AOP基于XML和注解的实现"><a href="#Spring-AOP基于XML和注解的实现" class="headerlink" title="Spring AOP基于XML和注解的实现"></a>Spring AOP基于XML和注解的实现</h2><h2 id="Spring-应用-Spring-JDBC-实现"><a href="#Spring-应用-Spring-JDBC-实现" class="headerlink" title="Spring 应用-Spring JDBC 实现"></a>Spring 应用-Spring JDBC 实现</h2><h2 id="Spring-应用-JdbcDaoSupport"><a href="#Spring-应用-JdbcDaoSupport" class="headerlink" title="Spring 应用-JdbcDaoSupport"></a>Spring 应用-JdbcDaoSupport</h2><h2 id="Spring-应用-事务支持"><a href="#Spring-应用-事务支持" class="headerlink" title="Spring 应用-事务支持"></a>Spring 应用-事务支持</h2><h2 id="Spring-与-MyBatis"><a href="#Spring-与-MyBatis" class="headerlink" title="Spring 与 MyBatis"></a>Spring 与 MyBatis</h2><p>Spring 循环依赖</p><p> A 创建过程中需要 B,于是 A 将自己放到三级缓里面 ,去实例化 B B 实例化的时候发现需要 A,于是 B 先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了! 然后把三级缓存里面的这个 A 放到二级缓存里面,并删除三级缓存里面的 A B 顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中状态) 然后回来接着创建 A,此时 B 已经创建结束,直接从一级缓存里面拿到 B ,然后完成创建,并将自己放到一级缓存里面。</p><p><a href="https://jishuin.proginn.com/p/763bfbd2c640" target="_blank" rel="noopener">https://jishuin.proginn.com/p/763bfbd2c640</a></p><p><a href="https://www.bilibili.com/read/cv3791985/" target="_blank" rel="noopener">https://www.bilibili.com/read/cv3791985/</a> (可以关闭Spring默认支持的单例模式中的循环依赖)</p><p><a href="https://developer.aliyun.com/article/766880" target="_blank" rel="noopener">https://developer.aliyun.com/article/766880</a></p><p><a href="https://developer.51cto.com/art/202005/615924.htm" target="_blank" rel="noopener">https://developer.51cto.com/art/202005/615924.htm</a></p><p><a href="https://www.cnblogs.com/jajian/p/10241932.html" target="_blank" rel="noopener">https://www.cnblogs.com/jajian/p/10241932.html</a></p><p><a href="https://segmentfault.com/a/1190000015221968" target="_blank" rel="noopener">https://segmentfault.com/a/1190000015221968</a></p><p>属性注册编辑器</p><p>Spring 拦截器原理</p><p>Spring 事件监听</p><p>Spring异步注解</p><p>Spring 事件驱动编程使用</p><p>Spring aop织入过程分析</p><h2 id="Servlet"><a href="#Servlet" class="headerlink" title="Servlet"></a>Servlet</h2><p><a href="https://juejin.im/post/6844903570681135117" target="_blank" rel="noopener">servlet到Spring mvc的简化之路</a></p><h2 id="Spring-源码"><a href="#Spring-源码" class="headerlink" title="Spring 源码"></a>Spring 源码</h2><p><a href="https://blog.csdn.net/u013510838/article/details/75092612" target="_blank" rel="noopener">Spring XML配置文件的解析流程</a></p><p><a href="https://blog.csdn.net/firefile/article/details/90735264" target="_blank" rel="noopener">Spring的bean加载过程</a></p><p><a href="http://www.tianxiaobo.com/2018/06/22/Spring-AOP-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90-%E6%8B%A6%E6%88%AA%E5%99%A8%E9%93%BE%E7%9A%84%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B/" target="_blank" rel="noopener">Spring AOP 源码分析 - 拦截器链的执行过程</a></p><p><a href="https://blog.csdn.net/zly9923218/article/details/51348583" target="_blank" rel="noopener">Spring AOP源码分析(生成代理对象)</a></p><p><a href="https://blog.csdn.net/win7system/article/details/90674757" target="_blank" rel="noopener">Spring MVC过程</a></p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><p>BeanFactory和FactoryBean的区别</p><p>BeanFactory 与 ApplicationContext区别</p><p>Spring 中Bean的循环引用如何解决?</p><p>Spring注解是什么时候加载的?</p><p>IOC容器只存放单例吗?</p><p><a href="https://blog.csdn.net/hxpjava1/article/details/55504513/" target="_blank" rel="noopener">spring多个AOP执行先后顺序(面试问题:怎么控制多个aop的执行循序)</a></p><p><a href="https://shimo.im/docs/Nj0bcFUy3SYyYnbI/" target="_blank" rel="noopener">spring AOP常见面试题目</a></p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p>[1] <a href="https://www.docs4dev.com/docs/en/spring-framework/5.1.3.RELEASE/reference/overview.html" target="_blank" rel="noopener">Spring 官方文档</a></p><p>[2] <a href="https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html" target="_blank" rel="noopener">Spring Framework Documentation</a></p><p>[3] <a href="https://www.docs4dev.com/docs/zh/spring-framework/5.1.3.RELEASE/reference" target="_blank" rel="noopener">Spring Framework 中文文档</a></p><p>[4] <a href="https://springboot.io/" target="_blank" rel="noopener">SpringBoot中文社区</a></p><p>[5] <a href="https://www.liaoxuefeng.com/wiki/1252599548343744" target="_blank" rel="noopener">廖雪峰的官方网站-JAVA教程</a></p>]]></content>
<categories>
<category> About Spring </category>
</categories>
</entry>
<entry>
<title>MyBatis</title>
<link href="/61918.html"/>
<url>/61918.html</url>
<content type="html"><![CDATA[<h2 id="源码"><a href="#源码" class="headerlink" title="源码"></a>源码</h2><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p><a href="https://www.tianxiaobo.com/" target="_blank" rel="noopener">《一本小小的MyBatis源码分析书》</a></p><p><a href="https://book.douban.com/subject/27087564/" target="_blank" rel="noopener">《MyBatis技术内幕》</a></p><p><a href="https://mybatis.org/mybatis-3/zh/index.html" target="_blank" rel="noopener">MyBatis官网</a></p>]]></content>
<categories>
<category> Database </category>
</categories>
</entry>
<entry>
<title>Netty</title>
<link href="/15056.html"/>
<url>/15056.html</url>
<content type="html"><![CDATA[<h2 id="网络编程"><a href="#网络编程" class="headerlink" title="网络编程"></a>网络编程</h2><h3 id="Linux-I-O模型介绍"><a href="#Linux-I-O模型介绍" class="headerlink" title="Linux I/O模型介绍"></a>Linux I/O模型介绍</h3><h3 id="Java-I-O"><a href="#Java-I-O" class="headerlink" title="Java I/O"></a>Java I/O</h3><h3 id="Java-NIO"><a href="#Java-NIO" class="headerlink" title="Java NIO"></a>Java NIO</h3><h3 id="Netty-粘包与半包"><a href="#Netty-粘包与半包" class="headerlink" title="Netty 粘包与半包"></a>Netty 粘包与半包</h3><h3 id="Netty-编解码器"><a href="#Netty-编解码器" class="headerlink" title="Netty 编解码器"></a>Netty 编解码器</h3><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p>[1] <a href="https://book.douban.com/subject/1500149/" target="_blank" rel="noopener">Unix网络编程</a></p><p>[2] <a href="https://book.douban.com/subject/24700704/" target="_blank" rel="noopener">Netty in action</a></p><p>[3] <a href="https://netty.io/" target="_blank" rel="noopener">Netty 官网</a></p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><p><a href="https://honeypps.com/backend/what-is-zero-copy/" target="_blank" rel="noopener">什么是零拷贝?</a></p>]]></content>
<categories>
<category> RPC </category>
</categories>
</entry>
<entry>
<title>网络安全</title>
<link href="/45331.html"/>
<url>/45331.html</url>
<content type="html"><![CDATA[<h1 id="密码学"><a href="#密码学" class="headerlink" title="密码学"></a>密码学</h1><h2 id="对称加密"><a href="#对称加密" class="headerlink" title="对称加密"></a>对称加密</h2><p>对称加密算法的加密和解密使用的密匙是相同的,也就是说如果通讯两方如果使用对称加密算法来加密通讯数据,那么通讯双方就需要都知道这个密匙,收到通讯数据后用这个密匙来解密数据。</p><p>这类算法在加密和解密时使用相同的密钥,或是使用两个可以简单地相互推算的密钥。事实上,这组密钥成为在两个或多个成员间的共同秘密,以便维持专属的通信联系。与非对称加密相比,要求双方获取相同的密钥是对称密钥加密的主要缺点之一。常见的对称加密算法有 <code>DES、3DES、AES、Blowfish、IDEA、RC5、RC6</code>。</p><p><strong>对称加密的速度比公钥加密快很多,在很多场合都需要对称加密</strong>。</p><h2 id="非对称加密"><a href="#非对称加密" class="headerlink" title="非对称加密"></a>非对称加密</h2><p>它需要两个密钥,<strong>一个是公开密钥,另一个是私有密钥;一个用作加密的时候,另一个则用作解密</strong>。使用其中一个密钥把明文加密后所得的密文,只能用相对应的另一个密钥才能解密得到原本的明文;甚至连最初用来加密的密钥也不能用作解密。由于加密和解密需要两个不同的密钥,故被称为非对称加密;</p><p>虽然两个密钥在数学上相关,但如果知道了其中一个,并不能凭此计算出另外一个;因此其中一个可以公开,称为 <strong>公钥</strong>,任意向外发布;不公开的密钥为 <strong>私钥</strong> ,必须由用户自行严格秘密保管,绝不透过任何途径向任何人提供,也不会透露给要通信的另一方,即使他被信任。</p><blockquote><p>公钥 & 私钥 均可以作为加密密钥</p></blockquote><h2 id="数字签名"><a href="#数字签名" class="headerlink" title="数字签名"></a>数字签名</h2><p>数字签名是一种类似写在纸上的签名,但是使用了 <strong>公钥加密领域的技术实现</strong> ,用于鉴别数字信息的方法。在网络上,我们可以使用“数字签名”来进行身份确认。数字签名是一个独一无二的数值,若公钥能通过验证,那我们就能确定对应的公钥的正确性,数字签名兼具这两种双重属性:“可确认性”及”不可否认性(不需要笔迹专家验证)”。</p><p>数字签名就是将公钥密码反过来使用。签名者将讯息用私钥加密(<strong>这是一种反用,因为通常非对称加密中私钥用于解密</strong>),然后公布公钥;验证者使用公钥将加密讯息解密并比对消息(一般签名对象为消息的散列值)。</p><h2 id="密码散列函数"><a href="#密码散列函数" class="headerlink" title="密码散列函数"></a>密码散列函数</h2><p>密码散列函数(英语:<code>Cryptographic hash function</code>),又译为加密散列函数、密码散列函数、加密散列函数,是散列函数的一种。它被认为是一种 <strong>单向函数</strong>,也就是说极其难以由散列函数输出的结果,回推输入的数据是什么。这种散列函数的输入数据,通常被称为消息( <code>message</code> ),而它的输出结果,经常被称为消息摘要( <code>message digest</code> )或摘要( <code>digest</code> )。</p>]]></content>
<categories>
<category> Basic </category>
</categories>
</entry>
<entry>
<title>Redis相关</title>
<link href="/10595.html"/>
<url>/10595.html</url>
<content type="html"><![CDATA[<h2 id="相关资料"><a href="#相关资料" class="headerlink" title="相关资料"></a>相关资料</h2><p><a href="https://redis.io/documentation" target="_blank" rel="noopener">Redis官方文档</a>(EN)</p><p><a href="http://redisdoc.com/string/index.html" target="_blank" rel="noopener">Redis文档</a>(CH)</p><p>Redis设计与实现</p><h2 id="面试常见问题"><a href="#面试常见问题" class="headerlink" title="面试常见问题"></a>面试常见问题</h2><p><a href="https://www.redis.com.cn/redis-interview-questions.html" target="_blank" rel="noopener">常见问题</a></p><h2 id="线程模型"><a href="#线程模型" class="headerlink" title="线程模型"></a>线程模型</h2><p>Redis 在处理网络请求是使用单线程模型,并通过 IO 多路复用来提高并发。但是在其他模块,比如:持久化,会使用多个线程。</p><p>Redis 内部使用文件事件处理器 <code>file event handler</code>,<strong>这个文件事件处理器是单线程的,所以 <code>Redis</code> 才叫做单线程的模型</strong>。它采用 IO 多路复用机制同时监听多个 <code>socket</code> ,将产生事件的 <code>socket</code> 压入内存队列中,事件分派器根据 <code>socket</code> 上的事件类型来选择对应的事件处理器进行处理。</p><p>文件事件处理器的结构包含 4 个部分:</p><ul><li>多个 socket</li><li>IO 多路复用程序</li><li>文件事件分派器</li><li>事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)</li></ul><p>多个 <code>socket</code> 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 <code>IO</code> 多路复用程序会监听多个 <code>socket</code> ,会将产生事件的 <code>socket</code> 放入队列中排队,事件分派器每次从队列中取出一个 <code>socket</code> ,根据 <code>socket</code> 的事件类型交给对应的事件处理器进行处理。</p><p>客户端与 Redis 的一次通信过程:</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/redis/images/f0dacdd3779b836ad75fe6b886af1fff.png" alt="image"></p><h3 id="为啥-Redis-单线程模型也能效率这么高?(能加速数据检索的原因)"><a href="#为啥-Redis-单线程模型也能效率这么高?(能加速数据检索的原因)" class="headerlink" title="为啥 Redis 单线程模型也能效率这么高?(能加速数据检索的原因)"></a>为啥 Redis 单线程模型也能效率这么高?(能加速数据检索的原因)</h3><ul><li>纯内存操作(基于内存操作,数据读写全部在内存中完成)</li><li>核心是基于非阻塞的 IO 多路复用机制</li><li>单线程反而避免了多线程的频繁上下文切换问题<h4 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h4><a href="https://jyjsjd.github.io/redis/redis-pipeline/" target="_blank" rel="noopener">用管道加速 Redis 查询</a></li></ul><h2 id="持久化"><a href="#持久化" class="headerlink" title="持久化"></a>持久化</h2><h3 id="RDB"><a href="#RDB" class="headerlink" title="RDB"></a>RDB</h3><p>RDB 持久化机制,是对 Redis 中的数据执行周期性的持久化。</p><ul><li>RDB 会生成多个数据文件,每个数据文件都代表了某一个时刻中 Redis 的数据,<strong>非常适合做冷备</strong></li><li>RDB 对 Redis 对外提供的读写服务,影响非常小,可以让 Redis 保持高性能,因为 Redis 主进程只需要 fork 一个子进程,让子进程执行磁盘 IO 操作来进行 RDB 持久化即可。</li><li>相对于 AOF 持久化机制来说,直接基于 RDB 数据文件来重启和恢复 Redis 进程,更加快速。</li><li>一般来说,RDB 数据快照文件,都是每隔 5 分钟,或者更长时间生成一次,这个时候就得接受一旦 Redis 进程宕机,那么会丢失最近 5 分钟的数据。</li><li>RDB 每次在 <code>fork</code> 子进程来执行 RDB 快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒。</li></ul><h3 id="AOF"><a href="#AOF" class="headerlink" title="AOF"></a>AOF</h3><p>AOF 机制对每条写入命令作为日志,以 <code>append-only</code> 的模式写入一个日志文件中,在 Redis 重启的时候,可以通过回放 AOF 日志中的写入指令来重新构建整个数据集</p><ul><li>AOF 可以更好的保护数据不丢失,一般 AOF 会每隔 1 秒,通过一个后台线程执行一次 <code>fsync</code> 操作,最多丢失 1 秒钟的数据。</li><li>AOF 日志文件以 <code>append-only</code> 模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复。</li><li>AOF 日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。因为在 <strong><code>rewrite log</code></strong> 的时候,会对其中的指令进行压缩,创建出一份需要恢复数据的最小日志出来。在创建新日志文件的时候,老的日志文件还是照常写入。当新的 <code>merge</code> 后的日志文件 <code>ready</code> 的时候,再交换新老日志文件即可。</li><li>AOF 日志文件的命令通过非常可读的方式进行记录,这个特性非常 <strong>适合做灾难性的误删除的紧急恢复</strong>。比如某人不小心用 <code>flushall</code> 命令清空了所有数据,只要这个时候后台 <code>rewrite</code> 还没有发生,那么就可以立即拷贝 AOF 文件,将最后一条 <code>flushall</code> 命令给删了,然后再将该 AOF 文件放回去,就可以通过恢复机制,自动恢复所有数据。</li><li>对于同一份数据来说,AOF 日志文件通常比 RDB 数据快照文件更大。</li><li><strong>AOF 开启后,支持的写 QPS 会比 RDB 支持的写 QPS 低</strong>,因为 AOF 一般会配置成每秒 <code>fsync</code> 一次日志文件,当然,每秒一次 <code>fsync</code> ,性能也还是很高的。(如果实时写入,那么 QPS 会大降,Redis 性能会大大降低)</li><li>以前 AOF 发生过 bug,就是通过 AOF 记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。所以说,类似 AOF 这种较为复杂的基于命令日志 / merge / 回放的方式,比基于 RDB 每次持久化一份完整的数据快照文件的方式,更加脆弱一些,容易有 bug。不过 AOF 就是为了避免 rewrite 过程导致的 bug,因此每次 rewrite 并不是基于旧的指令日志进行 merge 的,而是基于当时内存中的数据进行指令的重新构建,这样健壮性会好很多。</li></ul><h3 id="RDB-和-AOF-到底该如何选择"><a href="#RDB-和-AOF-到底该如何选择" class="headerlink" title="RDB 和 AOF 到底该如何选择"></a>RDB 和 AOF 到底该如何选择</h3><ol><li>不要仅仅使用 RDB,因为那样会导致你丢失很多数据;</li><li>也不要仅仅使用 AOF,因为那样有两个问题:第一,你通过 AOF 做冷备,没有 RDB 做冷备来的恢复速度更快;第二,RDB 每次简单粗暴生成数据快照,更加健壮,可以避免 AOF 这种复杂的备份和恢复机制的 bug;</li><li>Redis 支持同时开启开启两种持久化方式,我们可以综合使用 AOF 和 RDB 两种持久化机制,用 AOF 来保证数据不丢失,作为数据恢复的第一选择; 用 RDB 来做不同程度的冷备,在 AOF 文件都丢失或损坏不可用的时候,还可以使用 RDB 来进行快速的数据恢复。</li></ol><h2 id="一致性哈希算法"><a href="#一致性哈希算法" class="headerlink" title="一致性哈希算法"></a>一致性哈希算法</h2><p>一致哈希 是一种特殊的哈希算法。在使用一致哈希算法后,哈希表槽位数(大小)的改变平均只需要对 <code>K/n</code> 个关键字重新映射,其中 <code>K</code> 是关键字的数量,<code>n</code> 是槽位数量。然而在传统的哈希表中,添加或删除一个槽位的几乎需要对所有关键字进行重新映射。</p><blockquote><p>一致哈希也可用于实现健壮缓存来减少大型 Web 应用中系统部分失效带来的负面影响</p></blockquote><h3 id="需求"><a href="#需求" class="headerlink" title="需求"></a>需求</h3><p>在使用 <code>n</code> 台缓存服务器时,一种常用的负载均衡方式是,对资源 <code>o</code> 的请求使用 <code>hash(o)= o mod n</code> 来映射到某一台缓存服务器。当增加或减少一台缓存服务器时这种方式可能会改变所有资源对应的 <code>hash</code> 值,也就是所有的缓存都失效了,这会使得缓存服务器大量集中地向原始内容服务器更新缓存。</p><p>因此需要一致哈希算法来避免这样的问题。 一致哈希尽可能使同一个资源映射到同一台缓存服务器。这种方式要求增加一台缓存服务器时,新的服务器尽量分担存储其他所有服务器的缓存资源。减少一台缓存服务器时,其他所有服务器也可以尽量分担存储它的缓存资源。</p><p>一致哈希算法的主要思想是将每个缓存服务器与一个或多个哈希值域区间关联起来,其中区间边界通过计算缓存服务器对应的哈希值来决定。如果一个缓存服务器被移除,则它所对应的区间会被并入到邻近的区间,其他的缓存服务器不需要任何改变。</p><h3 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h3><p>一致哈希将每个对象映射到圆环边上的一个点,系统再将可用的节点机器映射到圆环的不同位置。查找某个对象对应的机器时,需要用一致哈希算法计算得到对象对应圆环边上位置,沿着圆环边上查找直到遇到某个节点机器,这台机器即为对象应该保存的位置。</p><p>当删除一台节点机器时,这台机器上保存的所有对象都要移动到下一台机器。添加一台机器到圆环边上某个点时,这个点的下一台机器需要将这个节点前对应的对象移动到新机器上。更改对象在节点机器上的分布可以通过调整节点机器的位置来实现。</p><h2 id="实践"><a href="#实践" class="headerlink" title="实践"></a><a href="https://yikun.github.io/2016/06/09/一致性哈希算法的理解与实践/" target="_blank" rel="noopener">实践</a></h2><blockquote><p>假设有1000w个数据项,100个存储节点,请设计一种算法合理地将他们存储在这些节点上。</p></blockquote><p>看一看普通Hash算法的原理:</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/redis/images/1c5e07626a9cadd5f1ea8acd85838067.png" alt="img"></p><pre><code>for item in range(ITEMS): k = md5(str(item)).digest() h = unpack_from(">I", k)[0] # 通过取余的方式进行映射 n = h % NODES node_stat[n] += 1</code></pre><p>普通的Hash算法均匀地将这些数据项打散到了这些节点上,并且分布最少和最多的存储节点数据项数目小于 <code>1%</code>。之所以分布均匀,主要是依赖 Hash 算法(实现使用的MD5算法)能够比较随机的分布。</p><p>然而,我们看看存在一个问题,由于 <strong>该算法使用节点数取余的方法,强依赖 <code>node</code> 的数目</strong>,因此,当是 <code>node</code> 数发生变化的时候,<code>item</code> 所对应的 <code>node</code> 发生剧烈变化,而发生变化的成本就是我们需要在 <code>node</code> 数发生变化的时候,数据需要迁移,这对存储产品来说显然是不能忍的。</p><h4 id="一致性哈希"><a href="#一致性哈希" class="headerlink" title="一致性哈希"></a>一致性哈希</h4><p>普通 <code>Hash</code> 算法的劣势,即当 <code>node</code> 数发生变化(增加、移除)后,数据项会被重新“打散”,导致大部分数据项不能落到原来的节点上,从而导致大量数据需要迁移。</p><p>那么,一个亟待解决的问题就变成了:当 <code>node</code> 数发生变化时,如何保证尽量少引起迁移呢?即当增加或者删除节点时,对于大多数 item ,保证原来分配到的某个 node ,现在仍然应该分配到那个 node ,将数据迁移量的降到最低。</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/redis/images/3de376ea57386b890483b27cf131f24d.png" alt="img"></p><pre><code>for n in range(NODES): h = _hash(n) ring.append(h) ring.sort() hash2node[h] = nfor item in range(ITEMS): h = _hash(item) n = bisect_left(ring, h) % NODES node_stat[hash2node[ring[n]]] += 1</code></pre><p><strong>虽然一致性Hash算法解决了节点变化导致的数据迁移问题,但是,数据项分布的均匀性很差</strong>。</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/redis/images/5e6b9afd23ff44415b434d05ed0449ce.png" alt="img"></p><p>主要是因为这 100 个节点 Hash 后,在环上分布不均匀,导致了每个节点实际占据环上的区间大小不一造成的。</p><h4 id="改进-–-虚节点"><a href="#改进-–-虚节点" class="headerlink" title="改进 – 虚节点"></a>改进 – 虚节点</h4><p>当我们将 node 进行哈希后,这些值并没有均匀地落在环上,因此,最终会导致,这些节点所管辖的范围并不均匀,最终导致了数据分布的不均匀。</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/redis/images/c807b7a0af060a874fdb27abf5caf289.png" alt="img"></p><pre><code>for n in range(NODES): for v in range(VNODES): h = _hash(str(n) + str(v)) # 构造ring ring.append(h) # 记录hash所对应节点 hash2node[h] = nring.sort()for item in range(ITEMS): h = _hash(str(item)) # 搜索ring上最近的hash n = bisect_left(ring, h) % (NODES*VNODES) node_stat[hash2node[ring[n]]] += 1</code></pre><p>通过增加虚节点的方法,使得每个节点在环上所“管辖”更加均匀。这样就既保证了在节点变化时,尽可能小的影响数据分布的变化,而同时又保证了数据分布的均匀。也就是靠增加“节点数量”加强管辖区间的均匀。</p><h2 id="集群"><a href="#集群" class="headerlink" title="集群"></a>集群</h2><h3 id="主从复制"><a href="#主从复制" class="headerlink" title="主从复制"></a>主从复制</h3><p>单机的 Redis ,能够承载的 QPS 大概就在上万到几万不等。对于缓存来说,一般都是用来支撑读高并发的。因此架构做成 <strong>主从(Master-Slave)架构</strong> ,一主多从,主负责写,并且将数据复制到其它的 Slave 节点,从节点负责读。所有的读请求全部走从节点。这样也可以很轻松实现水平扩容,支撑读高并发。</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/redis/images/redis-master-slave.png" alt="img"></p><p>Redis 默认采用异步方式复制数据到 Slave Node,同时 Slave Node 会周期性地确认自己每次复制的数据量:</p><ol><li>当 Master 和 Slave 网络连接顺畅时,Master 会持续向 Slave 推送命令,以保持在 Master 数据集合上执行的:客户端写、Key 过期、Key 淘汰等均在 Slave 数据集合上执行。</li><li>当 Master 和 Slave 网络连接由于网络问题、超时等中断时, Slave 会尝试重连并进行连接断开期间的命令 <strong>部分同步(partial resynchronization)</strong>。</li><li>当部分同步不可用时,Slave 会请求全量同步。在这个过程中,Master 会创建当前所有数据的镜像,发送给 Slave 并继续推送命令。</li></ol><p>Redis 主从复制包含以下几个要点:</p><ol><li>一个 Master 可以有多个 Slave</li><li>Slave 支持级联结构,即 Slave 可以连接到其他 Slave 上</li><li>Redis 在复制过程中,不阻塞 Master ,不论是全量同步还是部分同步</li><li>在大部分时间里,复制也不会阻塞 Slave 。当 Slave 在进行初始化同步时,Slave 会先使用旧的数据集提供服务。但当初始化同步完成时,会删除旧数据集,这时 Slave 会拒绝服务。</li><li>Redis 主从复制可以用来做水平扩容,以提供读写分离,或作为数据备份和高可用</li><li>在主从复制的情况下,可以通过配置避免数据持久化,将 Slave 作为数据的备份或开启 Slave 的 AOF。但是这种情况下也会有风险:当 Master 重启后数据集将清空,这时如果 Slave 同步 Master 就会导致数据也被清空</li></ol><h4 id="当-Master-不进行持久化如何保证数据安全"><a href="#当-Master-不进行持久化如何保证数据安全" class="headerlink" title="当 Master 不进行持久化如何保证数据安全"></a>当 Master 不进行持久化如何保证数据安全</h4><p>在生产环境中,强烈建议开启 Redis 持久化,不论是在 Master 还是在 Slave。如果由于磁盘速度等问题,不能开启持久化,那么需要 <strong>避免 Redis 进程的自动重启</strong>。</p><h3 id="哨兵"><a href="#哨兵" class="headerlink" title="哨兵"></a>哨兵</h3><p><code>Sentinel</code> 是 Redis 官方推荐的 <strong>高可用性( <code>HA</code> )解决方案</strong>,当用 Redis 做主从复制的高可用方案时,假如 Master 宕机了, Redis 本身都没有实现自动进行主备切换,而哨兵本身也是一个独立运行的进程,它能监控多个节点,发现 Master 宕机后能进行自动切换。</p><p>它的主要功能有以下几点</p><ul><li>集群监控:负责监控 Redis Master 和 Slave 进程是否正常工作。</li><li>消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。</li><li>故障转移:如果 Master node 挂掉了,会自动转移到 Slave node 上。</li><li>配置中心:如果故障转移发生了,通知 client 客户端新的 Master 地址。</li></ul><h4 id="哨兵的核心知识"><a href="#哨兵的核心知识" class="headerlink" title="哨兵的核心知识"></a>哨兵的核心知识</h4><ol><li>哨兵至少需要 3 个实例,来保证自己的健壮性。</li><li>哨兵 + Redis 主从的部署架构,是 <strong>不保证数据零丢失</strong> 的,只能保证 Redis 集群的高可用性。</li><li>对于哨兵 + Redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。</li><li>哨兵的个数与集群节点个数无关,每个哨兵都会 Check 所有节点</li><li>当启用哨兵后,客户端的连接是通过哨兵连接到 Node 的</li></ol><p>哨兵集群必须部署 2 个以上节点,如果哨兵集群仅仅部署了 2 个哨兵实例,<code>Quorum</code> = 1。</p><pre><code class="log">+----+ +----+| M1 |---------| R1 || S1 | | S2 |+----+ +----+</code></pre><p>如果 Master 宕机, <code>S1</code> 和 <code>S2</code> 中只要有 1 个哨兵认为 Master 宕机了,就可以进行切换,同时 <code>S1</code> 和 <code>S2</code> 会选举出一个哨兵来执行故障转移。但是同时这个时候,需要 <code>Majority</code> ,也就是超过半数的哨兵都是运行的。</p><p>如果此时仅仅是 <code>M1</code> 进程宕机了,哨兵 <code>s1</code> 正常运行,那么故障转移是 OK 的。但是如果是整个 <code>M1</code> 和 <code>S1</code> 运行的机器宕机了,那么哨兵只有 1 个,此时就没有 <code>Majority</code> 来允许执行故障转移,虽然另外一台机器上还有一个 R1,但是故障转移不会执行。</p><p>经典的 3 节点哨兵集群是这样的:</p><pre><code class="log"> +----+ | M1 | | S1 | +----+ |+----+ | +----+| R2 |----+----| R3 || S2 | | S3 |+----+ +----+</code></pre><p>配置 <code>Quorum=2</code>,如果 <code>M1</code> 所在机器宕机了,那么三个哨兵还剩下 2 个, <code>S2</code> 和 <code>S3</code> 可以一致认为 Master 宕机了,然后选举出一个来执行故障转移,同时 3 个哨兵的 <code>Majority</code> 是 2,所以还剩下的 2 个哨兵运行着,就可以允许执行故障转移。</p><h4 id="Slave-选主算法"><a href="#Slave-选主算法" class="headerlink" title="Slave 选主算法"></a>Slave 选主算法</h4><p>如果一个 Master 被认为宕机,而且 <code>Majority</code> 数量的哨兵都允许主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个 Slave 来,会考虑 Slave 的一些信息:</p><ul><li>跟 Master 断开连接的时长</li><li>Slave 优先级</li><li>复制 offset</li><li>run id</li></ul><p>接下来会对 Slave 进行排序:</p><ul><li>按照 Slave 优先级进行排序,Slave Priority 越低,优先级就越高。</li><li>如果 Slave Priority 相同,那么看 Replica Offset,哪个 Slave 复制了越多的数据,Offset 越靠后,优先级就越高。</li><li>如果上面两个条件都相同,那么选择一个 run id 比较小的那个 Slave。</li></ul><h3 id="Redis-Cluster"><a href="#Redis-Cluster" class="headerlink" title="Redis Cluster"></a>Redis Cluster</h3><p>Redis Cluster 是一种服务器 <code>Sharding</code> 技术,提供内置的高可用支持,部分 master 不可用时,还可以继续工作。Redis Cluster 功能强大,直接集成了 <strong>主从复制</strong> 和 <strong>哨兵</strong> 的功能。</p><ul><li><strong>高性能</strong>:在 Cluster 集群中没有代理,主从之间使用异步复制,并且不会对 Key 进行合并操作;</li><li><strong>可接受的写入安全</strong>:当客户端连接到 majority master 时集群尽最大努力保留所有客户端的写操作。通常情况下,在一小段窗口时间内写请求会被丢失,当客户端连接到 minority master 时这个窗口时间会很大;</li><li><strong>可用性</strong>:当 Redis Cluster 中大部分 master 是可达的,并且不可达 master 均有一个可用的 slave 时,Redis Cluster 能够在 <code>NODE_TIMEOUT</code> 时间后进行故障转移,使 Cluster 重新可用。此外,Cluster 还提供 <strong>副本迁移(replicas migration)</strong>,当 master 没有 slave 时,可从其他 master 下重新分配一个 slave ;</li></ul><blockquote><p>majority master:能与大多数 master 连通的 master minority master:未能与大多数 master 连通的 master</p></blockquote><h4 id="内部节点通信"><a href="#内部节点通信" class="headerlink" title="内部节点通信"></a>内部节点通信</h4><p>在 Cluster 架构下,每个 Redis 都需要开启额外的端口来进行节点间通信,这种机制被称之为 <strong>Cluster Bus</strong>。</p><p>Redis 维护集群元数据采用 <strong>gossip 协议</strong>,所有节点都持有一份元数据,不同的节点如果出现了元数据的变更,就不断将元数据发送给其它的节点,让其它节点也进行元数据的变更。</p><p>gossip 好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续打到所有节点上去更新,降低了压力;不好在于,元数据的更新有延时,可能导致集群中的一些操作会有一些滞后。</p><h4 id="寻址算法"><a href="#寻址算法" class="headerlink" title="寻址算法"></a>寻址算法</h4><p>Redis Cluster 有固定的 16384 个 Hash Slot,对每个 key 计算 <code>CRC16</code> 值,然后对 <code>16384</code> 取模,可以获取 key 对应的 Hash Slot。Redis Cluster 中<strong>每个 Master 都会持有部分 Slot</strong>,Slot 的分配在 Cluster 未进行重配置(reconfiguration)时是稳定的。当 Cluster 稳定时,一个 Hash Slot 只在一个 master 上提供服务。不过一个 master 会有一个或多个 slave ,以在发生网络分区或故障时,替换 master。这些 slave 还可以缓解 master 的读请求的压力。</p><blockquote><p>重配置:Hash Slot 从一个节点转移到另一个节点</p></blockquote><p>Keys hash tags 可以破坏上述的分配规则,Hash tags 是一种保证多个键被分配到同一个槽位的方法。</p><h4 id="重定向"><a href="#重定向" class="headerlink" title="重定向"></a>重定向</h4><p>Redis Cluster 为了提高性能,不会提供代理,而是使用重定向的方式让 client 连接到正确的节点。</p><h5 id="MOVED"><a href="#MOVED" class="headerlink" title="MOVED"></a>MOVED</h5><p>Redis 客户端可以向集群的任意一个节点发送查询请求,节点接收到请求后会对其进行解析,如果是操作单个 key 的命令或者是包含多个在相同槽位 key 的命令,那么该节点就会去查找这个 key 是属于哪个槽位的。如果 key 所属的槽位由该节点提供服务,那么就直接返回结果。否则就会返回一个 <code>MOVED</code> 错误:</p><pre><code class="log">GET x-MOVED 3999 127.0.0.1:6381</code></pre><p>这个错误包括了对应的 key 属于哪个槽位(3999)以及该槽位所在的节点的 IP 地址和端口号。client 收到这个错误信息后,就将这些信息存储起来以便可以更准确的找到正确的节点。</p><p>当客户端收到 <code>MOVED</code> 错误后,可以使用 <code>CLUSTER NODES</code> 或 <code>CLUSTER SLOTS</code> 命令来更新整个集群的信息,因为当重定向发生时,很少会是单个槽位的变更,一般都会是多个槽位一起更新。因此,在收到 <code>MOVED</code> 错误时,客户端应该尽早更新集群的分布信息。当集群达到稳定状态时,客户端保存的槽位和节点的对应信息都是正确的,cluster 的性能也会达到非常高效的状态。</p><h5 id="ASK"><a href="#ASK" class="headerlink" title="ASK"></a>ASK</h5><p>对于 Redis Cluster 来讲, <code>MOVED</code> 重定向意味着请求的 slot 永久的由另一个节点提供服务,而 <code>ASK</code> 重定向仅代表将当前查询重定向到指定节点,不影响后续查询。在 Redis Cluster 迁移的时候会用到 ASK 重定向,下面看下 ASK 的处理流程:</p><ol><li>Client 向节点 A 查询数据 <code>x</code>,A 发现数据 <code>x</code> 所在的 slot 状态为 <code>MIGRATING</code>,如果 <code>x</code> 存在则返回,否则返回 <code>ASK</code> 重定向;</li><li>Client 向 <code>ASK</code> 重定向节点 B 发送 <code>ASKING</code> ,再查询数据 <code>x</code>;</li><li>B 查找 <code>x</code> 发现其所在 slot 状态为 <code>IMPORTING</code>,则 B 会进行查询。若第二步未发送 <code>ASKING</code> ,则 B 会返回 <code>MOVED</code>命令,重定向到 A;</li></ol><p>Redis Cluster 的迁移是以槽位单位的,一个槽位从节点 A 迁移到节点 B 需要经过以下步骤:</p><ol><li>节点 A 将待迁移 slot 设置为 <code>MIGRATING</code> 状态,将 B 节点 slot 设置为 <code>IMPORTING</code> 状态</li><li>A 获取 slot 中的 key,逐个调用 <code>MIGRATE</code> 命令</li><li><code>MIGRATE</code> 会将特定的 key 从 A 迁移到 B,这个过程是原子操作(A、B均会进行加锁)</li></ol><h4 id="容错能力"><a href="#容错能力" class="headerlink" title="容错能力"></a>容错能力</h4><p>Redis Cluster和大多数集群一样,是通过心跳来判断一个节点是否存活的。心跳包的内容可以分为 header 和 gossip 消息两部分,其中header包含以下信息:</p><ul><li>NODE ID 节点在集群中的唯一标识</li><li>currentEpoch 和 configEpoch 字段</li><li>node flag,标识节点是 master 还是 slave ,另外还有一些其他的标识位</li><li>节点提供服务的 hash slot 的 bitmap</li><li>发送者的 TCP 端口</li><li>发送者认为的集群状态(down or ok)</li><li>如果是slave,则包含 master 的 NODE ID</li></ul><p>gossip包含了该节点认为的其他节点的状态,不过不是集群的全部节点。具体有以下信息:</p><ul><li>NODE ID</li><li>节点的IP和端口</li><li>NODE flags</li></ul><h5 id="故障检测"><a href="#故障检测" class="headerlink" title="故障检测"></a>故障检测</h5><p>故障检测用于识别集群中的不可达节点是否已下线,如果一个 master 下线,则会将它的 slave提 升为master。如果无法提升,则集群会处于错误状态。在 gossip 消息中,<code>NODE flags</code> 的值包括两种 PFAIL 和 FAIL。</p><p>如果一个节点发现另外一个节点不可达的时间超过 <code>NODE_TIMEOUT</code> ,则会将这个节点标记为 PFAIL,也就是 Possible failure。 PFAIL 标志只是一个节点本地的信息,为了使 slave 提升为 master ,需要将 PFAIL 升级为 FAIL 。当集群中大部分节点都将某个节点标记为 PFAIL 时,则可升级为 FAIL。</p><p>FAIL 状态是单向的,只能从 PFAIL 升级为 FAIL ,当节点重新可达时,可清除 FAIL 标记。</p><h2 id="数据结构"><a href="#数据结构" class="headerlink" title="数据结构"></a>数据结构</h2><p>Redis 的数据结构包含两个层面,首先是 API 层面,即 Redis Client 操作的数据结构。另外就是 Redis 在实现 API 层面的数据结构使用的底层数据结构。</p><p>Redis API 层面的数据结构主要包括:<code>String</code>、<code>List</code>、<code>Set</code>、<code>Sorted Set</code>、<code>Hash</code>、<code>BitMap</code>,这些数据结构在 <a href="https://redis.io/topics/data-types-intro" target="_blank" rel="noopener">Redis 官方文档</a>中有详细介绍。</p><p>下面我们主要介绍 Redis 底层数据结构,包括 <code>SDS</code>、<code>dict</code>、<code>ziplist</code>、<code>quicklist</code>、<code>skiplist</code>。</p><h3 id="SDS"><a href="#SDS" class="headerlink" title="SDS"></a>SDS</h3><p>Redis 没有直接使用 C 语言传统的字符串表示(以空字符结尾的字符数组,以下简称 C 字符串), 而是自己构建了一种名为 <strong>简单动态字符串(simple dynamic string,SDS)</strong>的抽象类型, 并将 SDS 用作 Redis 的默认字符串表示。</p><p>在 Redis 里面, C 字符串只会作为字符串字面量(string literal), 用在一些无须对字符串值进行修改的地方, 比如打印日志。</p><p>当 Redis 需要的不仅仅是一个字符串字面量, 而是一个可以被修改的字符串值时, Redis 就会使用 SDS 来表示字符串值: 比如在 Redis 的数据库里面, 包含字符串值的键值对在底层都是由 SDS 实现的。</p><table><thead><tr><th>C字符串</th><th>SDS</th></tr></thead><tbody><tr><td>获取字符串长度的复杂度为 O(N) 。</td><td>获取字符串长度的复杂度为 O(1) 。</td></tr><tr><td>API 是不安全的,可能会造成缓冲区溢出。</td><td>API 是安全的,不会造成缓冲区溢出。</td></tr><tr><td>修改字符串长度 N 次必然需要执行 N 次内存重分配。</td><td>修改字符串长度 N 次最多需要执行 N 次内存重分配。</td></tr><tr><td>只能保存文本数据。</td><td>可以保存文本或者二进制数据。</td></tr><tr><td>可以使用所有 <string.h> 库中的函数。</td><td>可以使用一部分 <string.h> 库中的函数。</td></tr></tbody></table><h4 id="缓冲区溢出"><a href="#缓冲区溢出" class="headerlink" title="缓冲区溢出"></a>缓冲区溢出</h4><p>因为 C 字符串不记录自身的长度, 所以 <code>strcat</code> 假定用户在执行这个函数时, 已经为 <code>dest</code> 分配了足够多的内存, 可以容纳 <code>src</code> 字符串中的所有内容, 而一旦这个假定不成立时, 就会产生缓冲区溢出。</p><p>举个例子, 假设程序里有两个在内存中紧邻着的 C 字符串 <code>s1</code> 和 <code>s2</code> , 其中 s1 保存了字符串 <code>"Redis"</code> , 而 s2 则保存了字符串 <code>"MongoDB"</code> , 如图所示。</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/redis/images/a9832e14ba184a4049f979e521ef050b.png" alt="img"></p><p>如果一个程序员决定通过执行:</p><pre><code>strcat(s1, " Cluster");</code></pre><p>将 <code>s1</code> 的内容修改为 <code>"Redis Cluster"</code> , 但粗心的他却忘了在执行 <code>strcat</code> 之前为 <code>s1</code> 分配足够的空间, 那么在 <code>strcat</code> 函数执行之后, <code>s1</code> 的数据将溢出到 <code>s2</code> 所在的空间中, 导致 <code>s2</code> 保存的内容被意外地修改, 如图所示。</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/redis/images/cc9ac0419ae3f5059076c2d66f867931.png" alt="img"></p><p>与 <code>C</code> 字符串不同, <code>SDS</code> 的空间分配策略完全杜绝了发生缓冲区溢出的可能性: <strong>当 SDS API 需要对 SDS 进行修改时, API 会先检查 SDS 的空间是否满足修改所需的要求</strong>, 如果不满足的话, API 会自动将 <code>SDS</code> 的空间扩展至执行修改所需的大小, 然后才执行实际的修改操作, 所以使用 <code>SDS</code> 既不需要手动修改 <code>SDS</code> 的空间大小, 也不会出现前面所说的缓冲区溢出问题。</p><h4 id="减少修改字符串时带来的内存重分配次数"><a href="#减少修改字符串时带来的内存重分配次数" class="headerlink" title="减少修改字符串时带来的内存重分配次数"></a>减少修改字符串时带来的内存重分配次数</h4><ul><li>空间预分配:解决 append 问题</li><li>惰性空间释放:解决 strim 问题</li></ul><h4 id="二进制安全"><a href="#二进制安全" class="headerlink" title="二进制安全"></a>二进制安全</h4><p>C 字符串中的字符必须符合某种编码(比如 <code>ASCII</code>), 并且 <strong>除了字符串的末尾之外, 字符串里面不能包含空字符</strong>, 否则最先被程序读入的空字符将被误认为是字符串结尾 —— 这些限制使得 C 字符串只能保存文本数据, 而不能保存像图片、音频、视频、压缩文件这样的二进制数据。</p><h3 id="dict"><a href="#dict" class="headerlink" title="dict"></a>dict</h3><p>在 Redis 中, dict 也是一个基于哈希表的算法。和传统的哈希算法类似,它采用哈希函数从 key 计算得到在哈希表中的位置,采用 <strong>拉链法</strong> 解决冲突,并在装载因子(load factor)超过预定值时自动扩展内存,引发重哈希(rehashing)。</p><p>Redis 的 dict 实现最显著的一个特点,就在于它的重哈希。它采用了一种称为 <strong>增量式重哈希(incremental rehashing)</strong> 的方法,在需要扩展内存时避免一次性对所有 key 进行重哈希,而是将重哈希操作分散到对于 dict 的各个增删改查的操作中去。这种方法能做到每次只对一小部分 key 进行重哈希,而每次重哈希之间不影响 dict 的操作。</p><blockquote><p>dict 之所以这样设计,是为了避免重哈希期间单个请求的响应时间剧烈增加。</p></blockquote><p>为了实现增量式重哈希(incremental rehashing),dict的数据结构里包含 <strong>两个哈希表</strong>。在重哈希期间,数据从一个哈希表向另一个哈希表迁移。</p><h3 id="ziplist"><a href="#ziplist" class="headerlink" title="ziplist"></a>ziplist</h3><p>ziplist 是一个经过特殊编码的 <strong>双向链表</strong>,它的设计目标就是为了提高存储效率。 ziplist 可以用于存储字符串或整数,其中整数是按真正的二进制表示进行编码的,而不是编码成字符串序列。它能以 O(1)<em>O</em>(1) 的时间复杂度在表的两端提供 <code>push</code> 和 <code>pop</code> 操作。</p><p>一个普通的双向链表,链表中每一项都占用独立的一块内存,各项之间用地址指针(或引用)连接起来。这种方式会带来大量的内存碎片,而且地址指针也会占用额外的内存。而 ziplist 却是将表中每一项存放在前后 <strong>连续的地址空间</strong> 内,一个 ziplist 整体占用一大块内存。它是一个表(list),但其实不是一个链表(linked list)。</p><p>另外,ziplist 为了在细节上节省内存,对于值的存储采用了 <strong>变长编码方式</strong>,大概意思是说,对于大的整数,就多用一些字节来存储,而对于小的整数,就少用一些字节来存储。ziplist 的底层结构如下所示:</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/redis/assists/redis_ziplist_struct.png" alt="img"></p><ul><li><strong>zlbytes</strong>: 32bit,表示 ziplist 占用的字节总数。</li><li><strong>zltail</strong>: 32bit,表示 ziplist 表中最后一项(entry)在 ziplist 中的偏移字节数。</li><li><strong>zllen</strong>: 16bit, 表示 ziplist 中数据项(entry)的个数。 zllen 可以表达的最大值为 2^{16}-1216−1 。当 ziplist 长度超过 2^{16}-1216−1 时, zllen 不表示长度,长度需要进行遍历计算。</li><li><strong>entry</strong>: 表示真正存放数据的数据项,长度不定。一个数据项(entry)也有它自己的内部结构。</li><li><strong>zlend</strong>: ziplist 最后1个字节,是一个结束标记,值固定等于 255。</li></ul><p>当ziplist变得很大的时候,它有如下几个缺点:</p><ul><li>每次插入或修改引发的 realloc 操作会有更大的概率造成内存拷贝,从而降低性能。</li><li>一旦发生内存拷贝,内存拷贝的成本也相应增加,因为要拷贝更大的一块数据。</li><li>当 ziplist 数据项过多的时候,在它上面查找指定的数据项就会性能变得很低,因为 ziplist 上的查找需要进行遍历。</li></ul><p>总之, ziplist 本来就设计为各个数据项挨在一起组成连续的内存空间,这种结构并不擅长做修改操作。一旦数据发生改动,就会引发内存realloc,可能导致内存拷贝。</p><h3 id="quicklist"><a href="#quicklist" class="headerlink" title="quicklist"></a>quicklist</h3><p>quicklist 是由 ziplist 为节点组成的双向链表。 ziplist 本身也是一个能维持数据项先后顺序的列表(按插入位置),而且是一个内存紧缩的列表(各个数据项在内存上前后相邻)。比如,一个包含 3 个节点的 quicklist ,如果每个节点的 ziplist 又包含 4 个数据项,那么对外表现上,这个 list 就总共包含 12 个数据项。</p><p>quicklist 的结构为什么这样设计呢?总结起来,大概又是一个空间和时间的折中:</p><ul><li>双向链表便于在表的两端进行 push 和 pop 操作,但是它的内存开销比较大。首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。</li><li>ziplist 由于是一整块连续内存,所以存储效率很高。但是 <strong>不利于修改操作</strong>,每次数据变动都会引发一次内存的 realloc (扩容)。特别是当 ziplist 长度很长的时候,一次 realloc 可能会导致大批量的数据拷贝,进一步降低性能。</li></ul><p>quicklist 节点上的 ziplist 要保持一个合理的长度。那到底多长合理呢?这可能取决于具体应用场景。实际上,Redis提供了一个配置参数<code>list-max-ziplist-size</code> ,就是为了让使用者可以来根据自己的情况进行调整。</p><h3 id="skiplist"><a href="#skiplist" class="headerlink" title="skiplist"></a>skiplist</h3><p><strong>跳跃表(skiplist)</strong> 是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。</p><p><strong>Redis 只在两个地方用到了跳跃表, 一个是实现有序集合键, 另一个是在集群节点中用作内部数据结构</strong>, 除此之外, 跳跃表在 Redis 里面没有其他用途。</p><h3 id="intset"><a href="#intset" class="headerlink" title="intset"></a>intset</h3><p>intset 是一个由整数组成的 <strong>有序集合</strong>,从而便于在上面进行二分查找,用于快速地判断一个元素是否属于这个集合。它在内存分配上与 ziplist 有些类似,是连续的一整块内存空间,而且对于大整数和小整数(按绝对值)采取了不同的编码,尽量对内存的使用进行了优化。</p><p>对于小集合使用 intset 来存储,主要的原因是节省内存。特别是当存储的元素个数较少的时候, dict 所带来的内存开销要大得多(包含两个哈希表、链表指针以及大量的其它元数据)。所以,当存储大量的小集合而且集合元素都是数字的时候,用 intset 能节省下一笔可观的内存空间。</p><p>实际上,从时间复杂度上比较, intset 的平均情况是没有 dict 性能高的。以查找为例,intset 是 O(\lg^n)<em>O</em>(lg<em>n</em>) 的,而 dict 可以认为是 O(1)<em>O</em>(1) 的。但是,由于使用 intset 的时候集合元素个数比较少,所以这个影响不大。</p><h3 id="API数据结构的实现"><a href="#API数据结构的实现" class="headerlink" title="API数据结构的实现"></a>API数据结构的实现</h3><table><thead><tr><th>API数据结构</th><th>限制</th><th>底层数据结构</th></tr></thead><tbody><tr><td>string</td><td>512 MB</td><td>SDS</td></tr><tr><td>list</td><td>最大长度 2^{32}-1232−1</td><td>quicklist</td></tr><tr><td>set</td><td>最大容量 2^{32}-1232−1</td><td>- intset(小整数集) - dict</td></tr><tr><td>sort set</td><td>最大容量 2^{32}-1232−1</td><td>- ziplist(小集合) - dict + skiplist</td></tr><tr><td>hash</td><td>最大KV容量 2^{32}-1232−1</td><td>- ziplist(小集合) - dict</td></tr><tr><td>bitmap</td><td>512 MB</td><td>SDS</td></tr></tbody></table><h2 id="缓存穿透、缓存击穿、缓存雪崩"><a href="#缓存穿透、缓存击穿、缓存雪崩" class="headerlink" title="缓存穿透、缓存击穿、缓存雪崩"></a>缓存穿透、缓存击穿、缓存雪崩</h2><h3 id="缓存穿透"><a href="#缓存穿透" class="headerlink" title="缓存穿透"></a>缓存穿透</h3><p>访问一个不存在的key,缓存不起作用,请求会穿透到 DB,流量大时 DB 会挂掉。</p><h4 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h4><ul><li>采用布隆过滤器,使用一个足够大的<code>bitmap</code>,用于存储可能访问的 <code>key</code>,不存在的key直接被过滤;</li><li>访问key未在DB查询到值,也将空值写进缓存,但可以设置较短过期时间。</li></ul><h3 id="缓存雪崩"><a href="#缓存雪崩" class="headerlink" title="缓存雪崩"></a>缓存雪崩</h3><p>大量的 key 设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。</p><h4 id="解决方案-1"><a href="#解决方案-1" class="headerlink" title="解决方案"></a>解决方案</h4><p>可以给缓存设置过期时间时加上一个随机值时间,使得每个 key 的过期时间分布开来,不会集中在同一时刻失效。</p><h3 id="缓存击穿"><a href="#缓存击穿" class="headerlink" title="缓存击穿"></a>缓存击穿</h3><p>对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一 key 缓存,前者则是很多key。</p><p>缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端 DB 压垮。</p><h4 id="解决方案-2"><a href="#解决方案-2" class="headerlink" title="解决方案"></a>解决方案</h4><p>在缓存失效的时候(判断拿出来的值为空),不是立即去 <code>load db</code> ,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的 <code>SETNX</code>)去 <code>set</code> 一个 <code>mutex key</code> ,当操作返回成功时,再进行 <code>load db</code> 的操作并回设缓存;否则,就重试整个 <code>get</code> 缓存的方法。</p><h2 id="数据淘汰机制"><a href="#数据淘汰机制" class="headerlink" title="数据淘汰机制"></a>数据淘汰机制</h2><h3 id="对象过期"><a href="#对象过期" class="headerlink" title="对象过期"></a>对象过期</h3><p>Redis回收过期对象的策略:定期删除+惰性删除</p><ul><li><strong>惰性删除</strong>:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key</li><li><strong>定期删除</strong>:由于惰性删除策略无法保证冷数据被及时删掉,所以Redis会定期主动淘汰一批已过期的key</li></ul><h3 id="内存淘汰"><a href="#内存淘汰" class="headerlink" title="内存淘汰"></a>内存淘汰</h3><p>Redis提供了下面几种淘汰策略供用户选择,其中默认的策略为noeviction策略:</p><ul><li><code>noeviction</code>:当内存使用达到阈值的时候,所有引起申请内存的命令会报错。</li><li><code>allkeys-lru</code>:在主键空间中,优先移除最近未使用的key。</li><li><code>volatile-lru</code>:在设置了过期时间的键空间中,优先移除最近未使用的key。</li><li><code>allkeys-random</code>:在主键空间中,随机移除某个key。</li><li><code>volatile-random</code>:在设置了过期时间的键空间中,随机移除某个key。</li><li><code>volatile-ttl</code>:在设置了过期时间的键空间中,具有更早过期时间的key优先移除。</li></ul><blockquote><p>这里补充一下主键空间和设置了过期时间的键空间,举个例子,假设我们有一批键存储在Redis中,则有那么一个哈希表用于存储这批键及其值,如果这批键中有一部分设置了过期时间,那么这批键还会被存储到另外一个哈希表中,这个哈希表中的值对应的是键被设置的过期时间。设置了过期时间的键空间为主键空间的子集。</p></blockquote><h4 id="非精准的LRU"><a href="#非精准的LRU" class="headerlink" title="非精准的LRU"></a>非精准的LRU</h4><p>上面提到的LRU(Least Recently Used)策略,实际上 <strong>Redis 实现的 LRU 并不是可靠的 LRU</strong>,也就是名义上我们使用LRU算法淘汰键,但是实际上被淘汰的键并不一定是真正的最久没用的,这里涉及到一个权衡的问题,如果需要在全部键空间内搜索最优解,则必然会增加系统的开销,Redis是单线程的,也就是同一个实例在每一个时刻只能服务于一个客户端,所以耗时的操作一定要谨慎。</p><p>为了在一定成本内实现相对的LRU,早期的 Redis 版本是 <strong>基于采样的 LRU</strong> ,也就是放弃全部键空间内搜索解改为采样空间搜索最优解。自从 Redis3.0 版本之后,Redis 作者对于基于采样的 LRU 进行了一些优化,目的是在一定的成本内让结果更靠近真实的 LRU。</p><h2 id="源码分析"><a href="#源码分析" class="headerlink" title="源码分析"></a>源码分析</h2><h2 id="Redis-应用场景"><a href="#Redis-应用场景" class="headerlink" title="Redis 应用场景"></a>Redis 应用场景</h2><p><a href="https://learnku.com/articles/33421" target="_blank" rel="noopener">应用场景</a></p><p><a href="https://www.lycecho.com/archives/14478" target="_blank" rel="noopener">redis作缓存的应用场景</a></p>]]></content>
<categories>
<category> Database </category>
</categories>
</entry>
<entry>
<title>Linux应用</title>
<link href="/59742.html"/>
<url>/59742.html</url>
<content type="html"><![CDATA[<h1 id="Linux-概览"><a href="#Linux-概览" class="headerlink" title="Linux 概览"></a>Linux 概览</h1><h1 id="Linux-常用命令"><a href="#Linux-常用命令" class="headerlink" title="Linux 常用命令"></a>Linux 常用命令</h1><h2 id="操作文件以及目录命令"><a href="#操作文件以及目录命令" class="headerlink" title="操作文件以及目录命令"></a>操作文件以及目录命令</h2><p>ls:</p><p>pwd:</p><p>clear:</p><p>cd:</p><p>mkdir:</p><p>cp:</p><p>mv:</p><p>rm:</p><p>touch:创建一个空文件</p><p><strong>tar:</strong>解压缩文件</p><p> 常用参数:</p><p> -c 建立一个压缩文件的参数指令(create)</p><p> -x 解开一个压缩文件的参数指令(extract)</p><p> -z 是否需要用 gzip 压缩</p><p> -v 压缩的过程中显示文件(verbose)</p><p> -f 使用档名,在 f 之后要立即接档名(file)</p><p>cat: </p><p>echo:显示变量的值;将内容写入指定文件,如果目标文件不存在,同时会把文件创建出来。</p><p>head/tail:查看文本中开头或结尾部分的内容</p><p>In:建立链接文件</p><p> 常用参数:</p><p> -s 对源文件建立符号连接,而非硬连接(symbolic)</p><h2 id="系统命令"><a href="#系统命令" class="headerlink" title="系统命令"></a>系统命令</h2><p>find:在文件系统中查找指定的文件</p><p> 语法:find 目录 -name 文件名称</p><p><strong>grep</strong>:在指定的文本文件中查找指定的字符串;</p><p> 语法:grep 文本内容 文件名称</p><p> 例子:grep linux install.log</p><p> 在前边命令的结果中查找内容;</p><p> 语法:命令 | grep 文本内容</p><p> 例子:ll | grep install</p><p>|:管道</p><p> 例子:ls -l | grep java</p><p> ls -l 命令的输出,会作为grep java命令的输入。</p><p><strong>ps</strong></p><p> 用途:显示瞬间的进程状态</p><p> 常用参数:</p><p> -a 显示所有用户的所有进程(包括其它用户)</p><p> -u 按用户名和启动时间的顺序来显示进程</p><p> -x 显示无控制终端的进程</p><p> 例子:ps aux | grep java</p><p> <strong>ps -ef | grep java</strong></p><p><strong>kill</strong></p><p> 用途:杀死一个进程</p><p> 语法:kill pid或者kill -9 pid(强制终止)</p><p>su:</p><p> 用途:切换用户(使用su命令切换用户时,需要知道目标用户的密码)</p><p> 语法:su -<user> 或者 su <user></p><p> 例子:su itcast</p><p>sudo:</p><p> 用途:获取临时目标用户权限,默认是root用户(使用sudo命令获取临时,不需要知道目标用户的密码,只需要输入当前用户密码)</p><p> 语法:sudo cd /root</p><p> </p><p>whoami:</p><p> 用途:查看当前用户</p><p> 语法:whoami</p><p>which:</p><p> 用途:查看可执行文件在哪</p><p> 语法:which 命令</p><p> 例子:which mkdir</p><p>mount:</p><p> 用途:挂载外设、挂载目录</p><p> 语法:mount <源目录> <目标目录></p><p> 例子:mount /usr/local/mnt /mnt/temp</p><p>dirname:</p><p> 用途:显示文件所在目录</p><p> 语法:dirname <目录/文件></p><p> 例子:dirname /usr/local</p><p>top:</p><p> 用途:显示当前系统中耗费资源最多的进程 </p><p> 语法:top</p><p>Ctrl+c:</p><p>df:</p><p> 用途:显示文件系统磁盘空间的使用情况</p><p>hostname:</p><p> 用途:查看当前主机名</p><p> 语法:hostname</p><p>free:</p><p> 用途:显示当前内存和交换空间的使用情况</p><p>shutdown:</p><p> 用途:关机/重启</p><p> 常用参数:</p><p> -r 关机后立即重启</p><p> -h 关机后不重新启动</p><p> 例子:shutdown -r 10 10分钟后重新启动</p><p> halt 关机后关闭电源 </p><p> reboot 重新启动</p><h2 id="帮助命令"><a href="#帮助命令" class="headerlink" title="帮助命令"></a>帮助命令</h2><p>help</p><p> 用途:查看帮助文档</p><p> 语法:help 命令</p><p> 例子:help if</p><h2 id="固定IP网络配置"><a href="#固定IP网络配置" class="headerlink" title="固定IP网络配置"></a>固定IP网络配置</h2><h1 id="VIM-文本编辑器"><a href="#VIM-文本编辑器" class="headerlink" title="VIM 文本编辑器"></a>VIM 文本编辑器</h1><h2 id="VIM-工作模式"><a href="#VIM-工作模式" class="headerlink" title="VIM 工作模式"></a>VIM 工作模式</h2><p><img src="/59742/vim_mode.png" alt="vim_mode"></p><h2 id="插入命令"><a href="#插入命令" class="headerlink" title="插入命令"></a>插入命令</h2><h2 id="定位命令"><a href="#定位命令" class="headerlink" title="定位命令"></a>定位命令</h2><h2 id="删除命令"><a href="#删除命令" class="headerlink" title="删除命令"></a>删除命令</h2><h2 id="复制粘贴"><a href="#复制粘贴" class="headerlink" title="复制粘贴"></a>复制粘贴</h2><h2 id="替换和取消"><a href="#替换和取消" class="headerlink" title="替换和取消"></a>替换和取消</h2><h2 id="退出命令"><a href="#退出命令" class="headerlink" title="退出命令"></a>退出命令</h2><h1 id="用户和组账户管理"><a href="#用户和组账户管理" class="headerlink" title="用户和组账户管理"></a>用户和组账户管理</h1><h2 id="操作用户命令"><a href="#操作用户命令" class="headerlink" title="操作用户命令"></a>操作用户命令</h2><h2 id="操作用户组命令"><a href="#操作用户组命令" class="headerlink" title="操作用户组命令"></a>操作用户组命令</h2><h1 id="权限管理"><a href="#权限管理" class="headerlink" title="权限管理"></a><strong>权限管理</strong></h1><h2 id="三种基本权限"><a href="#三种基本权限" class="headerlink" title="三种基本权限"></a>三种基本权限</h2><p> <img src="/59742/image_right_stratagy.png" alt="image_right_stratagy"></p><h2 id="更改操作权限"><a href="#更改操作权限" class="headerlink" title="更改操作权限"></a>更改操作权限</h2><p>语法:<strong>chmod [options] mode files</strong></p><p>注意:只能[文件属主]或[特权用户]才能进行文件权限的更改。</p><p>l mode</p><p>n 可以是数字形式</p><p>n 可以用who opcode permission英文表达式表示。</p><p>注意:可指定多个mode,以逗号分隔。</p><p>l <strong>options</strong></p><p>n -c,–changes</p><p>只输出被改变文件的信息</p><p>n -f,–silent,–quiet</p><p>当chmod不能改变文件模式时,不通知文件的用户</p><p>n –help</p><p>输出帮助信息。</p><p>n -R,–recursive</p><p>可递归遍历子目录,把修改应到目录下所有文件和子目录</p><p>n –reference=filename</p><p>参照filename的权限来设置权限</p><p>n -v,–verbose</p><p>无论修改是否成功,输出每个文件的信息</p><p>n –version</p><p>输出版本信息。</p><h3 id="通过who-opcode-permission英文表达式修改权限"><a href="#通过who-opcode-permission英文表达式修改权限" class="headerlink" title="通过who opcode permission英文表达式修改权限"></a>通过who opcode permission英文表达式修改权限</h3><p><strong>who</strong></p><p>u:用户</p><p>g:组</p><p>o:其它</p><p>a:所有用户(默认)</p><p><strong>opcode</strong></p><p>+:增加权限</p><p>-:删除权限</p><p>=:重新分配权限</p><p><strong>permission</strong></p><p>r:读</p><p>w:写</p><p>x:执行</p><p>s:设置用户(或组)的ID号</p><p>t:设置粘着位(sticky bit),防止文件或目录被非属主删除</p><p>u:用户的当前权限</p><p>g:组的当前权限</p><p>o:其他用户的当前权限</p><p>命令演示:</p><p>[root@redis01 ~]# chmod <strong>u+x</strong> file</p><h3 id="通过数字修改权限"><a href="#通过数字修改权限" class="headerlink" title="通过数字修改权限"></a>通过数字修改权限</h3><p><strong>可以使用三位[**</strong>八进制数字]<strong>**来表示权限</strong>,比如:777,751等,就可以表示权限。</p><p>第一位数字指定属主的权限</p><p>第二位数字指定用户组的权限</p><p>第三位数字指定其他用户的权限。</p><p>每个位置的数字是通过4(读)、2(写)、1(执行)三种数值的和来确定的。</p><p>如:</p><p>6(4+2)代表有读、写权限。</p><p>7(4+2+1)代表有读、写和执行的权限。</p><p>还可设置第四位,它位于三位权限序列的前面,第四位数字取值是4,2,1,代表意思如下:</p><p>4:执行时设置用户ID,用于授权给基于文件属主的进程,而不是给创建此进程的用户。</p><p>2:执行时设置用户组ID,用于授权给基于文件所在组的进程,而不是基于创建此进程的用户。</p><p>1:设置粘着位。</p><p>示例:[root@hyl01 ~]# chmod 751 house</p><h3 id="常用修该权限方式"><a href="#常用修该权限方式" class="headerlink" title="常用修该权限方式"></a>常用修该权限方式</h3><p>$ chmod u+x file 给file的属主增加执行权限</p><p>$ chmod 751 file 给file的属主分配读、写、执行(7)的权限,给file的所在组分配读、执行(5)的权限,给其他用户分配执行(1)的权限</p><p>$ chmod u=rwx,g=rx,o=x file 上例的另一种形式</p><p>$ chmod =r file 为所有用户分配读权限</p><p>$ chmod 444 file 同上例</p><p>$ chmod a-wx,a+r file 同上例</p><p>$ chmod -R u+r directory 递归地给directory目录下所有文件和子目录的属主分配读的权限</p><p>$ chmod 4755 设置用户ID,给属主分配读、写和执行权限,给组和其他用户分配读、执行的权限,并且该文件除了属主,不能被其用户进行删除或移动操作。</p><h1 id="软件安装命令"><a href="#软件安装命令" class="headerlink" title="软件安装命令"></a>软件安装命令</h1><h2 id="软件安装"><a href="#软件安装" class="headerlink" title="软件安装"></a>软件安装</h2><h2 id="防火墙"><a href="#防火墙" class="headerlink" title="防火墙"></a>防火墙</h2><p> 永久关闭</p><p> 临时关闭</p><p> 修该防火墙规则</p><h1 id="Shell脚本编写"><a href="#Shell脚本编写" class="headerlink" title="Shell脚本编写"></a>Shell脚本编写</h1><p><a href="https://haldir65.github.io/2018/11/04/2018-11-04-how-to-write-shell-scripts/" target="_blank" rel="noopener">编写shell脚本教程</a></p><h1 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h1><p>1.<a href="https://www.cnblogs.com/fullhouse/archive/2011/07/17/2108786.html" target="_blank" rel="noopener">Linux统计文本行数</a></p>]]></content>
<categories>
<category> Basic </category>
</categories>
</entry>
<entry>
<title>数据库</title>
<link href="/63394.html"/>
<url>/63394.html</url>
<content type="html"><![CDATA[<h1 id="InnoDB-索引"><a href="#InnoDB-索引" class="headerlink" title="InnoDB 索引"></a>InnoDB 索引</h1><h2 id="数据存储"><a href="#数据存储" class="headerlink" title="数据存储"></a>数据存储</h2><p>当 InnoDB 存储数据时,它可以使用不同的行格式进行存储;MySQL 5.7 版本支持以下格式的行存储方式:</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/mysql/innodb/index/images/81407aa14450d2d6fac1a70961880aac.png" alt="img"></p><blockquote><p><code>Antelope</code> 是 InnoDB 最开始支持的文件格式,它包含两种行格式 <code>Compact</code> 和 <code>Redundant</code> ,它最开始并没有名字; <code>Antelope</code> 的名字是在新的文件格式 <code>Barracuda</code> 出现后才起的, <code>Barracuda</code> 的出现引入了两种新的行格式 <code>Compressed</code> 和 <code>Dynamic</code> ;InnoDB 对于文件格式都会向前兼容,而官方文档中也对之后会出现的新文件格式预先定义好了名字:Cheetah、Dragon、Elk 等等。</p></blockquote><p>两种行记录格式 <code>Compact</code> 和 <code>Redundant</code> 在磁盘上按照以下方式存储:</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/mysql/innodb/index/images/a18d600fb632031a00937b1e667e446e.png" alt="img"></p><p><code>Compact</code> 和 <code>Redundant</code> 格式最大的不同就是记录格式的第一个部分;在 <code>Compact</code> 中,行记录的第一部分倒序存放了一行数据中列的长度(Length),而 <code>Redundant</code> 中存的是每一列的偏移量(Offset),从总体上上看, <code>Compact</code> 行记录格式相比 <code>Redundant</code> 格式能够减少 <code>20%</code> 的存储空间。</p><h3 id="行溢出数据"><a href="#行溢出数据" class="headerlink" title="行溢出数据"></a>行溢出数据</h3><p>当 InnoDB 使用 <code>Compact</code> 或者 <code>Redundant</code> 格式存储极长的 <code>VARCHAR</code> 或者 <code>BLOB</code> 这类大对象时,我们并不会直接将所有的内容都存放在数据页节点中,而是将数据中的前 <code>768</code> 个字节存储在数据页中,后面会通过偏移量指向溢出页(off-page),最大768字节的作用是便于创建 <strong>前缀索引</strong>。溢出页(off-page)不存储在 B+tree 中,<strong>使用的是uncompress BLOB page,并且每个字段的溢出都是存储独享</strong>。</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/mysql/innodb/index/images/19af5612d981bf2ae2ea7d5b5b9b26ac.png" alt="img"></p><p>但是当我们使用新的行记录格式 <code>Compressed</code> 或者 <code>Dynamic</code> 时都只会在行记录中保存 <code>20</code> 个字节的指针,实际的数据都会存放在溢出页面中。</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/mysql/innodb/index/images/f7dc83f1b5cfb5f428adc404ce3cfa13.png" alt="image"></p><p>当然在实际存储中,可能会对不同长度的 TEXT 和 BLOB 列进行优化。</p><blockquote><p>想要了解更多与 InnoDB 存储引擎中记录的数据格式的相关信息,可以阅读 <a href="https://dev.mysql.com/doc/internals/en/innodb-record-structure.html" target="_blank" rel="noopener">InnoDB Record Structure</a></p></blockquote><h3 id="数据页结构"><a href="#数据页结构" class="headerlink" title="数据页结构"></a>数据页结构</h3><p>与现有的大多数存储引擎一样,InnoDB 使用页作为磁盘管理的最小单位;数据在 InnoDB 存储引擎中都是按行存储的,每个 <code>16KB</code> 大小的页中可以存放 <code>2-200</code> 行的记录。</p><p>页是 InnoDB 存储引擎管理数据的最小磁盘单位,而 <code>B-Tree</code> 节点就是实际存放表中数据的页面,我们在这里将要介绍页是如何组织和存储记录的;首先,一个 InnoDB 页有以下七个部分:</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/mysql/innodb/index/images/771f5daaf406ec0990ca339c9a594bec.png" alt="image"></p><p>每一个页中包含了两对 <code>header/trailer</code>:内部的 <code>Page Header/Page Directory</code> 关心的是页的状态信息,而 <code>Fil Header/Fil Trailer</code> 关心的是记录页的头信息。</p><p>在页的头部和尾部之间就是用户记录和空闲空间了,每一个数据页中都包含 <code>Infimum</code> 和 <code>Supremum</code> 这两个虚拟的记录(可以理解为占位符), <code>Infimum</code> 记录是比该页中任何主键值都要小的值, <code>Supremum</code> 是该页中的最大值:</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/mysql/innodb/index/images/85f36113b83bba8aa1ceb1d75bc97271.png" alt="image"></p><p><code>User Records</code> 就是整个页面中真正用于存放行记录的部分,而 <code>Free Space</code> 就是空余空间了,它是一个链表的数据结构,为了保证插入和删除的效率,整个页面并不会按照主键顺序对所有记录进行排序,它会自动从左侧向右寻找空白节点进行插入,行记录在物理存储上并不是按照顺序的,它们之间的顺序是由 <code>next_record</code> 这一指针控制的。</p><p><code>B+</code> 树在查找对应的记录时,并不会直接从树中找出对应的行记录,它只能获取记录所在的页,将整个页加载到内存中,再通过 <code>Page Directory</code> 中存储的稀疏索引和 <code>n_owned、next_record</code> 属性取出对应的记录,不过因为这一操作是在内存中进行的,所以通常会忽略这部分查找的耗时。这样就存在一个命中率的问题,如果一个page中能够相对的存放足够多的行,那么命中率就会相对高一些,性能就会有提升。</p><p>B+树底层的叶子节点为一双向链表,因此 <strong>每个页中至少应该有两行记录</strong>,这就决定了 InnoDB 在存储一行数据的时候不能够超过 <code>8kb</code>,但事实上应该更小,因为还有一些 InnoDB 内部数据结构要存储。</p><p>通常我们认为 <code>blob</code> 这类的大对象的存储会把数据存放在 off-page,其实不然,<strong>关键点还是要看一个 page 中到底能否存放两行数据,blob 可以完全存放在数据页中(单行长度没有超过 <code>8kb</code>),而 <code>varchar</code> 类型的也有可能存放在溢出页中(单行长度超过 <code>8kb</code>,前 <code>768byte</code> 存放在数据页中)</strong>。</p><h2 id="索引"><a href="#索引" class="headerlink" title="索引"></a>索引</h2><p>索引是数据库中非常非常重要的概念,它是存储引擎能够快速定位记录的秘密武器,对于提升数据库的性能、减轻数据库服务器的负担有着非常重要的作用;<strong>索引优化是对查询性能优化的最有效手段</strong>,它能够轻松地将查询的性能提高几个数量级。</p><h3 id="索引优缺点"><a href="#索引优缺点" class="headerlink" title="索引优缺点"></a>索引优缺点</h3><h4 id="优点"><a href="#优点" class="headerlink" title="优点"></a>优点</h4><ul><li><p>索引大大减小了服务器需要扫描的数据量</p></li><li><p>索引可以帮助服务器避免排序和临时表</p></li><li><p>索引可以将随机IO变成顺序IO</p></li><li><p>索引对于InnoDB(对索引支持行级锁)非常重要,因为它可以让查询锁更少的元组。在MySQL5.1和更新的版本中,InnoDB可以在服务器端过滤掉行后就释放锁,但在早期的MySQL版本中,InnoDB直到事务提交时才会解锁。对不需要的元组的加锁,会增加锁的开销,降低并发性。 InnoDB仅对需要访问的元组加锁,而索引能够减少InnoDB访问的元组数。但是只有在存储引擎层过滤掉那些不需要的数据才能达到这种目的。一旦索引不允许InnoDB那样做(即索引达不到过滤的目的),MySQL服务器只能对InnoDB返回的数据进行WHERE操作,此时,已经无法避免对那些元组加锁了。如果查询不能使用索引,MySQL会进行全表扫描,并锁住每一个元组,不管是否真正需要。</p></li><li><ul><li>关于InnoDB、索引和锁:InnoDB在二级索引上使用共享锁(读锁),但访问主键索引需要排他锁(写锁)<h4 id="缺点"><a href="#缺点" class="headerlink" title="缺点"></a>缺点</h4></li></ul></li><li><p>虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE。因为更新表时,MySQL不仅要保存数据,还要保存索引文件。</p></li><li><p>建立索引会占用磁盘空间的索引文件。一般情况这个问题不太严重,但如果你在一个大表上创建了多种组合索引,索引文件的会膨胀很快。</p></li><li><p>如果某个数据列包含许多重复的内容,为它建立索引就没有太大的实际效果。</p></li><li><p>对于非常小的表,大部分情况下简单的全表扫描更高效;</p></li></ul><p>如果MySQL有大数据量的表,就为最经常查询和最经常排序的数据列建立索引或者优化查询语句,但是,索引只是提高效率的一个因素。</p><p>MySQL里同一个数据表里的索引总数限制为16个</p><h3 id="B-树"><a href="#B-树" class="headerlink" title="B+树"></a>B+树</h3><p>InnoDB 存储引擎在绝大多数情况下使用 B+ 树建立索引,这是关系型数据库中查找最为常用和有效的索引,但是 <strong>B+ 树索引并不能找到一个给定键对应的具体值,它只能找到数据行对应的页</strong>,然后正如上一节所提到的,数据库把整个页读入到内存中,并在内存中查找具体的数据行。</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/mysql/innodb/index/images/c60f9c70aa5f25cea0f109f4064e13ab.png" alt="img"></p><p>B+ 树是平衡树,它查找任意节点所耗费的时间都是完全相同的,比较的次数就是 B+ 树的高度;</p><blockquote><p><code>B+</code> 树的叶子节点存放所有指向关键字的指针,节点内部关键字记录和节点之间都根据关键字的大小排列。当顺序递增插入的时候,只有最后一个节点会在满掉的时候引起索引分裂,此时无需移动记录,只需创建一个新的节点即可。而当非递增插入的时候,会使得旧的节点分裂,还可能伴随移动记录,以便使得新数据能够插入其中。<strong>一般建议使用一列顺序递增的 ID 来作为主键</strong>,但不必是数据库的 <code>autoincrement</code> 字段,只要满足顺序增加即可,如 <code>twitter</code> 的 <code>snowflake</code> 即为顺序递增的 ID 生成器。</p></blockquote><h4 id="B-树的高度"><a href="#B-树的高度" class="headerlink" title="B+ 树的高度"></a>B+ 树的高度</h4><p>这里我们先假设 B+ 树高为2,即存在一个根节点和若干个叶子节点,那么这棵 B+ 树的存放总记录数为:根节点指针数*单个叶子节点记录行数。这里假设一行记录的大小为1k,那么一个页上的能放 16 行数据。假设主键ID为 bigint 类型,长度为 8 字节,而指针大小在 InnoDB 源码中设置为 6 字节,这样一共14字节,那么可以算出一棵高度为 2 的 B+ 树,能存放 16 \times 1024\div 14\times 16=1872016×1024÷14×16=18720 条这样的数据记录。</p><p>根据同样的原理我们可以算出一个高度为3的B+树可以存放: 1170\times 1170\times 16=21,902,4001170×1170×16=21,902,400 条这样的记录。所以在 InnoDB 中 B+ 树高度一般为 1~3 层,它就能满足千万级的数据存储。</p><h3 id="聚集索引"><a href="#聚集索引" class="headerlink" title="聚集索引"></a>聚集索引</h3><p>InnoDB 存储引擎中的表都是使用索引组织的,也就是按照键的顺序存放;聚集索引就是按照表中主键的顺序构建一颗 B+ 树,并在叶节点中存放表中的行记录数据。</p><blockquote><p>如果没有定义主键,则会使用非空的 UNIQUE键 做主键 ; 如果没有非空的 UNIQUE键 ,则系统生成一个6字节的 <code>rowid</code> 做主键;</p></blockquote><pre><code>CREATE TABLE users( id INT NOT NULL, first_name VARCHAR(20) NOT NULL, last_name VARCHAR(20) NOT NULL, age INT NOT NULL, PRIMARY KEY(id), KEY(last_name, first_name, age) KEY(first_name));</code></pre><p>如果使用上面的 SQL 在数据库中创建一张表,B+ 树就会使用 id 作为索引的键,并在叶子节点中存储一条记录中的所有信息。</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/mysql/innodb/index/images/4bc2f4c58303c2b20751ff20cd692d33.png" alt="img"></p><blockquote><p>图中对 B+ 树的描述与真实情况下 B+ 树中的数据结构有一些差别,不过这里想要表达的主要意思是:<strong>聚集索引叶节点中保存的是整条行记录,而不是其中的一部分</strong>。</p></blockquote><p>聚集索引与表的物理存储方式有着非常密切的关系,所有正常的表应该 <strong>有且仅有一个</strong> 聚集索引(绝大多数情况下都是主键),表中的所有行记录数据都是按照 <strong>聚集索引</strong> 的顺序存放的。</p><p>当我们使用聚集索引对表中的数据进行检索时,可以直接获得聚集索引所对应的整条行记录数据所在的页,不需要进行第二次操作。</p><h3 id="非聚集索引MyISAM-MySQL-5-5之前"><a href="#非聚集索引MyISAM-MySQL-5-5之前" class="headerlink" title="非聚集索引MyISAM(MySQL 5.5之前)"></a>非聚集索引MyISAM(MySQL 5.5之前)</h3><p><a href="https://juejin.im/post/6844903764273397768" target="_blank" rel="noopener">为何使用InnoDB,而非MyISAM</a></p><p><a href="https://blog.csdn.net/xlgen157387/article/details/68978320" target="_blank" rel="noopener">MySQL存储引擎MyISAM与InnoDB区别总结整理</a></p><h3 id="辅助索引"><a href="#辅助索引" class="headerlink" title="辅助索引"></a>辅助索引</h3><p>数据库将 <strong>所有的非聚集索引都划分为辅助索引</strong>,但是这个概念对我们理解辅助索引并没有什么帮助;辅助索引也是通过 B+ 树实现的,但是它的叶节点并不包含行记录的全部数据,仅包含索引中的所有键和一个用于查找对应行记录的『书签』,在 InnoDB 中这个书签就是当前记录的主键。</p><p>辅助索引的存在并不会影响聚集索引,因为聚集索引构成的 B+ 树是数据实际存储的形式,而辅助索引只用于加速数据的查找,所以一张表上往往有多个辅助索引以此来提升数据库的性能。</p><blockquote><p>一张表一定包含一个聚集索引构成的 B+ 树以及若干辅助索引的构成的 B+ 树。</p></blockquote><p><img src="https://hadyang.github.io/interview/docs/basic/database/mysql/innodb/index/images/1bef5c5161044e2cf889574577eef6c9.png" alt="img"></p><p>如果在表 <code>users</code> 中存在一个辅助索引 (<code>first_name, age</code>),那么它构成的 B+ 树大致就是上图这样,按照 (first_name, age) 的字母顺序对表中的数据进行排序,当查找到主键时,再通过聚集索引获取到整条行记录。</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/mysql/innodb/index/images/2f31d7b8720a113ae5a7ed3c48a1c9d4.png" alt="img"></p><p>上图展示了一个使用辅助索引查找一条表记录的过程:通过辅助索引查找到对应的主键,最后在聚集索引中使用主键获取对应的行记录,这也是通常情况下行记录的查找方式。</p><h3 id="覆盖索引"><a href="#覆盖索引" class="headerlink" title="覆盖索引"></a>覆盖索引</h3><p>聚簇索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录,这种行为被称之为 <strong>回表</strong>。回表会导致查询时多次读取磁盘,为减少IO MySQL 在辅助索引上进行优化,将辅助索引作为 <strong>覆盖索引</strong>(Covering index)。在查询的时候,如果 <code>SELECT</code> 子句中的字段为主键、辅助索引的键则不进行回表。</p><h3 id="联合索引"><a href="#联合索引" class="headerlink" title="联合索引"></a>联合索引</h3><p><a href="https://juejin.im/post/6844904073955639304" target="_blank" rel="noopener">联合索引在B+树上的存储结构及数据查找方式</a></p><h2 id="索引失效"><a href="#索引失效" class="headerlink" title="索引失效"></a>索引失效</h2><p>索引并不是时时都会生效的,比如以下几种情况,将导致索引失效:</p><ol><li>如果条件中有 or,即使其中有条件带索引也不会使用。要想使用or,又想让索引生效,只能将 or 条件中的每个列都加上索引</li><li>对于多列索引,不是使用的最左匹配,则不会使用索引。</li><li>如果 mysql 估计使用全表扫描要比使用索引快,则不使用索引。例如,使用<code><></code>、<code>not in</code> 、<code>not</code> <code>exist</code>,对于这三种情况大多数情况下认为结果集很大,MySQL 就有可能不使用索引。</li></ol><h2 id="索引使用(执行顺序)"><a href="#索引使用(执行顺序)" class="headerlink" title="索引使用(执行顺序)"></a>索引使用(执行顺序)</h2><ul><li>(7) - SELECT</li><li>(8) - DISTINCT <select_list></li><li>(1) - FROM <left_table></li><li>(3) - <join_type> JOIN <right_table></li><li>(2) - ON <join_condition></li><li>(4) - WHERE <where_condition></li><li>(5) - GROUP BY <group_by_list></li><li>(6) - HAVING <having_condition></li><li>(9) - ORDER BY <order_by_condition></li><li>(10) - LIMIT <limit_number></li></ul><p>关于 SQL 语句的执行顺序,有三个值得我们注意的地方:</p><ul><li><strong>FROM 才是 SQL 语句执行的第一步,并非 SELECT</strong>。 数据库在执行 SQL 语句的第一步是将数据从硬盘加载到数据缓冲区中,以便对这些数据进行操作。</li><li><strong>SELECT 是在大部分语句执行了之后才执行的,严格的说是在 FROM 和 GROUP BY 之后执行的</strong>。理解这一点是非常重要的,这就是你不能在 WHERE 中使用在 SELECT 中设定别名的字段作为判断条件的原因。</li><li><strong>无论在语法上还是在执行顺序上, UNION 总是排在在 ORDER BY 之前</strong>。很多人认为每个 UNION 段都能使用 ORDER BY 排序,但是根据 SQL 语言标准和各个数据库 SQL 的执行差异来看,这并不是真的。尽管某些数据库允许 SQL 语句对子查询(subqueries)或者派生表(derived tables)进行排序,但是这并不说明这个排序在 UNION 操作过后仍保持排序后的顺序。</li></ul><p>虽然SQL的逻辑查询是根据上述进行查询,但是数据库也许并不会完全按照逻辑查询处理的方式来进行查询。 MySQL 数据库有两个组件 <code>Parser</code>(分析SQL语句)和 <code>Optimizer</code>(优化)。</p><p>从官方手册上看,可以理解为, <code>MySQL</code> 采用了基于开销的优化器,以确定处理查询的最解方式,也就是说执行查询之前,都会先选择一条自以为最优的方案,然后执行这个方案来获取结果。在很多情况下, <code>MySQL</code> 能够计算最佳的可能查询计划,但在某些情况下, <code>MySQL</code> 没有关于数据的足够信息,或者是提供太多的相关数据信息,估测就不那么友好了。</p><p>存在索引的情况下,优化器优先使用条件用到索引且最优的方案。<strong>当 SQL 条件有多个索引可以选择, MySQL 优化器将直接使用效率最高的索引执行</strong>。</p><h2 id="索引优化"><a href="#索引优化" class="headerlink" title="索引优化"></a>索引优化</h2><h3 id="Explain-关键字"><a href="#Explain-关键字" class="headerlink" title="Explain 关键字"></a>Explain 关键字</h3><p><strong>使用EXPLAIN关键字可以模拟优化器执行SQL语句,从而知道MySQL是 如何处理你的SQL语句的。分析你的查询语句或是结构的性能瓶颈</strong> </p><p>(具体展开,见笔记)</p><h3 id="具体优化方法"><a href="#具体优化方法" class="headerlink" title="具体优化方法"></a>具体优化方法</h3><p>1.在执行常量等值查询时,改变索引列的顺序并不会更改explain的执行结果,因为mysql底层优化器会进行优化,但是推荐按照索引顺序列编写sql语句。</p><p>2.范围右边索引列失效,但是范围当前位置的索引是有效的。</p><p>3-具体见笔记</p><h2 id="慢查询优化"><a href="#慢查询优化" class="headerlink" title="慢查询优化"></a>慢查询优化</h2><p> <a href="https://tech.meituan.com/2014/06/30/mysql-index.html" target="_blank" rel="noopener">慢查询优化方法</a></p><p> <a href="">SQL慢查询优化</a></p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><p>1.<a href="https://zhuanlan.zhihu.com/p/29118331" target="_blank" rel="noopener">数据库索引,如何设计加速数据读取?</a></p><p>2.<a href="https://www.cnblogs.com/DarrenChan/p/8796922.html" target="_blank" rel="noopener">MySQL和Redis面试题小结</a></p><h1 id="InnoDB-并发控制"><a href="#InnoDB-并发控制" class="headerlink" title="InnoDB 并发控制"></a>InnoDB 并发控制</h1><h2 id="InnoDB-锁机制"><a href="#InnoDB-锁机制" class="headerlink" title="InnoDB 锁机制"></a>InnoDB 锁机制</h2><p><img src="https://hadyang.github.io/interview/docs/basic/database/mysql/innodb/concurrent/images/e86d2bb63f9ecf327e588f352bb26d3b.png" alt="img"></p><p>InnoDB默认使用行锁,实现了两种标准的行锁——共享锁与排他锁;</p><table><thead><tr><th>行锁类型</th><th>锁功能</th><th>锁兼容性</th><th>加锁</th><th>释放锁</th></tr></thead><tbody><tr><td>共享锁(读锁、S锁)</td><td>允许获取共享锁的亊务读数据</td><td>与共享锁兼容,与排它锁不兼容</td><td>只有 <code>SerializaWe</code> 隔离级别会默认为:读加共享锁;其他隔离级别下,可显示使用 <code>select...lock in share model</code> 为读加共享锁</td><td>在事务提交或回滚后会自动同时释放锁;除了使用 <code>start transaction</code> 的方式显式开启事务,InnoDB 也会自动为增删改査语句开启事务,并自动提交或回滚;(<code>autocommit=1</code>)</td></tr><tr><td>排它锁(写锁、X锁)</td><td>允许获取排它锁的事务更新或删除数据</td><td>与共享锁不兼容,与排它锁不兼容</td><td>在默认的 <code>Reapeatable Read</code> 隔离级别下,InnoDB 会自动为增删改操作的行加排它锁;也可显式使用 <code>select...for update</code> 为读加排它锁</td><td>…</td></tr></tbody></table><blockquote><ol><li>除了显式加锁的情况,其他情况下的加锁与解锁都无需人工干预</li><li>InnoDB 所有的行锁算法都是基于索引实现的,锁定的也都是索引或索引区间</li></ol></blockquote><h3 id="当前读-amp-快照读"><a href="#当前读-amp-快照读" class="headerlink" title="当前读 & 快照读"></a>当前读 & 快照读</h3><p><strong>当前读</strong>:即加锁读,读取记录的最新版本,会加锁保证其他并发事务不能修改当前记录,直至获取锁的事务释放锁;使用当前读的操作主要包括:<strong>显式加锁的读操作与插入/更新/删除等写操作</strong>,如下所示:</p><pre><code>select * from table where ? lock in share mode;select * from table where ? for update;insert into table values (…);update table set ? where ?;delete from table where ?;</code></pre><blockquote><p>注:当 <code>Update</code> SQL 被发给 <code>MySQL</code> 后, <code>MySQL Server</code> 会根据where条件,读取第一条满足条件的记录,然后 InnoDB 引擎会将第一条记录返回,并加锁,待 <code>MySQL Server</code> 收到这条加锁的记录之后,会再发起一个 <code>Update</code> 请求,更新这条记录。一条记录操作完成,再读取下一条记录,直至没有满足条件的记录为止。因此, <code>Update</code> 操作内部,就包含了当前读。同理, <code>Delete</code> 操作也一样。 <code>Insert</code> 操作会稍微有些不同,简单来说,就是 <code>Insert</code> 操作可能会触发 <code>Unique Key</code> 的冲突检查,也会进行一个当前读。</p></blockquote><p><strong>快照读:即不加锁读,读取记录的快照版本而非最新版本,通过MVCC实现</strong>;</p><p>InnoDB 默认的 <code>RR</code> 事务隔离级别下,不显式加<code>lock in share mode</code>与<code>for update</code>的 <code>select</code> 操作都属于快照读,保证事务执行过程中只有第一次读之前提交的修改和自己的修改可见,其他的均不可见;</p><h3 id="共享锁与独占锁"><a href="#共享锁与独占锁" class="headerlink" title="共享锁与独占锁"></a>共享锁与独占锁</h3><h3 id="意向锁"><a href="#意向锁" class="headerlink" title="意向锁"></a>意向锁</h3><p>InnoDB 支持多粒度的锁,允许表级锁和行级锁共存。一个类似于 <code>LOCK TABLES ... WRITE</code> 的语句会获得这个表的 <code>x</code> 锁。为了实现多粒度锁,InnoDB 使用了意向锁(简称 I 锁)。I 锁是表明一个事务稍后要获得针对一行记录的某种锁(<code>s or x</code>)的对应表的表级锁,有两种:</p><ul><li>意向排它锁(简称 IX 锁)表明一个事务意图在某个表中设置某些行的 x 锁</li><li>意向共享锁(简称 IS 锁)表明一个事务意图在某个表中设置某些行的 s 锁</li></ul><p><code>SELECT ... LOCK IN SHARE MODE</code> 设置一个 <code>IS</code> 锁, <code>SELECT ... FOR UPDATE</code> 设置一个 <code>IX</code> 锁。意向锁的原则如下:</p><ul><li>一个事务必须先持有该表上的 IS 或者更强的锁才能持有该表中某行的 S 锁</li><li>一个事务必须先持有该表上的 IX 锁才能持有该表中某行的 X 锁</li></ul><p>新请求的锁只有兼容已有锁才能被允许,否则必须等待不兼容的已有锁被释放。<strong>一个不兼容的锁请求不被允许是因为它会引起死锁,错误会发生</strong>。意向锁只会阻塞全表请求(比如 <code>LOCK TABLES ... WRITE</code> )。<strong>意向锁的主要目的是展示某人正在锁定表中一行,或者将要锁定一行</strong>。</p><h3 id="Record-Lock"><a href="#Record-Lock" class="headerlink" title="Record Lock"></a>Record Lock</h3><p>记录锁(Record Lock)是加到<strong>索引记录</strong>上的锁,假设我们存在下面的一张表 <code>users</code>:</p><pre><code> CREATE TABLE users( id INT NOT NULL AUTO_INCREMENT, last_name VARCHAR(255) NOT NULL, first_name VARCHAR(255), age INT, PRIMARY KEY(id), KEY(last_name), KEY(age) );</code></pre><p>如果我们使用 <code>id</code> 或者 <code>last_name</code> 作为 SQL 中 <code>WHERE</code> 语句的过滤条件,那么 InnoDB 就可以通过索引建立的 B+ 树找到行记录并添加索引,但是如果使用 <code>first_name</code> 作为过滤条件时,由于 InnoDB 不知道待修改的记录具体存放的位置,也无法对将要修改哪条记录提前做出判断就会锁定整个表。</p><h3 id="Gap-Lock"><a href="#Gap-Lock" class="headerlink" title="Gap Lock"></a>Gap Lock</h3><p>记录锁是在存储引擎中最为常见的锁,除了记录锁之外,InnoDB 中还存在间隙锁(Gap Lock),间隙锁是对索引记录中的一段连续区域的锁;当使用类似 <code>SELECT * FROM users WHERE id BETWEEN 10 AND 20 FOR UPDATE;</code> 的 SQL 语句时,就会阻止其他事务向表中插入 <code>id = 15</code> 的记录,因为整个范围都被间隙锁锁定了。</p><blockquote><p>间隙锁是存储引擎对于性能和并发做出的权衡,并且只用于某些事务隔离级别。</p></blockquote><p>虽然间隙锁中也分为共享锁和互斥锁,不过它们之间并不是互斥的,也就是不同的事务可以同时持有一段相同范围的共享锁和互斥锁,它唯一阻止的就是<strong>其他事务向这个范围中添加新的记录</strong>。</p><h4 id="间隙锁的缺点"><a href="#间隙锁的缺点" class="headerlink" title="间隙锁的缺点"></a>间隙锁的缺点</h4><ul><li>间隙锁有一个比较致命的弱点,就是当锁定一个范围键值之后,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下这可能会对性能造成很大的危害</li><li>当Query无法利用索引的时候, InnoDB会放弃使用行级别锁定而改用表级别的锁定,造成并发性能的降低;</li><li>当Quuery使用的索引并不包含所有过滤条件的时候,数据检索使用到的索引键所指向的数据可能有部分并不属于该Query的结果集的行列,但是也会被锁定,因为间隙锁锁定的是一个范围,而不是具体的索引键;</li><li>当Query在使用索引定位数据的时候,如果使用的索引键一样但访问的数据行不同的时候(索引只是过滤条件的一部分),一样会被锁定</li></ul><h4 id="Next-Key-Lock"><a href="#Next-Key-Lock" class="headerlink" title="Next-Key Lock"></a>Next-Key Lock</h4><p>Next-Key 锁相比前两者就稍微有一些复杂,它是记录锁和记录前的间隙锁的结合,在 <code>users</code> 表中有以下记录:</p><pre><code> +------|-------------|--------------|-------+ | id | last_name | first_name | age | |------|-------------|--------------|-------| | 4 | stark | tony | 21 | | 1 | tom | hiddleston | 30 | | 3 | morgan | freeman | 40 | | 5 | jeff | dean | 50 | | 2 | donald | trump | 80 | +------|-------------|--------------|-------+</code></pre><p>如果使用 Next-Key 锁,那么 Next-Key 锁就可以在需要的时候锁定以下的范围:</p><pre><code> (-∞, 21] (21, 30] (30, 40] (40, 50] (50, 80] (80, ∞)</code></pre><blockquote><p>既然叫 Next-Key 锁,锁定的应该是当前值和后面的范围,但是实际上却不是,Next-Key 锁锁定的是当前值和前面的范围。</p></blockquote><p>当我们更新一条记录,比如 <code>SELECT * FROM users WHERE age = 30 FOR UPDATE;</code>,InnoDB 不仅会在范围 <code>(21, 30]</code> 上加 Next-Key 锁,还会在这条该记录索引增长方向的范围 <code>(30, 40]</code> 加间隙锁,所以插入 <code>(21, 40]</code> 范围内的记录都会被锁定。</p><blockquote><p>Next-Key 锁的作用其实是为了解决幻读的问题。</p></blockquote><h3 id="插入意向锁"><a href="#插入意向锁" class="headerlink" title="插入意向锁"></a>插入意向锁</h3><p>插入意向锁是在插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号,亦即多个事务在相同的索引间隙插入时如果不是插入间隙中相同的位置就不需要互相等待。假设有索引值<code>4、7</code>,几个不同的事务准备插入<code>5、6</code>,每个锁都在获得插入行的独占锁之前用插入意向锁各自锁住了<code>4、7</code>之间的间隙,但是不阻塞对方因为插入行不冲突。</p><h3 id="自增锁"><a href="#自增锁" class="headerlink" title="自增锁"></a>自增锁</h3><p>自增锁是一个特殊的表级锁,事务插入自增列的时候需要获取,最简单情况下如果一个事务插入一个值到表中,任何其他事务都要等待,这样第一个事物才能获得连续的主键值。</p><h3 id="锁选择"><a href="#锁选择" class="headerlink" title="锁选择"></a>锁选择</h3><pre><code>+——-+————-+| id | name |+——-+————-+| 1 | title1 |+——-+————-+| 2 | title2 |+——-+————-+| 3 | title3 |+——-+————-+| 9 | title9 |+——-+————-+| 10 | title10 |+——-+————-+</code></pre><p>按照原理来说,<code>id>5 and id<7</code>这个查询条件,在表中找不到满足条件的项,因此会对第一个不满足条件的项(<code>id = 9</code>)上加GAP锁,防止后续其他事务插入满足条件的记录。</p><p>而 <strong>GAP 锁与GAP 锁是不冲突的</strong>,那么为什么两个同时执行<code>id>5 and id<7</code>查询的事务会冲突呢?</p><p>原因在于,<code>MySQL Server</code>并没有将<code>id<7</code>这个查询条件下降到<code>InnoDB</code>引擎层,因此<code>InnoDB</code>看到的查询,是<code>id>5</code>,正向扫描。读出的记录<code>id=9</code>,先加上<code>next key锁</code>(Lock X + GAP lock),然后返回给 MySQL Server 进行判断。 MySQL Server 此时才会判断返回的记录是否满足<code>id<7</code>的查询条件。此处不满足,查询结束。</p><p>因此,<code>id=9</code>记录上,真正持有的锁是<code>next key</code>锁,<strong>而<code>next key</code>锁之间是相互冲突的</strong>,这也说明了为什么两个<code>id>5 and id<7</code>查询的事务会冲突的原因。</p><h2 id="MVCC"><a href="#MVCC" class="headerlink" title="MVCC"></a>MVCC</h2><p>InnoDB 引擎支持 MVCC(Multiversion Concurrency Control):InnoDB 保存了行的历史版本,以支持事务的并发控制和回滚。这些历史信息保存在表空间的 <strong>回滚段(Rollback Segment)</strong> 里,回滚段中存储着 <strong>Undo Log</strong>。当事务需要进行回滚时,InnoDB 就会使用这些信息来进行 Undo 操作,同时这些信息也可用来实现 <strong>一致性读</strong>。</p><p>InnoDB 在存储的每行数据中都增加了三列隐藏属性:</p><ul><li><code>DB_TRX_ID</code>:最后一次插入或更新的事务ID</li><li><code>DB_ROLL_PTR</code>:指向已写入回滚段的 Undo Log 记录。如果这行记录是更新的,那么就可以根据这个 Undo Log 记录重建之间的数据</li><li><code>DB_ROW_ID</code>:自增序列,如果表未指定主键,则由该列作为主键</li></ul><p>在回滚段的 Undo Log 被分为 <code>Insert Undo Log</code> 和 <code>Update Undo Log</code>。Insert Undo Log 只是在事务回滚的时候需要,在事务提交后就可丢弃。Update Undo Log 不仅仅在回滚的时候需要,还要提供一致性读,所以只有在所有需要该 Update Undo Log 构建历史版本数据的事务都提交后才能丢弃。MySQL 建议尽量频繁的提交事务,这样可以保证 InnoDB 快速的丢弃 Update Undo Log,防止其过大。</p><p>在 InnoDB 中,行数据的物理删除不是立刻执行,InnoDB 会在行删除的 Undo Log 被丢弃时才会进行物理删除。这个过程被称之为 <strong>清理(Purge)</strong>,其执行过程十分迅速。</p><h3 id="MVCC-二级索引"><a href="#MVCC-二级索引" class="headerlink" title="MVCC 二级索引"></a>MVCC 二级索引</h3><p>InnoDB 在更新时对 二级索引 和 聚集索引的处理方式不一样。在聚集索引上的更新是原地更新(in-place),其中的隐藏属性 <code>DB_ROLL_PTR</code> 指向的 Undo Log 可以重建历史数据。但是二级索引没有隐藏属性,所以不能原地更新。</p><p>当二级索引的数据被更新时,旧的二级索引记录标记为 <strong>标记删除(delete-marked)</strong>,然后插入一条新的索引记录,最终标记删除的索引记录会被清除。当二级索引记录被标记为 delete-marked 或者有更新的事务更新时,InnoDB 会查找聚集索引。在聚集索引中检查行的 <code>DB_TRX_ID</code>,如果事务修改了记录,则从 Undo Log 中构建行数据的正确版本。如果二级索引记录被标记为 delete-marked 或者 二级索引有更新的事务更新,覆盖索引技术不会被使用(获取行任意数据均需要回表)。</p><h3 id="MVCC-vs-乐观锁"><a href="#MVCC-vs-乐观锁" class="headerlink" title="MVCC vs 乐观锁"></a>MVCC vs 乐观锁</h3><p><strong>MVCC 并不是一个与乐观和悲观并发控制对立的东西,它能够与两者很好的结合以增加事务的并发量</strong>,在目前最流行的 SQL 数据库 MySQL 和 PostgreSQL 中都对 MVCC 进行了实现;但是由于它们分别实现了悲观锁和乐观锁,所以 MVCC 实现的方式也不同。</p><p>MVCC 可以保证不阻塞地读到一致的数据。但是,MVCC 并没有对实现细节做约束,为此不同的数据库的语义有所不同,比如:</p><ul><li><code>postgres</code> 对写操作也是乐观并发控制;在表中保存同一行数据记录的多个不同版本,每次写操作,都是创建,而回避更新;在事务提交时,按版本号检查当前事务提交的数据是否存在写冲突,则抛异常告知用户,回滚事务;</li><li><code>innodb</code> 则只对读无锁,写操作仍是上锁的悲观并发控制,这也意味着,<code>innodb</code> 中只能见到因死锁和不变性约束而回滚,而见不到因为写冲突而回滚,不像 postgres 那样对数据修改在表中创建新纪录,而是每行数据只在表中保留一份,在更新数据时上行锁,同时将旧版数据写入 <code>undo log</code>。表和 undo log 中行数据都记录着事务ID,在检索时,只读取来自当前已提交的事务的行数据。</li></ul><p>可见 MVCC 中的写操作仍可以按悲观并发控制实现,而 <code>CAS</code> 的写操作只能是乐观并发控制。还有一个不同在于,MVCC 在语境中倾向于 “对多行数据打快照造平行宇宙”,然而 <code>CAS</code> 一般只是保护单行数据而已。比如 mongodb 有 CAS 的支持,但不能说这是 MVCC。</p><h2 id="InnoDB-事务隔离"><a href="#InnoDB-事务隔离" class="headerlink" title="InnoDB 事务隔离"></a>InnoDB 事务隔离</h2><h3 id="几种隔离级别"><a href="#几种隔离级别" class="headerlink" title="几种隔离级别"></a>几种隔离级别</h3><p>事务的隔离性是数据库处理数据的几大基础之一,而隔离级别其实就是提供给用户用于在性能和可靠性做出选择和权衡的配置项。</p><p>ISO 和 ANIS SQL 标准制定了四种事务隔离级别,而 InnoDB 遵循了 SQL:1992 标准中的四种隔离级别:<code>READ UNCOMMITED</code>、<code>READ COMMITED</code>、<code>REPEATABLE READ</code> 和 <code>SERIALIZABLE</code>;每个事务的隔离级别其实都比上一级多解决了一个问题:</p><ul><li><p><code>RAED UNCOMMITED</code>:使用查询语句不会加锁,可能会读到未提交的行(Dirty Read);</p><blockquote><p>可以读取未提交记录。此隔离级别,不会使用,忽略。</p></blockquote></li><li><p><code>READ COMMITED</code>:只对记录加记录锁,而不会在记录之间加间隙锁,所以允许新的记录插入到被锁定记录的附近,所以再多次使用查询语句时,可能得到不同的结果(Non-Repeatable Read);</p><blockquote><p>快照读忽略,本文不考虑。针对当前读,RC隔离级别保证对读取到的记录加锁 (记录锁),存在幻读现象。</p></blockquote></li><li><p><code>REPEATABLE READ</code>:快照读忽略,本文不考虑。针对当前读,<strong>RR隔离级别保证对读取到的记录加锁 (记录锁),同时保证对读取的范围加锁,新的满足查询条件的记录不能够插入 (间隙锁)</strong>,不存在幻读现象。</p></li><li><p><code>SERIALIZABLE</code>:从MVCC并发控制退化为基于锁的并发控制。不区别快照读与当前读,所有的读操作均为当前读,读加读锁 (S锁),写加写锁 (X锁)。</p><blockquote><p>Serializable隔离级别下,读写冲突,因此并发度急剧下降,在MySQL/InnoDB下不建议使用。</p></blockquote></li></ul><p>MySQL 中默认的事务隔离级别就是 <code>REPEATABLE READ</code>,但是它通过 Next-Key 锁也能够在某种程度上解决幻读的问题。</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/mysql/innodb/transaction/images/15a2370c552e932907f8b2d3587171ef.png" alt="image"></p><p>接下来,我们将数据库中创建如下的表并通过个例子来展示在不同的事务隔离级别之下,会发生什么样的问题:</p><pre><code> CREATE TABLE test( id INT NOT NULL, UNIQUE(id) );</code></pre><h3 id="脏读"><a href="#脏读" class="headerlink" title="脏读"></a>脏读</h3><blockquote><p>在一个事务中,读取了其他事务未提交的数据。</p></blockquote><p>当事务的隔离级别为 <code>READ UNCOMMITED</code> 时,我们在 <code>SESSION 2</code> 中插入的<strong>未提交</strong>数据在 <code>SESSION 1</code> 中是可以访问的。</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/mysql/innodb/transaction/images/e4696fae4a417bdd70dd04f0786647ed.png" alt="image"></p><h3 id="不可重复读"><a href="#不可重复读" class="headerlink" title="不可重复读"></a>不可重复读</h3><blockquote><p>在一个事务中,同一行记录被访问了两次却得到了不同的结果。</p></blockquote><p>当事务的隔离级别为 <code>READ COMMITED</code> 时,虽然解决了脏读的问题,但是如果在 <code>SESSION 1</code> 先查询了<strong>一行</strong>数据,在这之后 <code>SESSION 2</code> 中修改了同一行数据并且提交了修改,在这时,如果 <code>SESSION 1</code> 中再次使用相同的查询语句,就会发现两次查询的结果不一样。</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/mysql/innodb/transaction/images/d2d41261df71879fab0eb54771688d78.png" alt="image"></p><p>不可重复读的原因就是,在 <code>READ COMMITED</code> 的隔离级别下,存储引擎不会在查询记录时添加行锁,锁定 <code>id = 3</code> 这条记录。</p><h3 id="幻读"><a href="#幻读" class="headerlink" title="幻读"></a>幻读</h3><blockquote><p>在一个事务中,同一个范围内的记录被读取时,其他事务向这个范围添加了新的记录。</p></blockquote><p>重新开启了两个会话 <code>SESSION 1</code> 和 <code>SESSION 2</code>,在 <code>SESSION 1</code> 中我们查询全表的信息,没有得到任何记录;在 <code>SESSION 2</code> 中向表中插入一条数据并提交;由于 <code>REPEATABLE READ</code> 的原因,再次查询全表的数据时,我们获得到的仍然是空集,但是在向表中插入同样的数据却出现了错误。</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/mysql/innodb/transaction/images/b356bfdde7d52c6993a697c4529d2f6b.png" alt="image"></p><p>这种现象在数据库中就被称作幻读,虽然我们使用查询语句得到了一个空的集合,但是插入数据时却得到了错误,好像之前的查询是幻觉一样。</p><p>在标准的事务隔离级别中,幻读是由更高的隔离级别 <code>SERIALIZABLE</code> 解决的,但是它也可以通过 MySQL 提供的 <code>Next-Key</code> 锁解决:</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/mysql/innodb/transaction/images/8d3094a3893ae4d1806dfcb3a93b7dff.png" alt="image"></p><p><code>REPEATABLE READ</code> 和 <code>READ UNCOMMITED</code> 其实是矛盾的,如果保证了前者就看不到已经提交的事务,如果保证了后者,就会导致两次查询的结果不同,MySQL 为我们提供了一种折中的方式,能够在 <code>REPEATABLE READ</code> 模式下加锁访问已经提交的数据,其本身并不能解决幻读的问题,而是通过文章前面提到的 <code>Next-Key</code> 锁来解决。</p><h2 id="分库分表"><a href="#分库分表" class="headerlink" title="分库分表"></a>分库分表</h2><h3 id="垂直拆分"><a href="#垂直拆分" class="headerlink" title="垂直拆分"></a>垂直拆分</h3><p><strong>垂直分表</strong> 也就是 <em>大表拆小表</em>,基于列字段进行的。一般是表中的字段较多,将不常用的, 数据较大,长度较长(比如text类型字段)的拆分到 <em>扩展表</em>。 一般是针对那种几百列的大表,也避免查询时,数据量太大造成的 off-page 问题。</p><p><strong>垂直分库</strong> 针对的是一个系统中的不同业务进行拆分。将多个业务系统的数据放在单个数据库中(<strong>服务化</strong>拆分),这会让数据库的单库处理能力成为瓶颈。将单个数据库,按业务进行拆分,同一业务领域的数据表放到同一数据库中。并且多个数据库分布在多个机器上,防止由于单机的磁盘、内存、IO等资源造成 MySQL 性能下降。</p><p>数据库的连接资源比较宝贵且单机处理能力也有限,在高并发场景下,垂直分库一定程度上能够突破 IO、连接数等单机硬件资源的瓶颈。</p><h3 id="水平拆分"><a href="#水平拆分" class="headerlink" title="水平拆分"></a>水平拆分</h3><p>目前绝大多数应用采取的两种分库分表规则</p><ul><li><code>离散映射</code>:如 mod 或 dayofweek , 这种类型的映射能够很好的解决热点问题,但带来了数据迁移和历史数据问题。</li><li><code>连续映射</code>;如按 id 或 gmt_create_time 的连续范围做映射。这种类型的映射可以避免数据迁移,但又带来热点问题。</li></ul><p>随着数据量的增大,每个表或库的数据量都是各自增长。当一个表或库的数据量增长到了一个极限,要加库或加表的时候,介于这种分库分表算法的离散性,必需要做 <strong>数据迁移</strong> 才能完成。</p><p>考虑到数据增长的特点,如果我们以代表时间增长的字段,按递增的范围分库,则可以避免数据迁移。这样的方式下,在数据量再增加达到前几个库/表的上限时,则继续水平增加库表,原先的数据就不需要迁移了。但是这样的方式会带来一个 <strong>热点问题</strong>:当前的数据量达到某个库表的范围时,所有的插入操作,都集中在这个库/表了。</p><p>结合离散分库/分表和连续分库/分表的优点,可使要热点和新数据均匀分配在每个库,同时又保证易于水平扩展。分库分表的主要经历以下三个阶段:</p><h4 id="阶段一"><a href="#阶段一" class="headerlink" title="阶段一"></a>阶段一</h4><p>一个数据库,两个表,<code>rule0 = id % 2</code></p><pre><code>分库规则dbRule: “DB0″分表规则tbRule: “t” + (id % 2)</code></pre><p><img src="https://hadyang.github.io/interview/docs/basic/database/mysql/sharding/images/5-mysql-3a686.png" alt="img"></p><h4 id="阶段二"><a href="#阶段二" class="headerlink" title="阶段二"></a>阶段二</h4><p>当单库的数据量接近 1千万,单表的数据量接近 500 万时,进行扩容(数据量只是举例,具体扩容量要根据数据库和实际压力状况决定):增加一个数据库 <code>DB1</code>,将 <code>DB0.t0</code> 整表迁移到新库 <code>DB1.t1</code>。每个库各增加1个表,未来10M-20M的数据mod2分别写入这2个表:<code>t0_1,t1_1</code>:</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/mysql/sharding/images/5-mysql-6885c.png" alt="img"></p><p>分库规则dbRule:</p><pre><code>“DB” + (id % 2)</code></pre><p>分表规则tbRule:</p><pre><code> if(id < 1千万){ return "t"+ (id % 2); //1千万之前的数据,仍然放在t0和t1表。t1表从DB0搬迁到DB1库 }else if(id < 2千万){ return "t"+ (id % 2) +"_1"; //1千万之后的数据,各放到两个库的两个表中: t0_1,t1_1 }else{ throw new IllegalArgumentException("id outof range[20000000]:" + id); }</code></pre><p>这样 <code>10M</code> 以后的新生数据会均匀分布在 <code>DB0</code> 和 <code>DB1</code>; 插入更新和查询热点仍然能够在每个库中均匀分布。每个库中同时有老数据和不断增长的新数据。每表的数据仍然控制在 <code>500万</code> 以下。</p><h4 id="阶段三"><a href="#阶段三" class="headerlink" title="阶段三"></a>阶段三</h4><p>当两个库的容量接近上限继续水平扩展时,进行如下操作:</p><ul><li>新增加两个库:<code>DB2</code>和<code>DB3</code>,以<code>id % 4</code>分库。余数<code>0、1、2、3</code>分别对应<code>DB</code>的下标. <code>t0</code>和<code>t1</code>不变,</li><li>将<code>DB0.t0_1</code>整表迁移到<code>DB2</code>; 将<code>DB1.t1_1</code>整表迁移到<code>DB3</code></li></ul><p><code>20M-40M</code>的数据 mod4 分为 4 个表:<code>t0_2,t1_2,t2_2,t3_2</code>,分别放到4个库中:</p><p><img src="https://hadyang.github.io/interview/docs/basic/database/mysql/sharding/images/5-mysql-3f186.png" alt="img"></p><p>新的分库分表规则如下:</p><p>分库规则dbRule:</p><pre><code> if(id < 2千万){ //2千万之前的数据,4个表分别放到4个库 if(id < 1千万){ return "db"+ (id % 2); //原t0表仍在db0, t1表仍在db1 }else{ return "db"+ ((id % 2) +2); //原t0_1表从db0搬迁到db2; t1_1表从db1搬迁到db3 } }else if(id < 4千万){ return "db"+ (id % 4); //超过2千万的数据,平均分到4个库 }else{ throw new IllegalArgumentException("id out of range. id:"+id); }</code></pre><p>分表规则tbRule:</p><pre><code> if(id < 2千万){ //2千万之前的数据,表规则和原先完全一样,参见阶段二 if(id < 1千万){ return "t"+ (id % 2); //1千万之前的数据,仍然放在t0和t1表 }else{ return "t"+ (id % 2) +"_1"; //1千万之后的数据,仍然放在t0_1和t1_1表 } }else if(id < 4千万){ return "t"+ (id % 4)+"_2"; //超过2千万的数据分为4个表t0_2,t1_2,t2_2,t3_2 }else{ throw new IllegalArgumentException("id out of range. id:"+id); }</code></pre><p>随着时间的推移,当第一阶段的<code>t0/t1</code>,第二阶段的<code>t0_1/t1_1</code>逐渐成为历史数据,不再使用时,可以直接<code>truncate</code>掉整个表。省去了历史数据迁移的麻烦。</p><p>分库分表规则的设计和配置,长远说来必须满足以下要求</p><ul><li>可以动态推送修改</li><li><strong>规则可以分层级叠加</strong>,旧规则可以在新规则下继续使用,新规则是旧规则在更宽尺度上的拓展,以此支持新旧规则的兼容,避免数据迁移</li><li>用 <code>mod</code> 方式时,最好选 2 的指数级倍分库分表,这样方便以后切割。</li></ul><h3 id="数据迁移"><a href="#数据迁移" class="headerlink" title="数据迁移"></a>数据迁移</h3><p>在上述的水平扩容方案中,如何进行数据迁移,是在扩容中需要考虑的问题。一般情况下,数据迁移分为:停机迁移、双写迁移。</p><p><strong>停机迁移</strong> 是最简单、最安全、最快速的迁移方案,但一般线上业务系统很少允许停机迁移。在停机迁移中,首先停掉数据库 A 的写入请求,复制 A 数据到 B,待复制完成后,切换线上数据源。</p><p><strong>双写迁移</strong> 方案就是同时写两个库,一个是老库,一个是新库。也就是在线上系统里面,除了对所有老库的增删改地方,同时对新库同样执行增删改。主要经历以下三个阶段:</p><ol><li>导入历史数据,数据库双写(事务成功以老数据源为准),查询走老数据源,通过定时任务补全新老差异数据</li><li>新老数据无差异,依旧双写(事务成功以新数据源为准),查询走新数据源</li><li>稳定运行无误后,下线老数据源</li></ol><h3 id="Join"><a href="#Join" class="headerlink" title="Join"></a>Join</h3><p>在拆分之前,系统中很多列表和详情页所需的数据是可以通过 Join 来完成的。而拆分后,数据库可能是分布式在不同实例和不同的主机上,Join 将变得非常麻烦。首先要考虑下垂直分库的设计问题,如果可以调整,那就优先调整。如果无法调整的情况,可以考虑以下解决方案:</p><ul><li><strong>全局表</strong>:就是有可能系统中所有模块都可能会依赖到的一些表。为了避免跨库 join 查询,我们可以将这类表在其他每个数据库中均保存一份。同时,这类数据通常也很少发生修改(甚至几乎不会),所以也不用太担心 <em>一致性</em> 问题;</li><li><strong>字段冗余</strong>:字段冗余能带来便利,是一种 <em>空间换时间</em> 的体现。但其适用场景也比较有限,比较适合依赖字段较少的情况。最复杂的还是数据一致性问题,这点很难保证;</li><li><strong>系统层组装</strong>:在系统层面,通过调用不同模块的组件或者服务,获取到数据并进行字段拼装;</li></ul><h2 id="主从复制"><a href="#主从复制" class="headerlink" title="主从复制"></a>主从复制</h2><p>MySQL 主从复制涉及到三个线程,一个运行在主节点(log dump thread),其余两个(I/O thread, SQL thread)运行在从节点。</p><ul><li><strong>Log Dump Thread</strong>:当从节点连接主节点时,主节点会创建一个 log dump 线程,用于发送 bin-log 的内容。在读取 bin-log 中的操作时,此线程会对主节点上的 bin-log 加锁,当读取完成,甚至在发动给从节点之前,锁会被释放。</li><li><strong>I/O Thread</strong>:当从节点上执行 <code>start slave</code> 命令之后,从节点会创建一个 I/O 线程用来连接主节点,请求主库中更新的 bin-log。I/O线程接收到主节点 binlog dump 进程发来的更新之后,保存在本地 relay-log 中。</li><li><strong>SQL Thread</strong>:负责读取 relay log 中的内容,解析成具体的操作并执行,最终保证主从数据的一致性。</li></ul><p>一个 slave 节点可同时从多个 master 进行数据复制,在这种情况下,不同 master 的 bin-log 存储在不同的 relay log中。</p><h3 id="同步模式"><a href="#同步模式" class="headerlink" title="同步模式"></a>同步模式</h3><p><strong>异步模式(mysql async-mode)</strong>:MySQL增删改操作会全部记录在 binary log 中,当 slave 节点连接 master 时,会主动从 master 处获取最新的 bin log 文件。</p><p><strong>半同步模式(mysql semi-sync)</strong>:这种模式下主节点只需要接收到其中一台从节点的返回信息,就会 <code>commit</code> ;否则需要等待直到超时时间然后切换成异步模式再提交;这样做的目的可以使主从数据库的数据延迟缩小,可以提高数据安全性,确保了事务提交后,binlog 至少传输到了一个从节点上,不能保证从节点将此事务更新到 db 中。性能上会有一定的降低,响应时间会变长。</p><p><strong>全同步模式</strong> 是指主节点和从节点全部执行了commit并确认才会向客户端返回成功。</p><h3 id="主从复制的延迟问题"><a href="#主从复制的延迟问题" class="headerlink" title="主从复制的延迟问题"></a>主从复制的延迟问题</h3><p>进行主从同步的过程中,如果使用异步或半异步模式,均会有主从节点数据不一致的窗口时间。同时,从节点上的 <code>SQL Thread</code> 只能串行执行 <code>relay-log</code> 中的记录,当某条 DDL/DML 耗时较长时,会加剧这个窗口时间;再者在某些场景下会使用 slave 节点进行数据读取,这也可能导致数据加锁等待。基于以上原因在处理主从复制延迟问题上有以下几种方向:</p><ol><li>优化主从节点之间的网络延迟</li><li>降低 master 负载,以减少 TPS</li><li>降低 slave 负载,slave 只做备份使用,不提供服务</li><li>调整 slave 参数:关闭 slave bin-log 等</li><li>多线程的主从复制:不同 schema 下的表并发提交时的数据不会相互影响,即 slave 节点可以用对 relay log 中不同的 schema 各分配一个SQL Thread,来重放 relay log 中主库已经提交的事务</li></ol><h2 id="全局ID"><a href="#全局ID" class="headerlink" title="全局ID"></a>全局ID</h2><ul><li>数据库自增 id</li><li>设置数据库 sequence 或者表自增字段步长</li><li>UUID</li><li>Snowflake 算法</li></ul><h3 id="Snowflake"><a href="#Snowflake" class="headerlink" title="Snowflake"></a>Snowflake</h3><p>twitter 开源的分布式 id 生成算法,采用 <code>Scala</code> 语言实现,是把一个 <code>64</code> 位的 <code>long</code> 型的 <code>id</code> ,<code>1</code> 个 <code>bit</code> 是不用的,用其中的 <code>41</code> <code>bit</code> 作为毫秒数,用 <code>10</code> <code>bit</code> 作为工作机器 <code>id</code> ,<code>12</code> <code>bit</code> 作为序列号。</p><pre><code>|–1位符号位–|--41位时间戳–|--10位机器ID–|--12位序列号–|</code></pre><ul><li><strong>1 bit</strong>:不用,为啥呢?因为二进制里第一个 <code>bit</code> 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。</li><li><strong>41 bit</strong>:表示的是时间戳,单位是毫秒。<code>41 bit</code> 可以表示的数字多达 <code>2^41 - 1</code>,也就是可以标识 <code>2^41 - 1</code> 个毫秒值,换算成年就是表示<code>69</code>年的时间。</li><li><strong>10 bit</strong>:记录工作机器 <code>id</code>,代表的是这个服务最多可以部署在 <code>2^10</code>台机器上哪,也就是<code>1024</code>台机器。但是 10 bit 里 5 个 bit 代表机房 id,5 个 bit 代表机器 id。意思就是最多代表 <code>2^5</code>个机房(32个机房),每个机房里可以代表 <code>2^5</code> 个机器(32台机器)。</li><li><strong>12 bit</strong>:这个是用来记录同一个毫秒内产生的不同 id,<code>12 bit</code> 可以代表的最大正整数是 <code>2^12 - 1 = 4096</code>,也就是说可以用这个 <code>12 bit</code> 代表的数字来区分同一个毫秒内的 <code>4096</code> 个不同的 id。</li></ul><h4 id="Snowflake-的问题"><a href="#Snowflake-的问题" class="headerlink" title="Snowflake 的问题"></a>Snowflake 的问题</h4><p>Snowflake 这样依赖时间的 ID 生成算法注定存在一个问题:<strong>时间的准确度问题</strong>。这一算法有一个默认前提:分布式环境下时间获取总是准确的,即时间总是递增的。而现实环境中,这样的条件很难满足。总会因为硬件、软件、人的原因造成时间变化。如果你的硬件时间本身就比正常时间快,而你接入了一个 NTP 服务,每当进行 NTP 时间校准时,你的机器时间总会向后 <strong>回拨</strong> 一段时间,这时悲剧就来了:有极大可能性生成重复ID。</p><p>针对上面提到的两个问题,可如下改进:</p><ol><li>时间戳由毫秒变为秒</li><li>使用环形列表对时间戳对应的序列进行缓存</li><li>使用 CAS 操作避免大粒度悲观锁</li></ol><p>为了 <strong>缓解</strong> 时钟回拨问题,对之前的序列进行缓存,而原生算法很显然是不利于缓存的,最坏的情况下每秒需要缓存 1000 个值,这显然对内存很不友好。于是我将时间戳改为秒为单位,同时可以把省出来的位交给序列。此时缓存一个小时的数据(即可以容忍一个小时的时钟回拨)也就只需要缓存 3600 个序列,完全可以接受。改进后的 Snowflake 生成的ID是这样组成的:</p><pre><code>|–1位符号位–|--32位时间戳–|--10位机器ID–|--21位序列号–|</code></pre><blockquote><p>环形列表:即整个列表的容量是一定的,当列表满了以后再加入的元素会按照入列的先后顺序覆盖之前的元素。</p></blockquote><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p>【】</p>]]></content>
<categories>
<category> Basic </category>
</categories>
</entry>
<entry>
<title>操作系统</title>
<link href="/1079.html"/>
<url>/1079.html</url>
<content type="html"><![CDATA[<h2 id="计算机系统概述"><a href="#计算机系统概述" class="headerlink" title="计算机系统概述"></a>计算机系统概述</h2><p>基本构成</p><p>处理器</p><p>内存</p><p>输入/输出模块</p><p>系统总线</p><p>微处理器的演化</p><p>指令的执行</p><p>中断</p><p>分类:程序中断(算术溢出、除数为零、执行非法的机器指令、访问用户不允许的存储位置);时钟中断;I/O中断;硬件失效中断;</p><p>中断和指令周期</p><p>中断处理</p><p>程序状态字(PSW)、程序计数器(PC)</p><p>运行中断处理程序</p><p>保存被中断程序的所有状态信息并在以后恢复这些信息,这是十分重要的,因为中断并不是程序调用的一个例程,它可以在任何时候发生,因而可以在用户程序执行过程中的任何一点上发生,它的发生是不可预测的。</p><p>多个中断</p><p>顺序中断处理;嵌套中断处理;考虑优先级的中断处理</p><p>存储器的层次结构</p><p>价格、容量、访问时间之间存在着一定的折衷;</p><p>层次结构能够奏效的原因在于低层访问频率递减(基础是局部性原理)</p><p>高速缓存(利用局部性原理)</p><p>高速缓存对操作系统不可见,但它与其他存储管理硬件相互影响。</p><p>动机</p><p>高速缓存原理</p><p>局部性原理</p><p>高速缓存设计</p><p>设计点:高速缓存大小;块大小(大小影响命中率);映射函数(设计的命中率要高;函数越灵活,逻辑电路设计就越复杂);置换算法(最近最少算法);写策略(何时发生存储写操作;当块被置换时才发生写操作);高速缓存级数。</p><p>直接内存存取</p><p>可编程I/O</p><p>处理器必须等待很长时间,以确定I/O模块是否做好了接受或发送更多数据的准备。</p><p>中断驱动I/O</p><p> I/O传输速度受限于处理器测试设备和提供服务的速度</p><p>处理器忙于管理I/O传送的工作,必须执行很多指令以完成I/O传送</p><p>直接内存存取(DMA)</p><p>大量移动数据,DMA模块负责</p><p>多处理器和多核计算组织结构(提供并行的手段)</p><p>对称多处理器(SMP)</p><p>性能、可用性、将进式增长、可伸缩性</p><p>高速缓存的一致性问题</p><p>多核计算机(单芯片多处理器)</p><p>预取机制</p><h2 id="操作系统概述"><a href="#操作系统概述" class="headerlink" title="操作系统概述"></a>操作系统概述</h2><p>设计目标:方便、有效、扩展能力</p><p>系统程序:实用工具或库程序,实现了在创建程序、管理文件和控制I/O设备中经常使用的功能。</p><p>提供的服务:程序开发:编辑器;调试器(应用程序开发工具)</p><p>程序运行:操作系统为程序的运行调度资源</p><p> I/O设备访问:操作系统隐藏一些细节,并提供接口</p><p>文件访问控制:I/O设备的特性、访问保护机制</p><p>系统访问:对于共享或公共资源,操作系统控制权限以及资源竞争的冲突问题</p><p>错误检测和响应:设计一些机制来应对程序运行时的错误</p><p>记账</p><p>三种重要的接口:指令系统体系结构(ISA):硬件于软件的分界线</p><p>应用程序二进制接口(ABI):</p><p>应用程序编程接口(API):</p><p>操作系统实际上是一组计算机程序,给处理器提供指令。处理器自身也是资源,供操作系统调配。操作系统的一部分为内核程序(含操作系统最常使用的功能)和当前正在使用的其他操作系统程序</p><h2 id="操作系统发展历史"><a href="#操作系统发展历史" class="headerlink" title="操作系统发展历史"></a>操作系统发展历史</h2><p>串行处理</p><p>调度</p><p>准备时间:程序运行前的准备工作</p><p>简单批处理系统</p><p>监控程序(控制事件顺序,完成调度功能):中断处理;设备驱动;作业序列;控制语言解释器;控制权交给作业仅意味着处理器当前取得和执行得都是用户程序中得指令,而控制权返回给监控程序意味着处理器当前从监控程序中取指令并执行指令。其他功能:内存保护;定时器特权指令;中断;</p><p>处理器角度</p><p>缺点:内存交给监控程序;监控程序消耗了一部分机器时间</p><p>多道批处理系统(为解决简单批处理系统的问题而生)</p><p>支持I/O中断;DMA</p><p>待运行作业留在内存中,因此有内存管理。</p><p>运行多个作业,因此有调度算法。</p><p>分时系统(因为交互作业而生)</p><p>一提出新系统就会引发新的问题,如资源竞争的问题、文件系统保护的问题(授权)。</p><p>成就</p><p>进程(比作业这一概念更为通用):</p><p>一段可执行程序;程序所需相关数据;程序的执行上下文(进程状态);</p><p>内存管理</p><p>进程隔离;自动分配管理;支持模块化程序设计;保护和访问控制;长期存储;</p><p>文件系统是访问控制和保护的一个有用的单元</p><p>信息保护和安全</p><p>可用性;保密性;数据完整性;认证</p><p>调度和资源管理(运筹问题)</p><p>设计策略考虑的因素:公平性;有差别的响应性;有效性;</p><p>现代操作系统特征</p><p>微内核体系结构(只分配一些基本的功能;可使系统机构的设计更加简单、灵活,非常适合于分布式环境);</p><p>分布式操作系统;</p><p>多线程(可把应用程序的进程划分为同时运行的多个线程);</p><p>线程:包含上下文环境和栈中自身的数据区域;可以执行中断;</p><p>进程:一个或多个线程和相关系统资源(含有数据和代码的存储器空间、打开的文件和设备)的集合。严格对应于一个正在执行程序的概念。把进程分解为多个线程,程序员可以在很大程度上控制应用程序的<strong>模块性</strong>以相关<strong>事件的时间安排</strong>。应用线程时,线程间切换时的开销少,相对于进程间的切换。</p><p>面向对象设计;</p><p>对称多处理;</p><p>性能:多个进程可分别自不同的处理器上同时运行</p><p>可用性:单个处理器的失效并不会导致机器的额停止,相反,OS可继续运行,只是性能有所降低。</p><p>增量增长:可以添加额外的处理器来增强系统的功能;</p><p>可扩展性:根据系统配置的处理器数量,来提供不同嘉禾和性能特征的产品。</p><p> 面向对象设计:用于给小内核增加模块化的扩展。</p><p>容错性</p><p>可靠性:</p><p>平均失效时间(MTTF)</p><p>可用性:系统不可用时间为宕机时间</p><p>错误:</p><p>硬件设备或组件缺陷,如短路或线路损坏;</p><p>计算机程序中不正确的步骤、过程或数据定义。</p><p>永久性错误;硬盘磁头损坏、软件错误、通信部件损坏</p><p>临时性错误; </p><p>一瞬时错误:冲激噪声导致的位传输错误、电源故障</p><p>一间歇性错误:连接松动</p><p>系统的容错性是通过增加冗余度来实现的</p><p>空间冗余:多条并行线路并以多数输出的结果作为输出;备用域名服务器</p><p>时间冗余:检测到错误时重复某一功能或操作。对临时性错误有效,而对永久性的无效,检测异常时,用数据链路控制协议重传数据块。</p><p>信息冗余:通过复制或编码数据的方式来检测和修复位数据,进而提高容错性。存储系统用的差错控制编码电路和RAID磁盘所用的纠错技术。</p><p>用操作系统机制来提高容错性</p><p>进程隔离:</p><p>并发控制:</p><p>虚拟机:</p><p>监测点和回滚机制:</p><p>多处理器和多核操作系统设计考虑因素</p><p>对称多处理器操作系统设计考虑因素</p><p>并发进程或线程:</p><p>调度:</p><p>同步:锁是一种通用的同步机制</p><p>内存管理:分页机制</p><p>可靠性和容错性</p><p>多核操作系统设计考虑因素</p><p>多核设计的核心关注点在于将多核系统固有的并行能力与应用程序的性能需求相匹配。(①,硬件并行,即指令并行;处理器层次上的潜在并行能力,即在每个处理器上多道程序或多线程程序的执行能力;在多核上一个应用程序以并发多进程或多线程形式执行的潜在并行能力)</p><p>应用层并行:GCD为一种线程池机制</p><p>虚拟机方式:操作系统需要一块受保护的空间,以避免用户和程序的干扰,于是出现了内核模式和用户模式的区别。</p><p>Win8从根本上改变了操作系统的内核结构,尤其是线程管理和虚拟内存管理。</p><p>内核是操作系统中唯一不可抢占或分页的部分;内核使用C语言编写,采用的设计原理和面向对象设计密切相关。面向对象方法简化了进程间资源和数据的共享,便于保护资源免受未经许可的访问。</p><p>UNIX系统</p><p>所有的UNIX实现都是用C语言编写</p><p>Linux操作系统</p><p> Linux可加载模块特性:</p><p>动态链接:</p><p>可堆叠模块:</p><p>Android</p><p>操作系统可想象未资源的统一表示,它可被应用程序请求和访问。资源包括内存、网络接口和文件系统等。操作系统为应用程序创建这些资源的抽象表示后,就必须管理它们的使用,例如操作系统可允许资源共享,也可允许资源保护。</p><p>操作系统有序管理应用程序的执行所达到的目标:</p><p>资源对多个应用程序时可用的。</p><p>物理处理器在多个应用程序间切换,以保护所有程序都在执行中。</p><p>处理器和I/O设备能得到充分利用。</p><h2 id="进程"><a href="#进程" class="headerlink" title="进程"></a>进程</h2><p>进程定义,进程与进程控制块之间的关系</p><p>进程的基本元素:程序代码;相关数据集;进程控制块;</p><p>进程控制块:进程表示信息;进程状态信息;进程控制信息;</p><p>进程控制块是操作系统中最重要的数据结构,每个进程控制块都包含操作系统所需进程的所有信息。</p><p>进程描述:</p><p>操作系统的控制结构</p><p>维护四种不同类型的表:</p><p>内存表:</p><p>跟踪内存和外存</p><p>I/O表:</p><p>文件表:</p><p>进程表:</p><p>进程控制结构</p><p>进程位置</p><p>进程映像:程序;数据;栈;属性;</p><p>线程</p><p>进程控制:</p><p>执行模式</p><p>用户模式;系统模式(控制模式、内核模式)</p><p>操作系统内核的典型功能:进程管理;内存管理;I/O管理;支持功能(中断处理;记账;监视);</p><p>进程的创建</p><p>分配标识符</p><p>分配空间</p><p>初始化进程控制块</p><p>真确的链接</p><p>创建或扩充其他数据结构</p><p>进程的切换</p><p>何时切换</p><p>中断(时钟、I/O、陷阱(非法的文件访问))</p><p>内存失效</p><p>进程切换与模式切换不同,模式切换可在不改变运行态进程的状态的情况下出现,此时保存上下文并在以后恢复上下而温暖仅需要很少的开销。但是,若当前正在运行进程将转换为另一状态(就绪、阻塞),则操作系统必须使环境产生实质性的变化。</p><p>进程切换步骤:</p><p>操作系统的执行</p><p>OS与普通计算机软件以同样的方式运行,即它也是由处理器执行多额一个程序</p><p>OS会频繁地释放控制权,并依赖于处理器来恢复控制权。</p><p>无进程内核:进程这一概念仅适用于用户程序,而OS代码则是在特权模式下单独运行的实体。</p><p>较小计算机操作系统(PC、工作站):操作系统例程在用户进程内运行</p><p>基于进程的OS:把OS作为一组系统进程来实现。</p><p>Unix:</p><p>进程描述,采用复杂的数据结构对操作系统管理进程和分派进程所需的信息进行描述。</p><h2 id="线程:"><a href="#线程:" class="headerlink" title="线程:"></a>线程:</h2><p>进程的特点:</p><p>资源所有权:</p><p>调度/执行:</p><p>多线程:OS在单个进程内支持多个并发执行路径的能力。</p><p>在多线程环境中,进程定义为资源分配单元和一个保护单元。进程涉及资源所有权,而线程涉及程序的执行</p><p>创建一个线程要快;终止一个线程要快;线程之间切换的时间要少;提高了笔筒执行程序之间的通信的效率(进程间切换需要内核的介入,此时内核提供保护和通信机制,由于同一进程中多个线程共享内存和文件,因此就无需调用内核就可以相互通信);例如:文件服务器应用程序。前台和后台工作;异步处理;执行速度;模块化程序结构;</p><p>调度和分派实在线程基础上完成的,因此大多是于执行相关的信息可以保存在线程级的数据结构中。</p><p>挂起态对线程没有意义</p><p>线程同步带来的问题和使用的技术通常于进程同步相同</p><p>线程分类</p><p>用户级线程(ULT):</p><p>内核级线程(KLT):</p><p>使用ULT而非KLT的优点如下:</p><p>所有线程管理数据结构都在一个进程的用户地址空间中,线程切换不需要内核模式特权,因此进程不需要为了管理线程而切换到内核模式,进而节省了两次状态转换(从用户模式到内核模式,以及从内核模式返回用户模式)的开销</p><p>调度因应用程序的不同而不同。为了不要然乱底层的操作系统调度程序,可以做到为应用程序量身定做调度算法。</p><p> ULT可在任何操作系统中运行,不需要对底层内核进行修改以支持ULT,线程库是提供所有应用程序共享的一组应用级函数。</p><p>缺点:ULT执行一个系统调用时,不仅会阻赛线程,也会阻塞进程中的所有线程。</p><p>在纯ULT中,多线程应用程序不能利用多处理技术。</p><p>解决线程阻塞的方法:</p><p>把应用程序写成一个多进程程序而非多线程程序,但是,这种方法避开了线程的主要优点,每次切换都换成进程间的切换而导致开销过大。</p><p>使用“套管”技术</p><p>内核级线程:KLT方法,克服了ULT方法的两个缺点;内核例程自身也可是多线程的。</p><p>KLT的缺点:把控制权从一个线程传送到同一个进程内的另一个线程时,要切换到内核模式。</p><p>KLT、ULT、进程操作执行时间,相差数量级。而速度能否实现,则取决于应用程序的性质。若应用程序的大多数线程切换都西药内核模式访问时,基于ULT的方案不见得会比基于KLT的方案好。</p><p>有时使用混合的方法会克服ULT、KLT各自的缺点,如Solaris操作系统。</p><p>线程和进程的关系以及对性能的影响</p><p>线程于进程之间的比例关系</p><p>多核和多线程</p><p>加速比</p><p>从多核和多线程中获益的程序</p><p>Win8进程和线程管理</p><p>进程:由一个或多个进程组成</p><p>用户模式调度:是应用程序用于安排自己的线程的一种轻量级机制。</p><p>Solaris的线程和SMP管理</p><p>使用内核线程来处理中断;内核控制对数据结构的访问,并使用互斥原语在中断线程间进行同步;中断线程被赋予更高的优先级,高于所有其他类型的内核线程。</p><p>Linux的进程当作线程管理器</p><p> Linux中没有给线程单独定义数据结构,因此Linux中的进程和线程没有区别。</p><p>虽然同一个进程组的克隆进程共享同一内存空间,但不能共享同一个用户栈。所以clone()调用会为每个进程创建独立的栈空间。</p><p>命名空间可使一个进程拥有于其他相关命名空间下的其他进程不同的系统视图</p><p>安卓应用:安卓应用都包含一个或多个实例,而每个实例由一个或多个4种类型的应用程序组件组成。(活动、服务、内容提供器、广播接收器)</p><p>在一个给定的应用,其所有进程和线程都在相同的虚拟机中执行。按优先级层次结构来结束进程</p><p>常用栈、队列来对活动顺序控制</p><h2 id="并发性:互斥和同步"><a href="#并发性:互斥和同步" class="headerlink" title="并发性:互斥和同步"></a>并发性:互斥和同步</h2><p>操作系统设计的核心问题是进程和线程的管理</p><p>多道程序设计技术</p><p>多处理器技术</p><p>分布式处理技术</p><p>三种不同的上下文:</p><p>多应用程序</p><p>结构化</p><p>操作系统结构</p><p>共享资源保护问题</p><p>数据一致性</p><p>最终结果取决于多个进程的指令执行顺序</p><p>操作系统必须保护每个进程的数据和物理资源,避免其他进程的五一干扰。</p><p>进程的功能和输出结果必须与执行速度无关(相对于其他并发进程的执行速度)</p><p>与并发相关的术语:原子操作、临界区、死锁、活锁、互斥、竞争条件、饥饿</p><p>操作系统关注的问题:</p><p> OS必须能够跟踪不同的进程(进程控制块)</p><p> OS必须为每个活动进程分配和释放各种资源,这些资源包括</p><p>处理器时间;存储器;文件;I/O设备;</p><p>OS必须保护每个进程的数据和物理资源,避免其他进程的五一干扰,这涉及存储器、文件和I/O设备相关的技术。</p><p>支持并发的机制(满足互斥的机制)</p><p>硬件机制:</p><p>使用专用机器指令,虽然可以减少开销,但却很难成为一种通用的解决方案。</p><p>机器指令用于保证两个动作的原子性,指令完成后,另一个指令才能执行。</p><p>优点:适用于单处理器或共享内存的多处理器上的任意数量的进程;简单且易于证明;可用于支持多个临界区,每个临界区可以用它自己的变量定义。</p><p>缺点:使用了忙等待;可能饥饿;可能思索。</p><p>中断禁用:代价高;效率低不适用于多处理器结构中</p><p>软件机制:</p><p>让并发执行的进程承担这一责任,这类进程需要与另一个进程合作,而不需要程序设计语言或操作系统提供任何支持来实施互斥。但是这种方法会增加开销并存在缺陷。</p><p>进程之间的交互(解决与执行速度无关的问题):</p><p>互相不知道对方的存在(竞争);</p><p>进程间知道对方的存在(通过共享合作);</p><p>进程直接知道对方的存在(通过通信合作);</p><p>操作系统或程序设计语言中提供某种级别的支持。</p><p>信号量;管程;消息传递;</p><p>二元信号量(互斥锁);互斥量;条件变量;事件标志;信箱/消息;自旋锁;</p><p>资源的竞争:在不知道其他进程存在的情况下共享资源</p><p>(面临互斥、死锁、饥饿三个控制问题。只有一个进程能在临界区。)</p><p>进程通过共享合作:它们共享变量,每个进程并未明确地知道其他进程的存在,只知道要维护数据的完整性。</p><p>进程间通过通信合作:在传递消息的过程中进程间未共享任何对象,因而这类合作不需要互斥,但仍然存在思索和饥饿问题。</p><p>任何进程间的复杂合作都可以通过适当的信号结构得到满足。</p><p>s.count>=0:s.count是可执行semWait(t)而不被阻塞的进程数[期间无semSignal(s)执行]。这种情形允许信号量支持同步与互斥。</p><p>s.count<0:s.count的大小是阻塞在s.queue队列中的进程数。</p><p>并发中常见的问题:</p><p>生产者/消费者</p><p>问题是要确保这种情况:当缓存已满时,生产者不会继续向其中添加数据;当缓存为空时,消费者不会从中移走数据。</p><p>理发店问题:</p><p>信号量的实现:</p><p>硬件方案:硬件或固件实现</p><p>软件方案:Dekker算法或者Peterson算法</p><p>管程</p><p>和信号量相比,管程时一种程序设计语言结构,提供的功能和信号量相同,更易于控制。可以用管程锁定任何对象,对类似于链表之类的对象,可以用一个锁锁着整个链表,也可每个表用一个锁,还可以为表中的每个元素用一个锁。</p><p>管程是由一个或多个过程、一个初始化序列和局部数据组成的软件模块,主要特点如下:</p><p>局部数据只能被管程的过程访问;</p><p>一个进程通过调用管程的一个过程进入管程;</p><p>任何时候,只有一个进程在管程中执行,调用管程的其他进程都被阻塞,以等待管程可用。</p><p>管程提供了互斥机制;管程通过使用<strong>条件变量</strong>来支持同步,这些条件变量包含在管程中,并且只有在管程中才能被访问。</p><p>管程优于信号量之处在于,所有的同步机制都被限制在管程内部,因此不但<strong>容易验证同步的正确性,而且易于检测出错误</strong>。<strong>此外,若一个管程被正确地编写,则所有进程对受保护资源的访问都是正确的;而对于信号量,只有当所有访问资源的进程被正确编写时,资源才是正确的</strong>。</p><p>管程的通知与广播</p><p>广播可以使所有在该条件上等待的进程都置于就绪状态,当一个进程不知道由多少进程将被激活时,这种方式非常方便;此外,当一个进程难以准确地判定将激活哪一个进程时,也可使用广播。</p><p> Lampson/Redell管程由于Hoare管程的原因是,Lampson/Redell方法错误较少。在Lampson/Redell方法中,由于每个过程在受到信号后都检查管程变量,且由于使用了while结构,一个进程不正确地广播或发信号,不会导致受到信号的程序出错。受到信号的程序将检查相关的变量,如果期望的条件得不到满足,它会继续等待。</p><p> Lampson/Redell管程的另一个优点是,它有助于在程序结构中采用更加模块化的方法。例如,考虑一个缓冲区分配程序的实现,为了在顺序的进程间合作,必须满足两级条件:保持一致的数据结构。管程强制实施互斥,并在允许对缓冲区的另一个操作之前完成一个输入或输出操作;在1级条件的基础上,为该进程加上足够的内存,完成其分配请求。</p><p>消息传递</p><p>同步</p><p>阻塞send,阻塞receive;无阻塞send,阻塞receive;无阻带send,无阻塞receive;</p><p>寻址</p><p>直接寻址:</p><p>间接寻址:通过信箱传递信息。解除了发送者和接受者之间的耦合关系,可以更灵活地使用消息。发送者和接受者之间的关系可以是1对1(专用通信连接)、也可以是1对多(广播消息)、多对一(关系对客户服务器间地交互非常有用)、多对多(多个服务进程对多个客户进程提供服务)</p><p>消息格式</p><p>取决于消息机制的目标,以及该机制是运行在一台计算机上还是运行在分布式系统中。</p><p>排队原则</p><p>先进先出;优先级;允许接收者检查消息队列并选择下一次接受那个消息。</p><p>互斥</p><p>读者/写者的问题</p><p>读者优先</p><p>使用信号量的解决方案</p><p>写者优先</p><p>并发:死锁和饥饿</p><p>死锁:一组相互竞争系统资源或通信的进程间的“永久”阻塞。</p><p>可重用资源</p><p>资源:可重用资源和可消耗资源</p><p>可重用资源指一次仅供一个进程安全使用且不因使用而耗尽的资源。进程得到资源单元并使用后,会释放这些单元供其他进程再次使用。可重用资源的例子包括处理器、I/O通道、内存和外存、设备,以及诸如文件、数据库和信号量之类的数据结构。</p><p>并发程序设计非常具有挑战性,因此这类死锁的确会发生,而发生的原因 通常隐藏在复杂的程序逻辑中,因此检测非常困难。处理这类死锁的一个策略是,给系统设计施加关于资源请求顺序的约束。</p><p>内存请求,有时,不事先知道请求的存储空间总量,很难通过系统设计约束来处理这类死锁。解决这类特殊问题的最好办法是,使用虚存有效地消除这种可能性。</p><p>可消耗资源是指可被创建和销毁地资源。某种类型可消耗资源的额数量通常设有限制,无阻塞生产进程可以创建任意数量地这类资源。消费进程得到一个资源时,该资源就不再存在。可销耗资源地例子有中断、信号、消息和I/O缓冲区中的信息。</p><p>引发死锁的原因是一个设计错误,这类错误比较微妙,因而很难发现。此外,罕见大的事件组合也会导致死锁,因此只有当程序使用了相当常一段时间甚至几年之后,才可能出现这类问题(即发生死锁)。</p><p>不存在解决所有类型死锁的有效策略。</p><p> </p><p>Hlot的资源分配图</p><p>死锁的条件(而非充要条件):</p><p>互斥;占有等待;不可抢占;(死锁的必要条件,也就是联合进程图的敏感区产生的条件)</p><p>循环等待;</p><p>这四个条件构成了死锁的充分必要的条件。处理死锁的三种方法:一十采用某种策略消除四个条件中断额某个条件的出现来预防死锁;二是基于资源分配的当前状态做动态选择来避免死锁;三是试图检测死锁(满足四个条件)的存在并从死锁中恢复。</p><p>死锁预防:</p><p>设计一种系统来排除死锁的可能性</p><p>间接死锁预防:防止三个必要条件中的任何一个发生</p><p>直接死锁预防:防止循环等待的发生。</p><p>占有且等待: 预防时,可要求进程一次性地请求所有需要的资源,并阻塞这个进程直到所有请求都同时满足。这个方法存在的弊端也是应用程序在使用模块化程序设计或多线程结构时产生的实际问题。要同时请求所需的资源,应用程序需要直到其以后将在所有级别或所有模块中请求的所有资源。</p><p>预防不可抢占的进程;占有资源的进程进一步申请资源时若被拒绝,则该进程必须释放其最初占有的资源,必要时可再次申请这些资源和其他资源;一个进程请求当前被另一个进程占有的一个资源时,操作胸痛可以抢占另一个进程,要求它释放资源。</p><p>预防循环等待:定义资源类型的线性顺序来预防。若一个进程已经分配了R类型的资源,则其接下来请求的资源只能是哪些排在R类型之后的资源。</p><p>死锁避免:</p><p>允许三个必要条件,但通过明智的选择,可确保永远不会到达死锁点,因此死锁比避免与死锁预防相比,可允许更多的并发。在死锁避免中,是否允许当前的资源分配请求时通过判断该请求是否可能导致死锁来决定的。因此,死锁避免要直到未来进程资源亲求的情况。方法:若一个进程的请求会导致死锁,则不启动该进程;若进程增加的资源请求会导致死锁,则不允许这一资源分配。</p><p>进程启动拒绝:只有满足所有当前进程的最大亲求量及新的进程请求时,才会启动该进程。这个策略不是最优的,因为假设了最坏的情况:所有进程同时发出它们的最大请求。</p><p>资源分配拒绝(银行家算法——测试安全算法):</p><p>安全状态:至少一个资源分配序列不会导致死锁;不安全状态:指非安全的一个状态。</p><p>死锁避免的优点时,无需死锁预防中的抢占和回滚进程,且与死锁预防相比限制较少,但是,它在使用中也有许多限制:</p><p>必须事先声明每个进程请求的最大资源</p><p>所讨论的进程必须时无关的,</p><p>分配的资源数量必须时固定的</p><p>在占有资源时,进程不能推出。</p><p> 死锁检测</p><p>通过限制访问资源和进程上强约束来解决死锁问题</p><p>死锁可以频繁地在每个资源请求发生时进行,也可以进行得少一些,具体取决于发生死锁得可能性。</p><p>检测到死锁后,就需要采用某种策略类恢复死锁</p><p>取消所有死锁进程;</p><p>回滚,重启机制;</p><p>连续取消死锁进程直到不在存在死锁</p><p>连续抢占资源直到不在存在死锁</p><p> 一种综合的死锁策略</p><p>不同情况下采用不同的策略</p><p>可交换空间:一次性分配所有请求资源来预防死锁(进程交换所用外存中的存储块)</p><p>进程资源:死锁避免策略通常是很有效的,因为进程可以事先声明它们需要的这类资源。(可分配的设备,如磁带设备和文件)</p><p>内存:基于抢占的预防是最适合的策略(可按页或段分配给进程)</p><p>内部资源:可以使用基于资源排序的预防策略(I/O通道)</p><p> 哲学家就餐问题</p><p>可视为应用程序中包含并发执行的线程时,协调处理共享资源的代表性问题</p><p>基于信号量的解决方案</p><p>允许五个人,会存在死锁;只允许四个人就座,不会死锁与饥饿</p><p>基于管程的解决方案</p><p>管程不会发生死锁,因为在同一时刻只有一个进程进入管程。比如,第一位哲学家进入了管程保证了只要他拿起左边的额叉子,其右边的哲学家可以拿到其左边的叉子前,就一定可以拿到右边的叉子。</p><p>UNIX并发机制</p><p><strong>管道、消息和共享内存</strong>提供了进程间传递数据的方法,而<strong>信号量和信号</strong>则用于触发其他进程的行为。</p><p>Linux内核并发机制</p><p>管道;消息;共享内存;信号;实时信号</p><p>原子操作时内核同步法中最简单的。在原子操作的基础上,可构建更复杂的锁机制。</p><p>自旋锁(保护临界区)</p><p>Solaris线程同步原语</p><p>互斥锁;信号量;多读者单写者;条件变量;</p><p>Windows7的并发机制</p><p>等待函数;分派器对象;临界区;轻量级读写锁和条件变量;锁无关同步机制</p><p>Android进程间的通信</p><p>IPC中使用的机制是,在内核中新增了一个连接器,他提供了一个轻量级的远程程序调用功能,在内存和事务处理方面非常高效,非常适合嵌入式系统。</p><h2 id="I-0"><a href="#I-0" class="headerlink" title="I/0"></a>I/0</h2><h2 id="基本概念"><a href="#基本概念" class="headerlink" title="基本概念"></a>基本概念</h2><h3 id="文件描述符fd"><a href="#文件描述符fd" class="headerlink" title="文件描述符fd"></a>文件描述符fd</h3><p>文件描述符(<code>File descriptor</code>)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。</p><p>文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于 <code>UNIX、Linux</code> 这样的操作系统。</p><h3 id="缓存-I-O"><a href="#缓存-I-O" class="headerlink" title="缓存 I/O"></a>缓存 I/O</h3><p>缓存 <code>I/O</code> 又被称作标准 <code>I/O</code>,大多数文件系统的默认 <code>I/O</code> 操作都是缓存 <code>I/O</code>。在 <code>Linux</code> 的<code>缓存 I/O</code> 机制中,操作系统会将 <code>I/O</code> 的数据缓存在文件系统的页缓存( <code>page cache</code> )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。</p><p>缓存 I/O 的缺点:数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 <code>CPU</code> 以及内存开销是非常大的。</p><h2 id="IO模式"><a href="#IO模式" class="headerlink" title="IO模式"></a>IO模式</h2><p>刚才说了,对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:</p><ol><li>等待数据准备</li><li>将数据从内核拷贝到进程中</li></ol><p>正式因为这两个阶段,Linux系统产生了下面五种网络模式的方案。</p><ul><li><code>阻塞 I/O</code>(blocking IO)</li><li><code>非阻塞 I/O</code>(nonblocking IO)</li><li><code>I/O 多路复用</code>( IO multiplexing)</li><li><code>信号驱动 I/O</code>( signal driven IO)</li><li><code>异步 I/O</code>(asynchronous IO)</li></ul><blockquote><p>由于signal driven IO在实际中并不常用,所以这里只提及剩下的四种 IO Model。</p></blockquote><h3 id="阻塞IO"><a href="#阻塞IO" class="headerlink" title="阻塞IO"></a>阻塞IO</h3><p>在 <code>Linux</code> 中,默认情况下所有的 <code>socket</code> 都是 <code>blocking</code> ,一个典型的读操作流程大概是这样:</p><p><img src="https://hadyang.github.io/interview/docs/basic/os/io/images/359a774ea7d5d1e6ac08845023993796.png" alt="img"></p><p>当用户进程调用了 <code>recvfrom</code> 这个系统调用, <code>kernel</code> 就开始了 IO 的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的 <code>UDP</code> 包。这个时候 <code>kernel</code> 就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当 <code>kernel</code> 一直等到数据准备好了,它就会将数据从 <code>kernel</code> 中拷贝到用户内存,然后 <code>kernel</code> 返回结果,用户进程才解除 <code>block</code> 的状态,重新运行起来。</p><blockquote><p>blocking IO的特点就是在IO执行的两个阶段都被block了</p></blockquote><h3 id="非阻塞-I-O"><a href="#非阻塞-I-O" class="headerlink" title="非阻塞 I/O"></a>非阻塞 I/O</h3><p><code>Linux</code> 下,可以通过设置 <code>socket</code> 使其变为 <code>non-blocking</code> 。当对一个 <code>non-blocking socket</code> 执行读操作时,流程是这个样子:</p><p><img src="https://hadyang.github.io/interview/docs/basic/os/io/images/076dcab40e2b43efa5d1aa97d96a85e2.png" alt="img"></p><p>当用户进程发出 <code>read</code> 操作时,如果 <code>kernel</code> 中的数据还没有准备好,那么它并不会 <code>block</code> 用户进程,而是立刻返回一个 <code>error</code> 。从用户进程角度讲 ,它发起一个 <code>read</code> 操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个 <code>error</code> 时,它就知道数据还没有准备好,于是它可以再次发送 <code>read</code> 操作。一旦 <code>kernel</code> 中的数据准备好了,并且又再次收到了用户进程的 <code>system call</code> ,那么它马上就将数据拷贝到了用户内存,然后返回。</p><blockquote><p>nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有</p></blockquote><h3 id="IO多路复用"><a href="#IO多路复用" class="headerlink" title="IO多路复用"></a>IO多路复用</h3><p>IO多路复用就是我们说的 <code>select,poll,epoll</code> ,有些地方也称这种IO方式为 <code>event driven IO</code> 。<code>select/epoll</code> 的好处就在于单个 <code>process</code> 就可以同时处理多个网络连接的 IO 。它的基本原理就是 <code>select,poll,epoll</code> 这个 <code>function</code> 会不断的轮询所负责的所有 <code>socket</code> ,当某个 <code>socket</code> 有数据到达了,就通知用户进程。</p><p><img src="https://hadyang.github.io/interview/docs/basic/os/io/images/c6d2db53d71a8c76c2c9a546c5811773.png" alt="img"></p><p><strong>当用户进程调用了 select,那么整个进程会被 block</strong>,而同时, <code>kernel</code> 会监视所有 <code>select</code> 负责的 <code>socket</code> ,当任何一个 <code>socket</code> 中的数据准备好了, <code>select</code> 就会返回。这个时候用户进程再调用 <code>read</code> 操作,将数据从 <code>kernel</code> 拷贝到用户进程。</p><blockquote><p>I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,<code>select()</code> 函数就可以返回。</p></blockquote><p>这个图和 <code>blocking IO</code> 的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个 <code>system call</code> (<code>select</code> 和 <code>recvfrom</code>),而 <code>blocking IO</code> 只调用了一个 <code>system call</code> (<code>recvfrom</code>)。但是,用 <code>select</code> 的优势在于它可以同时处理多个 <code>connection</code> 。</p><p>所以,<strong>如果处理的连接数不是很高的话,使用 <code>select/epoll</code> 的 <code>web server</code> 不一定比使用 <code>multi-threading + blocking IO</code> 的 <code>web server</code> 性能更好,可能延迟还更大</strong>。<code>select/epoll</code> 的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。</p><p>在IO多路复用实际使用中,对于每一个socket,一般都设置成为 <code>non-blocking</code> ,但是,如上图所示,整个用户的 <code>process</code> 其实是一直被block的。只不过 <code>process</code> 是被 <code>select</code> 这个函数 <code>block</code> ,而不是被 <code>socket IO</code> 给 <code>block</code> 。</p><h4 id="基本概念-1"><a href="#基本概念-1" class="headerlink" title="基本概念"></a>基本概念</h4><p>在 I/O 编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者 <code>I/O 多路复用</code> 技术进行处理。<strong><code>I/O多路复用</code> 技术通过把多个I/O的阻塞复用到同一个selct的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求</strong>。<strong>与传统的 <code>多线程/多进程</code> 模型比,I/O多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源</strong>,I/O多路复用的主要应用场景如下。</p><ul><li>服务器需要同时处理多个处于监听状态或者多个连接状态的套接字</li><li>服务器需要同时处理多种网络协议的套接字</li></ul><p>目前支持I/O多路复用的系统调用有 <code>select、pselect、poll、epoll</code>,在Linux网络编程; 过程中,很长一段时间都使用 <code>select</code> 做轮询和网络事件通知,然而 <code>select</code> 的一些固有缺陷导致了它的应用受到了很大的限制。最终 <code>Linux</code> 不得不在新的内核版本中寻找 <code>select</code> 的替代方案,最终选择了 <code>epoll</code>。 <code>epoll</code> 与 <code>select</code> 的原理比较类似,为了克服 <code>select</code> 的缺点, <code>epoll</code> 作了很多重大改进,现总结如下。</p><h5 id="支持一个进程打开的-socket-描述符(FD)不受限制(仅受限于操作系统的最大文件句柄数)。"><a href="#支持一个进程打开的-socket-描述符(FD)不受限制(仅受限于操作系统的最大文件句柄数)。" class="headerlink" title="支持一个进程打开的 socket 描述符(FD)不受限制(仅受限于操作系统的最大文件句柄数)。"></a>支持一个进程打开的 socket 描述符(FD)不受限制(仅受限于操作系统的最大文件句柄数)。</h5><p><strong><code>select</code> 最大的缺陷就是单个进程所打开的 FD 是有一定限制的</strong>,它由 <code>FD_SETSIZE</code> 设置,默认值是 <code>1024</code> 。对于那些需要支持上万个 TCP 连接的大型服务器来说显然太少了。可以选择修改这个宏然后重新编译内核,不过这会带来网络效率的下降。我们也可以通过选择多进程的方案(传统的 Apache 方案)解决这个问题,不过虽然在 Linux上创建进程的代价比较小,但仍旧是不可忽视的,另外,进程间的数据交换非常麻烦,对于 Java 由于没有共享内存,需要通过 <code>Socket</code> 通信或者其他方式进行数据同步,这带来了额外的性能损耗,增加了程序复杂度,所以也不是一种完美的解决方案。值得庆幸的是, <code>epoll</code> 并没有这个限制,它所支持的 <code>FD</code> 上限是操作系统的 <strong>最大文件句柄数</strong>,这个数字远远大于 <code>1024</code> 。例如,在 <code>1 GB</code> 内存的机器上大约是 10万个句柄左右,具体的值可以通过<code>cat /proc/sys/fs/file- max</code> 察看,<strong>通常情况下这个值跟系统的内存关系比较大</strong>。</p><h5 id="I-O效率不会随着FD数目的增加而线性下降。"><a href="#I-O效率不会随着FD数目的增加而线性下降。" class="headerlink" title="I/O效率不会随着FD数目的增加而线性下降。"></a>I/O效率不会随着FD数目的增加而线性下降。</h5><p>传统的 <code>select/poll</code> 另-个致命弱点就是当你拥有一个很大的 <code>socket</code> 集合,由于网络延时或者链路空闲,任一时刻只有少部分的 <code>socket</code> 是“活跃”的,但是 <strong><code>select/poll</code> 每次调用都会线性扫描全部的集合,导致效率呈现线性下降</strong>。 <code>epoll</code> 不存在这个问题,它只会对“活跃”的 <code>socket</code> 进行操作,这是因为在内核实现中 <code>epoll</code> 是根据每个 <code>fd</code> 上面的 <code>callback</code> 函数实现的,那么,只有“活跃”的 <code>socket</code> 才会主动的去调用 <code>callback</code> 函数,其他 <code>idle</code> 状态 <code>socket</code> 则不会。<strong>在这点上, <code>epoll</code> 实现了一个伪 AIO</strong>。针对 <code>epoll</code> 和 <code>select</code> 性能对比的 <code>benchmark</code> 测试表明:<strong>如果所有的 <code>socket</code> 都处于活跃态,例如一个高速 <code>LAN</code> 环境, <code>epoll</code> 并不比 <code>select/poll</code> 效率高太多;相反,如果过多使用 <code>epoll_ ctl</code> , 效率相比还有稍微的下降。但是一旦使用 <code>idleconnections</code> 模拟 <code>WAN</code> 环境,<code>epoll</code> 的效率就远在 <code>select/poll</code> 之上了</strong>。</p><h5 id="使用-mmap-加速内核与用户空间的消息传递"><a href="#使用-mmap-加速内核与用户空间的消息传递" class="headerlink" title="使用 mmap 加速内核与用户空间的消息传递"></a>使用 mmap 加速内核与用户空间的消息传递</h5><p>无论是 <code>select</code>,<code>poll</code> 还是 <code>epoll</code> 都需要内核把 FD 消息通知给用户空间,如何避免不必要的内存复制(Zero Copy)就显得非常重要, <code>epoll</code> 是通过内核和用户空间 <code>mmap</code> 共享同一块内存来实现。</p><h5 id="Epoll-的-API-更加简单"><a href="#Epoll-的-API-更加简单" class="headerlink" title="Epoll 的 API 更加简单"></a>Epoll 的 API 更加简单</h5><p>包括创建一个 <code>epoll</code> 描述符、添加监听事件、阻塞等待所监听的事件发生,关闭 <code>epoll</code> 描述符等。</p><p>值得说明的是,用来克服 <code>select/poll</code> 缺点的方法不只有 <code>epoll</code> , <code>epoll</code> 只是一种 <code>Linux</code> 的 实现方案。在 <code>freeBSD</code> 下有 <code>kqueue</code></p><h3 id="Epoll-边缘触发-amp-水平触发"><a href="#Epoll-边缘触发-amp-水平触发" class="headerlink" title="Epoll 边缘触发&水平触发"></a>Epoll 边缘触发&水平触发</h3><p>epoll 对文件描述符的操作有两种模式:LT(<code>level trigger</code>)和ET(<code>edge trigger</code>)。LT模式是 <strong>默认模式</strong> ,LT模式与ET模式的区别如下:</p><ul><li><strong>LT模式</strong>:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用 epoll_wait 时,会再次响应应用程序并通知此事件。</li><li><strong>ET模式</strong>:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用 epoll_wait 时,不会再次响应应用程序并通知此事件。</li></ul><blockquote><p>ET模式 在很大程度上减少了 epoll 事件被重复触发的次数,因此 <strong>效率要比LT模式高</strong>。epoll 工作在ET模式的时候,<strong>必须使用非阻塞套接口</strong>,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。</p></blockquote><h2 id="异步-I-O"><a href="#异步-I-O" class="headerlink" title="异步 I/O"></a>异步 I/O</h2><p><img src="https://hadyang.github.io/interview/docs/basic/os/io/images/3b385bdf805241ee6cd0d4634bd7510a.png" alt="img"></p><p>用户进程发起 <code>read</code> 操作之后,立刻就可以开始去做其它的事。而另一方面,从 <code>kernel</code> 的角度,当它受到一个 <code>asynchronous read</code> 之后,首先它会立刻返回,所以不会对用户进程产生任何 <code>block</code> 。然后,<code>kernel</code> 会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,<code>kernel</code> 会给用户进程发送一个 <code>signal</code> ,告诉它 <code>read</code> 操作完成了。</p><h2 id="blocking-vs-non-blocking"><a href="#blocking-vs-non-blocking" class="headerlink" title="blocking vs non-blocking"></a>blocking vs non-blocking</h2><p>调用 <code>blocking IO</code> 会一直 <code>block</code> 住对应的进程直到操作完成,而 <code>non-blocking IO</code> 在 <code>kernel</code> 还准备数据的情况下会立刻返回。</p><h2 id="synchronous-IO-vs-asynchronous-IO"><a href="#synchronous-IO-vs-asynchronous-IO" class="headerlink" title="synchronous IO vs asynchronous IO"></a>synchronous IO vs asynchronous IO</h2><p>在说明<code>synchronous IO</code>和<code>asynchronous IO</code>的区别之前,需要先给出两者的定义。 <code>POSIX</code> 的定义是这样子的:</p><ul><li>A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;</li><li>An asynchronous I/O operation does not cause the requesting process to be blocked;</li></ul><p>两者的区别就在于 <code>synchronous IO</code> 做 <code>IO operation</code> 的时候会将 <code>process</code> 阻塞。按照这个定义,之前所述的 <code>blocking IO,non-blocking IO,IO multiplexing</code> 都属于 <code>synchronous IO</code>。</p><p>有人会说,<code>non-blocking IO</code> 并没有被 <code>block</code> 啊。这里有个非常 <strong>狡猾</strong> 的地方,定义中所指的 <code>IO operation</code> 是指真实的 IO 操作,就是例子中的 <code>recvfrom</code> 这个 <code>system call</code> 。<code>non-blocking IO</code> 在执行 <code>recvfrom</code> 这个 <code>system call</code> 的时候,如果 <code>kernel</code> 的数据没有准备好,这时候不会 <code>block</code> 进程。但是,当 <code>kernel</code> 中数据准备好的时候,<code>recvfrom</code> 会将数据从 <code>kernel</code> 拷贝到用户内存中,这个时候进程是被 <code>block</code> 了,在这段时间内,进程是被 <code>block</code> 的。</p><p>而 <code>asynchronous IO</code> 则不一样,当进程发起 <code>IO</code> 操作之后,就直接返回再也不理睬了,直到 <code>kernel</code> 发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被 <code>block</code> 。</p><p> 参考:</p><p>【1】<a href="https://segmentfault.com/a/1190000003063859" target="_blank" rel="noopener">https://segmentfault.com/a/1190000003063859</a></p><p>【2】<a href="https://hadyang.github.io/interview/docs/basic/os/io/" target="_blank" rel="noopener">https://hadyang.github.io/interview/docs/basic/os/io/</a></p><h2 id="内存:"><a href="#内存:" class="headerlink" title="内存:"></a>内存:</h2><p>内存管理</p><p>单道程序设计系统中,内存划分为两部分,一部分供操作系统使用(驻留监控程序,内核),另一部分供当前正在执行的程序使用。在多道程序设计系统中,必须在内存中进一步划分用户部分,以满足多个进程的要求,细分的任务由操作系统动态完成,这称为内存管理。</p><p>内存管理术语:页框(内存中固定长度的块);页(二级存储器中固定长度的块);段(二级存储器中变长数据块);</p><p>内存管理需求:</p><p>重定位;保护;共享;逻辑组织;物理组织;</p><p>重定位:</p><p>内存分区:</p><p>固定分区的内部碎片</p><p>动态分区的外部碎片</p><p>放置算法:</p><p>固定分配</p><p>动态分配</p><p>最佳适配</p><p>首次适配</p><p>下次适配</p><p>动态分配克服外部碎片的方法:压缩</p><p>伙伴系统(在并行系统中有许多应用):</p><p> UNIX内核存储分配中使用了一种经过改进后的伙伴系统</p><p>重定位的硬件支持(针对进程的换进换出的地址变化的问题):重定位加载器</p><p>分页:</p><p>分页对程序员是透明的,而分段对程序员是可见的</p><p>内存管理是操作系统中最重要、最复杂的任务之一。内存管理吧内存视为一个资源,可以分配给许多活动进程,或由多个活动进程共享。为有效地使用处理器和I/O设备,需要在内存中保留尽可能多地进程。此外,程序员在开发程序时最好能不受程序大小地限制。</p><p>内存管理的基本工具是分页和分段。</p><p>虚拟内存</p><p>虚拟内存术语</p><p>虚拟内存;虚拟地址;虚拟地址空间;地址空间;实地址;</p><p>进程中的所有内存访问都是逻辑地址,这些逻辑地址会在运行时动态地转换为物理地址。这意味着一个进程可被换入或换出内存,因此进程可在执行过程地不同时刻占据内存中地不同区域。</p><p>一个进程可划分为许多块,在执行过程中,这些块不需要连续地位于内存中,动态运行时地址转换和也变或段表的使用使得这一点成为可能。</p><p>局部性原理:描述了一个进程中程序和数据引用的集簇倾向。局部性原理表明虚拟内存是可行的,要使虚存比较使用并且高效。首先,必须由对所采用分页或分段方案的硬件支持;其次,操作系统必须有管理页或段在内存和辅助存储器之间移动的软件。</p><p>系统抖动:处理器的大部分时间都用于交换块而非执行指令</p><p>操作系统软件</p><p>是否使用虚存技术</p><p>是使用分页还是使用分段,或同时使用二者</p><p>为各种存储管理特征采用的算法。</p><p>分页</p><p>页表结构与设计</p><p>一级或多级方案来组织大型页表:页表大小于虚拟地址空间的大小成正比。</p><p>倒排页表:页号部分使用一个简单的散列函数映射到散列表中。</p><p>调度</p><p>转换检测缓冲区:为克服内存访问时间加倍,设置转换检测缓冲区,类似于高速缓冲储存器</p><p>每次虚存的访问都可能会引起两次物理内存访问:一次取相应的页表项,另一次取需要的数据。</p><p>虚存系统需要与高速缓系统交互</p><p>页尺寸设计:</p><p>内部碎片</p><p>基于大多数辅存设备的物理特性,希望页尺寸比较大,从而实现更有效的数据块传送。</p><p>与物理内存的大小和程序大小有关。</p><p>页尺寸对却也中断发生概率的影响使得硬件设计决策复杂,另外,缺页率还取决于分配给一个进程的页框数量。软件策略(分配给每个进程的内存总量)影响着硬件设计决策。</p><p>对于给定大小的TLB,当进程的内存大小增加且局部性降低时,TLB访问的命中率降低。</p><p>操作系统有时支持多种页尺寸,但是大多数商业系统仍然只支持一种页尺寸。</p><p>分段</p><p>优点:</p><p>简化了第不断增长的数据结构的处理</p><p>允许程序独立地改变或重新编译,而不是要求整个程序集重新链接和重新加载;</p><p>有助于进程间的共享</p><p>有助于保护</p><p>段页式:</p><p>分段和妇女也俄油长处。分页对程序员是透明的,它消除了外部碎片,因而能更有效地使用内存。此外,由于移入或移出内存的块是固定的、大小相等的,因而有可能开发出更精致的存储管理算法。分段对程序员是可见的,它具有处理不断增长的数据结构的能力,及支持共享和保护的能力。为结合二者的优点,有些系统配备了特殊的处理器硬件和操作系统软件来同时支持分段与分页。</p><p>虚存的实现需要硬件的支持</p><p>读取策略(决定某页何时取入内存)</p><p>放置策略(决定进程快驻留在实存中什么位置)</p><p><strong>置换策略</strong>和高速缓存大小</p><p>能提高分页的性能并允许使用较简单的页面置换策略的一种方法是<strong>页缓冲</strong>。</p><p>驻留集管理(为进程分配多大的内存)</p><p>解决可变分配、全局范围策略潜在性能问题的一种方法是使用页缓冲。</p><p>工作集策略</p><p>工作集的概念可用于知道有关驻留集大小的策略</p><p>缺页中断频率算法</p><p>可变采样间隔的工作集</p><p>清除策略(用于确定何时将已修改的一页写回辅存)</p><p>加载控制会影响到驻留在内存中进程的数量,这称为系统并发度。系统并发度会影响处理器的利用率</p><p>UNIX内存管理和Solaris内存管理</p><p>分页式虚存方案;内核内存分配器</p><p>Linux内存管理</p><p>进程虚拟没存和内核内存分配</p><p>多道程序设计的关键是调度。</p><p>调度类型:</p><p>长程调度:决定加入待执行进程池</p><p>中程调度:决定加入部分或全部位于内存中的进程合集</p><p>短程调度:决定处理器执行哪个可运行进程</p><p> I/O调度:决定可用I/O设备处理那个进程挂起的I/O亲求</p><p>处理器调度的目的是,满足系统目标(响应时间、吞吐率、处理器效率)的方式,把进程分配到一个或多个处理器上执行。</p><p>调度——调度属于队列管理问题,用于在排队环境中减少延迟并优化性能</p><p>单处理器调度</p><p>多处理器和实时调度</p><p>长程调度:控制了系统的并发度</p><p>中程调度:换入决定取决于管理系统并发度的需求。</p><p>短程调度(分派程序):</p><p>导致当前进程阻塞或抢占当前运行进程的事件发生时,调用短程调度程序。这类事件包括:</p><p>时钟中断;I/O中断;操作系统调用;信号(信号量)</p><p>调度算法:</p><p>短程调度规则</p><p>优先级的运用</p><p>决策模式:</p><p>抢占:虽然开销大,但是,能为所有进程提供较好的服务。</p><p>非抢占:</p><p>先来先服务:适合于处理器密集型的进程,但可能导致处理器和I/O设备都未得到充分利用。</p><p>轮转法:在通用的分时系统或事务处理系统中特别有效,但是 ,对处理器密集型进程和I/O密集型进程的处理不同。会让处理器密集型进程不公平地使用了大部分处理器时间。</p><p>虚拟轮转算法:避免轮转算法的不公平性</p><p>最短进程优先(SPN):适用于短进程,但是,难点在于需要知道或至少需要估计每个进程所需的处理时间。风险在于,只要持续不断提供更短的进程,长进程就有可能饥饿。</p><p>基于过去的时间序列预测将来的一种更为常用的技术是<strong>指数平均法,有益于最短进程优先</strong></p><p>最短剩余时间:在SPN上增加了抢占机制的策略。</p><p>最短响应比优先:</p><p>反馈法:基于抢占原则并使用动态优先级机制</p><p>反馈q=1:</p><p>反馈q=2:</p><p>排队分析</p><p>通过一些分析(排队分析、仿真建模、公平共享调度),得出了调度测量性能比较的通用结论。</p><p>假设</p><p>多处理器和实时调度</p><p>多处理器系统分类:</p><p>松耦合、分布式多处理器、集群:</p><p>专用处理器:如I/O处理器</p><p>紧耦合多处理器:由一系列共享同一个内存并受操作系统完全控制的处理器组成。</p><p>多处理器调度的设计问题:</p><p>把进程分配到处理器</p><p>在单处理器上使用多道程序设计</p><p>一个进程的实际分派;</p><p> 解决以上三个问题所用的方法通常取决于应用程序的粒度等级和可用处理器的数量</p><p>实时调度</p><p>实时计算正在成为越来越重要的原则。操作系统,特别市调度程序,可能是实时系统中最重要的组件。目前实时系统应用的例子包括实验控制、过程控制设别、机器人、空中交通管制、电信、军事指挥与控制系统,下一代系统还包括自动驾驶汽车、弹簧关节机器人控制器、智能制造中的系统查找、空间站和海底勘探。</p><p>实时计算:系统的正确性不仅取决于计算的逻辑结果,而且取决于产生结果的时间。</p><p>实时操作系统的特点:</p><p>实时调度</p><p>限期调度</p><p>速率单调调度</p><p>优先级反转</p><p>Linux调度</p><p>实时调度</p><p>非实时调度</p><p>UNIX SVR4调度</p><p>FreeBSD调度程序</p><p>优先级</p><p>对称多处理器与多核支持</p><p>Windows调度</p><p>多处理器调度比较复杂,需要花比较多的时间(2天左右)来读,</p><p>输入/输出和文件</p><p>由于存在许多不同的设备及这些设备的应用,因此,很难有一种通用的、一致的解决方案。</p><h2 id="文件管理"><a href="#文件管理" class="headerlink" title="文件管理"></a>文件管理</h2><h2 id="嵌入式系统"><a href="#嵌入式系统" class="headerlink" title="嵌入式系统"></a>嵌入式系统</h2><h2 id="虚拟机"><a href="#虚拟机" class="headerlink" title="虚拟机"></a>虚拟机</h2><h2 id="计算机安全技术"><a href="#计算机安全技术" class="headerlink" title="计算机安全技术"></a>计算机安全技术</h2><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><p>PE文件</p><p>PE文件的全称是<code>Portable Executable</code>,意为可移植的可执行的文件,常见的EXE、DLL、OCX、SYS、COM都是PE文件,PE文件是微软Windows操作系统上的程序文件(可能是间接被执行,如DLL)。</p><hr><h3 id="什么是活锁?与死锁有和区别?"><a href="#什么是活锁?与死锁有和区别?" class="headerlink" title="什么是活锁?与死锁有和区别?"></a>什么是活锁?与死锁有和区别?</h3><p>活锁指的是 <strong>任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败</strong>。 活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;<strong>活锁有可能自行解开,死锁则不能</strong>。</p><p>活锁应该是一系列进程在轮询地等待某个不可能为真的条件为真。活锁的时候进程是不会<code>blocked</code>,这会导致耗尽CPU资源。</p><p>为解决活锁可以引入一些随机性,例如如果检测到冲突,那么就暂停随机的一定时间进行重试。这回大大减少碰撞的可能性。典型的例子是以太网的<code>CSMA/CD</code>检测机制。</p><hr><h3 id="直接寻址与间接寻址?"><a href="#直接寻址与间接寻址?" class="headerlink" title="直接寻址与间接寻址?"></a>直接寻址与间接寻址?</h3><p>寻址方式就是处理器根据指令中给出的地址信息来寻找物理地址的方式,是确定本条指令的数据地址以及下一条要执行的指令地址的方法。在操作系统中分为指令寻址和操作数寻址。</p><p><strong>指令寻址</strong>:在内存中查找指令的方式。</p><ul><li><strong>顺序寻址方式</strong>:即采用PC计数器来计数指令的顺序;</li><li><strong>跳跃寻址方式</strong>:下条指令的地址码不是由程序计数器给出,而是由本条指令给出。</li></ul><p><strong>操作数寻址</strong>:形成操作数的有效地址的方法称为操作数的寻址方式。</p><ul><li><strong>立即寻址</strong>:操作数作为指令的一部分而直接写在指令中;</li><li><strong>直接寻址</strong>:直接寻址是一种基本的寻址方法。<strong>在指令格式的地址的字段中直接指出操作数在内存的地址。由于操作数的地址直接给出而不需要经过某种变换</strong>,所以称这种寻址方式为直接寻址方式。</li><li><strong>简介寻址</strong>:间接寻址是相对直接寻址而言的,在间接寻址的情况下,<strong>指令地址字段中的形式地址不是操作数的真正地址,而是操作数地址的指示器,或者说此形式地址单元的内容才是操作数的有效地址</strong>。</li></ul><hr><h3 id="如何从用户态切换到内核态?"><a href="#如何从用户态切换到内核态?" class="headerlink" title="如何从用户态切换到内核态?"></a>如何从用户态切换到内核态?</h3><ol><li>程序请求系统服务,执行系统调用</li><li>程序运行期间产生中断事件,运行程序被中断,转向中断处理程序处理</li><li>程序运行时产生异常事件,运行程序被打断,转向异常处理程序。</li></ol><p>这三种情况都是通过中断机制发生,可以说 <strong>中断和异常是用户态到内核态转换的仅有途径</strong>。</p><hr><h3 id="实时操作系统和分时操作系统的区别?"><a href="#实时操作系统和分时操作系统的区别?" class="headerlink" title="实时操作系统和分时操作系统的区别?"></a>实时操作系统和分时操作系统的区别?</h3><ul><li><strong>分时操作系统</strong>:<strong>多个联机用户同时适用一个计算机系统在各自终端上进行交互式会话,程序、数据和命令均在会话过程中提供,以问答方式控制程序运行</strong>。系统把处理器的时间划分为时间片轮流分配给各个连接终端。</li><li><strong>实时操作系统</strong>:当外部时间或数据产生时,能够对其予以接受并以足够快的速度进行处理,所得结果能够在规定时间内控制生产过程或对控制对象作出快速响应,并控制所有实时任务协调的操作系统。因而,<strong>提供及时响应和高可靠性是其主要特点</strong>。实时操作系统有硬实时和软实时之分,硬实时要求在规定的时间内必须完成操作,这是在操作系统设计时保证的;软实时则只要按照任务的优先级,尽可能快地完成操作即可。我们通常使用的操作系统在经过一定改变之后就可以变成实时操作系统。</li></ul><p>下面还要补充一个批处理操作系统:<strong>批处理是指用户将一批作业提交给操作系统后就不再干预,由操作系统控制它们自动运行。这种采用批量处理作业技术的操作系统称为批处理操作系统</strong>。批处理操作系统分为单道批处理系统和多道批处理系统。批处理操作系统不具有交互性,它是为了提高CPU的利用率而提出的一种操作系统。</p><p>如果某个操作系统兼有批处理、分时和实时处理的全部或两种功能,我们称为通用操作系统。</p>]]></content>
<categories>
<category> Basic </category>
</categories>
</entry>
<entry>
<title>计算机网络</title>
<link href="/28758.html"/>
<url>/28758.html</url>
<content type="html"><![CDATA[<p>计算机网络</p><h2 id="概览"><a href="#概览" class="headerlink" title="概览"></a>概览</h2><p> 完成端到端传输的工作,可简要分为三类工作</p><p> ① 文件传送模块(应用程序间)应用层(表示层、会话层) 应用层协议 TELNET、FTP、SMTP、HTTP 报文</p><p> ② 通信服务模块(可靠通信服务)运输层(TCP-可靠(报文段) 或UDP-不可靠(数据报))、网际层 (IP(IP数据报)、路由选择协议)</p><p> ③ 网络接入模块(网络接口)链路层(帧)、物理层(bit)</p><p> 物理媒体(双绞线、同轴电缆、光缆、无线信道)<strong>0层</strong></p><p> 各层主要的功能:差错控制、流量控制、分段和重装、重用和分用、连接建立和释放</p><p> 各层独立、灵活性好、结构上可分靠、易于实现和维护、能促进标准化</p><table><thead><tr><th><strong>层</strong></th><th><strong>功能</strong></th></tr></thead><tbody><tr><td>应用层</td><td>网络进程到应用程序。针对特定应用规定各层协议、时序、表示等,进行封装 。在端系统中用软件来实现,如HTTP等</td></tr><tr><td>表示层</td><td>数据表示形式,加密和解密,把机器相关的数据转换成独立于机器的数据。规定数据的格式化表示 ,数据格式的转换等</td></tr><tr><td>会话层</td><td>主机间通讯,管理应用程序之间的会话。规定通信时序 ;数据交换的定界、同步,创建检查点等</td></tr><tr><td>传输层</td><td>在网络的各个节点之间可靠地分发数据包。所有传输遗留问题;复用;流量;可靠</td></tr><tr><td>网络层</td><td>在网络的各个节点之间进行地址分配、路由和(不一定可靠的)分发报文。路由( IP寻址);拥塞控制。</td></tr><tr><td>数据链路层</td><td>一个可靠的点对点数据直链。检错与纠错(CRC码);多路访问;寻址</td></tr><tr><td>物理层</td><td>一个(不一定可靠的)点对点数据直链。定义机械特性;电气特性;功能特性;规程特性</td></tr></tbody></table><p> </p><p>端系统通过<strong>通信链路</strong>和<strong>分组交换机</strong>(<strong>路由器</strong>与<strong>分组交换机</strong>)连接到一起</p><p>端系统如何接入英特网?</p><p>因特网服务提供商(ISP)</p><p>互联网中地协议:端系统,分组交换机,其他英特网部件要遵循的规则(主要有TCP/IP协议)</p><p> 端系统中的软件如和向另一个端系统中的软件发送数据呢?</p><p>应用程序编程接口(API)</p><p>互联网中的标准:RFC文档(定义了许多的协议)等(为了解决一致性认识的问题)</p><p>协议:定义了在两个或多个通信实体之间交换的报文格式和次序,以及报文发送或接收一条报文或其他事件所采取的动作。</p><p>网络边缘(各种端系统(=主机——客户与服务器),运行各种程序的地方)</p><p>接入网:将端系统连接到其他边缘路由器</p><p>家庭接入:DSL、电缆、FTTH、拨号和卫星</p><p>企业接入:以太网和wifi</p><p>广域无线接入:3G、4G、5G、LTE</p><p>物理媒体</p><p>引导类媒体:双绞铜线、同轴电缆、光纤</p><p>非引导类媒体:陆地无线电信道、卫星无线电信道</p><p>网络核心(由<strong>分组交换机</strong>和<strong>链路</strong>组成)——信息如何在端到端之间转发?</p><p>分组:将长报文分组为较小的数据块,分组通过<strong>通信链路</strong>和<strong>分组交换机(路由器、链路层交换机)</strong>传送</p><p>分组交换(<strong>存储转发机制</strong>)——<strong>断续分配传输带宽地策略</strong></p><p><strong>在传输时一段一段地断续占用通信资源,而不是像电路交换一样先占用一个链路。</strong></p><p><strong>优点:高效(逐断占用)、灵活(独立地选择转发路由)、迅速(可不先建立连接就可以向其他主机发送)、可靠(路由选择协议;分布式多路由的分组交换网)</strong></p><p>存储转发传输</p><p>输入端:分组转发传输</p><p>排队时延和分组丢失</p><p>输出缓存(输出队列),输出缓存(内存中而非硬盘中)有排队延迟,缓存充满时,会出现分组丢失,也就时丢包的现象</p><p>转发表和<strong>路由选择协议(出故障时使用)</strong></p><p>使用目的地址来索引转发表,以决定适当的出路。路由选择协议用于自动地设定这些转发表</p><p>包=首部(包含目的地址、源地址等信息)+数据</p><p>报文交换(存储转发)——以报文为转发单位,因此延迟高,现在较少使用</p><p>电路交换(建立连接-通话-释放连接)——适用于传输大量数据,即不用考虑 连接建立时间</p><p><strong>比特流直达终点</strong></p><p>频分多路、时分多路、码分多路</p><p>计算机网络性能指标</p><p>速率bit/s、bps(bit per second) 100M=100Mb/s 额定速率</p><p>带宽 单位时间内从网络中的某一典到另一点所能通过的“最高数据率”</p><p>吞吐量 单位时间被通过某个网络的数据量</p><p>时延</p><p><strong>发送时延</strong></p><p>传播时延</p><p>处理时延</p><p>排队时延</p><p>时延带宽积 以比特为单位的链路长度</p><p>往返时间RTT(round-trip time )</p><p>利用率信道利用率于网络利用率过高会产生非常大的延迟(排队论)</p><p>计算机网络非性能指标</p><p>费用</p><p>质量</p><p>标准化</p><p>可靠性</p><p>可宽展性和可升级性</p><p>易于管理和维护</p><p>计算机网络体系结构(把庞大而复杂的问题,转化为若干较小的局部问题)</p><p>开放系统互连基本参考模型OSI/RM(OSI)</p><p>使用最广泛的时TCP/IP——事实上的国际标准</p><p>为网络中的数据交换而建立的规则、标准或者约定或者是控制两个对等实体(或多个实体)进行通信的规则的集合称为<strong>网络协议(语法-信息的结构或格式-信息的格式、语义-需要发出什么控制信息,完成什么动作-操作、同步-事件实现顺序的详细说明-时序关系)协议是水平的,服务是垂直的</strong></p><p><strong>计算机网络的各层及其协议的集合为体系结构(所应完成功能的定义)</strong></p><p>完成端到端传输的工作,可简要分为三类工作</p><p>① 文件传送模块(应用程序间)应用层(表示层、会话层) 应用层协议 TELNET、FTP、SMTP、HTTP 报文</p><p>② 通信服务模块(可靠通信服务)运输层(TCP-可靠(报文段) 或UDP-不可靠(数据报))、网际层 (IP(IP数据报)、路由选择协议)</p><p>③ 网络接入模块(网络接口)链路层(帧)、物理层(bit)</p><p>物理媒体(双绞线、同轴电缆、光缆、无线信道)<strong>0层</strong></p><p>各层主要的功能:差错控制、流量控制、分段和重装、重用和分用、连接建立和释放</p><p>各层独立、灵活性好、结构上可分靠、易于实现和维护、能促进标准化</p><p>设计的协议要考虑各种可能的情况</p><p> </p><p> IP协议的核心地位</p><h2 id="物理层:"><a href="#物理层:" class="headerlink" title="物理层:"></a>物理层:</h2><p>怎样才能在连接各种计算机的传输媒体上传输数据比特流?</p><p> 物理层的作用是要尽可能屏蔽掉传输媒体和通信手段的差异,从而只考虑如何完成本层的协议和服务。</p><p> 主要任务:</p><p> 机械特性;电气特性;功能特性;过程特性;</p><p> 数据传输方式:串行传输</p><p>信号种类:数字信号、模拟信号</p><p> 信道的基本概念:单向通信(单工——单方向;电视广播);双向通信(半双工通信——双向但是非同时;);双向同时通信(全双工通信——双向同时发送信息)</p><p>信号调制:基带调制(波形变换);带通调制(频率范围变换——调频、调幅、调相)</p><p>信道的极限容量:</p><p>信号传输的质量问题:信噪比;信道的极限传输速率(如果在极限速率以下传播就能够保证质量)</p><p>限制信道的传输速率问题:传输速率有上限,超过传输速率上限,就会出现严重的码间串扰的问题,使接受端对码元的识别成为不可能。</p><p>传输媒体(电信领域使用的电磁波频谱):</p><p>引导性传输媒体:双绞线;同轴电缆;光缆</p><p>非引导型传输媒体(自由空间,无限制传输,无线传输):</p><p>信道复用技术:</p><p>频分复用;时分复用;统计时分复用;</p><p>波分复用</p><p>码分复用</p><p>数字传输系统</p><p>宽带接入技术</p><p>有线带宽接入:</p><p>ADSL(居民使用)、DSL(企业使用)、</p><p>光线同轴混合网(HFC)</p><p>光纤到户FTTH</p><p>无线带宽接入:</p><h2 id="数据链路层:"><a href="#数据链路层:" class="headerlink" title="数据链路层:"></a>数据链路层:</h2><p>点对点信道和广播信道的特点</p><p>点对点信道的数据链路层:</p><p>链路;数据链路(物理线路+通信协议(硬件+软件;网络适配器实现));规程和协议是同义语;IP数据报(数据报、分组或者包)</p><p>点对点协议PPP:简单(对帧,不纠错、不排序、不流控,CRC通过就收下帧,不然就丢弃它);封装成帧;透明性;多种网络层协议;多种类型链路。PPPoE协议(宽带上网的主机使用的链路层协议);差错检测;检测连接状态;最大传输单元;网络层地址协商;数据压缩协商;</p><p>TCP/IP协议族中,可靠传输由运输层的TCP协议负责,所以PPP协议不需要进行纠错、设序号、流控。只支持点对点的链路通信,只支持全双工链路。</p><p>PPP协议组成</p><p>PPP协议的帧格式:各字段的意义(帧的组成:界定符、差错检验FCS、协议表示、);字节填充;零比特填充;</p><p> PPP协议的工作状态:</p><p>Ppp协议已不是纯粹的数据链路层的协议,它还包含了物理层和网络层的内容。</p><p>局域网的广播信道</p><p>局域网的工作层次跨越了数据链路层和物理层</p><p>共享信道</p><p>静态划分信道:频分复用;时分复用;波分复用;码分复用; 代价高,不适合于局域网使用。</p><p>动态媒体接入控制:多点接入</p><p>随机接入:碰撞问题; 重点</p><p>受控接入:在局域网中使用得较少</p><p>传统以太网</p><p>两个标准:DIXEthenet V2标准以及IEEE802.3标准</p><p>为了使数据链路层更好地适应多种局域网标准,局域网的的数据链路层就拆成了两个子层,逻辑链路控制(LLC——作用已经消失),媒体接入控制(MAC)</p><p>适配器的作用:计算机通过它来与外界的局域网连接;网络接口卡(NIC),即网卡。适配器与局域网之间通过串行传输方式传输,而适配器与计算机之间则通过计算机上的I/O总线以并行方式进行传输。 适配器实现的层次包含数据链路层、物理层。</p><p>CSMA/CD协议(解决在同一时间只能允许一台计算机发送数据的问题,避免冲突,也就是如何协调总线上各计算机的工作)——载波监听、碰撞检测;半双工通信:</p><p>(1)准备发送:</p><p>(2)检测信道:</p><p>(3)早发送的过程中仍不停地检测信道,即网络适配器要边发送边监听。</p><p>如果成功,在争用期间未检测到碰撞,帧肯定就能够发送成功。</p><p>如果失败,在争用期间检测到碰撞,就立即停止发送数据,并按规定发送人为干扰信号。适配器接着就执行指数退避算法,等待r倍512比特时间后,返回到步骤(2),继续检测信道。但是,若重传达16次认不能成功,则停止重传而上报错误。以太网每发送完一帧,一定要把已发送的帧暂时保留一下。如果在争用期间内检测出发生了碰撞,那么还要在推迟一段时间后再把这个暂时保留的帧重传一次。</p><p>凡是长度小于64字节的帧都是由于冲突而异常终止的无效帧;帧间最小间隔96比特</p><p>使用广播信道的以太网</p><p>集线器:在同一时刻至多只允许一个站发送数据;一个集线器有许多接口(8-16);集线器工作在物理层;集线器采用了专门的芯片,进行自适应串音回波抵消。</p><p>数据链路层的三个基本问题(封装成帧、透明传输、差错检测)</p><p>封装成帧 </p><p>帧首部与帧尾部的作用是进行帧定界(确定帧的界限)</p><p>帧定界符(SOH0100000001;EOT0400000100) 有利于差错控制和界定帧</p><p>透明传输:某个实际存在的事物看起来却是好像不存在一样</p><p>字节填充(字符填充) 用于解决透明传输问题</p><p>差错检测</p><p>比特差错、误码率、循环冗余检验CRC只能用于无差错接受</p><p>可靠传输:数据链路层的发送端发送什么,接收端就收到什么</p><p>差错传输:比特差错;没有出现比特差错,却出现了帧丢失、帧重复或帧失序(出现传输差错)。</p><p>数据链路层能实现无比特差错的传输,但是,还不是可靠传输。</p><p>数据传输链路通信质量好就不使用确认和重传机制,如果在数据链路层出现了差错,就有上层协议来完成。对于通信质量较差的无线传输链路,数据链路层协议使用确认和重传机制,数据链路层将向上提供可靠传输服务。</p><p>以太网的信道利用率</p><p>以太网中存在碰撞,信道利用率不能够达到100%</p><p>只有当参数a远小于1才能得到尽可能高的极限信道利用率。</p><p>以太网MAC层的硬件地址(平面地址)</p><p>标识系统是一个核心的问题。名字指出我们所要寻找的那个资源,地址指出那个资源在何处,路由告诉我们如何到达该处。</p><p>单播帧;广播帧;多播帧;</p><p>混杂方式的适配器(嗅探器)</p><p> MAC帧(DIX Ethernet V2标准;IEEE的802.3标准)</p><p> V2标准(市场上的):</p><p>扩展的以太网:</p><p>物理层扩展以太网</p><p>多级结构的集线器:碰撞域</p><p>数据链路层扩展:</p><p>网桥(数据链路层);通过内部接口管理软件和网桥协议实体来完成上述操作。</p><p>优点:过滤通信量,增大吞吐量;扩大了物理范围;提高了可靠性;可互连不同物理层,不同MAC子层和不同速率</p><p>缺点;增加了时延;没有流控功能;若通信量大,则会产生广播风暴;</p><p>网段:转发表</p><p>透明网桥(IEEE 802.1D):即插即用设备,只要把网桥接入局域网,不用人工配置转发表网桥就能工作(自学习算法来处理刚开始的空状态的转发表)。透明网桥还会记录收到帧的时间、网桥接入的时间,会把最新的状态记录下来,而把旧有状态删除。</p><p>自学习和转发帧的步骤:自学习算法、生成树算法(为了防止兜圈子,白白耗费资源)</p><p>源路由网桥:发现帧(广播时候的探测器),可以发现最佳路由,可实现负载均衡。</p><p>多接口网桥:</p><p>交换机混杂了路由和网桥的功能,是一个市场名词。以太网交换机实质上是一个多接口网桥。和透明网桥一样,也是即插即用设备。由于使用了专用的交换结构的芯片,交换效率就较高。每个用户独占传输带宽,也就是N*rate。具有多种速率的接口。可以使用存储转发方式进行转发,也可以使用直通的交换方式,但是,这不会检查错误就会把帧直接转发出去,所以有些时候会用基于软件的存储转发方式。另外,用以太网交换机能方便地实现虚拟局域网VLAN(使得网络不会因为传播过多地广播信息而引起性能恶化)</p><p>网桥和集线器的区别:</p><p>网桥是按照存储转发方式工作的,一定是先把整个帧收下来再进行处理,而不管目的地址是什么。</p><p>使用以太网进行宽带接入</p><p>PPPoE(再以太网上运行PPP)</p><p>区别于ADSL(涉及帧转换——在ADSL调制解调器中以太网帧转换为PPP<strong>帧</strong>)、使用电话线进行上网(非以太网上网)</p><h2 id="网络层"><a href="#网络层" class="headerlink" title="网络层"></a>网络层</h2><p>网络互连问题IP协议 网际控制报文协议ICMP 路由选择协议 IP多播 网络地址转换</p><p>虚拟互连网络的概念;IP地址与物理地址的关系;传统的分类的IP地址和无分类域间路由选择CIDR;路由选择协议的工作原理。</p><p>可靠交付应由谁负责?网络层向上只提供简单灵活的、无连接的、尽最大努力交付的数据报服务。网络再发送分组时不需要先家里连接。每个分组(IP数据报)独立发送,与其前后的分组无关(不编号)。网络层不提供服务质量承诺。路由器设计地简单,而可靠性就由运输层(用户主机)负责(差错处理、流量控制等)</p><p>网络层如何传送IP数据报?</p><p>网际协议IP、地址解析协议ARP、网际控制报文协议ICMP、网际组管理协议IGMP</p><p>连接网络的中间设备:物理层(转发器)、数据链路层(桥接器、网桥)、网络层(路由器)、网络层以上使用的中间设备(网关)</p><p>分类的IP地址:</p><p> IP地址:网络号+主机号</p><p> A类、B类、C类用于单播地址,用于一对一通信,D类(前四位为1110)用于多播(一对多通信),E类(前4位为1111)保留为以后使用;</p><p>点分十进制表示法</p><p>网络号全为零的IP地址表示保留地址,意为本网络,网络号为127的保留作为本地软件环回测试本主机的进程之间的通信之用。全0的主机号字段表示该IP地址是“本主机”所连接到的单个网络地址,而全1表示“所有的”,因此全1的主机号字段表示该网络上的所有主机。B类地址128.0.0.0不指派。C类网络地址192.0.0.0也是不指派的</p><p>网桥连成的是同一个网络(即网络号相同),而路由器连接的是两个网络(统一个路由器有两个不同网络号的IP地址)。</p><p>主机IP地址与硬件地址的区别:</p><p>物理地址是数据链路层和物理层使用的地址,而IP地址是网络层和以上各层使用的地址,是一种逻辑地址。</p><p> IP层抽象的互联网上只能看到IP数据报</p><p>路由器只根据目的站的IP地址网络号进行路由选择</p><p>再局域网的链路层,只能看见MAC帧</p><p>IP层抽象的互联网<strong>屏蔽</strong>了下层复杂的细节,只要在网络层上讨论问题,就能够使用统一的、抽象的IP地址研究主机和主机或路由器之间的通信。</p><p>主机或路由器怎样直到应该在MAC帧的首部填入什么样的硬件地址?</p><p>路由器中的路由表是怎样得出的?</p><p> ARP用在知道机器IP地址,但是,不知道硬件地址的情况,即求硬件地址(相应的由RARP协议——包含在DHCP中)</p><p> ARP高速缓存(本局域网上的主机和路由器的IP地址到硬件地址的映射表);ARP请求分组为广播,ARP响应分组为普通的单播;映射地址有生存时间;ARP的复杂过程都是由计算机软件自动进行的,对用户透明。</p><p> IP数据报的格式能说明IP协议都有什么功能</p><p> IP数据报:</p><p>数据链路层的MTU值,数据报超过了这个长度就要进行分片</p><p>生存时间TTL(time to live),后又变为跳数限制</p><p>IP层分组转发算法</p><p>特定主机路由、默认路由(0.0.0.0)</p><p>查找路由表、计算硬件地址、写入MAC帧首部</p><p>子网的划分:</p><p>没有划分子网,IP地址为二级网,划分了子网,IP地址为三级网(IP地址:<网络号>,<子网号>,<主机号>)。</p><p>子网掩码(用于判断是否在同一子网):不管网络有没有划分子网,只要把子网掩码和IP地址进行逐位“与”运算,就立即得出网络地址来,这样在路由器处理到来的分组就可采用同样的算法。</p><p>子网号全零与全1是较新的用法,不同路由器可能会有不同的设置</p><p>直接交付、间接交付、</p><p>构成超网(无分类编址CIDR)</p><p>无分类编址CIDR:</p><p>IP地址:{<网络前缀>、<主机号>}、斜线记法(CIDR记法)</p><p>CIDR地址块、地址掩码、路由聚合、构成超网</p><p>最长前缀匹配(最佳匹配,即选择地址更具体地那个)</p><p>CIDR的使用的好处是能够大大压缩路由表中的项目数</p><p>使用二叉线索查找路由表(减少路由表查找地时间——路由表中的数据结构和快速查找算法)、压缩技术</p><p>网际控制报文协议(有效转发IP数据包和提高交付成功的机会)</p><p> ICMP的应用:ping、tracert</p><p>路由选择协议:</p><p>路由算法:</p><p> 静态路由算法</p><p>动态路由算法</p><p>路由选择协议:</p><p>内部网关协议IGP(RIP、OSPE)</p><p>内部网关协议RIP:分布式的基于距离向量的路由选择协议。只适用于小型互联网。RIP协议让一个自治系统中的所有路由器和自己的相邻路由器定期交换路由信息,并不断更新其路由表,使得从每一个路由器到每一个目的网络的路由器条数最少的。实现简单、开销较小</p><p>RIP的报文格式</p><p>和哪些路由器交换信息、交换什么信息、在什么时候交换信息?</p><p>网络出问题时,要经过较长的时间才能能将此信息传送到所有的路由器(好消息传播快,坏消息传播慢)</p><p>OSPF(开放最短路径优先)协议,为克服RIP的缺点而开发;采用洪泛法来交换信息;有链路状态数据库,更新过程收敛快;</p><p>自治系统边界路由器;</p><p>外部网关协议EGP(BGP-4)</p><p>路径向量路由选择协议;找出较好的路径而不是最佳的路径;BGP发言人;</p><p>路由器构成</p><p>路由器的结构</p><p>路由选择部分,控制部分</p><p>分组转发部分;</p><p>转发和路由是有区别的</p><p>交换结构</p><p>通过存储器</p><p>通过总线</p><p>通过互连网络</p><p>IP多播(一对多通信,交互式会议)</p><p>多播路由器</p><p>D类地址表示多播组</p><p>本地局域网上的进行的硬件多播;另一种是在因特网的范围进行多播。</p><p>硬件多播:</p><p>虚拟专用网VPN和网络地址转换NAT</p><p>虚拟专用网VPN:</p><p>网路地址转换NAT:</p><h2 id="运输层:"><a href="#运输层:" class="headerlink" title="运输层:"></a>运输层:</h2><p>为相互通信的应用进程提供逻辑通信</p><p>端口和套接字的意义</p><p>无连接的UDP的特点</p><p>面向连接的TCP的特点</p><p>在不可靠的网络上实现可传输的工作原理,停止等待协议和ARQ协议</p><p>TCP的滑动窗口、流量控制、拥塞控制和连接管理</p><p>通信的真正端点并不是主机而是主机中的进程,端到端的通信是应用进程之间的通信。</p><p>协议端口号(端口)解决了特定机器上运行的特定进程指明为因特网上通信的最后的终点的问题,也就是说,虽然,通信的终点是进程,但是只要把报文交付到目的主机的某一个合适的目的端口,剩下的工作就由TCP来完成。这种端口是软件端口,区别于硬件端口。软件端口是应用层的各种协议进程于运输实体进行层间交付的一种地址。</p><p>因特网上的通信采用客户-服务器方式,因此运输层端口号分为两大类</p><p>服务器端使用的端口号:</p><p>熟知\系统端口号</p><p>登记端口号</p><p>客户端使用的端口号:</p><p>运输层的复用和分用功能</p><p>应用层所有的应用进程都可以通过运输层再送到IP层,这就是复用。</p><p>运输层从IP层收到数据后必须交付指明的应用进程,这就是分用。</p><p>无连接的UDP(不可靠的)</p><p>有时候很有效;在IP数据报上增加了很少一点的功能,复用和分用的功能,以及差错检测功能;</p><p>特点:无连接(减少了时延);尽最大努力交付(不保证可靠);面向报文的;没有拥塞控制();支持一对一、一对多、多对一和多对多的交互通信;首部开销小;</p><p> UDP可能会导致数据报丢失、拥塞问题</p><p>面向连接TCP(可靠的)</p><p>不提供广播或多播服务</p><p>主要特点:</p><p>TCP是面向连接的运输层协议;</p><p>每个TCP连接只是点对点</p><p> TCP连接的端点叫套接字或插口(端口号拼接到IP地址即构成了套接字)</p><p>套接字socket=(IP地址:端口号)</p><p>每一条TCP连接唯一地被通信两端地两个端点(即两个套接字)所确定</p><p> TCP提供可靠交付服务</p><p>全双工通信(设有发送缓存、接受缓存)</p><p>面向字节流</p><p>可靠传输</p><p>工作原理</p><p>停止等待协议:发送、发送后确认,然后再进行下一环;若超过了一段时间没有收到确认就认为刚才传送的分组丢失了(暂时保留已发送文本;分组和确认分组都必须进行编号;超时计时器的设定时间比平均往返时间长;);用流水线传输时可以提高信道利用率。</p><p>确认丢失和确认迟到</p><p>连续ARQ协议(滑动窗口是TCP协议的精髓)</p><p>累计确认,对按序到达的最后一个分组发送确认;TCP的分用功能也是通过端口实现的;字节流必须按顺序编号;</p><p>停止等待协议利用率低下</p><p>TCP报文</p><p> TCP可靠传输的实现</p><p>以字节为单位的可靠滑动;接受方B只能对按序收到的数据中的最高序号给出确认;缓存和窗口的关系;超时重传的时间选择(问题复杂),采用自适应的算法、Karn算法(能够使运输层分开有效的和无效的往返时间样本,从而改进了往返时间的估测);选择确认SACK,只传送缺少的数据而不传送已经正确到达接受方的数据,由于SACK文档并没有指明发送方应当怎样响应SACK,因此大多数的实现还是重传所有未被确认的数据块。</p><p>流量控制(让发送方的发送速率不要太快,要让接收方来得及接收,抑制发送端发送的数据,以便使接受端来得及接收)</p><p>利用滑动窗口实现流量控制;发送方的发送窗口不能超过接收方给出的接受窗口的数值;TCP的窗口单位是字节,不是报文段;持续计时器(探测报文);点对点通信量控制,是个端到端的问题;</p><p>如何控制TCP报文发送时机</p><p> Nagle算法,数据达到发送窗口大小的一半或者达到报文段的最大长度时,等等方法,用于提高网络的吞吐量</p><p>糊涂窗口综合症,窗口设置为,这样下去,网络的效率很低</p><p>拥塞控制(防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载)</p><p>一般原理</p><p>对网络资源需求的和大于可用的资源。拥塞控制是一个动他的过程,全局平衡了,才会使得问题解决。可以从控制论的角度来看这个问题,开环控制、闭环控制。</p><p>一般方法</p><p>慢开始:</p><p>拥塞窗口,且动态变化</p><p>慢开始算法(由小到大逐渐增大发送窗口,由小到大逐渐增大拥塞窗口数值),每经过一个传输轮次,拥塞窗口就加倍;慢开始门限;</p><p>拥塞避免:</p><p>让拥塞窗口缓慢地增大,即每经过一个往返时间RTT就把发送方地拥塞窗口加1,而不是加倍。</p><p>以上两个算法合称为AIMD算法,即加法增大乘法减少</p><p>快重传:</p><p>接受方每收到一个失序地报文段后就立即发出重复确认而不要等待自己发送数据时才进行捎带确认。</p><p>快恢复:不执行慢开始,而是使拥塞窗口设置为慢开始门限减半后的数值。</p><p>发送窗口的上限值为接受方窗口和拥塞窗口中较小的一个</p><p>网络层中对TCP拥塞控制影响最大的就是路由器的分组丢弃策略,尾部丢弃策略。为了避免全局同步,路由器采用随机早期检测RED的措施,这个RED算法和队列的最大门限、最小门限、丢弃概率有关系。</p><p> TCP的运输连接管理</p><p>要使每一方能够确知对方的存在</p><p>允许双方协商一些参数(最大窗口值,是否使用窗口扩大选项)</p><p>能够对运输实体资源分配</p><p>连接建立</p><p>采用客户服务器方式</p><p>数据传送</p><p>连接释放</p><p>三次握手是为了防止已失效的连接请求报文段突然的传送</p><p>四次握手释放连接为了保证A发送的最后一个ACK报文能够到达B;防止已失效的连接请求报文段;双向释放连接</p><h2 id="应用层(精确定义不同网络应用进程之间的通信规则)20190918"><a href="#应用层(精确定义不同网络应用进程之间的通信规则)20190918" class="headerlink" title="应用层(精确定义不同网络应用进程之间的通信规则)20190918"></a>应用层(精确定义不同网络应用进程之间的通信规则)20190918</h2><p>应用协议是网络协议的一部分,如,万维网应用是一种基于客户-服务器体系结构网络应用。万维网应用包含很多部件,有万维网浏览器、万维网服务器、万维网文档的格式标准,以及一个应用层协议。</p><p>域名系统、分布式的域名系统</p><p>因特网的域名结构:层次树状结构;权限域名服务器用于提升域名系统的运行效率;</p><p>根域名服务器;顶级域名服务器;权限域名服务器;本地域名服务器;</p><p>主域名服务器;辅助域名服务器(提高域名的可靠性)</p><p>查询时可采用递归查询(本机向本地域名服务器查寻);迭代查询(本地域名服务器向根域名服务器的查询);其中使用UDP数据报文</p><p>使用高速缓存来提高DNS查询效率</p><p>设置计时器以保证高速缓存中内容的正确性(通过减少时间值)</p><p> FTP协议相关</p><p>1995年,WWW的通信量才首次超过FTP</p><p>基于TCP的FTP;基于UDP的FTP</p><p>联机访问</p><p> FTP主要功能是减少或消除在不同操作系统下处理文件的不兼容性</p><p> FTP由主进程和若干从属进程(控制进程、数据传输进程)构成</p><p>NFS允许应用进程打开一个远地文件,并能在该文件的某一个特定的位置上开始读写数据,所以网络上传送的只是少量的修改数据</p><p> TFTP协议(简单文件传送协议),可用于UDP环境,TFTP代码占用的内存较小。</p><p>远程TELNET协议,终端仿真协议,客户服务器方式。网络虚拟终端NVT(NVT作为一个中间转换的格式)</p><p>万维网WWW,分布式的超媒体系统,是超文本系统的扩充。</p><p>万维网以客户端-服务器方式工作,客户程序先服务器发出请求,服务器程序向客户程序送回客户所要的万维文档。</p><p>怎样标志分布在整个英特网上的万维文档</p><p>统一资源定位符URL</p><p>用怎样的协议来实现万维网上的各种链接</p><p>超文本传送协议HTTP(使用TCP进行可靠地传送)</p><p>怎样使不同作者的不同风格的万维网文档,都能在因特网上的的各种主句上显示出来,同时使用户清楚地知道在什么地方存在着链接</p><p>超文本标记语言HTML</p><p>怎样使用户能够很方便地找到所需地信息?</p><p>搜索引擎</p><p>并行TCP连接可以缩短访问时间</p><p> HTTP/1.1协议,有非流水方式与流水方式,用于提高效率。</p><p>代理服务器(proxy server),万维网高速缓存,减少了访问因特网的缓存。</p><p>HTTP报文结构</p><p>状态码(包含在状态行中,状态行中有 HTTP的版本、状态码、解释状态码的简短语句)</p><p>服务器上存储用户信息</p><p> Cookie能够简化用户上网的过程</p><p>万维网文档</p><p> XML可扩展标记语言,将用户界面与结构化数据分隔开</p><p> XHTML可扩展超文本标记语言</p><p> CSS层叠样式表,一种样式表语言</p><p>动态万维网文档,由应用程序控制生成</p><p>通用网关接口CGI,是一种标准,定义了动态文档应当如何创建。CGI程序的正式名字是CGI脚本。</p><p>活动万维网文档,把所有的工作都转移给浏览器端,请求后,服务器会把活动文档<strong>程序</strong>副本返给浏览器。Java技术(java小应用程序)就是达到这样一种目的的程序</p><p> Java技术:程序设计语言;运行环境;类库;</p><p> Java小应用程序与具体技术无关</p><p>万维网的信息检索系统</p><p>搜索引擎</p><p>全文检索(spider):google</p><p>分类目录检索:yahoo;sina;sohu;163</p><p>垂直搜索引擎</p><p>元搜索引擎</p><p>电子邮件</p><p>用户代理(Outlook Express;Foxmail);邮件服务器(同时充当客户和服务器);邮件发送协议(SMTP)和邮件读取协议(POP3、IMAP),其中SMTP客户与服务器间采用TCP连接。</p><p>用户代理UA就是用户与电子邮件系统的接口,即电子邮件客户端软件(大多数情况)。</p><p> SMTP协议</p><p>采用客户-服务器方式;SMTP不使用中间的邮件服务器;SMTP本来是为传送ASCII码而不是传送二进制数据设计的。为了检查发送的电子邮件,就有了安全传输TLS</p><p> BCC 暗送 (暗自送邮件给别人)</p><p> POP3和IMAP</p><p> POP3</p><p>采用客户服务器工作方式</p><p> IMAP</p><p>用户可以在不同的地方使用不同的计算机随时上网阅读处理自己的邮件。</p><p>基于万维网络的电子邮件(Gmail、Hotmail、Yahoo!Mail、163、sina)</p><p>从浏览器中发送或者读取邮件使用的是HTTP协议</p><p>通用因特网邮件扩充MIME</p><p> SMTP限于传送7位的ASCII码;SMTP服务器会拒绝超过一定长度的邮件。MIME增加了邮件主体的就够,并定义了传送非ASCII码的编码规则</p><p>内容传送编码:7位ASCII码;quoted-printable;base64编码</p><p>动态主机配置协议DHCP</p><p>在协议软件中给这些参数赋值的动作叫做协议配置,具体的配置信息有哪些则取决于协议栈。</p><p>当运行客户软件的计算机移至一个新的网络时,就可以使用DHCP获取配信息而不用手工干预;采用客服服务器方式</p><p>租用期由服务器决定</p><p>简单网络管理协议SNMP</p><p>被管的设备有时也称作网络元素或网元,在被管理的设备中也会有一些不能被管理的对象。</p><p>每一个被管理的设备中都要运行一个程序以便和管理站中的管理程序进行通信,这些程序叫做网络管理代理程序,即代理。管理程序运行SNMP客户程序,而代理程序运行SNMP服务器程序,在被管理对象上运行的SNMP服务器程序不停地监听来自管理站地SNMP客户程序地请求。</p><p>基本原理:若要管理某个对象,就必然会给对象添加一些硬件或者软件,但这种“添加”必须对原有地影响尽量小些。</p><p> SNMP的网络管理由三个部分组成:SNMP本身、管理信息结构SMI、管理信息库MIB。SMI建立规则、MIB对变量进行说明、SNMP完成网管的动作。</p><p> SMI</p><p>对象的命名</p><p>对象命名树</p><p>被管对象的数据类型</p><p>简单类型</p><p>结构化类型</p><p>编码方法</p><p>基本编码规则BER ,ASN.1把所有元素都表示为T-L-V三个字段组成的序列。</p><p>管理信息库MIB</p><p>管理程序使用MIB中这些信息的值对网路进行管理(如读取或重置这些值)</p><p> SNMP的协议数据单元和报文</p><p>两种基本管理功能,读和写操作,通过探寻来实现这些操作。SNMP不是完全的探寻协议,允许不经过询问就发送某些信息,这种信息称为陷阱。使用探寻来监视,也使用陷阱机制报告特殊事件。</p><p>使用无连接的UDP</p><p>应用进程跨越网络通信</p><p>如果我们还有一些特定的应用需要因特网的支持,但这些应用又不能直接使用已经标准化的因特网应用协议,那么我们应当做哪些工作?</p><p>系统调用和应用编程接口</p><p>系统调用的机制(控制权转换)</p><p>套接字接口API(网络编程,应用进程与运输层协议之间的接口)。套接字以上的进程时受应用程序控制的,而在套接字一下的运输层协议软件则是受计算机操作系统的控制。</p><p>套接字描述符,用来表示对请求操作系统把网络通信所需的这些资源(存储空间、CPU时间、网络带宽)分配给应用进程这一过程中的资源。</p><p>UDP服务器值提供无连接服务,因此不使用listen和accept系统调用</p><h2 id="网络安全"><a href="#网络安全" class="headerlink" title="网络安全"></a>网络安全</h2><p>计算机网络面临的安全性威胁和计算机网络安全的主要内容</p><p>对称密钥密码体制和公钥密码体制的特点</p><p>数字签名和鉴别的概念</p><p>网络层安全协议IPsec协议和运输层安全协议SSL/TLS的要点</p><p>应用层电子邮件的安全措施</p><p>系统安全: 防火墙与入侵检测</p><p>安全威胁</p><p>主动攻击</p><p>篡改;</p><p>恶意程序;</p><p>计算机病毒</p><p>计算机蠕虫</p><p>特洛伊木马</p><p>逻辑炸弹</p><p>拒绝服务(DoS);</p><p>应对:加密技术+鉴别技术</p><p>被动攻击</p><p>截获;攻击者只观察和分析某一个协议数据单元PDU而不干扰信息流;了解交换数据的某种性质,这为被动攻击中的流量分析</p><p>应对:数据加密技术</p><p>无线网和移动网络</p><p>计算机网络通信安全目标</p><p>防止析出报文内容和流量分析</p><p>防止恶意程序</p><p>检测更改报文流和拒绝服务</p><p>计算机网络安全的内容(涉及密码技术)</p><p>保密性</p><p>访问控制中登录口令的设计、安全通信协议的设计、数字签名的设计</p><p>安全协议的设计 </p><p>访问控制</p><p>存取控制或接入控制</p><p>密码编码学和密码分析学合为密码学</p><p>如果不论截获了多少密文,但在密文中都没有足够的信息来唯一地确定出对应的明文,这一密码体制则为无条件安全的。如果一个密码体制中的密码,不能在一定的时间内被可以使用的计算资源破译,则这一密码体制称为在计算机上是安全的;数据加密标准DES和公钥密码体制为两个重要的里程碑。</p><p>两类密码体制</p><p>对称密钥密码体制(加密密钥与解密密钥是相同的密码体制)</p><p>保密性仅取决于对密钥的保密,而算法是公开的。</p><p>国际数据加密算法IDEA</p><p>公钥密码体制(使用不同的加密密钥与解密密钥)</p><p>由于对称密钥体制的米哟啊分配问题;对数字签名的需求。</p><p> RSA体制,基于数论的大数分解问题的体制</p><p>加密密钥是公开的,解密密钥是要保密的,算法也都公开。</p><p>任何加密方法的安全性取决于<strong>密钥的长度</strong>,以及攻破密文所需的<strong>计算量。</strong></p><p>数字签名</p><p>接收者能够核实发送者对报文的签名,即报文鉴别</p><p>接收者确信所收到的数据和发送者发送的完全一样而没有被篡改过,即报文的完整性</p><p>发送者事后不能抵赖对报文的签名,不可否认</p><p>鉴别</p><p>鉴别是要验证通信的对方的确市自己所要通信的对象,而不是其他的冒充者</p><p>报文鉴别</p><p>报文摘要MD,</p><p>报文摘要算法是一种散列函数,也是一种精心选用的单向函数。检验和算法也是单向的。即,不可进行逆运算。</p><p>报文摘要算法MD5</p><p>安全散列算法SHA,和MD5相似。 </p><p>实体鉴别</p><p>在系统接入的全部持续时间内和自己通信的对方实体只需验证一次。</p><p>重放攻击(不重数方法);IP欺骗</p><p>密钥分配</p><p>密钥管理(产生、分配、注入、验证、使用)</p><p>网外分配;网内分配方式;</p><p>对称密钥分配</p><p>密钥分配中心KDC;Kerberos V5协议,Kerberos使用不DES更加安全的先进的加密标准AES,只用于客户于服务器之间的鉴别</p><p>因特网使用的安全协议</p><p>网络层安全协议</p><p> IPsec协议</p><p>鉴别首部AH、封装安全有效载荷ESP</p><p>运输方式;隧道方式;</p><p>因特网密钥交换IKE(为IPsec创建安全关联SA)</p><p>运输层安全协议</p><p>安全套接字SSL;运输层安全TLS;(http变为https)</p><p>应用层安全协议</p><p>有关电子邮件的安全协议PGP协议</p><p>系统安全:防火墙与入侵检测</p><p>防火墙:特殊编程的路由器,实施访问控制策略</p><p>分组过滤路由器;</p><p>过滤规则基于网络层或运输层首部的信息</p><p>应用网关(代理服务器)</p><p>入侵检测系统IDS</p><p>基于特征的入侵检测;基于异常的入侵检测</p><p>因特网中的音频与视频服务20190925</p><p>实时流式协议RTSP;交互式音频/视频所使用的一些协议,实时运输协议RTP;实时控制协议RTCP、H.323以及会话发起协议SIP;</p><p>时延、时延抖动</p><p>改进“尽最努力交付”的服务的一些措施,</p><p>计算机网最初是为传送数据设计的。IP层提供“尽最大女里交付”服务以及每一个分组独立交付的策略,因特网的TCP协议可以很好地解决IP层不能提供可靠交付这一问题。</p><p>多媒体信息交付问题</p><p>边传输边播放</p><p>因特网是非等时的(设定缓存来解决)</p><p>缓存决定播放时延</p><p>运输层选用UDP而不是TCP,对于传送实时数据,宁可丢失少量分组,也不要让分组太晚到达。容忍丢失也是实时数据的另一个特点。</p><p>音频/视频服务</p><p>流式存储音频/视频(流媒体):边传送流媒体边播放</p><p>具有元文件的万维网服务器</p><p>元文件中保存了有关音频/视频文件的信息,使用HTTP服务。</p><p>媒体服务器</p><p>流式服务器</p><p>实时流式协议RTSP</p><p>为了给流式过程增加更多的功能而设计的协议,使媒体播放器能够控制多媒体流的传送。因特网录像机遥控协议。RTSP记录客户机所处于的状态;RTSP控制既可在TCP上传送,也可在UDP上传送;RTSP不规定数据在媒体播放器中应如何缓存。</p><p>交互式音频/视频</p><p> IP电话;</p><p> IP电话网关;电话网使用电路交换,因特网使用分组交换;</p><p>通话质量:通话双方端到端的时延和时延抖动;话音分组的丢失率;</p><p> Skype采用了P2P和全球索引</p><p> IP电话中采用了信令协议(H.323和SIP);话音分组的传送协议;</p><p>信令协议(H.323和SIP);直接传送音频/视频(RTP);保证服务质量(RSVP、RTCP)</p><p>尽最大努力交付的服务</p><p>可用性、差错率、响应时间、吞吐量、分组丢失率(提供某种服务等级)</p><p><strong>调度</strong>和<strong>管理机制</strong>是使因特网能够提供服务质量的重要措施</p><p>路由器:分类、管制、调度、呼唤接纳</p><p>调度机制(排队的规则)</p><p>分类、先进先出、优先级、公平排队、加权公平排队</p><p>管制机制</p><p>平均速率、峰值速率、突发长度</p><p>漏桶管制器</p><p>漏桶机制于加权机制结合</p><p>综合服务IntServ与资源预留协议RSVP</p><p>IntServ</p><p>资源预留;呼叫建立;</p><p>组成:资源预留协议RSVP(多树);接纳控制;分类器;调度器;</p><p>流:具有同样的源IP地址、源端口号、目的IP地址、目的端口号、协议表示符及服务质量需求的一连串分组。</p><p> RSVP是面向终点的,是运输层的控制协议。</p><p>IntServ的体系结构</p><p>存在问题:结构复杂;数目与流量数目成正比;服务质量登记数量太少,不够灵活。</p><p>区分服务DiffServ</p><p>在网络中增加区分服务的功能</p><p>无线网络和移动网络</p><p>无线网和移动网的数据链路层和传统的有线因特网的数据链路层相差很大</p><p>无线局域网的组成,DS和接入点AP的作用</p><p>无线局域网使用的CSMA/CA协议(与载波监听多点接入/碰撞检测CSMA/CD的区别)和无线局域网MAC帧使用的几种地址;</p><p>移动用户在移动时怎么保持IP地址不变</p><p>蜂窝移动通信网中对移动用户的路由选择问题</p><p>无线局域网协议标准IEEE 802.11,是无线以太网的标准,使用星型拓扑,中心接入点AP,在MAC层中使用CSMA/CA协议。最小构件是基本服务集BSS,基本服务集包括一个基站和若干移动站,接入点AP就是基本服务集内的基站;服务集标志SSID;</p><p>无线自组网络</p><p>无线传感器与物联网</p><p> CSMA/CA协议(802.11局域网的MAC层协议)</p><p>为了解决暴露站问题;隐蔽站问题;</p><p>不同于CSMA/CD ,CSMA/CA协议采用避免碰撞的方式,因为如果采用CD,一旦发生碰撞,就会浪费整个信道资源。</p><p>采用停止等待协议,使用链路层确认。</p><p>无线通信的质量比有线要差,因为会丢帧,因此会有链路层确认。</p><p>802.11设计了独特的MAC层,通过协调功能来确定在基本服务集BSS终得移动站,在什么时间能发送数据或接收数据。分布协调功能DCF(采用CSMA的分布式接入算法,让各个站通过征用信道来获取发送权;必须有的);点协调功能PCF(选用)</p><p>最常用的帧间间隔:SIFS;DIFS</p><p>802.11还采用了虚拟载波监听的机制</p><p> CSMA/CA算法</p><p>对信道进行预约</p><p>请求发送RTS;允许发送CTS;</p><p> 802.11的MAC帧</p><p>802.11数据帧的地址,四个地址字段。</p><p>序号控制字段、持续期字段和帧控制字段</p><p>个人无线局域网</p><p>无线城域网WMAN</p><p>蜂窝移动通信网</p><p>展望</p><p>下一代因特网</p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><p>1.TCP采用四次挥手关闭连接如图所示为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?</p><p> 这是因为服务端的 LISTEN 状态下的 SOCKET 当收到 SYN 报文的建连请求后,它可以把 ACK 和 SYN (ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。</p><p>2.视频面试传输协议到底是TCP还是UDP</p><p>QUIC,即快速UDP连接,既取了UDP的优势,也取了TCP的优势</p><p>参考:<a href="https://mp.weixin.qq.com/s/ZS38gDysXqlA7vZJzrC8qw" target="_blank" rel="noopener">https://mp.weixin.qq.com/s/ZS38gDysXqlA7vZJzrC8qw</a></p><p>3.301永久性重定向和302临时性重定向</p><p><a href="https://www.jianshu.com/p/887d16ba71b9" target="_blank" rel="noopener">301永久性重定向和302临时性重定向</a></p><p>4.HTTP状态码</p><p><a href="https://juejin.im/post/6844903983006351373" target="_blank" rel="noopener">HTTP状态码详解</a></p><p>5.输入url到页面加载发生了什么?</p><p><a href="https://segmentfault.com/a/1190000006879700" target="_blank" rel="noopener">输入url到页面加载都发生了什么</a></p><p><a href="https://juejin.im/post/6844903657318645767#heading-40" target="_blank" rel="noopener">前端性能优化之雅虎35条军规</a></p><p>6RESTful 协议(架构风格)</p><p> <a href="http://www.ruanyifeng.com/blog/2014/05/restful_api.html" target="_blank" rel="noopener">restful api设计</a></p>]]></content>
<categories>
<category> Basic </category>
</categories>
</entry>
<entry>
<title>程序员面试金典-6-Ch</title>
<link href="/41175.html"/>
<url>/41175.html</url>
<content type="html"><![CDATA[<p>行为面试题</p><table><thead><tr><th><strong>常见问题</strong></th><th><strong>项目 1</strong></th><th><strong>项目 2</strong></th><th><strong>项目 3</strong></th></tr></thead><tbody><tr><td>遇到过的挑战</td><td></td><td></td><td></td></tr><tr><td>遭遇过的滑铁卢</td><td></td><td></td><td></td></tr><tr><td>最享受什么</td><td></td><td></td><td></td></tr><tr><td>如何体现领导力</td><td></td><td></td><td></td></tr><tr><td>如何处理冲突</td><td></td><td></td><td></td></tr><tr><td>有哪些可改进之处</td><td></td><td></td><td></td></tr></tbody></table><p>确保你有1至3个项目可以拿得出手,并能就其细节侃侃而谈</p><p>自我介绍:</p><p> </p><p><strong>核心数据结构、算法及概念</strong></p><table><thead><tr><th><strong>数据结构</strong></th><th><strong>算法</strong></th><th><strong>概念</strong></th></tr></thead><tbody><tr><td>链表</td><td>广度优先搜索</td><td>位操作</td></tr><tr><td>树、单词查找树、图</td><td>深度优先搜索</td><td>内存(堆和栈)</td></tr><tr><td>栈和队列</td><td>二分查找</td><td>递归</td></tr><tr><td>堆</td><td>归并排序</td><td>动态规划</td></tr><tr><td>向量/数组列表</td><td>快排</td><td>大<img src="file:///C:/Users/ADMINI~1/AppData/Local/Temp/msohtmlclip1/01/clip_image001.gif" alt="O"> 时间及空间</td></tr><tr><td>散列表</td><td></td><td></td></tr></tbody></table><p>对于上述各项题目,务必掌握它们的具体用法、实现方法、应用场景以及空间和时间复杂度。</p><p>一种不错的方法就是练习如何实现数据结构和算法(先在纸上,然后在电脑上)。你会在这个过程中学到数据结构内部是如何工作的,这对很多面试而言都是不可或缺的。</p><p>你错过上面那段了吗?千万不要错过,这非常重要。如果对上面列出的某个数据结构和算法感觉不能运用自如,就从头开始练习吧。</p><p>其中,散列表是必不可少的一个题目。对这个数据结构,务必要胸有成竹。</p><p>涉及可扩展性或者内存排序限制等问题,同时,这张表可以拿来做速算</p><table><thead><tr><th><strong>2**</strong>的幂**</th><th><strong>准确值(*X* )</strong></th><th><strong>近似值</strong></th><th><strong><em>X\</em></strong> <strong>字节转换成MB、GB等</strong></th></tr></thead><tbody><tr><td>7</td><td>128</td><td></td><td></td></tr><tr><td>8</td><td>256</td><td></td><td></td></tr><tr><td>10</td><td>1024</td><td>一千</td><td>1 K</td></tr><tr><td>16</td><td>65 536</td><td></td><td>64 K</td></tr><tr><td>20</td><td>1 048 576</td><td>一百万</td><td>1 MB</td></tr><tr><td>30</td><td>1 073 741 824</td><td>十亿</td><td>1 GB</td></tr><tr><td>32</td><td>4 294 967 296</td><td></td><td>4 GB</td></tr><tr><td>40</td><td>1 099 511 627 776</td><td>一万亿</td><td>1 TB</td></tr></tbody></table><p>BUD优化:瓶颈、无用功、重复性工作</p><p>你一旦有了蛮力法,就应该努力优化该方法。以下技巧就有了用武之地。</p><p>(1) 寻找未使用的信息。你的面试官告诉过你数组是有序的吗?你如何利用这些信息?</p><p>(2) 换个新例子。很多时候,换个不同的例子会让你思路畅通,看到问题模式所在。</p><p>(3) 尝试错误解法。低效的例子能帮你看清优化的方法,一个错误的解法可能会帮助你找到正确的方法。比方说,如果让你从一个所有值可能都相等的集合中生成一个随机值。一个错误的方法可能是直接返回半随机值。可以返回任何值,但是可能某些值概率更大,进而思考为什么解决方案不是完美随机值。你能调整概率吗?</p><p>(4) 权衡时间、空间。有时存储额外的问题相关数据可能对优化运行时间有益。</p><p>(5) 预处理信息。有办法重新组织数据(排序等)或者预先计算一些有助于节省时间的值吗?</p><p>(6) 使用散列表。散列表在面试题中用途广泛,你应该第一个想到它。</p><p>(7) 考虑可想象的极限运行时间(详见7.9节)。</p><p>在蛮力法基础上试试这些技巧,寻找BUD的优化点。</p><p><strong>测试</strong></p><p>在现实中,不经过测试就不会签入代码;在面试中,未经过测试同样不要“提交”。</p><p>测试代码有两种办法:一种聪明的,一种不那么聪明的。</p><p>许多求职者会用最开始的例子来测试代码。那样做可能会发现一些bug,但同样会花很长时间。手动测试很慢。如果设计算法时真的使用了一个大而好的例子,那么测试时间就会很长,但最后可能只在代码末尾发现一些小问题。</p><p>你应该尝试以下方法。</p><p>(1) 从概念测试着手。概念测试就是阅读和分析代码的每一行。像代码评审那样思考,在心中解释每一行代码的含义。</p><p>(2) 跳着看代码。重点检查类似x = length-2 的行。对于for 循环,要尤为注意初始化的地方,比如i = 1 。当你真的去检查时,就很容易发现小错误。</p><p>(3) 热点代码。如果你编程经验足够丰富的话,就会知道哪些地方可能出错。递归中的基线条件、整数除法、二叉树中的空节点、链表迭代中的开始和结束,这些要反复检查才行。</p><p>(4) 短小精悍的用例。接下来开始尝试测试代码,使用真实、具体的用例。不要使用大而全的例子,比如前面用来开发算法的8元素数组,只需要使用3到4个元素的数组就够了。这样也可以发现相同的bug,但比大的快多了。</p><p>(5) 特殊用例。用空值、单个元素、极端情况和其他特殊情况检测代码。</p><p>发现了bug(很可能会)就要修复。但注意不要贸然修改。仔细斟酌,找出问题所在,找到最佳的修改方案,只有这样才能动手。</p><p>优化和解体技巧</p><p> 不事先带入专业的知识,试着把它当作日常生活问题来解决</p><p> 从约束少的,个例出发,再推广到适应多种情况的解决方法</p><p> 由基础例子出发推广得到解决办法</p><p> 遍历数据结构,挑选个合适的,说不定就有了个较好的解决办法</p><p> 想象问题的极限</p><p>好的代码</p><p> 多使用数据结构</p><p> 适当代码复用</p><p> 模块化</p><p> 灵活性和通用性</p><p> 错误检查</p><p> </p><p>数据结构</p><p> 数组与字符串</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_160" target="_blank" rel="noopener">9.1 数组与字符串</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_161" target="_blank" rel="noopener">9.1.1 散列表</a></p><p> 散列表是一种通过将键(key)映射为值(value)从而实现快速查找的数据结构。</p><p> 使用一个链表构成的数组与一个散列函数来实现散列表</p><p> 通过平衡二叉搜索树来实现散列表。该方法的查找时间是O(\log N) 。该方法的好处是用到的空间可能更少,因为我们不再需要分配一个大数组。还可以按照键的顺序进行迭代访问,在某些时候这样做很有用。</p><p> 散列表冲突解决方案</p><p> </p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_162" target="_blank" rel="noopener">9.1.2 ArrayList与可变长度数组</a></p><p> 这是面试中的一个基础数据结构。无论使用何种编程语言,都要确保能够熟练运用动态数组(链表)。</p><p> 最终扩容:复制n/2 个元素</p><p>之前的扩容:复制n/4 个元素</p><p>之前的扩容:复制n/8 个元素</p><p> </p><p>之前的扩容:复制n/16 个元素</p><p>……</p><p>第二次扩容:复制2个元素</p><p>第一次扩容:复制1个元素</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_163" target="_blank" rel="noopener">9.1.3 StringBuilder</a></p><p> StringBuilder 可以避免普通拼接字符串方法效率低的问题。它会直接创建一个足以容纳所有字符串的可变长度数组,等到拼接完成才将这些字符串转成一个字符串</p><p> 拉宾-卡普(Rabin-Karp)子串查找</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_164" target="_blank" rel="noopener">9.2 链表</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_165" target="_blank" rel="noopener">9.2.1 创建链表</a></p><p> 链表是一种用于表示一系列节点的数据结构。在单向链表中,每个节点指向链表中的下一个节点。而在双向链表中,每个节点同时具备指向前一个节点和后一个节点的指针</p><p> 与数组不同的是,无法在常数时间复杂度内访问链表的一个特定索引。这意味着如果要访问链表中的第K 个元素,需要迭代访问K 个元素。</p><p> </p><p>链表的好处在于你可以在常数时间复杂度内加入和删除元素。这对于某些特定的程序大有用处</p><p>切记:在面试中遇到链表题时,务必弄清楚它到底是单向链表还是双向链表</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_166" target="_blank" rel="noopener">9.2.2 删除单向链表中的节点</a></p><p> (1) 检查空指针;(2) 必要时更新表头(head)或表尾(tail)指针。</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_167" target="_blank" rel="noopener">9.2.3 “快行指针”技巧</a></p><p> </p><p> </p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_168" target="_blank" rel="noopener">9.2.4 递归问题</a></p><p> 许多链表问题都要用到递归。解决链表问题碰壁时,不妨试试递归法能否奏效。这里暂时不会深入探讨递归,后面会有专门章节予以讲解。</p><p>当然,还需注意递归算法至少要占用O(n) 的空间,其中n 为递归调用的层数。实际上,所有递归算法都可以 转换成迭代法,只是后者实现起来可能要复杂得多</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_169" target="_blank" rel="noopener">9.3 栈与队列</a></p><p> 熟练掌握数据结构的基本原理,栈与队列问题处理起来要容易得多。当然,有些问题也可能相当棘手。部分问题不过是对基本数据结构略作调整,其他问题则要难得多</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_170" target="_blank" rel="noopener">9.3.1 实现一个栈</a></p><p> 与数组不同的是,栈无法在常数时间复杂度内访问第i 个元素。但是,因为栈不需要在添加和删除操作时移动元素,所以可以在常数时间复杂度内完成此类操作。</p><p> 下面给出了栈的简单实现代码。注意,如果只从链表的一端添加和删除元素,栈也可以用链表实现。</p><p> 对于某些递归算法,栈通常大有用处。有时,你需要在递归时把临时数据加入到栈中,在回溯时(例如,在递归判断失败时)再删除该数据。栈是实现这类算法的一种直观方法。</p><p>当使用迭代法实现递归算法时,栈也可派上用场。(这是一个很好的练习项目。选择一个简单的递归算法并用迭代法实现该算法。)</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_171" target="_blank" rel="noopener">9.3.2 实现一个队列</a></p><p> 队列也可以用链表实现。事实上,只要元素是从链表的相反的两端添加和删除的,链表和队列本质上就是一样的。</p><p> 更新队列当中第一个和最后一个节点很容易出错,请务必再三确认。</p><p>队列常用于广度优先搜索或缓存的实现中。</p><p>例如,在广度优先搜索中,我们使用队列来存储需要被处理的节点。每处理一个节点时,就把其相邻节点加入到队列的尾端。这使得我们可以按照发现节点的顺序处理各个节点。</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_172" target="_blank" rel="noopener">9.4 树与图</a></p><p> 能够游刃有余地从无到有实现树或图,这是求职者必不可少的一种技能。</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_173" target="_blank" rel="noopener">9.4.1 树的类型</a></p><p> 9.4.1.1 树与二叉树</p><p> 9.4.1.2 二叉树与二叉搜索树</p><p> 二叉搜索树是二叉树的一种,该树的所有节点均需满足如下属性:全部左子孙节点 \leqslant n< 全部右子孙节点。</p><p> 二叉搜索树对于“相等”的定义可能会略有不同。根据一些定义,该类树不能有重复的值。在其他方面,重复的值将在右侧或者可以在任一侧。所有这些都是有效的定义,但你应该向面试官澄清该问题。</p><p> 9.4.1.3 平衡与不平衡</p><p> 平衡一棵树并不表示左子树和右子树的大小完全相同(如9.4.1.6节中的完美二叉树所示)。思考此类问题的一个方法是,“平衡”树实际上多半意味着“不是非常不平衡”的树。它的平衡性足以确保执行insert 和find 操作可以在O(\log n) 的时间复杂度内完成,但其并不一定是严格意义上的平衡树。</p><p> 平衡树的两种常见类型是红黑树(11.7节)和AVL树(11.6节)</p><p> 9.4.1.4 完整二叉树</p><p> 9.4.1.5 满二叉树</p><p> 9.4.1.6 完美二叉树</p><p> 完美二叉树既是完整二叉树,又是满二叉树。所有叶节点都处于同一层,而此层包含最大的节点数。</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_174" target="_blank" rel="noopener">9.4.2 二叉树的遍历</a></p><p> 面试之前,对实现中序、后序和前序遍历,你要做到轻车熟路,其中在面试中最常见的是中序遍历。</p><p> 9.4.2.1 中序遍历</p><pre><code>1. void inOrderTraversal(TreeNode node) {2. if (node != null) {3. inOrderTraversal(node.left);4. visit(node);5. inOrderTraversal(node.right);6. }7. }</code></pre><p> 9.4.2.2 前序遍历</p><pre><code>1. void preOrderTraversal(TreeNode node) {2. if (node != null) {3. visit(node);4. preOrderTraversal(node.left);5. preOrderTraversal(node.right);6. }7. }</code></pre><p> 9.4.2.3 后序遍历</p><pre><code>1. void postOrderTraversal(TreeNode node) {2. if (node != null) {3. postOrderTraversal(node.left);4. postOrderTraversal(node.right);5. visit(node);6. }7. }</code></pre><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_175" target="_blank" rel="noopener">9.4.3 二叉堆(小顶堆与大顶堆)</a></p><p> 在最小堆中有两个关键操作:insert 和extract_min 。</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_176" target="_blank" rel="noopener">9.4.4 单词查找树(前序树)</a></p><p> 单词查找树(有时被称为前序树)是一种有趣的数据结构。该数据结构多次出现在面试题目中,却在算法教科书中鲜有涉及。 </p><p> 通常情况下,单词查找树用于存储整个(英文)语言以便于快速前缀查找。虽然散列表可以快速查找字符串是否是有效的单词,但是它不能识别字符串是否是任何有效单词的前缀。单词查找树则可以很快做到这一点。</p><p> </p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_177" target="_blank" rel="noopener">9.4.5 图</a></p><p> 简单说来,图是节点与节点之间边的集合。</p><p> 在编程的过程中,有两种常见方法表示图。</p><p> 9.4.5.1 邻接链表法</p><p> 9.4.5.2 邻接矩阵法</p><p> 由链表(或数组,动态数组)组成的数组(或散列表)也可以存储邻接链表。</p><p> 可以使用于邻接链表的算法(广度搜索等)同样可以应用于邻接矩阵,但是其效率会有所降低。在邻接链表表示法中,你可以方便地迭代一个节点的相邻节点。在邻接矩阵表示法中,你需要迭代所有节点以便于找出某个节点的所有相邻节点。</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_178" target="_blank" rel="noopener">9.4.6 图的搜索</a></p><p> 值得注意的是,BFS和DFS通常用于不同的场景。如要访问图中所有节点,或者访问最少的节点直至找到想找的节点,DFS一般最为简单。但是,如果我们想找到两个节点中的最短路径(或任意路径),BFS一般说来更加适宜</p><p>9.4.6.2 深度优先搜索DFS</p><p> 注意,前序和树遍历的其他形式都是一种DFS。主要区别在于,对图实现该算法时,我们必须先检查该节点是否已访问。如果不这么做,就可能陷入无限循环。</p><p>9.4.6.2 广度优先搜索 BFS</p><p> 它是通过队列实现的。</p><p> 当面试官要求你实现BFS时,关键在于谨记队列的使用。</p><p>9.4.6.3 双向搜索</p><p> 双向搜索用于查找起始节点和目的节点间的最短路径。它本质上是从起始节点和目的节点同时开始的两个广度优先搜索。当两个搜索相遇时,我们即找到了一条路径。</p><p> </p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_179" target="_blank" rel="noopener">9.5 位操作</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_180" target="_blank" rel="noopener">9.5.1 手工位操作</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_181" target="_blank" rel="noopener">9.5.2 位操作原理与技巧</a></p><p> </p><table><thead><tr><th>x ^ 0s = x</th><th>x & 0s = 0</th><th>x | 0s = x</th></tr></thead><tbody><tr><td>x ^ 1s = ~x</td><td>x & 1s = x</td><td>x | 1s = 1s</td></tr><tr><td>x ^ x = 0</td><td>x & x = x</td><td>x | x = x</td></tr></tbody></table><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_182" target="_blank" rel="noopener">9.5.3 二进制补码与负数</a></p><p> </p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_183" target="_blank" rel="noopener">9.5.4 算术右移与逻辑右移</a></p><p> 有两种类型的右移操作符。算术右移基本上等同于将数除以2。逻辑右移则和我们亲眼看到的移动数位的操作一致。最好可以通过负数进行描述。</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_184" target="_blank" rel="noopener">9.5.5 常见位操作:获取与设置数位</a></p><p> 9.5.5.1 获取数位</p><p> 9.5.5.2 设置数位</p><p> 9.5.5.3 清零数位</p><p> 9.5.5.4 更新数位</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_185" target="_blank" rel="noopener">9.6 数学与逻辑题</a></p><p> </p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_186" target="_blank" rel="noopener">9.6.1 素数</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_187" target="_blank" rel="noopener">9.6.2 概率</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_188" target="_blank" rel="noopener">9.6.3 大声说出你的思路</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_189" target="_blank" rel="noopener">9.6.4 总结规律和模式</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_190" target="_blank" rel="noopener">9.6.5 略作变通</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_191" target="_blank" rel="noopener">9.6.6 触类旁通</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_192" target="_blank" rel="noopener">9.7 面向对象设计</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_193" target="_blank" rel="noopener">9.7.1 如何解答</a></p><p> 9.7.1.1 步骤1:处理不明确的地方</p><p> 9.7.1.2 步骤2:定义核心对象</p><p> 9.7.1.3 步骤3:分析对象关系</p><p> 9.7.1.4 步骤4:研究对象的动作</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_194" target="_blank" rel="noopener">9.7.2 设计模式</a></p><p> 单例设计(singleton)和工厂方法(factory method)设计模式常见于面试</p><p> 请不要误入歧途——总想着找到某一问题的“正确”设计模式。你需要创建适合于该问题的设计。有时,这样的设计或许是已经存在的模式,但很多情况下并不是</p><p> 需要说明的是,很多人不喜欢使用单例设计模式,甚至称其为“反模式”。原因之一是该模式会干扰单元测试。</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_195" target="_blank" rel="noopener">9.8 递归与动态规划</a></p><p> 尽管递归问题花样繁多,但题型大都类似。问题属不属于递归问题,就看它是否能分解为子问题。</p><p>当你听到问题的开头是这样的:“设计一个算法计算第n 个……”“列出前n 个……”“实现一个方法,计算所有……”等,那么这基本上就是递归问题</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_196" target="_blank" rel="noopener">9.8.1 解题思路</a></p><p> 根据递归的定义,递归的解就是基于子问题的解构建的。通常只要在f(n-1) 的解中加入、移除某些东西或者稍作修改就能算出f(n) 。而在其他情况下,你可能要分别计算每部分的解,然后合并成最后结果。</p><p>将问题分解为子问题的方式多种多样。其中最常用的三种就是自底向上、自上而下和数据分割</p><p> 9.8.1.1 自底向上的递归</p><p> 自底向上的递归往往最为直观。我们从解决问题的简单情况开始,比如,列表中只有一个元素时。然后再解决有2个元素、3个元素的情况,以此类推。关键在于,如何基于 上一种情况的答案(或者前面所有情况)得出后一种情况的解。</p><p> 9.8.1.2 自上而下的递归</p><p> 遇到这类问题时,试着把变量为N 的情况分解成子问题的解。但要注意:分解的子问题间是否有重叠。</p><p> </p><p> 9.8.1.3 数据分割的递归</p><p> 归并排序也是一个“数据分割”的递归。我们排序数组的每一半,之后将其合并。</p><p> </p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_197" target="_blank" rel="noopener">9.8.2 递归与迭代</a></p><p> 递归算法极其耗空间。每次递归调用都会增加一层新的方法入栈,简而言之,如果递归深度为n ,那么最少占用O(n) 的空间。</p><p> 鉴于此,用迭代实现递归算法往往更好。所有的 递归都可以用迭代实现,只不过有时会让代码超级复杂。所以有了递归算法之后,不要急于实现。先问问自己用迭代实现难不难,也可以和面试官讨论该如何权衡。</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_198" target="_blank" rel="noopener">9.8.3 动态规划及记忆法</a></p><p> 通常来说,动态规划就是使用递归算法发现重叠子问题(也就是重复的调用)。然后你可以缓存结果以备不时之需。</p><p>除此之外,你还可以研究递归调用的模式,实现其中重复的部分。这里仍然可以“缓存”中间结果。</p><p> 画递归调用树可以很好地用来计算递归算法运行时间。</p><p> 自上而下的动态规划(记忆法)</p><p> 自底向上的动态规划</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_199" target="_blank" rel="noopener">9.9 系统设计与可扩展性</a></p><p> </p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_200" target="_blank" rel="noopener">9.9.1 处理问题</a></p><p> 在面试中,你作为求职者应该起到主导作用。当然,这不是让你忽略面试官,相反地,要与面试官保持沟通。然而,你应主导问题:提出问题,讨论利弊,深入沟通,做出优化。</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_201" target="_blank" rel="noopener">9.9.2 循环渐进的设计</a></p><p> 9.9.2.1 步骤1:审题</p><p> 9.9.2.2 步骤2:作合理假设</p><p> 9.9.2.3 步骤3:画出主要组件</p><p> 9.9.2.4 步骤4:确定主要问题</p><p> 9.9.2.5 步骤5:针对主要问题重新设计</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_202" target="_blank" rel="noopener">9.9.3 逐步构建的方法:循序渐进</a></p><p> 9.9.3.1 步骤1:提出问题</p><p> </p><p> 9.9.3.2 步骤2:大胆假设</p><p> </p><p> 9.9.3.3 步骤3:切合实际</p><p> </p><p> 9.9.3.4 步骤4:解决问题</p><p> </p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_203" target="_blank" rel="noopener">9.9.4 关键概念</a></p><p> 9.9.4.1 水平扩展与垂直扩展</p><p> 9.9.4.2 负载均衡</p><p> 9.9.4.3 数据库反规范化和非关系型数据库</p><p> 9.9.4.4 数据库分区(分片)</p><p> 9.9.4.5 缓存</p><p> 9.9.4.6 异步处理与队列</p><p> 9.9.4.7 网络指标</p><p> 9.9.4.8 MapReduce</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_204" target="_blank" rel="noopener">9.9.5 系统设计要考虑的因素</a></p><p> 故障</p><p> 可用性与可靠性</p><p> 读多写少与写多读少</p><p> 安全性</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_205" target="_blank" rel="noopener">9.9.6 人无完人,系统亦然</a></p><p> 鉴于此,你的目标应该是,理解用例,仔细审题,作出合理的假设,根据假设给出一个可靠的设计,然后阐明设计的利弊。不要心存幻想,一味追求完美的系统。</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_206" target="_blank" rel="noopener">9.9.7 实例演示</a></p><p> 给定数百万份文件,如何找出所有包含某一组词的文件?这些词出现的顺序不定,但必须是完整的单词,也就是说,book与bookkeeper不能混为一谈。</p><p> 第一步是先忘记我们有数以百万计的文件,假装只有几十个文件。在这种情况下,如何实现findWords 呢?</p><p>现在,回到最初的问题。若有数百万份文件,会有什么问题?首先,我们可能需要将文件分散到多台机器上。此外,我们还要考虑很多因素,比如要查找的单词数量,在文件中重复出现的次数等,一台机器可能放不下完整的散列表。假设我们就要按这些限制因素进行设计。</p><p>文件分散到多台机器上会引出以下几个很关键的问题。</p><p>(1) 如何划分该散列表?我们可以按关键字划分,例如,某台机器上存放包含某个单词的全部文件,或者可以按文件来划分,这样一台机器上只会存放对应某个关键字的部分文件而非全部。</p><p>(2) 一旦决定了如何划分数据,我们可能需要在一台机器上对文件进行处理,并将结果推送到其他机器上。这个过程会是什么样呢?(注意:若按文件划分散列表,可能就不需要这一步。)</p><p>(3) 我们需要找到一种方法获知哪台机器拥有哪些数据。这个查找表会是什么样的?又该存储在什么地方?</p><p>这只是三个主要问题,可能还会有很多其他问题。</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_207" target="_blank" rel="noopener">9.10 排序与查找</a></p><p> 掌握常见的排序与查找算法大有裨益,因为很多排序与查找问题实际上只是将大家熟悉的算法稍作修改而已。因此,处理这类问题的诀窍在于,逐一考虑各种不同的排序算法,看看哪一种较为合适。</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_208" target="_blank" rel="noopener">9.10.1 常见的排序算法</a></p><p> 归并排序(merge sort)、快速排序(quick sort)和桶排序(bucket sort)是面试中最常用的3种类型</p><p> 9.10.1.1 冒泡排序|执行时间:平均情况与最差情况为O(n^2),存储空间:O(1)</p><p> 9.10.1.2 选择排序|执行时间:平均情况与最差情况为O(n^2),存储空间:O(1)</p><p> 9.10.1.3 归并排序|执行时间:平均情况与最差情况为O(n\log(n)) ,存储空间:看情况</p><p> 9.10.1.4 快速排序|执行时间:平均情况为O(n\log(n)),最差情况为O(n^2),存储空间:O(\log(n))</p><p> 9.10.1.5 基数排序|执行时间:O(kn)</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_209" target="_blank" rel="noopener">9.10.2 查找算法</a></p><p> 除了二分查找,还有很多种查找数据结构的方法,总之,我们不要拘泥于二分查找。比如说,你可以利用二叉树或使用散列表来查找某节点。</p><p> </p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_210" target="_blank" rel="noopener">9.11 测试</a></p><p>测试问题一般分为以下4类:(1) 测试现实生活中的事物(比如一支笔);(2) 测试一套软件;(3) 编写代码测试一个函数;(4) 调试解决已知问题。针对每一类题型,我们都会给出相应的解法。</p><p>请记住:处理这4类问题时,切勿假设使用者会做到运用自如,而是做好应对用户误用乱用软件的准备。</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_211" target="_blank" rel="noopener">9.11.1 面试官想考查什么</a></p><p> 表面上看,测试问题主要考查你能否想到周全完备的测试用例。除此之外,面试官还想考查以下几个方面。</p><p> 全局观</p><p> 懂整合</p><p> 会组织 </p><p> 可操作</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_212" target="_blank" rel="noopener">9.11.2 测试现实生活中的事物</a></p><p> 使用者是哪些人?做什么用</p><p> 考虑用户需求</p><p> 有哪些用例</p><p> 正常情况下的使用</p><p> 有哪些使用限制</p><p> 正常情况使用的限制</p><p> 压力条件与失效条件是什么</p><p> 非正常情况</p><p> 如何执行测试</p><p> 手动测试或自动化测试</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_213" target="_blank" rel="noopener">9.11.3 测试一套软件</a></p><p> 测试软件与测试现实生活的事物大同小异。主要差别在于软件测试往往更强调执行测试的细节。</p><p>请注意,软件测试主要涉及如下两个方面。</p><p> 手动测试与自动化测试</p><p> 黑盒测试与白盒测试</p><p>9.11.3.1 步骤1:要做黑盒测试还是白盒测试</p><p>9.11.3.2 步骤2:使用者是哪些人?做什么用</p><p>9.11.3.3 步骤3:有哪些用例</p><p>9.11.3.4 步骤4:有哪些使用限制</p><p>9.11.3.5 步骤5:压力条件和失效条件为何</p><p>9.11.3.6 步骤6:有哪些测试用例?如何执行测试</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_214" target="_blank" rel="noopener">9.11.4 测试一个函数</a></p><p>9.11.4.1 步骤1:定义测试用例</p><p>9.11.4.2 步骤2:定义预期结果</p><p>9.11.4.3 步骤3:编写测试代码</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_215" target="_blank" rel="noopener">9.11.5 调试与故障排除</a></p><p>9.11.5.1 步骤1:理清状况</p><p>9.11.5.2 步骤2:分解问题</p><p> 优秀的测试人员会逐一排查每个步骤,诊断定位问题所在。</p><p>9.11.5.3 步骤3:创建特定的、可控的测试</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_216" target="_blank" rel="noopener">9.12 C 和 C++</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_217" target="_blank" rel="noopener">9.12.1 类和继承</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_218" target="_blank" rel="noopener">9.12.2 构造函数和析构函数</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_219" target="_blank" rel="noopener">9.12.3 虚函数</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_220" target="_blank" rel="noopener">9.12.4 虚析构函数</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_221" target="_blank" rel="noopener">9.12.5 默认值</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_222" target="_blank" rel="noopener">9.12.6 操作符重载</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_223" target="_blank" rel="noopener">9.12.7 指针和引用</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_224" target="_blank" rel="noopener">9.12.8 模板</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_225" target="_blank" rel="noopener">9.13 Java</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_226" target="_blank" rel="noopener">9.13.1 如何处理</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_227" target="_blank" rel="noopener">9.13.2 重载与重写</a></p><p> 重载(overloading)是指两种方法的名称相同,但参数类型或个数不同</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_228" target="_blank" rel="noopener">9.13.3 集合框架</a></p><p> Java的集合框架(collection framework)至关重要,本书许多章节都有所涉及。下面介绍几个最常用的。</p><p> ArrayList</p><p> Vector </p><p> LinkedList </p><p> HashMap </p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_229" target="_blank" rel="noopener">9.14 数据库</a></p><p> </p><p> </p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_230" target="_blank" rel="noopener">9.14.1 SQL语法及各类变体</a></p><p> 显式连接(explicit join)和隐式连接(implicit join)的语法显示如下。这两条语句的作用一样,至于选用哪条全看个人喜好。</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_231" target="_blank" rel="noopener">9.14.2 规范化数据库和反规范化数据库</a></p><p> 规范化数据库的设计目标是将冗余降到最低,反规范化数据库则是为了优化读取时间。</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_232" target="_blank" rel="noopener">9.14.3 SQL语句</a></p><p> </p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_233" target="_blank" rel="noopener">9.14.4 小型数据库设计</a></p><p> 9.14.4.1 步骤1:处理不明确之处</p><p>9.14.4.2 步骤2:定义核心对象</p><p>9.14.4.3 步骤3:分析表之间的关系</p><p>9.14.4.4 步骤4:研究该有什么操作动作</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_234" target="_blank" rel="noopener">9.14.5 大型数据库设计</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_235" target="_blank" rel="noopener">9.15 线程与锁</a></p><p> 不管是什么公司,面试官常常会考查你对线程特别是对死锁的了解程度。</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_236" target="_blank" rel="noopener">9.15.1 Java线程</a></p><p> 在Java中,实现线程有以下两种方式:</p><p>通过实现java.lang.Runnable 接口;</p><p>通过扩展java.lang.Thread 类</p><p>9.15.1.1 实现Runnable接口</p><p>9.15.1.2 扩展Thread 类</p><p>9.15.1.3 扩展Thread 类与实现Runnable 接口</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_237" target="_blank" rel="noopener">9.15.2 同步和锁</a></p><p>关键字synchronized 和lock 是实现代码同步的基础。</p><p>9.15.2.1 同步方法</p><p>9.15.2.2 同步块</p><p>9.15.2.3 锁</p><p>若要实现更细粒度的控制,可以使用锁(lock)。锁(或监视器)用于对共享资源的同步访问,方法是将锁与共享资源关联在一起。线程必须先取得与资源关联的锁,才能访问共享资源。在任意时间点,最多只有一个线程能拿到锁,因此,只有一个线程可以访问共享资源。</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_238" target="_blank" rel="noopener">9.15.3 死锁及死锁的预防</a></p><p> 死锁的出现必须同时满足以下4个条件。</p><p>(1) 互斥 :某一时刻只有一个进程能访问某一资源。或者,更准确地说,对某一资源的访问有限制;若资源数量有限,也可能出现死锁。</p><p>(2) 持有并等待 :已持有某一资源的进程不必释放当前拥有的资源,就能要求更多的资源。</p><p>(3) 没有抢占 :一个进程不能强制另一个进程释放资源。</p><p>(4) 循环等待 :两个或两个以上的进程形成循环链,每个进程都在等待循环链中另一进程持有的资源。</p><p>若要预防死锁,只需避免上述任一条件,但这很棘手,因为其中有些条件很难满足。比如,想要避免条件(1)就很困难,因为许多资源同一时刻只能被一个进程使用(如打印机)。大部分预防死锁的算法都把重心放在避免条件(4)(即循环等待)上。</p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_239" target="_blank" rel="noopener">9.16 中等难题</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_240" target="_blank" rel="noopener">9.17 高难度题</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_241" target="_blank" rel="noopener">第 10 章 题目解法</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_242" target="_blank" rel="noopener">10.1 数组与字符串</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_243" target="_blank" rel="noopener">10.2 链表</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_244" target="_blank" rel="noopener">10.3 栈与队列</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_245" target="_blank" rel="noopener">10.4 树与图</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_246" target="_blank" rel="noopener">10.5 位操作</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_247" target="_blank" rel="noopener">10.6 数学与逻辑题</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_248" target="_blank" rel="noopener">10.7 面向对象设计</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_249" target="_blank" rel="noopener">10.8 递归与动态规划</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_250" target="_blank" rel="noopener">10.9 系统设计与可扩展性</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_251" target="_blank" rel="noopener">10.10 排序与查找</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_252" target="_blank" rel="noopener">10.11 测试</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_253" target="_blank" rel="noopener">10.12 C和C++</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_254" target="_blank" rel="noopener">10.13 Java</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_255" target="_blank" rel="noopener">10.14 数据库</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_256" target="_blank" rel="noopener">10.15 线程与锁</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_257" target="_blank" rel="noopener">10.16 中等难题</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_258" target="_blank" rel="noopener">10.17 高难度题</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_259" target="_blank" rel="noopener">第 11 章 进阶话题</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_260" target="_blank" rel="noopener">11.1 实用数学</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_261" target="_blank" rel="noopener">11.1.1 整数1至N的和</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_262" target="_blank" rel="noopener">11.1.2 2的幂的和</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_263" target="_blank" rel="noopener">11.1.3 对数的底</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_264" target="_blank" rel="noopener">11.1.4 排列</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_265" target="_blank" rel="noopener">11.1.5 组合</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_266" target="_blank" rel="noopener">11.1.6 归纳证明</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_267" target="_blank" rel="noopener">11.2 拓扑排序</a></p><p><a href="http://reader.epubee.com/books/mobile/68/68aef87e268c839b310672e61583b9af/text00000.html#nav_point_268" target="_blank" rel="noopener">11.3 Dijkstra算法</a></p><p> </p>]]></content>
<categories>
<category> 面试 </category>
</categories>
</entry>
<entry>
<title>设计模式</title>
<link href="/41682.html"/>
<url>/41682.html</url>
<content type="html"><![CDATA[<h1 id="设计模式"><a href="#设计模式" class="headerlink" title="设计模式"></a>设计模式</h1><h2 id="设计模式类型"><a href="#设计模式类型" class="headerlink" title="设计模式类型"></a>设计模式类型</h2><p>设计模式分为三种类型:</p><p>创建型模式:单例模式、抽象工厂模式、原型模式、建造者模式、工厂模式。</p><p>结构性模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。</p><p>行为型模式:模板方法模式、命令模式、访问者模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式(Interpreter模式)、状态模式、策略模式、职责链模式(责任链模式)</p><h2 id="设计模式的目的"><a href="#设计模式的目的" class="headerlink" title="设计模式的目的"></a>设计模式的目的</h2><p> 代码重用性;</p><p> 可读性;</p><p> 可扩展性;</p><p> 可靠性;</p><p> 促使高内聚,低耦合,提高软件的可维护性,通用性和扩展性,并降低软件的复杂度;</p><h2 id="设计模式的原则"><a href="#设计模式的原则" class="headerlink" title="设计模式的原则"></a>设计模式的原则</h2><p> 单一职责</p><p> 接口隔离</p><p> 依赖倒转:面向接口编程</p><p> 里氏替换:子类中尽量不要重写父类的方法,因为继承会让两个类耦合性增强了。在合适的时候,可以通过聚合、组合、依赖来解决问题</p><p> 开闭原则:对扩展开放,对修该关闭。用抽象构建框架,用实现扩展细节。</p><p> 迪米特法则:一个类对自己依赖的类知道的越少越好,从而减少耦合性。</p><p> 合成复用:尽量使用合成/聚合的方式,而不是使用继承。</p><p>面向对象->功能模块【设计模式+算法【数据结构】】->框架【使用到多种设计模式】->架构【服务器集群】</p><p>使用过什么设计模式,怎用使用的,解决了什么问题</p><h1 id="创建型模式"><a href="#创建型模式" class="headerlink" title="创建型模式"></a>创建型模式</h1><h2 id="单例设计模式"><a href="#单例设计模式" class="headerlink" title="单例设计模式"></a>单例设计模式</h2><p> 采取一定的方法保证在整个的软件系统中,对,某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。例如Hibernate 的SessionFactory</p><p> 单例的八种方式</p><p> 饿汉式(静态常量)</p><p> 构造器私有化</p><p> 类的内部创建对象</p><p> 向外暴露一个静态的公共方法 </p><p> 饿汉式(静态代码块)</p><p> </p><p> 懒汉式(线程不安全)</p><p> 懒汉式(线程安全,同步方法)</p><p> 懒汉式(线程安全,同步代码块)</p><p> 双重检查(推荐)</p><p> 静态内部类(推荐)</p><p> 枚举(推荐)</p><h3 id="源码中"><a href="#源码中" class="headerlink" title="源码中"></a>源码中</h3><p> JDK中java,lang.Runtime</p><h3 id="使用场景"><a href="#使用场景" class="headerlink" title="使用场景"></a>使用场景</h3><p> 频繁的进行创建和销毁的对象、创建对象耗时过多或耗费资源过多但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(如数据源、session工厂)</p><h2 id="抽象工厂模式"><a href="#抽象工厂模式" class="headerlink" title="抽象工厂模式"></a>抽象工厂模式</h2><h2 id="工厂模式"><a href="#工厂模式" class="headerlink" title="工厂模式"></a>工厂模式</h2><h3 id="简单工厂模式"><a href="#简单工厂模式" class="headerlink" title="简单工厂模式"></a>简单工厂模式</h3><h4 id="源码中-1"><a href="#源码中-1" class="headerlink" title="源码中"></a>源码中</h4><p> JDK中的Calendar类中使用了简单工厂模式</p><h3 id="工厂方法模式"><a href="#工厂方法模式" class="headerlink" title="工厂方法模式"></a>工厂方法模式</h3><p> 定义了一个创建对象的抽象方法,由子类决定要实例化的类,工厂方法模式将对象的实例化推迟到子类</p><h3 id="抽象工厂模式-1"><a href="#抽象工厂模式-1" class="headerlink" title="抽象工厂模式"></a>抽象工厂模式</h3><p> 定义了一个interface用于创建相关或有依赖关系的对象簇。</p><p>将实例化的代码提取出来,放到一个类中同一管理和维护,达到和主项目的依赖关系的解耦。从而提高项目的扩展和维护</p><p>创建对象实例时,不要直接new类,而是把这个new类的动作放在一个工厂的方法中,并返回。</p><p>不要让类继承具体类,二十继承抽象类或者时实现interface接口</p><p>不要覆盖记录中已经实现的方法</p><h2 id="原型模式"><a href="#原型模式" class="headerlink" title="原型模式"></a>原型模式</h2><p>原型实例知道创建对象的种类,并且通过拷贝这些原型,创建新的对象</p><h3 id="深拷贝与浅拷贝"><a href="#深拷贝与浅拷贝" class="headerlink" title="深拷贝与浅拷贝"></a>深拷贝与浅拷贝</h3><p>浅拷贝使用默认的clone()方法实现</p><p>深拷贝通过重写clone方法来实现以及通过对象序列化实现</p><h3 id="源码中-2"><a href="#源码中-2" class="headerlink" title="源码中"></a>源码中</h3><p>Spring 中原型bean的创建</p><h2 id="建造者模式"><a href="#建造者模式" class="headerlink" title="建造者模式"></a>建造者模式</h2><p>可以将复杂对象的建造过程抽象出来,使这个抽象过程的不同实现方法可以构造出不同表现的对象</p><h3 id="源码中-3"><a href="#源码中-3" class="headerlink" title="源码中"></a>源码中</h3><p>java.lang.StringBuilder</p><h1 id="结构型模式"><a href="#结构型模式" class="headerlink" title="结构型模式"></a>结构型模式</h1><h2 id="装饰者设计模式"><a href="#装饰者设计模式" class="headerlink" title="装饰者设计模式"></a>装饰者设计模式</h2><p>动态的将新功能附加到对象上。在对象功能扩展方面,它比继承更具有弹性,装饰着模式也体现了开闭原则。</p><h3 id="源码中-4"><a href="#源码中-4" class="headerlink" title="源码中"></a>源码中</h3><p> Java的IO结构,FileInputStream使用到了装饰者模式</p><h3 id="应用场景"><a href="#应用场景" class="headerlink" title="应用场景"></a>应用场景</h3><h2 id="适配器设计模式"><a href="#适配器设计模式" class="headerlink" title="适配器设计模式"></a>适配器设计模式</h2><p>将口哦个类的接口转换成客户期望的另一个接口表示,主要目的时兼容性。</p><p>分为三类:类适配器模式、对象适配器模式、接口适配器模式</p><p>类适配器</p><p> </p><p>对象适配器</p><p> 根据合成复用原则,使用组合替代继承,所以它解决了类适配器必须继承的局限性</p><p>接口适配器</p><p> 缺省适配器模式,适用于一个接口不想使用其所有方法的情况。</p><h3 id="源码中-5"><a href="#源码中-5" class="headerlink" title="源码中"></a>源码中</h3><p>SpringMvc中的HandlerAdapter</p><h2 id="桥接模式"><a href="#桥接模式" class="headerlink" title="桥接模式"></a>桥接模式</h2><p>将实现与抽象放在两个不同的类层次中,使两个层次可以独立改变</p><h3 id="源码中-6"><a href="#源码中-6" class="headerlink" title="源码中"></a>源码中</h3><p>JDBC中的Driver接口</p><h3 id="应用场景-1"><a href="#应用场景-1" class="headerlink" title="应用场景"></a>应用场景</h3><p>JDBC驱动</p><p>银行转账系统</p><p> 转账分类:网上转账,柜台转账,ATM转账</p><p> 转账用户类型:普通用户,银卡用户,金卡用户</p><p>消息管理</p><p> 消息类型:及时消息、延时消息</p><p> 消息分类:手机短信,邮件消息,QQ消息</p><h2 id="组合模式"><a href="#组合模式" class="headerlink" title="组合模式"></a>组合模式</h2><p>部分整体模式,创建了对象组的树形结构,将对象组合成树状结构以表示“整体-部分”的层次关系。</p><h3 id="源码中-7"><a href="#源码中-7" class="headerlink" title="源码中"></a>源码中</h3><p>Java的HashMap使用了组合模式</p><h3 id="应用场景-2"><a href="#应用场景-2" class="headerlink" title="应用场景"></a>应用场景</h3><p>需要遍历组织机构,或者处理的对象据哟树形i二狗时,非常适合使用组合模式。</p><h2 id="外观模式"><a href="#外观模式" class="headerlink" title="外观模式"></a>外观模式</h2><h3 id="源码中-8"><a href="#源码中-8" class="headerlink" title="源码中"></a>源码中</h3><p>MyBatis中的Configuration,创建MetaObject</p><h3 id="应用场景-3"><a href="#应用场景-3" class="headerlink" title="应用场景"></a>应用场景</h3><h2 id="享元模式"><a href="#享元模式" class="headerlink" title="享元模式"></a>享元模式</h2><p>运用共享技术支持大量细粒度的对象</p><p>内部状态和外部状态</p><p> 将对象的的信息分为两个部分,内部状态和外部状态。内部状态指对象共享出来的信息,存储在享元对象内部且不会随环境的改变而改变,外部状态指对象得依赖的一个标记,是随环境改变而改变的、不可共享的状态。</p><h3 id="源码中-9"><a href="#源码中-9" class="headerlink" title="源码中"></a>源码中</h3><p> JDK中的Interger</p><h3 id="应用场景-4"><a href="#应用场景-4" class="headerlink" title="应用场景"></a>应用场景</h3><p> 数据库连接池、String常量池、缓冲池等池技术</p><h2 id="代理模式"><a href="#代理模式" class="headerlink" title="代理模式"></a>代理模式</h2><p>为一个对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象。这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。</p><p> 代理模式分类:静态代理、动态代理(JDK代理、接口代理)和Cglib代理(可以在内存动态的创建对象,而不需要实现接口,他是属于动态代理的范畴)</p><p>静态代理</p><p> 静态代理需要定义接口或者父类,被代理对象(目标对象)与代理对象一起实现相同的接口或者是继承相同父类。</p><p>动态代理</p><p> 代理对象,不需要实现接口,但是目标对象要实现接口,否则不能用动态代理 </p><p>Cglib代理模式</p><p> 静态代理和JDK代理模式都需要目标丢向实现一个接口,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候可使用目标对象子类来实现代理。</p><p> 在内存中构建一个子类对象从而实现对目标对象功能扩展。它是一个强大的高性能的代码生成包,它可以在运行期扩展java类与实现java接口,它广泛的被许多AOP的框架使用,例如Spring AOP,实现方法拦截。</p><p> AOP编程中选择代理模式:</p><p> 目标对象需要实现接口,用JDK代理</p><p> 目标对象不需要实现接口,用Cglib代理。</p><p> Cglib包的底层是通过使用字节码处理框架ASM来转换字节码并生成新的类。</p><p> 代理的类不能final,否则报错java.lang.IllegaArgumentException。</p><p> 防火墙代理</p><p> 内网通过代理穿透防火墙,实现对公网的访问</p><p> 缓存代理</p><p> 当请求图片文件等资源时,先到缓存代理取,如果取到资源则OK,如果取不到资源,再到公网或者数据库取,然后缓存。</p><p> 远程代理</p><p> 远程对象的本地代表,通过它可以把远程对象当本地对象来调用。远程代理通过网络和真正的远程对象沟通信息。</p><p> 同步代理:主要使用在多线程编程中,完成多线程间同步工作。</p><h3 id="源码中-10"><a href="#源码中-10" class="headerlink" title="源码中"></a>源码中</h3><h3 id="应用场景-5"><a href="#应用场景-5" class="headerlink" title="应用场景"></a>应用场景</h3><h1 id="行为型模式"><a href="#行为型模式" class="headerlink" title="行为型模式"></a>行为型模式</h1><h2 id="模板方法"><a href="#模板方法" class="headerlink" title="模板方法"></a>模板方法</h2><p> 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构,就可以重定义该算法的某些特定步骤。</p><p> 模板方法模式的钩子方法</p><h3 id="源码中-11"><a href="#源码中-11" class="headerlink" title="源码中"></a>源码中</h3><p> Spring IOC 容器初始化时运用到模板方法模式</p><h3 id="应用场景-6"><a href="#应用场景-6" class="headerlink" title="应用场景"></a>应用场景</h3><h2 id="命令模式"><a href="#命令模式" class="headerlink" title="命令模式"></a>命令模式</h2><p>使得请求发送者</p><h3 id="源码中-12"><a href="#源码中-12" class="headerlink" title="源码中"></a>源码中</h3><p>Spring 框架的JdbcTemplate使用了命令模式</p><h3 id="应用场景-7"><a href="#应用场景-7" class="headerlink" title="应用场景"></a>应用场景</h3><p>界面的一个按钮都市一条命令、模拟CMD(DOS命令)订单的撤销/恢复、触发反馈机制。</p><h2 id="访问者模式"><a href="#访问者模式" class="headerlink" title="访问者模式"></a>访问者模式</h2><p>封装一些作用于某种数据结构的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。</p><p> 将数据结构和操作分离,解决数据结构和操作耦合的问题。</p><h3 id="源码中-13"><a href="#源码中-13" class="headerlink" title="源码中"></a>源码中</h3><h3 id="应用场景-8"><a href="#应用场景-8" class="headerlink" title="应用场景"></a>应用场景</h3><p> 需要对一个对象结构中的对象进行很多不同的操作(操作之间彼此没有关联),同时,需要避免让这些操作“污染”这些对象的类。</p><p> 适合做报表、UI、拦截器于过滤器,这种数据结构相对稳定的系统</p><h2 id="迭代器模式"><a href="#迭代器模式" class="headerlink" title="迭代器模式"></a>迭代器模式</h2><p> 提供一种遍历集合元素的同一的接口,使用一致方法遍历集合元素,不需要知道集合对象的底层表示,不会暴露内部的结构。</p><h3 id="源码中-14"><a href="#源码中-14" class="headerlink" title="源码中"></a>源码中</h3><p> JDK的ArrayList集合中就使用了迭代器模式。</p><h3 id="应用场景-9"><a href="#应用场景-9" class="headerlink" title="应用场景"></a>应用场景</h3><h2 id="观察者模式"><a href="#观察者模式" class="headerlink" title="观察者模式"></a>观察者模式</h2><p>对象之间多对一依赖的一种设计方案,被依赖的对象为Subject,依赖的对象为Observer,Subject通知Observer变化。</p><h3 id="源码中-15"><a href="#源码中-15" class="headerlink" title="源码中"></a>源码中</h3><p>JDK的Observable类使用了观察者模式</p><h3 id="应用场景-10"><a href="#应用场景-10" class="headerlink" title="应用场景"></a>应用场景</h3><h2 id="中介模式"><a href="#中介模式" class="headerlink" title="中介模式"></a>中介模式</h2><h3 id="源码中-16"><a href="#源码中-16" class="headerlink" title="源码中"></a>源码中</h3><h3 id="应用场景-11"><a href="#应用场景-11" class="headerlink" title="应用场景"></a>应用场景</h3><p> MVC架构模式,C(Controller 控制器)为中介中</p><h2 id="备忘录模式"><a href="#备忘录模式" class="headerlink" title="备忘录模式"></a>备忘录模式</h2><p>在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样之后就可以将该对象回复大原先保存的状态。</p><h3 id="源码中-17"><a href="#源码中-17" class="headerlink" title="源码中"></a>源码中</h3><h3 id="应用场景-12"><a href="#应用场景-12" class="headerlink" title="应用场景"></a>应用场景</h3><p> 后悔药;打游戏时的存档;Windows里的ctrl+z;IE中的后退;数据库的事务管理;</p><p> 为了节约内存,备忘录模式可以和原型模式配合使用</p><h2 id="解释器模式"><a href="#解释器模式" class="headerlink" title="解释器模式"></a>解释器模式</h2><p>是指给定一个语言(表达式),定义它的文法的一种表示,并定义一个解释器,使用该解释器来解释语言中的句子(表达式)</p><h3 id="源码中-18"><a href="#源码中-18" class="headerlink" title="源码中"></a>源码中</h3><p>Spring 框架中SpelExpressionParser</p><h3 id="应用场景-13"><a href="#应用场景-13" class="headerlink" title="应用场景"></a>应用场景</h3><p>可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。</p><p>编译器;运算表达式;正则表达式;机器人;</p><h2 id="状态模式"><a href="#状态模式" class="headerlink" title="状态模式"></a>状态模式</h2><p>主要用来解决对象在多种状态转换时,需要对外输出不同的行为的问题,状态和行为时一一对应的,状态之间可以相互转换。</p><p>当一个对象的内在状态改变时,允许改变其行为,</p><h3 id="源码中-19"><a href="#源码中-19" class="headerlink" title="源码中"></a>源码中</h3><p>借贷平台,订单的不同状态</p><h3 id="应用场景-14"><a href="#应用场景-14" class="headerlink" title="应用场景"></a>应用场景</h3><p>当一个事件或对象有很多种状态,状态之间会相互转换,对不同的状态要求有不同的行为的时候,可以考虑使用状态模式。</p><h2 id="策略模式"><a href="#策略模式" class="headerlink" title="策略模式"></a>策略模式</h2><p>定义算法族(策略组),分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户</p><p>把变化的代码从不变的代码种分离出来;</p><p>针对接口编程而不是具体类;</p><p>多用组合/聚合,少继承(客户通过组合方式使用策略)</p><p>避免了使用多重转移语句(if..else if.. else)</p><h3 id="源码中-20"><a href="#源码中-20" class="headerlink" title="源码中"></a>源码中</h3><p>JDK的Arrays的Comparator就使用了策略模式</p><h3 id="应用场景-15"><a href="#应用场景-15" class="headerlink" title="应用场景"></a>应用场景</h3><h2 id="责任链模式"><a href="#责任链模式" class="headerlink" title="责任链模式"></a>责任链模式</h2><p>为请求创建了一个接受者对象的链。着中国模式对请求的放者和接收者进行解耦</p><h3 id="源码中-21"><a href="#源码中-21" class="headerlink" title="源码中"></a>源码中</h3><p>SpringMVC-HandlerExecutionChain类就使用到职责责任链模式</p><h3 id="应用场景-16"><a href="#应用场景-16" class="headerlink" title="应用场景"></a>应用场景</h3><p>有多个对象可以处理同一个请求时,如,多级请求、请假/加薪等审批流程、Java Web中Tomcat对Encoding的处理、拦截器</p><p>设计模式,在应对变化的需求时,总结了许多模式,提升了系统的可维护性、可扩展性。</p><p> </p>]]></content>
<categories>
<category> Basic </category>
</categories>
</entry>
<entry>
<title>On Java 8</title>
<link href="/29494.html"/>
<url>/29494.html</url>
<content type="html"><![CDATA[<h2 id="万物皆对象"><a href="#万物皆对象" class="headerlink" title="万物皆对象"></a>万物皆对象</h2><p> 把万物看作对象,对象之间在传递信息。</p><p> 对象的创建,对象的存储(数量、类型),对象之间的关系处理(对象间传递信息),对象的销毁,异常处理</p><h2 id="抽象"><a href="#抽象" class="headerlink" title="抽象"></a>抽象</h2><h2 id="封装"><a href="#封装" class="headerlink" title="封装"></a>封装</h2><h2 id="继承"><a href="#继承" class="headerlink" title="继承"></a>继承</h2><p> Java 为单继承语言,有别于多继承的C++</p><h2 id="多态"><a href="#多态" class="headerlink" title="多态"></a>多态</h2><p> 多样式与向上转型</p><p> 多态和构造器</p><p> <strong>多态的好处</strong>在于解耦</p><h2 id="复用"><a href="#复用" class="headerlink" title="复用"></a>复用</h2><p> 组合和继承</p><p> 构造器加载顺序(对比销毁顺序)</p><h2 id="接口与抽象类"><a href="#接口与抽象类" class="headerlink" title="接口与抽象类"></a>接口与抽象类</h2><p> 接口为抽象类</p><p> 接口被用来建立类之间的协议</p><p> 接口中的守卫方法或虚拟扩展方法(default)</p><p> 类可以实现多个接口,但是抽象类只能继承单一抽象类</p><p> 面向接口编程,将接口与实现解耦可以应用于多种不同的实现</p><p> 选择问题:</p><p> 尽可能地抽象,更倾向使用接口而非抽象类</p><p> 接口和策略模式</p><p> 接口和工厂模式</p><p> 优先使用类而不是接口,若有必要使用接口,再对代码重构也不迟。如果只是单纯为了设计接口而设计接口,只会徒增复杂性。</p><h2 id="内部类"><a href="#内部类" class="headerlink" title="内部类"></a>内部类</h2><p> 内部自动类拥有对外部类所有成员的访问权,外部类与内部类产生的引用</p><p> 匿名内部类,与正规的继承相比有些受限,因为匿名内部类既可以扩展类,也可以实现接口,但是不能两者兼备,而如果是实现接口,也只能实现一个接口。</p><p> 嵌套类</p><p> <strong>内部类有效地实现了多重继承</strong>,每个内部类都能独立地继承自一个(接口的)实现,所以无论外部类是否已经继承了某个(接口的)实习,对于内部类都没有影响</p><p> 内部类的其他特性:</p><p> 内部类可以有多个实例</p><p> 再单个外部类中,可以让多个内部类以不同的方 式实现</p><p> 创建内部类对象的时刻并不依赖于外部类对象的 创建</p><p> 内部类并没有令人迷惑的“is-a”关系,它就是一个独立的实体 </p><p> 内部类提供的闭包功能,相比指针更灵活更安全</p><p> 回调的价值在于它的灵活性-可以再运行时动态地决定需要调用什么方法</p><p> 在控制框架使用内部类的价值</p><p> 局部内部类和匿名内部类</p><p>设计模式总是将变化的事物与保持不变的事物分离开,在这个模式中,模板方法是保持不变的事物,而可覆盖的方法就是变化的事物</p><h2 id="集合"><a href="#集合" class="headerlink" title="集合"></a><strong>集合</strong></h2><p> <strong>持有对象</strong>的思想</p><p> 泛型与类型安全的集合</p><p> 散列码和hashCode()</p><p> List可以在创建后添加或删除元素,并自行调整大小</p><p> collection</p><p> 迭代器,能够将遍历序列的操作与该序列的底层结构分离</p><p> for-in 和迭代器</p><p> 集合和迭代器</p><p> <img src="F:%5CBlog%5Csource_posts%5COn-Java-8%5Ccollection.png" alt="collection"></p><p> </p><p> 上图中用粗黑的框包裹的类为常用的类</p><p><img src="F:%5CBlog%5Csource_posts%5COn-Java-8%5Ccollection1.png" alt="collection1"></p><p><img src="F:%5CBlog%5Csource_posts%5COn-Java-8%5Cmap.png" alt="map"></p><h3 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h3><p>1.hashmap如何解决哈希碰撞,都有哪些方法?</p><p><a href="https://www.cnblogs.com/xuwc/p/8644945.html" target="_blank" rel="noopener">HashMap和HashTable源码分析</a></p><p><a href="https://tech.meituan.com/2016/06/24/java-hashmap.html" target="_blank" rel="noopener">java 8系列之重新认识HashMap</a></p><p><a href="https://www.taowong.com/blog/2019/01/07/hashmap-ask.html#hashtable" target="_blank" rel="noopener">HashMap连环问</a></p><p><a href="https://blog.csdn.net/single6/article/details/81353084" target="_blank" rel="noopener">为什么hashtable 桶数通常会取一个素数?如何有效避免hash结果值的碰撞</a></p><p>2.hashmap的内存利用率?如果想让内存利用率达到100%,并且时间复杂度降到O(1)怎么办?</p><p><a href="https://haldir65.github.io/2017/07/23/2017-07-23-from-java-code-to-java-heap/" target="_blank" rel="noopener">java对象内存占用分析</a></p><p>3.源码问题,集合类的底层实现</p><p>4.安全性问题,线程安全与否</p><p>5.<a href="https://weiwei02.github.io/2017/08/25/jdk/map%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/" target="_blank" rel="noopener">Map源码</a></p><p>6.<a href="https://zhuanlan.zhihu.com/p/48285067" target="_blank" rel="noopener">jdk、jre、jvm的区别</a></p><p> </p><h2 id="函数式编程"><a href="#函数式编程" class="headerlink" title="函数式编程"></a>函数式编程</h2><p> lambda表达式</p><p> 方法引用</p><p> 函数式接口:</p><p> 在使用函数式接口时,名称无关紧要,只要参数类型和返回类型相同</p><p> 高阶函数</p><p> 闭包,利用闭包可以轻松生成函数。支持闭包也叫变量捕获。只要有内部类就会有闭包</p><p> 等同final效果</p><p> 函数组合,多个还能输组合成新函数 </p><p> 柯里化和部分求值</p><p> 柯里化,将一个多参数函数转换为一系列单参数函数</p><p> 纯函数式编程,Scala,Clojure</p><h2 id="流式编程"><a href="#流式编程" class="headerlink" title="流式编程"></a>流式编程</h2><p> 集合优化了对象的存储,而流和对象有关。流是一系列与特定存储机制无关的元素。利用流,可以不迭代集合中的元素,就可以提取和操作数据。<strong>流的好处是,它使得程序更加短小和容易理解</strong>。</p><p> Lambda表达式和方法引用结合流式编程会更加简便、简洁。</p><p> 流式编程是一种声明式编程,声明要做什么,而非怎么做的编程风格。</p><p> 流式编程采用内部迭代。</p><p> 流是懒加载的。</p><p> 流操作,创建流,修该流元素,消费流元素</p><p> 创建流:</p><p> stream.of(); stream(); 集合通过stream()方法来产生一个流。</p><p> 中级流操作</p><p> optional类</p><p> 终端操作</p><h2 id="异常"><a href="#异常" class="headerlink" title="异常"></a>异常</h2><p> 异常处理程序,不仅能节省代码,而且把“描述在正常执行剁成中做什么事”和“出了问题怎么办”的代码相分离。异常机制使代码的阅读、编写和调试工作更加井井有条。</p><p> 基本异常</p><p> 异常捕获</p><p> 自定义异常</p><p> 异常声明</p><p> 重新抛出异常</p><p> 异常链</p><p> Java标准异常</p><p> finally 用于把除内存之外的资源恢复到他们初始状态时。如果把finally子句和带标签的break及continue配合使用,在Java里就没必要使用goto语句了</p><p> 异常丢失</p><p> 异常限制</p><p> 构造器和异常处理</p><p> Try-With-Resources和构造器异常处理</p><p> 异常匹配</p><p> 异常处理的一个重要原则是“只有在你知道如何处理的情况下才捕获异常”。异常处理的一个中哟啊目标就是把错误处理的代码同错误发生的地点相分离。</p><p> <strong>吞了异常</strong></p><p> 被检查的异常与并发症</p><p> 所有模型都是错误的,但有些是 能用的。</p><p> 反射和泛型就是用来补偿静态类型检查所带来的过多限制。</p><p> 好的程序设计语言能帮助程序员写出好程序,但无论哪种语言都避免不了程序员用它写出坏程序。</p><p> 把被检查的异常转换为不检查的异常</p><p> 异常链</p><p> 用RuntimeException来包装,被检查的异常</p><p> 异常指南</p><p> 1.尽可能使用 try-with-resource。</p><p> 2.在恰当的级别处理问题。(在知道该如何处理的情况下才捕获异常。)</p><p> 3.解决问题并且重新调用产生异常的方法。</p><p> 4.进行少许修补,然后绕过异常发生的地方继续执行。</p><p> 5.用别的数据进行计算,以代替方法预计会返回的值。</p><p> 6.把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层。</p><p> 7.把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层。</p><p> 8.终止程序。</p><p> 9.进行简化。(如果你的异常模式使问题变得太复杂,那用起来会非常痛苦也很烦人。)</p><p> 10.让类库和程序更安全。(这既是在为调试做短期投资,也是在为程序的健壮性做长期投资。)</p><p> 报告功能是异常的精髓所在</p><h2 id="代码校验"><a href="#代码校验" class="headerlink" title="代码校验"></a>代码校验</h2><p>让代码健壮的方法 </p><p> 测试</p><p> 单元测试</p><p> 前置条件(契约式设计DBC)</p><p> 断言</p><p> Java断言语法</p><p> Guava断言</p><p> 检查指令</p><p> 测试驱动开发(TDD)</p><p> 日志</p><p> 调试</p><p> JDB、图形化调试器</p><p> 基准测试</p><p> 剖析和优化</p><p> 剖析和优化</p><p> 优化准则</p><p> 避免为了性能牺牲代码的可读性。</p><p> 不要独立地看待性能。衡量与带来的收益相 比所需投入的工作量。</p><p> 程序的大小很重要。性能优化通常只对运行 了长时间的大型项目有价值。性能通常不是小项 目的关注点。</p><p> 运行起来程序比一心钻研它的性能具有更高 的优先级。一旦你已经有了可工作的程序,如有 必要的话,你可以使用剖析器提高它的效率。只 有当性能是关键因素时,才需要在设计/开发阶段 考虑性能。</p><p> 不要猜测瓶颈发生在哪。运行剖析器,让剖 析器告诉你。</p><p> 无论何时有可能的话,显式地设置实例为 null 表明你不再用它。这对垃圾收集器来说是个 有用的暗示。</p><p> <strong>static final</strong> 修饰的变量会被 JVM 优化从而 提高程序的运行速度。因而程序中的常量应该声 明 <strong>static final</strong>。</p><p> 风格检测</p><p> 静态错误分析</p><p> 代码重审</p><p> 结对编程</p><p> 重构:</p><p> 重构基石:</p><p> 测试</p><p> 自动构建</p><p> 版本控制</p><p> 持续集成</p><p> 持续集成服务器</p><p> 持续集成需要分布式版本管理,自动构建和自动 测测试系统作为基础</p><h2 id="文件"><a href="#文件" class="headerlink" title="文件"></a>文件</h2><p> 流与文件结合使得文件操作编程变得更加优雅</p><p> 文件和目录路径</p><p> 选取部分路径片段</p><p> 路径分析</p><p> FIile工具类</p><p> 文件系统</p><p> 路径监听</p><p> 文件删除和线程</p><p> 文件查找</p><p> 文件读写</p><p> </p><p> java.nio.file </p><p> java.nio.file.Files</p><h2 id="字符串"><a href="#字符串" class="headerlink" title="字符串"></a>字符串</h2><p> 字符串的不可变</p><p> 参数是为该方法提供信息的,而不是想让该方法 改变自己的</p><p> +的重载与StringBuilder</p><p> 不可变性与效率,string与stringbuilder类</p><p> 在有循环且有性能问题时,使用stringbuilder类</p><p> stringbuffer与stringbuilder,stringbuffer是线 程安全的,因此开销会大一些。</p><p> 意外递归</p><p> 打印对象内存地址,使用super.tostring(),而不 去使用this, 使用this会发生自动类型转换、递归调用</p><p> 字符串操作</p><p> 当需要改变字符串的内容时,<code>String</code> 类的方法 都会返回一个新的 <code>String</code> 对象。同时,如果内容不 改变,<code>String</code> 方法只是返回原始对象的一个引用而 已。这可以节约存储空间以及避免额外的开销</p><p> 在 Java 中,字符串操作还主要集中于<code>String</code>、 <code>StringBuffer</code> 和 <code>StringTokenizer</code> 类</p><p> 格式化输出</p><p> printf()</p><p> System.out.format()</p><p> Formatter类,</p><p> 在 Java 中,所有的格式化功能都是由 <code>java.util.Formatter</code> 类处理的。</p><p> 格式化修饰符</p><p> <code>Formatter</code> 转换</p><p> 还有许多不常用的类型转换与格式修饰符选 项,你可以在 JDK 文档中的 <code>Formatter</code> 类部分 找到它们。</p><p> String.format()</p><p> 在 <code>String.format()</code> 内部,它也是创建了 一个 <code>Formatter</code> 对象,然后将你传入的参数转 给 <code>Formatter</code>。不过,与其自己做这些事情,不 如使用便捷的 <code>String.format()</code> 方法,何况这 样的代码更清晰易读。</p><p> 一个十六进制转储(dump)工具</p><p> 为了打开及读入二进制文件,我们用到了另一个 工具 <code>Files.readAllBytes()</code>,这已经在 <a href="">Files章节</a> 介绍过了。这里的 <code>readAllBytes()</code> 方法将整个文件 以 <code>byte</code> 数组的形式返回</p><p> 正则表达式</p><p> 处理string的匹配、选择、编辑以及验证</p><p> <code>String.split()</code> 还有一个重载的版本,它允许 你限制字符串分割的次数</p><p> 创建正则表达式</p><p> 正则表达式的完整构造子列表,请参考JDK文档 <code>java.util.regex</code> 包中的 <code>Pattern</code>类</p><p> 当你学会了使用字符类(character classes)之 后,正则表达式的威力才能真正显现出来</p><p> 量词</p><p> 量词描述了一个模式捕获输入文本的方式</p><p> CharSequence</p><p> 接口 <code>CharSequence</code> 从 <code>CharBuffer</code>、 <code>String</code>、<code>StringBuffer</code>、<code>StringBuilder</code> 类中抽象 出了字符序列的一般化定义</p><p> Pattern<code>和</code>Matcher</p><p> java.util.regext.Matcher</p><p> <code>find()</code></p><p> <code>Matcher.find()</code> 方法可用来在 <code>CharSequence</code> 中查找多个匹配</p><p> start()<code>和</code>end()</p><p> <code>Pattern</code> 标记</p><p> split()</p><p> 替换操作</p><p> reset()</p><p> 正则表达式与 Java I/O</p><p> 扫描输入</p><p> <code>Scanner</code> 分隔符</p><p> StringTokenizer类</p><p> 在 Java 引入正则表达式(J2SE1.4)和 <code>Scanner</code> 类(Java SE5)之前,分割字符 串的唯一方法是使用 <code>StringTokenizer</code> 来 分词。不过,现在有了正则表达式和 <code>Scanner</code>,我们可以使用更加简单、更加简洁 的方式来完成同样的工作了</p><p> 基本上,我们可以放心地说, <code>StringTokenizer</code> 已经可以废弃不用了。</p><h2 id="运算符"><a href="#运算符" class="headerlink" title="运算符"></a>运算符</h2><p> <a href="https://www.jianshu.com/p/8cf5af30f245" target="_blank" rel="noopener">Java &、&&、|、||、^、<<、>>、~、>>>等运算符</a></p><h2 id="类型信息"><a href="#类型信息" class="headerlink" title="类型信息"></a>类型信息</h2><p> java如何在运行时识别对象和类信息:</p><p> “传统的”RTTI(运行时类型信息)</p><p> 使用 RTTI,我们可以查询某个 <code>Shape</code> 引用所指向对象的确切类型, 然后择或者剔除特例。</p><p> “反射”机制:允许我们在运行时发现和 使用类信息。</p><p> <code>Class</code> 对象</p><p> 类加载器</p><p> 原生类加载器与额外的类加载器</p><p> <code>Class</code> 对象仅在需要的时候才会被 加载,<code>static</code> 初始化是在类加载时进 行的</p><p> 无论何时,只要你想在运行时使用 类型信息,就必须先得到那个 <code>Class</code> 对 象的引 用。<code>Class.forName()</code> 就是 实现这个功能的一个便捷途径,因为使 用该方法你不需要先持有这个类型 的对 象。但是,如果你已经拥有了目标类的 对象,那就可以通过调用 <code>getClass()</code> 方法来获取 <code>Class</code> 引用了,这个方法来 自根类 <code>Object</code>,它将返回表示该对象 实际类型的 <code>Class</code> 对象的引用</p><p> <code>Class</code> 对象的 <code>newInstance()</code> 方 法是实现“虚拟构造器”的一种途径,虚 拟构造器可以让你在不知道一个类的确 切类型的时候,创建这个类的对象</p><p> 类字面常量</p><p> 类字面常量用于生成类对象的引用</p><p> 为了使用类而做的准备工作实际包含三 个步骤</p><p> 加载:这是由类加载器执行的。该步骤 将查找字节码(通常在 classpath 所指定的 路径中查找,但这并非是必须的),并从这 些字节码中创建一个 <code>Class</code> 对象</p><p> 链接:在链接阶段将验证类中的字节 码,为 <code>static</code> 字段分配存储空间,并且如 果需要的话,将解析这个类创建的对其他类 的所有引用。</p><p> 初始化:如果该类具有超类,则先初始 化超类,执行 <code>static</code> 初始化器和 <code>static</code> 初始化块。</p><p> 仅使用 <code>.class</code> 语法来获得对类对象的 引用不会引发初始化。但与此相反,使用 <code>Class.forName()</code> 来产生 <code>Class</code> 引用会 立即就进行初始化</p><p> 泛化的 <code>Class</code> 引用</p><p> 向 <code>Class</code> 引用添加泛型语法的原因只是 为了提供编译期类型检查</p><p> <code>cast()</code> 方法</p><p> Java 中用于 <code>Class</code> 引用的转型语 法</p><p> 类型转换检测 </p><p> 已知的 RTTI 类型</p><p> 传统的类型转换,如 “<code>(Shape)</code>”, 由 RTTI 确保转换的正确性,如果执行了 一个 错误的类型转换,就会抛出一 个 <code>ClassCastException</code> 异常。</p><p> 代表对象类型的 <code>Class</code> 对象. 通过 查询 <code>Class</code> 对象可以获取运行时所需的 信息.</p><p> RTTI 在 Java 中还有第三种形式,那就 是关键字 <code>instanceof</code></p><p> 使用类字面量</p><p> 使用类字面量重新实现 <code>PetCreator</code> 类的话,其结果在 很多方面都会更清晰。</p><p> 一个动态 <code>instanceof</code> 函数</p><p> <code>Class.isInstance()</code> 方法提供 了一种动态测试对象类型的方法。</p><p> <code>isInstance()</code> 方法消除了对 <code>instanceof</code> 表达式的需要</p><p> 递归计数</p><p> 可以使用 <code>Class.isAssignableFrom()</code> 而不是预加载 <code>Map</code> ,并创建一个不限于计数 <code>Pet</code> 的通用工具</p><p> 注册工厂</p><p> </p><p> 类的等价比较</p><p> 查询类型信息时,需要注意: instanceof 的形式(即 <code>instanceof</code> 或 <code>isInstance()</code> ,这两者产生的结果相同) 和与 Class 对象直接比较这两者间存在重要区别</p><p> <code>instanceof</code> 说的是“你是这个类,还是从这个类派生的类?”。而如果使用 <code>==</code> 比较实际的<code>Class</code> 对象,则与继承无关 —— 它要么是确切的类型,要么不是。</p><p> 反射:运行时类信息</p><p> 如果你不知道对象的确切类型,RTTI 会告 诉你。但是,有一个限制:必须在编 译时 知道类型,才能使用 RTTI 检测它,并对信息做 一些有用的事情。换句话说,编译器必须知道你 使用的所有类</p><p> 反射提供了检测可用方法并生成方法名称 的机制</p><p> 在运行时发现类信息的另一个令人信服的 动机是提供跨网络在远程平台上创建和执行对象 的能力。这称为<em>远程方法调用</em>(RMI),它使 Java 程序的对象分布在许多机器上</p><p> 重要的是要意识到反射没有什么魔力。当 你使用反射与未知类型的对象交互时,JVM 将查 看该对象,并看到它属于特定的类(就像普通的 RTTI)。在对其执行任何操作之前,必须加载 <code>Class</code> 对象。因此,该特定类型的 <code>.class</code> 文件必须在本地计算机上或通过网络对 JVM 仍 然可用。因此,RTTI 和反射的真正区别在于, 使用 RTTI 时,编译器在编译时会打开并检查 .class文件。换句话说,你可以用“正常”的方式 调用一个对象的所有方法。通过反射,.class文 件在编译时不可用;它由运行时环境 打开 并检查。</p><p> 类方法提取器</p><p> 反射是用来支持其他java特性的,如对象序 列化、动态提取有关类的信息</p><p> 编程时,当你不记得某个类是否有特定的 方法,并且不想在 JDK 文档中搜索索引或类层 次结构时,或者如果你不知道该类是否可以对 <code>Color</code> 对象执行任何操作时,该工具能节省不 少时间</p><p> 动态代理</p><p> 当你希望将额外的操作与“真实对象”做分离 时,代理可能会有所帮助,尤 其是当你想要轻松 地启用额外的操作时,反之亦然(设计模式就是 封装 变更—所以你必须改变一些东西以证明模 式的合理性)。例如,如果你想跟踪 RealObject中方法的调用,或衡量此类调用的开销,该怎么 办?你不想这部分 代码耦合到你的程序中,而代理能使你可以很轻松地添加或删除它</p><p> Java 的<em>动态代理</em>更进一步,不仅动态创建代理对 象而且动态处理对代理方法的调用。在动态代理上进行 的所有调用都被重定向到单个<em>调用处理程序</em>,该处理程 序负责发现调用的内容并决定如何处理</p><p> 可以通过调用静态方法Proxy.newProxyInstance()来创建动态代理,该方法需要一个类加载器(通常可以从已加载的对象中获取)</p><p> 通常执行代理操作,然后使用 <code>Method.invoke()</code> 将请求转发给被代理对象,并携带必要的参数。这在一开始看起来是有限制的,好像你只能执行一般的操作。但是,可以过滤某些方法调用,同时传递其他方法调用:</p><p> Optional类</p><p> 如果你使用内置的 <code>null</code> 来表示没有对象,每次 使用引用的时候就必须测试一下引用是否为 <code>null</code>,这 显得有点枯燥,而且势必会产生相当乏味的代码。问题 在于 <code>null</code> 没什么自己的行为,只会在你想用它执行 任何操作的时候产生 <code>NullPointException</code>。 <code>java.util.Optional</code>(首次出现是在<a href="https://github.com/LingCoder/OnJava8/tree/5d7462a629a410cb29f98938387daa0d846d8e4b/docs/book/docs/book/13-Functional-Programming.md">函数式编程</a>这 章)为 <code>null</code> 值提供了一个轻量级代理,<code>Optional</code> 对象可以防止你的代码直接抛出NullPointException。</p><p> 标记接口</p><p> 有时候使用一个<strong>标记接口</strong>来表示空值会更方便。标记接口里边什么都没有,你只要把它的名字当做标签来用就可以。</p><p> 假设存在许多不同类型的 <code>Robot</code>,我们想让每种 <code>Robot</code> 都创建一个 <code>Null</code> 对象来执行一些特殊的操作——在本例中,即提供 <code>Null</code> 对象所代表 <code>Robot</code> 的确切类型信息。这些信息是通过动态代理捕获的:</p><p> 无论何时,如果你需要一个空 <code>Robot</code> 对象,只需要调用 <code>newNullRobot()</code>,并传递需要代理的 <code>Robot</code> 的类型。这个代理满足了 <code>Robot</code> 和 <code>Null</code> 接口的需要,并提供了它所代理的类型的确切名字。</p><p> Mock 对象和桩</p><p> <strong>Mock 对象</strong>和 <strong>桩(Stub)</strong>在逻辑上都是 <code>Optional</code> 的变体。他们都是最终程序中所使用的“实际”对象的代理。不过,Mock 对象和桩都是假扮成那些可以传递实际信息的实际对象,而不是像 <code>Optional</code> 那样把包含潜在 <code>null</code> 值的对象隐藏</p><p> Mock 对象和桩之间的的差别在于程度不同。Mock 对象往往是轻量级的,且用于自测试。通常,为了处理各种不同的测试场景,我们会创建出很多 Mock 对象。而桩只是返回桩数据,它通常是重量级的,并且经常在多个测试中被复用。桩可以根据它们被调用的方式,通过配置进行修改。因此,桩是一种复杂对象,它可以做很多事情。至于 Mock 对象,如果你要做很多事,通常会创建大量又小又简单的 Mock 对象。</p><p> 接口和类型</p><p> <code>interface</code> 关键字的一个重要目标就是允许程序员隔离组件,进而<strong>降低耦合度</strong>。使用接口可以实现这一目标,但是通过类型信息,这种耦合性还是会传播出去——接口并不是对解耦的一种无懈可击的保障</p><p> 通过使用反射,仍然可以调用所有方法,甚至是 <code>private</code> 方法!如果知道方法名,你就可以在其 <code>Method</code> 对象上调用 <code>setAccessible(true)</code>,就像在 <code>callHiddenMethod()</code> 中看到的那样。</p><p> 任何方式都没法阻止反射调用那些非公共访问权限的方法。对于字段来说也是这样,即便是 <code>private</code> 字段:</p><p> 程序员往往对编程语言提供的访问控制过于自信,甚至认为 Java 在安全性上比其它提供了(明显)更宽松的访问控制的语言要优越。然而,正如你所看到的,事实并不是这样。</p><h2 id="泛型"><a href="#泛型" class="headerlink" title="泛型"></a>泛型</h2><p> 普通的类和方法只能使用特定的类型:基本数据类型或类类型。如果编写的代码需要应用于多种类型,这种严苛的限制对代码的束缚就会很大。</p><p> 多态是一种面向对象思想的泛化机制。你可以将方法的参数类型设为基类,这样的方法就可以接受任何派生类作为参数,包括暂时还不存在的类</p><p> 泛型实现了<em>参数化类型</em>,这样你编写的组件(通常是集合)可以适用于多种类型。“泛型”这个术语的含义是“适用于很多类型”。编程语言中泛型出现的初衷是通过解耦类或方法与所使用的类型之间的约束,使得类或方法具备最宽泛的表达力。随后你会发现 Java 中泛型的实现并没有那么“泛”,你可能会质疑“泛型”这个词是否合适用来描述这一功能。</p><p> 与 C++ 的比较</p><p> Java 中的泛型需要与 C++ 进行对比</p><p> 只有知道了某个技术不能做什么,你才能更好地做到所能做的</p><p> 简单泛型</p><p> 一个集合中存储多种不同类型的对象的情况很少见,通常而言,我们只会用集合存储同一种类型的对象。泛型的主要目的之一就是用来约定集合要存储什么类型的对象,并且通过编译器确保规约得以满足</p><p> 与其使用 <code>Object</code> ,我们更希望先指定一个类型占位符,稍后再决定具体使用什么类型。要达到这个目的,需要使用<em>类型参数</em>,用尖括号括住,放在类名后面。然后在使用这个类时,再用实际的类型替换此类型参数</p><p> Java 泛型的核心概念:你只需告诉编译器要使用什么类型,剩下的细节交给它来处理。</p><p> 一个元组类库</p><p> 有时一个方法需要能返回多个对象。而 <strong>return</strong> 语句只能返回单个对象,解决方法就是创建一个对象,用它打包想要返回的多个对象</p><p> 这个概念称为<em>元组*,它是将一组对象直接打包存储于单一对象中。可以从该对象读取其中的元素,但不允许向其中存储新对象(这个概念也称为 *数据传输对象</em> 或 <em>信使</em> )。</p><p> 一个堆栈类</p><p> 我们可以看出,泛型只不过是一种类型罢了(稍后我们会看到一些例外的情况)。</p><p> 内部类 <code>Node</code> 也是一个泛型,它拥有自己的类型参数</p><p> </p><p> RandomList</p><p> 作为容器的另一个例子,假设我们需要一个持有特定类型对象的列表,每次调用它的 <code>select()</code> 方法时都随机返回一个元素。如果希望这种列表可以适用于各种类型,就需要使用泛型</p><p> 泛型接口</p><p> 泛型也可以应用于接口。例如 <em>生成器*,这是一种专门负责创建对象的类。实际上,这是 *工厂方法</em> 设计模式的一种应用。不过,当使用生成器创建新的对象时,它不需要任何参数,而工厂方法一般需要参数。生成器无需额外的信息就知道如何创建新对象</p><p> 泛型方法</p><p> 到目前为止,我们已经研究了参数化整个类。其实还可以参数化类中的方法。类本身可能是泛型的,也可能不是,不过这与它的方法是否是泛型的并没有什么关系</p><p> 泛型方法独立于类而改变方法。作为准则,请“尽可能”使用泛型方法。通常将单个方法泛型化要比将整个类泛型化更清晰易懂</p><p> 要定义泛型方法,请将泛型参数列表放置在返回值之前</p><p> 变长参数和泛型方法</p><p> 泛型方法和变长参数列表可以很好地共存</p><p> 一个泛型的 Supplier</p><p> 这是一个为任意具有无参构造方法的类生成 <strong>Supplier</strong> 的类。为了减少键入,它还包括一个用于生成 <strong>BasicSupplier</strong> 的泛型方法</p><p> 简化元组的使用</p><p> 使用类型参数推断和静态导入,我们将把早期的元组重写为更通用的库</p><p> 一个 Set 工具</p><p> 对于泛型方法的另一个示例,请考虑由 <strong>Set</strong> 表示的数学关系。这些被方便地定义为可用于所有不同类型的泛型方法:</p><p> 构建复杂模型</p><p> 泛型的一个重要好处是能够简单安全地创建复杂模型</p><p> 泛型擦除</p><p> Java 泛型是使用擦除实现的。这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此,<code>List<String></code> 和 <code>List<Integer></code> 在运行时实际上是相同的类型。它们都被擦除成原生类型 <code>List</code></p><p> 理解擦除并知道如何处理它,是你在学习 Java 泛型时面临的最大障碍之一</p><p> 为了调用 <code>f()</code>,我们必须协助泛型类,给定泛型类一个边界,以此告诉编译器只能接受遵循这个边界的类型。这里重用了 <strong>extends</strong> 关键字。由于有了边界,下面的代码就能通过编译:</p><p> 泛型只有在类型参数比某个具体类型(以及其子类)更加“泛化”——代码能跨多个类工作时才有用。因此,类型参数和它们在有用的泛型代码中的应用,通常比简单的类替换更加复杂。但是,不能因此认为使用 <code><T extends HasF></code> 形式就是有缺陷的。例如,如果某个类有一个返回 <strong>T</strong> 的方法,那么泛型就有所帮助,因为它们之后将返回确切的类型</p><p> 你必须查看所有的代码,从而确定代码是否复杂到必须使用泛型的程度。</p><p> 迁移兼容性</p><p> 为了减少潜在的关于擦除的困惑,你必须清楚地认识到这不是一个语言特性。它是 Java 实现泛型的一种妥协,因为泛型不是 Java 语言出现时就有的,所以就有了这种妥协。它会使你痛苦,因此你需要尽早习惯它并了解为什么它会这样</p><p> 擦除减少了泛型的泛化性。泛型在 Java 中仍然是有用的,只是不如它们本来设想的那么有用,而原因就是擦除。</p><p> 擦除的核心动机是你可以在泛化的客户端上使用非泛型的类库,反之亦然。这经常被称为“迁移兼容性”。</p><p> 因此 Java 泛型不仅必须支持向后兼容性——现有的代码和类文件仍然合法,继续保持之前的含义——而且还必须支持迁移兼容性,使得类库能按照它们自己的步调变为泛型,当某个类库变为泛型时,不会破坏依赖于它的代码和应用。 </p><p> 擦除使得这种向泛型的迁移成为可能,允许非泛型的代码和泛型代码共存。</p><p> 类库毫无争议是编程语言的一部分,对生产效率有着极大的影响</p><p> </p><p> 擦除的问题</p><p> 因此,擦除主要的正当理由是从非泛化代码到泛化代码的转变过程,以及在不破坏现有类库的情况下将泛型融入到语言中。擦除允许你继续使用现有的非泛型客户端代码,直至客户端准备好用泛型重写这些代码。这是一个崇高的动机,因为它不会骤然破坏所有现有的代码。</p><p> 擦除和迁移兼容性意味着,使用泛型并不是强制的,</p><p> 边界处的动作</p><p> 因为擦除,我发现了泛型最令人困惑的方面是可以表示没有任何意义的事物</p><p> 对于在泛型中创建数组,使用 <code>Array.newInstance()</code> 是推荐的方式</p><p> 即使编译器无法得知 <code>add()</code> 中的 <strong>T</strong> 的任何信息,但它仍可以在编译期确保你放入 <strong>FilledList</strong> 中的对象是 <strong>T</strong> 类型。因此,即使擦除移除了方法或类中的实际类型的信息,编译器仍可以确保方法或类中使用的类型的内部一致性。</p><p> 泛型的所有动作都发生在边界处——对入参的编译器检查和对返回值的转型</p><p> 补偿擦除</p><p> 因为擦除,我们将失去执行泛型代码中某些操作的能力。无法在运行时知道确切类型</p><p> 有时,我们可以对这些问题进行编程,但是有时必须通过引入类型标签来补偿擦除。这意味着为所需的类型显式传递一个 <strong>Class</strong> 对象,以在类型表达式中使用它</p><p> 创建类型的实例</p><p> 试图在 <strong>Erased.java</strong> 中 <code>new T()</code> 是行不通的,部分原因是由于擦除,部分原因是编译器无法验证 <strong>T</strong> 是否具有默认(无参)构造函数。但是在 C++ 中,此操作自然,直接且安全(在编译时检查)</p><p> 泛型数组</p><p> 正如在 <strong>Erased.java</strong> 中所看到的,我们无法创建泛型数组。通用解决方案是在试图创建泛型数组的时候使用 <strong>ArrayList</strong></p><p> 成功创建泛型类型的数组的唯一方法是创建一个已擦除类型的新数组,并将其强制转换</p><p> 由于擦除,数组的运行时类型只能是 <code>Object[]</code> 。 如果我们立即将其转换为 <code>T[]</code> ,则在编译时会丢失数组的实际类型,并且编译器可能会错过一些潜在的错误检查。因此,最好在集合中使用 <code>Object[]</code> ,并在使用数组元素时向 <strong>T</strong> 添加强制类型转换</p><p> 边界</p><p> <em>边界</em>(bounds)在本章的前面进行了简要介绍。边界允许我们对泛型使用的参数类型施加约束。尽管这可以强制执行有关应用了泛型类型的规则,但潜在的更重要的效果是我们可以在绑定的类型中调用方法</p><p> 通配符</p><p> 真正的问题是我们在讨论的集合类型,而不是集合持有对象的类型。与数组不同,泛型没有内建的协变类型。这是因为数组是完全在语言中定义的,因此可以具有编译期和运行时的内建检查,但是在使用泛型时,编译器和运行时系统不知道你想用类型做什么,以及应该采用什么规则。</p><p> 但是,有时你想在两个类型间建立某种向上转型关系。通配符可以产生这种关系。</p><p> 编译器有多聪明</p><p> 如果创建了一个 <code>Holder<Apple></code>,就不能将其向上转型为 <code>Holder<Fruit></code>,但是可以向上转型为 <code>Holder<? extends Fruit></code></p><p> 逆变?</p><p> 还可以走另外一条路,即使用超类型通配符。这里,可以声明通配符是由某个特定类的任何基类来界定的,方法是指定 <code><?super MyClass></code> ,或者甚至使用类型参数: <code><?super T></code>(尽管你不能对泛型参数给出一个超类型边界;即不能声明 <code><T super MyClass></code> )。这使得你可以安全地传递一个类型对象到泛型类型中。因此,有了超类型通配符,就可以向 <strong>Collection</strong> 写入了</p><p> 无界通配符</p><p> 无界通配符 <code><?></code> 看起来意味着“任何事物”,因此使用无界通配符好像等价于使用原生类型。事实上,编译器初看起来是支持这种判断的</p><p> <strong>List</strong> 实际上表示“持有任何 <strong>Object</strong> 类型的原生 <strong>List</strong> ”,而 <code>List<?></code> 表示“具有某种特定类型的非原生 <strong>List</strong> ,只是我们不知道类型是什么。</p><p> 因此,使用确切类型来替代通配符类型的好处是,可以用泛型参数来做更多的事,但是使用通配符使得你必须接受范围更宽的参数化类型作为参数。因此,必须逐个情况地权衡利弊,找到更适合你的需求的方法。</p><p> 捕获转换</p><p> 有一种特殊情况需要使用 <code><?></code> 而不是原生类型。如果向一个使用 <code><?></code> 的方法传递原生类型,那么对编译器来说,可能会推断出实际的类型参数,使得这个方法可以回转并调用另一个使用这个确切类型的方法。</p><p> 捕获转换只有在这样的情况下可以工作:即在方法内部,你需要使用确切的类型。注意,不能从 <code>f2()</code> 中返回 <strong>T</strong>,因为 <strong>T</strong> 对于 <code>f2()</code> 来说是未知的。捕获转换十分有趣,但是非常受限。</p><p> 问题</p><p> 任何基本类型都不能作为类型参数。</p><p> 解决方法是使用基本类型的包装器类以及自动装箱机制。如果创建一个 <code>ArrayList<Integer></code>,并将基本类型 <strong>int</strong> 应用于这个集合,那么你将发现自动装箱机制将自动地实现 <strong>int</strong> 到 <strong>Integer</strong> 的双向转换——因此,这几乎就像是有一个 <code>ArrayList<int></code> 一样</p><p> 自动装箱机制解决了一些问题,但并没有解决所有问题。</p><p> 自动装箱不适用于数组,因此我们必须创建 <code>FillArray.fill()</code> 的重载版本,或创建产生 <strong>Wrapped</strong> 输出的生成器。 <strong>FillArray</strong> 仅比 <code>java.util.Arrays.setAll()</code> 有用一点,因为它返回填充的数组</p><p> 实现参数化接口</p><p> 个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口</p><p> 转型和警告</p><p> 使用带有泛型类型参数的转型或 <strong>instanceof</strong> 不会有任何效果</p><p> 通过泛型类来转型?</p><p> 重载</p><p> 当擦除后的参数不能产生唯一的参数列表时,你必须提供不同的方法名</p><p> 基类劫持接口</p><p> 一旦 <strong>Comparable</strong> 的类型参数设置为 <strong>ComparablePet</strong>,其他的实现类只能比较 <strong>ComparablePet</strong>:</p><p> 自限定的类型</p><p> 这就像两面镜子彼此照向对方所引起的目眩效果一样,是一种无限反射。<strong>SelfBounded</strong> 类接受泛型参数 <strong>T</strong>,而 <strong>T</strong> 由一个边界类限定,这个边界就是拥有 <strong>T</strong> 作为其参数的 <strong>SelfBounded</strong></p><p> 古怪的循环泛型</p><p> 这可以按照 Jim Coplien 在 C++ 中的<em>古怪的循环模版模式</em>的命名方式,称为古怪的循环泛型(CRG)。“古怪的循环”是指类相当古怪地出现在它自己的基类中这一事实。 为了理解其含义,努力大声说:“我在创建一个新类,它继承自一个泛型类型,这个泛型类型接受我的类的名字作为其参数。”当给出导出类的名字时,这个泛型基类能够实现什么呢?好吧,Java 中的泛型关乎参数和返回类型,因此它能够产生使用导出类作为其参数和返回类型的基类。它还能将导出类型用作其域类型,尽管这些将被擦除为 <strong>Object</strong> 的类型</p><p> 注意,这里有些东西很重要:新类 <strong>Subtype</strong> 接受的参数和返回的值具有 <strong>Subtype</strong> 类型而不仅仅是基类 <strong>BasicHolder</strong> 类型。这就是 CRG 的本质:基类用导出类替代其参数。这意味着泛型基类变成了一种其所有导出类的公共功能的模版,但是这些功能对于其所有参数和返回值,将使用导出类型。也就是说,在所产生的类中将使用确切类型而不是基类型。因此,在<strong>Subtype</strong> 中,传递给 <code>set()</code> 的参数和从 <code>get()</code> 返回的类型都是确切的 <strong>Subtype</strong>。</p><p> 自限定 ?</p><p> 自限定的参数有何意义呢?它可以保证类型参数必须与正在被定义的类相同。正如你在 B 类的定义中所看到的,还可以从使用了另一个 <strong>SelfBounded</strong> 参数的 <strong>SelfBounded</strong> 中导出,尽管在 <strong>A</strong> 类看到的用法看起来是主要的用法。对定义 <strong>E</strong> 的尝试说明不能使用不是 <strong>SelfBounded</strong> 的类型参数。 遗憾的是, <strong>F</strong> 可以编译,不会有任何警告,因此自限定惯用法不是可强制执行的。如果它确实很重要,可以要求一个外部工具来确保不会使用原生类型来替代参数化类型。 注意,可以移除自限定这个限制,这样所有的类仍旧是可以编译的,但是 <strong>E</strong> 也会因此而变得可编译</p><p> 因此很明显,自限定限制只能强制作用于继承关系</p><p> 参数协变 ?</p><p> 自限定类型的价值在于它们可以产生<em>协变参数类型</em>——方法参数类型会随子类而变化。</p><p> 自限定泛型事实上将产生确切的导出类型作为其返回值,就像在 <code>get()</code> 中所看到的一样</p><p> <code>set(derived)</code> 和 <code>set(base)</code> 都是合法的,因此 <code>DerivedSetter.set()</code> 没有覆盖 <code>OrdinarySetter.set()</code> ,而是重载了这个方法。从输出中可以看到,在 <strong>DerivedSetter</strong> 中有两个方法,因此基类版本仍旧是可用的,因此可以证明它被重载过。 但是,在使用自限定类型时,在导出类中只有一个方法,并且这个方法接受导出类型而不是基类型为参数:</p><p> 动态类型安全</p><p> 因为可以向 Java 5 之前的代码传递泛型集合,所以旧式代码仍旧有可能会破坏你的集合。Java 5 的 <strong>java.util.Collections</strong> 中有一组便利工具,可以解决在这种情况下的类型检查问题,它们是:静态方法 <code>checkedCollection()</code> 、<code>checkedList()</code>、 <code>checkedMap()</code> 、 <code>checkedSet()</code> 、<code>checkedSortedMap()</code>和 <code>checkedSortedSet()</code>。这些方法每一个都会将你希望动态检查的集合当作第一个参数接受,并将你希望强制要求的类型作为第二个参数接受。</p><p> 泛型异常</p><p> 由于擦除的原因,<strong>catch</strong> 语句不能捕获泛型类型的异常,因为在编译期和运行时都必须知道异常的确切类型。泛型类也不能直接或间接继承自 <strong>Throwable</strong>(这将进一步阻止你去定义不能捕获的泛型异常)。但是,类型参数可能会在一个方法的 <strong>throws</strong> 子句中用到。这使得你可以编写随检查型异常类型变化的泛型代码</p><p> 混型?</p><p> 术语<em>混型<em>随时间的推移好像拥有了无数的含义,但是其最基本的概念是混合多个类的能力,以产生一个可以表示混型中所有类型的类。这往往是你最后的手段,它将使组装多个类变得简单易行。 混型的价值之一是它们可以将特性和行为一致地应用于多个类之上。如果想在混型类中修改某些东西,作为一种意外的好处,这些修改将会应用于混型所应用的所有类型之上。正由于此,混型有一点</em>面向切面编程</em> (AOP) 的味道,而切面经常被建议用来解决混型问题</p><p> C++ 中的混型?</p><p> 泛型类不能直接继承自一个泛型参数</p><p> 与接口混合?</p><p> 一种更常见的推荐解决方案是使用接口来产生混型效果</p><p> 使用装饰器模式?</p><p> 装饰器是通过使用组合和形式化结构(可装饰物/装饰器层次结构)来实现的,而混型是基于继承的。因此可以将基于参数化类型的混型当作是一种泛型装饰器机制,这种机制不需要装饰器设计模式的继承结构。</p><p> 也就是说,尽管可以添加多个层,但是最后一层才是实际的类型,因此只有最后一层的方法是可视的,而混型的类型是所有被混合到一起的类型。因此对于装饰器来说,其明显的缺陷是它只能有效地工作于装饰中的一层(最后一层),而混型方法显然会更自然一些。因此,装饰器只是对由混型提出的问题的一种局限的解决方案</p><p> 与动态代理混合</p><p> 可以使用动态代理来创建一种比装饰器更贴近混型模型的机制(查看 <a href="https://github.com/LingCoder/OnJava8/tree/c9abc5c07ef502f6b0b83926993918a15febd576/docs/book/book/19-Type-Information.md">类型信息</a> 一章中关于 Java 的动态代理如何工作的解释)。通过使用动态代理,所产生的类的动态类型将会是已经混入的组合类型。 由于动态代理的限制,每个被混入的类都必须是某个接口的实现</p><p> 因为只有动态类型而不是静态类型才包含所有的混入类型,因此这仍旧不如 C++ 的方式好,因为可以在具有这些类型的对象上调用方法之前,你被强制要求必须先将这些对象向下转型到恰当的类型。</p><p> 潜在类型机制</p><p> 在本章的开头介绍过这样的思想,即要编写能够尽可能广泛地应用的代码。为了实现这一点,我们需要各种途径来放松对我们的代码将要作用的类型所作的限制,同时不丢失静态类型检查的好处。然后,我们就可以编写出无需修改就可以应用于更多情况的代码,即更加“泛化”的代码。</p><p> 泛型代码典型地只能在泛型类型上调用少量方法,而具有潜在类型机制的语言只要求实现某个方法子集,而不是某个特定类或接口,从而放松了这种限制(并且可以产生更加泛化的代码)。正由于此,潜在类型机制使得你可以横跨类继承结构,调用不属于某个公共接口的方法。因此,实际上一段代码可以声明:“我不关心你是什么类型,只要你可以 <code>speak()</code> 和 <code>sit()</code> 即可。”由于不要求具体类型,因此代码就可以更加泛化。</p><p> 潜在类型机制是一种代码组织和复用机制。有了它,编写出的代码相对于没有它编写出的代码,能够更容易地复用。代码组织和复用是所有计算机编程的基本手段:编写一次,多次使用,并在一个位置保存代码。因为我并未被要求去命名我的代码要操作于其上的确切接口,所以,有了潜在类型机制,我就可以编写更少的代码,并更容易地将其应用于多个地方</p><p> 支持潜在类型机制的语言包括 Python(可以从 <a href="http://www.Python.org" target="_blank" rel="noopener">www.Python.org</a> 免费下载)、C++、Ruby、SmallTalk 和 Go。Python 是动态类型语言(几乎所有的类型检查都发生在运行时),而 C++ 和 Go 是静态类型语言(类型检查发生在编译期),因此潜在类型机制不要求静态或动态类型检查</p><p> pyhton 中的潜在类型</p><p> <code>perform()</code> 不关心其参数的类型,因此我可以向它传递任何对象,只要该对象支持 <code>speak()</code> 和 <code>sit()</code> 方法。如果传递给 <code>perform()</code> 的对象不支持这些操作,那么将会得到运行时异常。</p><p> C++ 中的潜在类型</p><p> 在 Python 和 C++ 中,<strong>Dog</strong> 和 <strong>Robot</strong> 没有任何共同的东西,只是碰巧有两个方法具有相同的签名。从类型的观点看,它们是完全不同的类型。但是,<code>perform()</code> 不关心其参数的具体类型,并且潜在类型机制允许它接受这两种类型的对象。 C++ 确保了它实际上可以发送的那些消息,如果试图传递错误类型,编译器就会给你一个错误消息(这些错误消息从历史上看是相当可怕和冗长的,是 C++ 的模版名声欠佳的主要原因)。尽管它们是在不同时期实现这一点的,C++ 在编译期,而 Python 在运行时,但是这两种语言都可以确保类型不会被误用,因此被认为是强类型的。潜在类型机制没有损害强类型机制。</p><p> Go 中的潜在类型</p><p> <code>main()</code> 证明 <code>perform()</code> 确实对其参数的确切类型不在乎,只要可以在该参数上调用 <code>talk()</code> 和 <code>sit()</code> 即可。 但是,就像 C ++ 模板函数一样,在编译时检查类型。</p><p> java中的直接潜在类型</p><p> 因为泛型是在这场竞赛的后期才添加到 Java 中,因此没有任何机会可以去实现任何类型的潜在类型机制,因此 Java 没有对这种特性的支持。所以,初看起来,Java 的泛型机制比支持潜在类型机制的语言更“缺乏泛化性”。(使用擦除来实现 Java 泛型的实现有时称为第二类泛型类型)例如,在 Java 8 之前如果我们试图用 Java 实现上面 dogs-and-robots 的示例,那么就会被强制要求使用一个类或接口,并在边界表达式中指定它</p><p> 对缺乏潜在类型机制的补偿</p><p> 尽管 Java 不直接支持潜在类型机制,但是这并不意味着泛型代码不能在不同的类型层次结构之间应用。也就是说,我们仍旧可以创建真正的泛型代码,但是这需要付出一些额外的努力。</p><p> 反射 </p><p> 这些类完全是彼此分离的,没有任何公共基类(除了 <strong>Object</strong> )或接口。通过反射, <code>CommunicateReflectively.perform()</code> 能够动态地确定所需要的方法是否可用并调用它们。它甚至能够处理 <strong>Mime</strong> 只具有一个必需的方法这一事实,并能够部分实现其目标。</p><p> 将一个方法应用于序列 </p><p> 反射提供了一些有用的可能性,但是它将所有的类型检查都转移到了运行时,因此在许多情况下并不是我们所希望的。如果能够实现编译期类型检查,这通常会更符合要求。但是有可能实现编译期类型检查和潜在类型机制吗? </p><p> Java8 中的辅助潜在类型</p><p> 尽管传递未绑定的方法引用似乎要花很多力气,但潜在类型的最终目标还是可以实现的。 我们创建了一个代码片段 <code>CommunicateA.perform()</code> ,该代码可用于任何具有符合签名的方法引用的类型。 请注意,这与我们看到的其他语言中的潜在类型有所不同,因为这些语言不仅需要签名以符合规范,还需要方法名称。 因此,该技术可以说产生了更多的通用代码。</p><p> 使用<strong>Suppliers</strong>类的通用方法</p><p> 通过辅助潜在类型,我们可以定义本章其他部分中使用的 <strong>Suppliers</strong> 类。 此类包含使用生成器填充 <strong>Collection</strong> 的工具方法。 泛化这些操作很有意义:</p><p> 总结:类型转换真的如此之糟吗?</p><p> 使用泛型类型机制的最吸引人的地方,就是在使用集合类的地方,这些类包括诸如各种 <strong>List</strong> 、各种 <strong>Set</strong> 、各种 <strong>Map</strong> 等你在 <a href="https://github.com/LingCoder/OnJava8/tree/c9abc5c07ef502f6b0b83926993918a15febd576/docs/book/book/12-Collections.md">集合</a> 和 <a href="https://github.com/LingCoder/OnJava8/tree/c9abc5c07ef502f6b0b83926993918a15febd576/docs/book/book/Appendix-Collection-Topics.md">附录:集合主题</a> 这两章所见。在 Java 5 之前,当你将一个对象放置到集合中时,这个对象就会被向上转型为 <strong>Object</strong> ,因此你会丢失类型信息。当你想要将这个对象从集合中取回,用它去执行某些操作时,必须将其向下转型回正确的类型。我用的示例是持有 <strong>Cat</strong> 的 <strong>List</strong> (这个示例的一种使用苹果和桔子的变体在 <a href="https://github.com/LingCoder/OnJava8/tree/c9abc5c07ef502f6b0b83926993918a15febd576/docs/book/book/12-Collections.md">集合</a> 章节的开头展示过)。如果没有 Java 5 泛型版本的集合,你放到容集里和从集合中取回的都是 <strong>Object</strong> 。因此,我们很可能会将一个 <strong>Dog</strong> 放置到 <strong>Cat</strong> 的 <strong>List</strong> 中</p><p> 但是,泛型出现之前的 Java 并不会让你误用放入到集合中的对象。如果将一个 <strong>Dog</strong> 扔到 <strong>Cat</strong> 的集合中,并且试图将这个集合中的所有东西都当作 <strong>Cat</strong> 处理,那么当你从这个 <strong>Cat</strong> 集合中取回那个 <strong>Dog</strong> 引用,并试图将其转型为 <strong>Cat</strong> 时,就会得到一个 <strong>RuntimeException</strong> 。你仍旧可以发现问题,但是是在运行时而非编译期发现它的</p><p> 泛型正如其名称所暗示的:它是一种方法,通过它可以编写出更“泛化”的代码,这些代码对于它们能够作用的类型具有更少的限制,因此单个的代码段可以应用到更多的类型上。</p><p> 正如你在本章中看到的,编写真正泛化的“持有器”类( Java 的容器就是这种类)相当简单,但是编写出能够操作其泛型类型的泛化代码就需要额外的努力了,这些努力需要类创建者和类消费者共同付出,他们必须理解这些代码的概念和实现。这些额外的努力会增加使用这种特性的难度,并可能会因此而使其在某些场合缺乏可应用性,而在这些场合中,它可能会带来附加的价值。</p><h2 id="数组"><a href="#数组" class="headerlink" title="数组"></a>数组</h2><p> 随着 Java Collection 和 Stream 类中高级功能的不断增加,日常编程中使用数组的需求也在变少,所以你暂且可以放心地略读甚至跳过这一章。但是,即使你自己避免使用数组,也总会有需要阅读别人数组代码的那一天。</p><p> 数组特性</p><p> 将数组和其他类型的集合区分开来的原因有三:效率,类型,保存基本数据类型的能力。在 Java 中,使用数组存储和随机访问对象引用序列是非常高效的。数组是简单的线性序列,这使得对元素的访问变得非常快。然而这种高速也是有代价的,代价就是数组对象的大小是固定的,且在该数组的生存期内不能更改。</p><p> 不管在编译时还是运行时,Java都会阻止你犯向对象发送不正确消息的错误。然而不管怎样,使用数组都不会有更大的风险。比较好的地方在于,如果编译器报错,最终的用户更容易理解抛出异常的含义。</p><p> 一个数组可以保存基本数据类型,而一个预泛型的集合不可以。然而对于泛型而言,集合可以指定和检查他们保存对象的类型,而通过 <strong>自动装箱</strong> (autoboxing)机制,集合表现地就像它们可以保存基本数据类型一样,因为这种转换是自动的。</p><p> 数组和 <strong>ArrayList</strong> 之间的相似是设计者有意为之,所以在概念上,两者很容易切换。但是就像你在<a href="https://github.com/LingCoder/OnJava8/tree/3c08903cf0e58229fe8ae1360869bc55d6dd7024/docs/book/book/12-Collections.md">集合</a>中看到的,集合的功能明显多于数组。随着 Java 自动装箱技术的出现,通过集合使用基本数据类型几乎和通过数组一样简单。数组唯一剩下的优势就是效率。然而,当你解决一个更加普遍的问题时,数组可能限制太多,这种情形下,您可以使用集合类。</p><p> 用于显示数组的实用程序</p><p> 一等对象</p><p> 不管你使用的什么类型的数组,数组中的数据集实际上都是对堆中真正对象的引用。数组是保存指向其他对象的引用的对象,数组可以隐式地创建,作为数组初始化语法的一部分,也可以显式地创建,比如使用一个 <strong>new</strong> 表达式。数组对象的一部分(事实上,你唯一可以使用的方法)就是只读的 <strong>length</strong> 成员函数,它能告诉你数组对象中可以存储多少元素。<strong>[ ]</strong> 语法是你访问数组对象的唯一方式。</p><p> 返回数组</p><p> 假设你写了一个方法,这个方法不是返回一个元素,而是返回多个元素。对 C++/C 这样的语言来说这是很困难的,因为你无法返回一个数组,只能是返回一个指向数组的指针。这会带来一些问题,因为对数组生存期的控制变得很混乱,这会导致内存泄露。</p><p> 多维数组</p><p> 非基元的对象数组也可以定义为不规则数组</p><p> 数组初始化时使用自动装箱技术</p><p> 泛型数组</p><p> 一般来说,数组和泛型并不能很好的结合。你不能实例化参数化类型的数组,类型擦除需要删除参数类型信息,而且数组必须知道它们所保存的确切类型,以强制保证类型安全。但是,可以参数化数组本身的类型。</p><p> 如果你知道你不会进行向上类型转换,你的需求相对简单,那么可以创建一个泛型数组,它将提供基本的编译时类型检查。然而,一个泛型 <strong>Collection</strong> 实际上是一个比泛型数组更好的选择。</p><p> 一般来说,您会发现泛型在类或方法的边界上是有效的。在内部,擦除常常会使泛型不可使用。</p><p> Arrays的fill方法</p><p> </p><p> Arrays的setAll方法</p><p> 增量生成</p><p> </p><p> 随机生成</p><p> </p><p> 泛型和基本数组</p><p> 在本章的前面,我们被提醒,泛型不能和基元一起工作。在这种情况下,我们必须从基元数组转换为包装类型的数组,并且还必须从另一个方向转换。下面是一个转换器可以同时对所有类型的数据执行操作</p><p> 数组元素修改</p><p> </p><p> 数组并行</p><p> 用简单的方法编写代码。不要开始处理并行性,除非它成为一个问题。您仍然会遇到并行性。在本章中,我们将介绍一些为并行执行而编写的Java库方法。因此,您必须对它有足够的了解,以便进行基本的讨论,并避免出现错误。</p><p> parallelSetAll()</p><p> 流式编程产生优雅的代码</p><p> Arrays工具类</p><p> 您已经看到了 <strong>java.util.Arrays</strong> 中的 <strong>fill()</strong> 和 <strong>setAll()/parallelSetAll()</strong> 。该类包含许多其他有用的 <strong>静态</strong> 程序方法,我们将对此进行研究</p><p> 数组拷贝</p><p> 与使用for循环手工执行复制相比,<strong>copyOf()</strong> 和 <strong>copyOfRange()</strong> 复制数组要快得多。这些方法被重载以处理所有类型</p><p> 数组比较</p><p> 数组相等的含义:数组必须有相同数量的元素,并且每个元素必须与另一个数组中的对应元素相等,对每个元素使用 <strong>equals()</strong>(对于原生类型,使用原生类型的包装类的 <strong>equals()</strong> 方法;例如,int的Integer.equals()。</p><p> 流和数组</p><p> <strong>stream()</strong> 方法很容易从某些类型的数组中生成元素流。</p><p> 通常,将数组转换为流来生成所需的结果要比直接操作数组容易得多。请注意,即使流已经“用完”(您不能重复使用它),您仍然拥有该数组,因此您可以以其他方式使用它—-包括生成另一个流。</p><p> 数组排序</p><p> 编程设计的一个主要目标是“将易变的元素与稳定的元素分开”,在这里,保持不变的代码是一般的排序算法,但是变化的是对象的比较方式。因此,使用策略设计模式而不是将比较代码放入许多不同的排序源码中。使用策略模式时,变化的代码部分被封装在一个单独的类(策略对象)中。</p><p> Java有两种方式提供比较功能。第一种方法是通过实现 <strong>java.lang.Comparable</strong> 接口的原生方法。这是一个简单的接口,只含有一个方法 <strong>compareTo()</strong>。该方法接受另一个与参数类型相同的对象作为参数,如果当前对象小于参数,则产生一个负值;如果参数相等,则产生零值;如果当前对象大于参数,则产生一个正值。</p><p> Arrays.sort()的使用</p><p> Java标准库中使用的排序算法被设计为最适合您正在排序的类型—-原生类型的快速排序和对象的归并排序。</p><p> 并行排序</p><p> 如果排序性能是一个问题,那么可以使用 <strong>Java 8 parallelSort()</strong>,它为所有不可预见的情况(包括数组的排序区域或使用了比较器)提供了重载版本</p><p> binarySearch二分查找</p><p> 如果找到了搜索项,<strong>Arrays.binarySearch()</strong> 将生成一个大于或等于零的值。否则,它将产生一个负值,表示如果手动维护已排序的数组,则应该插入元素的位置。产生的值是 -(插入点) - 1 。插入点是大于键的第一个元素的索引,如果数组中的所有元素都小于指定的键,则是 <strong>a.size()</strong> 。</p><p> parallelPrefix并行前缀</p><p> 如前所述,使用流进行初始化非常优雅,但是对于大型数组,这种方法可能会耗尽堆空间。使用 <strong>setAll()</strong> 执行初始化更节省内存:</p><p> 因为正确使用 <strong>parallelPrefix()</strong> 可能相当复杂,所以通常应该只在存在内存或速度问题(或两者都有)时使用。否则,<strong>Stream.reduce()</strong> 应该是您的首选。</p><p> 在经历了这么多年的Java发展之后,我们可以很有趣地推测,如果重新开始,设计人员是否会将原生类型和低级数组放在该语言中(同样在JVM上运行的Scala语言不包括这些)。如果不考虑这些,就有可能开发出一种真正纯粹的面向对象语言(尽管有这样的说法,Java并不是一种纯粹的面向对象语言,这正是因为它的底层缺陷)。关于效率的最初争论总是令人信服的,但是随着时间的推移,我们已经看到了从这个想法向更高层次的组件(如集合)的演进。此外,如果集合可以像在某些语言中一样构建到核心语言中,那么编译器就有更好的机会进行优化。</p><p> </p><h2 id="枚举"><a href="#枚举" class="headerlink" title="枚举"></a>枚举</h2><p> </p><h2 id="注解"><a href="#注解" class="headerlink" title="注解"></a>注解</h2><h2 id="并发编程"><a href="#并发编程" class="headerlink" title="并发编程"></a>并发编程</h2><h2 id="设计模式"><a href="#设计模式" class="headerlink" title="设计模式"></a>设计模式</h2><p> </p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p> 类层次结构</p><p> 除了内存清理之外,所有的清理都不会自动发生</p><h2 id="Java-各个版本"><a href="#Java-各个版本" class="headerlink" title="Java 各个版本"></a>Java 各个版本</h2><h2 id="JDK源码"><a href="#JDK源码" class="headerlink" title="JDK源码"></a>JDK源码</h2><h2 id="JVM"><a href="#JVM" class="headerlink" title="JVM"></a>JVM</h2><h2 id="常见问题-1"><a href="#常见问题-1" class="headerlink" title="常见问题"></a>常见问题</h2><p><a href="https://tech.meituan.com/2015/02/26/serialization-vs-deserialization.html" target="_blank" rel="noopener">序列化与反序列化</a></p><p><a href="https://juejin.im/post/6844903848167866375" target="_blank" rel="noopener">Java8序列化</a></p><p><a href="https://dangdangdotcom.github.io/dubbox/serialization.html" target="_blank" rel="noopener">Dubbo序列化</a></p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p>[1] <a href="https://www.liaoxuefeng.com/wiki/1252599548343744" target="_blank" rel="noopener">廖雪峰java教程</a></p><p>[2] On java 8</p>]]></content>
<categories>
<category> Language </category>
</categories>
</entry>
<entry>
<title>算法与数据结构</title>
<link href="/51507.html"/>
<url>/51507.html</url>
<content type="html"><![CDATA[<h2 id="栈"><a href="#栈" class="headerlink" title="栈"></a>栈</h2><p> 问题类型:</p><p> 修改栈</p><p> 递归与栈</p><p> 树</p><p> 利用栈实现队列</p><p> 解法类型:</p><p> 单调栈</p><p> 双指针</p><p> 双栈</p><p> 观察并推导出数学公式</p><p> 语言自带工具类</p><p> </p><h2 id="队列"><a href="#队列" class="headerlink" title="队列"></a>队列</h2><p> 滑动窗口</p><h3 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h3><p><a href="https://juejin.im/post/6844903830199468039" target="_blank" rel="noopener">队列的实现方式及其应用</a></p><h2 id="双端队列"><a href="#双端队列" class="headerlink" title="双端队列"></a>双端队列</h2><p> 注意:Deque同时具有双端队列的性质</p><h2 id="堆"><a href="#堆" class="headerlink" title="堆"></a>堆</h2><p> 优先队列和堆</p><h2 id="优化算法的方法"><a href="#优化算法的方法" class="headerlink" title="优化算法的方法"></a>优化算法的方法</h2><p> 二分法??</p><h2 id="树"><a href="#树" class="headerlink" title="树"></a>树</h2><p>二叉树</p><p> L、D、R分别表示遍历左子树、访问根结点和遍历右子树</p><ul><li>先序(根)遍历:DLR</li><li>中序(根)遍历:LDR</li><li>后序(根)遍历:LRD</li></ul><blockquote><p>仅有前序和后序遍历,不能确定一个二叉树,必须有中序遍历的结果</p></blockquote><h3 id="二叉树的性质"><a href="#二叉树的性质" class="headerlink" title="二叉树的性质"></a>二叉树的性质</h3><ul><li><code>性质1</code>:在二叉树中第 i 层的结点数最多为 2^{i-1}2<em>i</em>−1 (i ≥ 1)</li><li><code>性质2</code>:高度为k的二叉树其结点总数最多为 2^{k}-12<em>k</em>-1 (k ≥ 1)</li><li><code>性质3</code>:对任意的非空二叉树 T ,如果叶结点的个数为n<em>0 ,而其度为 2 的结点数为n</em>2 ,则: n<em>0=</em>n2+1</li></ul><h3 id="满二叉树"><a href="#满二叉树" class="headerlink" title="满二叉树"></a>满二叉树</h3><p>深度为k,且有 2^k-1 个节点称之为 <strong>满二叉树</strong>;</p><ul><li><code>性质4</code>:第i层上的节点数为 2^{i-1} ;</li></ul><h3 id="完全二叉树"><a href="#完全二叉树" class="headerlink" title="完全二叉树"></a>完全二叉树</h3><p>深度为k,有n个节点的二叉树,当且仅当其每一个节点都与深度为k的满二叉树中,序号为1至n的节点对应时,称之为<code>完全二叉树</code>。</p><ul><li><code>性质5</code>:对于具有n个结点的完全二叉树的高度为 log_{2}^{n}+1</li></ul><h3 id="二叉树的构造"><a href="#二叉树的构造" class="headerlink" title="二叉树的构造"></a>二叉树的构造</h3><pre><code class="C">//n 表示当前结点字符Node* tree(vector<char> data, int n) { Node* node; if (n >= data.size()) return NULL; if (data[n] == '#') return NULL; node = new Node; node->data = data[n]; node->left = tree(data, n + 1); node->right = tree(data, n + 2); return node;}</code></pre><h2 id="堆-1"><a href="#堆-1" class="headerlink" title="堆"></a>堆</h2><p>堆通常是一个可以被看做一棵树的数组对象。堆的实现通过构造二叉堆(binary heap),实为二叉树的一种;</p><ul><li>任意节点小于(或大于)它的所有后裔,最小元(或最大元)在堆的根上(堆序性)。</li><li><strong>堆总是一棵完全树</strong>。即除了最底层,其他层的节点都被元素填满,且最底层尽可能地从左到右填入。</li></ul><p>将根节点最大的堆叫做<code>最大堆</code>或大根堆,根节点最小的堆叫做<code>最小堆</code>或小根堆。常见的堆有二叉堆、斐波那契堆等。</p><p>通常堆是通过一维数组来实现的。在数组起始位置为1的情形中:</p><ul><li>父节点i的左子节点在位置 2×<em>i</em> ;</li><li>父节点i的右子节点在位置 2×<em>i</em>+1 ;</li><li>子节点i的父节点在位置 i÷2 ;</li></ul><h2 id="霍夫曼树"><a href="#霍夫曼树" class="headerlink" title="霍夫曼树"></a>霍夫曼树</h2><p>霍夫曼树又称最优二叉树,<strong>是一种带权路径长度最短的二叉树</strong>。所谓树的带权路径长度,就是树中所有的叶结点的权值乘上其到根结点的路径长度(若根结点为0层,叶结点到根结点的路径长度为叶结点的层数)。<strong>树的路径长度是从树根到每一结点的路径长度之和</strong>,记为 WPL=W1\times L1+W2\times L2+W3\times L3+…+Wn\times Ln<em>W<strong>P</strong>L</em>=<em>W</em>1×<em>L</em>1+<em>W</em>2×<em>L</em>2+<em>W</em>3×<em>L</em>3+…+<em>W<strong>n<em>×</em>L</strong>n</em> ,N个权值Wi(i=1,2,…n)构成一棵有N个叶结点的二叉树,相应的叶结点的路径长度为Li(i=1,2,…n)。<strong>可以证明霍夫曼树的WPL是最小的</strong>。</p><h3 id="霍夫曼树构造"><a href="#霍夫曼树构造" class="headerlink" title="霍夫曼树构造"></a>霍夫曼树构造</h3><ol><li>根据给定的n个权值<code>(W1,W2...Wn)</code>,使对应节点构成n个二叉树的森林<code>T=(T1,T2...Tn)</code>,其中每个二叉树<code>Ti(1 <= i <= n)</code>中都有一个带权值为Wi的根节点,其左、右子树均为空。</li><li>在森林T中选取两个节点权值最小的子树,分别作为左、右子树构造一个新的二叉树,且置新的二叉树的根节点的权值为其左右子树上根节点权值之和。</li><li>在森林T中,用新得到的二叉树替代选取的两个二叉树。</li><li>重复2和3,直到T只包含一个树为止。这个数就是霍夫曼树。</li></ol><blockquote><p>定理:对于具有n个叶子节点的霍夫曼树,共有<code>2n-1</code>个节点。这是由于霍夫曼树只有度为0和度为2的结点,根据二叉树的性质 <code>n0 = n2 + 1</code>,因此度为2的结点个数为<code>n-1</code>个,总共有<code>2n-1</code>个节点。</p></blockquote><h3 id="霍夫曼编码"><a href="#霍夫曼编码" class="headerlink" title="霍夫曼编码"></a>霍夫曼编码</h3><p>对于一个霍夫曼树,所有左链接取’0’、右链接取’1’。从树根至树叶依序记录所有字母的编码。</p><h3 id="带权路径"><a href="#带权路径" class="headerlink" title="带权路径"></a>带权路径</h3><ul><li><code>结点的权</code>:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。</li><li><code>结点的带权路径</code>:从根结点到该结点之间的路径长度与该结点的权的乘积。</li><li><code>树的带权路径</code>:所有叶子结点的带权路径长度之和,记为<code>WPL</code>。</li></ul><h2 id="平衡二叉树"><a href="#平衡二叉树" class="headerlink" title="平衡二叉树"></a>平衡二叉树</h2><p> 一般的二叉查找树的查询复杂度是跟目标结点到树根的距离(即深度)有关,因此当结点的深度普遍较大时,查询的均摊复杂度会上升,为了更高效的查询,平衡树应运而生了。<strong>平衡指所有叶子的深度趋于平衡,更广义的是指在树上所有可能查找的均摊复杂度偏低。</strong></p><p> AVL树是最先发明的 <strong>自平衡二叉查找树</strong>。在AVL树中任何节点的两个子树的高度最大差别为一,所以它也被称为高度平衡树。</p><ul><li>它的左子树和右子树都是平衡二叉树。</li><li>左子树和右子树的深度之差的绝对值不超过1。</li></ul><p>增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。</p><ul><li>右旋:左结点转到根节点位置。</li><li>左旋:右节点转到根节点位置。</li></ul><blockquote><p>高度为<code>k</code>的AVL树,节点数N最多<code>2^k -1</code>,即满二叉树;</p></blockquote><p>红黑树</p><p> 红黑树是一种自平衡二叉查找树,每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:</p><ul><li>节点是红色或黑色。</li><li>根是黑色。</li><li>所有叶子都是黑色(叶子是NIL节点)。</li><li>每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)</li><li>从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。</li></ul><p> <img src="https://hadyang.github.io/interview/docs/basic/algo/tree/images/red_black_tree.png" alt="img"></p><p> 红黑树相对于AVL树来说,牺牲了部分平衡性以换取插入/删除操作时少量的旋转操作,整体来说性能要优于AVL树。</p><p> 这些约束确保了红黑树的关键特性:<strong>从根到叶子的最长的可能路径不多于最短的可能路径的两倍长</strong>。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限 <strong>允许红黑树在最坏情况下都是高效的</strong>,而不同于普通的二叉查找树。</p><p>在很多树数据结构的表示中,一个节点有可能只有一个子节点,而叶子节点包含数据。用这种范例表示红黑树是可能的,但是这会改变一些性质并使算法复杂。为此,本文中我们使用”nil叶子”或”空(null)叶子”,如上图所示,它不包含数据而只充当树在此结束的指示。<strong>这些节点在绘图中经常被省略,导致了这些树好像同上述原则相矛盾,而实际上不是这样</strong>。与此有关的结论是所有节点都有两个子节点,尽管其中的一个或两个可能是空叶子。</p><p>因为每一个红黑树也是一个特化的二叉查找树,因此红黑树上的只读操作与普通二叉查找树上的只读操作相同。然而,在红黑树上进行插入操作和删除操作会导致不再符合红黑树的性质。<strong>恢复红黑树的性质需要少量(O(log n))的颜色变更(实际是非常快速的)和不超过三次树旋转(对于插入操作是两次)</strong>。虽然插入和删除很复杂,但操作时间仍可以保持为O(log n)次。</p><h2 id="B树"><a href="#B树" class="headerlink" title="B树"></a>B树</h2><p>B树是一种自平衡的树,能够保持数据有序。这种数据结构能够让查找数据、顺序访问、插入数据及删除的动作,复杂度均为 O(n)<em>O</em>(<em>n</em>) 。总的来说,B树是一个泛化的二叉查找树,一个节点可以拥有两个以上的子节点。但其与自平衡二叉查找树不同,B树更适合大数据块的存储系统,例如:磁盘。</p><p>在B树中,内部(非叶子)节点可以拥有可变数量的子节点(数量范围预先定义好)。当数据被插入或从一个节点中移除,它的子节点数量发生变化。为了维持在预先设定的数量范围内,内部节点可能会被 <strong>合并</strong> 或者 <strong>分离</strong>。因为子节点数量有一定的允许范围,所以B树不需要像其他自平衡查找树那样频繁地重新保持平衡,但是由于节点 <strong>没有被完全填充</strong>,可能浪费了一些空间。子节点数量的上界和下界依特定的实现而设置。例如,在一个2-3 B树(通常简称2-3树),每一个内部节点只能有 2 或 3 个子节点。</p><p>根据 Knuth 的定义,一个 m 阶的B树是一个有以下属性的树:</p><ul><li>每一个节点最多有 m 个子节点</li><li>每一个非叶子节点(除根节点)最少有 m\div 2<em>m</em>÷2 个子节点</li><li>如果根节点不是叶子节点,那么它至少有两个子节点</li><li>有 k 个子节点的非叶子节点拥有 k − 1 个键</li><li>所有的叶子节点都在同一层</li></ul><p>每一个内部节点的键将节点的子树分开。例如,如果一个内部节点有 3 个子节点(子树),那么它就必须有两个键: a1 和 a2 。左边子树的所有值都必须小于 a1 ,中间子树的所有值都必须在 a1 和a2 之间,右边子树的所有值都必须大于 a2 。</p><p>B树内的节点可分为三类:</p><ul><li>内部节点:内部节点是除叶子节点和根节点之外的所有节点。它们通常被表示为一组有序的元素和指向子节点的指针。</li><li>根节点:根节点拥有的子节点数量的上限和内部节点相同,但是没有下限。</li><li>叶子节点:叶子节点对元素的数量有相同的限制,但是没有子节点,也没有指向子节点的指针。</li></ul><p><img src="https://hadyang.github.io/interview/docs/basic/algo/tree/images/b.png" alt="img"></p><h3 id="B树的查找"><a href="#B树的查找" class="headerlink" title="B树的查找"></a>B树的查找</h3><p>在B树中的查找给定关键字的方法 <strong>类似于二叉排序树上的查找,不同的是在每个节点上确定向下查找的路径不一定是二路的,而是n+1路的</strong>。因为节点内的关键字序列key[1..n]有序,故既可以使用顺序查找,也可以使用二分查找。在一棵B树上查找关键字为k的方法为:将k与根节点中的key[i]进行比较:</p><ol><li>若k=key[i],则查找成功;</li><li>若k<key[1],则沿指针ptr[0]所指的子树继续查找;</li><li>若key[i]<k<key[i+1],则沿着指针ptr[i]所指的子树继续查找;</li><li>若k>key[n],则沿着指针ptr[n]所指的子树继续查找。</li></ol><h3 id="B树的插入"><a href="#B树的插入" class="headerlink" title="B树的插入"></a>B树的插入</h3><p>将关键字k插入到B树的过程分两步完成:</p><ol><li>利用B树的查找算法查找出该关键字的插入节点(注意B树的插入节点一定属于最低非叶子节点层)。</li><li>判断该节点是否还有空位,即判断该节点是否满足n < m-1,若满足:直接把关键字k插入到该节点合适位置上;若不满足:分裂节点,取一新节点,把原节点上的关键字和k按升序排列后,从中间位置(m/2)处把关键字(不包括中间位置的关键字)分成两部分,左部分所含关键字放在旧节点中,右部分关键字放在新节点中,中间位置的关键字连同新节点的存储位置插入到双亲节点。如果双亲节点的关键字个数也超出max则再分裂。</li></ol><h3 id="B树的删除"><a href="#B树的删除" class="headerlink" title="B树的删除"></a>B树的删除</h3><p>首先查找B树中需删除的元素,如果该元素在B树中存在,则将该元素在其结点中进行删除;如果删除该元素后,首先判断该元素是否有左右孩子结点,如果有,则上移孩子结点中的某相近元素到父节点中,然后是移动之后的情况;如果没有,直接删除后,然后是移动之后的情况。</p><p>删除元素,移动相应元素之后,如果某结点中元素数目(即关键字数)小于Min(m/2)-1,则需要看其某相邻兄弟结点是否丰满,如果丰满,则向父节点借一个元素来满足条件;如果其相邻兄弟都刚脱贫,即借了之后其结点数目小于Min(m/2)-1,则该结点与其相邻的某一兄弟结点进行“合并”成一个结点,</p><h2 id="B-树"><a href="#B-树" class="headerlink" title="B+树"></a>B+树</h2><p><img src="https://hadyang.github.io/interview/docs/basic/algo/tree/images/b+.png" alt="img"></p><p>B+ 树是 B 树的变体,也是一种多路搜索树。m阶的 B+ 树和 B 树的主要差异如下:</p><ul><li>在B+树中,<strong>具有n个关键字的节点含有n个子树</strong>,即每个关键字对应一个子树,而在B树中,具有n个关键字的节点含有(n+1)个子树。</li><li>在B+树中,每个节点(除根节点外)中的关键字个数n的取值范围是[m/2] <= n <= m,根节点n的取值范围2 <=n <=m;而在B树中,除根节点外,其他所有非叶子节点的关键字个数:[m/2]-1 <= n <= m-1,根节点关键字个数为1 <= n <= m-1</li><li><strong>B+树中所有叶子节点包含了全部关键字</strong>,即其他非叶子节点中的关键字包含在叶子节点中,而在B树中,关键字是不重复的。</li><li><strong>B+树中所有非叶子节点仅起到索引的作用</strong>,即节点中每个索引项值含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。而在B树中,每个关键字对应一个记录的存储地址。</li><li><strong>在 B+ 树所有叶子节点链接成一个不定长的线性表</strong>。</li></ul><h3 id="B-树的查找"><a href="#B-树的查找" class="headerlink" title="B+树的查找"></a>B+树的查找</h3><p>在B+树中可以采用两种查找方式:</p><ul><li>直接从最小关键字开始顺序查找。</li><li>从B+树的根节点开始随机查找。这种查找方式与B树的查找方式类似,只是在分支节点上的关键字与查找值相等时,查找并不会结束,要继续查到叶子节点为止,此时若查找成功,则按所给指针取出对应元素。</li></ul><p>在B+树中,不管查找是否成功,<strong>每次查找都是经历一条树从根节点到叶子节点的路径</strong>。</p><h3 id="B-树的插入"><a href="#B-树的插入" class="headerlink" title="B+树的插入"></a>B+树的插入</h3><ol><li>首先,查找要插入其中的节点的位置。接着把值插入这个节点中。</li><li>如果没有节点处于违规状态则处理结束。</li><li>如果某个节点有过多元素,则把它分裂为两个节点,每个都有最小数目的元素。在树上递归向上继续这个处理直到到达根节点,如果根节点被分裂,则创建一个新根节点。为了使它工作,元素的最小和最大数目典型的必须选择为使最小数不小于最大数的一半。</li></ol><h3 id="B-树的删除"><a href="#B-树的删除" class="headerlink" title="B+树的删除"></a>B+树的删除</h3><ol><li>首先,查找要删除的值。接着从包含它的节点中删除这个值。</li><li>如果没有节点处于违规状态则处理结束。</li><li>如果节点处于违规状态则有两种可能情况:<ul><li>它的兄弟节点,可以把一个或多个它的子节点转移到当前节点,而把它返回为合法状态。如果是这样,在更改父节点和两个兄弟节点的分离值之后处理结束。</li><li>它的兄弟节点由于处在低边界上而没有额外的子节点。在这种情况下把两个兄弟节点合并到一个单一的节点中,而且我们递归到父节点上,因为它被删除了一个子节点。持续这个处理直到当前节点是合法状态或者到达根节点,在其上根节点的子节点被合并而且合并后的节点成为新的根节点。</li></ul></li></ol><h3 id="B-树的优势所在"><a href="#B-树的优势所在" class="headerlink" title="B+树的优势所在"></a>B+树的优势所在</h3><p>为什么说B+树比B树更适合实际应用中操作系统的文件索引和数据库索引?</p><ol><li>B+树的中间节点能存储更多指针</li><li><strong>B+树的查询效率更加稳定</strong>:关键字查询的路径长度相同</li><li><strong>减少回溯</strong>:由于B+树中叶子节点存在指针,所以在范围查找时不需要回溯到父节点,直接类型链表遍历即可,减少IO</li></ol><h2 id="Trie树"><a href="#Trie树" class="headerlink" title="Trie树"></a>Trie树</h2><p><code>Trie树</code>,又称前缀树,<code>字典树</code>, 是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。<strong>一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串</strong>。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。</p><p><strong>Trie树查询和插入时间复杂度都是 O(n),是一种以空间换时间的方法。当节点树较多的时候,Trie 树占用的内存会很大</strong>。</p><p>Trie树常用于搜索提示。如当输入一个网址,可以自动搜索出可能的选择。当没有完全匹配的搜索结果,可以返回前缀最相似的可能。 </p><h2 id="Hash"><a href="#Hash" class="headerlink" title="Hash"></a>Hash</h2><p>哈希表(Hash Table,也叫散列表),是根据关键码值 (Key-Value) 而直接进行访问的数据结构。也就是说,<strong>它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度</strong>。哈希表的实现主要需要解决两个问题,哈希函数和冲突解决。</p><h3 id="哈希函数"><a href="#哈希函数" class="headerlink" title="哈希函数"></a>哈希函数</h3><p>哈希函数也叫散列函数,它对不同的输出值得到一个固定长度的消息摘要。理想的哈希函数对于不同的输入应该产生不同的结构,<strong>同时散列结果应当具有同一性(输出值尽量均匀)和雪崩效应(微小的输入值变化使得输出值发生巨大的变化)</strong>。</p><h3 id="冲突解决"><a href="#冲突解决" class="headerlink" title="冲突解决"></a>冲突解决</h3><ul><li><p><code>开放地址法</code>:</p><p>以发生冲突的哈希地址为输入,通过某种哈希冲突函数得到一个新的空闲的哈希地址的方法有以下几种方式:</p></li><li><p><code>线性探查法</code>:从发生冲突的地址开始,依次探查下一个地址,直到找到一个空闲单元。</p></li><li><p><code>平方探查法</code>:设冲突地址为d0,则探查序列为:d0+1^2,d0-1^2,d0+2^2…</p></li></ul><ul><li><code>拉链法</code>:把所有的同义词用单链表链接起来。在这种方法下,哈希表每个单元中存放的不再是元素本身,而是相应同义词单链表的头指针。<code>HashMap</code>就是使用这种方法解决冲突的。</li></ul><p><img src="https://hadyang.github.io/interview/docs/basic/algo/hash/images/hashmap-structure.png" alt="img"></p><h2 id="最小生成树算法"><a href="#最小生成树算法" class="headerlink" title="最小生成树算法"></a>最小生成树算法</h2><ul><li><code>连通图</code>:在无向图G中,若从顶点i到顶点j有路径,则称顶点i和顶点j是连通的。若图G中任意两个顶点都连通,则称G为连通图。</li><li><code>生成树</code>:一个连通图的生成树是该连通图的一个极小连通子图,它含有全部顶点,但只有构成一个数的<code>(n-1)</code>条边。</li><li><code>最小生成树</code>:对于一个带权连通无向图G中的不同生成树,各树的边上的 <strong>权值之和最小</strong>。构造最小生成树的准则有三条:<ul><li>必须只使用该图中的边来构造最小生成树。</li><li>必须使用且仅使用<code>(n-1)</code>条边来连接图中的n个顶点。</li><li>不能使用产生回路的边。</li></ul></li></ul><h3 id="Prim算法"><a href="#Prim算法" class="headerlink" title="Prim算法"></a>Prim算法</h3><p>假设G=(V,E)是一个具有n个顶点的带权连通无向图,T(U,TE)是G的最小生成树,其中U是T的顶点集,TE是T的边集,则由G构造从起始顶点v出发的最小生成树T的步骤为:</p><ul><li>初始化U={v},以v到其他顶点的所有边为候选边(U中所有点到其他顶点的边)。</li><li>重复以下步骤(n-1)次,使得其他(n-1)个顶点被加入到U中。<ul><li>从候选边中挑选权值最小的边加入TE,设该边在<code>V-U</code>(这里是集合减)中的顶点是k,将k加入U中。</li><li>考察当前V-U中的所有顶点j,修改候选边,若边(k,j)的权值小于原来和顶点j关联的候选边,则用(k,j)取代后者作为候选边。</li></ul></li></ul><p><img src="https://hadyang.github.io/interview/docs/basic/algo/mst/images/prim.jpg" alt="img"></p><h3 id="Kruskal算法"><a href="#Kruskal算法" class="headerlink" title="Kruskal算法"></a>Kruskal算法</h3><p>假设G=(V,E)是一个具有n个顶点的带权连通无向图,T(U,TE)是G的最小生成树,其中U是T的顶点集,TE是T的边集,则由G构造从起始顶点v出发的最小生成树T的步骤为:</p><ul><li>置U的初始值等于V(即包含G中的全部顶点),TE的初始值为空</li><li>将图G中的边按权值从小到大的顺序依次选取,若选取的边未使生成树T形成回路,则加入TE,否则放弃,知道TE中包含(n-1)条边为止。</li></ul><h1 id="最短路径算法"><a href="#最短路径算法" class="headerlink" title="最短路径算法"></a>最短路径算法</h1><h2 id="Dijkstra-——-贪心算法"><a href="#Dijkstra-——-贪心算法" class="headerlink" title="Dijkstra —— 贪心算法"></a>Dijkstra —— 贪心算法</h2><blockquote><p>从一个顶点到其余顶点的最短路径</p></blockquote><p>设<code>G=(V,E)</code>是一个带权有向图,把图中顶点集合V分成两组,第1组为已求出最短路径的顶点(用S表示,初始时S只有一个源点,以后每求得一条最短路径<code>v,...k</code>,就将k加到集合S中,直到全部顶点都加入S)。第2组为其余未确定最短路径的顶点集合(用U表示),按最短路径长度的递增次序把第2组的顶点加入S中。</p><pre><code>步骤:1. 初始时,S只包含源点,即`S={v}`,顶点v到自己的距离为0。U包含除v外的其他顶点,v到U中顶点i的距离为边上的权。2. 从U中选取一个顶点u,顶点v到u的距离最小,然后把顶点u加入S中。3. 以顶点u为新考虑的中间点,修改v到U中各个点的距离。4. 重复以上步骤知道S包含所有顶点。</code></pre><h2 id="Floyd-——-动态规划"><a href="#Floyd-——-动态规划" class="headerlink" title="Floyd —— 动态规划"></a>Floyd —— 动态规划</h2><p>Floyd 算法是解决任意两点间的最短路径的一种算法,可以正确处理有向图或负权(但不可存在负权回路)的最短路径问题。该算法的时间复杂度为 O(N^{3})<em>O</em>(<em>N</em>3) ,空间复杂度为 O(N^{2})<em>O</em>(<em>N</em>2)</p><p>设 D_{i,j,k}<em>D**i</em>,<em>j</em>,<em>k</em> 为从 i<em>i</em> 到 j<em>j</em> 的只以 (1..k)(1..<em>k</em>) 集合中的节点为中间节点的最短路径的长度。</p><p>$$ D_{i,j,k}=\begin{cases} D_{i,j,k-1} & 最短路径不经过 k<br>D_{i,k,k-1}+D_{k,j,k-1} & 最短路径经过 k \end{cases} $$</p><p>因此, D_{i,j,k}=min(D_{i,k,k-1}+D_{k,j,k-1},D_{i,j,k-1})<em>D**i</em>,<em>j</em>,<em>k</em>=<em>m<strong>i</strong>n</em>(<em>D**i</em>,<em>k</em>,<em>k</em>−1+<em>D**k</em>,<em>j</em>,<em>k</em>−1,<em>D**i</em>,<em>j</em>,<em>k</em>−1) 。伪代码描述如下:</p><pre><code>// let dist be a |V| × |V| array of minimum distances initialized to ∞ (infinity) for each vertex v dist[v][v] ← 0 for each edge (u,v) dist[u][v] ← w(u,v) // the weight of the edge (u,v) for k from 1 to |V| for i from 1 to |V| for j from 1 to |V| if dist[i][j] > dist[i][k] + dist[k][j] dist[i][j] ← dist[i][k] + dist[k][j] end if</code></pre><h1 id="KMP算法"><a href="#KMP算法" class="headerlink" title="KMP算法"></a><a href="https://zh.wikipedia.org/wiki/克努斯-莫里斯-普拉特算法" target="_blank" rel="noopener">KMP算法</a></h1><p>KMP算法解决的问题是字符匹配,这个算法把字符匹配的时间复杂度缩小到<code>O(m+n)</code>,而空间复杂度也只有O(m),n是target的长度,m是pattern的长度。</p><ul><li>部分匹配表(Next数组):表的作用是 <strong>让算法无需多次匹配S中的任何字符</strong>。能够实现线性时间搜索的关键是 <strong>在不错过任何潜在匹配的情况下,我们”预搜索”这个模式串本身并将其译成一个包含所有可能失配的位置对应可以绕过最多无效字符的列表</strong>。</li><li>Next数组(前缀和前缀的比较):t为模式串,j为下标<ul><li><code>Next[0] = -1</code></li><li><code>Next[j] = MAX{ k | 0 < k < j | " t0 t1 ... tk " = "t ( j-k ) t ( j-k+1 ) ... t( j-1 )" }</code></li></ul></li></ul><p>|i| 0| 1| 2| 3| 4| 5 |6| |–| | t[i]| A| B| C| D| A| B| D| |next[i]| -1| 0 |0 |0 |0 |1 |2|</p><ul><li>NextVal数组:是一种优化后的Next数组,是为了解决类似<code>aaaab</code>这种模式串的匹配,减少重复的比较。 如果<code>t[next[j]]=t[j]</code>:<code>nextval[j]=nextval[next[j]]</code>,否则<code>nextval[j]=next[j]</code>。</li></ul><p>|i| 0| 1| 2| 3| 4| 5 |6| |–| | t | a| b| c| a| b| a |a| |next[j] | -1| 0 |0 |0 |1 |2 |1| |nextval[j] | -1| 0 |0 |-1 |0 |2 |1|</p><p>在上面的表格中,<code>t[next[4]]=t[4]=b</code>,所以<code>nextval[4]=nextval[next[4]]=0</code></p><h1 id="查找算法"><a href="#查找算法" class="headerlink" title="查找算法"></a>查找算法</h1><h2 id="ASL"><a href="#ASL" class="headerlink" title="ASL"></a>ASL</h2><p>由于查找算法的主要运算是关键字的比较,所以通常把查找过程中对关键字的平均比较次数(平均查找长度)作为衡量一个查找算法效率的标准。<code>ASL= ∑(n,i=1) Pi*Ci</code>,其中<code>n</code>为元素个数,<code>Pi</code>是查找第<code>i</code>个元素的概率,一般为<code>Pi=1/n</code>,<code>Ci</code>是找到第<code>i</code>个元素所需比较的次数。</p><h2 id="顺序查找"><a href="#顺序查找" class="headerlink" title="顺序查找"></a>顺序查找</h2><p>原理是让关键字与队列中的数从最后一个开始逐个比较,直到找出与给定关键字相同的数为止,它的缺点是效率低下。<strong>时间复杂度o(n)</strong>。</p><h2 id="折半查找"><a href="#折半查找" class="headerlink" title="折半查找"></a>折半查找</h2><p><strong>折半查找要求线性表是有序表</strong>。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。<strong>折半搜索每次把搜索区域减少一半,时间复杂度为O(log n)。</strong></p><ul><li><strong>可以借助二叉判定树求得折半查找的平均查找长度</strong>:<code>log2(n+1)-1</code>。</li><li>折半查找在失败时所需比较的关键字个数不超过判定树的深度,n个元素的判定树的深度和n个元素的完全二叉树的深度相同<code>log2(n)+1</code>。</li></ul><pre><code>public int binarySearchStandard(int[] num, int target){ int start = 0; int end = num.length - 1; while(start <= end){ //注意1 int mid = start + ((end - start) >> 1); if(num[mid] == target) return mid; else if(num[mid] > target){ end = mid - 1; //注意2 } else{ start = mid + 1; //注意3 } } return -1;}</code></pre><ul><li>如果是start < end,那么当target等于num[num.length-1]时,会找不到该值。</li><li>因为num[mid] > target, 所以如果有num[index] == target, index一定小于mid,能不能写成end = mid呢?举例来说:num = {1, 2, 5, 7, 9}; 如果写成end = mid,当循环到start = 0, end = 0时(即num[start] = 1, num[end] = 1时),mid将永远等于0,此时end也将永远等于0,陷入死循环。也就是说寻找target = -2时,程序将死循环。</li><li>因为num[mid] < target, 所以如果有num[index] == target, index一定大于mid,能不能写成start = mid呢?举例来说:num = {1, 2, 5, 7, 9}; 如果写成start = mid,当循环到start = 3, end = 4时(即num[start] = 7, num[end] = 9时),mid将永远等于3,此时start也将永远等于3,陷入死循环。也就是说寻找target = 9时,程序将死循环。</li></ul><h2 id="分块查找"><a href="#分块查找" class="headerlink" title="分块查找"></a>分块查找</h2><p>分块查找又称索引顺序查找,它是一种性能介于顺序查找和折半查找之间的查找方法。<strong>分块查找由于只要求索引表是有序的,对块内节点没有排序要求,因此特别适合于节点动态变化的情况</strong>。</p><h1 id="排序算法"><a href="#排序算法" class="headerlink" title="排序算法"></a>排序算法</h1><h2 id="常见排序算法"><a href="#常见排序算法" class="headerlink" title="常见排序算法"></a><a href="https://www.cnblogs.com/onepixel/articles/7674659.html" target="_blank" rel="noopener">常见排序算法</a></h2><p><img src="/51507/ways_classfication.png" alt="排序算法分类"></p><p><img src="/51507/ways_of_sort.png" alt="排序算法比较"></p><h3 id="稳定排序:"><a href="#稳定排序:" class="headerlink" title="稳定排序:"></a>稳定排序:</h3><ul><li><code>冒泡排序</code> — O(n²)</li><li><code>插入排序</code> — O(n²)</li><li><code>桶排序</code> — O(n); 需要 O(k) 额外空间</li><li><code>归并排序</code> — O(nlogn); 需要 O(n) 额外空间</li><li><code>二叉排序树排序</code> — O(n log n) 期望时间; O(n²)最坏时间; 需要 O(n) 额外空间</li><li><code>基数排序</code> — O(n·k); 需要 O(n) 额外空间</li></ul><h3 id="不稳定排序"><a href="#不稳定排序" class="headerlink" title="不稳定排序"></a>不稳定排序</h3><ul><li><code>选择排序</code> — O(n²)</li><li><code>希尔排序</code> — O(nlogn)</li><li><code>堆排序</code> — O(nlogn)</li><li><code>快速排序</code> — O(nlogn) 期望时间, O(n²) 最坏情况; 对于大的、乱数串行一般相信是最快的已知排序</li></ul><h2 id="交换排序"><a href="#交换排序" class="headerlink" title="交换排序"></a>交换排序</h2><h3 id="冒泡排序"><a href="#冒泡排序" class="headerlink" title="冒泡排序"></a>冒泡排序</h3><p>它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。<strong>冒泡排序总的平均时间复杂度为O(n^2)。冒泡排序是一种稳定排序算法。</strong></p><ul><li>比较相邻的元素。如果第一个比第二个大,就交换他们两个。</li><li>对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。</li><li>针对所有的元素重复以上的步骤,除了最后一个。</li><li>持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。</li></ul><pre><code class="c++">void bubble_sort(int a[], int n){ int i, j, temp; for (j = 0; j < n - 1; j++) for (i = 0; i < n - 1 - j; i++) { if(a[i] > a[i + 1]) { temp = a[i]; a[i] = a[i + 1]; a[i + 1] = temp; } }}</code></pre><h3 id="快速排序-快速排序-百度百科"><a href="#快速排序-快速排序-百度百科" class="headerlink" title="快速排序 快速排序-百度百科"></a>快速排序 <a href="http://baike.baidu.com/link?url=hyQPClbJy1SYY4esOZe9kANDIDxOrKxiSfq0HZl8c5eut40dZS-fd1V0jubijSv7RAogwy6HaQ-B1HbRgHf1hq" target="_blank" rel="noopener">快速排序-百度百科</a></h3><p>快速排序是一种 <strong>不稳定</strong> 的排序算法,平均时间复杂度为 <strong>O(nlogn)</strong>。<strong>快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。</strong> 步骤为:</p><ul><li>从数列中挑出一个元素,称为”基准”(pivot),</li><li>重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。</li><li>递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。</li></ul><blockquote><p>快排的时间花费主要在划分上,所以</p><ul><li>最坏情况:时间复杂度为<code>O(n^2)</code>。因为最坏情况发生在每次划分过程产生的两个区间分别包含<code>n-1</code>个元素和<code>1</code>个元素的时候。</li><li>最好情况:每次划分选取的基准都是当前无序区的中值。如果每次划分过程产生的区间大小都为n/2,则快速排序法运行就快得多了。</li></ul></blockquote><pre><code class="java">public void sort(int[] arr, int low, int high) { int l = low; int h = high; int povit = arr[low]; while (l < h) { while (l < h && arr[h] >= povit) h--; if (l < h) { arr[l] = arr[h]; l++; } while (l < h && arr[l] <= povit) l++; if (l < h) { arr[h] = arr[l]; h--; } } arr[l] = povit; System.out.print("l=" + (l + 1) + ";h=" + (h + 1) + ";povit=" + povit + "\n"); System.out.println(Arrays.toString(arr)); if (l - 1 > low) sort(arr, low, l - 1); if (h + 1 < high) sort(arr, h + 1, high);}</code></pre><h4 id="快排的优化"><a href="#快排的优化" class="headerlink" title="快排的优化"></a>快排的优化</h4><ol><li>当待排序序列的长度分割到一定大小后,使用插入排序。</li><li>快排函数在函数尾部有两次递归操作,我们可以对其使用尾递归优化。优化后,可以缩减堆栈深度,由原来的O(n)缩减为O(logn),将会提高性能。</li><li>从左、中、右三个数中取中间值。</li></ol><h2 id="插入排序"><a href="#插入排序" class="headerlink" title="插入排序"></a>插入排序</h2><h3 id="直接插入排序"><a href="#直接插入排序" class="headerlink" title="直接插入排序"></a>直接插入排序</h3><p>插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序,<strong>时间复杂度为O(n^2)。是稳定的排序方法。</strong> <strong>插入算法把要排序的数组分成两部分</strong>:第一部分包含了这个数组的所有元素,但将最后一个元素除外(让数组多一个空间才有插入的位置),而第二部分就只包含这一个元素(即待插入元素)。在第一部分排序完成后,再将这个最后元素插入到已排好序的第一部分中。</p><pre><code class="c++">void insert_sort(int* a, int len) { for (int i = 1; i < len; ++i) { int j = i - 1; int temp = a[i]; while (j >= 0 && temp < a[j]) { a[j + 1] = a[j]; j--; } a[j + 1] = temp; }}</code></pre><h3 id="希尔排序"><a href="#希尔排序" class="headerlink" title="希尔排序"></a><a href="https://zh.wikipedia.org/wiki/希尔排序" target="_blank" rel="noopener">希尔排序</a></h3><p>也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。<strong>希尔排序是非稳定排序算法。</strong></p><p>希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。</p><pre><code class="c++">void shell_sort(int* a, int len) { int step = len / 2; int temp; while (step > 0) { for (int i = step; i < len; ++i) { temp = a[i]; int j = i - step; while (j >= 0 && temp < a[j]) { a[j + step] = a[j]; j -= step; } a[j + step] = temp; } step /= 2; }}</code></pre><h2 id="选择排序"><a href="#选择排序" class="headerlink" title="选择排序"></a>选择排序</h2><h3 id="直接选择排序"><a href="#直接选择排序" class="headerlink" title="直接选择排序"></a>直接选择排序</h3><p>首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。<strong>实际适用的场合非常罕见。</strong></p><pre><code class="c++">void selection_sort(int arr[], int len) { int i, j, min, temp; for (i = 0; i < len - 1; i++) { min = i; for (j = i + 1; j < len; j++) if (arr[min] > arr[j]) min = j; temp = arr[min]; arr[min] = arr[i]; arr[i] = temp; }}</code></pre><h3 id="堆排序"><a href="#堆排序" class="headerlink" title="堆排序"></a>堆排序</h3><p>堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得在当前无序区中选取最大(或最小)关键字的记录变得简单。</p><ol><li>将数组分为有序区和无序区,在无序区中建立最大堆</li><li>将堆顶的数据与无序区末尾的数据交换</li><li>从后往前,直到所有数据排序完成</li></ol><pre><code class="java">public void heapSort(int[] nums) { for (int i = nums.length - 1; i >= 0; i--) { maxHeap(nums, 0, i); swap(nums, 0, i); }}public void maxHeap(int[] heap, int start, int end) { if (start == end) { return; } int parent = start; int childLeft = start * 2 + 1; int childRight = childLeft + 1; if (childLeft <= end) { maxHeap(heap, childLeft, end); if (heap[childLeft] > heap[parent]) { swap(heap, parent, childLeft); } } if (childRight <= end) { maxHeap(heap, childRight, end); if (heap[childRight] > heap[parent]) { swap(heap, parent, childRight); } }}private void swap(int[] nums, int a, int b) { int t = nums[a]; nums[a] = nums[b]; nums[b] = t;}</code></pre><h2 id="归并排序"><a href="#归并排序" class="headerlink" title="归并排序"></a>归并排序</h2><p>归并排序采用分治的思想:</p><ul><li>Divide:将n个元素平均划分为各含n/2个元素的子序列;</li><li>Conquer:递归的解决俩个规模为n/2的子问题;</li><li>Combine:合并俩个已排序的子序列。</li></ul><p>性能:时间复杂度总是为O(NlogN),空间复杂度也总为为O(N),算法与初始序列无关,排序是稳定的。</p><pre><code class="java">public void mergeSort(int[] array, int start, int end, int[] temp) { if (start >= end) { return; } int mid = (start + end) / 2; mergeSort(array, start, mid, temp); mergeSort(array, mid + 1, end, temp); int f = start, s = mid + 1; int t = 0; while (f <= mid && s <= end) { if (array[f] < array[s]) { temp[t++] = array[f++]; } else { temp[t++] = array[s++]; } } while (f <= mid) { temp[t++] = array[f++]; } while (s <= end) { temp[t++] = array[s++]; } for (int i = 0, j = start; i < t; i++) { array[j++] = temp[i]; }}</code></pre><h2 id="桶排序"><a href="#桶排序" class="headerlink" title="桶排序"></a>桶排序</h2><p>桶排序工作的原理是将 <strong>数组分到有限数量的桶</strong> 里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间 O(n)<em>O</em>(<em>n</em>) 。由于桶排序不是比较排序,他不受到 O(n\log n)<em>O</em>(<em>n</em>log<em>n</em>) 下限的影响。</p><p>桶排序以下列程序进行:</p><ul><li>设置一个定量的数组当作空桶子。</li><li>寻访序列,并且把项目一个一个放到对应的桶子去。</li><li>对每个不是空的桶子进行排序。</li><li>从不是空的桶子里把项目再放回原来的序列中。</li></ul><pre><code class="java">private int indexFor(int a, int min, int step) { return (a - min) / step;}public void bucketSort(int[] arr) { int max = arr[0], min = arr[0]; for (int a : arr) { if (max < a) max = a; if (min > a) min = a; } // 该值可根据实际情况选择 int bucketNum = max / 10 - min / 10 + 1; List buckList = new ArrayList<List<Integer>>(); // create bucket for (int i = 1; i <= bucketNum; i++) { buckList.add(new ArrayList<Integer>()); } // push into the bucket for (int i = 0; i < arr.length; i++) { int index = indexFor(arr[i], min, 10); ((ArrayList<Integer>) buckList.get(index)).add(arr[i]); } ArrayList<Integer> bucket = null; int index = 0; for (int i = 0; i < bucketNum; i++) { bucket = (ArrayList<Integer>) buckList.get(i); insertSort(bucket); for (int k : bucket) { arr[index++] = k; } }}// 把桶內元素插入排序private void insertSort(List<Integer> bucket) { for (int i = 1; i < bucket.size(); i++) { int temp = bucket.get(i); int j = i - 1; for (; j >= 0 && bucket.get(j) > temp; j--) { bucket.set(j + 1, bucket.get(j)); } bucket.set(j + 1, temp); }}</code></pre><h2 id="基数排序"><a href="#基数排序" class="headerlink" title="基数排序"></a>基数排序</h2><p>对于有d个关键字时,可以分别按关键字进行排序。有俩种方法:</p><ul><li>MSD:先从高位开始进行排序,在每个关键字上,可采用基数排序</li><li>LSD:先从低位开始进行排序,在每个关键字上,可采用桶排序</li></ul><blockquote><p>即通过每个数的每位数字的大小来比较</p></blockquote><pre><code class="java">//找出最大数字的位数int maxNum(int arr[], int len) { int _max = 0; for (int i = 0; i < len; ++i) { int d = 0; int a = arr[i]; while (a) { a /= 10; d++; } if (_max < d) { _max = d; } } return _max;}void radixSort(int *arr, int len) { int d = maxNum(arr, len); int *temp = new int[len]; int count[10]; int radix = 1; for (int i = 0; i < d; ++i) { for (int j = 0; j < 10; ++j) { count[j] = 0; } for (int k = 0; k < len; ++k) { count[(arr[k] / radix) % 10]++; } for (int l = 1; l < 10; ++l) { count[l] += count[l - 1]; } for (int m = 0; m < len; ++m) { int index = (arr[m] / radix) % 10; temp[count[index] - 1] = arr[m]; count[index]--; } for (int n = 0; n < len; ++n) { arr[n] = temp[n]; } radix *= 10; } delete (temp);}</code></pre><h2 id="拓扑排序"><a href="#拓扑排序" class="headerlink" title="拓扑排序"></a>拓扑排序</h2><p>在有向图中找拓扑序列的过程,就是拓扑排序。<strong>拓扑序列常常用于判定图是否有环</strong>。</p><ul><li>从有向图中选择一个入度为0的结点,输出它。</li><li>将这个结点以及该结点出发的所有边从图中删除。</li><li>重复前两步,直到没有入度为0的点。</li></ul><blockquote><p>如果所有点都被输出,即存在一个拓扑序列,则图没有环。</p></blockquote><h1 id="跳跃表"><a href="#跳跃表" class="headerlink" title="跳跃表"></a>跳跃表</h1><p>跳跃列表是一种数据结构。它允许快速查询一个有序连续元素的数据链表。跳跃列表的平均查找和插入时间复杂度都是 <code>O(log n)</code> ,优于普通队列的 <code>O(n)</code>。</p><p>快速查询是通过维护一个多层次的链表,且每一层链表中的元素是前一层链表元素的子集。一开始时,算法在最稀疏的层次进行搜索,直至需要查找的元素在该层两个相邻的元素中间。这时,算法将跳转到下一个层次,重复刚才的搜索,直到找到需要查找的元素为止。跳过的元素的方法可以是 <strong>随机性选择</strong> 或 <strong>确定性选择</strong>,其中前者更为常见。</p><p><img src="https://hadyang.github.io/interview/docs/basic/algo/skip_list/images/9d89be415d4f099d1eb4042af706f278.png" alt="img"></p><p>在查找目标元素时,从顶层列表、头元素起步。算法沿着每层链表搜索,直至找到一个大于或等于目标的元素,或者到达当前层列表末尾。如果该元素等于目标元素,则表明该元素已被找到;如果该元素大于目标元素或已到达链表末尾,则退回到当前层的上一个元素,然后转入下一层进行搜索。</p><p>跳跃列表不像平衡树等数据结构那样提供对最坏情况的性能保证:由于用来建造跳跃列表采用随机选取元素进入更高层的方法,在小概率情况下会生成一个不平衡的跳跃列表(<strong>最坏情况例如最底层仅有一个元素进入了更高层,此时跳跃列表的查找与普通列表一致</strong>)。但是在实际中它通常工作良好,<strong>随机化平衡方案也比平衡二叉查找树等数据结构中使用的确定性平衡方案容易实现</strong>。跳跃列表在并行计算中也很有用:插入可以在跳跃列表不同的部分并行地进行,而不用对数据结构进行全局的重新平衡。</p><p>跳跃表插入一个元素:</p><p><img src="https://hadyang.github.io/interview/docs/basic/algo/skip_list/images/e3ccf6537c3a42f6c6f1e8d7e26ba0ed.png" alt="img"></p><h2 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h2><p>因为跳跃列表中的元素可以在多个列表中,所以每个元素可以有多于一个指针。跳跃列表的插入和删除的实现与普通的链表操作类似,但高层元素必须在进行多个链表中进行插入或删除。</p><pre><code>package io.github.hadyang.leetcode.algo;import lombok.Getter;import lombok.Setter;import java.util.Arrays;import java.util.Random;/** * @author haoyang.shi */public class SkipList<K extends Comparable<K>, V> { @Getter @Setter static final class Node<K extends Comparable<K>, V> { private K key; private V value; private Node<K, V> up, down, pre, next; Node(K key, V value) { this.key = key; this.value = value; } @Override public String toString() { return "Node{" + "key=" + key + ", value=" + value + ", hashcode=" + hashCode() + ", up=" + (up == null ? "null" : up.hashCode()) + ", down=" + (down == null ? "null" : down.hashCode()) + ", pre=" + (pre == null ? "null" : pre.hashCode()) + ", next=" + (next == null ? "null" : next.hashCode()) + '}'; } } private Node<K, V> head;//k,v都是NULL private Integer levels = 0; private Integer length = 0; private Random random = new Random(System.currentTimeMillis()); public SkipList() { createNewLevel(); } public void put(K key, V value) { if (key == null || value == null) { return; } Node<K, V> newNode = new Node<>(key, value); insertNode(newNode); } private void insertNode(Node<K, V> newNode) { Node<K, V> curNode = findNode(newNode.getKey()); if (curNode.getKey() == null) { insertNext(curNode, newNode); } else if (curNode.getKey().compareTo(newNode.getKey()) == 0) { //update curNode.setValue(newNode.getValue()); return; } else { insertNext(curNode, newNode); } int currentLevel = 1; Node<K, V> oldTop = newNode; while (random.nextInt(100) < 50) { Node<K, V> newTop = new Node<>(newNode.getKey(), null); if (currentLevel >= levels) { createNewLevel(); } while (curNode.getPre() != null && curNode.getUp() == null) { curNode = curNode.getPre(); } if (curNode.getUp() == null) { continue; } curNode = curNode.getUp(); Node<K, V> curNodeNext = curNode.getNext(); curNode.setNext(newTop); newTop.setPre(curNode); newTop.setDown(oldTop); oldTop.setUp(newTop); newTop.setNext(curNodeNext); oldTop = newTop; currentLevel++; } } private void createNewLevel() { Node<K, V> newHead = new Node<>(null, null); if (this.head == null) { this.head = newHead; this.levels++; return; } this.head.setUp(newHead); newHead.setDown(this.head); this.head = newHead; this.levels++; } private void insertNext(Node<K, V> curNode, Node<K, V> newNode) { Node<K, V> curNodeNext = curNode.getNext(); newNode.setNext(curNodeNext); if (curNodeNext != null) { curNodeNext.setPre(newNode); } curNode.setNext(newNode); newNode.setPre(curNode); this.length++; } public V get(K key) { Node<K, V> node = findNode(key); if (key.equals(node.getKey())) { return node.getValue(); } return null; } private Node<K, V> findNode(K key) { Node<K, V> curNode = this.head; for (; ; ) { while (curNode.getNext() != null && curNode.getNext().getKey().compareTo(key) <= 0) { curNode = curNode.getNext(); } if (curNode.getDown() != null) { curNode = curNode.getDown(); } else { break; } } return curNode; } public void print() { Node<K, V> curI = this.head; String[][] strings = new String[levels][length + 1]; for (String[] string : strings) { Arrays.fill(string, "0"); } while (curI.getDown() != null) { curI = curI.getDown(); } System.out.println("levels:" + levels + "_" + "length:" + length); int i = 0; while (curI != null) { Node<K, V> curJ = curI; int j = levels - 1; while (curJ != null) { strings[j][i] = String.valueOf(curJ.getKey()); if (curJ.getUp() == null) { break; } curJ = curJ.getUp(); j--; } if (curI.getNext() == null) { break; } curI = curI.getNext(); i++; } for (String[] string : strings) { System.out.println(Arrays.toString(string)); } } public static void main(String[] args) { SkipList<Integer, String> skipList = new SkipList<>(); skipList.put(2, "B"); skipList.put(1, "A"); skipList.put(3, "C"); skipList.print(); System.out.println(skipList.get(2)); }</code></pre><h2 id="常见问题-1"><a href="#常见问题-1" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="链表"><a href="#链表" class="headerlink" title="链表"></a>链表</h3><p>1.<a href="https://blog.csdn.net/sinat_35261315/article/details/79205157" target="_blank" rel="noopener">判断链表是否有环</a></p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p>【1】<a href="https://hadyang.github.io/interview/docs/basic/algo/" target="_blank" rel="noopener">https://hadyang.github.io/interview/docs/basic/algo/</a></p><p>【2】<a href="https://www.cnblogs.com/onepixel/articles/7674659.html" target="_blank" rel="noopener">https://www.cnblogs.com/onepixel/articles/7674659.html</a></p>]]></content>
<categories>
<category> Basic </category>
</categories>
</entry>
<entry>
<title>壁纸-无水印</title>
<link href="/51748.html"/>
<url>/51748.html</url>
<content type="html"><![CDATA[<h2 id="从网站获取"><a href="#从网站获取" class="headerlink" title="从网站获取"></a>从网站获取</h2><p><a href="https://bing.ioliu.cn/" target="_blank" rel="noopener">历史必应</a></p><p><a href="http://lab.mkblog.cn/wallpaper/" target="_blank" rel="noopener">mkblog</a></p><p><a href="https://github.com/GallonHu/pic">github_GallonHu</a></p><h2 id="手动获取"><a href="#手动获取" class="headerlink" title="手动获取"></a>手动获取</h2><p>(1)打开必应官方网站</p><p>(2)鼠标右击图片,选择检查选项</p><p>(3) 找到Sources选项</p><p>(4)点击top栏目里最下方选项就能看到图片</p><p>(5)鼠标右击图片,选择保存</p>]]></content>
</entry>
<entry>
<title>JVM</title>
<link href="/17374.html"/>
<url>/17374.html</url>
<content type="html"><![CDATA[<h2 id="深入理解JAVA虚拟机-第三版"><a href="#深入理解JAVA虚拟机-第三版" class="headerlink" title="深入理解JAVA虚拟机 (第三版)"></a>深入理解JAVA虚拟机 (第三版)</h2><p> <img src="/17374/JVM.png" alt="JVM 虚拟机运行时数据区"></p><p>《深入理解JAVA虚拟》第三版主要围绕着上图来叙述,分为五大部分:</p><p> (1)JAVA发展历史:涉及JVM自诞生的版本到目前的版本的发展历程,其中,有讲述为何虚拟机会发生这样的演变,以及与其他类型虚拟机的的关系,并对虚拟机进行了展望。</p><p> (2)自动内存管理(运行时数据区):涉及内存管理的异常,像内存溢出异常、OutOfMemory Error等,垃圾收集器,内存分配策略,故障处理方法与工具,调优方法。</p><p> (3)JVM执行子系统(执行):类文件结构;JVM类加载的机制;字节码执行引擎;</p><p> (4)代码编译和优化;前端编译和优化(JAVA语法糖等);后端编译与优化(即时编译、编译器优化技术-<strong>方法内联、逃逸分析</strong>等)</p><p> (5)高效并发:如何实现并发(JAVA内存模型);如何高效实现并发(锁的介绍、<strong>锁优化</strong>-锁粗化、锁自旋、锁偏向等等)</p><h2 id="Java各版本比较"><a href="#Java各版本比较" class="headerlink" title="Java各版本比较"></a>Java各版本比较</h2><p>1.7</p><p>1.8</p><h2 id="深入理解的书籍"><a href="#深入理解的书籍" class="headerlink" title="深入理解的书籍"></a>深入理解的书籍</h2><p> 《JVM虚拟机规范》、《编译原理》、《汇编语言》</p><h2 id="书籍-pdf"><a href="#书籍-pdf" class="headerlink" title="书籍(pdf)"></a>书籍(pdf)</h2><p>若需要《深入理解JAVA虚拟机》第三版pdf,可发邮件至###[email protected]###(发送时去掉#)</p><p> </p>]]></content>
<categories>
<category> Advanced-java </category>
</categories>
</entry>
<entry>
<title>Transfer Learning</title>
<link href="/8787.html"/>
<url>/8787.html</url>
<content type="html"><![CDATA[<h1 id="跨领域情感分析"><a href="#跨领域情感分析" class="headerlink" title="跨领域情感分析"></a>跨领域情感分析</h1><h2 id="综述"><a href="#综述" class="headerlink" title="综述"></a>综述</h2><pre><code>近几年的综述</code></pre><h2 id="相关研究者"><a href="#相关研究者" class="headerlink" title="相关研究者"></a>相关研究者</h2><h3 id="国内研究者"><a href="#国内研究者" class="headerlink" title="国内研究者"></a>国内研究者</h3><p><a href="https://www.zhihu.com/people/jindongwang/activities" target="_blank" rel="noopener">迁移学习手册——王晋东</a></p><h3 id="国外研究者"><a href="#国外研究者" class="headerlink" title="国外研究者"></a>国外研究者</h3>]]></content>
<categories>
<category> NLP </category>
</categories>
</entry>