-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathatom.xml
657 lines (410 loc) · 566 KB
/
atom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
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
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Zhangfei's Blog</title>
<subtitle>Quick notes</subtitle>
<link href="/atom.xml" rel="self"/>
<link href="http://zhangfei.men/"/>
<updated>2020-07-28T01:08:16.457Z</updated>
<id>http://zhangfei.men/</id>
<author>
<name>Zhang Fei</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>Redis数据结构</title>
<link href="http://zhangfei.men/posts/85682d75/"/>
<id>http://zhangfei.men/posts/85682d75/</id>
<published>2020-04-28T02:44:01.000Z</published>
<updated>2020-07-28T01:08:16.457Z</updated>
<content type="html"><![CDATA[<h2 id="Redis数据类型简介"><a href="#Redis数据类型简介" class="headerlink" title="Redis数据类型简介"></a>Redis数据类型简介</h2><blockquote><p><a href="https://redis.io/topics/data-types-intro" target="_blank" rel="noopener">Introduction to Redis data types</a></p></blockquote><h3 id="Redis支持的所有数据结构列表"><a href="#Redis支持的所有数据结构列表" class="headerlink" title="Redis支持的所有数据结构列表"></a>Redis支持的所有数据结构列表</h3><ul><li><a href="https://redis.io/topics/data-types-intro#redis-strings" target="_blank" rel="noopener">String</a>: 字符串;</li><li><a href="https://redis.io/topics/data-types-intro#redis-lists" target="_blank" rel="noopener">List</a>: 根据插入顺序排序的字符串元素集合,基本上是一个双向链表;</li><li><a href="https://redis.io/topics/data-types-intro#redis-sets" target="_blank" rel="noopener">Set</a>: 不重复且无序的字符串元素的集合;</li><li><a href="https://redis.io/topics/data-types-intro#redis-sorted-sets" target="_blank" rel="noopener">SortedSet</a>: 排序集,类似于<code>Set</code>,但每个字符串元素都与一个浮点数(称为<code>score</code>)相关联,元素总是按<code>score</code>排序,因此与<code>Set</code>不同,可以检索一系列元素(例如,您可能会问:给我前10个,或后10个);</li><li><a href="https://redis.io/topics/data-types-intro#redis-hashes" target="_blank" rel="noopener">Hash</a>: 是由与值关联的字段组成的映射,key 和 value 都是字符串;</li><li><a href="https://redis.io/topics/data-types-intro#bitmaps" target="_blank" rel="noopener">Bitmap(Bit array)</a>: 通过特殊的命令,你可以将 String 值当作一系列 bits 处理:可以设置和清除单独的 bits,数出所有设为 1 的 bits 的数量,找到最前的被设为 1 或 0 的 bit,等等</li><li><a href="https://redis.io/topics/data-types-intro#hyperloglogs" target="_blank" rel="noopener">HyperLogLog</a>: 用于估计<code>Set</code>中元素数量的的的概率数据结构;</li><li><a href="https://redis.io/topics/streams-intro" target="_blank" rel="noopener">Stream (Redis 5.0+)</a>: 一种更抽象的日志数据类型:就像一个日志文件一样,总是以仅追加的方式操作,Redis的stream就是一种append only的数据类型。</li></ul><p><strong>Redis key 的大小最大为 512MB</strong></p><h3 id="String"><a href="#String" class="headerlink" title="String"></a>String</h3><p>值最大的容量为 512MB.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">> 127.0.0.1@6379 connected!</span><br><span class="line">> set key value</span><br><span class="line">OK</span><br><span class="line">> get key</span><br><span class="line">value</span><br><span class="line">> set key value2 nx // 当 key 不存在时才会成功,可以利用此特性来实现分布式锁</span><br><span class="line">null</span><br><span class="line">> get key</span><br><span class="line">value</span><br></pre></td></tr></table></figure><h3 id="List"><a href="#List" class="headerlink" title="List"></a>List</h3><p>Redis中List是通过链表来实现的</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line">> 127.0.0.1@6379 connected!</span><br><span class="line">> rpush list A // 在链表尾部(右侧)插入</span><br><span class="line">1</span><br><span class="line">> rpush list B</span><br><span class="line">2</span><br><span class="line">> lpush list C // 在链表头部(左侧)插入</span><br><span class="line">3</span><br><span class="line">> lrange list 0 -1 // 根据索引获取元素 负数表示从尾部开始计算 e.g. -1 表示列表的最后一个元素</span><br><span class="line">C</span><br><span class="line">A</span><br><span class="line">B</span><br><span class="line">> rpush list D E F G // rpush, lpush 都支持在单个调用中插入多个元素</span><br><span class="line">7</span><br><span class="line">> lrange list 0 -1</span><br><span class="line">C</span><br><span class="line">A</span><br><span class="line">B</span><br><span class="line">D</span><br><span class="line">E</span><br><span class="line">F</span><br><span class="line">G</span><br><span class="line">> rpop list // 从尾部(右侧)取出数据</span><br><span class="line">G</span><br><span class="line">> lpop list // 从头部(左侧)取出数据</span><br><span class="line">C</span><br><span class="line">> lrange list 0 -1</span><br><span class="line">A</span><br><span class="line">B</span><br><span class="line">D</span><br><span class="line">E</span><br><span class="line">F</span><br></pre></td></tr></table></figure><h3 id="Set"><a href="#Set" class="headerlink" title="Set"></a>Set</h3><p>Redis Set 是不重复且无序的字符串集合</p><h3 id="SortedSet"><a href="#SortedSet" class="headerlink" title="SortedSet"></a>SortedSet</h3><h3 id="Hash"><a href="#Hash" class="headerlink" title="Hash"></a>Hash</h3><h3 id="Bitmap"><a href="#Bitmap" class="headerlink" title="Bitmap"></a>Bitmap</h3><h3 id="HyperLogLog"><a href="#HyperLogLog" class="headerlink" title="HyperLogLog"></a>HyperLogLog</h3><h3 id="Stream"><a href="#Stream" class="headerlink" title="Stream"></a>Stream</h3><h3 id="其他值得注意的特性"><a href="#其他值得注意的特性" class="headerlink" title="其他值得注意的特性"></a>其他值得注意的特性</h3><ul><li>Pub/Sub</li></ul>]]></content>
<summary type="html">
<h2 id="Redis数据类型简介"><a href="#Redis数据类型简介" class="headerlink" title="Redis数据类型简介"></a>Redis数据类型简介</h2><blockquote>
<p><a href="https://redis.io/topics/data-types-intro" target="_blank" rel="noopener">Introduction to Redis data types</a></p>
</blockquote>
<h3 id="Redis支持的所有数据结构列表"><a href="#Redis支持的所有数据结构列表" class="headerlink" title="Redis支持的所有数据结构列表"></a>Redis支持的所有数据结构列表</h3><ul>
<li><a href="https://redis.io/topics/data-types-intro#redis-strings" target="_blank" rel="noopener">String</a>: 字符串;</li>
<li><a href="https://redis.io/topics/data-types-intro#redis-lists" target="_blank" rel="noopener">List</a>: 根据插入顺序排序的字符串元素集合,基本上是一个双向链表;</li>
<li><a href="https://redis.io/topics/data-types-intro#redis-sets" target="_blank" rel="noopener">Set</a>: 不重复且无序的字符串元素的集合;</li>
<li><a href="https://redis.io/topics/data-types-intro#redis-sorted-sets" target="_blank" rel="noopener">SortedSet</a>: 排序集,类似于<code>Set</code>,但每个字符串元素都与一个浮点数(称为<code>score</code>)相关联,元素总是按<code>score</code>排序,因此与<code>Set</code>不同,可以检索一系列元素(例如,您可能会问:给我前10个,或后10个);</li>
<li><a href="https://redis.io/topics/data-types-intro#redis-hashes" target="_blank" rel="noopener">Hash</a>: 是由与值关联的字段组成的映射,key 和 value 都是字符串;</li>
<li><a href="https://redis.io/topics/data-types-intro#bitmaps" target="_blank" rel="noopener">Bitmap(Bit array)</a>: 通过特殊的命令,你可以将 String 值当作一系列 bits 处理:可以设置和清除单独的 bits,数出所有设为 1 的 bits 的数量,找到最前的被设为 1 或 0 的 bit,等等</li>
<li><a href="https://redis.io/topics/data-types-intro#hyperloglogs" target="_blank" rel="noopener">HyperLogLog</a>: 用于估计<code>Set</code>中元素数量的的的概率数据结构;</li>
<li><a href="https://redis.io/topics/streams-intro" target="_blank" rel="noopener">Stream (Redis 5.0+)</a>: 一种更抽象的日志数据类型:就像一个日志文件一样,总是以仅追加的方式操作,Redis的stream就是一种append only的数据类型。</li>
</ul>
<p><strong>Redis key 的大小最大为 512MB</strong></p>
</summary>
<category term="Redis" scheme="http://zhangfei.men/categories/Redis/"/>
<category term="Redis" scheme="http://zhangfei.men/tags/Redis/"/>
<category term="Data structure" scheme="http://zhangfei.men/tags/Data-structure/"/>
</entry>
<entry>
<title>深入分析Synchronized原理</title>
<link href="http://zhangfei.men/posts/8baae715/"/>
<id>http://zhangfei.men/posts/8baae715/</id>
<published>2020-03-14T09:11:35.000Z</published>
<updated>2020-07-28T01:08:16.457Z</updated>
<content type="html"><![CDATA[<p>记得开始学习Java的时候,一遇到多线程情况就使用synchronized,相对于当时的我们来说synchronized是这么的神奇而又强大,那个时候我们赋予它一个名字“同步”,也成为了我们解决多线程情况的百试不爽的良药。但是,随着学习的进行我们知道在JDK1.5之前synchronized是一个重量级锁,相对于j.u.c.Lock,它会显得那么笨重,以至于我们认为它不是那么的高效而慢慢摒弃它。<br>不过,随着Javs SE 1.6对synchronized进行的各种优化后,synchronized并不会显得那么重了。下面来一起探索synchronized的基本使用、实现机制、Java是如何对它进行了优化、锁优化机制、锁的存储结构等升级过程。</p><h2 id="1-基本使用"><a href="#1-基本使用" class="headerlink" title="1 基本使用"></a>1 基本使用</h2><p>Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。Synchronized的作用主要有三个:</p><blockquote><ol><li>原子性:确保线程互斥的访问同步代码;</li><li>可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 “<strong>对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值</strong>” 来保证的;</li><li>有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”;</li></ol></blockquote><p>从语法上讲,Synchronized可以把任何一个非null对象作为”锁”,在HotSpot JVM实现中,<strong>锁有个专门的名字:对象监视器(Object Monitor)</strong>。<br>Synchronized总共有三种用法:</p><blockquote><ol><li>当synchronized作用在实例方法时,监视器锁(monitor)便是对象实例(this);</li><li>当synchronized作用在静态方法时,监视器锁(monitor)便是对象的Class实例,因为Class数据存在于永久代,因此静态方法锁相当于该类的一个全局锁;</li><li>当synchronized作用在某一个对象实例时,监视器锁(monitor)便是括号括起来的对象实例;</li></ol></blockquote><p>注意,synchronized 内置锁 是一种 对象锁(锁的是对象而非引用变量),<strong>作用粒度是对象 ,可以用来实现对 临界资源的同步互斥访问 ,是 可重入 的。其可重入最大的作用是避免死锁</strong>,如:</p><blockquote><p><strong>子类同步方法调用了父类同步方法,如没有可重入的特性,则会发生死锁;</strong></p></blockquote><h2 id="2-同步原理"><a href="#2-同步原理" class="headerlink" title="2 同步原理"></a>2 同步原理</h2><p>数据同步需要依赖锁,那锁的同步又依赖谁?<strong>synchronized给出的答案是在软件层面依赖JVM,而j.u.c.Lock给出的答案是在硬件层面依赖特殊的CPU指令。</strong><br>当一个线程访问同步代码块时,首先是需要得到锁才能执行同步代码,当退出或者抛出异常时必须要释放锁,那么它是如何来实现这个机制的呢?我们先看一段简单的代码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.paddx.test.concurrent;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SynchronizedDemo</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">method</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">synchronized</span> (<span class="keyword">this</span>) {</span><br><span class="line"> System.out.println(<span class="string">"Method 1 start"</span>);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>查看反编译后结果:</p><p><img data-src="/images/pasted-214.png" alt="upload successful"></p><center>反编译结果</center><ol><li><p><strong>monitorenter</strong>:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:</p><blockquote><ol><li>如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;</li><li>如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;</li><li>如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;</li></ol></blockquote></li><li><p>monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。</p><blockquote><p>monitorexit指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁;</p></blockquote></li></ol><p>通过上面两段描述,我们应该能很清楚的看出Synchronized的实现原理,<strong>Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。</strong><br>再来看一下同步方法:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.paddx.test.concurrent;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SynchronizedMethod</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title">method</span><span class="params">()</span> </span>{</span><br><span class="line"> System.out.println(<span class="string">"Hello World!"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>查看反编译后结果:</p><p><img data-src="/images/pasted-215.png" alt="upload successful"></p><center>反编译结果</center><p>从编译的结果来看,方法的同步并没有通过指令 <code>monitorenter</code> 和 <code>monitorexit</code> 来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了 <code>ACC_SYNCHRONIZED</code> 标示符。JVM就是根据该标示符来实现方法的同步的:</p><blockquote><p>当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。<strong>在方法执行期间,其他任何线程都无法再获得同一个monitor对象。</strong></p></blockquote><p>两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。</p><h2 id="3-同步概念"><a href="#3-同步概念" class="headerlink" title="3 同步概念"></a>3 同步概念</h2><h3 id="3-1-Java对象头"><a href="#3-1-Java对象头" class="headerlink" title="3.1 Java对象头"></a>3.1 Java对象头</h3><p>在JVM中 <strong>,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。</strong> 如下图所示:</p><p><img data-src="/images/pasted-216.png" alt="upload successful"></p><ol><li>实例数据:存放类的属性数据信息,包括父类的属性信息;</li><li>对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;</li><li><strong>对象头:Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是 如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。</strong></li></ol><p>Synchronized用的锁就是存在Java对象头里的,那么什么是Java对象头呢?Hotspot虚拟机的对象头主要包括两部分数据:<strong>Mark Word(标记字段)、</strong>Class Pointer(类型指针)。其中 Class Pointer是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。 Java对象头具体结构描述如下:</p><p><img data-src="/images/pasted-217.png" alt="upload successful"></p><center>Java对象头结构组成</center><p>Mark Word用于存储对象自身的运行时数据,如:哈希码(HashCode)、GC分代年龄、<strong>锁状态标志</strong>、线程持有的锁、偏向线程 ID、偏向时间戳等。比如锁膨胀就是借助Mark Word的偏向的线程ID 参考:<a href="https://www.cnblogs.com/aspirant/p/11705068.html" target="_blank" rel="noopener">JAVA锁的膨胀过程和优化(阿里)</a> 阿里也经常问的问题<br>下图是Java对象头 无锁状态下Mark Word部分的存储结构(32位虚拟机):</p><p><img data-src="/images/pasted-218.png" alt="upload successful"></p><center>Mark Word存储结构</center><p>对象头信息是与对象自身定义的数据无关的额外存储成本,但是考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说,Mark Word会随着程序的运行发生变化,可能变化为存储以下4种数据:</p><p><img data-src="/images/pasted-219.png" alt="upload successful"></p><center>Mark Word可能存储4种数据</center><p>在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:</p><p><img data-src="/images/pasted-220.png" alt="upload successful"></p><center>64位Mark Word存储结构</center><p>对象头的最后两位存储了锁的标志位,01是初始状态,未加锁,其对象头里存储的是对象本身的哈希码,随着锁级别的不同,对象头里会存储不同的内容。偏向锁存储的是当前占用此对象的线程ID;而轻量级则存储指向线程栈中锁记录的指针。从这里我们可以看到,“锁”这个东西,可能是个锁记录+对象头里的引用指针(判断线程是否拥有锁时将线程的锁记录地址和对象头里的指针地址比较),也可能是对象头里的线程ID(判断线程是否拥有锁时将线程的ID和对象头里存储的线程ID比较)。_ _</p><p><img data-src="/images/pasted-221.png" alt="upload successful"></p><center>HotSpot虚拟机对象头Mark Word</center><h3 id="3-2-对象头中Mark-Word与线程中Lock-Record"><a href="#3-2-对象头中Mark-Word与线程中Lock-Record" class="headerlink" title="3.2 对象头中Mark Word与线程中Lock Record"></a>3.2 对象头中Mark Word与线程中Lock Record</h3><p>在线程进入同步代码块的时候,如果此同步对象没有被锁定,即它的锁标志位是01,则虚拟机首先在当前线程的栈中创建我们称之为“锁记录(Lock Record)”的空间,用于存储锁对象的Mark Word的拷贝,官方把这个拷贝称为Displaced Mark Word。整个Mark Word及其拷贝至关重要。<br><strong>Lock Record是线程私有的数据结构</strong>,每一个线程都有一个可用Lock Record列表,同时还有一个全局的可用列表。每一个被锁住的对象Mark Word都会和一个Lock Record关联(对象头的MarkWord中的Lock Word指向Lock Record的起始地址),同时Lock Record中有一个Owner字段存放拥有该锁的线程的唯一标识(或者<code>object mark word</code>),表示该锁被这个线程占用。如下图所示为Lock Record的内部结构:</p><table><thead><tr><th>Lock Record</th><th>描述</th></tr></thead><tbody><tr><td>Owner</td><td>初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;</td></tr><tr><td>EntryQ</td><td>关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程;</td></tr><tr><td>RcThis</td><td>表示blocked或waiting在该monitor record上的所有线程的个数;</td></tr><tr><td>Nest</td><td>用来实现 重入锁的计数;</td></tr><tr><td>HashCode</td><td>保存从对象头拷贝过来的HashCode值(可能还包含GC age)。</td></tr><tr><td>Candidate</td><td>用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。</td></tr></tbody></table><h3 id="3-3-监视器(Monitor)"><a href="#3-3-监视器(Monitor)" class="headerlink" title="3.3 监视器(Monitor)"></a>3.3 监视器(Monitor)</h3><p>任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是 基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。 </p><ol><li><strong>MonitorEnter指令:插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁;</strong></li><li><strong>MonitorExit指令:插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit;</strong></li></ol><p>那什么是Monitor?可以把它理解为 一个同步工具,也可以描述为 一种同步机制,它通常被 描述为一个对象。<br>与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,<strong>每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁</strong>。<br>也就是通常说Synchronized的对象锁,MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">ObjectMonitor() {</span><br><span class="line"> _header = <span class="literal">NULL</span>;</span><br><span class="line"> _count = <span class="number">0</span>; <span class="comment">// 记录个数</span></span><br><span class="line"> _waiters = <span class="number">0</span>,</span><br><span class="line"> _recursions = <span class="number">0</span>;</span><br><span class="line"> _object = <span class="literal">NULL</span>;</span><br><span class="line"> _owner = <span class="literal">NULL</span>;</span><br><span class="line"> _WaitSet = <span class="literal">NULL</span>; <span class="comment">// 处于wait状态的线程,会被加入到_WaitSet</span></span><br><span class="line"> _WaitSetLock = <span class="number">0</span> ;</span><br><span class="line"> _Responsible = <span class="literal">NULL</span> ;</span><br><span class="line"> _succ = <span class="literal">NULL</span> ;</span><br><span class="line"> _cxq = <span class="literal">NULL</span> ;</span><br><span class="line"> FreeNext = <span class="literal">NULL</span> ;</span><br><span class="line"> _EntryList = <span class="literal">NULL</span> ; <span class="comment">// 处于等待锁block状态的线程,会被加入到该列表</span></span><br><span class="line"> _SpinFreq = <span class="number">0</span> ;</span><br><span class="line"> _SpinClock = <span class="number">0</span> ;</span><br><span class="line"> OwnerIsThread = <span class="number">0</span> ;</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:</p><blockquote><ol><li>首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;</li><li>若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒;</li><li>若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);</li></ol></blockquote><p>同时 <strong>,Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须在同步代码块中使用。</strong><br>监视器Monitor有两种同步方式:互斥与协作。多线程环境下线程之间如果需要共享数据,需要解决互斥访问数据的问题,监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。<br>什么时候需要协作? 比如:</p><blockquote><p>一个线程向缓冲区写数据,另一个线程从缓冲区读数据,如果读线程发现缓冲区为空就会等待,当写线程向缓冲区写入数据,就会唤醒读线程,这里读线程和写线程就是一个合作关系。JVM通过Object类的wait方法来使自己等待,在调用wait方法后,该线程会释放它持有的监视器,直到其他线程通知它才有执行的机会。一个线程调用notify方法通知在等待的线程,这个等待的线程并不会马上执行,而是要通知线程释放监视器后,它重新获取监视器才有执行的机会。如果刚好唤醒的这个线程需要的监视器被其他线程抢占,那么这个线程会继续等待。Object类中的notifyAll方法可以解决这个问题,它可以唤醒所有等待的线程,总有一个线程执行。</p></blockquote><p><img data-src="/images/pasted-222.png" alt="upload successful"></p><p>如上图所示,一个线程通过1号门进入Entry Set(入口区),如果在入口区没有线程等待,那么这个线程就会获取监视器成为监视器的Owner,然后执行监视区域的代码。如果在入口区中有其它线程在等待,那么新来的线程也会和这些线程一起等待。线程在持有监视器的过程中,有两个选择,一个是正常执行监视器区域的代码,释放监视器,通过5号门退出监视器;还有可能等待某个条件的出现,于是它会通过3号门到Wait Set(等待区)休息,直到相应的条件满足后再通过4号门进入重新获取监视器再执行。<br>注意:</p><blockquote><p>当一个线程释放监视器时,在入口区和等待区的等待线程都会去竞争监视器,如果入口区的线程赢了,会从2号门进入;如果等待区的线程赢了会从4号门进入。只有通过3号门才能进入等待区,在等待区中的线程只有通过4号门才能退出等待区,也就是说一个线程只有在持有监视器时才能执行wait操作,处于等待的线程只有再次获得监视器才能退出等待状态。</p></blockquote><h2 id="4-锁的优化"><a href="#4-锁的优化" class="headerlink" title="4 锁的优化"></a>4 锁的优化</h2><p>从JDK5引入了现代操作系统新增加的CAS原子操作( JDK5中并没有对synchronized关键字做优化,而是体现在J.U.C中,所以在该版本concurrent包有更好的性能 ),从JDK6开始,就对synchronized的实现机制进行了较大调整,包括使用JDK5引进的CAS自旋之外,还增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略。由于此关键字的优化使得性能极大提高,同时语义清晰、操作简单、无需手动关闭,所以推荐在允许的情况下尽量使用此关键字,同时在性能上此关键字还有优化的空间。<br>锁主要存在四种状态,依次是 <strong>:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态</strong>,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。<br>在 JDK 1.6 中默认是开启偏向锁和轻量级锁的,可以通过-XX:-UseBiasedLocking来禁用偏向锁。</p><h3 id="4-1-自旋锁"><a href="#4-1-自旋锁" class="headerlink" title="4.1 自旋锁"></a>4.1 自旋锁</h3><p>线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。<br>所以引入自旋锁,何谓自旋锁? <br>所谓自旋锁,就是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。<br>自旋锁适用于锁保护的临界区很小的情况,临界区很小的话,锁占用的时间就很短。自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了CPU处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,典型的占着茅坑不拉屎,这样反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。<br>自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启,在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整。<br>如果通过参数-XX:PreBlockSpin来调整自旋锁的自旋次数,会带来诸多不便。假如将参数调整为10,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如多自旋一两次就可以获取锁),是不是很尴尬。于是JDK1.6引入自适应的自旋锁,让虚拟机会变得越来越聪明。</p><h3 id="4-2-适应性自旋锁"><a href="#4-2-适应性自旋锁" class="headerlink" title="4.2 适应性自旋锁"></a>4.2 适应性自旋锁</h3><p>JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。那它如何进行适应性自旋呢? <br><strong>线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。</strong><br>有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。</p><h3 id="4-3-锁消除"><a href="#4-3-锁消除" class="headerlink" title="4.3 锁消除"></a>4.3 锁消除</h3><p>为了保证数据的完整性,在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。</p><blockquote><p>锁消除的依据是逃逸分析的数据支持</p></blockquote><p>如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义的请求锁的时间。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是对于程序员来说这还不清楚么?在明明知道不存在数据竞争的代码块前加上同步吗?但是有时候程序并不是我们所想的那样?虽然没有显示使用锁,但是在使用一些JDK的内置API时,如StringBuffer、Vector、HashTable等,这个时候会存在隐形的加锁操作。比如StringBuffer的append()方法,Vector的add()方法:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">vectorTest</span><span class="params">()</span></span>{</span><br><span class="line"> Vector<String> vector = <span class="keyword">new</span> Vector<String>();</span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">int</span> i = <span class="number">0</span> ; i < <span class="number">10</span> ; i++){</span><br><span class="line"> vector.add(i + <span class="string">""</span>);</span><br><span class="line"> }</span><br><span class="line"> System.out.println(vector);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在运行这段代码时,JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外,所以JVM可以大胆地将vector内部的加锁操作消除。</p><h3 id="4-4-锁粗化"><a href="#4-4-锁粗化" class="headerlink" title="4.4 锁粗化"></a>4.4 锁粗化</h3><p>在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是 为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。<br>在大多数的情况下,上述观点是正确的。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。<br>锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁<<br>如上面实例:</p><blockquote><p><strong>vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。</strong></p></blockquote><h3 id="4-5-偏向锁"><a href="#4-5-偏向锁" class="headerlink" title="4.5 偏向锁"></a>4.5 偏向锁</h3><p>偏向锁是JDK6中的重要引进,因为HotSpot作者经过研究实践发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。<br>偏向锁是在单线程执行代码块时使用的机制,如果在多线程并发的环境下(即线程A尚未执行完同步代码块,线程B发起了申请锁的申请),则一定会转化为轻量级锁或者重量级锁。<br>在JDK5中偏向锁默认是关闭的,而到了JDK6中偏向锁已经默认开启。如果并发数较大同时同步代码块执行时间较长,则被多个线程同时访问的概率就很大,就可以使用参数-XX:-UseBiasedLocking来禁止偏向锁(但这是个JVM参数,不能针对某个对象锁来单独设置)。<br>引入偏向锁主要目的是:为了在没有多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。因为轻量级锁的加锁解锁操作是需要依赖多次CAS原子指令的,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗也必须小于节省下来的CAS原子指令的性能消耗)。</p><blockquote><p>轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。</p></blockquote><p>那么偏向锁是如何来减少不必要的CAS操作呢?首先我们看下无竞争下锁存在什么问题:</p><blockquote><p><strong>现在几乎所有的锁都是可重入的,即已经获得锁的线程可以多次锁住/解锁监视对象,按照之前的HotSpot设计,每次加锁/解锁都会涉及到一些CAS操作(比如对等待队列的CAS操作),CAS操作会延迟本地调用,因此偏向锁的想法是 一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个线程,之后的多次调用则可以避免CAS操作,说白了就是置个变量,如果发现为true则无需再走各种加锁/解锁流程。</strong></p></blockquote><p>CAS为什么会引入本地延迟?这要从SMP(对称多处理器)架构说起,下图大概表明了SMP的结构:</p><p><img data-src="/images/pasted-223.png" alt="upload successful"></p><center>SMP(对称多处理器)架构</center><blockquote><p>其意思是 所有的CPU会共享一条系统总线(BUS),靠此总线连接主存。每个核都有自己的一级缓存,各核相对于BUS对称分布,因此这种结构称为“对称多处理器”。</p></blockquote><p>而CAS的全称为Compare-And-Swap,是一条CPU的原子指令,其作用是让CPU比较后原子地更新某个位置的值,经过调查发现,其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的接口。<br>例如:Core1和Core2可能会同时把主存中某个位置的值Load到自己的L1 Cache中,当Core1在自己的L1 Cache中修改这个位置的值时,会通过总线,使Core2中L1 Cache对应的值“失效”,而Core2一旦发现自己L1 Cache中的值失效(称为Cache命中缺失)则会通过总线从内存中加载该地址最新的值,大家通过总线的来回通信称为“Cache一致性流量”,因为总线被设计为固定的“通信能力”,如果Cache一致性流量过大,总线将成为瓶颈。而当Core1和Core2中的值再次一致时,称为“Cache一致性”,从这个层面来说,锁设计的终极目标便是减少Cache一致性流量。<br>而CAS恰好会导致Cache一致性流量,如果有很多线程都共享同一个对象,当某个Core CAS成功时必然会引起总线风暴,这就是所谓的本地延迟,本质上偏向锁就是为了消除CAS,降低Cache一致性流量。<br><em>Cache一致性:</em></p><blockquote><p>上面提到Cache一致性,其实是有协议支持的,现在通用的协议是MESI(最早由Intel开始支持),具体参考:<a href="http://en.wikipedia.org/wiki/MESI_protocol" target="_blank" rel="noopener">http://en.wikipedia.org/wiki/MESI_protocol</a>。</p></blockquote><p><em>Cache一致性流量的例外情况:</em></p><blockquote><p>其实也不是所有的CAS都会导致总线风暴,这跟Cache一致性协议有关,具体参考:<a href="http://blogs.oracle.com/dave/entry/biased_locking_in_hotspot" target="_blank" rel="noopener">http://blogs.oracle.com/dave/entry/biased_locking_in_hotspot</a></p></blockquote><p><em>NUMA(Non Uniform Memory Access Achitecture)架构:</em></p><blockquote><p>与SMP对应还有非对称多处理器架构,现在主要应用在一些高端处理器上,主要特点是没有总线,没有公用主存,每个Core有自己的内存,针对这种结构此处不做讨论。</p></blockquote><p>所以,当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程进入和退出同步块时不需要花费CAS操作来争夺锁资源,只需要检查是否为偏向锁、锁标识为以及ThreadID即可,处理流程如下:</p><blockquote><ol><li>检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁标识位为01;</li><li>若为可偏向状态,则测试线程ID是否为当前线程ID,如果是,则执行步骤(5),否则执行步骤(3);</li><li>如果测试线程ID不为当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行线程(4);</li><li>通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块;</li><li>执行同步代码块;</li></ol></blockquote><p>偏向锁的释放采用了 一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。偏向锁的撤销需要 等待全局安全点(这个时间点是上没有正在执行的代码)。其步骤如下:</p><blockquote><ol><li>暂停拥有偏向锁的线程;</li><li>判断锁对象是否还处于被锁定状态,否,则恢复到无锁状态(01),以允许其余线程竞争。是,则挂起持有锁的当前线程,并将指向当前线程的锁记录地址的指针放入对象头Mark Word,升级为轻量级锁状态(00),然后恢复持有锁的当前线程,进入轻量级锁的竞争模式;<br>注意:此处将 当前线程挂起再恢复的过程中并没有发生锁的转移,仍然在当前线程手中,只是穿插了个 “将对象头中的线程ID变更为指向锁记录地址的指针” 这么个事。</li></ol></blockquote><p><img data-src="/images/pasted-224.png" alt="upload successful"></p><center>偏向锁的获取和释放过程</center><h3 id="4-6-轻量级锁"><a href="#4-6-轻量级锁" class="headerlink" title="4.6 轻量级锁"></a>4.6 轻量级锁</h3><p>引入轻量级锁的主要目的是 在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下:</p><ol><li>在线程进入同步块时,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。此时线程堆栈与对象头的状态如下图所示:</li></ol><p><img data-src="/images/pasted-225.png" alt="upload successful"></p><center>轻量级锁CAS操作之前线程堆栈与对象的状态</center><ol start="2"><li>拷贝对象头中的Mark Word复制到锁记录(Lock Record)中;</li><li>拷贝成功后,虚拟机将使用CAS操作尝试将对象Mark Word中的Lock Word更新为指向当前线程Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤(4),否则执行步骤(5);</li><li>如果这个更新动作成功了,那么当前线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,此时线程堆栈与对象头的状态如下图所示:</li></ol><p><img data-src="/images/pasted-226.png" alt="upload successful"></p><center>轻量级锁CAS操作之后线程堆栈与对象的状态</center><ol start="5"><li>如果这个更新操作失败了,虚拟机首先会检查对象Mark Word中的Lock Word是否指向当前线程的栈帧,如果是,就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,进入自旋执行(3),若自旋结束时仍未获得锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,当前线程以及后面等待锁的线程也要进入阻塞状态。</li></ol><p>轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:</p><blockquote><ol><li>通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word;</li><li>如果替换成功,整个同步过程就完成了,恢复到无锁状态(01);</li><li>如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程;</li></ol></blockquote><p>对于轻量级锁,其性能提升的依据是 “对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢。</p><p><img data-src="/images/pasted-227.png" alt="upload successful"></p><center>轻量级锁的获取和释放过程</center><ol><li><p>为什么升级为轻量锁时要把对象头里的Mark Word复制到线程栈的锁记录中呢?</p><blockquote><p>因为在申请对象锁时 需要以该值作为CAS的比较条件,同时在升级到重量级锁的时候,能通过这个比较判定是否在持有锁的过程中此锁被其他线程申请过,如果被其他线程申请了,则在释放锁的时候要唤醒被挂起的线程。</p></blockquote></li><li><p>为什么会尝试CAS不成功以及什么情况下会不成功?</p><blockquote><p>CAS本身是不带锁机制的,其是通过比较而来。假设如下场景:线程A和线程B都在对象头里的锁标识为无锁状态进入,那么如线程A先更新对象头为其锁记录指针成功之后,线程B再用CAS去更新,就会发现此时的对象头已经不是其操作前的对象HashCode了,所以CAS会失败。也就是说,只有两个线程并发申请锁的时候会发生CAS失败。<br>然后线程B进行CAS自旋,等待对象头的锁标识重新变回无锁状态或对象头内容等于对象HashCode(因为这是线程B做CAS操作前的值),这也就意味着线程A执行结束(参见后面轻量级锁的撤销,只有线程A执行完毕撤销锁了才会重置对象头),此时线程B的CAS操作终于成功了,于是线程B获得了锁以及执行同步代码的权限。如果线程A的执行时间较长,线程B经过若干次CAS时钟没有成功,则锁膨胀为重量级锁,即线程B被挂起阻塞、等待重新调度。</p></blockquote></li></ol><p>此处,如何理解“轻量级”?“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。</p><blockquote><p>轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,必然就会导致轻量级锁膨胀为重量级锁。</p></blockquote><h3 id="4-7-重量级锁"><a href="#4-7-重量级锁" class="headerlink" title="4.7 重量级锁"></a>4.7 重量级锁</h3><p>Synchronized是通过对象内部的一个叫做 监视器锁(Monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”。</p><h3 id="4-8-重量级锁、轻量级锁和偏向锁之间转换"><a href="#4-8-重量级锁、轻量级锁和偏向锁之间转换" class="headerlink" title="4.8 重量级锁、轻量级锁和偏向锁之间转换"></a>4.8 重量级锁、轻量级锁和偏向锁之间转换</h3><p><img data-src="/images/pasted-228.png" alt="upload successful"></p><center>重量级锁、轻量级锁和偏向锁之间转换</center><p><img data-src="/images/pasted-229.png" alt="upload successful"></p><center>Synchronized偏向锁、轻量级锁及重量级锁转换流程</center><h2 id="5-锁的优劣"><a href="#5-锁的优劣" class="headerlink" title="5 锁的优劣"></a>5 锁的优劣</h2><p>各种锁并不是相互代替的,而是在不同场景下的不同选择,绝对不是说重量级锁就是不合适的。每种锁是只能升级,不能降级,即由偏向锁->轻量级锁->重量级锁,而这个过程就是开销逐渐加大的过程。</p><blockquote><ol><li>如果是单线程使用,那偏向锁毫无疑问代价最小,并且它就能解决问题,连CAS都不用做,仅仅在内存中比较下对象头就可以了;</li><li>如果出现了其他线程竞争,则偏向锁就会升级为轻量级锁;</li><li>如果其他线程通过一定次数的CAS尝试没有成功,则进入重量级锁;</li></ol></blockquote><p>在第3种情况下进入同步代码块就 要做偏向锁建立、偏向锁撤销、轻量级锁建立、升级到重量级锁,最终还是得靠重量级锁来解决问题,那这样的代价就比直接用重量级锁要大不少了。所以使用哪种技术,一定要看其所处的环境及场景,在绝大多数的情况下,偏向锁是有效的,这是基于HotSpot作者发现的“大多数锁只会由同一线程并发申请”的经验规律。</p><table><thead><tr><th>锁</th><th>优点</th><th>缺点</th><th>适用场景</th></tr></thead><tbody><tr><td>偏向锁</td><td>加锁和解锁不需要额外的消耗,和执行非同步方法比,仅存在纳秒级的差距。</td><td>如果线程间存在锁竞争,会带来额外的锁撤消的消耗。</td><td>适用于只有一个线程访问同步块场景。</td></tr><tr><td>轻量级锁</td><td>竞争的线程不会阻塞,提高了程序的响应速度。</td><td>如果始终得不到锁竞争的线程使用自旋会消耗CPU。</td><td>追求响应时间。同步块执行速度非常快。</td></tr><tr><td>重量级锁</td><td>线程竞争不使用自旋,不会消耗CPU。</td><td>线程阻塞,响应时间缓慢。</td><td>追求吞吐量。同步块执行速度较长。</td></tr></tbody></table><center>锁的优劣</center><h2 id="6-扩展资料"><a href="#6-扩展资料" class="headerlink" title="6 扩展资料"></a>6 扩展资料</h2><ol><li><a href="https://www.jianshu.com/p/c5058b6fe8e5" target="_blank" rel="noopener">JVM源码分析之synchronized实现</a></li><li><a href="https://blog.csdn.net/fei33423/article/details/30316377" target="_blank" rel="noopener">自旋锁、排队自旋锁、MCS锁、CLH锁</a></li><li><a href="https://blog.csdn.net/javazejian/article/details/72828483" target="_blank" rel="noopener">深入理解Java并发之synchronized实现原理</a></li></ol><hr><blockquote><p><strong>转自:</strong><a href="https://www.cnblogs.com/aspirant/p/11470858.html" target="_blank" rel="noopener"><strong>深入分析Synchronized原理(阿里面试题)</strong></a></p></blockquote>]]></content>
<summary type="html">
<p>记得开始学习Java的时候,一遇到多线程情况就使用synchronized,相对于当时的我们来说synchronized是这么的神奇而又强大,那个时候我们赋予它一个名字“同步”,也成为了我们解决多线程情况的百试不爽的良药。但是,随着学习的进行我们知道在JDK1.5之前synchronized是一个重量级锁,相对于j.u.c.Lock,它会显得那么笨重,以至于我们认为它不是那么的高效而慢慢摒弃它。<br>不过,随着Javs SE 1.6对synchronized进行的各种优化后,synchronized并不会显得那么重了。下面来一起探索synchronized的基本使用、实现机制、Java是如何对它进行了优化、锁优化机制、锁的存储结构等升级过程。</p>
<h2 id="1-基本使用"><a href="#1-基本使用" class="headerlink" title="1 基本使用"></a>1 基本使用</h2><p>Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。Synchronized的作用主要有三个:</p>
<blockquote>
<ol>
<li>原子性:确保线程互斥的访问同步代码;</li>
<li>可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 “<strong>对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值</strong>” 来保证的;</li>
<li>有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”;</li>
</ol>
</blockquote>
<p>从语法上讲,Synchronized可以把任何一个非null对象作为”锁”,在HotSpot JVM实现中,<strong>锁有个专门的名字:对象监视器(Object Monitor)</strong>。<br>Synchronized总共有三种用法:</p>
</summary>
<category term="Java基础" scheme="http://zhangfei.men/categories/Java%E5%9F%BA%E7%A1%80/"/>
<category term="Java" scheme="http://zhangfei.men/tags/Java/"/>
<category term="Synchronized" scheme="http://zhangfei.men/tags/Synchronized/"/>
<category term="Lock" scheme="http://zhangfei.men/tags/Lock/"/>
</entry>
<entry>
<title>Java7和Java8中的ConcurrentHashMap原理解析</title>
<link href="http://zhangfei.men/posts/cd7a25a4/"/>
<id>http://zhangfei.men/posts/cd7a25a4/</id>
<published>2020-02-20T03:33:32.000Z</published>
<updated>2020-07-28T01:08:16.453Z</updated>
<content type="html"><![CDATA[<h2 id="1-Java7中ConcurrentHashMap"><a href="#1-Java7中ConcurrentHashMap" class="headerlink" title="1. Java7中ConcurrentHashMap"></a>1. Java7中ConcurrentHashMap</h2><p>ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些。<br>整个 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表”部分“或”一段“的意思,所以很多地方都会将其描述为<strong>分段锁</strong>。注意,行文中,我很多地方用了<strong>“槽”</strong>来代表一个 segment。<br>简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。</p><p><img data-src="/images/pasted-61.png" alt="upload successful"></p><p><strong>concurrencyLevel</strong>:并行级别、并发数、Segment 数,怎么翻译不重要,理解它。默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。<br>再具体到每个 Segment 内部,其实每个 Segment 很像之前介绍的 HashMap,不过它要保证线程安全,所以处理起来要麻烦些。</p><h3 id="1-1-初始化"><a href="#1-1-初始化" class="headerlink" title="1.1. 初始化"></a>1.1. 初始化</h3><p><strong>initialCapacity</strong>:初始容量,这个值指的是整个 ConcurrentHashMap 的初始容量,实际操作的时候需要平均分给每个 Segment。<br><strong>loadFactor</strong>:负载因子,之前我们说了,Segment 数组不可以扩容,所以这个负载因子是给每个 Segment 内部使用的。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">ConcurrentHashMap</span><span class="params">(<span class="keyword">int</span> initialCapacity,</span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">float</span> loadFactor, <span class="keyword">int</span> concurrencyLevel)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (!(loadFactor > <span class="number">0</span>) || initialCapacity < <span class="number">0</span> || concurrencyLevel <= <span class="number">0</span>)</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> IllegalArgumentException();</span><br><span class="line"> <span class="keyword">if</span> (concurrencyLevel > MAX_SEGMENTS)</span><br><span class="line"> concurrencyLevel = MAX_SEGMENTS;</span><br><span class="line"> <span class="comment">// Find power-of-two sizes best matching arguments</span></span><br><span class="line"> <span class="keyword">int</span> sshift = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">int</span> ssize = <span class="number">1</span>;</span><br><span class="line"> <span class="comment">// 计算并行级别 ssize,因为要保持并行级别是 2 的 n 次方</span></span><br><span class="line"> <span class="keyword">while</span> (ssize < concurrencyLevel) {</span><br><span class="line"> ++sshift;</span><br><span class="line"> ssize <<= <span class="number">1</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 我们这里先不要那么烧脑,用默认值,concurrencyLevel 为 16,sshift 为 4</span></span><br><span class="line"> <span class="comment">// 那么计算出 segmentShift 为 28,segmentMask 为 15,后面会用到这两个值</span></span><br><span class="line"> <span class="keyword">this</span>.segmentShift = <span class="number">32</span> - sshift;</span><br><span class="line"> <span class="keyword">this</span>.segmentMask = ssize - <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">if</span> (initialCapacity > MAXIMUM_CAPACITY)</span><br><span class="line"> initialCapacity = MAXIMUM_CAPACITY;</span><br><span class="line"> <span class="comment">// initialCapacity 是设置整个 map 初始的大小,</span></span><br><span class="line"> <span class="comment">// 这里根据 initialCapacity 计算 Segment 数组中每个位置可以分到的大小</span></span><br><span class="line"> <span class="comment">// 如 initialCapacity 为 64,那么每个 Segment 或称之为"槽"可以分到 4 个</span></span><br><span class="line"> <span class="keyword">int</span> c = initialCapacity / ssize;</span><br><span class="line"> <span class="keyword">if</span> (c * ssize < initialCapacity)</span><br><span class="line"> ++c;</span><br><span class="line"> <span class="comment">// 默认 MIN_SEGMENT_TABLE_CAPACITY 是 2,这个值也是有讲究的,因为这样的话,对于具体的槽上,</span></span><br><span class="line"> <span class="comment">// 插入一个元素不至于扩容,插入第二个的时候才会扩容</span></span><br><span class="line"> <span class="keyword">int</span> cap = MIN_SEGMENT_TABLE_CAPACITY; </span><br><span class="line"> <span class="keyword">while</span> (cap < c)</span><br><span class="line"> cap <<= <span class="number">1</span>;</span><br><span class="line"> <span class="comment">// 创建 Segment 数组,</span></span><br><span class="line"> <span class="comment">// 并创建数组的第一个元素 segment[0]</span></span><br><span class="line"> Segment<K,V> s0 =</span><br><span class="line"> <span class="keyword">new</span> Segment<K,V>(loadFactor, (<span class="keyword">int</span>)(cap * loadFactor),</span><br><span class="line"> (HashEntry<K,V>[])<span class="keyword">new</span> HashEntry[cap]);</span><br><span class="line"> Segment<K,V>[] ss = (Segment<K,V>[])<span class="keyword">new</span> Segment[ssize];</span><br><span class="line"> <span class="comment">// 往数组写入 segment[0]</span></span><br><span class="line"> UNSAFE.putOrderedObject(ss, SBASE, s0); <span class="comment">// ordered write of segments[0]</span></span><br><span class="line"> <span class="keyword">this</span>.segments = ss;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>初始化完成,我们得到了一个 Segment 数组。<br>我们就当是用 new ConcurrentHashMap() 无参构造函数进行初始化的,那么初始化完成后:</p><ul><li>Segment 数组长度为 16,不可以扩容</li><li>Segment[i] 的默认大小为 2,负载因子是 0.75,得出初始阈值为 1.5,也就是以后插入第一个元素不会触发扩容,插入第二个会进行第一次扩容</li><li>这里初始化了 segment[0],其他位置还是 null,至于为什么要初始化 segment[0],后面的代码会介绍</li><li>当前 segmentShift 的值为 32 - 4 = 28,segmentMask 为 16 - 1 = 15,姑且把它们简单翻译为移位数和掩码,这两个值马上就会用到</li></ul><h3 id="1-2-put-过程分析"><a href="#1-2-put-过程分析" class="headerlink" title="1.2. put 过程分析"></a>1.2. put 过程分析</h3><p>我们先看 put 的主流程,对于其中的一些关键细节操作,后面会进行详细介绍。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> V <span class="title">put</span><span class="params">(K key, V value)</span> </span>{</span><br><span class="line"> Segment<K,V> s;</span><br><span class="line"> <span class="keyword">if</span> (value == <span class="keyword">null</span>)</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> NullPointerException();</span><br><span class="line"> <span class="comment">// 1. 计算 key 的 hash 值</span></span><br><span class="line"> <span class="keyword">int</span> hash = hash(key);</span><br><span class="line"> <span class="comment">// 2. 根据 hash 值找到 Segment 数组中的位置 j</span></span><br><span class="line"> <span class="comment">// hash 是 32 位,无符号右移 segmentShift(28) 位,剩下高 4 位,</span></span><br><span class="line"> <span class="comment">// 然后和 segmentMask(15) 做一次与操作,也就是说 j 是 hash 值的高 4 位,也就是槽的数组下标</span></span><br><span class="line"> <span class="keyword">int</span> j = (hash >>> segmentShift) & segmentMask;</span><br><span class="line"> <span class="comment">// 刚刚说了,初始化的时候初始化了 segment[0],但是其他位置还是 null,</span></span><br><span class="line"> <span class="comment">// ensureSegment(j) 对 segment[j] 进行初始化</span></span><br><span class="line"> <span class="keyword">if</span> ((s = (Segment<K,V>)UNSAFE.getObject <span class="comment">// nonvolatile; recheck</span></span><br><span class="line"> (segments, (j << SSHIFT) + SBASE)) == <span class="keyword">null</span>) <span class="comment">// in ensureSegment</span></span><br><span class="line"> s = ensureSegment(j);</span><br><span class="line"> <span class="comment">// 3. 插入新值到 槽 s 中</span></span><br><span class="line"> <span class="keyword">return</span> s.put(key, hash, value, <span class="keyword">false</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>第一层皮很简单,根据 hash 值很快就能找到相应的 Segment,之后就是 Segment 内部的 put 操作了。<br>Segment 内部是由 <code>数组+链表</code> 组成的。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">final</span> V <span class="title">put</span><span class="params">(K key, <span class="keyword">int</span> hash, V value, <span class="keyword">boolean</span> onlyIfAbsent)</span> </span>{</span><br><span class="line"> <span class="comment">// 在往该 segment 写入前,需要先获取该 segment 的独占锁</span></span><br><span class="line"> <span class="comment">// 先看主流程,后面还会具体介绍这部分内容</span></span><br><span class="line"> HashEntry<K,V> node = tryLock() ? <span class="keyword">null</span> :</span><br><span class="line"> scanAndLockForPut(key, hash, value);</span><br><span class="line"> V oldValue;</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="comment">// 这个是 segment 内部的数组</span></span><br><span class="line"> HashEntry<K,V>[] tab = table;</span><br><span class="line"> <span class="comment">// 再利用 hash 值,求应该放置的数组下标</span></span><br><span class="line"> <span class="keyword">int</span> index = (tab.length - <span class="number">1</span>) & hash;</span><br><span class="line"> <span class="comment">// first 是数组该位置处的链表的表头</span></span><br><span class="line"> HashEntry<K,V> first = entryAt(tab, index);</span><br><span class="line"> <span class="comment">// 下面这串 for 循环虽然很长,不过也很好理解,想想该位置没有任何元素和已经存在一个链表这两种情况</span></span><br><span class="line"> <span class="keyword">for</span> (HashEntry<K,V> e = first;;) {</span><br><span class="line"> <span class="keyword">if</span> (e != <span class="keyword">null</span>) {</span><br><span class="line"> K k;</span><br><span class="line"> <span class="keyword">if</span> ((k = e.key) == key ||</span><br><span class="line"> (e.hash == hash && key.equals(k))) {</span><br><span class="line"> oldValue = e.value;</span><br><span class="line"> <span class="keyword">if</span> (!onlyIfAbsent) {</span><br><span class="line"> <span class="comment">// 覆盖旧值</span></span><br><span class="line"> e.value = value;</span><br><span class="line"> ++modCount;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 继续顺着链表走</span></span><br><span class="line"> e = e.next;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// node 到底是不是 null,这个要看获取锁的过程,不过和这里都没有关系。</span></span><br><span class="line"> <span class="comment">// 如果不为 null,那就直接将它设置为链表表头;如果是null,初始化并设置为链表表头。</span></span><br><span class="line"> <span class="keyword">if</span> (node != <span class="keyword">null</span>)</span><br><span class="line"> node.setNext(first);</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> node = <span class="keyword">new</span> HashEntry<K,V>(hash, key, value, first);</span><br><span class="line"> <span class="keyword">int</span> c = count + <span class="number">1</span>;</span><br><span class="line"> <span class="comment">// 如果超过了该 segment 的阈值,这个 segment 需要扩容</span></span><br><span class="line"> <span class="keyword">if</span> (c > threshold && tab.length < MAXIMUM_CAPACITY)</span><br><span class="line"> rehash(node); <span class="comment">// 扩容后面也会具体分析</span></span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> <span class="comment">// 没有达到阈值,将 node 放到数组 tab 的 index 位置,</span></span><br><span class="line"> <span class="comment">// 其实就是将新的节点设置成原链表的表头</span></span><br><span class="line"> setEntryAt(tab, index, node);</span><br><span class="line"> ++modCount;</span><br><span class="line"> count = c;</span><br><span class="line"> oldValue = <span class="keyword">null</span>;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> <span class="comment">// 解锁</span></span><br><span class="line"> unlock();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> oldValue;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>整体流程还是比较简单的,由于有独占锁的保护,所以 segment 内部的操作并不复杂。至于这里面的并发问题,我们稍后再进行介绍。<br>到这里 put 操作就结束了,接下来,我们说一说其中几步关键的操作。</p><h3 id="1-3-初始化槽-ensureSegment"><a href="#1-3-初始化槽-ensureSegment" class="headerlink" title="1.3. 初始化槽: ensureSegment"></a>1.3. 初始化槽: ensureSegment</h3><p>ConcurrentHashMap 初始化的时候会初始化第一个槽 segment[0],对于其他槽来说,在插入第一个值的时候进行初始化。<br>这里需要考虑并发,因为很可能会有多个线程同时进来初始化同一个槽 segment[k],不过只要有一个成功了就可以。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> Segment<K,V> <span class="title">ensureSegment</span><span class="params">(<span class="keyword">int</span> k)</span> </span>{</span><br><span class="line"> <span class="keyword">final</span> Segment<K,V>[] ss = <span class="keyword">this</span>.segments;</span><br><span class="line"> <span class="keyword">long</span> u = (k << SSHIFT) + SBASE; <span class="comment">// raw offset</span></span><br><span class="line"> Segment<K,V> seg;</span><br><span class="line"> <span class="keyword">if</span> ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == <span class="keyword">null</span>) {</span><br><span class="line"> <span class="comment">// 这里看到为什么之前要初始化 segment[0] 了,</span></span><br><span class="line"> <span class="comment">// 使用当前 segment[0] 处的数组长度和负载因子来初始化 segment[k]</span></span><br><span class="line"> <span class="comment">// 为什么要用“当前”,因为 segment[0] 可能早就扩容过了</span></span><br><span class="line"> Segment<K,V> proto = ss[<span class="number">0</span>];</span><br><span class="line"> <span class="keyword">int</span> cap = proto.table.length;</span><br><span class="line"> <span class="keyword">float</span> lf = proto.loadFactor;</span><br><span class="line"> <span class="keyword">int</span> threshold = (<span class="keyword">int</span>)(cap * lf);</span><br><span class="line"> <span class="comment">// 初始化 segment[k] 内部的数组</span></span><br><span class="line"> HashEntry<K,V>[] tab = (HashEntry<K,V>[])<span class="keyword">new</span> HashEntry[cap];</span><br><span class="line"> <span class="keyword">if</span> ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))</span><br><span class="line"> == <span class="keyword">null</span>) { <span class="comment">// 再次检查一遍该槽是否被其他线程初始化了。</span></span><br><span class="line"> Segment<K,V> s = <span class="keyword">new</span> Segment<K,V>(lf, threshold, tab);</span><br><span class="line"> <span class="comment">// 使用 while 循环,内部用 CAS,当前线程成功设值或其他线程成功设值后,退出</span></span><br><span class="line"> <span class="keyword">while</span> ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))</span><br><span class="line"> == <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">if</span> (UNSAFE.compareAndSwapObject(ss, u, <span class="keyword">null</span>, seg = s))</span><br><span class="line"> <span class="keyword">break</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> seg;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>总的来说,ensureSegment(int k) 比较简单,对于并发操作使用 CAS 进行控制。</p><blockquote><p>我没搞懂这里为什么要搞一个 while 循环,CAS 失败不就代表有其他线程成功了吗,为什么要再进行判断?<br>感谢评论区的李子木,如果当前线程 CAS 失败,这里的 while 循环是为了将 seg 赋值返回。</p></blockquote><h3 id="1-4-获取写入锁-scanAndLockForPut"><a href="#1-4-获取写入锁-scanAndLockForPut" class="headerlink" title="1.4. 获取写入锁: scanAndLockForPut"></a>1.4. 获取写入锁: scanAndLockForPut</h3><p>前面我们看到,在往某个 segment 中 put 的时候,首先会调用 node = tryLock() ? null : scanAndLockForPut(key, hash, value),也就是说先进行一次 tryLock() 快速获取该 segment 的独占锁,如果失败,那么进入到 scanAndLockForPut 这个方法来获取锁。<br>下面我们来具体分析这个方法中是怎么控制加锁的。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> HashEntry<K,V> <span class="title">scanAndLockForPut</span><span class="params">(K key, <span class="keyword">int</span> hash, V value)</span> </span>{</span><br><span class="line"> HashEntry<K,V> first = entryForHash(<span class="keyword">this</span>, hash);</span><br><span class="line"> HashEntry<K,V> e = first;</span><br><span class="line"> HashEntry<K,V> node = <span class="keyword">null</span>;</span><br><span class="line"> <span class="keyword">int</span> retries = -<span class="number">1</span>; <span class="comment">// negative while locating node</span></span><br><span class="line"> <span class="comment">// 循环获取锁</span></span><br><span class="line"> <span class="keyword">while</span> (!tryLock()) {</span><br><span class="line"> HashEntry<K,V> f; <span class="comment">// to recheck first below</span></span><br><span class="line"> <span class="keyword">if</span> (retries < <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">if</span> (e == <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">if</span> (node == <span class="keyword">null</span>) <span class="comment">// speculatively create node</span></span><br><span class="line"> <span class="comment">// 进到这里说明数组该位置的链表是空的,没有任何元素</span></span><br><span class="line"> <span class="comment">// 当然,进到这里的另一个原因是 tryLock() 失败,所以该槽存在并发,不一定是该位置</span></span><br><span class="line"> node = <span class="keyword">new</span> HashEntry<K,V>(hash, key, value, <span class="keyword">null</span>);</span><br><span class="line"> retries = <span class="number">0</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (key.equals(e.key))</span><br><span class="line"> retries = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> <span class="comment">// 顺着链表往下走</span></span><br><span class="line"> e = e.next;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 重试次数如果超过 MAX_SCAN_RETRIES(单核1多核64),那么不抢了,进入到阻塞队列等待锁</span></span><br><span class="line"> <span class="comment">// lock() 是阻塞方法,直到获取锁后返回</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (++retries > MAX_SCAN_RETRIES) {</span><br><span class="line"> lock();</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> ((retries & <span class="number">1</span>) == <span class="number">0</span> &&</span><br><span class="line"> <span class="comment">// 这个时候是有大问题了,那就是有新的元素进到了链表,成为了新的表头</span></span><br><span class="line"> <span class="comment">// 所以这边的策略是,相当于重新走一遍这个 scanAndLockForPut 方法</span></span><br><span class="line"> (f = entryForHash(<span class="keyword">this</span>, hash)) != first) {</span><br><span class="line"> e = first = f; <span class="comment">// re-traverse if entry changed</span></span><br><span class="line"> retries = -<span class="number">1</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> node;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个方法有两个出口,一个是 tryLock() 成功了,循环终止,另一个就是重试次数超过了 MAX_SCAN_RETRIES,进到 lock() 方法,此方法会阻塞等待,直到成功拿到独占锁。<br>这个方法就是看似复杂,但是其实就是做了一件事,那就是获取该 segment 的独占锁,如果需要的话顺便实例化了一下 node。</p><h3 id="1-5-扩容-rehash"><a href="#1-5-扩容-rehash" class="headerlink" title="1.5. 扩容: rehash"></a>1.5. 扩容: rehash</h3><p>重复一下,segment 数组不能扩容,扩容是 segment 数组某个位置内部的数组 HashEntry[] 进行扩容,扩容后,容量为原来的 2 倍。<br>首先,我们要回顾一下触发扩容的地方,put 的时候,如果判断该值的插入会导致该 segment 的元素个数超过阈值,那么先进行扩容,再插值,读者这个时候可以回去 put 方法看一眼。<br>该方法不需要考虑并发,因为到这里的时候,是持有该 segment 的独占锁的。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 方法参数上的 node 是这次扩容后,需要添加到新的数组中的数据。</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">rehash</span><span class="params">(HashEntry<K,V> node)</span> </span>{</span><br><span class="line"> HashEntry<K,V>[] oldTable = table;</span><br><span class="line"> <span class="keyword">int</span> oldCapacity = oldTable.length;</span><br><span class="line"> <span class="comment">// 2 倍</span></span><br><span class="line"> <span class="keyword">int</span> newCapacity = oldCapacity << <span class="number">1</span>;</span><br><span class="line"> threshold = (<span class="keyword">int</span>)(newCapacity * loadFactor);</span><br><span class="line"> <span class="comment">// 创建新数组</span></span><br><span class="line"> HashEntry<K,V>[] newTable =</span><br><span class="line"> (HashEntry<K,V>[]) <span class="keyword">new</span> HashEntry[newCapacity];</span><br><span class="line"> <span class="comment">// 新的掩码,如从 16 扩容到 32,那么 sizeMask 为 31,对应二进制 ‘000...00011111’</span></span><br><span class="line"> <span class="keyword">int</span> sizeMask = newCapacity - <span class="number">1</span>;</span><br><span class="line"> <span class="comment">// 遍历原数组,老套路,将原数组位置 i 处的链表拆分到 新数组位置 i 和 i+oldCap 两个位置</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i < oldCapacity ; i++) {</span><br><span class="line"> <span class="comment">// e 是链表的第一个元素</span></span><br><span class="line"> HashEntry<K,V> e = oldTable[i];</span><br><span class="line"> <span class="keyword">if</span> (e != <span class="keyword">null</span>) {</span><br><span class="line"> HashEntry<K,V> next = e.next;</span><br><span class="line"> <span class="comment">// 计算应该放置在新数组中的位置,</span></span><br><span class="line"> <span class="comment">// 假设原数组长度为 16,e 在 oldTable[3] 处,那么 idx 只可能是 3 或者是 3 + 16 = 19</span></span><br><span class="line"> <span class="keyword">int</span> idx = e.hash & sizeMask;</span><br><span class="line"> <span class="keyword">if</span> (next == <span class="keyword">null</span>) <span class="comment">// 该位置处只有一个元素,那比较好办</span></span><br><span class="line"> newTable[idx] = e;</span><br><span class="line"> <span class="keyword">else</span> { <span class="comment">// Reuse consecutive sequence at same slot</span></span><br><span class="line"> <span class="comment">// e 是链表表头</span></span><br><span class="line"> HashEntry<K,V> lastRun = e;</span><br><span class="line"> <span class="comment">// idx 是当前链表的头结点 e 的新位置</span></span><br><span class="line"> <span class="keyword">int</span> lastIdx = idx;</span><br><span class="line"> <span class="comment">// 下面这个 for 循环会找到一个 lastRun 节点,这个节点之后的所有元素是将要放到一起的</span></span><br><span class="line"> <span class="keyword">for</span> (HashEntry<K,V> last = next;</span><br><span class="line"> last != <span class="keyword">null</span>;</span><br><span class="line"> last = last.next) {</span><br><span class="line"> <span class="keyword">int</span> k = last.hash & sizeMask;</span><br><span class="line"> <span class="keyword">if</span> (k != lastIdx) {</span><br><span class="line"> lastIdx = k;</span><br><span class="line"> lastRun = last;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 将 lastRun 及其之后的所有节点组成的这个链表放到 lastIdx 这个位置</span></span><br><span class="line"> newTable[lastIdx] = lastRun;</span><br><span class="line"> <span class="comment">// 下面的操作是处理 lastRun 之前的节点,</span></span><br><span class="line"> <span class="comment">// 这些节点可能分配在另一个链表中,也可能分配到上面的那个链表中</span></span><br><span class="line"> <span class="keyword">for</span> (HashEntry<K,V> p = e; p != lastRun; p = p.next) {</span><br><span class="line"> V v = p.value;</span><br><span class="line"> <span class="keyword">int</span> h = p.hash;</span><br><span class="line"> <span class="keyword">int</span> k = h & sizeMask;</span><br><span class="line"> HashEntry<K,V> n = newTable[k];</span><br><span class="line"> newTable[k] = <span class="keyword">new</span> HashEntry<K,V>(h, p.key, v, n);</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 class="comment">// 将新来的 node 放到新数组中刚刚的 两个链表之一 的 头部</span></span><br><span class="line"> <span class="keyword">int</span> nodeIndex = node.hash & sizeMask; <span class="comment">// add the new node</span></span><br><span class="line"> node.setNext(newTable[nodeIndex]);</span><br><span class="line"> newTable[nodeIndex] = node;</span><br><span class="line"> table = newTable;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里的扩容比之前的 HashMap 要复杂一些,代码难懂一点。上面有两个挨着的 for 循环,第一个 for 有什么用呢?<br>仔细一看发现,如果没有第一个 for 循环,也是可以工作的,但是,这个 for 循环下来,如果 lastRun 的后面还有比较多的节点,那么这次就是值得的。因为我们只需要克隆 lastRun 前面的节点,后面的一串节点跟着 lastRun 走就是了,不需要做任何操作。<br>我觉得 Doug Lea 的这个想法也是挺有意思的,不过比较坏的情况就是每次 lastRun 都是链表的最后一个元素或者很靠后的元素,那么这次遍历就有点浪费了。不过 Doug Lea 也说了,根据统计,如果使用默认的阈值,大约只有 1/6 的节点需要克隆。</p><h3 id="1-6-get过程分析"><a href="#1-6-get过程分析" class="headerlink" title="1.6. get过程分析"></a>1.6. get过程分析</h3><p>相对于 put 来说,get 真的不要太简单。</p><ul><li>计算 hash 值,找到 segment 数组中的具体位置,或我们前面用的“槽”</li><li>槽中也是一个数组,根据 hash 找到数组中具体的位置</li><li>到这里是链表了,顺着链表进行查找即可</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> V <span class="title">get</span><span class="params">(Object key)</span> </span>{</span><br><span class="line"> Segment<K,V> s; <span class="comment">// manually integrate access methods to reduce overhead</span></span><br><span class="line"> HashEntry<K,V>[] tab;</span><br><span class="line"> <span class="comment">// 1. hash 值</span></span><br><span class="line"> <span class="keyword">int</span> h = hash(key);</span><br><span class="line"> <span class="keyword">long</span> u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;</span><br><span class="line"> <span class="comment">// 2. 根据 hash 找到对应的 segment</span></span><br><span class="line"> <span class="keyword">if</span> ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != <span class="keyword">null</span> &&</span><br><span class="line"> (tab = s.table) != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="comment">// 3. 找到segment 内部数组相应位置的链表,遍历</span></span><br><span class="line"> <span class="keyword">for</span> (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile</span><br><span class="line"> (tab, ((<span class="keyword">long</span>)(((tab.length - <span class="number">1</span>) & h)) << TSHIFT) + TBASE);</span><br><span class="line"> e != <span class="keyword">null</span>; e = e.next) {</span><br><span class="line"> K k;</span><br><span class="line"> <span class="keyword">if</span> ((k = e.key) == key || (e.hash == h && key.equals(k)))</span><br><span class="line"> <span class="keyword">return</span> e.value;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="1-7-并发问题分析"><a href="#1-7-并发问题分析" class="headerlink" title="1.7. 并发问题分析"></a>1.7. 并发问题分析</h3><p>现在我们已经说完了 put 过程和 get 过程,我们可以看到 get 过程中是没有加锁的,那自然我们就需要去考虑并发问题。<br>添加节点的操作 put 和删除节点的操作 remove 都是要加 segment 上的独占锁的,所以它们之间自然不会有问题,我们需要考虑的问题就是 get 的时候在同一个 segment 中发生了 put 或 remove 操作。</p><ul><li>put 操作的线程安全性。<ol><li>初始化槽,这个我们之前就说过了,使用了 CAS 来初始化 Segment 中的数组。</li><li>添加节点到链表的操作是插入到表头的,所以,如果这个时候 get 操作在链表遍历的过程已经到了中间,是不会影响的。当然,另一个并发问题就是 get 操作在 put 之后,需要保证刚刚插入表头的节点被读取,这个依赖于 setEntryAt 方法中使用的 UNSAFE.putOrderedObject。</li><li>扩容。扩容是新创建了数组,然后进行迁移数据,最后面将 newTable 设置给属性 table。所以,如果 get 操作此时也在进行,那么也没关系,如果 get 先行,那么就是在旧的 table 上做查询操作;而 put 先行,那么 put 操作的可见性保证就是 table 使用了 volatile 关键字。</li></ol></li><li>remove 操作的线程安全性。</li></ul><p>remove 操作我们没有分析源码,所以这里说的读者感兴趣的话还是需要到源码中去求实一下的。<br>get 操作需要遍历链表,但是 remove 操作会”破坏”链表。<br>如果 remove 破坏的节点 get 操作已经过去了,那么这里不存在任何问题。<br>如果 remove 先破坏了一个节点,分两种情况考虑。 1、如果此节点是头结点,那么需要将头结点的 next 设置为数组该位置的元素,table 虽然使用了 volatile 修饰,但是 volatile 并不能提供数组内部操作的可见性保证,所以源码中使用了 UNSAFE 来操作数组,请看方法 setEntryAt。2、如果要删除的节点不是头结点,它会将要删除节点的后继节点接到前驱节点中,这里的并发保证就是 next 属性是 volatile 的。</p><h2 id="2-Java8-ConcurrentHashMap"><a href="#2-Java8-ConcurrentHashMap" class="headerlink" title="2. Java8 ConcurrentHashMap"></a>2. Java8 ConcurrentHashMap</h2><p>Java7 中实现的 ConcurrentHashMap 说实话还是比较复杂的,Java8 对 ConcurrentHashMap 进行了比较大的改动。建议读者可以参考 Java8 中 HashMap 相对于 Java7 HashMap 的改动,对于 ConcurrentHashMap,Java8 也引入了红黑树。<br>说实话,Java8 ConcurrentHashMap 源码真心不简单,最难的在于扩容,数据迁移操作不容易看懂。<br>我们先用一个示意图来描述下其结构:</p><p><img data-src="/images/pasted-62.png" alt="upload successful"></p><p>结构上和 Java8 的 HashMap 基本上一样,不过它要保证线程安全性,所以在源码上确实要复杂一些。</p><h3 id="2-1-初始化"><a href="#2-1-初始化" class="headerlink" title="2.1. 初始化"></a>2.1. 初始化</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 这构造函数里,什么都不干</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">ConcurrentHashMap</span><span class="params">()</span> </span>{</span><br><span class="line">}</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">ConcurrentHashMap</span><span class="params">(<span class="keyword">int</span> initialCapacity)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (initialCapacity < <span class="number">0</span>)</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> IllegalArgumentException();</span><br><span class="line"> <span class="keyword">int</span> cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> <span class="number">1</span>)) ?</span><br><span class="line"> MAXIMUM_CAPACITY :</span><br><span class="line"> tableSizeFor(initialCapacity + (initialCapacity >>> <span class="number">1</span>) + <span class="number">1</span>));</span><br><span class="line"> <span class="keyword">this</span>.sizeCtl = cap;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个初始化方法有点意思,通过提供初始容量,计算了 sizeCtl,sizeCtl = 【 (1.5 * initialCapacity + 1),然后向上取最近的 2 的 n 次方】。如 initialCapacity 为 10,那么得到 sizeCtl 为 16,如果 initialCapacity 为 11,得到 sizeCtl 为 32。<br>sizeCtl 这个属性使用的场景很多,不过只要跟着文章的思路来,就不会被它搞晕了。<br>如果你爱折腾,也可以看下另一个有三个参数的构造方法,这里我就不说了,大部分时候,我们会使用无参构造函数进行实例化,我们也按照这个思路来进行源码分析吧。</p><h3 id="2-2-put过程分析"><a href="#2-2-put过程分析" class="headerlink" title="2.2. put过程分析"></a>2.2. put过程分析</h3><p>仔细地一行一行代码看下去:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> V <span class="title">put</span><span class="params">(K key, V value)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> putVal(key, value, <span class="keyword">false</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">final</span> V <span class="title">putVal</span><span class="params">(K key, V value, <span class="keyword">boolean</span> onlyIfAbsent)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (key == <span class="keyword">null</span> || value == <span class="keyword">null</span>) <span class="keyword">throw</span> <span class="keyword">new</span> NullPointerException();</span><br><span class="line"> <span class="comment">// 得到 hash 值</span></span><br><span class="line"> <span class="keyword">int</span> hash = spread(key.hashCode());</span><br><span class="line"> <span class="comment">// 用于记录相应链表的长度</span></span><br><span class="line"> <span class="keyword">int</span> binCount = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">for</span> (Node<K,V>[] tab = table;;) {</span><br><span class="line"> Node<K,V> f; <span class="keyword">int</span> n, i, fh;</span><br><span class="line"> <span class="comment">// 如果数组"空",进行数组初始化</span></span><br><span class="line"> <span class="keyword">if</span> (tab == <span class="keyword">null</span> || (n = tab.length) == <span class="number">0</span>)</span><br><span class="line"> <span class="comment">// 初始化数组,后面会详细介绍</span></span><br><span class="line"> tab = initTable();</span><br><span class="line"> <span class="comment">// 找该 hash 值对应的数组下标,得到第一个节点 f</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> ((f = tabAt(tab, i = (n - <span class="number">1</span>) & hash)) == <span class="keyword">null</span>) {</span><br><span class="line"> <span class="comment">// 如果数组该位置为空,</span></span><br><span class="line"> <span class="comment">// 用一次 CAS 操作将这个新值放入其中即可,这个 put 操作差不多就结束了,可以拉到最后面了</span></span><br><span class="line"> <span class="comment">// 如果 CAS 失败,那就是有并发操作,进到下一个循环就好了</span></span><br><span class="line"> <span class="keyword">if</span> (casTabAt(tab, i, <span class="keyword">null</span>,</span><br><span class="line"> <span class="keyword">new</span> Node<K,V>(hash, key, value, <span class="keyword">null</span>)))</span><br><span class="line"> <span class="keyword">break</span>; <span class="comment">// no lock when adding to empty bin</span></span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// hash 居然可以等于 MOVED,这个需要到后面才能看明白,不过从名字上也能猜到,肯定是因为在扩容</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> ((fh = f.hash) == MOVED)</span><br><span class="line"> <span class="comment">// 帮助数据迁移,这个等到看完数据迁移部分的介绍后,再理解这个就很简单了</span></span><br><span class="line"> tab = helpTransfer(tab, f);</span><br><span class="line"> <span class="keyword">else</span> { <span class="comment">// 到这里就是说,f 是该位置的头结点,而且不为空</span></span><br><span class="line"> V oldVal = <span class="keyword">null</span>;</span><br><span class="line"> <span class="comment">// 获取数组该位置的头结点的监视器锁</span></span><br><span class="line"> <span class="keyword">synchronized</span> (f) {</span><br><span class="line"> <span class="keyword">if</span> (tabAt(tab, i) == f) {</span><br><span class="line"> <span class="keyword">if</span> (fh >= <span class="number">0</span>) { <span class="comment">// 头结点的 hash 值大于 0,说明是链表</span></span><br><span class="line"> <span class="comment">// 用于累加,记录链表的长度</span></span><br><span class="line"> binCount = <span class="number">1</span>;</span><br><span class="line"> <span class="comment">// 遍历链表</span></span><br><span class="line"> <span class="keyword">for</span> (Node<K,V> e = f;; ++binCount) {</span><br><span class="line"> K ek;</span><br><span class="line"> <span class="comment">// 如果发现了"相等"的 key,判断是否要进行值覆盖,然后也就可以 break 了</span></span><br><span class="line"> <span class="keyword">if</span> (e.hash == hash &&</span><br><span class="line"> ((ek = e.key) == key ||</span><br><span class="line"> (ek != <span class="keyword">null</span> && key.equals(ek)))) {</span><br><span class="line"> oldVal = e.val;</span><br><span class="line"> <span class="keyword">if</span> (!onlyIfAbsent)</span><br><span class="line"> e.val = value;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 到了链表的最末端,将这个新值放到链表的最后面</span></span><br><span class="line"> Node<K,V> pred = e;</span><br><span class="line"> <span class="keyword">if</span> ((e = e.next) == <span class="keyword">null</span>) {</span><br><span class="line"> pred.next = <span class="keyword">new</span> Node<K,V>(hash, key,</span><br><span class="line"> value, <span class="keyword">null</span>);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (f <span class="keyword">instanceof</span> TreeBin) { <span class="comment">// 红黑树</span></span><br><span class="line"> Node<K,V> p;</span><br><span class="line"> binCount = <span class="number">2</span>;</span><br><span class="line"> <span class="comment">// 调用红黑树的插值方法插入新节点</span></span><br><span class="line"> <span class="keyword">if</span> ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,</span><br><span class="line"> value)) != <span class="keyword">null</span>) {</span><br><span class="line"> oldVal = p.val;</span><br><span class="line"> <span class="keyword">if</span> (!onlyIfAbsent)</span><br><span class="line"> p.val = value;</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 class="keyword">if</span> (binCount != <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// 判断是否要将链表转换为红黑树,临界值和 HashMap 一样,也是 8</span></span><br><span class="line"> <span class="keyword">if</span> (binCount >= TREEIFY_THRESHOLD)</span><br><span class="line"> <span class="comment">// 这个方法和 HashMap 中稍微有一点点不同,那就是它不是一定会进行红黑树转换,</span></span><br><span class="line"> <span class="comment">// 如果当前数组的长度小于 64,那么会选择进行数组扩容,而不是转换为红黑树</span></span><br><span class="line"> <span class="comment">// 具体源码我们就不看了,扩容部分后面说</span></span><br><span class="line"> treeifyBin(tab, i);</span><br><span class="line"> <span class="keyword">if</span> (oldVal != <span class="keyword">null</span>)</span><br><span class="line"> <span class="keyword">return</span> oldVal;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">//</span></span><br><span class="line"> addCount(<span class="number">1L</span>, binCount);</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>put 的主流程看完了,但是至少留下了几个问题,第一个是初始化,第二个是扩容,第三个是帮助数据迁移,这些我们都会在后面进行一一介绍。</p><h3 id="2-3-初始化数组:initTable"><a href="#2-3-初始化数组:initTable" class="headerlink" title="2.3. 初始化数组:initTable"></a>2.3. 初始化数组:initTable</h3><p>这个比较简单,主要就是初始化一个合适大小的数组,然后会设置 sizeCtl。<br>初始化方法中的并发问题是通过对 sizeCtl 进行一个 CAS 操作来控制的。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> Node<K,V>[] initTable() {</span><br><span class="line"> Node<K,V>[] tab; <span class="keyword">int</span> sc;</span><br><span class="line"> <span class="keyword">while</span> ((tab = table) == <span class="keyword">null</span> || tab.length == <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// 初始化的"功劳"被其他线程"抢去"了</span></span><br><span class="line"> <span class="keyword">if</span> ((sc = sizeCtl) < <span class="number">0</span>)</span><br><span class="line"> Thread.yield(); <span class="comment">// lost initialization race; just spin</span></span><br><span class="line"> <span class="comment">// CAS 一下,将 sizeCtl 设置为 -1,代表抢到了锁</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (U.compareAndSwapInt(<span class="keyword">this</span>, SIZECTL, sc, -<span class="number">1</span>)) {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">if</span> ((tab = table) == <span class="keyword">null</span> || tab.length == <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// DEFAULT_CAPACITY 默认初始容量是 16</span></span><br><span class="line"> <span class="keyword">int</span> n = (sc > <span class="number">0</span>) ? sc : DEFAULT_CAPACITY;</span><br><span class="line"> <span class="comment">// 初始化数组,长度为 16 或初始化时提供的长度</span></span><br><span class="line"> Node<K,V>[] nt = (Node<K,V>[])<span class="keyword">new</span> Node<?,?>[n];</span><br><span class="line"> <span class="comment">// 将这个数组赋值给 table,table 是 volatile 的</span></span><br><span class="line"> table = tab = nt;</span><br><span class="line"> <span class="comment">// 如果 n 为 16 的话,那么这里 sc = 12</span></span><br><span class="line"> <span class="comment">// 其实就是 0.75 * n</span></span><br><span class="line"> sc = n - (n >>> <span class="number">2</span>);</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> <span class="comment">// 设置 sizeCtl 为 sc,我们就当是 12 吧</span></span><br><span class="line"> sizeCtl = sc;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> tab;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="2-4-链表转红黑树-treeifyBin"><a href="#2-4-链表转红黑树-treeifyBin" class="headerlink" title="2.4. 链表转红黑树: treeifyBin"></a>2.4. 链表转红黑树: treeifyBin</h3><p>前面我们在 put 源码分析也说过,treeifyBin 不一定就会进行红黑树转换,也可能是仅仅做数组扩容。我们还是进行源码分析吧。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">void</span> <span class="title">treeifyBin</span><span class="params">(Node<K,V>[] tab, <span class="keyword">int</span> index)</span> </span>{</span><br><span class="line"> Node<K,V> b; <span class="keyword">int</span> n, sc;</span><br><span class="line"> <span class="keyword">if</span> (tab != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="comment">// MIN_TREEIFY_CAPACITY 为 64</span></span><br><span class="line"> <span class="comment">// 所以,如果数组长度小于 64 的时候,其实也就是 32 或者 16 或者更小的时候,会进行数组扩容</span></span><br><span class="line"> <span class="keyword">if</span> ((n = tab.length) < MIN_TREEIFY_CAPACITY)</span><br><span class="line"> <span class="comment">// 后面我们再详细分析这个方法</span></span><br><span class="line"> tryPresize(n << <span class="number">1</span>);</span><br><span class="line"> <span class="comment">// b 是头结点</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> ((b = tabAt(tab, index)) != <span class="keyword">null</span> && b.hash >= <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// 加锁</span></span><br><span class="line"> <span class="keyword">synchronized</span> (b) {</span><br><span class="line"> <span class="keyword">if</span> (tabAt(tab, index) == b) {</span><br><span class="line"> <span class="comment">// 下面就是遍历链表,建立一颗红黑树</span></span><br><span class="line"> TreeNode<K,V> hd = <span class="keyword">null</span>, tl = <span class="keyword">null</span>;</span><br><span class="line"> <span class="keyword">for</span> (Node<K,V> e = b; e != <span class="keyword">null</span>; e = e.next) {</span><br><span class="line"> TreeNode<K,V> p =</span><br><span class="line"> <span class="keyword">new</span> TreeNode<K,V>(e.hash, e.key, e.val,</span><br><span class="line"> <span class="keyword">null</span>, <span class="keyword">null</span>);</span><br><span class="line"> <span class="keyword">if</span> ((p.prev = tl) == <span class="keyword">null</span>)</span><br><span class="line"> hd = p;</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> tl.next = p;</span><br><span class="line"> tl = p;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 将红黑树设置到数组相应位置中</span></span><br><span class="line"> setTabAt(tab, index, <span class="keyword">new</span> TreeBin<K,V>(hd));</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><h3 id="2-5-扩容:tryPresize"><a href="#2-5-扩容:tryPresize" class="headerlink" title="2.5. 扩容:tryPresize"></a>2.5. 扩容:tryPresize</h3><p>如果说 Java8 ConcurrentHashMap 的源码不简单,那么说的就是扩容操作和迁移操作。<br>这个方法要完完全全看懂还需要看之后的 transfer 方法,读者应该提前知道这点。<br>这里的扩容也是做翻倍扩容的,扩容后数组容量为原来的 2 倍。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 首先要说明的是,方法参数 size 传进来的时候就已经翻了倍了</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">void</span> <span class="title">tryPresize</span><span class="params">(<span class="keyword">int</span> size)</span> </span>{</span><br><span class="line"> <span class="comment">// c:size 的 1.5 倍,再加 1,再往上取最近的 2 的 n 次方。</span></span><br><span class="line"> <span class="keyword">int</span> c = (size >= (MAXIMUM_CAPACITY >>> <span class="number">1</span>)) ? MAXIMUM_CAPACITY :</span><br><span class="line"> tableSizeFor(size + (size >>> <span class="number">1</span>) + <span class="number">1</span>);</span><br><span class="line"> <span class="keyword">int</span> sc;</span><br><span class="line"> <span class="keyword">while</span> ((sc = sizeCtl) >= <span class="number">0</span>) {</span><br><span class="line"> Node<K,V>[] tab = table; <span class="keyword">int</span> n;</span><br><span class="line"> <span class="comment">// 这个 if 分支和之前说的初始化数组的代码基本上是一样的,在这里,我们可以不用管这块代码</span></span><br><span class="line"> <span class="keyword">if</span> (tab == <span class="keyword">null</span> || (n = tab.length) == <span class="number">0</span>) {</span><br><span class="line"> n = (sc > c) ? sc : c;</span><br><span class="line"> <span class="keyword">if</span> (U.compareAndSwapInt(<span class="keyword">this</span>, SIZECTL, sc, -<span class="number">1</span>)) {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">if</span> (table == tab) {</span><br><span class="line"> <span class="meta">@SuppressWarnings</span>(<span class="string">"unchecked"</span>)</span><br><span class="line"> Node<K,V>[] nt = (Node<K,V>[])<span class="keyword">new</span> Node<?,?>[n];</span><br><span class="line"> table = nt;</span><br><span class="line"> sc = n - (n >>> <span class="number">2</span>); <span class="comment">// 0.75 * n</span></span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> sizeCtl = sc;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (c <= sc || n >= MAXIMUM_CAPACITY)</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (tab == table) {</span><br><span class="line"> <span class="comment">// 我没看懂 rs 的真正含义是什么,不过也关系不大</span></span><br><span class="line"> <span class="keyword">int</span> rs = resizeStamp(n);</span><br><span class="line"> <span class="keyword">if</span> (sc < <span class="number">0</span>) {</span><br><span class="line"> Node<K,V>[] nt;</span><br><span class="line"> <span class="keyword">if</span> ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + <span class="number">1</span> ||</span><br><span class="line"> sc == rs + MAX_RESIZERS || (nt = nextTable) == <span class="keyword">null</span> ||</span><br><span class="line"> transferIndex <= <span class="number">0</span>)</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="comment">// 2. 用 CAS 将 sizeCtl 加 1,然后执行 transfer 方法</span></span><br><span class="line"> <span class="comment">// 此时 nextTab 不为 null</span></span><br><span class="line"> <span class="keyword">if</span> (U.compareAndSwapInt(<span class="keyword">this</span>, SIZECTL, sc, sc + <span class="number">1</span>))</span><br><span class="line"> transfer(tab, nt);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 1. 将 sizeCtl 设置为 (rs << RESIZE_STAMP_SHIFT) + 2)</span></span><br><span class="line"> <span class="comment">// 我是没看懂这个值真正的意义是什么?不过可以计算出来的是,结果是一个比较大的负数</span></span><br><span class="line"> <span class="comment">// 调用 transfer 方法,此时 nextTab 参数为 null</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (U.compareAndSwapInt(<span class="keyword">this</span>, SIZECTL, sc,</span><br><span class="line"> (rs << RESIZE_STAMP_SHIFT) + <span class="number">2</span>))</span><br><span class="line"> transfer(tab, <span class="keyword">null</span>);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个方法的核心在于 sizeCtl 值的操作,首先将其设置为一个负数,然后执行 transfer(tab, null),再下一个循环将 sizeCtl 加 1,并执行 transfer(tab, nt),之后可能是继续 sizeCtl 加 1,并执行 transfer(tab, nt)。<br>所以,可能的操作就是执行 1 次 transfer(tab, null) + 多次 transfer(tab, nt),这里怎么结束循环的需要看完 transfer 源码才清楚。</p><h3 id="2-6-数据迁移:transfer"><a href="#2-6-数据迁移:transfer" class="headerlink" title="2.6. 数据迁移:transfer"></a>2.6. 数据迁移:transfer</h3><p>下面这个方法很点长,将原来的 tab 数组的元素迁移到新的 nextTab 数组中。<br>虽然我们之前说的 tryPresize 方法中多次调用 transfer 不涉及多线程,但是这个 transfer 方法可以在其他地方被调用,典型地,我们之前在说 put 方法的时候就说过了,请往上看 put 方法,是不是有个地方调用了 helpTransfer 方法,helpTransfer 方法会调用 transfer 方法的。<br>此方法支持多线程执行,外围调用此方法的时候,会保证第一个发起数据迁移的线程,nextTab 参数为 null,之后再调用此方法的时候,nextTab 不会为 null。<br>阅读源码之前,先要理解并发操作的机制。原数组长度为 n,所以我们有 n 个迁移任务,让每个线程每次负责一个小任务是最简单的,每做完一个任务再检测是否有其他没做完的任务,帮助迁移就可以了,而 Doug Lea 使用了一个 stride,简单理解就是步长,每个线程每次负责迁移其中的一部分,如每次迁移 16 个小任务。所以,我们就需要一个全局的调度者来安排哪个线程执行哪几个任务,这个就是属性 transferIndex 的作用。<br>第一个发起数据迁移的线程会将 transferIndex 指向原数组最后的位置,然后从后往前的 stride 个任务属于第一个线程,然后将 transferIndex 指向新的位置,再往前的 stride 个任务属于第二个线程,依此类推。当然,这里说的第二个线程不是真的一定指代了第二个线程,也可以是同一个线程,这个读者应该能理解吧。其实就是将一个大的迁移任务分为了一个个任务包。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">void</span> <span class="title">transfer</span><span class="params">(Node<K,V>[] tab, Node<K,V>[] nextTab)</span> </span>{</span><br><span class="line"> <span class="keyword">int</span> n = tab.length, stride;</span><br><span class="line"> <span class="comment">// stride 在单核下直接等于 n,多核模式下为 (n>>>3)/NCPU,最小值是 16</span></span><br><span class="line"> <span class="comment">// stride 可以理解为”步长“,有 n 个位置是需要进行迁移的,</span></span><br><span class="line"> <span class="comment">// 将这 n 个任务分为多个任务包,每个任务包有 stride 个任务</span></span><br><span class="line"> <span class="keyword">if</span> ((stride = (NCPU > <span class="number">1</span>) ? (n >>> <span class="number">3</span>) / NCPU : n) < MIN_TRANSFER_STRIDE)</span><br><span class="line"> stride = MIN_TRANSFER_STRIDE; <span class="comment">// subdivide range</span></span><br><span class="line"> <span class="comment">// 如果 nextTab 为 null,先进行一次初始化</span></span><br><span class="line"> <span class="comment">// 前面我们说了,外围会保证第一个发起迁移的线程调用此方法时,参数 nextTab 为 null</span></span><br><span class="line"> <span class="comment">// 之后参与迁移的线程调用此方法时,nextTab 不会为 null</span></span><br><span class="line"> <span class="keyword">if</span> (nextTab == <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="comment">// 容量翻倍</span></span><br><span class="line"> Node<K,V>[] nt = (Node<K,V>[])<span class="keyword">new</span> Node<?,?>[n << <span class="number">1</span>];</span><br><span class="line"> nextTab = nt;</span><br><span class="line"> } <span class="keyword">catch</span> (Throwable ex) { <span class="comment">// try to cope with OOME</span></span><br><span class="line"> sizeCtl = Integer.MAX_VALUE;</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// nextTable 是 ConcurrentHashMap 中的属性</span></span><br><span class="line"> nextTable = nextTab;</span><br><span class="line"> <span class="comment">// transferIndex 也是 ConcurrentHashMap 的属性,用于控制迁移的位置</span></span><br><span class="line"> transferIndex = n;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">int</span> nextn = nextTab.length;</span><br><span class="line"> <span class="comment">// ForwardingNode 翻译过来就是正在被迁移的 Node</span></span><br><span class="line"> <span class="comment">// 这个构造方法会生成一个Node,key、value 和 next 都为 null,关键是 hash 为 MOVED</span></span><br><span class="line"> <span class="comment">// 后面我们会看到,原数组中位置 i 处的节点完成迁移工作后,</span></span><br><span class="line"> <span class="comment">// 就会将位置 i 处设置为这个 ForwardingNode,用来告诉其他线程该位置已经处理过了</span></span><br><span class="line"> <span class="comment">// 所以它其实相当于是一个标志。</span></span><br><span class="line"> ForwardingNode<K,V> fwd = <span class="keyword">new</span> ForwardingNode<K,V>(nextTab);</span><br><span class="line"> <span class="comment">// advance 指的是做完了一个位置的迁移工作,可以准备做下一个位置的了</span></span><br><span class="line"> <span class="keyword">boolean</span> advance = <span class="keyword">true</span>;</span><br><span class="line"> <span class="keyword">boolean</span> finishing = <span class="keyword">false</span>; <span class="comment">// to ensure sweep before committing nextTab</span></span><br><span class="line"> <span class="comment">/*</span></span><br><span class="line"><span class="comment"> * 下面这个 for 循环,最难理解的在前面,而要看懂它们,应该先看懂后面的,然后再倒回来看</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">// i 是位置索引,bound 是边界,注意是从后往前</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>, bound = <span class="number">0</span>;;) {</span><br><span class="line"> Node<K,V> f; <span class="keyword">int</span> fh;</span><br><span class="line"> <span class="comment">// 下面这个 while 真的是不好理解</span></span><br><span class="line"> <span class="comment">// advance 为 true 表示可以进行下一个位置的迁移了</span></span><br><span class="line"> <span class="comment">// 简单理解结局:i 指向了 transferIndex,bound 指向了 transferIndex-stride</span></span><br><span class="line"> <span class="keyword">while</span> (advance) {</span><br><span class="line"> <span class="keyword">int</span> nextIndex, nextBound;</span><br><span class="line"> <span class="keyword">if</span> (--i >= bound || finishing)</span><br><span class="line"> advance = <span class="keyword">false</span>;</span><br><span class="line"> <span class="comment">// 将 transferIndex 值赋给 nextIndex</span></span><br><span class="line"> <span class="comment">// 这里 transferIndex 一旦小于等于 0,说明原数组的所有位置都有相应的线程去处理了</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> ((nextIndex = transferIndex) <= <span class="number">0</span>) {</span><br><span class="line"> i = -<span class="number">1</span>;</span><br><span class="line"> advance = <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (U.compareAndSwapInt</span><br><span class="line"> (<span class="keyword">this</span>, TRANSFERINDEX, nextIndex,</span><br><span class="line"> nextBound = (nextIndex > stride ?</span><br><span class="line"> nextIndex - stride : <span class="number">0</span>))) {</span><br><span class="line"> <span class="comment">// 看括号中的代码,nextBound 是这次迁移任务的边界,注意,是从后往前</span></span><br><span class="line"> bound = nextBound;</span><br><span class="line"> i = nextIndex - <span class="number">1</span>;</span><br><span class="line"> advance = <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (i < <span class="number">0</span> || i >= n || i + n >= nextn) {</span><br><span class="line"> <span class="keyword">int</span> sc;</span><br><span class="line"> <span class="keyword">if</span> (finishing) {</span><br><span class="line"> <span class="comment">// 所有的迁移操作已经完成</span></span><br><span class="line"> nextTable = <span class="keyword">null</span>;</span><br><span class="line"> <span class="comment">// 将新的 nextTab 赋值给 table 属性,完成迁移</span></span><br><span class="line"> table = nextTab;</span><br><span class="line"> <span class="comment">// 重新计算 sizeCtl:n 是原数组长度,所以 sizeCtl 得出的值将是新数组长度的 0.75 倍</span></span><br><span class="line"> sizeCtl = (n << <span class="number">1</span>) - (n >>> <span class="number">1</span>);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 之前我们说过,sizeCtl 在迁移前会设置为 (rs << RESIZE_STAMP_SHIFT) + 2</span></span><br><span class="line"> <span class="comment">// 然后,每有一个线程参与迁移就会将 sizeCtl 加 1,</span></span><br><span class="line"> <span class="comment">// 这里使用 CAS 操作对 sizeCtl 进行减 1,代表做完了属于自己的任务</span></span><br><span class="line"> <span class="keyword">if</span> (U.compareAndSwapInt(<span class="keyword">this</span>, SIZECTL, sc = sizeCtl, sc - <span class="number">1</span>)) {</span><br><span class="line"> <span class="comment">// 任务结束,方法退出</span></span><br><span class="line"> <span class="keyword">if</span> ((sc - <span class="number">2</span>) != resizeStamp(n) << RESIZE_STAMP_SHIFT)</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> <span class="comment">// 到这里,说明 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT,</span></span><br><span class="line"> <span class="comment">// 也就是说,所有的迁移任务都做完了,也就会进入到上面的 if(finishing){} 分支了</span></span><br><span class="line"> finishing = advance = <span class="keyword">true</span>;</span><br><span class="line"> i = n; <span class="comment">// recheck before commit</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 如果位置 i 处是空的,没有任何节点,那么放入刚刚初始化的 ForwardingNode ”空节点“</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> ((f = tabAt(tab, i)) == <span class="keyword">null</span>)</span><br><span class="line"> advance = casTabAt(tab, i, <span class="keyword">null</span>, fwd);</span><br><span class="line"> <span class="comment">// 该位置处是一个 ForwardingNode,代表该位置已经迁移过了</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> ((fh = f.hash) == MOVED)</span><br><span class="line"> advance = <span class="keyword">true</span>; <span class="comment">// already processed</span></span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 对数组该位置处的结点加锁,开始处理数组该位置处的迁移工作</span></span><br><span class="line"> <span class="keyword">synchronized</span> (f) {</span><br><span class="line"> <span class="keyword">if</span> (tabAt(tab, i) == f) {</span><br><span class="line"> Node<K,V> ln, hn;</span><br><span class="line"> <span class="comment">// 头结点的 hash 大于 0,说明是链表的 Node 节点</span></span><br><span class="line"> <span class="keyword">if</span> (fh >= <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// 下面这一块和 Java7 中的 ConcurrentHashMap 迁移是差不多的,</span></span><br><span class="line"> <span class="comment">// 需要将链表一分为二,</span></span><br><span class="line"> <span class="comment">// 找到原链表中的 lastRun,然后 lastRun 及其之后的节点是一起进行迁移的</span></span><br><span class="line"> <span class="comment">// lastRun 之前的节点需要进行克隆,然后分到两个链表中</span></span><br><span class="line"> <span class="keyword">int</span> runBit = fh & n;</span><br><span class="line"> Node<K,V> lastRun = f;</span><br><span class="line"> <span class="keyword">for</span> (Node<K,V> p = f.next; p != <span class="keyword">null</span>; p = p.next) {</span><br><span class="line"> <span class="keyword">int</span> b = p.hash & n;</span><br><span class="line"> <span class="keyword">if</span> (b != runBit) {</span><br><span class="line"> runBit = b;</span><br><span class="line"> lastRun = p;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (runBit == <span class="number">0</span>) {</span><br><span class="line"> ln = lastRun;</span><br><span class="line"> hn = <span class="keyword">null</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> hn = lastRun;</span><br><span class="line"> ln = <span class="keyword">null</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">for</span> (Node<K,V> p = f; p != lastRun; p = p.next) {</span><br><span class="line"> <span class="keyword">int</span> ph = p.hash; K pk = p.key; V pv = p.val;</span><br><span class="line"> <span class="keyword">if</span> ((ph & n) == <span class="number">0</span>)</span><br><span class="line"> ln = <span class="keyword">new</span> Node<K,V>(ph, pk, pv, ln);</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> hn = <span class="keyword">new</span> Node<K,V>(ph, pk, pv, hn);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 其中的一个链表放在新数组的位置 i</span></span><br><span class="line"> setTabAt(nextTab, i, ln);</span><br><span class="line"> <span class="comment">// 另一个链表放在新数组的位置 i+n</span></span><br><span class="line"> setTabAt(nextTab, i + n, hn);</span><br><span class="line"> <span class="comment">// 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,</span></span><br><span class="line"> <span class="comment">// 其他线程一旦看到该位置的 hash 值为 MOVED,就不会进行迁移了</span></span><br><span class="line"> setTabAt(tab, i, fwd);</span><br><span class="line"> <span class="comment">// advance 设置为 true,代表该位置已经迁移完毕</span></span><br><span class="line"> advance = <span class="keyword">true</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (f <span class="keyword">instanceof</span> TreeBin) {</span><br><span class="line"> <span class="comment">// 红黑树的迁移</span></span><br><span class="line"> TreeBin<K,V> t = (TreeBin<K,V>)f;</span><br><span class="line"> TreeNode<K,V> lo = <span class="keyword">null</span>, loTail = <span class="keyword">null</span>;</span><br><span class="line"> TreeNode<K,V> hi = <span class="keyword">null</span>, hiTail = <span class="keyword">null</span>;</span><br><span class="line"> <span class="keyword">int</span> lc = <span class="number">0</span>, hc = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">for</span> (Node<K,V> e = t.first; e != <span class="keyword">null</span>; e = e.next) {</span><br><span class="line"> <span class="keyword">int</span> h = e.hash;</span><br><span class="line"> TreeNode<K,V> p = <span class="keyword">new</span> TreeNode<K,V></span><br><span class="line"> (h, e.key, e.val, <span class="keyword">null</span>, <span class="keyword">null</span>);</span><br><span class="line"> <span class="keyword">if</span> ((h & n) == <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">if</span> ((p.prev = loTail) == <span class="keyword">null</span>)</span><br><span class="line"> lo = p;</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> loTail.next = p;</span><br><span class="line"> loTail = p;</span><br><span class="line"> ++lc;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">if</span> ((p.prev = hiTail) == <span class="keyword">null</span>)</span><br><span class="line"> hi = p;</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> hiTail.next = p;</span><br><span class="line"> hiTail = p;</span><br><span class="line"> ++hc;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 如果一分为二后,节点数少于 8,那么将红黑树转换回链表</span></span><br><span class="line"> ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :</span><br><span class="line"> (hc != <span class="number">0</span>) ? <span class="keyword">new</span> TreeBin<K,V>(lo) : t;</span><br><span class="line"> hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :</span><br><span class="line"> (lc != <span class="number">0</span>) ? <span class="keyword">new</span> TreeBin<K,V>(hi) : t;</span><br><span class="line"> <span class="comment">// 将 ln 放置在新数组的位置 i</span></span><br><span class="line"> setTabAt(nextTab, i, ln);</span><br><span class="line"> <span class="comment">// 将 hn 放置在新数组的位置 i+n</span></span><br><span class="line"> setTabAt(nextTab, i + n, hn);</span><br><span class="line"> <span class="comment">// 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,</span></span><br><span class="line"> <span class="comment">// 其他线程一旦看到该位置的 hash 值为 MOVED,就不会进行迁移了</span></span><br><span class="line"> setTabAt(tab, i, fwd);</span><br><span class="line"> <span class="comment">// advance 设置为 true,代表该位置已经迁移完毕</span></span><br><span class="line"> advance = <span class="keyword">true</span>;</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>说到底,transfer 这个方法并没有实现所有的迁移任务,每次调用这个方法只实现了 transferIndex 往前 stride 个位置的迁移工作,其他的需要由外围来控制。<br>这个时候,再回去仔细看 tryPresize 方法可能就会更加清晰一些了。</p><h3 id="2-7-get过程分析"><a href="#2-7-get过程分析" class="headerlink" title="2.7. get过程分析"></a>2.7. get过程分析</h3><p>get 方法从来都是最简单的,这里也不例外:</p><ol><li>计算 hash 值</li><li>根据 hash 值找到数组对应位置: (n - 1) & h</li><li>根据该位置处结点性质进行相应查找<ul><li>如果该位置为 null,那么直接返回 null 就可以了</li><li>如果该位置处的节点刚好就是我们需要的,返回该节点的值即可</li><li>如果该位置节点的 hash 值小于 0,说明正在扩容,或者是红黑树,后面我们再介绍 find 方法</li><li>如果以上 3 条都不满足,那就是链表,进行遍历比对即可</li></ul></li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> V <span class="title">get</span><span class="params">(Object key)</span> </span>{</span><br><span class="line"> Node<K,V>[] tab; Node<K,V> e, p; <span class="keyword">int</span> n, eh; K ek;</span><br><span class="line"> <span class="keyword">int</span> h = spread(key.hashCode());</span><br><span class="line"> <span class="keyword">if</span> ((tab = table) != <span class="keyword">null</span> && (n = tab.length) > <span class="number">0</span> &&</span><br><span class="line"> (e = tabAt(tab, (n - <span class="number">1</span>) & h)) != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="comment">// 判断头结点是否就是我们需要的节点</span></span><br><span class="line"> <span class="keyword">if</span> ((eh = e.hash) == h) {</span><br><span class="line"> <span class="keyword">if</span> ((ek = e.key) == key || (ek != <span class="keyword">null</span> && key.equals(ek)))</span><br><span class="line"> <span class="keyword">return</span> e.val;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 如果头结点的 hash 小于 0,说明 正在扩容,或者该位置是红黑树</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (eh < <span class="number">0</span>)</span><br><span class="line"> <span class="comment">// 参考 ForwardingNode.find(int h, Object k) 和 TreeBin.find(int h, Object k)</span></span><br><span class="line"> <span class="keyword">return</span> (p = e.find(h, key)) != <span class="keyword">null</span> ? p.val : <span class="keyword">null</span>;</span><br><span class="line"> <span class="comment">// 遍历链表</span></span><br><span class="line"> <span class="keyword">while</span> ((e = e.next) != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">if</span> (e.hash == h &&</span><br><span class="line"> ((ek = e.key) == key || (ek != <span class="keyword">null</span> && key.equals(ek))))</span><br><span class="line"> <span class="keyword">return</span> e.val;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>简单说一句,此方法的大部分内容都很简单,只有正好碰到扩容的情况,ForwardingNode.find(int h, Object k) 稍微复杂一些,不过在了解了数据迁移的过程后,这个也就不难了,所以限于篇幅这里也不展开说了。</p><blockquote><p><strong>原文:</strong><a href="https://www.cnblogs.com/jajian/p/10385377.html" target="_blank" rel="noopener">https://www.cnblogs.com/jajian/p/10385377.html</a></p></blockquote>]]></content>
<summary type="html">
<h2 id="1-Java7中ConcurrentHashMap"><a href="#1-Java7中ConcurrentHashMap" class="headerlink" title="1. Java7中ConcurrentHashMap"></a>1. Java7中ConcurrentHashMap</h2><p>ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些。<br>整个 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表”部分“或”一段“的意思,所以很多地方都会将其描述为<strong>分段锁</strong>。注意,行文中,我很多地方用了<strong>“槽”</strong>来代表一个 segment。<br>简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。</p>
<p><img data-src="/images/pasted-61.png" alt="upload successful"></p>
<p><strong>concurrencyLevel</strong>:并行级别、并发数、Segment 数,怎么翻译不重要,理解它。默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。<br>再具体到每个 Segment 内部,其实每个 Segment 很像之前介绍的 HashMap,不过它要保证线程安全,所以处理起来要麻烦些。</p>
<h3 id="1-1-初始化"><a href="#1-1-初始化" class="headerlink" title="1.1. 初始化"></a>1.1. 初始化</h3>
</summary>
<category term="源码分析" scheme="http://zhangfei.men/categories/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/"/>
<category term="Java" scheme="http://zhangfei.men/categories/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/Java/"/>
<category term="Java" scheme="http://zhangfei.men/tags/Java/"/>
<category term="Map" scheme="http://zhangfei.men/tags/Map/"/>
<category term="Concurrency" scheme="http://zhangfei.men/tags/Concurrency/"/>
</entry>
<entry>
<title>Java7和Java8中的HashMap原理解析</title>
<link href="http://zhangfei.men/posts/9cb319e/"/>
<id>http://zhangfei.men/posts/9cb319e/</id>
<published>2020-02-20T03:05:11.000Z</published>
<updated>2020-07-28T01:08:16.453Z</updated>
<content type="html"><![CDATA[<p>HashMap可能是面试的时候必问的题目了,面试官为什么都偏爱拿这个问应聘者?因为HashMap它的设计结构和原理比较有意思,它既可以考初学者对Java集合的了解又可以深度的发现应聘者的数据结构功底。<br>阅读前提:本文分析的是源码,所以至少读者要熟悉它们的接口使用,同时,对于并发,读者至少要知道CAS、ReentrantLock、Unsafe操作这几个基本的知识,文中不会对这些知识进行介绍。Java8用到了红黑树,不过本文不会进行展开,感兴趣的读者请自行查找相关资料。</p><h2 id="1-Java7中HashMap"><a href="#1-Java7中HashMap" class="headerlink" title="1. Java7中HashMap"></a>1. Java7中HashMap</h2><p>HashMap是最简单的,一来我们非常熟悉,二来就是它不支持并发操作,所以源码也非常简单。<br>首先,我们用下面这张图来介绍HashMap的结构。</p><p><img data-src="/images/pasted-63.png" alt="upload successful"></p><p>大方向上,HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。为什么是这种的结构,这涉及到数据结构方面的知识了。</p><h3 id="1-1-HashMap的数据结构"><a href="#1-1-HashMap的数据结构" class="headerlink" title="1.1. HashMap的数据结构"></a>1.1. HashMap的数据结构</h3><p>数据结构中有数组和链表来实现对数据的存储,但这两者基本上是两个极端。<br><strong>数组</strong><br>数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;<br><strong>链表</strong><br>链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,为O(N)。<br>链表的特点是:寻址困难,插入和删除容易。<br><strong>哈希表</strong><br>那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表。哈希表(Hash table)既满足了数据的查找方便,同时不占用太多的内容空间,使用也十分方便。<br>哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法—— <strong>拉链法</strong>,我们可以理解为<strong>“链表的数组”</strong>,如图:</p><p><img data-src="/images/pasted-64.png" alt="upload successful"></p><p>当添加数据的时候,整个结构大致如下:</p><p><img data-src="/images/pasted-65.png" alt="upload successful"></p><p>从上图我们可以发现哈希表是由<code>数组+链表</code>组成的,一个长度为16的数组中,每个数组中元素存储的是一个链表的头结点。<br>那么这些元素是按照什么样的规则存储到数组中呢。一般情况我们首先想到的就是元素的 key 的哈希值对数组长度取模得到( <code>hash(key)%(length -1)</code>),这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,能不能找一种更快速,消耗更小的方式那?Java中是这样做的:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">int</span> <span class="title">indexFor</span><span class="params">(<span class="keyword">int</span> h, <span class="keyword">int</span> length)</span> </span>{ </span><br><span class="line"> <span class="keyword">return</span> h & (length-<span class="number">1</span>); </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们知道每个数据对象的hash对应唯一一个值,但是一个hash值不一定对应唯一的数据对象。如果两个不同对象的 hashCode 相同,此情况即称为<strong>哈希冲突</strong>。<br>比如上述HashMap中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置,然后依次放在数组中该位置的链表上。</p><blockquote><p><strong>注意:</strong><br>对于那些hash冲突的数据,最新(最后)put的值放在链表的头部,为什么这样做呢?因为我们程序设计中认为最新放进去的值它的使用率会更高些,放在链表头比较容易查询获取到。</p></blockquote><p>HashMap里面实现一个静态内部类Entry,Entry包含四个属性:key,value,hash值和用于单向链表的 next。从属性key,value我们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean,我们上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。上图中,每个绿色的实体是嵌套类Entry的实例。</p><ul><li><strong>capacity</strong>:当前数组容量,始终保持 <code>2^n</code>,可以扩容,扩容后数组大小为当前的2倍。</li><li><strong>loadFactor</strong>:负载因子,默认为<code>0.75</code>。</li><li><strong>threshold</strong>:扩容的阈值,等于 <code>capacity * loadFactor</code>。</li></ul><blockquote><p><strong>注意问题:</strong><br>1、<strong>扩容的数组的长度为什么保持 2^n?</strong><br>其实这是为了保证通过hash方式获取下标的时候分布均匀。数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。<br>2、<strong>为什么负载因子的值默认为 0.75?</strong><br>加载因子是表示Hash表中元素的填满的程度。<br>加载因子越大,填满的元素越多,空间利用率越高,但冲突的机会加大了。<br>反之,加载因子越小,填满的元素越少,冲突的机会减小,但空间浪费多了。<br>冲突的机会越大,则查找的成本越高。反之,查找的成本越小。<br>因此,必须在 “冲突的机会”与”空间利用率”之间寻找一种平衡与折衷。</p></blockquote><h3 id="1-2-put过程分析"><a href="#1-2-put过程分析" class="headerlink" title="1.2. put过程分析"></a>1.2. put过程分析</h3><p>还是比较简单的,跟着代码走一遍吧。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> V <span class="title">put</span><span class="params">(K key, V value)</span> </span>{</span><br><span class="line"> <span class="comment">// 当插入第一个元素的时候,需要先初始化数组大小</span></span><br><span class="line"> <span class="keyword">if</span> (table == EMPTY_TABLE) {</span><br><span class="line"> inflateTable(threshold);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 如果 key 为 null,感兴趣的可以往里看,最终会将这个 entry 放到 table[0] 中</span></span><br><span class="line"> <span class="keyword">if</span> (key == <span class="keyword">null</span>)</span><br><span class="line"> <span class="keyword">return</span> putForNullKey(value);</span><br><span class="line"> <span class="comment">// 1. 求 key 的 hash 值</span></span><br><span class="line"> <span class="keyword">int</span> hash = hash(key);</span><br><span class="line"> <span class="comment">// 2. 找到对应的数组下标</span></span><br><span class="line"> <span class="keyword">int</span> i = indexFor(hash, table.length);</span><br><span class="line"> <span class="comment">// 3. 遍历一下对应下标处的链表,看是否有重复的 key 已经存在,</span></span><br><span class="line"> <span class="comment">// 如果有,直接覆盖,put 方法返回旧值就结束了</span></span><br><span class="line"> <span class="keyword">for</span> (Entry<K,V> e = table[i]; e != <span class="keyword">null</span>; e = e.next) {</span><br><span class="line"> Object k;</span><br><span class="line"> <span class="keyword">if</span> (e.hash == hash && ((k = e.key) == key || key.equals(k))) {</span><br><span class="line"> V oldValue = e.value;</span><br><span class="line"> e.value = value;</span><br><span class="line"> e.recordAccess(<span class="keyword">this</span>);</span><br><span class="line"> <span class="keyword">return</span> oldValue;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> modCount++;</span><br><span class="line"> <span class="comment">// 4. 不存在重复的 key,将此 entry 添加到链表中,细节后面说</span></span><br><span class="line"> addEntry(hash, key, value, i);</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="1-3-数组初始化-inflateTable"><a href="#1-3-数组初始化-inflateTable" class="headerlink" title="1.3. 数组初始化(inflateTable)"></a>1.3. 数组初始化(inflateTable)</h3><p>在第一个元素插入HashMap的时候做一次数组的初始化,就是先确定初始的数组大小,并计算数组扩容的阈值。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">inflateTable</span><span class="params">(<span class="keyword">int</span> toSize)</span> </span>{</span><br><span class="line"> <span class="comment">// 保证数组大小一定是 2 的 n 次方。</span></span><br><span class="line"> <span class="comment">// 比如这样初始化:new HashMap(20),那么处理成初始数组大小是 32</span></span><br><span class="line"> <span class="keyword">int</span> capacity = roundUpToPowerOf2(toSize);</span><br><span class="line"> <span class="comment">// 计算扩容阈值:capacity * loadFactor</span></span><br><span class="line"> threshold = (<span class="keyword">int</span>) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + <span class="number">1</span>);</span><br><span class="line"> <span class="comment">// 算是初始化数组吧</span></span><br><span class="line"> table = <span class="keyword">new</span> Entry[capacity];</span><br><span class="line"> initHashSeedAsNeeded(capacity); <span class="comment">//ignore</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里有一个将数组大小保持为2的n次方的做法,Java7和Java8的HashMap和ConcurrentHashMap都有相应的要求,只不过实现的代码稍微有些不同,后面再看到的时候就知道了。</p><h3 id="1-4-计算具体数组位置-indexFor"><a href="#1-4-计算具体数组位置-indexFor" class="headerlink" title="1.4. 计算具体数组位置(indexFor)"></a>1.4. 计算具体数组位置(indexFor)</h3><p>这个简单,我们自己也能YY一个:使用key的hash值对数组长度进行取模就可以了。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">int</span> <span class="title">indexFor</span><span class="params">(<span class="keyword">int</span> hash, <span class="keyword">int</span> length)</span> </span>{</span><br><span class="line"> <span class="comment">// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";</span></span><br><span class="line"> <span class="keyword">return</span> hash & (length-<span class="number">1</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个方法很简单,简单说就是取hash值的低n位。如在数组长度为32的时候,其实取的就是key的hash值的低5位,作为它在数组中的下标位置。</p><h3 id="1-5-添加节点到链表中-addEntry"><a href="#1-5-添加节点到链表中-addEntry" class="headerlink" title="1.5. 添加节点到链表中(addEntry)"></a>1.5. 添加节点到链表中(addEntry)</h3><p>找到数组下标后,会先进行key判重,如果没有重复,就准备将新值放入到链表的表头。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">addEntry</span><span class="params">(<span class="keyword">int</span> hash, K key, V value, <span class="keyword">int</span> bucketIndex)</span> </span>{</span><br><span class="line"> <span class="comment">// 如果当前 HashMap 大小已经达到了阈值,并且新值要插入的数组位置已经有元素了,那么要扩容</span></span><br><span class="line"> <span class="keyword">if</span> ((size >= threshold) && (<span class="keyword">null</span> != table[bucketIndex])) {</span><br><span class="line"> <span class="comment">// 扩容,后面会介绍一下</span></span><br><span class="line"> resize(<span class="number">2</span> * table.length);</span><br><span class="line"> <span class="comment">// 扩容以后,重新计算 hash 值</span></span><br><span class="line"> hash = (<span class="keyword">null</span> != key) ? hash(key) : <span class="number">0</span>;</span><br><span class="line"> <span class="comment">// 重新计算扩容后的新的下标</span></span><br><span class="line"> bucketIndex = indexFor(hash, table.length);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 往下看</span></span><br><span class="line"> createEntry(hash, key, value, bucketIndex);</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 这个很简单,其实就是将新值放到链表的表头,然后 size++</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">createEntry</span><span class="params">(<span class="keyword">int</span> hash, K key, V value, <span class="keyword">int</span> bucketIndex)</span> </span>{</span><br><span class="line"> Entry<K,V> e = table[bucketIndex];</span><br><span class="line"> table[bucketIndex] = <span class="keyword">new</span> Entry<>(hash, key, value, e);</span><br><span class="line"> size++;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个方法的主要逻辑就是先判断是否需要扩容,需要的话先扩容,然后再将这个新的数据插入到扩容后的数组的相应位置处的链表的表头。</p><h3 id="1-6-数组扩容-resize"><a href="#1-6-数组扩容-resize" class="headerlink" title="1.6. 数组扩容(resize)"></a>1.6. 数组扩容(resize)</h3><p>前面我们看到,在插入新值的时候,如果当前的size已经达到了阈值,并且要插入的数组位置上已经有元素,那么就会触发扩容,扩容后,数组大小为原来的2倍。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">resize</span><span class="params">(<span class="keyword">int</span> newCapacity)</span> </span>{</span><br><span class="line"> Entry[] oldTable = table;</span><br><span class="line"> <span class="keyword">int</span> oldCapacity = oldTable.length;</span><br><span class="line"> <span class="keyword">if</span> (oldCapacity == MAXIMUM_CAPACITY) {</span><br><span class="line"> threshold = Integer.MAX_VALUE;</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 新的数组</span></span><br><span class="line"> Entry[] newTable = <span class="keyword">new</span> Entry[newCapacity];</span><br><span class="line"> <span class="comment">// 将原来数组中的值迁移到新的更大的数组中</span></span><br><span class="line"> transfer(newTable, initHashSeedAsNeeded(newCapacity));</span><br><span class="line"> table = newTable;</span><br><span class="line"> threshold = (<span class="keyword">int</span>)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + <span class="number">1</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>扩容就是用一个新的大数组替换原来的小数组,并将原来数组中的值迁移到新的数组中。<br>由于是双倍扩容,迁移过程中,会将原来table[i]中的链表的所有节点,分拆到新的数组的 <code>newTable[i]</code>和 <code>newTable[i + oldLength]</code> 位置上。如原来数组长度是16,那么扩容后,原来 table[0] 处的链表中的所有元素会被分配到新数组中 <code>newTable[0]</code> 和 <code>newTable[16]</code> 这两个位置。代码比较简单,这里就不展开了。</p><h3 id="1-7-get过程分析"><a href="#1-7-get过程分析" class="headerlink" title="1.7. get过程分析"></a>1.7. get过程分析</h3><p>相对于put过程,get过程是非常简单的。</p><ul><li>根据key计算hash值。</li><li>找到相应的数组下标:<code>hash & (length - 1)</code>。</li><li>遍历该数组位置处的链表,直到找到相等(==或equals)的key。</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> V <span class="title">get</span><span class="params">(Object key)</span> </span>{</span><br><span class="line"> <span class="comment">// 之前说过,key 为 null 的话,会被放到 table[0],所以只要遍历下 table[0] 处的链表就可以了</span></span><br><span class="line"> <span class="keyword">if</span> (key == <span class="keyword">null</span>)</span><br><span class="line"> <span class="keyword">return</span> getForNullKey();</span><br><span class="line"> <span class="comment">// </span></span><br><span class="line"> Entry<K,V> entry = getEntry(key);</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">null</span> == entry ? <span class="keyword">null</span> : entry.getValue();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>getEntry(key):</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">final</span> Entry<K,V> <span class="title">getEntry</span><span class="params">(Object key)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (size == <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">int</span> hash = (key == <span class="keyword">null</span>) ? <span class="number">0</span> : hash(key);</span><br><span class="line"> <span class="comment">// 确定数组下标,然后从头开始遍历链表,直到找到为止</span></span><br><span class="line"> <span class="keyword">for</span> (Entry<K,V> e = table[indexFor(hash, table.length)];</span><br><span class="line"> e != <span class="keyword">null</span>;</span><br><span class="line"> e = e.next) {</span><br><span class="line"> Object k;</span><br><span class="line"> <span class="keyword">if</span> (e.hash == hash &&</span><br><span class="line"> ((k = e.key) == key || (key != <span class="keyword">null</span> && key.equals(k))))</span><br><span class="line"> <span class="keyword">return</span> e;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="2-Java8中HashMap"><a href="#2-Java8中HashMap" class="headerlink" title="2. Java8中HashMap"></a>2. Java8中HashMap</h2><p>Java8对HashMap进行了一些修改,最大的不同就是利用了红黑树,所以其由 <code>数组+链表+红黑树 组成</code>。<br>根据Java7HashMap的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为O(n)。<br><strong>为了降低这部分的开销,在Java8中,当链表中的元素达到了8个时,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为O(logN)。</strong><br>来一张图简单示意一下吧:</p><p><img data-src="/images/pasted-66.png" alt="upload successful"></p><blockquote><p>注意,上图是示意图,主要是描述结构,不会达到这个状态的,因为这么多数据的时候早就扩容了。</p></blockquote><p>下面,我们还是用代码来介绍吧,个人感觉,Java8的源码可读性要差一些,不过精简一些。<br>Java7中使用Entry来代表每个HashMap中的数据节点,Java8中使用Node,基本没有区别,都是key,value,hash 和 next这四个属性,不过,Node只能用于链表的情况,红黑树的情况需要使用TreeNode。<br>我们根据数组元素中,第一个节点数据类型是Node还是TreeNode来判断该位置下是链表还是红黑树的。</p><h3 id="2-1-put过程分析"><a href="#2-1-put过程分析" class="headerlink" title="2.1. put过程分析"></a>2.1. put过程分析</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> V <span class="title">put</span><span class="params">(K key, V value)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> putVal(hash(key), key, value, <span class="keyword">false</span>, <span class="keyword">true</span>);</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 第三个参数 onlyIfAbsent 如果是 true,那么只有在不存在该 key 时才会进行 put 操作</span></span><br><span class="line"><span class="comment">// 第四个参数 evict 我们这里不关心</span></span><br><span class="line"><span class="function"><span class="keyword">final</span> V <span class="title">putVal</span><span class="params">(<span class="keyword">int</span> hash, K key, V value, <span class="keyword">boolean</span> onlyIfAbsent,</span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">boolean</span> evict)</span> </span>{</span><br><span class="line"> Node<K,V>[] tab; Node<K,V> p; <span class="keyword">int</span> n, i;</span><br><span class="line"> <span class="comment">// 第一次 put 值的时候,会触发下面的 resize(),类似 java7 的第一次 put 也要初始化数组长度</span></span><br><span class="line"> <span class="comment">// 第一次 resize 和后续的扩容有些不一样,因为这次是数组从 null 初始化到默认的 16 或自定义的初始容量</span></span><br><span class="line"> <span class="keyword">if</span> ((tab = table) == <span class="keyword">null</span> || (n = tab.length) == <span class="number">0</span>)</span><br><span class="line"> n = (tab = resize()).length;</span><br><span class="line"> <span class="comment">// 找到具体的数组下标,如果此位置没有值,那么直接初始化一下 Node 并放置在这个位置就可以了</span></span><br><span class="line"> <span class="keyword">if</span> ((p = tab[i = (n - <span class="number">1</span>) & hash]) == <span class="keyword">null</span>)</span><br><span class="line"> tab[i] = newNode(hash, key, value, <span class="keyword">null</span>);</span><br><span class="line"> <span class="keyword">else</span> {<span class="comment">// 数组该位置有数据</span></span><br><span class="line"> Node<K,V> e; K k;</span><br><span class="line"> <span class="comment">// 首先,判断该位置的第一个数据和我们要插入的数据,key 是不是"相等",如果是,取出这个节点</span></span><br><span class="line"> <span class="keyword">if</span> (p.hash == hash &&</span><br><span class="line"> ((k = p.key) == key || (key != <span class="keyword">null</span> && key.equals(k))))</span><br><span class="line"> e = p;</span><br><span class="line"> <span class="comment">// 如果该节点是代表红黑树的节点,调用红黑树的插值方法,本文不展开说红黑树</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (p <span class="keyword">instanceof</span> TreeNode)</span><br><span class="line"> e = ((TreeNode<K,V>)p).putTreeVal(<span class="keyword">this</span>, tab, hash, key, value);</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 到这里,说明数组该位置上是一个链表</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> binCount = <span class="number">0</span>; ; ++binCount) {</span><br><span class="line"> <span class="comment">// 插入到链表的最后面(Java7 是插入到链表的最前面)</span></span><br><span class="line"> <span class="keyword">if</span> ((e = p.next) == <span class="keyword">null</span>) {</span><br><span class="line"> p.next = newNode(hash, key, value, <span class="keyword">null</span>);</span><br><span class="line"> <span class="comment">// TREEIFY_THRESHOLD 为 8,所以,如果新插入的值是链表中的第 8 个</span></span><br><span class="line"> <span class="comment">// 会触发下面的 treeifyBin,也就是将链表转换为红黑树</span></span><br><span class="line"> <span class="keyword">if</span> (binCount >= TREEIFY_THRESHOLD - <span class="number">1</span>) <span class="comment">// -1 for 1st</span></span><br><span class="line"> treeifyBin(tab, hash);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 如果在该链表中找到了"相等"的 key(== 或 equals)</span></span><br><span class="line"> <span class="keyword">if</span> (e.hash == hash &&</span><br><span class="line"> ((k = e.key) == key || (key != <span class="keyword">null</span> && key.equals(k))))</span><br><span class="line"> <span class="comment">// 此时 break,那么 e 为链表中[与要插入的新值的 key "相等"]的 node</span></span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> p = e;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// e!=null 说明存在旧值的key与要插入的key"相等"</span></span><br><span class="line"> <span class="comment">// 对于我们分析的put操作,下面这个 if 其实就是进行 "值覆盖",然后返回旧值</span></span><br><span class="line"> <span class="keyword">if</span> (e != <span class="keyword">null</span>) {</span><br><span class="line"> V oldValue = e.value;</span><br><span class="line"> <span class="keyword">if</span> (!onlyIfAbsent || oldValue == <span class="keyword">null</span>)</span><br><span class="line"> e.value = value;</span><br><span class="line"> afterNodeAccess(e);</span><br><span class="line"> <span class="keyword">return</span> oldValue;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> ++modCount;</span><br><span class="line"> <span class="comment">// 如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容</span></span><br><span class="line"> <span class="keyword">if</span> (++size > threshold)</span><br><span class="line"> resize();</span><br><span class="line"> afterNodeInsertion(evict);</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>和 Java7 稍微有点不一样的地方就是,Java7 是先扩容后插入新值的,Java8 先插值再扩容,不过这个不重要。</p><h3 id="2-2-数组扩容"><a href="#2-2-数组扩容" class="headerlink" title="2.2. 数组扩容"></a>2.2. 数组扩容</h3><p>resize()方法用于初始化数组或数组扩容,每次扩容后,容量为原来的2倍,并进行数据迁移。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">final</span> Node<K,V>[] resize() {</span><br><span class="line"> Node<K,V>[] oldTab = table;</span><br><span class="line"> <span class="keyword">int</span> oldCap = (oldTab == <span class="keyword">null</span>) ? <span class="number">0</span> : oldTab.length;</span><br><span class="line"> <span class="keyword">int</span> oldThr = threshold;</span><br><span class="line"> <span class="keyword">int</span> newCap, newThr = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">if</span> (oldCap > <span class="number">0</span>) { <span class="comment">// 对应数组扩容</span></span><br><span class="line"> <span class="keyword">if</span> (oldCap >= MAXIMUM_CAPACITY) {</span><br><span class="line"> threshold = Integer.MAX_VALUE;</span><br><span class="line"> <span class="keyword">return</span> oldTab;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 将数组大小扩大一倍</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> ((newCap = oldCap << <span class="number">1</span>) < MAXIMUM_CAPACITY &&</span><br><span class="line"> oldCap >= DEFAULT_INITIAL_CAPACITY)</span><br><span class="line"> <span class="comment">// 将阈值扩大一倍</span></span><br><span class="line"> newThr = oldThr << <span class="number">1</span>; <span class="comment">// double threshold</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (oldThr > <span class="number">0</span>) <span class="comment">// 对应使用 new HashMap(int initialCapacity) 初始化后,第一次 put 的时候</span></span><br><span class="line"> newCap = oldThr;</span><br><span class="line"> <span class="keyword">else</span> {<span class="comment">// 对应使用 new HashMap() 初始化后,第一次 put 的时候</span></span><br><span class="line"> newCap = DEFAULT_INITIAL_CAPACITY;</span><br><span class="line"> newThr = (<span class="keyword">int</span>)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (newThr == <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">float</span> ft = (<span class="keyword">float</span>)newCap * loadFactor;</span><br><span class="line"> newThr = (newCap < MAXIMUM_CAPACITY && ft < (<span class="keyword">float</span>)MAXIMUM_CAPACITY ?</span><br><span class="line"> (<span class="keyword">int</span>)ft : Integer.MAX_VALUE);</span><br><span class="line"> }</span><br><span class="line"> threshold = newThr;</span><br><span class="line"> <span class="comment">// 用新的数组大小初始化新的数组</span></span><br><span class="line"> Node<K,V>[] newTab = (Node<K,V>[])<span class="keyword">new</span> Node[newCap];</span><br><span class="line"> table = newTab; <span class="comment">// 如果是初始化数组,到这里就结束了,返回 newTab 即可</span></span><br><span class="line"> <span class="keyword">if</span> (oldTab != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="comment">// 开始遍历原数组,进行数据迁移。</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> j = <span class="number">0</span>; j < oldCap; ++j) {</span><br><span class="line"> Node<K,V> e;</span><br><span class="line"> <span class="keyword">if</span> ((e = oldTab[j]) != <span class="keyword">null</span>) {</span><br><span class="line"> oldTab[j] = <span class="keyword">null</span>;</span><br><span class="line"> <span class="comment">// 如果该数组位置上只有单个元素,那就简单了,简单迁移这个元素就可以了</span></span><br><span class="line"> <span class="keyword">if</span> (e.next == <span class="keyword">null</span>)</span><br><span class="line"> newTab[e.hash & (newCap - <span class="number">1</span>)] = e;</span><br><span class="line"> <span class="comment">// 如果是红黑树,具体我们就不展开了</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (e <span class="keyword">instanceof</span> TreeNode)</span><br><span class="line"> ((TreeNode<K,V>)e).split(<span class="keyword">this</span>, newTab, j, oldCap);</span><br><span class="line"> <span class="keyword">else</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">// loHead、loTail 对应一条链表,hiHead、hiTail 对应另一条链表,代码还是比较简单的</span></span><br><span class="line"> Node<K,V> loHead = <span class="keyword">null</span>, loTail = <span class="keyword">null</span>;</span><br><span class="line"> Node<K,V> hiHead = <span class="keyword">null</span>, hiTail = <span class="keyword">null</span>;</span><br><span class="line"> Node<K,V> next;</span><br><span class="line"> <span class="keyword">do</span> {</span><br><span class="line"> next = e.next;</span><br><span class="line"> <span class="keyword">if</span> ((e.hash & oldCap) == <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">if</span> (loTail == <span class="keyword">null</span>)</span><br><span class="line"> loHead = e;</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> loTail.next = e;</span><br><span class="line"> loTail = e;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">if</span> (hiTail == <span class="keyword">null</span>)</span><br><span class="line"> hiHead = e;</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> hiTail.next = e;</span><br><span class="line"> hiTail = e;</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">while</span> ((e = next) != <span class="keyword">null</span>);</span><br><span class="line"> <span class="keyword">if</span> (loTail != <span class="keyword">null</span>) {</span><br><span class="line"> loTail.next = <span class="keyword">null</span>;</span><br><span class="line"> <span class="comment">// 第一条链表</span></span><br><span class="line"> newTab[j] = loHead;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (hiTail != <span class="keyword">null</span>) {</span><br><span class="line"> hiTail.next = <span class="keyword">null</span>;</span><br><span class="line"> <span class="comment">// 第二条链表的新的位置是 j + oldCap,这个很好理解</span></span><br><span class="line"> newTab[j + oldCap] = hiHead;</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 class="keyword">return</span> newTab;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="2-3-get过程分析"><a href="#2-3-get过程分析" class="headerlink" title="2.3. get过程分析"></a>2.3. get过程分析</h3><p>相对于put来说,get真的太简单了。</p><ul><li>计算key的hash值,根据hash值找到对应数组下标: <code>hash & (length-1)</code>.</li><li>判断数组该位置处的元素是否刚好就是我们要找的,如果不是,走第三步.</li><li>判断该元素类型是否是TreeNode,如果是,用红黑树的方法取数据,如果不是,走第四步.</li><li>遍历链表,直到找到相等(==或equals)的key.</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> V <span class="title">get</span><span class="params">(Object key)</span> </span>{</span><br><span class="line"> Node<K,V> e;</span><br><span class="line"> <span class="keyword">return</span> (e = getNode(hash(key), key)) == <span class="keyword">null</span> ? <span class="keyword">null</span> : e.value;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">final</span> Node<K,V> <span class="title">getNode</span><span class="params">(<span class="keyword">int</span> hash, Object key)</span> </span>{</span><br><span class="line"> Node<K,V>[] tab; Node<K,V> first, e; <span class="keyword">int</span> n; K k;</span><br><span class="line"> <span class="keyword">if</span> ((tab = table) != <span class="keyword">null</span> && (n = tab.length) > <span class="number">0</span> &&</span><br><span class="line"> (first = tab[(n - <span class="number">1</span>) & hash]) != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="comment">// 判断第一个节点是不是就是需要的</span></span><br><span class="line"> <span class="keyword">if</span> (first.hash == hash && <span class="comment">// always check first node</span></span><br><span class="line"> ((k = first.key) == key || (key != <span class="keyword">null</span> && key.equals(k))))</span><br><span class="line"> <span class="keyword">return</span> first;</span><br><span class="line"> <span class="keyword">if</span> ((e = first.next) != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="comment">// 判断是否是红黑树</span></span><br><span class="line"> <span class="keyword">if</span> (first <span class="keyword">instanceof</span> TreeNode)</span><br><span class="line"> <span class="keyword">return</span> ((TreeNode<K,V>)first).getTreeNode(hash, key);</span><br><span class="line"> <span class="comment">// 链表遍历</span></span><br><span class="line"> <span class="keyword">do</span> {</span><br><span class="line"> <span class="keyword">if</span> (e.hash == hash &&</span><br><span class="line"> ((k = e.key) == key || (key != <span class="keyword">null</span> && key.equals(k))))</span><br><span class="line"> <span class="keyword">return</span> e;</span><br><span class="line"> } <span class="keyword">while</span> ((e = e.next) != <span class="keyword">null</span>);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>参考:</p><ol><li><a href="https://javadoop.com/post/hashmap" target="_blank" rel="noopener">Javadoop</a></li><li><a href="https://blog.csdn.net/sd_csdn_scy/article/details/55510453" target="_blank" rel="noopener">HashMap中hash函数h & (length-1)详解</a></li><li><a href="https://blog.csdn.net/doujinlong1/article/details/81196048" target="_blank" rel="noopener">HashMap</a></li></ol><blockquote><p><strong>原文:</strong><a href="https://www.cnblogs.com/jajian/p/10385063.html" target="_blank" rel="noopener">https://www.cnblogs.com/jajian/p/10385063.html</a></p></blockquote>]]></content>
<summary type="html">
<p>HashMap可能是面试的时候必问的题目了,面试官为什么都偏爱拿这个问应聘者?因为HashMap它的设计结构和原理比较有意思,它既可以考初学者对Java集合的了解又可以深度的发现应聘者的数据结构功底。<br>阅读前提:本文分析的是源码,所以至少读者要熟悉它们的接口使用,同时,对于并发,读者至少要知道CAS、ReentrantLock、Unsafe操作这几个基本的知识,文中不会对这些知识进行介绍。Java8用到了红黑树,不过本文不会进行展开,感兴趣的读者请自行查找相关资料。</p>
<h2 id="1-Java7中HashMap"><a href="#1-Java7中HashMap" class="headerlink" title="1. Java7中HashMap"></a>1. Java7中HashMap</h2><p>HashMap是最简单的,一来我们非常熟悉,二来就是它不支持并发操作,所以源码也非常简单。<br>首先,我们用下面这张图来介绍HashMap的结构。</p>
<p><img data-src="/images/pasted-63.png" alt="upload successful"></p>
<p>大方向上,HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。为什么是这种的结构,这涉及到数据结构方面的知识了。</p>
</summary>
<category term="源码分析" scheme="http://zhangfei.men/categories/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/"/>
<category term="Java" scheme="http://zhangfei.men/categories/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/Java/"/>
<category term="Java" scheme="http://zhangfei.men/tags/Java/"/>
<category term="Map" scheme="http://zhangfei.men/tags/Map/"/>
</entry>
<entry>
<title>调研|5种分布式事务解决方案优缺点对比</title>
<link href="http://zhangfei.men/posts/3a646236/"/>
<id>http://zhangfei.men/posts/3a646236/</id>
<published>2020-02-19T09:33:03.000Z</published>
<updated>2020-07-28T01:08:16.457Z</updated>
<content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个东西,特别是在微服务架构中,几乎可以说是无法避免。</p><h3 id="ACID"><a href="#ACID" class="headerlink" title="ACID"></a>ACID</h3><p>指数据库事务正确执行的四个基本要素:</p><ul><li><strong>原子性(Atomicity)</strong></li><li><strong>一致性(Consistency)</strong></li><li><strong>隔离性(Isolation)</strong></li><li><strong>持久性(Durability)</strong></li></ul><h3 id="CAP"><a href="#CAP" class="headerlink" title="CAP"></a>CAP</h3><p>CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容忍性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。</p><ul><li><strong>一致性</strong>:在分布式系统中的所有数据备份,在同一时刻是否同样的值。</li><li><strong>可用性</strong>:在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。</li><li><strong>分区容忍性</strong>:以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。</li></ul><h3 id="BASE理论"><a href="#BASE理论" class="headerlink" title="BASE理论"></a>BASE理论</h3><p>BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:<strong>我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。</strong></p><ul><li>Basically Available(基本可用)</li><li>Soft state(软状态)</li><li>Eventually consistent(最终一致性)</li></ul><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h2><h3 id="01、两阶段提交(2PC)"><a href="#01、两阶段提交(2PC)" class="headerlink" title="01、两阶段提交(2PC)"></a>01、两阶段提交(2PC)</h3><p>两阶段提交2PC是分布式事务中最强大的事务类型之一,两段提交就是分两个阶段提交,第一阶段询问各个事务数据源是否准备好,第二阶段才真正将数据提交给事务数据源。为了保证该事务可以满足ACID,就要引入一个协调者(Cooradinator)。其他的节点被称为参与者(Participant)。协调者负责调度参与者的行为,并最终决定这些参与者是否要把事务进行提交。处理流程如下:</p><p><img data-src="/images/pasted-186.png" alt="upload successful"></p><h4 id="阶段一"><a href="#阶段一" class="headerlink" title="阶段一"></a>阶段一</h4><p>a) 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复。<br>b) 各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。<br>c) 如参与者执行成功,给协调者反馈 yes,否则反馈 no。</p><h4 id="阶段二"><a href="#阶段二" class="headerlink" title="阶段二"></a>阶段二</h4><p>如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(rollback)消息;否则,发送提交(commit)消息。两种情况处理如下:<br><strong>情况1:</strong>当所有参与者均反馈 yes,提交事务<br>a) 协调者向所有参与者发出正式提交事务的请求(即 commit 请求)。<br>b) 参与者执行 commit 请求,并释放整个事务期间占用的资源。<br>c) 各参与者向协调者反馈 ack(应答)完成的消息。<br>d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。<br><strong>情况2:</strong>当有一个参与者反馈 no,回滚事务<br>a) 协调者向所有参与者发出回滚请求(即 rollback 请求)。<br>b) 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。<br>c) 各参与者向协调者反馈 ack 完成的消息。<br>d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务。</p><h4 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h4><ol><li><strong>性能问题</strong>:所有参与者在事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈。</li><li><strong>可靠性问题</strong>:如果协调者存在单点故障问题,或出现故障,提供者将一直处于锁定状态。</li><li><strong>数据一致性问题</strong>:在阶段 2 中,如果出现协调者和参与者都挂了的情况,有可能导致数据不一致。</li></ol><h4 id="优点:"><a href="#优点:" class="headerlink" title="优点:"></a>优点:</h4><p>尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证强一致)。</p><h4 id="缺点:"><a href="#缺点:" class="headerlink" title="缺点:"></a>缺点:</h4><p>实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景。</p><h3 id="02、三阶段提交(3PC)"><a href="#02、三阶段提交(3PC)" class="headerlink" title="02、三阶段提交(3PC)"></a>02、三阶段提交(3PC)</h3><p>三阶段提交是在二阶段提交上的改进版本,3PC最关键要解决的就是协调者和参与者同时挂掉的问题,所以3PC把2PC的准备阶段再次一分为二,这样三阶段提交。处理流程如下:</p><p><img data-src="/images/pasted-187.png" alt="upload successful"></p><h4 id="阶段一-1"><a href="#阶段一-1" class="headerlink" title="阶段一"></a>阶段一</h4><p>a) 协调者向所有参与者发出包含事务内容的 canCommit 请求,询问是否可以提交事务,并等待所有参与者答复。<br>b) 参与者收到 canCommit 请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否则反馈 no。</p><h4 id="阶段二-1"><a href="#阶段二-1" class="headerlink" title="阶段二"></a>阶段二</h4><p>协调者根据参与者响应情况,有以下两种可能。<br><strong>情况1</strong>:所有参与者均反馈 yes,协调者预执行事务<br>a) 协调者向所有参与者发出 preCommit 请求,进入准备阶段。<br>b) 参与者收到 preCommit 请求后,执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。<br>c) 各参与者向协调者反馈 ack 响应或 no 响应,并等待最终指令。<br><strong>情况2</strong>:只要有一个参与者反馈 no,或者等待超时后协调者尚无法收到所有提供者的反馈,即中断事务<br>a) 协调者向所有参与者发出 abort 请求。<br>b) 无论收到协调者发出的 abort 请求,或者在等待协调者请求过程中出现超时,参与者均会中断事务。</p><h4 id="阶段三"><a href="#阶段三" class="headerlink" title="阶段三"></a>阶段三</h4><p>该阶段进行真正的事务提交,也可以分为以下两种情况。<br><strong>情况 1</strong>:所有参与者均反馈 ack 响应,执行真正的事务提交<br>a) 如果协调者处于工作状态,则向所有参与者发出 do Commit 请求。<br>b) 参与者收到 do Commit 请求后,会正式执行事务提交,并释放整个事务期间占用的资源。<br>c) 各参与者向协调者反馈 ack 完成的消息。<br>d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。<br><strong>情况2</strong>:只要有一个参与者反馈 no,或者等待超时后协调组尚无法收到所有提供者的反馈,即回滚事务。<br>a) 如果协调者处于工作状态,向所有参与者发出 rollback 请求。<br>b) 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。<br>c) 各参与者向协调组反馈 ack 完成的消息。<br>d) 协调组收到所有参与者反馈的 ack 消息后,即完成事务回滚。</p><h4 id="优点:-1"><a href="#优点:-1" class="headerlink" title="优点:"></a>优点:</h4><p>相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题。阶段 3 中协调者出现问题时,参与者会继续提交事务。</p><h4 id="缺点:-1"><a href="#缺点:-1" class="headerlink" title="缺点:"></a>缺点:</h4><p>数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 do commite 指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。</p><h3 id="03、补偿事务(TCC)"><a href="#03、补偿事务(TCC)" class="headerlink" title="03、补偿事务(TCC)"></a>03、补偿事务(TCC)</h3><p>TCC 是服务化的二阶段编程模型,采用的补偿机制:</p><p><img data-src="/images/pasted-188.png" alt="upload successful"></p><h4 id="条件:"><a href="#条件:" class="headerlink" title="条件:"></a>条件:</h4><ul><li>需要实现确认和补偿逻辑</li><li>需要支持幂等</li></ul><h4 id="处理流程:"><a href="#处理流程:" class="headerlink" title="处理流程:"></a>处理流程:</h4><h5 id="a-Try-阶段主要是对业务系统做检测及资源预留。"><a href="#a-Try-阶段主要是对业务系统做检测及资源预留。" class="headerlink" title="a) Try 阶段主要是对业务系统做检测及资源预留。"></a>a) Try 阶段主要是对业务系统做检测及资源预留。</h5><p>这个阶段主要完成:</p><ul><li>完成所有业务检查( 一致性 ) 。</li><li>预留必须业务资源( 准隔离性 ) 。</li><li>Try 尝试执行业务。</li></ul><h5 id="b-Confirm-阶段主要是对业务系统做确认提交。"><a href="#b-Confirm-阶段主要是对业务系统做确认提交。" class="headerlink" title="b) Confirm 阶段主要是对业务系统做确认提交。"></a>b) Confirm 阶段主要是对业务系统做确认提交。</h5><p>Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。</p><h5 id="c-Cancel-阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。"><a href="#c-Cancel-阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。" class="headerlink" title="c) Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。"></a>c) Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。</h5><h4 id="优点:-2"><a href="#优点:-2" class="headerlink" title="优点:"></a>优点:</h4><ul><li>性能提升:具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。</li><li>数据最终一致性:基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。</li><li>可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。</li></ul><h4 id="缺点"><a href="#缺点" class="headerlink" title="缺点:"></a>缺点:</h4><p>TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。</p><h3 id="04、本地消息表(消息队列)"><a href="#04、本地消息表(消息队列)" class="headerlink" title="04、本地消息表(消息队列)"></a>04、本地消息表(消息队列)</h3><p>其核心思想是将分布式事务拆分成本地事务进行处理。方案通过在消费者额外新建事务消息表,消费者处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,提供者基于消息中间件消费事务消息表中的事务。</p><p><img data-src="/images/pasted-189.png" alt="upload successful"></p><h4 id="条件:-1"><a href="#条件:-1" class="headerlink" title="条件: "></a>条件: </h4><ul><li><strong>服务消费者需要创建一张消息表,用来记录消息状态。</strong></li><li><strong>服务消费者和提供者需要支持幂等。</strong></li><li><strong>需要补偿逻辑。</strong></li><li><strong>每个节点上起定时线程,检查未处理完成或发出失败的消息,重新发出消息,即重试机制和幂等性机制。</strong></li></ul><h4 id="处理流程:-1"><a href="#处理流程:-1" class="headerlink" title="处理流程:"></a>处理流程:</h4><h5 id="1-服务消费者把业务数据和消息一同提交,发起事务。"><a href="#1-服务消费者把业务数据和消息一同提交,发起事务。" class="headerlink" title="1. 服务消费者把业务数据和消息一同提交,发起事务。"></a>1. 服务消费者把业务数据和消息一同提交,发起事务。</h5><h5 id="2-消息经过MQ发送到服务提供方,服务消费者等待处理结果。"><a href="#2-消息经过MQ发送到服务提供方,服务消费者等待处理结果。" class="headerlink" title="2. 消息经过MQ发送到服务提供方,服务消费者等待处理结果。"></a>2. 消息经过MQ发送到服务提供方,服务消费者等待处理结果。</h5><h5 id="3-服务提供方接收消息,完成业务逻辑并通知消费者已处理的消息。"><a href="#3-服务提供方接收消息,完成业务逻辑并通知消费者已处理的消息。" class="headerlink" title="3. 服务提供方接收消息,完成业务逻辑并通知消费者已处理的消息。"></a>3. 服务提供方接收消息,完成业务逻辑并通知消费者已处理的消息。</h5><h5 id="容错处理情况如下:"><a href="#容错处理情况如下:" class="headerlink" title="容错处理情况如下:"></a>容错处理情况如下:</h5><ul><li>当步骤1处理出错,事务回滚,相当于什么都没有发生。</li><li>当步骤2、3处理出错,由于消息保存在消费者表中,可以重新发送到MQ进行重试。</li><li>如果步骤3处理出错,且是业务上的失败,服务提供者发送消息通知消费者事务失败,且此时变为消费者发起回滚事务进行回滚逻辑。</li></ul><h4 id="优点:-3"><a href="#优点:-3" class="headerlink" title="优点:"></a>优点:</h4><p>从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对 MQ 中间件特性的依赖。</p><h4 id="缺点:-2"><a href="#缺点:-2" class="headerlink" title="缺点:"></a>缺点:</h4><p>与具体的业务场景绑定,耦合性强,不可公用。消息数据与业务数据同库,占用业务系统资源。业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限。</p><h3 id="04-1、MQ事务消息(最终一致性)"><a href="#04-1、MQ事务消息(最终一致性)" class="headerlink" title="04-1、MQ事务消息(最终一致性)"></a>04-1、MQ事务消息(最终一致性)</h3><p>支持事务消息的MQ,其支持事务消息的方式采用类似于二阶段提交。基于 MQ 的分布式事务方案其实是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。</p><p><img data-src="/images/pasted-190.png" alt="upload successful"></p><h4 id="条件:-2"><a href="#条件:-2" class="headerlink" title="条件:"></a>条件:</h4><p>a) 需要补偿逻辑<br>b) 业务处理逻辑需要幂等</p><h4 id="处理流程:-2"><a href="#处理流程:-2" class="headerlink" title="处理流程:"></a>处理流程:</h4><p>c) 消费者向MQ发送half消息。<br>d) MQ Server将消息持久化后,向发送方ack确认消息发送成功。<br>e) 消费者开始执行事务逻辑。<br>f) 消费者根据本地事务执行结果向MQ Server提交二次确认或者回滚。<br>g) MQ Server收到commit状态则将half消息标记可投递状态。<br>h) 服务提供者收到该消息,执行本地业务逻辑。返回处理结果。</p><h4 id="优点:-4"><a href="#优点:-4" class="headerlink" title="优点:"></a>优点:</h4><ul><li>消息数据独立存储,降低业务系统与消息系统之间的耦合。</li><li>吞吐量优于本地消息表方案。</li></ul><h4 id="缺点:-3"><a href="#缺点:-3" class="headerlink" title="缺点:"></a>缺点:</h4><ul><li>一次消息发送需要两次网络请求(half消息 + commit/rollback)。</li><li>需要实现消息回查接口。</li></ul><h3 id="05、Sagas事务模型(最终一致性)"><a href="#05、Sagas事务模型(最终一致性)" class="headerlink" title="05、Sagas事务模型(最终一致性)"></a>05、Sagas事务模型(最终一致性)</h3><p>Saga模式是一种分布式异步事务,一种最终一致性事务,是一种柔性事务,有两种不同的方式来实现saga事务,最流行的两种方式是:</p><h4 id="一、-事件-编排Choreography:没有中央协调器(没有单点风险)时,每个服务产生并聆听其他服务的事件,并决定是否应采取行动。"><a href="#一、-事件-编排Choreography:没有中央协调器(没有单点风险)时,每个服务产生并聆听其他服务的事件,并决定是否应采取行动。" class="headerlink" title="一、 事件/编排Choreography:没有中央协调器(没有单点风险)时,每个服务产生并聆听其他服务的事件,并决定是否应采取行动。"></a>一、 事件/编排Choreography:没有中央协调器(没有单点风险)时,每个服务产生并聆听其他服务的事件,并决定是否应采取行动。</h4><p>该实现第一个服务执行一个事务,然后发布一个事件。该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件,当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何Saga参与者听到都意味着事务结束。</p><p><img data-src="/images/pasted-191.png" alt="upload successful"></p><h5 id="处理流程:-3"><a href="#处理流程:-3" class="headerlink" title="处理流程:"></a>处理流程:</h5><ol><li>订单服务保存新订单,将状态设置为pengding挂起状态,并发布名为ORDER_CREATED_EVENT的事件。</li><li>支付服务监听ORDER_CREATED_EVENT,并公布事件BILLED_ORDER_EVENT。</li><li>库存服务监听BILLED_ORDER_EVENT,更新库存,并发布ORDER_PREPARED_EVENT。</li><li>货运服务监听ORDER_PREPARED_EVENT,然后交付产品。最后,它发布ORDER_DELIVERED_EVENT。</li><li>最后,订单服务侦听ORDER_DELIVERED_EVENT并设置订单的状态为concluded完成。</li></ol><p>假设库存服务在事务过程中失败了。进行回滚:</p><ol><li>库存服务产生PRODUCT_OUT_OF_STOCK_EVENT</li><li>订购服务和支付服务会监听到上面库存服务的这一事件:①支付服务会退款给客户。②订单服务将订单状态设置为失败。</li></ol><h5 id="优点:-5"><a href="#优点:-5" class="headerlink" title="优点:"></a>优点:</h5><p>事件/编排是实现Saga模式的自然方式; 它很简单,容易理解,不需要太多的努力来构建,所有参与者都是松散耦合的,因为他们彼此之间没有直接的耦合。如果您的事务涉及2至4个步骤,则可能是非常合适的。</p><h4 id="二、-命令-协调orchestrator:中央协调器负责集中处理事件的决策和业务逻辑排序。"><a href="#二、-命令-协调orchestrator:中央协调器负责集中处理事件的决策和业务逻辑排序。" class="headerlink" title="二、 命令/协调orchestrator:中央协调器负责集中处理事件的决策和业务逻辑排序。"></a>二、 命令/协调orchestrator:中央协调器负责集中处理事件的决策和业务逻辑排序。</h4><p>saga协调器orchestrator以命令/回复的方式与每项服务进行通信,告诉他们应该执行哪些操作。</p><p><img data-src="/images/pasted-192.png" alt="upload successful"></p><h5 id="处理流程:-4"><a href="#处理流程:-4" class="headerlink" title="处理流程:"></a>处理流程:</h5><ol><li>订单服务保存pending状态,并要求订单Saga协调器(简称OSO)开始启动订单事务。</li><li>OSO向收款服务发送执行收款命令,收款服务回复Payment Executed消息。</li><li>OSO向库存服务发送准备订单命令,库存服务将回复OrderPrepared消息。</li><li>OSO向货运服务发送订单发货命令,货运服务将回复Order Delivered消息。</li></ol><p>OSO订单Saga协调器必须事先知道执行“创建订单”事务所需的流程(通过读取BPM业务流程XML配置获得)。如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚。当你有一个中央协调器协调一切时,回滚要容易得多,因为协调器默认是执行正向流程,回滚时只要执行反向流程即可。</p><h5 id="优点:-6"><a href="#优点:-6" class="headerlink" title="优点:"></a>优点:</h5><ul><li>避免服务之间的循环依赖关系,因为saga协调器会调用saga参与者,但参与者不会调用协调器。</li><li>集中分布式事务的编排。</li><li>只需要执行命令/回复(其实回复消息也是一种事件消息),降低参与者的复杂性。</li><li>在添加新步骤时,事务复杂性保持线性,回滚更容易管理。</li><li>如果在第一笔交易还没有执行完,想改变有第二笔事务的目标对象,则可以轻松地将其暂停在协调器上,直到第一笔交易结束。</li></ul><h5 id="缺点:-4"><a href="#缺点:-4" class="headerlink" title="缺点:"></a>缺点:</h5><p>协调器中集中太多逻辑的风险。</p><blockquote><p>原文:<a href="https://www.kubernetes.org.cn/5580.html" target="_blank" rel="noopener">https://www.kubernetes.org.cn/5580.html</a></p></blockquote>]]></content>
<summary type="html">
<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个东西,特别是在微服务架构中,几乎可以说是无法避免。</p>
<h3 id="ACID"><a href="#ACID" class="headerlink" title="ACID"></a>ACID</h3><p>指数据库事务正确执行的四个基本要素:</p>
<ul>
<li><strong>原子性(Atomicity)</strong></li>
<li><strong>一致性(Consistency)</strong></li>
<li><strong>隔离性(Isolation)</strong></li>
<li><strong>持久性(Durability)</strong></li>
</ul>
</summary>
<category term="技术解决方案" scheme="http://zhangfei.men/categories/%E6%8A%80%E6%9C%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="分布式事务" scheme="http://zhangfei.men/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/"/>
</entry>
<entry>
<title>常用的分布式事务解决方案</title>
<link href="http://zhangfei.men/posts/96e108be/"/>
<id>http://zhangfei.men/posts/96e108be/</id>
<published>2020-02-19T09:11:30.000Z</published>
<updated>2020-07-28T01:08:16.457Z</updated>
<content type="html"><![CDATA[<blockquote><p>众所周知,数据库能实现<strong>本地事务</strong>,也就是在<strong>同一个数据库中</strong>,你可以允许一组操作要么全都正确执行,要么全都不执行。这里特别强调了<strong>本地事务</strong>,也就是目前的数据库只能支持同一个数据库中的事务。但现在的系统往往采用微服务架构,业务系统拥有独立的数据库,因此就出现了跨多个数据库的事务需求,这种事务即为“分布式事务”。那么在目前数据库不支持跨库事务的情况下,我们应该如何实现分布式事务呢?本文首先会为大家梳理分布式事务的基本概念和理论基础,然后介绍几种目前常用的分布式事务解决方案。废话不多说,那就开始吧~</p></blockquote><h2 id="什么是事务"><a href="#什么是事务" class="headerlink" title="什么是事务?"></a>什么是事务?</h2><p>事务由一组操作构成,我们希望这组操作能够全部正确执行,如果这一组操作中的任意一个步骤发生错误,那么就需要回滚之前已经完成的操作。也就是同一个事务中的所有操作,要么全都正确执行,要么全都不要执行。</p><h2 id="事务的四大特性-ACID"><a href="#事务的四大特性-ACID" class="headerlink" title="事务的四大特性 ACID"></a>事务的四大特性 ACID</h2><p>说到事务,就不得不提一下事务著名的四大特性。</p><ul><li>原子性<br>原子性要求,事务是一个不可分割的执行单元,事务中的所有操作要么全都执行,要么全都不执行。</li><li>一致性<br>一致性要求,事务在开始前和结束后,数据库的完整性约束没有被破坏。</li><li>隔离性<br>事务的执行是相互独立的,它们不会相互干扰,一个事务不会看到另一个正在运行过程中的事务的数据。</li><li>持久性<br>持久性要求,一个事务完成之后,事务的执行结果必须是持久化保存的。即使数据库发生崩溃,在数据库恢复后事务提交的结果仍然不会丢失。</li></ul><blockquote><p>注意:事务只能保证数据库的<strong>高可靠性</strong>,即数据库本身发生问题后,事务提交后的数据仍然能恢复;而如果不是数据库本身的故障,如硬盘损坏了,那么事务提交的数据可能就丢失了。这属于『<strong>高可用性</strong>』的范畴。因此,事务只能保证数据库的『高可靠性』,而『高可用性』需要整个系统共同配合实现。</p></blockquote><h2 id="事务的隔离级别"><a href="#事务的隔离级别" class="headerlink" title="事务的隔离级别"></a>事务的隔离级别</h2><p>这里扩展一下,对事务的<strong>隔离性</strong>做一个详细的解释。<br>在事务的四大特性ACID中,要求的隔离性是一种严格意义上的隔离,也就是多个事务是串行执行的,彼此之间不会受到任何干扰。这确实能够完全保证数据的安全性,但在实际业务系统中,这种方式性能不高。因此,数据库定义了四种隔离级别,隔离级别和数据库的性能是呈反比的,隔离级别越低,数据库性能越高,而隔离级别越高,数据库性能越差。</p><h3 id="事务并发执行会出现的问题"><a href="#事务并发执行会出现的问题" class="headerlink" title="事务并发执行会出现的问题"></a>事务并发执行会出现的问题</h3><p>我们先来看一下在不同的隔离级别下,数据库可能会出现的问题:</p><ol><li>更新丢失<br>当有两个并发执行的事务,更新同一行数据,那么有可能一个事务会把另一个事务的更新覆盖掉。<br>当数据库没有加任何锁操作的情况下会发生。</li><li>脏读<br>一个事务读到另一个尚未提交的事务中的数据。<br>该数据可能会被回滚从而失效。<br>如果第一个事务拿着失效的数据去处理那就发生错误了。</li><li>不可重复读<br>不可重复度的含义:一个事务对同一行数据读了两次,却得到了不同的结果。它具体分为如下两种情况:<ul><li>虚读:在事务1两次读取同一记录的过程中,事务2对该记录进行了修改,从而事务1第二次读到了不一样的记录。</li><li>幻读:事务1在两次查询的过程中,事务2对该表进行了插入、删除操作,从而事务1第二次查询的结果发生了变化。<blockquote><p>不可重复读 与 脏读 的区别?<br>脏读读到的是尚未提交的数据,而不可重复读读到的是已经提交的数据,只不过在两次读的过程中数据被另一个事务改过了。</p></blockquote></li></ul></li></ol><h3 id="数据库的四种隔离级别"><a href="#数据库的四种隔离级别" class="headerlink" title="数据库的四种隔离级别"></a>数据库的四种隔离级别</h3><p>数据库一共有如下四种隔离级别:</p><ol><li>Read uncommitted 读未提交<br>在该级别下,一个事务对一行数据修改的过程中,不允许另一个事务对该行数据进行修改,但允许另一个事务对该行数据读。<br>因此本级别下,不会出现更新丢失,但会出现脏读、不可重复读。</li><li>Read committed 读提交<br>在该级别下,未提交的写事务不允许其他事务访问该行,因此不会出现脏读;但是读取数据的事务允许其他事务的访问该行数据,因此会出现不可重复读的情况。</li><li>Repeatable read 重复读<br>在该级别下,读事务禁止写事务,但允许读事务,因此不会出现同一事务两次读到不同的数据的情况(不可重复读),且写事务禁止其他一切事务。</li><li>Serializable 序列化<br>该级别要求所有事务都必须串行执行,因此能避免一切因并发引起的问题,但效率很低。</li></ol><p>隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read</p><p>Committed。它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。</p><h2 id="什么是分布式事务?"><a href="#什么是分布式事务?" class="headerlink" title="什么是分布式事务?"></a>什么是分布式事务?</h2><p>到此为止,所介绍的事务都是基于单数据库的本地事务,目前的数据库仅支持单库事务,并不支持跨库事务。而随着微服务架构的普及,一个大型业务系统往往由若干个子系统构成,这些子系统又拥有各自独立的数据库。往往一个业务流程需要由多个子系统共同完成,而且这些操作可能需要在一个事务中完成。在微服务系统中,这些业务场景是普遍存在的。此时,我们就需要在数据库之上通过某种手段,实现支持跨数据库的事务支持,这也就是大家常说的“分布式事务”。<br>这里举一个分布式事务的典型例子——用户下单过程。<br>当我们的系统采用了微服务架构后,一个电商系统往往被拆分成如下几个子系统:商品系统、订单系统、支付系统、积分系统等。整个下单的过程如下:</p><ol><li>用户通过商品系统浏览商品,他看中了某一项商品,便点击下单</li><li>此时订单系统会生成一条订单</li><li>订单创建成功后,支付系统提供支付功能</li><li>当支付完成后,由积分系统为该用户增加积分</li></ol><p>上述步骤2、3、4需要在一个事务中完成。对于传统单体应用而言,实现事务非常简单,只需将这三个步骤放在一个方法A中,再用Spring的@Transactional注解标识该方法即可。Spring通过数据库的事务支持,保证这些步骤要么全都执行完成,要么全都不执行。但在这个微服务架构中,这三个步骤涉及三个系统,涉及三个数据库,此时我们必须在数据库和应用系统之间,通过某项黑科技,实现分布式事务的支持。</p><h2 id="CAP理论"><a href="#CAP理论" class="headerlink" title="CAP理论"></a>CAP理论</h2><p>CAP理论说的是:在一个分布式系统中,最多只能满足C、A、P中的两个需求。<br>CAP的含义:</p><ul><li>C:Consistency 一致性<br>同一数据的多个副本是否实时相同。</li><li>A:Availability 可用性<br>可用性:一定时间内 & 系统返回一个明确的结果 则称为该系统可用。</li><li>P:Partition tolerance 分区容错性<br>将同一服务分布在多个系统中,从而保证某一个系统宕机,仍然有其他系统提供相同的服务。</li></ul><p>CAP理论告诉我们,在分布式系统中,C、A、P三个条件中我们最多只能选择两个。那么问题来了,究竟选择哪两个条件较为合适呢?<br>对于一个业务系统来说,可用性和分区容错性是必须要满足的两个条件,并且这两者是相辅相成的。业务系统之所以使用分布式系统,主要原因有两个:</p><ul><li>提升整体性能<br>当业务量猛增,单个服务器已经无法满足我们的业务需求的时候,就需要使用分布式系统,使用多个节点提供相同的功能,从而整体上提升系统的性能,这就是使用分布式系统的第一个原因。</li><li>实现分区容错性<br>单一节点 或 多个节点处于相同的网络环境下,那么会存在一定的风险,万一该机房断电、该地区发生自然灾害,那么业务系统就全面瘫痪了。为了防止这一问题,采用分布式系统,将多个子系统分布在不同的地域、不同的机房中,从而保证系统高可用性。</li></ul><p>这说明分区容错性是分布式系统的根本,如果分区容错性不能满足,那使用分布式系统将失去意义。<br>此外,可用性对业务系统也尤为重要。在大谈用户体验的今天,如果业务系统时常出现“系统异常”、响应时间过长等情况,这使得用户对系统的好感度大打折扣,在互联网行业竞争激烈的今天,相同领域的竞争者不甚枚举,系统的间歇性不可用会立马导致用户流向竞争对手。因此,我们只能通过牺牲一致性来换取系统的<strong>可用性</strong>和<strong>分区容错性</strong>。这也就是下面要介绍的BASE理论。</p><h2 id="BASE理论"><a href="#BASE理论" class="headerlink" title="BASE理论"></a>BASE理论</h2><p>CAP理论告诉我们一个悲惨但不得不接受的事实——我们只能在C、A、P中选择两个条件。而对于业务系统而言,我们往往选择牺牲一致性来换取系统的可用性和分区容错性。不过这里要指出的是,所谓的“牺牲一致性”并不是完全放弃数据一致性,而是牺牲<strong>强一致性</strong>换取<strong>弱一致性</strong>。下面来介绍下BASE理论。</p><ul><li>BA:Basic Available 基本可用<ul><li>整个系统在某些不可抗力的情况下,仍然能够保证“可用性”,即一定时间内仍然能够返回一个明确的结果。只不过“基本可用”和“高可用”的区别是:<ul><li>“一定时间”可以适当延长<br>当举行大促时,响应时间可以适当延长</li><li>给部分用户返回一个降级页面<br>给部分用户直接返回一个降级页面,从而缓解服务器压力。但要注意,返回降级页面仍然是返回明确结果。</li></ul></li></ul></li><li>S:Soft State:柔性状态<br>同一数据的不同副本的状态,可以不需要实时一致。</li><li>E:Eventual Consisstency:最终一致性<br>同一数据的不同副本的状态,可以不需要实时一致,但一定要保证经过一定时间后仍然是一致的。</li></ul><h2 id="酸碱平衡"><a href="#酸碱平衡" class="headerlink" title="酸碱平衡"></a>酸碱平衡</h2><p>ACID能够保证事务的强一致性,即数据是实时一致的。这在本地事务中是没有问题的,在分布式事务中,强一致性会极大影响分布式系统的性能,因此分布式系统中遵循BASE理论即可。但分布式系统的不同业务场景对一致性的要求也不同。如交易场景下,就要求强一致性,此时就需要遵循ACID理论,而在注册成功后发送短信验证码等场景下,并不需要实时一致,因此遵循BASE理论即可。因此要根据具体业务场景,在ACID和BASE之间寻求平衡。</p><h2 id="分布式事务协议"><a href="#分布式事务协议" class="headerlink" title="分布式事务协议"></a>分布式事务协议</h2><p>下面介绍几种实现分布式事务的协议。</p><h3 id="两阶段提交协议-2PC"><a href="#两阶段提交协议-2PC" class="headerlink" title="两阶段提交协议 2PC"></a>两阶段提交协议 2PC</h3><p>分布式系统的一个难点是如何保证架构下多个节点在进行事务性操作的时候保持一致性。为实现这个目的,二阶段提交算法的成立基于以下假设:</p><ul><li>该分布式系统中,存在一个节点作为协调者(Coordinator),其他节点作为参与者(Cohorts)。且节点之间可以进行网络通信。</li><li>所有节点都采用预写式日志,且日志被写入后即被保持在可靠的存储设备上,即使节点损坏不会导致日志数据的消失。</li><li>所有节点不会永久性损坏,即使损坏后仍然可以恢复。</li></ul><p><strong>1. 第一阶段(投票阶段):</strong></p><ol><li>协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。</li><li>参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作)</li><li>各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个”同意”消息;如果参与者节点的事务操作实际执行失败,则它返回一个”中止”消息。</li></ol><p><strong>2. 第二阶段(提交执行阶段):</strong><br>当协调者节点从所有参与者节点获得的相应消息都为”同意”时:</p><ol><li>协调者节点向所有参与者节点发出”正式提交(commit)”的请求。</li><li>参与者节点正式完成操作,并释放在整个事务期间内占用的资源。</li><li>参与者节点向协调者节点发送”完成”消息。</li><li>协调者节点受到所有参与者节点反馈的”完成”消息后,完成事务。</li></ol><p>如果任一参与者节点在第一阶段返回的响应消息为”中止”,或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:</p><ol><li>协调者节点向所有参与者节点发出”回滚操作(rollback)”的请求。</li><li>参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。</li><li>参与者节点向协调者节点发送”回滚完成”消息。</li><li>协调者节点受到所有参与者节点反馈的”回滚完成”消息后,取消事务。</li></ol><p>不管最后结果如何,第二阶段都会结束当前事务。<br>二阶段提交看起来确实能够提供原子性的操作,但是不幸的事,二阶段提交还是有几个缺点的:</p><ol><li>执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。</li><li>参与者发生故障。协调者需要给每个参与者额外指定超时机制,超时后整个事务失败。(没有多少容错机制)</li><li>协调者发生故障。参与者会一直阻塞下去。需要额外的备机进行容错。(这个可以依赖后面要讲的Paxos协议实现HA)</li><li>二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。</li></ol><p>为此,Dale Skeen和Michael Stonebraker在“A Formal Model of Crash Recovery in a Distributed System”中提出了三阶段提交协议(3PC)。</p><h3 id="三阶段提交协议-3PC"><a href="#三阶段提交协议-3PC" class="headerlink" title="三阶段提交协议 3PC"></a>三阶段提交协议 3PC</h3><p>与两阶段提交不同的是,三阶段提交有两个改动点。</p><ul><li>引入超时机制。同时在协调者和参与者中都引入超时机制。</li><li>在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。</li></ul><p>也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。<br><strong>1. CanCommit阶段</strong><br>3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。</p><ol><li>事务询问<br>协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。</li><li>响应反馈<br>参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No</li></ol><p><strong>2. PreCommit阶段</strong><br>协调者根据参与者的反应情况来决定是否可以记性事务的PreCommit操作。根据响应情况,有以下两种可能。<br>假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。</p><ol><li>发送预提交请求<br>协调者向参与者发送PreCommit请求,并进入Prepared阶段。</li><li>事务预提交<br>参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。</li><li>响应反馈<br>如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。</li></ol><p>假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。</p><ol><li>发送中断请求<br>协调者向所有参与者发送abort请求。</li><li>中断事务<br>参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。</li></ol><p><strong>3. doCommit阶段</strong><br>该阶段进行真正的事务提交,也可以分为以下两种情况。<br>该阶段进行真正的事务提交,也可以分为以下两种情况。<br><strong>3.1 执行提交</strong></p><ol><li>发送提交请求<br>协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。</li><li>事务提交<br>参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。</li><li>响应反馈<br>事务提交完之后,向协调者发送Ack响应。</li><li>完成事务<br>协调者接收到所有参与者的ack响应之后,完成事务。</li></ol><p><strong>3.2 中断事务</strong><br>协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。</p><ol><li>发送中断请求<br>协调者向所有参与者发送abort请求</li><li>事务回滚<br>参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。</li><li>反馈结果<br>参与者完成事务回滚之后,向协调者发送ACK消息</li><li>中断事务<br>协调者接收到参与者反馈的ACK消息之后,执行事务的中断。</li></ol><h2 id="分布式事务的解决方案"><a href="#分布式事务的解决方案" class="headerlink" title="分布式事务的解决方案"></a>分布式事务的解决方案</h2><p>分布式事务的解决方案有如下几种:</p><ul><li>全局消息</li><li>基于可靠消息服务的分布式事务</li><li>TCC</li><li>最大努力通知</li></ul><h3 id="方案1:全局事务(DTP模型)"><a href="#方案1:全局事务(DTP模型)" class="headerlink" title="方案1:全局事务(DTP模型)"></a>方案1:全局事务(DTP模型)</h3><p>全局事务基于DTP模型实现。DTP是由X/Open组织提出的一种分布式事务模型——X/Open Distributed Transaction Processing Reference Model。它规定了要实现分布式事务,需要三种角色:</p><ul><li>AP:Application 应用系统<br>它就是我们开发的业务系统,在我们开发的过程中,可以使用资源管理器提供的事务接口来实现分布式事务。</li><li>TM:Transaction Manager 事务管理器<ul><li>分布式事务的实现由事务管理器来完成,它会提供分布式事务的操作接口供我们的业务系统调用。这些接口称为TX接口。</li><li>事务管理器还管理着所有的资源管理器,通过它们提供的XA接口来同一调度这些资源管理器,以实现分布式事务。</li><li>DTP只是一套实现分布式事务的规范,并没有定义具体如何实现分布式事务,TM可以采用2PC、3PC、Paxos等协议实现分布式事务。</li></ul></li><li>RM:Resource Manager 资源管理器<ul><li>能够提供数据服务的对象都可以是资源管理器,比如:数据库、消息中间件、缓存等。大部分场景下,数据库即为分布式事务中的资源管理器。</li><li>资源管理器能够提供单数据库的事务能力,它们通过XA接口,将本数据库的提交、回滚等能力提供给事务管理器调用,以帮助事务管理器实现分布式的事务管理。</li><li>XA是DTP模型定义的接口,用于向事务管理器提供该资源管理器(该数据库)的提交、回滚等能力。</li><li>DTP只是一套实现分布式事务的规范,RM具体的实现是由数据库厂商来完成的。</li></ul></li></ul><ol><li>有没有基于DTP模型的分布式事务中间件?</li><li>DTP模型有啥优缺点?</li></ol><h3 id="方案2:基于可靠消息服务的分布式事务"><a href="#方案2:基于可靠消息服务的分布式事务" class="headerlink" title="方案2:基于可靠消息服务的分布式事务"></a>方案2:基于可靠消息服务的分布式事务</h3><p>这种实现分布式事务的方式需要通过消息中间件来实现。假设有A和B两个系统,分别可以处理任务A和任务B。此时系统A中存在一个业务流程,需要将任务A和任务B在同一个事务中处理。下面来介绍基于消息中间件来实现这种分布式事务。</p><p><img data-src="/images/pasted-60.png" alt="upload successful"></p><ul><li>在系统A处理任务A前,首先向消息中间件发送一条消息</li><li>消息中间件收到后将该条消息持久化,但并不投递。此时下游系统B仍然不知道该条消息的存在。</li><li>消息中间件持久化成功后,便向系统A返回一个确认应答;</li><li>系统A收到确认应答后,则可以开始处理任务A;</li><li>任务A处理完成后,向消息中间件发送Commit请求。该请求发送完成后,对系统A而言,该事务的处理过程就结束了,此时它可以处理别的任务了。<br>但commit消息可能会在传输途中丢失,从而消息中间件并不会向系统B投递这条消息,从而系统就会出现不一致性。这个问题由消息中间件的事务回查机制完成,下文会介绍。</li><li>消息中间件收到Commit指令后,便向系统B投递该消息,从而触发任务B的执行;</li><li>当任务B执行完成后,系统B向消息中间件返回一个确认应答,告诉消息中间件该消息已经成功消费,此时,这个分布式事务完成。</li></ul><blockquote><p>上述过程可以得出如下几个结论:</p></blockquote><ol><li>消息中间件扮演者分布式事务协调者的角色。</li><li>系统A完成任务A后,到任务B执行完成之间,会存在一定的时间差。在这个时间差内,整个系统处于数据不一致的状态,但这短暂的不一致性是可以接受的,因为经过短暂的时间后,系统又可以保持数据一致性,满足BASE理论。</li></ol><p>上述过程中,如果任务A处理失败,那么需要进入回滚流程,如下图所示:</p><p><img data-src="/images/pasted-59.png" alt="upload successful"></p><ul><li>若系统A在处理任务A时失败,那么就会向消息中间件发送Rollback请求。和发送Commit请求一样,系统A发完之后便可以认为回滚已经完成,它便可以去做其他的事情。</li><li>消息中间件收到回滚请求后,直接将该消息丢弃,而不投递给系统B,从而不会触发系统B的任务B。</li></ul><blockquote><p>此时系统又处于一致性状态,因为任务A和任务B都没有执行。</p></blockquote><p>上面所介绍的Commit和Rollback都属于理想情况,但在实际系统中,Commit和Rollback指令都有可能在传输途中丢失。那么当出现这种情况的时候,消息中间件是如何保证数据一致性呢?——答案就是超时询问机制。</p><p><img data-src="/images/pasted-58.png" alt="upload successful"></p><p>系统A除了实现正常的业务流程外,还需提供一个事务询问的接口,供消息中间件调用。当消息中间件收到一条事务型消息后便开始计时,如果到了超时时间也没收到系统A发来的Commit或Rollback指令的话,就会主动调用系统A提供的事务询问接口询问该系统目前的状态。该接口会返回三种结果:</p><ul><li>提交<br>若获得的状态是“提交”,则将该消息投递给系统B。</li><li>回滚<br>若获得的状态是“回滚”,则直接将条消息丢弃。</li><li>处理中<br>若获得的状态是“处理中”,则继续等待。</li></ul><blockquote><p>消息中间件的超时询问机制能够防止上游系统因在传输过程中丢失Commit/Rollback指令而导致的系统不一致情况,而且能降低上游系统的阻塞时间,上游系统只要发出Commit/Rollback指令后便可以处理其他任务,无需等待确认应答。而Commit/Rollback指令丢失的情况通过超时询问机制来弥补,这样大大降低上游系统的阻塞时间,提升系统的并发度。</p></blockquote><p>下面来说一说消息投递过程的可靠性保证。<br>当上游系统执行完任务并向消息中间件提交了Commit指令后,便可以处理其他任务了,此时它可以认为事务已经完成,接下来消息中间件<strong>一定会保证消息被下游系统成功消费掉!</strong>那么这是怎么做到的呢?这由消息中间件的投递流程来保证。<br>消息中间件向下游系统投递完消息后便进入阻塞等待状态,下游系统便立即进行任务的处理,任务处理完成后便向消息中间件返回应答。消息中间件收到确认应答后便认为该事务处理完毕!<br>如果消息在投递过程中丢失,或消息的确认应答在返回途中丢失,那么消息中间件在等待确认应答超时之后就会重新投递,直到下游消费者返回消费成功响应为止。当然,一般消息中间件可以设置消息重试的次数和时间间隔,比如:当第一次投递失败后,每隔五分钟重试一次,一共重试3次。如果重试3次之后仍然投递失败,那么这条消息就需要人工干预。</p><p><img data-src="/images/pasted-57.png" alt="upload successful"></p><p><img data-src="/images/pasted-56.png" alt="upload successful"></p><blockquote><p>有的同学可能要问:消息投递失败后为什么不回滚消息,而是不断尝试重新投递?</p></blockquote><p>这就涉及到整套分布式事务系统的实现成本问题。<br>我们知道,当系统A将向消息中间件发送Commit指令后,它便去做别的事情了。如果此时消息投递失败,需要回滚的话,就需要让系统A事先提供回滚接口,这无疑增加了额外的开发成本,业务系统的复杂度也将提高。对于一个业务系统的设计目标是,在保证性能的前提下,最大限度地降低系统复杂度,从而能够降低系统的运维成本。</p><blockquote><p>不知大家是否发现,上游系统A向消息中间件提交Commit/Rollback消息采用的是异步方式,也就是当上游系统提交完消息后便可以去做别的事情,接下来提交、回滚就完全交给消息中间件来完成,并且完全信任消息中间件,认为它一定能正确地完成事务的提交或回滚。然而,消息中间件向下游系统投递消息的过程是同步的。也就是消息中间件将消息投递给下游系统后,它会阻塞等待,等下游系统成功处理完任务返回确认应答后才取消阻塞等待。为什么这两者在设计上是不一致的呢?</p></blockquote><p>首先,上游系统和消息中间件之间采用异步通信是为了提高系统并发度。业务系统直接和用户打交道,用户体验尤为重要,因此这种异步通信方式能够极大程度地降低用户等待时间。此外,异步通信相对于同步通信而言,没有了长时间的阻塞等待,因此系统的并发性也大大增加。但异步通信可能会引起Commit/Rollback指令丢失的问题,这就由消息中间件的超时询问机制来弥补。<br>那么,消息中间件和下游系统之间为什么要采用同步通信呢?<br>异步能提升系统性能,但随之会增加系统复杂度;而同步虽然降低系统并发度,但实现成本较低。因此,在对并发度要求不是很高的情况下,或者服务器资源较为充裕的情况下,我们可以选择同步来降低系统的复杂度。<br>我们知道,消息中间件是一个独立于业务系统的第三方中间件,它不和任何业务系统产生直接的耦合,它也不和用户产生直接的关联,它一般部署在独立的服务器集群上,具有良好的可扩展性,所以不必太过于担心它的性能,如果处理速度无法满足我们的要求,可以增加机器来解决。而且,即使消息中间件处理速度有一定的延迟那也是可以接受的,因为前面所介绍的BASE理论就告诉我们了,我们追求的是最终一致性,而非实时一致性,因此消息中间件产生的时延导致事务短暂的不一致是可以接受的。</p><h3 id="方案3:最大努力通知(定期校对)"><a href="#方案3:最大努力通知(定期校对)" class="headerlink" title="方案3:最大努力通知(定期校对)"></a>方案3:最大努力通知(定期校对)</h3><p>最大努力通知也被称为定期校对,其实在方案二中已经包含,这里再单独介绍,主要是为了知识体系的完整性。这种方案也需要消息中间件的参与,其过程如下:</p><p><img data-src="/images/pasted-55.png" alt="upload successful"></p><ul><li>上游系统在完成任务后,向消息中间件同步地发送一条消息,确保消息中间件成功持久化这条消息,然后上游系统可以去做别的事情了;</li><li>消息中间件收到消息后负责将该消息同步投递给相应的下游系统,并触发下游系统的任务执行;</li><li>当下游系统处理成功后,向消息中间件反馈确认应答,消息中间件便可以将该条消息删除,从而该事务完成。</li></ul><p>上面是一个理想化的过程,但在实际场景中,往往会出现如下几种意外情况:</p><ol><li>消息中间件向下游系统投递消息失败</li><li>上游系统向消息中间件发送消息失败</li></ol><p>对于第一种情况,消息中间件具有重试机制,我们可以在消息中间件中设置消息的重试次数和重试时间间隔,对于网络不稳定导致的消息投递失败的情况,往往重试几次后消息便可以成功投递,如果超过了重试的上限仍然投递失败,那么消息中间件不再投递该消息,而是记录在失败消息表中,消息中间件需要提供失败消息的查询接口,下游系统会定期查询失败消息,并将其消费,这就是所谓的“定期校对”。<br>如果重复投递和定期校对都不能解决问题,往往是因为下游系统出现了严重的错误,此时就需要人工干预。<br>对于第二种情况,需要在上游系统中建立消息重发机制。可以在上游系统建立一张本地消息表,并将 <strong>任务处理过程</strong> 和 <strong>向本地消息表中插入消息</strong></p><p>这两个步骤放在一个本地事务中完成。如果向本地消息表插入消息失败,那么就会触发回滚,之前的任务处理结果就会被取消。如果这量步都执行成功,那么该本地事务就完成了。接下来会有一个专门的消息发送者不断地发送本地消息表中的消息,如果发送失败它会返回重试。当然,也要给消息发送者设置重试的上限,一般而言,达到重试上限仍然发送失败,那就意味着消息中间件出现严重的问题,此时也只有人工干预才能解决问题。<br>对于不支持事务型消息的消息中间件,如果要实现分布式事务的话,就可以采用这种方式。它能够通过<strong>重试机制</strong>+<strong>定期校对</strong>实现分布式事务,但相比于第二种方案,它达到数据一致性的周期较长,而且还需要在上游系统中实现消息重试发布机制,以确保消息成功发布给消息中间件,这无疑增加了业务系统的开发成本,使得业务系统不够纯粹,并且这些额外的业务逻辑无疑会占用业务系统的硬件资源,从而影响性能。<br>因此,尽量选择支持事务型消息的消息中间件来实现分布式事务,如RocketMQ。</p><h3 id="方案4:TCC(两阶段型、补偿型)"><a href="#方案4:TCC(两阶段型、补偿型)" class="headerlink" title="方案4:TCC(两阶段型、补偿型)"></a>方案4:TCC(两阶段型、补偿型)</h3><p>TCC即为Try Confirm Cancel,它属于补偿型分布式事务。顾名思义,TCC实现分布式事务一共有三个步骤:</p><ul><li>Try:尝试待执行的业务<ul><li>这个过程并未执行业务,只是完成所有业务的一致性检查,并预留好执行所需的全部资源</li></ul></li><li>Confirm:执行业务<ul><li>这个过程真正开始执行业务,由于Try阶段已经完成了一致性检查,因此本过程直接执行,而不做任何检查。并且在执行的过程中,会使用到Try阶段预留的业务资源。</li></ul></li><li>Cancel:取消执行的业务<ul><li>若业务执行失败,则进入Cancel阶段,它会释放所有占用的业务资源,并回滚Confirm阶段执行的操作。</li></ul></li></ul><p>下面以一个转账的例子来解释下TCC实现分布式事务的过程。</p><blockquote><p>假设用户A用他的账户余额给用户B发一个100元的红包,并且余额系统和红包系统是两个独立的系统。</p></blockquote><ul><li>Try<ul><li>创建一条转账流水,并将流水的状态设为<strong>交易中</strong></li><li>将用户A的账户中扣除100元(预留业务资源)</li><li>Try成功之后,便进入Confirm阶段</li><li>Try过程发生任何异常,均进入Cancel阶段</li></ul></li><li>Confirm<ul><li>向B用户的红包账户中增加100元</li><li>将流水的状态设为<strong>交易已完成</strong></li><li>Confirm过程发生任何异常,均进入Cancel阶段</li><li>Confirm过程执行成功,则该事务结束</li></ul></li><li>Cancel<ul><li>将用户A的账户增加100元</li><li>将流水的状态设为<strong>交易失败</strong></li></ul></li></ul><p>在传统事务机制中,业务逻辑的执行和事务的处理,是在不同的阶段由不同的部件来完成的:业务逻辑部分访问资源实现数据存储,其处理是由业务系统负责;事务处理部分通过协调资源管理器以实现事务管理,其处理由事务管理器来负责。二者没有太多交互的地方,所以,传统事务管理器的事务处理逻辑,仅需要着眼于事务完成(commit/rollback)阶段,而不必关注业务执行阶段。</p><h4 id="TCC全局事务必须基于RM本地事务来实现全局事务"><a href="#TCC全局事务必须基于RM本地事务来实现全局事务" class="headerlink" title="TCC全局事务必须基于RM本地事务来实现全局事务"></a>TCC全局事务必须基于RM本地事务来实现全局事务</h4><p>TCC服务是由Try/Confirm/Cancel业务构成的,<br>其Try/Confirm/Cancel业务在执行时,会访问资源管理器(Resource Manager,下文简称RM)来存取数据。这些存取操作,必须要参与RM本地事务,以使其更改的数据要么都commit,要么都rollback。<br>这一点不难理解,考虑一下如下场景:</p><p><img data-src="/images/pasted-54.png" alt="upload successful"></p><p>假设图中的服务B没有基于RM本地事务(以RDBS为例,可通过设置auto-commit为true来模拟),那么一旦[B:Try]操作中途执行失败,TCC事务框架后续决定回滚全局事务时,该[B:Cancel]则需要判断[B:Try]中哪些操作已经写到DB、哪些操作还没有写到DB:假设[B:Try]业务有5个写库操作,[B:Cancel]业务则需要逐个判断这5个操作是否生效,并将生效的操作执行反向操作。<br>不幸的是,由于[B:Cancel]业务也有n(0<=n<=5)个反向的写库操作,此时一旦[B:Cancel]也中途出错,则后续的[B:Cancel]执行任务更加繁重。因为,相比第一次[B:Cancel]操作,后续的[B:Cancel]操作还需要判断先前的[B:Cancel]操作的n(0<=n<=5)个写库中哪几个已经执行、哪几个还没有执行,这就涉及到了幂等性问题。而对幂等性的保障,又很可能还需要涉及额外的写库操作,该写库操作又会因为没有RM本地事务的支持而存在类似问题。。。可想而知,如果不基于RM本地事务,TCC事务框架是无法有效的管理TCC全局事务的。<br>反之,基于RM本地事务的TCC事务,这种情况则会很容易处理:[B:Try]操作中途执行失败,TCC事务框架将其参与RM本地事务直接rollback即可。后续TCC事务框架决定回滚全局事务时,在知道“[B:Try]操作涉及的RM本地事务已经rollback”的情况下,根本无需执行[B:Cancel]操作。<br>换句话说,基于RM本地事务实现TCC事务框架时,一个TCC型服务的cancel业务要么执行,要么不执行,不需要考虑部分执行的情况。</p><h4 id="TCC事务框架应该提供Confirm-Cancel服务的幂等性保障"><a href="#TCC事务框架应该提供Confirm-Cancel服务的幂等性保障" class="headerlink" title="TCC事务框架应该提供Confirm/Cancel服务的幂等性保障"></a>TCC事务框架应该提供Confirm/Cancel服务的幂等性保障</h4><p>一般认为,服务的幂等性,是指针对同一个服务的多次(n>1)请求和对它的单次(n=1)请求,二者具有相同的副作用。<br>在TCC事务模型中,Confirm/Cancel业务可能会被重复调用,其原因很多。比如,全局事务在提交/回滚时会调用各TCC服务的Confirm/Cancel业务逻辑。执行这些Confirm/Cancel业务时,可能会出现如网络中断的故障而使得全局事务不能完成。因此,故障恢复机制后续仍然会重新提交/回滚这些未完成的全局事务,这样就会再次调用参与该全局事务的各TCC服务的Confirm/Cancel业务逻辑。<br>既然Confirm/Cancel业务可能会被多次调用,就需要保障其幂等性。<br>那么,应该由TCC事务框架来提供幂等性保障?还是应该由业务系统自行来保障幂等性呢?<br>个人认为,应该是由TCC事务框架来提供幂等性保障。如果仅仅只是极个别服务存在这个问题的话,那么由业务系统来负责也是可以的;然而,这是一类公共问题,毫无疑问,所有TCC服务的Confirm/Cancel业务存在幂等性问题。TCC服务的公共问题应该由TCC事务框架来解决;而且,考虑一下由业务系统来负责幂等性需要考虑的问题,就会发现,这无疑增大了业务系统的复杂度。</p><h2 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h2><ul><li><a href="http://www.infoq.com/cn/interviews/soa-chengli" target="_blank" rel="noopener">大规模SOA系统中的分布事务处理_程立</a></li><li><a href="https://cs.brown.edu/courses/cs227/archives/2012/papers/weaker/cidr07p15.pdf" target="_blank" rel="noopener">Life beyond Distributed Transactions: an Apostate’s Opinion</a></li><li><a href="http://www.bytesoft.org/tcc-intro/" target="_blank" rel="noopener">关于如何实现一个TCC分布式事务框架的一点思考</a></li><li><a href="http://www.enterpriseintegrationpatterns.com/patterns/conversation/TryConfirmCancel.html" target="_blank" rel="noopener">How can a requestor ensure a consistent outcome across multiple, independent providers</a></li><li><a href="http://www.hollischuang.com/archives/681#rd?sukey=3997c0719f1515205acb269da14295ad50b0186483fbd0a402a566f45b33525978b375ccc44dba3e85c4d645a320ba47" target="_blank" rel="noopener">关于分布式事务、两阶段提交协议、三阶提交协议</a></li><li><a href="https://en.wikipedia.org/wiki/Three-phase_commit_protocol_ei.cs.vt.edu/~cs5204/fall99/distributedDBMS/sreenu/3pc.html" target="_blank" rel="noopener">Three-phase commit protocol</a></li></ul><blockquote><p><strong>作者:</strong>大闲人柴毛毛<br><strong>原文链接:</strong><a href="https://juejin.im/post/5aa3c7736fb9a028bb189bca" target="_blank" rel="noopener">https://juejin.im/post/5aa3c7736fb9a028bb189bca</a></p></blockquote>]]></content>
<summary type="html">
<blockquote>
<p>众所周知,数据库能实现<strong>本地事务</strong>,也就是在<strong>同一个数据库中</strong>,你可以允许一组操作要么全都正确执行,要么全都不执行。这里特别强调了<strong>本地事务</strong>,也就是目前的数据库只能支持同一个数据库中的事务。但现在的系统往往采用微服务架构,业务系统拥有独立的数据库,因此就出现了跨多个数据库的事务需求,这种事务即为“分布式事务”。那么在目前数据库不支持跨库事务的情况下,我们应该如何实现分布式事务呢?本文首先会为大家梳理分布式事务的基本概念和理论基础,然后介绍几种目前常用的分布式事务解决方案。废话不多说,那就开始吧~</p>
</blockquote>
<h2 id="什么是事务"><a href="#什么是事务" class="headerlink" title="什么是事务?"></a>什么是事务?</h2><p>事务由一组操作构成,我们希望这组操作能够全部正确执行,如果这一组操作中的任意一个步骤发生错误,那么就需要回滚之前已经完成的操作。也就是同一个事务中的所有操作,要么全都正确执行,要么全都不要执行。</p>
<h2 id="事务的四大特性-ACID"><a href="#事务的四大特性-ACID" class="headerlink" title="事务的四大特性 ACID"></a>事务的四大特性 ACID</h2><p>说到事务,就不得不提一下事务著名的四大特性。</p>
</summary>
<category term="技术解决方案" scheme="http://zhangfei.men/categories/%E6%8A%80%E6%9C%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="分布式事务" scheme="http://zhangfei.men/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/"/>
</entry>
<entry>
<title>一文告诉你Spring是如何利用&quot;三级缓存&quot;巧妙解决Bean的循环依赖问题的</title>
<link href="http://zhangfei.men/posts/a4427452/"/>
<id>http://zhangfei.men/posts/a4427452/</id>
<published>2020-02-19T06:43:09.000Z</published>
<updated>2020-07-28T01:08:16.457Z</updated>
<content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p><strong>循环依赖:就是N个类循环(嵌套)引用</strong>。通俗的讲就是N个Bean互相引用对方,最终形成<code>闭环</code>。用一副经典的图示可以表示成这样(A、B、C都代表对象,虚线代表引用关系):<br><img data-src="/images/pasted-50.png" alt="upload successful"></p><blockquote><p>注意:其实可以N=1,也就是极限情况的循环依赖:<code>自己依赖自己</code><br>另需注意:这里指的循环引用不是方法之间的循环调用,<strong>而是对象的相互依赖关系</strong>。(方法之间循环调用若有出口也是能够正常work的)</p></blockquote><p>可以设想一下这个场景:如果在日常开发中我们用new对象的方式,若构造函数之间发生这种<strong>循环依赖</strong>的话,程序会在运行时一直循环调用<strong>最终导致内存溢出</strong>,示例代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Main</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception </span>{</span><br><span class="line"> System.out.println(<span class="keyword">new</span> A());</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 class="class"><span class="keyword">class</span> <span class="title">A</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">A</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">new</span> B();</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">B</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">B</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">new</span> A();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>运行报错:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Exception <span class="keyword">in</span> thread <span class="string">"main"</span> java.lang.StackOverflowError</span><br></pre></td></tr></table></figure><p>这是一个典型的循环依赖问题。本文说一下<code>Spring</code>是如果巧妙的解决平时我们会遇到的<code>三大循环依赖问题</code>的。</p><h2 id="Spring-Bean的循环依赖"><a href="#Spring-Bean的循环依赖" class="headerlink" title="Spring Bean的循环依赖"></a>Spring Bean的循环依赖</h2><p>谈到<code>Spring Bean</code>的循环依赖,有的小伙伴可能比较陌生,毕竟开发过程中好像对<code>循环依赖</code>这个概念<strong>无感知</strong>。其实不然,你有这种错觉,权是因为你工作在Spring的<code>襁褓</code>中,从而让你“高枕无忧”。<strong>我十分坚信,小伙伴们在平时业务开发中一定一定写过如下结构的代码:</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">AServiceImpl</span> <span class="keyword">implements</span> <span class="title">AService</span> </span>{</span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> BService bService;</span><br><span class="line"> ...</span><br><span class="line">}</span><br><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">BServiceImpl</span> <span class="keyword">implements</span> <span class="title">BService</span> </span>{</span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> AService aService;</span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这其实就是Spring环境下典型的循环依赖场景。但是很显然,这种循环依赖场景,Spring已经完美的帮我们解决和规避了问题。所以即使平时我们这样循环引用,也能够整成进行我们的coding之旅。</p><h2 id="Spring中三大循环依赖场景演示"><a href="#Spring中三大循环依赖场景演示" class="headerlink" title="Spring中三大循环依赖场景演示"></a>Spring中<code>三大循环依赖场景</code>演示</h2><p>在Spring环境中,因为我们的Bean的实例化、初始化都是交给了容器,因此它的循环依赖主要表现为下面三种场景。为了方便演示,我准备了如下两个类:<br><img data-src="/images/pasted-51.png" alt="upload successful"></p><h3 id="1、构造器注入循环依赖"><a href="#1、构造器注入循环依赖" class="headerlink" title="1、构造器注入循环依赖"></a>1、构造器注入循环依赖</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">A</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">A</span><span class="params">(B b)</span> </span>{</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">B</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">B</span><span class="params">(A a)</span> </span>{</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>结果:项目启动失败抛出异常<code>BeanCurrentlyInCreationException</code></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name <span class="string">'a'</span>: Requested bean is currently <span class="keyword">in</span> creation: Is there an unresolvable circular reference?</span><br><span class="line">at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:339)</span><br><span class="line">at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:215)</span><br><span class="line">at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)</span><br><span class="line">at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)</span><br></pre></td></tr></table></figure><blockquote><p>构造器注入构成的循环依赖,此种循环依赖方式<strong>是无法解决的</strong>,只能抛出<code>BeanCurrentlyInCreationException</code>异常表示循环依赖。这也是构造器注入的最大劣势(它有很多独特的优势,请小伙伴自行发掘)<br><code>根本原因</code>:Spring解决循环依赖依靠的是Bean的“中间态”这个概念,而这个中间态指的是<code>已经实例化</code>,但还没初始化的状态。而构造器是完成实例化的东东,所以构造器的循环依赖无法解决。</p></blockquote><h3 id="2、field属性注入(setter方法注入)循环依赖"><a href="#2、field属性注入(setter方法注入)循环依赖" class="headerlink" title="2、field属性注入(setter方法注入)循环依赖"></a>2、field属性注入(setter方法注入)循环依赖</h3><p>这种方式是我们<strong>最最最最</strong>为常用的依赖注入方式(所以猜都能猜到它肯定不会有问题啦):</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">A</span> </span>{</span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> B b;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">B</span> </span>{</span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> A a;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>结果:项目启动成功,能够正常work</strong></p><blockquote><p>备注:setter方法注入方式因为原理和字段注入方式类似,此处不多加演示</p></blockquote><h3 id="3、prototype-field属性注入循环依赖"><a href="#3、prototype-field属性注入循环依赖" class="headerlink" title="3、prototype field属性注入循环依赖"></a>3、<code>prototype</code> field属性注入循环依赖</h3><p><code>prototype</code>在平时使用情况较少,但是也并不是不会使用到,因此此种方式也需要引起重视。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Scope</span>(ConfigurableBeanFactory.SCOPE_PROTOTYPE)</span><br><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">A</span> </span>{</span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> B b;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="meta">@Scope</span>(ConfigurableBeanFactory.SCOPE_PROTOTYPE)</span><br><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">B</span> </span>{</span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> A a;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>结果:<strong>需要注意的是</strong>本例中<strong>启动时是不会报错的</strong>(因为非单例Bean<code>默认</code>不会初始化,而是使用时才会初始化),所以很简单咱们只需要手动<code>getBean()</code>或者在一个单例Bean内<code>@Autowired</code>一下它即可</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 在单例Bean内注入</span></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> A a;</span><br></pre></td></tr></table></figure><p>这样子启动就报错:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name <span class="string">'mytest.TestSpringBean'</span>: Unsatisfied dependency expressed through field <span class="string">'a'</span>; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name <span class="string">'a'</span>: Unsatisfied dependency expressed through field <span class="string">'b'</span>; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name <span class="string">'b'</span>: Unsatisfied dependency expressed through field <span class="string">'a'</span>; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name <span class="string">'a'</span>: Requested bean is currently <span class="keyword">in</span> creation: Is there an unresolvable circular reference?</span><br><span class="line"></span><br><span class="line">at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor<span class="variable">$AutowiredFieldElement</span>.inject(AutowiredAnnotationBeanPostProcessor.java:596)</span><br><span class="line">at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:90)</span><br><span class="line">at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:374)</span><br></pre></td></tr></table></figure><p>如何解决???可能有的小伙伴看到网上有说使用<code>@Lazy</code>注解解决:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Lazy</span></span><br><span class="line"><span class="meta">@Autowired</span></span><br><span class="line"><span class="keyword">private</span> A a;</span><br></pre></td></tr></table></figure><p>此处负责任的告诉你这样是解决不了问题的(<strong>可能会掩盖问题</strong>),<code>@Lazy</code>只是延迟初始化而已,当你真正使用到它(初始化)的时候,依旧会报如上异常。<br>对于Spring循环依赖的情况总结如下:</p><ol><li>不能解决的情况:<ol><li>构造器注入循环依赖</li><li><code>prototype</code> field属性注入循环依赖</li></ol></li><li>能解决的情况:<ol><li>field属性注入(setter方法注入)循环依赖</li></ol></li></ol><h2 id="Spring解决循环依赖的原理分析"><a href="#Spring解决循环依赖的原理分析" class="headerlink" title="Spring解决循环依赖的原理分析"></a>Spring解决循环依赖的原理分析</h2><p>在这之前需要明白java中所谓的<code>引用传递</code>和<code>值传递</code>的区别。</p><blockquote><p>说明:看到这句话可能有小伙伴就想喷我了。java中明明都是传递啊,这是我初学java时背了100遍的面试题,怎么可能有错???<br> 这就是我做这个申明的必要性:伙计,你的说法是正确的,<code>java中只有值传递</code>。但是本文借用<code>引用传递</code>来辅助讲解,希望小伙伴明白我想表达的意思。</p></blockquote><p><strong><code>Spring的循环依赖的理论依据基于Java的引用传递</code>**,当获得对象的引用时,</strong>对象的属性是可以延后设置的**。(但是构造器必须是在获取引用之前,毕竟你的引用是靠构造器给你生成的,儿子能先于爹出生?哈哈)</p><h3 id="Spring创建Bean的流程"><a href="#Spring创建Bean的流程" class="headerlink" title="Spring创建Bean的流程"></a>Spring创建Bean的流程</h3><p>首先需要了解是Spring它创建Bean的流程,我把它的大致调用栈绘图如下:<br><img data-src="/images/pasted-52.png" alt="upload successful"><br>对Bean的创建最为核心三个方法解释如下:</p><ul><li><code>createBeanInstance</code>:例化,其实也就是调用对象的<strong>构造方法</strong>实例化对象</li><li><code>populateBean</code>:填充属性,这一步主要是对bean的依赖属性进行注入(<code>@Autowired</code>)</li><li><code>initializeBean</code>:回到一些形如<code>initMethod</code>、<code>InitializingBean</code>等方法</li></ul><p>从对<code>单例Bean</code>的初始化可以看出,循环依赖主要发生在<strong>第二步(populateBean)</strong>,也就是field属性注入的处理。</p><h3 id="Spring容器的-39-三级缓存-39"><a href="#Spring容器的-39-三级缓存-39" class="headerlink" title="Spring容器的'三级缓存'"></a>Spring容器的<code>'三级缓存'</code></h3><p>在Spring容器的整个声明周期中,单例Bean有且仅有一个对象。这很容易让人想到可以用缓存来加速访问。<br>从源码中也可以看出Spring大量运用了Cache的手段,在循环依赖问题的解决过程中甚至不惜使用了“三级缓存”,这也便是它设计的精妙之处。<br><code>三级缓存</code>其实它更像是Spring容器工厂的内的<code>术语</code>,采用三级缓存模式来解决循环依赖问题,这三级缓存分别指:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">DefaultSingletonBeanRegistry</span> <span class="keyword">extends</span> <span class="title">SimpleAliasRegistry</span> <span class="keyword">implements</span> <span class="title">SingletonBeanRegistry</span> </span>{</span><br><span class="line">...</span><br><span class="line"><span class="comment">// 从上至下 分表代表这“三级缓存”</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> Map<String, Object> singletonObjects = <span class="keyword">new</span> ConcurrentHashMap<>(<span class="number">256</span>); <span class="comment">//一级缓存</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> Map<String, Object> earlySingletonObjects = <span class="keyword">new</span> HashMap<>(<span class="number">16</span>); <span class="comment">// 二级缓存</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> Map<String, ObjectFactory<?>> singletonFactories = <span class="keyword">new</span> HashMap<>(<span class="number">16</span>); <span class="comment">// 三级缓存</span></span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="comment">/** Names of beans that are currently in creation. */</span></span><br><span class="line"><span class="comment">// 这个缓存也十分重要:它表示bean创建过程中都会在里面呆着</span></span><br><span class="line"><span class="comment">// 它在Bean开始创建时放值,创建完成时会将其移出</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(<span class="keyword">new</span> ConcurrentHashMap<>(<span class="number">16</span>));</span><br><span class="line"></span><br><span class="line"><span class="comment">/** Names of beans that have already been created at least once. */</span></span><br><span class="line"><span class="comment">// 当这个Bean被创建完成后,会标记为这个 注意:这里是set集合 不会重复</span></span><br><span class="line"><span class="comment">// 至少被创建了一次的 都会放进这里</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> Set<String> alreadyCreated = Collections.newSetFromMap(<span class="keyword">new</span> ConcurrentHashMap<>(<span class="number">256</span>));</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>注:<code>AbstractBeanFactory</code>继承自<code>DefaultSingletonBeanRegistry</code>。</p><ol><li><code>singletonObjects</code>:用于存放完全初始化好的 bean,<strong>从该缓存中取出的 bean 可以直接使用</strong></li><li><code>earlySingletonObjects</code>:提前曝光的单例对象的cache,存放原始的 bean 对象(尚未填充属性),用于解决循环依赖</li><li><code>singletonFactories</code>:单例对象工厂的cache,存放 bean 工厂对象,用于解决循环依赖</li></ol><p><strong>获取单例Bean的源码如下:</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">DefaultSingletonBeanRegistry</span> <span class="keyword">extends</span> <span class="title">SimpleAliasRegistry</span> <span class="keyword">implements</span> <span class="title">SingletonBeanRegistry</span> </span>{</span><br><span class="line">...</span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="meta">@Nullable</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> Object <span class="title">getSingleton</span><span class="params">(String beanName)</span> </span>{</span><br><span class="line"><span class="keyword">return</span> getSingleton(beanName, <span class="keyword">true</span>);</span><br><span class="line">}</span><br><span class="line"><span class="meta">@Nullable</span></span><br><span class="line"><span class="function"><span class="keyword">protected</span> Object <span class="title">getSingleton</span><span class="params">(String beanName, <span class="keyword">boolean</span> allowEarlyReference)</span> </span>{</span><br><span class="line">Object singletonObject = <span class="keyword">this</span>.singletonObjects.get(beanName);</span><br><span class="line"><span class="keyword">if</span> (singletonObject == <span class="keyword">null</span> && isSingletonCurrentlyInCreation(beanName)) {</span><br><span class="line"><span class="keyword">synchronized</span> (<span class="keyword">this</span>.singletonObjects) {</span><br><span class="line">singletonObject = <span class="keyword">this</span>.earlySingletonObjects.get(beanName);</span><br><span class="line"><span class="keyword">if</span> (singletonObject == <span class="keyword">null</span> && allowEarlyReference) {</span><br><span class="line">ObjectFactory<?> singletonFactory = <span class="keyword">this</span>.singletonFactories.get(beanName);</span><br><span class="line"><span class="keyword">if</span> (singletonFactory != <span class="keyword">null</span>) {</span><br><span class="line">singletonObject = singletonFactory.getObject();</span><br><span class="line"><span class="keyword">this</span>.earlySingletonObjects.put(beanName, singletonObject);</span><br><span class="line"><span class="keyword">this</span>.singletonFactories.remove(beanName);</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 class="keyword">return</span> singletonObject;</span><br><span class="line">}</span><br><span class="line">...</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">isSingletonCurrentlyInCreation</span><span class="params">(String beanName)</span> </span>{</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">this</span>.singletonsCurrentlyInCreation.contains(beanName);</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">boolean</span> <span class="title">isActuallyInCreation</span><span class="params">(String beanName)</span> </span>{</span><br><span class="line"><span class="keyword">return</span> isSingletonCurrentlyInCreation(beanName);</span><br><span class="line">}</span><br><span class="line">...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol><li>先从<code>一级缓存singletonObjects</code>中去获取。(如果获取到就直接return)</li><li>如果获取不到或者对象正在创建中(<code>isSingletonCurrentlyInCreation()</code>),那就再从<code>二级缓存earlySingletonObjects</code>中获取。(如果获取到就直接return)</li><li>如果还是获取不到,且允许singletonFactories(allowEarlyReference=true)通过<code>getObject()</code>获取。就从<code>三级缓存singletonFactory</code>.getObject()获取。<strong>(如果获取到了就从</strong><code>**singletonFactories**</code><strong>中移除,并且放进</strong><code>**earlySingletonObjects**</code><strong>。其实也就是从三级缓存</strong><code>**移动(是剪切、不是复制哦!)**</code><strong>到了二级缓存)</strong><blockquote><p><strong>加入<code>singletonFactories</code>三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决</strong></p></blockquote></li></ol><p><code>getSingleton()</code>从缓存里获取单例对象步骤分析可知,Spring解决循环依赖的诀窍:<strong>就在于singletonFactories这个三级缓存</strong>。这个Cache里面都是<code>ObjectFactory</code>,它是解决问题的关键。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 它可以将创建对象的步骤封装到ObjectFactory中 交给自定义的Scope来选择是否需要创建对象来灵活的实现scope。 具体参见Scope接口</span></span><br><span class="line"><span class="meta">@FunctionalInterface</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">ObjectFactory</span><<span class="title">T</span>> </span>{</span><br><span class="line"><span class="function">T <span class="title">getObject</span><span class="params">()</span> <span class="keyword">throws</span> BeansException</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><blockquote><p>经过ObjectFactory.getObject()后,此时放进了二级缓存<code>earlySingletonObjects</code>内。这个时候对象已经实例化了,<code>虽然还不完美</code>,但是对象的引用已经可以被其它引用了。</p></blockquote><p><strong>此处说一下二级缓存<code>earlySingletonObjects</code>它里面的数据什么时候添加什么移除???</strong><br><strong>添加</strong>:向里面添加数据只有一个地方,就是上面说的<code>getSingleton()</code>里从三级缓存里挪过来<br><strong>移除</strong>:<code>addSingleton、addSingletonFactory、removeSingleton</code>从语义中可以看出添加单例、添加单例工厂<code>ObjectFactory</code>的时候都会删除二级缓存里面对应的缓存值,是互斥的</p><h3 id="源码解析"><a href="#源码解析" class="headerlink" title="源码解析"></a>源码解析</h3><p><code>Spring</code>容器会将每一个正在创建的Bean 标识符放在一个“当前创建Bean池”中,Bean标识符在创建过程中将一直保持在这个池中,而对于创建完毕的Bean将从<code>当前创建Bean池</code>中清除掉。<br> 这个“当前创建Bean池”指的是上面提到的<code>singletonsCurrentlyInCreation</code>那个集合。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">AbstractBeanFactory</span> <span class="keyword">extends</span> <span class="title">FactoryBeanRegistrySupport</span> <span class="keyword">implements</span> <span class="title">ConfigurableBeanFactory</span> </span>{</span><br><span class="line">...</span><br><span class="line"><span class="keyword">protected</span> <T> <span class="function">T <span class="title">doGetBean</span><span class="params">(<span class="keyword">final</span> String name, @Nullable <span class="keyword">final</span> Class<T> requiredType, @Nullable <span class="keyword">final</span> Object[] args, <span class="keyword">boolean</span> typeCheckOnly)</span> <span class="keyword">throws</span> BeansException </span>{</span><br><span class="line">...</span><br><span class="line"><span class="comment">// Eagerly check singleton cache for manually registered singletons.</span></span><br><span class="line"><span class="comment">// 先去获取一次,如果不为null,此处就会走缓存了</span></span><br><span class="line">Object sharedInstance = getSingleton(beanName);</span><br><span class="line">...</span><br><span class="line"><span class="comment">// 如果不是只检查类型,那就标记这个Bean被创建了,添加到缓存里 也就是所谓的 当前创建Bean池</span></span><br><span class="line"><span class="keyword">if</span> (!typeCheckOnly) {</span><br><span class="line">markBeanAsCreated(beanName);</span><br><span class="line">}</span><br><span class="line">...</span><br><span class="line"><span class="comment">// Create bean instance.</span></span><br><span class="line"><span class="keyword">if</span> (mbd.isSingleton()) {</span><br><span class="line"></span><br><span class="line"><span class="comment">// 这个getSingleton方法不是SingletonBeanRegistry的接口方法 属于实现类DefaultSingletonBeanRegistry的一个public重载方法</span></span><br><span class="line"><span class="comment">// 它的特点是在执行singletonFactory.getObject();前后会执行beforeSingletonCreation(beanName);和afterSingletonCreation(beanName); </span></span><br><span class="line"><span class="comment">// 也就是保证这个Bean在创建过程中,放入正在创建的缓存池里 可以看到它实际创建bean调用的是我们的createBean方法</span></span><br><span class="line">sharedInstance = getSingleton(beanName, () -> {</span><br><span class="line"><span class="keyword">try</span> {</span><br><span class="line"><span class="keyword">return</span> createBean(beanName, mbd, args);</span><br><span class="line">} <span class="keyword">catch</span> (BeansException ex) {</span><br><span class="line">destroySingleton(beanName);</span><br><span class="line"><span class="keyword">throw</span> ex;</span><br><span class="line">}</span><br><span class="line">});</span><br><span class="line">bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);</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 class="comment">// 抽象方法createBean所在地 这个接口方法是属于抽象父类AbstractBeanFactory的 实现在这个抽象类里</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">AbstractAutowireCapableBeanFactory</span> <span class="keyword">extends</span> <span class="title">AbstractBeanFactory</span> <span class="keyword">implements</span> <span class="title">AutowireCapableBeanFactory</span> </span>{</span><br><span class="line">...</span><br><span class="line"><span class="function"><span class="keyword">protected</span> Object <span class="title">doCreateBean</span><span class="params">(<span class="keyword">final</span> String beanName, <span class="keyword">final</span> RootBeanDefinition mbd, <span class="keyword">final</span> @Nullable Object[] args)</span> <span class="keyword">throws</span> BeanCreationException </span>{</span><br><span class="line">...</span><br><span class="line"><span class="comment">// 创建Bean对象,并且将对象包裹在BeanWrapper 中</span></span><br><span class="line">instanceWrapper = createBeanInstance(beanName, mbd, args);</span><br><span class="line"><span class="comment">// 再从Wrapper中把Bean原始对象(非代理!) 这个时候这个Bean就有地址值了,就能被引用了</span></span><br><span class="line"><span class="comment">// 注意:此处是原始对象,这点非常的重要</span></span><br><span class="line"><span class="keyword">final</span> Object bean = instanceWrapper.getWrappedInstance();</span><br><span class="line">...</span><br><span class="line"><span class="comment">// earlySingletonExposure 用于表示是否”提前暴露“原始对象的引用,用于解决循环依赖。</span></span><br><span class="line"><span class="comment">// 对于单例Bean,该变量一般为 true 但你也可以通过属性allowCircularReferences = false来关闭循环引用</span></span><br><span class="line"><span class="comment">// isSingletonCurrentlyInCreation(beanName) 表示当前bean必须在创建中才行</span></span><br><span class="line"><span class="keyword">boolean</span> earlySingletonExposure = (mbd.isSingleton() && <span class="keyword">this</span>.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));</span><br><span class="line"><span class="keyword">if</span> (earlySingletonExposure) {</span><br><span class="line"><span class="keyword">if</span> (logger.isTraceEnabled()) {</span><br><span class="line">logger.trace(<span class="string">"Eagerly caching bean '"</span> + beanName + <span class="string">"' to allow for resolving potential circular references"</span>);</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 上面讲过调用此方法放进一个ObjectFactory,二级缓存会对应删除的</span></span><br><span class="line"><span class="comment">// getEarlyBeanReference的作用:调用SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference()这个方法 否则啥都不做</span></span><br><span class="line"><span class="comment">// 也就是给调用者个机会,自己去实现暴露这个bean的应用的逻辑</span></span><br><span class="line"><span class="comment">// 比如在getEarlyBeanReference()里可以实现AOP的逻辑 参考自动代理创建器AbstractAutoProxyCreator 实现了这个方法来创建代理对象</span></span><br><span class="line"><span class="comment">// 若不需要执行AOP的逻辑,直接返回Bean</span></span><br><span class="line">addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));</span><br><span class="line">}</span><br><span class="line">Object exposedObject = bean; <span class="comment">//exposedObject 是最终返回的对象</span></span><br><span class="line">...</span><br><span class="line"><span class="comment">// 填充属于,解决@Autowired依赖</span></span><br><span class="line">populateBean(beanName, mbd, instanceWrapper);</span><br><span class="line"><span class="comment">// 执行初始化回调方法们</span></span><br><span class="line">exposedObject = initializeBean(beanName, exposedObject, mbd);</span><br><span class="line"></span><br><span class="line"><span class="comment">// earlySingletonExposure:如果你的bean允许被早期暴露出去 也就是说可以被循环引用 那这里就会进行检查</span></span><br><span class="line"><span class="comment">// 此段代码非常重要,但大多数人都忽略了它</span></span><br><span class="line"><span class="keyword">if</span> (earlySingletonExposure) {</span><br><span class="line"><span class="comment">// 此时一级缓存肯定还没数据,但是呢此时候二级缓存earlySingletonObjects也没数据</span></span><br><span class="line"><span class="comment">//注意,注意:第二参数为false 表示不会再去三级缓存里查了</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 此处非常巧妙的一点:::因为上面各式各样的实例化、初始化的后置处理器都执行了,如果你在上面执行了这一句</span></span><br><span class="line"><span class="comment">// ((ConfigurableListableBeanFactory)this.beanFactory).registerSingleton(beanName, bean);</span></span><br><span class="line"><span class="comment">// 那么此处得到的earlySingletonReference 的引用最终会是你手动放进去的Bean最终返回,完美的实现了"偷天换日" 特别适合中间件的设计</span></span><br><span class="line"><span class="comment">// 我们知道,执行完此doCreateBean后执行addSingleton() 其实就是把自己再添加一次 **再一次强调,完美实现偷天换日**</span></span><br><span class="line">Object earlySingletonReference = getSingleton(beanName, <span class="keyword">false</span>);</span><br><span class="line"><span class="keyword">if</span> (earlySingletonReference != <span class="keyword">null</span>) {</span><br><span class="line"></span><br><span class="line"><span class="comment">// 这个意思是如果经过了initializeBean()后,exposedObject还是木有变,那就可以大胆放心的返回了</span></span><br><span class="line"><span class="comment">// initializeBean会调用后置处理器,这个时候可以生成一个代理对象,那这个时候它哥俩就不会相等了 走else去判断吧</span></span><br><span class="line"><span class="keyword">if</span> (exposedObject == bean) {</span><br><span class="line">exposedObject = earlySingletonReference;</span><br><span class="line">} </span><br><span class="line"></span><br><span class="line"><span class="comment">// allowRawInjectionDespiteWrapping这个值默认是false</span></span><br><span class="line"><span class="comment">// hasDependentBean:若它有依赖的bean 那就需要继续校验了(若没有依赖的 就放过它)</span></span><br><span class="line"><span class="keyword">else</span> <span class="keyword">if</span> (!<span class="keyword">this</span>.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {</span><br><span class="line"><span class="comment">// 拿到它所依赖的Bean们 下面会遍历一个一个的去看</span></span><br><span class="line">String[] dependentBeans = getDependentBeans(beanName);</span><br><span class="line">Set<String> actualDependentBeans = <span class="keyword">new</span> LinkedHashSet<>(dependentBeans.length);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 一个个检查它所以Bean</span></span><br><span class="line"><span class="comment">// removeSingletonIfCreatedForTypeCheckOnly这个放见下面 在AbstractBeanFactory里面</span></span><br><span class="line"><span class="comment">// 简单的说,它如果判断到该dependentBean并没有在创建中的了的情况下,那就把它从所有缓存中移除 并且返回true</span></span><br><span class="line"><span class="comment">// 否则(比如确实在创建中) 那就返回false 进入我们的if里面 表示所谓的真正依赖</span></span><br><span class="line"><span class="comment">//(解释:就是真的需要依赖它先实例化,才能实例化自己的依赖)</span></span><br><span class="line"><span class="keyword">for</span> (String dependentBean : dependentBeans) {</span><br><span class="line"><span class="keyword">if</span> (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {</span><br><span class="line">actualDependentBeans.add(dependentBean);</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 若存在真正依赖,那就报错(不要等到内存移除你才报错,那是非常不友好的</span></span><br><span class="line"><span class="comment">// 这个异常是BeanCurrentlyInCreationException,报错日志也稍微留意一下,方便定位错误</span></span><br><span class="line"><span class="keyword">if</span> (!actualDependentBeans.isEmpty()) {</span><br><span class="line"><span class="keyword">throw</span> <span class="keyword">new</span> BeanCurrentlyInCreationException(beanName,</span><br><span class="line"><span class="string">"Bean with name '"</span> + beanName + <span class="string">"' has been injected into other beans ["</span> +</span><br><span class="line">StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +</span><br><span class="line"><span class="string">"] in its raw version as part of a circular reference, but has eventually been "</span> +</span><br><span class="line"><span class="string">"wrapped. This means that said other beans do not use the final version of the "</span> +</span><br><span class="line"><span class="string">"bean. This is often the result of over-eager type matching - consider using "</span> +</span><br><span class="line"><span class="string">"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example."</span>);</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 class="keyword">return</span> exposedObject;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 虽然是remove方法 但是它的返回值也非常重要</span></span><br><span class="line"><span class="comment">// 该方法唯一调用的地方就是循环依赖的最后检查处</span></span><br><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">boolean</span> <span class="title">removeSingletonIfCreatedForTypeCheckOnly</span><span class="params">(String beanName)</span> </span>{</span><br><span class="line"><span class="comment">// 如果这个bean不在创建中 比如是ForTypeCheckOnly的 那就移除掉</span></span><br><span class="line"><span class="keyword">if</span> (!<span class="keyword">this</span>.alreadyCreated.contains(beanName)) {</span><br><span class="line">removeSingleton(beanName);</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">else</span> {</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">false</span>;</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>这里举例:例如是<code>field</code>属性依赖注入,在<code>populateBean</code>时它就会先去完成它所依赖注入的那个bean的实例化、初始化过程,最终返回到本流程继续处理,<strong>因此Spring这样处理是不存在任何问题的。</strong><br>这里有个小细节:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (exposedObject == bean) {</span><br><span class="line">exposedObject = earlySingletonReference;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这一句如果<code>exposedObject == bean</code>表示最终返回的对象就是原始对象,说明在<code>populateBean</code>和<code>initializeBean</code>没对他代理过,那就啥话都不说了<code>exposedObject = earlySingletonReference</code>,最终把二级缓存里的引用返回即可。</p><h2 id="流程总结(非常重要)"><a href="#流程总结(非常重要)" class="headerlink" title="流程总结(非常重要)"></a>流程总结(<code>非常重要</code>)</h2><p>此处以如上的A、B类的互相依赖注入为例,在这里表达出<strong>关键代码</strong>的走势:<br>1、入口处即是<strong>实例化、初始化A这个单例Bean</strong>。<code>AbstractBeanFactory.doGetBean("a")</code></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">protected</span> <T> <span class="function">T <span class="title">doGetBean</span><span class="params">(...)</span></span>{</span><br><span class="line">... </span><br><span class="line"><span class="comment">// 标记beanName a是已经创建过至少一次的 它会一直存留在缓存里不会被移除(除非抛出了异常)</span></span><br><span class="line"><span class="comment">// 参见缓存Set<String> alreadyCreated = Collections.newSetFromMap(new ConcurrentHashMap<>(256))</span></span><br><span class="line"><span class="keyword">if</span> (!typeCheckOnly) {</span><br><span class="line">markBeanAsCreated(beanName);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 此时a不存在任何一级缓存中,且不是在创建中 所以此处返回null</span></span><br><span class="line"><span class="comment">// 此处若不为null,然后从缓存里拿就可以了(主要处理FactoryBean和BeanFactory情况吧)</span></span><br><span class="line">Object beanInstance = getSingleton(beanName, <span class="keyword">false</span>);</span><br><span class="line">...</span><br><span class="line"><span class="comment">// 这个getSingleton方法非常关键。</span></span><br><span class="line"><span class="comment">//1、标注a正在创建中;</span></span><br><span class="line"><span class="comment">//2、调用singletonObject = singletonFactory.getObject();(实际上调用的是createBean()方法) 因此这一步最为关键;</span></span><br><span class="line"><span class="comment">//3、此时实例已经创建完成 会把a移除整整创建的缓存中;</span></span><br><span class="line"><span class="comment">//4、执行addSingleton()添加进去。(备注:注册bean的接口方法为registerSingleton,它依赖于addSingleton方法)。</span></span><br><span class="line">sharedInstance = getSingleton(beanName, () -> { ... <span class="keyword">return</span> createBean(beanName, mbd, args); });</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>2、下面进入到最为复杂的<code>AbstractAutowireCapableBeanFactory.createBean/doCreateBean()</code>环节,创建A的实例</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> Object <span class="title">doCreateBean</span><span class="params">()</span></span>{</span><br><span class="line">...</span><br><span class="line"><span class="comment">// 使用构造器/工厂方法 instanceWrapper是一个BeanWrapper</span></span><br><span class="line">instanceWrapper = createBeanInstance(beanName, mbd, args);</span><br><span class="line"><span class="comment">// 此处bean为"原始Bean" 也就是这里的A实例对象:A@1234</span></span><br><span class="line"><span class="keyword">final</span> Object bean = instanceWrapper.getWrappedInstance();</span><br><span class="line">...</span><br><span class="line"><span class="comment">// 是否要提前暴露(允许循环依赖) 现在此处A是被允许的</span></span><br><span class="line"><span class="keyword">boolean</span> earlySingletonExposure = (mbd.isSingleton() && <span class="keyword">this</span>.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));</span><br><span class="line"></span><br><span class="line"><span class="comment">// 允许暴露,就把A绑定在ObjectFactory上,注册到三级缓存`singletonFactories`里面去保存着</span></span><br><span class="line"><span class="comment">// Tips:这里后置处理器的getEarlyBeanReference方法会被促发,自动代理创建器在此处创建代理对象(注意执行时机 为执行三级缓存的时候)</span></span><br><span class="line"><span class="keyword">if</span> (earlySingletonExposure) {</span><br><span class="line">addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));</span><br><span class="line">}</span><br><span class="line">...</span><br><span class="line"><span class="comment">// exposedObject 为最终返回的对象,此处为原始对象bean也就是A@1234,下面会有用处</span></span><br><span class="line">Object exposedObject = bean;</span><br><span class="line"><span class="comment">// 给A@1234属性完成赋值,@Autowired在此处起作用</span></span><br><span class="line"><span class="comment">// 因此此处会调用getBean("b"),so 会重复上面步骤创建B类的实例</span></span><br><span class="line"><span class="comment">// 此处我们假设B已经创建好了 为B@5678</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 需要注意的是在populateBean("b")的时候依赖有beanA,所以此时候调用getBean("a")最终会调用getSingleton("a"),</span></span><br><span class="line"><span class="comment">//此时候上面说到的getEarlyBeanReference方法就会被执行。这也解释为何我们@Autowired是个代理对象,而不是普通对象的根本原因</span></span><br><span class="line"></span><br><span class="line">populateBean(beanName, mbd, instanceWrapper);</span><br><span class="line"><span class="comment">// 实例化。这里会执行后置处理器BeanPostProcessor的两个方法</span></span><br><span class="line"><span class="comment">// 此处注意:postProcessAfterInitialization()是有可能返回一个代理对象的,这样exposedObject 就不再是原始对象了 特备注意哦</span></span><br><span class="line"><span class="comment">// 比如处理@Aysnc的AsyncAnnotationBeanPostProcessor它就是在这个时间里生成代理对象的(有坑,请小心使用@Aysnc)</span></span><br><span class="line">exposedObject = initializeBean(beanName, exposedObject, mbd);</span><br><span class="line"></span><br><span class="line">... <span class="comment">// 至此,相当于A@1234已经实例化完成、初始化完成(属性也全部赋值了)</span></span><br><span class="line"><span class="comment">// 这一步我把它理解为校验:校验:校验是否有循环引用问题</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (earlySingletonExposure) {</span><br><span class="line"><span class="comment">// 注意此处第二个参数传的false,表示不去三级缓存里singletonFactories再去调用一次getObject()方法了</span></span><br><span class="line"><span class="comment">// 上面建讲到了由于B在初始化的时候,会触发A的ObjectFactory.getObject() 所以a此处已经在二级缓存earlySingletonObjects里了</span></span><br><span class="line"><span class="comment">// 因此此处返回A的实例:A@1234</span></span><br><span class="line">Object earlySingletonReference = getSingleton(beanName, <span class="keyword">false</span>);</span><br><span class="line"><span class="keyword">if</span> (earlySingletonReference != <span class="keyword">null</span>) {</span><br><span class="line"></span><br><span class="line"><span class="comment">// 这个等式表示,exposedObject若没有再被代理过,这里就是相等的</span></span><br><span class="line"><span class="comment">// 显然此处我们的a对象的exposedObject它是没有被代理过的 所以if会进去</span></span><br><span class="line"><span class="comment">// 这种情况至此,就全部结束了</span></span><br><span class="line"><span class="keyword">if</span> (exposedObject == bean) {</span><br><span class="line">exposedObject = earlySingletonReference;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 继续以A为例,比如方法标注了@Aysnc注解,exposedObject此时候就是一个代理对象,因此就会进到这里来</span></span><br><span class="line"><span class="comment">//hasDependentBean(beanName)是肯定为true,因为getDependentBeans(beanName)得到的是["b"]这个依赖</span></span><br><span class="line"><span class="keyword">else</span> <span class="keyword">if</span> (!<span class="keyword">this</span>.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {</span><br><span class="line">String[] dependentBeans = getDependentBeans(beanName);</span><br><span class="line">Set<String> actualDependentBeans = <span class="keyword">new</span> LinkedHashSet<>(dependentBeans.length);</span><br><span class="line"></span><br><span class="line"><span class="comment">// A@1234依赖的是["b"],所以此处去检查b</span></span><br><span class="line"><span class="comment">// 如果最终存在实际依赖的bean:actualDependentBeans不为空 那就抛出异常 证明循环引用了</span></span><br><span class="line"><span class="keyword">for</span> (String dependentBean : dependentBeans) {</span><br><span class="line"><span class="comment">// 这个判断原则是:如果此时候b并还没有创建好,this.alreadyCreated.contains(beanName)=true表示此bean已经被创建过,就返回false</span></span><br><span class="line"><span class="comment">// 若该bean没有在alreadyCreated缓存里,就是说没被创建过(其实只有CreatedForTypeCheckOnly才会是此仓库)</span></span><br><span class="line"><span class="keyword">if</span> (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {</span><br><span class="line">actualDependentBeans.add(dependentBean);</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> (!actualDependentBeans.isEmpty()) {</span><br><span class="line"><span class="keyword">throw</span> <span class="keyword">new</span> BeanCurrentlyInCreationException(beanName,</span><br><span class="line"><span class="string">"Bean with name '"</span> + beanName + <span class="string">"' has been injected into other beans ["</span> +</span><br><span class="line">StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +</span><br><span class="line"><span class="string">"] in its raw version as part of a circular reference, but has eventually been "</span> +</span><br><span class="line"><span class="string">"wrapped. This means that said other beans do not use the final version of the "</span> +</span><br><span class="line"><span class="string">"bean. This is often the result of over-eager type matching - consider using "</span> +</span><br><span class="line"><span class="string">"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example."</span>);</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>由于关键代码部分的步骤不太好拆分,为了更具象表达,那么使用下面一副图示帮助小伙伴们理解:<br><img data-src="/images/pasted-53.png" alt="upload successful"></p><p><strong>最后的最后,由于我太暖心了_,再来个纯文字版的总结。</strong><br> 依旧以上面<code>A</code>、<code>B</code>类使用属性<code>field</code>注入循环依赖的例子为例,对整个流程做文字步骤总结如下:</p><ol><li>使用<code>context.getBean(A.class)</code>,旨在获取容器内的单例A(若A不存在,就会走A这个Bean的创建流程),显然初次获取A是不存在的,因此走<strong>A的创建之路</strong></li><li><code>实例化</code>A(注意此处仅仅是实例化),并将它放进<code>缓存</code>(此时A已经实例化完成,已经可以被引用了)</li><li><code>初始化</code>A:<code>@Autowired</code>依赖注入B(此时需要去容器内获取B)</li><li>为了完成依赖注入B,会通过<code>getBean(B)</code>去容器内找B。但此时B在容器内不存在,就走向<strong>B的创建之路</strong></li><li><code>实例化</code>B,并将其放入缓存。(此时B也能够被引用了)</li><li><code>初始化</code>B,<code>@Autowired</code>依赖注入A(此时需要去容器内获取A)</li><li><code>此处重要</code>:初始化B时会调用<code>getBean(A)</code>去容器内找到A,上面我们已经说过了此时候因为A已经实例化完成了并且放进了缓存里,所以这个时候去看缓存里是已经存在A的引用了的,所以<code>getBean(A)</code>能够正常返回</li><li><strong>B初始化成功</strong>(此时已经注入A成功了,已成功持有A的引用了),return(注意此处return相当于是返回最上面的<code>getBean(B)</code>这句代码,回到了初始化A的流程中)。</li><li>因为B实例已经成功返回了,因此最终<strong>A也初始化成功</strong></li><li><strong>到此,B持有的已经是初始化完成的A,A持有的也是初始化完成的B,完美</strong></li></ol><p>站的角度高一点,宏观上看Spring处理循环依赖的整个流程就是如此。希望这个宏观层面的总结能更加有助于小伙伴们对Spring解决循环依赖的原理的了解,<strong>同时也顺便能解释为何构造器循环依赖就不好使的原因。</strong></p><h2 id="循环依赖对AOP代理对象创建流程和结果的影响"><a href="#循环依赖对AOP代理对象创建流程和结果的影响" class="headerlink" title="循环依赖对AOP代理对象创建流程和结果的影响"></a>循环依赖对AOP代理对象创建<code>流程和结果</code>的影响</h2><p>我们都知道<strong>Spring AOP、事务</strong>等都是通过代理对象来实现的,而<strong>事务</strong>的代理对象是由自动代理创建器来自动完成的。也就是说Spring最终给我们放进容器里面的是一个代理对象,<strong>而非原始对象</strong>。<br>本文结合<code>循环依赖</code>,回头再看AOP代理对象的创建过程,和最终放进容器内的动作,非常有意思。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">HelloServiceImpl</span> <span class="keyword">implements</span> <span class="title">HelloService</span> </span>{</span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> HelloService helloService;</span><br><span class="line"> </span><br><span class="line"> <span class="meta">@Transactional</span></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> Object <span class="title">hello</span><span class="params">(Integer id)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"service hello"</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>此<code>Service</code>类使用到了事务,所以最终会生成一个JDK动态代理对象<code>Proxy</code>。刚好它又存在<code>自己引用自己</code>的循环依赖。看看这个Bean的创建概要描述如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> Object <span class="title">doCreateBean</span><span class="params">( ... )</span></span>{</span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="comment">// 这段告诉我们:如果允许循环依赖的话,此处会添加一个ObjectFactory到三级缓存里面,以备创建对象并且提前暴露引用</span></span><br><span class="line"><span class="comment">// 此处Tips:getEarlyBeanReference是后置处理器SmartInstantiationAwareBeanPostProcessor的一个方法,它的功效为:</span></span><br><span class="line"><span class="comment">// 保证自己被循环依赖的时候,即使被别的Bean @Autowire进去的也是代理对象 AOP自动代理创建器此方法里会创建的代理对象</span></span><br><span class="line"><span class="comment">// Eagerly cache singletons to be able to resolve circular references</span></span><br><span class="line"><span class="comment">// even when triggered by lifecycle interfaces like BeanFactoryAware.</span></span><br><span class="line"><span class="keyword">boolean</span> earlySingletonExposure = (mbd.isSingleton() && <span class="keyword">this</span>.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));</span><br><span class="line"><span class="keyword">if</span> (earlySingletonExposure) { <span class="comment">// 需要提前暴露(支持循环依赖),就注册一个ObjectFactory到三级缓存</span></span><br><span class="line">addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 此处注意:如果此处自己被循环依赖了 那它会走上面的getEarlyBeanReference,从而创建一个代理对象从三级缓存转移到二级缓存里</span></span><br><span class="line"><span class="comment">// 注意此时候对象还在二级缓存里,并没有在一级缓存。并且此时可以知道exposedObject仍旧是原始对象</span></span><br><span class="line">populateBean(beanName, mbd, instanceWrapper);</span><br><span class="line">exposedObject = initializeBean(beanName, exposedObject, mbd);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 经过这两大步后,exposedObject还是原始对象(注意此处以事务的AOP为例子的,</span></span><br><span class="line"><span class="comment">// 因为事务的AOP自动代理创建器在getEarlyBeanReference创建代理后,initializeBean就不会再重复创建了,二选一的,下面会有描述)</span></span><br><span class="line"></span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="comment">// 循环依赖校验(非常重要)</span></span><br><span class="line"><span class="keyword">if</span> (earlySingletonExposure) {</span><br><span class="line"><span class="comment">// 前面说了因为自己被循环依赖了,所以此时候代理对象还在二级缓存里(备注:本利讲解的是自己被循环依赖了的情况)</span></span><br><span class="line"><span class="comment">// so,此处getSingleton,就会把里面的对象拿出来,我们知道此时候它已经是个Proxy代理对象</span></span><br><span class="line"><span class="comment">// 最后赋值给exposedObject 然后return出去,进而最终被addSingleton()添加进一级缓存里面去 </span></span><br><span class="line"><span class="comment">// 这样就保证了我们容器里**最终实际上是代理对象**,而非原始对象</span></span><br><span class="line">Object earlySingletonReference = getSingleton(beanName, <span class="keyword">false</span>);</span><br><span class="line"><span class="keyword">if</span> (earlySingletonReference != <span class="keyword">null</span>) {</span><br><span class="line"><span class="keyword">if</span> (exposedObject == bean) { <span class="comment">// 这个判断不可少(因为如果initializeBean改变了exposedObject ,就不能这么玩了,否则就是两个对象了)</span></span><br><span class="line">exposedObject = earlySingletonReference;</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>上演示的是<code>代理对象+自己存在循环依赖</code>的case:Spring用三级缓存很巧妙的进行解决了。若是这种case:代理对象,但是自己并<strong>不存在循环依赖</strong>,过程稍微有点不一样儿了,如下描述:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> Object <span class="title">doCreateBean</span><span class="params">( ... )</span> </span>{</span><br><span class="line">...</span><br><span class="line"><span class="comment">// 这些语句依旧会执行,三级缓存里是会加入的 表示它支持被循环引用嘛</span></span><br><span class="line">addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));</span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="comment">// 此处注意,因为它没有被其它Bean循环引用(注意是循环引用,而不是直接引用),所以上面getEarlyBeanReference不会执行</span></span><br><span class="line"><span class="comment">// 也就是说此时二级缓存里并不会存在它 知晓这点特别的重要</span></span><br><span class="line">populateBean(beanName, mbd, instanceWrapper);</span><br><span class="line"><span class="comment">// 重点在这:AnnotationAwareAspectJAutoProxyCreator自动代理创建器此处的postProcessAfterInitialization方法里,会给创建一个代理对象返回</span></span><br><span class="line"><span class="comment">// 所以此部分执行完成后,exposedObject **已经是个代理对象**而不再是个原始对象了 此时二级缓存里依旧无它,更别提一级缓存了</span></span><br><span class="line">exposedObject = initializeBean(beanName, exposedObject, mbd);</span><br><span class="line"></span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="comment">// 循环依赖校验</span></span><br><span class="line"><span class="keyword">if</span> (earlySingletonExposure) {</span><br><span class="line"><span class="comment">// 前面说了一级、二级缓存里都木有它,然后这里传的又是false(表示不看三级缓存)</span></span><br><span class="line"><span class="comment">// 所以毋庸置疑earlySingletonReference = null so下面的逻辑就不用看了,直接return出去</span></span><br><span class="line"><span class="comment">// 然后执行addSingleton()方法,由此可知 容器里最终存在的也还是代理对象</span></span><br><span class="line">Object earlySingletonReference = getSingleton(beanName, <span class="keyword">false</span>);</span><br><span class="line"><span class="keyword">if</span> (earlySingletonReference != <span class="keyword">null</span>) {</span><br><span class="line"><span class="keyword">if</span> (exposedObject == bean) { <span class="comment">// 这个判断不可少(因为如果initializeBean改变了exposedObject ,就不能这么玩了,否则就是两个对象了)</span></span><br><span class="line">exposedObject = earlySingletonReference;</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>分析可知,即使自己只需要代理,并不被循环引用,最终存在Spring容器里的<strong>仍旧是</strong>代理对象。(so此时别人直接<code>@Autowired</code>进去的也是代理对象呀)<br><strong>终极case:如果我关闭Spring容器的循环依赖能力,也就是把<code>allowCircularReferences</code>设值为false,那么会不会造成什么问题呢?</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 它用于关闭循环引用(关闭后只要有循环引用现象就直接报错)</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MyBeanFactoryPostProcessor</span> <span class="keyword">implements</span> <span class="title">BeanFactoryPostProcessor</span> </span>{</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">postProcessBeanFactory</span><span class="params">(ConfigurableListableBeanFactory beanFactory)</span> <span class="keyword">throws</span> BeansException </span>{</span><br><span class="line"> ((AbstractAutowireCapableBeanFactory) beanFactory).setAllowCircularReferences(<span class="keyword">false</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>若关闭了循环依赖后,还存在上面A、B的循环依赖现象,启动便会报错如下:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name <span class="string">'a'</span>: Requested bean is currently <span class="keyword">in</span> creation: Is there an unresolvable circular reference?</span><br><span class="line">at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:339)</span><br><span class="line">at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:215)</span><br></pre></td></tr></table></figure><blockquote><p>注意此处异常类型也是<code>BeanCurrentlyInCreationException</code>异常,但是文案内容和上面强调的有所区别,它报错位置在:<code>DefaultSingletonBeanRegistry.beforeSingletonCreation</code>这个位置。</p></blockquote><p><code>报错浅析</code>:在实例化A后给其属性赋值时,会去实例化B。B实例化完成后会继续给B属性赋值,这时由于此时我们<code>关闭了循环依赖</code>,所以不存在<code>提前暴露</code>引用这么一说来给实用。因此B无法直接拿到A的引用地址,因此只能又去创建A的实例。<strong>而此时我们知道A其实已经正在创建中了</strong>,不能再创建了。so,就报错了。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">HelloServiceImpl</span> <span class="keyword">implements</span> <span class="title">HelloService</span> </span>{</span><br><span class="line"></span><br><span class="line"><span class="comment">// 因为管理了循环依赖,所以此处不能再依赖自己的</span></span><br><span class="line"><span class="comment">// 但是:我们的此bean还是需要AOP代理的</span></span><br><span class="line"> <span class="comment">//@Autowired</span></span><br><span class="line"> <span class="comment">//private HelloService helloService;</span></span><br><span class="line"> </span><br><span class="line"> <span class="meta">@Transactional</span></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> Object <span class="title">hello</span><span class="params">(Integer id)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"service hello"</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样它的大致运行如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> Object <span class="title">doCreateBean</span><span class="params">( ... )</span> </span>{</span><br><span class="line"><span class="comment">// 毫无疑问此时候earlySingletonExposure = false 也就是Bean都不会提前暴露引用了 显然就不能被循环依赖了。</span></span><br><span class="line"><span class="keyword">boolean</span> earlySingletonExposure = (mbd.isSingleton() && <span class="keyword">this</span>.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));</span><br><span class="line">...</span><br><span class="line">populateBean(beanName, mbd, instanceWrapper);</span><br><span class="line"><span class="comment">// 若是事务的AOP 在这里会为源生Bean创建代理对象(因为上面没有提前暴露这个代理)</span></span><br><span class="line">exposedObject = initializeBean(beanName, exposedObject, mbd);</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (earlySingletonExposure) {</span><br><span class="line">... 这里更不用说,因为earlySingletonExposure=<span class="keyword">false</span> 所以上面的代理对象exposedObject 直接<span class="keyword">return</span>了。</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看到即使把这个开关给关了,最终放进容器了的仍旧是代理对象,显然<code>@Autowired</code>给属性赋值的也一定是代理对象。<br>最后,以<code>AbstractAutoProxyCreator</code>为例看看自动代理创建器是怎么配合实现:循环依赖+创建代理</p><blockquote><p><code>AbstractAutoProxyCreator</code>是抽象类,它的三大实现子类<code>InfrastructureAdvisorAutoProxyCreator</code>、<code>AspectJAwareAdvisorAutoProxyCreator</code>、<code>AnnotationAwareAspectJAutoProxyCreator</code>小伙伴们应该会更加的熟悉些</p></blockquote><p>该抽象类实现了创建代理的动作:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// @since 13.10.2003 它实现代理创建的方法有如下两个</span></span><br><span class="line"><span class="comment">// 实现了SmartInstantiationAwareBeanPostProcessor 所以有方法getEarlyBeanReference来只能的解决循环引用问题:提前把代理对象暴露出去。</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">AbstractAutoProxyCreator</span> <span class="keyword">extends</span> <span class="title">ProxyProcessorSupport</span> <span class="keyword">implements</span> <span class="title">SmartInstantiationAwareBeanPostProcessor</span>, <span class="title">BeanFactoryAware</span> </span>{</span><br><span class="line">...</span><br><span class="line"><span class="comment">// 下面两个方法是自动代理创建器创建代理对象的唯二的两个节点。</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 提前暴露代理对象的引用 它肯定在postProcessAfterInitialization之前执行</span></span><br><span class="line"><span class="comment">// 所以它并不需要判断啥的 创建好后放进缓存earlyProxyReferences里 注意此处value是原始Bean</span></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> Object <span class="title">getEarlyBeanReference</span><span class="params">(Object bean, String beanName)</span> </span>{</span><br><span class="line">Object cacheKey = getCacheKey(bean.getClass(), beanName);</span><br><span class="line"><span class="keyword">this</span>.earlyProxyReferences.put(cacheKey, bean);</span><br><span class="line"><span class="keyword">return</span> wrapIfNecessary(bean, beanName, cacheKey);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 因为它会在getEarlyBeanReference之后执行,所以此处的重要逻辑是下面的判断</span></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> Object <span class="title">postProcessAfterInitialization</span><span class="params">(@Nullable Object bean, String beanName)</span> </span>{</span><br><span class="line"><span class="keyword">if</span> (bean != <span class="keyword">null</span>) {</span><br><span class="line">Object cacheKey = getCacheKey(bean.getClass(), beanName);</span><br><span class="line"><span class="comment">// remove方法返回被移除的value,上面说了它记录的是原始bean</span></span><br><span class="line"><span class="comment">// 若被循环引用了,那就是执行了上面的`getEarlyBeanReference`方法,所以此时remove返回值肯定是==bean的(注意此时方法入参的bean还是原始对象)</span></span><br><span class="line"><span class="comment">// 若没有被循环引用,getEarlyBeanReference()不执行 所以remove方法返回null,所以就进入if执行此处的创建代理对象方法</span></span><br><span class="line"><span class="keyword">if</span> (<span class="keyword">this</span>.earlyProxyReferences.remove(cacheKey) != bean) {</span><br><span class="line"><span class="keyword">return</span> wrapIfNecessary(bean, beanName, cacheKey);</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> bean;</span><br><span class="line">}</span><br><span class="line">...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>由上可知,自动代理创建器它保证了代理对象只会被创建一次,而且支持循环依赖的自动注入的依旧是代理对象。<br>**<code>上面分析了三种case,现给出结论如下:</code>**<br>不管是自己被循环依赖了还是没有,<strong>甚至是把Spring容器的循环依赖给关了</strong>,它对AOP代理的创建流程有影响,<strong>但对结果是无影响的。</strong><br>也就是说Spring很好的对调用者屏蔽了这些实现细节,使得使用者使用起来完全的无感知。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>解决此类问题的关键是要对<code>SpringIOC</code>和<code>DI</code>的整个流程做到心中有数,要理解好本文章,建议有【相关阅读】里文章的大量知识的铺垫,同时呢本文又能<strong>进一步</strong>的帮助小伙伴理解到Spring Bean的实例化、初始化流程。<br>本文还是花了我一番心思的,个人觉得对Spring这部分的处理流程描述得还是比较详细的,希望我的总结能够给大家带来帮助。<br><strong>另外为了避免循环依赖导致启动问题而又不会解决,有如下建议:</strong></p><ol><li><code>业务代码中</code>尽量不要使用构造器注入,即使它有很多优点。</li><li><code>业务代码中</code>为了简洁,尽量使用field注入而非setter方法注入</li><li>若你注入的同时,立马需要处理一些逻辑(一般见于框架设计中,业务代码中不太可能出现),可以使用setter方法注入辅助完成</li></ol><blockquote><p><strong>作者:</strong><a href="https://cloud.tencent.com/developer/user/6158873" target="_blank" rel="noopener">YourBatman</a><br><strong>原文:</strong><a href="https://cloud.tencent.com/developer/article/1497692" target="_blank" rel="noopener">https://cloud.tencent.com/developer/article/1497692</a></p></blockquote>]]></content>
<summary type="html">
<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p><strong>循环依赖:就是N个类循环(嵌套)引用</strong>。通俗的讲就是N个Bean互相引用对方,最终形成<code>闭环</code>。用一副经典的图示可以表示成这样(A、B、C都代表对象,虚线代表引用关系):<br><img data-src="/images/pasted-50.png" alt="upload successful"></p>
<blockquote>
<p>注意:其实可以N=1,也就是极限情况的循环依赖:<code>自己依赖自己</code><br>另需注意:这里指的循环引用不是方法之间的循环调用,<strong>而是对象的相互依赖关系</strong>。(方法之间循环调用若有出口也是能够正常work的)</p>
</blockquote>
<p>可以设想一下这个场景:如果在日常开发中我们用new对象的方式,若构造函数之间发生这种<strong>循环依赖</strong>的话,程序会在运行时一直循环调用<strong>最终导致内存溢出</strong>,示例代码如下:</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Main</span> </span>&#123;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line"> System.out.println(<span class="keyword">new</span> A());</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">A</span> </span>&#123;</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">A</span><span class="params">()</span> </span>&#123;</span><br><span class="line"> <span class="keyword">new</span> B();</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">B</span> </span>&#123;</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">B</span><span class="params">()</span> </span>&#123;</span><br><span class="line"> <span class="keyword">new</span> A();</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
</summary>
<category term="源码分析" scheme="http://zhangfei.men/categories/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/"/>
<category term="Spring" scheme="http://zhangfei.men/categories/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/Spring/"/>
<category term="Java" scheme="http://zhangfei.men/tags/Java/"/>
<category term="Spring" scheme="http://zhangfei.men/tags/Spring/"/>
</entry>
<entry>
<title>Java线上CPU占用过高问题排查思路</title>
<link href="http://zhangfei.men/posts/36f079e3/"/>
<id>http://zhangfei.men/posts/36f079e3/</id>
<published>2019-10-31T09:09:00.000Z</published>
<updated>2020-07-28T01:08:16.453Z</updated>
<content type="html"><![CDATA[<h4 id="一、根据-Java-进程-ID,用-ps-或-top-命令查询出-CPU-占用率高的线程"><a href="#一、根据-Java-进程-ID,用-ps-或-top-命令查询出-CPU-占用率高的线程" class="headerlink" title="一、根据 Java 进程 ID,用 ps 或 top 命令查询出 CPU 占用率高的线程"></a>一、根据 Java 进程 ID,用 <code>ps</code> 或 <code>top</code> 命令查询出 CPU 占用率高的线程</h4><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">ps -mp <pid> -o THREAD,tid,time | sort -rn | more // (sort -rn 已数值的方式进行逆序排列)</span><br><span class="line">// 或top -Hp <pid></span><br><span class="line">top - 08:31:16 up 30 min, 0 users, load average: 0.75, 0.59, 0.35</span><br><span class="line">Threads: 11 total, 1 running, 10 sleeping, 0 stopped, 0 zombie</span><br><span class="line"><span class="meta">%</span><span class="bash">Cpu(s): 3.5 us, 0.6 sy, 0.0 ni, 95.9 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st</span></span><br><span class="line">KiB Mem: 2046460 total, 1924856 used, 121604 free, 14396 buffers</span><br><span class="line">KiB Swap: 1048572 total, 0 used, 1048572 free. 1192532 cached Mem</span><br><span class="line"></span><br><span class="line"> PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND</span><br><span class="line"> 10 root 20 0 2557160 289824 15872 R 79.3 14.2 0:41.49 java</span><br><span class="line"> 11 root 20 0 2557160 289824 15872 S 13.2 14.2 0:06.78 java</span><br></pre></td></tr></table></figure><h4 id="二、转换线程-ID-为-16-进制"><a href="#二、转换线程-ID-为-16-进制" class="headerlink" title="二、转换线程 ID 为 16 进制"></a>二、转换线程 ID 为 16 进制</h4><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">printf "%x\n" <tid></span><br><span class="line">// printf "%x\n" 10</span><br><span class="line">// a</span><br></pre></td></tr></table></figure><h4 id="三、利用-JDK-提供的工具-jstack-打印导出线程信息"><a href="#三、利用-JDK-提供的工具-jstack-打印导出线程信息" class="headerlink" title="三、利用 JDK 提供的工具 jstack 打印导出线程信息"></a>三、利用 JDK 提供的工具 <code>jstack</code> 打印导出线程信息</h4><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">jstack <pid> | grep <16tid> -A 30 // 或导出 jstack <pid> >> jstack.txt 文件查看</span><br></pre></td></tr></table></figure><h4 id="四、查看线程信息并处理"><a href="#四、查看线程信息并处理" class="headerlink" title="四、查看线程信息并处理"></a>四、查看线程信息并处理</h4><h5 id="4-1-如果是用户线程"><a href="#4-1-如果是用户线程" class="headerlink" title="4.1 如果是用户线程"></a>4.1 如果是用户线程</h5><p><img data-src="/images/pasted-48.png" alt="upload successful"></p><p>查看相关代码并处理</p><p><strong>附 <code>jstack</code> 死锁日志</strong></p><p><img data-src="/images/pasted-47.png" alt="upload successful"></p><h5 id="4-2-如果是-Full-GC-次数过多"><a href="#4-2-如果是-Full-GC-次数过多" class="headerlink" title="4.2 如果是 Full GC 次数过多"></a>4.2 如果是 <strong>Full GC</strong> 次数过多</h5><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">"main" #1 prio=5 os_prio=0 tid=0x00007f8718009800 nid=0xb runnable [0x00007f871fe41000]</span><br><span class="line"> java.lang.Thread.State: RUNNABLE</span><br><span class="line">at com.aibaobei.chapter2.eg2.UserDemo.main(UserDemo.java:9)</span><br><span class="line"></span><br><span class="line">"VM Thread" os_prio=0 tid=0x00007f871806e000 nid=0xa runnable</span><br></pre></td></tr></table></figure><p><strong>nid=0xa</strong> 为系统线程 ID<br>使用 JDK 提供的工具 <code>jstat</code> 查看 GC 情况</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">jstat -gcutil <pid> 1000 10</span><br><span class="line">S0 S1 E O M CCS YGC YGCT FGC FGCT GCT</span><br><span class="line">0.00 0.00 0.00 75.07 59.09 59.60 3259 0.919 6517 7.715 8.635</span><br><span class="line">0.00 0.00 0.00 0.08 59.09 59.60 3306 0.930 6611 7.822 8.752</span><br><span class="line">0.00 0.00 0.00 0.08 59.09 59.60 3351 0.943 6701 7.924 8.867</span><br><span class="line">0.00 0.00 0.00 0.08 59.09 59.60 3397 0.955 6793 8.029 8.984</span><br></pre></td></tr></table></figure><p>使用 JDK 提供的 <code>jmap</code> 工具导出内存日志到 Eclipse mat工具进行查看</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">// 简单查看存活对象的大小数目</span><br><span class="line">jmap -histo:live <pid> | more</span><br><span class="line">// dump 内存</span><br><span class="line">jmap -dump:live,format=b,file=problem.bin <pid></span><br></pre></td></tr></table></figure><p><img data-src="/images/pasted-49.png" alt="upload successful"></p><p>主要有以下两种原因:</p><ol><li>代码中一次获取了大量的对象,导致内存溢出</li><li>内存占用不高,但是 Full GC 次数还是比较多,此时可能是显示的 <code>System.gc()</code> 调用导致 GC 次数过多,这可以通过添加 <code>-XX:+DisableExplicitGC</code> 来禁用JVM对显示GC的响应</li></ol><h4 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h4><p>通过 <code>ps</code> 或 <code>top</code> 命令找出 CPU 过高的线程,将其线程 ID 转换为十六进制,然后在 <code>jstack</code> 日志中查看该线程信息,分为以下两种情况:</p><ol><li>如果是正常的用户线程,则通过该线程的堆栈信息查看其具体是在哪处用户代码处运行比较消耗 CPU</li><li>如果该线程是 <strong>VM Thread</strong> 则通过 <code>jstat -gcutil <pid> <period> <times></code> 命令监控当前系统的 GC 状况,然后通过 <code>jmap dump:format=b,file=<filepath> <pid></code> 导出系统当前的内存数据,导出之后将内存情况放到 eclipse 的 mat 工具中进行分析即可得出内存中主要是什么对象比较消耗内存,进而可以处理相关代码</li></ol><blockquote><p>参考链接:</p><ul><li><a href="https://blog.csdn.net/baiye_xing/article/details/90483169" target="_blank" rel="noopener">https://blog.csdn.net/baiye_xing/article/details/90483169</a></li><li><a href="https://my.oschina.net/zhangxufeng/blog/3017521" target="_blank" rel="noopener">https://my.oschina.net/zhangxufeng/blog/3017521</a></li><li><a href="https://www.cnblogs.com/youxin/p/11229071.html" target="_blank" rel="noopener">https://www.cnblogs.com/youxin/p/11229071.html</a></li><li><a href="https://www.javatang.com/archives/2017/10/19/33151873.html" target="_blank" rel="noopener">JVM 故障分析及性能优化系列文章</a></li></ul></blockquote>]]></content>
<summary type="html">
<h4 id="一、根据-Java-进程-ID,用-ps-或-top-命令查询出-CPU-占用率高的线程"><a href="#一、根据-Java-进程-ID,用-ps-或-top-命令查询出-CPU-占用率高的线程" class="headerlink" title="一、根据 Java 进程 ID,用 ps 或 top 命令查询出 CPU 占用率高的线程"></a>一、根据 Java 进程 ID,用 <code>ps</code> 或 <code>top</code> 命令查询出 CPU 占用率高的线程</h4><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">ps -mp &lt;pid&gt; -o THREAD,tid,time | sort -rn | more // (sort -rn 已数值的方式进行逆序排列)</span><br><span class="line">// 或top -Hp &lt;pid&gt;</span><br><span class="line">top - 08:31:16 up 30 min, 0 users, load average: 0.75, 0.59, 0.35</span><br><span class="line">Threads: 11 total, 1 running, 10 sleeping, 0 stopped, 0 zombie</span><br><span class="line"><span class="meta">%</span><span class="bash">Cpu(s): 3.5 us, 0.6 sy, 0.0 ni, 95.9 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st</span></span><br><span class="line">KiB Mem: 2046460 total, 1924856 used, 121604 free, 14396 buffers</span><br><span class="line">KiB Swap: 1048572 total, 0 used, 1048572 free. 1192532 cached Mem</span><br><span class="line"></span><br><span class="line"> PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND</span><br><span class="line"> 10 root 20 0 2557160 289824 15872 R 79.3 14.2 0:41.49 java</span><br><span class="line"> 11 root 20 0 2557160 289824 15872 S 13.2 14.2 0:06.78 java</span><br></pre></td></tr></table></figure>
<h4 id="二、转换线程-ID-为-16-进制"><a href="#二、转换线程-ID-为-16-进制" class="headerlink" title="二、转换线程 ID 为 16 进制"></a>二、转换线程 ID 为 16 进制</h4><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">printf "%x\n" &lt;tid&gt;</span><br><span class="line">// printf "%x\n" 10</span><br><span class="line">// a</span><br></pre></td></tr></table></figure>
<h4 id="三、利用-JDK-提供的工具-jstack-打印导出线程信息"><a href="#三、利用-JDK-提供的工具-jstack-打印导出线程信息" class="headerlink" title="三、利用 JDK 提供的工具 jstack 打印导出线程信息"></a>三、利用 JDK 提供的工具 <code>jstack</code> 打印导出线程信息</h4>
</summary>
<category term="问题排查" scheme="http://zhangfei.men/categories/%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/"/>
<category term="Java" scheme="http://zhangfei.men/categories/%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/Java/"/>
<category term="Tool" scheme="http://zhangfei.men/tags/Tool/"/>
<category term="Java" scheme="http://zhangfei.men/tags/Java/"/>
<category term="OOM" scheme="http://zhangfei.men/tags/OOM/"/>
<category term="Full GC" scheme="http://zhangfei.men/tags/Full-GC/"/>
<category term="JVM command" scheme="http://zhangfei.men/tags/JVM-command/"/>
</entry>
<entry>
<title>Spring Boot添加admin监控</title>
<link href="http://zhangfei.men/posts/5ec24585/"/>
<id>http://zhangfei.men/posts/5ec24585/</id>
<published>2017-08-17T14:46:57.000Z</published>
<updated>2020-07-28T01:08:16.453Z</updated>
<content type="html"><![CDATA[<h2 id="什么是Spring-Boot-Admin?"><a href="#什么是Spring-Boot-Admin?" class="headerlink" title="什么是Spring Boot Admin?"></a>什么是Spring Boot Admin?</h2><p>Spring Boot Admin是一个用于管理和监控Spring Boot应用程序的Web应用程序。应用程序通过我们的Spring Boot Admin Client(通过HTTP)注册,或者使用Spring Cloud(例如Eureka, Consul)进行注册。</p><h2 id="入门"><a href="#入门" class="headerlink" title="入门"></a>入门</h2><h3 id="设置Admin-Server服务"><a href="#设置Admin-Server服务" class="headerlink" title="设置Admin Server服务"></a>设置Admin Server服务</h3><ul><li>添加<code>Spring Boot Admin Server starter</code>依赖:</li></ul><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">dependency</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">groupId</span>></span>de.codecentric<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">artifactId</span>></span>spring-boot-admin-starter-server<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">version</span>></span>${spring-boot-admin.version}<span class="tag"></<span class="name">version</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br><span class="line"><span class="tag"><<span class="name">dependency</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">groupId</span>></span>org.springframework.boot<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">artifactId</span>></span>spring-boot-starter-web<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br></pre></td></tr></table></figure><ul><li>在主配置类上添加<code>@EnableAdminServer</code>注解启用Server:</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="meta">@EnableAutoConfiguration</span></span><br><span class="line"><span class="meta">@EnableAdminServer</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SpringBootAdminApplication</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>{</span><br><span class="line"> SpringApplication.run(SpringBootAdminApplication<span class="class">.<span class="keyword">class</span>, <span class="title">args</span>)</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="注册客户端应用"><a href="#注册客户端应用" class="headerlink" title="注册客户端应用"></a>注册客户端应用</h3><h4 id="Spring-Boot-Admin-Client"><a href="#Spring-Boot-Admin-Client" class="headerlink" title="Spring Boot Admin Client"></a>Spring Boot Admin Client</h4><ul><li>添加<code>spring-boot-admin-starter-client</code>依赖:</li></ul><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">dependency</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">groupId</span>></span>de.codecentric<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">artifactId</span>></span>spring-boot-admin-starter-client<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">version</span>></span>${spring-boot-admin.version}<span class="tag"></<span class="name">version</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br></pre></td></tr></table></figure><ul><li>配置<code>Spring Boot Admin Server</code>的URL已注册应用:</li></ul><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">spring.boot.admin.client.url</span>=<span class="string">http://localhost:8080</span></span><br><span class="line"><span class="meta">management.endpoints.web.exposure.include</span>=<span class="string">*</span></span><br></pre></td></tr></table></figure><h4 id="使用-Spring-Cloud-Discovery-注册"><a href="#使用-Spring-Cloud-Discovery-注册" class="headerlink" title="使用 Spring Cloud Discovery 注册"></a>使用 Spring Cloud Discovery 注册</h4><ul><li>添加<code>spring-cloud-starter-eureka</code>依赖:</li></ul><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">dependency</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">groupId</span>></span>org.springframework.cloud<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">artifactId</span>></span>spring-cloud-starter-netflix-eureka-client<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br></pre></td></tr></table></figure><ul><li>添加<code>@EnableDiscoveryClient</code>注解启用服务发现:</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="meta">@EnableAutoConfiguration</span></span><br><span class="line"><span class="meta">@EnableDiscoveryClient</span></span><br><span class="line"><span class="meta">@EnableAdminServer</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SpringBootAdminApplication</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>{</span><br><span class="line"> SpringApplication.run(SpringBootAdminApplication<span class="class">.<span class="keyword">class</span>, <span class="title">args</span>)</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li>配置服务发现地址:</li></ul><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">eureka:</span></span><br><span class="line"> <span class="attr">instance:</span></span><br><span class="line"> <span class="attr">leaseRenewalIntervalInSeconds:</span> <span class="number">10</span></span><br><span class="line"> <span class="attr">health-check-url-path:</span> <span class="string">/actuator/health</span></span><br><span class="line"> <span class="attr">metadata-map:</span></span><br><span class="line"> <span class="attr">startup:</span> <span class="string">${random.int}</span> <span class="comment">#needed to trigger info and endpoint update after restart</span></span><br><span class="line"> <span class="attr">client:</span></span><br><span class="line"> <span class="attr">registryFetchIntervalSeconds:</span> <span class="number">5</span></span><br><span class="line"> <span class="attr">serviceUrl:</span></span><br><span class="line"> <span class="attr">defaultZone:</span> <span class="string">${EUREKA_SERVICE_URL:http://localhost:8761}/eureka/</span></span><br><span class="line"></span><br><span class="line"><span class="attr">management:</span></span><br><span class="line"> <span class="attr">endpoints:</span></span><br><span class="line"> <span class="attr">web:</span></span><br><span class="line"> <span class="attr">exposure:</span></span><br><span class="line"> <span class="attr">include:</span> <span class="string">"*"</span> </span><br><span class="line"> <span class="attr">endpoint:</span></span><br><span class="line"> <span class="attr">health:</span></span><br><span class="line"> <span class="attr">show-details:</span> <span class="string">ALWAYS</span></span><br></pre></td></tr></table></figure><h2 id="监控效果"><a href="#监控效果" class="headerlink" title="监控效果"></a>监控效果</h2><p>浏览器访问<a href="http://localhost:8080/" target="_blank" rel="noopener">http://localhost:8080</a></p><p><img data-src="https://github.com/codecentric/spring-boot-admin/raw/master/images/screenshot.png" alt="upload successful"></p><p><img data-src="https://github.com/codecentric/spring-boot-admin/raw/master/images/screenshot-details.png" alt="upload successful"></p><p><img data-src="https://github.com/codecentric/spring-boot-admin/raw/master/images/screenshot-metrics.png" alt="upload successful"></p><p><img data-src="https://github.com/codecentric/spring-boot-admin/raw/master/images/screenshot-logfile.png" alt="upload successful"></p><p><img data-src="https://github.com/codecentric/spring-boot-admin/raw/master/images/screenshot-environment.png" alt="upload successful"></p><p><img data-src="https://github.com/codecentric/spring-boot-admin/raw/master/images/screenshot-logging.png" alt="upload successful"></p><p><img data-src="https://github.com/codecentric/spring-boot-admin/raw/master/images/screenshot-jmx.png" alt="upload successful"></p><p><img data-src="https://github.com/codecentric/spring-boot-admin/raw/master/images/screenshot-threads.png" alt="upload successful"></p><p><img data-src="https://github.com/codecentric/spring-boot-admin/raw/master/images/screenshot-trace.png" alt="upload successful"></p><p><img data-src="https://github.com/codecentric/spring-boot-admin/raw/master/images/screenshot-journal.png" alt="upload successful"></p><h2 id="更多"><a href="#更多" class="headerlink" title="更多"></a>更多</h2><blockquote><p><a href="https://github.com/codecentric/spring-boot-admin" target="_blank" rel="noopener">Github</a><br><a href="https://codecentric.github.io/spring-boot-admin/current/" target="_blank" rel="noopener">更多功能和官方文档</a></p></blockquote>]]></content>
<summary type="html">
<h2 id="什么是Spring-Boot-Admin?"><a href="#什么是Spring-Boot-Admin?" class="headerlink" title="什么是Spring Boot Admin?"></a>什么是Spring Boot Admin?</h2><p>Spring Boot Admin是一个用于管理和监控Spring Boot应用程序的Web应用程序。应用程序通过我们的Spring Boot Admin Client(通过HTTP)注册,或者使用Spring Cloud(例如Eureka, Consul)进行注册。</p>
<h2 id="入门"><a href="#入门" class="headerlink" title="入门"></a>入门</h2><h3 id="设置Admin-Server服务"><a href="#设置Admin-Server服务" class="headerlink" title="设置Admin Server服务"></a>设置Admin Server服务</h3><ul>
<li>添加<code>Spring Boot Admin Server starter</code>依赖:</li>
</ul>
</summary>
<category term="Spring" scheme="http://zhangfei.men/categories/Spring/"/>
<category term="Java" scheme="http://zhangfei.men/tags/Java/"/>
<category term="Spring" scheme="http://zhangfei.men/tags/Spring/"/>
<category term="Spring Boot" scheme="http://zhangfei.men/tags/Spring-Boot/"/>
<category term="Monitor" scheme="http://zhangfei.men/tags/Monitor/"/>
</entry>
<entry>
<title>Spring Retry</title>
<link href="http://zhangfei.men/posts/69b13214/"/>
<id>http://zhangfei.men/posts/69b13214/</id>
<published>2017-08-14T15:50:34.000Z</published>
<updated>2020-07-28T01:08:16.453Z</updated>
<content type="html"><![CDATA[<h2 id="使用场景"><a href="#使用场景" class="headerlink" title="使用场景"></a>使用场景</h2><p>在实际工作中,重处理是一个非常常见的场景,比如:发送消息失败,调用远程服务失败,争抢锁失败,等等,这些错误可能是因为网络波动造成的,等待过后重处理就能成功.通常来说,会用try/catch,while循环之类的语法来进行重处理,但是这样的做法缺乏统一性,并且不是很方便,要多写很多代码.然而spring-retry却可以通过注解,在不入侵原有业务逻辑代码的方式下,优雅的实现重处理功能.</p><h2 id="Maven-Dependencies"><a href="#Maven-Dependencies" class="headerlink" title="Maven Dependencies"></a>Maven Dependencies</h2><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">dependency</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">groupId</span>></span>org.springframework.retry<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">artifactId</span>></span>spring-retry<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">version</span>></span>1.2.5.RELEASE<span class="tag"></<span class="name">version</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br></pre></td></tr></table></figure><h2 id="如何启用"><a href="#如何启用" class="headerlink" title="如何启用"></a>如何启用</h2><ul><li>Spring boot application</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="meta">@EnableRetry</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">AppConfig</span> </span>{ ... }</span><br></pre></td></tr></table></figure><p>指定方法上添加@Retryable,启用重试功能:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">MyService</span> </span>{</span><br><span class="line"> <span class="meta">@Retryable</span>(</span><br><span class="line"> value = { SQLException<span class="class">.<span class="keyword">class</span> },</span></span><br><span class="line"><span class="class"> <span class="title">maxAttempts</span> </span>= <span class="number">2</span>,</span><br><span class="line"> backoff = <span class="meta">@Backoff</span>(delay = <span class="number">5000</span>))</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">retryService</span><span class="params">(String sql)</span> <span class="keyword">throws</span> SQLException</span>;</span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>value</code>: 指定异常重试, <code>maxAttempts</code>: 最大重试次数, <code>backoff</code>: 延时, 单位毫秒<br>默认任何异常都重试, 最多3次, 延时1秒</p><p>为标注了@Retryable的方法单独指定执行的方法<br>指定方法上添加@Recover来开启重试失败后调用的方法:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">MyService</span> </span>{</span><br><span class="line"> ...</span><br><span class="line"> <span class="meta">@Recover</span></span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">recover</span><span class="params">(SQLException e, String sql)</span></span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><blockquote><p><a href="http://www.baeldung.com/spring-retry" target="_blank" rel="noopener">原文链接</a><br><a href="http://docs.spring.io/spring-batch/reference/html/retry.html" target="_blank" rel="noopener">文档地址http://docs.spring.io/spring-batch/reference/html/retry.html</a></p></blockquote>]]></content>
<summary type="html">
<h2 id="使用场景"><a href="#使用场景" class="headerlink" title="使用场景"></a>使用场景</h2><p>在实际工作中,重处理是一个非常常见的场景,比如:发送消息失败,调用远程服务失败,争抢锁失败,等等,这些错误可能是因为网络波动造成的,等待过后重处理就能成功.通常来说,会用try/catch,while循环之类的语法来进行重处理,但是这样的做法缺乏统一性,并且不是很方便,要多写很多代码.然而spring-retry却可以通过注解,在不入侵原有业务逻辑代码的方式下,优雅的实现重处理功能.</p>
<h2 id="Maven-Dependencies"><a href="#Maven-Dependencies" class="headerlink" title="Maven Dependencies"></a>Maven Dependencies</h2><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.retry<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-retry<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.2.5.RELEASE<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>
<h2 id="如何启用"><a href="#如何启用" class="headerlink" title="如何启用"></a>如何启用</h2>
</summary>
<category term="Spring" scheme="http://zhangfei.men/categories/Spring/"/>
<category term="Java" scheme="http://zhangfei.men/tags/Java/"/>
<category term="Spring" scheme="http://zhangfei.men/tags/Spring/"/>
<category term="Spring Boot" scheme="http://zhangfei.men/tags/Spring-Boot/"/>
</entry>
<entry>
<title>Spring为REST实现异常处理</title>
<link href="http://zhangfei.men/posts/ca1a43ab/"/>
<id>http://zhangfei.men/posts/ca1a43ab/</id>
<published>2017-08-12T16:30:24.000Z</published>
<updated>2020-07-28T01:08:16.457Z</updated>
<content type="html"><![CDATA[<blockquote><p>在Spring 3.2之前,在Spring MVC应用程序中处理异常的两种主要方法是:HandlerExceptionResolver或@ExceptionHandler注释。这两个都有一些明显的缺点。3.2之后,我们现在有了新的@ControllerAdvice注释来解决前面两个解决方案的局限性。所有这些都有一个共同点 - 他们处理分离问题非常好。应用程序可以正常抛出异常以指示某种类型的异常 - 然后将单独处理异常。</p></blockquote><h2 id="解决方案1-控制器级别-ExceptionHandler"><a href="#解决方案1-控制器级别-ExceptionHandler" class="headerlink" title="解决方案1 - 控制器级别@ExceptionHandler"></a>解决方案1 - 控制器级别@ExceptionHandler</h2><p>在@Controller class中定义一个方法来处理异常, 并加上@ExceptionHandler annotation:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">FooController</span></span>{</span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line"> <span class="meta">@ExceptionHandler</span>({ CustomException1<span class="class">.<span class="keyword">class</span>, <span class="title">CustomException2</span>.<span class="title">class</span> })</span></span><br><span class="line"><span class="class"> <span class="title">public</span> <span class="title">void</span> <span class="title">handleException</span>() </span>{</span><br><span class="line"> <span class="comment">//</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>主要缺点: @ExceptionHandler注释方法只对该该Controller有效, 不能全局使用。</p><h2 id="解决方案2-HandlerExceptionResolver"><a href="#解决方案2-HandlerExceptionResolver" class="headerlink" title="解决方案2 - HandlerExceptionResolver"></a>解决方案2 - HandlerExceptionResolver</h2><p>定义一个 HandlerExceptionResolver 统一处理决应用程序抛出的任何异常。</p><h3 id="Spring-3-1-ExceptionHandlerExceptionResolve"><a href="#Spring-3-1-ExceptionHandlerExceptionResolve" class="headerlink" title="Spring 3.1 ExceptionHandlerExceptionResolve"></a>Spring 3.1 ExceptionHandlerExceptionResolve</h3><p>默认在DispatcherServlet中启用, @ExceptionHandler就是通过它实现的</p><h3 id="Spring-3-0-DefaultHandlerExceptionResolver"><a href="#Spring-3-0-DefaultHandlerExceptionResolver" class="headerlink" title="Spring 3.0 DefaultHandlerExceptionResolver"></a>Spring 3.0 DefaultHandlerExceptionResolver</h3><p>默认在DispatcherServlet中启用, 他会将Spring的异常解析为相应的HTTP status codes, e.g. 400, 500 …<br><a href="http://docs.spring.io/spring/docs/3.2.x/spring-framework-reference/html/mvc.html#mvc-ann-rest-spring-mvc-exceptions" target="_blank" rel="noopener">完整的异常和对应的HTTP status code</a>, 但是他没有设置任何的response body.</p><h3 id="Spring-3-0-ResponseStatusExceptionResolver"><a href="#Spring-3-0-ResponseStatusExceptionResolver" class="headerlink" title="Spring 3.0 ResponseStatusExceptionResolver"></a>Spring 3.0 ResponseStatusExceptionResolver</h3><p>默认在DispatcherServlet中启用, 自定义异常的@ResponseStatus注释,并将这些异常映射到HTTP状态代码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@ResponseStatus</span>(value = HttpStatus.NOT_FOUND)</span><br><span class="line">publi <span class="class"><span class="keyword">class</span> <span class="title">ResourceNotFoundException</span> <span class="keyword">extends</span> <span class="title">RuntimeException</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">ResourceNotFoundException</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>();</span><br><span class="line"> }</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">ResourceNotFoundException</span><span class="params">(String message, Throwable cause)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>(message, cause);</span><br><span class="line"> }</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">ResourceNotFoundException</span><span class="params">(String message)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>(message);</span><br><span class="line"> }</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">ResourceNotFoundException</span><span class="params">(Throwable cause)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>(cause);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="自定义HandlerExceptionResolver"><a href="#自定义HandlerExceptionResolver" class="headerlink" title="自定义HandlerExceptionResolver"></a>自定义HandlerExceptionResolver</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">RestResponseStatusExceptionResolver</span> <span class="keyword">extends</span> <span class="title">AbstractHandlerExceptionResolver</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">protected</span> ModelAndView doResolveException</span><br><span class="line"> (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">if</span> (ex <span class="keyword">instanceof</span> IllegalArgumentException) {</span><br><span class="line"> <span class="keyword">return</span> handleIllegalArgument((IllegalArgumentException) ex, response, handler);</span><br><span class="line"> }</span><br><span class="line"> ...</span><br><span class="line"> } <span class="keyword">catch</span> (Exception handlerException) {</span><br><span class="line"> logger.warn(<span class="string">"Handling of ["</span> + ex.getClass().getName() + <span class="string">"]</span></span><br><span class="line"><span class="string"> resulted in Exception"</span>, handlerException);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> ModelAndView handleIllegalArgument</span><br><span class="line"> (IllegalArgumentException ex, HttpServletResponse response) <span class="keyword">throws</span> IOException {</span><br><span class="line"> response.sendError(HttpServletResponse.SC_CONFLICT);</span><br><span class="line"> String accept = request.getHeader(HttpHeaders.ACCEPT);</span><br><span class="line"> ...</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> ModelAndView();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="解决方案3-使用-ControllerAdvice-RestControllerAdvice-注解-需要Spring-3-2及以上de版本"><a href="#解决方案3-使用-ControllerAdvice-RestControllerAdvice-注解-需要Spring-3-2及以上de版本" class="headerlink" title="解决方案3 - 使用@ControllerAdvice(@RestControllerAdvice)注解(需要Spring 3.2及以上de版本)"></a>解决方案3 - 使用@ControllerAdvice(@RestControllerAdvice)注解(需要Spring 3.2及以上de版本)</h2><p>@ControllerAdvice注释来支持全局@ExceptionHandler。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@ControllerAdvice</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">RestResponseEntityExceptionHandler</span> <span class="keyword">extends</span> <span class="title">ResponseEntityExceptionHandler</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="meta">@ExceptionHandler</span>(value = { IllegalArgumentException<span class="class">.<span class="keyword">class</span>, <span class="title">IllegalStateException</span>.<span class="title">class</span> })</span></span><br><span class="line"><span class="class"> @<span class="title">ResponseBody</span></span></span><br><span class="line"><span class="class"> <span class="title">protected</span> <span class="title">ResponseEntity</span><<span class="title">Object</span>> <span class="title">handleConflict</span>(<span class="title">RuntimeException</span> <span class="title">ex</span>, <span class="title">WebRequest</span> <span class="title">request</span>) </span>{</span><br><span class="line"> String bodyOfResponse = <span class="string">"This should be application specific"</span>;</span><br><span class="line"> <span class="keyword">return</span> handleExceptionInternal(ex, bodyOfResponse,</span><br><span class="line"> <span class="keyword">new</span> HttpHeaders(), HttpStatus.CONFLICT, request);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>解决@ExceptionHandler不能全局处理。</p><blockquote><p>原文链接: <a href="https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/web.html#mvc-ann-exceptionhandler" target="_blank" rel="noopener">https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/web.html#mvc-ann-exceptionhandler</a></p></blockquote>]]></content>
<summary type="html">
<blockquote>
<p>在Spring 3.2之前,在Spring MVC应用程序中处理异常的两种主要方法是:HandlerExceptionResolver或@ExceptionHandler注释。这两个都有一些明显的缺点。3.2之后,我们现在有了新的@ControllerAdvice注释来解决前面两个解决方案的局限性。所有这些都有一个共同点 - 他们处理分离问题非常好。应用程序可以正常抛出异常以指示某种类型的异常 - 然后将单独处理异常。</p>
</blockquote>
<h2 id="解决方案1-控制器级别-ExceptionHandler"><a href="#解决方案1-控制器级别-ExceptionHandler" class="headerlink" title="解决方案1 - 控制器级别@ExceptionHandler"></a>解决方案1 - 控制器级别@ExceptionHandler</h2><p>在@Controller class中定义一个方法来处理异常, 并加上@ExceptionHandler annotation:</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">FooController</span></span>&#123;</span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line"> <span class="meta">@ExceptionHandler</span>(&#123; CustomException1<span class="class">.<span class="keyword">class</span>, <span class="title">CustomException2</span>.<span class="title">class</span> &#125;)</span></span><br><span class="line"><span class="class"> <span class="title">public</span> <span class="title">void</span> <span class="title">handleException</span>() </span>&#123;</span><br><span class="line"> <span class="comment">//</span></span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>主要缺点: @ExceptionHandler注释方法只对该该Controller有效, 不能全局使用。</p>
</summary>
<category term="Spring" scheme="http://zhangfei.men/categories/Spring/"/>
<category term="Java" scheme="http://zhangfei.men/tags/Java/"/>
<category term="Spring" scheme="http://zhangfei.men/tags/Spring/"/>
<category term="Spring Boot" scheme="http://zhangfei.men/tags/Spring-Boot/"/>
<category term="Rest" scheme="http://zhangfei.men/tags/Rest/"/>
</entry>
<entry>
<title>Springboot快速重启</title>
<link href="http://zhangfei.men/posts/eb48025f/"/>
<id>http://zhangfei.men/posts/eb48025f/</id>
<published>2017-08-09T15:15:52.000Z</published>
<updated>2020-07-28T01:08:16.457Z</updated>
<content type="html"><![CDATA[<p><img data-src="/images/pasted-43.png" alt="upload successful"></p><blockquote><p>在平时的开发过程中,大家一定遇到在修改某个类或者配置文件后需要手动重启应用程序才会生效的情况,可能大家对这样的事情也感到比较的烦。其实Springboot为了使应用程序的开发比较方便快捷,提供了一些额外的工具(spring-boot-devtools),其中就包括快速重启。接下来,我们介绍如何使用spring-boot-devtools。</p></blockquote><h2 id="如何使用"><a href="#如何使用" class="headerlink" title="如何使用"></a>如何使用</h2><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">dependency</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">groupId</span>></span>org.springframework.boot<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">artifactId</span>></span>spring-boot-devtools<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"> <span class="comment"><!-- 防止传递 --></span></span><br><span class="line"> <span class="tag"><<span class="name">optional</span>></span>true<span class="tag"></<span class="name">optional</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br></pre></td></tr></table></figure><p>需要说明的是,运行完全打包应用程序时,开发人员工具会自动禁用。如果应用程序通过java -jar启动,会被认为是生产应用。</p><h2 id="默认属性"><a href="#默认属性" class="headerlink" title="默认属性"></a>默认属性</h2><p>Springboot中一些包为了提升性能使用了缓存(例如,为了避免重复解析模板文件,模板引擎就缓存了编译之后的模板。此外,在访问静态文件时向响应中添加HTTP缓存头)。虽然缓存在生产环境中起到比较好的效果,但是在开发环境中却会起到反作用,它会让你不能及时看到你修改后的结果。基于这样的原因,spring-boot-devtools默认会禁用缓存。</p><p>我们可以通过源码看出,spring-boot-devtools禁用了哪些缓存:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Order</span>(Ordered.LOWEST_PRECEDENCE)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">DevToolsPropertyDefaultsPostProcessor</span> <span class="keyword">implements</span> <span class="title">EnvironmentPostProcessor</span> </span>{</span><br><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Map<String, Object> PROPERTIES;</span><br><span class="line"><span class="keyword">static</span> {</span><br><span class="line">Map<String, Object> properties = <span class="keyword">new</span> HashMap<>();</span><br><span class="line">properties.put(<span class="string">"spring.thymeleaf.cache"</span>, <span class="string">"false"</span>);</span><br><span class="line">properties.put(<span class="string">"spring.freemarker.cache"</span>, <span class="string">"false"</span>);</span><br><span class="line">properties.put(<span class="string">"spring.groovy.template.cache"</span>, <span class="string">"false"</span>);</span><br><span class="line">properties.put(<span class="string">"spring.mustache.cache"</span>, <span class="string">"false"</span>);</span><br><span class="line">properties.put(<span class="string">"server.session.persistent"</span>, <span class="string">"true"</span>);</span><br><span class="line">properties.put(<span class="string">"spring.h2.console.enabled"</span>, <span class="string">"true"</span>);</span><br><span class="line">properties.put(<span class="string">"spring.resources.cache-period"</span>, <span class="string">"0"</span>);</span><br><span class="line">properties.put(<span class="string">"spring.resources.chain.cache"</span>, <span class="string">"false"</span>);</span><br><span class="line">properties.put(<span class="string">"spring.template.provider.cache"</span>, <span class="string">"false"</span>);</span><br><span class="line">properties.put(<span class="string">"spring.mvc.log-resolved-exception"</span>, <span class="string">"true"</span>);</span><br><span class="line">properties.put(<span class="string">"server.servlet.jsp.init-parameters.development"</span>, <span class="string">"true"</span>);</span><br><span class="line">properties.put(<span class="string">"spring.reactor.stacktrace-mode.enabled"</span>, <span class="string">"true"</span>);</span><br><span class="line">PROPERTIES = Collections.unmodifiableMap(properties);</span><br><span class="line">}</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>你同时也可以在application.properties(yml)中通过上述的属性设置是否支持缓存。</p><h2 id="自动重启触发条件"><a href="#自动重启触发条件" class="headerlink" title="自动重启触发条件"></a>自动重启触发条件</h2><p>使用了spring-boot-devtools的应用程序在classpath上的文件发生变化时,重启应用程序。默认情况下,静态文件的修改是不会触发重启应用程序的,但是会触发live reload。</p><p>你可以通过</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line"> <span class="attr">devtools:</span></span><br><span class="line"> <span class="attr">restart:</span></span><br><span class="line"> <span class="attr">exclude:</span> <span class="string">static/**</span></span><br><span class="line"> <span class="attr">additional-exclude:</span> <span class="string">static/**,public/**</span></span><br></pre></td></tr></table></figure><p>来排除触发重启的文件。另外,当需要做到对不在classpath中文件进行修改时也触发重新启动,你可以通过spring.devtools.restart.additional-paths配置文件来将不在classpath中的文件夹路径加入到监控中,配合spring.devtools.restart.exclude来判断文件修改时是否重启。</p><h2 id="自动重启为什么会快"><a href="#自动重启为什么会快" class="headerlink" title="自动重启为什么会快"></a>自动重启为什么会快</h2><p>Springboot的自动重启技术是通过两个类加载器完成的,对于那些不会改变的类(比如第三方包)被加载到基础类加载器中,对于你正在开发的类被加载到重启的类加载器中。当应用程序重启时,重新启动类加载器将被丢弃,并创建一个新的类加载器。这就是为什么自动重启比冷启动要快的原因。</p><p>如果想重新加载基础类加载器中的jar包,可以新建一个META-INF/spring-devtools.properties,在这个文件中可以定义以restart.include.和restart.exclude.开头的属性来设置需要重新加载和不需要重新加载的jar</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">restart.exclude.toplinecommonlibs=/topline-common-[\w-]+.jar</span><br><span class="line">restart.include.toplinecommon=/topline-myproj-[\w-]+.jar</span><br></pre></td></tr></table></figure><h2 id="如何禁用"><a href="#如何禁用" class="headerlink" title="如何禁用"></a>如何禁用</h2><p>可以在application.properties(yml)中配置spring.devtools.restart.enable=false来禁用自动重启。</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line"> <span class="attr">devtools:</span></span><br><span class="line"> <span class="attr">restart:</span></span><br><span class="line"> <span class="attr">enable:</span> <span class="literal">false</span></span><br></pre></td></tr></table></figure><blockquote><p><a href="https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-devtools.html" target="_blank" rel="noopener">官方文档 https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-devtools.html</a></p></blockquote>]]></content>
<summary type="html">
<p><img data-src="/images/pasted-43.png" alt="upload successful"></p>
<blockquote>
<p>在平时的开发过程中,大家一定遇到在修改某个类或者配置文件后需要手动重启应用程序才会生效的情况,可能大家对这样的事情也感到比较的烦。其实Springboot为了使应用程序的开发比较方便快捷,提供了一些额外的工具(spring-boot-devtools),其中就包括快速重启。接下来,我们介绍如何使用spring-boot-devtools。</p>
</blockquote>
<h2 id="如何使用"><a href="#如何使用" class="headerlink" title="如何使用"></a>如何使用</h2><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-devtools<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"> <span class="comment">&lt;!-- 防止传递 --&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">optional</span>&gt;</span>true<span class="tag">&lt;/<span class="name">optional</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>
<p>需要说明的是,运行完全打包应用程序时,开发人员工具会自动禁用。如果应用程序通过java -jar启动,会被认为是生产应用。</p>
</summary>
<category term="Spring" scheme="http://zhangfei.men/categories/Spring/"/>
<category term="Java" scheme="http://zhangfei.men/tags/Java/"/>
<category term="Spring" scheme="http://zhangfei.men/tags/Spring/"/>
<category term="Spring Boot" scheme="http://zhangfei.men/tags/Spring-Boot/"/>
<category term="Boot Basics" scheme="http://zhangfei.men/tags/Boot-Basics/"/>
</entry>
<entry>
<title>基于Docker的CI/CD流水线实践</title>
<link href="http://zhangfei.men/posts/bdb4418d/"/>
<id>http://zhangfei.men/posts/bdb4418d/</id>
<published>2017-07-07T14:56:38.000Z</published>
<updated>2020-07-28T01:08:16.457Z</updated>
<content type="html"><![CDATA[<h2 id="概要"><a href="#概要" class="headerlink" title="概要"></a>概要</h2><p>随着DevOps理念不断的传播,大部分IT从业者对于DevOps本身也有了一定的了解和认识,然而企业内部想根据DevOps思想实践,这并不是一件很简单的事情。一方面由于企业内部的历史环境以及组织结构问题,另外一方面因为业界并没有一套标准的开源工具集可以借鉴(关于几家基于Docker创业的服务提供商暂时除外)。</p><p>那么该篇内容主要讲解如何借助开源工具结合CI/CD的场景,将Docker融入到部署单元中去,进行持续集成、测试到最终的持续部署,开发人员最终只需要去关注业务的访问入口就可以知道业务是否正常,并可以通过一系列的监控工具去及时发现业务异常。</p><p>在整个DevOps部署流水线中需要以下几个部分:CI部分、CD部分、服务调度(治理)部分、监控部分、日志部分。本篇文章将通过一个简单的go-web应用去进行基于Docker的CI/CD流水线的测试。</p><p><img data-src="/images/pasted-42.png" alt="upload successful"></p><h2 id="基于Docker的CI-CD的优势"><a href="#基于Docker的CI-CD的优势" class="headerlink" title="基于Docker的CI/CD的优势"></a>基于Docker的CI/CD的优势</h2><p>一个完整的流程入上图所示,用户(也就是开发人员)将包含Dockerfile的源码从本地push到Git服务器上,然后触发Jenkins进行构建源码,源码构建完成后紧接着进行Docker image的构建,一切构建完成之后,顺带将构建成功的image上传到企业内部的镜像仓库,到此刻为止,其实一个基本的CI(持续集成)已经算是结束,剩下的部分就是持续部署或者进行持续的交付开发产物了。在以前传统的软件发布模式中,持续集成的产物是编译打包好的代码,如果想要发布程序,发布系统需要在持续集成的制品库中去获得对应的代码,然后根据一系列的环境检查来准备应用的运行时环境,而在此过程中往往会涉及到比较多的基本组件依赖,所以在整体的发布周期内来看,还是有一些问题的。在Docker或者容器时代,我们将容器的镜像构建部分融入到持续集成(CI)环节,最终持续集成的产出物是一些已经处理好依赖关系,基本不需要人工进行二次干预的Docker image,而在CD环节,发布系统只需要设置和管理很少的信息就能够很快将image运行起来,快速地将业务发布出去。</p><p>在上面整个环节中,其实无非就是增加了Docker的那一层处理,但其实在整个软件开发的生命周期中,它是产生了极大的影响的。首先,部署系统不需要为统一的部署框架去做更多逻辑抽象,业务研发在开发代码的过程中选择自己依赖的base image即可,最终运行起来的业务也就是你当时提供的base image的模样;其次,由于base image已经处理好了相关的依赖,所以当发布系统拿到业务的image的时候,发布操作将会变得异常迅速,这对于互联网时代可谓是非常重要的;最后一点,也是我感受最深的,就是研发构建好的image可以在任何的Docker环境中run起来,研发人员不需要再关系环境一致性的问题,他们在自己本地的测试环境能够运行起来的应用,那么到生成环境也一定可以。</p><p>为什么第三点我感触比较深呢?因为以前经常有研发兄弟跑过来跟我们讲,我们代码在本地运行一切顺利,代码给你们上到生产就各种问题。所以如果在整个流程中使用Docker image来讲所有的环境固化,从此mm就再也不用担心和研发兄弟扯皮环境不一致的问题啦。</p><h2 id="基于Docker的CI-CD的开源方案实现"><a href="#基于Docker的CI-CD的开源方案实现" class="headerlink" title="基于Docker的CI/CD的开源方案实现"></a>基于Docker的CI/CD的开源方案实现</h2><h3 id="一、自助式Git管理工具Gogs的部署安装"><a href="#一、自助式Git管理工具Gogs的部署安装" class="headerlink" title="一、自助式Git管理工具Gogs的部署安装"></a>一、自助式Git管理工具Gogs的部署安装</h3><p>Gogs部署</p><p>Gogs部署在10.0.0.1主机上,映射到宿主机端口为32770</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ docker run -itd -p 32770:3000 -v /<span class="built_in">export</span>/CI-CD/mygit:/data --name jdjr-gogs gogs:17-04-25</span><br></pre></td></tr></table></figure><p>MySQL建库授权</p><p>MySQL部署在10.0.0.2上,映射到宿主机端口为32771</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ docker run -itd -p 32771:3306 --name jdjr-mysql pandora-mysql</span><br></pre></td></tr></table></figure><p>配置Gogs</p><p>上面两步没有问题之后就可以直接访问:ip:32770 (也就是Gogs暴露的端口)进行相关的配置。</p><p>配置数据库相关:</p><p><img data-src="/images/pasted-40.png" alt="upload successful"></p><p>配置Git地址:</p><p><img data-src="/images/pasted-39.png" alt="upload successful"></p><p>配置完成后进行初始化,并创建管理员用户后就可正常使用。</p><p>如图,现在正在使用的本地Git。</p><p><img data-src="/images/pasted-38.png" alt="upload successful"></p><p>现在就可以将源码托管在本地的Gogs仓库上了。</p><h3 id="二、Jenkins持续集成工具部署安装"><a href="#二、Jenkins持续集成工具部署安装" class="headerlink" title="二、Jenkins持续集成工具部署安装"></a>二、Jenkins持续集成工具部署安装</h3><p>Jenkins部署</p><p>Jenkins在官方的image基础上增加了go 1.7的编译环境,部署在10.0.0.2上,映射到宿主机端口32791。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ docker run -itd -p 32791:8080 -p 32790:50000 -v /<span class="built_in">export</span>/jenkins/:/var/jenkine_home/ --name jdjr-jenkins jdjr-jenkins</span><br></pre></td></tr></table></figure><blockquote><p>注意:需要将Jenkins相关数据以及编译环境映射到Docker宿主机上,因为后期编译完成后Jenkins容器需要docker build构建业务image。</p></blockquote><p>Jenkins容器运行起来之后,就可以直接访问10.0.0.2:32791进行初始化安装配置了。</p><p>在Web上面访问Jenkins地址进行初始化配置,需要写入ID进行解锁Jenkins(Web上会提示在哪个路径下存放,直接使用docker logs也可查看);解锁后就是正常的安装相关的Plugins了,只要网络没有问题,一般都正常通过。</p><p>Jenkine安装成功后界面如下:</p><p><img data-src="/images/pasted-37.png" alt="upload successful"></p><p>创建Jenkins项目,并配置构建脚本(也可通过相应的Plugins进行配置)。</p><p>创建一个新的名为test的项目,配置相关的源码管理以及构建条件以及相关的后续操作。</p><p><img data-src="/images/pasted-36.png" alt="upload successful"></p><p><img data-src="/images/pasted-35.png" alt="upload successful"></p><p><img data-src="/images/pasted-34.png" alt="upload successful"></p><p>配置Jenkins环境</p><p>注意:由上图可以看出来,Jenkins进行构建image和持续部署测试的过程都是通过SSH到远端去执行的,因此需要再Jenkins容器中生成SSH公私钥对,并和Jenkins的宿主机以及持续部署测试的宿主机进行免密认证。虽然Jenkins本身其实支持了很多种Plugin来支持管理Docker的,比如说Docker build step plugin、Docker Build Publish Plugin,但是由于过多的Plugin会造成实际环境中的维护成本大大增加,因此我们选择简单粗暴的脚本方式,上图中的Execute shell只是简单的示例。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ docker <span class="built_in">exec</span> -it myjenkins bash</span><br></pre></td></tr></table></figure><p>生成公私钥对之后,将公钥传给要远程部署的机器就OK了,目的是要让Jenkins容器能够免密登录远程服务器,并能执行sudo命令。</p><h3 id="三、通过配置Nginx反向代理来访问Git,Jenkins以及测试实例"><a href="#三、通过配置Nginx反向代理来访问Git,Jenkins以及测试实例" class="headerlink" title="三、通过配置Nginx反向代理来访问Git,Jenkins以及测试实例"></a>三、通过配置Nginx反向代理来访问Git,Jenkins以及测试实例</h3><p>反向代理Nginx部署在10.0.0.4:80上。</p><p>配置Nginx</p><blockquote><p>注意:centos6.8-jdjr-test-app:v2镜像默认是包含Nginx以及配置管理工具的。</p></blockquote><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ docker run -itd --name biaoge-nginx centos6.8-jdjr-test-app:v2</span><br></pre></td></tr></table></figure><blockquote><p>注意:此时Git上的源码还没有编译部署,我只是暂时定义了一个端10.0.0.3:32768,等完成整个CI/CD流程后直接访问web.biao.com就可以看到源码部署的效果。</p></blockquote><p>测试访问</p><p>在本地绑定如下hosts</p><blockquote><p>10.0.0.4 jenkins.biao.com</p></blockquote><p>访问mygit.biao.com上面的源码:</p><p><img data-src="/images/pasted-33.png" alt="upload successful"></p><p>访问jenkins.biao.com上的构建任务:</p><p><img data-src="/images/pasted-32.png" alt="upload successful"></p><p>注意:test项目在之前我们已经配置好了,所以可以直接触发构建部署。<br>手动触发构建部署:</p><p><img data-src="/images/pasted-31.png" alt="upload successful"></p><p>注意:在构建过程这里可以看到详细的构建过程,构建成功后便可以访问我们的goweb服务了。<br>访问web.biao.com服务:</p><p><img data-src="/images/pasted-30.png" alt="upload successful"></p><p>持续集成持续部署的效果</p><p>更新源码中的部分内容,进行重新构建访问。</p><p>修改web的源码<br>在Jenkins上进行再次构建:</p><p><img data-src="/images/pasted-29.png" alt="upload successful"></p><p><img data-src="/images/pasted-28.png" alt="upload successful"></p><p>再次访问web.biao.com服务:</p><p><img data-src="/images/pasted-27.png" alt="upload successful"></p><p>对比前后两个Web,发现不仅欢迎语由“biaoge”变成了“逼格运维说”,而且第二行的字符串由4e7853008397变为0ce402beclle,也就是是之前的那个Container已经被销毁,我们现在访问的web.biao.com是重新编译后运行在新的container里面的实例。</p>]]></content>
<summary type="html">
<h2 id="概要"><a href="#概要" class="headerlink" title="概要"></a>概要</h2><p>随着DevOps理念不断的传播,大部分IT从业者对于DevOps本身也有了一定的了解和认识,然而企业内部想根据DevOps思想实践,这并不是一件很简单的事情。一方面由于企业内部的历史环境以及组织结构问题,另外一方面因为业界并没有一套标准的开源工具集可以借鉴(关于几家基于Docker创业的服务提供商暂时除外)。</p>
<p>那么该篇内容主要讲解如何借助开源工具结合CI/CD的场景,将Docker融入到部署单元中去,进行持续集成、测试到最终的持续部署,开发人员最终只需要去关注业务的访问入口就可以知道业务是否正常,并可以通过一系列的监控工具去及时发现业务异常。</p>
<p>在整个DevOps部署流水线中需要以下几个部分:CI部分、CD部分、服务调度(治理)部分、监控部分、日志部分。本篇文章将通过一个简单的go-web应用去进行基于Docker的CI/CD流水线的测试。</p>
<p><img data-src="/images/pasted-42.png" alt="upload successful"></p>
</summary>
<category term="技术实战" scheme="http://zhangfei.men/categories/%E6%8A%80%E6%9C%AF%E5%AE%9E%E6%88%98/"/>
<category term="Docker" scheme="http://zhangfei.men/tags/Docker/"/>
<category term="Git" scheme="http://zhangfei.men/tags/Git/"/>
<category term="DevOps" scheme="http://zhangfei.men/tags/DevOps/"/>
<category term="CICD" scheme="http://zhangfei.men/tags/CICD/"/>
<category term="Jenkins" scheme="http://zhangfei.men/tags/Jenkins/"/>
</entry>
<entry>
<title>快速切换hosts文件的开源程序SwitchHosts</title>
<link href="http://zhangfei.men/posts/d04e924f/"/>
<id>http://zhangfei.men/posts/d04e924f/</id>
<published>2017-07-07T14:44:50.000Z</published>
<updated>2020-07-28T01:08:16.457Z</updated>
<content type="html"><![CDATA[<p>SwitchHosts 是一款用于快速切换 hosts 文件的开源小程序,基于 MIT 协议开源。拥有Windows版, Linux版和Mac OS 版。基于 Electron 开发,同时使用了 React、Ant Design 以及 CodeMirror 等框架/库。需要 Node.js 环境。</p><p><img data-src="/images/pasted-25.png" alt="upload successful"></p><p>功能特性包括:</p><ul><li>快速切换 hosts</li><li>hosts 文件语法高亮</li><li>在线/本地 hosts 方案选择</li><li>系统托盘图标快速切换</li><li>Host文件编辑时,点击行号快速切换注释</li><li>macOS: 支持 Alfred workflow 快速切换</li></ul><blockquote><p>Website <a href="https://oldj.github.io/SwitchHosts" target="_blank" rel="noopener">https://oldj.github.io/SwitchHosts</a><br>Github <a href="https://github.com/oldj/SwitchHosts" target="_blank" rel="noopener">https://github.com/oldj/SwitchHosts</a></p></blockquote>]]></content>
<summary type="html">
<p>SwitchHosts 是一款用于快速切换 hosts 文件的开源小程序,基于 MIT 协议开源。拥有Windows版, Linux版和Mac OS 版。基于 Electron 开发,同时使用了 React、Ant Design 以及 CodeMirror 等框架/库。需要 Node.js 环境。</p>
<p><img data-src="/images/pasted-25.png" alt="upload successful"></p>
<p>功能特性包括:</p>
<ul>
<li>快速切换 hosts</li>
<li>hosts 文件语法高亮</li>
<li>在线/本地 hosts 方案选择</li>
<li>系统托盘图标快速切换</li>
<li>Host文件编辑时,点击行号快速切换注释</li>
<li>macOS: 支持 Alfred workflow 快速切换</li>
</ul>
<blockquote>
<p>Website <a href="https://oldj.github.io/SwitchHosts" target="_blank" rel="noopener">https://oldj.github.io/SwitchHosts</a><br>Github <a href="https://github.com/oldj/SwitchHosts" target="_blank" rel="noopener">https://github.com/oldj/SwitchHosts</a></p>
</blockquote>
</summary>
<category term="工具" scheme="http://zhangfei.men/categories/%E5%B7%A5%E5%85%B7/"/>
<category term="Hosts" scheme="http://zhangfei.men/tags/Hosts/"/>
<category term="Software" scheme="http://zhangfei.men/tags/Software/"/>
</entry>
<entry>
<title>Docker Swarm 入门</title>
<link href="http://zhangfei.men/posts/18246ef3/"/>
<id>http://zhangfei.men/posts/18246ef3/</id>
<published>2017-07-06T15:23:31.000Z</published>
<updated>2020-07-28T01:08:16.453Z</updated>
<content type="html"><![CDATA[<blockquote><p>Swarm 在 Docker 1.12 版本之前属于一个独立的项目,在 Docker 1.12 版本发布之后,该项目合并到了 Docker 中,成为 Docker 的一个子命令。目前,Swarm 是 Docker 社区提供的唯一一个原生支持 Docker 集群管理的工具。它可以把多个 Docker 主机组成的系统转换为单一的虚拟 Docker 主机,使得容器可以组成跨主机的子网网络。</p></blockquote><h2 id="Swarm-认识"><a href="#Swarm-认识" class="headerlink" title="Swarm 认识"></a>Swarm 认识</h2><p>Swarm 是目前 Docker 官方唯一指定(绑定)的集群管理工具。Docker 1.12 内嵌了 swarm mode 集群管理模式。</p><p>为了方便演示跨主机网络,我们需要用到一个工具——Docker Machine,这个工具与 Docker Compose、Docker Swarm 并称 Docker 三剑客,下面我们来看看如何安装 Docker Machine:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ curl -L https://github.com/docker/machine/releases/download/v0.9.0-rc2/docker-machine-`uname -s`-`uname -m` >/tmp/docker-machine &&</span><br><span class="line"> chmod +x /tmp/docker-machine &&</span><br><span class="line"> sudo cp /tmp/docker-machine /usr/<span class="built_in">local</span>/bin/docker-machine</span><br></pre></td></tr></table></figure><p>安装过程和 Docker Compose 非常类似。现在 Docker 三剑客已经全部到齐了。<br>在开始之前,我们需要了解一些基本概念,有关集群的 Docker 命令如下:</p><ul><li>docker swarm:集群管理,子命令有 init, join,join-token, leave, update</li><li>docker node:节点管理,子命令有 demote, inspect,ls, promote, rm, ps, update</li><li>docker service:服务管理,子命令有 create, inspect, ps, ls ,rm , scale, update</li><li>docker stack/deploy:试验特性,用于多应用部署,等正式版加进来再说。</li></ul><h2 id="创建集群"><a href="#创建集群" class="headerlink" title="创建集群"></a>创建集群</h2><p>首先使用 Docker Machine 创建一个虚拟机作为 manger 节点。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">$ docker-machine create --driver virtualbox manager1</span><br><span class="line">Running pre-create checks...</span><br><span class="line">(manager1) Unable to get the latest Boot2Docker ISO release version: Get https://api.github.com/repos/boot2docker/boot2docker/releases/latest: dial tcp: lookup api.github.com on [::1]:53: server misbehaving</span><br><span class="line">Creating machine...</span><br><span class="line">(manager1) Unable to get the latest Boot2Docker ISO release version: Get https://api.github.com/repos/boot2docker/boot2docker/releases/latest: dial tcp: lookup api.github.com on [::1]:53: server misbehaving</span><br><span class="line">(manager1) Copying /home/zuolan/.docker/machine/cache/boot2docker.iso to /home/zuolan/.docker/machine/machines/manager1/boot2docker.iso...</span><br><span class="line">(manager1) Creating VirtualBox VM...</span><br><span class="line">(manager1) Creating SSH key...</span><br><span class="line">(manager1) Starting the VM...</span><br><span class="line">(manager1) Check network to re-create <span class="keyword">if</span> needed...</span><br><span class="line">(manager1) Found a new host-only adapter: <span class="string">"vboxnet0"</span></span><br><span class="line">(manager1) Waiting <span class="keyword">for</span> an IP...</span><br><span class="line">Waiting <span class="keyword">for</span> machine to be running, this may take a few minutes...</span><br><span class="line">Detecting operating system of created instance...</span><br><span class="line">Waiting <span class="keyword">for</span> SSH to be available...</span><br><span class="line">Detecting the provisioner...</span><br><span class="line">Provisioning with boot2docker...</span><br><span class="line">Copying certs to the <span class="built_in">local</span> machine directory...</span><br><span class="line">Copying certs to the remote machine...</span><br><span class="line">Setting Docker configuration on the remote daemon...</span><br><span class="line">Checking connection to Docker...</span><br><span class="line">Docker is up and running!</span><br><span class="line">To see how to connect your Docker Client to the Docker Engine running on this virtual machine, run: docker-machine env manager1</span><br></pre></td></tr></table></figure><p>查看虚拟机的环境变量等信息,包括虚拟机的 IP 地址:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">$ docker-machine env manager1</span><br><span class="line"><span class="built_in">export</span> DOCKER_TLS_VERIFY=<span class="string">"1"</span></span><br><span class="line"><span class="built_in">export</span> DOCKER_HOST=<span class="string">"tcp://192.168.99.100:2376"</span></span><br><span class="line"><span class="built_in">export</span> DOCKER_CERT_PATH=<span class="string">"/home/zuolan/.docker/machine/machines/manager1"</span></span><br><span class="line"><span class="built_in">export</span> DOCKER_MACHINE_NAME=<span class="string">"manager1"</span></span><br><span class="line"><span class="comment"># Run this command to configure your shell: </span></span><br><span class="line"><span class="comment"># eval $(docker-machine env manager1)</span></span><br></pre></td></tr></table></figure><p>然后再创建一个节点作为 work 节点。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ docker-machine create --driver virtualbox worker1</span><br></pre></td></tr></table></figure><p>现在我们有了两个虚拟主机,使用 Machine 的命令可以查看:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ docker-machine ls</span><br><span class="line">NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS</span><br><span class="line">manager1 - virtualbox Running tcp://192.168.99.100:2376 v1.12.3</span><br><span class="line">worker1 - virtualbox Running tcp://192.168.99.101:2376 v1.12.3</span><br></pre></td></tr></table></figure><p>但是目前这两台虚拟主机并没有什么联系,为了把它们联系起来,我们需要 Swarm 登场了。<br>因为我们使用的是 Docker Machine 创建的虚拟机,因此可以使用 docker-machine ssh 命令来操作虚拟机,在实际生产环境中,并不需要像下面那样操作,只需要执行 docker swarm 即可。</p><p>把 manager1 加入集群:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">$ docker-machine ssh manager1 docker swarm init --listen-addr 192.168.99.100:2377 --advertise-addr 192.168.99.100</span><br><span class="line">Swarm initialized: current node (23lkbq7uovqsg550qfzup59t6) is now a manager.</span><br><span class="line"></span><br><span class="line">To add a worker to this swarm, run the following <span class="built_in">command</span>:</span><br><span class="line"></span><br><span class="line"> docker swarm join \</span><br><span class="line"> --token SWMTKN-1-3z5rzoey0u6onkvvm58f7vgkser5d7z8sfshlu7s4oz2gztlvj-c036gwrakjejql06klrfc585r \</span><br><span class="line"> 192.168.99.100:2377</span><br><span class="line"></span><br><span class="line">To add a manager to this swarm, run <span class="string">'docker swarm join-token manager'</span> and follow the instructions.</span><br></pre></td></tr></table></figure><p>用 –listen-addr 指定监听的 ip 与端口,实际的 Swarm 命令格式如下,本例使用 Docker Machine 来连接虚拟机而已:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ docker swarm init --listen-addr <MANAGER-IP>:<PORT></span><br></pre></td></tr></table></figure><p>接下来,再把 work1 加入集群中:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ docker-machine ssh worker1 docker swarm join --token \</span><br><span class="line"> SWMTKN-1-3z5rzoey0u6onkvvm58f7vgkser5d7z8sfshlu7s4oz2gztlvj-c036gwrakjejql06klrfc585r \</span><br><span class="line"> 192.168.99.100:2377</span><br><span class="line">This node joined a swarm as a worker.</span><br></pre></td></tr></table></figure><p>上面 join 命令中可以添加 –listen-addr $WORKER1_IP:2377 作为监听准备,因为有时候可能会遇到把一个 work 节点提升为 manger 节点的可能,当然本例子没有这个打算就不添加这个参数了。</p><blockquote><p>注意:如果你在新建集群时遇到双网卡情况,可以指定使用哪个 IP,例如上面的例子会有可能遇到下面的错误。</p></blockquote><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ docker-machine ssh manager1 docker swarm init --listen-addr <span class="variable">$MANAGER1_IP</span>:2377</span><br><span class="line">Error response from daemon: could not choose an IP address to advertise since this system has multiple addresses on different interfaces (10.0.2.15 on eth0 and 192.168.99.100 on eth1) - specify one with --advertise-addr</span><br><span class="line"><span class="built_in">exit</span> status 1</span><br></pre></td></tr></table></figure><p>发生错误的原因是因为有两个 IP 地址,而 Swarm 不知道用户想使用哪个,因此要指定 IP。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">$ docker-machine ssh manager1 docker swarm init --advertise-addr 192.168.99.100 --listen-addr 192.168.99.100:2377 </span><br><span class="line">Swarm initialized: current node (ahvwxicunjd0z8g0eeosjztjx) is now a manager.</span><br><span class="line"></span><br><span class="line">To add a worker to this swarm, run the following <span class="built_in">command</span>:</span><br><span class="line"></span><br><span class="line"> docker swarm join \</span><br><span class="line"> --token SWMTKN-1-3z5rzoey0u6onkvvm58f7vgkser5d7z8sfshlu7s4oz2gztlvj-c036gwrakjejql06klrfc585r \</span><br><span class="line"> 192.168.99.100:2377</span><br><span class="line"></span><br><span class="line">To add a manager to this swarm, run <span class="string">'docker swarm join-token manager'</span> and follow the instructions.</span><br></pre></td></tr></table></figure><p>集群初始化成功。</p><p>现在我们新建了一个有两个节点的“集群”,现在进入其中一个管理节点使用 docker node 命令来查看节点信息:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ docker-machine ssh manager1 docker node ls</span><br><span class="line">ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS</span><br><span class="line">23lkbq7uovqsg550qfzup59t6 * manager1 Ready Active Leader</span><br><span class="line">dqb3fim8zvcob8sycri3hy98a worker1 Ready Active</span><br></pre></td></tr></table></figure><p>现在每个节点都归属于 Swarm,并都处在了待机状态。Manager1 是领导者,work1 是工人。</p><p>现在,我们继续新建虚拟机 manger2、worker2、worker3,现在已经有五个虚拟机了,使用 docker-machine ls 来查看虚拟机:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS</span><br><span class="line">manager1 - virtualbox Running tcp://192.168.99.100:2376 v1.12.3 </span><br><span class="line">manager2 - virtualbox Running tcp://192.168.99.105:2376 v1.12.3 </span><br><span class="line">worker1 - virtualbox Running tcp://192.168.99.102:2376 v1.12.3 </span><br><span class="line">worker2 - virtualbox Running tcp://192.168.99.103:2376 v1.12.3 </span><br><span class="line">worker3 - virtualbox Running tcp://192.168.99.104:2376 v1.12.3</span><br></pre></td></tr></table></figure><p>然后我们把剩余的虚拟机也加到集群中。</p><p>添加 worker2 到集群中:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ docker-machine ssh worker2 docker swarm join \</span><br><span class="line"> --token SWMTKN-1-3z5rzoey0u6onkvvm58f7vgkser5d7z8sfshlu7s4oz2gztlvj-c036gwrakjejql06klrfc585r \</span><br><span class="line"> 192.168.99.100:2377</span><br><span class="line">This node joined a swarm as a worker.</span><br></pre></td></tr></table></figure><p>添加 worker3 到集群中:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ docker-machine ssh worker3 docker swarm join \</span><br><span class="line"> --token SWMTKN-1-3z5rzoey0u6onkvvm58f7vgkser5d7z8sfshlu7s4oz2gztlvj-c036gwrakjejql06klrfc585r \</span><br><span class="line"> 192.168.99.100:2377</span><br><span class="line">This node joined a swarm as a worker.</span><br></pre></td></tr></table></figure><p>添加 manager2 到集群中:<br>先从 manager1 中获取 manager 的 token:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">$ docker-machine ssh manager1 docker swarm join-token manager</span><br><span class="line">To add a manager to this swarm, run the following <span class="built_in">command</span>:</span><br><span class="line"></span><br><span class="line"> docker swarm join \</span><br><span class="line"> --token SWMTKN-1-3z5rzoey0u6onkvvm58f7vgkser5d7z8sfshlu7s4oz2gztlvj-8tn855hkjdb6usrblo9iu700o \</span><br><span class="line">192.168.99.100:2377</span><br></pre></td></tr></table></figure><p>然后添加 manager2 到集群中:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ docker-machine ssh manager2 docker swarm join \</span><br><span class="line"> --token SWMTKN-1-3z5rzoey0u6onkvvm58f7vgkser5d7z8sfshlu7s4oz2gztlvj-8tn855hkjdb6usrblo9iu700o \</span><br><span class="line"> 192.168.99.100:2377</span><br><span class="line">This node joined a swarm as a manager.</span><br></pre></td></tr></table></figure><p>现在再来查看集群信息:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">$ docker-machine ssh manager2 docker node ls</span><br><span class="line">ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS</span><br><span class="line">16w80jnqy2k30yez4wbbaz1l8 worker1 Ready Active</span><br><span class="line">2gkwhzakejj72n5xoxruet71z worker2 Ready Active</span><br><span class="line">35kutfyn1ratch55fn7j3fs4x worker3 Ready Active</span><br><span class="line">a9r21g5iq1u6h31myprfwl8ln * manager2 Ready Active Reachable</span><br><span class="line">dpo7snxbz2a0dxvx6mf19p35z manager1 Ready Active Leader</span><br></pre></td></tr></table></figure><h2 id="建立跨主机网络"><a href="#建立跨主机网络" class="headerlink" title="建立跨主机网络"></a>建立跨主机网络</h2><p>为了演示更清晰,下面我们把宿主机也加入到集群之中,这样我们使用 Docker 命令操作会清晰很多。<br>直接在本地执行加入集群命令:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ docker swarm join \</span><br><span class="line"> --token SWMTKN-1-3z5rzoey0u6onkvvm58f7vgkser5d7z8sfshlu7s4oz2gztlvj-8tn855hkjdb6usrblo9iu700o \</span><br><span class="line"> 192.168.99.100:2377</span><br><span class="line">This node joined a swarm as a manager.</span><br></pre></td></tr></table></figure><p>现在我们有三台 manager,三台 worker。其中一台是宿主机,五台虚拟机。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">$ docker node ls</span><br><span class="line">ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS</span><br><span class="line">6z2rpk1t4xucffzlr2rpqb8u3 worker3 Ready Active</span><br><span class="line">7qbr0xd747qena4awx8bx101s * user-pc Ready Active Reachable</span><br><span class="line">9v93sav79jqrg0c7051rcxxev manager2 Ready Active Reachable</span><br><span class="line">a1ner3zxj3ubsiw4l3p28wrkj worker1 Ready Active</span><br><span class="line">a5w7h8j83i11qqi4vlu948mad worker2 Ready Active</span><br><span class="line">d4h7vuekklpd6189fcudpfy18 manager1 Ready Active Leader</span><br></pre></td></tr></table></figure><p>查看网络状态:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">$ docker network ls</span><br><span class="line">NETWORK ID NAME DRIVER SCOPE</span><br><span class="line">764ff31881e5 bridge bridge <span class="built_in">local</span></span><br><span class="line">fbd9a977aa03 host host <span class="built_in">local</span></span><br><span class="line">6p6xlousvsy2 ingress overlay swarm</span><br><span class="line">e81af24d643d none null <span class="built_in">local</span></span><br></pre></td></tr></table></figure><p>可以看到在 swarm 上默认已有一个名为 ingress 的 overlay 网络, 默认在 swarm 里使用,本例子中会创建一个新的 overlay 网络。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">$ docker network create --driver overlay swarm_test</span><br><span class="line">4dm8cy9y5delvs5vd0ghdd89s</span><br><span class="line">$ docker network ls</span><br><span class="line">NETWORK ID NAME DRIVER SCOPE</span><br><span class="line">764ff31881e5 bridge bridge <span class="built_in">local</span></span><br><span class="line">fbd9a977aa03 host host <span class="built_in">local</span></span><br><span class="line">6p6xlousvsy2 ingress overlay swarm</span><br><span class="line">e81af24d643d none null <span class="built_in">local</span></span><br><span class="line">4dm8cy9y5del swarm_test overlay swarm</span><br></pre></td></tr></table></figure><p>这样一个跨主机网络就搭建好了,但是现在这个网络只是处于待机状态,下一小节我们会在这个网络上部署应用。</p><h2 id="在跨主机网络上部署应用"><a href="#在跨主机网络上部署应用" class="headerlink" title="在跨主机网络上部署应用"></a>在跨主机网络上部署应用</h2><p>首先我们上面创建的节点都是没有镜像的,因此我们要逐一 pull 镜像到节点中,这里我们使用前面搭建的私有仓库。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line">$ docker-machine ssh manager1 docker pull reg.example.com/library/nginx:alpine</span><br><span class="line">alpine: Pulling from library/nginx</span><br><span class="line">e110a4a17941: Pulling fs layer</span><br><span class="line">... ...</span><br><span class="line">7648f5d87006: Pull complete</span><br><span class="line">Digest: sha256:65063cb82bf508fd5a731318e795b2abbfb0c22222f02ff5c6b30df7f23292fe</span><br><span class="line">Status: Downloaded newer image <span class="keyword">for</span> reg.example.com/library/nginx:alpine</span><br><span class="line">$ docker-machine ssh manager2 docker pull reg.example.com/library/nginx:alpine</span><br><span class="line">alpine: Pulling from library/nginx</span><br><span class="line">e110a4a17941: Pulling fs layer</span><br><span class="line">... ...</span><br><span class="line">7648f5d87006: Pull complete</span><br><span class="line">Digest: sha256:65063cb82bf508fd5a731318e795b2abbfb0c22222f02ff5c6b30df7f23292fe</span><br><span class="line">Status: Downloaded newer image <span class="keyword">for</span> reg.example.com/library/nginx:alpine</span><br><span class="line">$ docker-machine ssh worker1 docker pull reg.example.com/library/nginx:alpine</span><br><span class="line">alpine: Pulling from library/nginx</span><br><span class="line">e110a4a17941: Pulling fs layer</span><br><span class="line">... ...</span><br><span class="line">7648f5d87006: Pull complete</span><br><span class="line">Digest: sha256:65063cb82bf508fd5a731318e795b2abbfb0c22222f02ff5c6b30df7f23292fe</span><br><span class="line">Status: Downloaded newer image <span class="keyword">for</span> reg.example.com/library/nginx:alpine</span><br><span class="line">$ docker-machine ssh worker2 docker pull reg.example.com/library/nginx:alpine</span><br><span class="line">alpine: Pulling from library/nginx</span><br><span class="line">e110a4a17941: Pulling fs layer</span><br><span class="line">... ...</span><br><span class="line">7648f5d87006: Pull complete</span><br><span class="line">Digest: sha256:65063cb82bf508fd5a731318e795b2abbfb0c22222f02ff5c6b30df7f23292fe</span><br><span class="line">Status: Downloaded newer image <span class="keyword">for</span> reg.example.com/library/nginx:alpine</span><br><span class="line">$ docker-machine ssh worker3 docker pull reg.example.com/library/nginx:alpine</span><br><span class="line">alpine: Pulling from library/nginx</span><br><span class="line">e110a4a17941: Pulling fs layer</span><br><span class="line">... ...</span><br><span class="line">7648f5d87006: Pull complete</span><br><span class="line">Digest: sha256:65063cb82bf508fd5a731318e795b2abbfb0c22222f02ff5c6b30df7f23292fe</span><br><span class="line">Status: Downloaded newer image <span class="keyword">for</span> reg.example.com/library/nginx:alpine</span><br></pre></td></tr></table></figure><p>上面使用 docker pull 分别在五个虚拟机节点拉取 nginx:alpine 镜像。接下来我们要在五个节点部署一组 Nginx 服务。</p><p>部署的服务使用 swarm_test 跨主机网络。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ docker service create --replicas 2 --name helloworld --network=swarm_test nginx:alpine</span><br><span class="line">5gz0h2s5agh2d2libvzq6bhgs</span><br></pre></td></tr></table></figure><p>查看服务状态:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ docker service ls</span><br><span class="line">ID NAME REPLICAS IMAGE COMMAND</span><br><span class="line">5gz0h2s5agh2 helloworld 0/2 nginx:alpine</span><br></pre></td></tr></table></figure><p>查看 helloworld 服务详情(为了方便阅读,已调整输出内容):</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ docker service ps helloworld</span><br><span class="line">ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR</span><br><span class="line">ay081uome3 helloworld.1 nginx:alpine manager1 Running Preparing 2 seconds ago </span><br><span class="line">16cvore0c96 helloworld.2 nginx:alpine worker2 Running Preparing 2 seconds ago</span><br></pre></td></tr></table></figure><p>可以看到两个实例分别运行在两个节点上。</p><p>进入两个节点,查看服务状态(为了方便阅读,已调整输出内容):</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">$ docker-machine ssh manager1 docker ps -a</span><br><span class="line">CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES</span><br><span class="line">119f787622c2 nginx:alpine <span class="string">"nginx -g ..."</span> 4 minutes ago Up 4 minutes 80/tcp, 443/tcp hello ...</span><br><span class="line">$ docker-machine ssh worker2 docker ps -a</span><br><span class="line">CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES</span><br><span class="line">5db707401a06 nginx:alpine <span class="string">"nginx -g ..."</span> 4 minutes ago Up 4 minutes 80/tcp, 443/tcp hello ...</span><br></pre></td></tr></table></figure><p>上面输出做了调整,实际的 NAMES 值为:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">helloworld.1.ay081uome3eejeg4mspa8pdlx</span><br><span class="line">helloworld.2.16cvore0c96rby1vp0sny3mvt</span><br></pre></td></tr></table></figure><p>记住上面这两个实例的名称。现在我们来看这两个跨主机的容器是否能互通:<br>首先使用 Machine 进入 manager1 节点,然后使用 docker exec -i 命令进入 helloworld.1 容器中 ping 运行在 worker2 节点的 helloworld.2 容器。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">$ docker-machine ssh manager1 docker <span class="built_in">exec</span> -i helloworld.1.ay081uome3eejeg4mspa8pdlx \</span><br><span class="line"> ping helloworld.2.16cvore0c96rby1vp0sny3mvt</span><br><span class="line">PING helloworld.2.16cvore0c96rby1vp0sny3mvt (10.0.0.4): 56 data bytes</span><br><span class="line">64 bytes from 10.0.0.4: seq=0 ttl=64 time=0.591 ms</span><br><span class="line">64 bytes from 10.0.0.4: seq=1 ttl=64 time=0.594 ms</span><br><span class="line">64 bytes from 10.0.0.4: seq=2 ttl=64 time=0.624 ms</span><br><span class="line">64 bytes from 10.0.0.4: seq=3 ttl=64 time=0.612 ms</span><br><span class="line">^C</span><br></pre></td></tr></table></figure><p>然后使用 Machine 进入 worker2 节点,然后使用 docker exec -i 命令进入 helloworld.2 容器中 ping 运行在 manager1 节点的 helloworld.1 容器。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">$ docker-machine ssh worker2 docker <span class="built_in">exec</span> -i helloworld.2.16cvore0c96rby1vp0sny3mvt \</span><br><span class="line"> ping helloworld.1.ay081uome3eejeg4mspa8pdlx </span><br><span class="line">PING helloworld.1.ay081uome3eejeg4mspa8pdlx (10.0.0.3): 56 data bytes</span><br><span class="line">64 bytes from 10.0.0.3: seq=0 ttl=64 time=0.466 ms</span><br><span class="line">64 bytes from 10.0.0.3: seq=1 ttl=64 time=0.465 ms</span><br><span class="line">64 bytes from 10.0.0.3: seq=2 ttl=64 time=0.548 ms</span><br><span class="line">64 bytes from 10.0.0.3: seq=3 ttl=64 time=0.689 ms</span><br><span class="line">^C</span><br></pre></td></tr></table></figure><p>可以看到这两个跨主机的服务集群里面各个容器是可以互相连接的。</p><p>为了体现 Swarm 集群的优势,我们可以使用虚拟机的 ping 命令来测试对方虚拟机内的容器。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">$ docker-machine ssh worker2 ping helloworld.1.ay081uome3eejeg4mspa8pdlx</span><br><span class="line">PING helloworld.1.ay081uome3eejeg4mspa8pdlx (221.179.46.190): 56 data bytes</span><br><span class="line">64 bytes from 221.179.46.190: seq=0 ttl=63 time=48.651 ms</span><br><span class="line">64 bytes from 221.179.46.190: seq=1 ttl=63 time=63.239 ms</span><br><span class="line">64 bytes from 221.179.46.190: seq=2 ttl=63 time=47.686 ms</span><br><span class="line">64 bytes from 221.179.46.190: seq=3 ttl=63 time=61.232 ms</span><br><span class="line">^C</span><br><span class="line">$ docker-machine ssh manager1 ping helloworld.2.16cvore0c96rby1vp0sny3mvt</span><br><span class="line">PING helloworld.2.16cvore0c96rby1vp0sny3mvt (221.179.46.194): 56 data bytes</span><br><span class="line">64 bytes from 221.179.46.194: seq=0 ttl=63 time=30.150 ms</span><br><span class="line">64 bytes from 221.179.46.194: seq=1 ttl=63 time=54.455 ms</span><br><span class="line">64 bytes from 221.179.46.194: seq=2 ttl=63 time=73.862 ms</span><br><span class="line">64 bytes from 221.179.46.194: seq=3 ttl=63 time=53.171 ms</span><br><span class="line">^C</span><br></pre></td></tr></table></figure><p>上面我们使用了虚拟机内部的 ping 去测试容器的延迟,可以看到延迟明显比集群内部的 ping 值要高。</p><h2 id="Swarm-集群负载"><a href="#Swarm-集群负载" class="headerlink" title="Swarm 集群负载"></a>Swarm 集群负载</h2><p>现在我们已经学会了 Swarm 集群的部署方法,现在来搭建一个可访问的 Nginx 集群吧。体验最新版的 Swarm 所提供的自动服务发现与集群负载功能。<br>首先删掉上一节我们启动的 helloworld 服务:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ docker service rm helloworld</span><br><span class="line">helloworld</span><br></pre></td></tr></table></figure><p>然后在新建一个服务,提供端口映射参数,使得外界可以访问这些 Nginx 服务:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ docker service create --replicas 2 --name helloworld -p 7080:80 --network=swarm_test nginx:alpine</span><br><span class="line">9gfziifbii7a6zdqt56kocyun</span><br></pre></td></tr></table></figure><p>查看服务运行状态:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ docker service ls</span><br><span class="line">ID NAME REPLICAS IMAGE COMMAND</span><br><span class="line">9gfziifbii7a helloworld 2/2 nginx:alpine</span><br></pre></td></tr></table></figure><p>不知你有没有发现,虽然我们使用 –replicas 参数的值都是一样的,但是上一节中获取服务状态时,REPLICAS 返回的是 0/2,现在的 REPLICAS 返回的是 2/2。<br>同样使用 docker service ps 查看服务详细状态时(下面输出已经手动调整为更易读的格式),可以看到实例的 CURRENT STATE 中是 Running 状态的,而上一节中的 CURRENT STATE 中全部是处于 Preparing 状态。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ docker service ps helloworld</span><br><span class="line">ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR</span><br><span class="line">9ikr3agyi... helloworld.1 nginx:alpine user-pc Running Running 13 seconds ago </span><br><span class="line">7acmhj0u... helloworld.2 nginx:alpine worker2 Running Running 6 seconds ago</span><br></pre></td></tr></table></figure><p>这就涉及到 Swarm 内置的发现机制了,目前 Docker 1.12 中 Swarm 已经内置了服务发现工具,我们不再需要像以前使用 Etcd 或者 Consul 这些工具来配置服务发现。对于一个容器来说如果没有外部通信但又是运行中的状态会被服务发现工具认为是 Preparing 状态,本小节例子中因为映射了端口,因此有了 Running 状态。<br>现在我们来看 Swarm 另一个有趣的功能,当我们杀死其中一个节点时,会发生什么。<br>首先 kill 掉 worker2 的实例:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ docker-machine ssh worker2 docker <span class="built_in">kill</span> helloworld.2.7acmhj0udzusv1d7lu2tbuhu4</span><br><span class="line">helloworld.2.7acmhj0udzusv1d7lu2tbuhu4</span><br></pre></td></tr></table></figure><p>稍等几秒,再来看服务状态:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">$ docker service ps helloworld</span><br><span class="line">ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR</span><br><span class="line">9ikr3agyi... helloworld.1 nginx:alpine zuolan-pc Running Running 19 minutes ago </span><br><span class="line">8f866igpl... helloworld.2 nginx:alpine manager1 Running Running 4 seconds ago</span><br><span class="line">7acmhj0u... \_ helloworld.2 nginx:alpine worker2 Shutdown Failed 11 seconds ago ...<span class="built_in">exit</span>...</span><br><span class="line">$ docker service ls</span><br><span class="line">ID NAME REPLICAS IMAGE COMMAND</span><br><span class="line">9gfziifbii7a helloworld 2/2 nginx:alpine</span><br></pre></td></tr></table></figure><p>可以看到即使我们 kill 掉其中一个实例,Swarm 也会迅速把停止的容器撤下来,同时在节点中启动一个新的实例顶上来。这样服务依旧还是两个实例在运行。<br>此时如果你想添加更多实例可以使用 scale 命令:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ docker service scale helloworld=3</span><br><span class="line">helloworld scaled to 3</span><br></pre></td></tr></table></figure><p>查看服务详情,可以看到有三个实例启动了:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ docker service ps helloworld</span><br><span class="line">ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR</span><br><span class="line">9ikr3agyi... helloworld.1 nginx:alpine user-pc Running Running 30 minutes ago 8f866igpl... helloworld.2 nginx:alpine manager1 Running Running 11 minutes ago 7acmhj0u... \_ helloworld.2 nginx:alpine worker2 Shutdown Failed 11 minutes ago exit137</span><br><span class="line">1vexr1jm... helloworld.3 nginx:alpine worker2 Running Running 4 seconds ago</span><br></pre></td></tr></table></figure><p>现在如果想减少实例数量,一样可以使用 scale 命令:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ docker service scale helloworld=2</span><br><span class="line">helloworld scaled to 2</span><br></pre></td></tr></table></figure><p>至此,Swarm的主要用法都已经介绍完了,主要讲述了 Swarm 集群网络的创建与部署。介绍了 Swarm 的常规应用,包括 Swarm 的服务发现、负载均衡等,然后使用 Swarm 来配置跨主机容器网络,并在上面部署应用。</p><blockquote><p>转自: <a href="http://www.jianshu.com/p/9eb9995884a5" target="_blank" rel="noopener">http://www.jianshu.com/p/9eb9995884a5</a></p></blockquote>]]></content>
<summary type="html">
<blockquote>
<p>Swarm 在 Docker 1.12 版本之前属于一个独立的项目,在 Docker 1.12 版本发布之后,该项目合并到了 Docker 中,成为 Docker 的一个子命令。目前,Swarm 是 Docker 社区提供的唯一一个原生支持 Docker 集群管理的工具。它可以把多个 Docker 主机组成的系统转换为单一的虚拟 Docker 主机,使得容器可以组成跨主机的子网网络。</p>
</blockquote>
<h2 id="Swarm-认识"><a href="#Swarm-认识" class="headerlink" title="Swarm 认识"></a>Swarm 认识</h2><p>Swarm 是目前 Docker 官方唯一指定(绑定)的集群管理工具。Docker 1.12 内嵌了 swarm mode 集群管理模式。</p>
<p>为了方便演示跨主机网络,我们需要用到一个工具——Docker Machine,这个工具与 Docker Compose、Docker Swarm 并称 Docker 三剑客,下面我们来看看如何安装 Docker Machine:</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ curl -L https://github.com/docker/machine/releases/download/v0.9.0-rc2/docker-machine-`uname -s`-`uname -m` &gt;/tmp/docker-machine &amp;&amp;</span><br><span class="line"> chmod +x /tmp/docker-machine &amp;&amp;</span><br><span class="line"> sudo cp /tmp/docker-machine /usr/<span class="built_in">local</span>/bin/docker-machine</span><br></pre></td></tr></table></figure>
</summary>
<category term="容器技术" scheme="http://zhangfei.men/categories/%E5%AE%B9%E5%99%A8%E6%8A%80%E6%9C%AF/"/>
<category term="Docker" scheme="http://zhangfei.men/tags/Docker/"/>
<category term="Docker Swarm" scheme="http://zhangfei.men/tags/Docker-Swarm/"/>
</entry>
<entry>
<title>MyCLI:一个支持自动补全和语法高亮的MySQL客户端</title>
<link href="http://zhangfei.men/posts/2f03f1fe/"/>
<id>http://zhangfei.men/posts/2f03f1fe/</id>
<published>2017-06-07T09:24:06.000Z</published>
<updated>2020-07-28T01:08:16.453Z</updated>
<content type="html"><![CDATA[<p><img data-src="/images/pasted-19.png" alt="upload successful"><br>MyCLI 是一个易于使用的命令行客户端,可用于受欢迎的数据库管理系统 MySQL、MariaDB 和 Percona,支持自动补全和语法高亮。它是使用 prompt_toolkit 库写的,需要 Python 2.7、3.3、3.4、3.5 和 3.6 的支持。MyCLI 还支持通过 SSL 安全连接到 MySQL 服务器。</p><h2 id="MyCLI-的特性"><a href="#MyCLI-的特性" class="headerlink" title="MyCLI 的特性"></a>MyCLI 的特性</h2><ul><li>当你第一次使用它的时候,将会自动创建一个文件 ~/.myclirc。</li><li>当输入 SQL 的关键词和数据库中的表、视图和列时,支持自动补全。</li><li>默认情况下也支持智能补全,能根据上下文的相关性提供补全建议。</li></ul><p>比如:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">SELECT * FROM <Tab> - 这将显示出数据库中的表名。</span><br><span class="line">SELECT * FROM users WHERE <Tab> - 这将简单的显示出列名称。</span><br></pre></td></tr></table></figure><ul><li>通过使用 Pygents 支持语法高亮</li><li>支持 SSL 连接</li><li>提供多行查询支持</li><li>它可以将每一个查询和输出记录到一个文件中(默认情况下禁用)。</li><li>允许保存收藏一个查询(使用 \fs 别名 保存一个查询,并可使用 \f 别名 运行它)。</li><li>支持 SQL 语句执行和表查询计时</li><li>以更吸引人的方式打印表格数据</li></ul><h2 id="如何在-Linux-上为-MySQL-和-MariaDB-安装-MyCLI"><a href="#如何在-Linux-上为-MySQL-和-MariaDB-安装-MyCLI" class="headerlink" title="如何在 Linux 上为 MySQL 和 MariaDB 安装 MyCLI"></a>如何在 Linux 上为 MySQL 和 MariaDB 安装 MyCLI</h2><p>在 Debian/Ubuntu 发行版上,你可以很容易的像下面这样使用 apt 命令 来安装 MyCLI 包:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">sudo apt-get update</span><br><span class="line">sudo apt-get install mycli</span><br></pre></td></tr></table></figure><p>同样,在 Fedora 22+ 上也有 MyCLI 的可用包,你可以像下面这样使用 dnf 命令 来安装它:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo dnf install mycli</span><br></pre></td></tr></table></figure><p>对于其他 Linux 发行版,比如 RHEL/CentOS,你需要使用 Python 的 pip 工具来安装 MyCLI。首先,使用下面的命令来安装 pip:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ sudo yum install pip</span><br></pre></td></tr></table></figure><p>安装好 pip 以后,你可以像下面这样安装 MyCLI:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ sudo pip install mycli</span><br></pre></td></tr></table></figure><h2 id="在-Linux-中如何使用-MyCLI-连接-MySQL-和-MariaDB"><a href="#在-Linux-中如何使用-MyCLI-连接-MySQL-和-MariaDB" class="headerlink" title="在 Linux 中如何使用 MyCLI 连接 MySQL 和 MariaDB"></a>在 Linux 中如何使用 MyCLI 连接 MySQL 和 MariaDB</h2><p>安装好 MyCLI 以后,你可以像下面这样使用它:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ mycli -u root -h localhost</span><br></pre></td></tr></table></figure><h2 id="自动补全"><a href="#自动补全" class="headerlink" title="自动补全"></a>自动补全</h2><p>对于关键词和 SQL 函数可以进行简单的自动补全:</p><p><img data-src="/images/pasted-20.png" alt="upload successful"></p><h2 id="智能补全"><a href="#智能补全" class="headerlink" title="智能补全"></a>智能补全</h2><p>当输入 FROM 关键词以后会进行表名称的补全:</p><p><img data-src="/images/pasted-21.png" alt="upload successful"></p><h2 id="别名支持"><a href="#别名支持" class="headerlink" title="别名支持"></a>别名支持</h2><p>当表的名称设置别名以后,也支持列名称的补全:</p><p><img data-src="/images/pasted-22.png" alt="upload successful"></p><h2 id="语法高亮"><a href="#语法高亮" class="headerlink" title="语法高亮"></a>语法高亮</h2><p>支持 MySQL 语法高亮:</p><p><img data-src="/images/pasted-23.png" alt="upload successful"></p><h2 id="格式化-SQL-的输出"><a href="#格式化-SQL-的输出" class="headerlink" title="格式化 SQL 的输出"></a>格式化 SQL 的输出</h2><p>MySQL 的输出会通过 less 命令[1] 进行格式化输出:</p><p><img data-src="/images/pasted-24.png" alt="upload successful"></p><p>要登录 MySQL 并同时选择数据库,你可以使用和下面类似的命令:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ mycli local_database</span><br><span class="line">$ mycli -h localhost -u root app_db</span><br><span class="line">$ mycli mysql://amjith@localhost:3306/django_poll</span><br></pre></td></tr></table></figure><p>更多使用选项,请输入:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ mycli --<span class="built_in">help</span></span><br></pre></td></tr></table></figure><blockquote><p>MyCLI 主页: <a href="http://mycli.net/index" target="_blank" rel="noopener">http://mycli.net/index</a></p></blockquote>]]></content>
<summary type="html">
<p><img data-src="/images/pasted-19.png" alt="upload successful"><br>MyCLI 是一个易于使用的命令行客户端,可用于受欢迎的数据库管理系统 MySQL、MariaDB 和 Percona,支持自动补全和语法高亮。它是使用 prompt_toolkit 库写的,需要 Python 2.7、3.3、3.4、3.5 和 3.6 的支持。MyCLI 还支持通过 SSL 安全连接到 MySQL 服务器。</p>
<h2 id="MyCLI-的特性"><a href="#MyCLI-的特性" class="headerlink" title="MyCLI 的特性"></a>MyCLI 的特性</h2><ul>
<li>当你第一次使用它的时候,将会自动创建一个文件 ~/.myclirc。</li>
<li>当输入 SQL 的关键词和数据库中的表、视图和列时,支持自动补全。</li>
<li>默认情况下也支持智能补全,能根据上下文的相关性提供补全建议。</li>
</ul>
<p>比如:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">SELECT * FROM &lt;Tab&gt; - 这将显示出数据库中的表名。</span><br><span class="line">SELECT * FROM users WHERE &lt;Tab&gt; - 这将简单的显示出列名称。</span><br></pre></td></tr></table></figure>
</summary>
<category term="数据库" scheme="http://zhangfei.men/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
<category term="MySQL" scheme="http://zhangfei.men/tags/MySQL/"/>
<category term="Shell" scheme="http://zhangfei.men/tags/Shell/"/>
</entry>
<entry>
<title>你应该知道的5个Docker实用工具</title>
<link href="http://zhangfei.men/posts/112e552d/"/>
<id>http://zhangfei.men/posts/112e552d/</id>
<published>2017-05-27T05:44:37.000Z</published>
<updated>2020-07-28T01:08:16.457Z</updated>
<content type="html"><![CDATA[<p>【摘要】网上有很多不错的Docker工具,大部分在github上都是开源的。最近两年,我一直在使用Docker,并将其应用到了一些开发项目上。如果你刚开始使用Docker,你会发现它能应用到的实例远远多于预想。Docker能为你做更多,不会让你失望的!</p><p>Docker社区非常活跃,每天都有许多新的实用工具出现。因此,天天去检查更新,试图跟上社区的步伐确实有点困难。所以我在此分享在工作中收集到的一些有趣而实用的Docker工具,帮助大家提高日常工作效率。</p><p>下面开始一一介绍我在使用Docker的过程中找到的有用工具吧。</p><h3 id="watchtower:自动更新Docker容器"><a href="#watchtower:自动更新Docker容器" class="headerlink" title="watchtower:自动更新Docker容器"></a>watchtower:自动更新Docker容器</h3><p>watchtower监视容器运行过程,并且能够捕捉到容器中的变化。当watchtower检测到有镜像发生变化,会自动使用新镜像重启容器。我在本地开发环境中创建的最后一个镜像就用到了watchtower。</p><p>watchtower本身就像一个Docker镜像,所以它启动容器的方式和别的镜像无异。运行watchtower的命令如下:</p><p><img data-src="/images/pasted-4.png" alt="upload successful"></p><p>上面的代码中,我们用到了一个安装文件/var/run/docker.sock。这个文件主要用来使watchtower与Docker后台API交互。 interval30秒的选项主要用来定义watchtower的轮询间隔时间。watchtower还支持一些别的选项,具体可以查看他们的文档。</p><p>现在,开启一个容器,用watchtower来监控。</p><p><img data-src="/images/pasted-8.png" alt="upload successful"></p><p>watchtower会开始监控friendlyhello容器。接下来我把新镜像push到Docker Hub,watchtower接下来就会检测到有新镜像可用。它会关掉容器,然后用新镜像重启容器。这里会用到我们刚刚传到运行命令中的选项,换句话说,容器会在4000:80 公共端口选项上开启。</p><p>默认情况下,watchtower会轮询Dockder Hub注册表查找更新的镜像。你也可以通过在环境变量REPO_USER和REPO_PASS中添加指定注册表证书,来设置watchtower轮询私有注册表。</p><p>了解更多watchtower的用法,我推荐watchtower文档。</p><h3 id="docker-gc:收集垃圾容器和镜像"><a href="#docker-gc:收集垃圾容器和镜像" class="headerlink" title="docker-gc:收集垃圾容器和镜像"></a>docker-gc:收集垃圾容器和镜像</h3><p>docker-gc工具能够帮助Docker host清理不需要的容器和镜像。它可以删除存在一小时以上的容器。同时,它也可以删除没有容器的镜像。</p><p>docker-gc可以被当做脚本,也可以被视为容器。我们用容器方法运行docker-gc,用它来查找可以被删除的容器和镜像。</p><p><img data-src="/images/pasted-9.png" alt="upload successful"></p><p>在上述命令中,我们安装Docker socket文件,这样docker-gc就可以和Docker API进行交互。设置环境变量DRY_RUN=1,查找可被删除的容器和镜像。如果我们不这样设置,docker-gc直接删除它们。所以在删除之前,还是先确认一下。以上代码的输出结果如下:</p><p><img data-src="/images/pasted-10.png" alt="upload successful"></p><p>确认需要删除的容器和镜像之后,再次运行docker-gc来进行删除清理,这次就不用再设置DRY_RUN参数了。</p><p><img data-src="/images/pasted-11.png" alt="upload successful"></p><p>上述命令运行后的输出会告诉你哪些容器和镜像已经被docker-gc删除。</p><p>了解更多docker-gc支持的选项,我推荐阅读docker-gc documentation。</p><h3 id="docker-slim:给你的容器瘦身"><a href="#docker-slim:给你的容器瘦身" class="headerlink" title="docker-slim:给你的容器瘦身"></a>docker-slim:给你的容器瘦身</h3><p>如果你对Docker镜像的大小有过担忧,docker-slim绝对是一丸灵丹妙药。</p><p>docker-slim工具可以通过静态和动态分析,针对你的“胖镜像”创建对应的“瘦镜像”。在Github上下载二进制文件,即可使用docker-slim。该二进制文件在Linux和Mac可用。下载之后添加到路径PATH。</p><p>我创建了一个Docker镜像示例应用“friendlyhello”,Docker官方文档中有用到。这个镜像的大小如下图所示,194MB。</p><p><img data-src="/images/pasted-12.png" alt="upload successful"></p><p>这么简单的一个应用,我们就要下载194MB的数据。再来看看docker-slim究竟能让它“瘦”多少。</p><p><img data-src="/images/pasted-13.png" alt="upload successful"></p><p>docker-slim工具先是对“胖镜像”进行一系列的检测,最终创建了对应的“瘦镜像”。看一下“瘦镜像”的大小:</p><p><img data-src="/images/pasted-14.png" alt="upload successful"></p><p>正如上图所示,“瘦镜像”大小为24.9MB。开启容器,运行照旧。docker-slim对java、python、ruby、和Node.js应用都非常友好。</p><p>你自己也试一下吧,看看结果如何。以我个人的项目来说,我认为docker-slim在大部分情况下都能适用。阅读docker-slim文档了解更多。</p><h3 id="rocker:打破Dockerfile限制"><a href="#rocker:打破Dockerfile限制" class="headerlink" title="rocker:打破Dockerfile限制"></a>rocker:打破Dockerfile限制</h3><p>很多Docker用户都用Dockerfile来构建镜像。Dockerfile是定义命令的声明方式,通过在命令行调用这些命令,可以对镜像进行操作。</p><p>rocker给Dockerfile的指令集增加了新的指令。rocker是由Grammaryly创建的,原意是用来解决Dockerfile格式的问题。Grammaryly团队写过一篇博客解释当初的动机。我建议你也看一下这篇博客,可以更好的理解rocker。他们在博客中提出的两个关键问题是:</p><p>Docker镜像的大小<br>构建速度缓慢<br>博客还提到了rocker添加的一些新指令。查看rocker文档了解更多。</p><p>MOUNT用来分享volume,这样依赖管理工具就可以重用。<br>FROM指令在Dockerfile中也存在。rocker添加了不止一条FROM指令。这就意味着,一个Rockerfile可以通过创建多个镜像。首个指令集使用所有依赖来创建artifact,第二个指令集可以使用已有的artifact。这种做法极大的降低了镜像的大小。<br>TAG用来标记处于不同构建阶段的镜像。这样一来就不在需要手动标记镜像了。<br>PUSH用来把镜像push到registry。<br>ATTACH用来和中间步骤交互,在debug的时候非常有用。<br>安装rocker,对Mac用户来说,只要运行几条brew命令就行了:</p><p><img data-src="/images/pasted-15.png" alt="upload successful"></p><p>安装完成后,就可以使用rocker创建镜像。</p><p><img data-src="/images/pasted-16.png" alt="upload successful"></p><p>创建镜像并将其push到Docker Hub,可以用下面这条命令:</p><p><img data-src="/images/pasted-17.png" alt="upload successful"></p><p>rocker功能十分完备,了解更多,请参阅其文档。</p><h3 id="ctop:容器的顶层界面工具"><a href="#ctop:容器的顶层界面工具" class="headerlink" title="ctop:容器的顶层界面工具"></a>ctop:容器的顶层界面工具</h3><p>ctop是我最近才开始使用的工具,它可以为多个容器提供实时显示的数据视图。如果你是Mac用户,可以按下面的命令安装ctop。</p><p><img data-src="/images/pasted-18.png" alt="upload successful"></p><p>安装之后,只需配置DOCKER_HOST环境变量,即可使用ctop。</p><p>运行ctop命令,可以查看所有容器的状态。</p><p>运行</p><p>ctop-a命令,可以仅查看当前运行的容器。</p><p>ctop简单好用,查看机器上运行的容器非常方便。了解更多,请看ctop文档。</p>]]></content>
<summary type="html">
<p>【摘要】网上有很多不错的Docker工具,大部分在github上都是开源的。最近两年,我一直在使用Docker,并将其应用到了一些开发项目上。如果你刚开始使用Docker,你会发现它能应用到的实例远远多于预想。Docker能为你做更多,不会让你失望的!</p>
<p>Docker社区非常活跃,每天都有许多新的实用工具出现。因此,天天去检查更新,试图跟上社区的步伐确实有点困难。所以我在此分享在工作中收集到的一些有趣而实用的Docker工具,帮助大家提高日常工作效率。</p>
<p>下面开始一一介绍我在使用Docker的过程中找到的有用工具吧。</p>
<h3 id="watchtower:自动更新Docker容器"><a href="#watchtower:自动更新Docker容器" class="headerlink" title="watchtower:自动更新Docker容器"></a>watchtower:自动更新Docker容器</h3><p>watchtower监视容器运行过程,并且能够捕捉到容器中的变化。当watchtower检测到有镜像发生变化,会自动使用新镜像重启容器。我在本地开发环境中创建的最后一个镜像就用到了watchtower。</p>
</summary>
<category term="工具" scheme="http://zhangfei.men/categories/%E5%B7%A5%E5%85%B7/"/>
<category term="Docker" scheme="http://zhangfei.men/tags/Docker/"/>
<category term="Tool" scheme="http://zhangfei.men/tags/Tool/"/>
</entry>
<entry>
<title>Docker的Secrets管理</title>
<link href="http://zhangfei.men/posts/b8062d72/"/>
<id>http://zhangfei.men/posts/b8062d72/</id>
<published>2017-05-18T12:41:58.000Z</published>
<updated>2020-07-28T01:08:16.453Z</updated>
<content type="html"><![CDATA[<p>我相信当我们意识到重要且敏感的访问信息已经暴露到公共网络上,并可能使您的微服务无条件被访问。随着我们依赖于的开发出来的服务化的量不断增加, 这时跟踪敏感细节的数量也有所增加。为了应对这个问题,在“secrets managemen”领域出现了工具。</p><p>在这篇文章中,我们将看Docker Secrets,要求在Docker 1.13及更高版本的新秘密管理功能。</p><p>从Docker的角度来看,该功能不需要太多的工作,但是您可能需要重构应用程序以利用它。我们将介绍如何做到这一点的想法,但不是详细的。</p><p>Docker的 Secrets只适用于Docker群集,主要是因为这是秘密管理最有意义的领域。毕竟,Swarm是针对多个Docker实例需要在他们之间共享访问细节的生产用途。如果要在独立容器中使用秘密管理,则需要运行</p><p>scale值设置为1 的容器。适用于Mac和Windows的Docker不支持多节点群集模式,但您可以使用它们使用Docker Machine创建多节点群集。</p><p>创建两个机器,然后创建一个两个节点,并从该组中的一个swarm环境中运行本文中的案例。</p><h3 id="获得Secrets"><a href="#获得Secrets" class="headerlink" title="获得Secrets"></a>获得Secrets</h3><p>当您从命令行创建Secrets时,您可以使用所有可用的工具来创建随机密码和管道输出。例如,为数据库用户创建一个随机密码:</p><p>opensslrand-base6420|dockersecretcreatemariadb_password-</p><p>这将返回一个秘密的ID。</p><p>您需要再次发出此命令以生成MariaDB root用户的密码。您将需要这样才能开始使用,但您不需要为每项服务。</p><p>opensslrand-base6420|dockersecretcreatemariadb_root_password-</p><p>如果你已经忘记了你创建的秘密, 可以用ls查看,也可以用以下命令查看docker secret ls</p><h3 id="替换secrets"><a href="#替换secrets" class="headerlink" title="替换secrets"></a>替换secrets</h3><p>为了保持秘密,良好的秘密,服务之间的通信发生在您定义的覆盖网络中。它们只能通过调用其ID来在该覆盖网络中使用。</p><p>dockernetworkcreate-doverlaymariadb_private</p><p>这也将返回该网络的ID。再次,你可以docker network ls查看相关网络</p><h3 id="创建服务"><a href="#创建服务" class="headerlink" title="创建服务"></a>创建服务</h3><p>这个例子将有一个Docker节点运行MariaDB,一个运行Python的节点。在最终的应用程序中,Python应用程序将读取和写入数据库。</p><p>首先,添加一个MariaDB服务。此服务使用您创建的网络进行通信,之前创建的秘密保存为两个文件:一个用于根密码,一个用于默认用户密码。然后将所需的所有变量作为环境变量传递给服务。</p><p>dockerservicecreate\ –namemariadb\ –replicas1\ –networkmariadb_private\ –mounttype=volume,source=mydata,destination=/var/lib/mariadb\ –secretsource=mariadb_root_password,target=mariadb_root_password\ –secretsource=mariadb_password,target=mariadb_password\ -eMARIADB_ROOT_PASSWORD_FILE=”/run/secrets/mariadb_root_password”\ -eMARIADB_PASSWORD_FILE=”/run/secrets/mariadb_password”\ -eMARIADB_USER=”python”\ -eMARIADB_DATABASE=”python”\</p><p>Python实例再次使用您创建的专用网络,并复制网络中可访问的秘密。一个更好的(生产就绪的)选项将是创建您的应用程序在管理程序中需要的数据库,而不会给应用程序访问根密码,但这仅仅是一个例子。</p><p>dockerservicecreate\ –namecspython\ –replicas1\ –networkmariadb_private\ –publish50000:5000\ –mounttype=volume,source=pydata,destination=/var/www/html\ –secretsource=mariadb_root_password,target=python_root_password,mode=0400\ –secretsource=mariadb_password,target=python_password,mode=0400\ -ePYTHON_DB_USER=”python”\ -ePYTHON_DB_ROOT_PASSWORD_FILE=”/run/secrets/python_root_password”\ -ePYTHON_DB_PASSWORD_FILE=”/run/secrets/python_password”\ -ePYTHON_DB_HOST=”mariadb:3306”\ -ePYTHON_DB_NAME=”python”\</p><p>上面的示例使用我创建的一个简单的Docker映像,它设置用于使用Flask创建Web应用程序的软件包,用于提供Web页面和PyMySQL来进行数据库访问。代码没有做太多,但显示了如何从Docker容器访问环境变量。</p><p>例如,要连接到没有指定数据库的数据库服务器:</p><p>importos importMySQLdb db=MySQLdb.connect(host=os.environ[‘PYTHON_DB_HOST’], user=os.environ[‘PYTHON_DB_ROOT_USER’], passwd=os.environ[‘PYTHON_DB_PASSWORD_FILE’]) cur=db.cursor() print(db) db.close()</p><p>更新secrets</p><p>频繁更改敏感信息是个好习惯。但是,您可能知道,在应用程序中更新这些细节是一个沉闷的过程,最不愿意避免。通过服务,Docker Secrets管理允许您更改值,而无需更改代码。</p><p>创建一个新秘密:</p><p>opensslrand-base6420|dockersecretcreatemariadb_password_march-</p><p>从MariaDB服务中删除当前密码的访问权限:</p><p>dockerserviceupdate\ –secret-rmmariadb_password\</p><p>并让它访问新的秘密,将目标指向新的值:</p><p>dockerserviceupdate\ –secret-addsource=mariadb_password_march,target=mysql_password\</p><p>更新Python服务:</p><p>dockerserviceupdate\ –secret-rmmariadb_password\ –secret-addsource=mariadb_password_march,target=python_password,mode=0400\</p><p>并删除旧秘密:</p><p>dockersecretrmmariadb_password</p><h3 id="扩展说明"><a href="#扩展说明" class="headerlink" title="扩展说明"></a>扩展说明</h3><p>Docker Secrets是一个新功能,但Docker鼓励镜像维护人员尽快为Docker用户提供更好的安全性。这需要允许与上述示例类似的过程,其中容器可以从通过生成秘密而不是硬编码到应用中创建的文件来读取其需要的每个参数。这可以强制实施集装箱应用程序,因为容器可以来回走动,但是始终可以访问您的应用程序运行所需的重要信息。</p>]]></content>
<summary type="html">
<p>我相信当我们意识到重要且敏感的访问信息已经暴露到公共网络上,并可能使您的微服务无条件被访问。随着我们依赖于的开发出来的服务化的量不断增加, 这时跟踪敏感细节的数量也有所增加。为了应对这个问题,在“secrets managemen”领域出现了工具。</p>
<p>在这篇文章中,我们将看Docker Secrets,要求在Docker 1.13及更高版本的新秘密管理功能。</p>
<p>从Docker的角度来看,该功能不需要太多的工作,但是您可能需要重构应用程序以利用它。我们将介绍如何做到这一点的想法,但不是详细的。</p>
<p>Docker的 Secrets只适用于Docker群集,主要是因为这是秘密管理最有意义的领域。毕竟,Swarm是针对多个Docker实例需要在他们之间共享访问细节的生产用途。如果要在独立容器中使用秘密管理,则需要运行</p>
<p>scale值设置为1 的容器。适用于Mac和Windows的Docker不支持多节点群集模式,但您可以使用它们使用Docker Machine创建多节点群集。</p>
</summary>
<category term="容器技术" scheme="http://zhangfei.men/categories/%E5%AE%B9%E5%99%A8%E6%8A%80%E6%9C%AF/"/>
<category term="Docker" scheme="http://zhangfei.men/tags/Docker/"/>
<category term="Secret" scheme="http://zhangfei.men/tags/Secret/"/>
</entry>
<entry>
<title>四个Kubernetes集群管理工具</title>
<link href="http://zhangfei.men/posts/4fa5d2db/"/>
<id>http://zhangfei.men/posts/4fa5d2db/</id>
<published>2017-05-18T12:33:40.000Z</published>
<updated>2020-07-28T01:08:16.457Z</updated>
<content type="html"><![CDATA[<p>几乎所有用过Kubernetes的人都会发现其缺点,随着大K在负载平衡和工作管理方面的重大改进,用户可以将注意力逐渐转移到其他地方了,这里有四个项目可以减轻Kubernetes集群管理的负载。</p><p><img data-src="http://p1.pstatp.com/large/212f0004094fad2d1aa6" alt="Kubernetes"></p><h2 id="Kube-applier"><a href="#Kube-applier" class="headerlink" title="Kube-applier"></a>Kube-applier</h2><p>Kubernetes成功的关键是其与除Google以外的IT厂商和产品的接触。云存储公司Box收购了Kubernetes,并开放了一些用于帮助其内部部署的项目,kube-applier就是这样一个项目。</p><p>作为Kubernetes服务运行的Kube-applier,为Gube仓库中托管的Kubernetes集群提供了一组声明性配置文件,并将其持续应用于集群中的pod。无论何时对定义文件进行任何更改,它们都将被自动提取并应用于相关的pod。</p><p>更改也可以按计划或按需应用。Kube应用程序每次运行时都会记录其行为,并提供与Prometheus兼容的指标,以便用户及时了解影响集群的行为。</p><h2 id="Kubetop"><a href="#Kubetop" class="headerlink" title="Kubetop"></a>Kubetop</h2><p>有时最简单的工具反而是最有用的,比如Kubetop,它用Python编写,Kubetop会列出所有当前运行的节点,这些节点上所有的pod,这些pod中的所有容器,每个节点的CPU和内存利用率,类似于Unix/Linux top的命令。它不应该用来替代更精细的日志记录或报告工具,因为它产生的信息太简单了,但有时候简单会让阅读Kubernetes集群报告更节省时间。</p><p>如果您只需要快速了解哪些因素和命令行影响了集群,这是一个很方便的选项。Kubernetes的kubectl也有类似的功能,但是Kubetop的输出格式更加整齐。</p><h2 id="Kubectx-K8senv"><a href="#Kubectx-K8senv" class="headerlink" title="Kubectx/K8senv"></a>Kubectx/K8senv</h2><p>Kubernetes有一个“上下文”的概念,用于引用具有不同配置数据的离散集群。用kubectl命令行工具在上下文之间切换可能是冗长和笨拙的,所以第三方提出了在flash中切换上下文的方法。</p><p>一个简单的shell脚本,Kubectx可以为Kubernetes上下文分配短名称,并使用短名称在它们之间切换。将破折号(-)传递给kubectx,将被切换回以前的内容,而无需记住名称。该脚本还支持完成名称的选项卡,因此用户不必挖掘长名称并手动重新键入。</p><p>另外一个shell脚本K8senv要简单得多,但功能远远不够强大。例如,它不能在当前和最后一个上下文之间进行翻转。</p><h2 id="kubeadm-dind-cluster"><a href="#kubeadm-dind-cluster" class="headerlink" title="kubeadm-dind-cluster"></a>kubeadm-dind-cluster</h2><p>如果你想启动一个本地的单节点Kubernetes实例进行测试,那么Kubernetes提供了一个很好的默认组件:Minikube。但是对于那些想要测试和开发多节点集群Kubernetes的人还有一个选择:Mirantis的kubeadm-dind-cluster(KDC)。</p><p>KDC通过使用Kubernetes的kubeadm应用程序来启动由Docker容器而不是VM组成的集群。这可以让您在使用Kubernetes时更快地重新启动集群,因此可以更快速地查看任何代码更改造成的影响,也可以在持续集成环境中使用KDC,而不会遇到嵌套虚拟化问题。KDC运行跨平台的Linux,MacOS,Windows,并且不需要Go安装,因为它使用了Dockerized构建的Kubernetes。</p>]]></content>
<summary type="html">
<p>几乎所有用过Kubernetes的人都会发现其缺点,随着大K在负载平衡和工作管理方面的重大改进,用户可以将注意力逐渐转移到其他地方了,这里有四个项目可以减轻Kubernetes集群管理的负载。</p>
<p><img data-src="http://p1.pstatp.com/large/212f0004094fad2d1aa6" alt="Kubernetes"></p>
<h2 id="Kube-applier"><a href="#Kube-applier" class="headerlink" title="Kube-applier"></a>Kube-applier</h2><p>Kubernetes成功的关键是其与除Google以外的IT厂商和产品的接触。云存储公司Box收购了Kubernetes,并开放了一些用于帮助其内部部署的项目,kube-applier就是这样一个项目。</p>
<p>作为Kubernetes服务运行的Kube-applier,为Gube仓库中托管的Kubernetes集群提供了一组声明性配置文件,并将其持续应用于集群中的pod。无论何时对定义文件进行任何更改,它们都将被自动提取并应用于相关的pod。</p>
</summary>
<category term="工具" scheme="http://zhangfei.men/categories/%E5%B7%A5%E5%85%B7/"/>
<category term="Docker" scheme="http://zhangfei.men/tags/Docker/"/>
<category term="Kubernetes" scheme="http://zhangfei.men/tags/Kubernetes/"/>
</entry>
<entry>
<title>Java 9 中的 9 个新特性</title>
<link href="http://zhangfei.men/posts/d1d33620/"/>
<id>http://zhangfei.men/posts/d1d33620/</id>
<published>2017-05-16T14:52:00.000Z</published>
<updated>2020-07-28T01:08:16.453Z</updated>
<content type="html"><![CDATA[<h2 id="Java-9-中的-9-个新特性"><a href="#Java-9-中的-9-个新特性" class="headerlink" title="Java 9 中的 9 个新特性"></a>Java 9 中的 9 个新特性</h2><p><img data-src="/images/pasted-0.png" alt="upload successful"></p><p>Java 8 发布三年多之后,即将快到2017年7月下一个版本发布的日期了。 你可能已经听说过 Java 9 的模块系统,但是这个新版本还有许多其它的更新。 这里有九个令人兴奋的新功能将与 Java 9 一起发布。</p><h3 id="Java-平台级模块系统"><a href="#Java-平台级模块系统" class="headerlink" title="Java 平台级模块系统"></a>Java 平台级模块系统</h3><p>Java 9 的定义功能是一套全新的模块系统。当代码库越来越大,创建复杂,盘根错节的“意大利面条式代码”的几率呈指数级的增长。这时候就得面对两个基础的问题: 很难真正地对代码进行封装, 而系统并没有对不同部分(也就是 JAR 文件)之间的依赖关系有个明确的概念。每一个公共类都可以被类路径之下任何其它的公共类所访问到, 这样就会导致无意中使用了并不想被公开访问的 API。此外,类路径本身也存在问题: 你怎么知晓所有需要的 JAR 都已经有了, 或者是不是会有重复的项呢? 模块系统把这俩个问题都给解决了。</p><p>模块化的 JAR 文件都包含一个额外的模块描述器。在这个模块描述器中, 对其它模块的依赖是通过 “requires” 来表示的。另外, “exports” 语句控制着哪些包是可以被其它模块访问到的。所有不被导出的包默认都封装在模块的里面。如下是一个模块描述器的示例,存在于 “module-info.java” 文件中:</p><p>module blog {<br>我们可以如下展示模块:</p><p><img data-src="/images/pasted-1.png" alt="upload successful"></p><p>请注意,两个模块都包含封装的包,因为它们没有被导出(使用橙色盾牌可视化)。 没有人会偶然地使用来自这些包中的类。Java 平台本身也使用自己的模块系统进行了模块化。通过封装 JDK 的内部类,平台更安全,持续改进也更容易。</p><p>当启动一个模块化应用时, JVM 会验证是否所有的模块都能使用,这基于 <code>requires</code> 语句——比脆弱的类路径迈进了一大步。模块允许你更好地强制结构化封装你的应用并明确依赖。你可以在这个课程中学习更多关于 Java 9 中模块工作的信息 。</p><h3 id="Linking"><a href="#Linking" class="headerlink" title="Linking"></a>Linking</h3><p>当你使用具有显式依赖关系的模块和模块化的 JDK 时,新的可能性出现了。你的应用程序模块现在将声明其对其他应用程序模块的依赖以及对其所使用的 JDK 模块的依赖。为什么不使用这些信息创建一个最小的运行时环境,其中只包含运行应用程序所需的那些模块呢? 这可以通过 Java 9 中的新的 jlink 工具实现。你可以创建针对应用程序进行优化的最小运行时映像而不需要使用完全加载 JDK 安装版本。</p><h3 id="JShell-交互式-Java-REPL"><a href="#JShell-交互式-Java-REPL" class="headerlink" title="JShell: 交互式 Java REPL"></a>JShell: 交互式 Java REPL</h3><p>许多语言已经具有交互式编程环境,Java 现在加入了这个俱乐部。您可以从控制台启动 jshell ,并直接启动输入和执行 Java 代码。 jshell 的即时反馈使它成为探索 API 和尝试语言特性的好工具。</p><p><img data-src="/images/pasted-2.png" alt="upload successful"></p><p>测试一个 Java 正则表达式是一个很好的说明 jshell 如何使您的生活更轻松的例子。 交互式 shell 还可以提供良好的教学环境以及提高生产力,您可以在此了解更多信息。在教人们如何编写 Java 的过程中,不再需要解释 “public static void main(String [] args)” 这句废话。</p><h3 id="改进的-Javadoc"><a href="#改进的-Javadoc" class="headerlink" title="改进的 Javadoc"></a>改进的 Javadoc</h3><p>有时一些小事情可以带来很大的不同。你是否就像我一样在一直使用 Google 来查找正确的 Javadoc 页面呢? 这不再需要了。Javadoc 现在支持在 API 文档中的进行搜索。另外,Javadoc 的输出现在符合兼容 HTML5 标准。此外,你会注意到,每个 Javadoc 页面都包含有关 JDK 模块类或接口来源的信息。</p><p><img data-src="/images/pasted-3.png" alt="upload successful"></p><h3 id="集合工厂方法"><a href="#集合工厂方法" class="headerlink" title="集合工厂方法"></a>集合工厂方法</h3><p>通常,您希望在代码中创建一个集合(例如,List 或 Set ),并直接用一些元素填充它。 实例化集合,几个 “add” 调用,使得代码重复。 Java 9,添加了几种集合工厂方法:</p><p>Set<Integer> ints = Set.of(1, 2, 3);List<String> strings = List.of(“first”, “second”);<br>除了更短和更好阅读之外,这些方法也可以避免您选择特定的集合实现。 事实上,从工厂方法返回已放入数个元素的集合实现是高度优化的。这是可能的,因为它们是不可变的:在创建后,继续添加元素到这些集合会导致 “UnsupportedOperationException” 。</p><h3 id="改进的-Stream-API"><a href="#改进的-Stream-API" class="headerlink" title="改进的 Stream API"></a>改进的 Stream API</h3><p>长期以来,Stream API 都是 Java 标准库最好的改进之一。通过这套 API 可以在集合上建立用于转换的申明管道。在 Java 9 中它会变得更好。Stream 接口中添加了 4 个新的方法:dropWhile, takeWhile, ofNullable。还有个 iterate 方法的新重载方法,可以让你提供一个 Predicate (判断条件)来指定什么时候结束迭代:</p><p>IntStream.iterate(1, i -> i < 100, i -> i + 1).forEach(System.out::println);<br>第二个参数是一个 Lambda,它会在当前 IntStream 中的元素到达 100 的时候返回 true。因此这个简单的示例是向控制台打印 1 到 99。</p><p>除了对 Stream 本身的扩展,Optional 和 Stream 之间的结合也得到了改进。现在可以通过 Optional 的新方法 <code>stram</code> 将一个 Optional 对象转换为一个(可能是空的) Stream 对象:</p><p>Stream<Integer> s = Optional.of(1).stream();<br>在组合复杂的 Stream 管道时,将 Optional 转换为 Stream 非常有用。</p><h3 id="私有接口方法"><a href="#私有接口方法" class="headerlink" title="私有接口方法"></a>私有接口方法</h3><p>Java 8 为我们带来了接口的默认方法。 接口现在也可以包含行为,而不仅仅是方法签名。 但是,如果在接口上有几个默认方法,代码几乎相同,会发生什么情况? 通常,您将重构这些方法,调用一个可复用的私有方法。 但默认方法不能是私有的。 将复用代码创建为一个默认方法不是一个解决方案,因为该辅助方法会成为公共API的一部分。 使用 Java 9,您可以向接口添加私有辅助方法来解决此问题:</p><p>public interface MyInterface {<br>如果您使用默认方法开发 API ,那么私有接口方法可能有助于构建其实现。</p><h3 id="HTTP-2"><a href="#HTTP-2" class="headerlink" title="HTTP/2"></a>HTTP/2</h3><p>Java 9 中有新的方式来处理 HTTP 调用。这个迟到的特性用于代替老旧的 <code>HttpURLConnection</code> API,并提供对 WebSocket 和 HTTP/2 的支持。注意:新的 HttpClient API 在 Java 9 中以所谓的孵化器模块交付。也就是说,这套 API 不能保证 100% 完成。不过你可以在 Java 9 中开始使用这套 API:</p><p>HttpClient client = HttpClient.newHttpClient();HttpRequest req =<br>除了这个简单的请求/响应模型之外,HttpClient 还提供了新的 API 来处理 HTTP/2 的特性,比如流和服务端推送。</p><h3 id="多版本兼容-JAR"><a href="#多版本兼容-JAR" class="headerlink" title="多版本兼容 JAR"></a>多版本兼容 JAR</h3><p>我们最后要来着重介绍的这个特性对于库的维护者而言是个特别好的消息。当一个新版本的 Java 出现的时候,你的库用户要花费数年时间才会切换到这个新的版本。这就意味着库得去向后兼容你想要支持的最老的 Java 版本 (许多情况下就是 Java 6 或者 7)。这实际上意味着未来的很长一段时间,你都不能在库中运用 Java 9 所提供的新特性。幸运的是,多版本兼容 JAR 功能能让你创建仅在特定版本的 Java 环境中运行库程序时选择使用的 class 版本:</p><p>multirelease.jar<br>在上述场景中, multirelease.jar 可以在 Java 9 中使用, 不过 Helper 这个类使用的不是顶层的multirelease.Helper 这个 class, 而是处在“META-INF/versions/9”下面的这个。这是特别为 Java 9 准备的 class 版本,可以运用 Java 9 所提供的特性和库。同时,在早期的 Java 诸版本中使用这个 JAR 也是能运行的,因为较老版本的 Java 只会看到顶层的这个 Helper 类。</p>]]></content>
<summary type="html">
<h2 id="Java-9-中的-9-个新特性"><a href="#Java-9-中的-9-个新特性" class="headerlink" title="Java 9 中的 9 个新特性"></a>Java 9 中的 9 个新特性</h2><p><img data-src="/images/pasted-0.png" alt="upload successful"></p>
<p>Java 8 发布三年多之后,即将快到2017年7月下一个版本发布的日期了。 你可能已经听说过 Java 9 的模块系统,但是这个新版本还有许多其它的更新。 这里有九个令人兴奋的新功能将与 Java 9 一起发布。</p>
<h3 id="Java-平台级模块系统"><a href="#Java-平台级模块系统" class="headerlink" title="Java 平台级模块系统"></a>Java 平台级模块系统</h3><p>Java 9 的定义功能是一套全新的模块系统。当代码库越来越大,创建复杂,盘根错节的“意大利面条式代码”的几率呈指数级的增长。这时候就得面对两个基础的问题: 很难真正地对代码进行封装, 而系统并没有对不同部分(也就是 JAR 文件)之间的依赖关系有个明确的概念。每一个公共类都可以被类路径之下任何其它的公共类所访问到, 这样就会导致无意中使用了并不想被公开访问的 API。此外,类路径本身也存在问题: 你怎么知晓所有需要的 JAR 都已经有了, 或者是不是会有重复的项呢? 模块系统把这俩个问题都给解决了。</p>
</summary>
<category term="Java基础" scheme="http://zhangfei.men/categories/Java%E5%9F%BA%E7%A1%80/"/>
<category term="Java" scheme="http://zhangfei.men/tags/Java/"/>
</entry>
</feed>