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

Ценим время с потоками

Статья из группы Random

Предисловие. Дядя Петя

Итак, предположим, что мы захотели набрать бутылку воды. В наличии имеются бутылка и кран с водой дяди Пети. Дяде Пете сегодня установили новый кран, и он без умолку нахваливал его красоту. До этого он пользовался только старым засорившимся краном, поэтому очереди на разливе были колоссальные. Немного повозившись, со стороны разлива послышался звук набирающейся воды, спустя 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
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Stanislav Sukhanov Уровень 38
12 мая 2019
Разве не должно быть .join в последнем примере, что бы правильно посчитать время исполнения?