Вопрос или проблема
Я изучаю потоки в Java и столкнулся с двумя разными способами создания потока. Я не уверен в технических различиях между ними:
// Подход 1
Thread n = new Thread(new NumberGenerator(100));
// Подход 2
Thread n = new Thread(new NumberGenerator(100)::run);
Вот мой полный пример кода для контекста:
public class NumberGenerator implements Runnable {
private int limit;
public NumberGenerator(int limit) {
this.limit = limit;
}
@Override
public void run() {
for(int i = 0; i < limit; i++) {
System.out.println(i);
}
}
}
public class Main {
public static void main(String[] args) {
// Какой подход лучше и почему?
Thread t1 = new Thread(new NumberGenerator(100));
Thread t2 = new Thread(new NumberGenerator(100)::run);
t1.start();
t2.start();
}
}
Я протестировал оба подхода, и они, похоже, работают одинаково, но я хочу понять основные различия и лучшие практики.
Насколько я знаю, оба подхода используют этот конструктор в Thread.java
public Thread(Runnable target) {
this(null, target, "Thread-" + nextThreadNum(), 0);
}
Итак, можно ли рассматривать их как подклассы Runnable? Каковы условия для определения того, является ли объект Runnable в этом случае? Каковы технические различия между этими двумя подходами?
Этот код
Thread n = new Thread(new NumberGenerator(100)::run);
тот же самый, что и следующий код
Thread n = new Thread(new Runnable() {
public void run() {
new NumberGenerator(100).run();
}
});
Вы создаете два объекта Runnable
, что избыточно.
Проще и логичнее сделать это таким образом
Thread n = new Thread(new NumberGenerator(100));
Ответ или решение
Вопрос о создании потоков в Java является достаточно актуальным для разработчиков, стремящихся оптимизировать свои приложения. Рассмотрим два упомянутых подхода к реализации потоков, а также научимся различать их.
Подход 1: Прямое использование Runnable
Thread n = new Thread(new NumberGenerator(100));
В этом подходе вы создаете объект NumberGenerator
, передавая его в конструктор класса Thread
. На этом этапе создается экземпляр класса, который реализует интерфейс Runnable
. Когда метод start()
вызван, поток получает доступ к методу run()
этого Runnable
, и выполнение начинается с его содержания.
Это решение является наиболее прямолинейным и оптимальным. Вы создаете один и только один экземпляр NumberGenerator
, что снижает потребление памяти и ресурсы, необходимые для его создания.
Подход 2: Использование ссылки на метод
Thread n = new Thread(new NumberGenerator(100)::run);
В этом случае вы создаете объект NumberGenerator
, а затем передаете ссылку на метод run()
как Runnable
. На первый взгляд, это выглядит как более изящный способ вызова метода. Однако он несет в себе некоторые недостатки. Когда вы используете ссылку на метод, вы создаете новый экземпляр Runnable
, который под капотом фактически создает новый объект Runnable
, который вызывает метод run()
существующего объекта NumberGenerator
.
Таким образом, по сути, вы создаете два объекта Runnable
, и это создает избыточность. Это может привести к неэффективному использованию памяти, особенно если у вас есть множество потоков.
Сравнение подходов
-
Создание объектов:
- Первый подход создает один объект
NumberGenerator
. - Второй подход создает два объекта (
NumberGenerator
и новыйRunnable
).
- Первый подход создает один объект
-
Производительность:
- Первый подход более эффективен, так как уменьшает потребление памяти.
- Второй подход, хотя и выглядит элегантно, может негативно повлиять на производительность при большом количестве потоков из-за излишнего создания объектов.
-
Семантика:
- Первый подход более очевиден для понимания. Читая код, можно сразу увидеть, что происходит.
- Второй подход может вводить в заблуждение, так как не всегда очевидно, что происходит с точки зрения создания объектов.
Заключение
Хотя оба подхода в конечном итоге выполняют одно и то же действие, подход с прямым использованием Runnable
является предпочтительным с точки зрения производительности, простоты и читаемости. Подумайте о том, насколько важна эффективность и ясность вашего кода, особенно в многофункциональных и многопоточных приложениях.
При выборе подхода всегда стоит помнить о принципах ООП и стараться минимизировать избыточность. В этом контексте использование первого подхода представляется более логичным и оптимальным решением.