Java 中创建线程的所有方法

举报
千锋教育 发表于 2023/06/27 15:00:29 2023/06/27
【摘要】 Thread、Runnable、Callable、ExecutorService 和 Future - Java 中创建线程的所有方法1.通过扩展Thread课程创建线程最明显的(但在许多情况下不是正确的)方法是扩展Thread类并重写run()方法。仅当您想扩展 的功能时才使用它Thread。public class MyThread extends Thread { @Overri...

Thread、Runnable、Callable、ExecutorService 和 Future - Java 中创建线程的所有方法

企业微信截图_20230627145949.jpg

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()方法以及其他关闭执行器的方法,但这些超出了本文的范围。

ExecutorService4.与Callableand 一起 使用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,所以如果想创建单线程就用Callablewrite 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)

🤯 我为什么要使用它?

现在您可能想知道如果我们可以使用简单的RunnablewithThread并通过更简洁和有效的代码实现相同的目的,为什么我们要做所有这些复杂的事情。我们不应该!请记住:这只是一个例子。我们CompletableFuture可以解决各种各样的问题,包括将多个异步操作链接在一起、异常处理、完成处理和其他高级功能。因此,如果您正在开发一个需要它们的复杂项目,请不要重新发明轮子,而是使用此类高效的库和框架CompletableFuture。顺便说一下,您可以使用CompletableFuturewith ExecutorService

CompletableFuture在我看来,只要不需要,就不应该掌握。只需知道有一些方法可以异步执行任务而不阻塞。但如果您确实需要CompletableFuture,我建议您阅读本文以及“有用资源”部分中的其他文章。

结论

总而言之,Java 为多线程提供了各种选项,从简单ThreadRunnable复杂CompletableFuture。使用ThreadwithRunnable创建一个执行操作但不返回任何内容的新线程。如果你想使用多线程,最好选择ExecutorService. 如果您想返回某些东西,请将其与 一起使用Callable。对于复杂的事情,例如将多个异步操作链接在一起、异常处理和完成处理,请使用CompletableFuture和其他有用的框架。

文章如果没看够可以,B站搜索千锋教育


【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。