多线程基础
★★★第十七章 多线程基础
程序的进程和线程
相关概念
- 程序(
Program
):是为了完成特定任务,用某种语言编写的一组指令的集合。是静态的概念。 - 进程(
Process
):运行中的程序。进程是程序的一次执行过程,或是正在运行的一个程序,是动态过程:有它自身的产生、存在和消亡的过程。是动态的概念。 - 线程(
Thread
):线程是由进程创建的,是进程的一个实体。一个进程可以拥有多个线程。- 单线程:同一个时刻,只允许执行一个线程。
- 多线程:同一个时刻,可以执行多个线程。比如:一个QQ进程,同时打开多个聊天窗口;一个迅雷进程,同时下载多个文件。
- 并发:在同一个时间段,多个任务交替执行,造成一种“貌似同时执行”的错觉,简单的说,单核
cpu
实现的多任务就是并发。 - 并行:同一个时刻,多个任务同时执行。多核
cpu
可以实现并行。
★★★线程基本使用
- 创建线程的两种方式:
- 继承
Thread
类,重写run
方法。 - 实现
Runnable
接口,重写run
方法。
- 继承
1.继承Thread创建线程
1 | package com.f.chapter17.threaduse; |
输出结果为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19主线程:main。 i的值为0
喵喵... 线程名:Thread-0
喵喵... 线程名:Thread-0
主线程:main。 i的值为1
喵喵... 线程名:Thread-0
主线程:main。 i的值为2
喵喵... 线程名:Thread-0
主线程:main。 i的值为3
主线程:main。 i的值为4
喵喵... 线程名:Thread-0
喵喵... 线程名:Thread-0
主线程:main。 i的值为5
喵喵... 线程名:Thread-0
主线程:main。 i的值为6
喵喵... 线程名:Thread-0
主线程:main。 i的值为7
喵喵... 线程名:Thread-0
主线程:main。 i的值为8
......
★★★多线程机制
以上面的代码为例讲解多线程机制。
首先,当进程
ThreadUse
开始运行后,其会启动主线程(即main
线程)。由于
Cat
类继承了Thread
类,所以该类就可以当作线程使用。于是cat.start();
就相当于main
线程又启动了一个子线程Thread-0
,并且需要注意:当main
线程启动一个子线程Thread-0
后,主线程(main
线程)不会阻塞,会继续执行。然后
main
线程和Thread-0
线程同时执行,可以用下面的图来表示 进程ThreadUse
、main
线程和Thread-0
线程的关系:当
main
线程输出完毕后,main
线程退出,但此时Thread-0
线程还在执行(因为main
线程被设置为输出60次,Thread-0
被设置为输出80次,所以main
线程执行完毕后,Thread-0
线程还在执行),所以进程ThreadUse
还在运行,并没有退出。直到
Thread-0
线程输出完毕退出后,进程ThreadUse
才真正执行结束并退出。
注意:
main
线程在启动了子线程Thread-0
后,它自己并不会阻塞,会继续执行。main
线程在执行结束退出后,它所启动的子线程有可能还在继续执行,没有退出,则这些线程所在的进程就会继续运行,没有退出。main
线程在启动了子线程Thread-0
后,依然可以启动其他子线程。同时,子线程也可以继续启动其他子线程,即不仅只有main
线程才能启动子线程。
为什么是start而不是run
如果在之前的代码的
main
方法中,直接调用cat.run();
方法而不是使用cat.start();
方法,就相当于没有创建子线程,而是main
方法直接调用cat.run()
方法,于是输出结果如下:1
2
3
4
5
6喵喵... 线程名:main
喵喵... 线程名:main
喵喵... 线程名:main
喵喵... 线程名:main
喵喵... 线程名:main
......就变成了串行化的执行,即先把
cat.run()
方法执行完,再执行main
中的输出的方法。同时也可以看到,输出由
喵喵... 线程名:Thread-0
变为喵喵... 线程名:main
。我们来看
start
的源码:首先会进入
Thread
类的start
方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public synchronized void start() {
...
group.add(this);
boolean started = false;
try {
start0();
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
类的start0
方法:1
private native void start0();
这是一个
native
方法,由JVM
来调用,底层是C/C++
。真正实现多线程效果的,是
start0
方法。
2.Runnable创建线程
前面我们用类继承
Thread
类来创建线程。但是Java
是单继承的,在某些情况下一个类可能已经继承了某个父类,这时再用继承Thread
类的方法来创建线程显然不可能了。所以我们可以通过实现
Runnable
接口来创建线程。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
37package com.f.chapter17.threaduse;
/**
* @author fzy
* @date 2023/7/1 21:55
* 通过实现接口Runnable来创建线程
*/
public class ThreadUse02 {
public static void main(String[] args) {
Dog dog = new Dog();
//dog.start(); //由于Runnable没有start方法,所以不能这么做
//使用代理模式
Thread dogThread = new Thread(dog);
dogThread.start();
}
}
class Dog implements Runnable {
int cnt = 0;
public void run() {
while (true) {
cnt++;
System.out.println("小狗汪汪叫... " + "线程为:" + Thread.currentThread().getName() + " cnt为:" + cnt);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (cnt == 10) {
break;
}
}
}
}在使用 “实现
Runnable
接口,重写run
方法” 来创建线程时,由于Runnable
接口并没有start
方法,所以我们无法使用dog.start();
来创建子线程。为此,我们用代理模式来使得能够创建子线程。具体操作为:
- 将
dog
对象赋给Thread
类的target
。 - 通过
Thread
对象的start
方法,来调用start0
方法以创建线程。 - 该创建的线程发现
target
非空,所以会调用该target
的run
方法,即dog
对象的run
方法,由此实现了线程的创建。
即,将
Thread
对象作为Dog
对象的代理,去帮助Dog
对象创建线程并调用Dog
对象的run
方法,使用到了静态代理模式。- 将
Thread和Runnable的区别
从
Java
的设计来看,通过继承Thread
或者实现Runnable
接口来创建线程本质上没有区别,从jdk
帮助文档我们可以看到Thread
类本身就实现了Runnable
接口。实现
Runnable
接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制。如下面代码所示,
T
是实现了Runnable
接口的类。1
2
3
4
5
6
7T t = new T("hello");
Thread thread01 = new Thread(t);
Thread thread02 = new Thread(t);
thread01.start();
thread02.start();
System.out.println("主线程执行完毕");我们既用对象
t
创建了线程thread01
,又用对象t
创建了线程thread02
,这两个线程调用的都是t
的run
方法,由此实现了多个线程共享一个资源的情况,即**这两个线程处理的都是同一个对象t
**。
线程终止
当线程完成任务后,会自动退出。
通过使用变量来控制
run
方法退出的方式停止线程,即通知方式。关键在于
main
线程能够控制t
线程中的通知变量。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
46package com.f.chapter17.threadexit;
/**
* @author fzy
* @date 2023/7/2 10:59
*/
public class ThreadExit {
public static void main(String[] args) {
T t = new T();
Thread thread = new Thread(t);
thread.start();
System.out.println("主线程休眠10秒...");
try {
Thread.sleep(10 * 1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//main线程通知t线程退出
t.setLoop(false);
}
}
class T implements Runnable {
private int cnt = 0;
//设置一个控制变量
private boolean loop = true;
public void run() {
while (loop) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程还在运行中... " + (++cnt));
}
}
public void setLoop(boolean loop) {
this.loop = loop;
}
}
★线程常用方法
第一组方法:
setName
:设置线程名称,使之与参数name
相同。getName
:返回该线程的名称。start
:使该线程开始执行,Java
虚拟机底层会调用该线程的start0
方法。run
:调用线程对象的run
方法。setPriority
:更改线程的优先级。getPriority
:获取线程的优先级。sleep
:在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。interrupt
:中断线程。中断线程不是终止线程。
- 注意:
start
底层会创建新的线程,去调用run
,run
就是一个简单的方法调用,只用run
方法并不会启动新线程(在 “为什么是start而不是run” 小节已经说过)。- 线程优先级的范围:
MAX_PRIORITY
:10MIN_PRIORITY
:1NORM_PRIORITY
:5
interrupt
,中断线程,但并没有真正的结束线程,一般用于中断正在休眠的线程。sleep
,线程的静态方法,使当前线程休眠。
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
53package com.f.chapter17.threadmethod;
/**
* @author fzy
* @date 2023/7/2 11:16
*/
public class ThreadMethod {
public static void main(String[] args) {
T t = new T();
Thread thread = new Thread(t);
thread.setName("自定义的线程名:t");
thread.setPriority(Thread.MAX_PRIORITY);
System.out.println(thread.getName() + "的优先级为:" + thread.getPriority());
thread.start();
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("main线程 - " + i);
}
//main线程对t线程发出中断
thread.interrupt();
}
}
class T implements Runnable {
/*
* 下面的run方法的逻辑为:
* 1.先输出十次 Thread.currentThread().getName() + " - " + i
* 2.然后线程开始休眠20秒
* 3.如果线程被中断,就会退出休眠状态
* 4.然后继续下一个循环,即重复1、2、3的步骤(这里就说明中断线程不是终止线程,因为线程被中断后又开始下一个循环)
* */
public void run() {
while (true) {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " - " + i);
}
try {
System.out.println("开始休眠 20 秒...");
Thread.sleep(20 * 1000);
} catch (InterruptedException e) {
//当该线程执行到一个 interrupt 方法时,就会 catch 一个异常,然后在 catch 块可以加入自己的业务代码
//InterruptedException 是捕获到一个中断异常
System.out.println(Thread.currentThread().getName() + "的休眠被 interrupt 了。");
}
}
}
}输出结果为:
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自定义的线程名:t的优先级为:10
自定义的线程名:t - 0
自定义的线程名:t - 1
自定义的线程名:t - 2
自定义的线程名:t - 3
自定义的线程名:t - 4
自定义的线程名:t - 5
自定义的线程名:t - 6
自定义的线程名:t - 7
自定义的线程名:t - 8
自定义的线程名:t - 9
开始休眠 20 秒...
main线程 - 0
main线程 - 1
main线程 - 2
main线程 - 3
main线程 - 4
自定义的线程名:t的休眠被 interrupt 了。
自定义的线程名:t - 0
自定义的线程名:t - 1
自定义的线程名:t - 2
自定义的线程名:t - 3
自定义的线程名:t - 4
自定义的线程名:t - 5
自定义的线程名:t - 6
自定义的线程名:t - 7
自定义的线程名:t - 8
自定义的线程名:t - 9
开始休眠 20 秒...
自定义的线程名:t - 0
自定义的线程名:t - 1
自定义的线程名:t - 2
自定义的线程名:t - 3
自定义的线程名:t - 4
自定义的线程名:t - 5
......第二组方法:
Thread.yield()
:线程的礼让。调用该方法的线程会让出cpu
,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功,受内核态的CPU
资源的影响。join
:线程的插队。插队的线程一旦插队成功,则肯定先执行完插入的线程所有的任务。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
46package com.f.chapter17.threadmethod;
/**
* @author fzy
* @date 2023/7/2 21:40
* 案例:main线程创建一个子线程,每隔1s输出hello,输出20次,
* 主线程每隔1秒,输出hi,输出20次。
* 要求:两个线程同时执行,当主线程输出5次后,就让子线程运行完毕,主线程再继续输出。
*/
public class ThreadMethod02 {
public static void main(String[] args) {
PrintHello printHello = new PrintHello();
Thread thread = new Thread(printHello);
thread.start();
for (int i = 0; i < 20; i++) {
System.out.println("hi " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (i == 5) { //当主线程输出5次后,就让子线程运行完毕,主线程再继续输出
try {
thread.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
class PrintHello implements Runnable {
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("hello " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}