4 метода написания многопоточного кода на Java
Программирование

4 метода написания многопоточного кода на Java

В этой статье мы сравним несколько вариантов написания многопоточного кода на Java, чтобы вы могли лучше оценить, какой вариант использовать для вашего следующего проекта на Java

Многопоточность – это метод написания кода для параллельного выполнения задач.Java имеет отличную поддержку для написания многопоточного кода с ранних дней Java 1. 0. Недавние усовершенствования Java расширили возможности структурирования кода для включения многопоточности в Java-программы

В этой статье мы сравним несколько из этих вариантов, чтобы вы могли лучше понять, какой вариант использовать для вашего следующего проекта Java

Метод 1: Расширение класса Thread

Java предоставляет класс Thread , который может быть расширен для реализации метода run(). В этом методе run() вы реализуете свою задачу. Когда вы хотите запустить задачу в ее собственном потоке, вы можете создать экземпляр этого класса и вызвать его метод start(). Это запустит выполнение потока и запустит его до завершения (или завершит в виде исключения)

Вот простой класс Thread, который просто спит в течение заданного интервала времени, как способ имитации длительной работы

publicclassMyThreadextendsThread

privateintsleepFor;

publicMyThreadintsleepFor)
  this.sleepFor = sleepFor;

@Override
publicvoidrun()
  System.out.printf(' %s  thread starting
'
  Thread.currentThread().toString());
  try{ Thread.sleep(this.sleepFor); }
  catch(InterruptedException ex) {}
  System.out.printf(' %s  thread ending
'
  Thread.currentThread().toString());

Создайте экземпляр этого класса Thread, задав ему количество миллисекунд для сна

MyThread worker =newMyThread(sleepFor);

Запустите выполнение этого рабочего потока, вызвав его метод start(). Этот метод немедленно возвращает управление вызывающей стороне, не дожидаясь завершения потока

worker.start();
System.out.printf(' %s  main thread
', Thread.currentThread().toString());

А вот результат выполнения этого кода. Он показывает, что диагностика главного потока выводится перед выполнением рабочего потока

 Thread main,,main   main thread
 Thread Thread-,main   thread starting
 Thread Thread-,main   thread ending

Поскольку после запуска рабочего потока больше нет никаких утверждений, главный поток ждет завершения рабочего потока перед завершением программы. Это позволяет рабочему потоку завершить свою задачу

Метод 2: Использование экземпляра потока с возможностью выполнения

Java также предоставляет интерфейс под названием Runnable , который может быть реализован рабочим классом для выполнения задачи в его методе run(). Это альтернативный способ создания рабочего класса в отличие от расширения класса Thread (описанного выше)

Вот реализация класса worker, который теперь реализует Runnable вместо расширения Thread

publicclassMyThread2implementsRunnable
// same as above

Преимущество реализации интерфейса Runnable вместо расширения класса Thread заключается в том, что рабочий класс теперь может расширять специфичный для домена класс в иерархии классов

Что это значит?

Допустим, у вас есть класс Fruit , который реализует определенные общие характеристики фруктов. Теперь вы хотите реализовать класс Papaya , который специализируется на определенных характеристиках фруктов. Вы можете сделать это, если класс Papaya будет расширять класс Fruit

publicclassFruit
// fruit specifics here

publicclassPapayaextendsFruit
// override behavior specific to papaya here

Теперь предположим, что у вас есть какая-то трудоемкая задача, которую Papaya должна поддерживать и которая может быть выполнена в отдельном потоке. Этот случай можно решить, если класс Papaya реализует Runnable и предоставляет метод run(), в котором выполняется эта задача

publicclassPapayaextendsFruitimplementsRunnable
// override behavior specific to papaya here

@Override
publicvoidrun()
  // time consuming task here.

Чтобы запустить рабочий поток, вы создаете экземпляр класса worker и при создании передаете его экземпляру Thread. Когда вызывается метод start() в Thread, задача выполняется в отдельном потоке

Papaya papaya =newPapaya();
// set properties and invoke papaya methods here.
Thread thread =newThread(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 =newMyThread2(sleepFor);
Future<?> future = esvc.submit(worker);
System.out.printf(' %s  main thread
', Thread.currentThread().toString());
future.get();
esvc.shutdown();

Обратите внимание, что ExecutorService должен быть надлежащим образом закрыт, когда он больше не нужен для дальнейшего представления задач

Метод 4: Вызываемый объект, используемый с ExecutorService

Начиная с версии 1. 5, в Java появился новый интерфейс под названием Callable. Он похож на старый интерфейс Runnable с той разницей, что метод выполнения (называемый call() вместо run() ) может возвращать значение. Кроме того, он может объявлять, что может быть брошено Exception

ExecutorService может также принимать задачи, реализованные как Callable , и возвращать Future со значением, возвращенным методом при завершении

Вот пример класса Mango , который расширяет класс Fruit , определенный ранее, и реализует интерфейс Callable. Дорогая и трудоемкая задача выполняется в методе call()

publicclassMangoextendsFruitimplementsCallable
publicIntegercall()
  // expensive computation here
  returnnewInteger();

А вот код для передачи экземпляра класса в ExecutorService. Приведенный ниже код также ожидает завершения задачи и печатает возвращаемое значение

ExecutorService esvc = Executors.newSingleThreadExecutor();

MyCallable worker =newMyCallable(sleepFor);
Future future = esvc.submit(worker);
System.out.printf(' %s  main thread
', Thread.currentThread().toString());
System.out.println('Task returned: '+ future.get());
esvc.shutdown();

Что вы предпочитаете?

В этой статье мы изучили несколько методов написания многопоточного кода в Java. К ним относятся:

  1. Расширение класса Thread – самый простой метод, который был доступен с версии Java 1. 0.
  2. Если у вас есть класс, который должен расширять какой-то другой класс в иерархии классов, то вы можете реализовать интерфейс Runnable.
  3. Более современным средством для создания потоков является ExecutorService , который может принимать экземпляр Runnable в качестве задания для выполнения. Преимущество этого метода в том, что вы можете использовать пул потоков для выполнения задач. Пул потоков помогает экономить ресурсы за счет повторного использования потоков.
  4. Наконец, вы также можете создать задачу, реализовав интерфейс Callable и передав задачу службе ExecutorService.

Как вы думаете, какой из этих вариантов вы будете использовать в своем следующем проекте? Дайте нам знать в комментариях ниже

Теги

Об авторе

Алексей Белоусов

Привет, меня зовут Филипп. Я фрилансер энтузиаст . В свободное время занимаюсь переводом статей и пишу о потребительских технологиях для широкого круга изданий , не переставая питать большую страсть ко всему мобильному =)

Комментировать

Оставить комментарий