-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsearch.xml
More file actions
4165 lines (4165 loc) · 942 KB
/
search.xml
File metadata and controls
4165 lines (4165 loc) · 942 KB
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>209.长度最小的子数组</title>
<url>/posts/44730.html</url>
<content><![CDATA[<p><a href="https://leetcode-cn.com/problems/minimum-size-subarray-sum/">题目</a>描述:</p>
<span id="more"></span>
<blockquote>
<p>给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。</p>
<p>示例:</p>
<p>输入:s = 7, nums = [2,3,1,2,4,3] 输出:2 解释:子数组 [4,3] 是该条件下的长度最小的子数组。</p>
</blockquote>
<p>首先想到的肯定是暴力解法,使用两个for循环,然后不断地寻找满足条件的子序列,时间复杂度是O(n^2)。</p>
<p>下面介绍使用滑动窗口的解法。</p>
<p>滑动窗口其实就是双指针,通过两个指针不断改变子序列的起始位置和结束位置,直到找到长度最小的子序列。解题的关键就是需要注意如何移动起始位置和结束位置的指针。如果当前的窗口里面的值的和大于给定的值了,那么就可以向前移动初始位置的指针,而结束位置的指针只是用来遍历数组的。代码如下:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span> {</span><br><span class="line"> <span class="keyword">public</span> <span class="type">int</span> <span class="title function_">minSubArrayLen</span><span class="params">(<span class="type">int</span> target, <span class="type">int</span>[] nums)</span> {</span><br><span class="line"> <span class="type">int</span> <span class="variable">left</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"> <span class="type">int</span> <span class="variable">result</span> <span class="operator">=</span> Integer.MAX_VALUE;</span><br><span class="line"> <span class="type">int</span> <span class="variable">sum</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">for</span>(<span class="type">int</span> <span class="variable">right</span> <span class="operator">=</span> <span class="number">0</span>;right < nums.length; ++right){ <span class="comment">//结束指针遍历数组</span></span><br><span class="line"> sum += nums[right];</span><br><span class="line"> <span class="keyword">while</span>(sum >= target){</span><br><span class="line"> result = Math.min(result,right-left+<span class="number">1</span>);</span><br><span class="line"> sum -= nums[left++]; <span class="comment">//移动初始指针</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> result == Integer.MAX_VALUE?<span class="number">0</span>:result;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>时间复杂度为O(n)。</p>
]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
<tags>
<tag>滑动窗口</tag>
</tags>
</entry>
<entry>
<title>26.删除数组中的重复项</title>
<url>/posts/5982.html</url>
<content><![CDATA[<p><a href="https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/">题目</a>描述:</p>
<span id="more"></span>
<blockquote>
<p>给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。</p>
<p>不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。</p>
<p>示例1:</p>
<blockquote>
<p>输入:nums = [1,1,2]<br>输出:2, nums = [1,2]<br>解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。 </p>
</blockquote>
<p>示例2:</p>
<blockquote>
<p>输入:nums = [0,0,1,1,1,2,2,3,3,4]<br>输出:5, nums = [0,1,2,3,4]<br>解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。</p>
</blockquote>
</blockquote>
<p>使用双指针法,定义快慢指针,首先定义慢指针为0;快指针为1;然后循环快指针,如果快指针指向的元素不等于慢指针指向的元素,那么令快指针指向的元素等于慢指针指向的元素的后一个元素。最终返回慢指针+1。</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span> {</span><br><span class="line"> <span class="keyword">public</span> <span class="type">int</span> <span class="title function_">removeDuplicates</span><span class="params">(<span class="type">int</span>[] nums)</span> {</span><br><span class="line"> <span class="type">int</span> <span class="variable">lowIndex</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">for</span>(<span class="type">int</span> <span class="variable">fastIndex</span> <span class="operator">=</span> <span class="number">1</span>;fastIndex < nums.length; ++fastIndex){</span><br><span class="line"> <span class="keyword">if</span>(nums[lowIndex] != nums[fastIndex]){</span><br><span class="line"> nums[lowIndex + <span class="number">1</span>] = nums[fastIndex];</span><br><span class="line"> ++lowIndex;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> lowIndex + <span class="number">1</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
<tags>
<tag>双指针法</tag>
</tags>
</entry>
<entry>
<title>BTC中的数据结构</title>
<url>/posts/ds_btc.html</url>
<content><![CDATA[<h3 id="Hash-Pointer"><a href="#Hash-Pointer" class="headerlink" title="Hash Pointer"></a>Hash Pointer</h3><p>在程序运行过程中,需要用到数据。最简单的是直接获取数据,但当数据本身较大,需要占用较大空间时,明显会造成一定麻烦。因此,可以引入指针这一概念。当需要获取数据时,只需要按照指针所给的地址,去对应的位置读取数据即可,这样大大节省了内存空间。在实际中,为了便于程序移植性等原因,指针实际上存储的是逻辑地址而非物理地址。<br>区块链结构本身为一条链表,节点为区块。而传统链表实现,便是通过指针将各个节点串联起来而称为最终的链。如下便是我们最常见的一个链表:</p>
<p><img src="https://cdn.jsdelivr.net/gh/HelingCode/CDN-vulkhe-Pic@master/uPic/2023/12/17/20200214173054674.png" alt="在这里插入图片描述"></p>
<p>但在区块链系统中,并未采用指针,而是使用了<strong>哈希指针</strong>。<br>如下图对于该节点,我们可以看到有两个指针指向这个节点(实际上为一个),其中P为该节点的地址,H()为该节点的哈希值,该值与节点中内容有关。当节点(区块)中内容发生改变,该哈希值也会发生改变,从而保证了区块内容不能被篡改。</p>
<p><img src="https://cdn.jsdelivr.net/gh/HelingCode/CDN-vulkhe-Pic@master/uPic/2023/12/17/20200214173157391.png" alt="在这里插入图片描述"></p>
<p>在比特币中,其最基本的数据结构便是一个个区块形成的区块链。</p>
<p><strong>区块链与链表区别:</strong>哈希指针代替普通指针<br>如图为一个简单的区块链。其中,每个区块根据自己的区块内容生成自己的哈希值,此外,每个区块(除创世纪块)都保存有前一个区块的哈希值。需要注意的是,本区块哈希生成依赖于本区块内容,而本区块内容中又包含有前一个区块的哈希值。从而保证了区块内容不被篡改。<br>如图中所示,如果我们想要破坏区块链完整性。篡改B的内容,而C中保存有B的哈希值,所以C也得进行修改。而同样C后区块也得修改。而用户只需要记住最后一个区块链的哈希地址,就可以检测区块链上内容是否被篡改。<br>在实际应用中,一整条链可能会被切断分开保存在多个地方。若用户仅仅具有其中一段,当用到前面部分区块数据时,直接问系统中其他节点要即可,当要到之后,仅仅通过计算要到的最后一个哈希值和自己保存哈希值是否一致可以判断所给内容是否确实为区块链上真实的内容。</p>
<p><img src="https://cdn.jsdelivr.net/gh/HelingCode/CDN-vulkhe-Pic@master/uPic/2023/12/17/20200214173245117-20231217152518005.png" alt="在这里插入图片描述"></p>
<h3 id="Merkle-Tree"><a href="#Merkle-Tree" class="headerlink" title="Merkle Tree"></a>Merkle Tree</h3><p>Merkle Tree可以用于提供Merkle Proof。比特币中节点分为<strong>轻节点</strong>和<strong>全节点</strong>。全节点保存整个区块的所有内容,而轻节点仅仅保存区块的块头信息。</p>
<blockquote>
<p>为什么要分轻节点和全节点?<br>因为硬件的局限。一个区块大小为1MB,对于移动便携设备来说,如果存储区块的所有内容,则所需空间过大,而这是不现实的。所以轻节点只需要存储区块块头信息,全节点存储区块所有内容即可。</p>
</blockquote>
<p>当需要向轻节点证明某条交易是否被写入区块链,便需要用到Merkle proof。我们将交易到根节点这一条路径称为Merkle proof,全节点将整个Merkle proof发送给轻节点(如下图所示),轻节点即可根据其算出根哈希值,和自己保存的对比,从而验证该交易是否被写入区块链。只要沿着该路径,所有哈希值都正确,说明内容没有被修改过。</p>
<p><img src="https://cdn.jsdelivr.net/gh/HelingCode/CDN-vulkhe-Pic@master/uPic/2023/12/17/image-20221125150306565.png" alt="image-20221125150306565"></p>
<p>以上图为例,要验证黄色的交易是否被写到区块中,我们首先要请求第三层的红色的哈希值,然后根据黄色的交易求出第三层的绿色哈希值,然后通过这两个已知的哈希值求出第二层的绿色哈希值,再请求第二层的红色哈希值,以此类推最终求得根哈希值,然后与存储在自身结点中的根哈希值比较,若相同则证明交易已经写入到了区块中。</p>
<p>上面的验证是proof of membership 时间复杂度为O(logn)<br>关于proof of non-membership验证,如果交易是无序的,我们只能证明每个结点都是对的,说明没有该节点,时间复杂度为O(n)<br>如果交易是按hash值从小到大排序,我们对要检验的交易取hash,判断它应处的位置,然后计算他前后的两个结点的hash,若这两个结点确实相邻(merkle proof),则证明我们要找的结点不存在(若存在则会在这两个结点之间,这两个结点不会相邻),时间复杂度为O(logn)。</p>
]]></content>
<categories>
<category>区块链</category>
</categories>
<tags>
<tag>BTC</tag>
</tags>
</entry>
<entry>
<title>977.有序数组的平方</title>
<url>/posts/35851.html</url>
<content><![CDATA[<p><a href="https://leetcode-cn.com/problems/squares-of-a-sorted-array/">题目</a>描述:</p>
<span id="more"></span>
<blockquote>
<p>给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。</p>
<p>示例 1: 输入:nums = [-4,-1,0,3,10] 输出:[0,1,9,16,100] 解释:平方后,数组变为 [16,1,0,9,100],排序后,数组变为 [0,1,9,16,100]</p>
<p>示例 2: 输入:nums = [-7,-3,2,3,11] 输出:[4,9,9,49,121]</p>
</blockquote>
<p>简单的思路就是暴力破解了,没什么说的。</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span> {</span><br><span class="line"> <span class="keyword">public</span> <span class="type">int</span>[] sortedSquares(<span class="type">int</span>[] nums) {</span><br><span class="line"> <span class="keyword">for</span>(<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>;i < nums.length; ++i){</span><br><span class="line"> nums[i] *= nums[i];</span><br><span class="line"> }</span><br><span class="line"> Arrays.sort(nums);</span><br><span class="line"> <span class="keyword">return</span> nums;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>时间复杂度为O(nlogn)。</p>
<p>接下来使用双指针法。</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span> {</span><br><span class="line"> <span class="keyword">public</span> <span class="type">int</span>[] sortedSquares(<span class="type">int</span>[] nums) {</span><br><span class="line"> <span class="type">int</span> <span class="variable">l</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"> <span class="type">int</span> <span class="variable">r</span> <span class="operator">=</span> nums.length - <span class="number">1</span>;</span><br><span class="line"> <span class="type">int</span>[] res = <span class="keyword">new</span> <span class="title class_">int</span>[nums.length];</span><br><span class="line"> <span class="type">int</span> <span class="variable">k</span> <span class="operator">=</span> nums.length - <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">while</span>(l <= r){</span><br><span class="line"> <span class="keyword">if</span>(nums[l]*nums[l] > nums[r]*nums[r]){</span><br><span class="line"> res[k--] = nums[l]*nums[l++];</span><br><span class="line"> }<span class="keyword">else</span>{</span><br><span class="line"> res[k--] = nums[r]*nums[r--];</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> res;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
<tags>
<tag>双指针法</tag>
</tags>
</entry>
<entry>
<title>CentOS7启动Tomcat后浏览器无法访问的解决方法</title>
<url>/posts/2483.html</url>
<content><![CDATA[<p>自己的Linux系统是CentOS7,今天在学习Tomcat的使用的时候,在Linux里面启动成功之后,在Windows浏览器访问<code>http://ip地址:8080</code>失败,原来需要有防火墙的相关操作,要编辑/etc/sysconfig目录下面的iptables文件,但是CentOS7中没有这个文件,所以如果要对防火墙操作,需要更新或者重新安装防火墙服务。</p>
<span id="more"></span>
<p>解决步骤如下:</p>
<ol>
<li><p>安装或者更新iptables-services</p>
<p><code>yum install iptables-services</code></p>
</li>
<li><p>启动iptables</p>
<p><code>systemctl enable iptables</code></p>
<p>这个时候,/etc/sysconfig目录下面就有了iptables文件了。</p>
</li>
<li><p>打开iptables</p>
<p><code>systemctl start iptables</code></p>
</li>
<li><p>编辑防火墙配置</p>
<p><code>$ vi /etc/sysconfig/iptables</code></p>
</li>
<li><p>添加8080端口的防火墙,允许访问</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">*filter</span><br><span class="line">:INPUT ACCEPT [0:0]</span><br><span class="line">:FORWARD ACCEPT [0:0]</span><br><span class="line">:OUTPUT ACCEPT [0:0]</span><br><span class="line">-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT</span><br><span class="line">-A INPUT -p icmp -j ACCEPT</span><br><span class="line">-A INPUT -i lo -j ACCEPT</span><br><span class="line">-A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT</span><br><span class="line">-A INPUT -m state --state NEW -m tcp -p tcp --dport 80 -j ACCEPT</span><br><span class="line">-A INPUT -m state --state NEW -m tcp -p tcp --dport 3306 -j ACCEPT</span><br><span class="line">-A INPUT -m state --state NEW -m tcp -p tcp --dport 8080 -j ACCEPT</span><br><span class="line">-A INPUT -j REJECT --reject-with icmp-host-prohibited</span><br><span class="line">-A FORWARD -j REJECT --reject-with icmp-host-prohibited</span><br></pre></td></tr></table></figure>
</li>
<li><p>输入i开始编辑,ESC键结束编辑,wq保存并退出.</p>
</li>
<li><p>重启防火墙</p>
<p><code>$ service iptables restart</code></p>
</li>
</ol>
<p>之后就可以在本机通过浏览器访问<code>http://ip地址:8080</code>,就可以正常看到tomcat的默认欢迎页面了。</p>
]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Tomcat</tag>
</tags>
</entry>
<entry>
<title>Docker的安装及常用命令</title>
<url>/posts/docker.html</url>
<content><![CDATA[<h2 id="查看系统的内核版本"><a href="#查看系统的内核版本" class="headerlink" title="查看系统的内核版本"></a>查看系统的内核版本</h2><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">[root@ecs-29043 ~]# uname -r</span><br><span class="line">3.10.0-1160.53.1.el7.x86_64</span><br></pre></td></tr></table></figure>
<h2 id="yum-更新到最新版本"><a href="#yum-更新到最新版本" class="headerlink" title="yum 更新到最新版本"></a>yum 更新到最新版本</h2><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">[root@ecs-29043 ~]# sudo yum update</span><br></pre></td></tr></table></figure>
<h2 id="安装Docker所需的依赖包"><a href="#安装Docker所需的依赖包" class="headerlink" title="安装Docker所需的依赖包"></a>安装Docker所需的依赖包</h2><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">[root@ecs-29043 ~]# sudo yum install -y yum-utils device-mapper-persistent-data lvm2</span><br></pre></td></tr></table></figure>
<h2 id="设置Docker的yum的源"><a href="#设置Docker的yum的源" class="headerlink" title="设置Docker的yum的源"></a>设置Docker的yum的源</h2><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">[root@ecs-29043 ~]# sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo</span><br></pre></td></tr></table></figure>
<ul>
<li>鉴于国内网络问题,强烈建议使用国内源。以下是阿里云的软件源。<strong>如果是海外如AWS云就不要设置yum源</strong></li>
</ul>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">sudo yum-config-manager \ --add-repo \ https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo</span><br><span class="line">sudo sed -i 's/download.docker.com/mirrors.aliyun.com\/docker-ce/g' /etc/yum.repos.d/docker-ce.repo</span><br></pre></td></tr></table></figure>
<h2 id="查看仓库所有Docker版本"><a href="#查看仓库所有Docker版本" class="headerlink" title="查看仓库所有Docker版本"></a>查看仓库所有Docker版本</h2><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">[root@ecs-29043 ~]# yum list docker-ce --showduplicates | sort -r</span><br></pre></td></tr></table></figure>
<ul>
<li>这里可以看到你能安装的最新版本</li>
</ul>
<h2 id="安装Docker"><a href="#安装Docker" class="headerlink" title="安装Docker"></a>安装Docker</h2><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">[root@ecs-29043 ~]# sudo yum install docker</span><br></pre></td></tr></table></figure>
<ul>
<li>安装默认最新版本的 Docker</li>
</ul>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">[root@ecs-29043 ~]# sudo yum install <FQPN></span><br></pre></td></tr></table></figure>
<ul>
<li>安装指定版本,例如:sudo yum install <a href="http://docker-ce-20.10.11.ce/">docker-ce-20.10.11.ce</a></li>
</ul>
<h2 id="启动Docker并添加开机自启动"><a href="#启动Docker并添加开机自启动" class="headerlink" title="启动Docker并添加开机自启动"></a>启动Docker并添加开机自启动</h2><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">[root@ecs-29043 ~]# sudo systemctl start docker</span><br></pre></td></tr></table></figure>
<ul>
<li>启动 Docker</li>
</ul>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">[root@ecs-29043 ~]# systemctl enable docker</span><br></pre></td></tr></table></figure>
<ul>
<li>设置开机启动 Docker</li>
</ul>
<h2 id="查看-Docker-版本"><a href="#查看-Docker-版本" class="headerlink" title="查看 Docker 版本"></a>查看 Docker 版本</h2><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">[root@ecs-29043 ~]# docker --version</span><br><span class="line">Docker version 1.13.1, build 7d71120/1.13.1</span><br></pre></td></tr></table></figure>
<h2 id="卸载-Docker"><a href="#卸载-Docker" class="headerlink" title="卸载 Docker"></a>卸载 Docker</h2><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">[root@ecs-29043 ~]# sudo yum remove docker \</span><br><span class="line"> docker-client \</span><br><span class="line"> docker-client-latest \</span><br><span class="line"> docker-common \</span><br><span class="line"> docker-latest \</span><br><span class="line"> docker-latest-logrotate \</span><br><span class="line"> docker-logrotate \</span><br><span class="line"> docker-selinux \</span><br><span class="line"> docker-engine-selinux \</span><br><span class="line"> docker-engine</span><br></pre></td></tr></table></figure>
<h2 id="Docker-常用命令"><a href="#Docker-常用命令" class="headerlink" title="Docker 常用命令"></a>Docker 常用命令</h2><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">[root@ecs-29043 ~]# docker --help #Docker帮助</span><br><span class="line">[root@ecs-29043 ~]# docker --version #查看Docker版本</span><br><span class="line">[root@ecs-29043 ~]# docker search <image> #搜索镜像文件,如:docker search mysql</span><br><span class="line">[root@ecs-29043 ~]# docker pull <image> #拉取镜像文件, 如:docker pull mysql</span><br><span class="line">[root@ecs-29043 ~]# docker images #查看已经拉取下来的所以镜像文件</span><br><span class="line">[root@ecs-29043 ~]# docker rmi <image> #删除指定镜像文件</span><br><span class="line">[root@ecs-29043 ~]# docker run --name <name> -p 80:8080 -d <image> #发布指定镜像文件</span><br><span class="line">[root@ecs-29043 ~]# docker ps #查看正在运行的所有镜像</span><br><span class="line">[root@ecs-29043 ~]# docker ps -a #查看所有发布的镜像</span><br><span class="line">[root@ecs-29043 ~]# docker rm <image> #删除执行已发布的镜像</span><br></pre></td></tr></table></figure>
<h2 id="拉取Portainer"><a href="#拉取Portainer" class="headerlink" title="拉取Portainer"></a>拉取Portainer</h2><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">[root@ecs-29043 ~]# docker pull portainer/portainer</span><br></pre></td></tr></table></figure>
<h2 id="启动Portainer"><a href="#启动Portainer" class="headerlink" title="启动Portainer"></a>启动Portainer</h2><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">docker run -d --restart=always --name portainer -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock portainer/portainer</span><br></pre></td></tr></table></figure>
]]></content>
<categories>
<category>DevOps</category>
</categories>
<tags>
<tag>Docker</tag>
</tags>
</entry>
<entry>
<title>JDK动态代理和Cglib动态代理</title>
<url>/posts/spring_aop.html</url>
<content><![CDATA[<p><strong>代理模式</strong>是一种设计模式,能够使得在不修改源目标的前提下,额外扩展源目标的功能。即通过访问源目标的代理类,再由代理类去访问源目标。这样一来,要扩展功能,就无需修改源目标的代码了。只需要在代理类上增加就可以了。</p>
<p><img src="https://cdn.jsdelivr.net/gh/HelingCode/CDN-vulkhe-Pic@master/uPic/2023/12/17/2244490621-6140109245c01_fix732.png" alt="2244490621-6140109245c01_fix732"></p>
<p>在Java中,代理又分静态代理和动态代理2种,其中动态代理根据不同实现又区分基于接口的的动态代理和基于子类的动态代理。</p>
<p>代理类完成的功能:</p>
<ul>
<li>调用目标方法,执行目标方法的功能。</li>
<li>功能增强,在目标方法调用时,增加功能。</li>
</ul>
<h2 id="静态代理"><a href="#静态代理" class="headerlink" title="静态代理"></a>静态代理</h2><p>所谓静态代理,就是通过<strong>声明一个明确的代理类</strong>来访问源对象。</p>
<p>我们有2个接口,Person和Animal。每个接口各有2个实现类,UML如下图:</p>
<p><img src="https://cdn.jsdelivr.net/gh/HelingCode/CDN-vulkhe-Pic@master/uPic/2023/12/17/43042188-6140109a1cbc8.png" alt="43042188-6140109a1cbc8"></p>
<p>每个实现类中代码都差不多一致,用Student来举例(其他类和这个几乎一模一样)</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Student</span> <span class="keyword">implements</span> <span class="title class_">Person</span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> String name;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">Student</span><span class="params">()</span> {</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">Student</span><span class="params">(String name)</span> {</span><br><span class="line"> <span class="built_in">this</span>.name = name;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">wakeup</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(<span class="string">"学生"</span> + name + <span class="string">"早晨醒来啦"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">sleep</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(<span class="string">"学生"</span> + name + <span class="string">"晚上睡觉啦"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>假设我们现在要做一件事,就是在所有的实现类调用<code>wakeup()</code>前增加一行输出<code>早安~</code>,调用<code>sleep()</code>前增加一行输出<code>晚安~</code>。那我们只需要编写2个代理类<code>PersonProxy</code>和<code>AnimalProxy</code>:</p>
<p><strong>PersonProxy:</strong></p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">PersonProxy</span> <span class="keyword">implements</span> <span class="title class_">Person</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> Person person;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">PersonProxy</span><span class="params">(Person person)</span> {</span><br><span class="line"> <span class="built_in">this</span>.person = person;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">wakeup</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(<span class="string">"早安~"</span>);</span><br><span class="line"> person.wakeup();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">sleep</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(<span class="string">"晚安~"</span>);</span><br><span class="line"> person.sleep();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p><strong>AnimalProxy:</strong></p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">AnimalProxy</span> <span class="keyword">implements</span> <span class="title class_">Animal</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> Animal animal;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">AnimalProxy</span><span class="params">(Animal animal)</span> {</span><br><span class="line"> <span class="built_in">this</span>.animal = animal;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">wakeup</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(<span class="string">"早安~"</span>);</span><br><span class="line"> animal.wakeup();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">sleep</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(<span class="string">"晚安~"</span>);</span><br><span class="line"> animal.sleep();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p><strong>最终执行代码为:</strong></p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> {</span><br><span class="line"> <span class="type">Person</span> <span class="variable">student</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Student</span>(<span class="string">"张三"</span>);</span><br><span class="line"> <span class="type">PersonProxy</span> <span class="variable">studentProxy</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">PersonProxy</span>(student);</span><br><span class="line"> studentProxy.wakeup();</span><br><span class="line"> studentProxy.sleep();</span><br><span class="line"></span><br><span class="line"> <span class="type">Person</span> <span class="variable">doctor</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Doctor</span>(<span class="string">"王教授"</span>);</span><br><span class="line"> <span class="type">PersonProxy</span> <span class="variable">doctorProxy</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">PersonProxy</span>(doctor);</span><br><span class="line"> doctorProxy.wakeup();</span><br><span class="line"> doctorProxy.sleep();</span><br><span class="line"></span><br><span class="line"> <span class="type">Animal</span> <span class="variable">dog</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Dog</span>(<span class="string">"旺旺"</span>);</span><br><span class="line"> <span class="type">AnimalProxy</span> <span class="variable">dogProxy</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">AnimalProxy</span>(dog);</span><br><span class="line"> dogProxy.wakeup();</span><br><span class="line"> dogProxy.sleep();</span><br><span class="line"></span><br><span class="line"> <span class="type">Animal</span> <span class="variable">cat</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Cat</span>(<span class="string">"咪咪"</span>);</span><br><span class="line"> <span class="type">AnimalProxy</span> <span class="variable">catProxy</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">AnimalProxy</span>(cat);</span><br><span class="line"> catProxy.wakeup();</span><br><span class="line"> catProxy.sleep();</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p><strong>输出:</strong></p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line">早安~</span><br><span class="line">学生[张三]早晨醒来啦</span><br><span class="line">晚安~</span><br><span class="line">学生[张三]晚上睡觉啦</span><br><span class="line">早安~</span><br><span class="line">医生[王教授]早晨醒来啦</span><br><span class="line">晚安~</span><br><span class="line">医生[王教授]晚上睡觉啦</span><br><span class="line">早安~~</span><br><span class="line">小狗[旺旺]早晨醒来啦</span><br><span class="line">晚安~~</span><br><span class="line">小狗[旺旺]晚上睡觉啦</span><br><span class="line">早安~~</span><br><span class="line">小猫[咪咪]早晨醒来啦</span><br><span class="line">晚安~~</span><br><span class="line">小猫[咪咪]晚上睡觉啦</span><br></pre></td></tr></table></figure>
<p>这里用了2个代理类,分别代理了<code>Person</code>和<code>Animal</code>接口。</p>
<p>这种模式虽然好理解,但是缺点也很明显:</p>
<ul>
<li>会存在大量的冗余的代理类,这里演示了2个接口,如果有10个接口,就必须定义10个代理类。</li>
<li>不易维护,一旦接口更改,代理类和目标类都需要更改。</li>
<li>若类方法数量越来越多的时候,代理类的代码量十分庞大的。</li>
</ul>
<h2 id="JDK动态代理"><a href="#JDK动态代理" class="headerlink" title="JDK动态代理"></a>JDK动态代理</h2><p>在程序的执行过程中,使用JDK的反射机制,创建代理类对象,并动态地指定要代理的目标类。</p>
<p>JDK动态代理的实现主要依靠三个类:InvocationHandler,Method,Proxy。</p>
<ul>
<li><strong>InvocationHandler(调用处理器)接口</strong>:里面只有一个Invoke() 方法,该方法表示代理对象要执行的功能代码。代理类要完成的功能就写在invoke() 方法中。方法原型如下:</li>
</ul>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> Object <span class="title function_">invoke</span><span class="params">(Object proxy, Method method, Object[] args)</span> <span class="keyword">throws</span> Throwable;</span><br></pre></td></tr></table></figure>
<p>其中,<code>Object proxy</code> 表示JDK创建的代理对象,无需赋值。<code>Method method</code> 表示目标类中的方法,JDK提供method对象。<code>Object[] args</code> 表示目标类中方法的参数,JDK提供的。</p>
<p>InvocationHandler接口:表示你的代理要干什么。</p>
<p><strong>用法:</strong>1.创建类实现<code>InvocationHandler</code>接口 2.重写i<code>nvoke()</code> 方法,把原来静态代理类要完成的功能,写在这里。</p>
<ul>
<li><strong>Method类</strong>: 表示方法,确切的说是目标类中的方法。</li>
</ul>
<p>作用:通过Method可以执行某个目标类的方法,<code>Method.invoke();</code> </p>
<p><code>method.invoke(目标对象,方法的参数);</code></p>
<ul>
<li><strong>Proxy类</strong>:核心的对象,用来创建代理对象。之前的静态代理中都是new类的构造方法,现在我们是使用Proxy类的方法,代替new的使 用。</li>
</ul>
<p>方法:静态方法<code>newProxyInstance()</code>;</p>
<p>作用:创建代理对象,等同于静态代理中的用new创建代理对象。</p>
<p>方法原型如下:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> Object <span class="title function_">newProxyInstance</span><span class="params">(ClassLoader loader,</span></span><br><span class="line"><span class="params"> Class<?>[] interfaces,</span></span><br><span class="line"><span class="params"> InvocationHandler h)</span></span><br></pre></td></tr></table></figure>
<p>其中,<code>ClassLoader loader</code> 表示类加载器,负责向内存中加载对象,可以使用反射获取对象的ClassLoader。例如类a, <code>a.getClass().getClassLoader()</code>, 这里表示目标对象的类加载器。</p>
<p><code>Class<?>[] interfaces</code> : 接口,目标对象实现的接口,也是反射获取的。</p>
<p><code>InvocationHandler h</code> : 我们自己写的,代理类要完成的功能。</p>
<p>返回值<code>Object</code> : 就是生成的代理对象。</p>
<p>还是前面那个例子,用JDK动态代理类去实现的代码如下:</p>
<p><strong>创建一个JdkProxy类,用于统一代理:</strong></p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JdkProxy</span> <span class="keyword">implements</span> <span class="title class_">InvocationHandler</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> Object bean;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">JdkProxy</span><span class="params">(Object bean)</span> {</span><br><span class="line"> <span class="built_in">this</span>.bean = bean;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> Object <span class="title function_">invoke</span><span class="params">(Object proxy, Method method, Object[] args)</span> <span class="keyword">throws</span> Throwable {</span><br><span class="line"> <span class="type">String</span> <span class="variable">methodName</span> <span class="operator">=</span> method.getName();</span><br><span class="line"> <span class="keyword">if</span> (methodName.equals(<span class="string">"wakeup"</span>)){</span><br><span class="line"> System.out.println(<span class="string">"早安~~~"</span>);</span><br><span class="line"> }<span class="keyword">else</span> <span class="keyword">if</span>(methodName.equals(<span class="string">"sleep"</span>)){</span><br><span class="line"> System.out.println(<span class="string">"晚安~~~"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> method.invoke(bean, args);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p><strong>执行代码:</strong></p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> {</span><br><span class="line"> <span class="type">JdkProxy</span> <span class="variable">proxy</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">JdkProxy</span>(<span class="keyword">new</span> <span class="title class_">Student</span>(<span class="string">"张三"</span>));</span><br><span class="line"> <span class="comment">// student是代理对象,是com.sun.proxy.$Proxy类型的</span></span><br><span class="line"> <span class="type">Person</span> <span class="variable">student</span> <span class="operator">=</span> (Person) Proxy.newProxyInstance(proxy.getClass().getClassLoader(), <span class="keyword">new</span> <span class="title class_">Class</span>[]{Person.class}, proxy);</span><br><span class="line"> <span class="comment">// 通过代理对象执行方法调用,执行handler,student.wakeup(),此时执行的是proxy对象中的invoke()方法</span></span><br><span class="line"> student.wakeup();</span><br><span class="line"> student.sleep();</span><br><span class="line"></span><br><span class="line"> proxy = <span class="keyword">new</span> <span class="title class_">JdkProxy</span>(<span class="keyword">new</span> <span class="title class_">Doctor</span>(<span class="string">"王教授"</span>));</span><br><span class="line"> <span class="type">Person</span> <span class="variable">doctor</span> <span class="operator">=</span> (Person) Proxy.newProxyInstance(proxy.getClass().getClassLoader(), <span class="keyword">new</span> <span class="title class_">Class</span>[]{Person.class}, proxy);</span><br><span class="line"> doctor.wakeup();</span><br><span class="line"> doctor.sleep();</span><br><span class="line"></span><br><span class="line"> proxy = <span class="keyword">new</span> <span class="title class_">JdkProxy</span>(<span class="keyword">new</span> <span class="title class_">Dog</span>(<span class="string">"旺旺"</span>));</span><br><span class="line"> <span class="type">Animal</span> <span class="variable">dog</span> <span class="operator">=</span> (Animal) Proxy.newProxyInstance(proxy.getClass().getClassLoader(), <span class="keyword">new</span> <span class="title class_">Class</span>[]{Animal.class}, proxy);</span><br><span class="line"> dog.wakeup();</span><br><span class="line"> dog.sleep();</span><br><span class="line"></span><br><span class="line"> proxy = <span class="keyword">new</span> <span class="title class_">JdkProxy</span>(<span class="keyword">new</span> <span class="title class_">Cat</span>(<span class="string">"咪咪"</span>));</span><br><span class="line"> <span class="type">Animal</span> <span class="variable">cat</span> <span class="operator">=</span> (Animal) Proxy.newProxyInstance(proxy.getClass().getClassLoader(), <span class="keyword">new</span> <span class="title class_">Class</span>[]{Animal.class}, proxy);</span><br><span class="line"> cat.wakeup();</span><br><span class="line"> cat.sleep();</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>可以看到,相对于静态代理类来说,无论有多少接口,这里只需要一个代理类。核心代码也很简单。唯一需要注意的点有以下2点:</p>
<ul>
<li>JDK动态代理是需要声明接口的,创建一个动态代理类必须得给这个”虚拟“的类一个接口。可以看到,这时候经动态代理类创造之后的每个bean已经不是原来那个对象了。</li>
</ul>
<p><img src="https://cdn.jsdelivr.net/gh/HelingCode/CDN-vulkhe-Pic@master/uPic/2023/12/17/2796530636-614010da5d63f_fix732.png" alt="2796530636-614010da5d63f_fix732"></p>
<ul>
<li>为什么这里<code>JdkProxy</code>还需要构造传入原有的bean呢?因为处理完附加的功能外,需要执行原有bean的方法,以完成<code>代理</code>的职责。</li>
</ul>
<p>这里<code>JdkProxy</code>最核心的方法就是</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> Object <span class="title function_">invoke</span><span class="params">(Object proxy, Method method, Object[] args)</span> <span class="keyword">throws</span> Throwable</span><br></pre></td></tr></table></figure>
<p>其中proxy为代理过之后的对象(并不是目标对象),method为被代理的方法,args为方法的参数。</p>
<p>如果你不传原有的bean,直接用<code>method.invoke(proxy, args)</code>的话,那么就会陷入一个死循环。</p>
<h3 id="可以代理什么?"><a href="#可以代理什么?" class="headerlink" title="可以代理什么?"></a>可以代理什么?</h3><p>JDK的动态代理是也平时大家使用的最多的一种代理方式。也叫做接口代理。</p>
<p>JDK动态代理说白了只是根据接口”凭空“来生成类,至于具体的执行,都被代理到了<code>InvocationHandler</code> 的实现类里。上述例子我是需要继续执行原有bean的逻辑,才将原有的bean构造进来。只要你需要,你可以构造进任何对象到这个代理实现类。也就是说,你可以传入多个对象,或者说你什么类都不代理。只是为某一个接口”凭空“的生成多个代理实例,这多个代理实例最终都会进入<code>InvocationHandler</code>的实现类来执行某一个段共同的代码。</p>
<p>所以,在以往的项目中的一个实际场景就是,我有多个以y aml定义的规则文件,通过对yaml文件的扫描,来为每个yaml规则文件生成一个动态代理类。而实现这个,我只需要事先定义一个接口,和定义<code>InvocationHandler</code>的实现类就可以了,同时把yaml解析过的对象传入。最终这些动态代理类都会进入<code>invoke</code>方法来执行某个共同的逻辑。</p>
<h3 id="代理对象由谁产生?"><a href="#代理对象由谁产生?" class="headerlink" title="代理对象由谁产生?"></a>代理对象由谁产生?</h3><p>JVM,不像静态代理,我们得自己new个代理对象。</p>
<h3 id="代理对象实现了什么接口?"><a href="#代理对象实现了什么接口?" class="headerlink" title="代理对象实现了什么接口?"></a>代理对象实现了什么接口?</h3><p>实现的接口是目标对象实现的接口。 同静态代理中代理对象实现的接口。那个继承关系图还是相同的。 代理对象和目标对象都实现一个共同的接口。就是这个接口。 所以Proxy.newProxyInstance()方法返回的类型就是这个接口类型。</p>
<h3 id="代理对象的方法体是什么?"><a href="#代理对象的方法体是什么?" class="headerlink" title="代理对象的方法体是什么?"></a>代理对象的方法体是什么?</h3><p>代理对象的方法体中的内容就是拦截器中invoke方法中的内容。</p>
<p>所有代理对象的处理逻辑,控制是否执行目标对象的目标方法。都是在这个方法里面处理的。</p>
<h3 id="拦截器中的invoke方法中的method参数是在什么时候赋值的"><a href="#拦截器中的invoke方法中的method参数是在什么时候赋值的" class="headerlink" title="拦截器中的invoke方法中的method参数是在什么时候赋值的?"></a>拦截器中的invoke方法中的method参数是在什么时候赋值的?</h3><p>在客户端,代理对象调用目标方法的时候,此实例中为:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line">student.wakeup();</span><br></pre></td></tr></table></figure>
<p>实际上进入的是拦截器中的invoke方法,这时拦截器中的invoke方法中的method参数会被赋值。</p>
<h3 id="为什么叫JDK动态代理?"><a href="#为什么叫JDK动态代理?" class="headerlink" title="为什么叫JDK动态代理?"></a>为什么叫JDK动态代理?</h3><h2 id="CGLib动态代理"><a href="#CGLib动态代理" class="headerlink" title="CGLib动态代理"></a>CGLib动态代理</h2><p>CGLIB(Code Generation Library) 是一个开源项目。是一个强大的、高性能、高质量的Code生成类库,它可以在运行期拓展Java类与实现Java接口。它广泛地被许多AOP框架使用,例如Spring AOP。</p>
<p>使用JDK的Proxy实现代理,要求目标类与代理类实现相同的接口。若目标类不存在接口,则无法使用该方式实现。</p>
<p>但对于无接口的类,要为期创建动态代理,就要使用CGLIB来实现。CGLIB代理的生成原理是生成目标类的子类,通过子类对目标类的增强。这个子类就是代理对象。所以,使用CGLIB生成动态代理,要求目标类必须能被继承,即不能是final修饰的类。</p>
<p>CGLIB经常被应用在框架中,例如Spring,Hibernate等。Cglib的代理效率高于JDK。对于Cglib一般的开发中并不使用。</p>
<p>Spring在5.X之前默认的动态代理实现一直是jdk动态代理。但是从5.X开始,spring就开始默认使用Cglib来作为动态代理实现。并且springboot从2.X开始也转向了Cglib动态代理实现。</p>
<p>是什么导致了spring体系整体转投Cglib呢,jdk动态代理又有什么缺点呢?</p>
<p>那么我们现在就要来说下Cglib的动态代理。</p>
<p>Cglib是一个开源项目,它的底层是字节码处理框架ASM,Cglib提供了比jdk更为强大的动态代理。主要相比jdk动态代理的优势有:</p>
<ul>
<li>jdk动态代理只能基于接口,代理生成的对象只能赋值给接口变量,而Cglib就不存在这个问题,Cglib是通过生成子类来实现的,代理对象既可以赋值给实现类,又可以赋值给接口。</li>
<li>Cglib速度比jdk动态代理更快,性能更好。</li>
</ul>
<p>那何谓通过子类来实现呢?</p>
<p>还是前面那个例子,我们要实现相同的效果。代码如下</p>
<p><strong>创建CglibProxy类,用于统一代理:</strong></p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> org.springframework.cglib.proxy.Enhancer;</span><br><span class="line"><span class="keyword">import</span> org.springframework.cglib.proxy.MethodInterceptor;</span><br><span class="line"><span class="keyword">import</span> org.springframework.cglib.proxy.MethodProxy;</span><br><span class="line"><span class="keyword">import</span> java.lang.reflect.Method;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CglibProxy</span> <span class="keyword">implements</span> <span class="title class_">MethodInterceptor</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="type">Enhancer</span> <span class="variable">enhancer</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Enhancer</span>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> Object bean;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">CglibProxy</span><span class="params">(Object bean)</span> {</span><br><span class="line"> <span class="built_in">this</span>.bean = bean;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> Object <span class="title function_">getProxy</span><span class="params">()</span>{</span><br><span class="line"> <span class="comment">//设置需要创建子类的类</span></span><br><span class="line"> enhancer.setSuperclass(bean.getClass());</span><br><span class="line"> enhancer.setCallback(<span class="built_in">this</span>);</span><br><span class="line"> <span class="comment">//通过字节码技术动态创建子类实例</span></span><br><span class="line"> <span class="keyword">return</span> enhancer.create();</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">//实现MethodInterceptor接口方法</span></span><br><span class="line"> <span class="keyword">public</span> Object <span class="title function_">intercept</span><span class="params">(Object obj, Method method, Object[] args, MethodProxy proxy)</span> <span class="keyword">throws</span> Throwable {</span><br><span class="line"> <span class="type">String</span> <span class="variable">methodName</span> <span class="operator">=</span> method.getName();</span><br><span class="line"> <span class="keyword">if</span> (methodName.equals(<span class="string">"wakeup"</span>)){</span><br><span class="line"> System.out.println(<span class="string">"早安~~~"</span>);</span><br><span class="line"> }<span class="keyword">else</span> <span class="keyword">if</span>(methodName.equals(<span class="string">"sleep"</span>)){</span><br><span class="line"> System.out.println(<span class="string">"晚安~~~"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">//调用原bean的方法</span></span><br><span class="line"> <span class="keyword">return</span> method.invoke(bean,args);</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p><strong>执行代码:</strong></p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> {</span><br><span class="line"> <span class="type">CglibProxy</span> <span class="variable">proxy</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">CglibProxy</span>(<span class="keyword">new</span> <span class="title class_">Student</span>(<span class="string">"张三"</span>));</span><br><span class="line"> <span class="type">Student</span> <span class="variable">student</span> <span class="operator">=</span> (Student) proxy.getProxy();</span><br><span class="line"> student.wakeup();</span><br><span class="line"> student.sleep();</span><br><span class="line"></span><br><span class="line"> proxy = <span class="keyword">new</span> <span class="title class_">CglibProxy</span>(<span class="keyword">new</span> <span class="title class_">Doctor</span>(<span class="string">"王教授"</span>));</span><br><span class="line"> <span class="type">Doctor</span> <span class="variable">doctor</span> <span class="operator">=</span> (Doctor) proxy.getProxy();</span><br><span class="line"> doctor.wakeup();</span><br><span class="line"> doctor.sleep();</span><br><span class="line"></span><br><span class="line"> proxy = <span class="keyword">new</span> <span class="title class_">CglibProxy</span>(<span class="keyword">new</span> <span class="title class_">Dog</span>(<span class="string">"旺旺"</span>));</span><br><span class="line"> <span class="type">Dog</span> <span class="variable">dog</span> <span class="operator">=</span> (Dog) proxy.getProxy();</span><br><span class="line"> dog.wakeup();</span><br><span class="line"> dog.sleep();</span><br><span class="line"></span><br><span class="line"> proxy = <span class="keyword">new</span> <span class="title class_">CglibProxy</span>(<span class="keyword">new</span> <span class="title class_">Cat</span>(<span class="string">"咪咪"</span>));</span><br><span class="line"> <span class="type">Cat</span> <span class="variable">cat</span> <span class="operator">=</span> (Cat) proxy.getProxy();</span><br><span class="line"> cat.wakeup();</span><br><span class="line"> cat.sleep();</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>在这里用Cglib作为代理,其思路和jdk动态代理差不多。也需要把原始bean构造传入。</p>
<p>关键的代码在这里</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">//设置需要创建子类的类</span></span><br><span class="line">enhancer.setSuperclass(bean.getClass());</span><br><span class="line">enhancer.setCallback(<span class="built_in">this</span>);</span><br><span class="line"><span class="comment">//通过字节码技术动态创建子类实例</span></span><br><span class="line"><span class="keyword">return</span> enhancer.create();</span><br></pre></td></tr></table></figure>
<p>可以看到,Cglib”凭空”的创造了一个原bean的子类,并把Callback指向了this,也就是当前对象,也就是这个proxy对象。从而会调用<code>intercept</code>方法。而在<code>intercept</code>方法里,进行了附加功能的执行,最后还是调用了原始bean的相应方法。</p>
<p>在debug这个生成的代理对象时,我们也能看到,Cglib是凭空生成了原始bean的子类:</p>
<p><img src="https://cdn.jsdelivr.net/gh/HelingCode/CDN-vulkhe-Pic@master/uPic/2023/12/17/1853665691-614010e6b05ac_fix732.png" alt="1853665691-614010e6b05ac_fix732"></p>
]]></content>
<categories>
<category>Spring</category>
</categories>
<tags>
<tag>事务</tag>
</tags>
</entry>
<entry>
<title>Java多线程面试题</title>
<url>/posts/java-juc.html</url>
<content><![CDATA[<h3 id="🤣-用什么关键字修饰同步方法-stop-和suspend-方法为何不推荐使用?"><a href="#🤣-用什么关键字修饰同步方法-stop-和suspend-方法为何不推荐使用?" class="headerlink" title="🤣 用什么关键字修饰同步方法? stop()和suspend()方法为何不推荐使用?"></a>🤣 用什么关键字修饰同步方法? stop()和suspend()方法为何不推荐使用?</h3><p>用synchronized关键字修饰同步方法。反对使用stop(),是因为它不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。结果很难检查出真正的问题所在。suspend()方法容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁。此时,其他任何线程都不能访问锁定的资源,除非被”挂起”的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用suspend(),而应在自己的Thread类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()重新启动线程。</p>
<h3 id="😂-sleep-和-wait-有什么区别"><a href="#😂-sleep-和-wait-有什么区别" class="headerlink" title="😂 sleep() 和 wait() 有什么区别?"></a>😂 sleep() 和 wait() 有什么区别?</h3><p>sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,将执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。 wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。</p>
<p>sleep就是正在执行的线程主动让出cpu,cpu去执行其他线程,在sleep指定的时间过后,cpu才会回到这个线程上继续往下执行,如果当前线程进入了同步锁,sleep方法并不会释放锁,即使当前线程使用sleep方法让出了cpu,但其他被同步锁挡住了的线程也无法得到执行。wait是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了notify方法(notify并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放),该线程才能回到可运行状态等待获取锁。</p>
<h3 id="😄-同步和异步有何异同?"><a href="#😄-同步和异步有何异同?" class="headerlink" title="😄 同步和异步有何异同?"></a>😄 同步和异步有何异同?</h3><p>如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。</p>
<p>当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。</p>
<h3 id="😅-多线程有几种实现方法-同步有几种实现方法"><a href="#😅-多线程有几种实现方法-同步有几种实现方法" class="headerlink" title="😅 多线程有几种实现方法?同步有几种实现方法?"></a>😅 多线程有几种实现方法?同步有几种实现方法?</h3><p>多线程有两种实现方法,分别是继承Thread类与实现Runnable接口。</p>
<p>同步的实现方面有两种,分别是synchronized,wait与notify。</p>
<p>wait():使一个线程处于等待状态,并且释放所持有的对象的lock。</p>
<p>sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。</p>
<p>notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。 </p>
<p>notityAll():唤醒所有处于等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。</p>
<h3 id="😆-为什么我们调用-start-方法时会执行-run-方法,为什么我们不能直接调用-run-方法?"><a href="#😆-为什么我们调用-start-方法时会执行-run-方法,为什么我们不能直接调用-run-方法?" class="headerlink" title="😆 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?"></a>😆 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?</h3><p>new 一个 Thread,线程进入了新建状态。调用 <code>start()</code>方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 <code>start()</code> 会执行线程的相应准备工作,然后自动执行 <code>run()</code> 方法的内容,这是真正的多线程工作。 但是,直接执行 <code>run()</code> 方法,会把 <code>run()</code> 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。</p>
<h3 id="😶-多线程操作-static-变量会有影响吗?"><a href="#😶-多线程操作-static-变量会有影响吗?" class="headerlink" title="😶 多线程操作 static 变量会有影响吗?"></a>😶 多线程操作 static 变量会有影响吗?</h3><p>当多个线程执行同一个方法的时候,并且方法中使用了静态变量的时候,就会出现安全问题, 因为静态成员(static member)作为公共变量,是放在共享内存区域的。 多个线程共享一块内存区域,在不加任何保护情况下对其操作就会出现异常结果。</p>
<p><strong>解决方法:</strong></p>
<p>不使用共享内存,每个线程内存空间相互独立;<br>多线程共享一块内存区域,但是对这块共享区域加锁访问。对调用static变量的方法使用lock或synchronized</p>
<h3 id="🙄-为什么要使用多线程"><a href="#🙄-为什么要使用多线程" class="headerlink" title="🙄 为什么要使用多线程?"></a>🙄 为什么要使用多线程?</h3><p>从系统应用上来思考:</p>
<ul>
<li>线程可以比作是轻量级的进程,是程序执行的最小单位,线程间切换和调度的成本远远小于进程。另外,多核 CPU 时代,意味着多个线程可以同时运行,这减少了线程上下文切换的开销;</li>
<li>如今的系统,动不动就要求百万级甚至亿万级的并发量,而多线程并发编程,正是开发高并发系统的基础,利用好多线程机制,可以大大提高系统整体的并发能力以及性能。</li>
</ul>
<p>从计算机背后来探讨:</p>
<p><strong>单核时代:</strong> 在单核时代,多线程主要是为了提高 CPU 和 IO 设备的综合利用率。举个例子:当只有一个线程工作的时候,会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。可以简单地理解成,这两者的利用率最高都是 50%左右。但是当有两个线程的时候就不一样了,一个线程执行 CPU 计算时,另外一个线程就可以进行 IO 操作,这样 CPU 和 IO 设备两个的利用率就可以在理想情况下达到 100%;</p>
<p><strong>多核时代:</strong> 多核时代多线程主要是为了提高 CPU 利用率。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只有一个 CPU 核心被利用到,而创建多个线程,就可以让多个 CPU 核心被利用到,这样就提高了 CPU 的利用率。</p>
<p><strong>多线程相比单线程的优势,存在什么问题?</strong></p>
<p>多线程适用场景</p>
<p> 1)存在需要等待IO、网络或其他外部资源的任务。当前等待其他资源却依旧占用CPU的线程可让出CPU,让其他线程执行,大大提高了程序效率,充分利用了CPU资源。</p>
<p> 2)存在长时间占用CPU的任务。CPU以时间片为单位分配给各个线程,一个周期内各个线程都可以得到执行,而非卡在一个线程。而且多线程意味着分配到的CPU时间片也更多。</p>
<p>多线程弊端</p>
<p> 1)访问共享资源时要小心,需要更多的锁资源,同步更加复杂。</p>
<p> 2)<a href="https://so.csdn.net/so/search?q=内存&spm=1001.2101.3001.7020">内存</a>占用更多,资源开销更大。</p>
<p> 3)需要额外的线程调度和管理。如需要CPU时间来跟踪线程。</p>
<h3 id="👽-线程的基本概念、线程的基本状态以及状态之间的关系。"><a href="#👽-线程的基本概念、线程的基本状态以及状态之间的关系。" class="headerlink" title="👽 线程的基本概念、线程的基本状态以及状态之间的关系。"></a>👽 线程的基本概念、线程的基本状态以及状态之间的关系。</h3><p>一个程序中可以有多条执行线索同时执行,一个线程就是程序中的一条执行线索,每个线程上都关联有要执行的代码,即可以有多段程序代码同时运行,每个程序至少都有一个线程,即main方法执行的那个线程。如果只是一个cpu,从宏观上来看,cpu一会执行a线程,一会执行b线程,切换时间很快,给人的感觉是a,b在同时执行。</p>
<p>状态:就绪,运行,synchronize阻塞,wait和sleep挂起,结束。wait必须在synchronized内部调用。</p>
<p>调用线程的start方法后线程进入就绪状态,线程调度系统将就绪状态的线程转为运行状态,遇到synchronized语句时,由运行状态转为阻塞,当synchronized获得锁后,由阻塞转为运行,在这种情况可以调用wait方法转为挂起状态,当线程关联的代码执行完后,线程变为结束状态。</p>
<h3 id="👽-Java的线程模型"><a href="#👽-Java的线程模型" class="headerlink" title="👽 Java的线程模型"></a>👽 Java的线程模型</h3><p>1.用户线程与内核级线程</p>
<p>线程的实现可以分为两类:用户级线程(User-LevelThread, ULT)和内核级线程(Kemel-LevelThread, KLT)。用户线程由用户代码支持,内核线程由操作系统内核支持。</p>
<p>2.并发与并行</p>
<p><strong>并发:</strong>一个时间段内有很多的线程或进程在执行,但任何时间点上都只有一个在执行,多个线程或进程争抢时间片轮流执行。<br><strong>并行:</strong>一个时间段和时间点上都有多个线程或进程在执行。</p>
<p>3.多线程模型</p>
<p>多线程模型即用户级线程和内核级线程的不同连接方式,线程模型影响着并发规模及操作成本(开销)。</p>
<h3 id="👽-线程与进程区别?"><a href="#👽-线程与进程区别?" class="headerlink" title="👽 线程与进程区别?"></a>👽 线程与进程区别?</h3><p>进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。</p>
<p>线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的<strong>堆</strong>和<strong>方法区</strong>资源,但每个线程有自己的<strong>程序计数器</strong>、<strong>虚拟机栈</strong>和<strong>本地方法栈</strong>,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。</p>
<p><strong>线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。</strong></p>
<h3 id="👽-程序计数器为什么线程是私有的"><a href="#👽-程序计数器为什么线程是私有的" class="headerlink" title="👽 程序计数器为什么线程是私有的?"></a>👽 程序计数器为什么线程是私有的?</h3><p>程序计数器主要有下面两个作用:</p>
<ol>
<li>字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。</li>
<li>在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。</li>
</ol>
<p>需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。</p>
<p>所以,程序计数器私有主要是为了<strong>线程切换后能恢复到正确的执行位置</strong>。</p>
<h3 id="👽-虚拟机栈和本地方法栈为什么线程是私有的"><a href="#👽-虚拟机栈和本地方法栈为什么线程是私有的" class="headerlink" title="👽 虚拟机栈和本地方法栈为什么线程是私有的?"></a>👽 虚拟机栈和本地方法栈为什么线程是私有的?</h3><ul>
<li><strong>虚拟机栈:</strong> 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。</li>
<li><strong>本地方法栈:</strong> 和虚拟机栈所发挥的作用非常相似,区别是: <strong>虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。</strong> 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。</li>
</ul>
<p>所以,为了<strong>保证线程中的局部变量不被别的线程访问到</strong>,虚拟机栈和本地方法栈是线程私有的。</p>
<h3 id="👽-什么是线程上下文切换"><a href="#👽-什么是线程上下文切换" class="headerlink" title="👽 什么是线程上下文切换?"></a>👽 什么是线程上下文切换?</h3><p>线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。</p>
<ul>
<li>主动让出 CPU,比如调用了 <code>sleep()</code>, <code>wait()</code> 等。</li>
<li>时间片用完,因为操作系统要防止一个线程或者进程长时间占用CPU导致其他线程或者进程饿死。</li>
<li>调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。</li>
<li>被终止或结束运行</li>
</ul>
<p>这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的<strong>上下文切换</strong>。</p>
<p>上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。</p>
<h3 id="👽-Java多线程编程有几种线程间通信方式"><a href="#👽-Java多线程编程有几种线程间通信方式" class="headerlink" title="👽 Java多线程编程有几种线程间通信方式"></a>👽 Java多线程编程有几种线程间通信方式</h3><ol>
<li><p><strong>共享内存法</strong></p>
<p>volatile,synchronized</p>
</li>
<li><p><strong>wait/notify机制</strong></p>
<p>来自Object类的方法。当满足某种情况时A线程调用wait()方法放弃CPU时间片,并进入阻塞状态。当满足某种条件时,B线程调用notify()方法通知A线程。唤醒A线程,并让它进入可运行状态。</p>
</li>
<li><p><strong>Lock/Condition机制</strong></p>
<p>Condition是Java提供来实现<strong>等待/通知</strong>的类,Condition类还提供比wait/notify更丰富的功能,Condition对象是由lock对象所创建的。但是同一个锁可以创建多个Condition的对象,即创建多个对象监视器。这样的好处就是可以指定唤醒线程。notify唤醒的线程是随机唤醒一个。</p>
</li>
</ol>
<h3 id="😎-volatile如何实现内存可见性?"><a href="#😎-volatile如何实现内存可见性?" class="headerlink" title="😎 volatile如何实现内存可见性?"></a>😎 volatile如何实现内存可见性?</h3><p><strong>volatile为什么会出现:</strong></p>
<p>首先先分析一下没有volatile的情况下线程在自己的私有内存中对共享变量做出了改变之后无法及时告知其他线程,这就是volatile的作用,解决内存可见性问题。这种问题用synchronized关键字可以解决。但是一个问题是synchronized是重量级锁,同一时间内只允许一个线程去操作共享变量。操作完成之后再将改变后的变量值刷新回共享内存空间中。这样一来的话并发性就没有了。而且<strong>synchronized关键词的使用基于操作系统实现</strong>,会使得线程从用户态陷入内核态。这一步是很耗时间的。于是volatile应运而生。它是一个轻量级的synchronized。只是用来解决内存可见性问题的。</p>
<p><strong>1、volatile可见性实现原理:</strong></p>
<p>变量被volatile关键字修饰后,底层<strong>汇编指令</strong>中会出现一个<strong>lock前缀指令</strong>。会导致以下两种事情的发生:</p>
<ol>
<li>修改volatile变量时会<strong>强制</strong>将修改后的值刷新到主内存中。</li>
<li>修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新读取主内存中的值。</li>
</ol>
<p><strong>2、volatile有序性实现原理:</strong></p>
<p><strong>指令重排序:</strong>编译器在不改变单线程程序语义的前提下,会重新安排语句的执行顺序,指令重排序在单线程下不会有问题,但是在多线程下,可能会出现问题。</p>
<p>volatile有序性的保证就是通过<strong>禁止指令重排序</strong>来实现的。指令重排序包括编译器和处理器重排序,JMM会分别限制这两种指令重排序。禁止指令重排序又是通过加<strong>内存屏障</strong>实现的。</p>
<blockquote>
<p>内存屏障(memory barriers):也叫内存栅栏,是一种 CPU 指令,用于控制特定条件下的重排序和内存可见性问题。</p>
<ul>
<li>LoadLoad 屏障:对于这样的语句 Load1; LoadLoad; Load2,在 Load2 及后续读取操作要读取的数据被访问前,保证 Load1 要读取的数据被读取完毕。</li>
<li>StoreStore 屏障: 对于这样的语句 Store1; StoreStore; Store2,在 Store2 及后续写入操作执行前,保证 Store1 的写入操作对其它处理器可见。</li>
<li>LoadStore 屏障:对于这样的语句 Load1; LoadStore; Store2,在 Store2 及后续写入操作被刷出前,保证 Load1 要读取的数据被读取完毕。</li>
<li>StoreLoad 屏障:对于这样的语句 Store1; StoreLoad; Load2,在 Load2 及后续所有读取操作执行前,保证 Store1 的写入对所有处理器可见。它的开销是四种屏障中最大的。 在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。</li>
</ul>
</blockquote>
<p>添加了volatile关键字可以避免半初始化的指令重排。</p>
<h3 id="😎-volatile为什么不保证原子性?"><a href="#😎-volatile为什么不保证原子性?" class="headerlink" title="😎 volatile为什么不保证原子性?"></a>😎 volatile为什么不保证原子性?</h3><p>Java中只有对变量的赋值和读取是原子性的,其他的操作都不是原子性的。所以即使volatile即使能保证被修饰的变量具有可见性,但是不能保证原子性。</p>
<h3 id="😎-DCL单例为什么需要加volatile-(半初始化的指令重排)?"><a href="#😎-DCL单例为什么需要加volatile-(半初始化的指令重排)?" class="headerlink" title="😎 DCL单例为什么需要加volatile (半初始化的指令重排)?"></a>😎 DCL单例为什么需要加volatile (半初始化的指令重排)?</h3><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Singleton</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">volatile</span> <span class="keyword">static</span> Singleton uniqueInstance;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">Singleton</span><span class="params">()</span> {</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> Singleton <span class="title function_">getUniqueInstance</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">//先判断对象是否已经实例过,没有实例化过才进入加锁代码</span></span><br><span class="line"> <span class="keyword">if</span> (uniqueInstance == <span class="literal">null</span>) {</span><br><span class="line"> <span class="comment">//类对象加锁</span></span><br><span class="line"> <span class="keyword">synchronized</span> (Singleton.class) {</span><br><span class="line"> <span class="keyword">if</span> (uniqueInstance == <span class="literal">null</span>) {</span><br><span class="line"> uniqueInstance = <span class="keyword">new</span> <span class="title class_">Singleton</span>();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> uniqueInstance;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p><code>uniqueInstance = new Singleton();</code> 这段代码其实是分为三步执行:</p>
<ol>
<li>为 <code>uniqueInstance</code> 分配内存空间</li>
<li>初始化 <code>uniqueInstance</code></li>
<li>将 <code>uniqueInstance</code> 指向分配的内存地址</li>
</ol>
<p>但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 <code>getUniqueInstance</code>() 后发现 <code>uniqueInstance</code> 不为空,因此返回 <code>uniqueInstance</code>,但此时 <code>uniqueInstance</code> 还未被初始化。 使用 <code>volatile</code> 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。</p>
<p>volatile两个作用:保持线程可见性;<strong>禁止指令重排序</strong>。</p>
<p><strong>DCL单例需要加volatile,来禁止指令重排。</strong></p>
<p>由于java编译器允许处理器乱序执行(以获得最优的性能),new对象的操作不是原子性的。这句代码最终会被编译成多条汇编指令。所以需要volatile关键字来禁止指令重排。</p>
<p><strong>创建一个对象的过程中一旦出现了指令重排,可能就会获得半初始化的对象,</strong>即还没来得及赋值就先建立了引用关系。要避免这种情况的发生就要使用volatile关键字修饰实列变量。</p>
<p><strong>第一次判断singleton是否为null</strong><br> 第一次判断是在Synchronized同步代码块外进行判断,由于单例模式只会创建一个实例,并通过getInstance方法返回singleton对象,所以,第一次判断,是为了在singleton对象已经创建的情况下,避免进入同步代码块,提升效率。</p>
<p><strong>第二次判断singleton是否为null</strong><br> 第二次判断是为了避免以下情况的发生。<br> (1)假设:线程A已经经过第一次判断,判断singleton=null,准备进入同步代码块.<br> (2)此时线程B获得时间片,由于线程A并没有创建实例,所以,判断singleton仍然=null,所以线程B创建了实例singleton。<br> (3)此时,线程A再次获得时间片,由于刚刚经过第一次判断singleton=null(不会重复判断),进入同步代码块,这个时候,我们如果不加入第二次判断的话,那么线程A又会创造一个实例singleton,就不满足我们的单例模式的要求,所以第二次判断是很有必要的。</p>
<h3 id="😎-happen-before原则"><a href="#😎-happen-before原则" class="headerlink" title="😎 happen-before原则"></a>😎 happen-before原则</h3><ul>
<li>单线程 happen-before 原则:在同一个线程中,书写在前面的操作 happen- before 后面的操作。 锁的 happen-before 原则:同一个锁的 unlock 操作 happen-before 此锁的 lock 操作。</li>
<li>volatile 的 happen-before 原则:对一个 volatile 变量的写操作 happen- before 对此变量的任意操作(当然也包括写操作了)。</li>
<li>happen-before 的传递性原则:如果 A 操作 happen-before B 操作,B 操作 happen-before C 操作,那么 A 操作 happen-before C 操作。</li>
<li>线程启动的 happen-before 原则:同一个线程的 start 方法 happen-before 此线程的其它方法。</li>
<li>线程中断的 happen-before 原则 :对线程 interrupt 方法的调用 happen- before 被中断线程的检测到中断发送的代码。</li>
<li>线程终结的 happen-before 原则: 线程中的所有操作都 happen-before 线程的终止检测。</li>
<li>对象创建的 happen-before 原则: 一个对象的初始化完成先于他的 finalize 方法调用。</li>
</ul>
<h3 id="👻-Thread-Local-作用、原理、内存泄漏问题?"><a href="#👻-Thread-Local-作用、原理、内存泄漏问题?" class="headerlink" title="👻 Thread Local 作用、原理、内存泄漏问题?"></a>👻 Thread Local 作用、原理、内存泄漏问题?</h3><p><strong>作用:</strong></p>
<p><code>ThreadLocal</code>为解决多线程下的线程安全问题提供了一个新思路,它通过为每一个线程提供一个独立的变量副本解决了线程并发访问共享变量出现的安全问题。在很多情况下<code>ThreadLocal</code>比直接使用synchronized同步机制解决线程安全问题更加方便、简洁。且拥有更加高的并发性。</p>
<p><strong>原理:</strong></p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> T <span class="title function_">get</span><span class="params">()</span> { }</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">set</span><span class="params">(T value)</span> { }</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">remove</span><span class="params">()</span> { }</span><br><span class="line"><span class="keyword">protected</span> T <span class="title function_">initialValue</span><span class="params">()</span> { }</span><br></pre></td></tr></table></figure>
<ol>
<li>在每个线程Thread内部有一个<code>ThreadLocal.ThreadLocalMap</code>类型的成员变量<code>threadLocals</code>,这个<code>threadLocals</code>就是用来<strong>存储实际的变量副本</strong>的,键值为当前<code>ThreadLocal</code>的引用,value为变量副本(即T类型的变量)。</li>
<li>初始时,在Thread里面,<code>threadLocals</code>为空,当通过<code>ThreadLocal</code>变量调用get()方法或者set()方法,就会对Thread类中的<code>threadLocals</code>进行初始化,并且以当前<code>ThreadLocal</code>对象引用为键值,以<code>ThreadLocal</code>要保存的副本变量为value,存到<code>threadLocals</code>。</li>
<li>然后在当前线程里面,如果要使用副本变量,就可以通过get()方法在<code>threadLocals</code>里面查找。</li>
</ol>
<p><strong><code>ThreadLocal</code>内存泄漏</strong></p>
<p>由于<code>ThreadLocalMap</code>的key是弱引用,而<code>Value</code>是强引用。这就导致了一个问题,<code>ThreadLocal</code>在没有外部对象强引用时,发生GC时(无论是否OOM)弱引用Key会被回收。这个时候就会出现Entry中Key已经被回收,出现一个<code>null Key</code>的情况,外部读取<code>ThreadLocalMap</code>中的元素是无法通过null Key来找到Value的。因此如果当前线程的生命周期很长,一直存在,那么其内部的<code>ThreadLocalMap</code>对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:<code>Thread --> ThreadLocalMap-->Entry-->Value</code>,这条强引用链会导致<code>Entry</code>不会回收, <code>Value</code>也不会回收,但Entry中的Key却已经被回收的情况,造成<strong>内存泄漏</strong>。</p>
<p><strong>解决办法:</strong>每次使用完<code>ThreadLocal</code>,都调用它的<code>remove()</code>方法,清除数据。</p>
<p><strong><code>ThreadLocal</code>应用场景</strong></p>
<p>最常见的<code>ThreadLocal</code>使用场景为用来解决数据库连接、Session管理等。</p>
<h3 id="😎🤢-说说-synchronized-关键字和-volatile-关键字的区别"><a href="#😎🤢-说说-synchronized-关键字和-volatile-关键字的区别" class="headerlink" title="😎🤢 说说 synchronized 关键字和 volatile 关键字的区别"></a>😎🤢 说说 synchronized 关键字和 volatile 关键字的区别</h3><p><code>synchronized</code> 关键字和 <code>volatile</code> 关键字是两个互补的存在,而不是对立的存在!</p>
<ul>
<li><strong><code>volatile</code> 关键字</strong>是线程同步的<strong>轻量级实现</strong>,所以 <strong><code>volatile</code>性能肯定比<code>synchronized</code>关键字要好</strong> 。但是 <strong><code>volatile</code> 关键字只能用于变量而 <code>synchronized</code> 关键字可以修饰方法以及代码块</strong> 。</li>
<li><strong><code>volatile</code> 关键字能保证数据的可见性,但不能保证数据的原子性。<code>synchronized</code> 关键字两者都能保证。</strong></li>
<li><strong><code>volatile</code>关键字主要用于解决变量在多个线程之间的可见性,而 <code>synchronized</code> 关键字解决的是多个线程之间访问资源的同步性。</strong></li>
</ul>
<h3 id="😀-自旋锁-VS-适应性自旋锁"><a href="#😀-自旋锁-VS-适应性自旋锁" class="headerlink" title="😀 自旋锁 VS 适应性自旋锁"></a>😀 自旋锁 VS 适应性自旋锁</h3><p>阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。</p>
<p>在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。</p>
<p>而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是<strong>自旋锁</strong>。</p>
<p><img src="https://cdn.jsdelivr.net/gh/HelingCode/CDN-vulkhe-Pic@master/uPic/2023/12/17/452a3363-20231217152835634.png" alt="img"></p>
<p>自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。</p>
<p>自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。</p>
<p><img src="https://cdn.jsdelivr.net/gh/HelingCode/CDN-vulkhe-Pic@master/uPic/2023/12/17/83b3f85e.png" alt="img"></p>
<p>自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。</p>
<p><strong>自适应</strong>意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。</p>
<h3 id="🤢-Synchronized与ReentrantLock的区别?"><a href="#🤢-Synchronized与ReentrantLock的区别?" class="headerlink" title="🤢 Synchronized与ReentrantLock的区别?"></a>🤢 Synchronized与ReentrantLock的区别?</h3><p><strong>实现原理上:</strong></p>
<p><code>synchronized</code>是依靠<code>jvm</code>以及配合操作系统来实现,是一个<strong>关键字</strong>。<code>reentrantLock</code>是<code>jdk1.5</code>之后提供的<strong>API层面</strong>的互斥锁,实现了Lock接口。</p>
<p><strong>使用便利性上:</strong></p>
<p><code>synchronized</code>只需要添加上相关关键字即可,加锁与释放过程由操作系统完成。<code>reentrantLock</code>则需要手动加锁与释放锁。</p>
<p><strong>锁粒度与灵活度:</strong></p>
<p>reentrantLock<code>要强于</code>synchronized</p>
<p><code>reentrantLock</code>提供了<strong>三个高级功能</strong>:</p>
<ol>
<li><strong>等待可中断</strong>,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相对于Synchronized来说可以避免出现死锁的情况。通过<code>lock.lockInterruptibly()</code>来实现这个机制。</li>
<li>Synchronized锁是非公平锁,<code>ReentrantLock</code>默认的构造函数是创建的非公平锁,但可以通过参数true设为公平锁,但公平锁表现的性能不是很好。</li>
<li>一个<code>ReentrantLock</code>对象可以同时绑定多个对象。<code>ReenTrantLock</code>提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。</li>
</ol>
<p><strong>性能区别:</strong></p>
<p><code>synchronized</code>优化之后性能与<code>reentrantLock</code>已经不相上下了,官方甚至更建议使用<code>synchronized</code>关键字。</p>
<h3 id="🤢-synchronized修饰的对象"><a href="#🤢-synchronized修饰的对象" class="headerlink" title="🤢 synchronized修饰的对象"></a>🤢 synchronized修饰的对象</h3><ul>
<li>修饰一个类:其作用的范围是synchronized后面括号括起来的部分,<strong>作用的对象是这个类的所有对象</strong>;</li>
<li>修饰一个方法:被修饰的方法称为同步方法,其作用的范围是整个方法,<strong>作用的对象是调用这个方法的对象</strong>;</li>
<li>修饰一个静态的方法:其作用的范围是整个方法,<strong>作用的对象是这个类的所有对象</strong>;</li>
<li>修饰一个代码块:被修饰的代码块称为同步语句块,其作用范围是大括号{}括起来的代码块,<strong>作用的对象是调用这个代码块的对象</strong>;</li>
</ul>
<h3 id="🤢-Synchronized实现原理?"><a href="#🤢-Synchronized实现原理?" class="headerlink" title="🤢 Synchronized实现原理?"></a>🤢 Synchronized实现原理?</h3><p><strong><code>synchronized</code> 关键字解决的是多个线程之间访问资源的同步性,<code>synchronized</code>关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。</strong></p>
<p>每个对象(在对象头中)有一个<strong>监视器锁</strong><code>(monitor)</code>,当monitor被占用时就处于锁定状态。线程执行<code>monitorenter</code>(汇编指令)尝试获取monitor的所有权。实现原子性操作和解决共享变量的内存可见性问题。</p>
<ol>
<li>如果monitor计数器当前值为0,那么该线程进入monitor并将计数器加1,</li>
<li>如果当前monitor计数器值不为0,那么该线程阻塞并进入(OS维护的)队列等待,等到OS的调度。</li>
</ol>
<p>底层字节码被编译成<code>monitorenter</code>和<code>monitorexit</code>两个指令。线程执行<code>monitorexit</code>指令,monitor计数器减1,如果减到0了,表示当前线程不再拥有该监视器锁。等待队列中的线程有机会获得锁资源。</p>
<p><strong>内部处理过程</strong>(内部有两个队列waitSet和entryList):</p>
<ul>
<li>1、当多个线程进入同步代码块时,首先进入entryList</li>
<li>2、有一个线程获取到monitor锁后,就将对象头的Mark Word中的线程ID设置为当前线程,并且计数器+1</li>
<li>3、如果线程调用wait方法,将释放锁,当前线程ID置为null,计数器-1,同时进入waitSet等待被唤醒,调用notify或者notifyAll之后又会进入entryList竞争锁</li>
<li>4、如果线程执行完毕,同样释放锁,计数器-1,当前线程置为null</li>
</ul>
<p><img src="https://cdn.jsdelivr.net/gh/HelingCode/CDN-vulkhe-Pic@master/uPic/2023/12/17/image-20220701233410657-20231217152836086.png" alt="image-20220701233410657"></p>
<p>在synchronized大(优化)升级之前,是重量级锁,锁操作都要经过OS。向OS内核去申请。(<code>jdk1.5之后</code>)到现在的synchronized是有一个复杂的锁升级过程。</p>
<p><strong>无锁 -> 偏向锁 -> 自旋锁(轻量级锁) -> (重量级锁)悲观锁。</strong></p>
<p>以上的升级状态都记录在对象头中。</p>
<p><strong>偏向锁:</strong>hotspot虚拟机认为大多数时间是不存在锁竞争的,所以每次都会把锁分配给上一次获得锁的线程,直到出现了锁竞争。</p>
<p><strong>自旋锁:</strong>线程之间以CAS的方式进行锁资源的争抢。当一个线程自旋超过了10次或者当前自旋等待的线程超过了CPU核数的1/2(升级后优化为自适应自旋),会进行锁升级。</p>
<p><strong>synchronized:</strong> 向OS申请资源,从用户态切换到内核态。线程挂起进入<strong>等待队列</strong>,等待OS的调度。然后再映射回用户空间。</p>
<h3 id="🤢-获取对象的锁,这个“锁”到底是什么?如何确定对象的锁?"><a href="#🤢-获取对象的锁,这个“锁”到底是什么?如何确定对象的锁?" class="headerlink" title="🤢 获取对象的锁,这个“锁”到底是什么?如何确定对象的锁?"></a>🤢 获取对象的锁,这个“锁”到底是什么?如何确定对象的锁?</h3><p><code>synchronized</code> 同步语句块的实现使用的是 <code>monitorenter</code> 和 <code>monitorexit</code> 指令,其中 <code>monitorenter</code> 指令指向同步代码块的开始位置,<code>monitorexit</code> 指令则指明同步代码块的结束位置。当执行 <code>monitorenter</code> 指令时,线程试图获取锁也就是获取 <strong>对象监视器 <code>monitor</code></strong> 的持有权。</p>
<p>在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由ObjectMonitor 实现的。每个对象中都内置了一个 <code>ObjectMonitor</code>对象。</p>
<p>另外,<code>wait/notify</code>等方法也依赖于<code>monitor</code>对象,这就是为什么只有在同步的块或者方法中才能调用<code>wait/notify</code>等方法,否则会抛出<code>java.lang.IllegalMonitorStateException</code>的异常的原因。</p>
<p>在执行<code>monitorenter</code>时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。对象锁的的拥有者线程才可以执行 <code>monitorexit</code> 指令来释放锁。在执行 <code>monitorexit</code> 指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。</p>
<p><a href="https://mp.weixin.qq.com/s/VUwexGERUjTeMnDEpRMB3g">一文全面梳理各种锁机制</a></p>
<h3 id="🤢-Synchronized的锁升级过程"><a href="#🤢-Synchronized的锁升级过程" class="headerlink" title="🤢 Synchronized的锁升级过程"></a>🤢 Synchronized的锁升级过程</h3><p>首先为什么Synchronized能实现线程同步?</p>
<p>在回答这个问题之前我们需要了解两个重要的概念:“Java对象头”、“Monitor”。</p>
<p><strong>Java对象头</strong>:</p>
<p>synchronized是悲观锁,在操作同步资源之前需要给同步资源先加锁,这把锁就是存在Java对象头里的,而Java对象头又是什么呢?</p>
<p>我们以Hotspot虚拟机为例,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。</p>
<p><strong>Mark Word</strong>:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。</p>
<p><strong>Klass Point</strong>:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。</p>
<p><strong>Monitor:</strong></p>
<p>Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。</p>
<p>Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。</p>
<p>synchronized通过Monitor来实现线程同步,Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。</p>
<p>如同我们在自旋锁中提到的“阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长”。这种方式就是synchronized最初实现同步的方式,这就是JDK 6之前synchronized效率低的原因。这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”,JDK 6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。</p>
<p>所以目前锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。</p>
<p>通过上面的介绍,我们对synchronized的加锁机制以及相关知识有了一个了解,那么下面我们给出四种锁状态对应的的Mark Word内容,然后再分别讲解四种锁状态的思路以及特点:</p>
<p><img src="https://cdn.jsdelivr.net/gh/HelingCode/CDN-vulkhe-Pic@master/uPic/2023/12/17/image-20220707164758951.png" alt="image-20220707164758951"></p>
<p><strong>无锁</strong></p>
<p>无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。</p>
<p>无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。</p>
<p><strong>偏向锁</strong></p>
<p>偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。</p>
<p>在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。</p>
<p>当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。</p>
<p>偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态。</p>
<p>偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。</p>
<p><strong>轻量级锁</strong></p>
<p>是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。</p>
<p>在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的Mark Word复制到锁记录中。</p>
<p>拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。</p>
<p>如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。</p>
<p>如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。</p>
<p>若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。</p>
<p><strong>重量级锁</strong></p>
<p>升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。</p>
<p>整体的锁状态升级流程如下:</p>
<p><img src="https://cdn.jsdelivr.net/gh/HelingCode/CDN-vulkhe-Pic@master/uPic/2023/12/17/8afdf6f2-20231217152836445.png" alt="img"></p>
<p>综上,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。</p>
<h3 id="🤢-为什么说-Synchronized-是可重入锁?"><a href="#🤢-为什么说-Synchronized-是可重入锁?" class="headerlink" title="🤢 为什么说 Synchronized 是可重入锁?"></a>🤢 为什么说 Synchronized 是可重入锁?</h3><p><strong>“可重入锁”</strong> 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。</p>
<p><img src="https://cdn.jsdelivr.net/gh/HelingCode/CDN-vulkhe-Pic@master/uPic/2023/12/17/image-20220309201446490.png" alt="image-20220309201446490"></p>
<h3 id="🤢-synchronized可重入锁的实现?"><a href="#🤢-synchronized可重入锁的实现?" class="headerlink" title="🤢 synchronized可重入锁的实现?"></a>🤢 synchronized可重入锁的实现?</h3><p>重入锁实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。</p>
<h3 id="🤢-为什么说-Synchronized-是非公平锁"><a href="#🤢-为什么说-Synchronized-是非公平锁" class="headerlink" title="🤢 为什么说 Synchronized 是非公平锁?"></a>🤢 为什么说 Synchronized 是非公平锁?</h3><p>Synchronized底层是调用mutex锁的,内核提供的这个锁并不保证公平。而Java所提供的公平锁Lock实际上是由Java的API支持的(即对AQS的实现)</p>
<p>非公平是指在获取锁的行为上,并不是按照线程申请顺序进行分配的,当锁被释放后,所有线程都有机会获取到锁,这样提高了性能,但是可能会出现某些线程饥饿的情况。</p>
<h3 id="🤢-当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法"><a href="#🤢-当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法" class="headerlink" title="🤢 当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法?"></a>🤢 当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法?</h3><p>分几种情况:</p>
<p>1)其他方法前是否加了synchronized关键字,如果没加,则能。</p>
<p>2)如果这个方法内部调用了wait,则可以进入其他synchronized方法。</p>
<p>3)如果其他个方法都加了synchronized关键字,并且内部没有调用wait,则不能。</p>
<p>4)如果其他方法是static,它用的同步锁是当前类的字节码,与非静态的方法不能同步,因为非静态的方法用的是this。</p>
<h3 id="🤢-为什么说-Synchronized-是一个悲观锁?乐观锁的实现原理又是什么?"><a href="#🤢-为什么说-Synchronized-是一个悲观锁?乐观锁的实现原理又是什么?" class="headerlink" title="🤢 为什么说 Synchronized 是一个悲观锁?乐观锁的实现原理又是什么?"></a>🤢 为什么说 Synchronized 是一个悲观锁?乐观锁的实现原理又是什么?</h3><p>当 Synchronized升级为重量级锁时,他是一个悲观锁。<strong>获取不到锁资源的线程由OS统一管理</strong>,涉及到用户态到内核态的切换。</p>
<p>乐观锁就是,当一个线程想要对变量进行操作时,先读取变量值,然后真正更改时会再次对比当前值与自己之前读取的值是否相同,相同才会进行更改,不相同的话就会再次读取,然后在进行对比更改。主要是基于CAS实现。</p>
<h3 id="😡-悲观锁和乐观锁的区别"><a href="#😡-悲观锁和乐观锁的区别" class="headerlink" title="😡 悲观锁和乐观锁的区别"></a>😡 悲观锁和乐观锁的区别</h3><p><a href="https://www.cnblogs.com/kismetv/p/10787228.html#:~:text=%E4%B9%90%E8%A7%82%E9%94%81%EF%BC%9A%E4%B9%90%E8%A7%82%E9%94%81%E5%9C%A8,%E5%88%AB%E4%BA%BA%E4%BC%9A%E5%90%8C%E6%97%B6%E4%BF%AE%E6%94%B9%E6%95%B0%E6%8D%AE%E3%80%82">https://www.cnblogs.com/kismetv/p/10787228.html#:~:text=%E4%B9%90%E8%A7%82%E9%94%81%EF%BC%9A%E4%B9%90%E8%A7%82%E9%94%81%E5%9C%A8,%E5%88%AB%E4%BA%BA%E4%BC%9A%E5%90%8C%E6%97%B6%E4%BF%AE%E6%94%B9%E6%95%B0%E6%8D%AE%E3%80%82</a></p>
<p>乐观锁和悲观锁是两种思想,用于解决并发场景下的数据竞争问题。</p>
<ul>
<li>乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。</li>
<li>悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。</li>
</ul>
<h3 id="😡-MySQL如何实现乐观锁?"><a href="#😡-MySQL如何实现乐观锁?" class="headerlink" title="😡 MySQL如何实现乐观锁?"></a>😡 MySQL如何实现乐观锁?</h3><p>乐观锁认为数据一般情况下不会造成冲突,只有当数据去执行修改情况时,才会针对数据冲突做处理。这里是如何发现冲突了呢?常规的方式,都是在数据行上加一个版本号或者时间戳等字段。(本文使用version作为版本号方式,使用时间戳方式同理)</p>
<p>乐观锁的实现原理:</p>
<ol>
<li>一个事务在读取数据时,将对应的版本号字段读取出来,假设此时的版本号是1。</li>
<li>另外一个事务也是执行同样的读取操作。当事务一提交时,对版本号执行+1,此时该数据行的版本号就是2。</li>
<li>第二个事务执行修改操作时,针对业务数据做条件,并默认增加一个版本号作为where条件。此时修改语句中的版本号字段是不满足where条件,该事务执行失败。通过这种方式来达到锁的功能。</li>
</ol>
<p>悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。</p>
<p>乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。</p>
<h3 id="🧑-CAS原理"><a href="#🧑-CAS原理" class="headerlink" title="🧑 CAS原理"></a>🧑 CAS原理</h3><p>CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。</p>
<p>CAS算法涉及到三个操作数:</p>
<ul>
<li>需要读写的内存值 V。</li>
<li>进行比较的值 A。</li>
<li>要写入的新值 B。</li>
</ul>
<p>当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。</p>
<p>CAS具有原子性,他的原子性由CPU保证,由JNI调用c++硬件代码实现,<code>jdk</code>中提供了unsafe来进行这些操作。</p>
<h3 id="🧑-CAS乐观锁有什么缺点?"><a href="#🧑-CAS乐观锁有什么缺点?" class="headerlink" title="🧑 CAS乐观锁有什么缺点?"></a>🧑 CAS乐观锁有什么缺点?</h3><ol>
<li>乐观锁的情况下,如果线程并发度确实很高,那么大多数的线程都会处于自旋等待以获取锁对象的状态。这样会导致CPU占用过高。</li>
<li>CAS另一个缺点就是ABA问题。一个值从A改为B又改为A,则CAS认为没有发生变化,解决的方式是使用<strong>版本号</strong>来记录操作次数。或者使用Java中提供的AtomicStampedReference,增加了标志字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部满足条件才会更新</li>
<li>只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。<ul>
<li>Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。</li>
</ul>
</li>
</ol>
<p><a href="https://mp.weixin.qq.com/s/ID6_DQAuv6ire3u58XpSrg">CAS原理分析,解决银行转账ABA难题</a></p>
<h3 id="🐱-什么是锁消除和锁粗化?"><a href="#🐱-什么是锁消除和锁粗化?" class="headerlink" title="🐱 什么是锁消除和锁粗化?"></a>🐱 什么是锁消除和锁粗化?</h3><p>锁消除即删除不必要的加锁操作。JVM在运行时,对一些“在代码上要求同步,但是<strong>被检测到不可能存在共享数据竞争情况”的锁进行消除</strong>。根据代码逃逸技术,如果判断到一段代码中,<strong>堆上的数据不会逃逸出当前线程</strong>,那么就可以认为这段代码是线程安全的,无需加锁。</p>
<p>下面代码示例:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">LockClearTest</span> {</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> {</span><br><span class="line"> <span class="type">LockClearTest</span> <span class="variable">test</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">LockClearTest</span>();</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i < <span class="number">100000</span>; i++) {</span><br><span class="line"> test.append(<span class="string">"aaa"</span>, <span class="string">"bbb"</span>);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">append</span><span class="params">(String str1, String str2)</span> {</span><br><span class="line"> </span><br><span class="line"> <span class="type">StringBuffer</span> <span class="variable">stringBuffer</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">StringBuffer</span>();</span><br><span class="line"> stringBuffer.append(str1).append(str2);</span><br><span class="line"> }</span><br></pre></td></tr></table></figure>
<p>StringBuffer的append代码如下:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"> <span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">synchronized</span> StringBuffer <span class="title function_">append</span><span class="params">(String str)</span> {</span><br><span class="line"> toStringCache = <span class="literal">null</span>;</span><br><span class="line"> <span class="built_in">super</span>.append(str);</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">this</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>从上述代码看出来,StringBuffer的append是个同步方法,但是LockClearTest中的 StringBuffer 属于一个局部变量,不可能从该方法中逃逸出去(即stringBuffer的引用没有传递到该方法外,不会被其他线程引用),因此其实这过程是线程安全的,可以将锁消除。</p>
<p>假设一系列的连续操作都会<strong>对同一个对象反复加锁及解锁</strong>,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。</p>
<p>如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会<strong>扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。</strong></p>
<p>下面代码示例:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">StringBufferTest</span> {</span><br><span class="line"> </span><br><span class="line"> <span class="type">StringBuffer</span> <span class="variable">stringBuffer</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">StringBuffer</span>();</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">append</span><span class="params">()</span>{</span><br><span class="line"> stringBuffer.append(<span class="string">"a"</span>).append(<span class="string">"b"</span>).append(<span class="string">"c"</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>上述代码每次调用 stringBuffer.append 方法都需要加锁和解锁,如果JVM检测到有一连串的对同一个对象加锁和解锁的操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。</p>
<h3 id="🐱-AtomicInteger原子类源码解析"><a href="#🐱-AtomicInteger原子类源码解析" class="headerlink" title="🐱 AtomicInteger原子类源码解析"></a>🐱 AtomicInteger原子类源码解析</h3><p><img src="https://cdn.jsdelivr.net/gh/HelingCode/CDN-vulkhe-Pic@master/uPic/2023/12/17/feda866e.png" alt="img"></p>
<p>根据定义我们可以看出各属性的作用:</p>
<ul>
<li>unsafe: 获取并操作内存的数据。</li>
<li>valueOffset: 存储value在AtomicInteger中的偏移量。</li>
<li>value: 存储AtomicInteger的int值,该属性需要借助volatile关键字保证其在线程间是可见的。</li>
</ul>
<p>接下来,我们查看AtomicInteger的自增函数incrementAndGet()的源码时,发现自增函数底层调用的是unsafe.getAndAddInt()。但是由于JDK本身只有Unsafe.class,只通过class文件中的参数名,并不能很好的了解方法的作用,所以我们通过OpenJDK 8 来查看Unsafe的源码:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">// ------------------------- JDK 8 -------------------------</span></span><br><span class="line"><span class="comment">// AtomicInteger 自增方法</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="type">int</span> <span class="title function_">incrementAndGet</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> unsafe.getAndAddInt(<span class="built_in">this</span>, valueOffset, <span class="number">1</span>) + <span class="number">1</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// Unsafe.class</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="type">int</span> <span class="title function_">getAndAddInt</span><span class="params">(Object var1, <span class="type">long</span> var2, <span class="type">int</span> var4)</span> {</span><br><span class="line"> <span class="type">int</span> var5;</span><br><span class="line"> <span class="keyword">do</span> {</span><br><span class="line"> var5 = <span class="built_in">this</span>.getIntVolatile(var1, var2);</span><br><span class="line"> } <span class="keyword">while</span>(!<span class="built_in">this</span>.compareAndSwapInt(var1, var2, var5, var5 + var4));</span><br><span class="line"> <span class="keyword">return</span> var5;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// ------------------------- OpenJDK 8 -------------------------</span></span><br><span class="line"><span class="comment">// Unsafe.java</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="type">int</span> <span class="title function_">getAndAddInt</span><span class="params">(Object o, <span class="type">long</span> offset, <span class="type">int</span> delta)</span> {</span><br><span class="line"> <span class="type">int</span> v;</span><br><span class="line"> <span class="keyword">do</span> {</span><br><span class="line"> v = getIntVolatile(o, offset);</span><br><span class="line"> } <span class="keyword">while</span> (!compareAndSwapInt(o, offset, v, v + delta));</span><br><span class="line"> <span class="keyword">return</span> v;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>根据OpenJDK 8的源码我们可以看出,getAndAddInt()循环获取给定对象o中的偏移量处的值v,然后判断内存值是否等于v。如果相等则将内存值设置为 v + delta,否则返回false,继续循环进行重试,直到设置成功才能退出循环,并且将旧值返回。整个“比较+更新”操作封装在compareAndSwapInt()中,在JNI里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。</p>
<p>后续JDK通过CPU的cmpxchg指令,去比较寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。然后通过Java代码中的while循环再次调用cmpxchg指令进行重试,直到设置成功为止。</p>
<h3 id="🤢-ReentrantLock实现原理?"><a href="#🤢-ReentrantLock实现原理?" class="headerlink" title="🤢 ReentrantLock实现原理?"></a>🤢 ReentrantLock实现原理?</h3><p><img src="https://cdn.jsdelivr.net/gh/HelingCode/CDN-vulkhe-Pic@master/uPic/2023/12/17/7aadb272069d871bdee8bf3a218eed8136919-20231217152837112.png" alt="img"></p>
<p>加锁:</p>
<ul>
<li>通过 ReentrantLock 的加锁方法 Lock 进行加锁操作。</li>
<li>会调用到内部类 Sync 的 Lock 方法,由于 Sync#lock 是抽象方法,根据 ReentrantLock 初始化选择的公平锁和非公平锁,执行相关内部类的 Lock 方法,本质上都会执行 AQS 的 Acquire 方法。</li>
<li>AQS 的 Acquire 方法会执行 tryAcquire 方法,但是由于 tryAcquire 需要自定义同步器实现,因此执行了 ReentrantLock 中的 tryAcquire 方法,由于 ReentrantLock 是通过公平锁和非公平锁内部类实现的 tryAcquire 方法,因此会根据锁类型不同,执行不同的 tryAcquire。</li>
<li>tryAcquire 是获取锁逻辑,获取失败后,会执行框架 AQS 的后续逻辑,跟 ReentrantLock 自定义同步器无关。</li>
</ul>
<p>解锁:</p>
<ul>
<li>通过 ReentrantLock 的解锁方法 Unlock 进行解锁。</li>
<li>Unlock 会调用内部类 Sync 的 Release 方法,该方法继承于 AQS。</li>
<li>Release 中会调用 tryRelease 方法,tryRelease 需要自定义同步器实现,tryRelease 只在 ReentrantLock 中的 Sync 实现,因此可以看出,释放锁的过程,并不区分是否为公平锁。</li>
<li>释放成功后,所有处理由 AQS 框架完成,与自定义同步器无关。</li>
</ul>
<p>通过上面的描述,大概可以总结出 ReentrantLock 加锁解锁时 API 层核心方法的映射关系。</p>
<p><img src="https://cdn.jsdelivr.net/gh/HelingCode/CDN-vulkhe-Pic@master/uPic/2023/12/17/f30c631c8ebbf820d3e8fcb6eee3c0ef18748.png" alt="img"></p>
<h3 id="🤢-AQS原理?"><a href="#🤢-AQS原理?" class="headerlink" title="🤢 AQS原理?"></a>🤢 AQS原理?</h3><p>AQS框架是用来构建锁的同步器框架,包括了常用的<code>ReentrantLock</code>,<code>ReadWriteLock</code>,<code>CountDownLatch</code>等都是基于AQS框架来实现的。</p>
<p>AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 <strong>CLH 队列锁</strong>实现的,即将暂时获取不到锁的线程加入到队列中。</p>
<blockquote>
<p>CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。</p>
</blockquote>
<p>AQS 使用一个 int 成员变量state来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态state进行原子操作实现对其值的修改。一旦获取到锁,其他的线程将会被阻塞进入阻塞队列自旋,获得锁的线程释放锁的时候将会唤醒阻塞队列中的线程,释放锁的时候则会把 state 重新置为0,同时 当前线程ID 置为空。</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">volatile</span> <span class="type">int</span> state;<span class="comment">//共享变量,使用volatile修饰保证线程可见性</span></span><br></pre></td></tr></table></figure>
<p>状态信息通过 <code>protected</code> 类型的<code>getState()</code>,<code>setState()</code>,<code>compareAndSetState()</code> 进行操作</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">//返回同步状态的当前值</span></span><br><span class="line"><span class="keyword">protected</span> <span class="keyword">final</span> <span class="type">int</span> <span class="title function_">getState</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> state;</span><br><span class="line">}</span><br><span class="line"> <span class="comment">// 设置同步状态的值</span></span><br><span class="line"><span class="keyword">protected</span> <span class="keyword">final</span> <span class="keyword">void</span> <span class="title function_">setState</span><span class="params">(<span class="type">int</span> newState)</span> {</span><br><span class="line"> state = newState;</span><br><span class="line">}</span><br><span class="line"><span class="comment">//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)</span></span><br><span class="line"><span class="keyword">protected</span> <span class="keyword">final</span> <span class="type">boolean</span> <span class="title function_">compareAndSetState</span><span class="params">(<span class="type">int</span> expect, <span class="type">int</span> update)</span> {</span><br><span class="line"> <span class="keyword">return</span> unsafe.compareAndSwapInt(<span class="built_in">this</span>, stateOffset, expect, update);</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h3 id="🤢-AQS中的公平锁和非公平锁"><a href="#🤢-AQS中的公平锁和非公平锁" class="headerlink" title="🤢 AQS中的公平锁和非公平锁"></a>🤢 AQS中的公平锁和非公平锁</h3><ul>
<li><strong>公平锁</strong> :按照线程在队列中的排队顺序,先到者先拿到锁</li>
<li><strong>非公平锁</strong> :当线程要获取锁时,先通过两次 CAS 操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒。</li>
</ul>
<p><code>ReentrantLock</code> 默认采用非公平锁,因为考虑获得更好的性能,通过 <code>boolean</code> 来决定是否用公平锁(传入 true 用公平锁)。</p>
<p>总结:公平锁和非公平锁只有两处不同:</p>
<ol>
<li>非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。</li>
<li>非公平锁在 CAS 失败后,和公平锁一样都会进入到 <code>tryAcquire</code> 方法,在 <code>tryAcquire</code> 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。</li>
</ol>
<p>公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。</p>
<p>相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。</p>
<h3 id="🤢-AQS内部如何控制并发?"><a href="#🤢-AQS内部如何控制并发?" class="headerlink" title="🤢 AQS内部如何控制并发?"></a>🤢 AQS内部如何控制并发?</h3><p><code>AQS(AbstractQueuedSynchronizer)</code>是<code>J.U.C</code>包下<strong>lock</strong>实现的核心。主要是其提供的一个FIFO的队列来维护获取锁失败而进入阻塞的线程,以及一个volatile关键字修饰的state变量表示当前同步状态。当一个线程获取到同步状态(修改state=1),那么其他线程便无法获取,转而被构造成节点并加入同步队列中。<strong>加入队列的过程基于CAS算法。即比较当前线程认为的尾节点与当前节点,比较成功后才能正式加入队列尾部。</strong>队列头节点表示的为当前正在运行的线程,该线程执行结束后会激活它下面的一个线程进入执行状态。</p>
<p>FIFO同步队列控制并发。</p>
<h3 id="🤢-AQS为什么底层使用CAS和volatile?"><a href="#🤢-AQS为什么底层使用CAS和volatile?" class="headerlink" title="🤢 AQS为什么底层使用CAS和volatile?"></a>🤢 AQS为什么底层使用CAS和volatile?</h3><ol>
<li>AQS源码中<code>state</code>状态值使用<code>volatile</code>修饰保证内存的可见性。因为涉及到多线程对state的修改,必须保证其对所有线程的可见性。</li>
<li>CAS操作主要用于对state值的修改。</li>
</ol>
<h3 id="😡-了解CountDownLatch吗?"><a href="#😡-了解CountDownLatch吗?" class="headerlink" title="😡 了解CountDownLatch吗?"></a>😡 了解CountDownLatch吗?</h3><p>一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;</p>
<p>主要包含两个方法,一个是<code>countDown()</code>,一个是<code>await()</code>;以及一个计数器变量<code>cnt</code>。<code>countDown()</code> 方法用来给计数器<code>cnt</code>减一; <code>await()</code> 方法是用来阻塞当前线程,直到计数器为0的时候再唤醒线程继续执行;</p>
<h3 id="😡-了解CyclicBarrier吗?"><a href="#😡-了解CyclicBarrier吗?" class="headerlink" title="😡 了解CyclicBarrier吗?"></a>😡 了解CyclicBarrier吗?</h3><p>多个线程互相等待,直到到达同一个同步点,再继续一起执行。</p>
<p><img src="https://cdn.jsdelivr.net/gh/HelingCode/CDN-vulkhe-Pic@master/uPic/2023/12/17/image-20220909195445283-20231217152837474.png" alt="image-20220909195445283" /></p>
<h3 id="😡-了解Semaphore吗"><a href="#😡-了解Semaphore吗" class="headerlink" title="😡 了解Semaphore吗?"></a>😡 了解Semaphore吗?</h3><p>信号量,用于多个共享资源的互斥使用,也可以<strong>用来控制线程的并发量</strong>,类似于线程池的作用。</p>
<p>可以用于限制线程的并发数。 </p>
<h3 id="😓-产生死锁必须具备以下四个条件"><a href="#😓-产生死锁必须具备以下四个条件" class="headerlink" title="😓 产生死锁必须具备以下四个条件"></a>😓 产生死锁必须具备以下四个条件</h3><p>互斥条件:该资源任意一个时刻只由一个线程占用。</p>
<p>请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。</p>
<p>不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。</p>
<p>循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。</p>
<h3 id="😓-如何预防和避免线程死锁"><a href="#😓-如何预防和避免线程死锁" class="headerlink" title="😓 如何预防和避免线程死锁?"></a>😓 如何预防和避免线程死锁?</h3><p><a href="https://zhuanlan.zhihu.com/p/61221667">https://zhuanlan.zhihu.com/p/61221667</a></p>
<p><strong>如何预防死锁?</strong> 破坏死锁的产生的必要条件即可:</p>
<p> 1.<strong>破坏互斥条件</strong>:使资源同时访问而非互斥使用,就没有进程会阻塞在资源上,从而不发生死锁。</p>
<ol>
<li><strong>破坏请求与保持条件</strong> :采用静态分配的方式,静态分配的方式是指进程必须在执行之前就申请需要的全部资源,且直至所要的资源全部得到满足后才开始执行。</li>
<li><strong>破坏不剥夺条件</strong> :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。</li>
<li><strong>破坏循环等待条件</strong> :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。</li>
</ol>
<p><strong>如何避免死锁?</strong></p>
<p>避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。</p>
<p> <strong>安全状态</strong> 指的是系统能够按照某种进程推进顺序(P1、P2、P3…..Pn)来为每个进程分配所需资源,直到满足每个进程对资源的最大需求,使每个进程都可顺利完成。称<P1、P2、P3.....Pn>序列为安全序列。</p>
<h3 id="😓-死锁的产生、防止、避免、检测和解除"><a href="#😓-死锁的产生、防止、避免、检测和解除" class="headerlink" title="😓 死锁的产生、防止、避免、检测和解除"></a>😓 死锁的产生、防止、避免、检测和解除</h3><p><a href="https://zhuanlan.zhihu.com/p/61221667">https://zhuanlan.zhihu.com/p/61221667</a></p>
<h3 id="😭-线程池以及使用线程池的好处"><a href="#😭-线程池以及使用线程池的好处" class="headerlink" title="😭 线程池以及使用线程池的好处"></a>😭 线程池以及使用线程池的好处</h3><p>线程池(Thread Pool)是一种基于池化思想管理线程的工具,线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待监督管理者分配可并发执行的任务。这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。</p>
<p><strong>降低资源消耗</strong>:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。</p>
<p><strong>提高响应速度</strong>:当任务到达时,任务可以不需要等到线程创建就能立即执行。</p>
<p><strong>提高线程的可管理性</strong>:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控</p>
<p><strong>提供更多更强大的功能</strong>:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。</p>
<h3 id="😭-线程池的创建方式(7种)"><a href="#😭-线程池的创建方式(7种)" class="headerlink" title="😭 线程池的创建方式(7种)"></a>😭 线程池的创建方式(7种)</h3><p>线程池的创建方法总共有 7 种,但总体来说可分为 2 类:</p>
<ul>
<li>一类是通过 <code>ThreadPoolExecutor</code> 创建的线程池;</li>
<li>另一个类是通过 <code>Executors</code> 创建的线程池。</li>
</ul>
<p>线程池的创建方式总共包含以下 7 种(其中 6 种是通过 <code>Executors</code> 创建的,1 种是通过 <code>ThreadPoolExecutor</code> 创建的):</p>
<ol>
<li><strong>Executors.newFixedThreadPool(nThreads)</strong>:该方法创建一个固定数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行,若没有,则新的任务会被 暂存在一个任务<a href="https://so.csdn.net/so/search?q=队列&spm=1001.2101.3001.7020">队列</a>中,待有空闲线程时,便处理在任务队列中的任务。</li>
<li><strong>Executors.newCachedThreadPool</strong>:该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先 使用可复用的线程。 若所有线程均工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前安任务执行完毕后,将返回线程池进行复用。</li>
<li><strong>Executors.newSingleThreadExecutor</strong>:该方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。虽然是单线程池,但提供了工作队列,生命周期管理,工作线程维护等功能。</li>
<li><strong>Executors.newScheduledThreadPool</strong>:创建一个可以执行延迟任务的线程池;</li>
<li><strong>Executors.newSingleThreadScheduledExecutor</strong>:创建一个单线程的可以执行延迟任务的线程池;虽然是单线程池,但提供了工作队列,生命周期管理,工作线程维护等功能。</li>
<li><strong>Executors.newWorkStealingPool</strong>:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。</li>
<li><strong>ThreadPoolExecutor</strong>:最原始的创建线程池的方式,它包含了 7 个参数可供设置</li>
</ol>
<h3 id="😭-线程池中的工作队列"><a href="#😭-线程池中的工作队列" class="headerlink" title="😭 线程池中的工作队列"></a>😭 线程池中的工作队列</h3><p>参数workQueue 指被提交但未执行的任务队列,它是一个BlockingQueue接口的对象,仅用于存放Runnable对象。 根据队列功能分类,在ThreadPoolExecutor的构造函数中可使用一下几种BlockingQueue。</p>
<p><strong>直接提交的队列</strong>: 该功能由 <strong>SynchronousQueue</strong>对象提供。SynchronousQueue 是一个特殊的BlocingQueue。 它没有容量,每一个插入操作都要等待一个相应的删除操作,反之,每一个删除操作都要等待对应的插入操作。如皋市使用SynchronousQueue,提交的任务不会被真实的保存,而总是将新任务提交给线程执行,如果没有空闲的进程,则尝试创建新的进程,如果进程的数量已达到最大值,则执行拒绝策略。</p>
<p><strong>有界的任务队列</strong><br>有界的任务队列可以使用<strong>ArrayBlockingQueue</strong>实现。当使用有界队列时,若有新的任务需要执行,如果线程池的实际线程数小于corePoolSize,则会优先创建新的线程,若大于corePoolSize,则会将新任务假如等待队列。若等待队列已满,无法加入,在总线程数,不大于maximumPoolSize的前提下,创建新的进程执行任务。若大于maximumPoolSize,则执行拒绝策略。</p>
<p><strong>无界的任务队列</strong><br>无界的任务队列可以通过<strong>LinkedBlockingQueue</strong>类实现。与有界队列相反,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。当有新的任务到来,系统的线程数小于corePoolSize时,线程池会产生新的线程执行任务,但当系统的线程数达到corePoolSize后,就会继续增加。若后续仍有新的任务假如,而又没有空闲的线程资源,则任务直接进入对列等待。若任务创建和处理的速度差异很大,无界队列会保持快速增长,知道耗尽系统内存。</p>
<p><strong>任务优先队列</strong><br>优先任务队列是带有执行优先级的队列,它通过<strong>PriorityBlockingQueue</strong>实现,可以控制任务的只想你个先后顺序。它是一个特殊的无界队列。</p>
<h3 id="😭-执行-execute-方法和-submit-方法的区别是什么呢?"><a href="#😭-执行-execute-方法和-submit-方法的区别是什么呢?" class="headerlink" title="😭 执行 execute()方法和 submit()方法的区别是什么呢?"></a>😭 执行 execute()方法和 submit()方法的区别是什么呢?</h3><p><strong><code>execute()</code>方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;</strong></p>
<p><strong><code>submit()</code>方法用于提交需要返回值的任务。线程池会返回一个 <code>Future</code> 类型的对象,通过这个 <code>Future</code> 对象可以判断任务是否执行成功</strong>,并且可以通过 <code>Future</code> 的 <code>get()</code>方法来获取返回值,<code>get()</code>方法会阻塞当前线程直到任务完成,而使用 <code>get(long timeout,TimeUnit unit)</code>方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。</p>
<h3 id="😭-Thread和Runnable的关系"><a href="#😭-Thread和Runnable的关系" class="headerlink" title="😭 Thread和Runnable的关系"></a>😭 Thread和Runnable的关系</h3><p>对于Thread,我们是定义一个类继承Thread,实现Thread中的run方法,然后new一个这个类的对象,调用类的start方法。当执行 start() 后,线程进入就绪状态,当对应的线程抢占到 cpu 调度资源之后,进入运行状态,此时调用的是 run 方法,执行完毕之后就结束了。</p>
<p>对于Runnable,一般是定义一个类MyTask实现Runnable接口,重写其中的run方法,这里 MyTask 就是一个 Runnable,实现了 run() 方法,作为 Thread() 的入参。</p>
<p>在Runnable的接口定义中的run方法中,<strong>当一个对象继承并实现了 run() 方法,当线程 start() 后,会在该线程中单独执行该对象的 run() 方法。</strong>所以Runnable和T患儿add的关系如下:</p>
<ol>
<li>MyTask 继承 Runnable,并实现了 run() 方法;</li>
<li>Thread 初始化,将 MyTask 作为自己的成员变量;</li>
<li>Thread 执行 run() 方法,线程处于“就绪”状态;</li>
<li>等待 CPU 调度,执行 Thread 的 run() 方法,但是 run() 的内部实现,其实是执行的 MyTask.run() 方法,线程处于“运行”状态。</li>
</ol>
<p>在Thread的源码中,在 Thread 初始化时,<strong>MyTask 作为入参 target,最后赋值给 Thread.target</strong>。当执行 Thread.run() 时,<strong>其实是执行的 target.run(),即 MyTask.run(),这个是典型的策略模式</strong>:</p>
<p><img src="https://cdn.jsdelivr.net/gh/HelingCode/CDN-vulkhe-Pic@master/uPic/2023/12/17/640-20231217134127534-20231217152837660.png" alt="图片"></p>
<p>其实对于 Thread 和 Runable,其 run() 都是无返回值的,并且无法抛出异常,<strong>所以当你需要返回多线程的数据,就需要借助 Callable 和 Future。</strong></p>
<h3 id="😭-Callable和FutureTask的关系"><a href="#😭-Callable和FutureTask的关系" class="headerlink" title="😭 Callable和FutureTask的关系"></a>😭 Callable和FutureTask的关系</h3><p>Callable 是一个接口,里面有个 V call() 方法,这个 V 就是我们的返回值类型。</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">Callable</span><V> {</span><br><span class="line"> V <span class="title function_">call</span><span class="params">()</span> <span class="keyword">throws</span> Exception;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>我们一般会用匿名类的方式使用 Callable,call() 中是具体的业务逻辑:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line">Callable<String> callable = <span class="keyword">new</span> <span class="title class_">Callable</span><String>() {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">call</span><span class="params">()</span> <span class="keyword">throws</span> Exception {</span><br><span class="line"> <span class="comment">// 执行业务逻辑 ...</span></span><br><span class="line"> <span class="keyword">return</span> <span class="string">"this is Callable is running"</span>;</span><br><span class="line"> }</span><br><span class="line">};</span><br></pre></td></tr></table></figure>
<p>通过关系图谱,FutureTask 继承了 RunnableFuture,RunnableFuture 继承了 Runnable 和 Future.<strong>所以,FutureTask 也是个 Runnable !</strong>既然 FutureTask 是个 Runnable,肯定就需要实现.run() 方法,那么 FutureTask 也可以作为 Thread 的初始化入参,使用姿势如下:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">new</span> <span class="title class_">Thread</span>(FutureTask对象).start();</span><br></pre></td></tr></table></figure>
<p><strong>所以当执行 Thread.run() 时,其实是执行的 FutureTask.run()</strong></p>
<p><strong>Callable 和 FutureTask 的关系</strong>:</p>
<p>FutureTask 初始化时,Callable 必须作为 FutureTask 的初始化入参:</p>
<p><img src="https://cdn.jsdelivr.net/gh/HelingCode/CDN-vulkhe-Pic@master/uPic/2023/12/17/640-20220815133621982.png" alt="图片"></p>
<p>当执行 FutureTask.run() 时,其实执行的是 Callable.call(),<strong>所以,这里又是一个典型的策略模式 !</strong></p>
<p>现在我们应该可以很清楚知道 Thread 、Runnable、FutureTask 和 Callable 的关系:</p>
<ul>
<li>Thread.run() 执行的是 Runnable.run();</li>
<li>FutureTask 继承了 Runnable,并实现了 FutureTask.run();</li>
<li>FutureTask.run() 执行的是 Callable.run();</li>
<li>依次传递,最后 Thread.run(),其实是执行的 Callable.run()。</li>
</ul>
<p>所以整个设计方法,其实就是 2 个策略模式,<strong>Thread 和 Runnable 是一个策略模式,FutureTask 和 Callable 又是一个策略模式,最后通过 Runnable 和 FutureTask 的继承关系,将这 2 个策略模式组合在一起。</strong></p>
<h3 id="😭-Future是什么?怎么使用?"><a href="#😭-Future是什么?怎么使用?" class="headerlink" title="😭 Future是什么?怎么使用?"></a>😭 Future是什么?怎么使用?</h3><p>Future可以当成是我们收货的凭证,当某些任务非常耗时的时候,我们可以先另起一个线程异步执行这个耗时的任务,同时拿到这个Future凭证。当我们这个线程结束相关的任务,想要获得结果的时候,就调用其中的<code>get()</code>方法获得结果。</p>
<ol>
<li>新建一个<code>Callable</code>匿名函数实现类对象,将业务逻辑放在<code>call()</code>之中,同时将<code>`Callable</code>的泛型设置成我们想要的返回结果类型</li>
<li>将<code>Callable</code>匿名函数对象作为<code>FutureTask</code>的构造参数传入,创建一个<code>futureTask</code>对象</li>
<li>再将<code>futureTask</code>作为<code>Thread</code>的构造参数传入,开启另一线程执行逻辑</li>
<li>在需要得到结果时候调用<code>futureTask</code>的<code>get()</code>方法。</li>
</ol>
<p>我们看一下 Future 接口:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">Future</span><V> {</span><br><span class="line"> <span class="comment">// 取消任务,如果任务正在运行的,mayInterruptIfRunning为true时,表明这个任务会被打断的,并返回true;</span></span><br><span class="line"> <span class="comment">// 为false时,会等待这个任务执行完,返回true;若任务还没执行,取消任务后返回true,如任务执行完,返回false</span></span><br><span class="line"> <span class="type">boolean</span> <span class="title function_">cancel</span><span class="params">(<span class="type">boolean</span> mayInterruptIfRunning)</span>;</span><br><span class="line"> <span class="comment">// 判断任务是否被取消了,正常执行完不算被取消</span></span><br><span class="line"> <span class="type">boolean</span> <span class="title function_">isCancelled</span><span class="params">()</span>;</span><br><span class="line"> <span class="comment">// 判断任务是否已经执行完成,任务取消或发生异常也算是完成,返回true</span></span><br><span class="line"> <span class="type">boolean</span> <span class="title function_">isDone</span><span class="params">()</span>;</span><br><span class="line"> <span class="comment">// 获取任务返回结果,如果任务没有执行完成则等待完成将结果返回,如果获取的过程中发生异常就抛出异常,</span></span><br><span class="line"> <span class="comment">// 比如中断就会抛出InterruptedException异常等异常</span></span><br><span class="line"> V <span class="title function_">get</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException, ExecutionException;</span><br><span class="line"> <span class="comment">// 在规定的时间如果没有返回结果就会抛出TimeoutException异常</span></span><br><span class="line"> V <span class="title function_">get</span><span class="params">(<span class="type">long</span> timeout, TimeUnit unit)</span> <span class="keyword">throws</span> InterruptedException, ExecutionException, TimeoutException;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>对于 FutureTask,Callable 就是他的任务,而 FutureTask 内部维护了一个任务状态,所有的状态都是围绕这个任务来进行的,随着任务的进行,状态也在不断的更新。</p>
<p>FutureTask 继承了 Future,实现对任务的取消、数据获取、任务状态判断等功能。</p>
<p>比如我们经常会调用 get() 方法获取数据,如果任务没有执行完成,会将当前线程放入阻塞队列等待,当任务执行完后,会唤醒阻塞队列中的线程。</p>
<h3 id="😭-FutureTask用来解决什么问题?"><a href="#😭-FutureTask用来解决什么问题?" class="headerlink" title="😭 FutureTask用来解决什么问题?"></a>😭 FutureTask用来解决什么问题?</h3><p>FutureTask可看作对异步任务的封装,异步任务在它的封装下,可灵活进行阻塞获取结果或者中断。它继承了Runable以及Future接口,所以它可以灵活的作为Runnable给thread执行,也可作为Future得到callable的计算结果。</p>
<p><img src="https://cdn.jsdelivr.net/gh/HelingCode/CDN-vulkhe-Pic@master/uPic/2023/12/17/d0c8df53db8f45bf9295b1806d2db804~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp" alt="图片.png"></p>
<h3 id="😭-ThreadPoolExecutor构造函数重要参数分析"><a href="#😭-ThreadPoolExecutor构造函数重要参数分析" class="headerlink" title="😭 ThreadPoolExecutor构造函数重要参数分析"></a>😭 <code>ThreadPoolExecutor</code>构造函数重要参数分析</h3><p><strong><code>ThreadPoolExecutor</code> 3 个最重要的参数:</strong></p>
<ul>
<li><strong><code>corePoolSize</code> :</strong> 核心线程数定义了最小可以同时运行的线程数量。</li>
<li><strong><code>maximumPoolSize</code> :</strong> 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。</li>
<li><strong><code>workQueue</code>:</strong> 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。</li>
</ul>
<p><code>ThreadPoolExecutor</code>其他常见参数:</p>
<ol>
<li><strong><code>keepAliveTime</code></strong>:当线程池中的线程数量大于 <code>corePoolSize</code> 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 <code>keepAliveTime</code>才会被回收销毁;</li>
<li><strong><code>unit</code></strong> : <code>keepAliveTime</code> 参数的时间单位。</li>
<li><strong><code>threadFactory</code></strong> :线程工厂。</li>
<li><strong><code>handler</code></strong> :拒绝策略。</li>
</ol>
<p> <strong><code>ThreadPoolExecutor</code> 饱和策略定义:</strong></p>
<p>如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,<code>ThreadPoolTaskExecutor</code> 定义一些策略:</p>
<ul>
<li><strong><code>ThreadPoolExecutor.AbortPolicy</code>:</strong> 抛出 <code>RejectedExecutionException</code>来拒绝新任务的处理。</li>
<li><strong><code>ThreadPoolExecutor.CallerRunsPolicy</code>:</strong> 调用执行自己的线程运行任务,也就是直接在调用<code>execute</code>方法的线程中运行(<code>run</code>)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。</li>
<li><strong><code>ThreadPoolExecutor.DiscardPolicy</code>:</strong> 不处理新任务,直接丢弃掉。</li>
<li><strong><code>ThreadPoolExecutor.DiscardOldestPolicy</code>:</strong> 此策略将丢弃最早的未处理的任务请求,将工作队列中等待在最前面的任务丢弃,然后将新来的任务放进等待队列中。</li>
</ul>
<p>更多内容,参考<a href="https://mp.weixin.qq.com/s/SsbQc6WhFJxCarpRLGqzSA">史上最全ThreadPoolExecutor梳理(上篇)</a>,<a href="https://mp.weixin.qq.com/s/ZuD0amF-A4X7VeMuc_LJkg">史上最全ThreadPoolExecutor梳理(下篇)</a></p>
<h3 id="😭-提交一个任务到线程池的执行流程"><a href="#😭-提交一个任务到线程池的执行流程" class="headerlink" title="😭 提交一个任务到线程池的执行流程"></a>😭 提交一个任务到线程池的执行流程</h3><p>1.当线程池新加入一个线程时,首先判断当前线程数,是否小于coreSize,如果小于,则执行步骤2,否则执行3<br>2.创建新线程添加到线程池中,跳转结束<br>3.判断当前线程池等待队列是否已满,若已满,则跳转至步骤5<br>4.加入等待队列,等待线程池空闲,跳转结束<br>5.判断当前线程数是否已达到maximumPoolSize,若未达到,则跳转至步骤7<br>6.执行线程池拒绝策略,跳转结束<br>7.创建一个新线程,执行任务<br>8.跳转结束</p>
<h3 id="😭-线程池的参数怎么设置?"><a href="#😭-线程池的参数怎么设置?" class="headerlink" title="😭 线程池的参数怎么设置?"></a>😭 线程池的参数怎么设置?</h3><p><strong>如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的! CPU 根本没有得到充分利用。</strong></p>
<p><strong>但是,如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。</strong></p>
<p>有一个简单并且适用面比较广的公式:</p>
<ul>
<li><strong>CPU 密集型任务(N+1):</strong> 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。</li>
<li><strong>I/O 密集型任务(2N):</strong> 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。</li>
</ul>
<p><strong>如何判断是 CPU 密集任务还是 IO 密集任务?</strong></p>
<p>CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。</p>
<h3 id="🤣-BIO-NIO-AIO"><a href="#🤣-BIO-NIO-AIO" class="headerlink" title="🤣 BIO/NIO/AIO"></a>🤣 BIO/NIO/AIO</h3><h4 id="Java中的IO原理"><a href="#Java中的IO原理" class="headerlink" title="Java中的IO原理"></a>Java中的IO原理</h4><p>首先Java中的IO都是依赖操作系统内核进行的,我们程序中的IO读写其实调用的是操作系统内核中的read&write两大系统调用。</p>
<p>那内核是如何进行IO交互的呢?</p>
<ol>
<li>网卡收到经过网线传来的网络数据,并将网络数据写到内存中。</li>
<li>当网卡把数据写入到内存后,网卡向cpu发出一个中断信号,操作系统便能得知有新数据到来,再通过网卡中断程序去处理数据。</li>
<li>将内存中的网络数据写入到对应socket的接收缓冲区中。</li>
<li>当接收缓冲区的数据写好之后,应用程序开始进行数据处理。</li>
</ol>
<p>对应抽象到java的socket代码简单示例如下:</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SocketServer</span> {</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception {</span><br><span class="line"> <span class="comment">// 监听指定的端口</span></span><br><span class="line"> <span class="type">int</span> <span class="variable">port</span> <span class="operator">=</span> <span class="number">8080</span>;</span><br><span class="line"> <span class="type">ServerSocket</span> <span class="variable">server</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ServerSocket</span>(port);</span><br><span class="line"> <span class="comment">// server将一直等待连接的到来</span></span><br><span class="line"> <span class="type">Socket</span> <span class="variable">socket</span> <span class="operator">=</span> server.accept();</span><br><span class="line"> <span class="comment">// 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取</span></span><br><span class="line"> <span class="type">InputStream</span> <span class="variable">inputStream</span> <span class="operator">=</span> socket.getInputStream();</span><br><span class="line"> <span class="type">byte</span>[] bytes = <span class="keyword">new</span> <span class="title class_">byte</span>[<span class="number">1024</span>];</span><br><span class="line"> <span class="type">int</span> len;</span><br><span class="line"> <span class="keyword">while</span> ((len = inputStream.read(bytes)) != -<span class="number">1</span>) {</span><br><span class="line"> <span class="comment">//获取数据进行处理</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">message</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">String</span>(bytes, <span class="number">0</span>, len,<span class="string">"UTF-8"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// socket、server,流关闭操作,省略不表</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>可以看到这个过程和底层内核的网络IO很类似,主要体现在accept()等待从网络中的请求到来然后bytes[]数组作为缓冲区等待数据填满后进行处理。而BIO、NIO、AIO之间的区别就在于这些操作是同步还是异步,阻塞还是非阻塞。</p>
<p>所以我们引出同步异步,阻塞与非阻塞的概念。</p>
<h4 id="同步与异步"><a href="#同步与异步" class="headerlink" title="同步与异步"></a>同步与异步</h4><p>同步和异步指的是一个执行流程中每个方法是否必须依赖前一个方法完成后才可以继续执行。假设我们的执行流程中:依次是方法一和方法二。</p>
<p>同步指的是调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。即方法二一定要等到方法一执行完成后才可以执行。</p>
<p>异步指的是调用立刻返回,调用者不必等待方法内的代码执行结束,就可以继续后续的行为。(具体方法内的代码交由另外的线程执行完成后,可能会进行回调)。即执行方法一的时候,直接交给其他线程执行,不由主线程执行,也就不会阻塞主线程,所以方法二不必等到方法一完成即可开始执行。</p>
<p>同步与异步关注的是方法的执行方是主线程还是其他线程,主线程的话需要等待方法执行完成,其他线程的话无需等待立刻返回方法调用,主线程可以直接执行接下来的代码。</p>
<p>同步与异步是从多个线程之间的协调来实现效率差异。</p>
<blockquote>
<p>为什么需要异步呢?笔者认为异步的本质就是为了解决主线程的阻塞,所以网上很多讨论把同步异步、阻塞非阻塞进行了四种组合,其中一种就有异步阻塞这一情形,如果异步也是阻塞的?那为什么要特地进行异步操作呢?</p>
</blockquote>
<h4 id="阻塞与非阻塞"><a href="#阻塞与非阻塞" class="headerlink" title="阻塞与非阻塞"></a>阻塞与非阻塞</h4><p>阻塞与非阻塞指的是单个线程内遇到同步等待时,是否在原地不做任何操作。</p>
<p>阻塞指的是遇到同步等待后,一直在原地等待同步方法处理完成。</p>
<p>非阻塞指的是遇到同步等待,不在原地等待,先去做其他的操作,隔断时间再来观察同步方法是否完成。</p>
<p>阻塞与非阻塞关注的是线程是否在原地等待。</p>
<blockquote>
<p>笔者认为阻塞和非阻塞仅能与同步进行组合。而异步天然就是非阻塞的,而这个非阻塞是对主线程而言。(可能有人认为异步方法里面放入阻塞操作的话就是异步阻塞,但是思考一下,正是因为是阻塞操作所以才会将它放入异步方法中,不要阻塞主线程)</p>
</blockquote>
<h4 id="例子讲解"><a href="#例子讲解" class="headerlink" title="例子讲解"></a>例子讲解</h4><blockquote>
<p>海底捞很好吃,但是经常要排队。我们就以生活中的这个例子进行讲解。</p>
</blockquote>
<ul>
<li>A顾客去吃海底捞,就这样干坐着等了一小时,然后才开始吃火锅。(BIO)</li>
<li>B顾客去吃海底捞,他一看要等挺久,于是去逛商场,每次逛一会就跑回来看有没有排到他。于是他最后既购了物,又吃上海底捞了。(NIO)</li>
<li>C顾客去吃海底捞,由于他是高级会员,所以店长说,你去商场随便玩吧,等下有位置,我立马打电话给你。于是C顾客不用干坐着等,也不用每过一会儿就跑回来看有没有等到,最后也吃上了海底捞(AIO)</li>
</ul>
<blockquote>
<p>哪种方式更有效率呢?是不是一目了然呢?</p>
</blockquote>
<h4 id="BIO"><a href="#BIO" class="headerlink" title="BIO"></a>BIO</h4><p>BIO全称是Blocking IO,是JDK1.4之前的传统IO模型,本身是同步阻塞模式。服务器实现模式为一个连接一个线程,即客户端有连接请 求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造 成不必要的线程开销,当然可以通过线程池机制改善。 线程发起IO请求后,一直阻塞IO,直到缓冲区数据就绪后,再进入下一步操作。针对网络通信都是一请求一应答的方式,虽然简化了上层的应用开发,但在性能和可靠性方面存在着巨大瓶颈,试想一下如果每个请求都需要新建一个线程来专门处理,那么在高并发的场景下,机器资源很快就会被耗尽。</p>
<h4 id="NIO"><a href="#NIO" class="headerlink" title="NIO"></a>NIO</h4><p>NIO也叫Non-Blocking IO 是同步非阻塞的IO模型。服务器实现模式为一个请求一个线程,即客户端发送的连 接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求时才启动 一个线程进行处理。线程发起io请求后,立即返回(非阻塞io)。同步指的是必须等待IO缓冲区内的数据就绪,而非阻塞指的是,用户线程不原地等待IO缓冲区,可以先做一些其他操作,但是要定时轮询检查IO缓冲区数据是否就绪。Java中的NIO 是new IO的意思。其实是NIO加上IO多路复用技术。普通的NIO是线程轮询查看一个IO缓冲区是否就绪,而Java中的new IO指的是线程轮询地去查看一堆IO缓冲区中哪些就绪,这是一种IO多路复用的思想。IO多路复用模型中,将检查IO数据是否就绪的任务,交给系统级别的select或epoll模型,由系统进行监控,减轻用户线程负担。</p>
<p>NIO主要有buffer、channel、selector三种技术的整合,通过零拷贝的buffer取得数据,每一个客户端通过channel在selector(多路复用器)上进行注册。服务端不断轮询channel来获取客户端的信息。channel上有connect,accept(阻塞)、read(可读)、write(可写)四种状态标识。根据标识来进行后续操作。所以一个服务端可接收无限多的channel。不需要新开一个线程。大大提升了性能。</p>
<h4 id="AIO"><a href="#AIO" class="headerlink" title="AIO"></a>AIO</h4><p>AIO是真正意义上的异步非阻塞IO模型。服务器实现模式为一个有效请求一个线程,客户端的 IO 请 求都是由 OS 先完成了再通知服务器应用去启动线程进行处理。 上述NIO实现中,需要用户线程定时轮询,去检查IO缓冲区数据是否就绪,占用应用程序线程资源,其实轮询相当于还是阻塞的,并非真正解放当前线程,因为它还是需要去查询哪些IO就绪。而真正的理想的异步非阻塞IO应该让内核系统完成,用户线程只需要告诉内核,当缓冲区就绪后,通知我或者执行我交给你的回调函数。</p>
<p>AIO可以做到真正的异步的操作,但实现起来比较复杂,支持纯异步IO的操作系统非常少,目前也就windows是IOCP技术实现了,而在Linux上,底层还是是使用的epoll实现的。</p>
<h3 id="😇-线程安全有哪些实现思路"><a href="#😇-线程安全有哪些实现思路" class="headerlink" title="😇 线程安全有哪些实现思路?"></a>😇 线程安全有哪些实现思路?</h3><ol>
<li><strong>互斥同步</strong></li>
</ol>
<p>synchronized 和 ReentrantLock。</p>
<ol>
<li><strong>非阻塞同步</strong></li>
</ol>
<p>互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。</p>
<p>互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。</p>
<ul>
<li><code>CAS</code></li>
</ul>
<p>随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略: 先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。</p>
<p>乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是: 比较并交换(Compare-and-Swap,<code>CAS</code>)。<code>CAS</code> 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。</p>
<ul>
<li><code>AtomicInteger</code></li>
</ul>
<p><code>J.U.C</code> 包里面的整数原子类 <code>AtomicInteger</code>,其中的 <code>compareAndSet()</code> 和 <code>getAndIncrement()</code> 等方法都使用了 Unsafe 类的 <code>CAS</code> 操作。</p>
<ol>
<li><strong>无同步方案</strong></li>
</ol>
<p>要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。</p>
<ul>
<li>栈封闭</li>
</ul>
<p>多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。</p>
<ul>
<li>线程本地存储(Thread Local Storage)</li>
</ul>
<p>如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。</p>
]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>八股文</tag>
</tags>
</entry>
<entry>
<title>Java中 DecimalFormat 用法详解</title>
<url>/posts/43254.html</url>
<content><![CDATA[<h3 id="对Java中-DecimalFormat-的所有基础用法进行了一个汇总。"><a href="#对Java中-DecimalFormat-的所有基础用法进行了一个汇总。" class="headerlink" title="对Java中 DecimalFormat 的所有基础用法进行了一个汇总。"></a>对Java中 DecimalFormat 的所有基础用法进行了一个汇总。</h3><span id="more"></span>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> java.text.DecimalFormat;</span><br><span class="line"> </span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TestNumberFormat</span>{ </span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[]args)</span>{</span><br><span class="line"> <span class="type">double</span> <span class="variable">pi</span> <span class="operator">=</span> <span class="number">3.1415927</span>; <span class="comment">//圆周率</span></span><br><span class="line"> <span class="comment">//取一位整数</span></span><br><span class="line"> System.out.println(<span class="keyword">new</span> <span class="title class_">DecimalFormat</span>(<span class="string">"0"</span>).format(pi)); <span class="comment">//3</span></span><br><span class="line"> <span class="comment">//取一位整数和两位小数</span></span><br><span class="line"> System.out.println(<span class="keyword">new</span> <span class="title class_">DecimalFormat</span>(<span class="string">"0.00"</span>).format(pi)); <span class="comment">//3.14</span></span><br><span class="line"> <span class="comment">//取两位整数和三位小数,整数不足部分以0填补。</span></span><br><span class="line"> System.out.println(<span class="keyword">new</span> <span class="title class_">DecimalFormat</span>(<span class="string">"00.000"</span>).format(pi));<span class="comment">// 03.142</span></span><br><span class="line"> <span class="comment">//取所有整数部分</span></span><br><span class="line"> System.out.println(<span class="keyword">new</span> <span class="title class_">DecimalFormat</span>(<span class="string">"#"</span>).format(pi)); <span class="comment">//3</span></span><br><span class="line"> <span class="comment">//以百分比方式计数,并取两位小数</span></span><br><span class="line"> System.out.println(<span class="keyword">new</span> <span class="title class_">DecimalFormat</span>(<span class="string">"#.##%"</span>).format(pi)); <span class="comment">//314.16%</span></span><br><span class="line"> <span class="type">long</span> <span class="variable">c</span> <span class="operator">=</span><span class="number">299792458</span>; <span class="comment">//光速</span></span><br><span class="line"> <span class="comment">//显示为科学计数法,并取五位小数</span></span><br><span class="line"> System.out.println(<span class="keyword">new</span> <span class="title class_">DecimalFormat</span>(<span class="string">"#.#####E0"</span>).format(c)); <span class="comment">//2.99792E8</span></span><br><span class="line"> <span class="comment">//显示为两位整数的科学计数法,并取四位小数</span></span><br><span class="line"> System.out.println(<span class="keyword">new</span> <span class="title class_">DecimalFormat</span>(<span class="string">"00.####E0"</span>).format(c)); <span class="comment">//29.9792E7</span></span><br><span class="line"> <span class="comment">//每三位以逗号进行分隔。</span></span><br><span class="line"> System.out.println(<span class="keyword">new</span> <span class="title class_">DecimalFormat</span>(<span class="string">",###"</span>).format(c)); <span class="comment">//299,792,458</span></span><br><span class="line"> <span class="comment">//将格式嵌入文本</span></span><br><span class="line"> System.out.println(<span class="keyword">new</span> <span class="title class_">DecimalFormat</span>(<span class="string">"光速大小为每秒,###米。"</span>).format(c));</span><br><span class="line"> </span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>DecimalFormat 类主要靠 # 和 0 两种占位符号来指定数字长度。0 表示如果位数不足则以 0 填充,# 表示只要有可能就把数字拉上这个位置。上面的例子包含了差不多所有的基本用法</p>
]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title>Java集合面试题</title>
<url>/posts/java-collection.html</url>
<content><![CDATA[<p><img src="https://cdn.jsdelivr.net/gh/HelingCode/CDN-vulkhe-Pic@master/uPic/2023/12/17/java-collection-hierarchy.71519bdb-20231217153114487.png" alt=""></p>
<h3 id="😀-List、Map、Set三个接口,存取元素时,各有什么特点?"><a href="#😀-List、Map、Set三个接口,存取元素时,各有什么特点?" class="headerlink" title="😀 List、Map、Set三个接口,存取元素时,各有什么特点?"></a>😀 List、Map、Set三个接口,存取元素时,各有什么特点?</h3><p>首先,List与Set具有相似性,它们都是单列元素的集合,所以,它们有一个共同的父接口,叫Collection。Set里面不允许有重复的元素,所谓重复,即不能有两个相等(注意,不是仅仅是相同)的对象 ,即假设Set集合中有了一个A对象,现在我要向Set集合再存入一个B对象,但B对象与A对象equals相等,则B对象存储不进去,所以,Set集合的add方法有一个boolean的返回值,当集合中没有某个元素,此时add方法可成功加入该元素时,则返回true,当集合含有与某个元素equals相等的元素时,此时add方法无法加入该元素,返回结果为false。Set取元素时,没法说取第几个,只能以Iterator接口取得所有的元素,再逐一遍历各个元素。</p>
<p>List表示有先后顺序的集合。当我们多次调用add(Obj e)方法时,每次加入的对象按先来后到的顺序排序。有时候,也可以插队,即调用add(int index,Obj e)方法,就可以指定当前对象在集合中的存放位置。一个对象可以被反复存储进List中,每调用一次add方法,这个对象就被插入进集合中一次,其实,并不是把这个对象本身存储进了集合中,而是在集合中用一个索引变量指向这个对象,当这个对象被add多次时,即相当于集合中有多个索引指向了这个对象。List除了可以以Iterator接口取得所有的元素,再逐一遍历各个元素之外,还可以调用get(index i)来明确说明取第几个。</p>
<p>Map与List和Set不同,它是双列的集合,其中有put方法,定义如下:put(obj key,obj value),每次存储时,要存储一对key/value,不能存储重复的key,这个重复的规则也是按equals比较相等。取则可以根据key获得相应的value,即get(Object key)返回值为key 所对应的value。另外,也可以获得所有的key的集合,还可以获得所有的value的集合,还可以获得key和value组合成的Map.Entry对象的集合。</p>
<h3 id="😀-Set里的元素是不能重复的,那么用什么方法来区分重复与否呢-是用-还是equals-它们有何区别"><a href="#😀-Set里的元素是不能重复的,那么用什么方法来区分重复与否呢-是用-还是equals-它们有何区别" class="headerlink" title="😀 Set里的元素是不能重复的,那么用什么方法来区分重复与否呢? 是用==还是equals()? 它们有何区别?"></a>😀 Set里的元素是不能重复的,那么用什么方法来区分重复与否呢? 是用==还是equals()? 它们有何区别?</h3><p>Set里的元素是不能重复的,元素重复与否是使用equals()方法进行判断的。</p>
<p>equals()和==方法决定引用值是否指向同一对象equals()在类中被覆盖,为的是当两个分离的对象的内容和类型相配的话,返回真值。</p>
<h3 id="😀-你所知道的集合类都有哪些?主要方法?"><a href="#😀-你所知道的集合类都有哪些?主要方法?" class="headerlink" title="😀 你所知道的集合类都有哪些?主要方法?"></a>😀 你所知道的集合类都有哪些?主要方法?</h3><p>最常用的集合类是 List 和 Map。 List 的具体实现包括 ArrayList 和 Vector,它们是可变大小的列表,比较适合构建、存储和操作任何类型对象的元素列表。 List 适用于按数值索引访问元素的情形。</p>
<p>Map 集合类用于存储键值对,其中每个键映射到一个值。</p>
<p>List类会有get(int index)这样的方法,因为它可以按顺序取元素,而set类中没有get(int index)这样的方法。List和set都可以迭代出所有元素,迭代时先要得到一个iterator对象,所以,set和list类都有一个iterator方法,用于返回那个iterator对象。map可以返回三个集合,一个是返回所有的key的集合,另外一个返回的是所有value的集合,再一个返回的key和value组合成的EntrySet对象的集合,map也有get方法,参数是key,返回值是key对应的value。</p>
<h3 id="😀-说说-List-Set-Queue-Map-四者的区别?"><a href="#😀-说说-List-Set-Queue-Map-四者的区别?" class="headerlink" title="😀 说说 List, Set, Queue, Map 四者的区别?"></a>😀 说说 List, Set, Queue, Map 四者的区别?</h3><p><code>List</code>(对付顺序的好帮手): 存储的元素是有序的、可重复的。</p>
<p><code>Set</code>(注重独一无二的性质): 存储的元素是无序的、不可重复的。</p>
<p><code>Queue</code>(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。</p>
<p><code>Map</code>(用 key 来搜索的专家): 使用键值对(key-value)存储,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。</p>
<h3 id="😀-ArrayList-Vector-LinkedList的存储性能和特性"><a href="#😀-ArrayList-Vector-LinkedList的存储性能和特性" class="headerlink" title="😀 ArrayList,Vector, LinkedList的存储性能和特性"></a>😀 ArrayList,Vector, LinkedList的存储性能和特性</h3><p>ArrayList和Vector都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector由于使用了synchronized方法(线程安全),通常性能上较ArrayList差,而LinkedList使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。</p>
<p>LinkedList也是线程不安全的,LinkedList提供了一些方法,使得LinkedList可以被当作堆栈和队列来使用。</p>
<h3 id="😀-ArrayList和Vector的区别"><a href="#😀-ArrayList和Vector的区别" class="headerlink" title="😀 ArrayList和Vector的区别"></a>😀 ArrayList和Vector的区别</h3><p>这两个类都实现了List接口(List接口继承了Collection接口),他们都是有序集合,即存储在这两个集合中的元素的位置都是有顺序的,相当于一种动态的数组,我们以后可以按位置索引号取出某个元素,并且其中的数据是允许重复的,这是HashSet之类的集合的最大不同处,HashSet之类的集合不可以按索引号去检索其中的元素,也不允许有重复的元素。</p>
<p>ArrayList与Vector的区别,这主要包括两个方面:</p>
<p>(1)同步性:</p>
<p>Vector是线程安全的,也就是说是它的方法之间是线程同步的,而ArrayList是线程不安全的,它的方法之间是线程不同步的。如果只有一个线程会访问到集合,那最好是使用ArrayList,因为它不考虑线程安全,效率会高些;如果有多个线程会访问到集合,那最好是使用Vector,因为不需要我们自己再去考虑和编写线程安全的代码。</p>
<p>(2)数据增长:</p>
<p>ArrayList与Vector都有一个初始的容量大小,当存储进它们里面的元素的个数超过了容量时,就需要增加ArrayList与Vector的存储空间,每次要增加存储空间时,不是只增加一个存储单元,而是增加多个存储单元,每次增加的存储单元的个数在内存空间利用与程序效率之间要取得一定的平衡。Vector默认增长为原来两倍,而ArrayList的增长策略在文档中没有明确规定(从源代码看到的是增长为原来的1.5倍)。ArrayList与Vector都可以设置初始的空间大小,Vector还可以设置增长的空间大小,而ArrayList没有提供设置增长空间的方法。</p>
<p>总结:即Vector增长原来的一倍,ArrayList增加原来的0.5倍。</p>
<h3 id="😀-ArrayList的继承结构分析?"><a href="#😀-ArrayList的继承结构分析?" class="headerlink" title="😀 ArrayList的继承结构分析?"></a>😀 ArrayList的继承结构分析?</h3><p>动态数组,它提供了动态的增加和减少元素,实现了Collection和List接口,灵活的设置数组的大小等好处。</p>
<p>每个 ArrayList 实例都有一个容量。该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向 ArrayList 中不断添加元素,其容量也<strong>自动增长</strong>。</p>
<p><img src="https://cdn.jsdelivr.net/gh/HelingCode/CDN-vulkhe-Pic@master/uPic/2023/12/17/1460000039061646.png" alt="img"></p>
<p><strong>AbstractList<E></strong>:</p>
<blockquote>
<p>抽象接口类,目的是使用抽象类中已经实现的方法。</p>
<p>我们点开AbstractList<E>源码,会看到其实AbstractList<E>已经也实现了List<E>接口,为什么要先继承AbstractList<E>,而让AbstractList先实现List<E>?而不是让ArrayList直接实现List<E>?</p>
<p>这里是有一个思想,接口中全都是抽象的方法,而抽象类中可以有抽象方法,还可以有具体的实现方法,正是利用了这一点,让AbstractList<E>实现接口中一些通用的方法,而如ArrayList就继承这个AbstractList类,拿到一些通用的方法,然后自己在实现一些自己特有的方法,这样一来,让代码更简洁,就继承结构最底层的类中通用的方法都抽取出来,先一起实现了,减少重复代码。所以一般看到一个类上面还有一个抽象类,应该就是这个作用。</p>
</blockquote>
<p><strong>List<E></strong>:</p>
<blockquote>
<p>使用List的接口规范</p>
</blockquote>
<p><strong>RandomAccess</strong>:</p>
<blockquote>
<p>这个是一个标记性接口,通过查看api文档,它的作用就是用来快速随机存取,有关效率的问题,在实现了该接口的话,那么使用普通的for循环来遍历,性能更高,例如arrayList。而没有实现该接口的话,使用Iterator来迭代,这样性能更高,例如linkedList。所以这个标记性只是为了让我们知道我们用什么样的方式去获取数据性能更好。</p>
</blockquote>
<p><strong>Cloneable</strong>:</p>
<blockquote>
<p>目的是使用clone方法。<a href="https://link.segmentfault.com/?enc=TS3SthtM1gwt%2Fu6XVYl0SA%3D%3D.JzGMle0MaL%2BdGAAKBExqazZiXAwMDnJCJlxCRV75vFDyNsgWUHSONJrCUBWy9GKb">想具体了解此方法的,点击这里,这篇文章写的还是不错的Cloneable~~</a></p>
</blockquote>
<p><strong>Serializable</strong>:</p>
<blockquote>
<p>实现该序列化接口,表明该类可以被序列化,什么是序列化?简单的说,就是能够从类变成字节流传输,然后还能从字节流变成原来的类。</p>
</blockquote>
<h3 id="😀-ArrayList类分析"><a href="#😀-ArrayList类分析" class="headerlink" title="😀 ArrayList类分析"></a>😀 ArrayList类分析</h3><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ArrayList</span><E> <span class="keyword">extends</span> <span class="title class_">AbstractList</span><E></span><br><span class="line"> <span class="keyword">implements</span> <span class="title class_">List</span><E>, RandomAccess, Cloneable, java.io.Serializable</span><br><span class="line">{</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">long</span> <span class="variable">serialVersionUID</span> <span class="operator">=</span> <span class="number">8683452581122892189L</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 缺省容量</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">DEFAULT_CAPACITY</span> <span class="operator">=</span> <span class="number">10</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 有参构造缺省空数组</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Object[] EMPTY_ELEMENTDATA = {};</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 无参构造缺省空数组</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 数组元素(实际操作的数组,新增,删除等方法都是在此数组发生操作)</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">transient</span> Object[] elementData; <span class="comment">// non-private to simplify nested class access</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 实际数组的大小</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="type">int</span> size;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 数组的最大容量</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">MAX_ARRAY_SIZE</span> <span class="operator">=</span> Integer.MAX_VALUE - <span class="number">8</span>; </span><br></pre></td></tr></table></figure>
<p>(1)为什么数组最大容量是<strong>Integer.MAX_VALUE - 8</strong>,而不是<strong>Integer.MAX_VALUE</strong>?</p>
<p>其实源码中给了备注:意思应该是有些虚拟机在数组中保留了一些头信息。避免内存溢出!</p>
<p>(2)为什么定义了<strong>两个空数组</strong>?</p>
<p>首先定义空数组的根本原因是<strong>优化处理</strong>,如果一个应用中有很多这样ArrayList空实例的话,就会有很多的空数组,无疑是为了优化性能,所有ArrayList空实例都指向同一个空数组。两者都是用来减少空数组的创建,所有空ArrayList都共享空数组。两者的区别主要是用来起区分作用,针对有参无参的构造在扩容时做区分走不同的扩容逻辑,优化性能。</p>
<h4 id="构造方法"><a href="#构造方法" class="headerlink" title="构造方法"></a>构造方法</h4><p>(1)无参构造方法 <strong>ArrayList()</strong></p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 将空数组初始化大小为10(将空数组初始化大小为10,具体在什么时候初始化大小为10,待会儿会说到)</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">ArrayList</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// 将elementData元素数组初始化为空数组</span></span><br><span class="line"> <span class="built_in">this</span>.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;</span><br><span class="line"> }</span><br></pre></td></tr></table></figure>
<p>无参构造方法中,将元素数组elementData初始化为空数组。</p>
<p>(2)有参构造方法 <strong>ArrayList(int)</strong></p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 构造一个具有指定初始容量的列表</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> initialCapacity: 初始化数组的值</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">ArrayList</span><span class="params">(<span class="type">int</span> initialCapacity)</span> {</span><br><span class="line"> <span class="comment">//如果初始化的值大于0,则给定elementData一个长度为initialCapacity的数组</span></span><br><span class="line"> <span class="keyword">if</span> (initialCapacity > <span class="number">0</span>) { </span><br><span class="line"> <span class="built_in">this</span>.elementData = <span class="keyword">new</span> <span class="title class_">Object</span>[initialCapacity];</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (initialCapacity == <span class="number">0</span>) { <span class="comment">// 如果初始化的值等于0,则初始化为空数组</span></span><br><span class="line"> <span class="built_in">this</span>.elementData = EMPTY_ELEMENTDATA;</span><br><span class="line"> } <span class="keyword">else</span> { <span class="comment">//否则(小于0的情况)抛出异常</span></span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalArgumentException</span>(<span class="string">"Illegal Capacity: "</span>+</span><br><span class="line"> initialCapacity);</span><br><span class="line"> }</span><br><span class="line"> }</span><br></pre></td></tr></table></figure>
<p>(3)有参构造方法 <strong>ArrayList(Collection<? extends E> c)</strong></p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 构造一个指定元素的集合(此方法不太常用)</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> c </span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">ArrayList</span><span class="params">(Collection<? extends E> c)</span> {</span><br><span class="line"> <span class="comment">// 将集合转换为数组并赋值给elementData</span></span><br><span class="line"> elementData = c.toArray();</span><br><span class="line"> <span class="comment">// 如果集合的大小不为0</span></span><br><span class="line"> <span class="keyword">if</span> ((size = elementData.length) != <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// 如果转换后的数组不是泛型(object),则需要用Arrays的工具转换一下为object数组(这里不再对Arrays.copyOf展开论述)</span></span><br><span class="line"> <span class="keyword">if</span> (elementData.getClass() != Object[].class)</span><br><span class="line"> elementData = Arrays.copyOf(elementData, size, Object[].class);</span><br><span class="line"> } <span class="keyword">else</span> { <span class="comment">// 否则初始化elementData为一个空数组</span></span><br><span class="line"> <span class="built_in">this</span>.elementData = EMPTY_ELEMENTDATA;</span><br><span class="line"> }</span><br><span class="line"> }</span><br></pre></td></tr></table></figure>
<h3 id="😀-ArrayList常用方法源码分析"><a href="#😀-ArrayList常用方法源码分析" class="headerlink" title="😀 ArrayList常用方法源码分析"></a>😀 ArrayList常用方法源码分析</h3><h4 id="boolean-add-E-e"><a href="#boolean-add-E-e" class="headerlink" title="boolean add(E e)"></a>boolean add(E e)</h4><ol>
<li>确定内部容量是否够用,size是元素数组中数据的个数,因为要添加一个元素,所以size+1,先判断size+1的这个个数数组能否放得下,就在这个<code>ensureCapacityInternal</code>方法中去判断是否数组.length是否够用了,size + 1 作为<code>ensureCapacityInternal</code>方法的入参。然后将元素e赋值到elementData末尾<code>elementData[size++] = e;</code>。</li>
<li><code>ensureCapacityInternal</code>方法可以理解为一个中转方法,它里面又有一个ensureExplicitCapacity方法,然后这个方法里面又包了一个calculateCapacity(Object[] elementData, int minCapacity)方法。这个方法首先判断数组是不是空数组,如果是空数组(此时minCapacity = 0 + 1 = 1),就将minCapacity初始化为10,但此时仅仅是返回要初始化数组的大小,并没有真正初始化数组为10。如果不是空数组,则返回元素数组的size + 1。</li>
<li>现在ensureExplicitCapacity方法获得了minCapacity的值作为它的入参,方法首先将结构变化记录+1 在父类AbstractList中定义了一个int型的属性:modCount,记录了ArrayList结构性变化的次数。然后判断数组是否够用,如果不够用,就自动扩容。当初始化的集合为空数组时,此时minCapacity是10,而elementData的长度为0,所以需要扩容;当初始化的集合不为空是,也就是给定了大小,或已经初始化了元素,此时的minCapacity = 实际数组个数+1,此时判断集合不够用,也需要进行扩容,否则元素会溢出。</li>
<li>在扩容方法grow(int minCapacity)中,首先会获得元素数组的实际长度oldCapacity(也就是扩容前的数组大小),然后oldCapacity 扩容1.5倍赋值给newCapacity( >>为右移运算符,相当于除以2 即oldCapacity/2 ),<code>int newCapacity = oldCapacity + (oldCapacity >> 1);</code>。如果之前是个空数组的情况,则将数组扩容为10,此时才是真正初始化元素数组elementData大小为10,<code>if (newCapacity - minCapacity < 0) newCapacity = minCapacity;</code>。如果1.5倍的数组大小超过了集合的最大长度,则调用hugeCapacity方法,重新计算,也就是给定最大的集合长度。最后已经确定了大小,就将元素copy到elementData元素数组中<code>elementData = Arrays.copyOf(elementData, newCapacity);</code>。</li>
</ol>
<h4 id="void-add-int-index-E-element"><a href="#void-add-int-index-E-element" class="headerlink" title="void add(int index, E element)"></a>void add(int index, E element)</h4><p>首先进行参数校验,判断index是否大于size或者小于size,之后执行<code>ensureCapacityInternal(size + 1);</code>,然后执行<code>System.arraycopy(elementData, index, elementData, index + 1,size - index);</code>方法,这个方法就是将原数组中从index位置开始的元素复制到从index+1开始,也就是将index及其之后的元素向后移动一位。然后将指定元素覆盖到指定下标index。最后size + 1。</p>
<h4 id="boolean-remove-Object-o"><a href="#boolean-remove-Object-o" class="headerlink" title="boolean remove(Object o)"></a>boolean remove(Object o)</h4><p>首先校验要删除的元素下标是否有越界,然后将结构变化记录+1,然后获得旧数据,返回给开发人员,目的是让开发人员知道删除了哪个数据,之后计算需要元素需要移动的次数,然后使用System.arrayCopy进行元素移动。最后将最后一个元素置为空(元素前移,最后一位置为空)<code>elementData[--size] = null;</code>,让GC回收。</p>
<h4 id="void-clear"><a href="#void-clear" class="headerlink" title="void clear()"></a>void clear()</h4><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 清空集合</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">clear</span><span class="params">()</span> {</span><br><span class="line"> modCount++;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 将数组置为空,促使GC回收</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i < size; i++)</span><br><span class="line"> elementData[i] = <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line"> size = <span class="number">0</span>;</span><br><span class="line"> }</span><br></pre></td></tr></table></figure>
<h4 id="E-get-int-index"><a href="#E-get-int-index" class="headerlink" title="E get(int index)"></a>E get(int index)</h4><p>返回此列表中指定位置上的元素,检查给定的索引是否在范围内。 如果没有,则抛出一个适当的运行时异常。之后返回元素数组中指定index位置的数据</p>
<h4 id="E-set-int-index-E-element"><a href="#E-set-int-index-E-element" class="headerlink" title="E set(int index,E element)"></a>E set(int index,E element)</h4><p>覆盖相应下标的数据,检查给定的索引是否在范围内。 如果没有,则抛出一个适当的运行时异常。获取到旧数据,这里将旧数据返回出去,为了让开发者知道替换的是哪个值<code>E oldValue = elementData(index);</code>,将指定下标覆盖为新元素<code>elementData[index] = element;</code></p>
<h3 id="😀-ArrayList是线程安全的么?"><a href="#😀-ArrayList是线程安全的么?" class="headerlink" title="😀 ArrayList是线程安全的么?"></a>😀 ArrayList是线程安全的么?</h3><p>当然不是,线程安全版本的数组容器是Vector。Vector的实现很简单,就是把所有的方法统统加上synchronized就完事了。你也可以不使用Vector,用Collections.synchronizedList把一个普通ArrayList包装成一个线程安全版本的数组容器也可以,原理同Vector是一样的,就是给所有的方法套上一层synchronized。</p>
<h3 id="😀-Arraylist-与-LinkedList-区别"><a href="#😀-Arraylist-与-LinkedList-区别" class="headerlink" title="😀 Arraylist 与 LinkedList 区别?"></a>😀 Arraylist 与 LinkedList 区别?</h3><p><strong>是否保证线程安全:</strong> <code>ArrayList</code> 和 <code>LinkedList</code> 都是不同步的,也就是不保证线程安全;</p>
<p><strong>底层数据结构:</strong> <code>Arraylist</code> 底层使用的是 <strong><code>Object</code> 数组</strong>;<code>LinkedList</code> 底层使用的是 <strong>双向链表</strong> 数据结构</p>
<p><strong>插入和删除是否受元素位置的影响:</strong></p>
<ul>
<li><code>ArrayList</code> 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行<code>add(E e)</code>方法的时候, <code>ArrayList</code> 会默认将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(<code>add(int index, E element)</code>)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。</li>
<li><code>LinkedList</code> 采用链表存储,所以,如果是在头尾插入或者删除元素不受元素位置的影响(<code>add(E e)</code>、<code>addFirst(E e)</code>、<code>addLast(E e)</code>、<code>removeFirst()</code> 、 <code>removeLast()</code>),近似 O(1),如果是要在指定位置 <code>i</code> 插入和删除元素的话(<code>add(int index, E element)</code>,<code>remove(Object o)</code>) 时间复杂度近似为 O(n) ,因为需要先移动到指定位置再插入。</li>
</ul>
<p><strong>是否支持快速随机访问:</strong> <code>LinkedList</code> 不支持高效的随机元素访问,而 <code>ArrayList</code> 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于<code>get(int index)</code>方法)。</p>
<p><strong>遍历性能:</strong>论遍历ArrayList要比LinkedList快得多,ArrayList遍历最大的优势在于内存的连续性,CPU的内部缓存结构会缓存连续的内存片段,可以大幅降低读取内存的性能开销。</p>
<p><strong>内存空间占用:</strong> ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而且需要连续的内存空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。</p>
<h3 id="😀-ArrayList和LinkedList的使用选择"><a href="#😀-ArrayList和LinkedList的使用选择" class="headerlink" title="😀 ArrayList和LinkedList的使用选择"></a>😀 ArrayList和LinkedList的使用选择</h3><p>多数情况下,当你遇到访问元素比插入或者是删除元素更加频繁的时候,你应该使用ArrayList。<br>另外一方面,当你在某个特别的索引中,插入或者是删除元素更加频繁,或者你压根就不需要访问元素的时候,你会选择LinkedList。<br>这里的主要原因是,在ArrayList中访问元素的最糟糕的时间复杂度是”1″,而在LinkedList中可能就是”n”了。在ArrayList中增加或者删除某个元素,通常会调用<strong>System.arraycopy</strong>方法,这是一种极为消耗资源的操作,因此,在频繁的插入或者是删除元素的情况下,LinkedList的性能会更加好一点。</p>
<p>如果我们要展示的数据不需要进行排序,也基本不涉及到添加删除操作时,我们可以考虑使用ArrayList。</p>
<p>如果要展示的数据需要排序,那就不要用ArrayList了。直播时的评论展示,如果只考虑显示数据,不会有从中间的插入和删除操作,则用ArrayList是可以的。但是如果是qq的好友列表,使用ArrayList就不合适了。因为好友列表的排序是会随时变化的,一但有好友给你发消息,这位好友的位置就会跑到顶部去。</p>
<h3 id="😀-如何复制某个ArrayList到另一个ArrayList中去?"><a href="#😀-如何复制某个ArrayList到另一个ArrayList中去?" class="headerlink" title="😀 如何复制某个ArrayList到另一个ArrayList中去?"></a>😀 如何复制某个ArrayList到另一个ArrayList中去?</h3><ol>
<li>使用clone()方法,比如ArrayList newArray = oldArray<strong>.clone()</strong>;</li>
<li>使用ArrayList构造方法,比如:ArrayList myObject = <strong>new ArrayList</strong>(myTempObject);</li>
<li>使用Collection的copy方法。</li>
</ol>
<p>注意1和2是浅拷贝(shallow copy)。</p>
<h3 id="😀-Arrays-asList返回的List与new-ArrayList的区别"><a href="#😀-Arrays-asList返回的List与new-ArrayList的区别" class="headerlink" title="😀 Arrays.asList返回的List与new ArrayList的区别"></a>😀 Arrays.asList返回的List与new ArrayList的区别</h3><p>Arrays.asList返回的List是个固定大小的List,这个ArrayList不是util包中的ArrayList,而只是Arrays类的一个继承了AbstractList的内部类。这个类确实没有覆盖父类的实现。在AbstractList中,明确提到了不覆盖就会抛UnsupportedOperationException异常的方法有3个:add(int index, E element),set(int index, E element),remove(int index)。而上面的代码中只覆盖了set方法,可能会调用这几个方法的add(E element),clear(),addAll(int index, Collection<? extends E> c),甚至iterator()方法都没有覆盖,也就是说上面的几个方法都可能在调用中报错。由此可见JDK设计的这个返回List,只支持遍历和取值,不能做任何修改,只能作为传递值的桥梁。</p>
<h3 id="😀-无序性和不可重复性的含义是什么"><a href="#😀-无序性和不可重复性的含义是什么" class="headerlink" title="😀 无序性和不可重复性的含义是什么"></a>😀 无序性和不可重复性的含义是什么</h3><p>什么是无序性?无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的。</p>
<p>什么是不可重复性?不可重复性是指添加的元素按照 equals()判断时 ,返回 false,需要同时重写 equals()方法和 HashCode()方法。</p>
<h3 id="😀-比较-HashSet、LinkedHashSet-和-TreeSet-三者的异同"><a href="#😀-比较-HashSet、LinkedHashSet-和-TreeSet-三者的异同" class="headerlink" title="😀 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同"></a>😀 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同</h3><p><code>HashSet</code>、<code>LinkedHashSet</code> 和 <code>TreeSet</code> 都是 <code>Set</code> 接口的实现类,都能保证元素唯一,并且都不是线程安全的。</p>
<p><code>HashSet</code>、<code>LinkedHashSet</code> 和 <code>TreeSet</code> 的主要区别在于底层数据结构不同。<code>HashSet</code> 的底层数据结构是哈希表(基于 <code>HashMap</code> 实现)。<code>LinkedHashSet</code> 的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。<code>TreeSet</code> 底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。</p>
<p>底层数据结构不同又导致这三者的应用场景不同。<code>HashSet</code> 用于不需要保证元素插入和取出顺序的场景,<code>LinkedHashSet</code> 用于保证元素的插入和取出顺序满足 FIFO 的场景,<code>TreeSet</code> 用于支持对元素自定义排序规则的场景。</p>
<h3 id="😀-ArrayDeque-与-LinkedList-的区别"><a href="#😀-ArrayDeque-与-LinkedList-的区别" class="headerlink" title="😀 ArrayDeque 与 LinkedList 的区别"></a>😀 ArrayDeque 与 LinkedList 的区别</h3><p><code>ArrayDeque</code> 和 <code>LinkedList</code> 都实现了 <code>Deque</code> 接口,两者都具有队列的功能,但两者有什么区别呢?</p>
<ul>
<li><code>ArrayDeque</code> 是基于可变长的数组和双指针来实现,而 <code>LinkedList</code> 则通过链表来实现。</li>
<li><code>ArrayDeque</code> 不支持存储 <code>NULL</code> 数据,但 <code>LinkedList</code> 支持。</li>
<li><code>ArrayDeque</code> 是在 JDK1.6 才被引入的,而<code>LinkedList</code> 早在 JDK1.2 时就已经存在。</li>
<li><code>ArrayDeque</code> 插入时可能存在扩容过程, 不过均摊后的插入操作依然为 O(1)。虽然 <code>LinkedList</code> 不需要扩容,但是每次插入数据时均需要申请新的堆空间,均摊性能相比更慢。</li>
</ul>
<p>从性能的角度上,选用 <code>ArrayDeque</code> 来实现队列要比 <code>LinkedList</code> 更好。此外,<code>ArrayDeque</code> 也可以用于实现栈。</p>
<h3 id="😀-说一说-PriorityQueue"><a href="#😀-说一说-PriorityQueue" class="headerlink" title="😀 说一说 PriorityQueue"></a>😀 说一说 PriorityQueue</h3><p><code>PriorityQueue</code> 是在 JDK1.5 中被引入的, 其与 <code>Queue</code> 的区别在于元素出队顺序是与优先级相关的,即总是优先级最高的元素先出队。</p>
<p>这里列举其相关的一些要点:</p>
<ul>
<li><code>PriorityQueue</code> 利用了二叉堆的数据结构来实现的,底层使用可变长的数组来存储数据</li>
<li><code>PriorityQueue</code> 通过堆元素的上浮和下沉,实现了在 O(logn) 的时间复杂度内插入元素和删除堆顶元素。</li>
<li><code>PriorityQueue</code> 是非线程安全的,且不支持存储 <code>NULL</code> 和 <code>non-comparable</code> 的对象。</li>
<li><code>PriorityQueue</code> 默认是小顶堆,但可以接收一个 <code>Comparator</code> 作为构造参数,从而来自定义元素优先级的先后。</li>
</ul>
<h3 id="😀-HashSet-如何检查重复"><a href="#😀-HashSet-如何检查重复" class="headerlink" title="😀 HashSet 如何检查重复"></a>😀 HashSet 如何检查重复</h3><p>当你把对象加入<code>HashSet</code>时,<code>HashSet</code> 会先计算对象的<code>hashcode</code>值来判断对象加入的位置,同时也会与其他加入的对象的 <code>hashcode</code> 值作比较,如果没有相符的 <code>hashcode</code>,<code>HashSet</code> 会假设对象没有重复出现。但是如果发现有相同 <code>hashcode</code> 值的对象,这时会调用<code>equals()</code>方法来检查 <code>hashcode</code> 相等的对象是否真的相同。如果两者相同,<code>HashSet</code> 就不会让加入操作成功。</p>
<h3 id="😀-两个对象值相同-x-equals-y-true-,但却可有不同的hash-code,这句话对不对"><a href="#😀-两个对象值相同-x-equals-y-true-,但却可有不同的hash-code,这句话对不对" class="headerlink" title="😀 两个对象值相同(x.equals(y) == true),但却可有不同的hash code,这句话对不对?"></a>😀 两个对象值相同(x.equals(y) == true),但却可有不同的hash code,这句话对不对?</h3><p>对。</p>
<p>如果对象要保存在HashSet或HashMap中,它们的equals相等,那么,它们的hashcode值就必须相等。</p>
<p>如果不是要保存在HashSet或HashMap,则与hashcode没有什么关系了,这时候hashcode不等是可以的,例如arrayList存储的对象就不用实现hashcode,当然,我们没有理由不实现,通常都会去实现的。</p>
<h3 id="😀-Map有哪些常用的实现类及其作用?"><a href="#😀-Map有哪些常用的实现类及其作用?" class="headerlink" title="😀 Map有哪些常用的实现类及其作用?"></a>😀 <strong>Map有哪些常用的实现类及其作用?</strong></h3><p><strong>HashMap:</strong>上面也说了,<strong>HashMap的底层实现是数组+链表+红黑树的形式的,</strong>同时它的<strong>数组的默认初始容量是16、扩容因子为0.75,每次采用2倍的扩容。</strong>也就是说,每当我们数组中的存储容量达到75%的时候,就需要对数组容量进行2倍的扩容。</p>
<p><strong>HashTable:</strong>HashTable接口是线程安全,但是很早之前有使用,现在几乎属于一个遗留类了,<strong>在开发中不建议使用。</strong></p>
<p><strong>ConcurrentHashMap:</strong>这是现阶段使用使用比较多的一种线程安全的Map实现类。<strong>在1.7以前使用的是分段锁机制实现的线程安全的。但是在1.8以后使用synchronized关键字实现的线程安全。</strong></p>
<h3 id="😀-HashMap和Hashtable的区别"><a href="#😀-HashMap和Hashtable的区别" class="headerlink" title="😀 HashMap和Hashtable的区别"></a>😀 HashMap和Hashtable的区别</h3><p><strong>线程是否安全:</strong> <code>HashMap</code> 是非线程安全的,<code>Hashtable</code> 是线程安全的,因为 <code>Hashtable</code> 内部的方法基本都经过<code>synchronized</code> 修饰。(如果你要保证线程安全的话就使用 <code>ConcurrentHashMap</code> 吧!);</p>
<p><strong>效率:</strong> 因为线程安全的问题,<code>HashMap</code> 要比 <code>Hashtable</code> 效率高一点。另外,<code>Hashtable</code> 基本被淘汰,不要在代码中使用它;</p>
<p><strong>对 Null key 和 Null value 的支持:</strong> <code>HashMap</code> 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出 <code>NullPointerException</code>。</p>
<p><strong>初始容量大小和每次扩充容量大小的不同 :</strong> ① 创建时如果不指定容量初始值,<code>Hashtable</code> 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。<code>HashMap</code> 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 <code>HashMap</code> 会将其扩充为 2 的幂次方大小(<code>HashMap</code> 中的<code>tableSizeFor()</code>方法保证)。也就是说 <code>HashMap</code> 总是使用 2 的幂作为哈希表的大小.</p>
<p><strong>底层数据结构:</strong> JDK1.8 以后的 <code>HashMap</code> 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。</p>
<h3 id="😀-HashMap和TreeMap区别"><a href="#😀-HashMap和TreeMap区别" class="headerlink" title="😀 HashMap和TreeMap区别"></a>😀 HashMap和TreeMap区别</h3><ul>
<li><strong>从类的定义来看</strong>,HashMap和TreeMap都继承自AbstractMap,不同的是HashMap实现的是Map接口,而TreeMap实现的是NavigableMap接口。NavigableMap是SortedMap的一种,实现了对Map中key的排序。这样两者的第一个区别就出来了,TreeMap是排序的而HashMap不是。</li>
<li><strong>再看看HashMap和TreeMap的构造函数的区别。</strong></li>
</ul>
<p><img src="https://cdn.jsdelivr.net/gh/HelingCode/CDN-vulkhe-Pic@master/uPic/2023/12/17/image-20221017120506992-20231217153114911.png" alt="image-20221017120506992"></p>
<p>HashMap除了默认的无参构造函数之外,还可以接受两个参数initialCapacity和loadFactor。</p>
<p>HashMap的底层结构是Node的数组:</p>
<figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line">transient <span class="title class_">Node</span><K,V>[] table</span><br></pre></td></tr></table></figure>