文章目录
零、本讲学习目标
1、了解多线程的概念
2、掌握多线程创建的三种方式
3、熟悉创建多线程三种方式的主要区别
一、进程概述
(一)进程定义
在一个操作系统中,每个独立执行的程序都可称之为一个进程,也就是“正在运行的程序”。例如同时运行的QQ、360安全卫士、Eclipse开发工具等。
进程 = 程序 + 执行。当把一个程序从磁盘中加载到内存中,CPU去运算和处理这个进程(运行起来的程序就是进程)。
(二)三维度看待进程模型
维度 | 说明 |
---|---|
从内存维度 | 每个进程都独占一块地址空间,CPU处理进程实际上就是处理这个进程内存的首地址到尾地址的数据库信息 |
从执行的逻辑维度 | 每一个进程都可以被CPU所处理和计算,此外,每一个进程也可以挂起,让其他进程得以处理。在同一个时刻,只能有一个进程被cpu所处理。总结:进程模型,在宏观上是并行处理的,但是微观上看,是“串行”处理的(单核)。如果是多核架构,宏观和微观上都是并行处理的。 |
时间维度 | 每个进程执行一段时间之后,肯定都完成了一定的工作量。即进程是随时间向前推进的。 |
(三)进程说明
- 在多任务操作系统中,表面上看是支持进程并发执行的,例如可以一边听音乐一边聊天,但实际上这些进程并不是在同一时刻运行的。
- 在计算机中,所有的应用程序都是由CPU执行的,对于一个CPU而言,在某个时间点只能运行一个程序,也就是说只能执行一个进程,操作系统会为每一个进程分配一段有限的CPU使用时间,CPU在这段时间中执行某个进程,然后会在下一段时间切换到另一个进程中去执行。
- 由于CPU运行速度非常快,能在极短的时间内在不同的进程之间进行切换,所以给人以同时执行多个程序的感觉。
(四)进程三种状态
1、执行态(Running)
一个进程正在被CPU运行,这个进程成为执行态进程。
2、就绪态(Ready)
一个进程由于执行了很长时间,主动挂起,让其它进程得以处理,这个进程称之为就绪态进程。
3、阻塞态(Blocking)
- 一个进程由于发生了某些阻塞操作,比如I/O事件
- 用户主动将进程挂起,比如sleep操作
针对这两类进程,即使把CPU让给这个进程,CPU也处理不了;这样挂起的进程称之为阻塞态进程。
二、线程概述
(一)线程定义
- 在多任务操作系统中,每个运行的程序都是一个进程,用来执行不同的任务,而在一个进程中还可以有多个执行单元同时运行,来同时完成一个或多个程序任务,这些执行单元可以看做程序执行的一条条线索,被称为线程。
- 操作系统中的每一个进程中都至少存在一个线程,当一个Java程序启动时,就会产生一个进程,该进程中会默认创建一个线程,在这个线程上会运行main()方法中的代码。
(二)单线程与多线程
1、单线程
单线程都是按照调用顺序依次往下执行,没有出现多段程序代码交替运行的效果,而多线程程序在运行时,每个线程之间都是独立的,它们可以并发执行。
2、多线程
多线程可以充分利用CUP资源,进一步提升程序执行效率。多线程看似是同时并发执行的,其实不然,它们和进程一样,也是由CPU控制并轮流执行的,只不过CPU运行速度非常快,故而给人同时执行的感觉。
三、线程创建
Java为多线程开发提供了非常优秀的技术支持,在Java中,可以通过三种方式来实现多线程。
(一)继承Thread类,重写run()方法
Thread类是java.lang包下的一个线程类,用来实现Java多线程。
1、创建多线程的操作步骤
- 创建一个Thread线程类的子类(子线程),同时重写Thread类的run()方法
- 创建该子类的实例对象,并通过调用start()方法启动线程
2、案例演示:三个线程并发执行
- 创建Example3301
package net.hw.lesson33;
/**
* 功能:利用Thread类实现多线程
* 作者:华卫
* 日期:2020年06月16日
*/
public class Example3301 {
public static void main(String[] args) {
// 创建线程对象
Thread thread1 = new SimpleThread();
Thread thread2 = new SimpleThread();
Thread thread3 = new SimpleThread();
// 启动线程对象
thread1.start();
thread2.start();
thread3.start();
System.out.println("程序到此执行完毕。");
}
}
class SimpleThread extends Thread {
private static int count = 0;
private int id;
public SimpleThread() {
count++;
id = count;
}
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("线程[" + id + "] i = " + i);
}
try {
// 线程睡眠1000毫秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 运行程序,查看结果
- 简要说明:启动了三个线程,并不是第一个线程执行完了之后再执行第二个线程,也不是第二个线程执行完了再执行第三个线程,三个线程是并发执行的,可以看到主线程的最后一条输出语句最先看到结果,因为三个线程还在循环执行过程中。
3、案例演示:动态输出欢迎文字
- 创建Example3302
package net.hw.lesson33;
/**
* 功能:动态输出欢迎文字
* 作者:华卫
* 日期:2020年06月16日
*/
public class Example3302 {
public static void main(String[] args) {
// 定义单词数组
char[] words = "欢迎访问泸州职业技术学院".toCharArray();
// 创建线程(匿名类对象)
Thread thread = new Thread() {
@Override
public void run() {
for (int i = 0; i < words.length; i++) {
System.out.print(words[i]);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
// 启动线程
thread.start();
}
}
- 运行程序,查看效果
(二)实现Runnable接口,重写run()方法
Java只支持类的单继承,如果某个类已经继承了其他父类,就无法再继承Thread类来实现多线程。在这种情况下,就可以考虑通过实现Runnable接口的方式来实现多线程。
1、创建多线程的操作步骤
- 创建一个Runnable接口的实现类,同时重写接口中的run()方法;
- 创建Runnable接口的实现类对象;
- 使用Thread有参构造方法创建线程实例,并将Runnable接口的实现类的实例对象作为参数传入;
- 调用线程实例的start()方法启动线程。
2、案例演示:动态输出欢迎文字
- 创建Example3303
package net.hw.lesson33;
/**
* 功能:动态输出欢迎文字
* 作者:华卫
* 日期:2020年06月16日
*/
public class Example3303 {
// 定义单词数组
private static char[] words = "欢迎访问泸州职业技术学院".toCharArray();
static class WelcomeThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < words.length; i++) {
System.out.print(words[i]);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
// 创建线程对象
Thread thread = new Thread(new WelcomeThread());
// 启动线程
thread.start();
}
}
- 运行程序,查看结果
- 在实际项目开发中,基于Runnable接口创建多线程,并不会先去创建Runnable的实现类,再将实现类对象作为参数传给Thread类的有参构造方法,而是直接采用匿名内部类方式创建Runnable接口对象作为参数传给Thread类的有参构造方法。
- 创建Example3304
package net.hw.lesson33;
/**
* 功能:动态输出欢迎文字
* 作者:华卫
* 日期:2020年06月16日
*/
public class Example3304 {
public static void main(String[] args) {
// 定义单词数组
char[] words = "欢迎访问泸州职业技术学院".toCharArray();
// 创建线程对象
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < words.length; i++) {
System.out.print(words[i]);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
// 启动线程
thread.start();
}
}
- 运行程序,查看结果
- 采用Lambda表达式来简化代码
- 让线程睡眠,还是使用TimeUnit来实现
- 创建Example3304_
package net.hw.lesson33;
import java.util.concurrent.TimeUnit;
/**
* 功能:动态输出欢迎文字
* 作者:华卫
* 日期:2020年06月16日
*/
public class Example3304_ {
public static void main(String[] args) {
// 定义单词数组
char[] words = "欢迎访问泸州职业技术学院".toCharArray();
// 创建线程对象
Thread thread = new Thread(() -> {
for (int i = 0; i < words.length; i++) {
System.out.print(words[i]);
try {
// 线程睡眠1000毫秒
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 启动线程
thread.start();
}
}
- 运行程序,查看结果
(三)实现Callable接口,重写call()方法
通过Thread类和Runnable接口实现多线程时,需要重写run()方法,但是由于该方法没有返回值,因此无法从多个线程中获取返回结果。为了解决这个问题,从JDK 5开始,Java提供了一个新的Callable接口,来满足这种既能创建多线程又可以有返回值的需求
。
1、创建多线程的使用方法
Callable接口实现多线程是通过Thread类的有参构造方法传入Runnable接口类型的参数来实现多线程,不同的是,这里传入的是Runnable接口的子类FutureTask
对象作为参数,而FutureTask
对象中则封装带有返回值的Callable接口实现类。
2、创建多线程的操作步骤
- 创建一个Callable接口的实现类,同时重写Callable接口的call()方法;
- 创建Callable接口的实现类对象;
- 通过FutureTask线程结果处理类的有参构造方法来封装Callable接口实现类对象;
- 使用参数为FutureTask类对象的Thread有参构造方法创建Thread线程实例;
- 调用线程实例的start()方法启动线程。
3、FutureTask继承关系图
- Callable接口方式实现的多线程是通过FutureTask类来封装和管理返回结果的,该类的直接父接口是RunnableFuture。
- FutureTask本质是Runnable接口和Future接口的实现类,而Future则是JDK 5提供的用来管理线程执行返回结果的。
4、Future接口方法
方法声明 | 功能描述 |
---|---|
boolean cancel(boolean mayInterruptIfRunning) | 用于取消任务,参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行的任务 |
boolean isCancelled() | 判断任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true |
boolean isDone() | 判断任务是否已经完成,若任务完成,则返回true |
V get() | 用于获取执行结果,这个方法会发生阻塞,一直等到任务执行完毕才返回执行结果 |
V get(long timeout, TimeUnit unit) | 用于在指定时间内获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null |
5、案例演示:动态输出欢迎文件
package net.hw.lesson33;
import java.util.concurrent.*;
/**
* 功能:动态输出欢迎文件
* 作者:华卫
* 日期:2020年06月16日
*/
public class Example3305 {
// 定义单词数组
private static char[] words = "欢迎访问泸州职业技术学院".toCharArray();
// 1. 创建Callable实现类
static class WelcomeCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < words.length; i++) {
System.out.print(words[i]);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println();
return "欢迎词总共有[" + words.length + "]个文字。";
}
}
public static void main(String[] args) throws Exception {
// 2. 创建Callable接口实现类对象
WelcomeCallable wc = new WelcomeCallable();
// 3. 创建FutureTask对象
FutureTask<String> ft = new FutureTask<>(wc);
// 4. 创建线程对象
Thread thread = new Thread(ft, "thread");
// 5. 启动线程
thread.start();
// 6. 输出线程返回值
System.out.println("线程返回值:" + ft.get());
}
}
- 运行程序,查看结果
四、三种实现多线程方式的对比分析
多线程的实现方式有三种,其中Runnable接口和Callable接口实现多线程的方式基本相同,主要区别就是Callable接口中的方法有返回值,并且可以声明抛出异常。那么通过继承Thread类和其它两种接口实现多线程的方式有什么区别呢?
(一)案例:售票厅出售车票
通过一个应用场景来说明:假设售票厅有4个窗口可以发售某日某次列车的100张车票,这100张车票就可以看作共享资源,4个售票窗口相当于4个线程。为了直观显示窗口的售票情况,可以通过Thread的currentThread()方法得到当前线程的实例对象,然后调用getName()方法获取线程名称。
1、通过继承Thread类的方式实现多线程
- 创建Example3306
package net.hw.lesson33;
/**
* 功能:4窗口出售车票
* 作者:华卫
* 日期:2020年06月18日
*/
/**
* 售票窗口线程类
*/
class TicketWindow01 extends Thread {
private int tickets = 100;
public TicketWindow01(String name) {
super(name);
}
@Override
public void run() {
while (tickets > 0) {
System.out.println(Thread.currentThread().getName() + " 正在发售第[" + tickets-- + "]张车票。");
}
}
}
public class Example3306 {
public static void main(String[] args) {
// 创建四个售票窗口线程对象并启动
new TicketWindow01("窗口1").start();
new TicketWindow01("窗口2").start();
new TicketWindow01("窗口3").start();
new TicketWindow01("窗口4").start();
}
}
- 运行程序,查看结果
- 从上图可以看出,每张票都被发售了4次。出现这种现象的原因是4个线程没有共享100张票,而是各自出售了100张票。在程序中创建了4个TicketWindow01对象,就等于创建了四个售票程序,而每一个TicketWindow01对象都有一个tickets票据变量,这样每个线程在执行任务时都会独立的处理各自的资源,而不是共同处理一个售票资源。
- 现实中,售票系统的票资源是共享的,因此上述运行结果显然不合理。为了保证售票资源共享,在程序中只能创建一个售票对象,然后开启多个线程去共享同一个售票对象的售票方法,简单来说,就是4个线程运行同一个售票程序,这是就需要通过实现Runnable接口的方式来实现多线程。
2、通过实现Runnable接口的方式实现多线程
- 创建Example3307
package net.hw.lesson33;
/**
* 功能:4窗口出售车票
* 作者:华卫
* 日期:2020年06月18日
*/
/**
* 售票窗口线程类
*/
class TicketWindow02 implements Runnable {
private int tickets = 100;
@Override
public void run() {
while (tickets > 0) {
System.out.println(Thread.currentThread().getName() + " 正在发售第[" + tickets-- + "]张车票。");
}
}
}
public class Example3307 {
public static void main(String[] args) {
// 创建售票窗口对象
TicketWindow02 tw = new TicketWindow02();
// 基于售票窗口对象创建4个线程并启动
new Thread(tw, "窗口1").start();
new Thread(tw, "窗口2").start();
new Thread(tw, "窗口3").start();
new Thread(tw, "窗口4").start();
}
}
- 运行程序,查看结果
- - 通过Runnable接口方式创建4个线程,都是基于同一个TicketWindow02售票窗口对象,因此调用同一个售票窗口对象的run()方法,这样就确保了4个线程访问的是同一个tickets变量,共享100张车票。
(二)通过实现Runnable接口实现多线程的好处
通过实现Runnable
或Callable
接口,相对于继承Thread类实现多线程来说:
- 适合多个线程去处理同一个共享资源的情况
- 可以避免Java单根继承带来的局限性
事实上,实际开发中大部分的多线程应用都会采用Runnable
接口或者Callable
接口的方式实现多线程。