在 Java 中,从最底层的机制来讲,真正创建并启动一个新线程的操作是通过new Thread。当调用Thread类的start方法时,JVM 会为这个线程分配资源,包括栈空间等,并开始执行线程的逻辑。例如,Thread类内部会和操作系统的线程机制进行交互,来实现真正的线程创建和调度。
我们常说的实现Runnable和Callable接口本身并没有直接创建线程的功能。它们主要是定义了线程要执行的任务内容。Runnable接口的run方法和Callable接口的call方法包含了线程执行的逻辑。可以把它们看作是一种任务规范,用于告诉线程 “你要做什么”,而不是 “如何创建你”。
我们来详细说一下其异同
一、从线程创建本质和不同方式分析
(一)继承Thread类
- 原理:
Thread类是 Java 用于创建和控制线程的基础类。通过继承Thread类并重写run方法,就定义了线程的执行逻辑。当调用线程对象的start方法时,JVM 会为这个线程分配资源,如栈空间等,并且自动调用run方法,从而新线程开始执行定义好的逻辑。例如:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程执行的逻辑,例如打印当前线程名称:" + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
-
优缺点:
- 优点:这种方式简单直观,对于简单的线程逻辑比较容易理解和实现。
- 缺点:由于 Java 是单继承语言,若一个类已经继承了其他类,就无法再继承
Thread类来创建线程。而且对于多个线程有相同逻辑的情况,代码复用性较差。
(二)实现Runnable接口
- 原理:
Runnable接口定义了一个无返回值的run方法。实现这个接口,就能将线程执行逻辑封装在run方法中。然后把实现Runnable接口的类的实例作为参数传递给Thread类的构造函数来创建Thread对象,再调用start方法启动线程。这样,线程启动后会执行Runnable接口实现类中的run方法。例如:
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程执行的逻辑,例如打印当前线程名称:" + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
-
与
new Thread的关系及观点解释:从最底层的线程创建本质来说,Runnable接口本身没有直接创建线程的功能。虽然是一种创建线程的方式,但实际上还是依赖new Thread来启动线程。它主要是作为任务抽象,定义了线程要执行的任务内容,即run方法包含了任务逻辑,告诉线程要做什么,而不是如何创建线程。 -
优缺点:
- 优点:实现接口的方式避免了单继承的限制,代码复用性更好。可以让多个线程共享同一个
Runnable实现类的实例,方便对线程逻辑进行统一管理。 - 缺点:如果需要获取线程执行的结果,这种方式不太方便,因为
run方法没有返回值。
- 优点:实现接口的方式避免了单继承的限制,代码复用性更好。可以让多个线程共享同一个
(三)实现Callable接口(结合Future和FutureTask)
- 原理:
Callable接口定义了一个有返回值的call方法。实现Callable接口后,在call方法中定义线程执行逻辑并返回结果。接着将Callable实现类的实例包装到FutureTask对象中(FutureTask实现了RunnableFuture接口,而RunnableFuture继承了Runnable和Future接口),再将FutureTask对象作为参数传递给Thread类的构造函数来启动线程。通过Future接口的方法可以获取线程执行的结果。例如:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable<String> {
@Override
public String call() {
return "线程执行的结果,例如返回一个字符串";
}
}
public class Main {
public static void main(String[] args) {
MyCallable myCallable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<>(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
try {
System.out.println("获取线程执行的结果:" + futureTask.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
-
与
new Thread的关系及观点解释:和Runnable类似,Callable接口本身也没有直接创建线程的能力。它同样是作为任务定义的一种方式,需要借助FutureTask包装并通过new Thread来启动线程,重点在于定义任务的返回值逻辑。 -
优缺点:
- 优点:可以获取线程执行后的返回值,对于需要返回结果的线程任务非常有用。例如在分布式计算、异步任务处理等场景中,可以方便地获取计算结果。
- 缺点:代码相对复杂一些,需要处理
Future接口相关的操作,如get方法可能会抛出InterruptedException和ExecutionException异常,需要进行适当的异常处理。
(四)使用线程池(Executor框架)创建线程
- 原理:
Executor框架提供了一种方便的方式来管理和复用线程。它包括Executor接口、ExecutorService接口(Executor的子接口)、ThreadPoolExecutor类(实现ExecutorService接口)等。通过创建线程池对象(如ThreadPoolExecutor),可以向线程池提交任务(Runnable或Callable实现类)。线程池会根据配置(如核心线程数、最大线程数、队列容量等)来管理线程的创建、复用和销毁。例如,使用Executors工具类创建简单的线程池:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程执行的逻辑,例如打印当前线程名称:" + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
MyRunnable myRunnable = new MyRunnable();
executorService.submit(myRunnable);
}
executorService.shutdown();
}
}
-
与
new Thread的关系及观点解释:在使用线程池时,虽然从表面上看没有直接使用new Thread,但线程池内部实际上还是基于new Thread的原理来创建和管理线程。线程池是一种更高级的抽象,它在后台根据任务需求和配置来合理地创建、复用线程,而提交给线程池的Runnable或Callable任务依然是定义了线程要执行的内容。 -
优缺点:
-
优点:可以有效地管理线程资源,避免频繁地创建和销毁线程带来的性能开销。能够根据任务的数量和系统资源合理配置线程池的参数,提高系统的并发处理能力。对于大量的线程任务,线程池能够更好地进行资源分配和任务调度。
-
缺点:需要对线程池的参数(如核心线程数、最大线程数、队列容量等)进行合理配置,否则可能会出现资源浪费或者任务队列溢出等问题。同时,线程池的使用相对复杂一些,需要正确地处理线程池的生命周期(如启动、关闭)和任务提交、执行的相关操作。
-
从设计模式和职责分离角度看,这种设计符合职责分离原则。Thread类主要负责线程的创建、启动、暂停、恢复等操作,这些操作和操作系统线程管理相关,是底层的机制。而Runnable和Callable接口则专注于任务的定义,将任务内容从线程的创建和管理中分离出来,使代码更加清晰,便于维护和扩展。这种方式也体现了策略模式的思想,Runnable和Callable可以看作是不同的任务策略,Thread类则是执行这些策略的上下文,通过这种方式可以在运行时灵活地选择不同任务策略来执行,而无需修改线程的创建和启动机制。