多线程是编程中的一个重要概念,它允许程序同时执行多个任务,每个任务可以看作是一个线程。在Java中,多线程尤为常见且强大,它通过允许程序在并发环境下运行,提高了程序的执行效率和响应速度。以下是对Java多线程的详细讲解:
基本概念
- 线程(Thread):线程是进程中的实体,是CPU调度和分派的基本单位,它是比进程更小的独立运行的单位。线程一般不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器、一组寄存器和栈),但它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
- 进程(Process):进程是程序的一次动态执行过程,是程序代码、数据和相关资源的集合。一个进程可以拥有多个线程,这些线程共享进程的地址空间和系统资源。
Thread
类
在Java中,Thread
类是处理多线程的核心类。每个线程都是通过 Thread
类的一个实例来表示的。Java 允许你继承 Thread
类来创建新的线程类,或者实现 Runnable
接口。下面我将详细解释 Thread
类中的几个关键方法。
Thread类的常用方法
- public void run()
线程的任务方法。当线程启动时,会自动调用此方法。在继承Thread类创建线程时,通常需要重写此方法以定义线程的具体任务。 - public void start()
启动线程。调用此方法后,线程会进入就绪状态,等待CPU调度执行。注意,直接调用run()方法并不会启动新线程,而是像普通方法一样在当前线程中执行。 - public String getName()
获取当前线程的名称。默认情况下,线程的名称是"Thread-索引",其中索引是一个递增的整数。 - public void setName(String name)
为线程设置名称。通过此方法可以自定义线程的名称,便于在调试和日志记录中识别不同的线程。 - public static Thread currentThread()
获取当前执行的线程对象。此方法允许在代码中获取当前正在执行的线程实例,进而可以调用该线程的方法或属性。 - public static void sleep(long time)
让当前执行的线程休眠指定的毫秒数后,再继续执行。这是一个静态方法,用于暂停当前线程的执行,让出CPU资源给其他线程。 - public final void join()
让调用当前这个方法的线程先执行完。这个方法的作用是等待调用它的线程(即当前线程)终止。在join()方法返回之前,其他线程(即调用join()方法的线程)无法继续执行。
Thread类的常见构造器
- public Thread(String name)
可以为当前线程指定名称。通过构造器中的name参数,可以为线程设置一个易于识别的名称。 - public Thread(Runnable target)
封装Runnable对象成为线程对象。这种方式是实现多线程的另一种途径,即实现Runnable接口。通过这种方式,可以将线程的任务与线程本身分离,使得代码更加灵活。 - public Thread(Runnable target, String name)
封装Runnable对象成为线程对象,并指定线程名称。这个构造器结合了上述两种构造器的功能,既可以将线程的任务与线程本身分离,又可以自定义线程的名称。
创建方式一
通过继承Thread
类来创建线程
步骤 1: 定义子类继承Thread
类
首先,你需要定义一个子类来继承Java的java.lang.Thread
类。在这个子类中,你需要重写run
方法。run
方法是线程启动时执行的代码块。
步骤 2: 创建MyThread
类的对象
接着,你需要创建MyThread
类的一个或多个对象。这些对象就是线程实例。
步骤 3: 调用线程对象的start
方法启动线程
最后,你需要调用线程对象的start
方法来启动线程。调用start
方法会启动一个新的线程,并让这个线程执行其run
方法中的代码。注意,不要直接调用run
方法,这样会导致代码在调用它的线程(通常是主线程)中顺序执行,而不会创建新的线程。
为了举一个更明显的例子来说明通过继承Thread
类来创建线程,我们可以考虑一个简单的场景:有两个线程,一个负责打印奇数,另一个负责打印偶数。我们将分别创建两个类(OddThread
和EvenThread
)来继承Thread
类,并在它们的run
方法中实现打印奇数或偶数的逻辑。
// OddThread 类,继承自 Thread 类,用于打印奇数
class OddThread extends Thread {
private int start;
private int end;
public OddThread(int start, int end) {
this.start = start;
this.end = end;
}
@Override
public void run() {
for (int i = start; i <= end; i += 2) {
System.out.println(Thread.currentThread().getName() + " 打印奇数: " + i);
try {
// 为了更明显地看到线程切换,可以添加一些延迟
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// EvenThread 类,继承自 Thread 类,用于打印偶数
class EvenThread extends Thread {
private int start;
private int end;
public EvenThread(int start, int end) {
this.start = start;
if (end % 2 == 0) {
this.end = end;
} else {
this.end = end - 1; // 确保结束值是偶数
}
}
@Override
public void run() {
for (int i = start; i <= end; i += 2) {
System.out.println(Thread.currentThread().getName() + " 打印偶数: " + i);
try {
// 为了更明显地看到线程切换,可以添加一些延迟
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 主类
public class ThreadExample {
public static void main(String[] args) {
// 创建并启动打印奇数的线程
OddThread oddThread = new OddThread(1, 10);
oddThread.start();
// 创建并启动打印偶数的线程
EvenThread evenThread = new EvenThread(2, 10);
evenThread.start();
// 注意:主线程会继续执行,不会等待oddThread和evenThread执行完成
}
}
由于线程的执行是并发的,所以输出结果的顺序可能会有所不同,这取决于JVM的线程调度策略以及操作系统的线程管理机制。
优缺点
优点:编码简单
缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。
注意
创建方式二
通过实现Runnable
接口方式
1. 为什么使用Runnable接口?
在Java中,创建线程主要有两种方式:继承Thread
类和实现Runnable
接口。虽然继承Thread
类是一种直观的方式,但它限制了类的继承体系(因为Java不支持多重继承)。相比之下,实现Runnable
接口更为灵活,因为它允许你的类继承自其他类,并且仍然可以拥有多线程的能力。
2. Runnable接口简介
Runnable
是一个函数式接口(从Java 8开始),它只定义了一个方法:run()
。当你创建了一个实现了Runnable
接口的类的实例后,你可以将这个实例作为参数传递给Thread
类的构造函数,从而创建一个新的线程。
线程创建的基本步骤(使用Runnable
接口)
①定义Runnable
实现类:
首先,你需要定义一个类来实现Runnable
接口。实现接口意味着你必须提供run
方法的实现。这个run
方法将包含线程执行时所需的所有代码。
class MyTask implements Runnable {
@Override
public void run() {
// 在这里编写线程的任务代码
System.out.println(Thread.currentThread().getName() + " is running.");
// 假设这里有一些耗时的操作或逻辑处理
}
}
②创建Runnable
实现类的实例:
一旦你定义了Runnable
实现类,就可以创建这个类的实例了。这个实例将作为线程执行的任务。
Runnable myTask = new MyTask();
③将Runnable
实例传递给Thread
构造函数:
接下来,你需要将Runnable
实例作为参数传递给Thread
类的构造函数。这个构造函数会创建一个新的Thread
对象,这个对象封装了Runnable
实现类的实例。
Thread myThread = new Thread(myTask, "MyCustomThread");
注意,这里的第二个参数是可选的,用于指定线程的名称。如果省略,线程将使用默认名称。
启动线程:
最后,你需要调用线程的start
方法来启动线程。调用start
方法会导致JVM调用Runnable
实现类的run
方法,在新的线程中执行。
myThread.start();
重要的是要区分start
方法和run
方法。start
方法用于启动线程,而run
方法则定义了线程执行的任务。如果你直接调用run
方法(如myTask.run()
),那么run
方法中的代码将在当前线程中同步执行,而不是在新的线程中。
完整示例
将上述步骤组合起来,我们得到以下完整的示例:
class MyTask implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " is running, iteration " + i);
try {
// 模拟耗时操作
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
Runnable myTask = new MyTask();
// 创建并启动第一个线程
Thread thread1 = new Thread(myTask, "Thread-1");
thread1.start();
// 创建并启动第二个线程(注意:这里可以创建任意数量的线程)
Thread thread2 = new Thread(myTask, "Thread-2");
thread2.start();
// main线程继续执行其他任务或结束
System.out.println("Main thread is ending.");
}
}
在这个示例中,我们创建了两个线程(thread1
和thread2
),它们都执行相同的任务(由MyTask
类的run
方法定义)。这两个线程将并发执行,各自打印出自己的迭代次数,并模拟了耗时操作(通过Thread.sleep
)。同时,main
线程在启动了两个线程后继续执行并结束,而不会影响或等待这两个子线程的执行。
优点
任务类只是实现接口,可以继续继承其他类、实现其它接口,扩展性强。
创建方式三
通过Callable
接口和FutureTask
类来创建线程的方法
让我们详细讲解一下Java中通过Callable
接口和FutureTask
类来创建线程的方法,并以上面提到的类似代码来举例。
Callable接口
Callable
接口与Runnable
接口类似,都是为了被线程执行而设计的。但与Runnable
不同的是,Callable
可以返回值,并且它可以抛出异常。这使得Callable
接口在某些场景下比Runnable
更加灵活和强大。
FutureTask类
FutureTask
类实现了Future
和Runnable
接口。它可以将Callable
或Runnable
对象包装起来,以便有返回值的任务可以被提交给Executor
执行。如果任务通过Callable
包装,那么FutureTask
将返回执行结果;如果通过Runnable
包装,那么FutureTask
的get()
方法将返回null
。
构造器
public FutureTask<>(Callable<V> callable)
:这个构造器接受一个Callable<V>
类型的参数。Callable
是一个类似于Runnable
的接口,但它可以返回一个结果并且可以抛出一个异常。FutureTask
会将这个Callable
对象封装成一个可以异步执行的任务。与Runnable
不同,Callable
的call
方法可以有返回值,并且可以声明抛出异常。
方法
public V get() throws InterruptedException, ExecutionException
:这个方法用于等待计算完成,并检索其结果。如果在计算完成之前调用此方法,它将会阻塞当前线程,直到计算完成。此方法会抛出两种类型的异常:InterruptedException
:如果当前线程在等待过程中被中断,则会抛出此异常。ExecutionException
:如果计算抛出异常,则会通过此异常包装并抛出。
示例代码
假设我们有一个任务,该任务需要计算一个整数的阶乘,并返回结果。我们可以使用Callable
接口来定义这个任务,并使用FutureTask
来包装它,以便可以提交给线程池执行。
public class FactorialCallable implements Callable<Long> {
private int number;
public FactorialCallable(int number) {
this.number = number;
}
//重写call方法
@Override
public Long call() throws Exception {
//描述线程的任务,返回执行后的结果
long result = 1;
for (int i = 1; i <= number; i++) {
result *= i;
}
return result;
}
}
public class DirectThreadExample {
public static void main(String[] args) {
// 创建 Callable 任务实例
FactorialCallable task = new FactorialCallable(5);
// 由于 Thread 不能直接执行 Callable,我们需要将 Callable 包装为 FutureTask
FutureTask<Long> futureTask = new FutureTask<>(task);
// 创建一个新的 Thread,并将 FutureTask 作为其任务
Thread thread = new Thread(futureTask);
// 启动线程
thread.start();
try {
// 等待任务完成并获取结果
Long result = futureTask.get(); // 这会阻塞当前线程直到任务完成
System.out.println("Factorial of 5 is: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
// 注意:在实际应用中,你可能需要处理线程的中断和优雅关闭等逻辑
}
}
优点
线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
可以在线程执行完毕后去获取线程执行的结果。