JavaRush /Java блог /Random UA /Кава-брейк #113. 5 речей, які ви, ймовірно, не знали про ...

Кава-брейк #113. 5 речей, які ви, ймовірно, не знали про багатопоточність у Java. 10 розширень JetBrains для боротьби з технічним боргом

Стаття з групи Random UA

5 речей, які ви, ймовірно, не знали про багатопоточність в Java

Джерело: DZone Потік – це серце мови програмування Java. Навіть запуск програми Hello World потребує в основному потоку. При необхідності ми можемо додавати до програми інші потоки, якщо хочемо, щоб код нашої програми був більш функціональним та продуктивним. Якщо ж йдеться про веб-сервер, він одночасно обробляє сотні запитів одночасно. Для цього використовують кілька потоків. Кава-брейк #113.  5 речей, які ви, ймовірно, не знали про багатопоточність у Java.  10 розширень JetBrains для боротьби з технічним боргом - 1Потоки, безперечно, корисні, але робота з ними може бути складною для багатьох розробників. У цій статті я поділюся п'ятьма концепціями багатопоточності, про які початківці та досвідчені розробники можуть не знати.

1. Порядок програми та порядок виконання не збігаються

Коли ми пишемо код, то припускаємо, що він виконуватиметься саме так, як ми його пишемо. Однак, насправді це не так. Компілятор Java може змінити порядок виконання, щоб його оптимізувати, якщо він може визначити, що вихідні дані не зміняться в однопоточному коді. Подивіться на наступний фрагмент коду:
package ca.bazlur.playground;

import java.util.concurrent.Phaser;

public class ExecutionOrderDemo {
    private static class A {
        int x = 0;
    }

    private static final A sharedData1 = new A();
    private static final A sharedData2 = new A();

    public static void main(String[] args) {
        var phaser = new Phaser(3);
        var t1 = new Thread(() -> {
            phaser.arriveAndAwaitAdvance();
            var l1 = sharedData1;
            var l2 = l1.x;
            var l3 = sharedData2;
            var l4 = l3.x;
            var l5 = l1.x;
            System.out.println("Thread 1: " + l2 + "," + l4 + "," + l5);
        });
        var t2 = new Thread(() -> {
            phaser.arriveAndAwaitAdvance();
            var l6 = sharedData1;
            l6.x = 3;
            System.out.println("Thread 2: " + l6.x);
        });
        t1.start();
        t2.start();
        phaser.arriveAndDeregister();
    }
}
Цей код здається простим. У нас є два загальні екземпляри даних ( sharedData1 і sharedData2 ), які використовують два потоки. Коли ми виконуємо код, ми припускаємо, що висновок буде таким:
Thread 2: 3 Thread 1: 0,0,0
Але якщо ви запустите код кілька разів, побачите інший результат:
Thread 2: 3 Thread 1: 3,0,3 Thread 2: 3 Thread 1: 0,0,3 Thread 2: 3 Thread 1: 3,3,3 Thread 2: 3 Thread 1: 0,3,0 Thread 2 : 3 Thread 1: 0,3,3
Я не стверджую, що всі ці потоки відтворюватимуться на вашій машині саме так, але це цілком можливо.

2. Кількість Java-потоків обмежена

Створити потік у Java легко. Однак це не означає, що ми можемо створювати їх скільки завгодно. Кількість потоків обмежена. Ми можемо легко дізнатися скільки потоків ми можемо створити на конкретній машині за допомогою наступної програми:
package ca.bazlur.playground;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.LockSupport;

public class Playground {
    public static void main(String[] args) {
        var counter = new AtomicInteger();
        while (true) {
            new Thread(() -> {
                int count = counter.incrementAndGet();
                System.out.println("thread count = " + count);
                LockSupport.park();
            }).start();
        }
    }
}
Вищезгадана програма дуже проста. Вона створює потік у циклі, а потім паркує його, що означає, що потік відключається для подальшого використання, але виконує системний виклик та виділяє пам'ять. Програма продовжує створювати потоки доти, доки більше не зможе створювати, а потім видає виняток. Нас цікавить кількість, яку ми отримаємо, доки програма не викине виняток. На своєму комп'ютері я зміг створити лише 4065 потоків.

3. Занадто багато потоків не гарантує кращої продуктивності

Наївно вважати, що й у Java легко створюються потоки, це підвищує продуктивність програми. На жаль, це припущення помилкове у випадку з нашою традиційною моделлю багатопоточності, яку сьогодні надає Java. Насправді занадто багато потоків може знизити продуктивність програми. Давайте спочатку поставимо це питання: яку оптимальну максимальну кількість потоків ми можемо створити, щоб максимізувати продуктивність програми? Що ж, відповідь не така проста. Він дуже залежить від типу роботи, яку ми робимо. Якщо у нас є кілька незалежних завдань, і всі вони обчислювальні та не блокують будь-які зовнішні ресурси, то наявність великої кількості потоків не сильно покращить продуктивність. З іншого боку, якщо ми маємо 8-ядерний процесор, оптимальна кількість потоків може бути (8 + 1). У такому випадку ми можемо покладатися на паралельний потік, представлений Java 8. За замовчуванням паралельний потік використовує загальний пул Fork/Join. Він створює потоки, рівні кількості доступних процесорів, що достатньо їх інтенсивної роботи. Додавання більшої кількості потоків до роботи з інтенсивним використанням процесорів, де нічого не блокується, не призведе до підвищення продуктивності. Швидше, ми просто витрачатимемо ресурси. Примітка. Причина наявності додаткового потоку полягає в тому, що навіть потік з інтенсивними обчисленнями іноді викликає помилку сторінки або зупиняється з іншої причини. (Див.: Паралелізм Java на практиці , Брайан Гетц, стор. 170) Однак припустимо, наприклад, що завдання пов'язані з введенням-виводом. У цьому випадку вони залежать від зовнішнього зв'язку (наприклад, бази даних, інших API), тому має сенс більше потоків. Причина в тому, що, коли потік очікує в Rest API, інші потоки можуть продовжити роботу. Тепер ми можемо знову спитати, скільки потоків занадто багато для такого випадку? Дивлячись як. Немає ідеальних чисел, придатних всім випадків. Тому ми повинні провести адекватне тестування, щоб з'ясувати, що найкраще підходить для нашої конкретної робочої навантаження та програми. У типовому сценарії ми зазвичай маємо змішаний набір завдань. І справи у таких випадках йдуть до кінця. У своїй книзі "Java Concurrency in Practice" Брайан Гетц запропонував формулу, яку ми можемо використовувати в більшості випадків. Number of threads = Number of Available Cores * (1 + Wait time / Service time) Waiting time (Час очікування) може бути IO, наприклад, очікування відповіді HTTP, отримання блокування тощо. Service Time(Час обслуговування) – це час обчислень, наприклад, обробка HTTP-відповіді, маршалінг/демаршалінг тощо. Наприклад, програма викликає API, а потім обробляє його. Якщо у нас є 8 процесорів на сервері додатків, середній час відповіді API становить 100 мс, а час обробки відповіді – 20 мс, то ідеальний розмір потоку буде таким:
N = 8 * (1 + 100/20) = 48
Однак це надмірне спрощення; адекватне тестування має вирішальне значення визначення числа.

4. Багатопотоковість - це не паралелізм

Іноді ми використовуємо багатопоточність та паралелізм взаємозамінні, але це вже не зовсім актуально. Хоча Java ми досягаємо того й іншого за допомогою потоку, це дві різні речі. “У програмуванні багатопоточність – це окремий випадок незалежно від виконуваних процесів, а паралелізм – це одночасне виконання (можливо, пов'язаних) обчислень. Багатопотоковість - це взаємодія з великою кількістю речей одночасно. Паралелізм - це виконання безлічі речей одночасно”. Наведене вище визначення, дане Робом Пайком, є досить точним. Припустимо, ми маємо абсолютно незалежні завдання, і їх можна обчислити окремо. В цьому випадку ці завдання називаються паралельними і можуть виконуватися з пулом Fork/Join або паралельним потоком. З іншого боку, якщо ми маємо багато завдань, деякі з них можуть залежати від інших. Те, як ми вигадуємо і структуруємо, називається багатопоточністю. Вона пов'язана із структурою. Ми можемо захотіти виконувати кілька завдань одночасно задля досягнення певного результату, не обов'язково закінчувати одну швидше.

5. Project Loom дозволяє нам створювати мільйони потоків

У попередньому пункті я стверджував, що наявність великої кількості потоків не означає підвищення продуктивності програми. Проте в епоху мікросервісів ми взаємодіємо з надто великою кількістю сервісів, щоб виконувати конкретну роботу. У такому сценарії потоки більшу частину часу залишаються у заблокованому стані. Хоча сучасна ОС може обробляти мільйони відкритих сокетів, ми можемо відкрити багато каналів зв'язку, оскільки ми обмежені кількістю потоків. Але що, якщо створити мільйони потоків, і кожен із них використовуватиме відкритий сокет для взаємодії із зовнішнім світом? Це, безумовно, покращить нашу пропускну здатність програми. Щоб підтримати цю ідею, Java існує ініціатива під назвою Project Loom. Використовуючи її, ми можемо створити мільйони віртуальних потоків. Наприклад, використовуючи наступний фрагмент коду, я зміг створити 4,5 мільйонів потоків на своїй машині.
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.LockSupport;

public class Main {
    public static void main(String[] args) {
        var counter = new AtomicInteger();

        // 4_576_279
        while (true) {
            Thread.startVirtualThread(() -> {
                int count = counter.incrementAndGet();
                System.out.println("thread count = " + count);
                LockSupport.park();
            });
        }
    }
}
Для запуску цієї програми у вас повинна бути встановлена ​​Java 18, яку можна скачати тут . Запустити код можна за допомогою наступної команди: java --source 18 --enable-preview Main.java

10 розширень JetBrains для боротьби з технічним боргом

Джерело: DZone Багато команд розробників зазнають величезного тиску у питанні дотримання дедлайнів. Через це їм часто не вистачає часу на виправлення та очищення своєї кодової бази. Іноді у таких ситуаціях швидко накопичується технічний обов'язок. Допомогти у вирішенні цієї проблеми можуть розширення редактора. Давайте поглянемо на 10 найкращих розширень JetBrains для боротьби з технічним боргом (з підтримкою Java). Кава-брейк #113.  5 речей, які ви, ймовірно, не знали про багатопоточність у Java.  10 розширень JetBrains для боротьби з технічним боргом - 2

Інструменти рефакторингу та технічного боргу

1. RefactorInsight

RefactorInsight покращує показ змін коду в середовищі IDE, виводячи інформацію про рефакторинг.
  1. Розширення визначає рефакторинг у запитах на злиття.
  2. Позначає коміти, що містять рефакторинг.
  3. Допомагає перегляду рефакторингу будь-якої конкретної фіксації, вибраної на вкладці Git Log.
  4. Показує історію рефакторингу класів, методів та полів.

2. Stepsize Issue Tracker in IDE

Stepsize - чудовий засіб відстеження проблем для розробників. Розширення допомагає інженерам не тільки створювати якісніші TODO та коментарі до коду, але й розставляти пріоритети з технічного обов'язку, рефакторингу тощо:
  1. Stepsize дозволяє створювати та переглядати завдання у коді прямо в редакторі.
  2. Пошук проблем, що впливають на функції, над якими працюєте.
  3. Додавання завдань у свої sprints за допомогою інтеграції Jira, Asana, Linear, Azure DevOps та GitHub.

3. New Relic CodeStream

New Relic CodeStream – це платформа для спільної роботи розробників для обговорення та перевірки коду. Вона підтримує запити на вилучення з GitHub, BitBucket та GitLab, управління проблемами з Jira, Trello, Asana та 9 інших, а також забезпечує обговорення коду, пов'язуючи все це воєдино.
  1. Створення, перегляд та поєднання запитів на вилучення в GitHub.
  2. Отримання відгуків про незавершену роботу з попередніми перевірками коду.
  3. Обговорення з колегами щодо команди проблем у коді.

TODO та коментарі

4. Comments Highlighter

Цей плагін дозволяє створювати виділення рядків коментарів і ключових слів мови. Також плагін має можливість визначати користувальницькі токени для виділення рядків коментарів.

5. Better Comments

Розширення Better Comments допоможе вам створювати у коді більш зрозумілі коментарі. За допомогою цього розширення ви зможете класифікувати свої інструкції на:
  1. Оповіщення.
  2. Запити.
  3. TODO.
  4. Основні моменти.

Помилки та вразливості безпеки

6. SonarLint

SonarLint дозволяє усувати проблеми з кодом, перш ніж вони виникнуть. Також його можна використати як засіб перевірки орфографії. SonarLint виділяє помилки та вразливості в безпеці при написанні коду з чіткими інструкціями з виправлення, щоб ви могли виправити їх ще до того, як код буде зафіксований.

7. SpotBugs

Плагін SpotBugs забезпечує статичний аналіз байт-коду для пошуку помилок у коді Java з IntelliJ IDEA. SpotBugs - це інструмент виявлення дефектів для Java, який використовує статичний аналіз для пошуку більше 400 шаблонів помилок, таких як розіменування null pointer, нескінченні цикли рекурсивні, неправильне використання бібліотек Java і взаємоблокування. SpotBugs може виявляти сотні серйозних дефектів у великих додатках (зазвичай близько 1 дефекту на 1000-2000 рядків вихідних тверджень без коментарів).

8. Snyk Vulnerability Scanner

Snyk's Vulnerability Scanner допомагає знаходити та усувати вразливості у системі безпеки та проблеми з якістю коду у ваших проектах.
  1. Пошук та усунення проблем з безпекою.
  2. Перегляд списку різних типів проблем, розбитих на категорії.
  3. Відображення порад щодо усунення несправностей.

9. Zero Width Characters Locator

Цей плагін посилює перевірку і пошук помилок, що важко виявляються, пов'язаних з невидимими символами нульової ширини (invisible zero width characters) у вихідному коді і ресурсах. При використанні переконайтеся, що увімкнено перевірку “Zero width unicode character — Символ юнікоду нульової ширини”.

10. CodeMR

CodeMR – це інструмент для аналізу якості програмного забезпечення та статичного коду, який допомагає компаніям-розробникам програмного забезпечення розробляти якісніший код та програми. CodeMR візуалізує метрики коду та високорівневі атрибути якості (зв'язок, складність, зв'язність та розмір) у різних уявленнях, таких як Package Structure, TreeMap, Sunburst, Dependency та Graph Views.
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ