Java中run和start方法的底层区别
Java线程基础回顾
在深入探讨 run
和 start
方法的底层区别之前,我们先来回顾一下Java线程的基础知识。
线程是程序执行流的最小单元,Java通过 java.lang.Thread
类来支持多线程编程。每个线程都有一个执行路径,这个路径就是线程执行的代码。在Java中创建线程有两种常见方式:一是继承 Thread
类,二是实现 Runnable
接口。
继承Thread类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("This is a thread created by extending Thread class.");
}
}
然后可以通过以下方式启动线程:
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
实现Runnable接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("This is a thread created by implementing Runnable interface.");
}
}
接着这样启动线程:
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
无论是哪种方式,run
方法都是线程执行体的定义所在,而 start
方法则用于启动线程。
run
方法的本质
run
方法的定义
在 Thread
类中,run
方法的定义如下:
public class Thread implements Runnable {
//...
public void run() {
if (target != null) {
target.run();
}
}
//...
private Runnable target;
}
从这段代码可以看出,如果在创建 Thread
对象时传入了一个实现了 Runnable
接口的对象(即 target
不为 null
),那么 run
方法会调用 target
的 run
方法。如果没有传入 Runnable
对象,那么 run
方法执行时不做任何操作(因为 target
为 null
)。
run
方法的执行方式
当我们直接调用 run
方法时,它并不会开启一个新的线程,而是在当前线程中顺序执行。例如:
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Thread running: " + i);
}
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.run();
System.out.println("Main thread continues.");
}
}
在上述代码中,myThread.run()
执行时,会在 main
线程中按顺序执行 run
方法内的代码。只有当 run
方法执行完毕后,才会执行 System.out.println("Main thread continues.");
这行代码。这就像是调用普通的方法一样,并没有启动一个新的独立线程来执行 run
方法中的代码。
run
方法的作用
run
方法定义了线程要执行的具体任务。它是线程执行逻辑的载体,包含了我们希望线程独立执行的代码块。例如,在一个多线程下载器中,run
方法可能包含从网络下载数据的逻辑;在一个定时任务线程中,run
方法可能包含定时执行某些操作的逻辑。
start
方法的本质
start
方法的定义
start
方法在 Thread
类中的定义如下:
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
private native void start0();
这里可以看到 start
方法是 synchronized
修饰的,这是为了保证在多线程环境下对线程启动状态的正确处理。同时,start0
方法是一个本地方法(由JVM底层实现,使用 native
关键字修饰),它才是真正启动新线程的关键。
start
方法的执行流程
- 状态检查:
start
方法首先检查线程的状态threadStatus
。如果threadStatus
不为0,说明线程已经启动过或者处于其他不允许再次启动的状态,此时会抛出IllegalThreadStateException
异常。 - 线程组管理:将当前线程添加到所属的线程组
group
中。线程组是一种管理线程的机制,可以对一组线程进行统一的操作,比如设置优先级、进行安全检查等。 - 启动新线程:通过调用本地方法
start0
来启动新线程。start0
方法由JVM底层实现,它会创建一个新的操作系统线程,并将其与Java的Thread
对象关联起来。新线程启动后,会执行run
方法中的代码。 - 异常处理:如果
start0
方法执行失败(即started
为false
),会调用group.threadStartFailed(this)
方法来处理线程启动失败的情况。
start
方法的作用
start
方法的主要作用是启动一个新的线程。它使得JVM为线程分配系统资源,包括CPU时间片等,让线程进入可运行状态(Runnable
)。一旦线程进入可运行状态,就有可能被CPU调度执行,从而开始执行 run
方法中的代码。与直接调用 run
方法不同,start
方法启动的线程是独立于当前线程的,多个线程可以并发执行,提高程序的执行效率。
run
和 start
方法底层区别的深入分析
执行机制的区别
- 直接调用
run
方法:直接调用run
方法时,它是在当前线程的栈中执行的,就像调用普通的成员方法一样。当前线程会按顺序执行run
方法中的代码,直到run
方法执行完毕。在run
方法执行期间,不会有新的线程被创建,也不会有线程调度的发生。 - 调用
start
方法:调用start
方法时,JVM会创建一个新的操作系统线程,并将其与Java的Thread
对象关联。新线程会进入可运行状态,等待CPU调度。一旦被CPU调度到,新线程就会开始执行run
方法中的代码。这个过程涉及到操作系统的线程管理机制,包括线程的创建、调度和上下文切换等。
内存模型和资源分配的区别
- 直接调用
run
方法:由于run
方法是在当前线程中执行,它使用的是当前线程的栈空间和其他资源。例如,局部变量、方法调用等都在当前线程的栈中进行。这种方式不会为run
方法中的代码分配独立的线程资源。 - 调用
start
方法:当调用start
方法启动新线程时,JVM会为新线程分配独立的栈空间和其他必要的资源。每个线程都有自己独立的栈,用于存储局部变量、方法调用等信息。这种资源分配方式使得新线程能够独立于其他线程运行,具有更好的并发性和隔离性。
线程生命周期的影响
- 直接调用
run
方法:直接调用run
方法不会改变线程的生命周期状态。线程仍然处于创建状态(如果还没有调用过start
方法)或者已经启动状态(如果之前已经调用过start
方法)。run
方法执行完毕后,线程的状态不会发生像正常线程结束时那样的变化,例如不会进入TERMINATED
状态。 - 调用
start
方法:调用start
方法会使线程从NEW
状态转换到RUNNABLE
状态。一旦线程开始执行run
方法中的代码,它会经历运行、阻塞、等待等不同的生命周期状态,最终当run
方法执行完毕时,线程会进入TERMINATED
状态。这个过程符合Java线程的标准生命周期模型。
示例代码对比
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Thread running: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
// 直接调用run方法
System.out.println("Directly call run method:");
myThread.run();
// 重新创建一个线程对象,调用start方法
MyThread newThread = new MyThread();
System.out.println("Call start method:");
newThread.start();
// 主线程继续执行
for (int i = 0; i < 5; i++) {
System.out.println("Main thread running: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
在上述代码中,当直接调用 myThread.run()
时,run
方法中的代码会在 main
线程中顺序执行,main
线程会等待 run
方法执行完毕。而当调用 newThread.start()
时,新线程会独立执行 run
方法中的代码,与 main
线程并发执行,main
线程不会等待新线程执行完毕。
实际应用中的选择
场景一:顺序执行任务
如果我们希望在当前线程中顺序执行一段代码,而不需要并发执行,那么直接调用 run
方法就可以。例如,在一个简单的单线程应用中,我们可能有一些辅助性的任务,这些任务不需要独立的线程来执行,直接在当前线程中调用 run
方法实现的逻辑即可。
class HelperTask {
public void run() {
System.out.println("Performing helper task.");
// 一些简单的任务逻辑
}
}
public class Main {
public static void main(String[] args) {
HelperTask helperTask = new HelperTask();
helperTask.run();
System.out.println("Main task continues.");
}
}
场景二:并发执行任务
当我们需要实现并发执行的场景,比如在一个网络服务器中处理多个客户端请求,或者在一个数据分析程序中同时处理多个数据块,就需要使用 start
方法来启动新线程。
class ClientHandler extends Thread {
private int clientId;
public ClientHandler(int clientId) {
this.clientId = clientId;
}
@Override
public void run() {
System.out.println("Handling client " + clientId);
// 处理客户端请求的逻辑
}
}
public class Server {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
ClientHandler clientHandler = new ClientHandler(i);
clientHandler.start();
}
System.out.println("Server continues to accept new clients.");
}
}
在上述代码中,每个 ClientHandler
线程独立处理一个客户端请求,服务器可以同时处理多个客户端请求,提高了系统的并发处理能力。
总结常见误区
- 认为
run
和start
效果一样:很多初学者会认为直接调用run
方法和调用start
方法都能让线程执行,只是写法不同。但实际上,直接调用run
方法不会启动新线程,与调用start
方法启动新线程并发执行的效果完全不同。 - 多次调用
start
方法:有些开发者可能会尝试对已经启动过的线程再次调用start
方法,期望线程重新执行。但根据start
方法的实现,再次调用start
方法会抛出IllegalThreadStateException
异常,因为线程一旦启动,其状态就不允许再次启动。 - 忽略线程安全问题:在使用
start
方法启动多线程时,由于多个线程可能并发访问共享资源,容易出现线程安全问题。比如多个线程同时修改一个共享变量,如果没有适当的同步机制,就可能导致数据不一致等问题。而直接调用run
方法由于是在单线程中执行,不存在线程安全问题。
与其他编程语言线程启动方式的对比
- C++:在C++ 中,使用
std::thread
库来创建和管理线程。创建线程的方式如下:
#include <iostream>
#include <thread>
void threadFunction() {
std::cout << "This is a C++ thread." << std::endl;
}
int main() {
std::thread myThread(threadFunction);
myThread.join();
std::cout << "Main thread continues." << std::endl;
return 0;
}
与Java不同,C++ 直接通过函数指针来指定线程执行的代码,没有像Java中 run
方法这样的明确概念。而且C++ 线程的启动是直接通过 std::thread
对象的构造函数来实现,没有类似Java中 start
方法这样的显式启动步骤。
- Python:在Python中,使用
threading
模块来创建线程。示例如下:
import threading
def thread_function():
print("This is a Python thread.")
if __name__ == "__main__":
my_thread = threading.Thread(target = thread_function)
my_thread.start()
my_thread.join()
print("Main thread continues.")
Python的线程启动方式与Java类似,通过 Thread
类的 start
方法来启动线程。但Python中的线程由于全局解释器锁(GIL)的存在,在CPU密集型任务中并不能真正利用多核CPU的优势,而Java线程在这方面没有类似的限制。
底层实现原理的操作系统层面分析
- Windows操作系统:在Windows操作系统中,Java线程的底层实现依赖于Windows的线程机制。当Java程序调用
start
方法启动一个新线程时,JVM会通过JNI(Java Native Interface)调用Windows API来创建一个新的线程内核对象。这个内核对象包含了线程的上下文信息,如线程的栈空间、寄存器状态等。Windows的线程调度器会根据线程的优先级等因素来调度线程执行。 - Linux操作系统:在Linux操作系统中,Java线程是基于Linux的轻量级进程(LWP)实现的。LWP是一种共享内存空间的进程,它在性能上比普通进程更接近线程。当Java程序调用
start
方法时,JVM会通过JNI调用Linux的系统调用(如clone
系统调用)来创建一个新的LWP。Linux的内核调度器会负责调度这些LWP的执行。
性能方面的考虑
- 直接调用
run
方法:由于直接调用run
方法是在当前线程中执行,不存在线程创建、调度和上下文切换等开销,因此在性能上相对较高。但这种方式只适用于不需要并发执行的场景。 - 调用
start
方法:调用start
方法启动新线程会带来线程创建、调度和上下文切换等开销。线程创建需要分配栈空间等资源,调度需要操作系统的调度器进行决策,上下文切换需要保存和恢复线程的上下文信息。这些开销在高并发场景下可能会对性能产生一定影响。因此,在使用start
方法启动线程时,需要根据具体的应用场景进行性能优化,比如合理设置线程数量、优化线程任务逻辑等。
错误处理和异常机制
- 直接调用
run
方法:当直接调用run
方法时,如果run
方法中抛出异常,异常会在当前线程中传播。可以在调用run
方法的地方使用try - catch
块来捕获异常并进行处理。
class MyTask {
public void run() {
throw new RuntimeException("Task failed.");
}
}
public class Main {
public static void main(String[] args) {
MyTask myTask = new MyTask();
try {
myTask.run();
} catch (RuntimeException e) {
System.out.println("Caught exception: " + e.getMessage());
}
}
}
- 调用
start
方法:当通过start
方法启动线程后,如果run
方法中抛出未捕获的异常,默认情况下,这个异常会导致线程终止,但不会影响其他线程。在Java 8之前,要捕获线程中的异常比较麻烦,通常需要在线程内部的run
方法中使用try - catch
块来捕获异常。从Java 8开始,可以使用Thread.UncaughtExceptionHandler
来处理线程中未捕获的异常。
class MyThread extends Thread {
@Override
public void run() {
throw new RuntimeException("Thread failed.");
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.setUncaughtExceptionHandler((t, e) -> {
System.out.println("Caught exception in thread " + t.getName() + ": " + e.getMessage());
});
myThread.start();
}
}
总结
通过对Java中 run
和 start
方法底层区别的详细分析,我们了解到它们在执行机制、内存模型、线程生命周期、实际应用场景、性能等多个方面都存在显著差异。在实际编程中,我们需要根据具体的需求来选择合适的方式。对于需要并发执行的任务,应使用 start
方法来启动新线程;而对于不需要并发的简单任务,可以直接调用 run
方法。同时,我们还需要注意线程安全、异常处理等相关问题,以编写高效、稳定的多线程程序。希望本文的内容能帮助读者更好地理解和运用Java多线程编程。