Java基础:多线程
1. 多线程概述
1.1 多线程引入
由上图中程序的调用流程可知,这个程序只有一个执行流程,所以这样的程序就是单线程程序。假如一个程序有多条执行流程,那么,该程序就是多线程程序。
1.2 多线程概述
1.2.1 什么是进程?
进程就是正在运行的程序,是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。
1.2.2 多进程有什么意义呢?
单进程的计算机只能做一件事情,而我们现在的计算机都可以做多件事情。举例:一边玩游戏(游戏进程),一边听音乐(音乐进程)。也就是说现在的计算机都是支持多进程的,可以在一个时间段内执行多个任务。并且呢,可以提高CPU的使用率,解决了多部分代码同时运行的问题。
其实,多个应用程序同时执行都是CPU在做着快速的切换完成的。这个切换是随机的。CPU的切换是需要花费时间的,从而导致了效率的降低。
1.2.3 什么是线程?
线程是程序的执行单元,执行路径;是进程中的单个顺序控制流,是一条执行路径;一个进程如果只有一条执行路径,则称为单线程程序。一个进程如果有多条执行路径,则称为多线程程序。
1.2.4 多线程有什么意义呢?
多线程的存在,不是提高程序的执行速度。其实是为了提高应用程序的使用率。程序的执行其实都是在抢CPU的资源,CPU的执行权。多个进程是在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权。我们是不敢保证哪一个线程能够在哪个时刻抢到,所以线程的执行有随机性。
1.2.5 什么是并行、并发呢?
前者是逻辑上同时发生,指在某一个时间内同时运行多个程序;后者是物理上同时发生,指在某一个时间点同时运行多个程序。那么,我们能不能实现真正意义上的并发呢?答案是可以的,多个CPU就可以实现,不过你得知道如何调度和控制它们。
PS:
一个进程中可以有多个执行路径,称之为多线程。
一个进程中至少要有一个线程。
开启多个线程是为了同时运行多部分代码,每一个线程都有自己运行的内容,这个内容可以称为线程要执行的任务。
1.3 Java程序运行原理
Java 命令会启动 java 虚拟机,启动 JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 main 方法。所以 main方法运行在主线程中。在此之前的所有程序都是单线程的。
思考:JVM虚拟机的启动是单线程的还是多线程的?
答案:JVM启动时启动了多条线程,至少有两个线程可以分析的出来
执行main函数的线程,该线程的任务代码都定义在main函数中。
负责垃圾回收的线程。System类的gc方法告诉垃圾回收器调用finalize方法,但不一定立即执行。
2. 多线程的实现方案
由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。Java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。但是呢?Java可以去调用C/C++写好的程序来实现多线程程序。由C/C++去调用系统功能创建进程,然后由Java去调用这样的东西,然后提供一些类供我们使用。我们就可以实现多线程程序了。
2.1 多线程的实现方案一:继承Thread类,重写run()方法
定义一个类继承Thread类
覆盖Thread类中的run方法
直接创建Thread的子类对象创建线程
调用start方法开启线程并调用线程的任务run方法执行
package cn.itcast;
//多线程的实现方案一:继承Thread类,重写run()方法
//1、定义一个类继承Thread类。
class MyThread extends Thread {
private String name;
MyThread(String name) {
this.name = name;
}
// 2、覆盖Thread类中的run方法。
public void run() {
for (int x = 0; x < 5; x++) {
System.out.println(name + "...x=" + x + "...ThreadName="
+ Thread.currentThread().getName());
}
}
}
class ThreadTest {
public static void main(String[] args) {
// 3、直接创建Thread的子类对象创建线程。
MyThread d1 = new MyThread("黑马程序员");
MyThread d2 = new MyThread("中关村在线");
// 4、调用start方法开启线程并调用线程的任务run方法执行。
d1.start();
d2.start();
for (int x = 0; x < 5; x++) {
System.out.println("x = " + x + "...over..."
+ Thread.currentThread().getName());
}
}
}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
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
运行结果:
2.1.2 为什么要重写run()方法?
Thread类用于描述线程,线程是需要任务的。所以Thread类也有对任务的描述。这个任务就是通过Thread类中的run方法来体现。也就是说,run方法就是封装自定义线程运行任务的函数,run方法中定义的就是线程要运行的任务代码。所以只有继承Thread类,并复写run方法,将运行的代码定义在run方法中即可。
2.1.3 启动线程使用的是那个方法
启动线程调用的是start()方法,不是run()方法,run()方法只是封装了被线程执行的代码,调用run()只是普通方法的调用,无法启动线程。
2.1.4 线程能不能多次启动
不能,会出现IllegalThreadStateException非法的线程状态异常。
2.1.5 run()和start()方法的区别
run():仅仅是封装了被线程执行的代码,直接调用就是普通方法
start():首先是启动了线程,然后再由jvm去调用了该线程的run()方法
2.1.6 Thread类的基本获取和设置方法
public final String getName():获取线程的名称
public final void setName(String name):设置线程的名称
Thread(String name) :通过构造方法给线程起名字
思考:如何获取main方法所在的线程名称呢?
public static Thread currentThread() // 获取任意方法所在的线程名称1
1
2.2 多线程的实现方案二:实现Runnable接口
定义类实现Runnable接口
覆盖接口中的run方法,将线程的任务代码封装到run方法中
通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。为什么?因为线程的任务都封装在Runnable接口子类对象的run方法中。所以要在线程对象创建时就必须明确要运行的任务
调用线程对象的start方法开启线程
package cn.itcast;
//多线程的实现方案二:实现Runnable接口
//1、定义类实现Runnable接口。
class MyThread implements Runnable {
// 2、覆盖接口中的run方法,将线程的任务代码封装到run方法中。
public void run() {
show();
}
public void show() {
for (int x = 0; x < 5; x++) {
System.out.println(Thread.currentThread().getName() + "..." + x);
}
}
}
class ThreadTest {
public static void main(String[] args) {
MyThread d = new MyThread();
// 3、通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
// 4、调用线程对象的start方法开启线程。
t1.start();
t2.start();
}
}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
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
运行结果:
如何获取线程名称:Thread.currentThread().getName()
如何给线程设置名称:setName()、Thread(Runnable target, String name)
实现接口方式的好处:
可以避免由于Java单继承带来的局限性,所以,创建线程的第二种方式较为常用。
适合多个相同程序的代码去处理同一个资源的情况,把线程同程序的代码,数据有效分离,较好的体现了面向对象的设计思想。
2.3 多线程程序实现方案三:实现Callable接口
创建一个线程池对象,控制要创建几个线程对象。
public static ExecutorService newFixedThreadPool(int nThreads)1
1
这种线程池的线程可以执行:可以执行Runnable对象或者Callable对象代表的线程。
调用如下方法即可
Future<?> submit(Runnable task)
提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。
<T> Future<T> submit(Callable<T> task)
提交一个返回值的任务用于执行,返回一个表示任务的未决结果的 Future
结束线程:shutdown() 关闭线程
package cn.itcast;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Callable;
//Callable:是带泛型的接口。
//这里指定的泛型其实是call()方法的返回值类型。
class MyCallable implements Callable {
public Object call() throws Exception {
for (int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().getName() + ":" + x);
}
return null;
}
}
/*
* 多线程实现的方式3: A:创建一个线程池对象,控制要创建几个线程对象。 public static ExecutorService
* newFixedThreadPool(int nThreads) B:这种线程池的线程可以执行:
* 可以执行Runnable对象或者Callable对象代表的线程 做一个类实现Runnable接口。 C:调用如下方法即可 Future<?>
* submit(Runnable task) <T> Future<T> submit(Callable<T> task) D:我就要结束,可以吗? 可以。
*/
public class CallableDemo {
public static void main(String[] args) {
// 创建线程池对象
ExecutorService pool = Executors.newFixedThreadPool(2);
// 可以执行Runnable对象或者Callable对象代表的线程
pool.submit(new MyCallable());
pool.submit(new MyCallable());
// 结束
pool.shutdown();
}
}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
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
运行结果:
实现Callable的优缺点
好处:可以有返回值;可以抛出异常。
弊端:代码比较复杂,所以一般不用
2.4 匿名内部类方式使用多线程
new Thread(){代码…}.start(); //新创建一个线程并启动
new Thread(new Runnable(){代码…}).start(); //新创建一个线程并启动1
2
1
2
3. 线程调度和线程控制
3.1 线程调度
假如我们的计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到 CPU时间片,也就是使用权,才可以执行指令。那么Java是如何对线程进行调用的呢?
3.1.1 线程有两种调度模型
分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。
Java使用的是抢占式调度模型。
3.1.2 如何设置和获取线程优先级
public final intgetPriority(); //获取线程的优先级
public final voidsetPriority(int newPriority); //设置线程的优先级1
2
1
2
注意:线程默认的优先级是5;线程优先级的范围是:1-10;线程优先级高仅仅表示线程获取CPU的时间片的几率高,但是要在次数比较多,或者多次运行的时候才能卡到比较好的结果。
3.2 线程控制
4. 线程的生命周期
4.1 线程的状态
4.2 线程的生命周期图
5. 线程安全问题
5.1 判断一个程序是否会有线程安全问题的标准
是否是多线程环境
是否有共享数据
是否有多条语句操作共享数据
5.2 如何解决多线程安全问题呢?
基本思想:让程序没有安全问题的环境。
解决办法:同步机制:同步代码块、同步方法。把多个语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可。
5.2.1 解决线程安全问题实现1:同步代码块,格式如下
synchronized(对象){
需要同步的代码;
}1
2
3
1
2
3
package cn.itcast;
//卖票程序的同步代码块实现示例
class Ticket implements Runnable {
private int num = 10;
Object obj = new Object();
public void run() {
while (true) {
// 给可能出现问题的代码加锁
synchronized (obj) {
if (num > 0) {
// 显示线程名及余票数
System.out.println(Thread.currentThread().getName()
+ "...sale..." + num--);
}
}
}
}
}
class TicketDemo {
public static void main(String[] args) {
// 通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
// 调用线程对象的start方法开启线程。
t1.start();
t2.start();
t3.start();
t4.start();
}
}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
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