Лучшие практики для параллелизма в Java

Лучшие практики для параллелизма в Java

14 октября 2023 г.
Узнайте о лучших практиках параллелизма в Java, чтобы обеспечить эффективность, синхронизацию и отсутствие ошибок ваших многопоточных приложений.

Параллелизм в Java — это способность нескольких потоков выполняться скоординированно внутри программы. Хотя параллелизм может привести к более эффективному использованию ресурсов, он создает сложности, которыми необходимо тщательно управлять.

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

СМОТРИТЕ: Лучшие онлайн-курсы по изучению Java

Понимание параллелизма в Java

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

Синхронизация и блокировка

Синхронизированные методы

Синхронизированные методы позволяют одновременно выполнять метод для данного объекта только одному потоку. Это гарантирует, что критические разделы кода защищены от одновременного доступа, как показано в следующем примере:

общедоступный синхронизированный недействительный синхронизированныйМетод () {
    // Критический раздел
}

Синхронизированные блоки

Синхронизированные блоки обеспечивают более тонкий уровень управления, позволяя синхронизировать определенный объект, как показано ниже:

общественный недействительный someMethod() {
    синхронизировано (это) {
        // Критический раздел
    }
}

Класс ReentrantLock

Класс ReentrantLock обеспечивает большую гибкость, чем синхронизированные методы или блоки. Он обеспечивает более детальный контроль над блокировкой и предоставляет дополнительные функции, такие как справедливость, например:

Блокировка ReentrantLock = новый ReentrantLock();

общественный недействительный someMethod() {
    блокировка.блокировка();
    пытаться {
        // Критический раздел
    } окончательно {
        блокировка.разблокировка();
    }
}

Неустойчивое ключевое слово

Ключевое слово volutity гарантирует, что переменная всегда читается и записывается в основную память, а не зависит от локального кэша потока. Это полезно для переменных, к которым обращаются несколько потоков без дальнейшей синхронизации, например:

частное изменчивое логическое значение isRunning = true;

СМОТРИТЕ: Лучшие IDE для разработчиков Java (2023 г.)

Атомарные классы

Пакет Java java.util.concurrent.atomic предоставляет атомарные классы, которые позволяют выполнять атомарные операции над переменными. Эти классы очень эффективны и уменьшают необходимость в явной синхронизации, как показано ниже:

частный счетчик AtomicInteger = новый AtomicInteger(0);

Безопасность резьбы

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

Как избежать тупиков

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

Например, давайте рассмотрим сценарий, в котором двум потокам (threadA и threadB) необходимо получить блокировки двух ресурсов (Resource1 и Resource2). Чтобы избежать взаимоблокировок, оба потока должны получать блокировки в одном и том же порядке. Вот пример кода, демонстрирующий это:

общественный класс DeadlockAvoidanceExample {
    частный окончательный объект lock1 = новый объект();
    частный окончательный объект lock2 = новый объект();

    публичный недействительный метод1() {
        синхронизировано (блокировка1) {
            System.out.println("Thread " + Thread.currentThread().getName() + " получил блокировку1");

            // Имитируем некоторую работу
            пытаться {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                е.printStackTrace();
            }

            синхронизировано (блокировка2) {
                System.out.println("Thread " + Thread.currentThread().getName() + " получил блокировку lock2");
                // Поработаем с Resource1 и Resource2
            }
        }
    }

    публичный недействительный метод2() {
        синхронизировано (блокировка1) {
            System.out.println("Thread " + Thread.currentThread().getName() + " получил блокировку1");

            // Имитируем некоторую работу
            пытаться {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                е.printStackTrace();
            }

            синхронизировано (блокировка2) {
                System.out.println("Thread " + Thread.currentThread().getName() + " получил блокировку lock2");
                // Поработаем с Resource1 и Resource2
            }
        }
    }

    public static void main(String[] args) {
        Пример DeadlockAvoidanceExample = новый DeadlockAvoidanceExample();

        Поток threadA = новый поток(() -> example.method1());
        Поток threadB = новый поток(() -> example.method2());

        потокA.start();
        потокB.start();
    }
}

СМ.: Обзор шаблонов проектирования в Java

Утилиты параллелизма в Java

Исполнители и ThreadPool

Класс java.util.concurrent.Executors предоставляет фабричные методы для создания пулов потоков. Использование пула потоков может повысить производительность за счет повторного использования потоков вместо создания новых для каждой задачи.

Вот простой пример, демонстрирующий, как использовать класс Executors для создания пула потоков фиксированного размера и отправки задач на выполнение:

импортировать java.util.concurrent.ExecutorService;
импортировать java.util.concurrent.Executors;

общественный класс ExecutorsExample {

    public static void main(String[] args) {
        // Создаем пул потоков фиксированного размера с тремя потоками
        ИсполнительexecutorService = Executors.newFixedThreadPool(3);

        // Отправляем задачи в пул потоков
        for (int я = 1; я <= 5; я++) { int TaskId = я; исполнитель.submit(() -> {
                System.out.println("Задача " + TaskId + " выполняется " + Thread.currentThread().getName());
            });
        }

        // Завершаем работу исполнителя после отправки всех задач
        исполнитель.shutdown();
    }
}

Вызываемое и будущее

Интерфейс Callable позволяет потоку возвращать результат или генерировать исключение. Интерфейс Future представляет собой результат асинхронных вычислений, как показано ниже:

Вызываемая задача = () -> {
    // Выполняем вычисления
    вернуть результат;
};
Будущее будущее = executor.submit(задача);

CountDownLatch и CyclicBarrier

CountDownLatch и CyclicBarrier — это конструкции синхронизации, предоставляемые пакетом Java Concurrency (java.util.concurrent) для облегчения координации между несколькими потоками.

CountDownLatch — это механизм синхронизации, который позволяет одному или нескольким потокам дождаться завершения набора операций, прежде чем продолжить. Он инициализируется счетчиком, и каждая операция, которую необходимо дождаться, уменьшает этот счетчик. Когда счетчик достигает нуля, все ожидающие потоки освобождаются.

Вот простой пример кода, демонстрирующий CountDownLatch в действии:

импортировать java.util.concurrent.CountDownLatch;

общественный класс CountDownLatchExample {

    public static void main(String[] args) выдает InterruptedException {
        CountDownLatch latch = новый CountDownLatch(3);

        Выполняемая задача = () -> {
            System.out.println("Задача запущена");
            // Имитируем некоторую работу
            пытаться {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                е.printStackTrace();
            }
            System.out.println("Задание выполнено");
            защелка.countDown(); // Уменьшаем количество задержек
        };

        Поток thread1 = новый поток (задача);
        Поток thread2 = новый поток (задача);
        Поток thread3 = новый поток (задача);

        поток1.start();
        поток2.start();
        поток3.start();

        защелка.ожидание(); // Подождем, пока счетчик блокировок достигнет нуля
        System.out.println("Все задачи выполнены");
    }
}

CyclicBarrier — это точка синхронизации, в которой потоки должны ждать, пока не прибудет фиксированное количество потоков. Как только необходимое количество потоков поступит, все они освобождаются одновременно и могут продолжить работу, как показано в следующем фрагменте кода:

публичный финальный класс ImmutablePoint {
    частный финал int x;
    частный финал int y;

    public ImmutablePoint (int x, int y) {
        это.х = х;
        это.у = у;
    }

    общественный int getX() {
        вернуть х;
    }

    общественный интервал getY() {
        вернуть y;
    }

    public ImmutablePoint Translation (int dx, int dy) {
        вернуть новый ImmutablePoint(x + dx, y + dy);
    }
}

СМОТРИТЕ: Навигация по каталогам в Java

Неизменяемые объекты

Неизменяемые объекты по своей сути потокобезопасны, поскольку их состояние не может быть изменено после создания. Когда это возможно, предпочитайте неизменяемость изменяемому состоянию. Вот пример неизменяемого класса, представляющего точку в 2D-пространстве:

импортировать java.util.concurrent.ConcurrentHashMap;

общественный класс ConcurrentHashMapExample {

    public static void main(String[] args) {
        ConcurrentHashMap concurrentMap = новый ConcurrentHashMap<>();

        // Добавляем элементы на параллельную карту
        concurrentMap.put("А", 1);
        concurrentMap.put("Б", 2);
        concurrentMap.put("С", 3);

        // Получение элементов
        System.out.println("Значение ключа 'B': " + concurrentMap.get("B"));

        // Обновление элементов
        concurrentMap.put("Б", 4);

        // Удаление элементов
        concurrentMap.remove("C");

        // Итерация по карте
        concurrentMap.forEach((ключ, значение) -> {
            System.out.println("Ключ: " + ключ + ", Значение: " + значение);
        });
    }
}

Параллельные коллекции

Java предоставляет набор потокобезопасных коллекций в пакете java.util.concurrent. Эти коллекции предназначены для одновременного доступа и могут значительно упростить параллельное программирование.

Одной из популярных параллельных коллекций является ConcurrentHashMap, которая обеспечивает поточно-ориентированную реализацию хэш-карты. Например:

импортировать java.util.concurrent.ConcurrentHashMap;

общественный класс ConcurrentHashMapExample {

    public static void main(String[] args) {
        ConcurrentHashMap concurrentMap = новый ConcurrentHashMap<>();

        // Добавляем элементы на параллельную карту
        concurrentMap.put("А", 1);
        concurrentMap.put("Б", 2);
        concurrentMap.put("С", 3);

        // Получение элементов
        System.out.println("Значение ключа 'B': " + concurrentMap.get("B"));

        // Обновление элементов
        concurrentMap.put("Б", 4);

        // Удаление элементов
        concurrentMap.remove("C");

        // Итерация по карте
        concurrentMap.forEach((ключ, значение) -> {
            System.out.println("Ключ: " + ключ + ", Значение: " + значение);
        });
    }
}

Тестирование параллельного кода

Тестирование параллельного кода может оказаться сложной задачей. Рассмотрите возможность использования таких инструментов, как JUnit, и таких библиотек, как ConcurrentUnit, для написания эффективных тестов для параллельных программ.

Вопросы производительности

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

Обработка ошибок в параллельном коде

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

Распространенные ошибки и как их избежать

Неправильная синхронизация общих данных. Неспособность синхронизировать доступ к общим данным может привести к повреждению данных и непредвиденному поведению. Всегда используйте правильные механизмы синхронизации.

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

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

SEE: Алгоритмы одновременного доступа к различным структурам данных: обзор исследований (TechRepublic Premium)

Заключительные мысли о лучших практиках параллелизма в Java

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


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE