/
我给出的答案(部分).txt
871 lines (770 loc) · 62.3 KB
/
我给出的答案(部分).txt
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
1.java:
java8有哪些新特性?lamda与stream了解吗?
答:
(1)
接口中可以用default关键字实现公用的方法
只声明一个方法的接口可以作为函数式接口
Function接口
labmda表达式
stream流式编程(分为串行流stream和并行流paralleStream)
新的日期与时间类LocalDate、LocalTime、LocalDateTime
(2)
lamda可以实现函数式编程,减少代码量
stream提供了一种更优雅的遍历以及处理集合的编程方式,搭配lamda使用效果更好
stream还采用了惰性计算,使程序运行变得非常高效
(3)
函数式编程思维的例子:
命令式编程:
int a,b,r;
void add_abs(){
scanf("%d %d",&a,&b);
r = abs(a)+abs(b);
printf("%d",r);
}
有点函数式编程的意思,但还不太函数式的编程:
int add_abs(int a,int b){
int r = 0;
r += abs(a);
r += abs(b);
return r;
}
标准的函数式编程,用函数的组合表示程序的组合:
int add_abs(int a,int b){
return abs(a)+abs(b);
}
介绍下java的nio?
答:
nio由jdk1.4引入,提供了高速的、面向块的IO,而原来的IO是面向流(一次移动一个字节)的。
不过目前java.io.* 中的IO类已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。
nio的主要特性为channle通道、buffer缓冲区、selector轮询管理多个channel:
buffer:
Buffer是一个对象, 它包含一些要写入或者刚读出的数据。 在 NIO 中加入 Buffer 对象,体现了新库与原 I/O 的一个重要区别。在面向流的 I/O 中,您将数据直接写入或者将数据直接读到 Stream 对象中。
在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。
缓冲区实质上是一个数组,通常它是一个字节数组,但是也可以使用其他种类的数组,buffer支持所有的java类型,比如ByteBuffer,CharBuffer,IntBuffer,DoubleBuffer等。
但是一个缓冲区不仅仅是一个数组:
buffer中的重要属性,position当前位置,limit当前数量,capacity最大容量
buffer中的重要方法: put()将position值增加1,代表新写入一个
flip()将 limit 设置为当前 position,将 position 设置为 0,代表可以读取数据了。
clear()将 limit 设置为与 capacity 相同,设置 position 为 0,代表可以写入数据了。
channel:
Channel也是一个对象,可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做个比较,通道就像是流。
通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类), 而 通道 可以用于读、写或者同时用于读写。
nio的工作流程:
读取数据时先创建一个缓冲区,然后让通道将数据读到这个缓冲区中;
写入数据时先创建一个缓冲区,用数据填充它,然后让通道用这些数据来执行写入操作。
总之无论读写,都需要使用缓冲区。
selector:
selector可使一个单独的线程管理多个Channel,open方法可创建Selector,register方法向多路复用器器注册通道,可以监听的事件类型:读、写、连接、accept。注册事件后会产生一个SelectionKey:它表示SelectableChannel 和Selector 之间的注册关系,wakeup方法:使尚未返回的第一个选择操作立即返回,唤醒的原因是:注册了新的channel或者事件;channel关闭,取消注册;优先级更高的事件触发(如定时器事件),希望及时处理。
NIO的服务端建立过程:
Selector.open():打开一个Selector;ServerSocketChannel.open():创建服务端的Channel;bind():绑定到某个端口上。并配置非阻塞模式;register():注册Channel和关注的事件到Selector上;select()轮询拿到已经就绪的事件
io中缓冲区的作用?为什么缓冲区可以加快读取速度?
答:
进行系统调用是耗时操作,但是无论读取多少数据,每次读取消耗的时间都几乎相等。所以读取等量的数据时,使用缓冲区可以减少系统调用的次数,从而减少调用的时间。
将一堆砖头搬从A点搬到B点,直接用字节流的话,就是一块一块的搬,这个过程要执行很多次,效率上会很低。
如果是采用缓冲流,那就相当于给你一个小推车,每次把小推车装满再搬,这样次数就会大大 降低,效率上也会有提升。
原因就是因为io操作是很耗时的,如果每次只读一个字节,那么对于一个1KB的数据就要进行1000次IO,而如果使用一个1KB大小的缓冲区,那么就可以一次IO直接读完。
(java中创建1KB大小对象的方法:new byte[1024] 1MB:new byte[1024*1024])
介绍下netty?
答:
一个高性能、异步事件驱动的NIO框架,它提供了对TCP、UDP和文件传输的支持;
可使用接受/处理线程池,提高连接效率,对重连、心跳检测的简单支持;
使用更高效的socket底层,对epoll空轮询引起的cpu占用飙升在内部进行了处理,避免了直接使用NIO的陷阱,简化了NIO的处理方式。
了解java的深拷贝与浅拷贝吗?
1.直接赋值:A a1=a2,仅仅是复制了引用
2.浅拷贝:调用clone 方法,这种情况下对象是被复制了,但是如果对象内部有引用字段,那么仍然指向同一个引用的对象
3.深拷贝:可以自己实现clone方法,将所有引用字段都创建一个新的对象;
也可以利用序列化和反序列化实现深拷贝。
详细描述下java中的hashmap实现原理(附加:jdk1.8之前与1.8之后,hashmap的实现有什么区别)?
答:
(1)hashmap实现原理
hashmap的底层是一个数组,数组的大小为2的整数次幂,原因是因为计算hash取模运算时,h & n-1的性能比 h%n的好,并且在n为2的整数次幂时,两者计算结果等价。
hashmap的put分为两种情况:
没有hash冲突,则直接放入数组的指定下标位置;
有hash冲突,则插入数组指定下标处链表的头部,作为链表的新的头节点。
hahsmap存在一个loadFactor用来控制何时进行扩容,默认为0.75,比如大小为16的hashmap,当存放元素超过16*0.75=12时,将进行resize。
resize的操作会将当前数组的容量*2,然后重新计算所有元素在新数组中的位置即rehash。
resize和rehash操作非常耗时且容易造成线程安全问题,因此建议在使用hashmap时,指定一个合适的初始化大小以避免扩容。
hashmap不是线程安全的,但是提供了fail-fast机制,实现原理为hashmap中有个modcount的变量,当进行put操作时会将该变量值+1,在使用iterator遍历hashmap前,会将当前的modcount保存到一个副本expectedModCount中,然后在每次迭代时都去判断expectedModCount与modcount是否不一致了,如果是则代表在遍历的过程中有其他线程执行了put操作,此时将抛出ConcurrentModificationException。
(2)1.8前后的hashmap
1.8之前使用数组加链表实现
1.8之后使用数组加红黑树实现
hashmap中的loadFactor的取值大小对hashmap有什么影响?
答:
loadFactor是hashmap的扩容因子。
扩容因子取值越大则代表hash表内的空间利用率越高,扩容的次数会减少,但是正因为hash中的元素变多了,导致hash碰撞的概率增加,这会影响查询的效率。
扩容因子取值越小则会使得hash表内的空间利用率降低,扩容的次数会增多,但是由于hash中的元素变少了,使得hash碰撞的概率变小,可以提高查询的效率。
hashmap中put一万个元素应该怎么做?
答:
首先应该评估一万个元素是否会OOM,不会的话再考虑如何put,否则服务器就挂了。
如果想要用多线程put一万个元素,要求线程安全和效率兼顾的话不应使用hashmap或hashtable,而要用concurrenthashmap。
然后是重点,由于hashmap存在resize操作,resize操作是很消耗资源的,为了避免在put一万个元素的过程中不进行resize,需要将hashmap的初始容量设置为比10000大的最小的2的n次幂。(要求2的n次幂是因为这能够加速计算hash的过程,不过hashmap内部对容量进行了控制,可以直接将容量设置为10000,然后hashmap会自动将容量计算成距离10000最近的2的n次幂)
另外,还要考虑扩容因子的取值。
hashmap、hashtable、concurrenthashmap的区别?(附加:concurrenthashmap的实现原理)?
答:
(1)区别
hashtable是hashmap的线程安全版,在会引起线程安全问题的操作上都增加了synchronized关键字,不过这样也牺牲了性能,在并发环境下很低效。
因为当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。
concurrenthashmap也是hashmap的线程安全版,与hashtable的区别是concurrenthashmap在保证线程安全的同时很好的兼顾了性能。
(2)concurrenthashmap原理:
concurrenthashmap使用一个数组存放2的N次方个segement,而segement的结构其实和hashmap一样,因此concurrenthashmap相当于一个二级哈希表,其内保存着若干个子哈希表。
与hashtable不同,并发环境下concurrenthashmap仅需要锁住部分的segement,而不需要锁住所有数据。
HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
不过concurrenthashmap这样的实现方式也引来新的难题,既然是分段锁,那么如何求size,难道还是要锁全部?
ConcurrentHashMap的Size方法是一个嵌套循环,先尝试利用乐观锁解决问题,如果乐观锁无法解决,那么再采用悲观锁,大体逻辑如下:
1.遍历所有的Segment。
2.把Segment的元素数量累加起来。
3.把Segment的修改次数累加起来。
4.判断所有Segment的总修改次数是否大于上一次的总修改次数。如果大于,说明统计过程中有修改,重新统计,尝试次数+1;如果不是。说明没有修改,统计结束。
5.如果尝试次数超过阈值,则对每一个Segment加锁,再重新统计。
6.再次判断所有Segment的总修改次数是否大于上一次的总修改次数。由于已经加锁,次数一定和上次相等。
7.释放锁,统计结束
treemap、linkedhashmap的作用?
答:(1)treemap
TreeMap可以实现map中对象的排序,默认排序规则:按照key的字典顺序来排序(升序) 当然,也可以自定义排序规则:要实现Comparator接口。
(2)linkedhashmap
LinkedHashMap特性是可以按插入和访问顺序排序map中的对象。
hashset的实现原理?
答:
hashset基于hashmap实现,仅使用了hashmap的key值来存放数据,而hashmap的value则全部都存放一个特殊的值 Object PRESENT = new Object()。
arraylist、linkedlist、vector的区别?
答:
vector是线程安全版的arraylist。
linkedlist是链表,其内的元素在内存中不是连续存放的。
arraylist的查询性能极快,通过下标访问的话O(1),在两头插入元素还好,但是在中间插入元素的性能很差,要进行很多交换操作。
linkedlist的插入复杂度为O(1),但是查找的性能为O(n)。
如何判断一个对象是否可被gc?
答:
利用可达性判断:通过从gc-roots开始来进行扫描标记,如果一个对象到所有gc-roots之间都没有通路的话,代表该对象可被gc。
gc-roots的选择标准:
1.虚拟机栈(栈中的本地变量表)中引用的对象;
2.方法区中类静态属性引用的对象;
3.方法区中常量引用的对象;
4.本地方法栈中JNI(即一般说的Native方法)引用的对象。
jvm内存结构?
答:
jvm内存分为5各区域:程序计数器、虚拟机栈、本地方法栈,堆区,方法区
1.程序计数器:
当前线程所执行的字节码的行号指示器,用于实现循环以及分支。如果执行的是native方法,计数器是空值。
该区域不会发生OOM。
2.虚拟机栈:
每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。
该区域会引发stack-overflow(默认15层方法嵌套)和oom。
3.本地方法栈:
虚拟机栈起的作用一样,只不过方法栈为虚拟机使用到的Native方法服务。虚拟机规范并没有对这个区域有什么强制规定,因此在HotSpot虚拟机就干脆没有这块区域了,它和虚拟机栈是一起的。
4.堆区:
在虚拟机启动时创建,此内存唯一的目的就是存放对象实例。
堆还可以细分为新生代和老年代,新生代细致一点还分为Eden区、From Survivior区、To Survivor。
该区域会引发oom,也是jvm中gc最频繁的重点区域。
对于该区域,jvm采用分代收集策略进行gc。
5.方法区:
用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,虚拟机规范是把这块区域描述为堆的一个逻辑部分的,但实际它应该是要和堆区分开的。
HotSpot中,方法区≈永久代。
5.1运行时常量池:
运行时常量池是方法区的一部分。Class文件中除了有类的版本信息、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译期间生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中,另外翻译出来的直接引用也会存储在这个区域中。这个区域另外一个特点就是动态性,Java并不要求常量就一定要在编译期间才能产生,运行期间也可以在这个区域放入新的内容,String.intern()方法就是这个特性的应用。
ps:直接内存
想想还是把这块加上。直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致内存溢出问题。JDK1.4中新增加了NIO,引入了一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括RAM、SWAP区)大小以及处理器寻址空间的限制。
jvm的gc原理,每个内存区域使用什么gc算法?
答:
(1)新生代——minor gc
新生代采用标记-复制算法,eden区和survivior区的大小比例为8:1,当经过15次minor gc后仍然存活在survivior中的对象将被转移到老年代。15次这个阈值可以修改。
(2)老年代——full gc
老年代和永久代采用的是“标记-清除-压缩(Mark-Sweep-Compact)”。标记的过程是找出当前还存活的对象,并进行标记;清除则遍历整个内存区域,找出其中需要进行回收的区域;而压缩则把存活对象的内存移动到整个内存区域的一端,使得另一端是一块连续的空闲区域,方便进行内存分配和复制。
java的new关键字在jvm层面都做了哪些事?
1、虚拟机遇到一条new指令,首先去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。如果没有,那么必须先执行类的初始化过程。
2、类加载检查通过后,虚拟机为新生对象分配内存。对象所需内存大小在类加载完成后便可以完全确定,为对象分配空间无非就是从Java堆中划分出一块确定大小的内存而已。这个地方会有两个问题:
(1)如果内存是规整的,那么虚拟机将采用的是指针碰撞法来为对象分配内存。意思是所有用过的内存在一边,空闲的内存在另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离罢了。如果垃圾收集器选择的是Serial、ParNew这种基于压缩算法的,虚拟机采用这种分配方式。
(2)如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虚拟机将采用的是空闲列表法来为对象分配内存。意思是虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。如果垃圾收集器选择的是CMS这种基于标记-清除算法的,虚拟机采用这种分配方式。
另外一个问题及时保证new对象时候的线程安全性。因为可能出现虚拟机正在给对象A分配内存,指针还没有来得及修改,对象B又同时使用了原来的指针来分配内存的情况。虚拟机采用了CAS配上失败重试的方式保证更新更新操作的原子性和TLAB两种方式来解决这个问题。
3、内存分配结束,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。这一步保证了对象的实例字段在Java代码中可以不用赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值。
4、对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息存放在对象的对象头中。
5、执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
知道哪些jvm垃圾收集器?
答:
(1)新生代收集器:
Serial:
单线程收集器,使用停止复制算法,使用一个线程进行GC,其它工作线程暂停。
ParNew:
多线程并行收集器,使用停止复制算法,Serial收集器的多线程版,用多个线程进行GC,其它工作线程暂停,关注缩短垃圾收集时间。
Parallel Scavenge:
多线程并行:使用停止复制算法,关注CPU吞吐量,即运行用户代码的时间/总时间,比如:JVM运行100分钟,其中运行用户代码99分钟,垃圾收集1分钟,则吞吐量是99%,这种收集器能最高效率的利用CPU,适合运行后台运算,适合用户交互,提高用户体验。
(2)老年代收集器:
Serial Old:
单线程收集器,使用标记整理算法,使用单线程进行GC,其它工作线程暂停,jdk1.5之前与ParallelScavenge搭配使用。
Parallel Old:
多线程并行收集器,使用标记整理(与Serial Old不同,这里的整理是Summary(汇总)和Compact(压缩),汇总的意思就是将幸存的对象复制到预先准备好的区域,而不是像Sweep(清理)那样清理废弃的对象)算法,在Parallel Old执行时,仍然需要暂停其它线程。Parallel Old在多核计算中很有用。Parallel Old出现后(JDK1.6),与Parallel Scavenge配合有很好的效果,充分体现Parallel Scavenge收集器吞吐量优先的效果。
CMS:
多线程并发收集器,致力于获取最短回收停顿时间,使用标记清除算法,多线程,优点是并发收集(用户线程可以和GC线程同时工作),停顿小。
(3)G1收集器:
jdk1.7中提出的新收集器。
jvm是如何实现一次编译到处运行的?
答:
jvm体系有很多门语言,不止java一个,比如scala,kotlin。
这些jvm体系的语言有一个共同的特点就是平台无关性。
这是由于jvm屏蔽了平台间的兼容问题,而所有的jvm系语言最后都将被编译为class文件。
jvm负责解析class文件,然后利用本地操作系统来执行相关代码。
所以jvm能够做到一次编译到处运行。
java内存模型?(注意与jvm内存结构不是一回事)
答:
每个线程有自己的工作内存(映射到物理设备上就是高速缓存)
线程工作时,首先从主内存中读取数据到自己的工作内存中,执行完运算后将数据写入自己的工作内存中,而不是直接与主内存交互。
也因此线程中的数据同步到主内存是需要一定的时间的,这会造成线程安全问题。
十个线程同时++i会出现什么问题?为什么?如何解决?
答:
问题:
达不到预期结果,比预期结果值小。
原因:
和java内存模型有关,也和++i操作并非原子操作也有关。
++i被编译成字节码后其实分为四个步骤执行
1.getstatic //从主内存中读取i的值
2.iconst_1 //定义常量1
3.iadd //i增加1
4.putstatic //把count结果同步到主内存
这将导致多个线程可能同时读到相同的i值,然后重复的执行了+1的操作,导致i值只被增加了1次。
解决方法:
用AutomicInteger的自增方法来代替++i
或者用synchronized关键字
不能用volatile,因为volatile只能解决指令重排和可见性的问题,不能保证原子性。
automicinteger的自增实现原理?
答:
代码:
public final int getAndIncrement() {
for (;;) {
//先取出AtomicInteger的当前值
int current = get();
//对当前值加1操作
int next = current + 1;
//这里很关键,通过compareAndSet方法比较当前值有没有被其它线程修改过,若修改过返回false则再次进入compareAndSet方法判断
if (compareAndSet(current, next))
return current;
}
}
其中compareAndSet方法里面是调用了Unsafe类的compareAndSwapInt方法
如何在运行时估算一个java对象的大小?
java对象结构组成:对象头Header + 实例数据Instance Data + 对齐填充Padding
对象头:
对象头在32位机上占8bytes,64位机上占16bytes。
实例数据:
计算实例数据所占空间大小,则可利用反射拿到java对象中所有变量,计算每个变量占用的内存,然后累加,如果变量类型是个数组则需循环累加。
同样32位机上和64位机上的计算有所差异,因为引用Reference类型的变量在32位机上为4bytes,在64位机上则是8bytes。
对齐填充:
要保证(对象头 + 实例数据 + padding) % 8 == 0 且 0 <= padding < 8。
指针压缩:
jvm的指针压缩将会影响java对象的大小,开启指针压缩后header变为12bytes,引用类型变量变为4bytes。
有哪些可以监控jvm内存使用情况的工具?这些工具的实现原理?
答:
1.jps
与unix上的ps类似,用来显示本地的java进程,可以查看本地运行着几个java程序,并显示他们的进程号。
2.jstat
一个极强的监视VM内存工具。可以用来监视VM内存内的各种堆和非堆的大小及其内存使用量。
3.jmap
打印出某个java进程(使用pid)内存内的,所有‘对象’的情况(如:产生那些对象,及其数量)。
4.jconsole
一个java GUI监视工具,可以以图表化的形式显示各种数据。并可通过远程连接监视远程的服务器VM。
这些工具的原理是利用jmx(Java Management Extensions,即Java管理扩展)机制实现。
线程和进程的区别?
答:
进程:
包括切换上下文的程序执行时间总和:
进程 = CPU加载上下文 + CPU执行 + CPU保存上下文
线程:
CPU执行进程时是分成不同的小段执行的,比如一个文本编辑器程序,用户编辑文字时,程序需要并发的执行文字的自动保存,以及GUI的刷新。其中自动保存和GUI刷新就是2个线程,也就是说线程是进程实际的执行。
从上面的公式可以看出来,在同一个进程的不同线程执行时,上下文是没有切换的,因此线程是共享同一个进程的上下文的。
一个进程至少有一个线程。
java创建一个线程的几种方式?
答:
Thread、Runnable、Callable或者ExecutorService线程池也勉强算
悲观锁、乐观锁、自旋锁?
答:
悲观锁认为数据一定会被其他线程修改,因此直接锁住
乐观锁认为数据不一定会被修改,因此在执行前保存一个标记位,执行后在更新数据前比较这个标记位是否被修改了,如果被修改则代表数据已经不是最新的版本了,丢掉重来。
自旋锁则是因为线程被阻塞再唤醒的过程是耗时的,自旋锁在无法获得锁的情况下,不是将当前线程阻塞,而是让线程空转一会,在进行获取锁的尝试,这样能够提供效率。
就比如你去上厕所,发现坑位有人,但是你从厕所走回去又太远(线程阻塞),因此你选择在厕所门口转一转(自旋),等待里边的人出来。
当然也不能转太久,否则别人也这样的话厕所就站满人了(cpu耗尽),因此当发现等了很久都等不到时,需要自行离开。
独占锁、共享锁?读写锁?
答:
独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。
对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
对于Synchronized而言,当然是独享锁。
偏向锁、轻量级锁、重量级锁?
答:
这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
公平锁/非公平锁?
答:
公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
介绍下volatile与synchronized关键字的作用?
答:
volatile禁止指令重排,保证内存可见性,是轻量级的
synchronized实现并发锁,是重量级的
tomcat的实现原理?
利用java的socket编程实现http协议
序列化和反序列化?
答:
将对象转为二进制文件存储进行持久化或者进行网络通信(RPC——远程过程调用)的过程。
可以利用序列化进行java对象的深拷贝。
protostuff可以减小对象序列化后所占空间的大小。
线程有哪些状态?
答:
五种状态:新建、就绪、运行、阻塞、死亡
1.新建状态(New):新创建了一个线程对象。
2.就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。
该状态的线程位于“可运行线程池”中,变得可运行,只等待获取CPU的使用权。
即在就绪状态的进程除CPU之外,其它的运行所需资源都已全部获得。
3.运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4.阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。
直到线程进入就绪状态,才有机会转到运行状态。
5.死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
线程阻塞分为哪几种情况?
答:
三种情况:等待阻塞、同步阻塞、其他阻塞
1.等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。
进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒,
2.同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
3.其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。
当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
有哪些类型的线程池构造方法?
答:
1.Executors.newSingleThreadExecutor()创建单线程任务线程池
2.Executors.newFixedThreadPool(nThreads)创建固定数量线程线程池
3.Executors.newCachedThreadPool()创建以默认60秒为空闲时间的缓存线程池,核心线程数为0
4.Executors.newScheduledThreadPool(corePoolSize)创建可以控制执行时间的线程池
其中主要使用FixedThreadPool和CachedThreadPool:
FixedThreadPool:
FixedThreadPool中最多只有固定数目线程存在,一个线程实例请求加入FixedThreadPool时,如果该实例不存在,且没有达到线程池数目上限,则会创建一个实例,否则会先加入等待序列,当FixedThreadPool中有一个线程停止并移出线程池后,线程实例才能加入线程池,FixedThreadPool没有超时机制,适用于稳定且并发线程任务
CachedThreadPool:
一个线程实例加入CachedThreadPool时,如果该线程实例不存在CachedThreadPool,则创建一个,CachedThreadPool中线程实例默认超时时间为60s,超过这个时间,线程实例停止并被移出CachedThreadPool,适用于生存期短、异步的线程任务。
线程池有哪些重要的配置?
corePoolSize - 池中所保存的线程数,包括空闲线程。
maximumPoolSize-池中允许的最大线程数。
keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
unit - keepAliveTime 参数的时间单位。
workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute方法提交的 Runnable任务。
threadFactory - 执行程序创建新线程时使用的工厂。
handler - 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。
线程池中的线程数量达到最大值后会怎样?
答:
任务队列没有大小限制时:
1.如果线程数量<=核心线程数量,那么直接启动一个核心线程来执行任务,不会放入队列中。
2.如果线程数量>核心线程数,但<=最大线程数,并且任务队列是LinkedBlockingDeque的时候,超过核心线程数量的任务会放在任务队列中排队。
3.如果线程数量>核心线程数,但<=最大线程数,并且任务队列是SynchronousQueue的时候,线程池会创建新线程执行任务,这些任务也不会被放在任务队列中。这些线程属于非核心线程,在任务完成后,闲置时间达到了超时时间就会被清除。
4.如果线程数量>核心线程数,并且>最大线程数,当任务队列是LinkedBlockingDeque,会将超过核心线程的任务放在任务队列中排队。也就是当任务队列是LinkedBlockingDeque并且没有大小限制时,线程池的最大线程数设置是无效的,他的线程数最多不会超过核心线程数。
5.如果线程数量>核心线程数,并且>最大线程数,当任务队列是SynchronousQueue的时候,会因为线程池拒绝添加任务而抛出异常。
任务队列大小有限时:
1.当LinkedBlockingDeque塞满时,新增的任务会直接创建新线程来执行,当创建的线程数量超过最大线程数量时会抛异常。
2.SynchronousQueue没有数量限制。因为他根本不保持这些任务,而是直接交给线程池去执行。当任务数量超过最大线程数时会直接抛异常。
业务高峰过后,线程池中的线程数量多余的话会怎样?
答:
多余的线程会被释放掉,直到线程数回归corePoolSize,还可以进行配置,使得线程数归为0。
线程池容量能不能动态调整?
答:
可以。
ThreadPoolExecutor提供了动态调整线程池容量大小的方法:setCorePoolSize()和setMaximumPoolSize(),
setCorePoolSize:设置核心池大小
setMaximumPoolSize:设置线程池最大能创建的线程数目大小
当上述参数从小变大时,ThreadPoolExecutor进行线程赋值,还可能立即创建新的线程来执行任务。
2.spring:
servlet与springmvc有什么区别?
答:
直接用servlet的代码量会比使用springmvc的多很多。
也需要在web.xml中配置很多ervlet-mapping。
而springmvc使用一个公共的servlet,dispatchservlet接管所有url映射,使用spring的IOC以及注解扫描的能力,将url映射到对应的controller上。
springmvc也提供了很强大url参数解析功能,只需要在注解中配置一下就可以使用。
springboot与springmvc的比较大的区别,从使用者的角度来说?
答:
springboot整合了spring和springmvc,比起springmvc来减少了很多配置,并且不必再单独安装tomcat这些容器服务器了,springboot的项目用maven打成jar包后,直接跑起来就是一个网站,但是核心的业务开发并没有与springmvc有区别。
spring是引擎
springmvc是基于spring的一个mvc框架
springboot是基于spring4的一套快速开发整合包
springmvc中,要删除一个cookie,要用什么方式来操作?
答:
首先通过request拿到Cookies列表,遍历列表后找到要删除的cookie。
执行cookie.setValue(null);
cookie.setMaxage(0);
cookie.setPath("/");
response.addCookie(cookie);
即可删除
日常springmvc或springboot项目开发中,有采取过什么样子的方法来防止引起的安全问题?举个例子?
答:
避免CSRF跨站请求攻击,解决方法是首先判断http中header中的referer字段,查看是否来自可信的网站。
然后是在生成表单时附带一个随机数到客户端,客户端提交表单时也将这个随机数提交到服务端校验,如果为空或者不正确则代表该请求是伪造的。
spring的事务隔离级别?
答:
分5种
1.default--默认采用数据库的事务隔离级别(而mysql默认是可重复度)
2.read--uncommited读未提交(无法解决脏读、不可重复读、幻读的问题)
3.read--commited读已提交(解决脏读,无法解决不可重复读,幻读的问题)
4.repetable--read可重复读(解决脏读,不可重复读,无法解决幻读问题)
5.serializable串行化(解决脏读、不可重复读、幻读问题,但大大牺牲了性能)
spring的ioc初始化过程?
答:
定位:通过spring的配置文件或者java注解,定位所有bean类的存放位置
载入:将bean类加载到jvm,每个bean类对应一个BeanDefinition
注册:将BeanDefinition注册到一个hashmap中,即IOC容器,IOC通过这个hashmap来维护所有bean,此时bean不一定被初始化,因为可以配置init-lazy属性来控制是否懒加载
spring的ioc与aop的实现原理?
答:
ioc利用了类加载器和反射的原理获取所有的bean对象,然后用一个hashmap作为容器存放这些bean对象,之后用户的使用spring提供的依赖注入DI能力其实就是在围绕这个hashmap容器做文章。
aop则利用了aspectj的动态代理能力,代理原有的类,增加切面的逻辑。
实现动态代理的方法还有invokehandler与cglib。
invokehandler有个缺点就是被代理的类一定要实现某个接口。
cglib则要求被代理的类和方法不是final类型的,因为cglib的原理使用动态字节码生成的技术生成一个被代理类的子类来替代被代理的类。
spring cloud了解吗?
3.mybatis:
mybatis的优缺点?
答:
优点:
减少了大量代码量
sql与代码分离,降低耦合,便于维护管理
支持动态sql
支持bean对象到数据库表的映射
缺点 :
要求开发人员有sql功底
sql的编写依赖于具体的数据库,数据库移植性差
jdbc与mybatis的区别?
直接使用jdbc的工作量大,过程复杂,比如要先注册驱动和数据库,创建Connection,创建Statement执行sql,遍历resultSet读取数据转换为pojo,最后还要记得关闭数据连接。
因为jdbc实在不好用,因此出现了ORM对象映射关系对jdbc进行封装。
mybatis就是一种ORM框架,对jdbc进行良好的封装,使用SqlSessionFactoryBuilder来完成数据库的连接,将sql编写至xml文件中,使用自动映射的功能将sql的执行结果封装到对应的pojo中去,这一切都是在解耦和降低工作量。
并且使用Mybatis可以使得编码风格统一,提升了项目的可维护性。
mybatis如何实现表间关联查询?
答:
在mapper.xml文件里使用association作为一对一的关联,
使用collection作为一对多的关联。
使用resultmap配置这两个属性,然后写一个表关联的sql搞定,这种情况需要指定字段名和bean属性名的对应关系。
或者写两个sql搞定,在上文的两个属性中配置一个子sql,使用子sql的结果作为表关联的结果。
mybatis如何实现分页?
答:
直接写sql,比如mysql中使用offset和limit实现分页。
使用pagenitor和pagehelper这两个mybatis插件实现分页。
4.设计模式:
单例模式有哪些实现方式,是否线程安全?
答:
懒汉、饿汉、double-check、枚举等。
懒汉式不能保证线程安全,需要使用synchronized锁住整个getInstance方法,或者采用double-check
枚举时目前看来最优的单例实现方式,不过他是饿汉式的。
代理模式和装饰器模式的区别?
答:
这两种设计模式的实现都有点类似,
不过代理模式的重点是替代,而装饰器模式的重点是增强。
一个重要的区别是代理模式本身持有并初始化了一个原类型的对象,
而装饰器模式则是由外部将原类型的对象传入,则意味着可以不断的嵌套装饰器,使得类的功能越发强大。
工作中用过哪些设计模式?
答:
工厂、策略、建造者、模板模式
5.网络:
cookie和session的联系是什么?
答:
session是一种针对http这种无状态协议的用户身份识别解决方案。
当用户登录成功后,服务器在本地保存一份该用户的session数据用于之后的身份识别,然后使用set-cookie的方式将该session的唯一id,即session-id返回给客户端,客户端将session-id保存至本地,之后继续发起请求时,将session-id放在header中一起发送,服务端就可以利用这个标识来识别用户的身份了。
因此总的来说,cookie可以用来实现session。
session一致性问题?
答:
由于session保存在服务端,所以当服务器集群之后,就要考虑服务器之间的session共享的问题了。
因为服务器集群意味着需要进行负载均衡,如果用户在a服务器上登录后,下一次请求被转发到b服务器上了,b服务器并不认识这个用户,就会导致要重复登录的问题。
解决session一致性问题的方法:
1.客户端存储:
在cookie通过加密和混淆保存用户信息,而不是在服务端保存,这样每次用户发送请求时服务端都从cookie中解析出来用户的身份信息,就不存在session共享问题了。
缺点是信息容易暴露,并且每次请求都需要解析cookie,存在性能隐患。
2.第三方存储:
将session保存到第三方,比如一个redis服务器中,每次服务器拿到sessinoid,就去redis服务器中获取用户信息,这样也能解决问题。
3.server间的session共享:
这个需要靠server间的通信机制来实现,用户的session保存在某个服务器之后,该服务器将session发送给集群中的其他服务器进行共享。
4.一致性hash的负载均衡:
由于session一致问题其实是负载均衡导致用户的每次请求不一定落在同一台服务器上导致,那么可以在进行负载均衡时,采用一致性hash的算法,比如利用用户的id来计算hash,把hash对应到具体的服务器上,保证同一个用户的每次请求都被转发到相同的服务器,那么也就不存在session一致性问题了。
http的get与post的区别?
答:
get和post是http协议中的两种发送请求的方法。
http是什么?http是基于tcp/ip的关于数据如何在万维网中如何通信的协议。
http的底层是tcp/ip,因此get和post其实都是tcp链接,其实想要给get加上request body,或者给post带上url参数,在技术上是完全行得通的。
但是我们往往会去比较get和post方法间的区别,比如下面这些:
get在浏览器回退时是无害的,而post会再次提交请求;
get产生的url可以被作为书签,而post不可以;
get请求会被浏览器主动cache,而post不会,除非手动设置;
get请求只能进行url编码,而post支持多种编码方式;
get请求参数会被完整的保留在浏览器历史记录里,而post中的参数不会被保留;
get请求在url中传送的参数是有长度限制的,而post没有。
get比post更不安全,因为参数直接暴露在url上,所以不能用来传递敏感信息;
get中参数通过url传递,而post则将参数放在request body中。
那么既然技术实现上是没多大的差别的,为什么非要搞出这么多的区别呢?
原因:
TCP就像汽车,在网络的世界里,我们使用用TCP来运输数据,它很可靠,从来不会发生丢件少件的现象。
但是如果路上跑的全是看起来一模一样的汽车,那这个世界看起来是一团混乱,送急件的汽车可能被前面满载货物的汽车拦堵在路上,整个交通系统一定会瘫痪。
为了避免这种情况发生,交通规则HTTP诞生了。
HTTP给汽车运输设定了好几个服务类别,有GET, POST, PUT, DELETE等等,HTTP规定,当执行GET请求的时候,要给汽车贴上GET的标签(设置method为GET),
而且要求把传送的数据放在车顶上(url中)以方便记录。
如果是POST请求,就要在车上贴上POST的标签,并把货物放在车厢里。当然,你也可以在GET的时候往车厢内偷偷藏点货物,但是这是很不光彩;
也可以在POST的时候在车顶上也放一些数据,让人觉得傻乎乎的。HTTP只是个行为准则,而TCP才是GET和POST怎么实现的基本。
而关于参数大小限制的由来则是:
接着上面的例子,不同的浏览器(发起http请求)和服务器(接受http请求)就是不同的运输公司。 虽然理论上,你可以在车顶上无限的堆货物(url中无限加参数)。
但是运输公司可不傻,装货和卸货也是有很大成本的,他们会限制单次运输量来控制风险,数据量太大对浏览器和服务器都是很大负担。
业界不成文的规定是,(大多数)浏览器通常都会限制url长度在2K个字节,而(大多数)服务器最多处理64K大小的url。
超过的部分,恕不处理。如果你用GET服务,在request body偷偷藏了数据,不同服务器的处理方式也是不同的,有些服务器会帮你卸货,读出数据,有些服务器直接忽略。
所以,虽然GET可以带request body,也不能保证一定能被接收到哦。
GET和POST本质上就是TCP链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。
GET和POST还有一个重大区别,
简单的说:
GET产生一个TCP数据包;POST产生两个TCP数据包。
长的说:
对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);
而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。
也就是说,GET只需要汽车跑一趟就把货送到了,而POST得跑两趟,第一趟,先去和服务器打个招呼“嗨,我等下要送一批货来,你们打开门迎接我”,然后再回头把货送过去。
因为POST需要两步,时间上消耗的要多一点,看起来GET比POST更有效。因此Yahoo团队有推荐用GET替换POST来优化网站性能。但这是一个坑!跳入需谨慎。为什么?
1. GET与POST都有自己的语义,不能随便混用。
2. 据研究,在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。
3. 并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。
TCP的三次握手与四次挥手?
答:
三次握手:
为什么是三次握手?而不是2次,或者4次5次?
是因为三次握手是双方建立稳定通信链接的最小确认次数。
比如我们和朋友打电话的时候,往往会出现这样的对话:
A:“hello 能听见吗?”
B:“hi,我能听见你说话噢,你能听见我说话吗?”
A:“能听见,那么我们开始说正事吧...”
光看这个例子可能会人觉得是不是有毛病?电话接通就开始说正事啊,废话那么多干嘛,还来来回回的确认对方听不听得见?
其实我们考虑这样的情况,假设A给B打电话,B刚接起电话,A就blabla的说了5分钟,然后问b听明白了吗,结果发现b那边早就挂断了。
又或者,A问B能听见吗之后,B回复A能听见,这个时候假设B不去等待A的第三次确认,那么就可能出现B能听见A说话,而A不能听见B说话的问题。
这就是不进行双方连接确定的后果。
而如果进行三次以上的确认的话,又显得太罗嗦了,比如:
A:“hello 能听见吗?”
B:“hi,我能听见你说话噢,你能听见我说话吗?”
A:“能听见,你真的能听见我说话吗”
B:“你特么有毛病吧,刚才不是说了能听见吗....”
四次挥手:
为什么断开连接需要是四次的确认?
这是因为tcp连接是双工的,双工的意思就是连接的两边都可以进行信息的接受和发送,即同时为接收者和发送者。
既然是双工的,那么就意味着当某一方的数据发送完毕时,不能直接关闭链路,而是需要等待另一方的数据也确认发送完毕了,此时才能彻底断开连接。
因此,tcp使用四次挥手来确认链路的两边都完成了数据的传输,一个示例的过程如下:
A:“我这边数据发送完了,我先关闭我这个方向的数据传输了,不过我还能接收你的信息,你赶紧的啊”
B:“ok,了解了,我这边还有点数据没发完,你等一会”
(此时A、B间的数据流向就变成单向的了,因为A已经只接收数据而不再传输数据了)
(过了一段时间后)
B:“我这边的数据也发送完毕了,我们可以结束这次连接了”
A:“晓得了,下次再见”
TCP滑动窗口、拥塞控制、慢启动?
答:
使用这些机制的原因:
从使用者的角度来看,数据传输当然时每次传输的越多越好,传输的越快越好,巴不得一次性就全部传输完毕。
但是对于tcp协议来说,它需要考虑更复杂的问题,比如当网络连接不畅通的时候,发送数据包越快,就意味着丢失的数据包就越多,所以数据传输不能一味的追求快速。
另一方面,服务器处理数据包的能力是有限的,如果每次发来的数据包都特别大,那缓冲区会被顶满,服务器也可能不堪重负,导致数据丢失,因此数据传输也不能一味的追求大量。
滑动窗口:
为了解决服务器处理数据包的性能问题,tcp采取了滑动窗口机制,当服务器发现自己的数据处理速度较慢时,可以在以此数据接收确认包中,告诉数据发送方一个缩小后的窗口值。
发送方接到包后,发现窗口值变小了,就明白接收端的处理能力有限,之后发送的数据量就根据本次传来的窗口值大小来确定。
等什么时候接收端处理速度上来了,会再通知发送方一个新的更大一点的窗口值,那么发送方又可以一次发送更多的数据了。
而由于tcp是双向的,因此双方都需要窗口,而且很有可能双方的窗口大小不一致,因为双方的接收和发送的情况是不一样的。
拥塞控制、慢启动:
服务器发送数据包,当然是越快越好,但是发得太快,就有可能丢包。带宽小、路由器过热、缓存溢出等许多情况都会导致丢包。线路不好的话,发得越快,丢得越多。
最理想的状态是,在线路允许的情况下,达到最高速率。但是怎么知道对方线路的理想速率是多少呢?答案就是慢慢试。
tcp协议为了做到效率与可靠性的统一,设计了一个慢启动机制。开始的时候,发送得比较慢,然后根据丢包的情况,调整速率,如果不丢包,就逐步加快发送速度。
如果丢包,就降低发送速度。
TCP连接的本质是什么?
答:
确认对方是否存在的通信机制。
IP路由过程中,上一个包刚被当前路由器转发后,当前路由器挂了,之后会发生什么?TCP链路断了吗?
答:
tcp链路没断,因为tcp是ip协议的上层,tcp感知不到ip层到底是通过那一条路由线路来传输数据的。
当某一个路由器挂了之后,该路由器所在路由路线的相关路由节点将会更新自己的本地路由表,然后会将后来的数据包转发到其他正常工作的路由器上去,因此不影响整体的数据传输。
6.数据库:
mysql的事务隔离级别?
答:
read-uncommited 读未提交
read-commited 读已提交
repeatable-read 可重复读
serializable 串行化
mysql的索引实现原理?
答:
b+树
可以延申地去聊hash、二叉搜索树、b树的相关知识
mysql中如何利用sql进行分页查询?
答:
使用offset和limit
会写mysql存储过程吗?
答:
利用特定的语法比如定义变量、判断、循环等将多个sql组织起来实现复杂的业务逻辑
oracle中的over partition by(分析聚合函数)会用吗?
答:
可以用分析聚合函数来方便的求解每个班的前三名这种问题
join、left join、right join的区别?
答:
join只返回左右两表中都存在的数据行
left join返回左表的所有数据行,无论是否存在于右表
right join返回右表的所有数据行,无论是否存在与左表
介绍下数据库分库与分表?
答:
可以对某个特别庞大的业务表进行分表。
例如,一张上亿的用户表,可以利用用户的所在省份进行表拆分,
拆出来的每个子表只保存一个具体身份的用户数据,该子表的结构与用户主表一致。
然后采用一个路由表,进行路由,路由表中保存每个省份对于的子表表名,查询时需要依赖这个路由表进行。
分库则是数据库变得庞大之后,一个机器装不下了,于是需要进行分布式的存储。
mysql和oracle目前都支持主从表分布式存储模式,不过引入分布式就以为着要面对CAP问题,即一致性、可用性、分区性这三者间的取舍。
Oracle如何使用sql分页?
答:
SELECT * FROM
(
SELECT A.*, ROWNUM RN
FROM (SELECT * FROM TABLE_NAME) A
WHERE ROWNUM <= 40
)
WHERE RN >= 21
oracle表分区了解吗?
有没有sql优化的经验?有哪些sql优化的技巧?
7.redis:
redis中有多少种数据结构?
答:
主要是五种:string、hash、list、set、sortedlist
其实Pub/Sub应该也算一种,不过目前还没有使用过,不是很清楚
redis是单线程的吗?
答:
是的,redis使用c语言实现,是单线程的。
注意:单线程只是在描述redis处理网络请求时只用一个线程,而redis运行期间并不是只有一个线程的,比如在执行持久化的时候redis就会以子进程或者子线程的方式执行。
为什么redis很快?
答:
Redis采用的是基于内存的采用的是单进程单线程模型的 KV 数据库,由C语言编写,官方提供的数据是可以达到100000+的QPS(每秒内查询次数)。
这个数据不比采用单进程多线程的同样基于内存的 KV 数据库 Memcached 差。
具体原因:
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
4、使用多路I/O复用模型,非阻塞IO,采用多路 I/O复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且Redis在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈;
5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。
但是使用单线程的方式是无法发挥多核CPU性能,不过可以通过在单机开多个Redis实例来完善。
redis的RDB与AOF两种持久化机制的区别?
答:
RDB保存数据快照,是完整的数据,但是io操作较大,比较影响性能,数据量大时可能会造成reids服务暂时停止。
AOF是不断的记录所有客户端的操作,当重启redis时,还原这些操作已达到还原数据的效果。
RDB的保存频率较低,而且是触发式的,比如300s内发生10次以上的key被修改,则进行保存,这导致redis挂掉后容易丢失好几分钟内的数据,但是RDB在恢复大数据集时的速度很快。
AOF的保存频率较高,默认是每秒记录一次,这使得redis挂掉也只会丢失1s的数据。但是AOF文件往往大于RDB的体积,因为AOF不仅保存数据还保存操作。redis会在AOF文件体积过大时自动在后台进行AOF的重写,将一些可以合并的操作进行压缩。
如何利用redis实现分布式锁?
答:
setnx操作加一个expire失效时间
如何利用redis实现客户端恶意脚本批量请求的防攻击检测?
答:
利用incr记录一个计数器,key是用户id,value是用户访问次数初始为0,key的失效时间为1s,用户的每次访问都使该计数器值加1,当发现1s内计数器值超过一定的阈值比如10次,那么就执行logout操作,将用户重定向到登录页面。
如何利用一致性hash实现redis集群?
答:
redis3.0是支持Cluster主从模式的,不过利用一致性hash连接多个redis实例也可以实现。
比如创建3个redis实例,每个实例中都保存完整的数据。
用户发来请求时,通过一致性hash将用户定位到某个redis上去请求数据,这样就能利用多个redis来提速了,否则即使redis很快,但当请求数多的时候仍然需要一定的排队时间。
如果某个redis节点坏掉了,一致性hash算法可以保证将用户定位下一个正常的redis上去。
如何用redis实现消息队列?
答:
使用list的相关指令实现,比如lpush将消息存入队列,rpop将消息取出,或者brpop阻塞等待直到由消息可以取出。
redis缓存击穿怎么办?
答:
要在代码里保证缓存读取失败后,进行数据库查询,避免缓存挂掉就直接使得业务挂掉。
只是这种情况将可能会遇到数据库的性能瓶颈。
redis与memcached的区别?
答:
redis支持的数据结构和操作比memcached丰富
memcached只支持键值对
redis是单线程处理请求
memcached可以使用多核,因此在存储小数据上时redis快,但是存储100k以上的大数据时memcached性能高。
redis从哪个版本开始支持集群?
答:
3.0版本推出了redis的cluster功能
redis集群怎么部署?
答:
可以使用redis3.0以上版本提供的cluster模式
也可以自行部署多个redis实例,然后通过一致性hash进行负载均衡
8.web安全:
防止sql注入的方法是什么?
答:
sql预编译
sql预编译为什么可以防范注入?
答:
数据库进行sql预编译后,sql被编译为具体的执行计划,此时sql已经与参数分离,后续传来的所有参数都只被当作数据处理而不会被当成时sql的一部分,因此避免了sql注入。
DDOS(分布式拒绝服务攻击)?
CSRF(跨站点请求伪造攻击)?
XSS(跨站脚本攻击)?
SYN FLOOD 利用TCP第二次握手的原理进行ddos攻击?
NTP FLOOD 利用网络时间请求服务进行反射攻击?
9.算法与数据结构:
一致性哈希算法的实现原理?
一致性hash与普通hash有什么区别?
如何判断一个单链表是否有环?如何找到环的入口?
如何判断两个单链表是否相交?
如何求一颗平衡二叉树的高度和宽度?
答
深度:
使用递归,分别求出左子树的深度、右子树的深度,两个深度的较大值+1即可。
// 获取最大深度
public static int getMaxDepth(TreeNode root) {
if (root == null)
return 0;
else {
int left = getMaxDepth(root.left);
int right = getMaxDepth(root.right);
return 1 + Math.max(left, right);
}
}
宽度:
使用队列,层次遍历二叉树。在上一层遍历完成后,下一层的所有节点已经放到队列中,此时队列中的元素个数就是下一层的宽度。以此类推,依次遍历下一层即可求出二叉树的最大宽度。
// 获取最大宽度
public static int getMaxWidth(TreeNode root) {
if (root == null)
return 0;
Queue<TreeNode> queue = new ArrayDeque<TreeNode>();
int maxWitdth = 1; // 最大宽度
queue.add(root); // 入队
while (true) {
int len = queue.size(); // 当前层的节点个数
if (len == 0)
break;
while (len > 0) {// 如果当前层,还有节点
TreeNode t = queue.poll();
len--;
if (t.left != null)
queue.add(t.left); // 下一层节点入队
if (t.right != null)
queue.add(t.right);// 下一层节点入队
}
maxWitdth = Math.max(maxWitdth, queue.size());
}
return maxWitdth;
}
如何实现一个O(1)时间求最小值的栈?
两个栈实现一个队列?
两个队列实现一个栈?
bitmap(位图索引)?如何利用bitmap实现快速数据查询?
希尔、快排、归并、堆排?
如何镜像翻转一颗二叉树?
答:
(1)递归翻转每一颗子树
def invertTree(root):
if root is None:
return None
root.left, root.right = invertTree(root.right), invertTree(root.left)
return root
(2)重新构造出一颗左右节点倒换的新树:
def invert(node):
if node is None:
return None
else
return Tree(node.value, invert(node.right), invert(node.left))
如何求最长连续递增子序列?
如何求报数问题?(n个人围坐一圈报数,凡是报到m的人出队,问最后剩下的人的序号)
10.javascript:
javascript中的浅拷贝与深拷贝?
javascript的闭包是什么?
javascript的作用域、作用域链、执行环境、上下文这些概念的区别与联系?
javascript的原型链?
javascript中有几种遍历数组的方法?
javascript中如何删除数组中指定下标的元素?
javascript是单线程的吗?
javascript如何实现异步(比如setTimeout)?
11.vue:
vue可以用来做什么?
vue双向绑定的实现原理?
了解vue的virtual dom吗?
介绍下vue的生命周期?
介绍下vue-router?
vue的子路由嵌套?
vue的如何watch一个对象中的属性值的变动?
vue如何使得数组中的某一个对象的属性值变更时触发双向绑定?