-
Notifications
You must be signed in to change notification settings - Fork 0
/
search.xml
409 lines (409 loc) · 335 KB
/
search.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[comsol快速入门教程]]></title>
<url>%2F2018%2F05%2F09%2Fcomsol%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8%E6%95%99%E7%A8%8B%2F</url>
<content type="text"><![CDATA[我曾经大三的专业课老师要求全班学习COMSOL! 我自己参照官网的教程,写了个比较通俗易懂的入门教程,字多图多慎看(大三的时候写的)。 教你一步一步做出第一个项目。(其实COMSOL本身不难,主要是其中涉及的物理建模和数理方程的知识是不可或缺的) 扳手受力分析 我们平常用的扳手:如下图: 我们可以通过对它进行模拟分析,熟悉一下comsol的基本操作流程;(这是个简单的力学模拟,不涉及耦合场的模拟,当然,comsol最强大的功能是耦合场模拟,这个我们不急,先看完这个了解一下,具体的耦合场的例子推荐大家去看官方的help文档example2,这个例子也是上面的,相比官方来说,我的首先是中文比较好懂一点,然后解释比较直白通俗) 当然你也可能有些地方不懂,第一是有限元不太知道,第二就是数理方程边界条件不太知道,第三就是我没说明白!!! 本人也是个新手,刚看过三国,就学关公耍大刀了,大家将就着看吧,肯定会有差错,敬请原谅敬请原谅。。。。。 第一部分: 选择基本的研究领域,显然,我们这里研究的是结构力学模块中的固体力学,然后它跟时间无关,所以是稳态的研究。模型当然是选择3D的了。具体步骤有截图: 完成这四步以后,我们就可以进入comsol 的主界面了! Ps:这里稍微介绍一下,为什么我们一开始要选择不同的模块和领域,什么力学,电磁学,光学什么的,在于不同模块所需要的偏微分方程和边界条件都是不一样的(请回忆数学物理方程里的三个基本方程和相关的定界问题),comsol为我们预设了不同的微分方程所以我们不需要自己设(当然你可以选择自己设),只要点击相应的模块就行了。这是comsol的一大优势。 第二部分: 下面我们就开始设置相关的条件了。 首先我们要有一个模型,我们研究的是一个扳手,所以我们要有一个扳手的模型,可以自己绘制,这个扳手模型的绘制应该是比较复杂的,只用comsol基本功能可能比较难,可以用CAD软件区绘制,comsol支持CAD文件的导入。 上面扯了一大堆,对于我们来说,第一次就用comsol里面自带的几何模型库吧,正好里面有。我们可以选择导入,具体步骤如下: 上面那个默认的目录是:C:\Program Files\COMSOL\COMSOL44\models\COMSOL_Multiphysics\ Structural_Mechanics\wrench.mphbin 大家根据自己的安装情况不同可以找到它。 这时候,我们就把扳手的模型导入了,大家可以去随便点一点 这一排的按钮,看一看它们的作用是什么,可以随便点的,点不坏的,不用怕,我就不一一介绍了 下面我们就进行下一步了 模型选定了,接下来我们根据 它上面从上到下的顺序,选择材料属性 我们知道不同的材料的杨氏模量,泊松比是不一样的,所以要确定材料(具体请脑补朝玉大师的弹性力学与张亮分析) 大家可能会找不到它,它是在built-in里面的,需要先打开built-in再找。 然后就设置好材料了,大家可以看一看它的具体的数据 下面我们就进行第三步了,模型有了,材料也搞好了,开始设置边界条件吧!(最关键的一步) 微分方程之前我们选模块的时候就选好了,要解微分方程就看边界条件和初始条件了,这里跟时间无关(此处脑补操老师的数理方程) 扳手的受力,显然我们按住扳手的尾端,也就是在扳手的尾端施加力,扳手的前端是固定的 很明显,扳手的前端是固定端,添加固定边界条件 尾端要加上载荷(这里有问题请参考数理方程) 首先选择固定边界条件 加在扳手的前端上,变成蓝色说明已经被选定,这一步只要在扳手前端找到如图所示,点击鼠标就行了 以上两步,说明我们给扳手前端加了固定边界条件。 下面一步给扳手加上载荷: 跟上一步一样,左键选择添加边界载荷: 然后我们选定载荷作用的这一部分: 选好之后,我们看左边的栏目,添加相应的载荷属性(也就是力的方向和大小) 先选择total force,在写上力就行了,我写的是150,大家可以随便大小,但不要太大(太大,扳手承受不了就。。。。) 这里为什么有负号,因为压扳手的力是朝z轴负方向的。。。。。 到此就全部设置好了! 下面就进行网格化吧(这是有限元方法的基本步骤,不懂的请稍微看看有限元) 当然,目前也不需要你懂什么,稍微点两下就行了 再点 就行了,。。网格化完毕 这里我们用的是系统自动网格化,我们也可以选择人工的,人工的就要我们自己设置网格的大小,密度什么的。。。。 网格化完了,你的扳手就会变成这样: 全部设置都结束了 最后只要轻轻点一下计算就行了 如果你的电脑运行内存小于4g,那很不幸,你可能算不出来,要进行一些额外的步骤(现在4g以下的笔记本应该不多了,如果你是,就来问我吧) 如果你的内存小于4g的话,就请看下面: 没关系,可以用硬盘内存代替运行内存,多几个简单的步骤: 首先,你就不要点计算了,先进行以下步骤 右击 ,选择 ,然后展开 ,变成下面这个样子: 展开 ,并点击 在右边的设置窗口设置: 跟这个图上设置的一样吧 这个设置确保如果你的电脑运行低内存中计算,解算器将开始使用硬盘作为补充RAM。允许解算器使用硬盘而不是内存计算慢下来。 到此你就设置完了,可以像之前一样,右键 ,点击计算就行了。 你的内存大于4g就不用管,等它算完吧,可能要一些时间,一两分钟。。。 成功算完之后,你会看到: 是不是很神奇,整个扳手每个地方的受力都显示出来了(如果你的力不是150的话,可能情况跟我不一样。。。。。) 你看到的黑线的部分是扳手形变之前的位置,也就是初始位置。。。。 接下来,你可以稍微设置一下,就可以看到扳手的形变情况或者是受压力的情况。。。。(如果你的力不是150的话,可能情况跟我不一样。。。。。) 后面我就贴步骤,具体不啰嗦了,你基本也知道基本的流程了 这个扳手的建模我实际上是省略的全局定义的那一部分,这一部分在大型的建模过程中是排在第一步的,也就是设定参数,设定变量,设定函数什么的,比如我们就可以事先设定一个参数F来代表载荷,在填载荷的时候就可以直接写F不用写具体数值。。。。。但是我们这个小模型很显然就没必要了。。。。大家先有一个意识]]></content>
<categories>
<category>comsol</category>
</categories>
<tags>
<tag>comsol</tag>
</tags>
</entry>
<entry>
<title><![CDATA[一个故事带你搞懂ASCII,Unicode字符集和UTF-8编码]]></title>
<url>%2F2018%2F05%2F06%2F%E4%B8%80%E4%B8%AA%E6%95%85%E4%BA%8B%E5%B8%A6%E4%BD%A0%E6%90%9E%E6%87%82ASCII-Unicode%E5%AD%97%E7%AC%A6%E9%9B%86%E5%92%8CUTF-8%E7%BC%96%E7%A0%81%2F</url>
<content type="text"><![CDATA[熟悉html等知识的都知道,html中有一个重要的字段叫“content-type”,一般中文网站都是设置为“utf-8”编码,可能你还知道之所以设置为utf-8是为了正常的显示中文,但为什么utf-8就可以作为中文的编码呢?我们常常见到的ASCII码又是什么?Unicode字符集又是什么?它们之间有着什么样的关系呢?要搞清楚这些问题就得弄清楚字符的编码方式和各种常用的字符集。我查阅了一堆中文和外文的文献博客资料,研究了一波发现,有一篇已经不知道作者是谁的中文txt文档讲的绘声绘色,看的我如痴如醉,很好的解释了上述的问题,远比一些英文资料讲的还要清晰。 下面分享给大家这篇神文!希望大家可以彻底搞懂相关的字符集和编码的故事 随便说说字符集和编码 快下班时,爱问问题的小朋友Nico又问了一个问题:“sqlserver里面有char和nchar,那个n据说是指unicode的数据,这个是什么意思。”并不是所有简单的问题都很容易回答,就像这个问题一样。于是我答应专门写一篇BLOG来从头讲讲编码的故事。那么就让我们找个草堆坐下,先抽口烟,看看夜晚天空上的银河,然后想一想要从哪里开始讲起。嗯,也许这样开始比较好…… 很久很久以前,有一群人,他们决定用8个可以开合的晶体管来组合成不同的状态,以表示世界上的万物。他们看到8个开关状态是好的,于是他们把这称为”字节”。再后来,他们又做了一些可以处理这些字节的机器,机器开动了,可以用字节来组合出很多状态,状态开始变来变去。他们看到这样是好的,于是它们就这机器称为”计算机”。 开始计算机只在美国用。八位的字节一共可以组合出256(2的8次方)种不同的状态。他们把其中的编号从0开始的32种状态分别规定了特殊的用途,一但终端、打印机遇上约定好的这些字节被传过来时,就要做一些约定的动作。遇上00x10, 终端就换行,遇上0x07, 终端就向人们嘟嘟叫,例好遇上0x1b, 打印机就打印反白的字,或者终端就用彩色显示字母。他们看到这样很好,于是就把这些0x20以下的字节状态称为”控制码”。他们又把所有的空格、标点符号、数字、大小写字母分别用连续的字节状态表示,一直编到了第127号,这样计算机就可以用不同字节来存储英语的文字了。大家看到这样,都感觉很好,于是大家都把这个方案叫做 ANSI 的”Ascii”编码(American Standard Code for Information Interchange,美国信息互换标准代码)。当时世界上所有的计算机都用同样的ASCII方案来保存英文文字。后来,就像建造巴比伦塔一样,世界各地的都开始使用计算机,但是很多国家用的不是英文,他们的字母里有许多是ASCII里没有的,为了可以在计算机保存他们的文字,他们决定采用127号之后的空位来表示这些新的字母、符号,还加入了很多画表格时需要用下到的横线、竖线、交叉等形状,一直把序号编到了最后一个状态255。从128到255这一页的字符集被称”扩展字符集”。从此之后,贪婪的人类再没有新的状态可以用了,美帝国主义可能没有想到还有第三世界国家的人们也希望可以用到计算机吧!等中国人们得到计算机时,已经没有可以利用的字节状态来表示汉字,况且有6000多个常用汉字需要保存呢。但是这难不倒智慧的中国人民,我们不客气地把那些127号之后的奇异符号们直接取消掉, 规定:一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节)从0xA1用到0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们就可以组合出大约7000多个简体汉字了。在这些编码里,我们还把数学符号、罗马希腊的字母、日文的假名们都编进去了,连在 ASCII 里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的”全角”字符,而原来在127号以下的那些就叫”半角”字符了。中国人民看到这样很不错,于是就把这种汉字方案叫做 “GB2312”。GB2312 是对 ASCII 的中文扩展。但是中国的汉字太多了,我们很快就就发现有许多人的人名没有办法在这里打出来,特别是某些很会麻烦别人的国家领导人。于是我们不得不继续把 GB2312 没有用到的码位找出来老实不客气地用上。后来还是不够用,于是干脆不再要求低字节一定是127号之后的内码,只要第一个字节是大于127就固定表示这是一个汉字的开始,不管后面跟的是不是扩展字符集里的内容。结果扩展之后的编码方案被称为 GBK 标准,GBK 包括了 GB2312 的所有内容,同时又增加了近20000个新的汉字(包括繁体字)和符号。后来少数民族也要用电脑了,于是我们再扩展,又加了几千个新的少数民族的字,GBK 扩成了 GB18030。从此之后,中华民族的文化就可以在计算机时代中传承了。中国的程序员们看到这一系列汉字编码的标准是好的,于是通称他们叫做 “DBCS”(Double Byte Charecter Set 双字节字符集)。在DBCS系列标准里,最大的特点是两字节长的汉字字符和一字节长的英文字符并存于同一套编码方案里,因此他们写的程序为了支持中文处理,必须要注意字串里的每一个字节的值,如果这个值是大于127的,那么就认为一个双字节字符集里的字符出现了。那时候凡是受过加持,会编程的计算机僧侣们都要每天念下面这个咒语数百遍:“一个汉字算两个英文字符!一个汉字算两个英文字符……” 因为当时各个国家都像中国这样搞出一套自己的编码标准,结果互相之间谁也不懂谁的编码,谁也不支持别人的编码,连大陆和台湾这样只相隔了150海里,使用着同一种语言的兄弟地区,也分别采用了不同的 DBCS 编码方案——当时的中国人想让电脑显示汉字,就必须装上一个”汉字系统”,专门用来处理汉字的显示、输入的问题,但是那个台湾的愚昧封建人士写的算命程序就必须加装另一套支持 BIG5 编码的什么”倚天汉字系统”才可以用,装错了字符系统,显示就会乱了套!这怎么办?而且世界民族之林中还有那些一时用不上电脑的穷苦人民,他们的文字又怎么办?真是计算机的巴比伦塔命题啊!正在这时,大天使加百列及时出现了——一个叫 ISO (国际标谁化组织)的国际组织决定着手解决这个问题。他们采用的方法很简单:废了所有的地区性编码方案,重新搞一个包括了地球上所有文化、所有字母和符号的编码!他们打算叫它”Universal Multiple-Octet Coded Character Set”,简称 UCS, 俗称 “UNICODE”。UNICODE 开始制订时,计算机的存储器容量极大地发展了,空间再也不成为问题了。于是 ISO 就直接规定必须用两个字节,也就是16位来统一表示所有的字符,对于ascii里的那些“半角”字符,UNICODE 包持其原编码不变,只是将其长度由原来的8位扩展为16位,而其他文化和语言的字符则全部重新统一编码。由于”半角”英文符号只需要用到低8位,所以其高8位永远是0,因此这种大气的方案在保存英文文本时会多浪费一倍的空间。这时候,从旧社会里走过来的程序员开始发现一个奇怪的现象:他们的strlen函数靠不住了,一个汉字不再是相当于两个字符了,而是一个!是的,从 UNICODE 开始,无论是半角的英文字母,还是全角的汉字,它们都是统一的”一个字符”!同时,也都是统一的”两个字节”,请注意”字符”和”字节”两个术语的不同,“字节”是一个8位的物理存贮单元,而“字符”则是一个文化相关的符号。在UNICODE 中,一个字符就是两个字节。一个汉字算两个英文字符的时代已经快过去了。从前多种字符集存在时,那些做多语言软件的公司遇上过很大麻烦,他们为了在不同的国家销售同一套软件,就不得不在区域化软件时也加持那个双字节字符集咒语,不仅要处处小心不要搞错,还要把软件中的文字在不同的字符集中转来转去。UNICODE 对于他们来说是一个很好的一揽子解决方案,于是从 Windows NT 开始,MS 趁机把它们的操作系统改了一遍,把所有的核心代码都改成了用 UNICODE 方式工作的版本,从这时开始,WINDOWS 系统终于无需要加装各种本土语言系统,就可以显示全世界上所有文化的字符了。但是,UNICODE 在制订时没有考虑与任何一种现有的编码方案保持兼容,这使得 GBK 与UNICODE 在汉字的内码编排上完全是不一样的,没有一种简单的算术方法可以把文本内容从UNICODE编码和另一种编码进行转换,这种转换必须通过查表来进行。如前所述,UNICODE 是用两个字节来表示为一个字符,他总共可以组合出65535不同的字符,这大概已经可以覆盖世界上所有文化的符号。如果还不够也没有关系,ISO已经准备了UCS-4方案,说简单了就是四个字节来表示一个字符,这样我们就可以组合出21亿个不同的字符出来(最高位有其他用途),这大概可以用到银河联邦成立那一天吧! UNICODE 来到时,一起到来的还有计算机网络的兴起,UNICODE 如何在网络上传输也是一个必须考虑的问题,于是面向传输的众多 UTF(UCS Transfer Format)标准出现了,顾名思义,UTF8就是每次8个位传输数据,而UTF16就是每次16个位,只不过为了传输时的可靠性,从UNICODE到UTF时并不是直接的对应,而是要过一些算法和规则来转换。受到过网络编程加持的计算机僧侣们都知道,在网络里传递信息时有一个很重要的问题,就是对于数据高低位的解读方式,一些计算机是采用低位先发送的方法,例如我们PC机采用的 INTEL 架构,而另一些是采用高位先发送的方式,在网络中交换数据时,为了核对双方对于高低位的认识是否是一致的,采用了一种很简便的方法,就是在文本流的开始时向对方发送一个标志符——如果之后的文本是高位在位,那就发送”FEFF”,反之,则发送”FFFE”。不信你可以用二进制方式打开一个UTF-X格式的文件,看看开头两个字节是不是这两个字节? 讲到这里,我们再顺便说说一个很著名的奇怪现象:当你在 windows 的记事本里新建一个文件,输入”联通”两个字之后,保存,关闭,然后再次打开,你会发现这两个字已经消失了,代之的是几个乱码!呵呵,有人说这就是联通之所以拼不过移动的原因。其实这是因为GB2312编码与UTF8编码产生了编码冲撞的原因。从网上引来一段从UNICODE到UTF8的转换规则: UnicodeUTF-8 0000 - 007F0xxxxxxx 0080 - 07FF110xxxxx 10xxxxxx 0800 - FFFF1110xxxx 10xxxxxx 10xxxxxx 例如”汉”字的Unicode编码是6C49。6C49在0800-FFFF之间,所以要用3字节模板:1110xxxx 10xxxxxx 10xxxxxx。将6C49写成二进制是:0110 1100 0100 1001,将这个比特流按三字节模板的分段方法分为0110 110001 001001,依次代替模板中的x,得到:1110-0110 10-110001 10-001001,即E6 B1 89,这就是其UTF8的编码。而当你新建一个文本文件时,记事本的编码默认是ANSI, 如果你在ANSI的编码输入汉字,那么他实际就是GB系列的编码方式,在这种编码下,”联通”的内码是:c1 1100 0001aa 1010 1010cd 1100 1101a8 1010 1000注意到了吗?第一二个字节、第三四个字节的起始部分的都是”110”和”10”,正好与UTF8规则里的两字节模板是一致的,于是再次打开记事本时,记事本就误认为这是一个UTF8编码的文件,让我们把第一个字节的110和第二个字节的10去掉,我们就得到了”00001 101010”,再把各位对齐,补上前导的0,就得到了”0000 0000 0110 1010”,不好意思,这是UNICODE的006A,也就是小写的字母”j”,而之后的两字节用UTF8解码之后是0368,这个字符什么也不是。这就是只有”联通”两个字的文件没有办法在记事本里正常显示的原因。而如果你在”联通”之后多输入几个字,其他的字的编码不见得又恰好是110和10开始的字节,这样再次打开时,记事本就不会坚持这是一个utf8编码的文件,而会用ANSI的方式解读之,这时乱码又不出现了。 好了,终于可以回答NICO的问题了,在数据库里,有n前缀的字串类型就是UNICODE类型,这种类型中,固定用两个字节来表示一个字符,无论这个字符是汉字还是英文字母,或是别的什么。如果你要测试”abc汉字”这个串的长度,在没有n前缀的数据类型里,这个字串是7个字符的长度,因为一个汉字相当于两个字符。而在有n前缀的数据类型里,同样的测试串长度的函数将会告诉你是5个字符,因为一个汉字就是一个字符。]]></content>
<categories>
<category>网络</category>
</categories>
<tags>
<tag>网络</tag>
</tags>
</entry>
<entry>
<title><![CDATA[数字签名和数字证书究竟是什么?]]></title>
<url>%2F2018%2F03%2F04%2F%E6%95%B0%E5%AD%97%E7%AD%BE%E5%90%8D%E5%92%8C%E6%95%B0%E5%AD%97%E8%AF%81%E4%B9%A6%E7%A9%B6%E7%AB%9F%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F%2F</url>
<content type="text"><![CDATA[我们经常会见到数字签名和数字证书的身影,比如访问一些不安全的网站时,浏览器会提示,此网站的数字证书不可靠等。那么究竟什么是数字签名和数字证书呢?本文就将通过一个场景深入浅出的介绍数字签名和数字证书的概念! Bob有两个密钥,一个叫公钥Public Key,一个叫私钥Private Key。 Bob的公钥是公之于众的,所有需要的人都可以获得公钥,但Bob的私钥是自己私有的。密钥用来加密信息,将一段可以理解阅读的明文信息,用密钥进行加密,变成一段‘乱码’。因此,只有持有正确密钥的人,才能重新将这段加密后的信息,也就是‘乱码’,恢复成可以理解阅读的真实信息。Bob的两个密钥,公钥和私钥都可以将信息进行加密,并且能用对应的密钥将信息解码,也就是说,如果用Bob的公钥将信息加密,那么可以并且只可以用Bob的私钥将信息解码,反之,如果用Bob的私钥将信息加密,那么可以且只可以用Bob的公钥将信息解码! 所以Bob就可以利用自己的公钥和私钥进行信息的加密传输! 比如,Susan想要和Bob进行通信,考虑到信息的安全性,Susan可以利用Bob公之于众的公钥对所要传输的信息进行加密。这样,Bob收到信息后,就可以用自己的私钥对信息进行解码。假设此时有人窃取了Susan传给Bob的信息,但是由于没有Bob的私钥,无法对信息进行解码,所以即使窃取了信息,也无法阅读理解。 但是虽然黑客们无法解码Susan传给Bob的信息,却可以对信息进行篡改,破坏原有的信息,这样Bob收到被篡改的信息之后,再用自己的私钥进行解码,就会与Susan本来想要传达的信息出现不一致,这样也就相当于破坏了Susan和Bob的信息传输!学术上,我们将这种行为称为破坏信息的完整性!通俗的说,就是我得不到的信息,你也别想得到! 所以,现在的问题就是,我们如何保证信息的完整性,也就是保证信息不被破坏,或者说,当信息被破坏之后,接收方可以识别出,这个时候的信息是被破坏过的,就将其丢弃。 数字签名就可以解决上述的问题!根据数字签名,接收方接收到信息之后,可以判断信息是否被破坏过,如果没有被破坏,就可以正确的解码,如果被破坏,就直接丢弃。 数字签名可以保证对信息的任何篡改都可以被发现,从而保证信息传输过程中的完整性。 那么数字签名是如何实现对完整性保证的呢?关键技术就是hash,也就是哈希技术。 首先,Bob先对将要传输的信息进行hash,得到一串独一无二的字符,通常把hash之后的内容称为信息摘要message digest。我们都知道,hash往往是不可逆的,就是说,我们无法根据hash后的内容推断出hash前的原文。同时,不同的原文,会造成不同的hash结果,并且结果的差异是巨大甚至毫无规律的。也就是说,对原文进行再细微的修改,得到的hash后的内容都会与未经修改的原文的hash内容大相径庭。这样就保证了黑客对原文的任何修改都会被发现! Bob同时将hash后的信息摘要,用自己的私钥进行加密,这样就保证只有Bob的公钥才能对信息摘要进行正确的解码,这样就保证了信息摘要一定是来自Bob的,也就是起到了一个独一无二的签名的作用。加密后的信息摘要实际就是数字签名的内容。 最后,Bob再将数字签名附加到原文信息的后面,这样就形成了一个完整的带数字签名的信息报文。 Bob将带数字签名的信息报文传输给Pat。 Pat接收到信息之后,先利用Bob的公钥对数字签名进行解码,得到信息摘要,如果成功解码,就说明数字签名是来自Bob的,因为数字签名是Bob利用自己的私钥进行加密的,只有Bob的公钥可以进行解密。然后,Pat将信息原文进行hash得到自己hash的信息摘要,再与之前解码数字签名得到的信息摘要进行对比,如果相同,就说明原文信息是完整的,没有被篡改,反之,则确认信息被破坏了。 似乎现在,利用公钥和私钥以及数字签名,我们可以保证信息传输过程中的私密性和完整性。但还存在一个问题,就是公钥分发的问题,我们如果保证Bob的公钥被正确的分发给了Susan或者Pat等人呢?假设现在有一个中间人,他劫取了Bob发给Pat的公钥,然后私自伪造了一个假的公钥并加上Bob的名字,发给了Pat,这样就导致Pat永远实际上就是在跟中间人通信,和Bob也实际上在跟中间人通信,但都以为在跟对方通信。因此,现在的问题就是,Pat如何确认收到的公钥真的是Bob的公钥,而不是别人伪造的! 这个问题,其实可以类比一下现实生活中的问题。我们知道,公钥和私钥是成对存在的,也就是一个人一般都有一对独有的公钥和私钥。就好像我们每个人都有一个独有的身份证,我们把公钥类比为现实中的身份证,当我们在面对一个陌生人的时候,我们为了信任对方,一般可以查看对方的身份证,但此时就存在一个和上面中间人问题一样的漏洞,就是万一对方给的身份证是一个假的伪造的身份证呢?也就是万一对方给的是一个假的公钥呢?我们怎么识别真伪?现实中,我们往往会有一个身份证真伪的识别器,一般公安局等机构会有,也就是我们可以利用身份证真伪的识别器确认这个身份证的真假。我们仔细思考这个机制,实际上就是引入了一个独立的第三方机制,国家作为一个独立的第三方,给我们每个人创建了一个身份证,当我们需要验证身份证的真伪的时候,我们只需要找这个独立的第三方提供的真伪鉴别服务就可以验证身份证的真伪。 所以,相似的我们解决公钥分发问题的思路也就是引入一个独立的权威的第三方机构。 假设现在有一个数字证书的权威认证中心,这个中心会给Bob创建一个数字证书,这个数字证书包括了Bob的一些信息以及Bob的公钥。 那么,此时,想要跟Bob进行通信的人,就可以检查Bob的数字证书,然后向权威的数字证书的认证中心,去认证这是不是真实的Bob的数字证书,如果是,就可以从数字证书中获取到Bob的公钥,然后进行安全的通信。同时,就像现实生活中一样,我们不管进行任何涉及到资金或者安全问题的时候,都需要出示自己的身份证,并且对方会验证你的身份证的真假,也就是说,一个持有假身份证的人,或者没有身份信息的人是获取不了他人的信任的。同理,在网络通信中,如果在数字证书的认证中心中查询不到信息,那么就说明这样的通信方是不安全的,是不值得信任的!另一方面,数字证书除了解决了公钥分发和身份认证的问题,还加强了安全性。 详细的利用数字证书的通信过程如下: Bob想要和Pat进行通信,首先就要告知Pat自己的公钥,Bob先向Pat发送自己的数字证书,Pat收到数字证书后,会向权威的数字证书认证中心进行认证,确认是否是Bob的数字证书。(这个认证的过程,实际上也是通过公钥和私钥的机制,Pat会根据数字证书的类别,查找发布这个数字证书的中心的公钥,然后用相应的公钥对证书进行家解码,如果能正确解码则说明这个数字证书确实是此中心颁布的,然后根据解码后的信息验证是否是Bob的数字证书,最后从解码后的信息中,获取Bob的公钥)。然后,Pat可以利用Bob的公钥对Bob的数字签名进行解码,验证是否是Bob的数字签名,如果能正确解码,就说明数字签名是由Bob的私钥进行加密的。然后就进行完整性的验证,将信息原文进行hash,得到信息摘要,并与数字签名解码后得到的信息摘要进行对比,如果一致,就说明信息是完整的没有被篡改的! 上述的公钥分发和数字签名验证的过程似乎很复杂,但实际上,就跟我们验证身份证真伪一样,我们通常有一个识别器,只要将身份证放上去就可以得到结果,后面的实际过程往往不需要我们关心,网络通信中也是如此,往往会提供一个友好的用户接口,想要验证数字签名或者数字证书,其实就类似于我们点击一下按钮一样简单!]]></content>
<categories>
<category>网络</category>
</categories>
<tags>
<tag>网络</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入解析Backpropagation反向传播算法]]></title>
<url>%2F2018%2F01%2F26%2F%E6%B7%B1%E5%85%A5%E8%A7%A3%E6%9E%90Backpropagation%E5%8F%8D%E5%90%91%E4%BC%A0%E6%92%AD%E7%AE%97%E6%B3%95%2F</url>
<content type="text"><![CDATA[Backpropagation反向传播算法是神经网络理论中的最基本的算法,也是神经网络能够自主学习的根本原理,也就是神经网络的“智能”所在!所以掌握Backpropagation反向传播算法是极其重要的!能帮助我们更好的理解神经网络的本质!然而相关资料却晦涩难懂,或者是没有讲到Backpropagation反向传播算法的本质! 其实,Backpropagation反向传播算法的本质就是链式法则 和 动态规划!!! 为了更好的理解Further into Backpropagation的本质,我更新了五篇相关的文章,循序渐进,从函数的微分开始,讲到链式法则,然后引入动态规划优化反向传播算法,从单个神经元结构的反向传播计算,到嵌套多个神经元结构的反向传播计算,然后扩展到复杂函数的浅谈神经元结构的计算,从两层神经网络结构的反向传播,到多层神经网络的反向传播,并应用动态规划进行规划! Backpropagation反向传播算法系列文章: Towards-Backpropagation Into-Backpropagation Let’s practice Backpropagation Further into Backpropagation Surpass Backpropagation 文章相关代码可于Backpropagation下载]]></content>
<categories>
<category>机器学习</category>
</categories>
<tags>
<tag>机器学习</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Surpass-Backpropagation]]></title>
<url>%2F2018%2F01%2F26%2FSurpass-Backpropagation%2F</url>
<content type="text"><![CDATA[本文相关代码可以从Backpropagation下载 上篇文章Further into Backpropagation中,我们小试牛刀,将反向传播算法运用到了一个两层的神经网络结构中!然后往往实际中的神经网络拥有3层甚至更多层的结构,我们接下来就已一个三层的神经网络结构为例,分析如何运用动态规划来优化反向传播时微分的计算! Lets get started!!!如下的网络结构: 在正式分析神经网络之前,我们先修改一下权重矩阵的表示形式! 让我们以一个符号开始,它代表网络中任意方式的权重信息。我们将使用 来表示从网络第(l−1)层中第k个神经元指向l层中第j个神经元的连接权重。因此举个例子,下图中的权重就表示从第二层中第四个神经元指向第三层中第二个神经元的权重: 这个符号起始比较麻烦,的确需要一些努力才能掌握。但是通过努力你会发现它将变得简单和自然。符号中的一个不容易接受的地方就是j和k的位置关系。你可能认为用j来表示输入神经元,k表示输出神经元,而不是实际定义中反过来的方式。我将在下面解释这样做法的原因。 我将使用相似的符号来表示网络中的偏差和激活。明确地,我们使用blj来表示第l层中第j神经元的偏差,用alj来表示第l层中第j神经元的激活。下面的图将展示这些符号: 有了这些符号,第l层中第j神经元的激活alj就与第(l−1)层中所有激活相关。 能够用漂亮而简洁的向量格式进行重写 这个表达式能给我们更多的启发,某一层的激活与上一层的激活是有什么关系:我们只是将权重矩阵应用到激活上,然后再加上一个偏差向量,最后应用σ函数!顺便说一下,这个表达式诱发了前面提到的wljk符号。如果我们使用j来指示输入神经元,k来指示输出神经元,那么我们就需要替换表达式中的权重矩阵 用权重矩阵的转置。虽然这是一个很小的变化,但是烦人的是,我们将失去解释和思考的简单性“应用权重矩阵到激活上。这个全局视图非常简单和简洁(使用了很少的下标),相对于一个神经元到一个神经元的方式。也可以将其想象成一种避免下标混乱,而且还能保持精确的方法。这个表达式在实际中非常有用,因为许多矩阵库都能提供快速的矩阵乘法,向量加法和向量化。 我们间接的计算 这个值非常有用,我们将zl命名为:网络l层的加权输入。我们将在本章大量的使用加权输入zl。 Hadamard乘积s⊙t后向传播算法是基于通用的线性代数运算——就像向量加法,矩阵乘向量等等。但是有一个操作平常很少用到。特别的,假设s和t是相同维数的两个向量,那么我们使用s⊙t来表示两个向量元素级的乘法。 这种元素级的乘法有时叫做 Hadamard乘积或者Schur乘积。我们将把它叫做Hadamard乘积。好的矩阵库一般都能提供Hadamard乘积的快速实施,因此在实施后向传播时候就非常方便。 # 根据前面多篇文章所学,我们如果要写出第l层j个神经元的加权输入的微分应该不难,就是链式法则求导,如下: 这里我们假设l层是最后一层,那么此时求出了关于加权输入的微分,就可以继续微分求取关于权重的微分根据 所以,不难求出关于权重的微分 好,现在问题来了,如果我们再往前,求倒数第二层的某个权重,思路也是一致的,也就是要从最后一层一直往回算,为了避免公式太长,我们先求关于第l-1层第j个神经元的加权输入的微分 然后再根据 求取权重的微分! 可以看到,只是倒数第二层而已,我们的求取的公式就已经很长了,如果再有个几层,估计就已经爆炸了! 这个时候,就轮到动态规划出场了,动态规划就是在递归的过程中,保存已有的结果,下次计算的时候就不用再算了,可以直接从内存中红取结果。那么我们如何用在这里呢? 仔细观察第l层和l-1层的权重计算公式,我们不难发现,计算第l-1层要经过l层,而在第l-1层中 我们发现前面两个微分 不就是第l层的关于加权输入的微分么?也就是说,我们在第l层的权重的微分的计算的时候,就已经计算过这个了,然后在第l-1层的计算的时候还要用到这个。所以我们可以考虑,在第l层计算权重的微分的时候,就把这个值保存下来,这样在后续计算的时候就可以直接用了,这就是动态规划的思想! 我们保存每一层的 我们给这个值取了个名字叫敏感度矩阵,或者误差矩阵! 然后我们根据误差矩阵来进行反向传播,首先不难求出误差矩阵的初始值,也就是最后一层的误差矩阵,前面我们已经计算过,直接替换就行 然后就是关键的动态规划的递推式,这个其实我们也已经在前面求解出来了,参照前面已经分析得出的 会发现,相邻两层间误差矩阵的关系就是激活函数的微分和权重,我们将其简化成向量的形式就是 具体的证明如下,也就是链式法则的运用:首先,然后,得到,代入, 至此,一个完美的反向传播算法基本上已经大功告成了!还差最后一丢丢,就是已经有了每一层的敏感度矩阵,也就是每一层关于加权输入的微分,最后再计算我们需要的每一层关于权重和偏置的微分,自然也是手到擒来,直接利用微分求导:关于权重, 关于偏置, 后向传播算法后向传播等式给我们提供了一种计算代价函数梯度的方法。让我们用算法显示的写出它们: 输入x:为输入层设置对应的激活a1。 向前反馈:对于每一层l=1,2,3…,计算和 输出层误差δL:计算向量 后向传播误差:对每一层l=L-1,L-2,…2计算 输出:代价函数的梯度为 检查这个算法,你能看到为什么它被叫作后向传播。我们从最末一层开始向后计算各层误差δl。看起来将网络向后传播很奇怪。但是如果你再想一下后向传播的证明,向后移动就是因为代价是网络输出的函数。为了理解代价是如何跟随早期的权重和偏差进行的改变,我们需要不断的应用链式规则,向后来获取有用的表达式。 随机梯度下降的后向传播算法就像我上面描述那样,后向传播算法计算了某一个样本的代价函数的梯度C=Cx。实际上,通用的方式是将后向传播算法与随机梯度下降算法合并,我们便能计算许多训练样本的梯度。特别的,给出一小批训练样本mm,下面算法将基于小批训练样本进行梯度下降学习步骤: 输入训练样本集合 对于每一个训练样本x: 设置对应的输入激活,执行以下步骤(参照前文的后向传播算法): 向前反馈:对于每一层l=1,2,3…,计算和 输出层误差δL:计算向量 后向传播误差:对每一层l=L-1,L-2,…2计算 梯度下降:按照更新法则更新权重和偏置 当然,为了实施随机梯度下降,你也许要一个外部循环来产生小批量的训练样本,还需要一个外部循环来实施更多的训练代。我们将为了简化暂且忽略掉这些。 后向传播的实施代码抽象上理解了后向传播算法,我们就能根据以上算法,实现一个完整的神经网络的后向传播的算法了! 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149# %load network.py"""network.py~~~~~~~~~~IT WORKSA module to implement the stochastic gradient descent learningalgorithm for a feedforward neural network. Gradients are calculatedusing backpropagation. Note that I have focused on making the codesimple, easily readable, and easily modifiable. It is not optimized,and omits many desirable features."""#### Libraries# Standard libraryimport random# Third-party librariesimport numpy as npclass Network(object): def __init__(self, sizes): """The list ``sizes`` contains the number of neurons in the respective layers of the network. For example, if the list was [2, 3, 1] then it would be a three-layer network, with the first layer containing 2 neurons, the second layer 3 neurons, and the third layer 1 neuron. The biases and weights for the network are initialized randomly, using a Gaussian distribution with mean 0, and variance 1. Note that the first layer is assumed to be an input layer, and by convention we won't set any biases for those neurons, since biases are only ever used in computing the outputs from later layers.""" self.num_layers = len(sizes) self.sizes = sizes self.biases = [np.random.randn(y, 1) for y in sizes[1:]] self.weights = [np.random.randn(y, x) for x, y in zip(sizes[:-1], sizes[1:])] def feedforward(self, a): """Return the output of the network if ``a`` is input.""" for b, w in zip(self.biases, self.weights): a = sigmoid(np.dot(w, a)+b) return a def SGD(self, training_data, epochs, mini_batch_size, eta, test_data=None): """Train the neural network using mini-batch stochastic gradient descent. The ``training_data`` is a list of tuples ``(x, y)`` representing the training inputs and the desired outputs. The other non-optional parameters are self-explanatory. If ``test_data`` is provided then the network will be evaluated against the test data after each epoch, and partial progress printed out. This is useful for tracking progress, but slows things down substantially.""" training_data = list(training_data) n = len(training_data) if test_data: test_data = list(test_data) n_test = len(test_data) for j in range(epochs): random.shuffle(training_data) mini_batches = [ training_data[k:k+mini_batch_size] for k in range(0, n, mini_batch_size)] for mini_batch in mini_batches: self.update_mini_batch(mini_batch, eta) if test_data: print("Epoch {} : {} / {}".format(j,self.evaluate(test_data),n_test)); else: print("Epoch {} complete".format(j)) def update_mini_batch(self, mini_batch, eta): """Update the network's weights and biases by applying gradient descent using backpropagation to a single mini batch. The ``mini_batch`` is a list of tuples ``(x, y)``, and ``eta`` is the learning rate.""" nabla_b = [np.zeros(b.shape) for b in self.biases] nabla_w = [np.zeros(w.shape) for w in self.weights] for x, y in mini_batch: delta_nabla_b, delta_nabla_w = self.backprop(x, y) nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)] nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)] self.weights = [w-(eta/len(mini_batch))*nw for w, nw in zip(self.weights, nabla_w)] self.biases = [b-(eta/len(mini_batch))*nb for b, nb in zip(self.biases, nabla_b)] def backprop(self, x, y): """Return a tuple ``(nabla_b, nabla_w)`` representing the gradient for the cost function C_x. ``nabla_b`` and ``nabla_w`` are layer-by-layer lists of numpy arrays, similar to ``self.biases`` and ``self.weights``.""" nabla_b = [np.zeros(b.shape) for b in self.biases] nabla_w = [np.zeros(w.shape) for w in self.weights] # feedforward activation = x activations = [x] # list to store all the activations, layer by layer zs = [] # list to store all the z vectors, layer by layer for b, w in zip(self.biases, self.weights): z = np.dot(w, activation)+b zs.append(z) activation = sigmoid(z) activations.append(activation) # backward pass delta = self.cost_derivative(activations[-1], y) * \ sigmoid_prime(zs[-1]) nabla_b[-1] = delta nabla_w[-1] = np.dot(delta, activations[-2].transpose()) # Note that the variable l in the loop below is used a little # differently to the notation in Chapter 2 of the book. Here, # l = 1 means the last layer of neurons, l = 2 is the # second-last layer, and so on. It's a renumbering of the # scheme in the book, used here to take advantage of the fact # that Python can use negative indices in lists. for l in range(2, self.num_layers): z = zs[-l] sp = sigmoid_prime(z) delta = np.dot(self.weights[-l+1].transpose(), delta) * sp nabla_b[-l] = delta nabla_w[-l] = np.dot(delta, activations[-l-1].transpose()) return (nabla_b, nabla_w) def evaluate(self, test_data): """Return the number of test inputs for which the neural network outputs the correct result. Note that the neural network's output is assumed to be the index of whichever neuron in the final layer has the highest activation.""" test_results = [(np.argmax(self.feedforward(x)), y) for (x, y) in test_data] return sum(int(x == y) for (x, y) in test_results) def cost_derivative(self, output_activations, y): """Return the vector of partial derivatives \partial C_x / \partial a for the output activations.""" return (output_activations-y)#### Miscellaneous functionsdef sigmoid(z): """The sigmoid function.""" return 1.0/(1.0+np.exp(-z))def sigmoid_prime(z): """Derivative of the sigmoid function.""" return sigmoid(z)*(1-sigmoid(z)) 以上代码实现了一个完整的神经网络的类,里面包括前向传播,结合小批量随机梯度法实现的后向传播,可以直接应用于神经网络问题的求解! 写在最后终于,我们从微分开始,讲到链式法则,从单个简单的神经元到嵌套神经元,再到两层的神经网络,最后到多层的神经网络,从微分结合链式法则的暴力进行反向传播的计算,到引入动态规划的计算,引入敏感度函数,真正理解了神经网络的反向传播算法!希望能对读者理解神经网络的反向传播有一定的帮助 Further reading How the backpropagation algorithm works. A_Gentle_Introduction_to_Backpropagation. jasdeep06 本文相关代码可以从Backpropagation下载]]></content>
<categories>
<category>机器学习</category>
</categories>
<tags>
<tag>机器学习</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Further-into-Backpropagation]]></title>
<url>%2F2018%2F01%2F26%2FFurther-into-Backpropagation%2F</url>
<content type="text"><![CDATA[本文相关代码可以从Backpropagation下载 在上一篇文章Let’s practice Backpropagation,我们计算了一个带sigmoid函数的嵌套网络的反向传播!从这篇文章开始,我们正式进入实际的神经网络的反向传播!本文将以一个两层的神经网络结构为例子,并且利用矩阵的方法实现神经网络的反向传播训练算法! Lets get started!!!神经网络的结构如下: 上图的神经网络包括两层网络。第一层是输入层,包括三个神经元,第二层也就是输出层,包括了两个神经元。标准的神经网络中,sigmoid层也就是激活函数,是输出层的一部分,这里为了反向传播时计算微分更直观,就将其分开! 对于不了解基本神经网络的同学可以参考,对于不了解激活函数的同学可以参考‘神经网络’初探 我们下面来分析这个神经网络。首先,三个输入值被输入到输入层的三个节点中,因此我们的输入,用矩阵表示,应该是三维的。然后输入层将和各自的权重相乘,得到输出层,这里和权重的相乘,可以简化成矩阵的乘法运算。然后再输入到sigmoid函数中,进行激活计算,得到一个0-1之间的输出值。最后输出到cost function中,进行误差的计算,这里的cost function可以选取不同的计算函数,这里我们用交叉熵函数作为代价函数, cross-entropy 。单纯对于研究反向传播来说,我们都可以不需要知道这些一层层的函数是干嘛的,因为我们反向传播要求的只是微分而已。只要这些函数是可微的,不管结构在复杂,无非是链式求导的时候多求几个微分而已!反向传播的本质就是在微分的计算! Aim误差当然是越小越好,所以我们训练网络的目标是将cost function的值减小,这和我们之前几篇文章将输出结果增加正好是相反的,其实也很简单,只需要在更新的时候,减去步长和微分的乘积就行,将之前的+变成-!具体可以参考梯度下降法这里我们要更新的是权重的值,所以更新的方法如下: 这里的Wij代表,第i个输入节点到第j的输出节点的权重!只需要求出costfunction关于每个权重的微分即可! 首先,我们自然要先进行正向传播,也就是正向计算最后的输出cost function! Forward Propagation首先,我们将输入矩阵化,就是一个1*3的矩阵: 如果我们有多个样本的输入值,比如有n个输入,那么输入矩阵就可以写成n*3的矩阵! 权重矩阵如下: 然后输入层到输出层的计算,就可以简化成,两个矩阵的相乘: 正好得到一个 1x2 的矩阵,对应输出层的两个神经元,符合我们的预期,然后我们给第一个输出层的神经元标记y1,给第二个神经元标记为y2。 然后再进行激活函数的计算 Cost function得到输出层的输出并进行激活函数计算之后,就要输入到cost function中计算errors!。这里我们采用的交叉熵代价函数, cross-entropy 这里p是预期的值,q是我们经过神经网络计算得到的预测值,具体交叉熵函数的意义,可以参考 cross-entropy而我们只要知道我们要将C的值降低,利用反向传播算法,降低C的输出,所以我们就要求得C的微分,首先我们把C展开: 然后将我们网络中计算得到的输出层的输出带入进去: 这样我们就分析完了怎么进行这个神经网络的正向传播! Backpropagation反向传播之前,我们先回顾一下,每一层的输出结果 激活函数层的输出结果 输出层的输出结果 权重矩阵 输入层 明确了每层的值之后,我们要切记,我们反向传播所需的就是关于权重的微分,也就是 也就是我们要想办法求出C关于各个权重的微分!求微分的基本思路和之前是一样的,不管网络的结构多复杂,根本都是利用链式法则,一层层的从输出求导到输入!这里,我们会采取矩阵的算法来进行微分的求解,这可以让我们的求解方法更适合于编写程序,并且更直观! 首先我们看输出C是关于sigmoid层的输出y0的函数,然后y0又是关于输出层的输出y的函数,y同时又是输入层x与权重相乘而得来的。所以,基本就明确了,我们需要先求取C关于y0的微分,再求取y0关于y的微分,然后求取y关于w的微分,最后又链式法则相乘在一起,就得到了C关于w的微分! 首先从cost function到sigmoid layer 我们可以很容易写出微分: 写成矩阵的形式 从sigmoid层到输出层的微分,就是求取sigmoid函数的微分 变成矩阵的形式就是: 从输出层到输入层的微分就是关于权重的微分,我们先看y关于权重的形式 从这个形式不难得出关于权重的微分就是: 这样我们就可以运用链式法则,求取C关于权重W的微分了: 将每个微分的值带入: 将六个微分全部求取出来就是: 不难写成矩阵的形式: 这里T代表矩阵的转置,X代表矩阵的乘法,圆圈加点代表矩阵对应元素相乘,也就是element-wise product。 最后,我们就可以得到完整的权重更新的法则: 根据以上计算出的更新法则,编写python代码就很直观了 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748import numpy as npdef sigmoid(x): return 1/(1+np.exp(-x))def derivative_sigmoid(x): return np.multiply(sigmoid(x), (1-sigmoid(x)))# initialization# X : 1*3X = np.matrix("2, 4, -2")# W : 3*2W = np.random.normal(size=(3, 2))# labelycap = [0]# number of training of examplesnum_examples = 1# step sizeh = 0.01# forward-propogationy = np.dot(X, W)y_o = sigmoid(y)# loss calculationloss = -np.sum(np.log(y_o[range(num_examples), ycap]))print(loss) # outputs 3.6821105514(for you it would be different due to random initialization of weights.)# backprop startstemp1 = np.copy(y_o)# implementation of derivative of cost function with respect to y_otemp1[range(num_examples), ycap] = 1 / -(temp1[range(num_examples), ycap])temp = np.zeros_like(y_o)temp[range(num_examples), ycap] = 1# derivative of cost with respect to y_odcost = np.multiply(temp, temp1)# derivative of y_o with respect to ydy_o = derivative_sigmoid(y)# element-wise multiplicationdgrad = np.multiply(dcost, dy_o)dw = np.dot(X.T, dgrad)# weight-updateW -= h * dw# forward prop again with updated weight to find new lossy = np.dot(X, W)yo = sigmoid(y)loss = -np.sum(np.log(yo[range(num_examples), ycap]))print(loss) # 3.45476397276 outpus (again for you it would be different!) 运行程序,就会看到,进行反向传播,C的值也就是代价函数减少了!(由于初始权重是随机生成的,所以每次运行结果就不尽相同,但可以确定的,反向传播后的输出结果相对之前一定是减小的) 待续这篇文章将会在此结束!我们已经成功将反向传播的计算扩展到真实的两层的神经网络中,并且将计算过程矩阵化!下一篇Surpass Backpropagation就是反向传播算法的终结篇,将会实现一个多层的神经网络的反向传播,并且运用动态规划算法对反向传播中微分的计算进行优化! 本文相关代码可以从Backpropagation下载]]></content>
<categories>
<category>机器学习</category>
</categories>
<tags>
<tag>机器学习</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Practice-Backpropagation]]></title>
<url>%2F2018%2F01%2F26%2FPractice-Backpropagation%2F</url>
<content type="text"><![CDATA[本文相关代码可以从Backpropagation下载 在上一篇文章Into-Backpropagation,我们研究了一个嵌套神经元的反向传播的计算,了解到反向传播本质就是利用链式法则,求取所需要更新的变量的偏导数!但我们前文所研究的神经元是比较简单的,没有复杂的函数,也没有复杂的结构,而真实的神经网络中,往往神经元的函数和结构都比较复杂! 为了更好的过渡到复杂的神经网络中的反向传播,本文先引入复杂函数,也就是神经网络中最基本的激活函数,并联系如何计算反向传播,为后续进入神经网络的反向传播计算打下坚实的基础! Lets get started!!!我们将引入神经网络最常见的激活函数sigmoid函数! 实现这个单一神经元很简单123456import numpy as npdef sigmoid(x): return 1/(1+np.exp(-x))a=-2f=sigmoid(a)print(f) #outputs 0.1192 Aim接下来依旧是老套路,我们是=试着使输出值增加。首先我们 就要计算Sigmoid的函数的导数,根据微分的法则,我们可以求出 然后,就可以得到更新变量的方程: 我们用python实现:12345678910111213141516import numpy as npdef sigmoid(x): return 1./(1+np.exp(-x))def derivative_sigmoid(x): return sigmoid(x) * (1 - sigmoid(x))a = -2h = 0.1a = a + h * derivative_sigmoid(a)f = sigmoid(a)print(f) #outputs 0.1203 观察输出结果,0.1203比0.1192大.所以我们的算法成功将输出值增加! 现在我们已经知道如何对一个复杂的函数的神经元进行反向传播,从而改变输出值!那么,接下来我们就将复杂函数放到一个嵌套的神经网络结构中,看看如何进行反向传播的计算: 这个神经网络的结构就是在前文的基础上增加了一个sigmoid函数!我们先用python实现它的正向传播12345678910111213141516171819202122import numpy as npdef addition(x,y): return x+ydef product(x, y): return x * ydef sigmoid(x): return 1 / (1 + np.exp( -x ))a=1b=-2c=-3d=addition(a,b)e=product(c,d)f=sigmoid(e)print(f) #outputs 0.952574 现在我们开始计算反向传播,首先很明确的是,要进行反向传播,就得求得所要更新变量的微分: 所以我们需要的计算就是a,b,c三个变量的偏导数!具体的求解规则和前文一样就是倒着从输出往回推,看看经过了哪些神经元的计算,然后利用链式法则: 希望读者能独立推导出上述的公式! 得到上述微分的计算公式,我们就要开始实际计算这些微分值,不难求出 如果读者对此推导过程依旧有疑问,请重新阅读前两篇文章即能理解! 最后,就是编写程序来实现反向传播了! 12345678910111213141516171819202122232425262728293031323334353637383940414243444546import numpy as npdef addition(x, y): return x + ydef product(x, y): return x * ydef sigmoid(x): return 1 / (1 + np.exp(-x))def derivative_sigmoid(x): return sigmoid(x) * (1 - sigmoid(x))# initializationa = 1b = -2c = -3# forward-propogationd = addition(a, b)e = product(c, d)# step sizeh = 0.1# derivativesderivative_f_e = derivative_sigmoid(e)derivative_e_d = cderivative_e_c = dderivative_d_a = 1derivative_d_b = 1# backward-propogation (Chain rule)derivative_f_a = derivative_f_e * derivative_e_d * derivative_d_aderivative_f_b = derivative_f_e * derivative_e_d * derivative_d_bderivative_f_c = derivative_f_e * derivative_e_c# update-parametersa = a + h * derivative_f_ab = b + h * derivative_f_bc = c + h * derivative_f_cd = addition(a, b)e = product(c, d)f = sigmoid(e)print(f) # prints 0.9563 输出结果是0.9563比0.9525大,可以看到,经过一次反向传播,我们的输出值成功增加! 经过练习,我们可以发现,不管网络多复杂,无非是链式法则求导是复杂一些,只要我们能求出微分,就能进行反向传播! 待续我们目前练习的都还是比较简单的网络,但恭喜你已经了解到反向传播的最核心的思想!下一篇文章Further into Backpropagation,我们会正式引入一个真实的神经网络结构,然后进行反向传播的计算!并且利用矩阵来简化计算过程! 本文相关代码可以从Backpropagation下载]]></content>
<categories>
<category>机器学习</category>
</categories>
<tags>
<tag>机器学习</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Into-Backpropagation]]></title>
<url>%2F2018%2F01%2F26%2FInto-Backpropagation%2F</url>
<content type="text"><![CDATA[本文相关代码可以从Backpropagation下载 在上一篇文章Towards-Backpropagation,我们学习了如何利用函数的微分来更新变量值,是函数值发生相应的变化! 例如,对于函数我们想要更新变量a,b的值使f的值增加,就可以根据以下公式来更新 实际上这就是反向传播的最基本的思想!我们试想假设f函数是一个代价函数,神经网络的训练就是将代价函数的值变小,那么就是问题就变成了,对于一个代价函数f,我们将改变f的变量,使其f能减小,而f不就是关于每个神经元权重和偏置的函数么,12345678910111213141516171819202122232425接下来,我们就将问题慢慢复杂化,一步一步接近最终的神经网络中的反向传播!前文中,我们利用的是一个神经元,这里我们讲问题变复杂,变成两个神经元,并且是有嵌套关系的两个神经元!如下图:![image.png](http://upload-images.jianshu.io/upload_images/1234352-273f5828e57e5a23.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240),将输入值相加然后输出到第二个神经元,同时第二个神经元还接受输入c,并将两个值相乘,最后输出!这个简单网络的正向传播很容易写出来:```pythondef product(x, y): return x * ydef addition(x, y): return x + ydef forward(a, b, c): d = addition(a, b) return product(d, c)print(forward(5, -6, 7)) Our aim is still the same as was in last post viz;we want to manipulate the values of our inputs a,b,c in such a way that the value of output fincreases. 现在开始我们的训练吧!目标和之前一样,就是改变输入的a,b,c三个值,使函数f的值增加!之后我们就会发现,在这个过程中,我们会慢慢接触到反向传播的核心思想!previous post. 初看上去,这个网络似乎比之前的要复杂,但依照我们前一篇文章提出的思路!我们先看函数f,函数f是一个关于输入a,b,c的函数,想要让函数f的值增加,直接微分即可,然后加上步长与微分的乘积,如下: 所以,核心问题在此就变成怎么求解函数f关于a,b,c的微分!我们不能向之前一个神经元那样直接计算,因为此处的神经元是相互嵌套的。我们将函数f反着往回写!首先,与函数f直接关联的神经元,就是接受两个输入,一个来自第一个神经元的,一个来自输入c,我们把第一个神经元的输出记作d,那么函数f就可以写成 然后我们继续反着往前推,对于d,其实就是第一个神经元的输出,也就是可以直接写成: 这样我们就反向的把函数f简化成了两个函数: 熟悉微积分的朋友应该就知道,我们在此可以利用函数求微分的链式法则。首先考虑 分别求微分之后,如下: 然后再对求微分 现在我们有四个微分的值: 我们的目标是求取这三个微分的值: Backpropagation这个时候,就轮到链式法则出场了!链式法则其实就是: 如我们所见,链式法则有点代数的特点;因为莱布尼兹的导数符号表明两个分式中的du可以消掉,所以这个公式很好记忆。如果我们将导数看作变化率的话,直观上也很容易理解: 如果y的变化速度是u的a倍,u的变化速度是x的b倍,那么y的变化速度是x的ab倍。 或者用日常用语来说,如果车的速度是自行车的两倍,自行车的速度是步行的四倍,那么车的速度是步行的2⋅4=8倍。 所以此处我们对a,b应用链式法则,就能求取出微分: 所以最后求出微分就是: 根据以上求出的微分,我们就能很好的写出变量的更新规则: 我们用python实现上面的更新的过程:12345678910111213141516171819202122232425262728293031323334353637def product(x, y): return x * ydef addition(x, y): return x + ydef forward(a, b, c): d = addition(a, b) return product(d, c)print(forward(5, -6, 7))# output -7def update(a, b, c): d = addition(a, b) h = 0.01 derivative_f_d = c derivative_f_c = d derivative_d_a = 1 derivative_d_b = 1 derivative_f_a = derivative_f_d * derivative_d_a derivative_f_b = derivative_f_d * derivative_d_b a = a + h * derivative_f_a b = b + h * derivative_f_b c = c + h * derivative_f_c d = addition(a, b) return product(d, c)print(update(5, -6, 7))# output -6.0113999999999965 可以看到更新之后的输出确实增大了!说明我们现在已经可以实现嵌套神经元的变量参数的更新了!离真正的反向传播的又近了一步! Why did it work? 接下来,我们深入整个更新的过程,一步步分析看看究竟更新的本质是什么。首先,我们从输入到输出,分析一遍,前向传播,输入值为a=5,b=-6,c=7,很容易发现,第一个神经元的输出为d为1,然后第二个神经元输出为-7,所以最后结果就是-7!这就是此网络前向传播的过程! 然后我们开始反向传播,从输出开始分析,我们现在的目标是将输出的值增大,输出值是由-1*7得到的,现在要增加输出值,我们先不看微分,显然就是增加-1,减少7,这样就能使他们的乘积变大!我们再来计算微分,微分结果就是增加d的值,减少c的值。我们继续反向推理,这里d的值又是有a,b的值决定的!我们现在的目标又变成了对于第一个神经元,减少输出值,那么很显然,只要减少a和b的值就行了,我们运用链式法则求取a,b的微分,也能得到相同的结果!我们可以看到,当我们反向传播的时候,一个神经元的输入会变成上一个神经元的输出,然后他们之间相互影响,从而使传播下去! 我们倒着从输出分析到输入的过程就是反向传播的过程!我们通过计算微分可以从输出到输入更新变量的值,以使得输出朝着我们期待的方向的变化! 待续本文就在这里结束了!本文将前文的更新变量的算法扩展到嵌套的多个神经元中,并应用到了链式法则求微分!而且在这个过程中,其实我们已经逐渐接触到反向传播的基本思想! 下一篇文章Let’s practice Backpropagation,我们会将算法应用到一个标准的神经网络中,让我们看看真正的反向传播算法是什么样的! 本文相关代码可以从Backpropagation下载]]></content>
<categories>
<category>机器学习</category>
</categories>
<tags>
<tag>机器学习</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Towards-Backpropagation]]></title>
<url>%2F2018%2F01%2F26%2FTowards-Backpropagation%2F</url>
<content type="text"><![CDATA[本文相关代码可以从Backpropagation下载 想要理解backpropagation反向传播算法,就必须先理解微分!本文会以一个简单的神经元的例子来讲解backpropagation反向传播算法中的微分的概念。 这是一个非常简单的神经元,如图所示,接收两个输入a, b,然后在神经元内部对两个神经元进行相乘操作,得到a*b的输出! 然后,我们可以想象这样一个情景,现在对于某个特定的输入a,b,输出结果与预期结果相比较小,为了提高准确率,那我们就要将输出结果增加,以更符合预期的结果! 所以,我们现在的目标就是通过改变a,b的值来增加神经元的输出! Method 1第一个方法是很直观的,我们就随机的给输入a,b的两个值添加一个随机值,然后用一个步长step来控制增加的程度,用python代码实现: 12345678910111213141516import randomdef product(a, b): return a * bdef methodOne(): a = 5 b = 6 step = 0.01 a = a + step * (random.random()) b = b + step * (random.random()) print(product(a, b))methodOne() 运行程序,我们可以得到输出30.04698146865633(由于涉及到随机数,所以读者的运行结果会和此处有区别,但一定会是比30大的数),是比原本的输出30增加了相应值的,说明我们的目标实现了!但是如果我们继续测试,就会发现程序是存在问题的: 不可靠!如果我们改变输入值,会发现有时候值会增加,有时候又会减少!,比如以下这个例子: 123456789def testTwo(): a = -5 b = -6 step = 0.01 a = a + step * (random.random()) b = b + step * (random.random()) print(product(a, b))testTwo() 运行程序,我们会得到结果29.970177554213326,是比原本的输出30小的。只因为我们将输入值a,b变为了负数,这个算法就失效了!究其原因,因为我们对于不同的输入值,变化的都是random()函数,也就是一个在(0,1)之间的正数。而对于负数的输入,增加一个正数,最后反而导致绝对值减小,也就导致输出的结果变小了! 所以,通过以上这个例子,我们可以得出结论: 对于1a = a + step * (random.random()) 中step后面所乘的系数,我们不能一概而论,而是应该更多的控制它的值,也就是根据不同的输入值来控制,也就是说,step所乘的系数,也就是输入值的变化应该是一个关于输入值的函数 Method 2Method 1中的结论是,我们应该有一个step乘以的应该是一个关于输入值的函数。也就是我们需要知道在输入值那个点上,输出结果关于输入值的一个变化率,如果在这个点上,输出结果是随着输入值增加而增加,那么step乘以一个正数即可,反之,则需要乘以一个负数才能使输出结果增加!显然这就是函数在某点上的微分的定义! 首先我们将神经元内部的函数写出来 神经元输出是一个关于输入的函数!然后我们将输入值a增加一个h 此时新的输出值就变成12(a+h)*b```,展开就是 ab+hb1,所以我们可以看到此时输出值相对于原有的输出值的变化就是 h*b1,只要我们保证 h*b1是正值,就是说可以保证是输出结果增加,而保证正值的方法自然就是h和b同号!更深入的理解,我们将a的值增加h,最后导致输出结果,也就是 函数f的值增加 h*b1234567891011121314151617181920212223242526,这时候就可以理解b为函数f在点(a,b)处关于a的变化率!这就是微分的概念了,如果读者有微积分基础,会发现这里实际上f就是一个二元函数的微分,b就是函数f在点(a,b)的微分!微分的实质就是函数在某点的变化率,如果是多元函数就是函数在某点关于某个变量的变化率!多元函数对某一个变量微分时候,通常会将其他变量看作常量!![image.png](http://upload-images.jianshu.io/upload_images/1234352-aa33fce47827bb8e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)以上公式就是微分的计算方法!但实际上,学过微积分,都应该掌握了一套微分的基本法则,我们往往可以根据这套法则,直接写出函数的微分!可以参考[Derivative-rules](https://www.mathsisfun.com/calculus/derivatives-rules.html)对于我们这里的函数f,我们可以直接写出关于a的微分![image.png](http://upload-images.jianshu.io/upload_images/1234352-5915d878aa286509.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)自然,关于b的微分就是![image.png](http://upload-images.jianshu.io/upload_images/1234352-05dcf5a4602d489d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)求出了微分,就相当于知道了变量在某点的变化率,那么很自然,a,b的更新规则就是![image.png](http://upload-images.jianshu.io/upload_images/1234352-96eb7260f937de54.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)```pythondef methodTwo(a, b): step = 0.01 a = a + step * b b = b + step * a print(product(a, b))methodTwo(5, 6)methodTwo(-5, -6) 我们发现输出结果30.616035999999998和30.616035999999998,不管输入的值是正是负,都成功将输出结果增加了!而正是利用了微分的概念才能做到,微分实际上就是梯度,梯度就指明了函数上升最快的方向!读者可以参考笔者相关梯度下降的文章梯度下降 最后我们来详细分析一下,为什么利用微分更新输入可以做到? 我们深入分析微分几何意义!一个函数对某个变量的微分实际上就是函数在关于这个变量的变化率,变化率的方向是正的,也就是说的变化率,指的是增加的变化率!我们看看我们输入的更新方程 多元函数的微分关于某个变量的微分的意义就是,就是将其他变量全部看成常量,函数关于此变量的变化率,变化率的方向是正的,也就是说上升的方向,如果想要知道下降的变化率,即是负微分 我们再举一个单变量函数的例子来分析一下如何利用微分更新变量值 我们看到上面这个函数关于x先上升,再下降!我们假设两个场景 场景1: 首先,想象我们现在处于A点,我们想要改变变量x的值,从而使函数y的值增加。从图中我们可以清晰的看出,我们只要直接增加x的值,就可以增加函数值。然后结合微分,我们的更新方法就是 场景2: 现在想象我们在C点,也要改变x的值让函数值y增加,这时候我们从图像中可以看到,我们减少x的值,就可以增加函数值!而这正是由函数的微分决定的。我们求取函数关于x在此点的微分,会发现微分是负值,所以我们依然利用场景1中的更新公式,也可以达到增加函数值的目的。 微分会反映函数的变化趋势,如果我们想要让函数值增加,微分会告诉我们一个正确的更新变量的方向 场景3: 想象我们此时处于B的左侧,但是非常接近B点!从图像中,我们也会知道此时微分的是正的,也就是我们增加x的值就能使函数值y的值增加!但要注意的一点是,如果我们的step也就是步长如果过大,会导致跑到右边去了,可能还会导致函数值的下降!所以我们就要调整步长!这其实就是一个梯度上升的问题,与梯度下降类似,读者有兴趣可以参考笔者相关文章梯度下降 我会在这里结束本文的介绍!希望通过本文读者能对微分有一个理解,同时知道如何将微分利用到更新变量值中,从而改变函数值! 下一篇文章Into-Backpropagation我会将这个利用微分更新变量的方法,应用到多个神经元的场景中,慢慢读者就会接触到真正的backpropagation反向传播算法 本文相关代码可以从Backpropagation下载]]></content>
<categories>
<category>机器学习</category>
</categories>
<tags>
<tag>机器学习</tag>
</tags>
</entry>
<entry>
<title><![CDATA[初探--神经网络]]></title>
<url>%2F2018%2F01%2F18%2F%E5%88%9D%E6%8E%A2-%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%2F</url>
<content type="text"><![CDATA[感知器 激活函数 神经网络 小结 本文从感知器开始讲起,引入激活函数,最后引出了神经网络的基本概念和思想,希望能帮助读者对神经网络有一个初步的了解! 感知器人工神经网络的第一个里程碑是感知机perceptron, 但感知器本质上是用来决策的。 一个感知机其实是对神经元最基本概念的模拟 ,都未必有多少网络概念,他就是一个自动做决策的机器。 感知器纯粹从数学的角度的上看,其实就可以理解为一个黑盒函数,接受若干个输入,产生一个输出的结果,这个结果就代表了感知器所作出的决策! 举一个简单的例子,假设我们需要判断小明同学是否喜欢一个女生,主要考虑有以下三个因素,女生的颜值(0-10分),女生的身材(0-10分),女生的性格(0-10分),那么对于一个女生我们只需要将这三个因素量化出来,输入到感知器中,然后就能得到感知器给我们决策的结果。而感知器内部决策的原理,其实就是给不同的因素赋予不同的权重,因为不同的因素的重要性对小明来说,自然是不相同的。然后设置一个阈值,如果加权计算之后的结果大于等于这个阈值,就说明可以判断为喜欢,否则则是不喜欢!所以感知器本质上就是一个通过加权计算函数进行决策的工具! 根据上面这个公式,我们可以进一步简化,将阈值移到不等式的一边,并且将其称为偏移,那么所有的问题就统一成了一个‘阈值’为0的问题!偏移的意义其实就是阈值,你可以将偏移想象成使感知器如何更容易输出 1,或者用更加生物学术语,偏移是指衡量感知器触发的难易程度。对于一个大的偏移,感知器更容易输出 1。如果偏移负值很大,那么感知器将很难输出 1。实际应用中的感知器模型往往更加复杂,如下图所示: 激活函数感知器的学习过程就是通过改变感知器内部的权重和偏移,以使其的输出结果符合期望!但我们仔细观察前文的感知器模型,可以发现,每个感知器的输出可以看作是一个阶跃函数只有两种输出结果,要么是0,要么是1问题就出现了,这样的话,感知器似乎就变成了一个离散的函数!,如果我们稍微改变权重或者偏移,得到的结果就是要么不变,要么就感知器的输出彻底相反。而我们原本期望的是,每个感知器都对输出结果有一定的比重的贡献,单个感知器权重或偏移的变化应该是对输出结果产生微小影响的,而不是剧变。另一方面来讲,感知器模型本质上恶意理解为函数的拟合,如果感知器的输出都是离散的二元状态,并且是前文简单的加权形式,也就是线性的,那么只能进行线性的拟合,不具备处理非线性问题的能力!所以这个时候激活函数就出现了,激活函数就是在感知器加权计算之后,再输入到激活函数中进行计算,得到一个输出!我们以常见的激活函数sigmoid函数为例,加入激活函数之后,每个感知器的函数实际上就变成了如下形式我们观察一下,此时感知器函数的图像可以对比前文的阶跃的输出图像,我们将一个离散的输出变为一个连续的非线性的输出结果!同时,单个感知器权重和偏移的细微改变,只会对输出结果产生相应的平滑的影响,而不是阶跃式的影响!跟做人一样的道理,不要太武断,太极端,未加入激活函数的感知器模型,就属于非常极端的,要么0,要么1。而加入激活函数后,会是一个在0~1之间的值。 激活函数的理论解释激活函数是用来加入非线性因素的,解决线性模型所不能解决的问题。假设这么一个情景:我们有这个需求,就是二分类问题,如我要将下面的三角形和圆形点进行正确的分类,如下图:利用我们单层的感知机, 用它可以划出一条线, 把平面分割开:该感知器实现预测的功能步骤如下,就是我已经训练好了一个感知器模型,后面对于要预测的样本点,带入模型中,如果y>0,那么就说明是直线的右侧,也就是正类(我们这里是三角形),如果y<0,那么就说明是直线的左侧,也就是负类(我们这里是圆形 好吧,很容易能够看出,我给出的样本点根本不是线性可分的,一个感知器无论得到的直线怎么动,都不可能完全正确的将三角形与圆形区分出来,那么我们很容易想到用多个感知器来进行组合,以便获得更大的分类问题,好的,下面我们上图,看是否可行:好的,我们已经得到了多感知器分类器了,那么它的分类能力是否强大到能将非线性数据点正确分类开呢~我们来分析一下: 我们能够得到化简后就是 不管它怎么组合,最多就是线性方程的组合,最后得到的分类器本质还是一个线性方程,该处理不了的非线性问题,它还是处理不了。 所以如果没有激活函数,那么感知器模型实际上就是在拟合一个线性方程而已,这样的话,能够解决的问题,自然就是太局限了! 激活函数的作用就出来了,将一个线性的函数变为一个非线性的函数!我们依然以最常用的sigmoid激活函数为例:通过这个激活函数映射之后,输出很明显就是一个非线性函数!能不能解决一开始的非线性分类问题不清楚,但是至少说明有可能啊,上面不加入激活函数神经网络压根就不可能解决这个问题~ 同理,扩展到多个神经元组合的情况时候,表达能力就会更强~对应的组合图如下:(现在已经升级为三个非线性感知器在组合了) 最后再通过最优化损失函数的做法,我们能够学习到不断学习靠近能够正确分类三角形和圆形点的曲线,到底会学到什么曲线,不知道到底具体的样子,也许是下面这个~ 所以到这里为止,我们就解释了这个观点,加入激活函数是用来加入非线性因素的,解决线性模型所不能解决的问题。 神经网络介绍了感知器和激活函数,实际上我们已经将神经网络的基本概念了解的差不多了。将感知器套上激活函数实际上就是神经网络。和感知器模型一样,神经网络的基本单位是神经元,每个神经元分别接受输入和输出,但与感知器不同的是,除了进行加权计算,还需要利用激活函数输出! 假如我们有如下网络: 就像先前说的,网络的最左边一层被称为输入层,其中的神经元被称为输入神经元。最右边及输出层包含输出神经元,在这个例子中,只有一个单一的输出神经元。中间层被称为隐含层,因为里面的神经元既不是输入也不是输出。“隐含”这个术语可能听起来很神秘——当我第一次听到时候觉得一定有深层的哲学或者数学意义——但实际上它只表示“不是输入和输出”而已。上面的网络只包含了唯一个隐含层,但是一些网络可能有多层。比如,下面的4层网络具有2个隐含层: 神经网络的基本思想就是建立在感知器和激活函数上的。对于多个输入,在神经网络经过多个神经元计算之后,得到多个或者单个输出。检查输出结果是否与期望的一致,如果不一致,就对神经网络中神经元的权重进行调整,我们已经知道,神经元权重的细微调整会引起输出结果的细微变化,这样多个神经元组合起来,逐渐调整,直到符合预期的输出结果,我们就可以认为神经网络训练成功了!这里所说的训练调整的方法,利用到了梯度下降法,对神经网络进行反向传播,我们将在后续的文章进行详细的介绍! 小结本文从感知器模型开始,继而引入激活函数,最后引出了神经网络的基本结构和思想,后续将会详细介绍神经网络自主学习的原理! Further reading Neural Networks and Deep Learning Why use activation functions]]></content>
<categories>
<category>机器学习</category>
</categories>
<tags>
<tag>机器学习</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入浅出--梯度下降法及其实现]]></title>
<url>%2F2018%2F01%2F17%2F%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA-%E6%A2%AF%E5%BA%A6%E4%B8%8B%E9%99%8D%E6%B3%95%E5%8F%8A%E5%85%B6%E5%AE%9E%E7%8E%B0%2F</url>
<content type="text"><![CDATA[梯度下降的场景假设 梯度 梯度下降算法的数学解释 梯度下降算法的实例 梯度下降算法的实现 Further reading 本文将从一个下山的场景开始,先提出梯度下降算法的基本思想,进而从数学上解释梯度下降算法的原理,最后实现一个简单的梯度下降算法的实例! 梯度下降的场景假设 梯度下降法的基本思想可以类比为一个下山的过程。假设这样一个场景:一个人被困在山上,需要从山上下来(i.e. 找到山的最低点,也就是山谷)。但此时山上的浓雾很大,导致可视度很低。因此,下山的路径就无法确定,他必须利用自己周围的信息去找到下山的路径。这个时候,他就可以利用梯度下降算法来帮助自己下山。具体来说就是,以他当前的所处的位置为基准,寻找这个位置最陡峭的地方,然后朝着山的高度下降的地方走,同理,如果我们的目标是上山,也就是爬到山顶,那么此时应该是朝着最陡峭的方向往上走。然后每走一段距离,都反复采用同一个方法,最后就能成功的抵达山谷。我们同时可以假设这座山最陡峭的地方是无法通过肉眼立马观察出来的,而是需要一个复杂的工具来测量,同时,这个人此时正好拥有测量出最陡峭方向的能力。所以,此人每走一段距离,都需要一段时间来测量所在位置最陡峭的方向,这是比较耗时的。那么为了在太阳下山之前到达山底,就要尽可能的减少测量方向的次数。这是一个两难的选择,如果测量的频繁,可以保证下山的方向是绝对正确的,但又非常耗时,如果测量的过少,又有偏离轨道的风险。所以需要找到一个合适的测量方向的频率,来确保下山的方向不错误,同时又不至于耗时太多! 梯度下降梯度下降的基本过程就和下山的场景很类似。 首先,我们有一个可微分的函数。这个函数就代表着一座山。我们的目标就是找到这个函数的最小值,也就是山底。根据之前的场景假设,最快的下山的方式就是找到当前位置最陡峭的方向,然后沿着此方向向下走,对应到函数中,就是找到给定点的梯度 ,然后朝着梯度相反的方向,就能让函数值下降的最快!因为梯度的方向就是函数之变化最快的方向(在后面会详细解释)所以,我们重复利用这个方法,反复求取梯度,最后就能到达局部的最小值,这就类似于我们下山的过程。而求取梯度就确定了最陡峭的方向,也就是场景中测量方向的手段。那么为什么梯度的方向就是最陡峭的方向呢?接下来,我们从微分开始讲起 微分看待微分的意义,可以有不同的角度,最常用的两种是: 函数图像中,某点的切线的斜率 函数的变化率几个微分的例子: 上面的例子都是单变量的微分,当一个函数有多个变量的时候,就有了多变量的微分,即分别对每个变量进行求微分 梯度梯度实际上就是多变量微分的一般化。下面这个例子: 我们可以看到,梯度就是分别对每个变量进行微分,然后用逗号分割开,梯度是用<>包括起来,说明梯度其实一个向量。 梯度是微积分中一个很重要的概念,之前提到过梯度的意义 在单变量的函数中,梯度其实就是函数的微分,代表着函数在某个给定点的切线的斜率 在多变量函数中,梯度是一个向量,向量有方向,梯度的方向就指出了函数在给定点的上升最快的方向 这也就说明了为什么我们需要千方百计的求取梯度!我们需要到达山底,就需要在每一步观测到此时最陡峭的地方,梯度就恰巧告诉了我们这个方向。梯度的方向是函数在给定点上升最快的方向,那么梯度的反方向就是函数在给定点下降最快的方向,这正是我们所需要的。所以我们只要沿着梯度的方向一直走,就能走到局部的最低点! 梯度下降算法的数学解释上面我们花了大量的篇幅介绍梯度下降算法的基本思想和场景假设,以及梯度的概念和思想。下面我们就开始从数学上解释梯度下降算法的计算过程和思想!此公式的意义是:J是关于Θ的一个函数,我们当前所处的位置为Θ0点,要从这个点走到J的最小值点,也就是山底。首先我们先确定前进的方向,也就是梯度的反向,然后走一段距离的步长,也就是α,走完这个段步长,就到达了Θ1这个点! 下面就这个公式的几个常见的疑问: α是什么含义?α在梯度下降算法中被称作为学习率或者步长,意味着我们可以通过α来控制每一步走的距离,以保证不要步子跨的太大扯着蛋,哈哈,其实就是不要走太快,错过了最低点。同时也要保证不要走的太慢,导致太阳下山了,还没有走到山下。所以α的选择在梯度下降法中往往是很重要的!α不能太大也不能太小,太小的话,可能导致迟迟走不到最低点,太大的话,会导致错过最低点! 为什么要梯度要乘以一个负号?梯度前加一个负号,就意味着朝着梯度相反的方向前进!我们在前文提到,梯度的方向实际就是函数在此点上升最快的方向!而我们需要朝着下降最快的方向走,自然就是负的梯度的方向,所以此处需要加上负号 梯度下降算法的实例我们已经基本了解了梯度下降算法的计算过程,那么我们就来看几个梯度下降算法的小实例,首先从单变量的函数开始 单变量函数的梯度下降我们假设有一个单变量的函数函数的微分初始化,起点为学习率为根据梯度下降的计算公式我们开始进行梯度下降的迭代计算过程:如图,经过四次的运算,也就是走了四步,基本就抵达了函数的最低点,也就是山底 多变量函数的梯度下降我们假设有一个目标函数现在要通过梯度下降法计算这个函数的最小值。我们通过观察就能发现最小值其实就是 (0,0)点。但是接下来,我们会从梯度下降算法开始一步步计算到这个最小值!我们假设初始的起点为:初始的学习率为:函数的梯度为:进行多次迭代:我们发现,已经基本靠近函数的最小值点 梯度下降算法的实现下面我们将用python实现一个简单的梯度下降算法。场景是一个简单的线性回归的例子:假设现在我们有一系列的点,如下图所示 我们将用梯度下降法来拟合出这条直线! 首先,我们需要定义一个代价函数,在此我们选用均方误差代价函数 此公示中 m是数据集中点的个数 ½是一个常量,这样是为了在求梯度的时候,二次方乘下来就和这里的½抵消了,自然就没有多余的常数系数,方便后续的计算,同时对结果不会有影响 y 是数据集中每个点的真实y坐标的值 h 是我们的预测函数,根据每一个输入x,根据Θ 计算得到预测的y值,即 我们可以根据代价函数看到,代价函数中的变量有两个,所以是一个多变量的梯度下降问题,求解出代价函数的梯度,也就是分别对两个变量进行微分 明确了代价函数和梯度,以及预测的函数形式。我们就可以开始编写代码了。但在这之前,需要说明一点,就是为了方便代码的编写,我们会将所有的公式都转换为矩阵的形式,python中计算矩阵是非常方便的,同时代码也会变得非常的简洁。 为了转换为矩阵的计算,我们观察到预测函数的形式我们有两个变量,为了对这个公式进行矩阵化,我们可以给每一个点x增加一维,这一维的值固定为1,这一维将会乘到Θ0上。这样就方便我们统一矩阵化的计算 然后我们将代价函数和梯度转化为矩阵向量相乘的形式 coding time首先,我们需要定义数据集和学习率123456789101112131415161718import numpy as np# Size of the points dataset.m = 20# Points x-coordinate and dummy value (x0, x1).X0 = np.ones((m, 1))X1 = np.arange(1, m+1).reshape(m, 1)X = np.hstack((X0, X1))# Points y-coordinatey = np.array([ 3, 4, 5, 5, 2, 4, 7, 8, 11, 8, 12, 11, 13, 13, 16, 17, 18, 17, 19, 21]).reshape(m, 1)# The Learning Rate alpha.alpha = 0.01 接下来我们以矩阵向量的形式定义代价函数和代价函数的梯度123456789def error_function(theta, X, y): '''Error function J definition.''' diff = np.dot(X, theta) - y return (1./2*m) * np.dot(np.transpose(diff), diff)def gradient_function(theta, X, y): '''Gradient of the function J definition.''' diff = np.dot(X, theta) - y return (1./m) * np.dot(np.transpose(X), diff) 最后就是算法的核心部分,梯度下降迭代计算12345678def gradient_descent(X, y, alpha): '''Perform gradient descent.''' theta = np.array([1, 1]).reshape(2, 1) gradient = gradient_function(theta, X, y) while not np.all(np.absolute(gradient) <= 1e-5): theta = theta - alpha * gradient gradient = gradient_function(theta, X, y) return theta 当梯度小于1e-5时,说明已经进入了比较平滑的状态,类似于山谷的状态,这时候再继续迭代效果也不大了,所以这个时候可以退出循环! 完整的代码如下1234567891011121314151617181920212223242526272829303132333435363738394041import numpy as np# Size of the points dataset.m = 20# Points x-coordinate and dummy value (x0, x1).X0 = np.ones((m, 1))X1 = np.arange(1, m+1).reshape(m, 1)X = np.hstack((X0, X1))# Points y-coordinatey = np.array([ 3, 4, 5, 5, 2, 4, 7, 8, 11, 8, 12, 11, 13, 13, 16, 17, 18, 17, 19, 21]).reshape(m, 1)# The Learning Rate alpha.alpha = 0.01def error_function(theta, X, y): '''Error function J definition.''' diff = np.dot(X, theta) - y return (1./2*m) * np.dot(np.transpose(diff), diff)def gradient_function(theta, X, y): '''Gradient of the function J definition.''' diff = np.dot(X, theta) - y return (1./m) * np.dot(np.transpose(X), diff)def gradient_descent(X, y, alpha): '''Perform gradient descent.''' theta = np.array([1, 1]).reshape(2, 1) gradient = gradient_function(theta, X, y) while not np.all(np.absolute(gradient) <= 1e-5): theta = theta - alpha * gradient gradient = gradient_function(theta, X, y) return thetaoptimal = gradient_descent(X, y, alpha)print('optimal:', optimal)print('error function:', error_function(optimal, X, y)[0,0]) 运行代码,计算得到的结果如下 所拟合出的直线如下 小结至此,我们就基本介绍完了梯度下降法的基本思想和算法流程,并且用python实现了一个简单的梯度下降算法拟合直线的案例!最后,我们回到文章开头所提出的场景假设:这个下山的人实际上就代表了反向传播算法,下山的路径其实就代表着算法中一直在寻找的参数Θ,山上当前点的最陡峭的方向实际上就是代价函数在这一点的梯度方向,场景中观测最陡峭方向所用的工具就是微分 。在下一次观测之前的时间就是有我们算法中的学习率α所定义的。可以看到场景假设和梯度下降算法很好的完成了对应! Further reading Gradient Descent lecture notes from UD262 Udacity Georgia Tech ML Course. An overview of gradient descent optimization algorithms.]]></content>
<categories>
<category>机器学习</category>
</categories>
<tags>
<tag>机器学习</tag>
</tags>
</entry>
<entry>
<title><![CDATA[程序员面试资料]]></title>
<url>%2F2017%2F12%2F16%2F%E7%A8%8B%E5%BA%8F%E5%91%98%E9%9D%A2%E8%AF%95%E8%B5%84%E6%96%99%2F</url>
<content type="text"><![CDATA[工欲善其事必先利其器 对于程序员面试来说,所谓“器”其实就是手中的资料文档信息,有一份好的资料,可以少走很多弯路。但现在网上各种资料泛滥,经常让人眼花缭乱。所以就将面试中用的一些好资料分享出来,希望能给同学们提供一个参考,有所帮助!主要是面对即将毕业的同学准备校招面试,所以更注重基础! 目前主要有: Java语言相关 数据结构算法 数据库 网络 操作系统 场景和系统设计题 面试经验和心得 设计模式 在github上新建了一个项目,欢迎fork,并分享相关的有价值的资料文档!github地址:https://github.com/chi2liu/Interview_Material]]></content>
<categories>
<category>面试</category>
</categories>
<tags>
<tag>面试</tag>
</tags>
</entry>
<entry>
<title><![CDATA[如何准备校招技术面试+一只小菜鸟的面试之路]]></title>
<url>%2F2017%2F12%2F16%2F%E5%A6%82%E4%BD%95%E5%87%86%E5%A4%87%E6%A0%A1%E6%8B%9B%E6%8A%80%E6%9C%AF%E9%9D%A2%E8%AF%95-%E4%B8%80%E5%8F%AA%E5%B0%8F%E8%8F%9C%E9%B8%9F%E7%9A%84%E9%9D%A2%E8%AF%95%E4%B9%8B%E8%B7%AF%2F</url>
<content type="text"><![CDATA[写在前面 秋招一路走来很幸运,从最初的迷茫,到接连被否认,跌入低谷,然后慢慢调整心态,有缺憾才能有进步,正视的自己不足,静下心努力提高,勇敢尝试各种面试机会,因上努力,果上随缘,慢慢看到改变,收获肯定,重拾信心。校招像一个登山的过程,要一步一个脚印,不能因为某些失败,就半途而废,行百里者半九十,念念不忘,必有回响,只要一路坚持下来,就会有所收获。据身边所见,大部分一直坚持面试下去的同学,最后都拿到了满意的offer。写在这里,是对自己秋招的一次总结和怀念,也希望给那些和我一样迷茫过,怀疑过,失落过的人一些帮助和激励。 如何准备校招需长期积累(1)手撕代码能力现在大多数大厂的面试基本上都需要手写代码!基本逃不掉!所以这个能力是极其重要的,写得好很加分,写得不好就很可能GG了。建议这方面基础不太好的同学,提前至少半年开始刷题,主要是lintcode和leetcode。等到七八月份再刷题可能就真的来不及了,那时候基本每天都有笔试面试,所以如果算法代码能力不好的同学,一定要尽早刷题,算法是个内功,需要时间慢慢积累,唯一的方法就是多刷题,多写代码。Leetcode刷200道左右,编号前100的题目尽量能刷两遍,尽量做到大部分能独立写bug free的ac代码。现场面试的时候基本是手写代码,所以最好有时间也练一练在纸上写代码,找找感觉,尽量写的简洁干净,不要涂涂改改。 (2)项目经历和实习经历项目经历和实力经历是最需要平时积累的。不要仅仅就在完成任务这个层面,不要沉迷在项目的一些业务细节上,多关注框架,架构,优化方面的东西。可以深入去研究项目,比如做某些优化,用设计模式去重构一下,用到前沿的技术优化,比如redis缓存之类的东西,相信只要用心去研究所做的项目,等到面试的时候,去总结项目难点怎么回答就不困难了! 可临时抱佛脚 网络,数据库,操作系统,java或者C++等这些基础知识,面试的时候无非就是常见的那些面试题,都是固定的问题和答案,平常只要稍微关注一下即可,到了面试前花一两个星期临时抱佛脚看一看背一背基本就能应付过去。(如果要真正掌握这些知识,显然是需要花很大精力去钻研的,但就应付面试来说,记一记常考的点,搞清楚常问的点基本就够了) 场景题和系统设计题,很多同学比较怕这类题目,其实这类题目大多涉及一些架构设计的东西,不会要你回答的多么细致,能给出大概的思路即可。而且多是一些高并发高负载的系统设计题。可能大部分应届生都没法接触到这么高难度的开发,所以这时候主要就看你的想法和视野了。尝试去看一些架构方面的书和积累一些面试题就发现其实这类题都是一个套路。看着很难,但实际上只要知道这些技术的基本概念和作用就行了,答出思路基本就没问题,毕竟不会让你现场写一个高并发系统。 完整面经+总结Cvte提前批 一面(电话) 自我介绍 介绍你的项目 加密解密了解么?几种算法,讲一下你了解的 多线程了解么?什么是线程安全? 说一个你最熟悉的设计模式 讲一下你项目中用到了哪些设计模式 Java的hashmap的原理 Hashmap的线程安全性,什么是线程安全的?如何实现线程安全 数据库的索引了解么?介绍一下 数据库有哪些优化的方法?讲你自己知道的 为什么事务可以优化数据库? 二面(视频) 自我介绍 介绍项目 Mysql的数据库引擎,区别特点 设计模式了解?讲一下最熟悉的 写一个单例模式,答主写的是双检查锁单例,问了为什么用Volatile,synchronize 单例模式在你项目里哪些应用? 数据连接池采用了什么设计模式?意义是什么? 对高负载有了解么 你意向的技术方向是哪块?(答主回答的高并发,然后面试官说他是做高负载的) 对高并发有了解么? 校招首次面试,二面跪。发现存在以下问题:(1)准备不充分(自我介绍,项目介绍,项目难点等需提前准备)(2)基础不扎实(面试中问的数据库相关问题基本没答上来)(3)缺乏亮点(缺乏体现自己能力的东西,比如项目难点如何解决,对一些前沿技术的了解等)庆幸的当时只是八月初,大部分大厂的校招还没开始,有足够的时间去提高,比如深入复习数据库这一块的内容,深入去研究项目,了解一些前沿的新技术等。 阿里内推 一面(电话) 听说你有博客,博客里大概有什么内容? 项目介绍,最复杂的表 Hashmap的原理 Hashmap为什么大小是2的幂次 介绍一下红黑树 Arraylist的原理 Arraylist的扩容机制 为什么arraylist扩容是1.5倍 场景题:设计判断论文抄袭的系统 堆排序的原理 抽象工厂和工厂方法模式的区别 工厂模式的思想 object类你知道的方法 哪里用到了工厂模式 Forward和redirect的区别 二面(视频) 自我介绍 项目介绍 项目架构 项目难点 Synchronize关键字为什么jdk1.5后效率提高了 线程池的使用时的注意事项 Spring中autowire和resourse关键字的区别 Hashmap的原理 Hashmap的大小为什么指定为2的幂次 讲一下线程状态转移图 消息队列了解么 分布式了解么 阿里作为最想去的公司(毕竟是国内JAVA第一大厂)面试开始比较早,自己还处在面试菜鸟的阶段,最后挂在二面。一面面试官很好,体验不错。最后给我建议:希望我多去深入理解背后的原理,而不是仅仅停留在知道了解的层面。总体一面还是感觉不错。二面是整个秋招表现最不好的一次面试。答的很乱。这次面试看到自己的不足,第一,就是基础很不扎实,很多常问的面试题自己都不知道,第二,要深入去理解背后的原理,比如数据库的隔离级别具体的实现等等,第三,深入琢磨自己做的项目,用一些高大上的技术去装饰。阿里面试后,有了危机感,从0开始研究redis数据库,研究mysql数据库的一些常问的底层实现,以及spring的一些常见的面试题等一个经验:到了八九月份的时候,遇到一些自己不会的大块,比如数据库,很多人就直接放弃,觉得再学已经来不及,其实不然,面试的时候,问的东西基本是固定的,只要我们对常问的面试问题深入了解就行。所以八九月份发现自己还有东西完全不会的,也不要慌,沉下心去学几天,绝对来得及! 便利蜂内推(offer) 一面(电话) 自我介绍 项目介绍 volatile和synchronized 来个算法题:一个无序数组,其中一个数字出现的次数大于其他数字之和,求这个数字 (主元素) 答完再来一个:一个数组,有正有负,不改变顺序的情况下,求和最大的最长子序列 项目用到什么数据库?隔离级别?每个隔离级别各做了什么 数据库的索引?mysql不同引擎索引的区别 垃圾回收算法的过程 你了解的垃圾收集器? Cms收集器的过程 怎样进入老年代? 平时用到了什么设计模式? 讲一下你最熟的两个设计模式 用过什么系统?shell写过脚本吗? 讲你知道的Linux命令 便利蜂是校招拿到的第一个offer,虽然公司名气不大,但尽早拿到第一个offer,可以提高信心和稳住心态,对后续的面试很有帮助。所以前期一定要多投简历,能面的尽量面,反正最后你又不一定去,多面几次积累经验,等到大厂的面试开始,已经积累到了足够的面试经验,可以发挥的更好。便利蜂的面试也是幸运女神眷顾。面试前,在牛客网搜索了所有便利蜂java的面经,只找到一篇,就把那篇面经的问题好好研究了一遍。然后奇迹出现了,面试问的问题基本一模一样,估计是同一个面试官,所以答得很顺利。一个小技巧,当面试的时候遇到以前做过的算法题的时候,千万别说自己做过,请假装没做过,不然面试官会换题目的。经验:进入校招之后,多泡泡牛客网,多看看面经很有帮助,面试其实都大同小异,就那么些知识点,所以多看多总结。 拼多多 一面(现场面) 自我介绍 项目介绍 手撕算法:一棵二叉排序树,给定一个数,找到与给定数差值绝对值最小的数 场景题:设计一个系统,解决抢购时所需要的大量的短链接的功能,如何保证高并发,如何设计短链接 二面(现场面) 代码量多少 给了一张纸,各种名词,会的写出来 然后给它解释那些会的 设计题:设计一个系统,记录qq用户前一天的登录状态,提供16g内存和2tb的硬盘,要做到查询指定qq号的前一天的登录状态,快速查询O(1)复杂度 之前经历了那么多电话面,终于迎来了人生第一次现场技术面!果然第一次总是会有点痛苦,很遗憾的挂了!一面感觉还不错,项目简单聊了一下,感觉面试官兴趣不大。就开始手撕代码,第一次手撕代码,略紧张,最后还是想出来了。然后一个场景题,用到了刚学会的redis,现学现卖,感觉面试官还比较满意。二面一坐下,就感觉对面的面试官气场太强,一坐下,没让自我介绍,直接给了问代码量多少,我准备大概估算一下,结果被面试官打断,让我直接说,讲真,有点慌乱哈哈。就说了大概十万行。然后给了一张纸,大概有几十个专业名词,让我把会的勾出来,然后一个一个的解释。然后就出了一个系统设计题,一直没理解面试官的意思,最后在面试官讨论项目的过程中,面试官问是否可以通过value推到key,一下没经过思考,答了个是,答错了,然后面试官就说面试结束了。很多时候,如果面试官问到一个比较基础的问题,你没答上来,那你就跪了,所以回答之前要三思熟虑,别急着回答,好好想一想。 多益网络内推(offer) 一面(视频) 自我介绍 对面向对象的理解 介绍多态 Java新建线程有哪几种方式 线程池的作用 看过框架源码么 多益只有一面视频面,是秋招拿到的第二个offer。整个面试过程,感觉面试官都是问的一些很大的问题,就看你自己发挥了,尽量把知道的都回答,而且尽量回答的专业。在问到对面向对象的理解的时候,可能大部分人都会回答多态继承封装就没了。前几天正好看到面向对象的六原则和一法则,然后就给面试官吹了一波,面试官也没打断我,一直让我说,整个面试答得比较好的就是这里。所以还是要提前准备啊,像这种题目,提前准备过和没准备回答出来的效果完全不一样。 涂鸦移动内推(offer) 一面(现场) 自我介绍 项目介绍 数据库的索引原理 索引使用的注意事项 数据库的引擎 Java垃圾回收机制 Java的finalize,finally,final三个关键字的区别和应用场景 String类可以被继承么 手撕算法:假设你是一个专业的窃贼,准备沿着一条街打劫房屋。每个房子都存放着特定金额的钱。你面临的唯一约束条件是:相邻的房子装着相互联系的防盗系统,且 当相邻的两个房子同一天被打劫时,该系统会自动报警。给定一个非负整数列表,表示每个房子中存放的钱, 算一算,如果今晚去打劫,你最多可以得到多少钱 在不触动报警装置的情况下。 二面(电话) 自我介绍 对游戏的了解 项目介绍 算法题:给一个整数数组,找到两个数使得他们的和等于一个给定的数 target。 红黑树 Redis的应用 一面面试官说之前仔细看了我的博客,然后面试的内容就是让我讲一讲博客里写的内容,基本就是之前复习准备的数据库的知识,都游刃有余了。但感觉到自己讲的时候还是思维有点混乱,所以表达能力也很重要,有时间自己试着将一些常问的东西,自己私下表达一遍,尽量做到表达清楚专业有条理。然后手撕算法,leetcode原题,打劫房屋,自己当时只记得题目,但方法不记得了,就现场推了一遍动态规划的状态方程,结果很幸运推了出来。(这个还是得靠前期的刷题的积累)二面电话面问的比较简略,又用到了救命稻草redis,发现面试官都很喜欢问这个。可能因为是比较前沿的技术吧,现在企业用的比较多,但实际这个东西不是特别难。所以了解一下redis会很加分。不久之后,收到涂鸦移动的offer,应该是第四个offer,自己也开始慢慢更有信心了。虽然最后没去涂鸦,但面试的过程也学习了不少东西,自己在表达能力方面还需要加强。所以每次面试我们或多或少都能有所收获,多面试多经历多体验! 中国电信it研发中心(offer) 一面(现场) 自我介绍 项目介绍 项目里用的什么服务器 自己写一个tomcat服务器,你会怎么写 分布式服务器会出现哪些问题 怎么解决session一致性缓存的问题 Redis的优势和特点 一千万用户并发抢购,怎么设计 如果成功的用户有10万,redis存不下怎么处理 你项目中的难点 二面(现场) 自我介绍 项目介绍 介绍spring中的熟悉的注解 让你实现autowire注解的功能你会如何实现 Redis和mysql的区别 Redis的持久化有哪些方式,具体原理 中国电信算是拿到的第一个比较满意的offer,虽然不是互联网大厂,但是薪资地点发展都感觉不错,当时考虑,如果后面没拿到大厂的offer就去电信养老了。电信的面试官年纪偏大,问的问题也比较偏实践和设计,基本没问基础知识。一面一上来就让我写一个tomcat服务器,石化,还好面试官比较和善,慢慢提示我,然后又问我怎么保持session的一致性,这个其实是比较常见的问题,我之前没见过,面试官让我现场想,最后我想到的答案正好是正确答案,然后面试官出的场景题,我就将redis往上面套,无非就是缓存,消息队列这些技术去处理那些高并发的问题。所以答得还不错。可以看出,有时候面到你不会的东西,如果你能在面试官的引导下回答出来是很加分的,可能因为看到你的思考能力吧,更看重你的潜力!二面还是聚焦在redis这一块,(redis救命稻草,如果放假在家那几天没看,感觉后面的offer都可能拿不到了)。 中兴(offer) 专业面(现场) 自我介绍 项目介绍 你了解的设计模式,讲两个 Java collection类,集合,讲两个你了解的,说实现原理 Java线程池的作用 你觉得你在你实验室处于什么水平 综合面试(现场) 自我介绍 说一下你知道的设计模式 画一个策略模式的uml图 Java多线程的理解 内存屏障是什么 数据库索引 项目中的优化 然后开始聊人生 中兴软件岗的面试比较水,主要看你的学校背景。学校不错基本没问题 百度(offer) 一面(现场) 自我介绍 Java中的多态 Object类下的方法 Wait和notify的作用 Finalize的作用和使用场景 Hashcode和equals 为什么要同时重写hashcode和equals 不同时重写会出现哪些问题 Hashmap的原理 Hashmap如何变线程安全,每种方式的优缺点 垃圾回收机制 Jvm的参数你知道的说一下 设计模式了解的说一下啊 手撕一个单例模式 快速排序的思想讲一下 给个数组,模拟快排的过程 手写快排 设计题,一个图书馆管理系统,数据库怎么设计,需求自己定,题目很宽泛,面试官看你能考虑到哪些问题 二面(现场) 自我介绍 项目介绍 Redis的特点 分布式事务了解么 反爬虫的机制,有哪些方式 手撕算法:反转单链表 手撕算法:实现类似微博子结构的数据结构,输入一系列父子关系,输出一个类似微博评论的父子结构图 手写java多线程 手写java的soeket编程,服务端和客户端 进程间的通信方式 手撕算法: 爬楼梯,写出状态转移方程 智力题:时针分针什么时候重合 三面(现场) 由于三面面试官不懂java,我不熟c加加,所以全程尬聊 自我介绍 项目介绍 项目难点 手撕算法:给定一个数字三角形,找到从顶部到底部的最小路径和。每一步可以移动到下面一行的相邻数字上。 然后继续在这个问题上扩展 求出最短那条的路径 递归求出所有的路径 设计模式讲一下熟悉的 会不会滥用设计模式 多线程条件变量为什么要在while体里,为什么不是if 你遇到什么挫折 百度三轮技术面,每面都在手撕代码,很注重代码能力,还有智力题,也是比较特别的。一面主要聊了聊基础和写了简单的算法二面一开始聊了聊项目,然后就开始手撕代码,先撕了翻转单链表(这个算法算是面试常考了,整个秋招写了三次这个算法),然后就撕了一个比较难的算法,微博子结构,代码比较难写,写的比较慢,面试官催了好几次,但还是写出了,面试官评价:“代码风格不错,但是写的太慢,笔试会吃亏”,确实吃亏,笔试挂了好多哈哈。然后又是一轮写,先写多线程,然后写socket,socket不会写,就直接说了。面试的时候,有些问题答不上来也不要慌,最后问了一个智力题,三面遇到了语言不一样的面试官,于是就一直在写代码。 百度的面试很要求手撕算法的能力,考察的比较全面,智力题场景题均有涉及。 美团内推(offer) 一面(电话) 自我介绍 项目介绍 Redis介绍 了解redis源码么 了解redis集群么 Hashmap的原理 hashmap容量为什么是2的幂次 hashset的源码 object类你知道的方法 hashcode和equals 你重写过hashcode和equals么,要注意什么 假设现在一个学生类,有学号和姓名,我现在hashcode方法重写的时候,只将学号参与计算,会出现什么情况? 往set里面put一个学生对象,然后将这个学生对象的学号改了,再put进去,可以放进set么?并讲出为什么 Redis的持久化?有哪些方式,原理是什么? 讲一下稳定的排序算法和不稳定的排序算法 讲一下快速排序的思想 二面(现场) 自我介绍 讲一下数据的acid 什么是一致性 什么是隔离性 Mysql的隔离级别 每个隔离级别是如何解决 Mysql要加上nextkey锁,语句该怎么写 Java的内存模型,垃圾回收 线程池的参数 每个参数解释一遍 然后面试官设置了每个参数,给了是个线程,让描述出完整的线程池执行的流程 Nio和IO有什么区别 Nio和aio的区别 Spring的aop怎么实现 Spring的aop有哪些实现方式 动态代理的实现方式和区别 Linux了解么 怎么查看系统负载 Cpu load的参数如果为4,描述一下现在系统处于什么情况 Linux,查找磁盘上最大的文件的命令 Linux,如何查看系统日志文件 手撕算法:leeetcode原题 22,Generate Parentheses 三面(现场) 自我介绍 项目介绍 怎么管理项目成员 当意见不一致时,如何沟通开发成员,并举个例子 怎么保证项目的进度 数据库的索引原理 非聚簇索引和聚簇索引 索引的使用注意事项 联合索引 从底层解释最左匹配原则 Mysql对联合索引有优化么?会自动调整顺序么?哪个版本开始优化? Redis的应用 Redis的持久化的方式和原理 技术选型,一个新技术和一个稳定的旧技术,你会怎么选择,选择的考虑有哪些 说你印象最深的美团技术团队的三篇博客 最近在学什么新技术 你是怎么去接触一门新技术的 会看哪些书 怎么选择要看的书 美团是拿到的第一个大厂offer,也是美团面完后心态更稳了。唉,最有效提升信心的方法就是拿到一个满意的offer!整个美团流程比较长,由于是内推,9.7号接到电话面,电话面基本聊的基础,面试官很好,一直在引导。9.20的现场面,二面一开始太紧张,数据库的四个特性不记得了,一个持久性死活想不起来,还好影响不大,后面答的都比较好,问到数据库的隔离级别,我主动引入到底层实现原理,回答问题的时候,可以主动延伸一下,尽量将自己会的表达出来。面试之前,将牛客网上所有美团的面经扒了下来,看到很多出现率很高的问题,就提前准备了这些问题,果然在面试的时候碰到了,功夫不负有心人,准备工作没有白做。所以多刷牛客,多刷面经才是王道啊!最后就是手撕算法,leetcode原题,生成括号,dfs问题。一贯套路,假装之前没见过这道题,先讲一个暴力法,然后再写出代码!由于leetcode刷了差不多两遍,所以很顺畅的写出来了。(像这类题,如果之前完全没刷过或者没见过,现场写出来并不容易)三面就基本就是聊人生,偶尔带一点技术。现在大厂的面试基本逃不过手写代码,基本上都是leetcode的中等难度的原题或者类似的题目,所以前期一定要多刷题,如果算法能力不强的话,这个只能靠硬实力了。 华为(offer) 一面(现场) 自我介绍 项目介绍 项目架构 项目一个完整的执行流程(由于我是搞java的,而面试官是搞c的,所以全程尬聊) 项目优化 二面(现场) 自我介绍 项目介绍 怎么管理项目进度 平常的爱好 感觉面试官也不是搞java的,所以又是一阵尬聊 华为软件岗的面试比较水,主要看你的学校背景。学校不错基本没问题 苏宁内推(offer) 一面(现场) 自我介绍 项目介绍 面过哪些公司了 有哪些offer了 聊到多益,于是开始聊最近微博上很火的多益老板 得出结论,我和面试官都觉得多益老板三观有问题,但做游戏就是要偏执的人 你博客主要哪方面的 多线程并发包了解么 讲一下countDownLatch 面试过程就是聊人生,面试官先问了你有哪些offer。然后讨论了多益的老板的微博和价值观,估计是因为offer对实力也是一种认可,所以没怎么问技术就结束了,最后顺利拿到offer。(算是秋招拿的最容易的一个offer,就一轮面试聊了聊八卦就过了)但其他认识的同学,有被怼了50分钟技术的。所以啊,面试这个东西看缘分,期望老天保佑遇到对味的面试官。 腾讯 一面(现场) 自我介绍 项目介绍 Hibernate的作用,你对hibernate的理解 多线程的理解,如何保证线程安全 mysql数据库的引擎和区别 场景题:千万用户抢购,如何处理高并发,并且有一个链接,指向前一天抢购成功的用户,如何设计这个系统和数据库 如果后台处理抢购请求的服务器,每次最多承受200的负载,系统该怎么设计 手撕算法:最小公倍数和最大公约数 二面(现场) 自我介绍 项目介绍 项目里一个完整请求的流程 项目的优化 Hibernate和mybatis的区别 为什么用ssh框架 Mysql的容灾备份 Redis和memcache 的区别 为什么选择redis Java的full gc Full gc会导致什么问题 腾讯笔试似乎不怎么刷人(笔试做的很烂,依然收到了面试通知,周围认识的做了腾讯笔试的基本都收到了面试通知)。所以对待腾讯的笔试可以轻松一点。一面是个小姐姐,基本问的都是很大问题,看你的发挥,没怎么问基础。没想到一面能过,二面是一位中年大叔,不言自威,气场略强,感觉答的还可以,最后还是挂了,可能因为真的不招java吧。一面的面试官让我回去看一下分布式事务,然后我就真的看了,然后后面网易的面试都在问这个,如果没来面腾讯,估计后面网易也过不了。所以啊,面试真实一个学习的过程,不要错失大好的学习机会 招银网络科技(offer) 一面(现场) 自我介绍 写一个两个有序链表合并成一个有序链表 死锁是什么呢 怎么解决死锁 http请求流程 为什么负载均衡 怎么实现负载均衡 数据库挂了怎么办?除了热备份还有什么方法 讲讲你对spring的理解,不要把ioc和aop背给我听 二面(现场) 自我介绍 项目介绍 算法:找出两个数组相等的数,不能用其他数据结构 算法:给定一个数字,一个数组,找出数组中相加等于这两个数的和,不能用数据结构 算法:如何判断一个树是不是另一颗树的子树 如何解决并发访问的错误 招银算是银行类的公司面试最专业的。一面上来就是手撕算法,最后问spring的时候,问我对spring的理解,并且面试官提示我不要把ioc和aop背给我听,我就正好讲了自己的理解,ioc和aop都是为了降低代码侵入性和耦合度。所以,有时候一些常规的问题,我们最好能有自己的思考,如果是千篇一律的答案,并没有什么亮点,将自己的理解讲出来或许更好。二面基本都是在问算法,讲思路就可以了,这个时候就看自己的算法能力了。前期一定要多刷题! 网易(offer) 一面(现场) 自我介绍 项目介绍 项目难点(疯狂怼) I++操作是线程安全的么?怎么保证线程安全 场景题:设计一个下单系统,下单成功后可以给用户发优惠券 接上面场景题:服务器挂了,优惠券还没发怎么办 数据库挂了怎么怎么办 怎么保证一致性 分布式事务知道么 介绍分布式事务 你的职业规划 二面(现场) 自我介绍 项目介绍(又是狂怼) Nio的原理 Channel和buffer directBuffer和buffer的区别 nio和aio的区别 锁的实现原理 怎么解决缓存和主存的一致性问题 缓存还没更新到主存,服务器挂了怎么办 数据库挂了怎么办 网易的面试感觉运气比较好。也跟自己心态有关,这个时候已经压力不大了,挂了也无所谓,但往往就是这种平常心去面试的时候,发挥的会更好。所以,心态真的很重要。前两天腾讯面试官让下去了解的分布式事务,结果网易一面的场景题基本都是在这一块讨论,最后还具体问了分布式事务,正好现学现卖。所以,多面试,面试官说让你去了解,一定要去了解,多学习肯定没坏处。二面面试官听说我有博客,对我感觉不错,说很多程序员就缺乏总结的能力。平常积累的博客,感觉终于派上用场了,所以如果有空,维护一个技术博客,写写算法题解或者一些技术问题,就当是学习笔记。 携程(offer) 一面(现场) 自我介绍 项目介绍 项目难点(讲到用了消息队列优化,被面试官夸了一波) 了解hashmap么?讲讲原理 知道java GC?讲讲过程(又背了一遍) 手撕算法,写二叉的后序非递归遍历 手撕算法,翻转单链表 手撕算法,背包问题 携程的迷之测评,但面试还是相对基础,但也要求算法能力。第一个二叉的后序非递归遍历的算法题,算是校招中唯一一个没写出来的算法,不过最后在面试官提示下还是做出来了。 今日头条(offer) 一面(视频) 自我介绍 数据库优化有什么了解 索引的原理 联合索引,如果联合索引(a,b),现在查询a>0,b>0可以用到这个联合索引么?(太久没看,生疏了,答错了,答案应该是不能,只能用到a的索引,范围索引只能用到一列) 数据库优化中,有一个关键字可以分析执行过程,知道么(explain关键字) Java虚拟机了解么?使用中有什么注意事项? 了解设计模式么?手写一个单例模式 写一个算法吧,一个二维数组,每一行从左到右递增,每一列从上到下递增,给一个数,判断他是否在在二维数组中,在返回下标,不在返回(-1,-1) 比较基础的算法题,要做到最优。 了解nio么?讲一下nio的理解。 Linux命令了解么?查看网络状态的命令,查看内存占用的命令。Awk命令。 二面(视频) 自我介绍 http协议的chunk知道么?是干什么的? http的状态码了解么?说一下 301和302的区别 502和503的区别(这么久没面,早忘了,然后被面试官教育了一番,说没有真正掌握这些知识) Redis的数据结构的底层实现 Mysql集群数据是怎么同步的 手撕算法:一个链表,奇数位置递增,偶数位置递减,给链表排序。要求O(n)的时间复杂度 Select,poll,epoll的区别 数据库的索引原理 场景题,设计一个高并发的系统。 三面(视频) 自我介绍 服务器处理接受一个请求的过程 数据库的索引的原理 Innodb都是聚簇索引么? 设计一个文件分发系统,分发到10000台服务器,做到高效可靠,如何保证高效,如何保证可靠 有什么offer 你的优点缺点 你觉得你在同龄中处于什么水平 面头条的时候已经是十月底了。大半个月没有面试,之前准备的知识点比如http状态码之类的记忆性的东西,早就忘光了。二面的时候问了一堆状态码,全都不记得了。面试官态度很好,跟我说,你现在不记得了,说明你这些知识只是为了面试临时抱佛脚,并没有深入理解,所以时间一长就忘了。确实是这样。总的来说,头条的面试比较注重算法能力,两面都手写了算法,算法不算特别难,但是真正实现好,还是会有很多细节要考虑到,而且要写的代码都比较长。头条也比较喜欢出场景题,感觉面试官也没有标准答案,主要看你的回答和思路。临时抱佛脚背的知识会忘掉,但刷题后的代码能力解题能力却是内功,可以说一劳永逸的,所以多刷题很重要,面试的时候代码写的好或许可以弥补有些问题没答上来。 写在最后这张图是我对面试一个最直观的感受 回头看整个校招的面试过程,会发现面试就像在登山,你一定是往上爬的,只是爬得快慢之分,越面到后面,面试的越多,你的能力技巧经验都是越来越好!这是很多方面的因素造成的。 实力的提升面试同时也是一个学习和提高的机会,面试的越多,积累的经验,个人的能力和知识储备等各方面也都在慢慢提高,所以只要面试后及时总结,并努力去改进,实力就一定会有提升! 心态越来越好传说“80%的offer掌握在20%的人手中”,其实不无道理。实力当然是面试成功拿到offer的重要部分,但心态的影响也是至关重要的,万事开头难,当你拿到第一个满意的offer的时候,后续的面试都会变得顺利起来,offer会越来越好拿,这就是良好的心态起了重要作用。反之,当你迟迟拿不到一个满意的offer,心态就会越来越不稳,offer会变的越来越难拿。总结自己的面试过程,从美团面完基本确定可以拿到offer之后,整个人的心态就完全不一样了,更有底气和信心,反正已经有差不多满意的offer,后续的面试其实过不过反倒无所谓了。而往往就是这种无所谓的心态,也就是平常心,抱着去试一试玩一玩的心态,反而能在面试的时候发挥的更好,反而能在面试的时候显得更有自信更加从容,自然也就能够得到更好的结果。(面试的注意事项,面试的时候一定要保持自信,而不是慌乱,如果面试官感受到你的慌乱紧张而不是自信,那么显然会对你的实力有所怀疑) 运气运气这个东西完全是看天意了。但是如果连续参加五场面试,运气都不好,问的全是不会的,会的全都不问。这个时候一定要稳住!就跟抛硬币一样,连续抛了五次反面,也算是倒霉透顶了,但是要相信概率,总有扔到正面的时候,只要你继续扔下去。同样的道理,只要你坚持面试下去,总会有运气好的时候,面试的越多,机会也就越多,一定要尽早尽量多的参加各种面试,尤其是当你没拿到满意offer的时候。 最后在总结一下 Offer = 0.3心态 + 0.5实力 + 0.2运气,缺一不可 越早开始准备越好,准备的越充分越好 切忌眼高手低。对于普通同学(大神除外),前期尽量多利用小公司的面试机会来锻炼自己,积累经验。 在面试的过程中找准自己的定位,并且适当的根据面试情况及时调整自己的定位和目标,保持信心,但不能盲目自信。不管是大公司还是小公司,尽早拿到第一个offer。 心态要及时调整好! 不管面试失败还是成功,都一定要及时总结!]]></content>
<categories>
<category>面试</category>
</categories>
<tags>
<tag>面试</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java源码剖析之LinkedList]]></title>
<url>%2F2017%2F08%2F06%2FJava%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90%E4%B9%8BLinkedList%2F</url>
<content type="text"><![CDATA[本文对LinkedList的实现讨论都基于JDK8版本 Java中的LinkedList类实现了List接口和Deque接口,是一种链表类型的数据结构,支持高效的插入和删除操作,同时也实现了Deque接口,使得LinkedList类也具有队列的特性。LinkedList类的底层实现的数据结构是一个双端的链表。LinkedList类中有一个内部私有类Node,这个类就代表双端链表的节点Node。这个类有三个属性,分别是前驱节点,本节点的值,后继结点。源码中的实现是这样的。 1234567891011private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } } 注意这个节点的初始化方法,给定三个参数,分别前驱节点,本节点的值,后继结点。这个方法将在LinkedList的实现中多次调用。 下图是LinkedList内部结构的可视化,能够帮我们更好的理解LinkedList内部的结构。 双端链表由node组成,每个节点有两个reference指向前驱节点和后继结点,第一个节点的前驱节点为null,最后一个节点的后继节点为null。 LinkedList类有很多方法供我们调用。我们不会一一介绍,本文会详细介绍其中几个最核心最基本的方法,LinkedList的创建添加和删除基本都和这几个操作有关。 linkFirst() method首先我们介绍第一个方法,linkFirst(),顾名思义,这个方法是插入第一个节点,我们先直接上代码,看看它的具体实现1234567891011121314/** * Links e as first element. */ private void linkFirst(E e) { final Node<E> f = first; final Node<E> newNode = new Node<>(null, e, f); first = newNode; if (f == null) last = newNode; else f.prev = newNode; size++; modCount++; } 我们发现出现了两个变量,first和last这两个变量是LinkedList的成员变量,分别指向头结点和尾节点。他们是如下定义的: 123456/** * Pointer to first node. * Invariant: (first == null && last == null) || * (first.prev == null && first.item != null) */ transient Node<E> first; 123456/** * Pointer to last node. * Invariant: (first == null && last == null) || * (last.next == null && last.item != null) */ transient Node<E> last; 我们可以看到注释中的内容。first和last需要维持一个不变量,也就是first和last始终都要维持两种状态:首先,如果双端链表为空的时候,两个都必须为null如果链表不为空,那么first的前驱节点一定是null,first的item一定不为null,同理,last的后继节点一定是null,last的item一定不为null。 知道了first和last之后,我们就可以开始分析linkFirst的代码了。linkFirst的作用就是在first节点的前面插入一个节点,插入完之后,还要更新first节点为新插入的节点,并且同时维持last节点的不变量。 我们开始分析代码,首先用f来临时保存未插入前的first节点,然后调用的node的构造函数新建一个值为e的新节点,这个节点插入之后将作为first节点,所以新节点的前驱节点为null,值为e,后继节点是f,也就是未插入前的first节点。然后就是维持不变量,首先第一种情况,如果f==null,那就说明插入之前,链表是空的,那么新插入的节点不仅是first节点还是last节点,所以我们要更新last节点的状态,也就是last现在要指向新插入的newNode。如果f!=null那么就说明last节点不变,但是要更新f的前驱节点为newNode,维持first节点的不变量。最后size加一就完成了操作。 linkLast() method分析了linkFirst方法,对于 linkLast()的代码就很容易理解了,只不过是变成了插入到last节点的后面。我们直接看代码 1234567891011121314/** * Links e as last element. */ void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; } 到这里我们发现有这个两个方法,我们已经可以实现一个简单队列的插入操作,上面两个方法就可以理解为插入队头元素和队尾元素,这也说明了LinkedList是实现了Deque接口的。从源码中也可以看出,addfirst和addLast这两个方法内部就是直接调用了linkFirst和LinkLast12345678910111213141516171819/** * Inserts the specified element at the beginning of this list. * * @param e the element to add */ public void addFirst(E e) { linkFirst(e); } /** * Appends the specified element to the end of this list. * * <p>This method is equivalent to {@link #add}. * * @param e the element to add */ public void addLast(E e) { linkLast(e); } linkBefore(E e, Node succ)下面我们看一个linkBefore方法,从名字可以看出这个方法是在给定的节点前插入一个节点,可以说是linkFirst和linkLast方法的通用版。123456789101112131415/** * Inserts element e before non-null Node succ. */ void linkBefore(E e, Node<E> succ) { // assert succ != null; final Node<E> pred = succ.prev; final Node<E> newNode = new Node<>(pred, e, succ); succ.prev = newNode; if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++; } 我们可以看到代码的实现原理基本和前面的两个方法一致,这里是假设插入的这个节点的位置是非空的。 add(int index, E element)下面我们看add方法,这个方法就是最常用的,在指定下标插入一个节点。我们先来看下源码的实现,很简单1234567891011121314151617/** * Inserts the specified element at the specified position in this list. * Shifts the element currently at that position (if any) and any * subsequent elements to the right (adds one to their indices). * * @param index index at which the specified element is to be inserted * @param element element to be inserted * @throws IndexOutOfBoundsException {@inheritDoc} */ public void add(int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); } 首先判断给定的index是不是合法的,然后如果index==size,就说明要插入成为最后一个节点,直接调用linklast方法,否则就调用linkBefore方法,我们知道linkBefore需要给定两个参数,一个插入节点的值,一个指定的node,所以我们又调用了Node(index)去找到index的那个node。我们看一下Node node(int index)方法,这个方法就是找到给定index的node并返回,类似于数组的随机读取,但由于这里是链表,所以要进行查找123456789101112131415161718/** * Returns the (non-null) Node at the specified element index. */ Node<E> node(int index) { // assert isElementIndex(index); if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } } 我们看到node的实现并不是像我们想象的那样直接就线性从头查找,而是折半查找,有一个小优化,先判断index在前半段还是后半段,如果在前半段就从头开始找,如果在后半段就从后开始找,这样最坏情况也只要找一半就可以了。 LinkedList的源码实现并不复杂,我们只介绍这几个方法,相信你一定对于它的内部实现原理有了一定的了解,并且也学习到了优秀的代码书写风格和优化。对于remove操作,有兴趣的读者可以自行研究代码,它类似于add操作,也是基于三个基本方法来实现的。 unlinkFirst(Node f) unlinkLast(Node l) unlink(Node x)]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入理解Java_Runtime_Area_Java运行时数据区]]></title>
<url>%2F2017%2F08%2F05%2F%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java-Runtime-Area-Java%E8%BF%90%E8%A1%8C%E6%97%B6%E6%95%B0%E6%8D%AE%E5%8C%BA%2F</url>
<content type="text"><![CDATA[Java Runtime Area的分类 从线程的角度理解Java Runtime Area 从存储内容理解Java Runtime Area 方法区中究竟存储了哪些信息? 基本数据类型的成员变量放在jvm的哪块内存区域里? Java Runtime Area的分类Java Runtime Area主要可以分为六部分 : Program Counter (PC) Register 程序计数器 Java Virtual Machine Stacks Java虚拟机栈 Heap Memory Java堆 Method Area 方法区 Run-time Constant Pool 运行时常量池 Native Method Stacks 本地方法栈 具体的每个区域的内容和特点可以参考《深入理解Java虚拟机》,此书已经讲的很详细了。下面我们对这几个数据区域进行分类,分别从不同的视角来分析,加深我们的理解 从线程的角度理解Java Runtime Area首先,我们从区域是否是线程私有的还是所有线程共享的来分类: 程序计数器 Java虚拟机栈 本地方法栈都是线程私有的而Java堆**方法区**运行时常量池都是所有线程共享的 进一步理解: 对于线程私有的数据区域程序计数器 Java虚拟机栈 本地方法栈,他们的生存周期都是一致的,都是随着线程开始,而进行初始化随着线程结束而销毁 而对于线程共享的数据区域Java堆**方法区**运行时常量池,他们的生存周期都是一致的随着JVM的启动而分配内存随着JVM的关闭而销毁 从存储内容理解Java Runtime Area下面我们再根据不同区域所存储的数据类型进行分类:可以分为三类 方法区和常量池存储类的信息 堆内存存储对象信息 程序计数器,Java虚拟机栈,本地方法栈存储线程的信息 下图很清楚的说明 The heap space holds object data, the method area holds class code, and the native area holds references to the code and object data.堆存储object的data,方法区存储class的信息和code,native区域存储指向class信息和code的引用和指向对象的data的引用 下面这个图更详细的指出了三个区域存储的内容: 下面我们通过一个实际代码的例子,来说明; 看下面这段代码: 这段代码编译之后,就存储成如下这个样子: 易混淆的Java Runtime Area 的问题下面我们会对关于Java 运行时数据区易混淆的问题进行释疑 方法区中究竟存储了哪些信息?栈中存放了局部变量表等与方法有关的信息,但方法中还有指令代码这一重要内容,它既没有放在栈(Stack)中也没放在堆(Heap)中,那它放在哪呢?其实,方法区中除了包括你所说的“已加载的类的基本信息、常量、静态变量等”外,还包括编译器编译后的代码,而且这应该是方法区中主要的一部分,毕竟类中主要是方法和属性,而类中的属性,如果是实例域的话则新建对象后存储在堆(Heap)中,静态的话就如你所说存储在方法区中,因此该区域中方法占主要部分,这应该是此运行时数据区称为方法区的原因吧。 基本数据类型的成员变量放在jvm的哪块内存区域里?比如123class{private int i;} 有的朋友可能因为基本数据类型,就认为存储在栈中。但其实是存储在堆中的,因为这是属于对象的信息,每个对象都拥有不同的实例变量,这些实例变量都存储在堆中,不管是基本数据类型还是引用数据类型ava虚拟机栈是线程私有的,生命周期跟线程相同,每个方法调用的时候都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每个方法调用的过程,就代表了一个栈帧在虚拟机栈中入栈到出栈的过程,当进入一个方法时,这个方法在栈中需要分配多大的内存都是完全确定的,方法运行时不会改变局部变量表的大小——《深入理解java虚拟机第二版》很多java程序员一开始就被网上的一些教程所误导:基本数据类型放在栈中,数组和类的实例放在堆中。 这个说法不准确,事实上,如上面的实例变量i,他是存放在java堆中。因为它不是静态的变量,不会独立于类的实例而存在,而该类实例化之后,放在堆中,当然也包含了它的属性i。如果在方法中定义了int i = 0;则在局部变量表创建了两个对象:引用i和0。 这两个对象都是线程私有(安全)的。 比如定义了int[] is = new int[10]. 定义了两个对象,一个是is引用,放在局部变量表中,一个是长度为10的数组,放在堆中,这个数组,只能通过is来访问,方法结束后出栈,is被销毁,根据java的根搜索算法,判断数组不可达,就将它销毁了。]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
<tag>JVM</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java源码剖析之ArrayList]]></title>
<url>%2F2017%2F08%2F05%2FJava%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90%E4%B9%8BArrayList%2F</url>
<content type="text"><![CDATA[ArrayList使用的存储的数据结构 ArrayList的初始化 ArrayList是如何动态增长 ArrayList如何实现元素的移除 ArrayList小结 ArrayList是我们经常使用的一个数据结构,我们通常把其用作一个可变长度的动态数组使用,大部分时候,可以替代数组的作用,我们不用事先设定ArrayList的长度,只需要往里不断添加元素即可,ArrayList会动态增加容量。ArrayList是作为List接口的一个实现。那么ArrayList背后使用的数据结构是什么呢?ArrayList是如何保证动态增加容量,使得能够正确添加元素的呢? 要回答上面的问题,我们就需要对ArrayList的源码进行一番分析,深入了解其实现原理的话,我们就自然能够解答上述问题。 需要说明的是,本文所分析的源码引用自JDK 8版本 ArrayList使用的存储的数据结构从源码中我们可以发现,ArrayList使用的存储的数据结构是Object的对象数组。其实这也不能想象,我们知道ArrayList是支持随机存取的类似于数组,所以自然不可能是链表结构。1234567/** * The array buffer into which the elements of the ArrayList are stored. * The capacity of the ArrayList is the length of this array buffer. Any * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA * will be expanded to DEFAULT_CAPACITY when the first element is added. */ transient Object[] elementData; // non-private to simplify nested class access 我想大家一定对这里出现的transient关键字很疑惑,我们都知道ArrayList对象是可序列化的,但这里为什么要用transient关键字修饰它呢?查看源码,我们发现ArrayList实现了自己的readObject和writeObject方法,所以这保证了ArrayList的可序列化。具体序列化的知识我们在此不过多赘述。有兴趣的读者可以参考笔者关于序列化的文章。 ArrayList的初始化ArrayList提供了三个构造函数。下面我们依次来分析 public ArrayList(int initialCapacity) 当我们初始化的时候,给ArrayList指定一个初始化大小的时候,就会调用这个构造方法。1List<String> myList = new ArrayList<String>(7); 源码中这个方法的实现如下1234567891011121314151617/** * Constructs an empty list with the specified initial capacity. * * @param initialCapacity the initial capacity of the list * @throws IllegalArgumentException if the specified initial capacity * is negative */ public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } } 这里的EMPTY_ELEMENTDATA 实际上就是一个共享的空的Object数组对象。1234/** * Shared empty array instance used for empty instances. */ private static final Object[] EMPTY_ELEMENTDATA = {}; 上述代码很容易理解,如果用户指定的初始化容量大于0,就new一个相应大小的数组,如果指定的大小为0,就复制为共享的那个空的Object数组对象。如果小于0,就直接抛出异常。 public ArrayList() 默认的空的构造函数。我们一般会这么使用1myList = new ArrayList(); 源码中的实现是123456/** * Constructs an empty list with an initial capacity of ten. */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } 其中DEFAULTCAPACITY_EMPTY_ELEMENTDATA 定义为123456/** * Shared empty array instance used for default sized empty instances. We * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when * first element is added. */ private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 注释中解释的很清楚,就是说刚初始化的时候,会是一个共享的类变量,也就是一个Object空数组,当第一次add的时候,这个数组就会被初始化一个大小为10的数组。 public ArrayList(Collection<? extends E> c) 如果我们想要初始化一个list,这个list包含另外一个特定的collection的元素,那么我们就可以调用这个构造函数。我们通常会这么使用123456Set<Integer> set = new HashSet<>(); set.add(1); set.add(2); set.add(3); set.add(4);ArrayList<Integer> list = new ArrayList<>(set); 源码中是这么实现的12345678910111213141516171819/** * Constructs a list containing the elements of the specified * collection, in the order they are returned by the collection's * iterator. * * @param c the collection whose elements are to be placed into this list * @throws NullPointerException if the specified collection is null */ public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } } 首先调用给定的collection的toArray方法将其转换成一个Array。然后根据这个array的大小进行判断,如果不为0,就调用Arrays的copyOf的方法,复制到Object数组中,完成初始化,如果为0,就直接初始化为空的Object数组。 ArrayList是如何动态增长当我们像一个ArrayList中添加数组的时候,首先会先检查数组中是不是有足够的空间来存储这个新添加的元素。如果有的话,那就什么都不用做,直接添加。如果空间不够用了,那么就根据原始的容量增加原始容量的一半。源码中是如此实现的:1234567891011/** * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return <tt>true</tt> (as specified by {@link Collection#add}) */ public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } ensureCapacityInternal的实现如下:1234567private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } DEFAULT_CAPACITY为:1private static final int DEFAULT_CAPACITY = 10; 这也就实现了当我们不指定初始化大小的时候,添加第一个元素的时候,数组会扩容为10.1234567private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } 这个函数判断是否需要扩容,如果需要就调用grow方法扩容1234567891011121314151617/** * Increases the capacity to ensure that it can hold at least the * number of elements specified by the minimum capacity argument. * * @param minCapacity the desired minimum capacity */ private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); } 我们可以看到grow方法将数组扩容为原数组的1.5倍,调用的是Arrays.copy方法。在jdk6及之前的版本中,采用的还不是右移的方法1int newCapacity = (oldCapacity * 3)/2 + 1; 现在已经优化成右移了。 ArrayList如何实现元素的移除我们移除元素的时候,有两种方法,一是指定下标,二是指定对象12list.remove(3);//indexlist.remove("aaa");//object 下面先来分析第一种,也就是 public E remove(int index)源码中是如此实现的1234567891011121314151617181920212223/** * Removes the element at the specified position in this list. * Shifts any subsequent elements to the left (subtracts one from their * indices). * * @param index the index of the element to be removed * @return the element that was removed from the list * @throws IndexOutOfBoundsException {@inheritDoc} */ public E remove(int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work return oldValue; } 对于数组的元素删除算法我们应该很熟悉,删除一个数组元素,我们需要将这个元素后面的元素全部向前移动,并将size减1.我们看到源码中,首先检查下标是否在可用范围内。然后调用System.arrayCopy方法将右边的数组向左移动,并且将size减一,并置为null。 public boolean remove(Object o)源码中实现如下: 1234567891011121314151617181920212223242526272829/** * Removes the first occurrence of the specified element from this list, * if it is present. If the list does not contain the element, it is * unchanged. More formally, removes the element with the lowest index * <tt>i</tt> such that * <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt> * (if such an element exists). Returns <tt>true</tt> if this list * contained the specified element (or equivalently, if this list * changed as a result of the call). * * @param o element to be removed from this list, if present * @return <tt>true</tt> if this list contained the specified element */ public boolean remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; } 我们可以看到,这个remove方法会移除数组中第一个符合的给定对象,如果不存在就什么也不做,如果存在多个只移除第一个。fastRemove方法如下1234567891011121314151617181920212223242526272829/** * Removes the first occurrence of the specified element from this list, * if it is present. If the list does not contain the element, it is * unchanged. More formally, removes the element with the lowest index * <tt>i</tt> such that * <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt> * (if such an element exists). Returns <tt>true</tt> if this list * contained the specified element (or equivalently, if this list * changed as a result of the call). * * @param o element to be removed from this list, if present * @return <tt>true</tt> if this list contained the specified element */ public boolean remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; } 可以理解为简化版的remove(index)方法。 ArrayList小结 ArrayList是List接口的一个可变大小的数组的实现 ArrayList的内部是使用一个Object对象数组来存储元素的 初始化ArrayList的时候,可以指定初始化容量的大小,如果不指定,就会使用默认大小,为10 当添加一个新元素的时候,首先会检查容量是否足够添加这个元素,如果够就直接添加,如果不够就进行扩容,扩容为原数组容量的1.5倍 当删除一个元素的时候,会将数组右边的元素全部左移]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入解析Java垃圾回收机制]]></title>
<url>%2F2017%2F08%2F05%2F%E6%B7%B1%E5%85%A5%E8%A7%A3%E6%9E%90Java%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6%2F</url>
<content type="text"><![CDATA[引入垃圾回收 哪些内存需要回收? 引用计数法 可达性分析 如何回收 Marking 标记 Normal Deletion 清除 Deletion with Compacting 压缩 为什么需要分代收集? JVM的分代 新生代 老年代 永久代 分代垃圾收集过程详述 引入垃圾回收 程序计数器、 虚拟机栈、 本地方法栈3个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。 每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(尽管在运行期会由JIT编译器进行一些优化,但在本章基于概念模型的讨论中,大体上可以认为是编译期可知的),因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。 而Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存—–《深入理解Java虚拟机》 自动垃圾回收机制就是寻找Java堆中的对象,并对对象进行分类判别,寻找出正在使用的对象和已经不会使用的对象,然后把那些不会使用的对象从堆上清除。自动垃圾回收机制就是要解决三个问题: 哪些内存需要回收? 什么时候回收? 如何回收? 哪些内存需要回收?引用计数法对于第一个问题,也就是判断是否还需要使用,最简单的方法就是通过目前是否有引用指向这个对象,如果没有就说明这个对象不会再被使用了,如果有就说明这个对象可能还会继续被使用,这种通过引用是否存在的方法就叫做引用计数法,但这个方法存在一个问题就是无法解决对象循环引用的问题,因此又出现了可达性分析的方法来判断对象是否可以被会回收。 ##可达性分析这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。在Java语言中,可作为GC Roots的对象包括下面几种: 虚拟机栈(栈帧中的本地变量表)中引用的对象。 方法区中类静态属性引用的对象。 方法区中常量引用的对象。 本地方法栈中JNI(即一般说的Native方法)引用的对象。 如何回收垃圾收集器通常会帮我们在后台自动进行垃圾回收。关于具体的回收过程只要有以下这些步骤 Step 1: Marking 标记 第一步就是标记,也就是垃圾收集器会找出那些需要回收的对象所在的内存和不需要回收的对象所在的内存,并把它们标记出来,简单的说,也就是先找出垃圾在哪 所有堆中的对象都会被扫描一遍,以此来确定回收的对象,所以这通常会是一个相对比较耗时的过程 Step 2: Normal Deletion垃圾收集器会清除掉上一步标记出来的那些需要回收的对象区域 存在的问题就是碎片问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。 Step 2a: Deletion with Compacting 压缩由于简单的清除可能会存在碎片的问题,所以又出现了压缩清除的方法,也就是先清除需要回收的对象,然后再对内存进行压缩操作,将内存分成可用和不可用两大部分 为什么需要分代收集?就像前文所述,标记对象和压缩内存的过程在JVM中是不高效的,分配的对象越多,垃圾收集的时间就越长。但是,经过一些经验型性的统计分析表明,一个程序中大部分对象都是短命的! 下图就是一个类似的统计数据,纵坐标表示分配对象所占用的内存大小,横坐标表示自分配对象过去的时间 从图中我们看到,大部分对象没活多久就死了,存活较久的只是少类对象 JVM的分代为了增大垃圾收集的效率,所以JVM将堆进行分代,分为不同的部分,一般有三部分,新生代,老年代和永久代 新生代所有新new出来的对象都会最先出现在新生代中,当新生代这部分内存满了之后,就会发起一次垃圾收集事件,这种发生在新生代的垃圾收集称为Minor collections。这种收集通常比较快,因为新生代的大部分对象都是需要回收的,那些暂时无法回收的就会被移动到老年代。 Stop the World事件-所有minor garbage collections都是Stop the World事件,也就是意味着所有的应用线程都需要停止,直到垃圾回收的操作全部完成。类似于“你妈妈在给你打扫房间的时候,肯定也会让你老老实实地在椅子上或者房间外待着,如果她一边打扫,你一边乱扔纸屑,这房间还能打扫完?” 老年代老年代用来存储那些存活时间较长的对象。一般来说,我们会给新生代的对象限定一个存活的时间,当达到这个时间还没有被收集的时候就会被移动到老年代中。老年代区域的垃圾收集叫做major garbage collection Major garbage collection也是一个Stop the World事件。通常Major garbage collection都相对比较慢,因为老年代的收集包括了对所有对象的收集,也就是同时需要收集新生代和老年代的对象。 永久代The Permanent generation contains metadata required by the JVM to describe the classes and methods used in the application. The permanent generation is populated by the JVM at runtime based on classes in use by the application. In addition, Java SE library classes and methods may be stored here. Classes may get collected (unloaded) if the JVM finds they are no longer needed and space may be needed for other classes. The permanent generation is included in a full garbage collection. 分代垃圾收集过程详述我们已经知道垃圾回收所需要的方法和堆内存的分代,那么接下来我们就来具体看一下垃圾回收的具体过程 第一步 所有new出来的对象都会最先分配到新生代区域中,两个survivor区域初始化是为空的 第二步,当eden区域满了之后,就引发一次 minor garbage collection 第三步,当在minor garbage collection,存活下来的对象就会被移动到S0survivor区域 第四步,然后当eden区域又填满的时候,又会发生下一次的垃圾回收,存活的对象会被移动到survivor区域而未存活对象会被直接删除。但是,不同的是,在这次的垃圾回收中,存活对象和之前的survivor中的对象都会被移动到s1中。一旦所有对象都被移动到s1中,那么s2中的对象就会被清除,仔细观察图中的对象,数字表示经历的垃圾收集的次数。目前我们已经有不同的年龄对象了。 第五步,下一次垃圾回收的时候,又会重复上次的步骤,清除需要回收的对象,并且又切换一次survivor区域,所有存活的对象都被移动至s0。eden和s1区域被清除。 第六步,重复以上步骤,并记录对象的年龄,当有对象的年龄到达一定的阈值的时候,就将新生代中的对象移动到老年代中。在本例中,这个阈值为8. 第七步,接下来垃圾收集器就会重复以上步骤,不断的进行对象的清除和年代的移动 最后,我们观察上述过程可以发现,大部分的垃圾收集过程都是在新生代进行的,直到老年代中的内存不够用了才会发起一次 major GC,会进行标记和整理压缩。]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
<tag>JVM</tag>
</tags>
</entry>
<entry>
<title><![CDATA[浅谈Java为什么需要NIO]]></title>
<url>%2F2017%2F08%2F01%2F%E6%B5%85%E8%B0%88Java%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81NIO%2F</url>
<content type="text"><![CDATA[IO NIO 小结 I/OI/O ? 或者输入/输出 ? 指的是计算机与外部世界或者一个程序与计算机的其余部分的之间的接口。它对于任何计算机系统都非常关键,因而所有 I/O 的主体实际上是内置在操作系统中的。单独的程序一般是让系统为它们完成大部分的工作。在 Java 编程中,直到最近一直使用 流 的方式完成 I/O。所有 I/O 都被视为单个的字节的移动,通过一个称为 Stream 的对象一次移动一个字节。流 I/O 用于与外部世界接触。它也在内部使用,用于将对象转换为字节,然后再转换回对象。传统流IO的好处是使用简单,将底层的机制都抽象成流,但缺点就是性能不足。而且IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。以socket.read()为例子:传统的BIO里面socket.read(),如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。 NIO为什么要使用 NIO?NIO 的创建目的是为了让 Java 程序员可以实现高速 I/O 而无需编写自定义的本机代码。NIO 将最耗时的 I/O 操作(即填充和提取缓冲区)转移回操作系统,因而可以极大地提高速度。 流与块的比较原来的 I/O 库(在 java.io.*中) 与 NIO 最重要的区别是数据打包和传输的方式。正如前面提到的,原来的 I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。面向流 的 I/O 系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。为流式数据创建过滤器非常容易。链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的 I/O 通常相当慢。一个 面向块 的 I/O 系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。 NIO的buffer机制NIO性能的优势就来源于缓冲的机制,不管是读或者写都需要以块的形式写入到缓冲区中。NIO实际上让我们对IO的操作更接近于操作系统的实际过程。所有的系统I/O都分为两个阶段:等待就绪和操作。举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。以socket为例:先从应用层获取数据到内核的缓冲区,然后再从内核的缓冲区复制到进程的缓冲区。所以实际上底层的机制也是不断利用缓冲区来读写数据的。即使传统IO抽象成了从流直接读取数据,但本质上也依然是利用缓冲区来读取和写入数据。所以,为了更好的理解nio,我们就需要知道IO的底层机制,这样对我们将来理解channel和buffer就打下了基础。这里简单提一下,我们可以把bufffer就理解为内核缓冲区,所以不论读写,自然都要经过这个区域,读的话,先从设备读取数据到内核,再读到进程缓冲区,写的话,先从进程缓冲区写到内核,再从内核写回设备。 NIO的非阻塞机制NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。 下图是几种常见I/O模型的对比: 以socket.read()为例子: 传统的BIO里面socket.read(),如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。 对于NIO,如果TCP RecvBuffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。所以我们可以NIO实现同时监听多个IO通道,然后不断的轮询寻找可以读写的设备。 NIO的IO模型可以理解为是IO多路复用模型和非阻塞模型,同时还有事件驱动模型。这里需要知道一点,就是IO多路复用是一定需要实现非阻塞的。 小结NIO相对于IO流的优势: 非阻塞 buffer机制 流替代块 参考: https://tech.meituan.com/nio.html http://www.importnew.com/19816.html https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
<tag>NIO</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JavaNIO之Channel和Buffer]]></title>
<url>%2F2017%2F08%2F01%2FJavaNIO%E4%B9%8BChannel%E5%92%8CBuffer%2F</url>
<content type="text"><![CDATA[Channel Channel Characteristics Java NIO Channel Classes buffer 什么是缓冲区? 缓冲区类型 缓冲区内部细节 NIO Buffer Characteristics How to Read from NIO Buffer How to Write to NIO Buffer Java NIO 读写文件实例程序 Channel Java NIO中,channel用于数据的传输。类似于传统IO中的流的概念。channel的两端是buffer和一个entity,不同于IO中的流,channel是双向的,既可以写入,也可以读取。而流则是单向的,所以channel更加灵活。我们在读取数据或者写入数据的时候,都必须经过channel和buffer,也就是说,我们在读取数据的时候,先利用channel将IO设备中的数据读取到buffer,然后从buffer中读取,我们在写入数据的时候,先将数据写入到buffer,然后buffer中的数据再通过channel传到IO设备中。 我们知道NIO的特点就是将IO操作更加类似于底层IO的流程。我们可以通过底层IO的机制更好的理解channel。 所有的系统I/O都分为两个阶段:等待就绪和操作。 等待就绪就是从IO设备将数据读取到内核中的过程。 操作就是将数据从内核复制到进程缓冲区的过程。 channel就可以看作是IO设备和内核区域的一个桥梁,凡是与IO设备交互都必须通过channel,而buffer就可以看作是内核缓冲区。这样整个过程就很好理解了。 我们看一下读取的过程先从IO设备,网卡或者磁盘将内容读取到内核中,对应于NIO就是从网卡或磁盘利用channel将数据读到buffer中然后就是内核中的数据复制到进程缓冲区,对应于就是从buffer中读取数据 写入的过程则是:先从进程将数据写到内核中,对应于就是进程将数据写入到buffer中,然后内核中的数据再写入到网卡或者磁盘中,对应于就是,buffer中的数据利用channel传输到IO设备中。 以上其实就是NIO基本的利用channel和buffer进行读取和写入的流程。 Channel Characteristics 与传统IO中的流不同,channel是双向的,可读可写 channel从buffer中读取数据,写入数据也是先写入到buffer channel可以实现异步读写操作 channel可以设置为阻塞和非阻塞的模式 非阻塞模式意味着,当读不到数据或者缓冲区已满无法写入的时候,不会把线程睡眠 只有socket的channel可以设置为非阻塞模式,文件的channel是无法设置的。文件的IO一定是阻塞的 如果是文件channel的话,channel可以在channel之间传输数据 Java NIO Channel Classeschannel主要有两大类,四个具体的类 FileChannel文件的读写是不可以设置为非阻塞模式 SocketChannel根据tcp和udp,服务端和客户端,又可以分为, SocketChannel, ServerSocketChannel and DatagramChannel.它们是可以设置为非阻塞模式的 buffer什么是缓冲区?Buffer 是一个对象, 它包含一些要写入或者刚读出的数据。 在 NIO 中加入 Buffer 对象,体现了新库与原 I/O 的一个重要区别。在面向流的 I/O 中,您将数据直接写入或者将数据直接读到 Stream 对象中。在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。任何时候访问 NIO 中的数据,您都是将它放到缓冲区中。缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。但是一个缓冲区不 仅仅 是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。 缓冲区类型最常用的缓冲区类型是 ByteBuffer。一个 ByteBuffer 可以在其底层字节数组上进行 get/set 操作(即字节的获取和设置)。ByteBuffer 不是 NIO 中唯一的缓冲区类型。事实上,对于每一种基本 Java 类型都有一种缓冲区类型: ByteBuffer CharBuffer ShortBuffer IntBuffer LongBuffer FloatBuffer DoubleBuffer 每一个 Buffer 类都是 Buffer 接口的一个实例。 除了 ByteBuffer,每一个 Buffer 类都有完全一样的操作,只是它们所处理的数据类型不一样。因为大多数标准 I/O 操作都使用 ByteBuffer,所以它具有所有共享的缓冲区操作以及一些特有的操作。 缓冲区内部细节本节将介绍 NIO 中两个重要的缓冲区组件:状态变量和访问方法 (accessor)。状态变量是前一节中提到的”内部统计机制”的关键。每一个读/写操作都会改变缓冲区的状态。通过记录和跟踪这些变化,缓冲区就可能够内部地管理自己的资源。在从通道读取数据时,数据被放入到缓冲区。在有些情况下,可以将这个缓冲区直接写入另一个通道,但是在一般情况下,您还需要查看数据。这是使用 访问方法 get() 来完成的。同样,如果要将原始数据放入缓冲区中,就要使用访问方法 put()。 状态变量可以用三个值指定缓冲区在任意时刻的状态:position,limit,capacity这三个变量一起可以跟踪缓冲区的状态和它所包含的数据。我们将在下面的小节中详细分析每一个变量,还要介绍它们如何适应典型的读/写(输入/输出)进程。在这个例子中,我们假定要将数据从一个输入通道拷贝到一个输出通道。 Position您可以回想一下,缓冲区实际上就是美化了的数组。在从通道读取时,您将所读取的数据放到底层的数组中。 position 变量跟踪已经写了多少数据。更准确地说,它指定了下一个字节将放到数组的哪一个元素中。因此,如果您从通道中读三个字节到缓冲区中,那么缓冲区的 position 将会设置为3,指向数组中第四个元素。同样,在写入通道时,您是从缓冲区中获取数据。 position 值跟踪从缓冲区中获取了多少数据。更准确地说,它指定下一个字节来自数组的哪一个元素。因此如果从缓冲区写了5个字节到通道中,那么缓冲区的 position 将被设置为5,指向数组的第六个元素。 Limitlimit 变量表明还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。position 总是小于或者等于 limit。 Capacity缓冲区的 capacity 表明可以储存在缓冲区中的最大数据容量。实际上,它指定了底层数组的大小 ― 或者至少是指定了准许我们使用的底层数组的容量。limit 决不能大于 capacity。 实例:我们首先观察一个新创建的缓冲区。出于本例子的需要,我们假设这个缓冲区的 总容量 为8个字节。 Buffer 的状态如下所示: 回想一下 ,limit 决不能大于 capacity,此例中这两个值都被设置为 8。我们通过将它们指向数组的尾部之后(如果有第8个槽,则是第8个槽所在的位置)来说明这点。 position 设置为0。如果我们读一些数据到缓冲区中,那么下一个读取的数据就进入 slot 0 。如果我们从缓冲区写一些数据,从缓冲区读取的下一个字节就来自 slot 0 。 position 设置如下所示: 由于 capacity 不会改变,所以我们在下面的讨论中可以忽略它。第一次读取现在我们可以开始在新创建的缓冲区上进行读/写操作。首先从输入通道中读一些数据到缓冲区中。第一次读取得到三个字节。它们被放到数组中从 position 开始的位置,这时 position 被设置为 0。读完之后,position 就增加到 3,如下所示: limit 没有改变。第二次读取在第二次读取时,我们从输入通道读取另外两个字节到缓冲区中。这两个字节储存在由 position 所指定的位置上, position 因而增加 2: limit 没有改变。flip现在我们要将数据写到输出通道中。在这之前,我们必须调用 flip() 方法。这个方法做两件非常重要的事:它将 limit 设置为当前 position。它将 position 设置为 0。前一小节中的图显示了在 flip 之前缓冲区的情况。下面是在 flip 之后的缓冲区: 我们现在可以将数据从缓冲区写入通道了。 position 被设置为 0,这意味着我们得到的下一个字节是第一个字节。 limit 已被设置为原来的 position,这意味着它包括以前读到的所有字节,并且一个字节也不多。第一次写入在第一次写入时,我们从缓冲区中取四个字节并将它们写入输出通道。这使得 position 增加到 4,而 limit 不变,如下所示: 第二次写入我们只剩下一个字节可写了。 limit在我们调用 flip() 时被设置为 5,并且 position 不能超过 limit。所以最后一次写入操作从缓冲区取出一个字节并将它写入输出通道。这使得 position增加到 5,并保持 limit 不变,如下所示: clear最后一步是调用缓冲区的 clear() 方法。这个方法重设缓冲区以便接收更多的字节。 Clear 做两种非常重要的事情:它将 limit 设置为与 capacity 相同。它设置 position 为 0。下图显示了在调用 clear() 后缓冲区的状态: 缓冲区现在可以接收新的数据了。 NIO Buffer Characteristics buffer是java NIO中的块的基础 buffer可以提供一个固定大小的容器来读取和写入数据 任意一个buffer都是可读的,只有选中的buffer才可写 buffer是channel的端点 在只读的模式下,buffer的内容不可变,但是的他/她的几个变量,position,limit都是可变的 默认情况下,buffer不是线程安全的 How to Read from NIO Buffer 首先创建一个指定大小的buffer 1ByteBuffer byteBuffer = ByteBuffer.allocate(512); 将buffer转换为读模式 1byteBuffer.flip(); 然后从channel中读取数据到buffer中 1int numberOfBytes = fileChannel.read(byteBuffer); 用户从buffer中读取数据 1char c = (char)byteBuffer.get(); How to Write to NIO Buffer Create a buffer by allocating a size. 1ByteBuffer byteBuffer = ByteBuffer.allocate(512);//512 becomes the capacity Put data into buffer 1byteBuffer.put((byte) 0xff); Java NIO 读写文件实例程序下面的程序实现了一个简单的利用buffer和channel读取数据 1234567891011121314151617181920212223242526272829303132333435363738394041424344package Channel;import java.io.IOException;import java.nio.ByteBuffer;import java.nio.channels.FileChannel;import java.nio.file.Path;import java.nio.file.Paths;import java.nio.file.StandardOpenOption;public class BufferExample { public static void main(String[] args) throws IOException { Path path = Paths.get("temp.data"); write(path); read(path); } private static void write(Path path) throws IOException { String input = "NIO Buffer Hello World!"; byte[] inputBytes = input.getBytes(); ByteBuffer byteBuffer = ByteBuffer.allocate(inputBytes.length); byteBuffer.put(inputBytes); byteBuffer.flip(); FileChannel channelWrite = FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.WRITE); channelWrite.write(byteBuffer); channelWrite.close(); } private static void read(Path path) throws IOException { FileChannel channelRead = FileChannel.open(path); ByteBuffer byteBuffer = ByteBuffer.allocate(512); int readBytes = channelRead.read(byteBuffer); if(readBytes > 0) { byteBuffer.flip(); byte[] bytes = new byte[byteBuffer.remaining()]; byteBuffer.get(bytes); String fileContent = new String(bytes, "utf-8"); System.out.println("File Content: " + fileContent); } channelRead.close(); }} 参考 http://javapapers.com/java/java-nio-buffer/ http://www.importnew.com/19816.html https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
<tag>NIO</tag>
</tags>
</entry>
<entry>
<title><![CDATA[细谈Select,Poll,Epoll]]></title>
<url>%2F2017%2F07%2F31%2F%E7%BB%86%E8%B0%88Select-Poll-Epoll%2F</url>
<content type="text"><![CDATA[阻塞 io 模型 blocking IO 非阻塞 io 模型 nonblocking IO io多路复用模型 IO multiplexing 细谈 io 多路复用技术 select 和poll 细谈事件驱动–epoll 总结 操作系统在处理io的时候,主要有两个阶段: 等待数据传到io设备 io设备将数据复制到user space 我们一般将上述过程简化理解为: 等到数据传到kernel内核space kernel内核区域将数据复制到user space(理解为进程或者线程的缓冲区) 而根据这两个阶段而不同的操作方法,就会产生多种io模型,本文只讨论select,poll,epoll,所以只引出三种io模型。 阻塞 io 模型 blocking IO最常用的也就是阻塞io模型。默认情况下,所有文件操作都是阻塞的。我们以套接字接口为例来讲解此模型,在进程空间调用recvfrom,其系统调用知道数据包到达并且被复制到进程缓冲中或者发生错误时才会返回,在此期间会一直阻塞,所以进程在调用recvfrom开始到它返回的整段时间都是阻塞的,因此称之为阻塞io模型。 注意:在阻塞狀態下,程序是不會浪費CPU的,cpu只是不执行io操作了,还会去做别的。 应用层有数据过来,会调用recvfrom方法,但是这个时候应用层的数据还没复制到kernel中,将应用层数据复制到kerne这个阶段是需要时间的,所以recvfrom方法会阻塞,当内核中的数据准备好之后,recvfrom方法还不会返回,而是会发起一个系统调用将kernel中的数据复制到进程的缓冲区中,也就是user space,当这个工作完成之后,recvfrom才会返回并解除程序的阻塞。 所以我们总结可以发现,主要就是上面两个阶段 应用层数据到kernel kernel复制到user space 阻塞io模型就是将这个两个过程合并在一起,一起阻塞。而非阻塞模型则是将第一个过程的阻塞变成非阻塞,第二个阶段是系统调用,是必须阻塞的,所以非阻塞模型也是同步的,因为它们在kernel里的数据准备好之后,进行系统调用,将数据拷贝到进程缓冲区中。 非阻塞 io 模型 nonblocking IO就是对于第一个阶段,也就是应用层数据到kernel的过程中,recvfrom会轮询检查,如果kernel数据没有准备还,就返回一个EWOULDBLOCK错误。不断的轮询检查,直到发现kernel中的数据准备好了,就返回,然后进行系统调用,将数据从kernel拷贝到进程缓冲区中。有點類似busy-waiting的方法。 io多路复用模型 IO multiplexing 目的:因为阻塞模型在没有收到数据的时候就会阻塞卡住,如果一次需要接受多个socket fd的时候,就会导致必须处理完前面的fd,才能处理后面的fd,即使可能后面的fd比前面的fd还要先准备好,所以这样就会造成客户端的严重延迟。为了处理多个请求,我们自然先想到用多线程来处理多个socket fd,但是这样又会启动大量的线程,造成资源的浪费,所以这个时候就出现了io多路复用技术。就是用一个进程来处理多个fd的请求。 应用:适用于针对大量的io请求的情况,对于服务器必须在同时处理来自客户端的大量的io操作的时候,就非常适合 细谈 io 多路复用技术 select 和pollselectselect的工作流程:单个进程就可以同时处理多个网络连接的io请求(同时阻塞多个io操作)。基本原理就是程序呼叫select,然后整个程序就阻塞了,这时候,kernel就会轮询检查所有select负责的fd,当找到一个client中的数据准备好了,select就会返回,这个时候程序就会系统调用,将数据从kernel复制到进程缓冲区。 下图为select同时从多个客户端接受数据的过程 虽然服务器进程会被select阻塞,但是select会利用内核不断轮询监听其他客户端的io操作是否完成。 Poll介绍poll的原理与select非常相似,差别如下: 描述fd集合的方式不同,poll使用 pollfd 结构而不是select结构fd_set结构,所以poll是链式的,没有最大连接数的限制 poll有一个特点是水平触发,也就是通知程序fd就绪后,这次没有被处理,那么下次poll的时候会再次通知同个fd已经就绪。 select缺点 根据fd_size的定义,它的大小为32个整数大小(32位机器为32*32,所有共有1024bits可以记录fd),每个fd一个bit,所以最大只能同时处理1024个fd 每次要判断【有哪些event发生】这件事的成本很高,因为select(polling也是)采取主动轮询机制 1.每一次呼叫 select( ) 都需要先从 user space把 FD_SET复制到 kernel(约线性时间成本)为什么 select 不能像epoll一样,只做一次复制就好呢?每一次呼叫 select()前,FD_SET都可能更动,而 epoll 提供了共享记忆存储结构,所以不需要有 kernel 與 user之间的数据沟通 2.然后kernel还要轮询每个fd,约线性时间 假设现实中,有1百万个客户端同时与一个服务器保持着tcp连接,而每一个时刻,通常只有几百上千个tcp连接是活跃的,这时候我们仍然使用select/poll机制,kernel必须在搜寻完100万个fd之后,才能找到其中状态是active的,这样资源消耗大而且效率低下。 对于select和poll的上述缺点,就引进了一种新的技术,epoll技术 细谈事件驱动–epollepoll 提供了三个函数: int epoll_create(int size);建立一個 epoll 对象,并传回它的id int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);事件注册函数,将需要监听的事件和需要监听的fd交给epoll对象 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);等待注册的事件被触发或者timeout发生 epoll解决的问题: epoll没有fd数量限制epoll没有这个限制,我们知道每个epoll监听一个fd,所以最大数量与能打开的fd数量有关,一个g的内存的机器上,能打开10万个左右 epoll不需要每次都从user space 将fd set复制到内核kernelepoll在用epoll_ctl函数进行事件注册的时候,已经将fd复制到内核中,所以不需要每次都重新复制一次 select 和 poll 都是主動輪詢機制,需要拜訪每一個 FD;epoll是被动触发方式,给fd注册了相应事件的时候,我们为每一个fd指定了一个回调函数,当数据准备好之后,就会把就绪的fd加入一个就绪的队列中,epoll_wait的工作方式实际上就是在这个就绪队列中查看有没有就绪的fd,如果有,就唤醒就绪队列上的等待者,然后调用回调函数。 虽然epoll。poll。epoll都需要查看是否有fd就绪,但是epoll之所以是被动触发,就在于它只要去查找就绪队列中有没有fd,就绪的fd是主动加到队列中,epoll不需要一个个轮询确认。换一句话讲,就是select和poll只能通知有fd已经就绪了,但不能知道究竟是哪个fd就绪,所以select和poll就要去主动轮询一遍找到就绪的fd。而epoll则是不但可以知道有fd可以就绪,而且还具体可以知道就绪fd的编号,所以直接找到就可以,不用轮询。 总结 select, poll是为了解決同时大量IO的情況(尤其网络服务器),但是随着连接数越多,性能越差 epoll是select和poll的改进方案,在 linux 上可以取代 select 和 poll,可以处理大量连接的性能问题]]></content>
<categories>
<category>操作系统</category>
</categories>
</entry>
<entry>
<title><![CDATA[深入理解--异步和非阻塞]]></title>
<url>%2F2017%2F07%2F28%2F%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3-%E5%BC%82%E6%AD%A5%E5%92%8C%E9%9D%9E%E9%98%BB%E5%A1%9E%2F</url>
<content type="text"><![CDATA[异步和非阻塞的概念实际上已经出现了很长一段时间。但是异步真正开始流行起来,是因为AJAX技术逐渐成为主流的web开发技术。非阻塞的概念真正流行起来,是当java引入NIO,也可以称作非阻塞IO的API,开始走进主流的开发人员的视线,真正流行起来,也可以认为是node.js带来的。 同步 ,异步,阻塞,非阻塞这几个概念相互之间联系紧密,很难区分。很多程序员都不知道它们之间的具体的不同。本文就会详细讨论这个问题,希望能帮助读者更好的了解这几个概念 同步和阻塞首先,我们先开始介绍与异步和非阻塞对立的两个概念:同步和阻塞 对于web开发者来说,理解同步的概念相对比较容易,因为HTTP协议就是一个同步的协议。web浏览器向服务器发送一个请求并且等待它的响应。收到响应之后,浏览器才可以继续向服务器发送下一个请求,并且等待响应,周而复始的重复这个过程。在发送下一个请求之前必须等待响应的到达才行,这就成为了HTTP协议的一个巨大的性能瓶颈,当然为了解决这个问题,后来就出现了异步的AJAX技术。 阻塞的概念相对也是比较容易理解的。我们通过Java中的InputStream类的read方法来介绍阻塞的概念,文档中是这样描述read方法的: If no byte is available because the end of the stream has been reached, the value -1 is returned. This method blocks until input data is available, the end of the stream is detected, or an exception is thrown.意思就是,如果已经到了流的末尾没有可读取的数据,那么就会返回-1。这个方法会一直阻塞,直到有可读取的数据,或者已经读到了流的末尾,或者抛出一个异常。 这个方法的调用会一直阻塞,因为他会一直等待直到输入的数据可以用来读取。这通常会造成性能的瓶颈,因为这个方法会阻塞,导致无法继续执行随后的操作。 异步和非阻塞异步和非阻塞就是同步和阻塞的相反面。在直觉上,可能会感觉这两个概念会有一些类似,因为他们都可以允许你们的线程在等待结果或者返回的时候不需要挂起整个线程。但是他们又有不同,因为异步调用通常需要包括一个回调机制或者事件机制,去主动通知调用方此时响应的结果已经可用了。而非阻塞调用往往会先返回一个任意的结果,然后调用者会不定时的反复去尝试获取返回的结果,直到结果已经可用了。这里的区别就是一个主动通知和被动去询问。举个例子,你去音乐店买周杰伦的专辑,但老板告诉你,现在没货,你就回去了,等到货到了,准备好了,老板会主动打电话通知你,专辑已经到啦,快来买吧,这就是异步机制,是主动通知的。而非阻塞则是,老板不会主动通知你,而是你自己隔个一两天就去这家店主动问问,专辑到了么,直到有一次你询问的时候,终于发现专辑到了。非阻塞的概念常常用于I/O中,而异步的概念则相对应用的比较广泛。 特别的,异步I/O,意味着I/O操作是独立于当前的那个线程的操作而进行的。可以理解为,另外新开启了一个线程去执行I/O操作,当I/O操作完成之后会主动直接将结果返回。这里说的更详细一点就是,我们知道底层数据准备好之后,还要从内核区域拷贝到线程的缓冲区,非阻塞操作在这种意义上来说,又是同步的,因为非阻塞不会将这个拷贝数据的过程完成,而是当数据准备好了,告诉线程,你可以执行系统调用,将内核区域的数据拷贝到线程的缓冲区了,当然这个过程是同步,而且由于是系统调用,所以这个拷贝的过程也是阻塞的。而异步操作则不是,系统会开启一个线程当数据准备好了,这个线程还会完成这个从内核区将数据拷贝到线程缓冲区的过程,当数据拷贝完成了,才通知调用者,这时候调用者就直接可以用了。 我们在看一个更详细的异步I/O的例子: 我们假设同步I/O意味着发出一个I/O命令,然后一直等待,直到I/O操作完成。也就是说,你发出一个read命令,然后这个线程接下来的执行操作会一直等待,直到已经读到了内容。异步I/O则是你发出一个I/O命令,然后这个I/O不会立即完成。你可以先去执行接下来的程序。异步会实现一个接口,允许IO操作不阻塞当前的线程,而且当操作完成之后,会主动通知你操作已经完成。 Non-blocking 在这里有一个很好的解释: this StackOverflow answer: This term is mostly used with IO. What this means is that when you make a system call, it will return immediately with whatever result it has without putting your thread to sleep (with high probability). For example non-blocking read/write calls return with whatever they can do and expect caller to execute the call again. try_lock for example is non-blocking call. It will lock only if lock can be acquired. Usual semantics for systems calls is blocking. read will wait until it has some data and put calling thread to sleep.非阻塞I/O意味着当你发起一个系统调用的时候,他会立即返回一个结果,而不是将你的线程睡眠。非阻塞的读写操作,会收到一个立即的返回值,然后请求者会反复去重试,不断的去尝试,直到可以开始读写操作了。类似于忙等的状态,不断的测试,但是线程没有被阻塞。try_lock就是一个非阻塞的调用,他会尝试去获取锁,直到锁可以获取。通常来说,系统调用会进入内核,一般都是阻塞的,所以read操作往往是阻塞的,会等待可用数据,并且将线程休眠。 现在,我们应该对于异步和非阻塞的概念已经有所了解了。下面我们就举个现实中的例子来加强理解: 例如,传统的sockets API中,一个非阻塞的socket,通常会立即返回一个”would block” 的错误信息,然后需要调用独立的函数select or poll 去轮询检查什么时候可以再次尝试去读取。但是异步的sockets (windows的sockets支持异步操作),.Net框架中也有异步I/O模型。你调用一个方法开始某个操作,然后 框架会在这个操作完成的时候,回调通知你,操作完成了。]]></content>
<categories>
<category>操作系统</category>
</categories>
</entry>
<entry>
<title><![CDATA[设计模式之代理模式(Proxy模式)]]></title>
<url>%2F2017%2F07%2F22%2F%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B9%8B%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F%EF%BC%88Proxy%E6%A8%A1%E5%BC%8F%EF%BC%89%2F</url>
<content type="text"><![CDATA[代理模式的引入 代理模式的实例程序 代理模式的分析 代理模式的引入Proxy是代理人的意思,指的是代替别人进行工作的人。当不一定需要本人亲自去做的工作的时候,就可以寻找代理人去完成。但在代理模式中,往往是相反的,通常是代理人碰到工作,就交给被代理的对象去完成,代理人只完成一些准备工作或者收尾工作。如果读者了解过spring框架的话,就会知道aop也就是面向切面编程其实运用的就是动态代理模式,这可以让被代理的对象专注于完成自己的本职工作,而代理对象可以进行工作前的日志记录,时间计算,在工作之后进行日志记录,收尾工作等附加的功能,需要正式做工作的时候就交给被代理去做。就像插了两个刀到这个被代理的对象前后。所以形象的叫做面向切面编程。关于动态代理模式和静态代理模式,感兴趣的读者可以参考笔者的另一篇博文:Java动态代理与静态代理http://www.jianshu.com/p/b5e340ec9551 代理模式的实例程序我们会实现一个打印机,向屏幕打印一串字符串,然后交给代理对象去完成这个功能。 首先看一下类图: Printer类:123456789101112131415161718192021222324252627282930313233package Proxy;public class Printer implements Printable { private String name; public Printer() { heavyJob("正在生成Printer的实例"); } public Printer(String name) { // 构造函数 this.name = name; heavyJob("正在生成Printer的实例(" + name + ")"); } public void setPrinterName(String name) { // 设置名字 this.name = name; } public String getPrinterName() { // 获取名字 return name; } public void print(String string) { // 显示带打印机名字的文字 System.out.println("=== " + name + " ==="); System.out.println(string); } private void heavyJob(String msg) { // 重活 System.out.print(msg); for (int i = 0; i < 5; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { } System.out.print("."); } System.out.println("结束。"); }} Printable接口:123456789package Proxy;public interface Printable { public abstract void setPrinterName(String name); public abstract String getPrinterName(); public abstract void print(String string); } PrinterProxy类,利用反射机制,动态生成被代理的对象,并且延迟初始化到需要调用它的时候再初始化123456789101112131415161718192021222324252627282930313233343536package Proxy;public class PrinterProxy implements Printable { private String name; // 名字 private Printable real; // “本人” private String className; // “本人”的类名 public PrinterProxy(String name, String className) { // 构造函数 this.name = name; this.className = className; } public synchronized void setPrinterName(String name) { // 设置名字 if (real != null) { real.setPrinterName(name); // 同时设置“本人”的名字 } this.name = name; } public String getPrinterName() { // 获取名字 return name; } public void print(String string) { // 显示 realize(); real.print(string); } private synchronized void realize() { // 生成“本人” if (real == null) { try { real = (Printable)Class.forName(className).newInstance(); real.setPrinterName(name); } catch (ClassNotFoundException e) { System.err.println("没有找到 " + className + " 类。"); } catch (Exception e) { e.printStackTrace(); } } }} Main类测试:1234567891011package Proxy;public class Main { public static void main(String[] args) { Printable p = new PrinterProxy("Alice", "Proxy.Printer"); System.out.println("现在的名字是" + p.getPrinterName() + "。"); p.setPrinterName("Bob"); System.out.println("现在的名字是" + p.getPrinterName() + "。"); p.print("Hello, world."); }} 运行结果: 代理模式分析代理模式中的角色: Subject(主体)Subject角色定义了使proxy和realsubject角色之间具有一致性的接口。这个接口提供了一个使用的好处,就是client不必却分它使用的是代理对象还是真实对象。对应实例中Printable角色 Proxy(代理人)Proxy角色会尽量处理来自Client角色的请求。只有当自己不能处理的时候,就交给工作交给真实对象。代理对象只有在有必要时才会生成真实的对象。实例中对应的是PrinterProxy对象。 RealSubject(真实对象)就是实际完成工作的对象,对应实例中的Printer对象。 代理模式的类图: 用代理人来提升速度关键就在于延迟初始化。我们可以等到需要使用到真实对象的功能才初始化。这样的好处就是可以提升性能。从我们的实例中可能看不出这个优势,假设我们有一个大型系统,如果我们都在系统启动的时候,把所有功能初始化,所有实例初始化,那么显然系统的启动将会变得很慢。但如果我们采用代理模式,那么就会在必须的时候,在初始化对象。这样就加快了系统的启动速度。 代理和委托其实我们学习了那么多设计模式,是不是感觉委托简直无处不在。几乎每个设计模式都会用到委托,代理模式也不意外,就是代理了对象委托了真实对象。因为委托可以是对象之间发生联系,互相调用。所以委托在很多设计模式中都存在。]]></content>
<categories>
<category>设计模式</category>
</categories>
<tags>
<tag>设计模式</tag>
</tags>
</entry>
<entry>
<title><![CDATA[一篇文章搞懂面试中leetcode位操作算法题]]></title>
<url>%2F2017%2F07%2F20%2F%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E6%90%9E%E6%87%82%E9%9D%A2%E8%AF%95%E4%B8%ADleetcode%E4%BD%8D%E6%93%8D%E4%BD%9C%E7%AE%97%E6%B3%95%E9%A2%98%2F</url>
<content type="text"><![CDATA[Single Number落单的数 落单的数 IISingle Number II Single Number III落单的数 III Number of 1 Bits Counting Bits Reverse Bits Missing Number Sum of Two Integers Power of Two Power of Four 本文将根据题目总结常用的位操作常用的解决算法问题的技巧如读者对基本的位操作概念还不熟悉,可以先参考笔者的文章浅谈程序设计中的位操作http://www.jianshu.com/p/294fc6605bb1 Single Number落单的数给出2*n + 1 个的数字,除其中一个数字之外其他每个数字均出现两次,找到这个数字。 思路:一个数字和自己进行异或操作会是0,由于异或操作满足交换定律,一个数和0进行异或操作还是本身。所以这道题目的思路就来了,将所有出现两次的数异或就都变成了0,最后剩的那个数和0异或就还是本身。直接将数组所有数异或,就可以找出那个落单的数 12345678public class Solution { public int singleNumber(int[] nums) { int res = 0; for(int i=0;i<nums.length;i++) res ^= nums[i]; return res; }} 落单的数 IISingle Number II给出3*n + 1 个的数字,除其中一个数字之外其他每个数字均出现三次,找到这个数字。 思路:java中int是32位的,所以我们利用一个32的数组,分别记录每一位1的情况,如果出现三次就清0,最后留下来的就是那个只出现1次的数字在那一位上的情况,然后进行移位复原1234567891011121314151617public class Solution { public int singleNumber(int[] A) { int[] bits = new int[32]; int res = 0; for(int i=0;i<32;i++) { for(int j=0;j<A.length;j++) { bits[i] += (A[j]>>i) & 1; } res = res | ((bits[i]%3) << i); } return res; }} 如果要找出现4次或者出现5次等的情况,只要%5就行了。 Single Number III落单的数 III给出2*n + 2个的数字,除其中两个数字之外其他每个数字均出现两次,找到这两个数字。 思路如果能把这两个不同的数字分开,那么直接采取落单的数1的方法异或就可以了。所以,我们先考虑将所有数异或,最后得到的结果是两个不同的数的异或结果,然后我们找到最后一位为1的位,为1代表这两个数在这一位上是不同的。然后用这个位与数组中每个数相与,就可以把数分成两部分,一部分里有第一个不同的数,另一部分有第二个不同的数,所以这个时候我们只要直接异或就可以得到结果了。123456789101112131415161718192021222324public class Solution { public int[] singleNumber(int[] A) { int xor = 0; for(int i=0;i<A.length;i++) { xor ^= A[i]; } // a&(a-1)将最后为1的一位变成0 int lastbit = xor - (xor & (xor -1)); //取出最后一个为1的位 int group0 = 0, group1 = 0; for(int i=0;i<A.length;i++) { if((lastbit & A[i]) == 0) group0 ^= A[i]; else group1 ^= A[i]; } return new int[]{group0,group1}; }} Number of 1 BitsWrite a function that takes an unsigned integer and returns the number of ’1’ bits it has (also known as the Hamming weight).For example, the 32-bit integer ’11’ has binary representation 00000000000000000000000000001011, so the function should return 3. 思路:依次拿每一位与1进行比较,统计1的个数,然后逻辑右移,不能用算数右移,算数右移会在高位加一。1234567891011public class Solution { // you need to treat n as an unsigned value public int hammingWeight(int n) { int ones = 0; while(n!=0) { ones += (n & 1); n = n >>> 1; } return ones; }} 还有一种方法,我们知道n&(n-1)会把n中最后为1的一位变成0。所以我们调用n&(n-1),看看调几次这个数会变成0,就说明有几个1.12345678910111213public class Solution { // you need to treat n as an unsigned value public int hammingWeight(int n) { int sum = 0; while(n != 0) { sum++; n = n & (n-1); } return sum; }} Counting BitsGiven a non negative integer number num. For every numbers i in the range 0 ≤ i ≤ num calculate the number of 1’s in their binary representation and return them as an array. Example:For num = 5 you should return [0,1,1,2,1,2]. 思路:我们当然可以利用上一题的方法,直接每个数计算一次但也发现是存在规律的 12345678910public class Solution { public int[] countBits(int num) { int[] res = new int[num+1]; for(int i=1;i<=num;i++) res[i] = res[i>>1] + (i & 1); return res; }} Reverse BitsReverse bits of a given 32 bits unsigned integer. For example, given input 43261596 (represented in binary as 00000010100101000001111010011100), return 964176192 (represented in binary as 00111001011110000010100101000000). 思路:利用位操作,先交换相邻的两位,再交换的四位,再交换相邻的八位。举个例子;我们交换12345678可以先变成 21436587再变成43218765最后87654321,交换成功 对于32位也是如此的思路。关键如何用位操作实现,首先交换两位的话,可以先分别取出前一位x & (10101010101010101101010。。。。)换成16进制就是x & (0xaaaaaaaa)取出前一位,因为要与要有后一位交换,所以右移一位,因为只是单纯的交换,所以是逻辑右移(x & 0xaaaaaaaa) >>> 1然后对后一位也进行相应的操作,很容易得出(x & 0x555555555) << 1最后分别将前一位后一位合起来,使用或操作就可以了所以,第一次交换后x = ((x & 0xaaaaaaaa) >>> 1) | ((x & 0x55555555) << 1); 然后进行第二次交换:取出前两位x & (1100110011001100……)也就是 x & 0xcccccccc.后面的步骤都是一样的思路 x = ((x & 0xcccccccc) >>> 2) | ((x & 0x33333333) << 2); 第三次交换x = ((x & 0xf0f0f0f0) >>> 4) | ((x & 0x0f0f0f0f) << 4); 第四次交换x = ((x & 0xff00ff00) >>> 8) | ((x & 0x00ff00ff) << 8); 第四次交换x = ((x & 0xffff0000) >>> 16) | ((x & 0x0000ffff) << 16);交换成功 代码就是上面的交换的过程1234567891011public class Solution { // you need treat n as an unsigned value public int reverseBits(int x) { x = ((x & 0xaaaaaaaa) >>> 1) | ((x & 0x55555555) << 1); x = ((x & 0xcccccccc) >>> 2) | ((x & 0x33333333) << 2); x = ((x & 0xf0f0f0f0) >>> 4) | ((x & 0x0f0f0f0f) << 4); x = ((x & 0xff00ff00) >>> 8) | ((x & 0x00ff00ff) << 8); x = ((x & 0xffff0000) >>> 16) | ((x & 0x0000ffff) << 16); return x; }} Missing Number给出一个包含 0 .. N 中 N 个数的序列,找出0 .. N 中没有出现在序列中的那个数。 12345678910public class Solution { public int missingNumber(int[] nums) { int xor = 0, i = 0; for (i = 0; i < nums.length; i++) { xor = xor ^ i ^ nums[i]; } return xor ^ i; }} Sum of Two Integers位操作实现A+B的操作是常见的算法题。lintcode上就有一道容易题是这样。123456789101112131415class Solution { /* * param a: The first integer * param b: The second integer * return: The sum of a and b */ public int aplusb(int a, int b) { // write your code here, try to do it without arithmetic operators. if(a==0)return b; if(b==0)return a; int x1 = a^b; int x2 = (a&b)<<1; return aplusb(x1,x2); }}; 上述代码就实现了不用+操作符,利用位操作实现两个数的相加操作。现在我们来讲解位操作实现两个数相加的原理首先,十进制中,我们知道,7+8,不进位和是5,进位是1,然后我们可以根据不进位和和进位5+1*10算出最后的结果15。类似二进制也可以采取这种方法比如a = 3,b = 6a : 0011b : 0110不进位和: 0101 也就是5进位:0010 也就是2所以a+b变成5 + (2<<1)5 01012<<1 0100不进位和 0001 = 1进位 0100 = 4因此 a + b就变成了1 + 4 << 1然后有1 00014<<1 1000不进位和 1001 = 9进位 0000 = 0当时进位为0时,不进位和为9即a + b之和。 可以发现上述是一个递归的过程,所以也就不难写出代码了。求两个数的不进位和实际上就是将两个数异或操作即可。 Power of TwoGiven an integer, write a function to determine if it is a power of two.要为2的次方1,2,4,8,16也就是每位分别单独为1110100100010000所以n & (n-1)必须为01234567public class Solution { public boolean isPowerOfTwo(int n) { if(n<=0) return false; return (n & (n-1)) == 0; }} Power of FourGiven an integer (signed 32 bits), write a function to check whether it is a power of 4.按照上一题的思路,我们先列举出几个4的次方数,观察他门的规律1100100001000000我们发现不仅要2的次方的性质,还要满足 1所在的位必须是奇数位,所以我们取出奇数位,由于,1只在奇数位,所以取出奇数位后,应该还和原来的数相等取奇数位的方法在反转bit那题中已经讲过,就是x & 0x55555555 12345public class Solution { public boolean isPowerOfFour(int num) { return (num > 0) && ((num & (num - 1)) == 0) && ((num & 0x55555555) == num); }}]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
<tags>
<tag>LeetCode</tag>
</tags>
</entry>
<entry>
<title><![CDATA[一篇文章搞懂红黑树的原理及实现]]></title>
<url>%2F2017%2F07%2F13%2F%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E6%90%9E%E6%87%82%E7%BA%A2%E9%BB%91%E6%A0%91%E7%9A%84%E5%8E%9F%E7%90%86%E5%8F%8A%E5%AE%9E%E7%8E%B0%2F</url>
<content type="text"><![CDATA[2-3-4 Tree(2-3-4树)二叉查找树(Binary Search Tree,简称BST)是一棵二叉树,它的左子节点的值比父节点的值要小,右节点的值要比父节点的值大。它的高度决定了它的查找效率。我们知道二叉查找树。每个节点只可以有一个key,而2-3-4树就是将节点的key的数量增加,可以有多个key,并且2-3-4树可以保持完美平衡(Perfect balance. Every path from root to leaf has same length)什么是完美平衡(Perfect balance)?实际上就是每条从根节点到叶节点的路径的高度都是一样的(Every path from root to leaf has same length) 2-3-4树的名字是根据子节点数来确定的。我们看2-3-4树的key的种类。 2-node: one key, two children.一个key值,两个儿子节点 3-node: two keys, three children。两个key值,三个儿子节点 4-node: three keys, four children.三个key值,四个儿子节点 其中2-node的左孩子就代表比key小,右孩子就代表比key大,3-node的左孩子代表比第一个key小,中间的孩子代表值位于第一个key和第二个key之间,右孩子代表值大于第二个key4-node同理 我们看一棵2-3-4树的例子 2-3-4树的查找(Search in a 2-3-4 Tree)2-3-4的查找类似于BST的查找,只要递归找到所在的子树就可以。 比较要查找的key与当前节点中的key值 根据key值选择要查找的key所存在的子树区间 重复上述步骤(递归实现),直到查找到key 2-3-4的插入(Insertion in a 2-3-4 Tree)2-3-4的插入有几种情况,下面我们会一一的讨论。 首先,如果是向一个2-node插入节点的话,那么直接将它转换为3-node就可以了。 如果我们是向3-node插入,就将3-node变成4-node即可 那么问题就来了,如果我们向4-node插入呢?显然我们无法直接插入,因为4-node已经是最大的节点了。这时,我们就需要对节点进行一些变换 最常用的方法就是将4-node的中间节点向上移动,移动到父节点中。这样就可以插入了。 但这个方法有一个问题,就是如果父节点是4-node,那么就无法切分中间节点了。 一种解决的方法就是ensures that the “current” node is not a 4-node,我们做出保证父节点永远不会是4-node,就是说4-node不会出现在最后一层以外的层上。 所以我们需要维持一个不变的条件,当前的父节点永远不会是4-node 也就是,4-node的子节点中又有一个子节点是不可能的最底部的节点中不可能出现4-node,只可能出现2-node和3-node 下面分别分析切分4-node的几种不同情况 首先,4-node 的父节点是2-node 的情况,同样是把中间节点向上移动 对于4-node的父节点是3-node的情况,也是同样处理,将middle中间节点向上移动 我们看一下如何从空开始插入建立一个2-3-4树 下面,我们通过动态添加一个完整的2-3-4的过程,说明2-3-4树的插入和构建过程 我们发现,2-3-4树所有叶子节点都在同一个高度上。 我们分析2-3-4树的效率: 2-3-4的高度的最坏情况(全是2-node),也就相当于演变成了平衡二叉树 : 相当于平衡二叉树 lgN 2-3-4树高度的最好情况(全是4-node),log4 N = 1/2 lg N(但我们知道这种情况是不可能出现的,因为我们要求4-node的父节点或者子节点不能是4-node) 对于100万个节点,2-3-4树的高度会在10~20之间 对于10亿的节点,2-3-4树的高度会在15~30之间 由此来看,2-3-4树的效率比平衡二叉树要好,但是问题在于,2-3-4树并不好实现 首先,我们需要用三种不同类型的节点代表2-3-4node 然后,在插入节点的时候,我们可能需要进行大量的切分4-node的工作 我们可能也需要频繁的在三种节点之间进行转换 一个简单的伪码实现:123456789101112private void insert(Key key, Val val){Node x = root;while (x.getTheCorrectChild(key) != null){x = x.getTheCorrectChild(key);if (x.is4Node()) x.split();}if (x.is2Node()) x.make3Node(key, val);else if (x.is3Node()) x.make4Node(key, val);return x;} 为了更好的利用2-3-4树平衡高度的特点,同时又更好的便于实现,我们就引入了红黑树。 红黑树 我们用最常见的平衡二叉树来代表2-3-4树 我们通过给节点区分红色和黑色来区分三种不同的节点。(红边指向下的节点为红节点) 这样我们就可以BST来代表2-3-4树了。看下面的例子 但是存在一个问题,就是对于一棵2-3-4树可能有多种不同的表示,这是在于对于3-node的表示,红色的边可以向左倾,也可以向右倾。 所以就要考虑很多情况。 但我们在此只考虑左倾的情况,所以这种树也叫做左倾红黑树这样,对于任何一棵2-3-4树,我们都可以得到一棵唯一对应的左倾红黑树 对于左倾红黑树,我们还有以下要求,就是不能以下情况的节点情况: 首先,由于是左倾的,就不能出现右倾的3-node 其次,不允许出现两个红边连在一起的情况,变成2-3-4树的情况就是不允许两个3-node相互连接 左倾红黑树的ADT1234567891011121314151617181920212223242526272829public class BST<Key extends Comparable<Key>, Value>{private static final boolean RED = true;private static final boolean BLACK = false;private Node root;private class Node{Key key;Value val;Node left, right;boolean color;Node(Key key, Value val, boolean color){this.key = key;this.val = val;this.color = color;}}public Value get(Key key)// Search method.public void put(Key key, Value val)// Insert method.}private boolean isRed(Node x){if (x == null) return false;return (x.color == RED);} 红黑树的get方法和BST是一样的1234567891011121314151617181920public Value get(Key key){Node x = root;while (x != null){int cmp = key.compareTo(x.key);if (cmp == 0) return x.val;else if (cmp < 0) x = x.left;else if (cmp > 0) x = x.right;}return null;}public Key min(){Node x = root;while (x != null) x = x.left;if (x == null) return null;else return x.key;} 左倾红黑树的插入插入操作是红黑树中最复杂的操作之一。因为不仅要插入还要维持红黑颜色的。 首先,我们先介绍如何对红黑树的一些节点进行转换操作 左旋操作左旋操作就是将右倾的3-node变成左倾的3-node 123456789private Node rotateLeft(Node h){Node x = h.right;h.right = x.left;x.left = h;x.color = x.left.color;x.left.color = RED;return x;} 右旋操作就是与左旋操作相反 123456789private Node rotateRight(Node h){Node x = h.left;h.left = x.right;x.right = h;x.color = x.right.color;x.right.color = RED;return x;} 下面我们来具体分析插入操作 当我们要向红黑树的底部插入一个节点的时候,就可能出现多种情况 如果我们向2-node的节点插入的话,有两种情况,如果插入左孩子,那么直接插入就可以,但如果插入的是右孩子,为了保持左倾,插入之后,我们需要进行一个左旋操作 我们可以看到这种情况对应于2-3-4树就是想2-node插入变成3-node 下面一种情况,就是我们向3-node插入一个节点,那么我们就需要将它变成2-3-4树中对应的树节点这也是为什么我们之前定义的不允许的情况中的第二种,不允许两条红边连在一起,也就是不允许两个红节点互为父子节点,因为插入的节点一定是红节点。 向3-node插入有三种情况: 向4-node插入: 根据我们之前在2-3-4树中学习的可以知道,我们需要对4-node进行切分,切分的方法就是将4-node的中间节点向上移动到父节点中。 首先,当父节点是2-node时候: 有两种情况 我们发现在红黑树中进行切分工作很简单,只要将两个红节点变成黑,然后父节点变成红就可以了。这个变换的过程,我们叫做 color flip。 代码如下:1234567private Node colorFlip(Node h){x.color = !x.color;x.left.color = !x.left.color;x.right.color = !x.right.color;return x;} 过程如下图 对于父节点为3-node的情况: 观察这五种情况,我们发现首先都是先惊醒color flip操作,然后就变成了之前的操作,左旋和右旋。 我们可以把上面这些插入操作总结,然后实现一个统一适用的插入算法 首先,向空节点插入一个节点,一定为红节点 12if (h == null)return new Node(key, value, RED); 如果出现了4-node的情况,我们我们就进行color flip 12if (isRed(h.left) && isRed(h.right))colorFlip(h); 调整右倾的节点 12if (isRed(h.right))h = rotateLeft(h); 对连续的两个红节点进行转换 12if (isRed(h.left) && isRed(h.left.left))h = rotateRight(h); 左倾红黑树插入算法的实现123456789101112131415if (h == null)return new Node(key, val, RED);if (isRed(h.left) && isRed(h.right))colorFlip(h);int cmp = key.compareTo(h.key);if (cmp == 0) h.val = val;else if (cmp < 0)h.left = insert(h.left, key, val);elseh.right = insert(h.right, key, val);if (isRed(h.right))h = rotateLeft(h);if (isRed(h.left) && isRed(h.left.left))h = rotateRight(h);return h; 这里代码的执行顺序是很重要的。 比如如果我们把colorfilp移到最后,那么会出现什么情况? 由于每次在最后都将4-node 进行color flip了,那么自然红黑树中不存在4-node了,所以就变成了2-3树的红黑树 我们可以对比普通红黑树的插入算法的实现1234567891011121314151617181920212223242526272829303132333435private Node insert(Node x, Key key, Value val, boolean sw){if (x == null)return new Node(key, value, RED);int cmp = key.compareTo(x.key);if (isRed(x.left) && isRed(x.right)){x.color = RED;x.left.color = BLACK;x.right.color = BLACK;}if (cmp == 0) x.val = val;else if (cmp < 0)){x.left = insert(x.left, key, val, false);if (isRed(x) && isRed(x.left) && sw)x = rotR(x);if (isRed(x.left) && isRed(x.left.left)){x = rotR(x);x.color = BLACK; x.right.color = RED;}}else // if (cmp > 0){x.right = insert(x.right, key, val, true);if (isRed(h) && isRed(x.right) && !sw)x = rotL(x);if (isRed(h.right) && isRed(h.right.right)){x = rotL(x);x.color = BLACK; x.left.color = RED;}}return x; 左倾红黑树的删除操作首先我们介绍一下,删除完成之后,如何调整红黑树为左倾的红黑树?这里有一个方法,主要就是进行三个调整的步骤12345678910private Node fixUp(Node h){if (isRed(h.right))h = rotateLeft(h);if (isRed(h.left) && isRed(h.left.left))h = rotateRight(h);if (isRed(h.left) && isRed(h.right))colorFlip(h);return h;} 删除操作的原则 删除的当前节点不能是2-node 如果有必要可以变换成4-node 从底部删除节点 向上的fix过程中,消除4-node 红黑树的删除操作与插入操作一样,极其复杂,所以先从相对容易的情况开始考虑 删除最大节点显然最大节点一定是在最右边 如果我们删除的节点在3-node或者4-node中,我们直接删除掉就可以了。 最复杂的情况,是我们要删除的节点是2-node,如果我门直接删除就会破坏红黑树的平衡,所以我们再删除之前,要进行一定的变换,变成3-node或者4-node,也就是借一个或者两个节点过来。 根据父节点的不同。3-node或者4-node和兄弟节点的不同可以分为六种情况,但其中又可以分为两类 第一种处理方法就是兄弟节点不是2-node,就可以直接从兄弟节点借一个节点过来 第二种处理方法兄弟节点是2-node,则从父节点中借一个过来,然后和兄弟节点合并成一个4-node 这六种情况的条件根据2-3-4树转换成红黑树,就是h.right和h.right.left均为黑色。但其中有需要分为两种对于上述提到的第二种处理方法,处理比较简单,直接color flip即可 其中这种情况的条件就是左子节点为2-node,也就是h.left.left为黑。 对于h.left.left为红的情况,就对应上述的第一种处理方法,首先color filp,然后还要借一个节点过来 所以将上面两种方法合并:12345678910private Node moveRedRight(Node h){colorFlip(h);if (isRed(h.left.left)){h = rotateRight(h);colorFlip(h);}return h;} 然后我们就可以得到删除最大节点的算法:12345678910111213141516public void deleteMax(){root = deleteMax(root);root.color = BLACK;}private Node deleteMax(Node h){if (isRed(h.left))h = rotateRight(h);if (h.right == null)return null;if (!isRed(h.right) && !isRed(h.right.left))h = moveRedRight(h);h.left = deleteMax(h.left);return fixUp(h);} 流程就是: 首先如果左旋则变为右旋,因为找最大节点在最右边 如果,已经到了最底部,那么直接移除就行,移除的要求是最底部的节点一定是red 如果遇到了2-node就借一个节点 继续往下递归查找 删除完毕,就恢复红黑树 我们下面看两个例子 删除红黑树最小节点最小节点的方法与最大节点的类似,只不过是从最右边变成了最左边 思想还是一样的 首先,不变量,就是h或者h的left一定是红色的。遇到底部的红节点,就直接删除了。 然后就是对于2-node需要从兄弟节点中借一个节点变成3-node或者4-node2-node的条件就是,h.left和h.left.left均为黑色的。然后其中又有两种情况,如果h.right.left为黑,则说明兄弟节点也是2-node,就从父节点借节点,直接color flip即可 如果h.right.left为红,则可以直接从兄弟节点借一个节点过来。 代码是1234567891011private Node moveRedLeft(Node h){colorFlip(h);if (isRed(h.right.left)){h.right = rotateRight(h.right);h = rotateLeft(h);colorFlip(h);}return h;} 最后归纳得到删除最小节点的代码1234567891011121314public void deleteMin(){root = deleteMin(root);root.color = BLACK;}private Node deleteMin(Node h){if (h.left == null)return null;if (!isRed(h.left) && !isRed(h.left.left))h = moveRedLeft(h);h.left = deleteMin(h.left);return fixUp(h);} 流程如下 两个删除最小节点的例子: 删除任意节点我们学习了怎么删除最大节点和最小节点,下面我们开始研究最复杂的情况,就是删除任意节点其实思路是一样的,如果所要删除的节点在3-node或者4-node中,根据2-3-4树的性质直接删除就可以了。最复杂的情况是如果是2-node,那么删除就会引起不平衡。所以就得从兄弟节点中借一个节点,但由于是任意节点,不像删除最大最小的情况,确定是左边或者右边,而是有很多种情况。根据理论研究,9 6 + 27 9 + 81 * 12 = 1269多种情况(我也不知道咋算的哈哈,有兴趣去看论文)所以单纯的这种思路是不行的。 我们变换想法,类似于堆,我们如果要删除一个节点,把要删除的那个节点和最底部的节点交换,然后就变成删除最底部的节点,就可以转换成删除最大节点或者最小节点了。这也就是我们为什么要先讲最大节点和最小节点。同时这样也把问题简化了,因为删除最大和最小节点的方法我们已经分析出来了。 如果我们要删除D节点,我们可以选择用D节点左子树的最大节点或者右子树的最小节点来替换D的值,然后再删除最大节点或者最小节点就可以了。 代码如下:123h.key = min(h.right);h.value = get(h.right, h.key);h.right = deleteMin(h.right); 对于删除的节点在最底部的情况,则我们可以直接利用前面最大节点和最小节点的方法往下搜索就行了,如果所删除的节点比当前节点大,就往右搜索,采取删除最大节点的搜索路径,反之,就往左搜索。 红黑树删除任意节点的代码1234567891011121314151617181920212223242526private Node delete(Node h, Key key){int cmp = key.compareTo(h.key);if (cmp < 0){if (!isRed(h.left) && !isRed(h.left.left))h = moveRedLeft(h);h.left = delete(h.left, key);}else{if (isRed(h.left)) h = leanRight(h);if (cmp == 0 && (h.right == null))return null;if (!isRed(h.right) && !isRed(h.right.left))h = moveRedRight(h);if (cmp == 0){h.key = min(h.right);h.value = get(h.right, h.key);h.right = deleteMin(h.right);}else h.right = delete(h.right, key);}return fixUp(h);} 流程图如下: 总结至此,我们就基本讲完了红黑树的基本原理和实现。我们首先从2-3-4树开始讲起,然后引出红黑树其实就是2-3-4树的BST的表示。接着介绍插入和删除算法。很少需要我们自己手动实现红黑树,但我们需要对红黑树的基本原理,作用,算法的思路有一个基本的了解。这篇文章的目的就在此。 本文主要基于http://www.cs.princeton.edu/~rs/talks/LLRB/RedBlack.pdf这篇文章来讲解的,有兴趣的可以参考]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
<tags>
<tag>红黑树</tag>
</tags>
</entry>
<entry>
<title><![CDATA[设计模式之桥接模式(Bridge_模式)]]></title>
<url>%2F2017%2F07%2F13%2F%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B9%8B%E6%A1%A5%E6%8E%A5%E6%A8%A1%E5%BC%8F%EF%BC%88Bridge-%E6%A8%A1%E5%BC%8F%EF%BC%89%2F</url>
<content type="text"><![CDATA[类的功能层次 类的实现层次 桥接模式的具体事例 小结 Bridge的意思是桥梁,作用就是将两边连接起来。桥接模式的作用也是如此,桥接模式分别类的功能层次和类的实现层次连接起来。 这里出现了两个可能有点陌生的词汇,类的功能层次和类的实现层次。 所以我们先来介绍这两种的层次结构,因为桥接模式就是为了连接这两种层次结构。 类的功能层次用于添加的新的功能,假如现在有一个类,我们想在这个类中添加一个新的功能,同时又不改变原有的类,那么我们可以采用继承的方法,继承自这个类,然后在继承的类中添加一个具体的新的方法。这就是类的功能层次。 父类拥有基本的功能 子类对类的功能进行扩展,添加的新的功能 注意:类的功能层次不能太深 类的实现层次用于添加新的实现。假如我们现在有一个抽象类或者接口,里面定义了相应的方法,但是没有实现,对于不同的具体的实现我们需要继承这个抽象类或者实现接口,这就是类的实现层次。 父类通过声明抽象方法来定义接口 子类通过实现具体方法来实现接口 类的层次结构的混杂与分离所以学习了类的功能层次和实现层次之后,我们在编写子类的就可以考虑一个问题,我们要添加功能还是添加实现。当类的层次结构只有一层的时候,功能层次结构与实现层次结构是混在一起的,这样就容易是类的层次结构变得复杂难以理解。因此,我们需要将类的功能层次和实现层次分离为两个独立的层次结构,但又不能的简单的分开,分开之后又要添加某种联系,这种联系就是桥梁,也就是我们本文要讲的桥接模式。 桥接模式的具体实例这个实例的功能就是打印显示某个东西。 我们先考虑类的功能层次 类的功能层次只需要考虑具体需要考虑哪些,具体的实现交给实现层次去实现,那么功能层次为了调用实现层次,就需要持有一个实现层次的对象,就是委托。 功能层次的基类Display123456789101112131415161718192021222324252627package Bridge;public class Display { private DisplatImpl impl; public Display(DisplatImpl impl) { this.impl = impl; } public void open() { impl.rawOpen(); } public void print() { impl.rawPrint(); } public void close() { impl.rawClose(); } public final void display() { open(); print(); close(); }} 然后我们给这个类添加功能,可以多次显示的功能COuntPlay:123456789101112131415package Bridge;public class CountDisplay extends Display{ public CountDisplay(DisplatImpl impl) { super(impl); } public void multiDisplay(int times) { open(); for(int i=0;i<times;i++) print(); close(); }} 类的实现层次 首先,类的实现层次的基类应该是一个接口或者抽象类,他定义了需要实现的方法1234567package Bridge;public abstract class DisplatImpl { public abstract void rawOpen(); public abstract void rawPrint(); public abstract void rawClose();} 然后我们看一个真正的具体实现,实现了上述的接口123456789101112131415161718192021222324public class StringDisplayImpl extends DisplayImpl { private String string; // 要显示的字符串 private int width; // 以字节单位计算出的字符串的宽度 public StringDisplayImpl(String string) { // 构造函数接收要显示的字符串string this.string = string; // 将它保存在字段中 this.width = string.getBytes().length; // 把字符串的宽度也保存在字段中,以供使用。 } public void rawOpen() { printLine(); } public void rawPrint() { System.out.println("|" + string + "|"); // 前后加上"|"并显示 } public void rawClose() { printLine(); } private void printLine() { System.out.print("+"); // 显示用来表示方框的角的"+" for (int i = 0; i < width; i++) { // 显示width个"-" System.out.print("-"); // 将其用作方框的边框 } System.out.println("+"); // 显示用来表示方框的角的"+" }} 最后我们来调用这两个层次:1234567891011public class Main { public static void main(String[] args) { Display d1 = new Display(new StringDisplayImpl("Hello, China.")); Display d2 = new CountDisplay(new StringDisplayImpl("Hello, World.")); CountDisplay d3 = new CountDisplay(new StringDisplayImpl("Hello, Universe.")); d1.display(); d2.display(); d3.display(); d3.multiDisplay(5); }} 运行结果: 上述实例的类图: Bridge模式的类图也是类似的: 小结 分开后更容易扩展桥接模式的特点是将类的功能层次和实现层次分开。分开之后的好处就是有利于对它们进行扩展,当要添加新的功能的时候,只要在功能层次添加类就可以了。不必对类的实现层次做任何修改。而且增加的功能可以被所有的实现使用。 例如,如果我们程序中依赖操作系统的部分划分为max,windows和linux版,我们就可以利用类的桥接层次中的实现层次来表现这些依赖操作系统的部分。]]></content>
<categories>
<category>设计模式</category>
</categories>
<tags>
<tag>设计模式</tag>
</tags>
</entry>
<entry>
<title><![CDATA[TCP/IP之路由算法]]></title>
<url>%2F2017%2F06%2F05%2FTCP-IP%E4%B9%8B%E8%B7%AF%E7%94%B1%E7%AE%97%E6%B3%95%2F</url>
<content type="text"><![CDATA[网络层的重要功能就是路由和转发。而路由是根据路由器根据所维护的路由表进行路由选择。所以,如果创建和更新转发表就是一个很重要的问题。通常,在路由时,我们总是选取所需代价最小的一条路由。 首先,我们需要将网络进行抽象,最常见的抽象就是,将网络抽象成图结构。 每段链路的费用可以总是1,或者是,带宽的倒数、拥塞程度等。 关键问题: 源到目的(如u到z)的最小费用路径是什么?所谓的路由算法: 寻找最小费用路径的算法。 路由算法的分类静态路由 vs 动态路由静态路由就是所有路由信息由人工静态配置好,以后需要更新的话,就要重新配置。 手工配置 路由更新慢 优先级高 动态路由就是在网络随时根据网络拓扑结构的结构的变化,进行动态更新 路由更新快 定期更新 及时响应链路费用或网络拓扑变化 全局信息 vs 分散信息有的路由算法需要所有路由器掌握完整的网络拓扑和链路费用信息,也就是对网络的全局有一个了解最有代表性的就是链路状态(LS)路由算法。 有的路由算法只需要路由器只掌握物理相连的邻居以及链路费用。通过邻居间信息交换、运算的迭代过程来更新路由信息。最有代表性的就是距离向量(DV)路由算法。 链路状态路由算法 首先将网络抽象,然后利用图算法中的最短路径算法,Dijkstra 算法。 所有结点(路由器)掌握网络拓扑和链路费用 通过“链路状态广播” 所有结点拥有相同信息 利用Dijkstra 算法计算从一个结点(“源” )到达所有其他结点的最短路径。从而可以获得该节点的转发表。然后对不同的节点进行迭代,就可以使所有节点都得到自己的转发表。 c(x,y): 结点x到结点y链路费用;如果x和y不直接相连,则=∞ D(v): 从源到目的v的当前路径费用值 p(v): 沿从源到v的当前路径, v的前序结点 N’: 已经找到最小费用路径的结点集合 1234567891011121314151 初始化:2 N' = {u}3 for 所有结点v4 if v毗邻u5 then D(v) = c(u,v)6 else D(v) = ∞78 Loop9 找出不在 N’中的w ,满足D(w)最小10 将w加入N'11 更新w的所有不在N’中的邻居v的D(v) :12 D(v) = min( D(v), D(w) + c(w,v) )13 /*到达v的新费用或者是原先到达v的费用,或者是14 已知的到达w的最短路径费用加上w到v的费用 */15 until 所有结点在N’中 算法复杂性: n个结点 每次迭代: 需要检测所有不在集合N’中的结点w n(n+1)/2次比较: O(n2) 更高效的实现: O(nlogn) 算法可能存在震荡现象 当链路状态更新的太快并且不断变化的时候,假设我们发出一个分组,结果还没到目的地,路由表就更新了,然后这个数据报就一直在路由间切换,最后由于ttl到0,直接丢弃。这就是震荡现象。 距离向量(Distance Vector)路由算法 重点:结点获得最短路径的下一跳, 该信息用于转发表中! 核心思想: 每个结点不定时地将其自身的DV估计发送给其邻居 当x接收到邻居的新的DV估计时, 即依据B-F更新其自身的距离向量估计: Dx(y)将最终收敛于实际的最小费用 dx(y) 异步迭代: 引发每次局部迭代的因素 局部链路费用改变 来自邻居的DV更新 分布式: 每个结点只当DV变化时才通告给邻居 邻居在必要时(其DV更新后发生改变)再通告它们的邻居 距离向量路由算法:举例 如果链路发生变化,距离向量节点会怎么样呢? 链路费用变化: 结点检测本地链路费用变化 更新路由信息,重新计算距离向量 如果DV改变,通告所有邻居 交换过程 t0 : y检测到链路费用改变 ,更新DV,通告其邻居. t1 : z收到y的DV更新,更新其距离向量表,计算到达x的最新最小费用,更新其DV,并发送给其所有邻居. t2 : y收到z的DV更新, 更新其距离向量表,重新计算y的DV,未发生改变,不再向z发送DV. “好消息传播快! ”“坏消息会怎么样呢? ”如果是坏消息,很可能就会出现无穷计数的问题: 我们发现 坏消息传播慢!—“无穷计数(count to infinity)”问题! 无穷计数问题的解决方法毒性逆转(poisoned reverse):如果一个结点(e.g. Z)到达某目的(e.g.X)的最小费用路径是通过某个邻居(e.g.Y),则通告给该邻居结点到达该目的的距离为无穷大 毒性逆转能否彻底解决无穷计数问题?显然是不行的,如果过于复杂的网络,我们发现毒性逆转也需要经过很多的步骤。 定义最大度量(maximum metric)定义一个最大的有效费用值,如15跳步, 16跳步表示∞ 层次路由我们前面的算法是将网络抽象成一张图,但实际上,网络都是很大的,节点数量远超过我们想象,如果我们单纯的使用以上的算法显然是不可行的。 将任意规模网络抽象为一个图计算路由-过于理想化 标识所有路由器 “扁平”网络——在实际网络(尤其是大规模网络)中, 不可行! 网络规模: 考虑6亿目的结点的网络 路由表几乎无法存储! 路由计算过程的信息( e.g. 链路状态分组、DV)交换量巨大,会淹没链路! 另一方面,就是网络管理自治的问题,不同的网络可以采取不同的方法进行路由。管理自治: 每个网络的管理可能都期望自主控制其网内的路由 互联网(internet) = 网络之网络(network of networks) 层次路由就是解决这样的问题,和网络领域中的问题是一样,继续抽象出一层网络。聚合路由器为一个区域:自治系统AS(autonomous systems)然后再把自治系统看成节点进行路由,对于自治系统内就采取自己的路由方法。这就是抽象成了两层。 同一AS内的路由器运行相同的路由协议(算法) 自治系统内部路由协议(“ intra-AS” routing protocol) 不同自治系统内的路由器可以运行不同的AS内部路由协议 网关路由器(gateway router): 位于AS“边缘” 通过链路连接其他AS的网关路由器 转发表由AS内部路由算法与AS间路由算法共同配置 AS内部路由算法设置AS内部目的网络路由入口(entries) AS内部路由算法与AS间路由算法共同设置AS外部目的网络路由入口 假设AS1内某路由器收到一个目的地址在AS1之外的数据报:路由器应该将该数据报转发给哪个网关路由器呢? AS1必须:1.学习到哪些目的网络可以通过AS2到达,哪些可以通过AS3到达2.将这些网络可达性信息传播给AS1内部路由器 以上这些都是 例: 路由器1d的转发表设置 假设AS1学习到(通过AS间路由协议):子网x可以通过AS3 (网关 1c)到达,但不能通过AS2到达,AS间路由协议向所有内部路由器传播该可达性信息 为了配置转发表,路由器1d必须确定应该将去往子网x的数据报转发给哪个网关?这个任务也是由AS间路由协议完成! 假设AS1通过AS间路由协议学习到:子网x通过AS3和AS2均可到达 为了配置转发表,路由器1d必须确定应该将去往子网x的数据报转发给哪个网关? 这个任务也是由AS间路由协议完成! 热土豆路由: 将分组发送给最近的网关路由器.]]></content>
<categories>
<category>网络</category>
</categories>
<tags>
<tag>TCP/IP</tag>
</tags>
</entry>
<entry>
<title><![CDATA[TCP/IP之拥塞控制]]></title>
<url>%2F2017%2F05%2F31%2FTCP-IP%E4%B9%8B%E6%8B%A5%E5%A1%9E%E6%8E%A7%E5%88%B6%2F</url>
<content type="text"><![CDATA[拥塞(Congestion)给一个非正式定义就是:“太多发送主机发送了太多数据或者发送速度太快,以至于网络无法处理”如果网络中发生了拥塞,会出现如下表现: 分组丢失(路由器缓存溢出) 分组延迟过大(在路由器缓存中排队) 和可靠数据传输一样都是网络领域中的top-10的问题。 拥塞现象是指到达[通信子网]中某一部分的分组数量过多,使得该部分网络来不及处理,以致引起这部分乃至整个网络性能下降的现象,严重时甚至会导致网络通信业务陷入停顿,即出现[死锁]现象。这种现象跟公路网中经常所见的交通拥挤一样,当节假日公路网中车辆大量增加时,各种走向的车流相互干扰,使每辆车到达目的地的时间都相对增加(即延迟增加),甚至有时在某段公路上车辆因堵塞而无法开动(即发生局部[死锁]我们先讨论一下拥塞的成因和代价 拥塞的成因和代价我们通过假设不同的场景渐进式分析拥塞的成因和代价 场景一我们假设 两个senders,两个receivers 一个路由器, 无限缓存 没有重传 我们假设原始数据的发送速度为in,到达接受方的速度为out,由于路由器的速度限制,即使无限缓存,到达的最大速度也无法超过路由器的速度 即使发送速度再快,到达速度也无法超过路由器的速度,所以时延在拥塞发生的时候会无限增大。 拥塞时分组延迟太大 达到最大throughput 场景2我们假设 一个路由器, 有限buffers Sender重传分组 可以分为一下几种情况讨论 情况a: Sender能够通过某种机制获知路由器buffer信息,有空闲才发 情况b: 丢失后才重发,显然这样到达速度会减小 情况c:分组丢失和定时器超时后都重发,显然到达速度进一步减小 拥塞的代价: 对给定的”goodput”,要做更多的工作 (重传) 造成资源的浪费 场景三 四个发送方 多跳 超时/重传 随着拥塞的严重,整个网络可能陷入瘫痪,到达速度趋近于0,类似于死锁的状态 拥塞的另一个代价: 当分组被drop时,任何用于该分组的“上游”传输能力全都被浪费掉,相当于白传了,浪费了资源和传输能力 拥塞控制的方法端到端拥塞控制: 网络层不需要显式的提供支持 端系统通过观察loss,delay等网络行为判断是否发生拥塞 TCP采取这种方法网络辅助的拥塞控制: 路由器向发送方显式地反馈网络拥塞信息简单的拥塞指示(1bit):SNA,DECbit, TCP/IP ECN, ATM) 指示发送方应该采取何种速度 案例:ATM ABR拥塞控制 ABR:available bit rate “弹性服务” 如果发送方路径“underloaded”使用可用带宽 如果发送方路径拥塞将发送速率降到最低保障速率 RM(resource management)cells 发送方发送 交换机设置RM cell位(网络辅助)• NI bit: rate不许增长• CI bit: 拥塞指示 RM cell由接收方返回给发送方 TCP拥塞控制TCP拥塞控制的基本原理Sender限制发送速率 CongWin可以动态调整以改变发送速率,并且反映所感知到的网络拥塞。 那么问题来了,如何感知网络拥塞: Loss事件=timeout或3个重复ACK 发生loss事件后,发送方降低速率 感知到网络拥塞后,需要动态调整发送速率,以减轻网络的拥塞状况,如何调整发送速率,一般有两个方法: 加性增—乘性减: AIMD 慢启动: SS 加性增—乘性减: AIMD顾名思义,这种方法就是先简单的增加,遇到拥塞的情况,就乘性减少。原理:逐渐增加发送速率,谨慎探测可用带宽,直到发生loss。Additive Increase: 每个RTT将CongWin增大一个MSS——拥塞避免Multiplicative Decrease: 发生loss后将CongWin减半。AIMD方法会使congwin呈锯齿状的波动 首先慢慢增加,当遇到拥塞时,减为一半,然后又继续慢慢增加,直到遇到拥塞后又减为一半,这样往复就会出现锯齿状的波动。 TCP慢启动: SS我们考虑下面这种情况:TCP连接建立时,CongWin=1 例:MSS=500 byte,RTT=200msec 初始速率=20k bps我们发现在这种情况下,可用带宽可能远远高于初始速率,如果我们采用加性增的方法就太慢了,我们希望快速增长到可用带宽。这就慢启动算法的思想:当连接开始时,指数性增长。指数性增长。每个RTT将CongWin翻倍。收到每个ACK进行操作。初始速率很慢,但是快速攀升。 那么问题来了,我们什么时候才进行线性增长来避免拥塞控制?我们通过设置一个变量 Threshold,Loss事件发生时, Threshold被设为Loss事件前CongWin值的1/2。然后如果cogwin到了Threshold,就开始线性增长。 如图所示,初始的Threshold变量为8,所以当指数增长大于等于8的时候,就开始线性增长,然后直到发生loss事件,Threshold变为发生loss事件时的一半,也就是6,然后继续指数增长到Threshold,又开始线性增长。 那么我们如何判断loss事件的发生呢?我们分为两种情况来处理: 3个重复ACKs:CongWin切到一半,然后线性增长 Timeout事件:CongWin直接设为1个MSS,然后指数增长,达到threshold后, 再线性增长 我们想想这样做的原因,因为3个重复ACKs表示网络还能够传输一些 segments,timeout事件表明拥塞更为严重。 慢启动算法:123456789101112131415Th = ?CongWin = 1 MSS/* slow start or exponential increase */While (No Packet Loss and CongWin < Th) {send CongWin TCP segmentsfor each ACK increase CongWin by 1}/* congestion avoidance or linear increase */While (No Packet Loss) {send CongWin TCP segmentsfor CongWin ACKs, increase CongWin by 1}Th = CongWin/2If (3 Dup ACKs) CongWin = Th;If (timeout) CongWin=1; 一个TCP连接总是以1 KB的最大段长发送TCP段,发送方有足够多的数据要发送。当拥塞窗口为16 KB时发生了超时,如果接下来的4个RTT(往返时间)时间内的TCP段的传输都是成功的,那么当第4个RTT时间内发送的所有TCP段都得到肯定应答时,拥塞窗口大小是多少? 解:threshold=16/2=8 KB, CongWin=1 KB, 1个RTT后, CongWin=2 KB ,2个RTT后, CongWin=4 KB ,3个RTT后, CongWin=8 KB ,Slowstart isover; 4个RTT后, CongWin=9 KB]]></content>
<categories>
<category>网络</category>
</categories>
<tags>
<tag>TCP/IP</tag>
</tags>
</entry>
<entry>
<title><![CDATA[TCP/IP之可靠数据传输原理]]></title>
<url>%2F2017%2F05%2F24%2FTCP-IP%E4%B9%8B%E5%8F%AF%E9%9D%A0%E6%95%B0%E6%8D%AE%E4%BC%A0%E8%BE%93%E5%8E%9F%E7%90%86%2F</url>
<content type="text"><![CDATA[可靠数据传输对于应用层、传输层、链路层都很重要,是网络领域的Top10问题。对于传输层来说,由于相邻的网络层是不可靠的,所以要在传输层实现可靠数据传输(rdt)就比较复杂。那么我们来了,究竟怎样才是可靠? 我们将讨论一下几个方面的内容信道的(不可靠)特性可靠数据传输的需求Rdt 1.0Rdt 2.0, rdt 2.1, rdt 2.2Rdt 3.0流水线与滑动窗口协议GBNSR 什么是可靠? 不错就是传输的数据包没有错误 不丢传输的数据包不丢失 不乱传输的数据包顺序要保持正确 为了更好的说明,我们采取渐进式的设计可靠数据传输的发送方和接收方。 我们考虑第一个版本的可靠数据传输 Rdt 1.0: 可靠信道上的可靠数据传输 假设 底层信道完全可靠 不会发生错误(bit error) 不会丢弃分组显然有了这个假设的话,发送方和接收方只要能正确接收数据就可以了,所以他们是相互独立的,因为发过来的数据保证一定是正确的。 Rdt 2.0: 产生位错误的信道我们假设底层信道可能翻转分组中的位(bit) 首先如何判断错误,我们可以利用校验和来判断是否发生位错误那么发现了错误,我们该如何处理呢?第一种思路当然是纠正错误,但是这样实现的难度和代价都比较大,在计算机网络中,我们一般都会采取第二种思路第二种思路就是直接重传,如果我们发现了错误,很自然,那我们就重传一次,直到接受方收到正确的分组。还有一个问题就是假设接收方发现了错误,如果告知发送方已经发生了错误呢?其实处理起来也很简单,就是向接收方发送一个信号,代表出现错误,如果没错误就发送一个信号,表示没错误。 如何从错误中恢复? 确认机制(Acknowledgements, ACK): 接收方显式地告知发送方分组已正确接收 NAK:接收方显式地告知发送方分组有错误 发送方收到NAK后,重传分组 基于这种重传机制的rdt协议称为ARQ(Automatic Repeat reQuest)协议 Rdt 2.0中引入的新机制 差错检测 接收方反馈控制消息: ACK/NAK 重传 下面两个图分别模拟了有错误和无错误场景:无错误场景有错误场景 Rdt 2.1: 发送方, 应对ACK/NAK破坏我们看rdt2.0有什么问题,我们知道确认信号也需要通过信道传播,那么如果ack,nck的信号发生了错误呢?发送方应该怎么处理?显然发生了错误,我们就应该重传但是这里,又有一个问题,接收方怎么知道发送方这次新传过来的是新的报文段还是因为ack出错而重传的报文段呢?显然我们需要区分,上一个报文段和当前的报文段,我们给报文段编写好序号就可以了,而且只需要0,1两个序号,一个表示上次的报文段,一个表示新接受的。这样接收方如果收到0,就知道这次不是新的报文段,可能是上次ack出错了,发送方无法确认,就重传了上次的报文段,所以接收方需要丢掉这个报文段,然后再次传一次ack确认信号,如果收到的是序号为1的报文段,则接收方直接接受就可以了。 Rdt 2.1 vs. Rdt 2.0发送方: 为每个分组增加了序列号 两个序列号(0, 1)就够用,为什么? 需校验ACK/NAK消息是否发生错误 状态数量翻倍 状态必须“记住”“当前”的分组序列号接收方: 需判断分组是否是重复 当前所处状态提供了期望收到分组的序列号 注意:接收方无法知道ACK/NAK是否被发送方正确收到 Rdt 2.2: 无NAK消息协议我们考虑一下我们真的需要两个确认信号ack和nck么? 与rdt 2.1功能相同,但是只使用ACK如何实现? 接收方通过ACK告知最后一个被正确接收的分组 在ACK消息中显式地加入被确认分组的序列号 发送方收到重复ACK之后,采取与收到NAK消息相同的动作 重传当前分组 Rdt 3.0到rdt2.2为止,我们基本解决了“不错”的要求,即报文和确认信息在信道上发生了错误的话,我们都可以很好的解决,解决的方法其实就是重传那么我们接下来就该解决不丢的问题。如果信道既可能发生错误,也可能丢失分组,怎么办?“校验和 + 序列号 + ACK + 重传”够用吗?显然是不够用的’我们假设这时候ack不是错误而是直接丢了,那么发送方就会无限制的等着接收方的ack,同时接收方也会无限制的等着发送方的新报文。这样就陷入了类似死锁的机制,如果不加以处理,那么网络就卡死在这里了。那么我们该如何处理呢?方法:发送方等待“合理”时间 如果没收到ACK,重传 如果分组或ACK只是延迟而不是丢了 重传会产生重复,序列号机制能够处理 接收方需在ACK中显式告知所确认的分组 需要定时器 rdt3.0效率Rdt 3.0能够正确工作,但性能很差示例:1Gbps链路,15ms端到端传播延迟,1KB分组 发送方利用率:发送方发送时间百分比 在1Gbps链路上每30毫秒才发送一个分组33KB/sec 网络协议限制了物理资源的利用 这样低效率的原因是,我们采取的是停-等操作就是说发送方发了一个数据包,就停下来了,直到得到来自接收方的确认才发送第二个,这样就造成了很多的空余时间都在空闲等待。 流水线机制与滑动窗口协议为了改进停等机制所造成的效率低下,我们可以采用流水线的机制,一次发送多条报文段,充分利用空闲的时间 允许发送方在收到ACK之前连续发送多个分组 更大的序列号范围 发送方和/或接收方需要更大的存储空间以缓存分组 进一步的,我们采用滑动窗口协议,顾名思义,就是发送给定窗口大小的报文数,随着报文被接收确认,同时窗口可以动态的向前滑动 滑动窗口协议: Sliding-window protocol窗口 允许使用的序列号范围 窗口尺寸为N:最多有N个等待确认的消息滑动窗口 随着协议的运行,窗口在序列号空间内向前滑动滑动窗口协议:GBN, SR Go-Back-N(GBN)协议 如图所示,窗口大小N,最多允许N个分组未确认。ACK(n): 确认到序列号n(包含n)的分组均已被正确接收 可能收到重复ACK为没收到确认的分组设置计时器(timer)超时Timeout(n)事件: 重传序列号大于等于n,还未收到ACK的所有分组ACK机制: 发送拥有最高序列号的、已被正确接收的分组的ACK 可能产生重复ACK 只需要记住唯一的expectedseqnum接收方是没有缓存的,所以接收方对于乱序到达的分组直接丢弃,并且重新发送目前为止接收到的分组中序列号最大的按序到达的分组 简单的习题: 数据链路层采用后退N帧(GBN)协议,发送方已经发送了编号为0~7的帧。当计时器超时时,若发送方只收到0、2、3号帧的确认,则发送方需要重发的帧数是多少?分别是那几个帧? 解:根据GBN协议工作原理,GBN协议的确认是累积确认,所以此时发送端需要重发的帧数是4个,依次分别是4、5、6、7号帧 Selective Repeat协议GBN有什么缺陷?由于GBN接收方没有缓存,对于非按序的分组直接丢弃,就会造成很多到达的分组由于顺序乱了,却白发了,需要再次重新发送。显然为了提高效率,我们可以在接收方设置缓存,对于未按序达到的分组,先存起来,而不是直接丢弃。这就是选择重复协议的思想接收方对每个分组单独进行确认 设置缓存机制,缓存乱序到达的分组发送方只重传那些没收到ACK的分组 为每个分组设置定时器发送方窗口 N个连续的序列号 限制已发送且未确认的分组 从图中我们可以看到,接收方是动态移动滑动窗口的,只有当窗口部分前面的全部正确接受并确认了,才向前移动。 可靠数据传输原理与协议回顾信道的(不可靠)特性可靠数据传输的需求Rdt 1.0Rdt 2.0, rdt 2.1, rdt 2.2Rdt 3.0流水线与滑动窗口协议GBNSR]]></content>
<categories>
<category>网络</category>
</categories>
<tags>
<tag>TCP/IP</tag>
</tags>
</entry>
<entry>
<title><![CDATA[排列类算法问题大总结]]></title>
<url>%2F2017%2F03%2F26%2F%E6%8E%92%E5%88%97%E7%B1%BB%E7%AE%97%E6%B3%95%E9%97%AE%E9%A2%98%E5%A4%A7%E6%80%BB%E7%BB%93%2F</url>
<content type="text"><![CDATA[全排列 带重复元素的排列 下一个排列 上一个排列 第 k 个排列 排列序号 排列序号II 全排列给定一个数字列表,返回其所有可能的排列。 注意事项 你可以假设没有重复数字。样例给出一个列表[1,2,3],其全排列为: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]] 分析可以用递归和非递归解决 首先递归法,也是利用了回溯法和深度优先搜索。 我们考虑一个一个将数组元素加入到排列中,递归求解,就好像下面的解答树: 添加的时候排除掉相同的元素即可,回溯法我们经常会设置一个已访问标识数组,来表示数组被访问过,但这里不用这样,因为如果list里面已经包含就说明已经访问过了,所以只要判断,跳过已有的元素即可。再考虑递归的结束条件,当元素都添加足够就结束了,添加足够的意思就是,元素个数等于数组的长度。 1234567891011121314151617181920212223242526272829303132333435363738class Solution { /** * @param nums: A list of integers. * @return: A list of permutations. */ public List<List<Integer>> permute(int[] nums) { List<List<Integer>> res = new ArrayList<>(); if(nums == null) return res; if(nums.length == 0) { res.add(new ArrayList<Integer>()); return res; } ArrayList<Integer> list = new ArrayList<>(); dfs(res, list, nums); return res; } private void dfs(List<List<Integer>> res, ArrayList<Integer> list, int[] nums) { int n = nums.length; if(list.size() == n) { res.add(new ArrayList<Integer>(list)); return; } for(int i = 0;i < n;i++) { if(list.contains(nums[i])) continue; list.add(nums[i]); dfs(res, list, nums); list.remove(list.size() - 1); } }} 非递归实现思路是这样的,就是高中的排列组合知识,运用插入法即可,假设有i个元素的排列组合,那么对于i+1个元素,可以考虑就是将i+1的元素插入到上述的排列的每一个位置即可。 123456789101112131415161718192021222324252627282930313233class Solution { /** * @param nums: A list of integers. * @return: A list of permutations. */ public List<List<Integer>> permute(int[] nums) { List<List<Integer>> res = new ArrayList<List<Integer>>(); if ( nums == null) return res; if( nums.length == 0) { res.add(new ArrayList<Integer>()); return res; } List<Integer> list = new ArrayList<>(); list.add(nums[0]); res.add(new ArrayList<Integer>(list)); for(int i=1;i<nums.length;i++) { int size1 = res.size(); for(int j=0;j<size1;j++) { int size2 = res.get(0).size(); for(int k=0;k<=size2;k++) { ArrayList<Integer> temp = new ArrayList<>(res.get(0)); temp.add(k,nums[i]); res.add(temp); } res.remove(0); } } return res; }} 带重复元素的全排列给出一个具有重复数字的列表,找出列表所有不同的排列。 样例给出列表 [1,2,2],不同的排列有: 代码123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960class Solution { /** * @param nums: A list of integers. * @return: A list of unique permutations. */ public List<List<Integer>> permuteUnique(int[] nums) { ArrayList<List<Integer>> res = new ArrayList<>(); if(nums == null) return null; if(nums.length == 0) { res.add(new ArrayList<Integer>()); return res; } ArrayList<Integer> list = new ArrayList<>(); //先将数组排序,这样相同元素将会出现在一起 Arrays.sort(nums); int n = nums.length; int[] visited = new int[n]; for(int i=0;i<n;i++) visited[i] = 0;//0标识未访问 helper(res, list, visited, nums); return res; } private void helper(ArrayList<List<Integer>> res, ArrayList<Integer> list, int[] visited, int[] nums) { if(nums.length == list.size()) { res.add( new ArrayList<Integer>(list)); } for(int i=0;i<nums.length;i++) { if(visited[i] == 1 || i!= 0 && (visited[i-1] == 0 && nums[i] == nums[i-1])) continue; /* 上面的判断主要是为了去除重复元素影响。 比如,给出一个排好序的数组,[1,2,2],那么第一个2和第二2如果在结果中互换位置, 我们也认为是同一种方案,所以我们强制要求相同的数字,原来排在前面的,在结果 当中也应该排在前面,这样就保证了唯一性。所以当前面的2还没有使用的时候,就 不应该让后面的2使用。 */ list.add(nums[i]); visited[i] = 1; helper(res, list, visited, nums); list.remove(list.size()-1); visited[i] = 0; } }} 下一个排列给定一个若干整数的排列,给出按正数大小进行字典序从小到大排序后的下一个排列。 如果没有下一个排列,则输出字典序最小的序列。 样例左边是原始排列,右边是对应的下一个排列。 1,2,3 → 1,3,2 3,2,1 → 1,2,3 1,1,5 → 1,5,1 分析这道题让我们求下一个排列顺序,有题目中给的例子可以看出来,如果给定数组是降序,则说明是全排列的最后一种情况,则下一个排列就是最初始情况,可以参见之前的博客 Permutations 全排列。我们再来看下面一个例子,有如下的一个数组1 2 7 4 3 1下一个排列为:1 3 1 2 4 7那么是如何得到的呢,我们通过观察原数组可以发现,如果从末尾往前看,数字逐渐变大,到了2时才减小的,然后我们再从后往前找第一个比2大的数字,是3,那么我们交换2和3,再把此时3后面的所有数字转置一下即可,步骤如下:1 2 7 4 3 11 2 7 4 3 11 3 7 4 2 11 3 1 2 4 7 所以我们要做的就是找到第一个比peak元素大的数字,交换,然后反转 123456789101112131415161718192021222324252627282930313233343536public class Solution { /** * @param nums: an array of integers * @return: return nothing (void), do not return anything, modify nums in-place instead */ public int[] nextPermutation(int[] nums) { int i = nums.length - 2; while (i >= 0 && nums[i + 1] <= nums[i]) { i--; } if (i >= 0) { int j = nums.length - 1; while (j >= 0 && nums[j] <= nums[i]) { j--; } swap(nums, i, j); } reverse(nums, i + 1); return nums; } private void reverse(int[] nums, int start) { int i = start, j = nums.length - 1; while (i < j) { swap(nums, i, j); i++; j--; } } private void swap(int[] nums, int i, int j) { int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; }} 上一个排列给定一个整数数组来表示排列,找出其上一个排列。 注意事项 排列中可能包含重复的整数 样例给出排列[1,3,2,3],其上一个排列是[1,2,3,3] 给出排列[1,2,3,4],其上一个排列是[4,3,2,1] 分析与求下一个排列是一样的方法,只是相应的操作变反即可123456789101112131415161718192021222324252627282930313233public class Solution { /** * @param nums: A list of integers * @return: A list of integers that's previous permuation */ public void swapItem(ArrayList<Integer> nums, int i, int j) { Integer tmp = nums.get(i); nums.set(i, nums.get(j)); nums.set(j, tmp); } public void swapList(ArrayList<Integer> nums, int i, int j) { while ( i < j) { swapItem(nums, i, j); i ++; j --; } } public ArrayList<Integer> previousPermuation(ArrayList<Integer> nums) { int len = nums.size(); if ( len <= 1) return nums; int i = len - 1; while ( i > 0 && nums.get(i) >= nums.get(i-1) ) i --; swapList(nums, i, len - 1); if ( i != 0) { int j = i; while ( nums.get(j) >= nums.get(i-1) ) j++; swapItem(nums, j, i-1); } return nums; }} 第k个排列给定 n 和 k,求123..n组成的排列中的第 k 个排列。 注意事项 1 ≤ n ≤ 9 样例对于 n = 3, 所有的排列如下: 123132213231312321如果 k = 4, 第4个排列为,231. 分析康托展开的公式:(不用记,看形势就行,下面会有例子) X=an(n-1)!+an-1(n-2)!+…+ai(i-1)!+…+a21!+a1*0! ai为整数,并且0<=ai<i(1<=i<=n) 适用范围:没有重复元素的全排列 N个数的第k个排序,例子,1,2,3,4共有4!种排列,1234,1243,1324等等。按顺序应该是 1234 1243 1324 1342 1423 1432等等 可以通过STL中next_permutation(begin, end);来算下一个全排列,理论上你要算n个数的第k个排列只要调用k-1次next_permutation()就行,但是一般来说肯定会超时的,因为next_permutation的时间复杂度是O(n)(如果自己写出来next_permutation时间复杂度比n大就要注意了,其中一个容易疏忽的地方是最后排序可以用reverse而不是sort)。所以如果用这个的话时间复杂度是O(N^2)。 而用康托展开只要O(n)就行,下面来说说具体怎么做: 题目:找出第16个n = 5的序列(12345) 首先第十六个也就是要前面有15个数,要调用15次next_permutation函数。 根据第一行的那个全排列公式,15 / 4! = 0 …15 =》 有0个数比他小的数是1,所以第一位是1 拿走刚才的余数15,用15 / 3! = 2 …3 => 剩下的数里有两个数比他小的是4(1已经没了),所以第二位是4 拿走余数3, 用 3 / 2! = 1 …1 =》 剩下的数里有一个数比他小的是3,所以第三位是3 拿走余数1, 用 1/ 1! = 1 …0 => 剩下的数里有一个数比他小的是 5(只剩2和5了),所以第四位是5 所以排列是 1,4,3,5,2 12345678910111213141516171819202122232425262728293031323334353637383940class Solution { /** * @param n: n * @param k: the kth permutation * @return: return the k-th permutation */ public String getPermutation(int n, int k) { StringBuilder sb = new StringBuilder(); boolean[] used = new boolean[n]; k = k - 1; int factor = 1; for (int i = 1; i < n; i++) { factor *= i; } for (int i = 0; i < n; i++) { int index = k / factor; k = k % factor; for (int j = 0; j < n; j++) { if (used[j] == false) { if (index == 0) { used[j] = true; sb.append((char) ('0' + j + 1)); break; } else { index--; } } } if (i < n - 1) { factor = factor / (n - 1 - i); } } return sb.toString(); }} 排列序号给出一个不含重复数字的排列,求这些数字的所有排列按字典序排序后该排列的编号。其中,编号从1开始。 样例例如,排列 [1,2,4] 是第 1 个排列。 分析这道题是求第k个排列的反向思维 已知是n = 5,求14352是它的第几个序列?(同一道题) 用刚才的那道题的反向思维: 第一位是1,有0个数小于1,即0* 4! 第二位是4,有2个数小于4,即2* 3! 第三位是3,有1个数小于3,即1* 2! 第四位是5,有1个数小于5,即1* 1! 第五位是2,不过不用算,因为肯定是0 所以14352是 n = 5的第 0 + 12 + 2 + 1 + 0 = 15 + 1(求的是第几个,所以要加一) = 16 第16个,跟刚才那道题一样,证明对了 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657public class Solution { /** * @param A an integer array * @return a long integer */ public long permutationIndex(int[] A) { // Write your code here HashMap<Integer, Integer> hash = new HashMap<Integer, Integer>(); for (int i = 0; i < A.length; i++) { if (hash.containsKey(A[i])) hash.put(A[i], hash.get(A[i]) + 1); else { hash.put(A[i], 1); } } long ans = 0; for (int i = 0; i < A.length; i++) { for (int j = i + 1; j < A.length; j++) { if (A[j] < A[i]) { hash.put(A[j], hash.get(A[j])-1); ans += generateNum(hash); hash.put(A[j], hash.get(A[j])+1); } } hash.put(A[i], hash.get(A[i])-1); } return ans+1; } long fac(int numerator) { long now = 1; for (int i = 1; i <= numerator; i++) { now *= (long) i; } return now; } long generateNum(HashMap<Integer, Integer> hash) { long denominator = 1; int sum = 0; for (int val : hash.values()) { if(val == 0 ) continue; denominator *= fac(val); sum += val; } if(sum==0) { return sum; } return fac(sum) / denominator; }} 排列序号II给出一个可能包含重复数字的排列,求这些数字的所有排列按字典序排序后该排列在其中的编号。编号从1开始。 样例给出排列[1, 4, 2, 2],其编号为3。 分析这道题基于查找不存在重复元素中排列序号的基础之上, 即P(n) = P(n-1)+C(n-1) C(n-1) = (首元素为小于当前元素,之后的全排列值)P(1) = 1; 而不存在重复元素的全排列值C(n-1) = (n-1)!*k(k为首元素之后小于当前元素的个数) 在存在重复元素的排列中首先全排列的值的求法变为: C(n-1) = (n-1)!/(A1!A2!···Aj!)k(其中Ai 为重复元素的个数,k为小于首元素前不重复的个数) 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253/** * @param A an integer array * @return a long integer */ long fac(int numerator) { long now = 1; for (int i = 1; i <= numerator; i++) { now *= (long) i; } return now; } long generateNum(HashMap<Integer, Integer> hash) { long denominator = 1; int sum = 0; for (int val : hash.values()) { if(val == 0 ) continue; denominator *= fac(val); sum += val; } if(sum==0) { return sum; } return fac(sum) / denominator; } public long permutationIndexII(int[] A) { HashMap<Integer, Integer> hash = new HashMap<Integer, Integer>(); for (int i = 0; i < A.length; i++) { if (hash.containsKey(A[i])) hash.put(A[i], hash.get(A[i]) + 1); else { hash.put(A[i], 1); } } long ans = 0; for (int i = 0; i < A.length; i++) { HashMap<Integer, Integer> flag = new HashMap<Integer, Integer>(); for (int j = i + 1; j < A.length; j++) { if (A[j] < A[i] && !flag.containsKey(A[j])) { flag.put(A[j], 1); hash.put(A[j], hash.get(A[j])-1); ans += generateNum(hash); hash.put(A[j], hash.get(A[j])+1); } } hash.put(A[i], hash.get(A[i])-1); } return ans + 1; }]]></content>
<categories>
<category>数据结构与算法</category>
</categories>
<tags>
<tag>LeetCode</tag>
</tags>
</entry>
<entry>
<title><![CDATA[设计模式之生成器模式(Builder_Pattern)]]></title>
<url>%2F2017%2F03%2F15%2F%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B9%8B%E7%94%9F%E6%88%90%E5%99%A8%E6%A8%A1%E5%BC%8F%EF%BC%88Builder-Pattern%EF%BC%89%2F</url>
<content type="text"><![CDATA[生成器模式的核心是当构建生成一个对象的时候,需要包含多个步骤,虽然每个步骤具体的实现不同,但是都遵循一定的流程与规则举个例子,我们如果构建生成一台电脑,那么我们可能需要这么几个步骤 需要一个主机 需要一个显示器 需要一个键盘 需要一个鼠标 需要音响等 虽然我们具体在构建一台主机的时候,每个对象的实际步骤是不一样的,比如,有的对象构建了i7cpu的主机,有的对象构建了i5cpu的主机,有的对象构建了普通键盘,有的对象构建了机械键盘等。但不管怎样,你总是需要经过一个步骤就是构建一台主机,一台键盘。对于这个例子,我们就可以使用生成器模式来生成一台电脑,他需要通过多个步骤来生成。 所以,我们可以将生成器模式理解为,假设我们有一个对象需要建立,这个对象是由多个组件(Component)组合而成,每个组件的建立都比较复杂,但运用组件来建立所需的对象非常简单,所以我们就可以将构建复杂组件的步骤与运用组件构建对象分离,使用builder模式可以建立。 生成器模式的类图如下: 下面我们就根据这个例子来实现一个生成器模式,生成一台电脑 首先我们需要一个电脑类:123456789101112131415161718192021222324package Builder;public class Computer { public String master; public String screen; public String keyboard; public String mouse; public String audio; public void setMaster(String master) { this.master = master; } public void setScreen(String screen) { this.screen = screen; } public void setKeyboard(String keyboard) { this.keyboard = keyboard; } public void setMouse(String mouse) { this.mouse = mouse; } public void setAudio(String audio) { this.audio = audio; }} 然后我们建立一个抽象的builder类:123456789101112131415161718192021package Builder;public abstract class ComputerBuilder { protected Computer computer; public Computer getComputer() { return computer; } public void buildComputer() { computer = new Computer(); System.out.println("生成了一台电脑!!!"); } public abstract void buildMaster(); public abstract void buildScreen(); public abstract void buildKeyboard(); public abstract void buildMouse(); public abstract void buildAudio();} 然后我们实现两个具体的builder类,分别是惠普电脑的builder和戴尔电脑的builder123456789101112131415161718192021222324252627282930313233343536373839package Builder;public class HPComputerBuilder extends ComputerBuilder { @Override public void buildMaster() { // TODO Auto-generated method stub computer.setMaster("i7,16g,512SSD,1060"); System.out.println("(i7,16g,512SSD,1060)的惠普主机"); } @Override public void buildScreen() { // TODO Auto-generated method stub computer.setScreen("1080p"); System.out.println("(1080p)的惠普显示屏"); } @Override public void buildKeyboard() { // TODO Auto-generated method stub computer.setKeyboard("cherry 青轴机械键盘"); System.out.println("(cherry 青轴机械键盘)的键盘"); } @Override public void buildMouse() { // TODO Auto-generated method stub computer.setMouse("MI 鼠标"); System.out.println("(MI 鼠标)的鼠标"); } @Override public void buildAudio() { // TODO Auto-generated method stub computer.setAudio("飞利浦 音响"); System.out.println("(飞利浦 音响)的音响"); }} 1234567891011121314151617181920212223242526272829303132333435363738394041package Builder;public class DELLComputerBuilder extends ComputerBuilder { @Override public void buildMaster() { // TODO Auto-generated method stub computer.setMaster("i7,32g,1TSSD,1060"); System.out.println("(i7,32g,1TSSD,1060)的戴尔主机"); } @Override public void buildScreen() { // TODO Auto-generated method stub computer.setScreen("4k"); System.out.println("(4k)的dell显示屏"); } @Override public void buildKeyboard() { // TODO Auto-generated method stub computer.setKeyboard("cherry 黑轴机械键盘"); System.out.println("(cherry 黑轴机械键盘)的键盘"); } @Override public void buildMouse() { // TODO Auto-generated method stub computer.setMouse("MI 鼠标"); System.out.println("(MI 鼠标)的鼠标"); } @Override public void buildAudio() { // TODO Auto-generated method stub computer.setAudio("飞利浦 音响"); System.out.println("(飞利浦 音响)的音响"); }} 然后我们实现一个director类:123456789101112131415161718192021222324package Builder;public class Director { private ComputerBuilder computerBuilder; public void setComputerBuilder(ComputerBuilder computerBuilder) { this.computerBuilder = computerBuilder; } public Computer getComputer() { return computerBuilder.getComputer(); } public void constructComputer() { computerBuilder.buildComputer(); computerBuilder.buildMaster(); computerBuilder.buildScreen(); computerBuilder.buildKeyboard(); computerBuilder.buildMouse(); computerBuilder.buildAudio(); }} 最后我们测试一下代码:123456789101112131415161718package Builder;public class ComputerCustomer { public static void main(String[] args) { // TODO Auto-generated method stub Director director = new Director(); ComputerBuilder hp = new HPComputerBuilder(); director.setComputerBuilder(hp); director.constructComputer(); //get the pc Computer pc = director.getComputer(); }} 生成器模式的优缺点优点 将一个对象分解为各个组件 将对象组件的构造封装起来 可以控制整个对象的生成过程 缺点对不同类型的对象需要实现不同的具体构造器的类,这可能回答大大增加类的数量 生成器模式的实际应用Builder pattern has been used in a lot of libraries. However, there is a common mistake here. Consider the following example of StringBuilder which is a class from Java standard library. Does it utilize the Builder pattern? 生成器模式在许多类库中都使用了。但是严格来说,却有些错误。比如这个例子,我们考虑java标准库中的StringBuilder类,它使用了生成器模式么? 12345StringBuilder strBuilder= new StringBuilder();strBuilder.append("one");strBuilder.append("two");strBuilder.append("three");String str= strBuilder.toString(); 在标准库中,StringBuilder继承自AbstractStringBuilderappend方法是这个生成过程中的一步,就像我们构建电脑时,先构建主机这样的步骤一样。toString方法也是生成过程中的一步,而且是构建过程中的最后一步。然而,这里的不同是没有director,所以严格来说这不是一个标准的生成器模式。我们程序的调用者好像就是director可以生成我们自己的String。 生成器模式与工厂模式的不同生成器模式构建对象的时候,对象通常构建的过程中需要多个步骤,就像我们例子中的先有主机,再有显示屏,再有鼠标等等,生成器模式的作用就是将这些复杂的构建过程封装起来。工厂模式构建对象的时候通常就只有一个步骤,调用一个工厂方法就可以生成一个对象。]]></content>
<categories>
<category>设计模式</category>
</categories>
<tags>
<tag>设计模式</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深度解析Java多线程的内存模型]]></title>
<url>%2F2017%2F03%2F12%2F%E6%B7%B1%E5%BA%A6%E8%A7%A3%E6%9E%90Java%E5%A4%9A%E7%BA%BF%E7%A8%8B%E7%9A%84%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B%2F</url>
<content type="text"><![CDATA[内部java内存模型 硬件层面的内存模型 Java内存模型和硬件内存模型的联系 共享对象的可见性 资源竞速 Java内存模型很好的说明了JVM是如何在内存里工作的,JVM可以理解为java执行的一个操作系统,作为一个操作系统就有内存模型,这就是我们常说的JAVA内存模型。 如果我们想正确的写多线程的并行程序。理解好java内存模型在多线程下的工作方式是及其重要的,这可以帮我们更好的理解底层的工作方式。java内存模型说明了不同的线程怎样以及何时可以看到其他线程写入共享变量的值,以及同步程序怎么共享变量。最初的java内存模型不够好,存在很多的不足,所以在java1.5z中,java内存模型的版本的进行了一次重大的更新与改进,并且在java8中仍然被使用。 内部java内存模型JVM的内部的内存模型分为了两部分,thread stack和heap,也就是线程栈和堆,我们将复杂的内存模型抽象成下图: 每一个在JVM中运行的线程在内存里都会有属于自己的线程栈。线程栈一般包含这个线程的方法执行到哪一个点了这些信息,也被称作“call stack”,当线程执行代码,调用栈就会随着执行的状态改变。 线程栈也包括了每个方法执行时的local 变量,所有的方法也都存储在线程栈上,一个线程可以只能访问自己的线程栈。每个线程自己创建的本地本地变量对其他线程是不可见的,也就是私有的,即使两个线程调用的是同一个方法,每个线程会分别保存一份本地变量,各自属于各自的线程栈。 所有基本类型的local变量( boolean, byte, short, char, int, long, float, double)全都被存储在线程栈里,而且对其他线程是不可见的,一个线程可能会传递一份基本类型的变量值的一份拷贝给另一个线程,但是自己本身的变量是不能共享的,只能传递拷贝。 堆中存储着java程序中new出来的对象,不管是哪个线程new出来的对象,都存在一起,而且不区分是哪个线程的对象。这些对象里面也包括那些原始类型的对象版本(e.g. Byte, Integer, Long etc.). 不管这个对象是分配给本地变量还是成员变量,最终都是存在堆里。 下面这个图就说明了线程栈中存储了local变量,堆中存储着对象object。 一个原始数据类型的本地变量将完全被存储在线程栈中。 本地变量也可以是指向对象的引用,在这种情况下,本地变量存在线程栈上,但是对象本身是存在堆上。 一个对象可能包含方法这些方法同时也会包含本地变量,这些本地变量也是存储在线程栈上面,即使他们所属于的对象和方法是存在堆上的。 一个对象的成员变量是跟随着对象本身存储在堆上的,不管成员变量是原始数据类型还是指向对象的引用。 静态的类变量一般也存储在堆上,根据类的定义。 存储在堆上的对象可以被所有的线程通过引用来访问。当一个线程持有一个对象的引用时,他同时也就可以访问这个对象的成员变量了。如果两个线程同时调用同一个对象的一个方法,他们就会都拥有这个对象的成员变量,但是每一个线程会享有自己私有的本地变量。 下面这张图就说明以上的内容 两个线程有一系列的本地变量。其中一个本地变量(Local Variable 2)指向堆中的object3.这两个线程每个都有指向同一个对象object3的不同引用。他们的引用是本地变量,都存在各自的线程栈中,虽然这两个不同的引用是指向同一个对象的。 我们还可以发现,共有的对象object3有指向object2和object4的引用,这些引用是作为object3中的成员变量存在的。通过object3中的成员变量的引用,两个线程都可以访问到object2和object4. 这个图也说明了指向堆中不同对象的本地变量。例如图中的object1和object5,不是同一个对象。理论上,所有的线程都可以访问堆中的对象,只要这个线程持有堆中对象的引用。但是这个图中,每个线程只有这两个对象中的一个引用。 下面,我们将写一段实际的代码,这段代码的内存模型就跟上图一样:1234567891011121314151617181920212223public class MyRunnable implements Runnable() { public void run() { methodOne(); } public void methodOne() { int localVariable1 = 45; MySharedObject localVariable2 = MySharedObject.sharedInstance; //... do more with local variables. methodTwo(); } public void methodTwo() { Integer localVariable1 = new Integer(99); //... do more with local variable. }} 12345678910111213141516public class MySharedObject { //static variable pointing to instance of MySharedObject public static final MySharedObject sharedInstance = new MySharedObject(); //member variables pointing to two objects on the heap public Integer object2 = new Integer(22); public Integer object4 = new Integer(44); public long member1 = 12345; public long member1 = 67890;} 如果两个线程执行run方法那么,上图的内存模型就会是这段程序的执行结果。 methodOne()声明了一个原始数据类型的本地变量,int类型的localVariable1 ,和一个指向对象的引用的本地变量(localVariable2). 每个线程执行methodOne()的时候都会创建属于自己的一份本地变量的拷贝,也就是 localVariable1 and localVariable2在他们各自的线程栈的空间中。localVariable1将会被完全对其他线程是不可见的,只存在与每个线程自己的线程栈空间中。一个线程不能看到其他线程对localVariable1所做的改变与操作,是不可见的。 每个线程执行methodOne()方法的时候也会创建localVariable2的拷贝,但是不同的localVariable2的拷贝最终却指向同一个堆上的对象。这段代码让localVariable2指向之前通过一个静态变量引用的对象。静态变量只会存在一份,不会有多余的拷贝,而且静态变量是存在堆中的。所以,localVariable2的两份拷贝同时指向同一个MySharedObject对象的实例,与此同时,还有一个堆中的静态变量也指向这个对象实例。这个对象就是对应上图中的object3. 我们发现MySharedObject 包含这两个成员变量。这些成员变量跟对象一样存储在堆上。这两个成员变量指向两个integer对象。这两个对象分别对应上图中的object2和object4. 我们发现,methodTwo() 创建了一个本地变量叫做localVariable1。这个本地变量是一个对象的引用,他指向一个integer对象。这个方法将本地变量localVariable1指向一个新的值。在执行methodTwo()的时候,每个线程都会持有一份localVariable1的拷贝。这两个Integer对象将会被初始化在堆上,但是因为每次执行这个方法的时候,这个方法都会创建一个新的对象,所以两个线程会拥有独立的对象实例。这两个对象就对应上图中的object1和object5. 我们发现MySharedObject 中的成员变量是原始数据类型,但由于他们是成员变量,所以依旧存储在堆上。只有本地变量存储在线程栈中。 硬件层面的内存模型硬件层面的内存内存结构与JVM中的内存结构是有不同的,对我们来说,正确理解掌握硬件层面的内存模型是很必要的,这可以帮助我们理解java多线程的底层机制,更要了解java内存模型如何在硬件内存结构上工作。这一章将讲述硬件层面内存模型,下一部分将讲述java如何结合硬件工作。 下图是一个简化的现代计算机硬件结构图: 现代计算机通常会有两个甚至更多的cpu,这些cpu可能还会有多个核心,这个意义是,拥有多个cpu的计算机可能会有多个线程在同时执行,每个cpu都可以在任何给定的时间运行一个线程。这就意味着如果我们的java程序是多线程的,在内部就每个线程就会有一个cpu在同时执行。 每个cpu都会有一系列的寄存器registers在cpu的内存中,而且这些寄存器是很重要的。cpu在寄存器上进行计算操作比在主内存中进行计算要快的多。这是因为cpu访问寄存器的速度比访问内存要快得多。 每个cpu也会有一个cpu的cache内存。这是因为cpu访问cache比访问内存的速度要快得多,但是却比访问的寄存器要慢一些,所以cache的速度是介于寄存器和内存的。一些cpu还有多级cache,比如(Level 1 and Level 2),但是这对于我们理解java内存模型关系不大,我们只需要cpu有三层内存结构,寄存器-cache-内存(RAM). 一台计算机一般都会有主内存也就是RAM,所有cpu都可以访问主内存,主内存的容量一般远比cache大得多。 一般的,当cpu需要访问内存的时候,他会先读取一部分主内存到cache中,甚至,会读取一部分cache到内部的寄存器中,然后再在寄存器进行计算操作。当cpu将计算结果写回内存中时,他会flush寄存器和cache中的数据,然后将值写回至内存中。 当cpu要求cache去存储其他内容时,也会将cache中的内容flush到内存中。cpu的cache可以边写入一部分数据到内存,边写入一部分到自己cache中,所以在更新数据,不必要全部清空cache,可以边读边写。一般的,cache真正更新数据是在更小的内存块上,叫做“cache lines”。多个“cache lines”可能正在读取数据到cache中,而另一部分可能正在将数据写回到内存中。 Java内存模型和硬件内存模型的联系上文已经提到,java内存模型和硬件内存模型是不同的。硬件内存模型不区分堆和栈。在硬件层面,所有的线程栈和堆都被存储在主内存中,一部分线程栈和堆可能有时候会出现在cpu cache中和cpu寄存器中。下图可以说明这个问题: 当对象和变量被存储在不同的内存区域的时候,很多问题就可能发生,主要有以下两类问题: 当线程对一些共享数据进行更新或者写操作时,可见性的问题 当读写共享数据产生资源竞速的问题接下来的部分就会讨论这两个问题 共享对象的可见性如果多个线程在共享一个对象,没有正确使用volatile或者synchronize声明,更新共享对象的时候就可能出现其他线程不可见的问题。 我们假设共享对象初始化主内存中。一个在cpu中运行的线程读取共享对象到cache中。这时候,随着程序的执行,可能导致共享对象发生一些变化。只要cpu的cache还没有被写回到主内存中,这个共享对象的变化就对其他在cpu上运行的线程不可见。这种情况下,每个线程都会有持有一份自己对于共享对象的拷贝,这份拷贝存储在各自的cpu的cache中,而且对于其他线程是不可见的。 下图说明了大致的情况,在左边cpu执行的线程将共享对象读取到cache中,并且将他的值改变为2.这个变化对右边的cpu的其他线程是不可见的,因为对于变量count的更新还没有被写回到主内存中。 想要解决这个共享对象可见性的问题,可以使用java的volatile关键字(参见笔者的另一篇volatile的博文),这个关键字可以保证所给定的变量都是直接从主内存中读取,而且每当更新时就立即写回到内存中,所以可以保证变化是及时可见的。 资源竞速如果多个线程共享一个对象,而且多个线程需要更新共享对象中的变量,那么就可能造成资源竞速的发生。 假设线程A读取读取一个共享对象的变量count到cpu的cache中,同时,线程B也执行同样的步骤,但是是读取到一个不同的CPU的cache中,现在线程A给count加一,线程B也做同样的事情,现在这个变量被加了两次,分别在不同的cpu的cache中。 如果这两次递增操作是被按顺序先后执行的,这个变量count就会被加两次而且比最初的值加了2,写回到主内存中。 然而,如果这两个递增操作是并发执行的,且没有正确的进行同步操作,写回内存的时候,更新后的值只会被加一,虽然实际上是进行了两次递增操作。下图就说明了程序并发执行的时候,产生的资源竞速的问题: 想要解决这个问题,我们可以使用java中的synchronize关键字。synchronize可以保证只有一个线程能进入那些被声明为synchronize的代码段中。同步的线程可以保证所有同步代码段中的变量都会从内存中读取,而且当线程离开代码块的时候,所有更新后的值都会被写回主内存中,不管这个变量有没有被声明volatile。 小结本文详细的剖析了java内存模型和硬件层面的内存模型,并且分析了硬件和java是怎么在内存模型上合作联系的。这对于我们接下来理解java多线程的概念是及其重要的,打下了牢固的基础。]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
<tag>多线程</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java动态代理与静态代理]]></title>
<url>%2F2017%2F03%2F04%2FJava%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E4%B8%8E%E9%9D%99%E6%80%81%E4%BB%A3%E7%90%86%2F</url>
<content type="text"><![CDATA[我们先看一个简单的例子,当我们需要程序中加入方法执行的日志信息的时候,很显然我们最容易想到的实现方法,就是在方法前后插入日志记录信息。 123456789101112131415import java.util.logging.*;public class HelloSpeaker { private Logger logger = Logger.getLogger(this.getClass().getName()); public void hello(String name) { // 方法執行開始時留下日誌 logger.log(Level.INFO, "hello method starts...."); // 程式主要功能 System.out.println("Hello, " + name); // 方法執行完畢前留下日誌 logger.log(Level.INFO, "hello method ends...."); }} 然后这种实现方式有明显的不足,这种切入式的代码(Cross-cutting),会使得HelloSpeaker拥有了本该不属于他的职责,要在hello的同时记录日志。 试想一下,如果程序中的代码到处都是这种日志需求,那么我们的就必须在到处都加上这些日志代码,想必那是很大的工作量,而且当我们需要修改密码的时候,将会变得更加复杂,维护起来变得困难,所以我们自然想到封装,由于很多对象都需要日志记录这种需求,我们何不把日志行为分离出来。 这时候就可以代理模式解决这个问题,代理又分为静态代理(Static proxy)和动态代理(Dynamic proxy) 静态代理在静态代理模式中,代理与被代理对象必须实现同一个接口,代理专注于实现日志记录需求,并在合适的时候,调用被代理对象,这样被代理对象就可以专注于执行业务逻辑。 改进上面那个例子首先定义一个接口 IHello.java 123456package Reflection;public interface IHello { public void hello(String name);} 然后专注于业务逻辑实现HelloSpeaker实现上面这个接口: HelloSpeaker.java 1234567891011package Reflection;public class HelloSpeaker implements IHello { @Override public void hello(String name) { System.out.println("Hello, " + name); } } 可以看到,在这个类中没有日志记录的代码,其只需要专注于实现业务功能,而记录日志的工作则可以交给代理对象来实现,代理对象也要实现Ihello接口: HelloProxy.java 1234567891011121314151617181920212223242526272829package Reflection;import java.util.logging.*; public class HelloProxy implements IHello { private Logger logger = Logger.getLogger(this.getClass().getName()); private IHello helloObject; public HelloProxy(IHello helloObject) { this.helloObject = helloObject; } public void hello(String name) { // 日誌服務 log("hello method starts...."); // 執行商務邏輯 helloObject.hello(name); // 日誌服務 log("hello method ends...."); } private void log(String msg) { logger.log(Level.INFO, msg); }} 我们可以看到在hello方法的实现中,前后插入了日志记录的方法。下面我们就测试一下1234567public class ProxyDemo { public static void main(String[] args) { IHello proxy = new HelloProxy(new HelloSpeaker()); proxy.hello("Justin"); }} 程序中执行hello方法的是代理对象,实例化代理对象的时候,必须传入被代理对象,而且声明代理对象的时候,必须使用代理对象和被代理对象共同实现的接口,以便实现多态。 代理对象将代理真正执行hello方法的被代理对象来执行hello,并在执行的前后加入日志记录的操作这样就可以使业务代码专注于业务实现。 这就是静态代理 动态代理jdk1.3加入了动态代理相关的API,从上面静态代理的例子我们知道,静态代理,需要为被代理对象和方法实现撰写特定的代理对象,显然这样做并不灵活,我们希望可以有一个公用的代理,可以动态的实现对不同对象的代理,这就需要利用到反射机制和动态代理机制。在动态代理中,一个handler可以代理服务各种对象,首先,每一个handler都必须继承实现java.lang.reflect.InvocationHandler接口,下面具体实例说明,依然是上面那个记录日志的例子 LogHandler.java 12345678910111213141516171819202122232425262728293031323334353637383940package Reflection;import java.util.logging.*; import java.lang.reflect.*; public class LogHandler implements InvocationHandler { private Logger logger = Logger.getLogger(this.getClass().getName()); private Object delegate; public Object bind(Object delegate) { this.delegate = delegate; return Proxy.newProxyInstance( delegate.getClass().getClassLoader(), delegate.getClass().getInterfaces(), this); } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result = null; try { log("method starts..." + method); result = method.invoke(delegate, args); logger.log(Level.INFO, "method ends..." + method); } catch (Exception e){ log(e.toString()); } return result; } private void log(String message) { logger.log(Level.INFO, message); }} 具体来说就是使用Proxy.newProxyInstance()静态方法new一个代理对象出来,底层会使用反射机制,建立代理对象的时候,需要传入被代理对象的class,以及被代理对象的所实现的接口,以及代理方法调用的调用程序 InvocationHandler,即实现 InvocationHandler接口的对象。这个对象会返回一个指定类指定接口,指定 InvocationHandler的代理类实例,这个实例执行方法时,每次都会调用 InvocationHandler的invoke方法,invoke方法会传入被代理对象的方法与方法参数,实际方法的执行会交给method.invoke().所以我们就可以在其前后加上日志记录的工作。 接下来我们就来测试一下,使用logHandler的bind方法来绑定代理对象: 123456789101112131415161718package Reflection;import java.lang.reflect.Proxy;public class ProxyDemo { public static void main(String[] args) { LogHandler logHandler = new LogHandler(); IHello helloProxy = (IHello) logHandler.bind(new HelloSpeaker()); helloProxy.hello("baba"); }}]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入理解javascript中的原型]]></title>
<url>%2F2017%2F01%2F25%2F%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3javascript%E4%B8%AD%E7%9A%84%E5%8E%9F%E5%9E%8B%2F</url>
<content type="text"><![CDATA[原型prototype是javascript中极其重要的概念之一,但也是比较容易引起混淆的地方。我们需要花费一些时间和精力好好理解原型的概念,这对于我们学习javascript是必须的。 原型的概念真正理解什么是原型是学习原型理论的关键。很多人在此产生了混淆,没有真正理解,自然后续疑惑更多。 首先,我们明确原型是一个对象,其次,最重要的是, Every function has a prototype property and it contains an object 这句话就是说,每个函数都有一个属性叫做原型,这个属性指向一个对象。也就是说,原型是函数对象的属性,不是所有对象的属性,对象经过构造函数new出来,那么这个new出来的对象的构造函数有一个属性叫原型。明确这一点很重要。 The prototype property is a property that is available to you as soon as you define the function. Its initial value is an “empty” object. 每次你定义一个函数的时候,这个函数的原型属性也就被定义出来了,也就可以使用了,如果不对它进行显示赋值的话,那么它的初始值就是一个空的对象Object。所以,综上我们知道我们讨论原型的时候,都是基于函数的,有了一个函数对象,就有了原型。切记这一点,讨论原型,不能脱离了函数,它是原型真正归属的地方, 原型只是函数的一个属性 ! 12345function foo(a,b) { return a+b;}foo.prototypefoo.constructor chrome控制台测试结果 我们可以看到函数foo的原型是空对象Object,所有函数的构造函数都是Function。 使用原型给对象添加方法和属性不使用原型,使用构造函数给对象添加属性和方法的是通过this,像下面这样。1234567function Gadget(name, color) { this.name = name; this.color = color; this.whatAreYou = function() { return 'I am ' + this.color + ' ' + this.name; }} Gadget是一个构造函数,作为一个函数,它有一个属性,这个属性是原型,它指向一个对象,目前我们没有设置这个属性,所以它是一个空的对象。 Adding methods and properties to the prototype property of the constructorfunction is another way to add functionality to the objects this constructor produces 当我们有了原型之后,我们可以给构造函数的原型对象添加属性和方法来。像下面这样12345Gadget.prototype.price = 100;Gadget.prototype.rating = 3;Gadget.prototype.getInfo = function() { return 'Rating: ' + this.rating +', price: ' + this.price;} 给原型添加了属性和方法后,原型所指的对象也会更新 使用原型对象的属性和方法我们使用原型的对象和方法不会在直接在构造函数上使用,而是通过构造函数new出一个对象,那么new出来的对象就会有构造函数原型里的属性和方法。 这里很容易造成误解,我们需要强调newtoy这个new出来的对象是没有原型的,原型只是函数对象的一个属性,newtoy是通过构造函数new出来的对象,所以他不是函数对象,也没有prototype属性,我们在chrome的控制台里自然也无法访问他的prototype属性。但我们可以通过构造函数访问。我们知道每个对象都有constructor属性,newtoy的constructor属性就指向Gadget,那么我们通过constructor可以访问到prototype。 到这里,我们对为什么要通过constructor.protptype访问属性应该清楚了。(笔者第一次接触原型就没看懂这个),切记,原型是函数对象的属性,只有函数对象才有原型就容易理解了。 原型的实时性这里特别需要提出,原型是实时的,意思就是原型对象的属性和方法会实时更新。其实很好理解,javascript中对象是通过引用传递的,原型对象只有一份,不是new出一个对象就复制一份,所以我们对原型的操作和更新,会影响到所有的对象。这就是原型对象的实时性。 自身属性与原型属性这里涉及到javascript是如何搜索属性和方法的,javascript会先在对象的自身属性里寻找,如果找到了就输出,如果在自身属性里没有找到,那么接着到构造函数的原型属性里去找,如果找到了就输出,如果没找到,就null。所以,如果碰到了自身属性和原型属性里有同名属性,那么根据javascript寻找属性的过程,显然,如果我们直接访问的话,会得到自身属性里面的值。 我们加下来做一个小实验,寻找toString方法是谁的属性,一步步寻找 通过实验我们可以发现,原来toString方法是object的原型对象的方法。 isPrototypeOf()Object的原型里还有这样一个方法isPrototypeOf(),这个方法可以返回一个特定的对象是不是另一个对象的原型,实际这里不准确,因为我们知道只有函数对象有原型属性,普通对象通过构造函数new出来,自动继承了构造的函数原型的属性方法。但这个方法是可以直接判断,而不需要先取出constructor对象再访问prototype。看下面的例子:123456789101112function Human(name) { this.name = name;}var monkey = { hair:true, feeds:'banana',}Human.prototype = monkey;var chi = new Human('chi'); 我们知道chi这个对象是没有原型属性的,它有的是他的构造函数的原型属性monkey。但isPrototypeOf直接判断,实际上是省略了获取构造函数的过程,搞清楚这里面的区别。object还有一个getPrototypeOf方法,基本用法和isPrototype一样,参考下面的代码: 神秘的proto链接我们之前访问对象的原型,都要先取得构造函数然后访问prototype12chi.constructor.prototype;newtoy.constructor.prototype; 这样是不是特别别扭,所以各个浏览器一般都会给出一个proto属性,前后分别有双下划线,对象的这个属性可以直接访问到构造函数的原型。这就很方便了。所以proto与prototype是有很大区别的。区别就在此。proto是实例对象用来直接访问构造函数的属性,prototype是函数对象的原型属性。 1chi.constructor.prototype == chi.__proto__ 显然现在已经很容易弄清楚了proto和prototype的区别了。 原型的陷阱原型在使用的时候有一个陷阱: 在我们完全替换掉原型对象的时候,原型会失去实时性,同时原型的构造函数属性不可靠,不是理论上应该的值。这个陷进说的是什么呢?好像不太明白举个例子我们就懂了12345678910function Dog() { this.tail = true;}var benji = new Dog();var rusty = new Dog();Dog.prototype.say = function () { return 'Woof!';}; 我们进行测试: 直到这里一切都是正常的接下来我们将原型对象整个替换掉1234Dog.prototype = { paws: 4, hair: true}; 通过测试我们发现,我们没法访问刚刚更新的原型对象,却能访问之前的原型对象,这说明没有实现实时性。 我们继续测试 我们发现这时新建的对象可以访问更新后的原型,但是构造方法又不对了,本来constructor属性应该指向dog,结果却指向了Object。这就是javascript中的原型陷阱。 我们很容易解决这个问题,只要在更新原型对象后面,重新指定构造函数即可。1Dog.prototype.constructor = Dog; 这样所有就按正常的运行了 所以我们切记在替换掉原型对象之后,切记重新设置constructor.prototype 小结我们大概介绍了原型中容易混淆的问题,主要有以下几方面: 所有函数都有一个属性prototype,这就是我们指的原型,他的初始值是一个空的对象 你可以原型对象添加属性和方法,甚至直接用另一个对象替换他 当你用构造函数new出一个对象之后,这个对象可以访问构造函数的原型对象的属性和方法 对象的自身属性搜索的优先级比原型的属性要高 proto属性的神秘连接及其同prototype的区别 prototype使用中的陷阱]]></content>
<categories>
<category>javascript</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[设计模式之装饰者模式Decorator_Pattern]]></title>
<url>%2F2016%2F07%2F27%2F%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B9%8B%E8%A3%85%E9%A5%B0%E8%80%85%E6%A8%A1%E5%BC%8FDecorator-Pattern%2F</url>
<content type="text"><![CDATA[装饰者模式可以做到在不修改任何底层代码的情况下,给对象增加的新的方法。首先,我们通过对一个现实问题的模拟分析,了解什么是装饰者模式以及装饰者模式的作用。 问题提出咖啡店在街头随处可见。我们以咖啡店的饮品订单系统为例。假设我们要设计一个饮品的订单系统。设计了一个这样的类图: Beverage是一个 抽象类,所有咖啡店的饮品都必须继承这个类,description是饮品的描述信息,cost()是计算此种饮品的价格。 我们会遇到这样的问题,在购买饮品的时候,我们可以要求在其中加入不同的调料配品,比如,摩卡(Mocha),加奶泡等。除了原本饮料需要的价格的外,咖啡店会根据所加入调料的再收取不同的费用。 如果按照之前的设计方式,那么会出现如下的情况: 显然这似乎已经是类爆炸了! 而且我们永远无法预测,顾客会选取怎样的调料的搭配,每当出现一个新的调料搭配时,我们就需要增加一个新的类。更加糟糕的是,当原料配料的价格上涨后或者下降后,那么所有涉及到这种配料的类都得重新改过。这简直是个噩梦!很显然这很不符合我们设计模式的原则。作为一个程序员,我们是决不能容忍这种情况发生的! 那么我们该如何设计呢? 这里就需要用到我们的装饰者模式! 引出装饰者模式让我们转换思路,我们以饮品beverage为主体,在运行时以顾客选择的调料来装饰beverage。比如,如果顾客想要摩卡和奶泡的拿铁咖啡,我们要做的应该是这样的: 取一个拿铁咖啡的对象 用摩卡对象装饰它 用奶泡对象装饰它 调用cost方法计算价钱,并依赖委托将配料摩卡和奶泡加上去。 会先计算whip的cost然后调用mocha的cost,然后调用拿铁的cost,这样就计算出了总价格。这样就是实现的装饰者模式解决这个问题的思路。下面我们看一下装饰者模式的定义,以及代码实现的基本思路 定义装饰者模式装饰者模式动态的将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。 这个类图就是装饰者模式的实现方式。更详细的是如下这个版本的类图。 下面我们就根据这个类图来解决我们之前在实现咖啡店饮料系统上遇到的问题。 分析设计类图: beverage相当于抽象的component类,具体的component和decorator都需要继承实现这个抽象类。 四个具体的饮料的类,相当于concrete component!每一个类代表了一个饮料类型。 condimentDecorator是抽象的decorator类,它是所有调料类的抽象,它保存了beverage的一个引用。 调料装饰者类继承自condimentDecorator,是各种具体调料的实现,他们都实现了cost方法。 上面有一个非常关键的地方,就是我们注意到装饰者和被装饰者必须是一样的类型,也就是拥有共同的超类。这样做是因为我们要装饰者必须能取代被装饰者。这样我们就可以利用对象的组合,将调料和饮料的行为组合起来。这符合我们之前提到的设计原则多用组合,少用继承 实现装饰者模式如果看到这里还是不太清楚,也没关系,接下来我们将具体实现代码,对装饰者模式有一个直观根本的了解。 首先实现beverage和condiment两个抽象类 1234567891011package abstractComponent;public abstract class Beverage { protected String description = "Unknow Beverage"; public String getDescription() { return description; } public abstract double cost();} 1234567package abstractDecrator;import abstractComponent.Beverage;public abstract class CondimentDecorator extends Beverage { public abstract String getDescription();} 然后我们实现具体的饮料类 12345678910111213package concreteComponent;import abstractComponent.Beverage;public class Coco extends Beverage { public Coco(){ description = "Coco"; } public double cost(){ return 0.89; }} 12345678910111213package concreteComponent;import abstractComponent.Beverage;public class Espresso extends Beverage { public Espresso() { description = "Espresso"; } public double cost() { return 1.99; }} 我们再实现具体的装饰者类,也就是调料类 123456789101112131415161718192021222324252627package concreteDecorator;import abstractComponent.Beverage;import abstractDecrator.CondimentDecorator;public class Mocha extends CondimentDecorator { Beverage beverage; public Mocha(Beverage beverage){ this.beverage = beverage; } @Override public double cost() { // TODO Auto-generated method stub return .20 + beverage.cost(); } @Override public String getDescription() { // TODO Auto-generated method stub return beverage.getDescription() + ", Mocha"; }} 1234567891011121314151617181920212223242526package concreteDecorator;import abstractComponent.Beverage;import abstractDecrator.CondimentDecorator;public class Soy extends CondimentDecorator { Beverage beverage; public Soy(Beverage beverage){ this.beverage = beverage; } @Override public String getDescription() { // TODO Auto-generated method stub return beverage.getDescription() + ", Soy"; } @Override public double cost() { // TODO Auto-generated method stub return .15 + beverage.cost(); }} 1234567891011121314151617181920212223242526package concreteDecorator;import abstractComponent.Beverage;import abstractDecrator.CondimentDecorator;public class Whip extends CondimentDecorator { Beverage beverage; public Whip(Beverage beverage){ this.beverage = beverage; } @Override public String getDescription() { // TODO Auto-generated method stub return beverage.getDescription() + " , whip"; } @Override public double cost() { // TODO Auto-generated method stub return .10 + beverage.cost(); }} 最后编写一个测试类,来测试我们装饰者模式的效果如何 1234567891011121314151617181920212223242526272829import concreteComponent.Coco;import concreteComponent.Espresso;import concreteDecorator.Mocha;import concreteDecorator.Soy;import concreteDecorator.Whip;import abstractComponent.Beverage;public class Test { public static void main(String[] args) { // TODO Auto-generated method stub Beverage beverage = new Espresso(); System.out.println( beverage.getDescription() + "$" + beverage.cost()); Beverage beverage2 = new Coco(); beverage2 = new Mocha(beverage2); beverage2 = new Mocha(beverage2); beverage2 = new Whip(beverage2); System.out.println( beverage2.getDescription() + "$" + beverage2.cost()); Beverage beverage3 = new Espresso(); beverage3 = new Whip(beverage3); beverage3 = new Mocha(beverage3); beverage3 = new Soy(beverage3); System.out.println( beverage3.getDescription() + "$" + beverage3.cost()); }} 总结与分析通过装饰者模式我们可以很好的解决咖啡店的问题,用装饰者去包装组件,可以达到很好的可扩展性。 装饰者模式用到的技术主要有两种就是组合和委托,这帮助我们动态的在运行时加上新的行为。 装饰者模式意味着一群装饰者类,这些类用来包装装饰者。 装饰者和被装饰者类实际上具有相同类型的。 装饰者可以在被装饰者的行为前面或后面加上自己的行为,甚至完全覆盖。 但装饰者模式的使用会导致出现很多小对象,就是装饰者对象,过度使用也会使程序变得复杂。]]></content>
<categories>
<category>设计模式</category>
</categories>
<tags>
<tag>设计模式</tag>
</tags>
</entry>
<entry>
<title><![CDATA[设计模式之观察者模式(Observer_Pattern)]]></title>
<url>%2F2016%2F07%2F23%2F%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B9%8B%E8%A7%82%E5%AF%9F%E8%80%85%E6%A8%A1%E5%BC%8F%EF%BC%88Observer-Pattern%EF%BC%89%2F</url>
<content type="text"><![CDATA[在正式介绍观察者模式前,我们先引用生活中的小例子来模拟观察者,先对观察者模式有一个整体的感觉。 现实模拟报纸和杂志的故事。我们看看报纸和杂志的订阅是怎么一回事: 报纸的任务就是出版报纸 我们向某家报社订阅报纸,只要他们有新报纸出版,就会给你送来,只要你是他们的订户,你就会一直得到新报纸 当你们不想再看报纸的时候,向报社取消订阅,他们就不会再送报纸来,你也不会再收到报纸 只要报社还在运营,就会有人向他们订阅或者取消报纸 这其实就可以理解为是一种观察者模式。报社出版者被认为是观察者模式中的Subject,订阅报纸的人被认为是观察者模式中的Observer。具体的观察者模式的subject和observer我们后面会介绍。 订阅者通常有很多个,他们订阅或者取消需要通知出版者。出版者当报纸有更新时,就会把新报纸一起推送给订阅者,所有订阅者都会收到出版社的所有更新。再举个常见的例子,我们常见的手机app,网易新闻或者其他类。只要我们安装了这个这个应用,并在app设置接收应用的消息通知,那么当app有新消息通知时,我们就会收到新消息。这里,我们用户就是观察者,app就是Subject。 观察者模式定义观察者模式是设计模式中很常用的一个模式。比较严格的解释是: 观察者模式定义了对象之间的一对多的依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。 跟图中的例子一样,主题和观察者定义了一对多的关系。观察者依赖于此主题,只要主题状态一有变化,观察者就会被通知。 观察者模式的类图可以很好的观察者模式的设计思想 观察者的设计方式有很多种,但其中实现Subject和observer接口的设计方式是最常用的、 Subject的接口有三个方法,分别是注册观察者,移除观察者和通知观察者。对象通过Subject接口注册成为观察者,同事也可以通过它从解除观察者的身份,也就是之前例子中的取消订阅报纸。 每个Subject通常可以有很多个观察者 具体的Subject对象需要实现Subject接口的三个方法,其中notify方法是用于当状态发生变化时,来通知观察者update,里面一般要调用观察者接口的update方法。 所有的观察者都需要实现Observer接口,并实现其中的update方法,以便当主题状态发生变化,观察者得到主题的通知。用于Subject具体实现类的notify方法的调用。 具体的Observer都需要继承至接口,同时他们必须注册到具体的Subject对象,以成为一个观察者,并得到更新。 观察者实现的设计原则 观察者模式提供了一种对象设计,让主题和观察者之间松耦合 关于观察者的一切,主题只需要知道观察者实现了某个接口也就是Observer接口,主题不需要知道观察者的具体的实现类是谁,做了些什么或者其他任何细节,主题都不需要知道。 任何时候我们都可以增加新的观察者,因为主题唯一依赖的东西是一个实现Observer接口的对象列表,所以我们可以随时增加观察者。事实上,在运行时我们可以用新的观察者取代现有的观察者,主题不会受任何影响。同样的,也可以在任何时候删除观察者。 当有新的类型的观察者出现时,主题的代码不会发生修改。假如我们有个新的具体类需要当观察者,我们不需要为了兼容新类型而修改主题的代码,所需要的只是在新的类里实现此观察者的接口,然后注册为观察者即可。 这里体现了一个设计原则就是 为了交互对象之间的松耦合设计而努力 争取让对象之间的互相依赖降到最低 代码实现我们考虑这样一个问题:实现一个气象站监测应用。 有三个部分,气象站(获取实际气象数据的装置),weatherData对象(追踪来自气象站的数据,并更新布告板)和布告板(显示目前天气的状况给用户看) 我们要做到的就是建立一个应用,利用weatherdata对象获取数据,并更新三个布告板。我们对气象站的初步设计图: 根据观察者设计了一个类图,接下来我们实现这个类图。从建立接口开始, 1234567package com.liu.itf;public interface Subject { public void registerObserver(Observers o); public void removeObserver(Observers o); public void notifyObserver();} 12345package com.liu.itf;public interface Observers { public void update(float temp, float humidity, float pressure);} 12345package com.liu.itf;public interface DisplayElement { public void display();} 接下来在weatherdata类中实现Subject接口12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455package com.liu.model;import java.util.ArrayList;import com.liu.itf.Observers;import com.liu.itf.Subject;public class WeatherData implements Subject { private ArrayList<Observers> observers; private float temperature; private float pressure; private float humidity; public WeatherData() { // TODO Auto-generated constructor stub observers = new ArrayList<Observers>(); } @Override public void registerObserver(Observers o) { // TODO Auto-generated method stub observers.add(o); } @Override public void removeObserver(Observers o) { // TODO Auto-generated method stub int i = observers.indexOf(o); if(i>=0) { observers.remove(o); } } @Override public void notifyObserver() { // TODO Auto-generated method stub for(int i=0;i<observers.size();i++) { Observers observer = (Observers)observers.get(i); observer.update(temperature, humidity, pressure); } } public void measurementsChanged() { notifyObserver(); } public void setMeasurements(float temperature, float humidity,float pressure) { this.humidity = humidity; this.temperature = temperature; this.pressure = pressure; measurementsChanged(); }} 布告板类作为观察者实现观察者接口和display接口123456789101112131415161718192021222324252627282930313233package com.liu.view;import com.liu.itf.DisplayElement;import com.liu.itf.Observers;import com.liu.itf.Subject;public class CurrentConditionDisplay implements DisplayElement, Observers { private float temperature; private float pressure; private float humidity; private Subject weatherData; public CurrentConditionDisplay(Subject weatherData) { this.weatherData = weatherData; weatherData.registerObserver(this); } @Override public void update(float temp, float humidity, float pressure) { // TODO Auto-generated method stub this.temperature = temp; this.humidity = humidity; display(); } @Override public void display() { // TODO Auto-generated method stub System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity"); }} 编写一个测试类:1234567891011121314import com.liu.model.WeatherData;import com.liu.view.CurrentConditionDisplay;public class WeatherStation { public static void main(String[] args) { WeatherData weatherData = new WeatherData(); CurrentConditionDisplay currentConditionDisplay = new CurrentConditionDisplay(weatherData); weatherData.setMeasurements(80, 45, 30.4f); }} 小结 观察者定义了对象之间一对多的关系。 主题用一个共同的接口来更新观察者 观察者和主题之间用松耦合的方式连接,主题不知道观察者的细节,只知道观察者实现了观察者接口]]></content>
<categories>
<category>设计模式</category>
</categories>
<tags>
<tag>设计模式</tag>
</tags>
</entry>
<entry>
<title><![CDATA[设计模式之策略模式(Strategy Pattern)]]></title>
<url>%2F2016%2F07%2F20%2F%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B9%8B%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F%EF%BC%88Strategy-Pattern%EF%BC%89%2F</url>
<content type="text"><![CDATA[策略模式,是我们接触到的第一个设计模式,也是较容易理解的一个模式。我们可以给它下一个定义:定义了算法族,分别封装起来,让它们之间可以互相转换,此模式让算法的独立于使用算法的客户维基百科上的定义是:a software design pattern that enables an algorithm’s behavior to be selected at runtime.维基百科上的强调了算法行为是在运行时决定的,这正是策略模式很关键的一点。 引子假设我们现在要设计一个鸭子类Duck类,然后让不同的鸭子继承于它。我们把目光聚焦到鸭子的行为上。如果我们要给鸭子增加一个行为“fly”,第一个想法,在抽象类duck里添加一个fly方法就可,其余鸭子继承实现这个方法。但是这就出现了一个问题,并不是所有鸭子都会飞,我们反而让一些本不具备这个fly行为的鸭子也具有该行为。那怎么办呢?利用继承来提供鸭子的行为,会导致下面这些后果: 代码在多个子类中重复,如果两类不同鸭子需要同一种fly行为,我们就要在两个类里分别覆盖两次,这样万一维护起来是非常困难的 很难知道所有鸭子的全部行为 运行时的行为不容易改变 改变会一发动全身,造成其他鸭子不想要改变 设计原则1软件开发中,我们常常需要遵守的设计原则是:把可能需要变化的地方独立出来,不要和那些不需要变化的代码混在一起这样代码变化引起的不经意后果变少,系统变得更有弹性实际就是尽量让系统中某部分的改变不影响其他部分的变化。 #提取鸭子的的行为根据设计原则,鸭子飞行的行为会发生变化,所以我们需要将fly行为单独提取出来。同理,我们提取出两个鸭子可能变化的行为fly和quack鸭叫。用两组类分别代表fly和quack行为。 #设计原则2那么我们如何那两组鸭子行为的类呢?这里引出第二个我们提出的设计原则:面对接口编程,而不是面对实现编程这样就可以实现在运行时改变鸭子的行为。我们不会直接指定特定的行为给鸭子。而是声明两个接口FlyBehavior和QuackBehavior。我们制造的其他一系列的类专门来实现FlyBehavior和QuackBehavior,这组就成为行为类,或者算法类。用行为类来实现接口而不是利用duck类来实现。 实现鸭子的行为根据设计原则2,可以让飞行和鸭叫行为的动作被其他对象复用,因为这些为行为已经与鸭子类无关了。而且当我们新增一些行为的时候,不会影响到既有的行为类,也不会影响鸭子类。太棒了! 很多同学都觉得这里用类来代表行为是不是觉得很奇怪。在大家默认里,类应该是代表某种东西的,类应该拥有状态与行为。这里,我们需要纠正这个观点,一个行为也可以具有各种属性和函数。类不仅仅是用来代表东西事物的。 整合实现我们设计的鸭子类首先,在duck类中加入两个实例变量,分别声明为两个接口的类型,每个鸭子对象都会动态的设置这些变量以便在运行时引用正确的行为类型 duck类的实现123456789101112131415161718192021222324252627282930313233343536package strategyPattern;//抽象的Duck类,所有鸭子都继承public abstract class Duck { //为行为接口类型声明两个引用变量,所有鸭子类都继承它们 FlyBehavior flyBehavior; QuackBehavior quackBehavior; public void setFlyBehavior(FlyBehavior flyBehavior) { this.flyBehavior = flyBehavior; } public void setQuackBehavior(QuackBehavior quackBehavior) { this.quackBehavior = quackBehavior; } public Duck() { } public abstract void display(); public void performFly() { flyBehavior.fly(); //委托给fly行为类实现 } public void performQuack() { quackBehavior.quack(); //委托给quack行为类实现 } //swim是所有鸭子都共同拥有的方法,所以可以直接在在duck类中实现 public void swim() { System.out.println("All ducks float, even decoys!"); }} FlyBehavior接口的实现:123456package strategyPattern;//所有飞行行为类必须实现的接口public interface FlyBehavior { public void fly();} 两个fly行为实现类,继承至FlyBehavior接口1234567891011package strategyPattern;public class FlyWithWings implements FlyBehavior { @Override public void fly() { // TODO Auto-generated method stub System.out.println("I'm flying with wings!"); }} 1234567891011package strategyPattern;public class FlyNoWay implements FlyBehavior { @Override public void fly() { // TODO Auto-generated method stub System.out.println("I can't fly!"); }} Quack接口及其三个行为实现类:1234567891011package strategyPattern;public class Quack implements QuackBehavior { @Override public void quack() { // TODO Auto-generated method stub System.out.println("Quack"); }} 1234567891011package strategyPattern;public class MuteQuack implements QuackBehavior { @Override public void quack() { // TODO Auto-generated method stub System.out.println("<<silence>>"); }} 1234567891011package strategyPattern;public class Squeak implements QuackBehavior { @Override public void quack() { // TODO Auto-generated method stub System.out.println("Squeak"); }} 12345package strategyPattern;public interface QuackBehavior { public void quack();} 一个MallardDuck类继承至Duck类:12345678910111213141516package strategyPattern;public class MallardDuck extends Duck { public MallardDuck() { quackBehavior = new Quack(); flyBehavior = new FlyWithWings(); } @Override public void display() { // TODO Auto-generated method stub System.out.println("I'm a real Mallard duck!"); }} 测试类123456789101112package strategyPattern;public class MiniDuckSimulator { public static void main(String[] args) { Duck mallard = new MallardDuck(); //这里调用mallardduck继承来的performFly方法,进而委托给该对象的quackBehavior对象处理 //也就是最后是调用了继承来的quackBehavior引用对象的quack()方法 mallard.performFly(); mallard.performQuack(); }} 运行结果: 在这里为了实现动态的改变鸭子的行为,我们可以新建一个flyrocketPowered行为类,然后动态的改变其行为:1234567891011package strategyPattern;public class FlyRocketPowered implements FlyBehavior { @Override public void fly() { // TODO Auto-generated method stub System.out.println("I'm flying with a rocket "); }} 1234567891011121314151617package strategyPattern;public class MiniDuckSimulator { public static void main(String[] args) { Duck mallard = new MallardDuck(); //这里调用mallardduck继承来的performFly方法,进而委托给该对象的quackBehavior对象处理 //也就是最后是调用了继承来的quackBehavior引用对象的quack()方法 mallard.performFly(); mallard.performQuack(); Duck model = new ModelDuck(); model.performFly(); model.setFlyBehavior(new FlyRocketPowered()); model.performFly(); }} 运行结果: 每一个鸭子都有一个FlyBehavior和一个quackBehavior,好将飞行和鸭叫委托给他们代为处理。当你将两个类结合起来使用时,如同本例,这就是组合composition。这种做法和继承不同的地方在于,鸭子的行为不是继承来的而是和适当行为对象那个组合来的。设计原则3:多用组合 少用继承 #策略模式总结三个设计原则: 封装变化,分开变化与不变 多用组合,少用继承 面向接口编程,而不是面对实现编程 策略模式:定义了算法族,分别封装起来,让它们之间可以互相转换,此模式让算法的独立于使用算法的客户。 实现策略模式,我们需要对行为或算法实现各自的接口,具体的实现交给继承自这些接口的行为类,不需要在我们的主类鸭子中实现。主类鸭子声明两个接口的引用的实例变量,并设计set方法,这样就能在运行时动态的改变行为。实现独立和复用。]]></content>
<categories>
<category>设计模式</category>
</categories>
<tags>
<tag>设计模式</tag>
</tags>
</entry>
<entry>
<title><![CDATA[session和cookies会话机制详解]]></title>
<url>%2F2016%2F07%2F20%2Fsession%E5%92%8Ccookies%E4%BC%9A%E8%AF%9D%E6%9C%BA%E5%88%B6%E8%AF%A6%E8%A7%A3%2F</url>
<content type="text"><![CDATA[session management会话管理的原理 web请求与响应基于http,而http是无状态协议。所以我们为了跨越多个请求保留用户的状态,需要利用某种工具帮助我们记录与识别每一次请求及请求的其他信息。举个栗子,我们在淘宝购物的时候,首先添加了一本《C++ primer》进入购物车,然后我们又继续去搜索《thinking in java》,继续添加购物车,这时购物车应该有两本书。但如果我们不采取session management会话管理的话,基于http无状态协议,我们在第二次向购物车发出添加请求时,他是无法知道我们第一次添加请求的信息的。所以,我们就需要session management会话管理! 会话管理的基本方式会话管理的基本主要有隐藏域,cookies,与URL重写这几种实现方式。用得较多的是后两种。 隐藏域实现会话管理以一个网络注册信息填写为例。我们在填注册信息的时候,经常遇到填完一个页面的内容之后,还要继续填写下一个页面的内容。但由于http的无状态,那么容易造成的后果,当进入第二页填写的时候,服务器已经不记得我们上一页填写了什么。怎么利用隐藏域解决这个问题呢?顾名思义,其实就是既然服务器不会记得两次请求间的关系,那就由浏览器在每次请求时主动告诉服务器多次请求间的必要信息,但是上一页的信息并不显示在第二页中,而是采用隐藏域的方式。然而显然这种方式是存在各种问题的。比如关掉网页之后,就会遗失信息,而且查看网页源代码时,容易暴露信息,安全性不高。隐藏域并不是servlet/jsp实际会话管理的机制。 cookie实现会话管理cookie是什么?举个简单的例子,现在当我们浏览网站的时候,经常会自动保存账号与密码,这样下次访问的时候,就可以直接登录了。这种技术的实现就是利用了cookie技术。 cookie是存储key-value对的一个文件,务必记住,它是由服务器将cookie添加到response里一并返回给客户端,然后客户端会自动把response里的cookie接收下来,并且保存到本地,下次发出请求的时候,就会把cookie附加在request里,服务器在根据request里的cookie遍历搜索是否有与之符合的信息 具体cookie的实现我们会在后面详细讲到 URL重写实现会话管理URL重写就是将需要记录的信息附加在请求的链接背后,以链接参数的形式发送给服务器识别。具体实现的过程会在后文结合cookie详解。 servlet&jsp中的session会话管理机制 利用httpsession对象进行会话管理。httpsession对象可以保存跨同一个客户多个请求的会话状态。 换句话说,与一个特定客户的整个会话期间看,httpsession会持久储存。 对于会话期间客户做的所有请求,从中得到的所有信息都可以用httpsession对象保存。 httpsession的工作机制 以之前的问卷调查为例,当一个新客户小明填写问卷时,服务器会生成一个httpsession对象,用于保存会话期间小明所选择的信息,服务器会以setAttribute的方式将其保存到httpsession对象中。每个客户会有一个独立的httpsession对象,保存这个客户所有请求所需要保存的信息。 服务器如何识别所有的请求是否来自同一个客户?客户需要一个会话ID来标识自己。就跟我们每个人的身份证号一样。对于客户的第一个请求,容器会生成一个唯一的会话ID,并通过相应把它返回给用户,客户在以后发回一个请求中发回这个会话ID,容器看到ID之后,就会找到匹配的会话,并把这个会话与请求关联。 实现存储会话ID的就是通过cookie! cookie存储在客户端,是被服务器放在response里发回客户端的,以后每次request时,都会把cookie加入到request里。而session是存在服务器的,以属性的形式将会话中的信息存到httpsession对象中。调用时,只要通过httpsession对象调用相应attribute即可。 很多地方总是把session与cookie分开单独讲。但我们通过前面的介绍,不难知道,session实现其会话管理机制时,在如何确定所有请求是否来自同一个客户时,是利用了cookie技术的。所以不应该将cookie与session完全分开讲。 这里产生这个误解的原因。是因为我们对session的会话管理机制不够了解。因为容器在创建session对象时,会帮我们实现所有cookie相关的工作,而我们只需要实现这一句: 1HttpSession session = request.getSession(); 记住: 这个方法不只是创建一个会话,而是会完成所有与cookie相关的工作,只是容器都自动帮我们实现了。我们来看看容器在背后默默为我们做了什么: 建立新的httpsession对象 生成唯一的会话ID 建立新的会话对象 把会话ID与cookie关联 在响应中设置cookie cookie所有的工作都在后台进行。看到这里,是不是很爽?容器几乎帮我们实现了所有cookie工作。从请求中得到会话ID只需一行代码:1HttpSession session = request.getSession(); 与上一部分为响应生成会话ID是一致的其中也在后台实现了一些步骤:if(请求包含一个会话ID) 找到与该ID匹配的会话else if(没有会话ID或者没有匹配的ID) 创建一个新的会话。还是那句话: cookie所有工作都在后台自动进行 cookie的更多用处cookie原先设计的初衷就是为了帮助支持会话状态。但是因为cookie的简便性,容器为我们封装了大量操作。现在cookie已经被越来越运用到各个方面。首先, 我们明确cookie是存在客户端的,实际上就是在客户端与服务端交换的一小段数据(一个name/string对)。由于session在用户关闭浏览器后,会话结束,就会消失,cookie随之应该也会消失。但servlet的API中提供了一些方法,可以让客户端的cookie存活的时间更久一点。这就是cookie相对于session的一大优势所在。我们目前常用的记住用户名和密码,下次登录就是利用cookie在session消失后,还能存活实现的。所以,我们可以定制cookie为我们实现各种功能。]]></content>
<categories>
<category>Web</category>
</categories>
<tags>
<tag>Web</tag>
</tags>
</entry>
</search>