-
Notifications
You must be signed in to change notification settings - Fork 684
/
JavaMultiThread.md
1840 lines (1397 loc) · 84.3 KB
/
JavaMultiThread.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
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
(PS:扫描[首页里面的二维码](README.md)进群,分享我自己在看的技术资料给大家,希望和大家一起学习进步!)
# 多线程专题
#### [1.进程与线程的区别是什么?](#进程与线程的区别是什么?)
#### [2.进程间如何通信?](#进程间如何通信?)
#### [3.Java中单例有哪些写法?](#Java中单例有哪些写法?)
#### [4.Java中创建线程有哪些方式?](#Java中创建线程有哪些方式?)
#### [5.如何解决序列化时可以创建出单例对象的问题?](#如何解决序列化时可以创建出单例对象的问题?)
#### [6.volatile关键字有什么用?怎么理解可见性,一般什么场景去用可见性?](#volatile关键字有什么用?怎么理解可见性,一般什么场景去用可见性?)
#### [7.Java中线程的状态是怎么样的?](#Java中线程的状态是怎么样的?)
#### [8.wait(),join(),sleep()方法有什么作用?](#wait(),join(),sleep()方法有什么作用?)
#### [9.Thread.sleep(),Object.wait(),LockSupport.park()有什么区别?](#Thread.sleep(),Object.wait(),LockSupport.park()有什么区别?)
#### [10.谈一谈你对线程中断的理解?](#谈一谈你对线程中断的理解?)
#### [11.线程间怎么通信?](#线程间怎么通信?)
#### [12.怎么实现实现一个生产者消费者?](#怎么实现实现一个生产者消费者?)
#### [13.谈一谈你对线程池的理解?](#谈一谈你对线程池的理解?)
#### [14.线程池有哪些状态?](#线程池有哪些状态?)
### 进程与线程的区别是什么?
#### 批处理操作系统
**批处理操作系统**就是把一系列需要操作的指令写下来,形成一个清单,一次性交给计算机。用户将多个需要执行的程序写在磁带上,然后交由计算机去读取并逐个执行这些程序,并将输出结果写在另一个磁带上。
批处理操作系统在一定程度上提高了计算机的效率,但是由于**批处理操作系统的指令运行方式仍然是串行的,内存中始终只有一个程序在运行**,后面的程序需要等待前面的程序执行完成后才能开始执行,而前面的程序有时会由于I/O操作、网络等原因阻塞,导致CPU闲置所以**批处理操作效率也不高**。
#### 进程的提出
批处理操作系统的瓶颈在于内存中只存在一个程序,进程的提出,可以让内存中存在多个程序,每个程序对应一个进程,进程是操作系统资源分配的最小单位。CPU采用时间片轮转的方式运行进程:CPU为每个进程分配一个时间段,称作它的时间片。如果在时间片结束时进程还在运行,则暂停这个进程的运行,并且CPU分配给另一个进程(这个过程叫做上下文切换)。如果进程在时间片结束前阻塞或结束,则CPU立即进行切换,不用等待时间片用完。多进程的好处在于一个在进行IO操作时可以让出CPU时间片,让CPU执行其他进程的任务。
#### 线程的提出
随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程,线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。
#### 进程和线程的区别
进程是计算机中已运行程序的实体,进程是操作系统资源分配的最小单位。而线程是在进程中执行的一个任务,是CPU调度和执行的最小单位。他们两个本质的区别是是否**单独占有内存地址空间及其它系统资源(比如I/O)**:
* 进程单独占有一定的内存地址空间,所以进程间存在内存隔离,数据是分开的,数据共享复杂但是同步简单,各个进程之间互不干扰;而线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。
* 进程单独占有一定的内存地址空间,一个进程出现问题不会影响其他进程,不影响主程序的稳定性,可靠性高;一个线程崩溃可能影响整个程序的稳定性,可靠性较低。
* 进程单独占有一定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,开销较大;线程只需要保存寄存器和栈信息,开销较小。
另外一个重要区别是,进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即CPU分配时间的单位 。
#### 独立性
Linux系统会给每个进程分配4G的虚拟地址空间(0到3G是User地址空间,3到4G部分是kernel地址空间),进程具备私有的地址空间,未经允许,一个用户进程不能访问其他进程的地址空间。
#### 动态性
程序是一个静态的指令集合,而进程是正在操作系统中运行的指令集合,进程有自己的生命周期和各种不同的状态。
五态模型一般指的是:
**新建态**(创建一个进程)
**就绪态**(已经获取到资源,准备好了,进入运行队列,一旦获得时间片可以立即执行)
**运行态**(获取到了时间片,执行程序)
**阻塞态**(运行过程中等待获取其他资源,I/O请求等)
**终止态**(进程被杀死了)
#### 并发性
多个进程可以在CPU上并发执行。
线程是独立运行和调度的最小单位,线程会共享进程的虚拟空间,一个进程会对应多个线程。在Java中,线程拥有自己私有的程序计数器,虚拟机栈,本地方法栈。
#### PS:虚拟内存
虚拟内存是一种逻辑上扩充物理内存的技术。基本思想是用软、硬件技术把内存与外存这两级存储器当做一级存储器来用。虚拟内存技术的实现利用了自动覆盖和交换技术。简单的说就是将硬盘的一部分作为内存来使用。
#### PS:虚拟地址空间
每个进程有4G的地址空间,在运行程序时,只有一部分数据是真正加载到内存中的,内存管理单元将虚拟地址转换为物理地址,如果内存中不存在这部分数据,那么会使用页面置换方法,将内存页置换出来,然后将外存中的数据加入到内存中,使得程序正常运行。
### 进程间如何通信?
进程间通信的方式主要有管道,
#### 管道
调用pipe函数在内存中开辟一块缓冲区,管道半双工的(即数据只能在一个方向上流动),具有固定的读端和写端,调用
```
#include <unistd.h>
int pipe(int pipefd[2]);
```
### Java中创建线程有哪些方式?
#### 第一种 继承Thread类,重写Run方法
这种方法就是通过自定义CustomThread类继承Thread类,重写run()方法,然后创建CustomThread的对象,然后调用start()方法,JVM会创建出一个新线程,并且为线程创建方法调用栈和程序计数器,此时线程处于就绪状态,当线程获取CPU时间片后,线程会进入到运行状态,会去调用run()方法。并且创建CustomThread类的对象的线程(这里的例子中是主线程)与调用run()方法的线程之间是并发的,也就是在执行run()方法时,主线程可以去执行其他操作。
```java
class CustomThread extends Thread {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"线程调用了main方法");
for (int i = 0; i < 10; i++) {
if (i == 1) {
CustomThread customThread = new CustomThread();
customThread.start();
System.out.println(Thread.currentThread().getName()+"线程--i是"+i);
}
}
System.out.println("main()方法执行完毕!");
}
void run() {
System.out.println(Thread.currentThread().getName()+"线程调用了run()方法");
for (int j = 0; j < 5; j++) {
System.out.println(Thread.currentThread().getName()+"线程--j是"+j);
}
System.out.println("run()方法执行完毕!");
}
}
```
输出结果如下:
```
main线程调用了main方法
Thread-0线程调用了run()方法
Thread-0线程--j是0
main线程--i是1
Thread-0线程--j是2
Thread-0线程--j是3
Thread-0线程--j是4
run()方法执行完毕!
main()方法执行完毕!
```
可以看到在创建一个CustomThread对象,调用start()方法后,Thread-0调用了run方法,进行for循环,对j进行打印,与此同时,main线程并没有被阻塞,而是继续执行for循环,对i进行打印。
##### 执行原理
首先我们可以来看看start的源码,首先会判断threadStatus是否为0,如果不为0会抛出异常。然后会将当前对象添加到线程组,最后调用start0方法,因为是native方法,看不到源码,根据上面的执行结果来看,JVM新建了一个线程调用了run方法。
```java
private native void start0();
public synchronized void start() {
//判断当前Thread对象是否是新建态,否则抛出异常
if (threadStatus != 0)
throw new IllegalThreadStateException();
//将当前对象添加到线程组
group.add(this);
boolean started = false;
try {
start0();//这是一个native方法,调用后JVM会新建一个线程来调用run方法
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
```
扩展问题:多次调用Thread对象的start()方法会怎么样?
会抛出IllegalThreadStateException异常。其实在Thread#start()方法里面的的注释中有提到,多次调用start()方法是非法的,所以在上面的start()方法源码中一开始就是对threadStatus进行判断,不为0就会抛出IllegalThreadStateException异常。
![image-20200105144159345](../static/image-20200105144159345.png)
##### 注意事项:
start()方法中判断threadStatus是否为0,是判断当前线程是否新建态,0是代表新建态(上图中的源码注释里面有提到),而不是就绪态,因为Java的Thread类中,Thread的Runnable状态包括了线程的就绪态和运行态,(Thread的state为RUNNABLE时(也就是threadStatus为4时),代表线程为就绪态或运行态)。执行start()方法的线程还不是JVM新建的线程,所以不是就绪态。有一些技术文章把这里弄错了,例如这一篇[《深入浅出线程Thread类的start()方法和run()方法》](https://juejin.im/post/5b09274af265da0de25759d5)
![image-20200105144031591](../static/image-20200105144031591.png)
##### 总结
这种方式的缺点很明显,就是需要继承Thread类,而且实际上我们的需求可能仅仅是希望某些操作被一个其他的线程来执行,所以有了第二种方法。
#### 第二种 实现Runnable接口
这种方式就是创建一个类(例如下面代码中的Target类),实现Runnable接口的Run方法,然后将Target类的实例对象作为Thread的构造器入参target,实际的线程对象还是Thread实例,只不过线程Thread与线程执行体(Target类的run方法)分离了,耦合度更低一些。
```java
class ThreadTarget implements Runnable {
void run() {
System.out.println(Thread.currentThread().getName()+"线程执行了run方法");
}
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"线程执行了main方法");
ThreadTarget target = new ThreadTarget();
Thread thread = new Thread(target);
thread.start();
}
}
```
输出结果如下:
![image-20200105163553969](../static/image-20200105163553969.png)
##### 原理
之所以有这种实现方法,是因为Thread类的run()方法中会判断成员变量target是否为空,不为空就会调用target类的run方法。
```java
private Runnable target;
public void run() {
if (target != null) {
target.run();
}
}
```
##### 另外一种写法
这种实现方式也有其他的写法,可以不创建Target类。
##### 匿名内部类
可以不创建Target类,可以使用匿名内部类的方式来实现,因此上面的代码也可以按以下方式写:
```java
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程执行了run方法");
}
});
thread.start();
```
##### Lamda表达式
在Java8之后,使用了@FunctionalInterface注解来修饰Runnable接口,表明Runnable接口是一个函数式接口,有且只有一个抽象方法,可以Lambda方式来创建Runnable对象,比使用匿名类的方式更加简洁一些。
```java
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
```
因此上面的代码也可以按以下方式写:
```java
Thread thread = new Thread(()->{
System.out.println(Thread.currentThread().getName()+"线程执行了run方法");
})
thread.start()
```
##### 总结
这种写法不用继承Thread,但是同样也有缺点,就是线程方法体(也就是run方法)不能设置返回值。
#### 第三种 实现Callable接口
Runnable接口中的run()方法是没有返回值,如果我们需要执行的任务带返回值就不能使用Runnable接口。创建一个类CallableTarget,实现Callable接口,实现带有**返回值的call()方法**,然后根据CallableTarget创建一个任务FutureTask,然后根据FutureTask来创建一个线程Thread,调用Thread的start方法可以执行任务。
```java
public class CallableTarget implements Callable<Integer> {
public Integer call() throws InterruptedException {
System.out.println(Thread.currentThread().getName()+"线程执行了call方法");
Thread.sleep(5000);
return 1;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println(Thread.currentThread().getName()+"线程执行了main方法");
CallableTarget callableTarget = new CallableTarget();
FutureTask<Integer> task = new FutureTask<Integer>(callableTarget);
Thread thread = new Thread(task);
thread.start();
Integer result = task.get();//当前线程会阻塞,一直等到结果返回。
System.out.println("执行完毕,打印result="+result);
System.out.println("执行完毕");
}
}
```
原理就是Thread类默认的run()方法实现是会去调用自身实例变量target的run()方法,(target就是我们构造Thread传入的FutureTask),而FutureTask的run方法中就会调用Callable接口的实例的call()方法。
```java
//Thread类的run方法实现
@Override
public void run() {
if (target != null) {
//这里target就是我们在创建Thread时传入的FutureTask实例变量
target.run();
}
}
//FutureTask类的run方法实现
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
//在这里会调用Callable实例的call方法
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
```
### Java中的Runnable、Callable、Future、FutureTask的区别和联系?
最原始的通过新建线程执行任务的方法就是我们去新建一个类,继承Thread,然后去重写run()方法,但是这样限制太大了,Java也不支持多继承。所以有了Runnable。
##### Runnable
Runnable是一个接口,只需要新建一个类实现这个接口,然后重写run方法,将该类的实例作为创建Thread的入参,线程运行时就会调用该实例的run方法。
```java
@FunctionalInterfacepublic interface Runnable {
public abstract void run();
}
```
Thread.start()方法->Thread.run()方法->target.run()方法
##### Callable
Callable跟Runnable类似,也是一个接口。只不过它的call方法有返回值,可以供程序接收任务执行的结果。
```java
@FunctionalInterfacepublic interface Callable<V> {
V call() throws Exception;
}
```
##### Future
Future也是一个接口,Future就像是一个管理的容器一样,进一步对Runable和Callable的实例进行封装,定义了一些方法。取消任务的cancel()方法,查询任务是否完成的isDone()方法,获取执行结果的get()方法,带有超时时间来获取执行结果的get()方法。
```java
public interface Future<V> {
//mayInterruptIfRunning代表是否强制中断
//为true,如果任务已经执行,那么会调用Thread.interrupt()方法设置中断标识
//为false,如果任务已经执行,就只会将任务状态标记为取消,而不会去设置中断标识
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
```
##### FutureTask
因为Future只是一个接口,并不能实例化,可以认为FutureTask就是Future接口的实现类,FutureTask实现了RunnableFuture接口,而RunnableFuture接口继承Runnable接口和Future接口。
```java
public class FutureTask<V> implements RunnableFuture<V> {
...
}
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
```
##### 使用案例
使用时,Runnable实现类的实例可以作为Thread的入参使用,而Callable只能使用FutureTask进行封装使用。
```java
//Runnable配合Thread进行使用
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
//任务的代码
}
});
//Callable使用FutureTask封装后,配合线程池进行使用
ExecutorService pool = Executors.newSingleThreadExecutor();
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
//任务的代码
return null;
}
});
pool.submit(task);
//Runnable使用FutureTask封装后,配合线程池进行使用
FutureTask task1 = new FutureTask(new Runnable() {
@Override
public void run() {
//任务的代码
}
});
pool.submit(task1);
```
### Java中单例有哪些写法?
正确并且可以做到延迟加载的写法其实就是三种:
1.使用volatile修饰变量并且双重校验的写法来实现。
2.使用静态内部类来实现(类A有一个静态内部类B,类B有一个静态变量instance,类A的getInstance()方法会返回类B的静态变量instance,因为只有调用getInstance()方法时才会加载静态内部类B,这种写法缺点是不能传参。)
3.使用枚举来实现
#### 第1种 不加锁(裸奔写法)
多线程执行时,可能会在instance完成初始化之前,其他线性线程判断instance为null,从而也执行第二步的代码,导致初始化覆盖。
```java
public class UnsafeLazyInitialization {
private static Instance instance;
public static Instance getInstance() {
if (instance == null) //1
instance = new Instance(); //2
}
return instance;
}
```
#### 第2种-对方法加sychronize锁(俗称的懒汉模式)
初始化完成以后,每次调用getInstance()方法都需要获取同步锁,导致不必要的开销。
```java
public class Singleton {
private static Singleton instance;
public synchronized static Singleton getInstance() {
if (instance == null)
instance = new Instance();
return instance;
}
}
```
#### 第3种-使用静态变量(俗称的饿汉模式)
```
public class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
```
这种方法是缺点在于不能做到延时加载,在第一次调用getInstance()方法之前,如果Singleton类被使用到,那么就会对instance变量初始化。
#### 第4种-使用双重检查锁定
代码如下:
```java
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) { //双重检查存在的意义在于可能会有多个线程进入第一个判断,然后竞争同步锁,线程A得到了同步锁,创建了一个Singleton实例,赋值给instance,然后释放同步锁,此时线程B获得同步锁,又会创建一个Singleton实例,造成初始化覆盖。
instance = new Singleton();
}
}
}
return instance;
}
}
```
instance = new Singleton();
这句代码在执行时会分解为三个步骤:
1.为对象分配内存空间。
2.执行初始化的代码。
3.将分配好的内存地址设置给instance引用。
但是编译器会对指令进行重排序,只能保证单线程执行时结果不会变化,也就是可能第3步会在第2步之前执行,某个线程A刚好执行完第3步,正在执行第2步时,此时如果有其他线程B进入if (instance == null)判断,会发现instance不为null,然后将instance返回,但是实际上instance还没有完成初始化,线程B会访问到一个未初始化完成的instance对象。所以需要像第5种解法一样使用volatile来修饰变量,防止重排序。
#### 第5种 基于 volatile 的双重检查锁定的解决方案
代码如下:
```java
public class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null)//双重检查存在的意义在于可能会有多个线程进入第一个判断,然后竞争同步锁,线程A得到了同步锁,创建了一个Singleton实例,赋值给instance,然后释放同步锁,此时线程B获得同步锁,又会创建一个Singleton实例,造成初始化覆盖。
instance = new Singleton();
}
}
return instance;
}
}
```
volatile可以保证变量的内存可见性及防止指令重排。
volatile修饰的变量在编译后,会多出一个lock前缀指令,lock前缀指令相当于一个内存屏障(内存栅栏),有三个作用:
* 确保指令重排序时,内存屏障前的指令不会排到后面去,内存屏障后的指令不会排到前面去。
* 强制对变量在线程工作内存中的修改操作立即写入到物理内存。
* 如果是写操作,会导致其他CPU中对这个变量的缓存失效,强制其他CPU中的线程在获取变量时从物理内存中获取更新后的值。
所以使用volatile修饰后不会出现第3种写法中由于指令重排序导致的问题。
#### 第6种 - 使用静态内部类来实现
```java
class Test {
public static Signleton getInstance() {
return Signleton.instance ; // 只有调用getInstance()方法时,才会引用到静态内部类Signleton,从而会触发Signleton类的instance变量的初始化,以此实现懒加载的目的。
}
private static class Signleton {
private static Signleton instance = new Signleton();
}
}
```
因为JVM底层通过加锁实现,保证一个类只会被加载一次,多个线程在对类进行初始化时,只有一个线程会获得锁,然后对类进行初始化,其他线程会阻塞等待。所以可以使用上面的代码来保证instance只会被初始化一次,这种写法的问题在于创建单例时不能传参。
#### 7.使用枚举来实现单例
```java
public enum Singleton {
//每个元素就是一个单例
INSTANCE;
//自定义的一些方法
public void method(){}
}
```
这种写法比较简洁,但是不太便于阅读和理解,所以实际开发中应用得比较少,而且由于枚举类是不能通过反射来创建实例的(反射方法newInstance中判断是枚举类型,会抛出IllegalArgumentException异常),所以可以防止反射。而且由于枚举类型的反序列化是通过java.lang.Enum的valueOf方法来实现的,不能自定义序列化方法,可以防止通过序列化来创建多个单例。
### 如何解决序列化时可以创建出单例对象的问题?
如果将单例对象序列化成字节序列后,然后再反序列成对象,那么就可以创建出一个新的单例对象,从而导致单例不唯一,避免发生这种情况的解决方案是在单例类中实现readResolve()方法。
```java
public class Singleton implements java.io.Serializable {
private Object readResolve() {
return INSTANCE;
}
}
```
通过实现readResolve方法,ObjectInputStream实例对象在调用readObject()方法进行反序列化时,就会判断相应的类是否实现了readResolve()方法,如果实现了,就会调用readResolve()方法返回一个对象作为反序列化的结果,而不是去创建一个新的对象。
### volatile关键字有什么用?怎么理解可见性,一般什么场景去用可见性?
当线程进行一个volatile变量的写操作时,JIT编译器生成的汇编指令会在写操作的指令后面加上一个“lock”指令。
Java代码如下:
```java
instance = new Singleton(); // instance是volatile变量
转变成汇编代码,如下。
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
```
“lock”有三个作用:
1.将当前CPU缓存行的数据会写回到系统内存。
2.这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效。
3.确保指令重排序时,内存屏障前的指令不会排到后面去,内存屏障后的指令不会排到前面去。
可见性可以理解为一个线程的写操作可以立即被其他线程得知。为了提高CPU处理速度,CPU一般不直接与内存进行通信,而是将系统内存的数据读到内部缓存,再进行操作。对于普通的变量,修改完不知道何时会更新到系统内存。但是如果是对volatile修饰的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在的缓存行的数据立即写回到系统内存。但是即便写回到系统内存,其他CPU中的缓存行数据还是旧的,为了保证数据一致性,其他CPU会嗅探在总线上传播的数据来检查自己的缓存行的值是否过期,当CPU发现缓存行对应的内存地址被修改,那么就会将当前缓存行设置为无效,下次当CPU对这个缓存行上的数据进行修改时,会重新从系统内存中把数据读到处理器缓存里。
##### 使用场景
##### 读写锁
如果需要实现一个读写锁,每次只能一个线程去写数据,但是有多个线程来读数据,就synchronized同步锁来对set方法加锁,get方法不加锁, 使用volatile来修饰变量,保证内存可见性,不然多个线程可能会在变量修改后还读到一个旧值。
```java
volatile Integer a;
//可以实现一写多读的场景,保证并发修改数据时的正确。
set(Integer c) {
synchronized(this.a) {
this.a = c;
}
}
get() {
return a;
}
```
##### 状态位
用于做状态位标志,如果多个线程去需要根据一个状态位来执行一些操作,使用volatile修饰可以保证内存可见性。
用于单例模式用于保证内存可见性,以及防止指令重排序。
### Java中线程的状态是怎么样的?
在操作系统中,线程等同于轻量级的进程。
![img](../static/4621.png)
所以传统的操作系统线程一般有以下状态
1. **新建状态**:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
2. **就绪状态**:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
3. **运行状态:**
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
4. **阻塞状态:**
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
- 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
- 同步阻塞:线程在获取 synchronized同步锁失败(因为同步锁被其他线程占用)。
- 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
5. **死亡状态:**
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
但是Java中Thread对象的状态划分跟传统的操作系统线程状态有一些区别。
```java
public enum State {
NEW,//新建态
RUNNABLE,//运行态
BLOCKED,//阻塞态
WAITING,//等待态
TIMED_WAITING,//有时间限制的等待态
TERMINATED;//死亡态
}
```
![线程状态图](../static/watermark.jpeg)
#### NEW 新建态
处于NEW状态的线程此时尚未启动,还没调用Thread实例的start()方法。
#### RUNNABLE 运行态
表示当前线程正在运行中。处于RUNNABLE状态的线程可能在Java虚拟机中运行,也有可能在等待其他系统资源(比如I/O)。
> Java线程的**RUNNABLE**状态其实是包括了传统操作系统线程的**ready**和**running**两个状态的。
#### BLOCKED 阻塞态
阻塞状态。线程没有申请到synchronize同步锁,就会处于阻塞状态,等待锁的释放以进入同步区。
#### WAITING 等待态
等待状态。处于等待状态的线程变成RUNNABLE状态需要其他线程唤醒。
调用如下3个方法会使线程进入等待状态:
- Object.wait():使当前线程处于等待状态直到另一个线程调用notify唤醒它;
- Thread.join():等待线程执行完毕,底层调用的是Object实例的wait()方法;
- LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度。
#### TIMED_WAITING 超时等待状态
超时等待状态。线程等待一个具体的时间,时间到后会被自动唤醒。
调用如下方法会使线程进入超时等待状态:
- Thread.sleep(long millis):使当前线程睡眠指定时间;
- Object.wait(long timeout):线程休眠指定时间,等待期间可以通过notify()/notifyAll()唤醒;
- Thread.join(long millis):等待当前线程最多执行millis毫秒,如果millis为0,则会一直执行;
- LockSupport.parkNanos(long nanos): 除非获得调用许可,否则禁用当前线程进行线程调度指定时间;
- LockSupport.parkUntil(long deadline):同上,也是禁止线程进行调度指定时间;
#### TERMINATED 终止态
终止状态。此时线程已执行完毕。
#### 状态转换
1.BLOCKED与RUNNABLE状态的转换
处于BLOCKED状态的线程是因为在等待锁的释放,当获得锁之后就转换为RUNNABLE状态。
2.WAITING状态与RUNNABLE状态的转换
**Object.wait()**,**Thread.join()**和**LockSupport.park()**这3个方法可以使线程从RUNNABLE状态转为WAITING状态。
3.TIMED_WAITING与RUNNABLE状态转换
TIMED_WAITING与WAITING状态类似,只是TIMED_WAITING状态等待的时间是指定的。
调用**Thread.sleep(long)**,**Object.wait(long)**,**Thread.join(long)**会使得RUNNABLE状态转换为TIMED_WAITING状态
### wait(),join(),sleep()方法有什么作用?
首先需要对wait(),join(),sleep()方法进行介绍。
#### Object.wait()方法是什么?
调用wait()方法前线程必须持有对象Object的锁。线程调用wait()方法后,会释放当前的Object锁,进入锁的monitor对象的等待队列,直到有其他线程调用notify()/notifyAll()方法唤醒等待锁的线程。
需要注意的是,其他线程调用notify()方法只会唤醒单个等待锁的线程,如果有多个线程都在等待这个锁的话,不一定会唤醒到之前调用wait()方法的线程。
同样,调用notifyAll()方法唤醒所有等待锁的线程之后,也不一定会马上把时间片分给刚才放弃锁的那个线程,具体要看系统的调度。
#### Thread.join()方法是什么?
join()方法是Thread类的一个实例方法。它的作用是让当前线程陷入“等待”状态,等join的这个线程threadA执行完成后,再继续执行当前线程。
实现原理是join()方法本身是一个sychronized修饰的方法,也就是调用join()这个方法需要先获取threadA的锁,获得锁之后再调用wait()方法来进行等待,一直到threadA执行完成后,threadA会调用notify_all()方法,唤醒所有等待的线程,当前线程才会结束等待。
```java
Thread threadA = new Thread();
threadA.join();
```
join()方法的源码:
```java
public final void join() throws InterruptedException {
join(0);//0的话代表没有超时时间一直等下去
}
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
```
这是jvm中Thead的源码,在线程执行结束后会调用notify_all来唤醒等待的线程。
```java
//一个c++函数:
void JavaThread::exit(bool destroy_vm, ExitType exit_type) ;
//里面有一个贼不起眼的一行代码
ensure_join(this);
static void ensure_join(JavaThread* thread) {
Handle threadObj(thread, thread->threadObj());
ObjectLocker lock(threadObj, thread);
thread->clear_pending_exception();
java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
java_lang_Thread::set_thread(threadObj(), NULL);
//同志们看到了没,别的不用看,就看这一句
//thread就是当前线程,是啥?就是刚才例子中说的threadA线程
lock.notify_all(thread);
thread->clear_pending_exception();
}
```
#### sleep()方法是什么?
sleep方法是Thread类的一个静态方法。它的作用是让当前线程睡眠一段时间。:**sleep方法是不会释放当前线程持有的锁,而wait方法会。**
sleep与wait方法的区别:
- wait可以指定时间,也可以不指定;而sleep必须指定时间。
- wait释放cpu资源,同时释放锁;sleep释放cpu资源,但是不释放锁,所以易死锁。(调用join()方法也不会释放锁)
- wait必须放在同步块或同步方法中,而sleep可以再任意位置。
参考文章:
http://redspider.group:4000/article/01/4.html
https://www.jianshu.com/p/5d88b122a050
### Thread.sleep(),Object.wait(),LockSupport.park()有什么区别?
1.这三个方法都会让线程挂起,释放CPU时间片,进入到阻塞态。但是Object.wait()需要释放锁,所以必须在synchronized同步锁中使用,同理配套的Object.notify()也是。而Thead.sleep(),LockSupport.park()不需要在synchronized同步锁中使用,并且在调用时也不会释放锁。
2.由于Thread.sleep()没有对应的唤醒线程的方法,所以必须指定超时时间,超过时间后,线程恢复。所以调用Thread.sleep()后的线程一般是出于TIME_WAITING状态,而调用了Object.wait(),LockSupport.park()的方法是进入到WAITING状态。
3.Object.wait()对应的唤醒方法为Object.notify(),LockSupport.park()对应的唤醒方法为LockSupport.unpark()。
4.在代码中必须能保证wait方法比notify方法先执行,如果notify方法比wait方法早执行的话,就会导致因wait方法进入休眠的线程接收不到唤醒通知的问题。而park、unpark则不会有这个问题,我们可以先调用unpark方法释放一个许可证,这样后面线程调用park方法时,发现已经许可证了,就可以直接获取许可证而不用进入休眠状态了。(**LockSupport.park() 的实现原理是通过二元信号量做的阻塞,要注意的是,这个信号量最多只能加到1,也就是无论执行多少次unpark()方法,也最多只会有一个许可证。**)
5.三种方法让线程进入阻塞态后,都可以响应中断,也就是调用Thread.interrupt()方法会设置中断标志位,之前执行Thread.sleep(),Object.wait()了的线程会抛出InterruptedException异常,然后需要代码进行处理。而调用了park()方法的线程在响应中断只会相当于一次正常的唤醒操作(等价于调用unpark()方法),让线程唤醒,继续执行后面的代码,不会抛出InterruptedException异常。
![img](../static/5bff9535e4b04dd2799a6ae8.png)
参考链接:
https://blog.csdn.net/u013332124/article/details/84647915
### 谈一谈你对线程中断的理解?
在Java中认为,一个线程不应该由其他线程来强制中断或者停止,所以一些会强制中断线程的方法Thread.stop(), Thread.suspend()方法都已经废弃了。所以一般是通过调用thread.interrupt();方法来设置线程的中断标识,
1.这样如果线程是处于阻塞状态,会抛出InterruptedException异常,代码可以进行捕获,进行一些处理。(例如Object#wait、Thread#sleep、BlockingQueue#put、BlockingQueue#take。其中BlockingQueue主要调用conditon.await()方法进行等待,底层通过LockSupport.park()实现)
2.如果线程是处于RUNNABLE状态,也就是正常运行,调用thread.interrupt();只是会设置中断标志位,不会有什么其他操作。
```java
//将线程的中断标识设置为true
thread.interrupt();
//判断线程的中断标识是否为true
thread.isInterrupted()
//会返回当前的线程中断状态,并且重置线程的中断标识,将中断标识设置为false
thread.interrupted()
```
### 线程执行的任务可以终止吗?
##### 1.设置中断
FutureTask提供了cancel(boolean mayInterruptIfRunning)方法来取消任务,并且
如果入参为false,如果任务已经在执行,那么任务就不会被取消。
如果入参为true,如果任务已经在执行,那么会调用Thread的interrupt()方法来设置线程的中断标识,如果线程处于阻塞状态,会抛出InterruptedException异常,如果正常状态只是设置标志位,修改interrupted变量的值。所以如果要取消任务只能在任务内部中调用thread.isInterrupted()方法获取当前线程的中断状态,自行取消。
##### 2.线程的stop方法
线程的stop()方法可以让线程停止执行,释放所有的锁,抛出ThreadDeath这种Error。但是在释放锁之后,没有办法让受这些锁保护的资源,对象处于一个安全,一致的状态。(例如有一个变量a,本来的值是0,你的线程任务是将a++后然后再进行a--。正常情况下任务执行完之后,其他线程取到这个变量a的值应该是0,但是如果之前调用了Thread.stop方法时,正好是在a++之后,那么变量a就会是1,这样其他线程取到的a就是出于不一致的状态。)
### 让线程顺序执行有哪些方法?
##### 1.主线程Join
就是调用threadA.start()方法让线程A先执行,然后主线程调用threadA.join()方法,然后主线程进入TIME_WAITING状态,直到threadA执行结束后,主线程才能继续往下执行,执行线程B的任务。(join方法的底层实现其实是调用了threadA的wait()方法,当线程A执行完毕后,会自动调用notifyAll()方法唤醒所有线程。)
示例代码如下:
```java
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
//执行threadA的任务
}
});
Thread threadB= new Thread(new Runnable() {
@Override
public void run() {
//执行threadB的任务
}
});
//执行线程A任务
threadA.start();
//主线程进行等待
threadA.join();
//执行线程B的任务
threadB.start();
```
##### 子线程Join
就是让线程B的任务在执行时,调用threadA.join()方法,这样就只有等线程A的任务执行完成后,才会执行线程B。
```java
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
//执行threadA的任务
}
});
Thread threadB= new Thread(new Runnable() {
@Override
public void run() {
//子线程进行等待,知道threadA任务执行完毕
threadA.join();
//执行threadB的任务
}
});
//执行线程A任务
threadA.start();
//执行线程B的任务
threadB.start();
```
##### 单线程池法
就是使用Executors.newSingleThreadExecutor()这个线程池,这个线程池的特点就是只有一个执行线程,可以保证任务按顺序执行。
```java
ExecutorService pool = Executors.newSingleThreadExecutor();
//提交任务A
executorService.submit(taskA);
//提交任务B
executorService.submit(taskB);
```
##### 等待通知法(wait和notify)
就是在线程B中调用Object.waiting()方法进行等待,线程A执行完毕后调用Object.notify()方法进行唤醒。(这种方法有两个缺点,一个是Object.waiting()和notify()方法必须在同步代码块中调用,第二个是如果线程A执行过快,先调用了object.notify()方法,就会导致线程B后面一直得不到唤醒。)
```java
final Object object = new Object();
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
//执行threadA的任务
synchronized(object) {
object.notify();
}
}
});
Thread threadB= new Thread(new Runnable() {
@Override
public void run() {
synchronized(object) {
//子线程进行等待,知道threadA任务执行完毕
object.wait();
//执行threadB的任务
}
}
});
```
##### 等待通知法(await和singal)
具体实现就是Reentrantlock可以创建出一个Condition实例queue,可以认为是一个等待队列,线程B调用queue.await()就会进行等待,直到线程A执行完毕调用queue.signal()来唤醒线程B。
```java
final ReentrantLock lock = new ReentrantLock();
final Condition queue1 = lock.newCondition();
final Object object = new Object();
final Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
//执行threadA的任务
lock.lock();
try {
//唤醒线程B的任务
queue1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行了任务A2");
lock.unlock();
}
});
final Thread threadB= new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
//子线程进行等待,知道threadA任务执行完毕
try {
queue1.await();
System.out.println("执行了任务B2");
} catch (InterruptedException e) {
e.printStackTrace();
}
//执行threadB的任务
lock.unlock();
}
});