Java高级程序设计笔记 • 【第2章 多线程(一)】

时间:2023-06-11 13:25:08

全部章节   >>>>


本章目录

2.1 线程的概述

2.1.1 进程

2.1.2 多线程优势

2.1.3 Thread 类

2.1.4 实践练习

2.2 Runnable接口

2.2.1 Runnable接口

2.2.2 使用 Runnable接口实现多线程

2.2.3 Thread和Runnable

2.2.4 实践练习

2.3 控制线程

2.3.1 线程休眠

2.3.2 join线程

2.3.3 实践练习

2.4 线程的生命周期

2.4.1 新建和就绪状态

2.4.2 线程运行状态

2.4.3 线程阻塞状态

2.4.4 线程死亡状态

2.4.5 线程执行状态图

2.4.6 实践练习

总结:


2.1 线程的概述

程序的运行默认仅包含一个主线程,所有的工作都由主线程承担,当任务繁重耗时,会对于主线程产生很大压力,甚至引起阻塞卡顿

将繁重耗时的任务分出一些子线程去执行,相当于多人做一件事,能大大降低主线程的阻塞,同时提高程序运行效率

2.1.1 进程

几乎所有的操作系统都只是同时运行多个任务,一个任务通常只对应一个程序,每个运行的程序就是一个进程

当一个程序进入到内存中运行时,即变为一个进程。进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位

进程具有以下特点: 独立性、动态性、并发性

线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程

线程之间是独立运行,线程的执行是抢占式的,即当前任何运行的线程在任何时候都有可能被停止,被其他线程抢走对 CPU 的使用权

Java高级程序设计笔记 • 【第2章 多线程(一)】

2.1.2 多线程优势

多线程来实现并发比使用多进程实现并发的性能要高,在执行耗时任务时,开启多个线程能提高执行效率

使用多线程编程具有以下优点:

  • 进程之间不共享内存,但线程之间共享内存非常容易
  • 创建线程代价较小,能提高任务执行效率
  • Java 内置了多线程功能支持,大大简化了多线程编程实现

2.1.3 Thread 类

Java 使用 java.lang.Thread 类表示线程,所有的线程对象都必须是 Thread 类或其子类的实例

通过继承 Thread 类来创建并启动多线程的步骤:

  • 定义类继承Thread 类,并重写该类的 run() 方法(run方法是线程的执行主体方法)
  • 创建 Thread 子类的实例,即创建线程对象 调用线程对象的 start() 方法,启动该线程、

Thread 类常用方法

方法名

作用

void setName(String name)

设置线程的名字

String getName()

返回当前线程的名字

Thread currentThread()

返回当前正在执行的线程对象

void start()

启动线程

String getAbsolutePath()

返回此 File 对象所对应的绝对路径名

void run()

线程一旦执行,便调用此方法

boolean isLive()

判断当前线程是否活跃,活跃返回 true,否则,返回 false

Thread类实现多线程

示例:实现步骤1

  1. 创建类继承Thread类,并且重写run方法
  2. run方法中完成线程执行任务的代码
public class ThreadSimple extends Thread {
private int i = 0;
// 重写 run() 方法
public void run() {
for (; i < 50; i++) {
// getName() 用于获取当前线程的名字
System.out.println(this.getName() + "\t" + i);
}
}

注意:

  • 进行多线程编程时,Java 程序运行时默认的主线程即为 main() 方法体,该方法就是主要线程的线程执行体
  • 使用继承 Thread 类的方式创建线程类时,多个线程之间无法共享线程类的实例变量(案例线程类中声明的变量i无法被多个线程共享使用)

2.1.4 实践练习

2.2 Runnable接口

2.2.1 Runnable接口

通过继承Thread类虽然可以实现多线程,但是无法实现多线程之间的资源共享,通过Runnable接口实现多线程可以很好解决这个问题

Runnable 接口的使用步骤:

  1. 定义 Runnable 接口的实现类,并重写该接口的 run() 方法为线程执行主体
  2. 创建 Runnable 实现类的实例,并以该实例作为 Thread 实例操作的目标
  3. 创建 Thread 类对象时传入实现Runnable接口的类对象

说明:Runnable对象仅作为 Thread 类对象操作的目标对象,而实际的线程对象依然是 Thread 类的实例

2.2.2 使用 Runnable接口实现多线程

示例:实现步骤1

  1. 创建类实现Runnable接口,并且重写run方法
  2. run方法中完成线程执行任务的代码
public class RunnableSimple implements Runnable {
private int i = 1;
// 重写 run() 方法,run() 方法同样是线程执行体
public void run() {
for (; i <= 20; i++) {
// 当实现 Runnable 接口时,获取当前线程,只能使用 Thread.currentThread() 方法
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
}
}

示例:实现步骤2

  1. 在测试类的main方法中,创建Runnable子类实例对象,作为线程对象执行的目标
  2. 创建Thread类实例,传入Runnable子类对象(这里可以多个线程使用同一个目标)
  3. 调用start()方法启动线程
// 创建 RunnableSimple 实例
RunnableSimple rs = new RunnableSimple();
// 创建 Thread 线程实例,该线程操作 rs 对象
Thread thread = new Thread(rs);
thread.setName(“ 线程 A”); // 设置第一个线程的名字
// 创建第 2 个 Thread 线程实例
Thread thread_02 = new Thread(rs);
thread_02.setName(" 线程 B");
// 启动线程 A
thread.start();
// 启动线程 B
thread_02.start();

2.2.3 Thread和Runnable

Thread 类和 Runnable 接口都可以实现多线程,但是Thread方式的子类直接就是线程类,实例化后直接启动;

而Runnable方式创建的类仅为线程的执行提供目标,最终还是依靠Thread类创建线程对象

Runnable方式的优势:

适合多个相同程序代码的线程去处理同一个资源

避免 Java 特性中的单根继承限制、更能体现面向对象特点

增加程序健壮性。在数据被共享时,仍然可以保持代码和数据的分离

经验:在实现多线程时,更推荐大家使用 Runnable 接口

2.2.4 实践练习

2.3 控制线程

2.3.1 线程休眠

Thread 类的 sleep(long millis) 方法可以让当前正在执行的线程暂停多少毫秒,并进入阻塞状态

调用 sleep() 方法进入阻塞状态后,在其睡眠时间段内,该线程不会获得执行的机会

示例:使用 sleep() 方法休眠线程,实现每隔 500 毫秒出现一颗五角星

public void run() {
// 循环 20 次,打印出 20 颗五角星
for (int i = 0; i < 20; i++) {
try {
// 线程休眠 500 毫秒
Thread.sleep(500);
System.out.print(" ☆ ");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

2.3.2 join线程

Thread 类的 join() 方法可以让一条线程等待另一条线程完成

如:在线程 B 中,调用了线程 A 的 join() 方法后线程 B 则会被挂起,并进入阻塞状态,直到线程 A 执行完毕后才会继续执行线程 B

示例:实现两个线程之间等待效果

分析:实现步骤

  1. 创建线程类A继承Thread类,重写run方法
  2. 创建线程类B继承Thread类,B类中需要声明线程A类的对象引用,以便在B类中可以使用线程A类对象
  3. 线程B类run方法中,调用线程A对象的join()方法,此时线程B会处于等待状态,直到线程A执行完毕
  4. 创建测试类,创建线程A和B,分别线程A、B并运行

Java高级程序设计笔记 • 【第2章 多线程(一)】

2.3.3 实践练习

2.4 线程的生命周期

当线程被创建启动后,该线程并非一启动就会被执行,也不会一直处于执行状态。在线程的生命周期中,要经过新建>>就绪>>运行>>阻塞>>死亡这 5 种状态

注意:多线程并非同时执行,而是CPU在多个线程之间来回快速切换执行,而线程本身也在运行和阻塞状态之间切换

2.4.1 新建和就绪状态

当程序使用 new 关键字创建线程后,该线程就处于新建状态

当线程对象调用了 start() 方法后,该线程就处于就绪状态,处于就绪状态的线程  并未开始运行,仅表示该线程可以运行,该线程何时开始运行,取决于 JVM中线程调度器的调度

提示:启动线程使用 start() 方法,而不是 run() 方法 永远不要调用线程对象的 run() 方法

2.4.2 线程运行状态

如果处于就绪状态的线程获得了 CPU,开始执行 run() 方法的线程执行体,则该线程处于运行状态

对于单CPU的机器,任何时刻只有一个线程处于运行状态,它会在多个线程之间来回切换执行

对于多CPU的机器,将会有与 CPU 个数相同的线程并行执行

2.4.3 线程阻塞状态

当发生如下情况时,线程将进入阻塞状态:

  • 线程调用 sleep() 方法主动放弃占有的 CPU 资源
  • 线程调用了一个阻塞式的 I/O 方法,在方法之间,该线程被阻塞

当发生如下特定的情况时,可以解除上述阻塞,让该线程重新进入就绪状态:

  • 调用 sleep() 方法的线程经过了指定的时间
  • 线程调用的阻塞式 I/O 方法已经返回

2.4.4 线程死亡状态

线程会以如下方式结束,结束后就处于死亡状态

run() 方法执行完成,线程正常结束

线程抛出一个未捕获的 ExceptionError

直接调用线程的 stop() 方法来结束该线程,通常不推荐使用该方法

提醒:不要试图对一个已经死亡的线程调用其 start() 方法使其重新启动,否则会抛出异常

2.4.5 线程执行状态图

Java高级程序设计笔记 • 【第2章 多线程(一)】

2.4.6 实践练习

总结:

  • 进程一般代表一个应用程序,一个进程中可以包含多个线程
  • 合理使用多线程能够提高程序的执行效率,处理高并发应用
  • 线程的创建有继承Thread类和实现Runnable接口两种方式,通过Runnable方式可以更加容易实现多线程之间资源共享
  • 通过sleep可以使线程进入休眠状态,通过join方法可以让线程处于等待,其他线程执行完毕后继续执行
  • 线程生命周期包括:新建 就绪 运行 阻塞 死亡5种状态