-
Notifications
You must be signed in to change notification settings - Fork 357
/
Java面试题集.md
872 lines (639 loc) · 40.9 KB
/
Java面试题集.md
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
## Java
### 谈谈对Java多态的理解?
> 多态是指父类的某个方法被子类重写时,可以产生自己的功能行为,同一个操作作用于不同对象,可以有不同的解释,产生不同的执行结果。
多态的三个必要条件:
1. 继承父类。
2. 重写父类的方法。
3. 父类的引用指向子类对象。
### 静态方法与静态成员变量可以被继承吗,为什么?
静态方法与静态成员变量可以被继承,但是不能被重写。它对子类隐藏,因此静态方法也不能实现多态。
### 为什么Java里的匿名内部类只能访问final修饰的外部变量?
匿名内部类用法
```java
public class TryUsingAnonymousClass {
public void useMyInterface() {
final Integer number = 123;
System.out.println(number);
MyInterface myInterface = new MyInterface() {
@Override
public void doSomething() {
System.out.println(number);
}
};
myInterface.doSomething();
System.out.println(number);
}
}
```
编译后的结果
```java
class TryUsingAnonymousClass$1
implements MyInterface {
private final TryUsingAnonymousClass this$0;
private final Integer paramInteger;
TryUsingAnonymousClass$1(TryUsingAnonymousClass this$0, Integer paramInteger) {
this.this$0 = this$0;
this.paramInteger = paramInteger;
}
public void doSomething() {
System.out.println(this.paramInteger);
}
}
```
因为匿名内部类最终用会编译成一个单独的类,而被该类使用的变量会以构造函数参数的形式传递给该类,例如:Integer paramInteger,如果变量
不定义成final的,paramInteger在匿名内部类被可以被修改,进而造成和外部的paramInteger不一致的问题,为了避免这种不一致的情况,因为Java
规定匿名内部类只能访问final修饰的外部变量。
### 讲一下Java的编码方式?
为什么需要编码
> 计算机存储信息的最小单元是一个字节即8bit,所以能表示的范围是0~255,这个范围无法保存所有的字符,所以需要一个新的数据结构char来表示这些字符,从char到byte需要编码。
常见的编码方式有以下几种:
- ASCII:总共有 128 个,用一个字节的低 7 位表示,0~31 是控制字符如换行回车删除等;32~126 是打印字符,可以通过键盘输入并且能够显示出来。
- GBK:码范围是 8140~FEFE(去掉 XX7F)总共有 23940 个码位,它能表示 21003 个汉字,它的编码是和 GB2312 兼容的,也就是说用 GB2312 编码的汉字可以用 GBK 来解码,并且不会有乱码。
- UTF-16:UTF-16 具体定义了 Unicode 字符在计算机中存取方法。UTF-16 用两个字节来表示 Unicode 转化格式,这个是定长的表示方法,不论什么字符都可以用两个字节表示,两个字节是 16 个 bit,所以叫 UTF-16。UTF-16 表示字符非常方便,每两个字节表示一个字符,这个在字符串操作时就大大简化了操作,这也是 Java 以 UTF-16 作为内存的字符存储格式的一个很重要的原因。
- UTF-8:统一采用两个字节表示一个字符,虽然在表示上非常简单方便,但是也有其缺点,有很大一部分字符用一个字节就可以表示的现在要两个字节表示,存储空间放大了一倍,在现在的网络带宽还非常有限的今天,这样会增大网络传输的流量,而且也没必要。而 UTF-8 采用了一种变长技术,每个编码区域有不同的字码长度。不同类型的字符可以是由 1~6 个字节组成。
Java中需要编码的地方一般都在字符到字节的转换上,这个一般包括磁盘IO和网络IO。
> Reader 类是 Java 的 I/O 中读字符的父类,而 InputStream 类是读字节的父类,InputStreamReader 类就是关联字节到字符的桥梁,它负责在 I/O 过程中处理读取字节到字符的转换,而具体字节到字符的解码实现它由 StreamDecoder 去实现,在 StreamDecoder 解码过程中必须由用户指定 Charset 编码格式。
### 静态代理与动态代理区别是什么,分别用在什么样的场景里?
静态代理与动态代理的区别在于代理类生成的时间不同,如果需要对多个类进行代理,并且代理的功能都是一样的,用静态代理重复编写代理类就非常的麻烦,可以用动态代理动态的生成代理类。
```java
// 为目标对象生成代理对象
public Object getProxyInstance() {
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("开启事务");
// 执行目标对象方法
Object returnValue = method.invoke(target, args);
System.out.println("提交事务");
return null;
}
});
}
```
### 描述一下Java的异常体系?
1. Error是程序无法处理的错误,比如OutOfMemoryError、ThreadDeath等。这些异常发生时, Java虚拟机(JVM)一般会选择线程终止。
2. Exception是程序本身可以处理的异常,这种异常分两大类运行时异常和非运行时异常,程序中应当尽可能去处理这些异常。运行时异常都是RuntimeException类及其子类异常,如NullPointerException、IndexOutOfBoundsException等,
这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的, 程序应该从逻辑角度尽可能避免这类异常的发生。
### 描述一个类的加载过程?
Person person = new Person()
1. 查找Person.class,并加载到内存中。
2. 执行类里的静态代码块。
3. 在堆内存里开辟内存空间,并分配内存地址。
4. 在堆内存里建立对象的属性,并进行默认的初始化。
5. 对属性进行显示初始化。
6. 对对象进行构造代码块初始化。
7. 调用对象的构造函数进行初始化。
8. 将对象的地址赋值给person变量。
### Java对象的生命周期是什么?
1. 加载:将类的信息加载到JVM的方法区,然后在堆区中实例化一个java.lang.Class对象,作为方法去中这个类的信息入口。
2. 连接:验证:验证类是否合法。准备:为静态变量分配内存并设置JVM默认值,非静态变量不会分配内存。解析:将常量池里的符号引用转换为直接引用。
3. 初始化:初始化类的静态赋值语句和静态代码块,主动引用会被触发类的初始化,被动引用不会触发类的初始化。
4. 使用:执行类的初始化,主动引用会被触发类的初始化,被动引用不会触发类的初始化。
5. 卸载:卸载过程就是清楚堆里类的信息,以下情况会被卸载:① 类的所有实例都已经被回收。② 类的ClassLoader被回收。③ 类的CLass对象没有被任何地方引用,无法在任何地方通过
反射访问该类。
### 描述一下类的加载机制?
> 类的加载就是虚拟机通过一个类的全限定名来获取描述此类的二进制字节流,而完成这个加载动作的就是类加载器。
类和类加载器息息相关,判定两个类是否相等,只有在这两个类被同一个类加载器加载的情况下才有意义,否则即便是两个类来自同一个Class文件,被不同类加载器加载,它们也是不相等的。
注:这里的相等性保函Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果以及Instance关键字对对象所属关系的判定结果等。
类加载器可以分为三类:
- 启动类加载器(Bootstrap ClassLoader):负责加载<JAVA_HOME>\lib目录下或者被-Xbootclasspath参数所指定的路径的,并且是被虚拟机所识别的库到内存中。
- 扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录下或者被java.ext.dirs系统变量所指定的路径的所有类库到内存中。
- 应用类加载器(Application ClassLoader):负责加载用户类路径上的指定类库,如果应用程序中没有实现自己的类加载器,一般就是这个类加载器去加载应用程序中的类库。
这么多类加载器,那么当类在加载的时候会使用哪个加载器呢?🤔
这个时候就要提到类加载器的双亲委派模型,流程图如下所示:
<img src="https://github.com/guoxiaoxing/android-open-source-project-analysis/raw/master/art/native/vm/classloader_model_structure.png" width="600"/>
双亲委派模型的整个工作流程非常的简单,如下所示:
> 如果一个类加载器收到了加载类的请求,它不会自己立即去加载类,它会先去请求父类加载器,每个层次的类加载器都是如此。层层传递,直到传递到最高层的类加载器,只有当
父类加载器反馈自己无法加载这个类,才会有当前子类加载器去加载该类。
关于双亲委派机制,在ClassLoader源码里也可以看出,如下所示:
```java
public abstract class ClassLoader {
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//首先,检查该类是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//先调用父类加载器去加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
//如果父类加载器没有加载到该类,则自己去执行加载
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
}
}
return c;
}
}
```
为什么要这么做呢?🤔
这是为了要让越基础的类由越高层的类加载器加载,例如Object类,无论哪个类加载器去尝试加载这个类,最终都会传递给最高层的类加载器去加载,前面我们也说过,类的相等性是由
类与其类加载器共同判定的,这样Object类无论在何种类加载器环境下都是同一个类。
相反如果没有双亲委派模型,那么每个类加载器都会去加载Object,那么系统中就会出现多个不同的Object类了,如此一来系统的最基础的行为也就无法保证了。
### 描述一下GC的原理和回收策略?
提到垃圾回收,我们可以先思考一下,如果我们去做垃圾回收需要解决哪些问题? 🤔
一般说来,我们要解决一些三个问题:
- 哪些内存回收?
- 什么时候回收?
- 如何回收?
这些问题分别对应着引用管理和回收策略等方案。
提到引用,我们都知道Java中有四种引用类型:
- 强引用:代码中普遍存在的,只要强引用还存在,垃圾收集器就不会回收掉被引用的对象。
- 软引用:SoftReference,用来描述还有用但是非必须的对象,当内存不足的时候回回收这类对象。
- 弱引用:WeakReference,用来描述非必须对象,弱引用的对象只能生存到下一次GC发生时,当GC发生时,无论内存是否足够,都会回收该对象。
- 虚引用:PhantomReference,一个对象是否有虚引用的存在,完全不会对其生存时间产生影响,也无法通过虚引用取得一个对象的引用,它存在的唯一目的是在这个对象被回收时可以收到一个系统通知。
不同的引用类型,在做GC时会区别对待,我们平时生成的Java对象,默认都是强引用,也就是说只要强引用还在,GC就不会回收,那么如何判断强引用是否存在呢?🤔
一个简单的思路就是:引用计数法,有对这个对象的引用就+1,不再引用就-1,但是这种方式看起来简单美好,但它却不嫩解决循环引用计数的问题。
因此可达性分析算法登上历史舞台😎,用它来判断对象的引用是否存在。
> 可达性分析算法通过一系列称为GC Roots的对象作为起始点,从这些节点从上向下搜索,搜索走过的路径称为引用链,当一个对象没有任何引用链
与GC Roots连接时就说明此对象不可用,也就是对象不可达。
GC Roots对象通常包括:
- 虚拟机栈中引用的对象(栈帧中的本地变量表)
- 方法去中类的静态属性引用的对象
- 方法区中常量引用的对象
- Native方法引用的对象
可达性分析算法整个流程如下所示:
1. 第一次标记:对象在经过可达性分析后发现没有与GC Roots有引用链,则进行第一次标记并进行一次筛选,筛选条件是:该对象是否有必要执行finalize()方法。没有覆盖finalize()方法或者finalize()方法已经被执行过都会被
认为**没有必要执行**。
- 如果有必要执行:则该对象会被放在一个F-Queue队列,并稍后在由虚拟机建立的低优先级Finalizer线程中触发该对象的finalize()方法,但不保证一定等待它执行结束,因为如果这个对象的finalize()方法发生了死循环或者执行
时间较长的情况,会阻塞F-Queue队列里的其他对象,影响GC。
2. 第二次标记:GC对F-Queue队列里的对象进行第二次标记,如果在第二次标记时该对象又成功被引用,则会被移除即将回收的集合,否则会被回收。
### 接口和抽象类有什么区别?
共同点
1. 是上层的抽象层。
2. 都不能被实例化。
3. 都能包含抽象的方法,这些抽象的方法用于描述类具备的功能,但是不比提供具体的实现。
区别
1. 在抽象类中可以写非抽象的方法,从而避免在子类中重复书写他们,这样可以提高代码的复用性,这是抽象类的优势,接口中只能有抽象的方法。
2. 一个类只能继承一个直接父类,这个父类可以是具体的类也可是抽象类,但是一个类可以实现多个接口。
### 内部类、静态内部类在业务中的应用场景是什么?
- 静态内部类:只是为了降低包的深度,方便类的使用,静态内部类适用于包含类当中,但又不依赖与外在的类,不用使用外在类的非静态属性和方法,只是为了方便管理类结构而定义。在创建静态内部类的时候,不需要外部类对象的引用。
- 非静态内部类:持有外部类的引用,可以自由使用外部类的所有变量和方法。
### synchronized与ReentrantLock有什么区别?
synchronized是互斥同步的一种实现。
> synchronized:当某个线程访问被synchronized标记的方法或代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个
线程才会释放该对象的锁,其他线程才能执行这个方法或代码块。
前面我们已经说了volatile关键字,这里我们举个例子来综合分析volatile与synchronized关键字的使用。
:point_up:举个栗子
```java
public class Singleton {
//volatile保证了:1 instance在多线程并发的可见性 2 禁止instance在操作是的指令重排序
private volatile static Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
//第一次判空,保证不必要的同步
if (instance == null) {
//synchronized对Singleton加全局所,保证每次只要一个线程创建实例
synchronized (Singleton.class) {
//第二次判空时为了在null的情况下创建实例
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
```
这是一个经典的DCL单例。
它的字节码如下:
<img src="https://github.com/guoxiaoxing/android-open-source-project-analysis/raw/master/art/native/process/synchronized_bytecode.png"/>
可以看到被synchronized同步的代码块,会在前后分别加上monitorenter和monitorexit,这两个字节码都需要指定加锁和解锁的对象。
关于加锁和解锁的对象:
- synchronized代码块 :同步代码块,作用范围是整个代码块,作用对象是调用这个代码块的对象。
- synchronized方法 :同步方法,作用范围是整个方法,作用对象是调用这个方法的对象。
- synchronized静态方法 :同步静态方法,作用范围是整个静态方法,作用对象是调用这个类的所有对象。
- synchronized(this):作用范围是该对象中所有被synchronized标记的变量、方法或代码块,作用对象是对象本身。
- synchronized(ClassName.class) :作用范围是静态的方法或者静态变量,作用对象是Class对象。
synchronized(this)添加的是对象锁,synchronized(ClassName.class)添加的是类锁,它们的区别如下:
> 对象锁:Java的所有对象都含有1个互斥锁,这个锁由JVM自动获取和释放。线程进入synchronized方法的时候获取该对象的锁,当然如果已经有线程获取了这个对象的锁,那么当前线
程会等待;synchronized方法正常返回或者抛异常而终止,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的好处,方法抛异常的时候,锁仍然可以由JVM来自动释放。
> 类锁:对象锁是用来控制实例方法之间的同步,类锁是用来控制静态方法(或静态变量互斥体)之间的同步。其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理
解锁定实例方法和静态方法的区别的。我们都知道,java类可能会有很多个对象,但是只有1个Class对象,也就是说类的不同实例之间共享该类的Class对象。Class对象其实也仅仅是1个
java对象,只不过有点特殊而已。由于每个java对象都有1个互斥锁,而类的静态方法是需要Class对象。所以所谓的类锁,不过是Class对象的锁而已。获取类的Class对象有好几种,最简
单的就是MyClass.class的方式。 类锁和对象锁不是同一个东西,一个是类的Class对象的锁,一个是类的实例的锁。也就是说:一个线程访问静态synchronized的时候,允许另一个线程访
问对象的实例synchronized方法。反过来也是成立的,因为他们需要的锁是不同的。
### volatile的原理是什么?
volatile也是互斥同步的一种实现,不过它非常的轻量级。
volatile有两条关键的语义:
- 保证被volatile修饰的变量对所有线程都是可见的
- 禁止进行指令重排序
要理解volatile关键字,我们得先从Java的线程模型开始说起。如图所示:
<img src="https://github.com/guoxiaoxing/android-open-source-project-analysis/raw/master/art/native/process/java_memory_model.png"/>
Java内存模型规定了所有字段(这些字段包括实例字段、静态字段等,不包括局部变量、方法参数等,因为这些是线程私有的,并不存在竞争)都存在主内存中,每个线程会
有自己的工作内存,工作内存里保存了线程所使用到的变量在主内存里的副本拷贝,线程对变量的操作只能在工作内存里进行,而不能直接读写主内存,当然不同内存之间也
无法直接访问对方的工作内存,也就是说主内存时线程传值的媒介。
我们来理解第一句话:
> 保证被volatile修饰的变量对所有线程都是可见的
如何保证可见性?🤔
被volatile修饰的变量在工作内存修改后会被强制写回主内存,其他线程在使用时也会强制从主内存刷新,这样就保证了一致性。
关于“保证被volatile修饰的变量对所有线程都是可见的”,有种常见的错误理解:
>错误理解:由于volatile修饰的变量在各个线程里都是一致的,所以基于volatile变量的运算在多线程并发的情况下是安全的。
这句话的前半部分是对的,后半部分却错了,因此它忘记考虑变量的操作是否具有原子性这一问题。
:point_up:举个栗子
```java
private volatile int start = 0;
private void volatileKeyword() {
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
start++;
}
}
};
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(runnable);
thread.start();
}
Log.d(TAG, "start = " + start);
}
```
<img src="https://github.com/guoxiaoxing/android-open-source-project-analysis/raw/master/art/native/process/volatile_thread_safe.png"/>
这段代码启动了10个线程,每次10次自增,按道理最终结果应该是100,但是结果并非如此。
为什么会这样?:thinking:
仔细看一下start++,它其实并非一个原子操作,简单来看,它有两步:
1. 取出start的值,因为有volatile的修饰,这时候的值是正确的。
2. 自增,但是自增的时候,别的线程可能已经把start加大了,这种情况下就有可能把较小的start写回主内存中。
所以volatile只能保证可见性,在不符合以下场景下我们依然需要通过加锁来保证原子性:
- 运算结果并不依赖变量当前的值,或者只有单一线程修改变量的值。(要么结果不依赖当前值,要么操作是原子性的,要么只要一个线程修改变量的值)
- 变量不需要与其他状态变量共同参与不变约束
比方说我们会在线程里加个boolean变量,来判断线程是否停止,这种情况就非常适合使用volatile。
我们再来理解第二句话。
- 禁止进行指令重排序
什么是指令重排序?🤔
> 指令重排序是值指令乱序执行,即在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开为获取下一条指令所需数据而造成的等待,通过乱序执行的技术,提供执行效率。
指令重排序绘制被volatile修饰的变量的赋值操作前,添加一个内存屏障,指令重排序时不能把后面的指令重排序的内存屏障之前的位置。
关于指令重排序不是本篇文章重点讨论的内容,更多细节可以参考[指令重排序](https://tech.meituan.com/java-memory-reordering.html)。
### 如何防止反射、序列化攻击单例?
枚举单例
```java
public enum Singleton {
INSTANCE {
@Override
protected void read() {
System.out.println("read");
}
@Override
protected void write() {
System.out.println("write");
}
};
protected abstract void read();
protected abstract void write();
}
```
class文件:
```java
public abstract class Singleton extends Enum
{
private Singleton(String s, int i)
{
super(s, i);
}
protected abstract void read();
protected abstract void write();
public static Singleton[] values()
{
Singleton asingleton[];
int i;
Singleton asingleton1[];
System.arraycopy(asingleton = ENUM$VALUES, 0, asingleton1 = new Singleton[i = asingleton.length], 0, i);
return asingleton1;
}
public static Singleton valueOf(String s)
{
return (Singleton)Enum.valueOf(singleton/Singleton, s);
}
Singleton(String s, int i, Singleton singleton)
{
this(s, i);
}
public static final Singleton INSTANCE;
private static final Singleton ENUM$VALUES[];
static
{
INSTANCE = new Singleton("INSTANCE", 0) {
protected void read()
{
System.out.println("read");
}
protected void write()
{
System.out.println("write");
}
};
ENUM$VALUES = (new Singleton[] {
INSTANCE
});
}
}
```
- 类的修饰abstract,所以没法实例化,反射也无能为力。
- 关于线程安全的保证,其实是通过类加载机制来保证的,我们看看INSTANCE的实例化时机,是在static块中,JVM加载类的过程显然是线程安全的。
- 对于防止反序列化生成新实例的问题还不是很明白,一般的方法我们会在该类中添加上如下方法,不过枚举中也没有显示的写明该方法。
```java
//readResolve to prevent another instance of Singleton
private Object readResolve(){
return INSTANCE;
}
```
### 线程为什么阻塞,为和要使用多线程?
使用多线程更多的是为了提高CPU的并发,可以让CPU同事处理多个事情,多线程场景的使用场景:
1. 为了不让耗时操作阻塞主线程,开启新线程执行耗时操作。
2. 某种任务虽然耗时但是不消耗CPU,例如:磁盘IO,可以开启新线程来做,可以显著的提高效率。
3. 优先级比较低的任务,但是需要经常去做,例如:GC,可以开启新线程来做。
### 了解线程的生命周期吗,描述一下?
线程状态流程图图
<img src="https://github.com/guoxiaoxing/android-open-source-project-analysis/raw/master/art/native/process/java_thread_state.png"/>
- NEW:创建状态,线程创建之后,但是还未启动。
- RUNNABLE:运行状态,处于运行状态的线程,但有可能处于等待状态,例如等待CPU、IO等。
- WAITING:等待状态,一般是调用了wait()、join()、LockSupport.spark()等方法。
- TIMED_WAITING:超时等待状态,也就是带时间的等待状态。一般是调用了wait(time)、join(time)、LockSupport.sparkNanos()、LockSupport.sparkUnit()等方法。
- BLOCKED:阻塞状态,等待锁的释放,例如调用了synchronized增加了锁。
- TERMINATED:终止状态,一般是线程完成任务后退出或者异常终止。
NEW、WAITING、TIMED_WAITING都比较好理解,我们重点说一说RUNNABLE运行态和BLOCKED阻塞态。
线程进入RUNNABLE运行态一般分为五种情况:
- 线程调用sleep(time)后查出了休眠时间
- 线程调用的阻塞IO已经返回,阻塞方法执行完毕
- 线程成功的获取了资源锁
- 线程正在等待某个通知,成功的获得了其他线程发出的通知
- 线程处于挂起状态,然后调用了resume()恢复方法,解除了挂起。
线程进入BLOCKED阻塞态一般也分为五种情况:
- 线程调用sleep()方法主动放弃占有的资源
- 线程调用了阻塞式IO的方法,在该方法返回前,该线程被阻塞。
- 线程视图获得一个资源锁,但是该资源锁正被其他线程锁持有。
- 线程正在等待某个通知
- 线程调度器调用suspend()方法将该线程挂起
我们再来看看和线程状态相关的一些方法。
- sleep()方法让当前正在执行的线程在指定时间内暂停执行,正在执行的线程可以通过Thread.currentThread()方法获取。
- yield()方法放弃线程持有的CPU资源,将其让给其他任务去占用CPU执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得CPU时间片。
- wait()方法是当前执行代码的线程进行等待,将当前线程放入预执行队列,并在wait()所在的代码处停止执行,知道接到通知或者被中断为止。该方法可以使得调用该方法的线程释放共享资源的锁,
然后从运行状态退出,进入等待队列,直到再次被唤醒。该方法只能在同步代码块里调用,否则会抛出IllegalMonitorStateException异常。
- wait(long millis)方法等待某一段时间内是否有线程对锁进行唤醒,如果超过了这个时间则自动唤醒。
- notify()方法用来通知那些可能等待该对象的对象锁的其他线程,该方法可以随机唤醒等待队列中等同一共享资源的一个线程,并使该线程退出等待队列,进入可运行状态。
- notifyAll()方法可以是所有正在等待队列中等待同一共享资源的全部线程从等待状态退出,进入可运行状态,一般会是优先级高的线程先执行,但是根据虚拟机的实现不同,也有可能是随机执行。
- join()方法可以让调用它的线程正常执行完成后,再去执行该线程后面的代码,它具有让线程排队的作用。
### 线程池了解吗,有几种线程池,应用场景是什么?
> 线程池用来管理线程,避免频繁的CPU时间片切换造成的CPU资源消耗。
Executors类提供了一系列工厂方法用来创建线程池。这些线程是适用于不同的场景。
- newCachedThreadPool():无界可自动回收线程池,查看线程池中有没有以前建立的线程,如果有则复用,如果没有则建立一个新的线程加入池中,池中的线程超过60s不活动则自动终止。适用于生命
周期比较短的异步任务。
- newFixedThreadPool(int nThreads):固定大小线程池,与newCachedThreadPool()类似,但是池中持有固定数目的线程,不能随时创建线程,如果创建新线程时,超过了固定
线程数,则放在队列里等待,直到池中的某个线程被移除时,才加入池中。适用于很稳定、很正规的并发线程,多用于服务器。
- newScheduledThreadPool(int corePoolSize):周期任务线程池,该线程池的线程可以按照delay依次执行线程,也可以周期执行。
- newSingleThreadExecutor():单例线程池,任意时间内池中只有一个线程。
### ThreadLocal的原理了解吗?
ThreadLocal是一个关于创建线程局部变量的类。使用场景如下所示:
1. 实现单个线程单例以及单个线程上下文信息存储,比如交易id等。
2. 实现线程安全,非线程安全的对象使用ThreadLocal之后就会变得线程安全,因为每个线程都会有一个对应的实例。
3. 承载一些线程相关的数据,避免在方法中来回传递参数。
### wait和notify机制,手写一下生产者和消费者模型?
生成者消费者模型
生产者和消费者在同一时间段内共用同一个存储空间,生产者往存储空间中添加产品,消费者从存储空间中取走产品,当存储空间为空时,消费者阻塞,当存储空间满时,生产者阻塞。
wait()和notify()方法的实现生成者消费者模型,缓冲区满和为空时都调用wait()方法等待,当生产者生产了一个产品或者消费者消费了一个产品之后会唤醒所有线程。
```java
public class ProducerAndCustomerModel {
private static Integer count = 0;
private static final Integer FULL = 10;
private static String LOCK = "lock";
public static void main(String[] args) {
Test1 test1 = new Test1();
new Thread(test1.new Producer()).start();
new Thread(test1.new Consumer()).start();
new Thread(test1.new Producer()).start();
new Thread(test1.new Consumer()).start();
new Thread(test1.new Producer()).start();
new Thread(test1.new Consumer()).start();
new Thread(test1.new Producer()).start();
new Thread(test1.new Consumer()).start();
}
class Producer implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (LOCK) {
while (count == FULL) {
try {
LOCK.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
count++;
System.out.println(Thread.currentThread().getName() + "生产者生产,目前总共有" + count);
LOCK.notifyAll();
}
}
}
}
class Consumer implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LOCK) {
while (count == 0) {
try {
LOCK.wait();
} catch (Exception e) {
}
}
count--;
System.out.println(Thread.currentThread().getName() + "消费者消费,目前总共有" + count);
LOCK.notifyAll();
}
}
}
}
}
```
### 死锁是如何发生的,如何避免死锁?
当线程A持有独占锁a,并尝试去获取独占锁b的同时,线程B持有独占锁b,并尝试获取独占锁a的情况下,就会发生AB两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
```java
public class DeadLockDemo {
public static void main(String[] args) {
// 线程a
Thread td1 = new Thread(new Runnable() {
public void run() {
DeadLockDemo.method1();
}
});
// 线程b
Thread td2 = new Thread(new Runnable() {
public void run() {
DeadLockDemo.method2();
}
});
td1.start();
td2.start();
}
public static void method1() {
synchronized (String.class) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程a尝试获取integer.class");
synchronized (Integer.class) {
}
}
}
public static void method2() {
synchronized (Integer.class) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程b尝试获取String.class");
synchronized (String.class) {
}
}
}
}
```
造成死锁的四个条件:
1. 互斥条件:一个资源每次只能被一个线程使用。
2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
3. 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
在并发程序中,避免了逻辑中出现复数个线程互相持有对方线程所需要的独占锁的的情况,就可以避免死锁,如下所示:
```java
public class BreakDeadLockDemo {
public static void main(String[] args) {
// 线程a
Thread td1 = new Thread(new Runnable() {
public void run() {
DeadLockDemo2.method1();
}
});
// 线程b
Thread td2 = new Thread(new Runnable() {
public void run() {
DeadLockDemo2.method2();
}
});
td1.start();
td2.start();
}
public static void method1() {
synchronized (String.class) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程a尝试获取integer.class");
synchronized (Integer.class) {
System.out.println("线程a获取到integer.class");
}
}
}
public static void method2() {
// 不再获取线程a需要的Integer.class锁。
synchronized (String.class) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程b尝试获取Integer.class");
synchronized (Integer.class) {
System.out.println("线程b获取到Integer.class");
}
}
}
}
```
### 了解Java注解的原理吗,注解如何获取?
注解相当于一种标记,在程序中加了注解就等于为程序打上了某种标记。程序可以利用ava的反射机制来了解你的类及各种元素上有无何种标记,针对不同的标记,就去做相
应的事件。标记可以加在包,类,字段,方法,方法的参数以及局部变量上。
### String为什么要设计成不可变,StringBuffer与StringBuilder有什么区别?
1. String是不可变的(修改String时,不会在原有的内存地址修改,而是重新指向一个新对象),String用final修饰,不可继承,String本质上是个final的char[]数组,所以char[]数组的内存地址不会被修改,而且String
也没有对外暴露修改char[]数组的方法。不可变性可以保证线程安全以及字符串串常量池的实现。
2. StringBuffer是线程安全的。
3. StringBuilder是非线程安全的。
### Java里的幂等性了解吗?
> 幂等性原本是数学上的一个概念,即:f(x) = f(f(x)),对同一个系统,使用同样的条件,一次请求和重复的多次请求对系统资源的影响是一致的。
幂等性最为常见的应用就是电商的客户付款,试想一下,如果你在付款的时候因为网络等各种问题失败了,然后又去重复的付了一次,是一种多么糟糕的体验。幂等性
就是为了解决这样的问题。
实现幂等性可可以使用Token机制。
> 核心思想是为每一次操作生成一个唯一性的凭证,也就是token。一个token在操作的每一个阶段只有一次执行权,一旦执行成功则保存执行结果。对
重复的请求,返回同一个结果。
例如:电商平台上的订单id就是最适合的token。当用户下单时,会经历多个环节,比如生成订单,减库存,减优惠券等等。每一个环节执行时都先检
测一下该订单id是否已经执行过这一步骤,对未执行的请求,执行操作并缓存结果,而对已经执行过的id,则直接返回之前的执行结果,不做任何操
作。这样可以在最大程度上避免操作的重复执行问题,缓存起来的执行结果也能用于事务的控制等。
### Java泛型了解吗,知道它的运行机制吗?
> 泛型是为了参数化类型。
为什么使用泛型?
1. 相对于使用Object这种简单粗暴的方式,泛型提供了一种参数化的能力,使得数据的类型可以像参数一样被传递进来,这提供了一种扩展能力。
2. 当数据类型确定以后,提供了一种类型检测机制,只有相匹配的数据才可以正常赋值,否则编译错误,增强了安全性。
3. 泛型提高了代码的可读性,不必等到运行时采取执行类型转换,在编写代码阶段,程序员就可以通过参数书写正确的数据类型。
除了用 <T> 表示泛型外,还有 <?> 这种形式。? 被称为通配符。
- <?> 被称作无限定的通配符。
- <? extends T> 被称作有上限的通配符。
- <? super T> 被称作有下限的通配符。
### Java里的反射为何会消耗性能?
反射慢主要因为反射是动态类型,导致没办法运用JVM的一些优化措施。
### Java的类型擦除,知道它的原理吗?
> 泛型信息只存在代码编译阶段,在进入JVM之前,与泛型相关的信息都会被擦除掉。
在类型擦除的时候,如果泛型类里的类型参数没有指定上限,例如:<T>,则会被转成Object类型,如果指定了上限,例如:<T extends String>,则会
被传换成对应的类型上限。
### 闭包了解吗,Java里有闭包吗?
> 「函数」和「函数内部能访问到的变量」(也叫环境)的总和,就是一个闭包。
```kotlin
fun main(args: Array<String>) {
test
}
val test = if (5 > 3) {
print("yes")
} else {
print("no")
}
```
### Lambda表达式了解吗?
> Lambda 表达式俗称匿名函数。Kotlin 的 Lambda表达式更“纯粹”一点, 因为它是真正把Lambda抽象为了一种类型,而 Java 8 的 Lambda 只是单方法匿名接口实现的语法糖罢了。
```kotlin
val printMsg = { msg: String ->
println(msg)
}
fun main(args: Array<String>) {
printMsg("hello")
}
```
### 高阶函数了解吗?
> 当定义一个闭包作为参数的函数,称这个函数为高阶函数。
```kotlin
fun main(args: Array<String>) {
log("world", printMsg)
}
val printMsg = { str: String ->
println(str)
}
val log = { str: String, printLog: (String) -> Unit ->
printLog(str)
}
```