Який спосіб потрібно викликати для запуску потоку?
Для створення нового потоку ми можемо створити новий клас або наслідуючи його від класу Thread або реалізуючи в класі інтерфейс Runnable .
Спадкування від класу Thread
Створимо свій клас на основі Thread:
class JThread extends Thread < JThread (String name) < super (name); >public void run() < System.out.printf("%s started. \n", Thread.currentThread().getName()); try< Thread.sleep(500); >catch(InterruptedException e) < System.out.println("Thread has been interrupted"); >System.out.printf("%s fiished. \n", Thread.currentThread().getName()); > > public class Program < public static void main(String[] args) < System.out.println("Main thread started. "); new JThread("JThread").start(); System.out.println("Main thread finished. "); >>
Клас потоку називається JThread. Передбачається, що конструктор класу передається ім'я потоку, яке потім передається конструктор базового класу. У конструктор свого класу потоку ми можемо передати різні дані, але головне, щоб у ньому викликався конструктор базового класу Thread, який передається ім'я потоку.
І також JThread перевизначається метод run() , код якого власне і буде представляти весь той код, який виконується в потоці.
У методі main для запуску потоку JThread у нього викликається метод start() , після чого починається виконання коду, який визначений у методі run:
new JThread("JThread").start();
Main thread started. Main thread finished. JThread started. JThread finished.
Тут у методі main конструктор JThread передається довільна назва потоку, і потім викликається метод start() . По суті, цей метод якраз і викликає перевизначений метод run() класу JThread.
Зауважте, що головний потік завершує роботу раніше, ніж породжений ним дочірній потік JThread.
Аналогічно до створення одного потоку ми можемо запускати відразу кілька потоків:
public static void main(String[] args)Main thread started. Main thread finished. JThread 2 started. JThread 5 started. JThread 4 started. JThread 1 started. JThread 3 started. JThread 1 finished. JThread 2 finished. JThread 5 finished. JThread 4 finished. JThread 3 finished.
Очікування завершення потоку
При запуску потоків у прикладах вище Main thread завершувався до дочірнього потоку. Як правило, найбільш поширеною ситуацією є випадок, коли Main thread завершується останнім. Для цього треба застосувати метод join(). У цьому випадку поточний потік буде очікувати завершення потоку, для якого викликаний метод join:
public static void main(String[] args) < System.out.println("Main thread started. "); JThread t=new JThread("JThread"); t.start(); try<t.join(); >catch(InterruptedException e) < System.out.printf("%s has been interrupted", t.getName()); >System.out.println("Main thread finished. "); >
Метод join() змушує викликав потік (у разі Main thread) очікувати завершення викликаного потоку, котрій і застосовується метод join (у разі JThread).
Main thread started. JThread started. JThread finished. Main thread finished.
Якщо у програмі використовується кілька дочірніх потоків, і треба, щоб Main thread завершувався після дочірніх, то для кожного дочірнього потоку треба викликати метод join.
Реалізація інтерфейсу Runnable
Інший спосіб визначення потоку представляє реалізація інтерфейсу Runnable. Цей інтерфейс має один метод run:
interface RunnableУ методі run() власне визначається весь код, який виконується при запуску потоку.
Після визначення об'єкта Runnable він передається в один із конструкторів класу Thread:
Thread(Runnable runnable, String threadName)
Для реалізації інтерфейсу визначимо наступний клас MyThread:
class MyThread implements Runnable < public void run()< System.out.printf("%s started."\n", Thread.currentThread().getName()); try< Thread.sleep(500); >catch(InterruptedException e) < System.out.println("Thread має бути interrupted"); printf("%s finished. \n", Thread.currentThread().getName()); > > public class Program < public static void main(String[] args) < System.out.println("Main thread started. "); Thread myThread = новий Thread(new MyThread(),"MyThread"); myThread.start();
Реалізація інтерфейсу Runnable багато в чому аналогічна до перевизначення класу Thread. Також у методі run визначається найпростіший код, який присипляє потік на 500 мілісекунд.
У методі main викликається конструктор Thread, який передається об'єкт MyThread. І щоб запустити потік, викликається метод start(). У результаті консоль виведе щось на кшталт наступного:
Main thread started. Main thread finished. MyThread started. MyThread finished.
Оскільки Runnable фактично представляє функціональний інтерфейс, який визначає один метод, то об'єкт цього інтерфейсу ми можемо уявити як лямбда-выражения:
public class Program < public static void main(String[] args) < System.out.println("Main thread started. "); Runnable r = ()-> < System.out.printf("%s started. \n", Thread.currentThread().getName()); try< Thread.sleep(500); >catch(InterruptedException e) < System.out.println("Thread has been interrupted"); >System.out.printf("%s finished. \n", Thread.currentThread().getName()); >; Thread myThread = New Thread (r, "MyThread"); myThread.start(); System.out.println("Main thread finished. "); > >
Потоки. Клас Thread та інтерфейс Runnable
У російській термінології за терміном Thread зміцнився переклад "Потік". Хоча це слово також можна перекласти як "Нити". Іноді у зарубіжних навчальних матеріалах поняття потоку пояснюється саме на нитках. Продовжимо логічний ряд – там де нитки, там і клубок. А де клубок, там і кіт. Відразу видно, що перекладачі не мали котів. Так і виникла плутанина.Тим більше, що існують інші потоки під терміном Stream. Перекладачі, взагалі дивний народ.
Коли запускається будь-яка програма, то починає виконуватися потік, званий головним потоком (Main). Від нього народжуються дочірні потоки. Головний потік, зазвичай, є останнім потоком, завершальним виконання програми.
Незважаючи на те, що головний потік створюється автоматично, ним можна керувати через об'єкт класу Thread. Для цього потрібно викликати метод currentThread(), Після чого можна керувати потоком.
Клас Thread містить кілька методів управління потоками.
- getName() - отримати ім'я потоку
- getPriority() - отримати пріоритет потоку
- isAlive() - Визначити, чи виконується потік
- join() - очікувати завершення потоку
- run() - Запуск потоку. У ньому пишіть свій код
- sleep() - Зупинити потік на заданий час
- start() - запустити потік
Отримаємо інформацію про головний потік та змінимо його ім'я.
Thread mainThread = Thread.currentThread(); mInfoTextView.setText("Поточний потік:" + mainThread.getName()); // Змінюємо ім'я та виводимо в текстовому полі mainThread.setName("CatThread"); mInfoTextView.append("\nНове ім'я потоку: " + mainThread.getName());
Ім'я у головного потоку за промовчанням main, яке ми замінили на CatThread.
Викликаємо інформацію про назву потоку без вказівки методу.
Thread mainThread = Thread.currentThread(); mInfoTextView.setText("Поточний потік:" + mainThread);
У цьому випадку можна побачити рядок Thread[main,5,main] - Ім'я потоку, його пріоритет та ім'я його групи.
Створення власного потоку
Створити власний потік не складно. Досить успадковуватися від класу Thread.
Оголосимо всередині нашого класу внутрішній клас і викличемо його по клацанню, викликавши метод start().
public class MyThread extends Thread < public void run() < Log.d(TAG, "Мій потік запущений. "); >> public void onClick(View view)
Як варіант, перенести виклик методу start() у конструктор.
public void onClick(View view) < MyThread myThread = New MyThread(); >public class MyThread extends Thread < // Конструктор MyThread() < // Створюємо новий потік super("Другий потік"); Log.i(TAG, "Створено другий потік" + this); start(); // Запускаємо потік > public void run () < Log.d (TAG, " Мій потік запущений. " ); try < for (int i = 5; i > 0; i--) < Log.i(TAG, "Другий потік:" + i); Thread.sleep(500); >> catch (InterruptedException e) < Log.i(TAG, "Другий потік перервано"); >> >
Створення потоку з інтерфейсом Runnable
Є складніший варіант створення потоку. Для створення нового потоку необхідно реалізувати інтерфейс Runnable. Ви можете створити потік з будь-якого об'єкта, що реалізує інтерфейс Runnable та оголосити метод run().
Усередині методу run() ви розміщуєте код нового потоку. Цей потік завершиться, коли метод поверне керування.
Коли ви оголосите новий клас з інтерфейсом Runnable, вам потрібно використовувати конструктор:
Thread(Runnable об'єкт_потоку, String ім'я_потоку)
У першому параметрі вказується екземпляр класу, що реалізує інтерфейс. Він визначає, де розпочнеться виконання потоку. У другому параметрі передається ім'я потоку.
Після створення нового потоку його потрібно запустити за допомогою методу start(), який, по суті, виконує виклик методу run().
Створимо новий потік усередині навчального проекту у вигляді вкладеного класу та запустимо його.
package ru.alexanderklimov.expresscourse; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import static ru.alexanderklimov.expresscourse.R.id.textViewInfo; public class MainActivity extends AppCompatActivity < final String TAG = "ExpressCourse"; private Button mButton; private EditText mResultEditText; private TextView mInfoTextView; @Override protected void onCreate(Bundle savedInstanceState) < super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mButton = findViewById(R.id.buttonGetResult); mResultEditText = findViewById(R.id.editText); mInfoTextView = findViewById(textViewInfo); >public void onClick(View view) < new MyRunnable(); // Створюємо новий потік try < for (int i = 5; i > 0; i--) < Log.i (TAG, "Головний потік:" + i); Thread.sleep(1000); >> catch (InterruptedException e) < Log.i(TAG, "Головний потік перерваний"); >> class MyRunnable implements Runnable < Thread thread; // Конструктор MyRunnable() < // Створюємо новий другий потік thread = New Thread (this, "Потік для прикладу"); Log.i(TAG, "Створено другий потік" + thread); thread.start(); // Запускаємо потік >// Обов'язковий метод для інтерфейсу Runnable public void run() < try < for (int i = 5; i > 0; i--) ; Thread.sleep(500); >> catch (InterruptedException e) < Log.i(TAG, "Другий потік перервано"); >> > >
Усередині конструктора MyRunnable() ми створюємо новий об'єкт класу Thread
thread = new Thread(this, "Потік для прикладу");
У першому параметрі використовувався об'єкт this, що означає бажання викликати метод run() цього об'єкта. Далі викликається метод start(), внаслідок чого запускається виконання потоку, починаючи з методу run(). У свою чергу, метод запускає цикл для нашого потоку. Після виклику методу start(), конструктор MyRunnable() повертає керування додатком. Коли головний потік продовжує свою роботу, він входить до свого циклу. Після цього обидва потоки виконуються паралельно.
Можна запускати кілька потоків, а не лише другий потік на додаток до першого. Це може призвести до проблем, коли два потоки намагаються працювати з однією змінною одночасно.
Ключове слово syncronized - синхронізовані методи
Для вирішення проблеми із потоками, які можуть внести плутанину, використовується синхронізація.
Метод може мати модифікатор syncronized. Коли потік знаходиться всередині синхронізованого методу, всі інші потоки, які намагаються викликати його в тому самому екземплярі, повинні очікувати. Це дозволяє виключити плутанину, коли кілька потоків намагаються викликати метод.
syncronized void meow(String msg);
Крім того, ключове слово syncronized можна використовувати як оператор. Ви можете укласти в блок syncronized виклики методів якогось класу:
Looper
Потік має у своєму складі сутності Looper, Handler, MessageQueue.
Кожен потік має один унікальний Looper і може мати багато Handler.
Вважайте Looper допоміжним об'єктом потоку, що керує ним. Він обробляє вхідні повідомлення, а також дає вказівку потоку завершитись у потрібний момент.
Потік отримує свій Looper і MessageQueue через метод Looper.prepare() після запуску. Looper.prepare() ідентифікує потк, що викликає, створює Looper і MessageQueue і пов'язує потік із ними у сховище ThreadLocal. Метод Looper.loop() слід викликати для запуску Looper. Завершити його роботу можна через метод looper.quit().
class LooperThread extends Thread < public Handler mHandler; public void run() < Looper.prepare(); mHandler = new Handler() < public void handleMessage(Message msg) < // process incoming messages here >>; Looper.loop(); > >
Використовуйте статичний метод getMainLooper() для доступу до Looper головного потоку:
Looper mainLooper = Looper.getMainLooper();
Створимо два потоки. Один запустимо переважно потоці, а другий окремо від основного. Нам буде достатньо двох кнопок та мітки.
Зверніть увагу на те, як запускаються потоки. Перший потік запускається за допомогою методу start(), а другий - run(). Потім перевіряємо, у якому потоці ми.
package ru.alexanderklimov.as23; import android.os.Bundle; import android.os.Looper; import android.support.v7.app.AppCompatActivity; import android.view.View; import android.widget.Button; import android.widget.TextView; public class MainActivity extends AppCompatActivity < TextView mInfoTextView; @Override protected void onCreate(Bundle savedInstanceState) < super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button startButton = (Button) findViewById(R.id.button_start); Button runButton = (Button) findViewById(R.id.button_run); mInfoTextView = (TextView) findViewById(R.id.textview_info); startButton.setOnClickListener(new View.OnClickListener() < @Override public void onClick(View view) < Thread thread = new Thread(new MyRunnable()); thread.start(); //у фоновому потоці >>); runButton.setOnClickListener(new View.OnClickListener() < @Override public void onClick(View view) < Thread thread = new Thread(new MyRunnable()); thread.run(); //у поточному потоці >>); > private class MyRunnable implements Runnable < @Override public void run() < // Перевіряємо, в якому потоці знаходимося if (Looper.getMainLooper().getThread() == Thread.currentThread()) < mInfoTextView.setText("В основному потоці"); >else < runOnUiThread(new Runnable() < @Override public void run() < mInfoTextView.setText("У фоновому потоці"); >>); > > > >
Ця тема досить складна і для більшості не становить інтересу та необхідності вивчати.
В Android потоки в чистому вигляді використовуються все рідше і рідше, система має власні способи.
Як запустити потік у Java
У цьому посібнику ми збираємося вивчити різні способи запуску потоку та виконання паралельних завдань.
Це дуже корисно, особливо при роботі з тривалими або повторюваними операціями, які не можуть виконуватися в основному потоці , або коли взаємодія з інтерфейсом користувача не можна призупинити в очікуванні результатів операції.
Щоб дізнатися більше про деталі потоків, обов'язково прочитайте наш підручник про життєвий цикл потоку Java.
2. Основи запуску потоку
Ми можемо легко написати деяку логіку, яка виконується у паралельному потоці, використовуючи структуру Thread.
Давайте спробуємо базовий приклад, розширивши клас Thread:
public
class
NewThread
extends
Thread
public
void
run()
long
startTime
=
System.currentTimeMillis();
int
i
=
0;
while
(true)
System.out.println(this.getName()
+
": New Thread is running. "
+
i++);
try
//Wait for one sec so it не буде print too fast
Thread.sleep(1000);
>
catch
(InterruptedException
e)
e.printStackTrace();
>
...
>
>
>
І тепер ми пишемо другий клас для ініціалізації та запуску нашого потоку:
public
class
SingleThreadExample
public
static
void
main(String[]
args)
NewThread
t
=
new
NewThread();
t.start();
>
>
Ми повинні викликати метод start() для потоків у стані NEW (еквівалент не запущеного). В іншому випадку Java видасть екземпляр виключення IllegalThreadStateException .
Тепер припустимо, що нам потрібно запустити кілька потоків:
public
class
MultipleThreadsExample
public
static
void
main(String[]
args)
NewThread
t1
=
new
NewThread();
t1.setName("MyThread-1");
NewThread
t2
=
new
NewThread();
t2.setName("MyThread-2");
t1.start();
t2.start();
>
>
Наш код, як і раніше, виглядає досить просто і дуже схожий на приклади, які ми можемо знайти в Інтернеті.
Звісно, це далеко не готовий до виробництва код, де дуже важливо правильно керувати ресурсами, щоб уникнути надто великого перемикання контексту або надто великого використання пам'яті.
Отже, щоб підготуватись до роботи, нам тепер потрібно написати додатковий шаблон для роботи з:
- послідовне створення нових потоків
- кількість одночасних живих потоків
- звільнення потоків: дуже важливо для потоків демона, щоб уникнути витоків
Якщо ми захочемо, ми можемо написати власний код для всіх цих сценаріїв і навіть для деяких інших, але навіщо нам винаходити велосипед?
3. Платформа ExecutorService Framework
ExecutorService реалізує шаблон проектування пулу потоків (також званий моделлю реплікованого робочого або робочої групи) і піклується про управління потоками, про яке ми згадували вище, а також додає деякі дуже корисні функції, такі як повторне використання потоків та черги завдань.
Зокрема, дуже важливою є можливість повторного використання потоків: у великомасштабному додатку виділення та звільнення безлічі об'єктів потоку створює значні накладні витрати на управління пам'яттю.
З робочими потоками ми мінімізуємо накладні витрати, спричинені створенням потоку.
Щоб спростити налаштування пулу, ExecutorService постачається з простим конструктором і деякими параметрами налаштування, такими як тип черги, мінімальна та максимальна кількість потоків та угода про їх імена.
Для отримання додаткових відомостей про ExecutorService див. посібник з Java ExecutorService.
4. Запуск завдання з виконавцями
Завдяки цій потужній структурі ми можемо переключити своє мислення із запуску потоків на відправлення завдань.
Давайте подивимося, як ми можемо надіслати асинхронне завдання нашому виконавцю:
ExecutorService
executor
=
Executors.newFixedThreadPool(10);
...
executor.submit(()
->
new
Task();
>);
Ми можемо використовувати два методи: execute , який нічого не повертає, і submit , який повертає Future , що інкапсулює результат обчислення.
Для отримання додаткових відомостей про ф'ючерси див. посібник з java.util.concurrent.Future .
5. Запуск завдання за допомогою CompletableFutures
Щоб отримати остаточний результат з об'єкта Future ми можемо використовувати метод get , доступний в об'єкті, але це заблокує батьківський потік до кінця обчислень.
Як альтернатива ми могли б уникнути блокування, додавши більше логіки в наше завдання, але ми повинні збільшити складність нашого коду.
У Java 1.8 з'явилася нова структура поверх конструкції Future для кращої роботи з результатом обчислень: CompletableFuture.
CompletableFuture реалізує CompletableStage , який додає широкий вибір методів для приєднання зворотних викликів та дозволяє уникнути всіх операцій, необхідних для виконання операцій над результатом після того, як він буде готовий.
Реалізація відправлення завдання набагато простіше:
CompletableFuture.supplyAsync(()
->
"Hello");
SupplyAsync приймає Supplier, що містить код, який ми хочемо виконати асинхронно - у нашому випадку параметр lambda.
Тепер завдання неявно відправляється в ForkJoinPool.commonPool() , або ми можемо вказати кращого виконавця як другий параметр.
Щоб дізнатися більше про CompletableFuture, ознайомтесь з нашим посібником з CompletableFuture .
6. Запуск відкладених або періодичних завдань
При роботі зі складними веб-додатками нам може знадобитися запускати завдання у певний час, можливо, регулярно.
У Java є кілька інструментів, які можуть допомогти нам виконувати відкладені або повторювані операції:
- java.util.Таймер
- java.util.concurrent.ScheduledThreadPoolExecutor
6.1. Таймер
Таймер – це засіб для планування завдань для майбутнього виконання у фоновому потоці.
Завдання можуть бути заплановані для одноразового виконання або повторного виконання через рівні проміжки часу.
Давайте подивимося, як виглядає код, якщо ми хочемо запустити завдання після однієї секунди затримки:
TimerTask
task
=
new
TimerTask()
public
void
run()
System.out.println("Task performed on: "
+
new
Date()
+
"n"
+
"Thread's name: "
+
Thread.currentThread().getName());
>
>;
Timer
timer
=
new
Timer("Timer");
long
delay
=
1000L;
timer.schedule(task,
delay);
Тепер давайте додамо повторюваний розклад:
timer.scheduleAtFixedRate(repeatedTask,
delay,
період);
Цього разу завдання буде запущено після зазначеної затримки і повторюватиметься після певного періоду часу.
Для отримання додаткової інформації, будь ласка, прочитайте наш посібник з Java Timer .
6.2. Запланований потік пулу виконавців
ScheduledThreadPoolExecutor має методи, аналогічні класу Timer:
ScheduledExecutorService
executorService
=
Executors.NewScheduledThreadPool(2);
ScheduledFutureObject>
resultFuture
=
executorService.schedule(callableTask,
1,
TimeUnit.SECONDS);
Щоб закінчити наш приклад, ми використовуємо scheduleAtFixedRate() для повторюваних завдань:
ScheduledFutureObject>
resultFuture
=
executorService.scheduleAtFixedRate(runnableTask,
100,
450,
TimeUnit.MILLISECONDS);
Наведений вище код виконає завдання після початкової затримки в 100 мілісекунд, а потім те саме завдання буде виконувати кожні 450 мілісекунд.
Якщо процесор не може вчасно завершити обробку завдання до наступного виникнення, ScheduledExecutorService буде чекати на завершення поточного завдання, перш ніж запускати наступне.
Щоб уникнути цього часу очікування, ми можемо використовувати scheduleWithFixedDelay() , який, як випливає з назви, гарантує фіксовану затримку між ітераціями завдання.
Для отримання додаткових відомостей про ScheduledExecutorService див. посібник з Java ExecutorService.
6.3. Який інструмент кращий?
Якщо ми запустимо наведені вище приклади, результат обчислень буде таким самим.
Отже, як вибрати правильний інструмент ?
Коли фреймворк пропонує кілька варіантів, важливо розуміти технологію, що лежить в основі, щоб прийняти обґрунтоване рішення.
Давайте спробуємо поринути трохи глибше під капот.
Таймер:
- не дає гарантій реального часу: він планує завдання, використовуючи метод Object.wait (long)
- є один фоновий потік, тому завдання виконуються послідовно, і тривале завдання може затримувати інші
- Винятки часу виконання, створені в TimerTask, вб'ють єдиний доступний потік, тим самим вбивши Timer
Розклад ТреадпулЕкзекутор :
- можна налаштувати з будь-якою кількістю потоків
- може використовувати всі доступні ядра ЦП
- перехоплює винятки під час виконання та дозволяє нам обробляти їх, якщо ми хочемо (шляхом перевизначення методу afterExecute з ThreadPoolExecutor )
- скасовує завдання, що викликало виняток, дозволяючи іншим продовжувати роботу
- покладається на систему планування ОС для відстеження часових поясів, затримок, сонячного часу і т.д.
- надає спільний API, якщо нам потрібна координація між кількома завданнями, наприклад, очікування завершення всіх надісланих завдань
- надає найкращий API для управління життєвим циклом потоку
Тепер вибір очевидний, правда?
7. Різниця між Future та ScheduledFuture
У прикладах коду ми можемо спостерігати, що ScheduledThreadPoolExecutor повертає певний тип Future : ScheduledFuture .
ScheduledFuture розширює інтерфейси Future і Delayed , успадковуючи, таким чином, додатковий метод getDelay , що повертає затримку, пов'язану з поточним завданням. Він розширений за допомогою RunnableScheduledFuture , який додає метод для перевірки того, чи є завдання періодичною.
ScheduledThreadPoolExecutor реалізує всі ці конструкції через внутрішній клас ScheduledFutureTask та використовує їх для управління життєвим циклом завдання.
8. Висновки
У цьому посібнику ми експериментували з різними платформами, доступними для запуску потоків та паралельного виконання завдань.
Потім ми заглибилися в різницю між Timer і ScheduledThreadPoolExecutor.
Вихідний код статті доступний на GitHub.
- 1. Вступ
- 2. Основи запуску потоку
- 3. Платформа ExecutorService Framework
- 4. Запуск завдання із виконавцями
- 5. Запуск завдання за допомогою CompletableFutures
- 6. Запуск відкладених чи періодичних завдань
- 6.1. Таймер
- 6.2. Запланований потік пулу виконавців
- 6.3. Який інструмент кращий?
