MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java中run和start方法的底层区别

2022-06-232.7k 阅读

Java线程基础回顾

在深入探讨 runstart 方法的底层区别之前,我们先来回顾一下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 方法会调用 targetrun 方法。如果没有传入 Runnable 对象,那么 run 方法执行时不做任何操作(因为 targetnull)。

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 方法的执行流程

  1. 状态检查start 方法首先检查线程的状态 threadStatus。如果 threadStatus 不为0,说明线程已经启动过或者处于其他不允许再次启动的状态,此时会抛出 IllegalThreadStateException 异常。
  2. 线程组管理:将当前线程添加到所属的线程组 group 中。线程组是一种管理线程的机制,可以对一组线程进行统一的操作,比如设置优先级、进行安全检查等。
  3. 启动新线程:通过调用本地方法 start0 来启动新线程。start0 方法由JVM底层实现,它会创建一个新的操作系统线程,并将其与Java的 Thread 对象关联起来。新线程启动后,会执行 run 方法中的代码。
  4. 异常处理:如果 start0 方法执行失败(即 startedfalse),会调用 group.threadStartFailed(this) 方法来处理线程启动失败的情况。

start 方法的作用

start 方法的主要作用是启动一个新的线程。它使得JVM为线程分配系统资源,包括CPU时间片等,让线程进入可运行状态(Runnable)。一旦线程进入可运行状态,就有可能被CPU调度执行,从而开始执行 run 方法中的代码。与直接调用 run 方法不同,start 方法启动的线程是独立于当前线程的,多个线程可以并发执行,提高程序的执行效率。

runstart 方法底层区别的深入分析

执行机制的区别

  1. 直接调用 run 方法:直接调用 run 方法时,它是在当前线程的栈中执行的,就像调用普通的成员方法一样。当前线程会按顺序执行 run 方法中的代码,直到 run 方法执行完毕。在 run 方法执行期间,不会有新的线程被创建,也不会有线程调度的发生。
  2. 调用 start 方法:调用 start 方法时,JVM会创建一个新的操作系统线程,并将其与Java的 Thread 对象关联。新线程会进入可运行状态,等待CPU调度。一旦被CPU调度到,新线程就会开始执行 run 方法中的代码。这个过程涉及到操作系统的线程管理机制,包括线程的创建、调度和上下文切换等。

内存模型和资源分配的区别

  1. 直接调用 run 方法:由于 run 方法是在当前线程中执行,它使用的是当前线程的栈空间和其他资源。例如,局部变量、方法调用等都在当前线程的栈中进行。这种方式不会为 run 方法中的代码分配独立的线程资源。
  2. 调用 start 方法:当调用 start 方法启动新线程时,JVM会为新线程分配独立的栈空间和其他必要的资源。每个线程都有自己独立的栈,用于存储局部变量、方法调用等信息。这种资源分配方式使得新线程能够独立于其他线程运行,具有更好的并发性和隔离性。

线程生命周期的影响

  1. 直接调用 run 方法:直接调用 run 方法不会改变线程的生命周期状态。线程仍然处于创建状态(如果还没有调用过 start 方法)或者已经启动状态(如果之前已经调用过 start 方法)。run 方法执行完毕后,线程的状态不会发生像正常线程结束时那样的变化,例如不会进入 TERMINATED 状态。
  2. 调用 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 线程独立处理一个客户端请求,服务器可以同时处理多个客户端请求,提高了系统的并发处理能力。

总结常见误区

  1. 认为 runstart 效果一样:很多初学者会认为直接调用 run 方法和调用 start 方法都能让线程执行,只是写法不同。但实际上,直接调用 run 方法不会启动新线程,与调用 start 方法启动新线程并发执行的效果完全不同。
  2. 多次调用 start 方法:有些开发者可能会尝试对已经启动过的线程再次调用 start 方法,期望线程重新执行。但根据 start 方法的实现,再次调用 start 方法会抛出 IllegalThreadStateException 异常,因为线程一旦启动,其状态就不允许再次启动。
  3. 忽略线程安全问题:在使用 start 方法启动多线程时,由于多个线程可能并发访问共享资源,容易出现线程安全问题。比如多个线程同时修改一个共享变量,如果没有适当的同步机制,就可能导致数据不一致等问题。而直接调用 run 方法由于是在单线程中执行,不存在线程安全问题。

与其他编程语言线程启动方式的对比

  1. 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 方法这样的显式启动步骤。

  1. 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线程在这方面没有类似的限制。

底层实现原理的操作系统层面分析

  1. Windows操作系统:在Windows操作系统中,Java线程的底层实现依赖于Windows的线程机制。当Java程序调用 start 方法启动一个新线程时,JVM会通过JNI(Java Native Interface)调用Windows API来创建一个新的线程内核对象。这个内核对象包含了线程的上下文信息,如线程的栈空间、寄存器状态等。Windows的线程调度器会根据线程的优先级等因素来调度线程执行。
  2. Linux操作系统:在Linux操作系统中,Java线程是基于Linux的轻量级进程(LWP)实现的。LWP是一种共享内存空间的进程,它在性能上比普通进程更接近线程。当Java程序调用 start 方法时,JVM会通过JNI调用Linux的系统调用(如 clone 系统调用)来创建一个新的LWP。Linux的内核调度器会负责调度这些LWP的执行。

性能方面的考虑

  1. 直接调用 run 方法:由于直接调用 run 方法是在当前线程中执行,不存在线程创建、调度和上下文切换等开销,因此在性能上相对较高。但这种方式只适用于不需要并发执行的场景。
  2. 调用 start 方法:调用 start 方法启动新线程会带来线程创建、调度和上下文切换等开销。线程创建需要分配栈空间等资源,调度需要操作系统的调度器进行决策,上下文切换需要保存和恢复线程的上下文信息。这些开销在高并发场景下可能会对性能产生一定影响。因此,在使用 start 方法启动线程时,需要根据具体的应用场景进行性能优化,比如合理设置线程数量、优化线程任务逻辑等。

错误处理和异常机制

  1. 直接调用 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());
        }
    }
}
  1. 调用 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中 runstart 方法底层区别的详细分析,我们了解到它们在执行机制、内存模型、线程生命周期、实际应用场景、性能等多个方面都存在显著差异。在实际编程中,我们需要根据具体的需求来选择合适的方式。对于需要并发执行的任务,应使用 start 方法来启动新线程;而对于不需要并发的简单任务,可以直接调用 run 方法。同时,我们还需要注意线程安全、异常处理等相关问题,以编写高效、稳定的多线程程序。希望本文的内容能帮助读者更好地理解和运用Java多线程编程。