Многопоточность — это метод написания кода для параллельного выполнения задач. Java имеет отличную поддержку для написания многопоточного кода с первых дней Java 1.0. Недавние усовершенствования Java расширили способы структурирования кода для включения многопоточности в программы Java.
В этой статье мы сравниваем некоторые из этих вариантов, чтобы вы могли лучше оценить, какой вариант использовать для вашего следующего проекта Java t.
Метод 1: Расширение класса Thread
Java предоставляет класс Thread, который можно расширить для реализации метода run () . Этот метод run () используется для реализации вашей задачи. Если вы хотите запустить задачу в своем собственном потоке, вы можете создать экземпляр этого класса и вызвать его метод start () . Это запускает выполнение потока и завершается (или завершается в исключении).
Вот простой класс Thread, который просто спит в течение заданного интервала как способ имитации длительной операции.
public class MyThread extends Thread { private int sleepFor; public MyThread(int sleepFor) { this.sleepFor = sleepFor; } @Override public void run() { System.out.printf("[%s] thread starting\n", Thread.currentThread().toString()); try { Thread.sleep(this.sleepFor); } catch(InterruptedException ex) {} System.out.printf("[%s] thread ending\n", Thread.currentThread().toString()); } }
Создайте экземпляр этого класса Thread, указав количество спящих миллисекунд.
MyThread worker = new MyThread(sleepFor);
Начните выполнение этого рабочего потока, вызвав его метод start (). Этот метод возвращает управление немедленно вызывающей стороне, не ожидая завершения потока.
worker.start(); System.out.printf("[%s] main thread\n", Thread.currentThread().toString());
И вот результат выполнения этого кода. Это указывает на то, что диагностика основного потока печатается до выполнения рабочего потока.
[Thread[main,5,main]] main thread [Thread[Thread-0,5,main]] thread starting [Thread[Thread-0,5,main]] thread ending
Поскольку после запуска рабочего потока больше нет операторов, основной поток ожидает завершения рабочего потока до завершения программы. Это позволяет рабочему потоку выполнить свою задачу.
Способ 2: использование экземпляра потока с работоспособным
Java также предоставляет интерфейс Runnable, который может быть реализован рабочим классом для выполнения задачи в его методе run () . Это альтернативный способ создания рабочего класса в отличие от расширения класса Thread (описанного выше).
Вот реализация рабочего класса, который теперь реализует Runnable вместо расширения Thread.
public class MyThread2 implements Runnable { // same as above }
Преимущество реализации интерфейса Runnable вместо расширения класса Thread заключается в том, что рабочий класс теперь может расширять доменный класс внутри иерархии классов.
Что это значит?
Допустим, например, у вас есть класс Fruit, который реализует определенные общие характеристики фруктов. Теперь вы хотите реализовать класс папайи, который специализируется на определенных характеристиках фруктов. Вы можете сделать это, добавив класс Papaya в класс Fruit .
public class Fruit { // fruit specifics here } public class Papaya extends Fruit { // override behavior specific to papaya here }
Теперь предположим, что у вас есть какое-то трудоемкое задание, которое нужно поддерживать Papaya, которое можно выполнить в отдельном потоке. Этот случай может быть обработан с помощью класса Papaya, реализующего Runnable и предоставляющего метод run (), где выполняется эта задача.
public class Papaya extends Fruit implements Runnable { // override behavior specific to papaya here @Override public void run() { // time consuming task here. } }
Чтобы запустить рабочий поток, вы создаете экземпляр рабочего класса и передаете его в экземпляр Thread при создании. Когда вызывается метод start () потока, задача выполняется в отдельном потоке.
Papaya papaya = new Papaya(); // set properties and invoke papaya methods here. Thread thread = new Thread(papaya); thread.start();
И это краткое изложение того, как использовать Runnable для реализации задачи, выполняемой в потоке.
Метод 3: Выполнить Runnable с ExecutorService
Начиная с версии 1.5, Java предоставляет ExecutorService в качестве новой парадигмы для создания и управления потоками в программе. Он обобщает концепцию выполнения потоков, абстрагируя их от создания.
Это потому, что вы можете запускать свои задачи в пуле потоков так же легко, как используя отдельный поток для каждой задачи. Это позволяет вашей программе отслеживать и управлять тем, сколько потоков используется для рабочих задач.
Предположим, у вас есть 100 рабочих задач, ожидающих выполнения. Если вы запустите один поток на каждого работника (как показано выше), в вашей программе будет 100 потоков, что может привести к узким местам в других частях программы. Вместо этого, если вы используете пул потоков, скажем, с 10 выделенными потоками, ваши 100 задач будут выполняться этими потоками один за другим, поэтому ваша программа не будет нуждаться в ресурсах. Кроме того, эти потоки пула потоков можно настроить так, чтобы они зависали для выполнения дополнительных задач за вас.
ExecutorService принимает задачу Runnable (объяснено выше) и запускает задачу в подходящее время. Метод submit () , который принимает задачу Runnable, возвращает экземпляр класса с именем Future , который позволяет вызывающей стороне отслеживать состояние задачи. В частности, метод get () позволяет вызывающей стороне ожидать завершения задачи (и предоставляет код возврата, если таковой имеется).
В приведенном ниже примере мы создаем ExecutorService с использованием статического метода newSingleThreadExecutor () , который, как следует из названия, создает единый поток для выполнения задач. Если при выполнении одной задачи отправляется больше задач, ExecutorService помещает эти задачи в очередь для последующего выполнения.
Реализация Runnable, которую мы здесь используем, такая же, как описанная выше.
ExecutorService esvc = Executors.newSingleThreadExecutor(); Runnable worker = new MyThread2(sleepFor); Future<?> future = esvc.submit(worker); System.out.printf("[%s] main thread\n", Thread.currentThread().toString()); future.get(); esvc.shutdown();
Обратите внимание, что служба ExecutorService должна быть корректно закрыта, когда она больше не нужна для дальнейшей отправки задач.
Метод 4: вызываемое используется с ExecutorService
Начиная с версии 1.5, Java представила новый интерфейс под названием Callable . Он аналогичен старому интерфейсу Runnable с той разницей, что метод выполнения (вызываемый call () вместо run () ) может возвращать значение. Кроме того, он также может объявить, что может быть сгенерировано исключение .
ExecutorService также может принимать задачи, реализованные как Callable, и возвращает Future со значением, возвращаемым методом по завершении.
Вот пример класса Mango, который расширяет класс Fruit, определенный ранее, и реализует интерфейс Callable . В методе call () выполняется дорогостоящая и трудоемкая задача.
public class Mango extends Fruit implements Callable { public Integer call() { // expensive computation here return new Integer(0); } }
А вот код для отправки экземпляра класса в ExecutorService. Приведенный ниже код также ожидает завершения задачи и печатает возвращаемое значение.
ExecutorService esvc = Executors.newSingleThreadExecutor(); MyCallable worker = new MyCallable(sleepFor); Future future = esvc.submit(worker); System.out.printf("[%s] main thread\n", Thread.currentThread().toString()); System.out.println("Task returned: " + future.get()); esvc.shutdown();
Что ты предпочитаешь?
В этой статье мы изучили несколько методов написания многопоточного кода на Java. Они включают:
- Расширение класса Thread является самым базовым и было доступно в Java 1.0.
- Если у вас есть класс, который должен расширять какой-то другой класс в иерархии классов, вы можете реализовать интерфейс Runnable .
- Более современное средство для создания потоков — это ExecutorService, который может принимать экземпляр Runnable в качестве задачи для запуска. Преимущество этого метода в том, что вы можете использовать пул потоков для выполнения задач. Пул потоков помогает в сохранении ресурсов путем повторного использования потоков.
- Наконец, вы также можете создать задачу, реализовав интерфейс Callable и отправив задачу в ExecutorService.
Как вы думаете, какие из этих опций вы будете использовать в своем следующем проекте? Дайте нам знать в комментариях ниже.