Java 中创建线程的所有方法
Thread、Runnable、Callable、ExecutorService 和 Future - Java 中创建线程的所有方法
1.通过扩展Thread
课程
创建线程最明显的(但在许多情况下不是正确的)方法是扩展Thread
类并重写run()
方法。仅当您想扩展 的功能时才使用它Thread
。
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello from a new thread!");
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
2.通过实现Runnable
接口
如果你不想扩展Thread
功能而只想在新线程中做一些事情,那么实现 Runnable 是最好的方法。但是创建它时,您只需指定应该在单独的线程中执行的操作。要实际启动它,您应该创建一个新的Thread
,将其Runnable
作为参数传递并运行该start()
方法。
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello from a new thread!");
}
});
thread.start();
如果您要Runnable
多次使用,则值得创建一个单独的接口而不是使用匿名类。否则,您应该使用 lambda 使其更加简洁(为了清楚起见,我没有使用 lambda)。
☠️ Java 线程是如何死亡的?
当一个线程完成了它被编程要做的所有操作时,它就会死亡。发生这种情况后,您将无法再次启动它。为了演示这一点,我编写了这段代码,您不需要分析该代码,但可能有助于理解该主题,特别是对于那些喜欢深入研究的人。您也可以尝试自己执行此操作,然后查看我的代码,这将是该主题的良好实践。
public static void main(String[] args) throws Exception{
Thread stopTestThread = new Thread(() -> {
System.out.println("Hello from " + Thread.currentThread().getName());
for (int i = 0; i < 3; i++){
System.out.println("I'm running " + (i+1));
// this method tells the current thread (which is stopTestThread in our
// case, as the code is written within Runnable run() method) to pause
// for 1 sec (1000 milliseconds)
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("Now I'm going to stop");
}, "StopTestThread");
// the following line invokes the stopTestThread for the first time
// the code in the lambda gets executed and the thread stops
stopTestThread.start();
stopTestThread.join();
// now it's main (root) thread's job
// this is the thread that runs the app by default
System.out.println();
System.out.println("Hello from " + Thread.currentThread().getName());
System.out.println("Trying to invoke " + stopTestThread.getName());
// We'll get IllegalThreadStateException, because a thread
// can't be started for the second time
stopTestThread.start();
}
输出:
Hello from StopTestThread
I'm running 1
I'm running 2
I'm running 3
Now I'm going to stop
Hello from main
Trying to invoke StopTestThread
Exception in thread "main" java.lang.IllegalThreadStateException
在此代码中,我们创建一个新线程并使用 2 参数构造函数将其称为 StopTestThread。在第一个参数中,我们使用 lambda 创建一个新的 Runnable 告诉线程要做什么。特别是,该线程将向用户打招呼,运行 3 秒,并告知何时停止。
然后执行新创建的 StopTestThread。当单独的线程执行其工作时,默认执行每个 Java 程序的主线程将继续其工作。它打印一些语句,然后尝试再次启动 StopTestThread。
您可能会注意到,尽管 StopTestThread 已死亡,我们仍然可以调用它的getName()
和isAlive()
方法。然而,这并不意味着我们可以让它起死回生。
join()
您可能还注意到,除非我们编写stopTestThread.join()
StopTestThread,否则 main 将同时工作,并且不会按顺序打印各行,这在我们的情况下是不需要的。
此方法告诉调用它的线程(在我们的例子中是主线程)等待,直到调用它的线程 (stopTestThread) 完成其工作。
另一种选择是指定时间间隔作为 的参数join
。这样,线程将暂停一段时间,然后恢复其任务,无论另一个线程是否已完成操作。
3.通过创建新的线程池
当您在程序中使用多个线程时,您可能会这样做以加速进程。但是一个Java线程对应一个系统线程,正如上一篇文章中提到的,它们的数量受到CPU及其核心数量的限制。另请记住,还有其他应用程序也需要一些线程。
创建太多线程(例如 100 个)效率不高,因为只会调度其中的一些线程。其余的将等到正在执行的人完成工作并死亡。只有那时他们才会取代他们的位置。此外,许多线程的诞生和消亡都会消耗大量的时间和资源。
这就是为什么,您应该更喜欢线程池而不是多个Thread
. 这种方式允许我们创建合理数量的线程,只要它们完成一项操作就不会死亡,而是会切换到另一个线程。方法如下:
ExecutorService executorService = Executors.newFixedThreadPool(threadsNumber);
for (int i = 0; i < 20; i++) {
executorService.submit(() -> System.out.println("Hello from: " + Thread.currentThread().getName()));
}
executorService.submit(() -> System.out.println("Another task executed by " + Thread.currentThread().getName()));
executorService.shutdown();
输出:
Hello from: pool-1-thread-3
Hello from: pool-1-thread-8
Hello from: pool-1-thread-6
Hello from: pool-1-thread-6
Hello from: pool-1-thread-6
Hello from: pool-1-thread-6
Hello from: pool-1-thread-6
Hello from: pool-1-thread-6
Hello from: pool-1-thread-6
Hello from: pool-1-thread-6
Hello from: pool-1-thread-6
Hello from: pool-1-thread-6
Hello from: pool-1-thread-6
Hello from: pool-1-thread-6
Hello from: pool-1-thread-7
Hello from: pool-1-thread-5
Hello from: pool-1-thread-3
Hello from: pool-1-thread-2
Hello from: pool-1-thread-1
Hello from: pool-1-thread-4
Another task executed by pool-1-thread-6
为此,您只需执行相同的操作,但使用ExecutorService
. 在上面的代码中,创建了一个新的 ThreadPool,其线程数等于threadsNumber
。通常,创建与 JVM 可用的处理器一样多的线程是一个不错的决定:
int threadsNumber = Runtime.getRuntime().availableProcessors();
该数字的值取决于计算机及其当前状态,例如可用资源和正在运行的应用程序。例如,我的电脑当时有 8 个可用的 CPU 核心。
所以上面的代码只是创建了 X 个线程,这些线程执行一些操作,20 次,在本例中,打印它们的名称。请注意,该操作总共执行 20 次,而不是每个线程单独执行。您还可以像我在 for 循环下面所做的那样提交其他任务。
输出显示程序员无法控制哪个线程执行任务,但 ExecutorService 和 ThreadScheduler 可以控制。正如您所看到的,由于某种原因,第 6 个线程完成了大部分工作,而且完全没问题。每次的结果都会不同。
submit()
方法告诉线程应该做什么并启动它。您还应该关闭执行器以阻止程序永远运行。
还有其他用于创建线程池的工厂方法、其他重载submit()
方法以及其他关闭执行器的方法,但这些超出了本文的范围。
ExecutorService
4.与Callable
and 一起 使用Future
一直以来,我们都使用创建了新线程Runnable
。但您可能已经注意到它有一个缺点 - 它无法返回值。
当然,您可以创建一个数据类并用于Runnable
保留一些值。但有一个问题。在单线程程序中,您可以在填充变量后立即使用该变量。在多线程程序中,一个变量可以在不同的线程中被赋值。如何判断变量是否有值?
Callable
所以这种方法并不有效,并且可能会导致错误,但是可以通过使用以下方法来解决问题Runnable
:
executorService.submit(new Callable<>() {
@Override
public Integer call() {
return new Random().nextInt();
}
});
这段代码创建了一个单独的线程来组成一个随机int
值并返回它。
基本上,您执行相同的操作,但是当您向 提交任务时ExecutorService
,您使用Callable
. 同样,为了清晰起见,我使用了 Anonymous 类,但为了简洁起见,您应该使用 lambda。
如果您将“通过创建新线程池”部分中的代码片段中的相应部分替换为使用的代码片段Callable
,则代码将编译并运行而不会出现任何问题,但您将不会获得返回的结果,因为您只是不这样做t 将其分配给任何变量。
为了得到结果,你应该写:
// If your value is not int, set the generic type.
Future<Integer> future = executor.submit(() -> new Random().nextInt());
在我们的例子中,我们生成一个随机数int
,这是立即完成的。但您通常会想要执行一些代码来执行一些长时间操作,然后才返回结果。这就是为什么我们不能简单地将结果分配给 anint
或 smth else。Future
被用来代替。Future
是一个占位符。只要新线程尚未完成其工作,它就不包含任何值。
当单独的线程正在计算某些内容时,主线程继续其工作。当您认为终于可以计算出该值时,您可以写入future.get()
并获取实际值。但要小心:这一次如果值还没有被赋值并且 future 仍然是空的,主线程将不得不等待直到它发生。幸运的是,我们有一些有用的方法来控制该过程,例如:
// wait for 1 sec and then throw TimeoutException, if it still hasn't finished
future.get(1, TimeUnit.SECONDS);
future.cancel(true);
future.isCancelled();
future.isDone();
最后Callable
不能与with一起使用Thread
,所以如果想创建单线程就用Callable
write Executors.*newSingleThreadExecutor*()
。
ExecutorService
这是with的完整代码Callable
:
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return new Random().nextInt();
});
try {
System.out.println("Result: " + future.get(1, TimeUnit.SECONDS));
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} catch (TimeoutException e) {
System.out.println("Couldn't complete the task before timeout");
}
executor.shutdown();
}
输出:
Couldn't complete the task before timeout
Process finished with exit code 0
5. 通过使用CompletableFuture
(对于那些想要更深入了解的人)
您可能已经注意到,虽然Future
提供了方便的功能,但我们仍然无法在填充任务后立即执行任务,因为我们不知道确切的时间。future
实际上,我们可以,但是这种方法可能会导致主线程阻塞(如果仍然是占位符,则必须等待)。
如果我们不想阻塞主线程,为什么不使用另一个单独的线程💁♂️?好的,但是我们如何通知它任务已经执行了呢?CompletableFuture
可以帮助。让我们考虑以下代码:
public static void main(String[] args) {
String[] names = {"Daniel", "Mary", "Alex", "Andrew"};
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println("Hello from " + Thread.currentThread().getName());
System.out.println("I'll give you the name in 5 sec");
for (int i = 5; i > 0; i--){
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Exception while sleeping in " + Thread.currentThread().getName());
e.printStackTrace();
}
}
return randomElement(names);
});
completableFuture.thenAcceptAsync(name -> {
System.out.println("Hello from " + Thread.currentThread().getName());
System.out.println("Now I'm going to print you the name");
System.out.println("Result: " + name);
});
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
System.out.println("Exception while sleeping in " + Thread.currentThread().getName());
e.printStackTrace();
}
}
private static String randomElement(String[] array) {
return array[new Random().nextInt(array.length)];
}
在这段代码中,我们CompletableFuture<T>
使用工厂方法创建了一个supplyAsync
。lambda 表达式中的代码将在单独的线程中执行。究竟会发生什么是这个线程将打印 5 秒倒计时到控制台,然后从数组返回一个随机名称names[]
,该名称将分配给completableFuture
变量。
目前,它类似于Future
,我们可以使用 来从占位符中获取值.get()
。但随后我们调用completableFuture.thenAcceptAsync
,它会创建一个新线程,并且 lambda 表达式中的任务会在变量填充后立即执行。 现在,我们已经实现了我们想要的功能。completableFuture
当其他线程仔细地从数组中选择一个名称并打印它时,主线程做了一些更重要的事情(休眠 10 秒)😆。
😈 守护线程 - 另一个重要的事情
这里有一件有趣的事情。如果删除调用 的 try-catch 块Thread.sleep(10_000)
,输出将如下所示:
Process finished with exit code 0
你认为为什么会这样?控制台上没有打印任何内容。
那是因为我们没有写join()
。主线程不会等到其他线程完成。由于它没有被编程为在启动它们后执行任何操作,因此它完成工作并退出程序。但是,其他线程仍在运行,但不会在控制台上打印任何内容,因为它们无权访问它。如果我们调用它,join()
它会等待它们完成。
但它仅适用于😈守护线程。join()
除非被调用,否则主线程永远不会等待它们完成。在我们的例子中,内部创建的线程completableFuture
是守护线程。守护线程的另一个例子是垃圾收集器(它可能在程序完成后仍然运行)。
有关守护线程的更多信息:如果创建的线程是由另一个守护线程创建的,则默认情况下该线程是守护线程,反之亦然。主线程默认是非守护线程。
那么为什么内部创建的线程completableFuture
是守护进程呢?因为CompletableFuture.supplyAsync
是由内部管理的ForkJoinPool
。默认行为ForkJoinPool
是创建守护线程。
您可以使用以下方法控制线程各自的行为:
public final boolean isDaemon()
public final void setDaemon(boolean on)
🤯 我为什么要使用它?
现在您可能想知道如果我们可以使用简单的Runnable
withThread
并通过更简洁和有效的代码实现相同的目的,为什么我们要做所有这些复杂的事情。我们不应该!请记住:这只是一个例子。我们CompletableFuture
可以解决各种各样的问题,包括将多个异步操作链接在一起、异常处理、完成处理和其他高级功能。因此,如果您正在开发一个需要它们的复杂项目,请不要重新发明轮子,而是使用此类高效的库和框架CompletableFuture
。顺便说一下,您可以使用CompletableFuture
with ExecutorService
。
CompletableFuture
在我看来,只要不需要,就不应该掌握。只需知道有一些方法可以异步执行任务而不阻塞。但如果您确实需要CompletableFuture
,我建议您阅读本文以及“有用资源”部分中的其他文章。
结论
总而言之,Java 为多线程提供了各种选项,从简单Thread
到Runnable
复杂CompletableFuture
。使用Thread
withRunnable
创建一个执行操作但不返回任何内容的新线程。如果你想使用多线程,最好选择ExecutorService
. 如果您想返回某些东西,请将其与 一起使用Callable
。对于复杂的事情,例如将多个异步操作链接在一起、异常处理和完成处理,请使用CompletableFuture
和其他有用的框架。
文章如果没看够可以,B站搜索千锋教育
- 点赞
- 收藏
- 关注作者
评论(0)