-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathatom.xml
505 lines (242 loc) · 240 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
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>个人博客</title>
<link href="https://lightnine/github.io/atom.xml" rel="self"/>
<link href="https://lightnine/github.io/"/>
<updated>2024-06-25T06:19:49.663Z</updated>
<id>https://lightnine/github.io/</id>
<author>
<name>liang</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>linux有关网络操作的11个命令</title>
<link href="https://lightnine/github.io/linux%E6%9C%89%E5%85%B3%E7%BD%91%E7%BB%9C%E6%93%8D%E4%BD%9C%E7%9A%8411%E4%B8%AA%E5%91%BD%E4%BB%A4.html"/>
<id>https://lightnine/github.io/linux%E6%9C%89%E5%85%B3%E7%BD%91%E7%BB%9C%E6%93%8D%E4%BD%9C%E7%9A%8411%E4%B8%AA%E5%91%BD%E4%BB%A4.html</id>
<published>2024-06-25T06:18:12.000Z</published>
<updated>2024-06-25T06:19:49.663Z</updated>
<content type="html"><![CDATA[<p>下面的11个命令在进行linux网络操作很有用,特意记录下.</p><blockquote><p>原文链接:<a href="https://github.com/oldratlee/translations/blob/master/how-to-work-with-network-from-linux-terminal/README.md">https://github.com/oldratlee/translations/blob/master/how-to-work-with-network-from-linux-terminal/README.md</a></p></blockquote><h2 id="curl-amp-wget"><a href="#curl-amp-wget" class="headerlink" title="curl & wget"></a>curl & wget</h2><p>curl和wget都可以下载文件</p>]]></content>
<summary type="html"><p>下面的11个命令在进行linux网络操作很有用,特意记录下.</p>
<blockquote>
<p>原文链接:<a href="https://github.com/oldratlee/translations/blob/master/how-to-work-with-n</summary>
<category term="Linux" scheme="https://lightnine/github.io/categories/Linux/"/>
</entry>
<entry>
<title>'编写可读代码的艺术'读书笔记</title>
<link href="https://lightnine/github.io/%E7%BC%96%E5%86%99%E5%8F%AF%E8%AF%BB%E4%BB%A3%E7%A0%81%E7%9A%84%E8%89%BA%E6%9C%AF-%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0.html"/>
<id>https://lightnine/github.io/%E7%BC%96%E5%86%99%E5%8F%AF%E8%AF%BB%E4%BB%A3%E7%A0%81%E7%9A%84%E8%89%BA%E6%9C%AF-%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0.html</id>
<published>2024-06-10T05:29:50.000Z</published>
<updated>2024-06-10T07:14:59.411Z</updated>
<content type="html"><![CDATA[<blockquote><p>最近读了《the art of readable code》这本书,书的内容是叫你如何写出可读性高的代码。觉得里面的很多例子和观点有很大的参考价值,所以在这篇博客中记录下来。</p></blockquote><p>书中有个很强的观点,代码是给人看的,不是给机器看的。所以写代码要将可读性放到第一位。写代码时要时刻考虑这段代码别人是不是容易阅读。<br>全书分为四部分,分别为:</p><ul><li>代码外观上的改进</li><li>简化循环和代码逻辑</li><li>重新组织代码</li><li>单元测试可读性</li></ul><h2 id="代码外观上的改进"><a href="#代码外观上的改进" class="headerlink" title="代码外观上的改进"></a>代码外观上的改进</h2><p>这一部分主要是从变量命名、函数命名、注释等方面介绍如何提升代码可读性。</p><h3 id="命名和注释"><a href="#命名和注释" class="headerlink" title="命名和注释"></a>命名和注释</h3><p>“代码中最困难的两件事情,是命名和缓存失效”。可见命名的困难性。在选择名称时,我们遵循如下原则:</p><ul><li><strong>选择能够准确表达代码意图的名称</strong><figure class="highlight python"><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">// GetPage函数不能表示page从哪里获取。推荐使用FetchPage和DownloadPage进行替换。</span><br><span class="line"><span class="keyword">def</span> <span class="title function_">GetPage</span>(<span class="params">url</span>):</span><br><span class="line"> ...</span><br></pre></td></tr></table></figure>下面展示了英文中单词对应的同义词</li></ul><div class="table-container"><table><thead><tr><th>单词</th><th>同义词</th></tr></thead><tbody><tr><td>send</td><td>deliver,dispatch,announce,distribute,route</td></tr><tr><td>find</td><td>search,extract,locate,recover</td></tr><tr><td>start</td><td>launch,create,begin,open</td></tr><tr><td>make</td><td>add, push, enqueue</td></tr></tbody></table></div><ul><li>避免使用泛化的名称,比如tmp、retval之类的东西。<br>但在一些特殊情况下可以使用,比如循环、交换两个数时的临时变量中</li><li>名称中附带额外信息<br>下面展示了一些具体的例子</li></ul><div class="table-container"><table><thead><tr><th>函数参数</th><th>推荐重构后的内容</th></tr></thead><tbody><tr><td>Start(int delay)</td><td>delay -> delay_secs</td></tr><tr><td>CreateCache(int size)</td><td>size -> size_mb</td></tr><tr><td>ThrottleDownload(float limit)</td><td>limit -M max_kbps</td></tr></tbody></table></div><ul><li><p>根据变量的作用域选择合适的名称<br><strong>变量的作用域如果比较长,则变量名称尽量多携带信息;变量作用域如果比较短,则变量名称推荐用简单的、简短的。</strong></p></li><li><p>英文中一些推荐变量命名做法</p><ul><li>使用min和max表示下限和上限(包含)</li><li>使用first和last表示范围,其中last包含最后一个数</li><li>使用begin和end表示范围,其中end是最后一个数的下一个数</li></ul></li><li>布尔命名中避免携带否定词<br>disable_ssl就没有use_ssl 好</li><li>代码需要有段落</li><li>如果代码表达的意思已经很明确了,那么没有必要添加注释</li></ul><h2 id="简化循环和代码逻辑"><a href="#简化循环和代码逻辑" class="headerlink" title="简化循环和代码逻辑"></a>简化循环和代码逻辑</h2><ul><li>尽早从函数返回(卫语句使用)</li><li>避免使用do-while循环语句</li><li>减少代码中的嵌套(比如可以使用提前返回)</li><li>代码中的巨大表达式要进行简化</li><li>代码中使用的变量越多,程序阅读起来就越困难<figure class="highlight python"><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">下面的now完全没有必要,直接用后面的内容替换now就行</span><br><span class="line">now = datetime.datetime.now()</span><br><span class="line">root_message.last_view_time = now</span><br></pre></td></tr></table></figure></li><li>尽量降低变量的作用域。(全局变量越少越好,其实就是降低耦合度)</li><li>变量应该在需要的时候进行定义,而不是在函数的开头就定义当前函数中需要的所有变量</li><li>变量改变的越多,追踪变量的当前值就越困难(尽量使用只写一次的变量,比如java中的final,go中的const等)</li></ul><h2 id="重新组织代码"><a href="#重新组织代码" class="headerlink" title="重新组织代码"></a>重新组织代码</h2><ul><li>首先用自然语言描述代码需要完成的功能,然后在进行实现;这个方法在重构代码时也非常有用。</li><li>尽量创建通用的代码,与项目无关的代码尽量放到独立的模块中,比如util模块中。</li><li>代码应该只做一件事情(do only one task at a time);借鉴:unix哲学中,一个函数只做一件事。</li><li>对于用到的语言,第三方库要尽量掌握,并熟悉常用的函数。</li></ul><h2 id="单元测试可读性"><a href="#单元测试可读性" class="headerlink" title="单元测试可读性"></a>单元测试可读性</h2><ul><li>测试代码提高可读性要遵循一个原则是:对用户隐藏不重要的细节,将重要的细节进行展示。</li><li>测试的输入内容应该是完全覆盖被测代码,并且在满足这一前提下是最简单的</li><li>好的代码是更加容易测试的</li><li>在测试代码中,可以使用帮助函数来简化测试代码。</li></ul>]]></content>
<summary type="html"><blockquote>
<p>最近读了《the art of readable code》这本书,书的内容是叫你如何写出可读性高的代码。觉得里面的很多例子和观点有很大的参考价值,所以在这篇博客中记录下来。</p>
</blockquote>
<p>书中有个很强的观点,代码是给人</summary>
</entry>
<entry>
<title>运行kubectl run会发生什么(what happens when i type kubectl run)</title>
<link href="https://lightnine/github.io/%E8%BF%90%E8%A1%8Ckubectl-run%E4%BC%9A%E5%8F%91%E7%94%9F%E4%BB%80%E4%B9%88-what-happens-when-i-type-kubectl-run.html"/>
<id>https://lightnine/github.io/%E8%BF%90%E8%A1%8Ckubectl-run%E4%BC%9A%E5%8F%91%E7%94%9F%E4%BB%80%E4%B9%88-what-happens-when-i-type-kubectl-run.html</id>
<published>2024-04-17T12:09:04.000Z</published>
<updated>2024-06-10T07:00:19.066Z</updated>
<content type="html"><![CDATA[<blockquote><p>原文链接:<a href="https://github.com/jamiehannaford/what-happens-when-k8s">https://github.com/jamiehannaford/what-happens-when-k8s</a></p><p>想像一下当你运行下面的命令时<br><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">kubectl create deployment nginx --image=nginx --replica=3</span><br></pre></td></tr></table></figure><br>在一切顺利的情况下,在k8s集群中能够看到生成了3个pod。那么在底层到底发生了什么?本篇文章试图解决一个请求从客户端到kubelet整体的流程,并且也链接了源码。</p></blockquote><h2 id="目录"><a href="#目录" class="headerlink" title="目录"></a>目录</h2><ol><li>kubectl<ul><li>Validation and generators(验证和生成)</li><li>API groups and version negotiation(API组和版本协商)</li><li>Client auth(客户端认证)</li></ul></li><li>kube-apiserver<ul><li>Authentication(认证)</li><li>Authorization(鉴权)</li><li>Admission control(admission控制)</li></ul></li><li>etcd</li><li>Initializers</li><li>Control loops<ul><li>Deployments controller</li><li>ReplicaSet controller</li><li>Informers</li><li>Scheduler</li></ul></li><li>kubelet<ul><li>Pod sync</li><li>CRI and pause containers</li><li>CNI and pod networking</li><li>Inter-host networking</li><li>Container startup</li></ul></li><li>Wrap-up(总结)</li></ol><h2 id="kubectl"><a href="#kubectl" class="headerlink" title="kubectl"></a>kubectl</h2><h3 id="Validation-and-generators"><a href="#Validation-and-generators" class="headerlink" title="Validation and generators"></a>Validation and generators</h3><p>针对开头提到的命令,在命令行输入回车后,会发生什么呢?<br>kubectl首先会进行客户端侧的验证。针对非法的命令(比如不支持的资源或镜像不合法)能够快速失败提示,避免发送到kube-apiserver,从而降低k8s负载。<br>经过验证后,kubectl组装将要发送到api-server的http请求。在k8s中,获取或者改变k8s中的状态都要通过apiserver,apiserver负责与etcd交互。kubectl使用generators来构建http请求,generators是负责序列化的一种抽象(注:可以简单的把generators理解为帮帮助用户构建完成的资源的工具,比如kubectl create中我们只指定了部分的内容,剩下的内容是generators帮助我们生成的)。<br>kubectl run 命令可以通过使用—generator参数指定运行多种资源,而不仅仅是deployments。如果不使用—generator参数,kubectl可以主动推断要运行的资源。<br>例如,参数<code>--restart-policy=Always</code>会触发deployments,而<code>--restrart-policy=Never</code>会触发pods。kubectl也会根据其他的参数确定需要执行的其他动作。比如记录命令(回滚或审计),或者<code>--dry-run</code><br>在确定要创建的资源是Deployment后,kubectl将会使用<code>DeploymnetAppsV1</code> generator来根据命令行参数构造runtime object。runtime object是k8s resource的通用表示。</p><h3 id="API-groups-and-version-negotiation"><a href="#API-groups-and-version-negotiation" class="headerlink" title="API groups and version negotiation"></a>API groups and version negotiation</h3><p>k8s使用版本API,并且每个版本有自己所属的API groups。API groups意味着一些相似资源的集合。这比使用单调递增的版本号更加灵活。比如deployment的api group是apps,最近的版本是v1。这也是在deployment中写<code>apiVersion: apps/v1</code>的原因。<br>kubectl生成了runtime object后,kubectl开始找到正确的API group和version,并且会组装versioned client。versioned client能够意识到资源的各种REST 语义。这个阶段被称为版本协商,它涉及kubectl扫描远程API上的/apis路径,从而检索所有可能的API组。由于kube-apiserver在此路径上公开了其模式文档(以OpenAPI格式),因此客户端可以很容易执行自己的发现。<br>为了提高性能,kubectl会将OpenAPI schema缓存到 ~/.kube/cache/discovery 目录中。如果想要看到API 发现的过程,那么可以将这个缓存目录删除,然后运行kubectl命令时带上-v参数。你将会看到查询API versions的所有http请求。<br>kubectl最后一步是发送http请求。一旦接收到成功的响应后,那么kubectl就会按照期望的格式打印结果。</p><h3 id="Client-auth"><a href="#Client-auth" class="headerlink" title="Client auth"></a>Client auth</h3><p>为了成功发送请求,kubectl需要能够处理认证信息。用户的凭证一般存储在<code>kubeconfig</code>文件中,但是有时候也在其他地方。kubectl做了下面的事情来决定用户凭证:</p><ul><li>如果kubectl中使用了<code>--kubeconfig</code>,那么就使用这个参数指定的文件</li><li>如果当前环境变量中定义了<code>$KUBECONFIG</code>,那么使用这个环境变量指定的文件</li><li>否则使用推荐的主目录,比如<code>~/.kube</code>,然后使用此目录中的第一个文件<br>当kubectl解析了凭证文件后,kubectl会决定当前需要使用的上下文,当前需要连接的集群,与当前用户关联的认证信息。如果kubectl中提供了指定的参数,比如<code>--username</code>, 那么这些参数将会覆盖凭证文件的配置。一旦有了这些信息,那么kubectl将会用这些信息去修饰http请求:</li><li>使用tls.TLSConfig来发送x509证书</li><li>bearer token放到http header中的”Authorization”</li><li>username 和 password通过http basic authentication发送</li><li>OpenID认证流程是由用户事先手动处理的,生成一个类似于Bearer令牌的token,然后发送。</li></ul><h2 id="kube-apiserver"><a href="#kube-apiserver" class="headerlink" title="kube-apiserver"></a>kube-apiserver</h2><h3 id="Authentication-认证,确定用户是谁"><a href="#Authentication-认证,确定用户是谁" class="headerlink" title="Authentication(认证,确定用户是谁)"></a>Authentication(认证,确定用户是谁)</h3><p>k8s客户端和k8s系统组件与k8s交互主要是通过kube-apiserver来进行,比如获取和存储系统状态。kube-apiserver第一步要做的就是要验证请求的身份信息,这一步叫做认证(authentication).<br>apiserver如何进行认证呢?首先,apiserver在启动时,它会首先检查启动参数,然后组装一系列的认证器(authenticators)。举个例子,比如<code>--client-ca-file</code>传递进来,那么会添加x509认证器;如果提供<code>--token-auth-file</code>,那么会添加token认证器。apiserver收到的每个请求都会经过这些认证器,并且会依次进行认证,直到有一个认证器成功,那么请求就通过了。</p><ul><li>x509 handler 将会检查请求的证书是否有效,并且是否在指定的ca文件中</li><li>bearer token handler 将会检查token(http Authorization header)是否在指定的文件中(由<code>--token-auth-file</code>指定)</li><li>basic handler仅仅简单的检查http 请求中的basic 认证信息是否与本地状态匹配<br>如果每个认证器都认证失败了,那么一个组装的错误(每个错误的认证)会返回。如果认证成功,那么在http header中的<code>Authorization</code>会移除,并且用户信息会添加到上下文中。这样后续的apiserver处理逻辑(比如authorization和admission controllers)能够直接获取到用户信息。</li></ul><h3 id="Authorization-鉴权,确定用户是否有权限"><a href="#Authorization-鉴权,确定用户是否有权限" class="headerlink" title="Authorization(鉴权,确定用户是否有权限)"></a>Authorization(鉴权,确定用户是否有权限)</h3><p>经过认证后,apiserver会进行鉴权检查,确定执行的请求动作是否允许。<br>apiserver处理鉴权跟认证类似。基于启动的参数,apiserver会组装一系列的鉴权器(authorizers), 这些鉴权器会依次检查请求。如果所有的鉴权都没有通过,那么请求会被拒绝并返回给客户端。如果其中某个鉴权器通过,那么请求会继续。<br>下面展示了一些鉴权器:</p><ul><li>webhook,负责与k8s集群外的http(s) service进行交互的</li><li>ABAC,执行定义在static文件中的鉴权策略</li><li>RBAC,采用RBAC角色来进行鉴权</li><li>Node,确保node clients(比如kubelet)只能够访问托管到自身上的k8s资源。<h3 id="Admission-control"><a href="#Admission-control" class="headerlink" title="Admission control"></a>Admission control</h3>从apiserver的角度看,它已经确定了请求是谁以及请求是否能够执行。但是对于kubernetes,系统的其他部分对于能够做什么和不能做什么都有自己的想法。这就是admission controller要做的事情。<br>鉴权侧重于用户是否有权限,而admission controllers是用来确保请求符合期望以及遵守集群规则。amission controller是对象存储到etcd之前的最后一个处理,因此它封装了系统剩余的检查以确定请求不会产生意想不到的后果。<br>admission controllers的逻辑与authenticators和authorizers类似,但是不同之处是:不像authenticator和authorizers链式调用,如果一个admission controller失败,那么整个admission chain会失败。<br>amission controllers比较优秀的设计是专注于提升扩展性。每个controller都作为一个插件放置到<code>plugin/pkg/admission</code>目录下,每个controller都满足一个比较小的接口定义。然后编译到k8s中。<br>admission controllers通常分为这几类,资源管理、安全性、默认设置和引用一致性。下面列出了专注于资源管理的admission controller:</li><li>InitialResources:根据过去资源使用情况,设置默认的资源限制</li><li>LimitRanger:设置容器资源的request和limit或者为资源设置资源上界。</li><li>ResourceQuota:确保集群中资源使用量不会超过配额</li></ul><h2 id="etcd"><a href="#etcd" class="headerlink" title="etcd"></a>etcd</h2><p>apiserver验证了请求后,下一步apiserver反序列化http请求,从http请求中构建runtime objects(类似于kubectl generators的反过程),然后将objects存储到底层存储中。将这个过程细化如下。<br>首先,apiserver在接受到请求后是怎么知道如何处理呢?这个过程比较复杂。我们首先看下apiserver在启动时都会做什么:</p><ol><li>当前apiserver启动时,它会创建server chain,是一个组合的调用链。基本上是由多个apiserver组成的。</li><li>server chain中有个通用的apiserver,是一个默认的实现</li><li>openAPI schema会填充到apiserver的配置中</li><li>apiserver遍历所有在openAPI schema中定义的api groups,然后为每个api group都配置一个storage provider,这些storage provider是一个通用的存储抽象。apiserver主要通过与这些storage provider交互来存储和检索数据。</li><li>在每个api group中,apiserver会遍历所有的版本,然后为每个http 路径配置rest mapping。这样apiserver就能够将收到的外部请求与对应的处理逻辑关联起来</li><li>针对我们上面的例子,apiserver已经注册了post handler。这个handler被委托用来创建资源<br>到这里,apiserver已经知道有哪些http route存在,并且如何将handler跟route进行映射。让我们现在看下http 请求进来后的处理</li><li>如果请求跟某个特定的route匹配上,那么就会交给对应的handler进行处理。如果没有匹配上,那么会采用基于url 路径的方式进行匹配。如果针对这个路径也没有匹配上,那么一个not found handler会被调用,然后返回404错误。</li><li>针对我们的例子,有一个注册的路由会调用createHandler方法。它首先解码http请求,执行基础的验证。比如确保http提供的json格式复合对应的版本资源。</li><li>触发审计以及最后的admission</li><li>专用的storage provider将资源存储到etcd中。etcd中资源的key通常是<code><namespace>/<name></code>,但是这个格式是可以配置的</li><li>创建中的任何错误都会被捕获,最后storage provider会执行get请求来确保资源一定是创建成功的。如果需要,还会调用而外的后处理逻辑。</li><li>构造http 响应并返回<br>通过上面的步骤,我们知道apiserver做了很多的步骤。当前deployment已经成功的在etcd中创建了。但是当前deployment还没有被调度<h2 id="Initializers"><a href="#Initializers" class="headerlink" title="Initializers"></a>Initializers</h2>在object存储到etcd之后,apiserver或者调度器并不能立马看到它。直到一系列的initializers执行完毕后,才会看到这个对象。初始化控制器将资源类型和要执行的逻辑进行关联。如果一个资源没有注册初始化控制器,那么初始化阶段会调用,这个资源立马会被apiserver或调度器看到。<br>初始化控制器给我们提供了扩展k8s的能力,比如:</li></ol><ul><li>给pod注入代理sidecar 容器,或者获取特定的注解</li><li>向特定的命名空间下的pod注入证书</li><li>阻止创建小于20个字符的secret小于<br><code>initializerConfiguration</code>允许你声明哪些资源运行哪些initializers。假设我们想要一个在每个pod创建时都运行的initializer,我们的配置如下:<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></pre></td><td class="code"><pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">admissionregistration.k8s.io/v1alpha1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">InitializerConfiguration</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line"> <span class="attr">name:</span> <span class="string">custom-pod-initializer</span></span><br><span class="line"><span class="attr">initializers:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">podimage.example.com</span></span><br><span class="line"> <span class="attr">rules:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">apiGroups:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">""</span></span><br><span class="line"> <span class="attr">apiVersions:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">v1</span></span><br><span class="line"> <span class="attr">resources:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">pods</span></span><br></pre></td></tr></table></figure>上面的InitializerConfiguration会给每个pod的<code>metadata.initializers.pending</code>字段添加<code>podimage.example.com</code>。initializer controller已经提前部署好,initializer会扫描每个新建的pod。当发现pod的<code>metadata.initializers.pending</code>字段不为空时,initializer controller就会执行。执行完后,会将对应的<code>podimage.example.com</code>删除。当所有的初始化器执行完毕并且<code>pending</code>字段为空,则此对象被认为已经初始化完成。<br>可能你已经发现一个问题,如果资源对于apiserver不可见,那么我们这里的初始化控制器怎么处理呢?apiserver暴露了一个<code>includeUninitialized</code>查询参数来返回所有的对象,包括那些还未初始化的对象。</li></ul><h2 id="Control-loops"><a href="#Control-loops" class="headerlink" title="Control loops"></a>Control loops</h2><h3 id="Deployments-controller"><a href="#Deployments-controller" class="headerlink" title="Deployments controller"></a>Deployments controller</h3><p>到现在这个阶段,deployment已经存储到etcd中并且初始化逻辑已经完成了。接下来就要设置资源拓扑。deployment就是replicasets的集合,而replicaSet就是pod的集合。k8s如何从http 请求中去创建这些对象呢?这就是k8s内置controller需要做的内容。<br>k8s在整个系统中大量使用了”控制器”思想。控制器就是一个异步过程,调谐k8s当前的状态到期望状态。每个控制器有自己的职责,并且由<code>kube-controller-manager</code>组件负责并发运行。下面介绍deployment controller<br>在 deployment存储到etcd并且初始化完成后,那么deployment就能够被kube-apiserver看到。deployment controller能够检测到可用的deployment资源以及对于资源的变更。在我们的例子中,deployment controller通过informer为创建事件注册了一个特定的回调。<br>当deployment可用时,handler开始执行,将对象添加到内部的工作队列中。当开始处理这个对象时,控制器会检查deployment,发现deployment没有关联的ReplicaSet或者Pod记录。这是通过使用标签选择器来查询kube-apiserver来完成的。有趣的是,这个同步过程是状态未知的:调谐新的记录跟调谐旧的记录是一样的工作方式。<br>在意识到相关的ReplcaSet和Pod不存在后,deployment controller会开启scaling process来解析状态。创建一个ReplicaSet资源,给它分配标签选择器,然后给定数字1的版本号。ReplicaSet中的PodSpec是从deployment清单中拷贝而来,类似于其他相关的元数据。有些时候,deployment记录在这个过程后也需要对应的更新(比如,设定了截止日期)<br>更新deployment中的status字段,然后控制器重新进行调谐过程,然后等待deployment达到期望的状态。由于deployment controller仅仅关心创建ReplicaSets,接下来的工作需要由下一个控制器来完成,既ReplicaSet controller。</p><h3 id="ReplicaSet-controller"><a href="#ReplicaSet-controller" class="headerlink" title="ReplicaSet controller"></a>ReplicaSet controller</h3><p>在前面的步骤中,Deployments controller创建了ReplicaSet,但是还没有创建Pods。这就是ReplicaSet 控制器发挥作用的时刻。ReplicaSet controller的主要工作就是监视ReplicaSet的生命周期和对应的Pods资源。类似于大多数其他的控制器,它通过在特定事件上触发handler来实现。<br>我们感兴趣的事件是创建。当ReplicaSet被创建后。ReplicaSet controller检查新的replicaSet的状态,然后发现当前的状态跟期望的状态不一致。它试图通过增加属于ReplicaSet的Pod数量来达到目标状态。它以谨慎的方式来创建它们,确保ReplicaSet的数量始终匹配。<br>创建Pods的过程是批量处理的,以<code>SlowStartInitiaBatchSize</code>开始,然后每次都以两倍的数量来启动其他的Pods。这样做的目的是确保当pod创建失败时,避免大量的http 请求到apiserver。如果要失败,可以采用对组件影响最小的方式来进行。<br>k8s通过Owner References(child资源中的一个字段,用来指向父资源的id)来确保对象的引用关系。这样做,不仅能够确保一旦父资源被删除,子资源也会被删除(级联删除),同时还能够保证父资源不会竞争子资源(想象一下,两个潜在的父资源认为他们拥有同样的子资源)<br>Owner Reference另一个好处是,它是有状态的。如果控制器重启了,那么重启过程中,不会影响到这些资源,因为资源拓扑是独立于controler的。这种对隔离的关注也渗透到控制器本身的设计中:它们不应该操作它们没有明确拥有的资源。相反,控制器应该只操作它们明确拥有的资源。<br>然而,有时候会存在”孤儿”资源,比如下面这些情况:</p><ol><li>父资源删除,但是子资源没有删除</li><li>垃圾回收策略禁止删除子资源<br>如果发生了这种情况,那么控制器会确保这些“孤儿”资源被新的父资源领养。多个父资源能够竞争性的去领养孤儿资源,但是只有一个能够成功。<h3 id="Informers"><a href="#Informers" class="headerlink" title="Informers"></a>Informers</h3>rbac authorizer 或者 deployment controller需要获取集群状态,从而实现相关逻辑。比如以rbac authorizer为例,当请求进入时,authenticator需要将用户的初始状态保存起来以供后续使用。rbac authorizer后续会使用用户的初始表示来获取在etcd中相关的角色和角色绑定关系。控制器应该如何访问和修改这些资源?k8s中提供了informers来实现。<br>informer是一种模式,既允许控制器订阅存储时间以及查询资源列表。informer除了提供一种抽象外,它还封装了大量的工作,比如缓存(缓存很重要,不仅可以减少apiserver连接数,同时还可以较少服务端和客户端的序列化)。informer还提供了线程安全的方式来操作资源。<h3 id="Scheduler"><a href="#Scheduler" class="headerlink" title="Scheduler"></a>Scheduler</h3>在所有的控制器都运行后,当前在etcd中会存在一个deployment,一个ReplicaSet以及三个pod资源。此时,pod的状态是<code>Pending</code>,因为它们还没有被调度。最后的一个控制器是scheduler,它负责调度pod。<br>scheduler是作为控制面的一个标准组件,它的运行机制跟其他的控制器类似。它监听事件,然后尝试将状态调整到期望状态。在这个例子中,调调度器筛选出<code>NodeName</code>为空的pod,然后尝试将它们调度到合适的节点上。<br>为了找到合适的节点,scheduler使用了专门的调度算法。默认的调度算法工作如下:</li><li>scheduler启动时,会注册一系列的默认predicates。这些predicates会检查pod是否满足特定的条件,比如:如果PodSpec中明确指定了CPU或RAM资源,如果Node上的资源不满足,那么此Node就会被过滤掉。</li><li>一旦选择了一些符合条件的节点,那么接下来priority函数将会给这些节点打分。比如:为了在集群中使得资源均匀分配,那么节点剩余资源较多的节点会得到更高的分数,最终选择一个分数最高的节点作为目标节点。<br>当找到合适节点后,scheduler会创建一个Binding Object,其中名称和UID会匹配对应的pod。Binding Object的ObjectReference字段会包含选择的目标节点。通过post请求会发送给apiserver。<br>当apiserver接受到Binding object后,它会反序列化请求,然后将信息更新到pod对象上:设置pod的NodeName字段,添加相关的注解以及将<code>PodScheduled</code>设置为<code>True</code>.<br>一旦scheudler将pod调度到目标节点后,剩下的工作就交与kubelet来完成了。<blockquote><p>注:定制调度器。通过<code>--policy-config-file</code>可以定制predicate和priority函数。我们可以在k8s集群中运行特定的调度器,比如采用deployment运行。如果在PodSpec中指定了<code>schedulerName</code>,那么k8s会将调度过程交给这个调度器执行。</p></blockquote></li></ol><h2 id="kubelet"><a href="#kubelet" class="headerlink" title="kubelet"></a>kubelet</h2><h3 id="Pod-sync"><a href="#Pod-sync" class="headerlink" title="Pod sync"></a>Pod sync</h3><p>让我们总结下上面的过程:</p><ol><li>HTTP请求经过认证、鉴权以及admission controller阶段;</li><li>一个deployment、一个replicaSet、三个pod持久化到etcd中</li><li>一系列初始化器运行</li><li>最后,每个pod被调度到合适的节点上<br>到这个时候,数据仅仅存在于etcd中。接下来,就需要将pod在worker节点上启动。这个是通过kubelet组件来实现的。<br>kubelet是k8s集群中运行在每个节点上的组件,主要负责管理pod的生命周期。这意味kubelet需要将pod转换成具体的容器、网络等。同时,它也负责处理卷挂载、容器日志、垃圾收集以及其他重要的事情。<br>另一种考虑kubelet的方式是将它看做是一个控制器。kubectl从kube-apiserver每个20秒钟(可以配置)查询pod信息,挑选出<code>NodeName</code>于当前节点名称一致的pod。然后它将pod与本地内部缓存中pod的信息进行比较,如果发现有差异就进行同步。下面来具体看下同步的过程是怎么样的:</li><li>如果pod是要进行创建,那么kubelet会注册一些metrics,这些metrics主要给prometheus用于追踪pod的延迟。</li><li>kubectl生成PodStatus对象,PodStatus代表了Pod的当前状态(Phase)。pod的状态是pod生命周期的更高一级的抽象。比如<code>Pending</code>,<code>Running</code>,<code>Succeeded</code>,<code>Failed</code>和<code>Unknown</code>。产生这些状态是比较复杂的,让我们深挖一下:<ul><li>首先,一系列的<code>PodSyncHandlers</code>会顺序执行。每个handler都会检查pod是否应该在当前节点上。如果任意的handler判断pod不应该在当前节点上,那么pod的phase将会变成<code>PodFailed</code>,并且pod会从此节点上被剔除。比如在Job资源中设置了<code>activeDeadlineSeconds</code>,超过了设置的时间,那么pod就会被剔除。</li><li>接下来,pod的phase会由初始化容器以及主容器的状态来决定。因为在这里容器还没有启动,这些容器会被归类为等待。具有等待容器的pod的phase都是<code>Pending</code></li><li>最后,Pod Condition会由容器的Condition来决定。因为我们的容器当前还没有被容器运行时创建,所以kubelet会将<code>PodReady</code> condition设置为False</li></ul></li><li>当PodStatus产生后,PodStatus对象会被发送到Pod的status manager中。该管理器的任务是通过apiserver异步更新etcd记录</li><li>接下来,将运行一系列准入处理程序,以确保pod具有正确的安全权限。这包括强制的AppArmor profiles 和 NO_NEW_PRIVS。在这个阶段被拒绝的pod将无限期地处于挂起状态。</li><li>如果kubelet在启动时指定了<code>cgroups-per-qos</code>参数,那么kubelet将会为pod创建cgroups并且会应用这些资源参数。这是为了实现对pod更好地服务质量(QoS)处理。</li><li>给pod创建数据目录。包括pod目录(通常是:<code>/var/run/kubelet/pods/<podID></code>),卷目录(<code><podDir>/volumes</code>)以及插件目录(<code><podDir>/plugins</code>)</li><li>卷管理器(volume manager)等待<code>Spec.Volumes</code>中定义的卷准备好。根据挂载卷的类型,有些pod需要等待更长的时间(比如云或NFS卷)。</li><li>在<code>Spec.ImagePullSecrets</code>中定义的secrets资源,会从apiserver中获取。从而后续会注入到容器中</li><li>容器运行时开始启动容器<h3 id="CRI-and-pause-containers"><a href="#CRI-and-pause-containers" class="headerlink" title="CRI and pause containers"></a>CRI and pause containers</h3>到现在,大多数工作已经完成了,容器已经准备好启动了。启动容器的部分叫做容器运行时(Container Runtime,比如<code>docker</code>或<code>rkt</code>)。<br>为了增加扩展性,kubelet自从v1.5.0版本以来,提出了CRI(Container Runtime Interface),CRI用于与具体的容器运行时进行交互。总的来说,CRI是一个抽象层。kubelet和容器运行时通过protocal buffers以及对应的gRPC API接口进行交互。CRI带来了很大的好处,如果后续替换下层的容器运行时,核心的k8s代码不需要做任何修改。<br>我们回到部署容器的过程。当pod启动后,kubelet通过RPC启动<code>RunPodSandbox</code>。沙箱是一个CRI术语,用来描述一组容器,在k8s中被称作pod。<br>在我们的例子中,我们使用了docker。在沙箱中首先创建一个”pause”容器。pause容器主要是为了给pod中其他的容器提供服务,提供必要的资源。这些资源包括linux namespace(IPC、网络、PID等)。如果你不熟悉linux中容器如何工作。我们快速说一下。linux内核有命名空间的概念,命名空间主要是隔离专有的资源,比如CPU、内存。然后将这些资源指定给特定的进程,从而保证资源只能被指定的进程使用。Cgroups主要是给linux提供了资源分配的方式。Docker利用了linux命名空间和Cgroups来实现容器。<br>pause容器提供了所有的命名空间,并且允许子容器共享他们。作为同一个网络命名空间的一部分,一个好处是同一个pod中的容器可以通过localhost互相引用。pause容器的第二个角色是,PID命名空间如何工作。进程形成了等级树,最顶层的init进程,负责收割死进程。在pause容器创建后,它被检查点指向磁盘,然后启动。<h3 id="CNI-and-pod-networking"><a href="#CNI-and-pod-networking" class="headerlink" title="CNI and pod networking"></a>CNI and pod networking</h3>pod当前具有了基本的内容:pause容器持有所有的命名空间,从而pod内部的容器可以交流。但是网络如何工作以及如何设置?<br>kubelet将设置网络的工作委托给了CNI(Container Network Interface)插件。CNI的工作方式类似于Container Runtime Interface。简单来说,CNI提供了一种抽象,用于给不同的网络提供者使用不同的网络实现。kubelet与这些注册的CNI插件通过流式json数据进行交流。下面展示了json配置的例子:<figure class="highlight json"><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="punctuation">{</span></span><br><span class="line"> <span class="attr">"cniVersion"</span><span class="punctuation">:</span> <span class="string">"0.3.1"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"name"</span><span class="punctuation">:</span> <span class="string">"bridge"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"type"</span><span class="punctuation">:</span><span class="string">"bridge"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"bridge"</span><span class="punctuation">:</span> <span class="string">"cnio0"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"isGateway"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"isMasq"</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"ipam"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"type"</span><span class="punctuation">:</span> <span class="string">"host-local"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"ranges"</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line"> <span class="punctuation">[</span><span class="punctuation">{</span><span class="attr">"subnet"</span><span class="punctuation">:</span> <span class="string">"${POD_CIDR}"</span><span class="punctuation">}</span><span class="punctuation">]</span></span><br><span class="line"> <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"routes"</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="punctuation">{</span><span class="attr">"dst"</span><span class="punctuation">:</span> <span class="string">"0.0.0.0/0"</span><span class="punctuation">}</span><span class="punctuation">]</span></span><br><span class="line"> <span class="punctuation">}</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></table></figure>json也为pod提供了特定的额外的元数据,比如通过<code>CNI_ARGS</code>环境变量提供名称和命名空间。<br>接下来发生的事情与具体的CNI插件有关,我们看看<code>bridge</code> CNI插件的工作:</li><li>插件首先在root命名空间中设置一个本地的linux bridge,这个bridge为当前host(本机)上的所有容器服务</li><li>接下来,插件创建veth pair,其中一端在pause容器,另一端在bridge中。可以将veth pair想象成一个管道,一端连接到容器,另一端连接到root 网络命名空间,从而允许容器和root网络命名空间进行通信。</li><li>现在应该给pause容器分配IP地址,并且设置路由。这样做之后pod就有了自己的ip地址。ip地址分配是委托给IPAM,IPAM在json中定义。<ul><li>IPAM插件类似于主要的网络插件:通过二进制调用并且有标准化的接口。每种IPAM插件都必须去顶容器的IP/子网,网络和路由,然后将这些信息返回给CNI插件。最常用的IPAM插件是<code>host-local</code>,它从一个预先定义好的地址范围中分配IP地址。它将已经分配的ip地址存储在本地的文件系统中,从而确保在本机上不会重复使用ip地址。</li></ul></li><li>kubelet会指定一个内部的DNS服务器ip地址给CNI插件,保证容器的<code>resolv.conf</code>文件被正确设置。<br>一定这个过程完成,CNI插件会返回json数据给kubelet,表明网络设置的结果。<h3 id="Inter-host-networking"><a href="#Inter-host-networking" class="headerlink" title="Inter-host networking"></a>Inter-host networking</h3>我们现在仅仅说了容器如何同主机进行通信。但是如果pod在不同的主机上,那么pod之间如何通信呢?<br>这通常是基于一种叫做overlay networking的技术来实现,这是一种跨多个主机动态同步路由的方法。一个流行的overlay netwoker实现是Flannel。Flannel的核心作用是在不同节点之间提供IPv4网络。Flannel不会控制容器如何跟主机通信(这是CNI的职责),而是负责流量在不同主机间的转发。为了实现这个功能,Flannel为主机选择了一个子网,将这个信息存储到etcd中。它保留集群路由的本地表示,并将传出的数据包封装在UDP数据报中,确保它到达正确的主机。<h3 id="Container-startup"><a href="#Container-startup" class="headerlink" title="Container startup"></a>Container startup</h3>所有的网络工作都完成了,接下来就是启动工作容器了。<br>一旦沙箱完成初始化并且仍然是活跃的,那么kubelet将会为其创建工作容器。kubelet首先启动定义在PodSpec中定义的初始化容器,然后启动主容器。主要过程如下:</li><li>拉取容器的镜像。PodSpec中定义的secrets用于私有仓库</li><li>通过CRI创建容器.填充<code>ContainerConfig</code>结构体(包括启动命令、镜像、标签、挂载、环境变量等),数据来自于PodSpec。然后通过protobufs协议发送给CRI插件。对于Docker来说,docker会反序列化请求,然后填充到自己的config结构体,然后发送给docker的后台进程。在这个过程中,它将一些元数据标签(如容器类型、日志路径、沙盒id)应用到容器。</li><li>然后,它向CPU管理器注册容器,这是1.8中的一个新的alpha功能,通过使用UpdateContainerResources CRI方法将容器分配给本地节点上的CPU集。</li><li>容器启动</li><li>若定义了post-start hook,则执行这些hook。hooks可以是<code>Exec</code>(在容器中执行特定的命令)或<code>Http</code>(执行一个http请求)。如果PostStart hook需要很长时间运行、或者失败,那么容器将不会到达running状态</li></ol><h2 id="Wrap-up-总结"><a href="#Wrap-up-总结" class="headerlink" title="Wrap-up(总结)"></a>Wrap-up(总结)</h2><p>到这里,已经产生了三个容器,他们可能运行在一个或者多个工作节点上。所有的网络、卷、secret都已经被kubelet填充,并且通过CRI插件填充到了容器中。</p>]]></content>
<summary type="html"><blockquote>
<p>原文链接:<a href="https://github.com/jamiehannaford/what-happens-when-k8s">https://github.com/jamiehannaford/what-happens-when-k</summary>
<category term="k8s" scheme="https://lightnine/github.io/categories/k8s/"/>
</entry>
<entry>
<title>redis study</title>
<link href="https://lightnine/github.io/redis-study.html"/>
<id>https://lightnine/github.io/redis-study.html</id>
<published>2024-04-01T11:33:09.000Z</published>
<updated>2024-06-10T07:00:19.059Z</updated>
<content type="html"><![CDATA[<h2 id="redis-数据结构"><a href="#redis-数据结构" class="headerlink" title="redis 数据结构"></a>redis 数据结构</h2><h3 id="简单动态字符串"><a href="#简单动态字符串" class="headerlink" title="简单动态字符串"></a>简单动态字符串</h3><p>redis中字符串是自己定义的结构,名为SDS。不是用的c语言的字符串。<br>SDS的定义如下:<br><figure class="highlight c"><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="class"><span class="keyword">struct</span> <span class="title">sdshdr</span> {</span></span><br><span class="line"> <span class="comment">// 记录buf数组中已使用字节的数量</span></span><br><span class="line"> <span class="type">int</span> len;</span><br><span class="line"> <span class="comment">// 记录buf数组中未使用字节的数量</span></span><br><span class="line"> <span class="type">int</span> <span class="built_in">free</span>;</span><br><span class="line"> <span class="comment">// 字节数组,用于存储字符串;其中最后一位保存'\0'字符</span></span><br><span class="line"> <span class="type">char</span> buf[];</span><br><span class="line">}</span><br></pre></td></tr></table></figure><br>为什么要自己实现,而不是复用c中对于字符串的定义?</p><ul><li>获取字符串长度,在O(1)复杂度获取</li><li>杜绝缓冲区溢出。SDS在进行修改时,会检查SDS空间是否满足要求。若不满足,则会进行扩容。</li><li>二进制安全,c中的字符串必须符合某种编码格式(比如ASCII),并且除了字符串的末尾之外,字符串中不能包含空字符。而sds使用len记录字符串长度,所以是二进制安全的。</li></ul><p>SDS的扩容机制:</p><ul><li>空间预分配(若对sds进行修改,先将len设置为需要的空间大小)<ul><li>若sds中len小于1MB,则将free设置为跟len一样的大小</li><li>若sds中len大于1MB,则将free设置为1MB</li></ul></li><li>惰性空间释放<ul><li>若释放sds中内容,则free中进行增加。实际占用的空间不释放;当然也提供了相应的api,在需要时,真正释放sds未使用空间。</li></ul></li></ul><h3 id="链表"><a href="#链表" class="headerlink" title="链表"></a>链表</h3><p>redis中链表结构<br>链表中节点定义<br><figure class="highlight c"><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="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">listNode</span> {</span></span><br><span class="line"> <span class="comment">// 前置节点</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">listNode</span> *<span class="title">prev</span>;</span></span><br><span class="line"> <span class="comment">// 后置节点</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">listNode</span> *<span class="title">next</span>;</span></span><br><span class="line"> <span class="comment">// 节点值</span></span><br><span class="line"> <span class="type">void</span> *value;</span><br><span class="line">} listNode;</span><br></pre></td></tr></table></figure><br>链表定义<br><figure class="highlight c"><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="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">list</span> {</span></span><br><span class="line"> <span class="comment">// 表头节点</span></span><br><span class="line"> listNode *head;</span><br><span class="line"> <span class="comment">// 表尾节点</span></span><br><span class="line"> listNode *tail;</span><br><span class="line"> <span class="comment">// 链表包含的节点数量</span></span><br><span class="line"> <span class="type">unsigned</span> <span class="type">long</span> len;</span><br><span class="line"> <span class="comment">// 节点值复制函数</span></span><br><span class="line"> <span class="type">void</span> *(*dup) (<span class="type">void</span> *ptr);</span><br><span class="line"> <span class="comment">// 节点值释放函数</span></span><br><span class="line"> <span class="type">void</span> (*<span class="built_in">free</span>) (<span class="type">void</span> *ptr);</span><br><span class="line"> <span class="comment">// 节点值对比函数</span></span><br><span class="line"> <span class="type">int</span> (*match) (<span class="type">void</span> *ptr, <span class="type">void</span> *key);</span><br><span class="line">} <span class="built_in">list</span>;</span><br></pre></td></tr></table></figure><br>注:链表节点使用 void*指针保存节点值,并且通过list中的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以保存各种不同类型的值。</p><h3 id="字典"><a href="#字典" class="headerlink" title="字典"></a>字典</h3><h3 id="跳跃表"><a href="#跳跃表" class="headerlink" title="跳跃表"></a>跳跃表</h3><h3 id="整数集合"><a href="#整数集合" class="headerlink" title="整数集合"></a>整数集合</h3><h3 id="压缩列表"><a href="#压缩列表" class="headerlink" title="压缩列表"></a>压缩列表</h3><h3 id="对象"><a href="#对象" class="headerlink" title="对象"></a>对象</h3>]]></content>
<summary type="html"><h2 id="redis-数据结构"><a href="#redis-数据结构" class="headerlink" title="redis 数据结构"></a>redis 数据结构</h2><h3 id="简单动态字符串"><a href="#简单动态字符串" class</summary>
<category term="redis" scheme="https://lightnine/github.io/categories/redis/"/>
<category term="redis" scheme="https://lightnine/github.io/tags/redis/"/>
</entry>
<entry>
<title>Top 5 things every apache kafka developer should know</title>
<link href="https://lightnine/github.io/Top-5-things-every-apache-kafka-developer-should-know.html"/>
<id>https://lightnine/github.io/Top-5-things-every-apache-kafka-developer-should-know.html</id>
<published>2023-12-09T14:20:21.000Z</published>
<updated>2024-06-10T07:00:19.016Z</updated>
<content type="html"><![CDATA[<h1 id="top-5-things-every-apache-kafka-developer-should-know"><a href="#top-5-things-every-apache-kafka-developer-should-know" class="headerlink" title="top 5 things every apache kafka developer should know"></a>top 5 things every apache kafka developer should know</h1><blockquote><p>翻译自:<a href="https://www.confluent.io/blog/top-5-things-every-apache-kafka-developer-should-know">https://www.confluent.io/blog/top-5-things-every-apache-kafka-developer-should-know</a><br>介绍下面5个内容</p><ul><li>理解消息传递和持久化保证</li><li>学习producer api中的新的粘性分区(learn about the new sticky partitioner in the producer API)</li><li>利用cooperative rebalancing(协同重平衡)来避免消费者组执行rebalance时的stop the world</li><li>掌握常用命令行工具<ul><li>kafka console producer</li><li>kafka consule consumer</li><li>dump log</li><li>delete records</li></ul></li><li>使用record headers的能力<ul><li>为kafka记录增加headers</li><li>检索headers</li></ul></li></ul></blockquote><h2 id="Tip1:理解消息传递和持久化保证"><a href="#Tip1:理解消息传递和持久化保证" class="headerlink" title="Tip1:理解消息传递和持久化保证"></a>Tip1:理解消息传递和持久化保证</h2><p>针对数据持久化,KafkaProducer提供了不同的配置。acks 配置指定了当生产者接受到多少消息确认后,才认为记录已经成功发送到broker上。kafka提供了以下三种选择:</p><ul><li>none: 生产者不等待broker的确认,发送消息后就认为已经成功发送到broker上。</li><li>one: 生产者等待leader broker的确认(leader broker有一个),一定收到确认,就认为消息发送成功</li><li>all: 生产者需要等待所有的ISR(in-sync replicas) broker都确认消息后,才认为消息发送成功。<br>如果需要更到的发送吞吐量,可以损失一定的数据,那么可以使用none或one。而如果应用不能容忍数据丢失,那么可以设置all,但是这样吞吐量会降低。<br>这里需要说明下acks=all的情况。下面的场景描述中,producer都是使用acks=all来发送消息,并且topic副本数是3,一个leader,两个follower。<br>情况1:如果这些副本中的记录偏移量是一致的,那么他们被认为是in-sync的。如下面所示,producer采用acks=all的情况:<img src="/Top-5-things-every-apache-kafka-developer-should-know/producer-ack-1024x691.png" class="" title="producer ack">情况2:假设由于某些情况(网络分区,负载过高等),导致两个follower没有跟上leader,那么follower就不是in sync的。此时生产者发送消息,那么实际的确认只会有一个。acks=all并不是指定有多少副本必须在in-sync。leader broker始终跟自己是同步的。<img src="/Top-5-things-every-apache-kafka-developer-should-know/in-sync-replicas-1024x710.png" class="" title="in-sync acks"></li></ul><p>一般来说,设置acks=all, 我们的要求通常都是所有副本都应该确认,或者至少大量的in sync副本应该确认。如果不是这样,那么应该抛出异常知道所有副本都在in sync中。<br>为了满足这个要求,kafka提供了这样一个配置:min.insync.replicas. 这个配置强制指定多少个副本写成功才被认为真正写成功。需要注意的是,min.insync.replicas配置在是broker或者topic级别,而不是在producer上。min.insync.replicas默认值是1。所以为了避免上面说的情况,在三个副本的情况下,需要将min.insync.replicas设置为2。<br><img src="/Top-5-things-every-apache-kafka-developer-should-know/not-enough-replicas-1.png" class="" title="not-enough-replicas"><br>上图中展示了in sync中的副本不满足min.insync.replicas要求的情况,此时producer发送的消息,leader broker不会将记录添加到log中,而是会抛出NotEnoughReplicasException 或者 NotEnoughReplicasAfterAppendException。副本与leader不一致被认为是一种可以重试的错误,所以producer会重试直到成功或者达到超时时间(默认值两分钟)<a href="https://kafka.apache.org/documentation/#delivery.timeout.ms">delivery.timeout.ms</a>。<br>如果需要非常高的数据持久化保证,那么应该同时设置min.insync.replicas和acks=all.</p><h2 id="Tip-2-Learn-about-the-new-sticky-partitioner-in-the-producer-API"><a href="#Tip-2-Learn-about-the-new-sticky-partitioner-in-the-producer-API" class="headerlink" title="Tip 2: Learn about the new sticky partitioner in the producer API"></a>Tip 2: Learn about the new sticky partitioner in the producer API</h2><p>kafka需要partition来提升吞吐量并且将消息均衡到不同的broker上。kafka的消息记录是key/value格式,其中key可以为null。kafka producer在发送消息时,不会立即发送,而是将消息放置到对应的partition batch中(类似缓存),待缓存满了,在一次发送。batch是一种增加网络利用的有效方式。在将消息发送到partition中,通常有三种方式来决定发送到哪个partition上。</p><ul><li>方式1:在发送消息时,直接指定消息对应的partition。这种情况,producer直接使用这个partition</li><li>方式2:如果没有提供partition,消息包含key,那么producer会使用key的hash值来决定partition。</li><li>方式3:如果既没有key也没有提供partition信息,那么kafka会使用round-robin的方式将消息发送到不同的partition中。producer会将第一个消息发送到partition 0,第二个消息发送到partition 1,以此类推。</li></ul><p>下图展示了方式3:<br><img src="/Top-5-things-every-apache-kafka-developer-should-know/producer-partition-updated.png" class="" title="producer-partition-updated"><br>round robin方法对于将消息均衡到不同的partition上工作的很好。但是存在一个缺点,由于producer是依次将消息发送到不同的partition batch中,那么有可能会出现每个partition中的batch都填充不满。比如下面展示的,topic有三个partition。假设应用产生了9条消息,并且消息没有key,所有的消息几乎是同时发送,如下图:<br><img src="/Top-5-things-every-apache-kafka-developer-should-know/sparse_batches_sent-updated.png" class="" title="sparse_batches_sent-updated"><br>9条记录分散到是三个batch中,每个batch有三条。但是如果我们将9条消息放到一个batch中会更好。更少的batch使用更少的网络带宽并且对于broker的负载更小。<br>kafka 2.4.0新增了sticky partitioner approach. 这种方法能够将消息发送到一个partition的batch中直到此batch满了。然后,发送这个batch,sticky partitioner使用下一个partition的batch。如下图展示了使用sticky partitioner的例子:<br><img src="/Top-5-things-every-apache-kafka-developer-should-know/batch-partition-updated.png" class="" title="batch-partition-updated"><br>通过使用sticky partitioner方法,我们减少了请求次数,同时也减少了请求队列上的负载,也减少了系统延迟。需要注意的时,sticky partitioner仍然是将消息均衡放置到不同的partition batch中。可以将这种认为是per-batch round robin 或者 eventually even approach。<br>如果想要更多了解sticky 模式,可以参考<a href="https://www.confluent.io/blog/apache-kafka-producer-improvements-sticky-partitioner/"> Apache Kafka Producer Improvements with the Sticky Partitioner</a></p><h2 id="Tip-3-Avoid-“stop-the-world”-consumer-group-rebalances-by-using-cooperative-rebalancing"><a href="#Tip-3-Avoid-“stop-the-world”-consumer-group-rebalances-by-using-cooperative-rebalancing" class="headerlink" title="Tip 3: Avoid “stop-the world” consumer group rebalances by using cooperative rebalancing"></a>Tip 3: Avoid “stop-the world” consumer group rebalances by using cooperative rebalancing</h2><p>kafka是一个分布式系统,而分布式系统中一个重要的事情就是如何处理失败。kafka处理失败的方式之一是使用consumer group,consumer group管理多个consumer。如果其中一个consumer停止,kafka会进行rebalance从而确保另一个consumer能够接管这个工作。<br>从2.4版本开始,kafka引入了一个新的rebalance协议,cooperative rebalancing。在深入了解cooperative rebalancing之前,先来了解一下consumer group的基础。<br>假设一个分布式应用(比如一个微服务的多个副本)有个多个consumer,订阅同一个topic。这些consumer组成了一个consumer group,具有同样的.group.id。在consumer group中的每个consumer负责从一个或多个partition中消费消息。这些partition的分配是由consumer group中的leader进行的。如下图所示:<br><img src="/Top-5-things-every-apache-kafka-developer-should-know/six-partitions.png" class="" title="six-partitions"><br>从图中可以看到,总共有6个partition,在理想的情况下,每个consumer负责消费两个partition。但是如果其中的某个应用失败了或者不能连接网络。那么对应的partition中的消息是不是就不能被消费直到应用恢复?幸运的是,由于consumer rebalancing协议的存在,不会发生这种情况。<br>下图展示了consumer group protocal过程:<br><img src="/Top-5-things-every-apache-kafka-developer-should-know/minus-consumer-2.png" class="" title="minus-consumer-2"><br>如上图,consumer2由于某些原因失败了。group coordinator将它从组中移除然后触发rebalance。rebalance尝试将工作负载在组内所有工作的consumer上进行均衡分布。在这个例子中,consumer2离开了组,rebalance会将consumer2拥有的partition分配给组内其他的consumer。所以对于一个consumer group,如果其中consumer失败了, 那么对于这些partition的处理不会产生影响。<br>但是,默认的rebalance协议有个缺点。在rebalance过程中,每个consumer都会放弃之前获得的partition(这会造成consumer停止消费),知道topic下所有的partition都被重新分配。这种情况被称为stop the world rebalance。为了解决这个问题,依靠ConsumerPartitionAssignor实例,consumer简单的重新获取之前分配的partition,所以在这些partition上仍然能够继续消费。<br>上述描述的实现被称为<a href="https://www.confluent.io/blog/cooperative-rebalancing-in-kafka-streams-consumer-ksqldb/">eager rebalancing</a>, 因为它优先考虑的是针对一个consumer group中,不会有两个consumer同时对于一个partition拥有主权。<br>虽然对于同一个topic下的某个partition不能具有相同的consumer非常重要,但是有一种更好的方法,既能够提供安全性同时还不会暂停处理,既<a href="https://www.confluent.io/blog/incremental-cooperative-rebalancing-in-kafka/">incremental cooperative rebalancing</a>。这个方法在kafka2.3版本的kafka connect中被首次引入,现在已经在consumer group 协议中实现了。利用cooperative 方法,消费者不会在rebalance开始时主动放弃partition的所有权。在cooperative方法中,consumer group中的所有成员会将当前的分配进行编码然后将信息发送到group leader中。group leader决定那个partition需要修改对应的consumer。而不是一开始就完全从新分配。之后第二次rebalance发起,但是这一次,仅仅涉及到那些需要改变所有权的分区。这有可能是撤销不在用的partition或者新增的partition。对于那些没有改变所有权的分区,这些分区中的数据会继续进行处理。<br>这种处理办法解决了stop-the-world,而仅仅是暂停了哪些需要修改所有权分区的消费。这带来了更少的rebalance代驾以及降低了完成rebalance的时间。即使rebalance时间很长也没有关系,因为现在数据仍然被处理。使用CooperativeStickyAssignor能够开启这个功能。<br>如果要开启这个功能,则需要将partition.assignment.strategy设置为使用CooperativeStickyAssignor。这种设置完全是在客户端测,所以仅仅更新客户端版本即可。而在Kafka Stream中,这个功能是默认开启的。</p><h2 id="Tip-4:掌握命令行工具"><a href="#Tip-4:掌握命令行工具" class="headerlink" title="Tip 4:掌握命令行工具"></a>Tip 4:掌握命令行工具</h2><p>下面介绍了4种在平时工作中使用最多的工具。</p><h3 id="kafka-console-producer"><a href="#kafka-console-producer" class="headerlink" title="kafka console producer"></a>kafka console producer</h3><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"><span class="comment"># 开启发送者程序, 发送的消息只有value,没有key</span></span><br><span class="line">kafka-console-producer --topic <topic> --broker-list <broker-host:port></span><br><span class="line"><span class="comment"># 发送消息,发送的消息包含key 和 value</span></span><br><span class="line">kafka-console-producer --topic <topic> --broker-list <broker-host:port> --property parse.key=<span class="literal">true</span> --property key.separator=<span class="string">":"</span></span><br></pre></td></tr></table></figure><h3 id="kafka-console-consumer"><a href="#kafka-console-consumer" class="headerlink" title="kafka console consumer"></a>kafka console consumer</h3><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"><span class="comment"># 消费指定topic中的消息</span></span><br><span class="line">kafka-console-consumer --topic <topic> --bootstrap-server <broker-host:port></span><br><span class="line"><span class="comment"># 指定从开始的地方消费</span></span><br><span class="line">kafka-console-consumer --topic <topic> --bootstrap-server <broker-host:port> --from-beginning</span><br><span class="line"><span class="comment"># 默认情况下consumer只会打印消息的value,如果想要打印消息的key,则输入下面命令</span></span><br><span class="line">kafka-console-consumer --topic <topic> --bootstrap-server <broker-host:port> --property print.key=<span class="literal">true</span> --property key.separator=<span class="string">":"</span></span><br></pre></td></tr></table></figure><h3 id="Dump-log"><a href="#Dump-log" class="headerlink" title="Dump log"></a>Dump log</h3><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"><span class="comment"># 指定打印topic为example-0中的日志,参数--print-data-log表示输出日志</span></span><br><span class="line"><span class="comment"># 不过一般在生产环境中不会使用这个命令</span></span><br><span class="line">kafka-dump-log --print-data-log --files ./var/lib/kafka/data/example-0/00000000000000000000.<span class="built_in">log</span></span><br></pre></td></tr></table></figure><h3 id="delete-records"><a href="#delete-records" class="headerlink" title="delete records"></a>delete records</h3><p>kafka提供了配置来控制数据保留,包括时间和数据大小</p><ul><li>数据保留的时间由 log.retention.hours 控制,默认值是168hour,也就是一周</li><li>configuration.log.retention.bytes 控制segment文件最大是多少。默认值是-1, 也就是不限制大小</li></ul><p>如果想要删除数据,可以使用下述命令:<br><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">kafka-delete-records --bootstrap-server <broker-host:port> \</span><br><span class="line"> --offset-json-file offsets.json</span><br></pre></td></tr></table></figure><br>offsets.json 文件内容如下:<br><figure class="highlight json"><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="punctuation">{</span></span><br><span class="line"> <span class="attr">"partitions"</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line"> <span class="punctuation">{</span><span class="attr">"topic"</span><span class="punctuation">:</span> <span class="string">"example"</span><span class="punctuation">,</span> <span class="attr">"partition"</span><span class="punctuation">:</span> <span class="number">0</span><span class="punctuation">,</span> <span class="attr">"offset"</span><span class="punctuation">:</span> <span class="number">-1</span><span class="punctuation">}</span></span><br><span class="line"> <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"version"</span><span class="punctuation">:</span><span class="number">1</span></span><br><span class="line"> <span class="punctuation">}</span></span><br></pre></td></tr></table></figure><br>参数介绍如下:</p><ul><li>topic:指定要删除数据对应的topic</li><li>partition:指定需要删除数据对应的partition</li><li>offset:指定从哪个offset开始删除,注:是删除offset之前的数据。-1表示删除当前HW之前的数据,HW(high watermark)表示能够开始消费的位置</li></ul><h2 id="Tip5:使用record-headers的能力"><a href="#Tip5:使用record-headers的能力" class="headerlink" title="Tip5:使用record headers的能力"></a>Tip5:使用record headers的能力</h2><p>Record headers可以给kafka消息添加一些元数据,并且不是给消息的key value添加额外的信息。比如如果你想要在消息中嵌入一些信息,如表示消息来源系统,也是是想要增加一些审计功能。<br>为什么不能将这些额外的数据添加到key中。因为给key中添加数据会带来两个潜在的问题</p><ol><li>首先,如果你使用的是压缩主题,那么给key添加信息会使得消息不正确。这样压缩不会像之前起作用</li><li>其次,给key添加额外的信息有可能会影响数据的partition分布</li></ol><h1 id="回顾"><a href="#回顾" class="headerlink" title="回顾"></a>回顾</h1><p>我们了解了kafka的五个tips,我们理解了下面的知识点</p><ol><li>消息持久性以及和消息传递之间的关系</li><li>producer API中的sticky partitioner</li><li>command line tools</li><li>record headers的能力</li></ol>]]></content>
<summary type="html"><h1 id="top-5-things-every-apache-kafka-developer-should-know"><a href="#top-5-things-every-apache-kafka-developer-should-know" class="heade</summary>
<category term="kafka" scheme="https://lightnine/github.io/categories/kafka/"/>
<category term="kafka" scheme="https://lightnine/github.io/tags/kafka/"/>
</entry>
<entry>
<title>java-zero-copy</title>
<link href="https://lightnine/github.io/java-zero-copy.html"/>
<id>https://lightnine/github.io/java-zero-copy.html</id>
<published>2023-12-04T03:17:19.000Z</published>
<updated>2024-12-12T03:52:34.292Z</updated>
<content type="html"><![CDATA[<blockquote><p>本篇内容主要翻译自<a href="https://developer.ibm.com/articles/j-zerocopy/">Efficient data transfer through zero copy</a>,包括有些自己的思考</p></blockquote><h1 id="java-zero-copy"><a href="#java-zero-copy" class="headerlink" title="java zero copy"></a>java zero copy</h1><p>很多网页应用有大量的静态内容,针对这些静态内容主要的处理逻辑就是从磁盘读取数据,然后将数据写入到响应的socket中。这项工作应该需要较少的CPU资源。但是有时候并不是。内核从磁盘读取数据,然后将数据拷贝到应用中。之后应用将数据写入到内核,然后推送到socket中。实际上,应用程序在这里扮演了一个无效率的中间层,既将数据从磁盘写入到socket。<br>每一次当数据在用户空间和内核空间转换时,数据都会被拷贝,而这会消耗cpu cycles以及内存带宽。幸运的是,我们可以采用zero copy技术来避免内核和应用程序之间的数据拷贝。应用程序使用zero copy技术来请求内核直接将数据从磁盘文件拷贝到socket中,而不需要经过应用程序。zero copy技术能够极大的提升应用程序性能并且减少内核空间和用户空间之间的切换。<br>Java中使用<code>java.nio.channels.FileChannel</code>中的<code>transferTo()</code>方法在linux和Unix系统重实现zero copy。使用<code>transferTo()</code>方法能够直接将字节数据从一个channel写入到另一个channel中,而数据不需要经过应用程序。本篇文章首先展示使用传统的拷贝方法消耗的资源,然后展示使用zero copy获得的性能提升。</p><h2 id="数据传输-传统方法"><a href="#数据传输-传统方法" class="headerlink" title="数据传输:传统方法"></a>数据传输:传统方法</h2><p>想想一个简单的场景,从一个文件读取数据,然后将数据通过网络写入到另一个程序中。核心操作如下所示。<br><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">File.read(fileDesc, buf, len);</span><br><span class="line">Socket.send(socket, buf, len);</span><br></pre></td></tr></table></figure><br>虽然看起来很简单,但是这个操作需要再内核空间和用户空间4次上下文切换,以及4次数据拷贝。下图展示了具体过程<br><img src="/java-zero-copy/figure1.gif" class="" title="传统的数据拷贝过程"><br>下图展示了上下文切换过程<br><img src="/java-zero-copy/figure2.gif" class="" title="上下文切换"></p><p>主要的步骤如下:</p><ol><li><code>read()</code>方法从user mode 转换到 kernel mode。在read内部是发一起了一次系统调用<code>sys_read()</code>从文件读取数据。第一次数据拷贝是由DMA引擎来执行,DMA从磁盘读取文件,然后将数据保存到内核缓冲区中。</li><li>请求的数据被从内核缓冲区拷贝到用户缓冲区,<code>read()</code>方法返回。这引起了第二次的上下文切换。现在数据是在用户空间中的缓冲区中。</li><li><code>send()</code>方法调用引起用户空间到内核空间的切换。这次会将数据从用户空间拷贝到内核缓冲区。这一次数据是放置到另外一个内核缓冲区中,与目的socket相关联。</li><li><code>send()</code>方法返回,又引起了一次上下文切换。DMA将数据从内核缓冲区拷贝到网卡缓冲区中,这是第四次数据拷贝。</li></ol><p>我们为什么不直接将数据拷贝到用户空间,而要经过内核空间呢?首先因为用户进程没有办法直接读取硬盘,这是基于保护的目的,都要经过操作系统的处理,才能读取到硬盘内容;其次这是因为操作系统引入内核缓冲区是为了提升性能。操作系统读取数据都会预读取一些数据,这样在应用程序读取额外的数据时,可以不用发起系统调用从硬盘中读取,而是直接从内核缓冲区获取即可。而写入过程,可以完全实现为异步过程。既应用程序只需要将数据写入到内核缓冲区中,而不是写入到磁盘中。<br>但是,设想当前应用程序需要处理的数据要远远大于内核空间缓冲区的大小。而此时,数据需要在磁盘,内核缓冲区,用户空间中来回拷贝。这会严重影响性能。<br>Zero copy技术是解决这个问题的方法。</p><h2 id="数据传输:zero-copy方法"><a href="#数据传输:zero-copy方法" class="headerlink" title="数据传输:zero copy方法"></a>数据传输:zero copy方法</h2><p>如果你仔细检查上面的过程,你会发现第二次和第三次数据拷贝可以省略。应用程序针对这些数据什么也不做。因此数据可以被直接从内核缓冲区拷贝到socket buffer中。<code>transferTo()</code>方法可以完成这个操作。下面展示了此方法<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">transferTo</span><span class="params">(<span class="type">long</span> position, <span class="type">long</span> count, WritableByteChannel target)</span>;</span><br></pre></td></tr></table></figure><br><code>transferTo()</code>方法将数据从文件channel拷贝到target channel中。这个方法依赖底层操作系统对于zero copy的支持。在UNIX或linux中,使用的是<code>sendfile()</code>系统调用。如下所示:<br><figure class="highlight c"><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">#<span class="keyword">include</span> <span class="string"><sys/socket.h></span></span></span><br><span class="line"><span class="type">ssize_t</span> <span class="title function_">sendfile</span><span class="params">(<span class="type">int</span> out_fd, <span class="type">int</span> in_fd, <span class="type">off_t</span> *offset, <span class="type">size_t</span> count)</span>;</span><br></pre></td></tr></table></figure><br>下图展示了<code>transferTo()</code>方法执行过程<br><img src="/java-zero-copy/figure3.gif" class="" title="transferTo过程"></p><p>下图展示了具体的上下文切换<br><img src="/java-zero-copy/figure4.gif" class="" title="上下文切换"></p><p><code>transferTo()</code>方法执行过程如下:</p><ol><li>DMA引擎将文件内容拷贝到内核缓冲区。然后数据从内核缓冲区拷贝到socket buffer中。(涉及到两次数据拷贝)</li><li>第三次数据拷贝发生在DMA引擎将数据从socket buffer拷贝到网卡中。</li></ol><p>我们将上下文切换从4次降低到2次,数据拷贝从4次降低到3次(其中仅有一个数据拷贝需要CPU参与,图中序号2)。但是这还没有达到zero copy的目的。如果底层网卡支持gather操作,那么我们可以减少内核空间中的数据重复。在linux kernel2.4及以后版本中,socket buffer已经支持了这个操作。这个方法不仅仅减少了上下文切换并且也消除了CPU参与的数据拷贝。具体如下:</p><ol><li><code>transferTo()</code>方法将文件内容拷贝到内核缓冲区,由DMA引擎执行</li><li>不需要将数据拷贝进socket buffer中,仅仅将数据的位置以及数据长度添加到socket buffer中。DMA引擎直接将kernel buffer中的数据拷贝到网卡中。<br>下图展示了包含gather操作的<code>transferTo()</code><img src="/java-zero-copy/figure5.gif" class="" title="gather操作的transferTo方法"></li></ol><h2 id="性能比较"><a href="#性能比较" class="headerlink" title="性能比较"></a>性能比较</h2><p>使用java实现文件传输,采用传统的IO和nio来分别实现进行对比。完整代码<a href="https://github.com/lightnine/j-zerocopy">参考</a>.其中客户端是主要的视线,服务端仅仅读取数据。</p><h3 id="传统IO-client-代码"><a href="#传统IO-client-代码" class="headerlink" title="传统IO client 代码"></a>传统IO client 代码</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 1. create socket and connect to server</span></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> socket = <span class="keyword">new</span> <span class="title class_">Socket</span>(Common.SERVER, port);</span><br><span class="line"> System.out.println(<span class="string">"Connected with server "</span> + socket.getInetAddress() + <span class="string">":"</span> + socket.getPort());</span><br><span class="line"> } <span class="keyword">catch</span> (UnknownHostException e) {</span><br><span class="line"> System.out.println(e);</span><br><span class="line"> System.exit(Common.ERROR);</span><br><span class="line"> } <span class="keyword">catch</span> (IOException e) {</span><br><span class="line"> System.out.println(e);</span><br><span class="line"> System.exit(Common.ERROR);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 2. send data to server</span></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> inputStream = Files.newInputStream(Paths.get(fileName));</span><br><span class="line"> output = <span class="keyword">new</span> <span class="title class_">DataOutputStream</span>(socket.getOutputStream());</span><br><span class="line"> <span class="type">long</span> <span class="variable">start</span> <span class="operator">=</span> System.currentTimeMillis();</span><br><span class="line"> <span class="type">byte</span>[] b = <span class="keyword">new</span> <span class="title class_">byte</span>[<span class="number">4096</span>];</span><br><span class="line"> <span class="type">long</span> <span class="variable">read</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"> <span class="type">long</span> <span class="variable">total</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"> <span class="comment">// read function cause user mode to kernel mode,</span></span><br><span class="line"> <span class="comment">// and DMA engine read file content from disk to kernel buffer</span></span><br><span class="line"> <span class="comment">// then copy kernel buffer to the b array. This cause another context switch</span></span><br><span class="line"> <span class="comment">// then when read return, cause kernel mode to user mode</span></span><br><span class="line"> <span class="comment">// Summary: two context switch, two copy(one cpu copy)</span></span><br><span class="line"> <span class="keyword">while</span> ((read = inputStream.read(b)) >= <span class="number">0</span>) {</span><br><span class="line"> total = total + read;</span><br><span class="line"> System.out.println(<span class="string">"total size:"</span> + total);</span><br><span class="line"> <span class="comment">// write function cause user mode to kernel mode,</span></span><br><span class="line"> <span class="comment">// and copy data from b array to socket buffer,</span></span><br><span class="line"> <span class="comment">// then DMA engine copy socket buffer to nic(network interface) buffer</span></span><br><span class="line"> <span class="comment">// then when write return, cause kernel mode to user mode,</span></span><br><span class="line"> <span class="comment">// Summary: two context switch, two copy(one cpu copy)</span></span><br><span class="line"> output.write(b);</span><br><span class="line"> }</span><br><span class="line"> System.out.println(<span class="string">"bytes send: "</span> + total + <span class="string">" and totalTime(ms):"</span> + (System.currentTimeMillis() - start));</span><br><span class="line"> } <span class="keyword">catch</span> (IOException e) {</span><br><span class="line"> System.out.println(e);</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><h3 id="nio-client-代码"><a href="#nio-client-代码" class="headerlink" title="nio client 代码"></a>nio client 代码</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testSendfile</span><span class="params">()</span> <span class="keyword">throws</span> IOException {</span><br><span class="line"> <span class="comment">// 1. get file size(bytes)</span></span><br><span class="line"> <span class="type">Path</span> <span class="variable">path</span> <span class="operator">=</span> Paths.get(fileName);</span><br><span class="line"> <span class="type">long</span> <span class="variable">fsize</span> <span class="operator">=</span> Files.size(path);</span><br><span class="line"></span><br><span class="line"> <span class="type">SocketAddress</span> <span class="variable">sad</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">InetSocketAddress</span>(Common.SERVER, port);</span><br><span class="line"> <span class="type">SocketChannel</span> <span class="variable">sc</span> <span class="operator">=</span> SocketChannel.open();</span><br><span class="line"> sc.connect(sad);</span><br><span class="line"> sc.configureBlocking(<span class="literal">true</span>);</span><br><span class="line"> <span class="type">FileInputStream</span> <span class="variable">fis</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">FileInputStream</span>(fileName);</span><br><span class="line"> <span class="type">FileChannel</span> <span class="variable">fc</span> <span class="operator">=</span> fis.getChannel();</span><br><span class="line"> <span class="type">long</span> <span class="variable">start</span> <span class="operator">=</span> System.currentTimeMillis();</span><br><span class="line"> <span class="type">long</span> <span class="variable">curnset</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// in linux kernel 2.4 and later</span></span><br><span class="line"> <span class="comment">// transferTo() function cause user mode to kernel mode</span></span><br><span class="line"> <span class="comment">// DMA engine copy data from disk to kernel buffer</span></span><br><span class="line"> <span class="comment">// then just copy data position and data length to kernel buffer</span></span><br><span class="line"> <span class="comment">// then DMA engine copy kernel buffer to NIC buffer</span></span><br><span class="line"> <span class="comment">// when transferTo return, cause another context switch</span></span><br><span class="line"> <span class="comment">// Summary: two context switch, two copy(zero CPU copy)</span></span><br><span class="line"> curnset = fc.transferTo(<span class="number">0</span>, fsize, sc);</span><br><span class="line"> System.out.println(<span class="string">"total bytes transferred: "</span> + curnset + <span class="string">" and time taken in MS: "</span> +</span><br><span class="line"> (System.currentTimeMillis() - start) );</span><br><span class="line"></span><br><span class="line"> fc.close();</span><br><span class="line"> fis.close();</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>性能比较如下:</p><div class="table-container"><table><thead><tr><th>file size</th><th>traditional(ms)</th><th>nio(ms)</th></tr></thead><tbody><tr><td>12MB</td><td>50</td><td>18</td></tr><tr><td>221MB</td><td>690</td><td>314</td></tr><tr><td>2.5G</td><td>15496</td><td>2610</td></tr></tbody></table></div><p>评测环境:</p><ol><li>java : openjdk version “1.8.0_382”, OpenJDK Runtime Environment (build 1.8.0_382-b05), OpenJDK 64-Bit Server VM (build 25.382-b05, mixed mode)</li><li>linux: CentOS Linux release 7.6 (Final)</li><li>kernel: 4.14.0_1-0-0-51</li></ol>]]></content>
<summary type="html"><blockquote>
<p>本篇内容主要翻译自<a href="https://developer.ibm.com/articles/j-zerocopy/">Efficient data transfer through zero copy</a>,包括有些自己的思考</p</summary>
<category term="linux" scheme="https://lightnine/github.io/categories/linux/"/>
<category term="java" scheme="https://lightnine/github.io/tags/java/"/>
<category term="linux" scheme="https://lightnine/github.io/tags/linux/"/>
<category term="zero-copy" scheme="https://lightnine/github.io/tags/zero-copy/"/>
</entry>
<entry>
<title>Read/Write locks in java(java中的读写锁)</title>
<link href="https://lightnine/github.io/Read-Write-locks-in-java-java%E4%B8%AD%E7%9A%84%E8%AF%BB%E5%86%99%E9%94%81.html"/>
<id>https://lightnine/github.io/Read-Write-locks-in-java-java%E4%B8%AD%E7%9A%84%E8%AF%BB%E5%86%99%E9%94%81.html</id>
<published>2019-05-17T10:38:44.000Z</published>
<updated>2024-06-10T07:00:19.013Z</updated>
<content type="html"><![CDATA[<p>本篇文章主要介绍读写锁的一些原理及实现。翻译原文<a href="http://tutorials.jenkov.com/java-concurrency/read-write-locks.html">地址</a></p><h1 id="Java中的读写锁"><a href="#Java中的读写锁" class="headerlink" title="Java中的读写锁"></a>Java中的读写锁</h1><p>假设一个java应用程序需要读以及写一些资源,但是写的频率要远远低于读。多个读线程读取资源不会有什么问题。但是如果一个线程想要写资源,那么同时就不能有其他线程读或写这个资源。为了能够允许多个读线程和一个写线程,我们需要读写锁。<br>虽然Java中提供了读写锁的实现,但是我们还是要知道读写锁背后的原理,这样才能在实际使用中处理具体的问题。</p><h2 id="Java中实现读写锁"><a href="#Java中实现读写锁" class="headerlink" title="Java中实现读写锁"></a>Java中实现读写锁</h2><p>总结一下对于获取资源的读权限和写权限可以如下:<br>读权限:当没有其他线程写当前资源或者没有其他线程请求当前资源的写权限,那么当前线程就能够获取资源的读权限<br>写权限:如果没有其他线程读或者写当前资源,当前线程就能获取资源的写权限<br>只要没有其他线程正在写资源或者没有其他线程请求写资源,那么当前线程就能够读取资源。如果读线程发生的很多,但是又没有提升写线程的优先级,那么就可能发生”饥饿”现象。</p><h2 id="可重入读写锁"><a href="#可重入读写锁" class="headerlink" title="可重入读写锁"></a>可重入读写锁</h2><h2 id="可重入读锁"><a href="#可重入读锁" class="headerlink" title="可重入读锁"></a>可重入读锁</h2><h2 id="可重入写锁"><a href="#可重入写锁" class="headerlink" title="可重入写锁"></a>可重入写锁</h2><h2 id="读锁升级到写锁"><a href="#读锁升级到写锁" class="headerlink" title="读锁升级到写锁"></a>读锁升级到写锁</h2><h2 id="写锁降级到读锁"><a href="#写锁降级到读锁" class="headerlink" title="写锁降级到读锁"></a>写锁降级到读锁</h2><h2 id="可重入读写锁的完整实现"><a href="#可重入读写锁的完整实现" class="headerlink" title="可重入读写锁的完整实现"></a>可重入读写锁的完整实现</h2><h2 id="在finally中调用unlock"><a href="#在finally中调用unlock" class="headerlink" title="在finally中调用unlock"></a>在finally中调用unlock</h2>]]></content>
<summary type="html"><p>本篇文章主要介绍读写锁的一些原理及实现。翻译原文<a href="http://tutorials.jenkov.com/java-concurrency/read-write-locks.html">地址</a></p>
<h1 id="Java中的读写锁"><a hre</summary>
<category term="java" scheme="https://lightnine/github.io/categories/java/"/>
<category term="并发" scheme="https://lightnine/github.io/tags/%E5%B9%B6%E5%8F%91/"/>
</entry>
<entry>
<title>Thread Signaling</title>
<link href="https://lightnine/github.io/Thread-Signaling.html"/>
<id>https://lightnine/github.io/Thread-Signaling.html</id>
<published>2019-05-14T02:52:48.000Z</published>
<updated>2024-06-10T07:00:19.014Z</updated>
<content type="html"><![CDATA[<h1 id="线程信号量及wait,notify方法"><a href="#线程信号量及wait,notify方法" class="headerlink" title="线程信号量及wait,notify方法"></a>线程信号量及wait,notify方法</h1><p>本篇主要介绍线程之间如何进行信号的通知。同时介绍wait,notify底层的一些实现。</p><h2 id="通过共享对象进行信号通知"><a href="#通过共享对象进行信号通知" class="headerlink" title="通过共享对象进行信号通知"></a>通过共享对象进行信号通知</h2><p>最简单的进行线程之间通知的方式就是采用共享变量的方式。比如下面的代码<br><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">public</span> <span class="keyword">class</span> <span class="title class_">MySignal</span>{</span><br><span class="line"> <span class="keyword">protected</span> <span class="type">boolean</span> <span class="variable">hasDataToProcess</span> <span class="operator">=</span> <span class="literal">false</span>;</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="type">boolean</span> <span class="title function_">hasDataToProcess</span><span class="params">()</span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">this</span>.hasDataToProcess;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title function_">setHasDataToProcess</span><span class="params">(<span class="type">boolean</span> hasData)</span>{</span><br><span class="line"> <span class="built_in">this</span>.hasDataToProcess = hasData; </span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><br>线程A和线程B共享同一个MySingal实例,当线程A处理好数据后,可以设置hasDataToProcess属性为true,然后线程B获取到此属性。从而完成线程之间的信号通知。当然,如果线程A和线程B不是在同一个MySingal实例上进行的,则不能进行信号的传递。</p><h2 id="忙等待"><a href="#忙等待" class="headerlink" title="忙等待"></a>忙等待</h2><p>在采用MySingal的例子中,一般会采用下面的代码来判断一个线程是否可以进行处理了。<br><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="keyword">protected</span> <span class="type">MySignal</span> <span class="variable">sharedSignal</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">MySingal</span>()</span><br><span class="line"><span class="keyword">while</span>(!sharedSignal.hasDataToProcess()){</span><br><span class="line"> <span class="comment">//do nothing... busy waiting</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><br>从代码中可以看到,检测hasDataToProcess属性是在一个while循环中,如果hasDataToProcess为false,这就会造成线程一直在执行while语句,造成忙等待。这会造成CPU资源的浪费。</p><h2 id="wait,notify,notifyAll使用"><a href="#wait,notify,notifyAll使用" class="headerlink" title="wait,notify,notifyAll使用"></a>wait,notify,notifyAll使用</h2><p>一般在Java中,我们一般会采用wait,notify或notifyAll进行线程之间信号的传递。线程调用某一个对象上的wait方法前,必须首先获取该对象上的锁,才能执行此对象上的wait方法。在调用notify或者notifyAll之前,也是要获取对应对象上的锁。调用wait方法时,会释放此对象上的锁,线程进入阻塞状态,等待信号。而调用notify后,会随机唤醒一个同对象上的线程,但是必须是退出了notify对应的synchronized块后,被唤醒的线程才能继续执行,因为被唤醒的线程还要获取对象上的锁。如下面的代码所示:<br><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="keyword">class</span> <span class="title class_">MonitorObject</span>{</span><br><span class="line">}</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyWaitNotify</span>{</span><br><span class="line"> <span class="type">MonitorObject</span> <span class="variable">myMonitorObject</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">MonitorObject</span>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">doWait</span><span class="params">()</span>{</span><br><span class="line"> <span class="keyword">synchronized</span>(myMonitorObject){</span><br><span class="line"> <span class="keyword">try</span>{</span><br><span class="line"> myMonitorObject.wait();</span><br><span class="line"> } <span class="keyword">catch</span>(InterruptedException e){...}</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">doNotify</span><span class="params">()</span>{</span><br><span class="line"> <span class="keyword">synchronized</span>(myMonitorObject){</span><br><span class="line"> myMonitorObject.notify();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><br>从代码中,可以看到在myMonitorObject上调用wait方法之前,会先获取myMonitorObject上的锁。在调用notify方法之前,也是要先获取myMonitorObject上的锁。在下面的内容中我会简单介绍wait和notify的底层原理。</p><h3 id="wait-notify-notifyAll在Object源码中的介绍"><a href="#wait-notify-notifyAll在Object源码中的介绍" class="headerlink" title="wait notify notifyAll在Object源码中的介绍"></a>wait notify notifyAll在Object源码中的介绍</h3><p>在JDK1.8中,Object对象中有三个wait(),wait(long timeout),wait(long timeout, int nanos)这三个方法,它们的主要区别是后两个方法增加了等待的时间。<br>下面是JDK1.8中关于wait(long timeout)方法的描述:</p><blockquote><p>/**</p><pre><code> * Causes the current thread to wait until either another thread invokes the * {@link java.lang.Object#notify()} method or the * {@link java.lang.Object#notifyAll()} method for this object, or a * specified amount of time has elapsed. * <p> * The current thread must own this object's monitor. * <p> * This method causes the current thread (call it <var>T</var>) to * place itself in the wait set for this object and then to relinquish * any and all synchronization claims on this object. Thread <var>T</var> * becomes disabled for thread scheduling purposes and lies dormant * until one of four things happens: * <ul> * <li>Some other thread invokes the {@code notify} method for this * object and thread <var>T</var> happens to be arbitrarily chosen as * the thread to be awakened. * <li>Some other thread invokes the {@code notifyAll} method for this * object. * <li>Some other thread {@linkplain Thread#interrupt() interrupts} * thread <var>T</var>. * <li>The specified amount of real time has elapsed, more or less. If * {@code timeout} is zero, however, then real time is not taken into * consideration and the thread simply waits until notified. * </ul> * The thread <var>T</var> is then removed from the wait set for this * object and re-enabled for thread scheduling. It then competes in the * usual manner with other threads for the right to synchronize on the * object; once it has gained control of the object, all its * synchronization claims on the object are restored to the status quo * ante - that is, to the situation as of the time that the {@code wait} * method was invoked. Thread <var>T</var> then returns from the * invocation of the {@code wait} method. Thus, on return from the * {@code wait} method, the synchronization state of the object and of * thread {@code T} is exactly as it was when the {@code wait} method * was invoked. * <p> * A thread can also wake up without being notified, interrupted, or * timing out, a so-called <i>spurious wakeup</i>. While this will rarely * occur in practice, applications must guard against it by testing for * the condition that should have caused the thread to be awakened, and * continuing to wait if the condition is not satisfied. In other words, * waits should always occur in loops, like this one: * <pre> * synchronized (obj) { * while (&lt;condition does not hold&gt;) * obj.wait(timeout); * ... // Perform action appropriate to condition * } * </pre> * (For more information on this topic, see Section 3.2.3 in Doug Lea's * "Concurrent Programming in Java (Second Edition)" (Addison-Wesley, * 2000), or Item 50 in Joshua Bloch's "Effective Java Programming * Language Guide" (Addison-Wesley, 2001). * * <p>If the current thread is {@linkplain java.lang.Thread#interrupt() * interrupted} by any thread before or while it is waiting, then an * {@code InterruptedException} is thrown. This exception is not * thrown until the lock status of this object has been restored as * described above. * * <p> * Note that the {@code wait} method, as it places the current thread * into the wait set for this object, unlocks only this object; any * other objects on which the current thread may be synchronized remain * locked while the thread waits. * <p> * This method should only be called by a thread that is the owner * of this object's monitor. See the {@code notify} method for a * description of the ways in which a thread can become the owner of * a monitor. * * @param timeout the maximum time to wait in milliseconds. * @throws IllegalArgumentException if the value of timeout is * negative. * @throws IllegalMonitorStateException if the current thread is not * the owner of the object's monitor. * @throws InterruptedException if any thread interrupted the * current thread before or while the current thread * was waiting for a notification. The <i>interrupted * status</i> of the current thread is cleared when * this exception is thrown. * @see java.lang.Object#notify() * @see java.lang.Object#notifyAll() */public final native void wait(long timeout) throws InterruptedException;</code></pre></blockquote><p>从代码的注释中,我们可以获得以下一些内容:</p><ol><li>调用wait的线程一定要获取对象上的monitor,在调用wait后,会释放该对象上的锁。</li><li>发生以下四种情况线程会被唤醒:<ul><li>其他的线程在相同的对象上调用了notify</li><li>其他的线程在相同的对象上调用了notifyAll</li><li>其他的线程终止了当前线程(interrupt方法),会扔出InterruptedException异常</li><li>指定的最大等待时间到了</li></ul></li><li>当线程被唤醒时,当前的线程会从对象的等待集合中移除,重新进入线程调度阶段。之后会跟其他线程竞争获取对象上的锁。</li><li>线程可能在没有上述四种情况发生的时候,被唤醒。这被称为伪唤醒,下面会有详细介绍。</li></ol><h3 id="浅析Object-monitor的底层原理"><a href="#浅析Object-monitor的底层原理" class="headerlink" title="浅析Object monitor的底层原理"></a>浅析Object monitor的底层原理</h3><p>在JVM实现获取对象上的锁,是通过monitor进行实现的。图示如下:<br><img src="/Thread-Signaling/monitor.png" class="" title="java-memory-model"><br>当一个线程需要获取 Object 的锁时,会被放入 EntrySet 中进行等待,如果该线程获取到了锁,成为当前锁的 owner。如果根据程序逻辑,一个已经获得了锁的线程缺少某些外部条件,而无法继续进行下去(例如生产者发现队列已满或者消费者发现队列为空),那么该线程可以通过调用 wait 方法将锁释放,进入 wait set 中阻塞进行等待,其它线程在这个时候有机会获得锁,去干其它的事情,从而使得之前不成立的外部条件成立,这样先前被阻塞的线程就可以重新进入 EntrySet 去竞争锁。</p><h3 id="notify和notifyAll区别"><a href="#notify和notifyAll区别" class="headerlink" title="notify和notifyAll区别"></a>notify和notifyAll区别</h3><p>乍一看,notify和notifyAll的区别很简单,就是notify只能随机选择一个处于等待状态的线程进行唤醒;而notifyAll可以唤醒所有处于等待状态下的线程,但是也是只有一个线程能够继续执行。<br>如果结合上面的图片,我们就能更好地进行理解。当线程在对象上调用notify方法时,随机选择一个处于等待状态的线程,并且把该线程放置在该对象的EntrySet列表中。而如果调用的是notifyAll方法时,所有的处于等待状态下的线程都会进入到EntrySet中,从而多个线程进行对象上的锁。notify有点类似于网络中的单播,而notifyAll类似于多播。</p><h2 id="丢失信号"><a href="#丢失信号" class="headerlink" title="丢失信号"></a>丢失信号</h2><p>设想一下这种情况,有一个线程首先调用了notify方法,然后其他的线程调用了wait方法。那么处于wait下的线程将不会接受到之前线程发送的notify信号。如下面代码所示,<a href="https://github.com/lightnine/daydayup/blob/master/src/main/java/com/leon/concurrent/waitNotify/WaitNotifyMissSingal.java">代码地址</a>:<br><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="keyword">public</span> <span class="keyword">class</span> <span class="title class_">WaitNotifyMissSingal</span> {</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> {</span><br><span class="line"> <span class="keyword">final</span> <span class="type">Object</span> <span class="variable">lock</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Object</span>();</span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">Thread</span>(<span class="keyword">new</span> <span class="title class_">Runnable</span>() {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(<span class="string">"thread B is waiting to get lock"</span>);</span><br><span class="line"> <span class="keyword">synchronized</span> (lock) {</span><br><span class="line"> System.out.println(<span class="string">"thread B get lock"</span>);</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> TimeUnit.SECONDS.sleep(<span class="number">5</span>);</span><br><span class="line"> } <span class="keyword">catch</span> (InterruptedException e) {</span><br><span class="line"> e.printStackTrace();</span><br><span class="line"> }</span><br><span class="line"> lock.notify();</span><br><span class="line"> System.out.println(<span class="string">"thread B do notify method"</span>);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }).start();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">Thread</span>(<span class="keyword">new</span> <span class="title class_">Runnable</span>() {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(<span class="string">"thread A is waiting to get lock"</span>);</span><br><span class="line"> <span class="keyword">synchronized</span> (lock) {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> System.out.println(<span class="string">"thread A get lock"</span>);</span><br><span class="line"> TimeUnit.SECONDS.sleep(<span class="number">1</span>);</span><br><span class="line"> System.out.println(<span class="string">"thread A do wait method"</span>);</span><br><span class="line"> lock.wait();</span><br><span class="line"> System.out.println(<span class="string">"wait end"</span>);</span><br><span class="line"> } <span class="keyword">catch</span> (InterruptedException e) {</span><br><span class="line"> e.printStackTrace();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }).start();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><br>运行上面的代码,程序会一直运行下去。那么如何解决这个问题呢,其实在Object的wait的方法注释中也有对应的说明。我们可以把通知信号保存在信号类的成员变量中。<a href="https://github.com/lightnine/daydayup/blob/master/src/main/java/com/leon/concurrent/waitNotify/MyWaitNotify2.java">代码地址</a><br><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="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyWaitNotify2</span>{</span><br><span class="line"> <span class="type">MonitorObject</span> <span class="variable">myMonitorObject</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">MonitorObject</span>();</span><br><span class="line"> <span class="type">boolean</span> <span class="variable">wasSignalled</span> <span class="operator">=</span> <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">doWait</span><span class="params">()</span>{</span><br><span class="line"> <span class="keyword">synchronized</span>(myMonitorObject){</span><br><span class="line"> <span class="keyword">if</span>(!wasSignalled){</span><br><span class="line"> <span class="keyword">try</span>{</span><br><span class="line"> myMonitorObject.wait();</span><br><span class="line"> } <span class="keyword">catch</span>(InterruptedException e){...}</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">//clear signal and continue running.</span></span><br><span class="line"> wasSignalled = <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">doNotify</span><span class="params">()</span>{</span><br><span class="line"> <span class="keyword">synchronized</span>(myMonitorObject){</span><br><span class="line"> wasSignalled = <span class="literal">true</span>;</span><br><span class="line"> myMonitorObject.notify();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><br>在上面的代码中,将信号保存在了wasSingalled变量中,只要调用了notify方法,就是将wasSignalled设置为true,表示有线程执行了notify。在doWait方法中,如果wasSignalled为false,则当前的线程会执行wait方法,进入等待状态;而当wasSignalled为true时,不会执行wait方法,只会将wasSignalled设置为false。</p><h2 id="伪唤醒"><a href="#伪唤醒" class="headerlink" title="伪唤醒"></a>伪唤醒</h2><p>因为一些底层操作系统的原因,具体的可以查看unix操作系统相关内容。简单理解,就是当线程没有接受到唤醒信号时,而线程被错误的唤醒。为了防止伪唤醒,一定要在while循环中检查信号变量的值。这样的循环也叫自旋锁。代码如下:<br><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyWaitNotify3</span>{</span><br><span class="line"></span><br><span class="line"> <span class="type">MonitorObject</span> <span class="variable">myMonitorObject</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">MonitorObject</span>();</span><br><span class="line"> <span class="type">boolean</span> <span class="variable">wasSignalled</span> <span class="operator">=</span> <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">doWait</span><span class="params">()</span>{</span><br><span class="line"> <span class="keyword">synchronized</span>(myMonitorObject){</span><br><span class="line"> <span class="keyword">while</span>(!wasSignalled){</span><br><span class="line"> <span class="keyword">try</span>{</span><br><span class="line"> myMonitorObject.wait();</span><br><span class="line"> } <span class="keyword">catch</span>(InterruptedException e){...}</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">//clear signal and continue running.</span></span><br><span class="line"> wasSignalled = <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">doNotify</span><span class="params">()</span>{</span><br><span class="line"> <span class="keyword">synchronized</span>(myMonitorObject){</span><br><span class="line"> wasSignalled = <span class="literal">true</span>;</span><br><span class="line"> myMonitorObject.notify();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><h2 id="不要在常量字符串或全局对象上调用wait方法"><a href="#不要在常量字符串或全局对象上调用wait方法" class="headerlink" title="不要在常量字符串或全局对象上调用wait方法"></a>不要在常量字符串或全局对象上调用wait方法</h2><p>请看下面的例子<br><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyWaitNotify</span>{</span><br><span class="line"></span><br><span class="line"> <span class="type">String</span> <span class="variable">myMonitorObject</span> <span class="operator">=</span> <span class="string">""</span>;</span><br><span class="line"> <span class="type">boolean</span> <span class="variable">wasSignalled</span> <span class="operator">=</span> <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">doWait</span><span class="params">()</span>{</span><br><span class="line"> <span class="keyword">synchronized</span>(myMonitorObject){</span><br><span class="line"> <span class="keyword">while</span>(!wasSignalled){</span><br><span class="line"> <span class="keyword">try</span>{</span><br><span class="line"> myMonitorObject.wait();</span><br><span class="line"> } <span class="keyword">catch</span>(InterruptedException e){...}</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">//clear signal and continue running.</span></span><br><span class="line"> wasSignalled = <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">doNotify</span><span class="params">()</span>{</span><br><span class="line"> <span class="keyword">synchronized</span>(myMonitorObject){</span><br><span class="line"> wasSignalled = <span class="literal">true</span>;</span><br><span class="line"> myMonitorObject.notify();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><br>从代码中我们可以看到,在上锁时是对myMonitorObject对象上锁,也是在myMonitorObject上调用wait方法,而myMonitorObject是一个空的字符串。我们知道,JVM会将相同的字符串看成同一个对象。也就说说,如果有两个MyWaitNotify实例,则这两个实例中的myMonitorObject是指向同一个对象的。那么一个在实例MyWaitNotify上调用wait的线程会被在另一个实例MyWaitNotify上调用doNotify方法的线程唤醒。下面的图表示了实例的成员指向了相同的对象。<br><img src="/Thread-Signaling/strings-wait-notify.png" class="" title="strings-wait-notify"><br>在上图中,4个线程是在相同的String常量上调用wait和notify方法,但是信号wasSignalled仍然是单独存放在对应的实例对象上。即一个在MyWaitNotify1实例上调用doNotify方法的线程可能会唤醒在MyWaitNotify2实例上等待的线程,但是唤醒信号仍然是单独保存在MyWaitNotify1实例中。<br>如果仔细看上面的程序,发现当在第二个MyWaitNotify2实例上调用doNotify方法时,会唤醒线程A或者B,但是由于在while循环中的wasSignalled变量,对于MyWaitNotify1实例仍然是false。所以被唤醒的线程A或者B从wait启动,但是会再次进入while循环调用wait,再次进入阻塞状态。这跟伪唤醒很像。<br>由于在doNotify方法中调用的notify方法,此方法不像notifyAll方法,notify方法只会唤醒一个线程,如果是线程C调用的doNotify,本来想唤醒的是线程D。但是有可能会错误的唤醒线程A或B,并且线程A或B会修改对应实例上的wasSignalled变量。而发给D的信号就丢失了,就有点像信号丢失的情况。如果将doNotify方法中的notify替换成notifyAll就不会有这个问题。但是这是一个坏主意,当应用仅仅只需要唤醒一个线程时,没有任何理由要把所有的线程都唤醒。<br>注意,对于wait/notify的情况,永远不要使用全局的对象例如string常量。在wait和notify上使用的对象针对对应的实例必须是独一的。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="http://tutorials.jenkov.com/java-concurrency/thread-signaling.html">http://tutorials.jenkov.com/java-concurrency/thread-signaling.html</a></li><li><a href="http://www.php.cn/java-article-410323.html">http://www.php.cn/java-article-410323.html</a></li></ol>]]></content>
<summary type="html"><h1 id="线程信号量及wait,notify方法"><a href="#线程信号量及wait,notify方法" class="headerlink" title="线程信号量及wait,notify方法"></a>线程信号量及wait,notify方法</h1><p>本篇</summary>
<category term="java" scheme="https://lightnine/github.io/categories/java/"/>
<category term="并发" scheme="https://lightnine/github.io/tags/%E5%B9%B6%E5%8F%91/"/>
</entry>
<entry>
<title>java synchronized keyword</title>
<link href="https://lightnine/github.io/java-synchronized-keyword.html"/>
<id>https://lightnine/github.io/java-synchronized-keyword.html</id>
<published>2019-05-10T11:39:54.000Z</published>
<updated>2024-06-10T07:00:19.050Z</updated>
<content type="html"><![CDATA[<h1 id="java-synchronized关键字"><a href="#java-synchronized关键字" class="headerlink" title="java synchronized关键字"></a>java synchronized关键字</h1><p>在平常进行java并发程序的开发过程中,synchronized关键字的出现频率很高。但是synchronized底层是如何实现的,synchronized都有哪些具体的用法。本篇将会在下面进行详细讲解。<br>synchronized关键字主要是用来进行同步操作。synchronized关键字修饰的内容,每次都只能有一个线程进入,如果其他线程想要进入相同的代码块,那么必须等前一个线程释放代码块对应的锁,其他的线程才能进入此代码块。但是同一个线程能够多次进入一个相同的同步块,也就是synchronized具有可重入锁的特性。<br>总的来说,在java中,主要在三个地方使用synchronized关键字</p><ol><li>类的实例方法</li><li>类的静态方法</li><li>修饰部分代码块</li></ol><h2 id="synchronized用在实例方法上"><a href="#synchronized用在实例方法上" class="headerlink" title="synchronized用在实例方法上"></a>synchronized用在实例方法上</h2><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="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyClassInstance</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="type">int</span> count;</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title function_">add</span><span class="params">(<span class="type">int</span> value)</span>{</span><br><span class="line"> <span class="built_in">this</span>.count += value;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在上面的代码中展示了,synchronized应用在实例方法的方式。当synchronized用在实例方法上时,每当线程进入此方法时,会尝试获取对应的类实例上的锁(注意是类实例)。如果没有其他的线程持有此实例上的锁,那么线程会获取此实例锁,然后运行此方法。<br>下面的图显示了使用javap进行反编译后的内容(运行:<code>javap -v MyClassInstance.class</code>):<br><img src="/java-synchronized-keyword/MyClassInstance.png" class="" title="java-memory-model"><br>图中仅仅展示了add方法反编译后的内容,可以看到在方法的flags标记中出现了ACC_SYNCHRONIZED。这个就是表明前面方法是用synchronized关键字修饰的。这个跟同步代码块的反编译结果是不同的。但是在线程执行同步操作时,都是要获取对应对象上的锁。</p><h2 id="synchronized用在类的静态方法上"><a href="#synchronized用在类的静态方法上" class="headerlink" title="synchronized用在类的静态方法上"></a>synchronized用在类的静态方法上</h2><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="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyClassStatic</span> {</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title function_">add</span><span class="params">(<span class="type">int</span> value)</span> {</span><br><span class="line"> System.out.print(value);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>上面代码展示了如何在静态方法上使用synchronized关键字。当线程尝试进入类的静态方法时,会尝试获取类上的锁(注意是类上的锁,跟类实例上的锁不同的东西)。如果没有其他线程持有此类上的锁,那么当前线程会获取此类上的锁,然后运行此方法。<br>下面展示了反编译的结果(运行命令:<code>javap -v MyClassStatic.class</code>)<br><img src="/java-synchronized-keyword/MyClassStatic.png" class="" title="java-memory-model"><br>可以从反编译的结果中看到在方法的flags中也是包含了ACC_SYNCHRONIZED.</p><h2 id="synchronized用在代码块上"><a href="#synchronized用在代码块上" class="headerlink" title="synchronized用在代码块上"></a>synchronized用在代码块上</h2><p>如果synchronized关键字用在代码块上,会在其之后括号中的对象获取锁。<br><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="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyClass</span> {</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">log</span><span class="params">(String msg)</span> {</span><br><span class="line"> <span class="keyword">synchronized</span>(<span class="built_in">this</span>) {</span><br><span class="line"> System.out.println(msg);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">func</span><span class="params">(String msg)</span> {</span><br><span class="line"> <span class="keyword">synchronized</span>(MyClass.class) {</span><br><span class="line"> System.out.print(msg);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><br>在上面的代码中,在方法log中,当线程进入log方法,然后执行synchronized关键字修饰的代码块。线程会尝试获取当前类实例上的锁,因为括号中使用的是this关键字。而在func方法中,线程会尝试获取MyClass类的锁。注意这两个锁是不同的。所以一个线程可以执行log中的同步代码块,而同时另一个线程也可以执行func中的同步代码块。<br>反编译结果如下(运行命令:<code>javap -v MyClass.class</code>):<br><img src="/java-synchronized-keyword/MyClass1.png" class="" title="java-memory-model"><br>可以看到在方法的签名中是没有ACC_SYNCHRONIZED的。但是在代码中出现了monitorenter和monitorexit。<br><strong>monitorenter</strong>:</p><blockquote><p>Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:<br>• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.<br>• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.<br>• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.</p></blockquote><p>引用中的内容来自JVM规范。这段话的大概意思为:<br>每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:</p><ol><li>如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。</li><li>如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.</li><li>如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。</li></ol><p><strong>monitorexit</strong>:</p><blockquote><p>The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.<br>The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.</p></blockquote><p>这段话的大概意思为:<br>执行monitorexit的线程必须是objectref所对应的monitor的所有者。<br>指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。<br>通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。<br>一个monitorenter都会对应有一个monitorexit。但是我们从反编译的结果中,可以看到多出了一个monitorexit,即第18行。因为在synchronized中的代码遇到异常时,会释放锁。第一个 monitorexit 指令如果正确执行,会走到下面的 goto 指令,直接跳转到 21 行 return,而如果发生异常,下面的 astore_3 和 aload_2 指令会继续执行异常问题,下一步会继续执行 monitorexit 指令退出同步。</p><p>当然,有时候,我们也可以这样用synchronized关键字修饰代码块<br><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="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyClass2</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="type">Object</span> <span class="variable">obj</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Object</span>();</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="type">Object</span> <span class="variable">obj2</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Object</span>();</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">log</span><span class="params">(String msg)</span> {</span><br><span class="line"> <span class="keyword">synchronized</span>(obj) {</span><br><span class="line"> System.out.print(msg);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">func</span><span class="params">(String msg)</span> {</span><br><span class="line"> <span class="keyword">synchronized</span>(obj2) {</span><br><span class="line"> System.out.print(msg);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><br>在上面的代码中,log方法中的同步块是对obj实例进行加锁,注意每个MyClass2实例中的obj都是不同的。而func中的同步块是对obj2静态成员进行加锁。</p>]]></content>
<summary type="html"><h1 id="java-synchronized关键字"><a href="#java-synchronized关键字" class="headerlink" title="java synchronized关键字"></a>java synchronized关键字</h1><</summary>
<category term="java" scheme="https://lightnine/github.io/categories/java/"/>
<category term="并发" scheme="https://lightnine/github.io/tags/%E5%B9%B6%E5%8F%91/"/>
</entry>
<entry>
<title>Java Memory Model</title>
<link href="https://lightnine/github.io/Java-Memory-Model.html"/>
<id>https://lightnine/github.io/Java-Memory-Model.html</id>
<published>2019-05-06T10:58:33.000Z</published>
<updated>2024-06-10T07:00:19.009Z</updated>
<content type="html"><![CDATA[<p>Java内存模型规定了Java虚拟机如何跟计算机的内存协同工作。因为Java虚拟机模拟了计算机,所以自然Java虚拟机包括内存模型。</p><p>正确理解Java内存模型对于编写正确的并发程序非常重要。Java内存模型规定了线程何时以及怎么读取其他线程写的值,还有就是在获取共享变量时如何进行同步操作。</p><p>最初的Java内存模型是有缺陷的,因此在Java5中进行了修改,并且这个版本的Java内存模型一直到Java8都在使用。</p><h1 id="Java内存模型"><a href="#Java内存模型" class="headerlink" title="Java内存模型"></a>Java内存模型</h1><p>JVM中的内存模型将内存分为线程栈内存和堆内存。下面的图从逻辑上展示了Java内存模型:<br><img src="/Java-Memory-Model/java-memory-model-1.png" class="" title="java-memory-model"></p><p>每个在JVM中运行的线程都有它自己栈空间。线程的栈空间中包含了线程调用方法执行到那一刻的数据。随着线程执行它的代码,调用栈也随之改变。</p><p>线程的栈空间同样也包含每个执行的方法的局部变量。线程只能获取它自己的栈空间,其中包含的局部变量对于其他线程是不可见的。即使两个线程执行相同的代码,这两个线程也是在各自的线程栈空间中创建各自的局部变量。</p><p>所有的原型类型(boolean,byte,short,char,int,long,float,double)的局部变量都是存储在线程的栈空间中。一个线程可能传递一个原型变量的副本给另一个线程,但是另一个线程并不能共享这个原型变量。</p><p>不管哪个线程创建了对象,这些对象都是存储在堆空间中。这也包括原型类型的包装器类型(e.g. Byte, Integer)。不管对象是被分配给局部变量还是作为另一个对象的成员变量,这个对象都是存储在堆空间中。</p><p>下面的图中说明了调用栈和局部变量存储在线程的栈空间中,而对象存储在堆空间中:</p><img src="/Java-Memory-Model/java-memory-model-2.png" class="" title="java-memory-model"><p>如果局部变量是原型类型,那么这个变量在线程的栈空间中。</p><p>如果局部变量是一个指向对象的引用类型,那么这个引用是在线程的栈空间,但是对象本身是在堆空间中。</p><p>如果一个对象包含方法,同时这些方法包含局部变量。那么这些局部变量(原型类型或者引用)是保存在线程栈空间中,即使这些方法所在的对象是存储在堆空间中。</p><p>一个对象的成员变量是跟对象一起存储在堆内存中,不管这个成员变量是原型还是指向一个对象的引用。</p><p>类的静态变量也是跟类的定义一起存在堆内存中。</p><p>堆空间中对象能够被所有拥有指向此对象的引用的线程访问到。当一个线程能够访问一个对象时,那么这个线程也能够访问此对象的成员变量(这里要看这个对象的封装性)。如果两个线程同时调用了同一个对象的一个方法,那么这两个线程将能够访问这个对象的成员变量,但是每个线程都会获得局部变量的一份拷贝。</p><p>下面的图说明了上面描述的内容:</p><img src="/Java-Memory-Model/java-memory-model-3.png" class="" title="java-memory-model"><p>在上图中,两个线程有一系列的局部变量。其中一个局部变量(Local Variable 2)指向了堆内存上的共享对象(Object3)。这两个线程分别拥有一个指向同一个对像的引用,这两个引用是不同的。这些引用是局部变量,所以存储在各自线程的栈空间中。而这两个不同的引用指向了堆上的同一个对象。</p><p>注意到共享对象(Object3)有两个引用指向了Object2和Object4,如图中箭头所示。通过Object3中的成员变量引用,这两个线程能够获取Object2和Object4。</p><blockquote><p>The diagram also shows a local variable which point to two different objects on the heap. In this case the references point to two different objects (Object 1 and Object 5), not the same object. In theory both threads could access both Object 1 and Object 5, if both threads had references to both objects. But in the diagram above each thread only has a reference to one of the two objects.</p></blockquote><p>上图中同样了展示了一个局部变量指向堆上两个不同的对象。指向不同对象(Object1, Object5)的引用不是同一个对象。理论上,如果这两个线程有指向这两个对象的引用,那么这两个线程都能够访问Object1和Object5。但是在上图中每个线程只有指向其中一个对象的引用。</p><p>那么,在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><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="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyRunnable</span> <span class="keyword">implements</span> <span class="title class_">Runnable</span>() {</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> {</span><br><span class="line"> methodOne();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">methodOne</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">int</span> <span class="variable">localVariable1</span> <span class="operator">=</span> <span class="number">45</span>;</span><br><span class="line"></span><br><span class="line"> <span class="type">MySharedObject</span> <span class="variable">localVariable2</span> <span class="operator">=</span></span><br><span class="line"> MySharedObject.sharedInstance;</span><br><span class="line"></span><br><span class="line"> <span class="comment">//... do more with local variables.</span></span><br><span class="line"></span><br><span class="line"> methodTwo();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">methodTwo</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">Integer</span> <span class="variable">localVariable1</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Integer</span>(<span class="number">99</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//... do more with local variable.</span></span><br><span class="line"> }</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MySharedObject</span> {</span><br><span class="line"></span><br><span class="line"> <span class="comment">//static variable pointing to instance of MySharedObject</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">MySharedObject</span> <span class="variable">sharedInstance</span> <span class="operator">=</span></span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">MySharedObject</span>();</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="comment">//member variables pointing to two objects on the heap</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="type">Integer</span> <span class="variable">object2</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Integer</span>(<span class="number">22</span>);</span><br><span class="line"> <span class="keyword">public</span> <span class="type">Integer</span> <span class="variable">object4</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Integer</span>(<span class="number">44</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="type">long</span> <span class="variable">member1</span> <span class="operator">=</span> <span class="number">12345</span>;</span><br><span class="line"> <span class="keyword">public</span> <span class="type">long</span> <span class="variable">member1</span> <span class="operator">=</span> <span class="number">67890</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如果两个线程执行run()方法,那么此代码就表明了上图所示的内存分布。run方法首先调用methodOne方法,然后methodOne方法调用methodTwo方法。</p><p>在methodOne方法中定义了一个原型的局部变量localVariable1,同时定义了一个指向对象的引用localVariable2.</p><p>每个执行methodOne方法的线程都会在各自的线程栈空间中创建localVariable1和localVariable2的副本。localVariable1在两个线程中是完全独立的,仅仅存在于对应线程的栈空间中。一个线程不能看到其他线程对于localVariable1变量的修改。</p><p>执行methodOne方法的线程同样会创建localVariable2的副本。但是,这两个localVariable2的副本是指向在堆上的同一个对象。而localVariable2引用指向的对象是一个类的静态成员变量。而类的静态变量在堆中只存在一份。所以两个localVariable2指向同一个MySharedObject对象的实例,MySharedObject实例存储在堆内存上,对应图中的Object3对象。</p><p>我们在来看看methodTwo方法是如何创建localVariable1局部变量的。这个局部变量是指向一个Integer对象的引用。methodTwo方法每次都重新创建一个Integer实例。局部变量localVariable2引用是存储在对应的栈空间中,而对应的Integer对象是在堆内存中,因为每次执行methodTwo方法都会创建Integer对象,所以在堆内存中会有两个Integer对象。即对应于图中的Object1和Object5对象。</p><p>在MySharedObject中有两个long类型的局部变量,因为这些变量是类的成员变量,所以它们跟具体的对象一起保存在堆内存中。仅仅局部变量是保存在栈内存中的。</p><h1 id="硬件内存架构"><a href="#硬件内存架构" class="headerlink" title="硬件内存架构"></a>硬件内存架构</h1><p>硬件内存跟Java内存模型是有些不一样的。为了更好地理解Java内存模型,我们需要了解硬件内存架构。下面的图简单描述了现代计算机的硬件架构:</p><img src="/Java-Memory-Model/java-memory-model-4.png" class="" title="java-memory-model"><p>现代的计算机通常会有两个以上的CPU,同时这些CPU可能有多个核。这也意味着我们可以将多个线程同时运行在多个CPU上。在给定的时间点,每个CPU都能运行一个线程。即如果你的Java程序是多线程的,那么能够在多个CPU上同时运行(并行)。</p><p>在上图中我们可以看到每个CPU内部都有一系列的寄存器。CPU在寄存器上执行的操作要比在主存中的操作快。这是因为CPU能够更快的获取寄存器上的内容。</p><p>每个CPU都有一个对应的缓存内存。获取缓存中的内容要快于获取主存中的内容,但是没有获取寄存器中的内容快。CPU缓存的速度要介于寄存器和主存之间。有些CPU可能会有多级的缓存。</p><p>一个计算机包含一个主存(RAM),所有的CPU都能够获取主存中的内容。主存的容量要远远大于缓存的容量。</p><p>如果一个CPU要读取主存的内容,通常只会读取主存中部分区域的内容到CPU缓存中,然后在从缓存读取到寄存器中,之后进行计算。当CPU需要写结果到主存中,它会将寄存机中的值刷新到缓存中,然后在之后的某个时间点,在将缓存中的内容刷新到主存中。</p><p>当CPU需要存储缓存中的值时,会将缓存中的值刷新到主存中。同时CPU缓存也可以局部的刷新缓存值以及写出缓存值。</p><h1 id="Java内存模型和硬件内存架构之间的桥接"><a href="#Java内存模型和硬件内存架构之间的桥接" class="headerlink" title="Java内存模型和硬件内存架构之间的桥接"></a>Java内存模型和硬件内存架构之间的桥接</h1><p>像前面说明的,Java内存模型和硬件内存架构是不同的。硬件内存架构不会区分线程栈和堆空间。在硬件中,线程栈和堆空间都是在主存中的。同时,部分线程栈和堆内存会出现在CPU缓存或者CPU寄存器中。下面的图进行了说明:</p><img src="/Java-Memory-Model/java-memory-model-5.png" class="" title="java-memory-model"><p>当对象和变量存储在不同的内存区域时,会出现一些问题。主要的两个问题如下:</p><ol><li>共享变量的可见性(可见性,即一个线程能够及时的看到另一个线程对于共享变量的修改)</li><li>竞态条件(即读取,检查,写入共享变量)</li></ol><p>下面会依次介绍这两个问题。</p><h2 id="共享变量的可见性"><a href="#共享变量的可见性" class="headerlink" title="共享变量的可见性"></a>共享变量的可见性</h2><p>如果两个线程共享一个对象,但是没有采用合适的volatile关键字或者同步操作,那么当一个线程更新共享对象时,可能另一个线程并不能看到更新后的值。</p><p>想象一下,一个共享对象最初是在主存中,一个CPU上的线程读取了此对象到CPU缓存中,之后对于这个共享对象进行了修改。只要CPU缓存没有刷新到主存中,那么运行在其他CPU上的线程是不能读取到修改后的共享变量的值的。这会造成其他CPU只能看到修改之间的值。</p><p>下面的图说明了这种情况。在左边的CPU上运行的线程将共享对象读取到CPU缓存中,然后将它的count变量修改为2.由于没有将count的修改刷新到主存中,所以在右侧CPU上运行的线程不能看到这个修改。</p><img src="/Java-Memory-Model/java-memory-model-6.png" class="" title="java-memory-model"><p>为了解决这个问题,我们可以使用Java中的<a href="http://tutorials.jenkov.com/java-concurrency/volatile.html">volatile</a>关键字.volatile关键字的作用是保证每次都是从主存中读取变量的值,同时如果修改了此变量,那么此变量会立刻写回到主存中。</p><h2 id="竞态条件"><a href="#竞态条件" class="headerlink" title="竞态条件"></a>竞态条件</h2><p>如果两个线程或多个线程共享一个对象,那么当多余一个线程修改此变量时,竞态条件就可能会出现。</p><p>想象一下,线程A将一个共享对象的count变量读取进CPU缓存,线程B也将count读取到另一个CPU缓存。现在线程A在count加上1,同时线程B也在count上加上1.现在变量被增加了两次,在每个CPU缓存中各一次。</p><p>如果加1操作是顺序进行的,那么count的值会加上2,然后写回到主存中。</p><p>但是,这两个操作是在没有正确同步的情况下同时进行的。尽管线程A或者B会将count修改后的值写回到主存,但是更新后的值始终比原来的值大1.</p><p>下面的图说明了上面描述的竞态条件:</p><img src="/Java-Memory-Model/java-memory-model-7.png" class="" title="java-memory-model"><p>为了解决这个问题,可以使用Java的同步块,即<a href="http://tutorials.jenkov.com/java-concurrency/synchronized.html">synchronized</a>关键字。同步块保证了在同步块中获取到的变量都是从主存中读取的,并且当线程离开同步块时,所有修改的变量会被刷新到主存中,而不管这个变量是否被volatile声明。</p>]]></content>
<summary type="html"><p>Java内存模型规定了Java虚拟机如何跟计算机的内存协同工作。因为Java虚拟机模拟了计算机,所以自然Java虚拟机包括内存模型。</p>
<p>正确理解Java内存模型对于编写正确的并发程序非常重要。Java内存模型规定了线程何时以及怎么读取其他线程写的值,还有就是在获</summary>
<category term="java" scheme="https://lightnine/github.io/categories/java/"/>
<category term="并发" scheme="https://lightnine/github.io/tags/%E5%B9%B6%E5%8F%91/"/>
<category term="内存模型" scheme="https://lightnine/github.io/tags/%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/"/>
</entry>
<entry>
<title>用静态工厂方法替换构造函数</title>
<link href="https://lightnine/github.io/%E7%94%A8%E9%9D%99%E6%80%81%E5%B7%A5%E5%8E%82%E6%96%B9%E6%B3%95%E6%9B%BF%E6%8D%A2%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0.html"/>
<id>https://lightnine/github.io/%E7%94%A8%E9%9D%99%E6%80%81%E5%B7%A5%E5%8E%82%E6%96%B9%E6%B3%95%E6%9B%BF%E6%8D%A2%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0.html</id>
<published>2019-04-10T09:01:33.000Z</published>
<updated>2024-06-10T07:00:19.060Z</updated>
<content type="html"><![CDATA[<p>这是根据effective java中的Item1条目来写的。在写java程序时,我们构造一个实例用的最多的就是调用类的构造函数.如<code>A a = new A()</code>.但是还存在一种方法可以获取类的实例。而且相对于调用类的构造函数有很多好处,那就类的静态工厂函数。比如,在Java中的<code>Boolean.valueOf(boolean b)</code>方法:<br><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">public</span> <span class="keyword">static</span> Boolean <span class="title function_">valueOf</span><span class="params">(<span class="type">boolean</span> b)</span> {</span><br><span class="line"> <span class="keyword">return</span> b ? Boolean.TRUE:Boolean.FALSE; </span><br><span class="line">}</span><br></pre></td></tr></table></figure><br>静态工厂函数相对于构造函数有优点也有缺点,下面会依次进行说明</p><h1 id="优点"><a href="#优点" class="headerlink" title="优点"></a>优点</h1><h2 id="静态工厂函数相对于构造函数有具体的名字"><a href="#静态工厂函数相对于构造函数有具体的名字" class="headerlink" title="静态工厂函数相对于构造函数有具体的名字"></a>静态工厂函数相对于构造函数有具体的名字</h2><p>首先,有具体的名字有什么好处呢?如果有名字,可以让调用者更加清楚此函数的作用。因为构造函数的名字要相同,那如果构造函数做的事情不一样,则只能通过增加或删除构造函数的入参才能表达不同的构造函数。这样子对于调用者会造成负担,调用者必须了解构造函数代码做的事情,才能知道此构造函数的真正目的。而使用静态工厂函数,可以通过合理取函数名称,来表达不同的目的以及区别。</p><h2 id="静态工厂函数每次被调用时可以不需要创建新的对象"><a href="#静态工厂函数每次被调用时可以不需要创建新的对象" class="headerlink" title="静态工厂函数每次被调用时可以不需要创建新的对象"></a>静态工厂函数每次被调用时可以不需要创建新的对象</h2><p>一般我们调用构造函数时,都是会返回一个新的对象。但是有时候我们不需要返回新的对象,比如单例模式。</p><h2 id="静态工厂函数可以返回静态工厂函数返回类型的子类型"><a href="#静态工厂函数可以返回静态工厂函数返回类型的子类型" class="headerlink" title="静态工厂函数可以返回静态工厂函数返回类型的子类型"></a>静态工厂函数可以返回静态工厂函数返回类型的子类型</h2><p>这个是什么意思呢?比如我们拿java中的Collections的静态工厂方法<code>synchronizedMap</code>为例:<br><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">public</span> <span class="keyword">static</span> <K,V> Map<K,V> <span class="title function_">synchronizedMap</span><span class="params">(Map<K,V> m)</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">SynchronizedMap</span><>(m);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><br>可以看到函数定义的返回类型是<code>Map</code>,但是实际返回的类型是<code>SynchronizedMap</code>.但是这样做有什么好处呢?<br>这种方式可以隐藏实现类,从而形成一个非常紧凑的API。我们还是以Collections类为例。Collections框架中有45个接口实现工具实现,提供了不可修改集合、同步集合等。这些都是通过Collections提供静态工厂函数来实现,而且返回的子类型都是非公共的。所以Collections框架API比它导出45个独立的公共类要小的多,这样不仅减少了API的数量,同时也减少了框架使用者需要了解的概念。框架使用者不需要了解这45个实现类。同时因为返回的是接口类型,这也是很好的做法。</p><h2 id="第四个优点,静态工厂函数返回的类可以根据输入参数来改变"><a href="#第四个优点,静态工厂函数返回的类可以根据输入参数来改变" class="headerlink" title="第四个优点,静态工厂函数返回的类可以根据输入参数来改变"></a>第四个优点,静态工厂函数返回的类可以根据输入参数来改变</h2><p>静态工厂方法返回的类可以根据版本而改变。Java中的<code>EnumSet</code>类没有公共构造函数,只有静态工厂方法。<br><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="keyword">public</span> <span class="keyword">static</span> <E <span class="keyword">extends</span> <span class="title class_">Enum</span><E>> EnumSet<E> <span class="title function_">noneOf</span><span class="params">(Class<E> elementType)</span> {</span><br><span class="line"> Enum<?>[] universe = getUniverse(elementType);</span><br><span class="line"> <span class="keyword">if</span> (universe == <span class="literal">null</span>)</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">ClassCastException</span>(elementType + <span class="string">" not an enum"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (universe.length <= <span class="number">64</span>)</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">RegularEnumSet</span><>(elementType, universe);</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">JumboEnumSet</span><>(elementType, universe);</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><br>通过代码我们可以看出,noneOf返回两个子类中的一个实例,这取决于底层enum类型的大小。如果有64个或更少的元素,那么返回一个long类型的RegularEnumSet实例;如果enum类型有65或更多的元素,方法将返回一个long[]类型的JumboEnumSet实例。<br>客户端感知不到这两个实现类的存在。如果 RegularEnumSet 不再为小型 enum 类型提供性能优势,它可能会在未来的版本中被消除,而不会产生不良影响。类似地,如果事实证明 EnumSet 有益于性能,未来的版本可以添加第三或第四个 EnumSet 实现。客户端既不知道也不关心从工厂返回的对象的类;它们只关心它是 EnumSet 的某个子类。</p><h2 id="第五个优点,编写包含静态工厂函数的类时,此静态工厂函数返回对象所属的类可以不存在"><a href="#第五个优点,编写包含静态工厂函数的类时,此静态工厂函数返回对象所属的类可以不存在" class="headerlink" title="第五个优点,编写包含静态工厂函数的类时,此静态工厂函数返回对象所属的类可以不存在"></a>第五个优点,编写包含静态工厂函数的类时,此静态工厂函数返回对象所属的类可以不存在</h2><p>上面这句话,听起来有点难以理解。这里做个例子进行说明,比如我们正在编写类A,其中包含factory1 静态工厂函数,factory1函数的定义返回类型是接口 Interface1,而实际</p><p><strong>这一点暂时没有理解</strong></p><h1 id="缺点"><a href="#缺点" class="headerlink" title="缺点"></a>缺点</h1><h2 id="第一个缺点,类如果没有公有或者受保护的构造器,那么此类就不能被继承。"><a href="#第一个缺点,类如果没有公有或者受保护的构造器,那么此类就不能被继承。" class="headerlink" title="第一个缺点,类如果没有公有或者受保护的构造器,那么此类就不能被继承。"></a>第一个缺点,类如果没有公有或者受保护的构造器,那么此类就不能被继承。</h2><p>例如,我们不能继承Collection框架中的任何类</p><h2 id="第二个缺点,我们不太容易在类文档中找到静态工厂函数"><a href="#第二个缺点,我们不太容易在类文档中找到静态工厂函数" class="headerlink" title="第二个缺点,我们不太容易在类文档中找到静态工厂函数"></a>第二个缺点,我们不太容易在类文档中找到静态工厂函数</h2><p>因为Javadoc 工具不能明确的标注静态工厂函数,所以在文档中静态工厂函数就不会突出显示。而构造函数在文档中会突出显示。我们也可以遵守一些给静态工厂函数起名的规范,下面是一些建议:</p><ol><li>from: 将输入转为与之对应的相关类,接受一个参数,然后返回对应的类实例<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">Date</span> <span class="variable">d</span> <span class="operator">=</span> Date.from(instant);</span><br></pre></td></tr></table></figure></li><li>of:接受多个参数,返回包含多个参数的类实例<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);</span><br></pre></td></tr></table></figure></li><li>valueOf: 介于from和of之间<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">BigInteger</span> <span class="variable">prime</span> <span class="operator">=</span> BigInteger.valueOf(Integer.MAX_VALUE);</span><br></pre></td></tr></table></figure></li><li>instance 或者getInstance:返回参数对应的类实例,但是值有可能会改变<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">StackWalker</span> <span class="variable">luke</span> <span class="operator">=</span> StackWalker.getInstance(options);</span><br></pre></td></tr></table></figure></li><li>create 或者 newInstance: 类似于instance或getInstance,但是每次调用都会返回一个新的实例<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">Object</span> <span class="variable">newArray</span> <span class="operator">=</span> Array.newIntance(classObject, arrayLen);</span><br></pre></td></tr></table></figure></li><li>getType: 类似getInstance,获取实例,但是静态工厂函数在另一个类中。Type表面返回实例的类型<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">FileStore</span> <span class="variable">fs</span> <span class="operator">=</span> Files.getFileStore(path);</span><br></pre></td></tr></table></figure></li><li>newType: 类似newInstance,但是静态工厂函数在另一个类中<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">BufferedReader</span> <span class="variable">br</span> <span class="operator">=</span> Files.newBufferedReader(path);</span><br></pre></td></tr></table></figure></li><li>type: 一种精确的转换<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">List<Complaint> litany = Collections.list(legacyLitany);</span><br></pre></td></tr></table></figure></li></ol><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>构造器和静态工厂函数有各自的优点和缺点,但是一般来说,静态工厂函数要比构造器好。在平常的编程过程中,我们不要一上来就直接写构造函数,可以先考虑一下,用静态工厂函数是不是更好。</p>]]></content>
<summary type="html"><p>这是根据effective java中的Item1条目来写的。在写java程序时,我们构造一个实例用的最多的就是调用类的构造函数.如<code>A a = new A()</code>.但是还存在一种方法可以获取类的实例。而且相对于调用类的构造函数有很多好处,那就类的静态工</summary>
<category term="effective java" scheme="https://lightnine/github.io/categories/effective-java/"/>
</entry>
<entry>
<title>effective java third edition notes</title>
<link href="https://lightnine/github.io/effective-java-third-edition-notes.html"/>
<id>https://lightnine/github.io/effective-java-third-edition-notes.html</id>
<published>2019-04-10T08:42:41.000Z</published>
<updated>2024-06-10T07:00:19.050Z</updated>
<content type="html"><![CDATA[<h1 id="effective-java-third-edition-notes"><a href="#effective-java-third-edition-notes" class="headerlink" title="effective java third edition notes"></a>effective java third edition notes</h1><p>记录阅读《effective java》得一些收获</p><h2 id="第二章-创建和销毁对象"><a href="#第二章-创建和销毁对象" class="headerlink" title="第二章 创建和销毁对象"></a>第二章 创建和销毁对象</h2>]]></content>
<summary type="html"><h1 id="effective-java-third-edition-notes"><a href="#effective-java-third-edition-notes" class="headerlink" title="effective java third edi</summary>
</entry>
<entry>
<title>左耳朵耗子-陈皓经历(转)</title>
<link href="https://lightnine/github.io/%E5%B7%A6%E8%80%B3%E6%9C%B5%E8%80%97%E5%AD%90-%E9%99%88%E7%9A%93%E7%BB%8F%E5%8E%86-%E8%BD%AC.html"/>
<id>https://lightnine/github.io/%E5%B7%A6%E8%80%B3%E6%9C%B5%E8%80%97%E5%AD%90-%E9%99%88%E7%9A%93%E7%BB%8F%E5%8E%86-%E8%BD%AC.html</id>
<published>2019-03-30T14:35:12.000Z</published>
<updated>2024-06-10T07:00:19.060Z</updated>
<content type="html"><![CDATA[<h1 id="左耳朵耗子-陈皓"><a href="#左耳朵耗子-陈皓" class="headerlink" title="左耳朵耗子-陈皓"></a>左耳朵耗子-陈皓</h1><h2 id="一、个人简历"><a href="#一、个人简历" class="headerlink" title="一、个人简历"></a>一、个人简历</h2><p>陈皓,coolshell.cn博客博主。<br>14年以上软件开发相关工作经验,8年以上项目和团队管理经验。<br>擅长底层技术架构,软件工程。<br>对高性能,高可用,分布式,高并发,大规模数据处理系统有一定研究。<br>技术擅长C/C++/Java和Unix/Linux/Windows。</p><h2 id="二、轶事和思考"><a href="#二、轶事和思考" class="headerlink" title="二、轶事和思考"></a>二、轶事和思考</h2><h3 id="2-1、毅然辞掉银行工作"><a href="#2-1、毅然辞掉银行工作" class="headerlink" title="2.1、毅然辞掉银行工作"></a>2.1、毅然辞掉银行工作</h3><p>我当时在银行做银行网络、银行的电子邮件系统和办公自动化系统。当时正处在银行信息化的阶段,加上当时互联网和IT业刚刚火起来,得到这份工作其实是很幸运的。银行正值扩张电子信息化业务的时候,其实应该有很多事可做,但是当时的主要工作都是由厂商来干。比如说IBM或Cisco拿下单子来,会把工作外包给系统集成商。作为一位技术人员,其实可以发挥的空间并不大,多数时间我只是出了问题打电话的角色。没有人会教你任何事,出了问题,就是打电话,然后按照他们的指导来完成工作。但这个还不是促使我离职的最主要原因,我离开是因为互联网和IT业的兴起让我有些心向往之,有想去看一看的冲动。我还记得当时的辞职书是这么写的:“本人对现有工作毫无兴趣,申请辞职”。处长说,“你可以这么写,但是要加上‘经调解无效’,另外,分给你的房就不能要了”。我说好啊。就这样就辞去了工作,去了上海。老实说,这个决定真不好做,因为几乎所有的朋友和亲人都很反对。</p><h3 id="2-2、选择上海闯荡的原因"><a href="#2-2、选择上海闯荡的原因" class="headerlink" title="2.2、选择上海闯荡的原因"></a>2.2、选择上海闯荡的原因</h3><p>当时选择上海是有原因的,我觉得在当时的环境(2000年)下,上海的发展比较不错。没有选择深圳的原因是个人感觉那是因为政治原因凭空冒出来的一座城市,我不是很喜欢。北京我有很多同学,而我想去一个陌生的地方。但是后来发现上海也不是做技术的地方,过得有些压抑,初来到上海的时候经常会被人瞧不起,毕竟是刚刚来到大城市。<br>我当时感觉银行束缚了我,想看看自己可以跑多远,能发挥出多大的价值。于是决定出来闯一闯,主要就是要去经历一些应该去经历的事情,不希望老了以后会后悔年轻的时候没有去。当时IT产业的发展是一个大趋势,我感觉我必须要去一座大城市,去经历一些东西。在小地方基本没有这些机会。要学会游泳就必须要跳到水里去呛两口水,所以我就义无反顾地出来了。</p><h3 id="2-3、工作挫折和自省"><a href="#2-3、工作挫折和自省" class="headerlink" title="2.3、工作挫折和自省"></a>2.3、工作挫折和自省</h3><p>我仍然记得自己拎着皮箱站在上海火车站的样子,举目无亲。原来在老家的时候觉得自己还挺厉害的,自以为不愁找不到好工作。不过事实却不是这样的。<br>我还记得第一次去面试时,(面试官)问了很多和C相关的问题,问了我半个小时,我一个问题都答不上来。我一直低着头,好像被审问的犯罪分子一样。我从大学毕业出来就没经历过什么面试,再加上自己内向的性格,所以,整个过程我都在低着头,不敢看别人一眼。最后,面试官问了我一个问题是“有不懂的问题你会怎么办”,这样的问题我都不敢回答,其实这道题的答案不过就是“问别人”或是“自己看书”或是“上网查资料”什么的。很显然,这场面试我肯定是被灭掉了。但这还没完,最后面试官对我说:“你出来干什么,像你这种性格根本不适合到大城市来”。<br>我当时被严重地打击了,感觉到自己确实有一些东西很差。第一个是性格差,不知道怎么与人交往;第二个是技术差,很多问题不知道;第三个就是视野狭窄,没见过世面。后面的几家公司的面试都大同小异。一个人在异地他乡,经历了这些事情,心里会非常地恐慌,“我这条路是不是走错了?”,我经常这样问自己。<br>面对这样的情况,我被逼迫着一定要改变自己。因为,离开银行时,我的家人、同学和朋友都很反对我出来,如果这样灰溜溜地回去,我面对不了他们。而前面的人还看不起我。我当时的处境真的很难堪,就像爬在悬崖中间,上不去也下不来。所以,当时只有一个想法,就是要证明自己不是那么差的人。人被逼到那个份上,活得就比较简单,哪有什么职业发展规划,只想拼命地多学技术,提高自己的能力。这个经历有点像是一剂兴奋剂,同时也相当阵痛。但是回头想想,第一个面试官应该是我最感谢的人。</p><h3 id="2-4、疯狂成长"><a href="#2-4、疯狂成长" class="headerlink" title="2.4、疯狂成长"></a>2.4、疯狂成长</h3><p>在同学的帮助下我找到了在上海的第一份工作。南天公司,这是一家给银行做系统集成软件的公司,大学毕业时本来也可以进去,现在绕了一圈而且还是靠同学帮助进去的,所以那时的心态还很不平稳;另一方面因为以前是做银行的,是甲方,现在成了乙方了,两边的人都用异样的眼光看我,心态非常不好。<br>不过,这是个技术不错的企业,国内早期很多搞Unix/C的高手都是从这个公司培养出来的。我当时的技术还是不行,比如说到了用户站点以后,不知道怎么做,我曾经误操作把用户的数据删掉了。经常犯低级错误,不但没做好自己的工作,反而还给别人添了麻烦。这些经历都让我有一种“技术焦虑感”,或者叫“技术忧郁症”。我觉得自己这也不行,那也不行。这也是我今天仍然在拼命学习的原因。这就好像我们经常在参加工作多年后还会梦见自己的英语四级没过,或者是期末考试没过一样。我经常会梦见的是项目又做砸了,又把用户的系统搞乱了,一大堆人要审我、要训斥我。<br>因为技术差,沟通差,不会面试,所以,我决定经常出去面试,基本上每周都要去,不管懂不懂,也不管是什么公司,也不管别人鄙不鄙视我,反正一有机会就去面试,多见见人这样可以让我的性格有所改善,同时,也可以知道社会上需要一些什么样的技能,把别人面我回答不上来的东西都记下来,然后回头找答案。那个时候我会经常去上海书城看书,看很多很多的书。我学的东西很杂,什么做网页,Windows,Unix,Java,.NET,flash,连3DMax/Photoshop我也学,还去考CCNA的认证等等。这样散乱地学习两年后,我才慢慢确定了要走C/C++/Unix/Windows系统底层的路子。而这样扑天盖地学习的结果有一个好处就是,我成长的速度相当之快。我自己摸索到了适合我的学习方法(从基础和原理上学习),从而不再害怕各种新的技术。那时,所有人都在休黄金周出去玩的时候,我还呆在办公室或住处看书学习。<br>等到一年半之后,用句赵本山的台词说,我在面试中学会抢答了。面试官的问题没问完,我就能说出答案了。其实,基本上是面一个公司过一个(当然都是一些小公司),此时,我就开始挑公司了。<br>感到技术能力不行就去学技术,交往能力不行我就去面试,这两个问题都可以通过大量地实践和努力来弥补,但是眼界这个东西没有办法通过努力来弥补。所以,当时非常想去一些更大的公司看看,如果能去外企更好。</p><h3 id="2-5、变得不一样"><a href="#2-5、变得不一样" class="headerlink" title="2.5、变得不一样"></a>2.5、变得不一样</h3><p>我还记得,有一天,有一个和网络相关的技术问题,同事们搞了三四个通宵,也没弄明白,后来想起我好像在看这方面的书,他们就让我去看看、试试,结果我只用了20分钟就搞定了。基础真的很重要,这受益于我看了《TCP/IP详解》这套书。<br>后来,我去了一家做电信软件的公司,他们让我做PowerBuilder,尽管我当时想做的是C++,但是因为当时各种原因很需要这份工作,就去了。进了那里的第一天发现公司里有一个论坛,上面都是一些技术上悬而未决的问题,都是关于Windows/C++的。我一看,都是些很简单的问题,一下午的时间就被我全部解决掉了,我的基础知识发挥了作用。于是,当天下午我一下子就被调到了核心组。不过,我只在那里呆了两个多月,因为那时我已经不愁找工作了,这期间有两家北京的公司录用了我,于是,02年我就来到了北京,去到一家做分布式计算平台软件的公司。<br>在上海的这两年的时间,从什么都不是,到得到工作上的全面肯定。那段时间感觉自己牛得不得了,有些狂妄和骄傲了,经常上网和不认识的人争论一些很傻的问题,后来发展到对当时的领导以及银行客户的领导不敬,总觉得这些人太二。现在回头看过去,我觉得那是我人生特定时期的记号,人生的痕迹。</p><h3 id="2-6、建立coolshell-cn的原因"><a href="#2-6、建立coolshell-cn的原因" class="headerlink" title="2.6、建立coolshell.cn的原因"></a>2.6、建立coolshell.cn的原因</h3><p>我2002年在CSDN开了一个blog,当时叫专家专栏。开个专栏很简单,只要发6个帖子。我也不是什么专家,只是喜欢看书、喜欢学习而已,也喜欢做一些学习笔记。那时候没有笔记本也没有台式机,市面上好像也没有U盘和移动硬盘。正好有CSDN这么一个地方,就去CSDN的站点上把自己的一些学习笔记放在了上面。后来03年的时候技术专栏转到了博客,因为CSDN对其博客经营得不好,我09年就离开了csdn,创建了酷壳。花了4500块钱,租了一个server。我离开那里主要有两个原因,一个是因为当时CSDN博客有一些性能上的问题,.NET架构嘛,大家都懂的。另外一个原因就是当时出现了很多博客营销的站点,有点像今天的36氪。好像那时候出现最早的叫煎蛋,那上面会有一些报纸上不会出现的国外的趣闻,是以博客的方式形成的媒体。这和常规的以日记形式出现的博客大不一样。煎蛋、有意思吧等这些博客让我看到了博客还能这样写,我觉得很好玩儿。而我当时也经常会去国外社区看一些文章,也能看到一些有意思的东西(因为我当时有了学习瓶颈,国内的网站已经满足不了我了)。心想,既然这些东西这么有意思,我为什么不自己开一个博客呢?<br>我老婆是学新闻编辑的,她鄙视我说,你的博客虽然有很多人读,但是只能算是个书呆子的博客,全是一些书呆子式的文章。我有些不服,我觉得技术人员不全是书呆子,我们这个圈子里也有很多有趣的东西,只不过是你不知道而已。于是我想弄一个有意思的、有娱乐性质的东西,里面都是技术圈里面有意思的事儿,但是很多技术圈以外的人也能看懂。一开始酷壳和CSDN博客的风格完全迥然,如果有技术性的文章我还会在CSDN上贴,但是后来我就完全抛弃了原来CSDN上的博客。酷壳的初衷是希望很多人都可以来上面发表一些东西,但是可能是我写得太多了,别人就被压制住了。<br>现在博客更新频率是一周一篇,一开始的时候一周三篇。磨刀不误砍柴工,总是有时间来做这些事的。我经常看书,需要把学到的东西整理成学习笔记。自从在CSDN上写博客的时候,就有这样的习惯了,而且又有“技术焦虑症”,害怕跟不上,所以维护博客的事对我来说是很自然的。<br>现在我已经不用自己再租服务器了,由于酷壳的访问量比较有保证,我提供了广告位,就免费得到服务器了。</p><h3 id="2-7、对于新技术的态度"><a href="#2-7、对于新技术的态度" class="headerlink" title="2.7、对于新技术的态度"></a>2.7、对于新技术的态度</h3><p>遇到新技术我会去了解,但不会把很大的精力放在这。这些技术尚不成熟,我只需要跟得住就可以了。我的团队自己想学什么我都不干涉,但是用到项目里的技术,必须是很成熟的,(技术应用)十年以上可能是一个门槛。有人说技术更新换代很快,我一点儿都不这样想。虽然有不成熟的技术不断地涌出,但是成熟的技术,比如Unix,40多年,C,40多年,C++,30多年,Java也有将近20年了……,所以,技术并不多啊。还有很多技术比如Ruby,Lisp这样的,它们没有进入主流的原因主要是缺少企业级的应用背景。<br>如果要捋一个脉络下来,70年代Unix的出现,是软件发展方面的一个里程碑,那个时期的C语言,也是语言方面的里程碑。当时所有的项目都在Unix/C上,全世界人都在用这两样东西写软件。Linux跟随的是Unix,Windows下的开发也是C。这时候出现的C++很自然就被大家接受了,企业级的系统很自然就会迁移到这上面,C++虽然接过了C的接力棒,但是它的问题是它没有一个企业方面的架构,否则也不会有今天的Java。C++和C非常接近,它只不过是C的一个扩展,长年没有一个企业架构的框架。而Java出现之后,IBM把企业架构这部分的需求接了过来,J2EE的出现让C/C++捉襟见肘了,后面还有了.NET,但可惜的是这只局限在Windows平台上。这些就是企业级软件方面语言层面这条线上的技术主干。</p><p>另外一条脉络就是互联网方面的(HTML/CSS/JS/LAMP…)。这条脉络和上述的那条C/C++/Java的我都没有放,作为一个有技术忧虑症的人,这两条软件开发的主线一定不能放弃。无论是应用还是学术,我都会看,知识不愁多。何必搞应用的和搞学术的分开阵营,互相看不起呢?都是知识,学就好了。<br>技术的发展要根植于历史,而不是未来。不要和我描述这个技术的未来会多么美好,用这个技术可以实现什么花哨的东西。很多常青的技术都是承前的。所以说“某某(技术)要火”这样的话是没有意义的,等它火了、应用多了咱们再说嘛。有些人说不学C/C++也是没有问题的,我对此的回应是:如果连主干都可以不学的话,还有什么其他的好学呢?极端一点,我要这么说:这些是计算机发展的根、脉络、祖师爷,这样的东西怎么可以不学呢?大部分学校虽然都会教授C,但是教得都不好。学校喜欢教微软的东西,老师好教学生好学。我不是说Windows不好,但那不是计算机文化的主干,那只是微软的主干、PC的主干。整个计算机文化的主干肯定是源起于Unix/C这条线上(注意,我说的是文化不是技术)。我也写过很多与Unix文化相关的文章,大家可以看看我写的“Unix传奇”。</p><h3 id="2-8、对于学校计算机科学教育的看法"><a href="#2-8、对于学校计算机科学教育的看法" class="headerlink" title="2.8、对于学校计算机科学教育的看法"></a>2.8、对于学校计算机科学教育的看法</h3><p>学校教的大部分都是知识密集型的技术,但是社会上的企业大部分都是劳动密集型的。什么是劳动密集型的企业呢?麦当劳炸薯条就是劳动密集型的工作,用不到学校教授的那些知识。如果有一天你不炸薯条了,而要去做更大更专业的东西,学校里的知识就会派上用场。有人说一个语言、一个技术,能解决问题能用就行了,我不这样认为。我觉得你应该至少要知道这些演变和进化的过程。而如果你要解决一些业务和技术难题,就需要抓住某种技术很深入地学习,当成艺术一样来学习。</p><p>我在“软件开发‘三重门’”里说过,第一重门是业务功能,在这重门里,的确是会编程就可以了;第二重门是业务性能,在这一重门里,技术的基础就很管用了,比如:操作系统的文件管理,进程调度,内存管理,网络的七层模型,TCP/UDP的协议,语言用法、编译和类库的实现,数据结构,算法等等就非常关键了;第三重门是业务智能,在这一重门里,你会发现很多东西都很学院派了,比如,搜索算法,推荐算法,预测,统计,机器学习,图像识别,分布式架构和算法等等,你需要读很多计算机学院派的论文。<br>总之,这主要看你职业生涯的背景了,如果你整天被当作劳动力来使用,你用到的技术就比较浅,比较实用,但是如果你做一些知识密集型的工作,你就需要用心来搞搞研究,就会发现你需要理论上的知识。比如说,我之前做过的跨国库存调配,需要知道最短路径的算法,而我现在在亚马逊做的库存预测系统,数据挖掘的那些东西都需要很强的数学建模、算法、数据挖掘的功底。<br>我觉得真正的高手都来自知识密集型的学院派。他们更强的是,可以把那些理论的基础知识应用到现在的业务上来。但很可惜,我们国内今天的教育并没有很好地把那些学院派的理论知识和现实的业务问题很好地结合起来。比如说一些哈希表或二叉树的数据结构,如果我们的学校在讲述这些知识的时候能够结合实际的业务问题,效果会非常不错,比如:设计一个IP地址和地理位置的查询系统,设计一个分布式的NoSQL的数据库,或是设计一个地理位置的检索应用等等。在学习操作系统的时候,如果老师可以带学生做一个手机或嵌入式操作系统,或是研究一下Unix System V或是Linux的源码的话,会更有意思。在学习网络知识的时候,能带学生重点学一下以太网和TCP/IP的特性,并调优,或是能做一个网络上的Pub/Sub消息系统或是做一个像Nginx一样的web server,那会更好。如果在学图形学的过程中能带领学生实践开发一个作图工具或是一个游戏引擎,那会更有意思。<br>总之,我们的教育和现实脱节太严重了,教的东西无论是在技术还是在实践上都严重落后和脱节,没有通过实际的业务或技术问题来教学生那些理论知识,这是一个失败。</p><h3 id="2-9、如何在压力下,享受技术带来的快乐"><a href="#2-9、如何在压力下,享受技术带来的快乐" class="headerlink" title="2.9、如何在压力下,享受技术带来的快乐"></a>2.9、如何在压力下,享受技术带来的快乐</h3><p>中国人中庸的思想,入世和出世,每天的工作就是入世。举个例子,在上海的时候,给交通银行做项目的时候,每周休息一天,早九点到晚十点,每天工作12个小时,这样的工作持续了一整年,没有节假日,项目上的技术也没什么意思。当时我晚上十点回到住处,还想学一些C++/Java和Unix/Windows的技术,于是就看书到晚上11:30,每天如此,一年下来学到很多东西,时间没有荒废,心里就很开心。我觉得当时是快乐的,因为有成长的感觉是快乐的。</p><p>现在的我,工作、写博客、养孩子,事情其实更多。我早上7:30起床,会浏览一下国外的新闻,hacker news,tech church,reddit,highavailability之类的站点,9点上班。晚上6、7点钟下班,开始带孩子。十点钟孩子睡了觉,我会开始重新细读一下这一天都发生了些什么事情。这个时间也有可能会用来看书。学习的过程我是不喜欢被打断的,所以从十点到十二点,家人都睡了,这正是我连续学习的好时间。可能从晚上11:30开始,我会做点笔记或者写博客。我现在对酷壳文章的质量要求比较高一些,所以大概积累一个星期的时间才可以生成一篇文章。每天我大概都在一两点钟才会睡觉。没办法,我有技术焦虑症。但是觉得这样的生活很充实,也很踏实。<br>另外,任何一门技术玩深了,都是很有意思的。有些人形成了一个价值取向,“我只做什么,绝不做什么”。前段时间有一个刚来亚马逊的工程师,他原来做的是数据挖掘推荐系统,后来公司重组要他做前端,他不肯。我觉得,前端后端都是编程,Javascript是编程,C++也是编程。编程不在于你用什么语言去coding,而是你组织程序、设计软件的能力,只要你上升到脑力劳动上来,用什么都一样,技术无贵贱就是这个意思。<br>回到问题,怎么才能享受到快乐呢?第一,入世和出世要分开,不要让世俗的东西打扰到你的内心世界,你的情绪不应该为别人所控,也不应该被世俗所污染,活得真实,你才会快乐。第二点就是要有热情,有了热情,你的心情就会很好,加班都可以是快乐的,想一想我们整个通宵用来打游戏的时光,虽然很累,但是你也很开心,这都是因为有了热情的缘故。</p><h3 id="2-10、做自己是最难的"><a href="#2-10、做自己是最难的" class="headerlink" title="2.10、做自己是最难的"></a>2.10、做自己是最难的</h3><p>我承认我活在我的精神家园里面。我推荐大家看一下王小波的《我的精神家园》,这篇文章对我的影响非常大。看了这篇文章,你就会明白我为什么要躺在自己的池子里,如果不想被这个社会所污染,就必须要躺在自己的池子里。做大众是很容易的,做自己是最难的。当你老了的时候,回想过去,如果你是为自己而活的,你总会觉得很踏实。可能有人会觉得我偏激,没关系,为什么要所有人看法都一致呢?世界因为不同而美丽,多元化的价值观并不冲突。</p><p>转载自:<a href="http://www.ituring.com.cn/article/9174">http://www.ituring.com.cn/article/9174</a></p>]]></content>
<summary type="html"><h1 id="左耳朵耗子-陈皓"><a href="#左耳朵耗子-陈皓" class="headerlink" title="左耳朵耗子-陈皓"></a>左耳朵耗子-陈皓</h1><h2 id="一、个人简历"><a href="#一、个人简历" class="headerli</summary>
<category term="编程人生" scheme="https://lightnine/github.io/categories/%E7%BC%96%E7%A8%8B%E4%BA%BA%E7%94%9F/"/>
</entry>
<entry>
<title>pip以及conda镜像源修改及命令使用</title>
<link href="https://lightnine/github.io/pip%E4%BB%A5%E5%8F%8Aconda%E9%95%9C%E5%83%8F%E6%BA%90%E4%BF%AE%E6%94%B9%E5%8F%8A%E5%91%BD%E4%BB%A4%E4%BD%BF%E7%94%A8.html"/>
<id>https://lightnine/github.io/pip%E4%BB%A5%E5%8F%8Aconda%E9%95%9C%E5%83%8F%E6%BA%90%E4%BF%AE%E6%94%B9%E5%8F%8A%E5%91%BD%E4%BB%A4%E4%BD%BF%E7%94%A8.html</id>
<published>2019-03-25T08:57:15.000Z</published>
<updated>2024-06-10T07:00:19.058Z</updated>
<content type="html"><![CDATA[<h1 id="Python包的安装"><a href="#Python包的安装" class="headerlink" title="Python包的安装"></a>Python包的安装</h1><p>在国内环境下,因为网络原因,所以Python下很多包安装不了或者安装的速度很慢。这里主要介绍下如何修改conda以及pip对应的镜像源。</p><h2 id="pip"><a href="#pip" class="headerlink" title="pip"></a>pip</h2><p>pip主要是用来管理python包的工具,类似于Maven工具。</p><h3 id="临时修改pip安装源"><a href="#临时修改pip安装源" class="headerlink" title="临时修改pip安装源"></a>临时修改pip安装源</h3><p>比如我们要安装gevent包,我们可以输入一下命令</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">pip install -i https://pypi.tuna.tsinghua.edu.cn/simple gevent</span><br></pre></td></tr></table></figure><p>这样就会从清华这边的镜像去安装gevent库.其中<code>-i</code>参数指定了使用清华的pip源</p><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">pip install packagename -i http://pypi.douban.com/simple --trusted-host pypi.douban.com</span><br></pre></td></tr></table></figure><p>其中<code>--trusted-host</code> 参数是指设置为受信源,否则在安全性较高的连接下是连接不上的</p><h3 id="永久修改"><a href="#永久修改" class="headerlink" title="永久修改"></a>永久修改</h3><p>在用户根目录(~,而非系统根目录 / )下添加配置~/.pip/pip.conf目录添加可信源,如果目录文件不存在,可直接创建。写入如下内容:</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></pre></td><td class="code"><pre><span class="line">[<span class="string">global</span>]</span><br><span class="line"><span class="string">index-url=http://pypi.douban.com/simple</span></span><br><span class="line"><span class="string">trusted-host</span> <span class="string">=</span> <span class="string">pypi.douban.com</span> </span><br></pre></td></tr></table></figure><p>这里添加的是豆瓣源,也可以添加清华源</p><h2 id="conda使用及源修改"><a href="#conda使用及源修改" class="headerlink" title="conda使用及源修改"></a>conda使用及源修改</h2><p>conda是Anaconda中用来安装python包的工具。在Anaconda中将镜像分为两类,一类是官方的python包,放在anaconda中;另一类是第三方的python包,放在conda-forge中。</p><p>采用conda 安装python包时,可以使用以下命令:</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">conda install -c https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/linux-64 joblib</span><br></pre></td></tr></table></figure><p>其中参数<code>-c</code>指定了镜像源的通道,这里实在anaconda官方中安装joblib</p><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">conda install -c https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/linux-64 jieba</span><br></pre></td></tr></table></figure><p>这里是在conda-forge中安装jieba第三方的Python包</p>]]></content>
<summary type="html"><h1 id="Python包的安装"><a href="#Python包的安装" class="headerlink" title="Python包的安装"></a>Python包的安装</h1><p>在国内环境下,因为网络原因,所以Python下很多包安装不了或者安装的速度很</summary>
<category term="python" scheme="https://lightnine/github.io/categories/python/"/>
<category term="pip" scheme="https://lightnine/github.io/tags/pip/"/>
<category term="conda" scheme="https://lightnine/github.io/tags/conda/"/>
</entry>
<entry>
<title>BIO NIO AIO详解</title>
<link href="https://lightnine/github.io/BIO-NIO-AIO%E8%AF%A6%E8%A7%A3.html"/>
<id>https://lightnine/github.io/BIO-NIO-AIO%E8%AF%A6%E8%A7%A3.html</id>
<published>2019-03-23T08:20:19.000Z</published>
<updated>2024-06-10T07:00:19.000Z</updated>
<content type="html"><![CDATA[<h1 id="Java-BIO-NIO-AIO详解"><a href="#Java-BIO-NIO-AIO详解" class="headerlink" title="Java BIO NIO AIO详解"></a>Java BIO NIO AIO详解</h1><h2 id="同步,异步,阻塞,非阻塞"><a href="#同步,异步,阻塞,非阻塞" class="headerlink" title="同步,异步,阻塞,非阻塞"></a>同步,异步,阻塞,非阻塞</h2><p>这几个概念理解起来确实比较困难,特别是同步和阻塞,异步和非阻塞。首先要明确的一个概念是同步和异步主要是关注的是消息通信机制,所以同步和异步主要是关注客户端和服务端两个方面的消息如何通信。而阻塞和非阻塞主要是等待调用结果时的状态,所以关注的主要是当前线程在等待结果时能够做什么,如果在等待结果时当前线程能够做其他的事,则线程是非阻塞的;如果只能等待返回结果,则当前线程是阻塞的。下面举个例子来具体说明一下:<br>比如你跟书店老板打电话,确认是否书店中有哈利波特这本书。书店老板电话没有挂断,说你等一下,我现在查一下,你一直在等待,此时就是一种同步通信。而如果老板说我晚点电话通知你,然后挂断电话,此时就是异步通信。<br>让我们在换一个视角,还是打电话确定是否有哈利波特这本书这件事情。在打电话的过程中,你什么都没有干,只是一直在等待,那么此时你就是处于阻塞状态。而如果此时你正在看电视,此时你就是非阻塞状态,但是需要不定时的检查一下电话那边有没有回复。</p><blockquote><p>所谓同步,就是在发出一个<strong>调用</strong>时,在没有得到结果之前,该<strong>调用</strong>就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由<strong>调用者</strong>主动等待这个<strong>调用</strong>的结果.<br>而异步则是相反,<strong>调用</strong>在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在<strong>调用</strong>发出后,<strong>被调用者</strong>通过状态、通知来通知调用者,或通过回调函数处理这个调用。<br>阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。<br>在IO操作中,有以下四种组合:</p></blockquote><ol><li>同步阻塞IO:调用者发起IO操作请求,等待IO操作完成(阻塞)。IO操作的过程需要等待,等待服务端返回结果,操作执行完成后返回结果(同步)</li><li>同步非阻塞:调用者发起IO操作请求,询问IO操作的的状态,如果未完成,则立即返回;如果完成,则返回结果(非阻塞)。IO操作的过程需要等待执行完成才返回结果(同步)</li><li>异步阻塞:调用者发起IO操作请求,等待IO操作完成在返回(阻塞)。IO操作的过程不需要等待,操作完成后通过通知或者回调获取结果(异步)</li><li>异步非阻塞:调用者发起IO操作请求,询问IO操作的状态,如果未完成,则立即返回;如果完成,则返回结果(非阻塞)。IO操作的过程不需要等待,操作完成后通过通知或回调获得结果(异步)<br>在下面的具体介绍中,我会进行具体的说明。</li></ol><h2 id="Java-IO-操作类"><a href="#Java-IO-操作类" class="headerlink" title="Java IO 操作类"></a>Java IO 操作类</h2><p>Java中进行IO操作的类一般分为以下四类:</p><ol><li>字节流的输入和输出:InputStream和OutputStream</li><li>字符流的输入和输出</li><li>网络编程Socket</li><li></li></ol><h2 id="BIO"><a href="#BIO" class="headerlink" title="BIO"></a>BIO</h2><p>首先BIO是同步阻塞调用。阻塞是因为服务端在调用accept方法时,服务端会一直阻塞在accept方法上,直到在对应的端口上接收到数据;同步是因为客户端会一直等待服务端执行完成才返回结果。可以想一下JavaScript中的Ajax请求,在异步Ajax请求发出后,浏览器会执行接下来的JS代码,直到服务端发回处理结果,然后执行对应的回调函数。这是典型的异步请求。</p><h2 id="NIO"><a href="#NIO" class="headerlink" title="NIO"></a>NIO</h2><p>NIO(New IO or Non-Block IO)是一种同步非阻塞的通信模式。NIO客户端和服务器之间通过Channel通信。NIO可以在Channel进行读写操作。这些Channel都会被注册在Selector多路复用器上。Selector通过一个线程不停的轮询这些Channel。找出已经准备就绪的Channel执行IO操作。说是非阻塞是因为NIO通过一个线程轮询,实现千万个客户端的请求。说是同步是因为客户端一直在等待服务端执行完成才返回结果。</p><h2 id="AIO"><a href="#AIO" class="headerlink" title="AIO"></a>AIO</h2><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2>]]></content>
<summary type="html"><h1 id="Java-BIO-NIO-AIO详解"><a href="#Java-BIO-NIO-AIO详解" class="headerlink" title="Java BIO NIO AIO详解"></a>Java BIO NIO AIO详解</h1><h2 id="同</summary>
<category term="java" scheme="https://lightnine/github.io/categories/java/"/>
<category term="IO" scheme="https://lightnine/github.io/tags/IO/"/>
<category term="BIO" scheme="https://lightnine/github.io/tags/BIO/"/>
<category term="NIO" scheme="https://lightnine/github.io/tags/NIO/"/>
</entry>
<entry>
<title>2019-plan-and-learn</title>
<link href="https://lightnine/github.io/2019-plan-and-learn.html"/>
<id>https://lightnine/github.io/2019-plan-and-learn.html</id>
<published>2019-01-05T09:08:14.000Z</published>
<updated>2024-06-10T07:00:18.999Z</updated>
<content type="html"><![CDATA[<h1 id="2019计划"><a href="#2019计划" class="headerlink" title="2019计划"></a>2019计划</h1><p>激励<a href="http://www.ituring.com.cn/article/9174">左耳朵耗子</a></p><h2 id="目标"><a href="#目标" class="headerlink" title="目标"></a>目标</h2><ol><li>掌握tensorflow的使用</li><li>掌握Python以及常用库(numpy,matplotlib,pandas)的使用</li><li>掌握常用机器学习算法(SVM,AdaBoost,LightGBM)的使用以及理论</li><li>掌握深度学习常用算法应用和理论</li><li>掌握英语单词5000个,提高自己的英语发音</li><li>spark大数据分析技术</li><li>linux相关技术</li><li>Java相关技术</li></ol><h2 id="具体实现"><a href="#具体实现" class="headerlink" title="具体实现"></a>具体实现</h2><p>今天看了一篇虎扑上面<非CS专业转行机器学习/人工智能阶段性成功,分享一点个人经验吧>的帖子,感触比较多,摘一点帖子里面的内容,为之后的学习提供一定的建议。<br>楼主的情况是没有人工智能基础的,为了找到人工智能的工作准备了两年时间。</p><p>编程:掌握一门语言,要达到非常熟悉的阶段。<br>数据结构:掌握常用的数据结构,多做leetcode上面的编程题目,至少要刷一下easy和medium模式的题目,写的时候考虑test case<br>机器学习:</p><pre><code>1. andrew ng的machine learning,仔细看课件和习题2. hands on machine learning with scikit-learn and tensorflow,并且在pythhon中实践还有课后习题3. bishop的pattern recognition and machine learning4. 李彦宏的机器学习和深度学习(自己加的)5. 李航的统计机器学习(自己加的)</code></pre><h2 id="Java相关技术"><a href="#Java相关技术" class="headerlink" title="Java相关技术"></a>Java相关技术</h2><p>参考了<a href="https://www.v2ex.com/t/546203">v2ex</a> 或者<a href="https://github.com/farmerjohngit/myblog">github</a><br>这里做了一个思维导图<br><img src="/2019-plan-and-learn/Java%E7%9B%B8%E5%85%B3%E7%9F%A5%E8%AF%86%E5%AD%A6%E4%B9%A0%E6%80%9D%E7%BB%B4%E5%AF%BC%E5%9B%BE.PNG" class="" title="Java相关知识学习思维导图"><br>下面进行思维导图的一些解释。<strong>同时对这些技能点进行查漏补缺,同时也会在之后的过程中添加更多的技能点</strong></p><h3 id="集合"><a href="#集合" class="headerlink" title="集合"></a>集合</h3><p>集合主要是java.util包下的非线程安全和线程安全集合</p><h4 id="非线程安全"><a href="#非线程安全" class="headerlink" title="非线程安全"></a>非线程安全</h4><ol><li>List: ArrayList与LinkedList的实现和区别</li><li>Map:<ul><li>HashMap:了解其数据结构,源码,hash冲突如何解决(链表和红黑树),扩容时机,扩容时避免rehash优化</li><li>LinkedHashMap:了解基本原理,哪两种有序,如何实现LRU</li><li>TreeMap:了解数据结构,了解其key对象为什么必须要实现Compare接口,如何用它实现一致性哈希</li></ul></li><li>Set:基本上是由map实现,简单看看就好</li></ol><p><strong>常见问题</strong></p><ul><li>hashmap 如何解决 hash 冲突,为什么 hashmap 中的链表需要转成红黑树?</li><li>hashmap 什么时候会触发扩容?</li><li>jdk1.8 之前并发操作 hashmap 时为什么会有死循环的问题?</li><li>hashmap 扩容时每个 entry 需要再计算一次 hash 吗?</li><li>hashmap 的数组长度为什么要保证是 2 的幂?</li><li>如何用 LinkedHashMap 实现 LRU ?</li><li>如何用 TreeMap 实现一致性 hash ?</li></ul><h4 id="线程安全的集合"><a href="#线程安全的集合" class="headerlink" title="线程安全的集合"></a>线程安全的集合</h4><ol><li>Collection.synchronized:了解其实现原理</li><li>CopyOnWriteArrayList:了解写时复制机制,了解其适用场景,思考为什么没有ConcurrentArrayList</li><li>ConcurrentHashMap:了解实现原理,扩容时做的优化,与HashTable的对比</li><li>BlockingQueue:了解 LinkedBlockingQueue、ArrayBlockingQueue、DelayQueue、SynchronousQueue</li></ol><p><strong>常见问题</strong></p><ul><li>ConcurrentHashMap 是如何在保证并发安全的同时提高性能?</li><li>ConcurrentHashMap 是如何让多线程同时参与扩容?</li><li>LinkedBlockingQueue、DelayQueue 是如何实现的?</li><li>CopyOnWriteArrayList 是如何保证线程安全的?</li></ul><h3 id="并发"><a href="#并发" class="headerlink" title="并发"></a>并发</h3><ol><li>synchronized:了解偏向锁、轻量级锁、重量级锁的概念以及升级机制、以及和 ReentrantLock 的区别</li><li>CAS:了解 AtomicInteger 实现原理、CAS 适用场景、如何实现乐观锁</li><li>AQS:了解 AQS 内部实现、及依靠 AQS 的同步类比如 ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier 等的实现</li><li>ThreadLocal:了解 ThreadLocal 使用场景和内部实现</li><li>ThreadPoolExecutor:了解线程池的工作原理以及几个重要参数的设置</li></ol><p><strong>常见问题</strong></p><ul><li>synchronized 与 ReentrantLock 的区别?</li><li>乐观锁和悲观锁的区别?</li><li>如何实现一个乐观锁?</li><li>AQS 是如何唤醒下一个线程的?</li><li>ReentrantLock 如何实现公平和非公平锁是如何实现?</li><li>CountDownLatch 和 CyclicBarrier 的区别?各自适用于什么场景?</li><li>适用 ThreadLocal 时要注意什么?比如说内存泄漏?</li><li>说一说往线程池里提交一个任务会发生什么?</li><li>线程池的几个参数如何设置?</li><li>线程池的非核心线程什么时候会被释放?</li><li>如何排查死锁?</li></ul><h3 id="引用"><a href="#引用" class="headerlink" title="引用"></a>引用</h3><p>了解 Java 中的软引用、弱引用、虚引用的适用场景以及释放机制</p><p><strong>常见问题</strong></p><ul><li>软引用什么时候会被释放</li><li>弱引用什么时候会被释放</li></ul><h3 id="类加载"><a href="#类加载" class="headerlink" title="类加载"></a>类加载</h3><p>了解双亲委派机制</p><p><strong>常见问题</strong></p><ul><li>双亲委派机制的作用?</li><li>Tomcat 的 classloader 结构</li><li>如何自己实现一个 classloader 打破双亲委派</li></ul><h3 id="IO"><a href="#IO" class="headerlink" title="IO"></a>IO</h3><p>了解 BIO 和 NIO 的区别、了解多路复用机制</p><p><strong>常见问题</strong></p><ul><li>同步阻塞、同步非阻塞、异步的区别?</li><li>select、poll、eopll 的区别?</li><li>java NIO 与 BIO 的区别?</li><li>refactor 线程模型是什么?</li></ul><h3 id="JVM"><a href="#JVM" class="headerlink" title="JVM"></a>JVM</h3><p>了解GC和内存区域</p><ol><li>垃圾回收基本原理、几种常见的垃圾回收器的特性、重点了解 CMS (或 G1 )以及一些重要的参数</li><li>能说清 jvm 的内存划分</li></ol><p><strong>常见问题</strong></p><ul><li>CMS GC 回收分为哪几个阶段?分别做了什么事情?</li><li>CMS 有哪些重要参数?</li><li>Concurrent Model Failure 和 ParNew promotion failed 什么情况下会发生?</li><li>CMS 的优缺点?</li><li>有做过哪些 GC 调优?</li><li>为什么要划分成年轻代和老年代?</li><li>年轻代为什么被划分成 eden、survivor 区域?</li><li>年轻代为什么采用的是复制算法?</li><li>老年代为什么采用的是标记清除、标记整理算法</li><li>什么情况下使用堆外内存?要注意些什么?</li><li>堆外内存如何被回收?</li><li>jvm 内存区域划分是怎样的?</li></ul><h3 id="Spring"><a href="#Spring" class="headerlink" title="Spring"></a>Spring</h3><p>bean 的生命周期、循环依赖问题、spring cloud (如项目中有用过)、AOP 的实现、spring 事务传播</p><p><strong>常见问题</strong></p><ul><li>java 动态代理和 cglib 动态代理的区别(经常结合 spring 一起问所以就放这里了)</li><li>spring 中 bean 的生命周期是怎样的?</li><li>属性注入和构造器注入哪种会有循环依赖的问题?</li></ul><h3 id="Mysql"><a href="#Mysql" class="headerlink" title="Mysql"></a>Mysql</h3><p>事务隔离级别、锁、索引的数据结构、聚簇索引和非聚簇索引、最左匹配原则、查询优化( explain 等命令)</p><p><strong>常见问题</strong></p><ul><li>Mysql(innondb 下同) 有哪几种事务隔离级别?</li><li>不同事务隔离级别分别会加哪些锁?</li><li>mysql 的行锁、表锁、间隙锁、意向锁分别是做什么的?</li><li>说说什么是最左匹配?</li><li>如何优化慢查询?</li><li>mysql 索引为什么用的是 b+ tree 而不是 b tree、红黑树</li><li>分库分表如何选择分表键</li><li>分库分表的情况下,查询时一般是如何做排序的?</li></ul><h3 id="算法"><a href="#算法" class="headerlink" title="算法"></a>算法</h3><p>准备一下leetcode上的算法(easy,medium)</p>]]></content>
<summary type="html"><h1 id="2019计划"><a href="#2019计划" class="headerlink" title="2019计划"></a>2019计划</h1><p>激励<a href="http://www.ituring.com.cn/article/9174">左耳朵</summary>
<category term="plan" scheme="https://lightnine/github.io/categories/plan/"/>
<category term="2019" scheme="https://lightnine/github.io/tags/2019/"/>
</entry>
<entry>
<title>tensorboard介绍</title>
<link href="https://lightnine/github.io/tensorboard%E4%BB%8B%E7%BB%8D.html"/>
<id>https://lightnine/github.io/tensorboard%E4%BB%8B%E7%BB%8D.html</id>
<published>2018-12-26T03:23:15.000Z</published>
<updated>2024-06-10T07:00:19.060Z</updated>
<content type="html"><![CDATA[<p>最近因为工作需要,看了下tensorboard的使用方式,这里做了简单的学习记录。使用的tensorflow版本是<strong>1.10.0</strong>,tensorboard的版本也是一样。</p><h1 id="tensorboard介绍"><a href="#tensorboard介绍" class="headerlink" title="tensorboard介绍"></a>tensorboard介绍</h1><p>tensorboard主要是为了查看tensorflow程序,将程序进行图示化,可以用来查看程序的运行情况,也可以用来debug程序。</p><h2 id="tensorflow写tensorboard日志"><a href="#tensorflow写tensorboard日志" class="headerlink" title="tensorflow写tensorboard日志"></a>tensorflow写tensorboard日志</h2><p>在程序中主要使用<code>tf.summary.FileWriter</code>来将需要记录的事件日志记录到硬盘中。这个函数主要由三个参数需要注意的,如果想要查看详细的内容,可以去源码中查找具体的使用方式。</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></pre></td><td class="code"><pre><span class="line"><span class="string">logdir:指定日志写入的具体目录</span></span><br><span class="line"><span class="string">graph:指定记录的tensorflow图</span></span><br><span class="line"><span class="string">filename_suffix:事件日志文件的后缀</span></span><br></pre></td></tr></table></figure><h2 id="tensorboard启动"><a href="#tensorboard启动" class="headerlink" title="tensorboard启动"></a>tensorboard启动</h2><p>tensorboard的启动是在命令行中,输入<code>tensorboard --logdir=/path/to/log</code>命令启动,然后打开ip+port在浏览器中进行查看。查看tensorboard的参数信息,可以运行<code>tensorboard --help</code>来进行查看。</p><figure class="highlight yaml"><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="string">--inspect:</span> <span class="string">用来查看事件日志信息,并不启动tensorboard</span></span><br><span class="line"><span class="string">--logdir:用来指定事件日志的存储目录</span></span><br></pre></td></tr></table></figure><p>tensorboard主要是扫描logdir指定目录下的内容,扫描文件名带有<em>.tfevents.</em>的文件进行展示。如果logdir下有多个目录,则在浏览器中可以分别进行浏览。如果有多个相同的文件,则会显示时间戳距离最近的文件。文件名中带有时间戳。例如:<strong>events.out.tfevents.1545795299.DESKTOP-123</strong></p>]]></content>
<summary type="html"><p>最近因为工作需要,看了下tensorboard的使用方式,这里做了简单的学习记录。使用的tensorflow版本是<strong>1.10.0</strong>,tensorboard的版本也是一样。</p>
<h1 id="tensorboard介绍"><a href="</summary>
<category term="tensorflow" scheme="https://lightnine/github.io/categories/tensorflow/"/>
<category term="tensorboard" scheme="https://lightnine/github.io/tags/tensorboard/"/>
</entry>
<entry>
<title>欧洲四国行</title>
<link href="https://lightnine/github.io/%E6%AC%A7%E6%B4%B2%E5%9B%9B%E5%9B%BD%E8%A1%8C.html"/>
<id>https://lightnine/github.io/%E6%AC%A7%E6%B4%B2%E5%9B%9B%E5%9B%BD%E8%A1%8C.html</id>
<published>2018-12-22T12:36:16.000Z</published>
<updated>2024-06-10T07:00:19.060Z</updated>
<content type="html"><![CDATA[<p>今年九月份去了一趟欧洲,总共去了有十五天的样子。去了比利时,西班牙(巴萨罗那),葡萄牙(里斯本),法国(巴黎)四个国家。除了比利时,其他的三个国家都是只去了一个城市。</p><h1 id="比利时"><a href="#比利时" class="headerlink" title="比利时"></a>比利时</h1><p>比利时是我到达欧洲的第一站,第一天主要是在布鲁塞尔市内玩。去了市中心的大广场,雨果笔下世界上最美丽的广场。第一眼看到此广场,给我很大的震撼,整个广场四周全部是欧式建筑,有巴洛克式,也有哥特式。建筑上面的人物雕塑也非常精美。置身其中,你能感觉到建筑帮助我直达它所在的那个年代,像我展示着那个时代的辉煌。而且这种建筑不单单只是作为居住或者商业的功能,它的美感也是其重要的组成部分。第二天去了比利时的古城-布鲁日,还有靠近海边的城市奥斯坦德。早上早早出发,乘坐火车前往布鲁日,到了地方,渐渐的天空中开始飘起雨。可能因为前一天看了布鲁塞尔的大广场,来到布鲁日,虽然建筑很好看,但是并没有给我很大的震撼了。现在回想起来,也就能记得当时主要是沿布鲁日古城区转了转,并没有特别深的印象了。因为准备工作做得不足,本来想要去奥斯坦德看下北海的,但是到了地方之后,发现太冷了,也就没有转,下了火车就又回去了,也是一个遗憾吧。<br>来比利时之前,并不是怎么了解这个国家。来了之后发现国家虽然小,但是有名的东西还是很多的。首先是啤酒。可能说到啤酒,大家想到的一个就是德国。但是来了比利时之后,才发现比利时啤酒才真的是啤酒的王国。首先不像德国啤酒种类较为较为单一,这里的啤酒种类非常丰富,据说有四五百种。而且大街上酒吧也非常密集。假如超市里面只有六个货架,那么光酒就能占三个货架,哈哈。来到此地,怎么能不尝尝呢?去超市买了七八种啤酒,还有一瓶产自法国勃艮第的红酒。三天时间全部被我喝完了,这里的啤酒真的名不虚传,味道真不错,比国内的啤酒强太多了。哎,现在想喝也很难喝到了。说到啤酒,为啥比利时这么多呢?据说因为比利时地势较为低,水不好喝,比较涩口,所以采用本地的水和啤酒花酿做了啤酒。原来比利时人是将啤酒作为水的替代品了。虽然啤酒种类很多,但是这里有一种啤酒很难喝到。它就是修道院啤酒。虽然价格不是很贵,但是真的很难买到,而且被评价为最好喝的啤酒。首先,全球能够生产修道院啤酒只有七个修道院,荷兰有一家,比利时占了剩下六家,三家在比利时的荷语区,三家在比利时的法语区。并且每年生产的啤酒就那么多,不会有多的。这些啤酒都是修道院里面的修士酿造的。修士除了平时进行宗教的学习,剩下很多的时间都在潜心专研如何酿造好喝的啤酒。比利时除了啤酒以外,还有它的巧克力也是全球闻名。路边的巧克力店很多。还有炸薯条。</p><h1 id="巴塞罗那"><a href="#巴塞罗那" class="headerlink" title="巴塞罗那"></a>巴塞罗那</h1><h1 id="里斯本"><a href="#里斯本" class="headerlink" title="里斯本"></a>里斯本</h1><h1 id="巴黎"><a href="#巴黎" class="headerlink" title="巴黎"></a>巴黎</h1>]]></content>
<summary type="html"><p>今年九月份去了一趟欧洲,总共去了有十五天的样子。去了比利时,西班牙(巴萨罗那),葡萄牙(里斯本),法国(巴黎)四个国家。除了比利时,其他的三个国家都是只去了一个城市。</p>
<h1 id="比利时"><a href="#比利时" class="headerlink" ti</summary>
<category term="欧洲" scheme="https://lightnine/github.io/categories/%E6%AC%A7%E6%B4%B2/"/>
<category term="旅行" scheme="https://lightnine/github.io/categories/%E6%AC%A7%E6%B4%B2/%E6%97%85%E8%A1%8C/"/>
<category term="比利时" scheme="https://lightnine/github.io/tags/%E6%AF%94%E5%88%A9%E6%97%B6/"/>
<category term="巴塞罗那" scheme="https://lightnine/github.io/tags/%E5%B7%B4%E5%A1%9E%E7%BD%97%E9%82%A3/"/>
<category term="里斯本" scheme="https://lightnine/github.io/tags/%E9%87%8C%E6%96%AF%E6%9C%AC/"/>
<category term="巴黎" scheme="https://lightnine/github.io/tags/%E5%B7%B4%E9%BB%8E/"/>
</entry>
<entry>
<title>docker安装sshd以及修改镜像源和软件源</title>
<link href="https://lightnine/github.io/docker%E5%AE%89%E8%A3%85sshd%E4%BB%A5%E5%8F%8A%E4%BF%AE%E6%94%B9%E9%95%9C%E5%83%8F%E6%BA%90%E5%92%8C%E8%BD%AF%E4%BB%B6%E6%BA%90.html"/>
<id>https://lightnine/github.io/docker%E5%AE%89%E8%A3%85sshd%E4%BB%A5%E5%8F%8A%E4%BF%AE%E6%94%B9%E9%95%9C%E5%83%8F%E6%BA%90%E5%92%8C%E8%BD%AF%E4%BB%B6%E6%BA%90.html</id>
<published>2018-12-20T08:26:08.000Z</published>
<updated>2024-06-10T07:00:19.050Z</updated>
<content type="html"><![CDATA[<h1 id="总览"><a href="#总览" class="headerlink" title="总览"></a>总览</h1><p>因为最近公司需要在容器中跑深度学习任务,所以需要tensorflow的镜像。并且要求在要通过ssh进行容器的通信。这篇文章主要介绍如何在docker容器中安装软件,如何修改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></pre></td><td class="code"><pre><span class="line"><span class="built_in">uname</span> -r : 3.10.0-862.11.6.el7.x86_64</span><br><span class="line"><span class="built_in">cat</span> /etc/redhat-release : CentOS Linux release 7.5.1804 (Core)</span><br><span class="line">docker version : 18.06.0-ce</span><br></pre></td></tr></table></figure><p>镜像主要选择的是官方的tensorflow:1.12.0-py3版本,镜像里面使用的系统环境下面会介绍</p><h2 id="修改docker镜像源"><a href="#修改docker镜像源" class="headerlink" title="修改docker镜像源"></a>修改docker镜像源</h2><p><a href="https://www.docker-cn.com/registry-mirror">!参考</a><br>由于docker 官方的镜像源在国外,导致下载速度很慢。所以第一步是修改docker镜像源地址。我这里采用永久修改docker镜像源地址。打开<code>/etc/docker/daemon.json</code>(如果没有此文件,则新建),添加如下内容:</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></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="attr">"registry-mirrors":</span> [<span class="string">"https://registry.docker-cn.com"</span>]</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>然后运行<code>docker pull registry.docker-cn.com/tensorflow/tensorflow:1.12.0-py3</code><br>即可以拉取镜像,而且可以看到速度提升很快</p><h2 id="启动镜像"><a href="#启动镜像" class="headerlink" title="启动镜像"></a>启动镜像</h2><p>我们可以使用<code>docker images</code>查看本机有的镜像。针对上面拉取的tensorflow:1.12.0-py3镜像,我们执行下面命令启动</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 run -it [image_name] /bin/bash</span><br></pre></td></tr></table></figure><p>此时我们应该是在启动的容器中。我们可以查看容器具体的系统信息。因为tensorflow基础镜像是ubuntu,所以这里运行以下命令,因为容器中一般不会有vim命令,所以采用cat命令参看系统信息。注意下面的命令都是在容器中键入</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"><span class="built_in">uname</span> -r</span><br><span class="line"><span class="built_in">cat</span> /etc/issue</span><br></pre></td></tr></table></figure><p>查看得到系统信息为:Ubuntu 16.04.5 LTS</p><h2 id="修改容器中软件源地址"><a href="#修改容器中软件源地址" class="headerlink" title="修改容器中软件源地址"></a>修改容器中软件源地址</h2><p>由于容器中软件源默认地址是国外的,这里换成国内的。</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"><span class="built_in">cd</span> /etc/apt</span><br><span class="line"><span class="built_in">cp</span> sources.list sources.list.bak</span><br></pre></td></tr></table></figure><p>这里使用阿里的镜像源,将sources.list里面的内容修改为如下内容:</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><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="comment"># 默认注释了源码镜像以提高 apt update 速度,如有需要可自行取消注释</span></span><br><span class="line"><span class="string">deb</span> <span class="string">http://mirrors.aliyun.com/ubuntu/</span> <span class="string">xenial</span> <span class="string">main</span> <span class="string">restricted</span> <span class="string">universe</span> <span class="string">multiverse</span></span><br><span class="line"><span class="comment"># deb-src http://mirrors.aliyun.com/ubuntu/ xenial main main restricted universe multiverse</span></span><br><span class="line"><span class="string">deb</span> <span class="string">http://mirrors.aliyun.com/ubuntu/</span> <span class="string">xenial-updates</span> <span class="string">main</span> <span class="string">restricted</span> <span class="string">universe</span> <span class="string">multiverse</span></span><br><span class="line"><span class="comment"># deb-src http://mirrors.aliyun.com/ubuntu/ xenial-updates main restricted universe multiverse</span></span><br><span class="line"><span class="string">deb</span> <span class="string">http://mirrors.aliyun.com/ubuntu/</span> <span class="string">xenial-backports</span> <span class="string">main</span> <span class="string">restricted</span> <span class="string">universe</span> <span class="string">multiverse</span></span><br><span class="line"><span class="comment"># deb-src http://mirrors.aliyun.com/ubuntu/ xenial-backports main restricted universe multiverse</span></span><br><span class="line"><span class="string">deb</span> <span class="string">http://mirrors.aliyun.com/ubuntu/</span> <span class="string">xenial-security</span> <span class="string">main</span> <span class="string">restricted</span> <span class="string">universe</span> <span class="string">multiverse</span></span><br><span class="line"><span class="comment"># deb-src http://mirrors.aliyun.com/ubuntu/ xenial-security main restricted universe multiverse</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 预发布软件源,不建议启用</span></span><br><span class="line"><span class="comment"># deb http://mirrors.aliyun.com/ubuntu/ xenial-proposed main restricted universe multiverse</span></span><br><span class="line"><span class="comment"># deb-src http://mirrors.aliyun.com/ubuntu/ xenial-proposed main restricted universe multiverse</span></span><br></pre></td></tr></table></figure><h2 id="容器中安装软件"><a href="#容器中安装软件" class="headerlink" title="容器中安装软件"></a>容器中安装软件</h2><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">passwd</span><br><span class="line">apt-get update</span><br><span class="line">apt-get install vim</span><br><span class="line">apt-get install openssh-sshd</span><br></pre></td></tr></table></figure><p>修改root密码以及安装完上面vim和openssh-sshd两个软件后,修改openssh-sshd配置文件内容。打开<code>/etc/ssh/sshd_config</code>将<code>PermitRootLogin</code>后面添加yes,即允许以root用户登录。后面启动sshd命令可能会报错,具体问题可以自己上网上搜索</p><h2 id="生成镜像"><a href="#生成镜像" class="headerlink" title="生成镜像"></a>生成镜像</h2><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">docker commit [containerID] [image_name]</span><br></pre></td></tr></table></figure><p>执行<code>docker images</code>可以看到生成的镜像。启动此镜像并开启sshd服务命令如下</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 run -d -p 50001:22 [images_name] /usr/sbin/sshd -D</span><br><span class="line">其中 -d表示已后台方式启动容器,-p挂载端口 ,/usr/sbin/sshd -D 是启动命令</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html"><h1 id="总览"><a href="#总览" class="headerlink" title="总览"></a>总览</h1><p>因为最近公司需要在容器中跑深度学习任务,所以需要tensorflow的镜像。并且要求在要通过ssh进行容器的通信。这篇文章主要介绍如何在do</summary>
<category term="docker" scheme="https://lightnine/github.io/categories/docker/"/>
<category term="镜像源" scheme="https://lightnine/github.io/tags/%E9%95%9C%E5%83%8F%E6%BA%90/"/>
<category term="软件源" scheme="https://lightnine/github.io/tags/%E8%BD%AF%E4%BB%B6%E6%BA%90/"/>
</entry>
<entry>
<title>集成算法</title>
<link href="https://lightnine/github.io/%E9%9B%86%E6%88%90%E7%AE%97%E6%B3%95.html"/>
<id>https://lightnine/github.io/%E9%9B%86%E6%88%90%E7%AE%97%E6%B3%95.html</id>
<published>2018-12-01T12:17:34.000Z</published>
<updated>2024-06-10T07:00:19.066Z</updated>
</entry>
</feed>