创建线程的三种基本方式
多线程的基本概念,线程创建的三种基本方式及线程的生命周期等其他线程相关的简要介绍
Author: Msuenb
Date: 2023-02-14
多线程基本概念
-
程序(program):为完成特定任务、用某种语言编写的一组指令的集合。即指一 段静态的代码,静态对象。
-
进程(process):程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程,有它自身的产生、存在和消亡的过程。
-
线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径。
注意:
- 程序是静态的,进程是动态的
- 进程作为资源分配的单位,线程作为调度和执行的单位
- 一个进程中的多个线程共享该进程的内存地址空间,多个线程操作共享的系统资源可能就会带来安全的隐患
线程创建和使用
JVM 允许程序运行多个线程,通过java.lang.Thread
类实现。
- 每个线程都是通过某个特定的 Thread 对象的
run()
方法来完成操作的,经常把run()
方法的主体称为线程体 - 通过该 Thread 对象的
start()
方法来启动这个线程,而非直接调用run()
继承Thread类
通过继承 Thread 类,创建线程并启动过程如下:
- 定义子类继承 Thread 类
- 子类中重写 Thread 类中的
run()
方法 - 创建 Thread 子类对象,即创建了线程对象
- 调用线程对象的
start()
方法,启动线程,去执行run()
方法
示例代码:
public class ThreadTest01 {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread(); // 创建线程对象
// myThread.run(); // 错误
myThread.start(); // 启动线程
// myThread.start(); // 不能重复调用 start()方法 会抛出IllegalThreadStateException异常
Thread.sleep(50);
System.out.println("main() 方法继续执行...");
}
}
class MyThread extends Thread {
@Override
public void run() { // 重写run()方法
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + " 正在执行... " + i); // 打印当前线程名称
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
注意:
- 如果自己手动启动
run()
方法,那么就只是普通方法,没有启动多线程模式run()
方法由 JVM 调用,什么时候调用,执行的过程控制都由操作系统的CPU调度算法决定- 一个线程只能调用一次
start()
方法启动,如果重复将抛出IllegalThreadStateException
异常
子线程的创建和启动过程:
注意:启动子线程执行时,main线程并不会停止等待子线程执行完,而是继续执行
实现Runnable接口
通过实现 Runnable 接口,创建线程并启动过程如下:
- 定义子类,实现 Runnable 接口,
- 子类中重写 Runnable 接口的
run()
方法 - 通过 Thread 类的含参构造器创建线程对象
- 将 Runnable接口的子类对象作为实际参数传递给 Thread 类的构造器中
- 调用 Thread 类的
start()
方法,启动线程,执行run()
方法
示例代码:
public class ThreadTest02 {
public static void main(String[] args) {
MyThread02 myThread02 = new MyThread02(); // 创建实现类对象
Thread thread = new Thread(myThread02); // 创建线程对象
thread.setName("Thread04"); // 设置线程名称
thread.start(); // 启动线程
boolean alive;
do {
Thread.sleep(200);
alive = thread.isAlive();
System.out.println("线程" + thread.getName() + "是否在运行:" + alive);
} while (alive);
}
}
class MyThread02 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + " 正在执行... " + i); // 获取当前线程名称
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
实现方式的好处:
- 避免了单继承的局限性
- 多个线程可以共享同一个接口实现类的对象,适合多个相同的线程处理同一个资源
实现Callable接口
与使用 Runnable 相比,Callable 功能更强大些,相比于 Runnable 接口的run()
方法,Callable 接口的call()
方法支持泛型返回值,可以抛出异常。
使用 Callable 接口创建线程需要借助 FutureTask 类,FutureTask 同时实现了 Runnable 和 Future 接口。它既可以作为 Runnable被线程调用,又可以作为 Future 得到 Callable 的返回值。
Future接口可以对具体 Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。FutureTask 是Future接口的唯一实现类。
通过实现 Runnable 接口,创建线程并启动过程如下:
- 定义子类,实现 Callable 接口,
- 子类中重写 Callable 接口的
call()
方法 - 创建子类对象,并将其作为参数,创建 FutureTask 对象
- 通过 Thread 类的含参构造器创建线程对象
- 将创建的FutureTask对象作为实际参数传递给 Thread 类的构造器中
- 调用 Thread 类的
start()
方法,启动线程,执行run()
方法
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadTest03 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread03 myThread03 = new MyThread03();
FutureTask<Integer> futureTask = new FutureTask<>(myThread03);
Thread thread = new Thread(futureTask);
thread.start();
// main 线程会被阻塞 直到futureTask获取到返回值
Integer integer = futureTask.get();
System.out.println(integer);
}
}
class MyThread03 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
for (int i = 0; i < 20; i++) {
System.out.println("子线程在执行..." + i);
Thread.sleep(50);
}
return 200;
}
}
除了以上三种创建线程的基本方式,在Java中还可以Executor和Fork-Join框架通过创建线程池的方式使用多线程。
线程的其他相关
Thread类有关方法:
void start()
: 启动线程,并执行对象的run()
方法run()
: 线程在被调度时执行的操作String getName()
: 获取线程名称void SetName(String name)
: 设置线程名称static Thread currentThread()
: 返回当前线程static void yield()
: 线程让步,让出CPU,让其他线程执行,但礼让的时间不确定,不一定成功。资源紧张时成功率高些join()
: 在一个程序执行流中调用其他线程的join()
方法,调用线程将被阻塞,直到 join 的线程执行完毕。static void sleep()
: 令当前活动的线程在指定时间内放弃CPU资源,时间到后重新排队stop()
: 强制结束线程生命期,不推荐使用boolean isAlive()
: 判断线程是否存活
有些方法在上面已经使用过,这里演示一些yield()
和join()
方法的使用:
public class YieldTest {
public static void main(String[] args) throws InterruptedException {
Thread t2 = new Thread(new MyThread05());
t2.start();
for (int i = 1; i <= 10; i++) {
Thread.sleep(500);
System.out.println("主线程(小弟)吃了 " + i + " 个包子");
if (i == 5) {
System.out.println("主线程(小弟) 让 子线程(老大)先吃...");
// t2.join(); // 线程插队
Thread.yield(); // 线程礼让 不一定成功
}
}
}
}
class MyThread05 implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("子线程(老大)吃了 " + i + " 个包子");
}
}
}
Java中线程的分类
Java中的线程分为两类:一种是守护线程,一种是用户线程。
守护线程它是在后台运行的,它的任务是为其他线程提供服务的。JVM 的垃圾回收线程就是典型的守护线程。
守护线程有个特点,就是如果所有非守护线程都死亡,那么守护线程自动死亡。
调用setDaemon(true)
方法可将指定线程设置为守护线程。必须在线程启动之前设置,否则会报IllegalThreadStateException
异常。
public class DaemonTest {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new DaemonThread());
thread.setDaemon(true); // 设置为守护线程
thread.start();
Thread.sleep(5000); // main线程结束后 守护线程也会结束
}
}
class DaemonThread implements Runnable {
@Override
public void run() {
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("我是守护线程...");
}
}
}
线程优先级
每个线程都有一定的优先级,优先级高的线程将获得较多的执行机会。
每个线程默认的优先级都与创建它的父线程具有相同的优先级。
Thread类提供了setPriority(int newPriority)
和getPriority()
方法类设置和获取线程的优先级,其中setPriority方法需要一个整数,并且范围在[1,10]之间,通常推荐设置Thread类的三个优先级常量:
- MAX_PRIORITY(10):最高优先级
- MIN _PRIORITY (1):最低优先级
- NORM_PRIORITY (5):普通优先级,默认情况下main线程具有普通优先级。
示例:
- 获取main线程对象的名称和优先级。
- 声明一个匿名内部类继承Thread类,重写run方法,在run方法中获取线程名称和优先级。设置该线程优先级为最高优先级并启动该线程。
public class PriorityTest {
public static void main(String[] args) {
Thread t = new Thread() {
@Override
public void run() {
System.out.println(getName() + "的优先级:" + getPriority());
}
};
t.setPriority(Thread.MAX_PRIORITY);
t.start();
System.out.println(Thread.currentThread().getName() +"的优先级:" + Thread.currentThread().getPriority());
}
}
线程的生命周期
简单来说,线程的生命周期有五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、终止(Terminated)。CPU需要在多条线程之间切换,于是线程状态会多次在运行、阻塞、就绪之间切换。
- 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
- 终止:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
线程状态转换如下图: