Нить в java

У цій статті я торкнуся велику і складну тему багатопоточності в Java. Зрозуміло, про все в одній статті я не зможу розповісти, тому торкнуся лише самі основні теми.


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

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

Якщо ж у нас багатоядерний процесор, то потоки дозволять істотно поліпшити продуктивність, переклавши роботу на інші ядра.


Що ж, досить теорії, перейдемо до практики.


створення потоків
В Java для роботи з потоками служить клас Thread. Є два способи створення нового потоку, але їх можна записати куди більшим числом способів.

Перший варіант, це розширення класу Thread.

public class CustomThread extends Thread # 123;

System. out. println # 40; 6 # 41; ;


Можна ще побавитися з Reflection API, але це вже якось самі.


переривання потоків
Так-так, саме переривання, а не зупинка або пауза. Потоки не можна зупинити стандартними засобами, тому що з цим пов'язано безліч проблем. Тому методи stop, suspend, pause і destroy в класі Thread позначені як deprecated. Так що зупинку доводиться робити самому.

Взагалі, потік в Java працює до тих пір, поки виконується код в його методі run. Якщо в run буде проста операція, скажімо, висновок тексту в консоль, то потік відразу ж завершиться після цієї операції. Якщо ж ми в потоці записуємо дані в циклі в 100 файлів, то потік буде працювати до тих пір, поки не виконає своє завдання.

І, до речі, якщо потік працює в фоні і ви закриваєте програму, то процес цієї програми все одно залишиться висіти в тлі, поки не завершаться всі потоки. Щоб по закінченню програми гарантовано отримати завершення потоків, потрібно зробити з потоку демона. Звучить смішно, але так і є:

thread. setDaemon # 40; true # 41;


Отже, у нас є потік, який виконує в циклі якусь повторювану операцію і нам потрібно в певний момент часу його завершити. Найбільш очевидний спосіб, це завести логічну змінну і перевіряти її стан. Найчастіше роблять так:

private boolean isRunning;

All threads finished. Counter = 5000

Як це буде працювати:
- Ми створили два потоки і запустили їх.
- Припустимо, перший потік запустився швидше і увійшов в цикл while. Другий поки запускається.
- Перший потік бачить блок synchronized. Виконується перевірка - чи немає зараз в цьому блоці інших потоків? Ні, тому перший потік заходить в блок. Другий поки що увійшов в цикл while.
- Перший потік зараз в циклі for збільшує лічильник. Другий потік доходить до блоку synchronized. Знову виконується перевірка і оскільки потік всередині є, дозвіл увійти всередину ніхто не почув, а значить другий потік чекає.
- Перший потік все ще в циклі for. Другий потік все так же чекає.
- Нарешті, перший потік виходить з циклу for і залишає область синхронізації. Другий потік отримує дозвіл увійти всередину.

Таким чином, виходить синхронізована робота потоків.


Блоки синхронізації слід розставляти з розумом. Якби ми зробили ось так:

private static void printCounter # 40; # 41; # 123;

synchronized # 40; Counter. class # 41; # 123;

while # 40; Counter. get # 40; # 41; <5000 ) {

for # 40; int i = 0; i <10 ; i ++ ) {

Counter. increment # 40; # 41; ;

Thread. sleep # 40; 1 # 41; ;

# 125; catch # 40; InterruptedException ex # 41; # 123;

Thread. currentThread # 40; # 41 ;. interrupt # 40; # 41; ;


ми б теж отримали вірний результат 5000, ось тільки працював би у нас тільки один потік:
- Створюємо два потоки і запускаємо їх.
- Припустимо, перший потік запустився швидше і увійшов в блок синхронізації. Другий поки запускається.
- Перший потік тепер в циклі while. Другий потік зустрів блок synchonized і не отримав дозвіл увійти.
- Перший потік працює. Другий чекає.
- Через деякий кількість часу, перший потік збільшив лічильник до 5000 і вийшов з циклів і блоку синхронізації. Другому потоку дозволяється увійти всередину.
- Перший потік завершив роботу. Другий потік перевірив, що умова Counter.get () <5000 уже не выполняется и не вошёл в цикл while. Покинул блок синхронизации и завершился.


Інший варіант вирішення проблеми з лічильником - зробити його методи get і increment синхронізованими. Тоді блок синхронізації в методі run не знадобиться.

public class SynchronizedCounter # 123;

private static int counter = 0;


Тобто ми заходимо в блок синхронізації, монітор. Повертаємо значення і виходимо з монітора. Якщо під час критичної секції відбулося виключення, ми також виходимо з монітора.

У байткод synchronized методу інструкцій monitorenter і monitorexit не було, але це не означає, що немає входу в монітор. Прапор SYNCHRONIZED у методу говорить JVM про те, що всі ці інструкції потрібно виконати. Тобто, вони не з'являються в коді, але приховані в JVM - вона все одно їх виконає.


Забігаючи вперед, продемонструю ще одне можливе рішення проблеми. У пакеті java.util.concurrent є безліч класів для різних багатопотокових потреб. Одним з таких класів є AtomicInteger. який робить операції над числами атомарними.

public class AtomicCounter # 123;

private static final AtomicInteger counter = new AtomicInteger # 40; # 41; ;

public static int get # 40; # 41; # 123;

return counter. get # 40; # 41; ;

public static void increment # 40; # 41; # 123;

counter. incrementAndGet # 40; # 41; ;


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


Синхронізація. Приклад проектування многопоточного додатки
На довершення статті, хочу показати приклад невеликого додатки і важливість правильного проектування потоків.
Припустимо, у нас є 20 файлів з даними. Нам потрібно найбільш ефективно прочитати ці файли і вивести дані на екран. Дані, у міру читання, будуть додаватися до списку і вже звідти виводитися на екран.

Є клас, який відповідає за панель малювання, туди ми будемо додавати об'єкти у міру читання:

private PaintPanel panel;

private void processFile # 40; File file # 41; # 123;

System. out. printf # 40; "Process file% s in% s thread \ n". file. getName # 40; # 41 ;. Thread. currentThread # 40; # 41 ;. getName # 40; # 41; # 41; ;

try # 40; FileInputStream fis = new FileInputStream # 40; file # 41; ;

DataInputStream dis = new DataInputStream # 40; fis # 41; # 41; # 123;

while # 40; dis. available # 40; # 41;> 0 # 41; # 123;

Triangle triangle = Triangle. read # 40; dis # 41; ;

panel. addTriangle # 40; triangle # 41; ;

Thread. sleep # 40; 1 # 41; ;

# 125; catch # 40; IOException | InterruptedException ie # 41; # 123;


Тепер помилок немає, але обробка 20 файлів займає близько п'яти хвилин. Причому, перші файли читаються швидко, а потім робота сповільнюється. Спробуйте зрозуміти, чому так відбувається.


▌ Варіант 2. Один файл - один потік

Маючи 20 потоків логічно припустити, що робота пройде в 20 разів швидше. Так би воно і було, май наш процесор хоча б 20 ядер. В іншому випадку ми лише створимо додаткове навантаження і всі наші дані можуть отрісовиваться не надто плавно.

List threads = new ArrayList <> # 40; files. length # 41; ;

for # 40; File file. files # 41; # 123;

Thread thread = new Thread # 40; # 40; # 41; -> processFile # 40; file # 41; # 41; ;

threads. add # 40; thread # 41; ;

thread. start # 40; # 41; ;

for # 40; Thread thread. threads # 41; # 123;

thread. join # 40; # 41; ;

# 125; catch # 40; InterruptedException e # 41; # 123;

Thread. currentThread # 40; # 41 ;. interrupt # 40; # 41; ;


Проте, час роботи тепер: 40 секунд. Це довго. Та ж проблема, що уповільнює роботу в першому варіанті, уповільнює все і зараз. Повинен бути спосіб позбутися від synchronized блоків і такий спосіб є.


▌ Варіант 3. Використання синхронізованого списку

Для того, щоб зробити зі звичайного списку синхронізований, викликаємо метод

Collections. synchronizedList # 40; list # 41;


Тепер ми отримаємо список, у якого всі методи будуть synchronized. Але, на жаль, итератор, який використовується в foreach таким чином не вийде синхронізувати і нам доведеться або обертати його в synchronized блок, або відмовитися від нього на користь простого перебору елементів в циклі for.

public class PaintPanelSynchronizedList extends PaintPanel # 123;

private final List triangles;

public PaintPanelSynchronizedList # 40; int width, int height # 41; # 123;

super # 40; width, height # 41; ;

triangles = Collections. synchronizedList # 40; new ArrayList <> # 40; # 41; # 41; ;


▌ Варіант 4. Обмеження кількості потоків

Ми можемо обмежити кількість потоків, створивши пул:

ExecutorService es = Executors. newFixedThreadPool # 40; maxThreads # 41; ;


Тепер можна створити хоч сотню потоків, виконуватися в один момент часу будуть не більше maxThreads потоків.

Обмежимо пул п'ятьма потоками

ExecutorService es = Executors. newFixedThreadPool # 40; 5 # 41; ;

for # 40; File file. files # 41; # 123;

es. execute # 40; # 40; # 41; -> processFile # 40; file # 41; # 41; ;

es. shutdown # 40; # 41; ;

es. awaitTermination # 40; 2. TimeUnit. MINUTES # 41; ;

# 125; catch # 40; InterruptedException e # 41; # 123;

Thread. currentThread # 40; # 41 ;. interrupt # 40; # 41; ;


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

Кращою практикою, є обмеження пулу потоків за кількістю процесорів в системі:

Executors. newFixedThreadPool # 40; Runtime. getRuntime # 40; # 41 ;. availableProcessors # 40; # 41; # 41; ;


▌ Варіант 5. java.util.concurrent

Схожі статті