Понимание разницы между двумя подходами реализации потоков в Java?

Вопрос или проблема

Я изучаю потоки в 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, и это создает избыточность. Это может привести к неэффективному использованию памяти, особенно если у вас есть множество потоков.

Сравнение подходов

  1. Создание объектов:

    • Первый подход создает один объект NumberGenerator.
    • Второй подход создает два объекта (NumberGenerator и новый Runnable).
  2. Производительность:

    • Первый подход более эффективен, так как уменьшает потребление памяти.
    • Второй подход, хотя и выглядит элегантно, может негативно повлиять на производительность при большом количестве потоков из-за излишнего создания объектов.
  3. Семантика:

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

Заключение

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

При выборе подхода всегда стоит помнить о принципах ООП и стараться минимизировать избыточность. В этом контексте использование первого подхода представляется более логичным и оптимальным решением.

Оцените материал
Добавить комментарий

Капча загружается...