前言:在Java中,Thread
类是实现多线程编程的核心。它允许程序同时执行多个任务,提高应用程序的响应能力和性能。通过Thread
类,开发者可以轻松创建和管理线程,并实现复杂的并发操作。接下来,我们将探讨Thread
类的基本用法及其在实际开发中的应用。
在开始讲解并查集之前,先让我们看一下本文大致的讲解内容
1.Java中线程的回顾
线程是程序中执行代码的最小单位,通常被称为执行流。每个线程都有自己独立的运行路径,并且可以与其他线程并行执行代码。多个线程共享同一进程的资源,比如内存空间和文件句柄,但它们之间可以独立调度。
——这就像在现实生活中,我们有多个员工(线程)在公司(进程)里同时工作,虽然他们共享办公空间和资源,但每个人都有各自的任务要完成。
在程序中,线程能够提高并发性,处理多任务,减少等待时间。线程的概念可以通过如下的代码理解:
public class ThreadDemo {
private static class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在运行");
}
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start(); // 启动第一个线程
t2.start(); // 启动第二个线程
}
}
在这个例子中,MyThread
类继承了 Thread
,我们通过 start()
方法启动了两个线程 t1
和 t2
,它们分别执行各自的任务。线程可以独立运行,即使它们共享相同的进程资源。
——所以我们为什么需要线程来帮助我们工作呢?
多线程编程的需求主要来自两个方面:提高多核CPU的利用率 和 处理I/O等待问题。
提高多核CPU的利用率 随着硬件的发展,CPU的单核性能提升遇到了瓶颈,现代的计算机通常采用多核CPU。为了充分利用这些多核资源,程序需要能够并行处理多个任务,这时多线程编程就成为了一个关键手段。例如,如果一个程序能够同时在多个CPU核心上运行不同的线程,整体计算效率就会成倍提升。
解决I/O等待问题 有时,程序需要等待某些耗时的I/O操作(如文件读写或网络请求)完成,而在等待这些操作时,CPU的资源可能会被浪费掉。通过引入多线程,程序可以在等待I/O时继续执行其他任务,从而提高整体效率。就像在餐馆里,厨师可以同时准备多道菜,而不是等待每道菜完成后再开始下一个。
这里我们在使用一个日常生活中的例子来进行理解:
假设你在银行办理业务,需要分别处理财务转账、员工福利发放、社保缴纳。如果只有一个员工来处理所有这些事务,他需要排队等候多个业务完成,这会拖慢整个流程。而如果每个业务都有专门的员工负责,它们可以同时进行,显著提高效率。这就是多线程在程序中的表现。
——通过上述的回顾,我相信读者已经可以回想起有关Java中线程的内容了!!!
2.Thread线程的五种创建方式
回顾完有关Java中线程的知识之后,让我们正式的开始本篇文章的主要内容,那么一开始,让我们先看一下在Java中如何去创建一个线程,其有以下五种创建方式:
(1)继承 Thread
类创建线程
这是最直接的方式,通过继承 Thread
类并重写其 run()
方法来定义线程的执行逻辑。Thread
类本身实现了 Runnable
接口,因此每个 Thread
对象都可以被当作一个线程。通过调用 start()
方法,线程会开始运行,操作系统的线程调度器会选择适当的时机执行 run()
方法中的代码。
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行");
}
}
public class ThreadExample {
public static void main(String[] args) {
MyThread thread1 = new MyThread(); // 创建线程对象
thread1.start(); // 启动线程
}
}
该种方式创建线程的优点:
-
简单直观,适合需要对线程本身进行自定义控制的场景
该种方式创建线程的缺点:
-
Java 不支持多继承,如果线程类已经继承了其他类,就不能再继承
Thread
类,因此这种方式具有局限性。
(2)实现 Runnable
接口创建线程
相比继承 Thread
类,实现 Runnable
接口 是更灵活的方式,因为它解耦了线程的任务与线程对象本身。通过实现 Runnable
接口并将其传递给 Thread
对象,我们可以实现类似的功能。这种方式尤其适合在需要继承其他类时使用。
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行");
}
}
public class RunnableExample {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable); // 创建线程对象并传入 Runnable 对象
thread.start(); // 启动线程
}
}
该种方式创建线程的优点:
-
线程任务与线程对象分离,符合面向接口编程的原则。
-
适用于需要继承其他类的场景,更加灵活。
该种方式创建线程的缺点:
-
与直接继承
Thread
相比,代码稍显复杂,需要额外的Runnable
对象。
(3)使用匿名内部类创建 Thread
子类对象
为了简化代码,Java 提供了匿名内部类的方式来创建线程对象。这种方式通过定义一个匿名子类并重写 run()
方法,可以在一行代码中同时创建线程并指定其任务。匿名内部类适合用于短小的、一次性的线程任务。
public class AnonymousThreadExample {
public static void main(String[] args) {
// 使用匿名类创建 Thread 子类对象
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("匿名类线程 " + Thread.currentThread().getName() + " 正在执行");
}
};
thread.start(); // 启动线程
}
}
该种方式创建线程的优点:
-
代码简洁,适合一次性任务,避免创建额外的类文件。
该种方式创建线程的缺点:
-
可读性较差,特别是对于复杂任务时,不易维护。
-
由于是匿名类,不能复用或扩展其功能。
(4)使用匿名内部类创建 Runnable
子类对象
类似于匿名 Thread
子类的方式,匿名内部类 也可以用于创建 Runnable
对象,然后将其传递给 Thread
。这种方式依然能够通过实现 Runnable
接口来定义线程任务,并保持代码简洁
public class AnonymousRunnableExample {
public static void main(String[] args) {
// 使用匿名类创建 Runnable 子类对象
Runnable myRunnable = new Runnable() {
@Override
public void run() {
System.out.println("匿名类 Runnable 线程 " + Thread.currentThread().getName() + " 正在执行");
}
};
Thread thread = new Thread(myRunnable); // 创建并启动线程
thread.start();
}
}
该种方式创建线程的优点:
-
灵活且代码简洁,能够避免创建多余的类文件。
-
线程任务与
Thread
对象分离,保持了良好的代码结构。
该种方式创建线程的缺点:
-
可读性较差,对于复杂任务的实现不够清晰。
-
匿名内部类不能被复用。
(5)使用 Lambda 表达式创建线程
在 Java 8 之后,Lambda 表达式大大简化了代码的书写。当任务逻辑较为简单时,可以用 Lambda 表达式代替匿名内部类来实现 Runnable
接口。Lambda 表达式使代码更简洁、易读,同时避免了不必要的语法冗余。
public class LambdaThreadExample {
public static void main(String[] args) {
// 使用 Lambda 表达式创建 Runnable 对象
Thread thread = new Thread(() -> {
System.out.println("Lambda 线程 " + Thread.currentThread().getName() + " 正在执行");
});
thread.start(); // 启动线程
}
}
该种方式创建线程的优点:
-
代码极其简洁,尤其适合简单的线程任务。
-
提升代码的可读性,减少样板代码的编写。
该种方式创建线程的缺点:
-
仅适用于 Java 8 及以上版本。
-
不适合需要大量逻辑或复杂任务的场景。
——以上就是Java中创建线程的五种方式了!!!
3.多线程的状态与调度
了解了Java中的线程如何去创建之后,现在让我们深入Java中的Thread类,看看其状态和其如何去调度的。
(1)线程状态
了解完了如何在Java中去创建一个线程之后,在让我们看一下Java中的线程的状态以及调度,在Java中,线程的生命周期可以分为五种状态:
NEW(新建状态):线程对象被创建,但尚未调用
start()
方法,此时线程还未开始运行。RUNNABLE(可运行状态):线程已经调用
start()
方法,进入就绪队列,等待CPU的调度,或者正在运行中。BLOCKED(阻塞状态):线程等待获取锁时处于阻塞状态。比如当线程试图进入一个
synchronized
块但锁被其他线程占用时,它就会进入BLOCKED
状态。WAITING(等待状态):线程主动等待某个条件的触发,比如调用了
wait()
方法,等待其他线程调用notify()
。TERMINATED(终止状态):线程执行完毕或被异常终止,此时线程已经结束运行。
这里我们使用一段代码来对线程的状态进行控制并打印其状态:
public class ThreadStateDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
for (int i = 0; i < 1000000; i++) { /* 模拟任务执行 */ }
});
System.out.println("线程状态:" + thread.getState()); // NEW
thread.start();
while (thread.isAlive()) {
System.out.println("线程状态:" + thread.getState()); // RUNNABLE
}
System.out.println("线程状态:" + thread.getState()); // TERMINATED
}
}
在这个代码中,线程 thread
在不同时刻有不同的状态:NEW
表示线程刚刚创建,RUNNABLE
表示线程正在运行,TERMINATED
表示线程已经执行完毕(读者可能不太理解其中的方法,不管美食,读者只要根据该代码对上述的线程状态进行理解即可)。
(2)线程调度
线程的调度由操作系统的线程调度器管理,Java中的线程调度是抢占式的,这意味着线程的执行顺序由系统决定,而不是由程序直接控制。操作系统会根据线程的优先级、系统负载等因素来决定哪个线程获得CPU的时间片。
因此,线程的执行顺序是不确定的,这也是为什么在多线程编程中会出现一些竞争问题,后面我们将进一步讨论。
这样我们就了解了有关多线程的状态与调度了。
4.Thread类的属性及基本方法的使用
通过上述的讲解,我们相信读者已经对Java中的线程有了初步的理解了,那么现在开始,我们就开始学习Java中Thread类的属性和操作Java中线程的方法了。
【1】Thread类的基本属性
在Java中 Thread
类有一些重要的属性,它们用于表示和控制线程的状态和行为,理解这些属性有助于更好地调试和优化多线程程序,以下是 Thread
类中的几个常见属性:
(1)线程 ID(ID)
每个线程都有一个唯一的ID,表示线程的标识符。这个ID是系统自动分配的,且不同线程的ID永远不会重复。线程ID用于唯一标识每个线程,无论线程是否运行,ID都可以帮助我们区分不同的线程。开发者可以通过 getId()
方法获取线程的ID:
long id = thread.getId();
System.out.println("线程 ID: " + id);
线程ID主要用于低级别的系统操作或调试,它是线程的唯一标识符,不能被更改。
(2)线程名称(Name)
线程名称是开发者可以手动指定的属性。通过设置有意义的名称,开发者可以在调试或日志中快速定位某个线程的执行情况。线程的名称可以在创建时指定,也可以在运行过程中通过 setName()
方法更改:
thread.setName("WorkerThread");
String name = thread.getName();
System.out.println("线程名称: " + name);
线程名称不影响线程的执行逻辑,但它对开发者了解线程的功能和状态起到重要作用,特别是在多线程调试中非常有用。
(3)线程状态(State)
线程状态表示线程当前的执行状态,Thread.State
是一个枚举类型,包含了线程的几种典型状态,如 NEW
(新建)、RUNNABLE
(可运行)、BLOCKED
(阻塞)、WAITING
(等待)、TIMED_WAITING
(计时等待)和 TERMINATED
(终止)。开发者可以通过 getState()
方法获取线程的当前状态:
Thread.State state = thread.getState();
System.out.println("线程状态: " + state);
线程状态的变化能够帮助开发者理解线程的生命周期和调度行为,尤其是在遇到阻塞或死锁问题时,状态信息能够提供有力的调试依据。
(4)线程优先级(Priority)
每个线程都有一个优先级,范围从 1 到 10。线程优先级是操作系统用来调度线程的一个参考值,优先级高的线程更有可能获得 CPU 时间片。开发者可以通过 setPriority()
和 getPriority()
方法来设置和获取线程的优先级:
thread.setPriority(Thread.MAX_PRIORITY);
int priority = thread.getPriority();
System.out.println("线程优先级: " + priority);
尽管优先级会影响线程的调度顺序,但并不能保证线程优先级较高的线程一定会首先执行,因为线程调度最终由操作系统控制。
(5)后台线程(Daemon)
后台线程,也称为守护线程,是一种特殊的线程类型。当所有非后台线程结束时,JVM 会自动退出,即使后台线程仍然在运行。典型的后台线程包括垃圾回收线程,它们在系统后台执行一些不需要用户直接干预的操作。可以通过 setDaemon(true)
来将一个线程设置为后台线程:
thread.setDaemon(true);
boolean isDaemon = thread.isDaemon();
System.out.println("是否为后台线程: " + isDaemon);
后台线程非常适合用于执行长期运行且不依赖于用户输入的任务,例如监控、清理任务等。但需要注意的是,JVM 退出时后台线程会被强制停止,可能导致任务未能完全执行完毕。
(6)线程存活状态(Alive)
一个线程在调用 start()
方法之后即被视为“存活”,直到它的 run()
方法执行完毕或者被强制中断后才会结束。开发者可以通过 isAlive()
方法检查线程是否仍在运行:
boolean isAlive = thread.isAlive();
System.out.println("线程是否存活: " + isAlive);
线程的存活状态能够帮助开发者判断一个线程是否已经结束执行,这在需要等待线程完成任务时非常有用。
(7)线程中断标志(Interrupted)
当一个线程被其他线程调用 interrupt()
方法时,它会被标记为中断状态。线程可以通过 isInterrupted()
或 interrupted()
方法来检查自身是否被中断:
thread.interrupt(); // 中断线程
boolean isInterrupted = thread.isInterrupted();
System.out.println("线程是否被中断: " + isInterrupted);
中断通常用于停止线程的执行,特别是在需要安全终止线程时,开发者可以通过捕获 InterruptedException
来终止阻塞中的线程操作(例如 sleep()
)。
以上就是Java中线程的一些基本属性了!!!
【2】Thread类的常用方法
(1)启动一个线程 - start()
在 Java 中,创建了一个 Thread
对象并不意味着线程会立即执行。线程的创建仅仅是分配了一些系统资源,但要让线程真正开始执行,还需要调用 start()
方法。当 start()
方法被调用时,线程进入到 可运行状态(RUNNABLE
),然后等待被系统的线程调度器选中并执行其 run()
方法。
注意: 直接调用
run()
方法不会启动新线程,它只是将run()
方法当作普通的方法来执行,运行在当前调用线程中。真正启动线程的方式是调用start()
方法,它会在 JVM 中真正创建一个新线程。
以下为一个代码案例:
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 线程正在执行");
}
}
public class ThreadStartExample {
public static void main(String[] args) {
MyThread thread = new MyThread(); // 创建线程
System.out.println("线程状态:" + thread.getState()); // NEW
thread.start(); // 启动线程
System.out.println("线程状态:" + thread.getState()); // RUNNABLE
}
}
在上面的代码中,线程在 start()
调用后,从 新建状态(NEW
)切换为 可运行状态(RUNNABLE
),此时线程可以被操作系统调度并执行。
(2)中断一个线程 - interrupt()
在多线程编程中,有时需要安全地停止一个正在执行的线程。Java 提供了 interrupt()
方法,用于通知线程停止执行。需要注意的是,interrupt()
并不会强制终止线程,而是通过设置线程的中断状态来通知线程自己是否应该终止。
线程可以通过检查其中断状态来决定是否结束运行。如果线程正在执行阻塞操作(如 sleep()
或 wait()
),那么 interrupt()
会导致这些操作抛出 InterruptedException
,从而跳出阻塞状态并执行相应的终止逻辑。
以下为一个代码案例:
public class ThreadInterruptExample {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 正在执行");
Thread.sleep(1000); // 模拟工作
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 被中断");
}
});
thread.start();
Thread.sleep(3000); // 主线程等待3秒
thread.interrupt(); // 中断线程
}
}
thread.interrupt()
会中断线程,使得 sleep()
操作抛出 InterruptedException
,从而线程终止。
(3)等待一个线程 - join()
有时候,在主线程或其他线程中,我们希望等待某个线程执行完毕再继续执行后续操作。此时,可以使用 join()
方法。join()
会让调用线程进入 等待状态,直到目标线程执行完毕或达到指定的等待时间(如果提供了超时参数)。通过 join()
方法,我们可以确保一个线程在另一个线程执行完成之后才继续执行。
以下为一个代码案例:
public class ThreadJoinExample {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + " 正在工作");
try {
Thread.sleep(1000); // 模拟工作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " 工作完成");
});
Thread thread2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 准备开始工作");
});
thread1.start();
thread1.join(); // 主线程等待 thread1 完成
thread2.start(); // 只有 thread1 完成后,thread2 才会启动
}
}
在这个例子中,主线程会调用 thread1.join()
来等待 thread1
完成,只有当 thread1
执行完毕后,thread2
才会开始执行。
(4)获取当前线程引用 - Thread.currentThread()
在多线程编程中,有时我们需要获取当前正在执行的线程对象。Java 提供了静态方法 Thread.currentThread()
,它返回对当前线程的引用。通过这个方法,开发者可以获取当前线程的名称、ID、优先级等属性,或对当前线程进行操作(例如检查中断状态)。
public class CurrentThreadExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
// 获取当前线程引用
Thread currentThread = Thread.currentThread();
System.out.println("当前线程名称: " + currentThread.getName());
});
thread.start();
// 获取主线程的引用
Thread mainThread = Thread.currentThread();
System.out.println("主线程名称: " + mainThread.getName());
}
}
在这个示例中,我们使用了 Thread.currentThread()
获取当前线程的引用,并打印了线程的名称。无论是在新启动的线程中还是主线程中,这个方法都可以用于获取当前执行线程的相关信息。
(5)休眠当前线程 - Thread.sleep()
在多线程编程中,常常需要让线程在某些时刻暂停执行一段时间。Java 提供了 Thread.sleep()
方法,可以让线程进入 休眠状态。线程在休眠期间不会占用 CPU 资源,但它仍然保持着某些锁定状态。当休眠时间结束后,线程会恢复到 就绪状态,等待 CPU 的调度。
需要注意的是,sleep()
可能会抛出 InterruptedException
,因此在调用时通常需要处理该异常。
public class ThreadSleepExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 开始休眠");
Thread.sleep(3000); // 线程休眠3秒
System.out.println(Thread.currentThread().getName() + " 休眠结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
}
}
在这个例子中,线程通过 Thread.sleep()
休眠了 3 秒钟,然后继续执行剩下的代码。sleep()
的休眠时间是最低限制,也就是说,线程休眠的时间可能会比指定的时间稍长,因为休眠结束后线程需要等待系统的调度。
以上是关于如何启动、等待、获取当前线程、休眠和中断线程的详细解释,这些操作都是 Java 多线程编程中常用的基本方法,希望读者可以根据上述的案例以及讲解对Java中的Thread的类有自己独到的理解!