-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.xml
1101 lines (966 loc) · 149 KB
/
index.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Gao J1e's Blog</title>
<link>https://gj1e.github.io/</link>
<description>Recent content on Gao J1e's Blog</description>
<generator>Hugo -- gohugo.io</generator>
<language>en-us</language>
<copyright>郜杰</copyright>
<lastBuildDate>Mon, 23 Mar 2020 21:24:26 +0800</lastBuildDate>
<atom:link href="https://gj1e.github.io/index.xml" rel="self" type="application/rss+xml" />
<item>
<title>CopyOnWriteArrayList</title>
<link>https://gj1e.github.io/posts/2020/03/copyonwritearraylist/</link>
<pubDate>Mon, 23 Mar 2020 21:24:26 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2020/03/copyonwritearraylist/</guid>
<description> 使用场景 适用于读操作远远多于写操作的场景。也是线程安全的一个并发容器。 CopyOnWriteArrayList就是读操作不加锁,只有写操作加锁,而且写操作也不会阻塞读操作,只有写写才会阻塞。
实现原理 CopyOnWriteArrayList的所有可变操作(add,set)并不是在原有的数组上进行修改,而是通过创建底层数组的副本,然后将新元素插入到副本中,然后在用副本将原来的数组替换掉。这样就可以保证写操作不会阻塞到读取操作了。
CopyOnWriteArrayList读取操作 读取操作是不加锁的,原因就是底层数组不会发生修改,只会被另一个数组进行替换,所以可以保证数据安全。
CopyOnWriteArrayList的写操作 写操作是加锁的,保证了同步,防止了多个线程同时Copy出来多个数组副本。
源码解析参考
</description>
</item>
<item>
<title>Java线程池</title>
<link>https://gj1e.github.io/posts/2020/03/java%E7%BA%BF%E7%A8%8B%E6%B1%A0/</link>
<pubDate>Mon, 23 Mar 2020 21:22:54 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2020/03/java%E7%BA%BF%E7%A8%8B%E6%B1%A0/</guid>
<description> 怎么创建线程池 方法一:通过构造方法
方法二:通过Executor框架的工具类Executors来实现三种类型的线程池
FixedThreadPool:该方法返回一个固定数量的线程池。 SingleThreadPool:该法方法返回一个只有一个线程的线程池。 CachedThreadPool:该方法返回一个可根据需要创建新线程的线程池。 为什么要创建线程池呢 降低资源消耗,通过重复利用已经创建的线程,来降低线程创建和销毁时的消耗。
提高响应速度,当任务到达时,可以不用等待线程的创建的时间而直接执行。
提高线程的可管理性,线程是一种稀缺资源,不能无限制创建得节制,否则就降低稳定性。先用线程池,就可以对线程进行统一的调配,管制和监控。
线程池的处理流程 理解这个流程很简单,大家可以想象一下自己住在大学宿舍(带独卫的) 当一个任务到来时(当你想上厕所时)
第一步肯定要先看看核心线程池满了没有,没有满就添加到核心线程池中。 (首先你肯定先看自己宿舍的厕所有没有人用,要是没人用呢肯定不多逼逼,直接解决。)
第二步,如果核心线程池满了,呢就看看工作队列有没有满,没满则加入。 (宿舍厕所被舍友用着,也没办法,你只能排队等着对吧,巧就巧在你的其它舍友也在排队。你等不了。就只能想其它办法。)
第三步,如果工作队列满了,就看看线程池有没有满,没满则加入。 (这时你最佳的解决办法是看看宿舍楼道的公共厕所有没有位置,有位置就解决了。)
第四步,如果线程池也满了,呢就会执行相应的处理策略。 (如果你在第三步都没有解决掉上厕所这个问题,你的朋友可能就会给你出出主意)
在线程池处于饱和状态时,系统默认提供了以下4中策略:
AbortPolicy:直接抛出异常。 (你上不了厕所,你别说抛异常,就是抛啥也不太好使呀。)
CallerRunsPolicy:只用调用者所在线程来运行任务。 (对不起,这个我不知到怎么来比喻,原谅我文采不够。)
DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。 (也就是说,你可以回宿舍,给舍友做个舔狗,让排在第一个的舍友别上厕所,把位置让给你。)
DiscardPolicy:不处理,丢弃掉。(重要) (对不起,我真的不信你能把这个感觉憋回去。)
当然,你也可以根据需要来实现RejectedExecutionHandler接口自定义策略。(觉得朋友的解决方案不靠谱,你可以自己想方案来解决)
参考《Java并发编程的艺术》
</description>
</item>
<item>
<title>ArrayList与LinkedList</title>
<link>https://gj1e.github.io/posts/2020/03/arraylist%E4%B8%8Elinkedlist/</link>
<pubDate>Mon, 23 Mar 2020 21:22:11 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2020/03/arraylist%E4%B8%8Elinkedlist/</guid>
<description> ArrayList底层使用的是一个数组来存储元素,初始容量大小为10。扩容的方式为(旧容量*3)/2+1。也就是说每次扩容大概会增加50%,如果你有一个包含大量元素的ArrayList,那么最终将会有大量的空间浪费掉。
小建议:在创建ArrayList时,要养成合理预估数据量大小,尽量在创建时就把预估容量大小初始化好。
注意:以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为10。
LinkedList底层采用的是一个双向链表结构(JDK1.6之前为循环链表,JDK1.7之后取消了循环。)
ArrayList与LinkedList的区别 ArrayList底层采用数组来保存元素,LinkedList底层采用双向链表来存储元素。
ArrayList支持快速随机访问,LinkedList不支持。
ArrayList的空间浪费体检在列表结尾留有一定的容量空间,LinkedList的空间浪费则体现在每个元素都需要消耗相当的空间用来存储指针和数据。
ArrayList和LinkedList在尾部添加或删除一个元素的开销时间都是固定的。如果在ArrayList中间或者头部插入或删除一个元素,就意味着列表中的剩余元素都会移动,而在LinkedList中间或头部插入或删除一个元素,所用的时间是固定的。
</description>
</item>
<item>
<title>Java基础知识点</title>
<link>https://gj1e.github.io/posts/2020/03/java%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86%E7%82%B9/</link>
<pubDate>Sun, 22 Mar 2020 22:26:39 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2020/03/java%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86%E7%82%B9/</guid>
<description>字符型常量和字符串常量的区别 形式上:
字符型常量使用单引号引起的一个字符,字符串常量是双引号引起的若干的字符。 含义上:
字符常量相当于一个(ASCII值)可以参加表达式运算;字符串常量表示的是一个地址值,表示该字符串在内存中的地址。 内存上:
char型为2个字节,字符串常量为若干个字节。 Constructor可以被重写吗 构造器不可以被重写,但是可以被重载。
重载与重写的区别 重载就是发生在同一个类型。方法名相同,参数类型,个数不同,返回值和访问修饰符也可以不同。
重写就是子类继承父类,子类重写父类允许子类所能访问的方法(言外之意就是父类中private修饰符修饰的方法,子类不能重写)
重写规则:方法名和参数类型必须相同,返回值范围小于等于父类,抛出异常的范围也要小于等于父类,访问修饰符的范围的要大于等于父类。
String、StringBuffer和StringBuilder的区别,String为啥是不可变的 可变性:
String之所以不可变是因为String底层使用了final修饰符来修饰保存字符串的字符数组。private final char value[]
StringBuffer和StringBuilder可变是因为它俩都继承自AbstractStringBuilder类,而AbstractStringBuilder中底层用来保存字符串的数组没有用final关键字来修饰。
安全性:
String是不可变的,可以看作常量,所以是线程安全的。 StringBuffer对父类AbstractStringBuilder中提供的一些对字符串操作的方法增加了同步锁,或者对调用的方法增加了同步锁,所以StringBuffer是线程安全的。 StringBuilder没有对父类中提供的方法,或者在调用父类方法时没有加锁,所以SringBuilder不是线程安全的。 性能:
每次对String类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String对象。
StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象的引用。
同等情况下StringBuilder比StringBuffer的性能高10%左右,但是线程是不安全的。
接口与抽象类的区别 如果一个类含有抽象方法,则称这个类为抽象类。抽象类必须在类前用Abstract关键字来修饰,抽象方法就是没有具体实现的方法。
抽象类中可以含有非抽象方法,但是接口不可以。 &gt; 注意:JDK1.8开始接口可以定义静态方法,可以直接用接口名调用,但实现类和实现是不可以调用的。
接口中的变量会被隐式的指定为public static final 变量,方法则会被隐式的指定为public abstract 方法。 抽象方法则可以使用public protected和default这些修饰符。
一个类可以实现多个接口,但是只能继承一个类。</description>
</item>
<item>
<title>ThreadLocal探秘</title>
<link>https://gj1e.github.io/posts/2020/03/threadlocal%E6%8E%A2%E7%A7%98/</link>
<pubDate>Wed, 18 Mar 2020 21:50:32 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2020/03/threadlocal%E6%8E%A2%E7%A7%98/</guid>
<description>ThreadLocal被称作为线程本地变量,或者线程的本地存储。
ThreadLocal可以让每个线程绑定自己的值。
ThreadLocal将变量在每个线程中都创建了一个副本,这样每个线程就可以访问自己内部的副本变量,它们可以通过get()和set()方法来获取默认值,或者将变量设置为当前线程所要保存的副本的值,这样就能避免了线程安全的问题。
ThreadLocal原理 ThreadLocal内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,它的键值为当前ThreadLocal的变量,value为变量副本。ThreadLocalMap是ThreadLocal的一个内部类。
初始化时,在Thread里,threadLocals为空,通过ThreadLocal变量调用get或set方法就会对threadLocals进行初始化,并且以当前ThreadLocal变量为键值,value为当前ThreadLocal所要保存的值。
然后在当前线程里,如果要使用哪个ThreadLocal变量的副本值,就通过这个ThreadLocal的变量调用get()方法来获取副本值。
ThreadLocal内存泄漏问题 ThreadLocalMap中使用的key为弱引用,而value为强引用,所以如果ThreadLocal没有被外部引用的情况下,在垃圾回收时,Key会被清理掉。这样,ThreadLocalMap中就会出现key为null的Entry。
如果我们不采取任何措施的话,value永远也无法被GC回收,这样就会产生内存泄露。
ThreadLocalMap实现中已经考虑了这种情况,所以在调用get(),set(),remove()方法时会自动清理掉key为null的记录。</description>
</item>
<item>
<title>ConcurrentHashMap探秘</title>
<link>https://gj1e.github.io/posts/2020/03/concurrenthashmap%E6%8E%A2%E7%A7%98/</link>
<pubDate>Wed, 18 Mar 2020 21:08:55 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2020/03/concurrenthashmap%E6%8E%A2%E7%A7%98/</guid>
<description> 它是HashMap的多线程版本,是线程安全的。可以在并发环境下直接使用。为什么放着好好的HashTable不用又要从新实现一个ConcurrentHashMap呢?就是因为效率问题(虽然你安全,但是太慢了,没有爽感。)这个就不一样,即安全,效率还高,这酸爽你能顶的住?
ConcurrentHashMap是将数据分成一段一段存储,然后给每个数据段配一把锁,当一个线程访问占用启用一个数据段的同时,其它数据段也能被其他线程访问。(这种思想就类比公共厕所里的一排蹲坑就行。)
注意:ConcurrentHashMap读不加锁,下文会有解释。
ConcurrentHashMap之内部数据结构 JDK1.7: ConcurrentHashMap是由segment数组结构和HashEntry组成,segment实现了ReentrantLock,所以扮演了一个可重入锁的角色。HashEntry用于存储键值对数据。
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。
注意:初始化时容器里面默认的锁的个数,也就是Segment数组长度为16;具体解释可以看《Java并发编程艺术》P279
图片出处见参考
JDK1.8: ConcurrentHashMap取消了分段锁,采用的CAS+Synchronized来保证并发安全。
数据结构和HashMap1.8的类似,数组+链表/红黑树。当链表长度大于8时,会将链表转为红黑树,来保证查询效率。 Synchronized只锁定当前链表的头节点,或红黑树的首节点。
图片出处见参考
ConcurrentHashMap的get()方法 Segment的get操作非常高效,先经过一次再散列,然后使用这个散列值通过散 列运算定位到Segment,再通过散列算法定位到元素。整个过程时不加锁的,除非读到的值是空才会加锁重读。这就是比HashTable效率高的其中一个点。
为什么不(带)加(TT)锁,还能保证安全呢?
就是因为它的get方法里将要使用的共享变量都定义成了volatile类型了,(这操作真骚,直接从根儿上做了绝育)例如用来统计segment大小的count字段,和用于存储HashEntry的值value。
现在我们解释一下为啥定义成了volatile类型就可以保证安全了呢?
首先,volatile类型的变量可以保证线程之间的修改可见性,能够被多线程同时读取,并且还保证你读的值不过期,这就很骚。
但是有利有弊。缺点就是只能被单线程写,只有一种情况下可以被多线程写,呢就是要写入的值不依赖于原值。
为什么可以保证读到的值不是过期值呢?
因为happens-before原则嘛,任意对volatile类型变量的写,happens-before对这个变量的读。换成人话就是对volatile类型的变量的写操作优先于读操作。
ConcurrentHashMap的put()方法 put方法需要对共享变量进行写操作,所以需要对操作的共享变量加锁,(真刀真枪的时候,还是加锁放心)。put方法是首先定位到具体的Segment,然后在Segment里进行数据的写入操作。整个的写入操作分为两步
第一步:首先判断是否需要扩容 插入元素之前,先判断Segment种的HashEntry数组是否达到阈值(threshlod),如果超过阈值则对数组进行扩容。(看到这里的时候想一想HashMap是先插入在判断,还是先判断再插入)在扩容时,首先会创建一个容量是原来2倍的数组,然后将原数组中的元素进行新一次散列,然后再插入到新数组中。
注意:ConcurrentHashMap不会对整个容器进行扩容,而是只对某个Segment进行扩容。
第二步:进行插入 将需要插入的元素通过散列定位,然后插入。(找准了,再插入)
ConcurrentHashMap的size()方法 size()方法就是来统计整个ConcurrentHashMap里元素的大小,也就是统计出每个segment的大小,然后求和。看似简单,但是细想又不是很简单,虽然,count是个volatile类型的全局变量,但是顶不住有其它线程在统计的时候捣乱呀。比如,当把所有的segment的大小求出准备进行相加时,又有其它线程对count进行了修改,这样统计的结果就不准确了。可是如果在统计过程中将其它方法锁住,呢岂不是效率太低了嘛?酸爽的体验感就有瑕疵了。
牛逼的大佬们给出的解决方案就是,先尝试2次不加锁进行统计,如果在统计过程中,count发生了变化,呢我再去加锁,再来进行统计。(有没有觉得很像自己,先裸爽,顶不住了在保护,既爽还持久,是不是很骚?)
那么是如何来感知count发生变化的呢?
使用了一个modCount变量,在put、remove和clean方法里操作元素前都会将变量modCount进行加1,那么在统计size 前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。
觉得有点CAS的味道。(个人感觉,感觉不对别喷我)
参考 《Java并发编程的艺术》 大佬博客以及图片出处 </description>
</item>
<item>
<title>JVM堆外内存回收</title>
<link>https://gj1e.github.io/posts/2020/03/jvm%E5%A0%86%E5%A4%96%E5%86%85%E5%AD%98%E5%9B%9E%E6%94%B6/</link>
<pubDate>Sun, 15 Mar 2020 17:58:46 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2020/03/jvm%E5%A0%86%E5%A4%96%E5%86%85%E5%AD%98%E5%9B%9E%E6%94%B6/</guid>
<description>堆外内存 JVM启动时分配的内存,称为堆内存,与之相对的,在代码中还可以使用堆外内存,比如Netty,广泛使用了堆外内存,但是这部分的内存并不归JVM管理,GC算法并不会对它们进行回收,所以在使用堆外内存时,要格外小心,防止内存一直得不到释放,造成线上故障。
堆外内存的回收机制 用一个存储在堆内存中的DirectByteBuffer对象来对堆外内存进行操作。每个DirectByteBuffer对象在进行初始化时,都会创建一个Clearn对象,这个Clearn对象会在合适的时候执行unSafe.freeMemory(address),从而回收这块堆内存。
当初始化一块堆外内存时,对象的引用关系如下:
first是cleaner类的静态变量,cleaner对象在初始化的时候会被添加到Cleaner链表中,和first形成引用关系。 ReferenceQueue是用来保存Clearner对象的。
如果该DirectByteBuffer在一次GC中被回收了,那么就只有Cleaner对象唯一存有该堆外内存数据(开始地址,大小容量),在下一次FullGC时,会将该Cleaner对象放入到ReferenceQueue队列中,并触发clean方法。
Cleaner对象的clean方法主要作用是:
将自己从Cleaner链表中删除,从而在下次FullGC时可以被回收掉。 释放对外内存
public void run() { if (address == 0) { // Paranoia return; } unsafe.freeMemory(address); address = 0; Bits.unreserveMemory(size, capacity); } 如果JVM一直不进行FullGC,那么无效的Cleaner对象会越来越多并且也没有办法放入到ReferenceQueue中,从而堆外内存也就无法进行释放? 其实在初始化DirectByteBuffer对象时,如果当前堆外内存的条件比较苛刻时,就会主动调用system.gc(),强制执行FullGC。
不过很多线上环境的JVM参数有-XX:+DisableExplicitGC,导致了System.gc()等于一个空函数,根本不会触发FGC,这一点在使用Netty框架时需要注意是否会出问题。
参考博客</description>
</item>
<item>
<title>Dubbo源码解析——服务暴露过程</title>
<link>https://gj1e.github.io/posts/2020/03/dubbo%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%E6%9C%8D%E5%8A%A1%E6%9A%B4%E9%9C%B2%E8%BF%87%E7%A8%8B/</link>
<pubDate>Sat, 14 Mar 2020 21:44:08 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2020/03/dubbo%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%E6%9C%8D%E5%8A%A1%E6%9A%B4%E9%9C%B2%E8%BF%87%E7%A8%8B/</guid>
<description> 暴露过程解析 首先Dubbo的服务暴露有2种,一种是延迟暴漏,一种是正常的没有设置延迟的。
不管是不是延迟暴露,Dubbo服务暴露都是通过serviceConfig的export()方法来进行的。
使用export初始化时,会将Bean对象转换为URL,将Bean所有属性转换为URL的参数。
export首先判断是否为延迟暴漏,(这里就按没有设置延迟暴露)然后执行doExport。
doExport方法执行一些列的检查,例如配置信息,填充各种属性等等,就是在开始之前把所有的东西都准备好。
然后会调用doExportUrls方法。该方法首先会调用loadRegistries把所有注册中心的URL获取到,然后遍历调用doExportUrlsFor1Protocol方法。
doExportUrlsFor1Protocol根据不同的协议将服务以URL形式暴露。如果scope配置为none则不暴露,如果服务未配置成remote,则本地暴露exportLocal,如果未配置成local,则注册服务registryProcotol。
不管是本地暴露,还是暴露为远程服务,都得先获取Invoker,然后导成Exporter。获取Invoker的方法是 Invoker&lt;?&gt; invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString())); 这个Invoker其实就是provider.xml中你配置的dubbo:service这个标签中引用的实现类的一个包装类,然后还带上了URL
以暴露为远程服务为例,将Invoker导出为Exporter时,分两种:其它协议类型的Invoker和registry类型的Invoker。代码入口是Exporter&lt;?&gt; exporter = protocol.export(invoker);
registry类型的Invoker会进入到RegistryProtocol的export方法。RegistryProtocol负责注册服务到注册中心和向注册中心订阅服务。执行到final ExporterChangeableWrapper&lt;T&gt; exporter = doLocalExport(originInvoker);会调用dubboProtocol.export中的openServer(url)开启netty服务监听端口。
然后返回,去向注册中心注册提供者。
这个注册过程大概就是:
根据Invoker获取具体的Registry实例并连接到注册中心。因为注册中心用的是ZK,所以会调用ZK的RegistryFactory中的getRegistry(url)。
getRegistry(url)这个方法在创建注册中心时是加了Lock锁的,保证了注册中心的单一实例。
获取注册到注册中心的URL。
然后调用registry.register(registedProviderUrl)注册到注册中心。
提供者向注册中心订阅所有注册服务的覆盖配置,registry会去订阅覆盖配置的服务,当注册中心有此服务的覆盖配置注册进来时,推送消息给提供者,重新暴露服务,这由管理页面完成。
最后返回一个新的Exporter实例给上层的ServiceConfig进行缓存。
时序图 参考博文:
详细的源码讲解——推荐
简短的源码讲解——推荐
</description>
</item>
<item>
<title>Hashmap总结</title>
<link>https://gj1e.github.io/posts/2020/03/hashmap%E6%80%BB%E7%BB%93/</link>
<pubDate>Tue, 03 Mar 2020 20:55:05 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2020/03/hashmap%E6%80%BB%E7%BB%93/</guid>
<description> Hashmap是线程安全的吗 不是。Hashmap中的方法是非同步的,并不能直接在多线程并发的环境下直接使用。假如线程A和线程B同时对同一个数组位置调用addEntry,那么这两个线程会同时获得现在的头节点,然后A写入新的头节点之后,B也写入新的头节点,那么B的写入操作会覆盖掉A的写入操作,造成A写入操作的丢失。
Hashmap和Hashtable的区别 继承不同
Hashmap继承自abstractMap Hashtable继承自dictionary Hashtable是线程安全的,Hashmap不是。
Hashtable中的key和value都不允许出现null值。Hashmap可以,Hashmap允许key可以为null,这样的键只能有一个。但是允许多个key所对应的value为null。
数组的初始容量大小和容量增长方式不一样
Hashtable的数组初始大小为11,增长方式是old*2+1 Hashmap的数组初始大小为16,增长方式是翻倍 计算下标的方式不同
Hashtable是h%length Hashmap是h&amp;(length-1) 哈希值的使用不同
Hashtable是直接使用对象的Hashcode Hashmap是重新计算哈希值 Hashmap的初始容量为什么设置为16 首先,length为2的整数次幂的话,h&amp;(length-1)就相当于对length取模,提高了运算效率。 其次,是为了保证散列的均匀性,length为2的整数次幂的话,length-1为奇数,奇数的最后一位是1,h&amp;(length-1)的结果就有可能为奇数,也有可能为偶数,也就是结果的最后一次有可能是0,也有可能是1。这样便保证了散列的均匀性。如果length为奇数,length-1就是偶数,最后一位是0。这样h&amp;(length-1)的结果就只能为偶数,也就是最后一位只能是0,这样任何哈希值都只能被散列在偶数的下标位置上,浪费了近一半的空间。 所以length为2的整数次幂是为了是哈希值发生碰撞的概率更小,散列的更加均匀。 Hashmap之put()方法的执行过程 图片出处见水印
</description>
</item>
<item>
<title>JVM堆空间知识点</title>
<link>https://gj1e.github.io/posts/2019/12/jvm%E5%A0%86%E7%A9%BA%E9%97%B4%E7%9F%A5%E8%AF%86%E7%82%B9/</link>
<pubDate>Wed, 25 Dec 2019 20:50:39 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/12/jvm%E5%A0%86%E7%A9%BA%E9%97%B4%E7%9F%A5%E8%AF%86%E7%82%B9/</guid>
<description> 堆空间的基本结构 Java堆是垃圾收集器管理的主要区域,因此也被称作GC堆。
对象从新生代到老年代的过程 “eden区”,“From&rdquo;区和&rdquo;To&rdquo;区都属于新生代,“old”区属于老年代。
一般新生成的对象都出现在eden区。在一次新生代垃圾回收后,如果对象还存活,则会被复制到两个“survivor”区中的一个,假设是From区(From和To没有任何区别)。
当From区被填满时,这个区域经过垃圾回收人存货的对象将会被复制进‘To’区,原From区被清空,并且从eden区过来的数据将直接进入To区域。
当To区被填满时,之前从From区域过来的那部分数据如果仍在活动,则将会被放入老年代。
两个Servivor区域总有一个会是空的
</description>
</item>
<item>
<title>剑指Offer(Java实现)20题</title>
<link>https://gj1e.github.io/posts/2019/12/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B020%E9%A2%98/</link>
<pubDate>Wed, 18 Dec 2019 20:37:28 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/12/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B020%E9%A2%98/</guid>
<description>面试题20:表示数值的字符串 问题 请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串&quot;+100&quot;,&quot;5e2&quot;,&quot;-123&quot;,&quot;3.1416&quot;和&quot;-1E-16&quot;都表示数值。但是&quot;12e&quot;,&quot;1a3.14&quot;,&quot;1.2.3&quot;,&quot;+-5&quot;和&quot;12e+4.3&quot;都不是。 /** * @Author GJ1e * @Create 2019/12/18 * @Time 19:28 * */ public class Solution20 { public boolean isNumeric(char[] str){ if (str==null || str.length&lt;=0) return false; //长度为1,则必须是数字0-9 if (str.length==1){ return str[0]&gt;='0' &amp;&amp; str[0]&lt;='9'; } boolean hasSign = false;//+-符号标志 boolean hasDot = false; //'.'小数点标志 boolean hasE = false; //E或e标志 for (int i = 0; i &lt; str.length; i++) { if (str[i]=='+' || str[i]=='-'){ //第一次出现且不在开头,则前一个字符必须是E或e if (!</description>
</item>
<item>
<title>二分查找</title>
<link>https://gj1e.github.io/posts/2019/12/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/</link>
<pubDate>Tue, 17 Dec 2019 19:59:58 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/12/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/</guid>
<description> 二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。 查找过程 首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功 算法要求 必须是按顺序排序好的数组才可以使用二分查找算法,进行关键字查找。
/** * @Author GJ1e * @Create 2019/9/21 * @Time 20:04 * */ public class BinarySearch { //非递归二分查找 private static int search (int[] nums,int key){ int low = 0; int high = nums.length -1; while (low&lt;=high){ int mid = low + (high-low)/2; if (nums[mid]&lt;key) {low = mid+1;} else if (nums[mid]&gt;key) {high = mid-1;} else return mid; } return low; //若查找元素不存在则返回数组的第一个元素 } //递归二分查找 private static int recursion (int[] nums,int start,int end,int key){ if(start&lt;=end){ int mid = start + (end-start)/2; if(nums[mid]&gt;key) return recursion(nums,start,mid-1,key); else if (nums[mid]&lt;key) return recursion(nums,mid+1,end,key); else return mid; } return start; } } </description>
</item>
<item>
<title>Lombok笔记</title>
<link>https://gj1e.github.io/posts/2019/12/lombok%E7%AC%94%E8%AE%B0/</link>
<pubDate>Mon, 16 Dec 2019 20:36:08 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/12/lombok%E7%AC%94%E8%AE%B0/</guid>
<description>lombok是一款在java开发中简洁化代码十分有用的插件工具。使用lombok注解,目的和作用就在于不用再去写经常反复去写的(如Getter,Setter,Constructor等)一些代码了。
首先引入 maven 依赖:
&lt;dependency&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;version&gt;1.16.6&lt;/version&gt; &lt;/dependency&gt; 在类上添加注释效果如下: @Getter //为字段创建getter方法 @Setter //为字段创建setter @EqualsAndHashCode //实现equals()和hashCode() @ToString //实现toString() @Data //使用上面四个注解的特征 @Cleanup //关闭流 @Synchronized //对象上同步 @SneakyThrows //抛出异常 @NoArgsConstructor //注解在类上;为类提供一个无参的构造方法 @AllArgsConstructor //注解在类上;为类提供一个全参的构造方法 @Data //注解在类上;提供类所有属性的 getting 和 setting 方法,此外还提供了equals、canEqual、hashCode、toString 方法 @Setter //可用在类或属性上;为属性提供 setting 方法 @Getter //可用在类或属性上;为属性提供 getting 方法 </description>
</item>
<item>
<title>选择排序</title>
<link>https://gj1e.github.io/posts/2019/12/%E9%80%89%E6%8B%A9%E6%8E%92%E5%BA%8F/</link>
<pubDate>Sat, 14 Dec 2019 23:44:53 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/12/%E9%80%89%E6%8B%A9%E6%8E%92%E5%BA%8F/</guid>
<description> 选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧 算法步骤 首先在未排序的序列中找到最小(大)元素,然后将它放在序列的最前(后)面。
再从剩下的元素中寻找最小(大)元素,然后将它放在已排序序列的前(后)面。
重复第二步,直到所有元素排序完毕。
/** * @Author GJ1e * @Create 2019/9/21 * @Time 16:30 * */ public class SelectionSort { private static void sort (Comparable[] a){ for(int i = 0;i&lt;a.length-1;i++){ //循环N轮 int min = i; //记录最小元素的下标 for (int j = i+1; j &lt; a.length; j++) { //每轮比较N-i次 if(a[min].compareTo(a[j])&gt;0) //a[min]&gt;a[j],更新最小值的下标 min = j; } if (i!= min) { //最小元素的下标值和原来不一样就更新。 Comparable temp = a[min]; a[min] = a[i]; a[i] = temp; } } } } </description>
</item>
<item>
<title>插入排序</title>
<link>https://gj1e.github.io/posts/2019/12/%E6%8F%92%E5%85%A5%E6%8E%92%E5%BA%8F/</link>
<pubDate>Thu, 12 Dec 2019 23:29:33 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/12/%E6%8F%92%E5%85%A5%E6%8E%92%E5%BA%8F/</guid>
<description> 插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。 “算法步骤”
将第一个待排序序列的元素看作一个有序序列,把第二个到最后一个元素看作是未排序的序列。
从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置,(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
/** * @Author GJ1e * @Create 2019/9/21 * @Time 17:44 * */ public class InsertionSort { private static void sort (Comparable[] a){ for (int i = 1;i&lt;a.length;i++){ //从第二个元素开始,第一个元素默认已排序好。 for(int j = i;j&gt;0&amp;&amp;a[j].compareTo(a[j-1])&lt;0;j--){ //a[j]&lt;a[j-1],交换。 Comparable temp = a[j]; a[j] = a[j-1]; a[j-1] = temp; } } } } </description>
</item>
<item>
<title>生产者与消费者问题</title>
<link>https://gj1e.github.io/posts/2019/12/%E7%94%9F%E4%BA%A7%E8%80%85%E4%B8%8E%E6%B6%88%E8%B4%B9%E8%80%85%E9%97%AE%E9%A2%98/</link>
<pubDate>Sat, 07 Dec 2019 20:59:43 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/12/%E7%94%9F%E4%BA%A7%E8%80%85%E4%B8%8E%E6%B6%88%E8%B4%B9%E8%80%85%E9%97%AE%E9%A2%98/</guid>
<description>问题描述 生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。生产者生成一定量的数据放到缓冲区中,然后重复此过程;与此同时,消费者也在缓冲区消耗这些数据。生产者和消费者之间必须保持同步,要保证生产者不会在缓冲区满时放入数据,消费者也不会在缓冲区空时消耗数据。不够完善的解决方法容易出现死锁的情况,此时进程都在等待唤醒。 解决问题的核心 保证同一资源被多个线程并发访问时的完整性。常用的同步方法是采用信号或加锁机制,保证资源在任意时刻至多被一个线程访问。
代码实现 利用Synchronized,wait() / notify()方法实现
import java.util.LinkedList; /** * @Author GJ1e * @Create 2019/12/7 * @Time 19:09 * * 缓冲区 */ public class Storage { private final int STORAGE_MAX = 10; //缓冲区载体 private LinkedList&lt;Object&gt; list = new LinkedList&lt;Object&gt;(); public void proudce(){ synchronized(list){ while (list.size()+1 &gt; 10){ System.out.println(&quot;生产者&quot;+Thread.currentThread().getName()+&quot;仓库已满&quot;); try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } list.add(new Object()); System.out.println(&quot;生产者&quot;+Thread.currentThread().getName() +&quot;生产一个商品,现在库存为&quot;+list.</description>
</item>
<item>
<title>快速排序</title>
<link>https://gj1e.github.io/posts/2019/12/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F/</link>
<pubDate>Fri, 06 Dec 2019 23:08:21 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/12/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F/</guid>
<description>快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。
在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
/** * @Author GJ1e * @Create 2019/9/21 * @Time 11:26 * */ public class QuickSort { public static void sort (Comparable[] a,int start,int end){ if(start&gt;=end) return; int j = partition(a,start,end); sort(a,start,j-1); sort(a,j+1,end); } private static int partition (Comparable[] a,int start,int end){ int i = start; int j = end+1; Comparable v = a[start]; //设置切分元素 while (true){ while (v.</description>
</item>
<item>
<title>冒泡排序</title>
<link>https://gj1e.github.io/posts/2019/12/%E5%86%92%E6%B3%A1%E6%8E%92%E5%BA%8F/</link>
<pubDate>Fri, 06 Dec 2019 22:53:25 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/12/%E5%86%92%E6%B3%A1%E6%8E%92%E5%BA%8F/</guid>
<description> 冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来说并没有什么太大作用。
算法步骤 比较相邻的两个元素,如果前者比后者大,就交换。
对每一对相邻的元素做同样的工作,从开始的第一对,到结尾的最后一对。都执行完之后,最后一个元素是最大的。
继续针对所有的元素重复以上的步骤,最后一个除外。
持续每次对越来越少的元素重复以上的步骤,直到没有任何一对元素需要比较。
/** * @Author GJ1e * @Create 2019/9/21 * @Time 15:47 * */ public class BubbleSort { private static void sort (Comparable[] a){ if(a==null||a.length&lt;=1) return; for (int i = 0;i&lt;a.length-1;i++){ //设置一个flag标记 boolean flag = true; for (int j = 0;j&lt;a.length-1;j++){ if (a[j].compareTo(a[j+1])&gt;0){ Comparable temp = a[j]; a[j]=a[j+1]; a[j+1]=temp; flag = false; } } if (flag) break; } } } </description>
</item>
<item>
<title>DBLeetcode182</title>
<link>https://gj1e.github.io/posts/2019/12/dbleetcode182/</link>
<pubDate>Wed, 04 Dec 2019 23:17:36 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/12/dbleetcode182/</guid>
<description>Leetcode 182.查找重复的电子邮件 题目:
编写一个 SQL 查询,查找 Person 表中所有重复的电子邮箱。
示例: +----+---------+ | Id | Email | +----+---------+ | 1 | [email protected] | | 2 | [email protected] | | 3 | [email protected] | +----+---------+ 根据以上输入,你的查询应返回以下结果:
+---------+ | Email | +---------+ | [email protected] | +---------+ 说明:所有电子邮箱都是小写字母。
思路:
利用GROUP BY 和 HAVING 从Person中挑选出现超过一次的Email,通过Email分组。
SELECT Email FROM Person GROUP BY Email HAVING COUNT(Email)&gt;1; 题目地址</description>
</item>
<item>
<title>DBLeetcode181</title>
<link>https://gj1e.github.io/posts/2019/12/dbleetcode181/</link>
<pubDate>Mon, 02 Dec 2019 22:47:08 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/12/dbleetcode181/</guid>
<description>Leetcode 181.超过经理收入的员工 题目:
Employee 表包含所有员工,他们的经理也属于员工。每个员工都有一个 Id,此外还有一列对应员工的经理的 Id。
+----+-------+--------+-----------+ | Id | Name | Salary | ManagerId | +----+-------+--------+-----------+ | 1 | Joe | 70000 | 3 | | 2 | Henry | 80000 | 4 | | 3 | Sam | 60000 | NULL | | 4 | Max | 90000 | NULL | +----+-------+--------+-----------+ 给定 Employee 表,编写一个 SQL 查询,该查询可以获取收入超过他们经理的员工的姓名。在上面的表格中,Joe 是唯一一个收入超过他的经理的员工。
+----------+ | Employee | +----------+ | Joe | +----------+ 思路:</description>
</item>
<item>
<title>DBLeetcode176</title>
<link>https://gj1e.github.io/posts/2019/12/dbleetcode176/</link>
<pubDate>Sun, 01 Dec 2019 16:53:13 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/12/dbleetcode176/</guid>
<description>Leetcode 176.第二高的薪水 题目: 编写一个 SQL 查询,获取Employee表中第二高的薪水(Salary)
+----+--------+ | Id | Salary | +----+--------+ | 1 | 100 | | 2 | 200 | | 3 | 300 | +----+--------+ 例如上述 Employee 表,SQL查询应该返回 200 作为第二高的薪水。如果不存在第二高的薪水,那么查询应返回 null。
+---------------------+ | SecondHighestSalary | +---------------------+ | 200 | +---------------------+ 思路:
首先查出所有员工的薪水,去重。然后按薪水降序排序,用limit选取第二高的薪水。
嵌套查询,首先从表中查出最高的薪水,然后查询比全表最高薪水低的最高薪水,就是第二高的薪水。
SQL查询语句中的 limit 与 offset 的区别:
limit y 分句表示: 读取 y 条数据
limit x, y 分句表示: 跳过 x 条数据,读取 y 条数据</description>
</item>
<item>
<title>DBLeetcode175</title>
<link>https://gj1e.github.io/posts/2019/12/dbleetcode175/</link>
<pubDate>Sun, 01 Dec 2019 16:32:16 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/12/dbleetcode175/</guid>
<description>Leetcode 175.组合两个表 题目:
表1: Person
+-------------+---------+ | 列名 | 类型 | +-------------+---------+ | PersonId | int | | FirstName | varchar | | LastName | varchar | +-------------+---------+ PersonId 是上表主键 表2: Address
+-------------+---------+ | 列名 | 类型 | +-------------+---------+ | AddressId | int | | PersonId | int | | City | varchar | | State | varchar | +-------------+---------+ AddressId 是上表主键 编写一个 SQL 查询,满足条件:无论 person 是否有地址信息,都需要基于上述两表提供 person 的以下信息: FirstName, LastName, City, State 思路:</description>
</item>
<item>
<title>Redis的持久化</title>
<link>https://gj1e.github.io/posts/2019/11/redis%E7%9A%84%E6%8C%81%E4%B9%85%E5%8C%96/</link>
<pubDate>Tue, 26 Nov 2019 20:25:20 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/11/redis%E7%9A%84%E6%8C%81%E4%B9%85%E5%8C%96/</guid>
<description> Redis支持内存数据持久化,将存储在内存中的数据以某种形式持久化到硬盘中,Redis重启后仍能加载硬盘中的数据重新使用。
Redis有两种持久化方式:一种RDB,一种AOF。
Redis默认的持久化方式为RDB,两种持久化方式可以单独使用,也可结合使用。
RDB RDB方式是通过快照来完成的,当符合一定的条件时,Redis会自动将内存中的所有数据进行快照并存储到硬盘上。进行快照的条件在配置文件中指定,有两个参数:时间和改动的键的个数。
当在指定时间内被更改的键的个数大于指定数值时,就会进行快照。
Redis在以RDB方式持久化数据时,是以一个子进程来做的,也就是说进行RDB持久化时,不会影响Redis的主进程对外的使用。
RDB并不能绝对保证数据的不丢失
RDB的快照过程: Redis使用fork函数复制一份当前进程(父进程)的副本(子进程)。 父进程继续接受并处理客户端发来的命令,而子进程开始将内存中的数据写入到硬盘中的临时文件。 当子进程写入完所有数据后,会用该临时文件替换旧的RDB文件(默认是压缩过的)。 可以通过SAVE和BGSAVE命令来手动快照,前者是由主进程进行快照,会阻塞其它请求;后者是通过fork子进程来进行快照。
AOF AOF持久化策略是将发送到Redis端的每一条命令都记录下来,并保存到硬盘中的AOF文件,AOF文件和RDB文件位置相同。 AOF持久化方式是默认关闭的,通过配置文件中的appendonly参数设为YES开启。
AOF文件对于查询的操作不做记录。
AOF文件到磁盘的同步策略 appendfsync always 每次都同步(最安全,但也最慢) appendfsync everysec 每秒同步(默认的同步策略) appendfsync no 不主动同步,由操作系统来决定 文件默认先写到缓存中,系统每30秒同步一次,才是真正写入到磁盘
</description>
</item>
<item>
<title>Redis</title>
<link>https://gj1e.github.io/posts/2019/11/redis/</link>
<pubDate>Sun, 24 Nov 2019 18:48:38 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/11/redis/</guid>
<description>什么是Redis Redis是一个用C语言写的,开源、支持网络、基于内存、可选持久性的、非关系型、Key-Value数据库。它是一个内存中的数据结构存储系统,可以用作数据库,缓存和消息中间件。 使用Redis有哪些好处 速度快:因为数据结构在内存中类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
支持丰富的数据类型:String,list,set,sorted set,hash
支持事务:操作都是原子性,所谓原子性就是对数据的更改,要么全部执行,要么全部不执行。
丰富的特性:可用于缓存,按Key设置过期时间,过期后将会自动删除。
Redis和Memcached区别和比较 Redis不仅仅支持简单的k/v类型的数据,同时还提供string(字符串)、list(链表)、set(集合)、zset(sorted set &ndash;有序集合)和hash(哈希类型)等数据结构的存储。 memcache支持简单的数据类型,String。
Redis支持数据的备份,即master-slave模式的数据备份。
Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memecache把数据全部存在内存之中
redis的速度比memcached快很多
Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的IO复用模型。
有持久化需求或者对数据结构和处理有高级要求的应用,选择redis,其他简单的key/value存储,选择memcached。 对于两者的选择需要要看具体的应用场景,如果需要缓存的数据只是key-value这样简单的结构时,则还是采用memcache,它也足够的稳定可靠。 如果涉及到存储,排序等一系列复杂的操作时,毫无疑问选择redis。
Redis有哪几种数据淘汰策略 volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据进行淘汰。
volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据进行淘汰。
volatile-random:从已设置过期时间的数据集中随机选择数据进行淘汰。
allkeys-lru:从数据集中挑选最近最少使用的数据进行淘汰。
allkeys-random:从数据集中随机选择数据进行淘汰。
no-enviction:禁止淘汰数据。
Redis的内存回收算法 LRU和引用计数器算法 Redis分布式锁 待总结。。。</description>
</item>
<item>
<title>剑指Offer(Java实现)19题</title>
<link>https://gj1e.github.io/posts/2019/11/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B019%E9%A2%98/</link>
<pubDate>Sat, 23 Nov 2019 23:52:11 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/11/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B019%E9%A2%98/</guid>
<description>面试题19:正则表达式匹配 题目: 请实现一个函数用来匹配包含&rsquo;.&lsquo;和&rsquo;&lsquo;的正则表达式。模式中的字符&rsquo;.&lsquo;表示任意一个字符,而&rsquo;&lsquo;表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串&rdquo;aaa&rdquo;与模式&rdquo;a.a&rdquo;和&rdquo;ab*ac*a&rdquo;匹配,但与&rdquo;aa.a&rdquo;及&rdquo;ab*a&rdquo;均不匹配。 思路: 当字符串中的字符和模式中的字符相匹配时,或者当模式中的字符为&rdquo;.&ldquo;时,字符串和模式串同时向后移动继续匹配。 当模式中的第二个字符是“*”时,一种选择是模式上向后移动两个字符。这相当于*和它前面的字符被忽略了,因为*可以匹配字符串中的0哥字符。如果模式中的第一个字符和字符串中的第一个字符相匹配,则字符串向后移动一个字符,而模式上有两种选择:可以在模式上向后移动两个字符,也可以保持模式不变。 /** * @Author GJ1e * @Create 2019/12/1 * @Time 19:54 * */ public class Solution19 { public boolean match(char[] str, char[] pattern) { if (str == null || pattern == null) { return false; } return matchRecur(str, pattern, 0, 0); } private boolean matchRecur(char[] str, char[] pattern, int s, int p) { if (s == str.</description>
</item>
<item>
<title>剑指Offer(Java实现)18-1题</title>
<link>https://gj1e.github.io/posts/2019/11/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B018-1%E9%A2%98/</link>
<pubDate>Thu, 21 Nov 2019 23:22:09 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/11/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B018-1%E9%A2%98/</guid>
<description>面试题18(二):删除链表中重复的结点 问题
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。例如,链表1-&gt;2-&gt;3-&gt;3-&gt;4-&gt;4-&gt;5 处理后为 1-&gt;2-&gt;5
注意重复的结点不保留:并不是将重复结点删除到只剩一个,而是重复结点的全部会被删除。所以链表1-&gt;2-&gt;3-&gt;3-&gt;4-&gt;4-&gt;5不是1-&gt;2-&gt;3-&gt;4-&gt;5
思路 从头遍历整个链表,如果当前节点的值与下一个节点的值相同,就认为是重复节点,进行删除。为了使删除之后的链表不断开,则把当前节点的前一个节点与后面值比当前节点大的节点相连接。 /** * @Author GJ1e * @Create 2019/12/18 * @Time 18:34 * */ public class Solution18_1 { private class ListNode{ int value; ListNode next = null; } public ListNode deleteDuplication(ListNode pHead){ if (pHead==null || pHead.next==null) return null; ListNode pNode = pHead; ListNode pPreNode = null; while (pNode!=null &amp;&amp; pNode.next!=null){ //当前节点和下一个节点的值相同,进入删除。 if (pNode.value == pNode.</description>
</item>
<item>
<title>剑指Offer(Java实现)18题</title>
<link>https://gj1e.github.io/posts/2019/11/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B018%E9%A2%98/</link>
<pubDate>Tue, 19 Nov 2019 21:50:49 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/11/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B018%E9%A2%98/</guid>
<description>面试题18:删除链表的节点 问题: 给定单向链表的头指针和一个节点指针,定义一个函数在O(1)时间内删除该节点。 思路:
删除链表中的某个节点,并不一定是删除本身的节点。 可以把要删除节点的下一个节点的内容复制到需要删除的节点上覆盖原有的内容,然后在把下一个节点删除,这也就是相当于把需要的节点删除了。 异常情况:
1.删除节点是尾节点 2.链表只有一个节点 3.空链表,或删除节点为空。 注:因收到O(1)时间的限制,以下的解法均基于一个假设:要删除的节点的确在链表中。
/** * @Author GJ1e * @Create 2019/9/14 * @Time 14:32 * */ public class Solution18 { //定义链表 class ListNode{ int value; ListNode next = null; } public ListNode deleteListNode(ListNode listNode , ListNode deleteNode){ if (listNode == null || deleteNode == null) return listNode; if(deleteNode.next != null) //删除的节点不是尾节点 { ListNode temp = deleteNode.</description>
</item>
<item>
<title>剑指Offer(Java实现)17题</title>
<link>https://gj1e.github.io/posts/2019/11/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B017%E9%A2%98/</link>
<pubDate>Tue, 19 Nov 2019 21:44:14 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/11/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B017%E9%A2%98/</guid>
<description>面试题17:打印从1到最大的N位数 题目: 输入数字n,按顺序打印出从1到最大的n位十进制数。比如输入3,则打印1,2,3一直到最大的3位数999. 思路: 大数问题,注意溢出,用字符串来描述数字。 把问题转换成数字排列的解法,n位所有十进制数,其实就是n个从0到9的全排列。 也就是说,我们把数字的每一位都从0到9排列一趟,就得到了所有十进制数。 注意,最后打印时,要把排在数字前面的0过滤掉。 /** * @Author GJ1e * @Create 2019/11/19 * @Time 20:44 * */ public class Solution17 { public void print1ToMaxOfNDigits(int n){ if (n&lt;=0) return; char[] numbers = new char[n+1]; numbers[n] = '\0'; for (int i = 0; i &lt; 10; i++) { numbers[0] = (char)(i+'0'); print1ToMaxOfNDigitsRecursively(numbers,n,0); //调用递归函数进行数字每一位的全排列 } } private void print1ToMaxOfNDigitsRecursively(char[] numbers, int length, int index){ if (length-1 == index){ //递归结束的条件,就是设置了数字的最后一位。 printNumber(numbers); //调用打印函数 return; } for (int i = 0; i &lt; 10; i++) { numbers[index+1] = (char)(i+'0'); print1ToMaxOfNDigitsRecursively(numbers, length, index+1); } } private static void printNumber(char[] numbers){ boolean isBegin0 = true; for (int i = 0; i &lt; numbers.</description>
</item>
<item>
<title>剑指Offer(Java实现)16题</title>
<link>https://gj1e.github.io/posts/2019/11/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B016%E9%A2%98/</link>
<pubDate>Tue, 19 Nov 2019 21:32:12 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/11/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B016%E9%A2%98/</guid>
<description>面试题16:数值的整数次方 题目: 实现函数double Power(double base, int exponent),求base的exponent次方。不得使用库函数,不需要考虑大数问题。 思路:
当指数为负数的时候,先对指数求绝对值,算出次方的结果之后再取倒数。
当base为0,且指数为负数时,返回0.
/** * @Author GJ1e * @Create 2019/11/9 * @Time 21:34 * */ public class Solution16 { public double power(double base, int exponent){ Double base1 = base; if (base1.compareTo(0.0)==0 &amp;&amp; exponent&lt;0){ return 0.0; } int absExponent = exponent; if (exponent &lt; 0) //指数为负数时,先求指数的绝对值 absExponent = -exponent; double result = powerCore(base, absExponent); //调用函数,计算次方的结果 if (exponent &lt; 0) //指数为负数,对次方结果取倒数 result = 1.</description>
</item>
<item>
<title>面试题整理二</title>
<link>https://gj1e.github.io/posts/2019/11/%E9%9D%A2%E8%AF%95%E9%A2%98%E6%95%B4%E7%90%86%E4%BA%8C/</link>
<pubDate>Mon, 18 Nov 2019 23:08:31 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/11/%E9%9D%A2%E8%AF%95%E9%A2%98%E6%95%B4%E7%90%86%E4%BA%8C/</guid>
<description> 笔试题 </description>
</item>
<item>
<title>MySQL知识点(一)</title>
<link>https://gj1e.github.io/posts/2019/11/mysql%E7%9F%A5%E8%AF%86%E7%82%B9%E4%B8%80/</link>
<pubDate>Thu, 14 Nov 2019 22:40:46 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/11/mysql%E7%9F%A5%E8%AF%86%E7%82%B9%E4%B8%80/</guid>
<description>MySQL的存储引擎 MyISAM
MyISAM是MySQL的默认数据库引擎(5.5版之前),但MyISAM不支持事务和行级锁,而且最大的缺陷就是崩溃后无法安全恢复。 InnoDB
5.5版本之后,MySQL引入了InnoDB(事务性数据库引擎),MySQL 5.5版本后默认的存储引擎为InnoDB。 两者对比
是否支持行级锁 : MyISAM 只有表级锁(table-level locking),而InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。
是否支持事务和崩溃后的安全恢复: MyISAM 强调的是性能,每次查询具有原子性,其执行速度比InnoDB类型更快,但是不提供事务支持。但是InnoDB 提供事务支持事务,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。
是否支持外键: MyISAM不支持,而InnoDB支持。
索引 MySQL索引使用的数据结构主要有BTree索引 和 哈希索引 。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。
MySQL的BTree索引使用的是B树中的B+Tree,但对于主要的两种存储引擎的实现方式是不同的。
MyISAM:B+Tree叶节点的data域存放的是数据记录的地址。在索引检索的时候,首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“非聚簇索引”。
InnoDB:其数据文件本身就是索引文件。相比MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按B+Tree组织的一个索引结构,树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。这被称为“聚簇索引(或聚集索引)”。而其余的索引都作为辅助索引,辅助索引的data域存储相应记录主键的值而不是地址,这也是和MyISAM不同的地方。在根据主索引搜索时,直接找到key所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。 因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。 PS:整理自《Java工程师修炼之道》
事务 事务是逻辑上的一组操作,要么都执行,要么都不执行。 事物的四大特性(ACID) 原子性(Atomicity):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
一致性(Consistency):执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
隔离性(Isolation):发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
持久性(Durability): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
并发事务带来的问题 脏读(Dirty read):当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。</description>
</item>
<item>
<title>剑指Offer(Java实现)15题</title>
<link>https://gj1e.github.io/posts/2019/11/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B015%E9%A2%98/</link>
<pubDate>Sat, 09 Nov 2019 21:36:03 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/11/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B015%E9%A2%98/</guid>
<description> 面试题15:二进制中1的个数 题目: 请实现一个函数,输入一个整数,输出该数二进制表示中1的个数。 例如:把9表示成二进制是1001,有2位是1.因此如果输入9,则该函数输出2。 思路: 一个整数减去1,在和原数进行&amp;运算,会把该整数最右边的1变成0。那么一个整数的二进制有多少个1,就可以进行多少次这样的操作。 例如:12的二进制为1100,1100-1=1011,1011&amp;1100=1000; 这样,原1100中最右边的1变为了0; /** * @Author GJ1e * @Create 2019/11/9 * @Time 21:13 * */ public class Solution15 { public static int numberOf1(int n){ int count = 0; while (n&gt;0){ count++; n = (n-1)&amp;n; //能进行多少次就有多少个1 } return count; } } </description>
</item>
<item>
<title>ReentrantLock</title>
<link>https://gj1e.github.io/posts/2019/11/reentrantlock/</link>
<pubDate>Fri, 08 Nov 2019 23:03:54 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/11/reentrantlock/</guid>
<description>Reentrantlock是一个可重入锁,在高竞争条件下有更好的性能,且可以中断。
Reentrantlock是基于AQS实现的,AQS是基于FIFO队列实现的,整个AQS是典型的模板模式的应用,对于队列FIFO的各种操作,在AQS中都已经实现。AQS的子类一般只需要重写tryAcquire(int arg)尝试获得锁,和tryRlease(int arg)尝试释放锁两个方法即可。
Reentrantlock中有一个抽象类Sync继承于AbstractQueuedSynchronizer。
private final Sync sync; /** * Base of synchronization control for this lock. Subclassed * into fair and nonfair versions below. Uses AQS state to * represent the number of holds on the lock. */ abstract static class Sync extends AbstractQueuedSynchronizer { ... } Reentrantlock根据传入构造函数的布尔类型的参数,实例化出Sync的实现类FairSync和NonfairSync,分别表示公平的Sync和非公平的Sync,也就是公平锁,和非公平锁。
以比较常用的非公平锁为例:
假设线程1,调用了Reentrantlock中的lock()方法那么线程1就会独占该锁。
线程1获得锁就做了两件事:
设置AQS的state为1。
设置AQS的Thread为当前线程。
这两步做完之后就表示线程1独占了该锁。当线程2想要获得锁时,会尝试利用CAS去判断state状态是否为0,如果为0就设置为1。这一步操作肯定是失败的,因为线程1已经将state设置成了1。所以线程2就会执行lock()方法中的else分支调用acquire()方法,进而调用tryAcquire()方法,尝试获取一次锁,如果还是失败就会将这个线程加入到等待队列中。
Reentrantlock的使用场景 需要使用可重入锁时。
并发竞争很高的环境下。
需要使用可中断锁。
尝试等待执行,如果发现已经在执行,则尝试等待一段时间。等待超时,则不执行。</description>
</item>
<item>
<title>Volatile关键字</title>
<link>https://gj1e.github.io/posts/2019/11/volatile%E5%85%B3%E9%94%AE%E5%AD%97/</link>
<pubDate>Tue, 05 Nov 2019 17:07:31 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/11/volatile%E5%85%B3%E9%94%AE%E5%AD%97/</guid>
<description> volatile的本质是在告诉JVM当前变量在寄存器中的值是不确定的,需要从主存中读取。
vloatile仅能使用在变量级别,仅能实现变量的修改可见性。但不具备原子性。
volatile不会造成线程的阻塞。
volatile标记的变量不会被编译器优化。
volatile的不变性 将当前处理器缓存行的数据写回到系统内存。
这个写回内存的操作会引起在其它CPU里缓存了该内存地址的数据无效。
volatile禁止指令重排序 普通变量仅仅会保证在方法执行过程中所有依赖赋值的结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。 volatile与synchronized的区别(重点) volatile、final、synchronized都可以实现可见性。
volatile的本质是在告诉JVM当前变量在寄存器中的值是不确定的,需要从主存中读取。synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞。
vloatile仅能使用在变量级别,synchronized可以使用在变量,方法和代码块。
volatile仅能实现变量的修改可见性。但不具备原子性。而synchronized则可以保证变量的修改可见性和原子性。
volatile不会造成线程的阻塞,而synchronized可能会造成线程阻塞。
volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化。
</description>
</item>
<item>
<title>TCP三次握手的原因及缺陷</title>
<link>https://gj1e.github.io/posts/2019/11/tcp%E4%B8%89%E6%AC%A1%E6%8F%A1%E6%89%8B%E7%9A%84%E5%8E%9F%E5%9B%A0%E5%8F%8A%E7%BC%BA%E9%99%B7/</link>
<pubDate>Tue, 05 Nov 2019 16:28:49 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/11/tcp%E4%B8%89%E6%AC%A1%E6%8F%A1%E6%89%8B%E7%9A%84%E5%8E%9F%E5%9B%A0%E5%8F%8A%E7%BC%BA%E9%99%B7/</guid>
<description> TCP为什么是三次握手 前两次比较容易理解,第三次握手看似比较多余,其实不是。第三次握手主要是为了防止已失效的请求报文段又突然传送到了服务端而产生连接的误判。
比如:客户端发送了一个连接请求的报文段A到服务端,但是在某些网络节点上长时间滞留了,而后客户端又超时重发了一个连接请求的报文段B到服务端,而后正常建立连接,数据传送完毕之后释放连接。但是请求报文段A延迟了一段时间之后又发送到了服务端,报文段A原本是一个早已失效的报文段,但是服务端收到之后会误以为客户端又发来了一次连接请求,于是向客户端发出确认报文,并同意建立连接。
那么问题就出现了:假如这里没有第三次握手,这时只要服务端发送了确认,新的连接就建立了,但是由于客户端没有发出建立连接的请求,因此不会理会服务端的确认,也不会向服务端发送数据。而服务端却认为新的连接已经建立了,并一直等待客户端发送数据,直到超出保活计数器的设定值,而将客户端判定为出了问题,才会关闭这个连接。这样就浪费了很多服务器的资源。
如果采用三次握手,客户端就不会向服务端发出确认,服务端由于收不到确认,就知道客户端没有要求建立连接,从而不建立该连接。
TCP三次握手的缺陷(扩充了解) 原文出处:https://blog.csdn.net/xtzmm1215/article/details/47385079
SYN- Flood攻击是当前网络上最为常见的DDoS攻击,也是最为经典的拒绝服务攻击,它就是利用了TCP协议实现上的一个缺陷,通过向网络服务所在端口发送大量 的伪造源地址的攻击报文,就可能造成目标服务器中的半开连接队列被占满,从而阻止其他合法用户进行访问。这种攻击早在1996年就被发现,但至今仍然显示 出强大的生命力。很多操作系统,甚至防火墙、路由器都无法有效地防御这种攻击,而且由于它可以方便地伪造源地址,追查起来非常困难。它的数据包特征通常 是,源发送了大量的SYN包,并且缺少三次握手的最后一步握手ACK回复。
原理:攻击者首先伪造地址对 服务器发起SYN请求,服务器回应(SYN+ACK)包,而真实的IP会认为,我没有发送请求,不作回应。服务 器没有收到回应,这样的话,服务器不知 道(SYN+ACK)是否发送成功,默认情况下会重试5次(tcp_syn_retries)。这样的话,对于服务器的内存,带宽都有很大的消耗。攻击者 如果处于公网,可以伪造IP的话,对于服务器就很难根据IP来判断攻击者,给防护带来很大的困难。
解决办法:
无效连接监视释放 这种方法不停的监视系统中半开连接和不活动连接,当达到一定阈值时拆除这些连接,释放系统资源。这种绝对公平的方法往往也会将正常的连接的请求也会被释放掉,“伤敌一千,自损八百”
延缓TCB分配方法 SYN Flood关键是利用了,SYN数据报文一到,系统立即分配TCB资源,从而占用了系统资源,因此有俩种技术来解决这一问题
Syn Cache技术 这种技术在收到SYN时不急着去分配TCB,而是先回应一个ACK报文,并在一个专用的HASH表中(Cache)中保存这种半开连接,直到收到正确的ACK报文再去分配TCB
Syn Cookie技术 Syn Cookie技术则完全不使用任何存储资源,它使用一种特殊的算法生成Sequence Number,这种算法考虑到了对方的IP、端口、己方IP、端口的固定信息,以及对方无法知道而己方比较固定的一些信息,如MSS、时间等,在收到对方 的ACK报文后,重新计算一遍,看其是否与对方回应报文中的(Sequence Number-1)相同,从而决定是否分配TCB资源
使用SYN Proxy防火墙 原理:对试图穿越的SYN请求进行验证之后才放行 </description>
</item>
<item>
<title>Synchronized关键字</title>
<link>https://gj1e.github.io/posts/2019/11/synchronized%E5%85%B3%E9%94%AE%E5%AD%97/</link>
<pubDate>Sun, 03 Nov 2019 16:35:53 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/11/synchronized%E5%85%B3%E9%94%AE%E5%AD%97/</guid>
<description>synchronized关键字的作用 synchronized关键字解决的是多个线程之间访问资源的同步性。
synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
怎么使用synchronized关键字 synchronized关键字最主要的三种使用方式:
修饰实例方法:给当前对象实例加锁,进入同步代码块之前要先获得当前对象实例的锁。
修饰静态方法:也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象。因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁。
单例模式-双重校验锁 public class Singleton { private volatile static Singleton uniqueInstance; private Singleton() { } public static Singleton getUniqueInstance() { //先判断对象是否已经实例过,没有实例化过才进入加锁代码 if (uniqueInstance == null) { //类对象加锁 synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; } } uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton();这段代码其实是分为三步执行:</description>
</item>
<item>
<title>Java并发基础知识点</title>
<link>https://gj1e.github.io/posts/2019/11/java%E5%B9%B6%E5%8F%91%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86%E7%82%B9/</link>
<pubDate>Sun, 03 Nov 2019 15:21:25 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/11/java%E5%B9%B6%E5%8F%91%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86%E7%82%B9/</guid>
<description>线程和进程 进程:进程是程序的一次执行过程,是系统运行程序的基本单位。进程是动态的,随着程序的运行而创建,结束就消亡。
线程:与进程相似,比进程更小,一个进程在执行时可以产生多个线程。
同类的多个线程可以共享进程的堆和方法区,但虚拟机,本地方法栈,程序计数器为线程私有。
并发与并行 并发:同一时间段,多个任务都在执行(单位时间内不一定同时执行)
并行:单位时间内多个任务同时执行。
Java中的线程有几种状态 新建状态:新创建了一个线程对象。
就绪状态:线程对象创建之后,其它线程调用了该对象的start()方法,此时线程处于就绪状态,并未运行,等待获取CPU的使用权。
运行状态:就绪状态的线程获取到CPU的使用权之后,执行程序代码。
阻塞状态:阻塞状态是线程因为某些原因放弃了CPU的使用权,暂停运行,直到线程到就绪态,才有机会再到运行状态。
死亡状态:线程执行完了,或者线程因为异常退出了run()方法,该线程结束生命周期。
死锁 线程A持有资源2,线程B持有资源1,他们都想申请对方的资源,然后这两个线程就会相互等待造成死锁。 产生死锁的条件 互斥条件:该资源任意时刻只能由一个线程占用。
请求与保持条件:一个进程因请求资源而阻塞时,对已经获得资源保持不放。
不可剥夺条件:线程已获得的资源在未使用完之前,不能被其它线程强行剥夺,只能自己释放。
循环等待条件:若干个进程之间形成一种头尾相接的循环等待资源关系。
如何避免死锁 破坏四个条件中的一个就好。
破坏互斥条件:这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的。
破坏请求与保持条件:一次性申请所有资源。
破坏不可剥夺条件:占用部分资源的线程申请其它资源,申请不到就主动释放自己占有的资源。
破坏循环等待条件:按序申请资源,按反序释放资源。
sleep()方法和wait()方法的区别和共同点 共同点:
两者都可以暂停线程的执行。 区别:
Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。 wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法? new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。</description>
</item>
<item>
<title>剑指Offer(Java实现)14题</title>
<link>https://gj1e.github.io/posts/2019/11/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B014%E9%A2%98/</link>
<pubDate>Fri, 01 Nov 2019 21:58:24 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/11/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B014%E9%A2%98/</guid>
<description>面试题14:剪绳子 题目: 给你一根长度为n绳子,请把绳子剪成m段(m、n都是整数,n&gt;1并且m≥1)。每段的绳子的长度记为k[0]、k[1]、……、k[m]。k[0]*k[1]*…*k[m]可能的最大乘积是多少?例如当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到最大的乘积18。 思路: 尽可能多地减去长度为3的绳子段,当绳子最后剩下的长度为4的时候,不能再剪去长度为3的绳子段。 此时更好的方法是把绳子剪成长度为2的两段,因为2*2 &gt; 3*1。 package com.gj1e.question; /** * @Author GJ1e * @Create 2019/11/1 * @Time 23:27 * */ public class Solution14 { public int maxProductAfterCutting(int length){ if (length &lt; 2) return 0; if (length == 2) return 1; if (length == 3) return 2; int timesOf3 = length / 3; // 尽可能多地减去长度为3的绳子段 // 当绳子最后剩下的长度为4的时候,不能再剪去长度为3的绳子段。 // 此时更好的方法是把绳子剪成长度为2的两段,因为2*2 &gt; 3*1。 if (length - timesOf3*3 == 1) timesOf3 -= 1; int timesOf2 = (length - timesOf3 * 3) / 2; return (int) (Math.</description>
</item>
<item>
<title>JVM垃圾收集器</title>
<link>https://gj1e.github.io/posts/2019/10/jvm%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8/</link>
<pubDate>Wed, 30 Oct 2019 20:41:32 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/10/jvm%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8/</guid>
<description>垃圾收集器主要用于堆内存中。
图中垃圾收集器之间有连线的,就代表可以相互配合使用。
Serial、ParNew、Parallel Scavenge用于新生代;CMS、Serial Old、Paralled Old用于老年代。 并且他们相互之间以相对固定的组合使用。G1是一个独立的收集器不依赖其他6种收集器。
Serial收集器 它是单线程的收集器,使用复制算法。在进行垃圾回收时需要停止其他的所有工作线程(Stop The World)。 ParNew收集器 它其实就是Serial收集器的多线程版本,使用的是复制算法。
它在单线程环境下没有Serial好,因为存在线程开销。但随着CPU的增加就可以体现出优势。
它默认开启的收集线程数与CPU的数量相同。
Parallel Scavenge收集器 用于新生代,多线程并行,使用复制算法与ParNew相似。
与ParNew不同的是,它更关注垃圾回收的吞吐量。它提供两个参数,一个用于控制最大垃圾收集停顿时间,一个用于控制吞吐量大小。
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
最大垃圾收集停顿时间越小,系统设置新生代越少,GC频率增加。停顿时间缩短是以牺牲吞吐量和新生代空间换取的
Parallel Old收集器 Parallel Old收集器,老年代的收集器,是Parallel Scavenge老年代的版本。但是使用的是标记-整理算法。
Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较不错的应用组合,在注重吞吐量及CPU资源敏感的场合,可以优先考虑Parallel Scavenge收集器+Parallel Old收集器。
Serial Old收集器 Serial Old收集器,老年代的收集器,与Serial一样是单线程。不同的是算法用的是标记-整理,因为老年代里面对象的存活率高,如果依旧是用复制算法,需要复制的内容较多,性能较差。 CMS收集器 它是一种以最短垃圾回收停顿时间为目标的收集器,使用的是标记——清除算法。 运作过程分为四步:初始标记-&gt;并发标记-&gt;重新标记-&gt;并发清理。其中初始标记和重新标记这两步仍然需要暂停用户所有线程。
初始标记:仅仅只标记一下GCRoots可以直接关联到的对象,速度很快。
并发标记:是从GCRoot开始继续向下进行标记。
重新标记:是为了修正并发标记期间发生变动的标记,这个阶段停顿时间比初始标记长,比并发标记短。
并发清除:清除老年代的垃圾。
缺点:</description>
</item>
<item>
<title>JVM垃圾收集算法</title>
<link>https://gj1e.github.io/posts/2019/10/jvm%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E7%AE%97%E6%B3%95/</link>
<pubDate>Wed, 30 Oct 2019 20:41:22 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/10/jvm%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E7%AE%97%E6%B3%95/</guid>
<description> JVM的垃圾收集算法常见有四种:标记清除算法,标记整理算法,复制算法,和分代收集算法
标记——清除 该算法分为“标记”和“清除&rdquo;两个阶段。
首先标记出所需要回收的对象,在标记完成之后统一回收所有被标记的对象。
它是最基础的收集算法。
缺点: 1. 效率问题 2. 空间问题(标记清除之后会产生大量不连续的碎片)
标记——整理 根据老年代的特点推出的一种标记算法,标记过程和“标记清除&rdquo;算法一样,但是后续不是对回收的对象进行回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。 复制算法 它可以将内存划分为大小相同的两块,每次只使用其中的一块。当这一块内存用完了,它就将还存活着的对象复制到另外一块内存中,然后把使用过的内存空间一次清理掉。 每次的内存回收都是对内存区间的一半进行回收。 缺点: 1. 会浪费一定的内存空间 2. 对象的存活率较高时,复制会降低效率
分代收集算法 这种算法是根据对象存活周期的不同,将内存分为几块,一般是把Java堆分为新生代和老年代。然后根据各个年代的特点采用适当的收集算法。
新生代:
每次垃圾收集时都会有大批对象死去,存活率较低,只有少量存活。这样可以采用复制算法,只需付出少量的复制成本就可以完成收集。 老年代:
老年代对象的特点就是存活率较高,没有额外的空间进行分配担保,这样就必须使用&rdquo;标记——整理&rdquo;或者“标记——清除”算法来进行回收。 </description>
</item>
<item>
<title>剑指Offer(Java实现)13题</title>
<link>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B013%E9%A2%98/</link>
<pubDate>Tue, 29 Oct 2019 20:41:11 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B013%E9%A2%98/</guid>
<description>面试题13:机器人的运动范围 题目: 地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子? 思路: 这个题和12题一样,也是用回溯法的思路来解。 在这个矩阵中,除边界上的格子之外,其他的格子都有4个相邻的格子。 机器人从坐标(0,0)开始移动,准备进入下一个格子(i,j)之前检查是否满足限制条件能否进入,如果可以进入就+1,同时用一个布尔矩阵来记录可以进入的格子,然后再来判断它能否进入四个相邻的格子(i,j-1),(i-1,j),(i,j+1),(i+1,j)。 /** * @Author GJ1e * @Create 2019/10/29 * @Time 9:41 * */ public class Solution13 { public int movingCount(int threshold, int rows, int cols){ if (threshold&lt;0 || rows&lt;=0 || cols&lt;=0) //异常矩阵检查 return -1; boolean[] visite = new boolean[rows*cols]; int count = movingCountCore(threshold,rows,cols,0,0,visite); //以(0,0)为起点坐标 return count; } /** * * @param threshold 进入格子的限制值 * @param rows 矩阵的行 * @param cols 矩阵的列 * @param row 起点的行坐标 * @param col 起点的列坐标 * @param visit 用来记录已经进入过的格子 * @return */ public int movingCountCore(int threshold, int rows, int cols, int row, int col, boolean[] visit){ int count = 0; if (row&gt;=0 &amp;&amp; row&gt;rows &amp;&amp; col&gt;=0 &amp;&amp; col&gt;cols &amp;&amp; (getDigitSum(row)+getDigitSum(col))&lt;=threshold &amp;&amp; !</description>
</item>
<item>
<title>剑指Offer(Java实现)12题</title>
<link>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B012%E9%A2%98/</link>
<pubDate>Sun, 27 Oct 2019 21:26:14 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B012%E9%A2%98/</guid>
<description>面试题12:矩阵中的路径 题目:
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。例如:
a b t g c f c s j d e h 这个字符矩阵中包含一条字符串bfce的路径。但矩阵中不把韩字符串“abfb”的路径,因为字符串的第二个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。
思路:
回溯法,这是一个可以用回朔法解决的典型题。 首先,在矩阵中任选一个格子作为路径的起点。如果路径上的第i个字符不是ch,那么这个格子不可能处在路径上的第i个位置。 如果路径上的第i个字符正好是ch,那么往相邻的格子寻找路径上的第i+1个字符。除在矩阵边界上的格子之外,其他格子都有4个相邻的格子。重复这个过程直到路径上的所有字符都在矩阵中找到相应的位置。
由于回朔法的递归特性,路径可以被开成一个栈。当在矩阵中定位了路径中前n个字符的位置之后,在与第n个字符对应的格子的周围都没有找到第n+1个字符,这个时候只要在路径上回到第n-1个字符,重新定位第n个字符。
由于路径不能重复进入矩阵的格子,还需要定义和字符矩阵大小一样的布尔值矩阵,用来标识路径是否已经进入每个格子。 当矩阵中坐标为(row,col)的格子和路径字符串中下标pathLength的字符一样时,从4个相邻的格子(row,col-1),(row-1,col),(row,col+1)以及(row+1,col)中去定位路径字符串中下一个字符如果4个相邻的格子都没有匹配字符串中pathLength+1的字符,表明当前路径字符串中下标为pathLength的字符在矩阵中的定位不正确,我们需要回到前一个字符(pathLength-1)然后重新定位。
一直重复这个过程,直到路径字符串上所有字符都在矩阵中找到合适的位置
/** * @Author GJ1e * @Create 2019/10/26 * @Time 20:16 * */ public class Solution12 { public boolean hasPath(char[] matrix, int rows, int cols, char[] str){ if (matrix==null || rows&lt;=0 || cols&lt;=0 || str==null) //非法矩阵和异常字符串检查 return false; boolean[] visited = new boolean[rows*cols]; int pathLength = 0; //字符串索引初始下标和路径长度 for (int row = 0; row &lt; rows; row++) { for (int col = 0; col &lt; cols; col++) { if (hasPathCore(matrix,rows,cols,str,row,col,visited,pathLength)) return true; } } return false; } /** * * @param matrix 字符矩阵 * @param rows 矩阵的行 * @param cols 矩阵的列 * @param str 字符串 * @param row 索引的行 * @param col 索引的列 * @param visited 布尔矩阵,用来标识路径是否已经进入了每个格子 * @param pathLength 记录字符串的索引下标和路径的长度 * @return */ public boolean hasPathCore(char[] matrix, int rows, int cols, char[] str, int row, int col, boolean[] visited, int pathLength){ boolean flag = false; if (row&gt;=0 &amp;&amp; row&lt;rows &amp;&amp; col&gt;=0 &amp;&amp; col&lt;cols&amp;&amp; !</description>
</item>
<item>
<title>剑指Offer(Java实现)11题</title>
<link>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B011%E9%A2%98/</link>
<pubDate>Sun, 27 Oct 2019 21:26:10 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B011%E9%A2%98/</guid>
<description>面试题11:旋转数组的最小数字 问题: 把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 思路:
二分查找算法的思路来解。 因为数组是非递减(注意不是严格递增,会出现:2,2,2,2,1,2这样的)且排序的数组。 mid = low +(high-low)/2 有三种情况: 第一种:array[mid] &gt; array[high],这种类似于{3,4,5,1,2}最小的数字在mid的右边。所以:low = mid+1
第二种:array[mid] &lt; array[high],这种类似于{1,1,1,3,4}最小的数字有可能就是mid或者在mid的左边。所以:high = mid
第三种:array[mid] == array[high],这种类似于{1,0,1,1,1}或{1,1,1,0,1}这时最小的数字不好判断,只能一个一个试。所以:high = high-1;
/** * @Author GJ1e * @Create 2019/10/26 * @Time 19:49 * */ public class Solution11 { public int minNumberInRotateArray(int [] array){ if (array==null || array.length &lt;= 0) return -1; int low = 0; int high = array.</description>
</item>
<item>
<title>剑指Offer(Java实现)10-3题</title>
<link>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B010-3%E9%A2%98/</link>
<pubDate>Sun, 27 Oct 2019 21:26:01 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B010-3%E9%A2%98/</guid>
<description>面试题10的扩展题:矩形覆盖 题目: 我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2*1的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法? 思路: 斐波那契数列的变形题,思路就是递归循环。 2*n的大矩形,和n个2*1的小矩形,其中n*2为大矩阵的大小,有以下几种情形: n &lt;= 0 大矩形为2*0,直接return 0; n = 1 大矩形为2*1,只有一种摆放方法,return1; n = 2 大矩形为2*2,有两种摆放方法,return2; n = 3 分为两步考虑:有三种摆放方法,所以f(3)=f(1)+f(2) 以此类推,f(n)=f(n-1)+f(n-2) /** * @Author GJ1e * @Create 2019/10/27 * @Time 22:20 * */ public class Solution10_3 { public int RectCover(int n){ if (n&lt;=0) return 0; else if (n==1) return 1; else if (n==2) return 2; else return RectCover(n-1) + RectCover(n-2); } } 此代码已在牛客网AC</description>
</item>
<item>
<title>面试题整理一</title>
<link>https://gj1e.github.io/posts/2019/10/%E9%9D%A2%E8%AF%95%E9%A2%98%E6%95%B4%E7%90%86%E4%B8%80/</link>
<pubDate>Wed, 23 Oct 2019 17:05:19 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/10/%E9%9D%A2%E8%AF%95%E9%A2%98%E6%95%B4%E7%90%86%E4%B8%80/</guid>
<description>Java ConcurrentHashMap线程安全的具体实现方式/底层具体实现 String和StringBuffer、StringBuilder的区别是什么? String为什么是不可变的? CMS垃圾收集器可以和哪些垃圾收集器配合使用 数据结构与算法 红黑树的特点 已整理,待上传。
LRU怎么实现的 MySQL MySQL在使用索引时需要注意什么 已整理,待上传。
为什么MySQL可重复读的隔离级别就可以防止幻读 Redis Redis中String的最大Value Redis中各数据结构的使用场景 Redis持久化机制 高并发下怎么防止商品超卖 勿在浮沙筑高台。</description>
</item>
<item>
<title>RabbitMQ</title>
<link>https://gj1e.github.io/posts/2019/10/rabbitmq/</link>
<pubDate>Wed, 23 Oct 2019 14:46:44 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/10/rabbitmq/</guid>
<description>RabbitMQ是一个消息队列,消息队列是分布式系统中重要的组件。消息队列可以比作是一个存放消息的容器,当我们需要使用消息的时候可以取出消息供自己使用。使用消息队列主要是为了通过异步处理提高系统性能和削峰、降低系统耦合性。 使用场景 消息队列常见的使用场景最核心的有三个:解耦,异步,削峰
解耦 消息发送者(生产者)发送消息到消息队列中,消息接收者从消息队列中获取消息。两者之间没有直接耦合。 对新增业务,只要对消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计。 异步和削峰 在不使用消息队列服务器的时候,用户请求数据直接写入数据库,在高并发的情况下数据库压力剧增,使得响应速度变慢。
在使用消息队列之后,用户的请求数据发送给消息队列之后立即返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库。消息队列处理速度快于数据库,且消息队列也比数据库具有更好的伸缩性,所以响应速度会得到大幅度改善。
消息队列具有很好的削峰功能的作用,通过异步处理,将短时间的高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。
需要特别注意的是,用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验,写数据库等操作中可能失败。因此使用消息队列进行异步处理之后,需要适当修改业务流程。
使用消息队列的缺点 系统可用性降低:系统引入的外部依赖越多,越容易怪掉。在加入MQ之前,你不用考虑消息丢失或者说MQ挂掉等情况。但是引入MQ之后你就需要去考虑了。 系统复杂性提高:加入MQ之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等问题。 一致性问题:虽然消息队列可以实现异步,消息队列带来的异步确实可以提高系统的响应速度。但是万一消息的真正消费者并没有正确的消费消息,这样就会导致数据不一致的情况了。 如何保证消息不被重复消费(幂等性) 拿到数据要写库,就先根据数据的主键查一下,如果数据库中存在这些数据,就不要插入了,Update一下。 可以基于数据库的唯一键来保证重复数据不会插入多条,因为有唯一键约束了,重复插入数据指挥报错,不会导致数据库中出现脏数据。 如果是写Redis就没啥问题,因为set天然幂等性。 如何保证消息队列的高可用 RabbitMQ是基于主从来做高可用的。 RabbitMQ有三种模式:单机模式,普通集群模式,镜像集群模式 单机模式很少使用,普通集群模式只是提高了系统吞吐量,让集群中多个节点来服务某个队列的读写操作。 镜像集群模式是真正实现RabbitMQ高可用的模式。镜像集群模式中创建的队列,无论元数据,还是队列中的消息,都会存在于多个实例上,然后你每次写消息到队列的时候,都会自动和多个实例队列进行消息同步。 好处:任何一台机器宕掉,不会影响其他机器的使用。 坏处:性能开销太大,消息同步所有的机器会导致网络带宽压力和消耗比较重。 如何保证消息的可靠性传输(消息丢失的处理) RabbitMQ中有三个角色:
消息生产者:向队列中发布消息的角色。 消息代理者:RabbitMQ自己,它不生产消息,也不对消息进行消费。就是起一个消息容器,对消息进行分发的作用。 消息消费者:从队列中获取消息进行消费的角色。 对于消息生产者 可以选择RabbitMQ提供的事务功能,就是生产者再发送数据前开启RabbitMQ的事务,然后再发送消息。如果消息没有被成功接收,生产者就会接收到异常报错,此时可以回滚事务,重新发送。如果消息被接收到了,就可以提交事务。缺点就是:太耗费性能,会降低吞吐量。
可以开启confirm模式,在生产者那里开启后,你每次写的消息都会被分配一个全局唯一ID。如果消息写入到了队列中,那么RabbitMQ会回传一个ACK的消息。如果MQ没能处理这个消息,会回调一个你的NACK接口,告诉你消息没收到,然后你可以重新发送。
事务机制和confirm机制最大的区别在于事务机制是同步的,你提交一个事务之后会阻塞在那儿,但confirm机制是异步的,你发送完消息就可以接着发下一个,如果消息被接受了之后,它会异步回调接口通知你。
对于消息代理者 开启RabbitMQ的持久化,讲就是将消息写入之后会持久化到磁盘,这样就算是自己挂掉了,也会在恢复之后自动读取之前存储的数据。
设置消息持久化有两步:
创建队列时将其设置为持久化。(队列持久化,只会把队列中的元数据持久化,不会把队列中的消息持久化的磁盘上) 发送消息时,将消息也设置为持久化,这样RabbitMQ就会将消息持久化到磁盘上。 持久化可以和生产者的confirm机制配合起来,只有消息被持久化到磁盘上了,才会通知生产者ACK。这样就算是在持久化之前RabbitMQ挂掉了,数据丢了,但生产者没收到ACK,这样你自己也可以重发消息。</description>
</item>
<item>
<title>Java对象的创建及类加载的过程</title>
<link>https://gj1e.github.io/posts/2019/10/java%E5%AF%B9%E8%B1%A1%E7%9A%84%E5%88%9B%E5%BB%BA%E5%8F%8A%E7%B1%BB%E5%8A%A0%E8%BD%BD%E7%9A%84%E8%BF%87%E7%A8%8B/</link>
<pubDate>Tue, 22 Oct 2019 18:47:37 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/10/java%E5%AF%B9%E8%B1%A1%E7%9A%84%E5%88%9B%E5%BB%BA%E5%8F%8A%E7%B1%BB%E5%8A%A0%E8%BD%BD%E7%9A%84%E8%BF%87%E7%A8%8B/</guid>
<description>Java对象的创建过程 创建过程分为五步:
类加载检查
当Java虚拟据遇到一条new指令时,首先检查这个指令中的参数能否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载,解析,初始化。如果没有,则先执行相应的类加载过程。 分配内存空间
当类加载检查通过之后,就应该为对象分配内存空间。对象所需的内存大小在类加载完成后就能确定。 分配的方式主要有指针碰撞和空闲列表两种方式,选择哪种分配方式由Java堆是否规整来决定,而 Java 堆内存是否规整,取决于 GC 收集器的算法是&rdquo;标记-清除&rdquo;,还是&rdquo;标记-整理&rdquo;(也称作&rdquo;标记-压缩&rdquo;),值得注意的是,复制算法内存也是规整的 初始化零值
当内存分配完毕之后,就应该将分配到的内存空间初始化为零值(不包括对象头)。 这一步操作保证了对象的实例字段在Java代码中可以不赋值就直接使用,程序可以访问到这些字段的数据类型所对应的零值。 设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如元数据信息,哈希码,GC分代年龄等信息) 执行init方法
执行new指令之后,紧接着会执行init方法,把对象按照程序员的意愿进行初始化。这样一个对象才算是完全产生出来。 内存分配的两种方式 指针碰撞
用过的内存全部整合到一边,没用过的放在另一边,中间有一个分界指针。分配时,只需要向着没有用过的方向,将该指针移动对象内存大小的位置即可。 GC收集器:Serial,ParNew - 适用于堆内存规整的情况下(没有内存碎片)。 空闲列表
虚拟机会维护一个列表,记录上哪些内存块是可用的。再分配时,从列表中找到一块足够的空间划分给对象实例,并更新列表上的记录。 GC收集器:CMS 适用于堆内存不规整的情况。 内存分配并发问题 CAS+失败重试
CAS是乐观锁的一种实现方式,虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。 TLAB
每个线程在Java堆中预先分配一小块内存,哪个线程要分配内存,就在哪个线程的TALB上分配,当对象大于TALB中的剩余内存,或TALB的内存用尽时,在采用上面的CAS进行内存分配。 Java中类加载的过程 类加载的过程为:加载——&gt;验证——&gt;准备——&gt;解析——&gt;初始化,总共五步。
加载 通过全类名获取定义此类的二进制字节流。 将字节流所代表的静态存储结构转化为方法区的运行时存储结构。 在内存中生成一个代表该类的Class对象,做为方法区访问的入口。 数组类型不通过类加载器创建,它由Java虚拟机直接创建。
验证 验证Class文件中的字节流是否符合Java虚拟机的规范,包括元数据信息的验证和文件格式的验证等。</description>
</item>
<item>
<title>JVM内存区域</title>
<link>https://gj1e.github.io/posts/2019/10/jvm%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F/</link>
<pubDate>Tue, 22 Oct 2019 15:15:49 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/10/jvm%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F/</guid>
<description>运行时数据区 Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。
JDK1.8之前的
JDK1.8 和之前的版本略有不同,
线程共享
堆,方法区,直接内存 线程私有
程序计数器,本地方法栈,虚拟机栈 堆 堆是Java虚拟机所管理的内存中最大的一块,Java堆是所有线程共享的,主要用来存放实例对象以及数组。
堆也是垃圾回收的主要区域,垃圾回收主要采用的是分代回收,分为新生代和老年代。
Java堆可以是内存上不连续的的区域,只要逻辑上联系即可。
堆空间不足时会抛出:OutOfMemoryError。
方法区 方法区和Java堆一样,也是线程共享的。方法区主要用来存放已被虚拟机加载的类信息,常量,静态变量,和编译后的代码。
方法区空间不足时会抛出:OutOfMemoryError。
运行时常量池 运行时常量池是方法区的一部分,用于存放编译期间所产生的字面量和符号引用。这部分内容将在类加载后进入方法区的运行时常量池中。
运行时常量池,相比于Class文件中的常量池一个重要的特征就是具备动态性,可以在运行期间,通过String类的intern()方法将新的常量放入池中。
运行时常量池受方法区的内存限制,当常量池无法在申请到足够的内存时会抛出: OutOfMemoryError。
程序计数器 程序计数器是虚拟机管理的内存中较小的一块,它可以看作是当前程序执行的字节码文件的行号指示器。
它是线程私有的,各线程之间的程序计数器,相互独立,互不干扰。
程序计数器是唯一一个不会出现OutOfMemoryError的内存区域。
它的生命周期和线程一样。
虚拟机栈 虚拟机栈和程序计数器一样,也是线程私有的,它的生命周期和线程相同。描述的是Java方法执行的内存模型。
每个方法在执行时,都会创建一个栈帧,栈帧中拥有局部变量表,操作数栈,动态链接,方法出口等信息。
每一次函数调用都会有一个对应的栈帧被压入java栈,函数调用结束后,都会有一个相应的栈帧被弹出。</description>
</item>
<item>
<title>计网5层体系结构简介</title>
<link>https://gj1e.github.io/posts/2019/10/%E8%AE%A1%E7%BD%915%E5%B1%82%E4%BD%93%E7%B3%BB%E7%BB%93%E6%9E%84%E7%AE%80%E4%BB%8B/</link>
<pubDate>Thu, 17 Oct 2019 16:28:41 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/10/%E8%AE%A1%E7%BD%915%E5%B1%82%E4%BD%93%E7%B3%BB%E7%BB%93%E6%9E%84%E7%AE%80%E4%BB%8B/</guid>
<description>学习计算机网络时我们一般采用折中的办法,也就是中和 OSI 和 TCP/IP 的优点,采用一种只有五层协议的体系结构,这样既简洁又能将概念阐述清楚。 应用层 应用层(application-layer)的任务是通过应用进程间的交互来完成特定网络应用。应用层协议定义的是应用进程(进程:主机中正在运行的程序)间的通信和交互的规则。对于不同的网络应用需要不同的应用层协议。在互联网中应用层协议很多,如域名系统DNS,支持万维网应用的 HTTP协议,支持电子邮件的 SMTP协议等等。我们把应用层交互的数据单元称为报文。
域名系统 域名系统(Domain Name System缩写 DNS,Domain Name被译为域名)是因特网的一项核心服务,它作为可以将域名和IP地址相互映射的一个分布式数据库,能够使人更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。(百度百科)例如:一个公司的 Web 网站可看作是它在网上的门户,而域名就相当于其门牌地址,通常域名都使用该公司的名称或简称。例如上面提到的微软公司的域名,类似的还有:IBM 公司的域名是 www.ibm.com、Oracle 公司的域名是 www.oracle.com、Cisco公司的域名是 www.cisco.com 等。
HTTP协议 超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的 WWW(万维网) 文件都必须遵守这个标准。设计 HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法。(百度百科)
运输层 运输层(transport layer)的主要任务就是负责向两台主机进程之间的通信提供通用的数据传输服务。应用进程利用该服务传送应用层报文。“通用的”是指并不针对某一个特定的网络应用,而是多种应用可以使用同一个运输层服务。由于一台主机可同时运行多个线程,因此运输层有复用和分用的功能。所谓复用就是指多个应用层进程可同时使用下面运输层的服务,分用和复用相反,是运输层把收到的信息分别交付上面应用层中的相应进程。
运输层主要使用以下两种协议: 传输控制协议 TCP(Transmission Control Protocol)&ndash;提供面向连接的,可靠的数据传输服务。
用户数据协议 UDP(User Datagram Protocol)&ndash;提供无连接的,尽最大努力的数据传输服务(不保证数据传输的可靠性)。
网络层 在计算机网络中进行通信的两个计算机之间可能会经过很多个数据链路,也可能还要经过很多通信子网。网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送。 在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和包进行传送。在 TCP/IP 体系结构中,由于网络层使用 IP 协议,因此分组也叫 IP 数据报 ,简称 数据报。
这里要注意:不要把运输层的“用户数据报 UDP ”和网络层的“ IP 数据报”弄混。另外,无论是哪一层的数据单元,都可笼统地用“分组”来表示。</description>
</item>
<item>
<title>TCP协议与UDP协议</title>
<link>https://gj1e.github.io/posts/2019/10/tcp%E5%8D%8F%E8%AE%AE%E4%B8%8Eudp%E5%8D%8F%E8%AE%AE/</link>
<pubDate>Thu, 17 Oct 2019 16:27:33 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/10/tcp%E5%8D%8F%E8%AE%AE%E4%B8%8Eudp%E5%8D%8F%E8%AE%AE/</guid>
<description> TCP和UDP的区别 TCP是面向连接的协议,也就是说在收发数据前必须先和对方建立起可靠的连接。一个TCP协议的连接需要经过“三次”会话才能建立起来。TCP协议提供超时重发,丢弃重复数据,校验数据,流量控制等功能。TCP协议可以保证将数据从一端传送到另一端,当数据从A端传送到B端时,B端会向A端发送一个确认包(ACK包)来告知A端“你发送的数据我已收到”,UDP就没有这种确认机制,这就是为什么说TCP是可靠的,UDP是不可靠的。TCP提供了可靠的数据传输,但是TCP协议提供的拥塞控制,重传机制,数据校验等功能会加大网络带宽的开销,因为虚拟信道是持续存在的,并且网络中还会出现大量的ACK包,和FIN包。
UDP协议是无连接的不可靠协议,并且没有超时重传机制,会发生丢包,收到重复包,乱序等情况。但是UDP网络开销小,适合实时通讯例如QQ语音,QQ视频,直播等等。
当我们看重的是数据传输的完整性,可控制性和可靠性时,TCP协议是最好的选择。但当我们看重数据的传输性能而不是完整性时,UDP是最好的选择。
TCP的三次握手 客户端建立连接控制模块TCB,向服务端发送请求连接的报文段,其中报文段的首部SYN=1,为自己选择一个初始的序号seq=i。TCP规定SYN=1的报文段不允许携带数据,但是要消耗掉一个序号,发送完毕客户端进入同步发送(SYN_sent)状态。
服务端收到请求之后,如果同意建立连接,会向客户端发送确认。确认报文中的ACK=1,SYN=1,确认序号ack=i+1,为自己选择一个初始序号seq=k。该报文段也不能携带数据,但要消耗掉一个序号。发送完毕,服务端进入同步收到(SYN_RCVD)状态。
客户端收到后,还要向服务端给出确认。确认报文中ACK=1,确认号ack=k+1,自己的序号为seq=i+1。ACK报文段可以携带数据,不携带数据不消耗序号。如果不携带数据则下一个序号仍为seq=i+1。
TCP的四次挥手 客户端向服务端发出释放连接的报文后,客户端就停止发送数据。释放连接的报文中FIN=1,初始序号seq=u(等于前面已经传送过来的数据的最后一个字节的序号+1)。此时客户端进入终止等待状态(FIN_wait1),TCP规定,FIN报文即使不携带数据也要消耗掉一个序号。 服务端收到后,要想客户端发送确认。确认报文中ACK=1,确认号ack=u+1,为自己选择一个初始序号seq=v,自己进入到关闭等待状态(CLOSE_wait)。此时处于半关闭状态,客户端没有数据要发送了,但是服务端可能还有数据要发送,客户端依旧要接收。 服务端数据发送完毕之后,会向客户端发出释放连接的报文。报文中FIN=1,ACK=1,ack=u+1,seq=w。此时服务端进入最后确认状态(LAST_ACK)。 客户端收到服务端的释放连接报文后,必须向服务端发送确认报文。确认报文中ACK=1,ack=w+1,seq=u+1。此时客户端进入到时间等待状态(TIME_wait)。此时TCP连接还没有释放,必须经过2MSL的时间,当客户端撤销相应的TCB之后才进入CLOSED状态,但是服务端只要收到客户端发出的确认报文后,就马上进入CLOSED状态。 为什么TCP连接的时候是三次握手,关闭却是四次挥手 建立连接时服务端收到客户端发送的SYN连接请求报文后,可以直接发送ACK+SYN报文来回复,其中ACK用来应答,SYN用来同步。但是释放连接时,服务端收到客户端发来的FIN报文之后,可能并不会立即关闭连接,只能先向客户端发送一个ACK报文来应答,告诉客户端你发送的FIN报文我已经收到了。然后等服务端的数据都发送完毕了,才能发送FIN报文,因此需要4步。 为什么Time_wait状态要经过2MSL的时间才能进入CLOSED状态 虽然按道理,四个报文发送完毕我们就可以关闭连接了。但是,我们必须假设网络是不可靠的,有可能最后一个ACK报文丢失,客户端的TIME_ wait状态就是用来重发ACK报文的,如果服务端没有收到我们发送的ACK报文,它就会不断重复发送FIN片段。所以客户端不能立即关闭,必须确认服务端收到了该ACK报文。当客户端发送完最后一个ACK报文,进入到Time_wait状态之后,会设置一个计数器,等待2MSL的时间。如果2MSL时间内,收到了服务端发送的FIN报文,客户端要再一次向服务端发送ACK报文,并将计时器重置,继续等待2MSL的时间。如果在2MSL的时间内,没有接收到服务端发来的FIN报文,就证明服务端收到了最后一个ACK报文,客户端进入CLOSED状态,结束TCP连接。 MSL指一个报文片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大的时间。如果直到2MSL客户端都没有接收到服务端的FIN报文,那么客户端就认为服务端接收到了ACK。
</description>
</item>
<item>
<title>剑指Offer(Java实现)10-2题</title>
<link>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B010-2%E9%A2%98/</link>
<pubDate>Tue, 15 Oct 2019 23:16:32 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B010-2%E9%A2%98/</guid>
<description>面试题10_1的扩展题(牛客网的变态跳台阶问题) 题目: 一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。 思路: 引用两个牛客网上的高赞思路: 第一个:
f(1) = 1
f(2) = f(2-1) + f(2-2) //f(2-2) 表示2阶一次跳2阶的次数。
f(3) = f(3-1) + f(3-2) + f(3-3)
&hellip;
f(n) = f(n-1) + f(n-2) + f(n-3) + &hellip; + f(n-(n-1)) + f(n-n)
说明:
1)这里的f(n) 代表的是n个台阶有一次1,2,&hellip;n阶的 跳法数。
2)n = 1时,只有1种跳法,f(1) = 1
3)n = 2时,会有两个跳得方式,一次1阶或者2阶,这回归到了问题(1) ,f(2) = f(2-1) + f(2-2)
4) n = 3时,会有三种跳得方式,1阶、2阶、3阶, 那么就是第一次跳出1阶后面剩下:f(3-1);第一次跳出2阶,剩下f(3-2);第一次3阶,那么剩下f(3-3)
因此结论是f(3) = f(3-1)+f(3-2)+f(3-3)</description>
</item>
<item>
<title>剑指Offer(Java实现)10-1题</title>
<link>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B010-1%E9%A2%98/</link>
<pubDate>Tue, 15 Oct 2019 23:16:23 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B010-1%E9%A2%98/</guid>
<description>面试题10的扩展题:青蛙跳台阶问题 题目: 一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。 思路: 和面试题10的思路差不多,一种递归,一种找规律。 找规律:f(0)=0,f(1)=1,f(2)=2,f(3)=f(1)+f(2),f(4)=f(2)+f(3)···以此类推。 /** * @Author GJ1e * @Create 2019/10/15 * @Time 20:18 * */ public class Solution10_1 { //递归 public int JumpFloor(int n){ if (n&lt;=0) return 0; else if (n==1) return 1; else if (n==2) return 2; return JumpFloor(n-1) + JumpFloor(n-2); } //找规律 public int JumpFloor01(int n){ if (n&lt;=0) return 0; else if (n==1) return 1; else if (n==2) return 2; int jumpOne = 1; int jumpTwo = 2; int jumpN = 0; for (int i = 3; i &lt;= n; i++) { jumpN = jumpOne + jumpTwo; jumpOne = jumpTwo; jumpTwo = jumpN; } return jumpN; } } 两种方法的代码都已AC</description>
</item>
<item>
<title>剑指Offer(Java实现)10题</title>
<link>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B010%E9%A2%98/</link>
<pubDate>Tue, 15 Oct 2019 23:14:53 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B010%E9%A2%98/</guid>
<description>面试题10:斐波那契数列 题目: 大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。 思路: 第一种递归。 第二种找规律,f(0)=0,f(1)=1,f(2)=f(1)+f(0),f(3)=f(2)+f(1)···以此类推 /** * @Author GJ1e * @Create 2019/10/15 * @Time 20:05 * */ public class Solution10 { //递归 public int fibonacci01(int n){ if (n&lt;=0) return 0; else if (n == 1) return 1; return fibonacci01(n-1) + fibonacci01(n-2); } //找规律 public int fibonacci02(int n){ if (n&lt;=0) return 0; else if (n == 1) return 1; int fibOne = 1; int fibTwo = 0; int fibN = 0; for (int i = 2; i &lt;= n; i++) { fibN = fibOne + fibTwo; fibTwo = fibOne; fibOne = fibN; } return fibN; } } 两种方法的代码都已AC</description>
</item>
<item>
<title>剑指Offer(Java实现)09题</title>
<link>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B009%E9%A2%98/</link>
<pubDate>Tue, 15 Oct 2019 23:14:41 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B009%E9%A2%98/</guid>
<description>面试题09:用两个栈实现队列 题目: 用两个栈实现一个队列。完成队列的Push和Pop操作。队列中的元素为int类型。 思路:
栈的特点是先进后厨,队列的特点是先进先出。 队列:入队顺序:1,2,3,4;出队顺序:1,2,3,4 栈:Push顺序:1,2,3,4;Pop顺序:4,3,2,1
首先插入元素1,随便把它插入哪个栈,比如我们就把它插入stack1,此时stack1中的元素有{1},stack2为空。
在压入两个元素2,3还是插入stack1,此时stack1中的元素有{1,2,3}其中元素3位于栈顶。这时如果删除一个元素,按照队列先进先出的规则,应该删除元素1。但是元素1并不位于栈顶,因此不能直接删除,如果我们把stack1中的元素依次弹出并压入stack2中,则stack2中元素的顺序正好和stack1中的顺序相反,stack2中元素的顺序为{3,2,1}.这是元素1就位于栈顶了,就可以直接弹出了,且stack2中所有元素的弹出顺序都和队列出队的顺序一样。
因此可以总结出,当stack2不位空时,stack2中的栈顶元素就是最先入队列的元素,直接弹出即可。当stack2位空时,我们可就把stack1中的元素逐个弹出并压入stack2中。如果接下来在插入一个元素4,则还是把它压入stack1。
/** * @Author GJ1e * @Create 2019/10/15 * @Time 18:39 * */ public class Solution { Stack&lt;Integer&gt; stack1 = new Stack&lt;Integer&gt;(); Stack&lt;Integer&gt; stack2 = new Stack&lt;Integer&gt;(); //队列的Push操作。 public void Push(int elemen){ stack1.push(elemen); } //队列的Pop操作 public int Pop(){ if (stack1.empty() &amp;&amp; stack2.empty()) throw new RuntimeException(&quot;请添加元素&quot;); if (stack2.empty()){ //stack2为空就把stack1中的元素弹出并压入stack2中。 while (!</description>
</item>
<item>
<title>剑指Offer(Java实现)08题</title>
<link>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B008%E9%A2%98/</link>
<pubDate>Tue, 15 Oct 2019 23:14:33 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B008%E9%A2%98/</guid>
<description>面试题08:二叉树的下一个节点 题目: 给定一棵二叉树和其中的一个节点,如何找出中序遍历的下一个节点?树中的节点除了有两个分别指向左、右子节点的指针,还有一个指向父节点的指针。 思路:
二叉树中序遍历序列的顺序:左,根,右。 根据二叉树中序遍历的规则,我们可以将树中的节点分为以下几种情况:
1、节点有右子树,那么它的下一个节点,就是它自己的右子树中的最左子节点。
(也就是从右子节点出发,一直沿着指向左子节点的指针,就能找到它的下一个节点)
2、节点没有右子树,并且还是自己父节点的左子节点,那么它的下一个节点就是自己的父节点。
3、节点没有右子树,并且还是自己父节点的右子节点。对于这样的节点,我们可以沿着它指向父节点的指针一直向上遍历,直到找到一个是自己父节点的左子节点的节点。如果这样的节点存在,那么这个节点的父节点就是我们要找的下一个节点。
/** * @Author GJ1e * @Create 2019/10/10 * @Time 20:30 * */ public class Solution { //定义二叉树结构体 class BinaryTreeNode{ int vaule; BinaryTreeNode left = null; BinaryTreeNode right = null; BinaryTreeNode parent = null; public BinaryTreeNode(int vaule){ this.vaule = vaule; } } public BinaryTreeNode getNextNode(BinaryTreeNode pNode){ if (pNode==null) return null; BinaryTreeNode pNext = null; if (pNext.</description>
</item>
<item>
<title>剑指Offer(Java实现)07题</title>
<link>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B007%E9%A2%98/</link>
<pubDate>Tue, 15 Oct 2019 23:14:13 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B007%E9%A2%98/</guid>
<description>面试题07:重建二叉树 题目: 输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。 例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。 思路: 现根据先序序列找二叉树的根节点,先序序列的第一个节点就是root节点。 然后找出中序序列中root节点的位置。 根据中序序列的性质,root节点的左边为左子树的节点,右边为右子树的节点。 然后用递归的方式重建二叉树的左子树和右子树。 /** * @Author GJ1e * @Create 2019/10/26 * @Time 14:59 * */ public class Solution { class BinaryTreeNode{ int vaule; BinaryTreeNode left = null; BinaryTreeNode right = null; public BinaryTreeNode(int vaule){ this.vaule = vaule; } } public BinaryTreeNode reConstructBinaryTree(int [] pre, int [] in) { if (pre==null || pre.length&lt;=0 || in==null || in.</description>
</item>
<item>
<title>分布式一致性协议2PC&3PC</title>
<link>https://gj1e.github.io/posts/2019/10/%E5%88%86%E5%B8%83%E5%BC%8F%E4%B8%80%E8%87%B4%E6%80%A7%E5%8D%8F%E8%AE%AE2pc3pc/</link>
<pubDate>Fri, 11 Oct 2019 17:50:15 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/10/%E5%88%86%E5%B8%83%E5%BC%8F%E4%B8%80%E8%87%B4%E6%80%A7%E5%8D%8F%E8%AE%AE2pc3pc/</guid>
<description>在分布式系统中,每一个机器节点最然都能够,明确知道自己在进行事务操作的过程中是成功还是失败,但却无法直接获取其他分布式节点的操作结果。因为事务操作需要跨越多个分布式节点时,需要引入一个协调者统一调度所有节点的执行逻辑。
2PC 2pc协议共分为提交事务请求,执行事务提交两个阶段。
阶段一:提交事务请求 事务询问
协调者向所有参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应。 执行事务
各参与者节点执行事务操作,并将其操作写入到本地事务日志中。 各参与者向协调者反馈事务询问的响应
如果参与者成功执行了事务操作,那么就反馈给协调者YES响应,表示事务可以执行。 如果参与者没有成功过事务操作,那么就反馈给协调者No响应,表示事务不可以执行。 阶段二:执行事务提交 在阶段二中协调者会根据参与者反馈的情况来决定,最终是否可以进行事务的提交操作。
假如协调者收到参与者的反馈都是YES时,那么就会执行事务提交。
协调者向所有参与者发送正式提交事务的请求(即Commit请求)。 参与者执行Commit请求,并释放整个事务期间占用的资源。 各参与者向协调者反馈ACK完成的消息。 协调者收到所有参与者反馈的ACK消息后,即完成事务提交。 假如协调者收到任何一个参与者反馈NO,那么就执行中断事务
协调者向所有参与者发出回滚请求(即RollBack请求) 参与者根据阶段一事务日志中的操作执行回滚操作,并释放整个事务期间占用的资源。 各参与者向协调者反馈ACK完成的消息。 协调者收到所有参与者反馈的ACK消息后,完成事务中断。 2PC的缺陷 同步阻塞:即所偶参与的事务逻辑均处于阻塞状态。
单点故障:协调者存在单点故障问题,如果协调者出现故障,参与者将一直处于锁定状态。
脑裂问题:在阶段二中,如果只有部分参与者接受并执行了Commit请求,会导致节点数据不一致。
2PC的优点 原理简单,实现方便。
3PC 三阶段提交协议,是2pc的改进版本,即将事务的提交过程分为CanCommit,PreCommit,DoCommit三个阶段呢来进行处理。
阶段一:CanCommit 协调者向所有参与者发出包含事务内容的CanCommit请求,询问是否可以提交事务,并等待所有参与者的反馈。
参与者收到CanCommit请求后,如果认为可以执行事务操作,则反馈YES,并进入预备状态,否则反馈NO。
阶段二:PreCommit 此阶段分两种情况
事务预提交:(所有参与者反馈YES时)
协调者向所有参与者发出PreCommit请求,进入准备阶段。 参与者收到PreCommit请求后,执行事务操作,并记录到事务日志中。(但不提交事务) 各参与者向协调者反馈ACK响应或者NO响应,并等待最终指令。 中断事务:(任何一个参与者反馈NO或者等待超时后,协调者无法收到所有参与者的反馈时)</description>
</item>
<item>
<title>ZAB集群数据同步过程</title>
<link>https://gj1e.github.io/posts/2019/10/zab%E9%9B%86%E7%BE%A4%E6%95%B0%E6%8D%AE%E5%90%8C%E6%AD%A5%E8%BF%87%E7%A8%8B/</link>
<pubDate>Fri, 11 Oct 2019 10:36:18 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/10/zab%E9%9B%86%E7%BE%A4%E6%95%B0%E6%8D%AE%E5%90%8C%E6%AD%A5%E8%BF%87%E7%A8%8B/</guid>
<description> ZXID zxid是Zookeeper中事务的全局唯一ID。
ZXID有两部分组成:一部分为Leader的选举周期Epoch值;一部分为事务的递增计数器
同步过程 对于准Leadr,所有的Follower会向准Leader发送一个自己最后一次接受的事务的Epoch值。
当准Leader收到集群中过半的Follower发送的Epoch值之后,在这些Epoch值中选出一个最大值,将这个值+1得到新的Epoch值,并将这个新的Epoch值发送给集群中的Follower。
当Follower收到准Leader发送的Epoch值后,会将其与自己的Epoch值进行比较,若小于,则更新自己的Epoch值为新的值,并向Leader发送ACK信息,ACK信息中包含了自己的Epoch值和自己的历史事务集合。
Leader收到Follower发送的ACK信息之后,会在所有的历史事务集合中选出一个ZXID为最大的历史事务集合作为自己的初始化事务集合。
准Leader将Epoch值与初始化事务集合发送给集群中过半的Follower。Leader会为每一个Follower准备一个队列,并将那些没有被各个Follower同步的事务,以Proposal的形式发送给各个Follower,并在后面追加Commint消息,表示该事务已经被提交。
当Follower收到后,会接受并执行初始化事务集合,然后反馈给准Leader表明自己已处理。
当Leader收到Follower的反馈后,会向Follower发送Commint消息,Follower收到Commit消息后提交事务,完成数据同步。
</description>
</item>
<item>
<title>剑指Offer(Java实现)06题</title>
<link>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B006%E9%A2%98/</link>
<pubDate>Tue, 08 Oct 2019 16:06:51 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B006%E9%A2%98/</guid>
<description>面试题6:从尾到头打印链表 题目: 输入一个链表的头节点,从尾到头反过来打印出每个节点的值 思路1:
遍历的顺序是从头到尾,输出的顺序是从尾到头。也就是说,第一个遍历的节点,最后一个输出;最后一个遍历的节点,第一个输出。这就是典型的“先进后出”。所以可以选择“栈”来解决这个问题。 思路2:
这个题也可以用递归的方式来解,递归的本质就是一个栈结构。 import java.util.ArrayList; import java.util.Stack; public class Solution { //链表的定义 class ListNode{ int value; ListNode next = null; ListNode(int value){ this.value = value; } } public ArrayList&lt;Integer&gt; printListFromTailToHead(ListNode listNode){ ArrayList&lt;Integer&gt; arrayList = new ArrayList&lt;Integer&gt;(); if (listNode == null) //异常输入 return arrayList; Stack&lt;Integer&gt; stack = new Stack&lt;Integer&gt;(); while(listNode!= null) //链表压栈 { stack.push(listNode.value); listNode = listNode.</description>
</item>
<item>
<title>剑指Offer(Java实现)05题</title>
<link>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B005%E9%A2%98/</link>
<pubDate>Tue, 08 Oct 2019 15:27:20 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B005%E9%A2%98/</guid>
<description>面试题5:替换空格 题目: 请实现一个函数,将一个字符串中的每个空格替换成“%20”。 例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy. 思路: 就是一个空格变成了%20,也就是说每有一个空格,长度要增加2,所以首先先计算有多少个空格,这样长度就能增加多少,得到增加后的长度Length。 然后new一个Length长度的字符数组,从尾到头开始复制原来的数组,如果复制过程中,如果字符不是空格,直接复制,如果字符是空格,那么需要把这个空格变成%20(这个复制过程就是把新建的数组比如现在到了 K这个位置,然后就是K,K-1,K-2这三个位置依次变成0,2,%这三个字符,因为是从后往前复制的所以是倒序),重复这个过程就行。 方法一 /** * @Author GJ1e * @Create 2019/9/11 * @Time 20:17 */ public class Solution { public String replaceSpace(StringBuffer str) { if(str==null) return null; int space = 0; for(int i=0;i&lt;str.length();i++){ //记录空格数量 if(str.charAt(i)==' ') space++; } int indexOld = str.length()-1; //原字符串末尾 int newStrLength = str.length()+2*space; //新字符串长度 int indexNew = newStrLength-1; //新字符串末尾 str.setLength(newStrLength); //增加原字符串的长度,防止下标越界 while(indexOld !</description>
</item>
<item>
<title>剑指Offer(Java实现)04题</title>
<link>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B004%E9%A2%98/</link>
<pubDate>Tue, 08 Oct 2019 11:41:26 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/10/%E5%89%91%E6%8C%87offerjava%E5%AE%9E%E7%8E%B004%E9%A2%98/</guid>
<description>面试题4:二维数组中的查找 题目: 在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。 思路:
就是比较矩阵的右上角的数与target的大小,如果target比这个矩阵右上角的数大,由于矩阵的右上角元素A是A所在行的最大的值,所以target肯定不在A所在的行了,所以这时候就应该就在除去第一行的剩下的行中去找这个target; 如果target比矩阵右上角的数A小,那么由于A所在的列中A是最小的,那么target就在除去最右边的列的其它的列; 如果相等,返回true; /** * @Author GJ1e * @Create 2019/9/11 * @Time 18:58 */ public class Solution { public boolean arrayFind(int target , int[][] array){ //检查异常输入 if(array==null || array.length&lt;0 || array[0].length&lt;0) return false; int rows = 0; //行 int cols = array[0].length-1; //列 while(rows&lt;=array.length-1 &amp;&amp; cols&gt;=0){ if(target&gt;array[rows][cols]) //target大于矩阵左上角的数,说明target大于这一行上的所有数 rows++; //进入下一行继续比较 else if(target&lt;array[rows][cols]) //target小于矩阵左上角的数,说明target小于这一列上的所有数 cols--; //进入下一列继续比较 else //找到target return true; } return false; } } 此代码在牛客网已AC</description>
</item>
<item>
<title>Dubbo</title>
<link>https://gj1e.github.io/posts/2019/10/dubbo/</link>
<pubDate>Mon, 07 Oct 2019 20:53:26 +0800</pubDate>
<guid>https://gj1e.github.io/posts/2019/10/dubbo/</guid>
<description>Dubbo简介 Dubbo是一个分布式服务框架,以及SOA治理方案。其主要功能包括:
高性能NIO通讯以多协议集成 服务动态寻址与路由 软负载均衡与容错 依赖分析与降级 Dubbo的五个节点 Provider:暴露服务的服务提供方
Consumer:调用远程服务的服务消费方