写在前面
有时候在项目开发中,时不时的会遇到多线程方面的问题。可是要想驾驭它,就必须了解Java多线程方面的知识,认识它的内在。结合我自己的开发经验以及对Java多线程的一些学习过程,将写几篇关于Java多线程的博客,分享给大家,如有错误,请大家指正。
目录
-
线程和进程
-
Thread和Runnable
-
线程的状态
-
生产者和消费者模型:wait/notify
-
后台线程
-
interrupt
-
线程合并join
-
UncaughtExceptionHandler
线程和进程
线程和进程的概念相信大家都知道,我来说下我的理解。操作系统可以将系统的一些资源,如CPU,内存等交给一段运行起来的程序,即进程。进程之间一般是相互独立的,比如Nginx的工作模型中有一个master进程,多个worker进程,如果有worker进程“挂”了,并不会影响其他worker进程处理请求。在进程内部可以有多个并发执行流,即线程。线程的存在必须依赖进程,同时一个进程内部的多个线程,它们共享进程的资源,相互之间可以影响。线程是轻量级的进程,它的创建、销毁、切换都比进程要小。
|
Thread和Runnable
说起Java的多线程,我们脑海里面可能有如下的想法: 要么extends Thread,然后重写run(),调用start()
或者implements Runnable , 实现run(),将Runnable实例传递给Thread构造方法,调用start()
呵呵,其实,以前,我也是这么想的。
那么,在Java线程中,Thread和Runnable到底是什么,各自扮演着什么角色?
看一下Thread的类声明吧:
1
|
public
class
Thread
implements
Runnable
|
从上面的来看,好像它们是“一家人”。
我们可以扫一扫Thread的源码,发现:
1
2
3
4
5
|
private
Runnable target;
public
Thread(Runnable target) {
init(
null
, target,
"Thread-"
+ nextThreadNum(),
0
);
}
|
其实,Runnable接口扮演的是一个任务的角色,通过提供run()来明确这个任务要做什么。对于Thread而言,默认情况下,它调用的就是你传递给它的Runnable的run()方法。Thread类中的target就是用来接收你传递进来的任务的,这样任务是任务,线程是线程,线程可以执行任务,就这么简单!
|
线程的状态
线程的状态很重要! 就我自己的感觉而言,我们写多线程方面的代码,就是在进行线程的状态转换从而完成业务上的要求,如果我们不清楚线程的状态,那么我们将无从下手,更加看不懂别人的代码!
根据我的理解,画了张图,一起来看下吧。
![1.png wKioL1TDQY-hNnPdAACMYASm-2g174.jpg](https://i-blog.csdnimg.cn/blog_migrate/b9ace7712a1d3652356c89354e79a4bf.jpeg)
线程有5个基本状态:新建、就绪、运行、阻塞、死亡。
当我们新建了一个Thread类的对象时,在JVM中只是存在了一个Thread对象而已,此时OS中并没有真正存在一个线程,只有一个操作线程的外壳,此时为新建状态。
如果我们调用了start()方法,那么将从新建状态转为就绪状态。start()方法的调用结束使得OS完成了内核调用,线程已经存在。需要注意的是,start()调用,run()方法一般并不会同步调用,因为run()的调用是CPU的选择,什么时候调用,我们无法准确干预。
当start()调用结束,系统相关资源准备完毕,那么就会进入运行状态,调用run()。如果在run过程中,失去了CPU时间片,将回到就绪状态。比如yield()方法,这个方法会让出CPU的使用权,希望给其他人用一用,当然这只是它的想法,也许yield()调用进入就绪状态后,马上CPU又调度他执行。
运行状态中,如果发生了sleep/等待锁/IO阻塞/wait等,将进入阻塞状态。如果在阻塞状态中,睡好了/锁来了/IO完成了/notify来了,那么就完成了阻塞状态的解除,从而进入到就绪状态,重新等待CPU的调度。
正常情况下,当run()结束或者发生了未捕获的异常,就会进入死亡状态。
|
生产者和消费者模型:wait/notify
前面说了那么多理论,下面我们就来点代码练练手吧!
业务场景: 有一个篮子,容量是10个,生产者生产馒头放入其中,消费者从篮子里面拿馒头吃。如果生产者发现篮子里面的馒头已经满了,就不再生产,消费者可以消费。如果消费者发现篮子里面的馒头空了,就停止消费,生产者可以生产。
馒头Model:
篮子容器:
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
|
class
LanZhi{
private
int
size;
private
int
index =
0
;
private
ManTou[] foods;
public
LanZhi(
int
size){
this
.size = size;
this
.foods =
new
ManTou[size];
}
public
synchronized
void
produce(ManTou m){
if
(index == size){
try
{
this
.wait();
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
foods[index] = m;
++index;
System.out.println(
"生产一个馒头,现在馒头总共有:"
+ index);
this
.notify();
}
public
synchronized
ManTou eat(){
ManTou m =
null
;
if
(index ==
0
){
try
{
this
.wait();
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
m = foods[--index];
System.out.println(
"吃掉一个馒头,现在馒头总共有:"
+ index);
this
.notify();
return
m;
}
}
|
生产任务:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
class
ProduceRunnable
implements
Runnable{
private
LanZhi l;
public
ProduceRunnable(LanZhi l){
this
.l = l;
}
@Override
public
void
run() {
while
(
true
){
l.produce(
new
ManTou());
try
{
Thread.sleep(
300
);
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
}
}
|
消费任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
class
ConsumeRunnable
implements
Runnable{
private
LanZhi l;
public
ConsumeRunnable(LanZhi l){
this
.l = l;
}
@Override
public
void
run() {
while
(
true
){
l.eat();
try
{
Thread.sleep(
1000
);
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
}
}
|
主线程调用:
1
2
3
4
5
|
public
static
void
main(String[] args) {
LanZhi l =
new
LanZhi(
10
);
new
Thread(
new
ProduceRunnable(l)).start();
new
Thread(
new
ConsumeRunnable(l)).start();
}
|
说明: wait/notify这两个方法都是在object中定义的方法,都应该在临界区域内发生调用(也就是synchronized区域),否则会发生异常。一个线程好不容易拿到了“门票”,为什么要调用wait呢?因为有些时候,我们拿到了锁,准备进行一些操作的时候,发现并不满足业务上的一些要求,就需要wati了。就好比,我们进入了卫生间,关上了门,蹲在了马桶上,此时发现,没有带纸,没有办法,只好退出卫生间!
|
后台线程
线程分为前台线程和后台线程。 首先需要注意的是,JVM认为一旦所有的前台线程运行完毕,只剩下后台线程的话,那么就意味着程序要结束了。
举个例子:
1
2
3
4
5
6
7
8
|
class
Daemon
extends
Thread{
@Override
public
void
run() {
for
(
int
i =
0
; i <
10
; i++){
System.out.println(
"i : "
+ i);
}
}
}
|
主线程调用:
1
2
3
4
5
6
|
public
static
void
main(String[] args) {
Daemon d =
new
Daemon();
d.setDaemon(
true
);
d.start();
System.out.println(
"hello"
);
}
|
1
2
3
4
5
6
7
8
9
10
|
运行结果:
hello
i :
0
i :
1
i :
2
i :
3
i :
4
i :
5
i :
6
i :
7
|
hello的输出,意味着前台线程已经运行完毕了,那么JVM将会结束整个进程,并且不保证执行完后台线程。那么后台线程可以用来干嘛?比如Java的垃圾回收线程就是一个后台线程,而且它的调度优先级比较低,它不会随便去和其他线程争夺使用CPU。 |
interrupt
interrupt,即中断的意思,那中断意味着什么?
我们先看看下面的:
1
2
3
|
public
final
native
void
wait(
long
timeout)
throws
InterruptedException;
public
static
native
void
sleep(
long
millis)
throws
InterruptedException;
public
final
synchronized
void
join(
long
millis)
throws
InterruptedException
|
我们知道有些方法会抛出中断异常,那什么时候抛呢?
再去扫一眼Thread源码,发现:
1
2
3
4
5
6
7
|
public
static
boolean
interrupted() {
return
currentThread().isInterrupted(
true
);
}
public
boolean
isInterrupted() {
return
isInterrupted(
false
);
}
private
native
boolean
isInterrupted(
boolean
ClearInterrupted);
|
在默认情况下,显然,线程的中断标志是false。当我们调用了interrupted()后,实际上是将线程中断标志设置为true。
看一个小例子:
1
2
3
4
5
6
7
8
9
10
11
12
|
class
Thread1
extends
Thread{
@Override
public
void
run() {
try
{
Thread.sleep(
10000
);
System.out.println(
"sleep over..."
);
}
catch
(InterruptedException e) {
System.out.println(
"出现中断异常"
);
}
}
}
|
主线程调用:
1
2
3
|
Thread1 t1 =
new
Thread1();
t1.start();
t1.interrupt();
|
运行结果: 出现中断异常
说明interrupt()的调用,只是做了一个标记并产生异常抛出而已,并没有真正的唤醒线程。
|
线程合并join
线程合并?为什么多个线程要合并?什么时候会出现需要合并呢?怎么合并?
假设有这么一个业务排序场景:
我们有N个这样的块,每个块中存放数字,而且块间有序、块内是无序的,如下: 第一块数字范围:[0,1000] 第二块数字范围:[1001,2000] 第三块数字范围:[2001,3000] ...
分析: 不应该对所有块的所有数字来一次全排序,很显然的是,我们想利用块间有序这个条件,能不能对每一块进行排序。要知道,第一块排序和第二块排序,他们是相互独立的,没有锁的限制,完全可以并行进行。那我们的初步想法是,启动N个线程对N个块进行排序,当N个线程都结束,就完成了排序了。但是要知道,我们无法保证每个排序线程的完成顺序,但是业务上,我们确实需要当排序完毕后进行XXX操作。 当然,我们可以写一个循环进行遍历,来获取N个排序线程的状态,从而决定排序完毕后的操作什么时候进行。其实,Java已经提供了join(),来完成这方面的需要。
实例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
class
SortRunnable
implements
Runnable{
int
[] array;
public
SortRunnable(
int
[] array){
this
.array = array;
}
@Override
public
void
run() {
for
(
int
i =
0
; i < array.length ; i++){
for
(
int
j = i+
1
; j < array.length ; j++){
if
(array[i] > array[j]){
try
{
Thread.sleep(
1000
);
}
catch
(InterruptedException e) {
e.printStackTrace();
}
int
tmp = array[i] ^ array[j];
array[i] = tmp ^ array[i];
array[j] = tmp ^ array[j];
}
}
}
}
}
|
主线程调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
int
[] array1 = {
1
,
9
,
8
,
7
};
int
[] array2 = {
10
,
30
,
25
};
Thread t1 =
new
Thread(
new
SortRunnable(array1));
Thread t2 =
new
Thread(
new
SortRunnable(array2));
t1.start();
t2.start();
t1.join();
t2.join();
for
(
int
tmp : array1){
System.out.print(tmp +
" , "
);
}
System.out.println();
for
(
int
tmp : array2){
System.out.print(tmp +
" , "
);
}
|
|
UncaughtExceptionHandler
直接看一个例子,大家就能明白!
1
2
3
4
5
6
|
class
MyException
implements
UncaughtExceptionHandler{
@Override
public
void
uncaughtException(Thread t, Throwable e) {
System.out.println(
"捕捉到线程中未处理的异常"
);
}
}
|
主线程调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
Thread t =
new
Thread(){
@Override
public
void
run() {
System.out.println(
"start..."
);
Integer.parseInt(
"a"
);
System.out.println(
"end..."
);
}
};
t.setUncaughtExceptionHandler(
new
MyException());
t.start();
|
运行结果: start... 捕捉到线程中未处理的异常
说明: 如果在run()中出现了我们没有处理的异常,那么我们还有一次“后悔药”可以吃~
|
本文转自zfz_linux_boy 51CTO博客,原文链接:http://blog.51cto.com/zhangfengzhe/1607712,如需转载请自行联系原作者