JavaRush /Java блог /Random UA /Цінуємо час із потоками
Andrei
2 рівень

Цінуємо час із потоками

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

Передмова. Дядя Петя

Отже, припустимо, що ми захотіли набрати пляшку води. В наявності є пляшка та кран з водою дядька Петі. Дяді Пете сьогодні встановабо новий кран, і він без хвилювання нахвалював його красу. До цього він користувався тільки старим краном, що засмітився, тому черги на розливі були колосальні. Трохи повозившись, з боку розливу почувся звук води, що набирається, через 2 хвабони пляшка все ще знаходиться в стадії наповнення, за нами зібралася звична черга, а в голові малюється образ того, як дбайливий дядько Петя відбирає тільки кращі молекули H2O в нашу пляшку. Навчений життям дядько Петя заспокоює особливо агресивних і обіцяє закінчити якнайшвидше. Покінчивши з пляшкою, він бере наступну і включає звичний натиск, який не розкриває всіх можливостей нового крана. Люди незадоволені. Цінуємо час з потоками - 1

Теорія

Багатопотоковість - це властивість платформи створювати кілька потоків у рамках одного процесу. Створення та виконання потоку набагато простіше, ніж створення процесу, тому при необхідності реалізувати кілька паралельних дій в одній програмі використовуються додаткові потоки. У JVM будь-яка програма запускається переважно потоці, а вже з нього запускаються інші. У межах процесу потоки здатні обмінюватися даними між собою. При запуску нового потоку його можна оголосити як користувальницький за допомогою методу

setDaemon(true);
такі потоки автоматично завершаться, якщо не залишиться інших працюючих потоків. Потоки мають пріоритет роботи (вибір пріоритету не гарантує, що потік із вищим пріоритетом завершиться швидше за поток з нижчим пріоритетом).
  • MIN_PRIORITY
  • NORM_PRIORITY (default)
  • MAX_PRIORITY
Основні методи під час роботи з потоками:
  • run()- Виконує потік
  • start()- Запускає потік
  • getName()- Повертає ім'я потоку
  • setName()- Задає ім'я потоку
  • wait()– успадкований метод, потік очікує виклику методу notify()з іншого потоку
  • notify()– успадкований метод, відновлює раніше зупинений потік
  • notifyAll()– успадкований метод, відновлює раніше зупинені потоки
  • sleep()- Зупиняє потік на заданий час
  • join()- Чекає завершення потоку
  • interrupt()– перериває виконання потоку
Більше методів можна знайти тут Саме час задуматися про нові потоки, якщо у вашій програмі є:
  • Доступ до мережі
  • Доступ до файлової системи
  • GUI

Клас Thread

Потоки в Java представлені у вигляді класу Threadта його спадкоємців. Наведений нижче приклад є найпростішою реалізацією потокового класу.

import static java.lang.System.out;

public class ExampleThread extends Thread{
    
    public static void main(String[] args) {
        out.println("Основной поток");
        new ExampleThread().start();
    }

    @Override
    public void run() {
        out.println("Новый поток");
    }
}
В результаті отримаємо

Основной поток
Новый поток
Тут ми створюємо наш клас і робимо його спадкоємцем класу Thread, після чого пишемо метод main() для запуску основного потоку та перевизначаємо метод run()класу Thread. Тепер створивши екземпляр нашого класу і виконавши його успадкований метод, start()ми запустимо новий потік, в якому виконається все, що описано в тілі методу run(). Звучить складно, але глянувши на код прикладу, все має бути зрозуміло.

Інтерфейс Runnable

Oracle також пропонує для запуску нового потоку реалізовувати інтерфейс Runnable, що дає нам більшу гнучкість у розробці, ніж єдине доступне успадкування у попередньому прикладі (якщо зазирнути у вихідні класи Threadможна побачити, що він також реалізує інтерфейс Runnable). Застосуємо метод створення нового потоку, що рекомендується.

import static java.lang.System.out;

public class ExampleRunnable implements Runnable {
    
    public static void main(String[] args) {
        out.println("Основной поток");
        new Thread(new ExampleRunnable()).start();
    }   

    @Override
    public void run() {
        out.println("Новый поток");        
    }
}
В результаті отримаємо

Основной поток
Новый поток
Приклади дуже подібні, т.к. при написанні коду нам довелося реалізувати абстрактний метод run(), описаний в інтерфейсі Runnable. Запуск нового потоку трохи відрізняється. Ми створабо екземпляр класу Thread, передавши як параметр посилання на екземпляр нашої реалізації інтерфейсу Runnable. Саме такий підхід дозволяє створювати нові потоки без прямого спадкування класу Thread.

Довгі операції

Наступний приклад наочно покаже переваги використання кількох потоків. Припустимо, у нас є просте завдання, що вимагає кількох тривалих обчислень, до цієї статті ми вирішували б її в методі, main()можливо, розбивши для зручності сприйняття на окремі методи, можливо навіть класи, але суть була б одна. Усі операції відбувалися б послідовно одна одною. Давайте змоделюємо великовагові обчислення та заміряємо час їх виконання.

public class ComputeClass {
    
    public static void main(String[] args) {    
        // Узнаем стартовое время программы
        long startTime = System.currentTimeMillis();
        
        // Определяем долгосрочные операции
        for(double i = 0; i < 999999999; i++){            
        }
        System.out.println("complete 1");        
        for(double i = 0; i < 999999999; i++){            
        }
        System.out.println("complete 2");        
        for(double i = 0; i < 999999999; i++){            
        }
        System.out.println("complete 3");
        
        //Вычисляем и выводим время выполнения программы
        long timeSpent = System.currentTimeMillis() - startTime;
        System.out.println("программа выполнялась " + timeSpent + " миллисекунд");
    }    
}
В результаті отримаємо

complete 1
complete 2
complete 3
программа выполнялась 9885 миллисекунд
Час виконання залишає бажати кращого, а ми весь цей час дивимося на порожній екран виводу, і ситуація дуже скидається на історію про дядька Петю тільки тепер у його ролі ми, розробники, які не скористалися всіма можливостями сучасних пристроїв. Виправлятимемося.

public class ComputeClass {
    
    public static void main(String[] args) {    
        // Узнаем стартовое время программы
        long startTime = System.currentTimeMillis();
        
        // Определяем долгосрочные операции
        new MyThread(1).start();
        new MyThread(2).start();
        for(double i = 0; i < 999999999; i++){            
        }
        System.out.println("complete 3");
        
        //Вычисляем и выводим время выполнения программы
        long timeSpent = System.currentTimeMillis() - startTime;
        System.out.println("программа выполнялась " + timeSpent + " миллисекунд");
    }    
}

class MyThread extends Thread{
int n;

MyThread(int n){
    this.n = n;
}

    @Override
    public void run() {
        for(double i = 0; i < 999999999; i++){            
        }
        System.out.println("complete " + n); 
    }    
}
В результаті отримаємо

complete 1
complete 2
complete 3
программа выполнялась 3466 миллисекунд
Час роботи значно скоротилося (цей ефект може бути не досягнутий або взагалі збільшити час виконання на процесорах, що не підтримують багатопоточність). Варто зауважити, що потоки можуть завершитися не по порядку, і якщо розробнику необхідна передбачуваність дій, він повинен реалізувати її самостійно під конкретний випадок.

Групи потоків

Потоки Java можна об'єднувати в групи, для цього використовується клас ThreadGroup. До складу груп можуть входити як одиночні потоки, і цілі групи. Це може виявитися зручним, якщо вам треба перервати пов'язані потоки, наприклад, з мережею при обриві з'єднання. Докладніше про групи можна почитати тут Сподіваюся, тепер тема стала вам більш зрозумілою і ваші користувачі будуть задоволені.
Цінуємо час з потоками - 2
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ