Как стать автором
Обновить

Многопоточность в Java. Работа с потоками

Многопоточность в Java — это одновременное выполнение двух или более потоков для максимального использования центрального процессора (CPU — central processing unit). Каждый поток работает параллельно и не требует отдельной области памяти. К тому же, переключение контекста между потоками занимает меньше времени.

Использование многопоточности:

  • Лучшее использование одного центрального процессора: если поток ожидает ответ на запрос, отправленный по сети, другой поток в это время может использовать CPU для выполнения других задач. Кроме того, если у компьютера несколько CPU или у CPU несколько исполнительных ядер, многопоточность позволяет приложению использовать эти дополнительные ядра.

  • Оптимальное использование нескольких центральных процессоров или их ядер: необходимо использовать несколько потоков в приложении, чтобы задействовать все CPU или ядра CPU. Один поток может использовать максимум один CPU, иногда даже не полностью.

  • Улучшенный user experience в плане скорости ответа на запрос: например, если нажать на кнопку в графическом интерфейсе, то это действие отправит запрос по сети: здесь важно, какой поток выполняет этот запрос. Если используется тот же поток, который обновляет/уведомляет графический интерфейс, тогда пользователь может столкнуться с зависанием интерфейса, ожидающего ответа на запрос. Но этот запрос может выполнить фоновый поток, чтобы поток в графическом интерфейсе мог в это время реагировать на другие запросы пользователя.

  • Улучшенный user experience в плане справедливости распределения ресурсов: многопоточность позволяет справедливо распределять ресурсы компьютера между пользователями. Представьте сервер, который принимает запросы от клиентов и у него есть только один поток для выполнения этих запросов. Если клиент отправляет запрос, для обработки которого нужно много времени, все остальные запросы вынуждены ждать до тех пор, пока он завершится. Когда каждый клиентский запрос выполняется собственным потоком, ни одна задача не сможет полностью захватить CPU.

Процессы в Java: определение и функции

  • Процесс состоит из кода и данных. Он создается операционной системой при запуске приложения, является достаточно ресурсоемким и обладает собственным виртуальным адресным пространством.

  • Процессы работают независимо друг от друга. Они не имеют прямого доступа к общим данным в других процессах.

  • Операционная система выделяет ресурсы для процесса — память и время на выполнение.

  • Если один из процессов заблокирован, то ни один другой процесс не может выполняться, пока он не будет разблокирован.

  • Для создания нового процесса обычно дублируется родительский процесс.

  • Процесс может контролировать дочерние процессы, но не процессы того же уровня.

Что такое потоки

Поток — наименьшее составляющее процесса. Потоки могут выполняться параллельно друг с другом. Их также часто называют легковесными процессами. Они используют адресное пространство процесса и делят его с другими потоками.

Потоки могут контролироваться друг друга и общаться посредством методов wait()notify()notifyAll().

Состояния потоков

Потоки могут пребывать в нескольких состояниях:

  • New – когда создается экземпляр класса Thread, поток находится в состоянии new. Он пока еще не работает.

  • Running — поток запущен и процессор начинает его выполнение. Во время выполнения состояние потока также может измениться на Runnable, Dead или Blocked.

  • Suspended — запущенный поток приостанавливает свою работу, затем можно возобновить его выполнение. Поток начнет работать с того места, где его остановили.

  • Blocked — поток ожидает высвобождения ресурсов или завершение операции ввода-вывода. Находясь в этом состоянии поток не потребляет процессорное время.

  • Terminated — поток немедленно завершает свое выполнение. Его работу нельзя возобновить. Причинами завершения потока могут быть ситуации, когда код потока полностью выполнен или во время выполнения потока произошла ошибка (например, ошибка сегментации или необработанного исключения).

  • Dead — после того, как поток завершил свое выполнение, его состояние меняется на dead, то есть он завершает свой жизненный цикл.

Способы запуска потоков

Приложение, создающее экземпляр класса Thread, должно предоставить код, который будет работать в этом потоке. Существует два способа, чтобы добиться этого:

  • Предоставить реализацию объекта Runnable. Интерфейс Runnable определяет единственный метод — run, который должен содержать код, выполняющийся в потоке. Объект Runnable передается конструктору Thread. Например:

public class HelloRunnable implements Runnable {
    public void run() {
        System.out.println("Hello from a thread!");
    }
    public static void main(String args[]) {
        (new Thread(new HelloRunnable())).start();
    }
}
  • Использовать подкласс Thread. Класс Thread сам реализует Runnable, хотя его метод run не делает ничего. Можно объявить класс Thread подклассом, предоставляя собственную реализацию метода run, как в примере:

public class HelloThread extends Thread {
    public void run() {
        System.out.println("Hello from a thread!");
    }
    public static void main(String args[]) {
        (new HelloThread()).start();
    }
}

Обратите внимание, что оба примера вызывают Thread.start, чтобы запустить новый поток.

Какой из способов выбрать? Первый — с использованием объекта Runnable — более общий, потому что этот объект может превратить отличный от Thread класс в подкласс. Этот способ более гибкий и может использоваться для высокоуровневых API управления потоками.

Второй способ больше подходит для простых приложений, но есть условие: класс задачи должен быть потомком Thread.

Завершение процесса и потоки-демоны

В Java процесс завершается тогда, когда завершаются все его основные и дочерние потоки.

Потоки-демоны — это низкоприоритетные потоки, работающие в фоновом режиме для выполнения таких задач, как сбор «мусора»: они освобождают память неиспользованных объектов и очищают кэш. Большинство потоков JVM (Java Virtual Machine) являются потоками-демонами. 

Свойства потоков-демонов:

  • Не влияют на закрытие JVM, когда все пользовательские потоки завершили свое исполнение;

  • JVM сама закрывается, когда все пользовательские потоки перестают выполняться;

  • Если JVM обнаружит работающий поток-демон, она завершит его, после чего закроется. JVM не учитывает, работает поток или нет.

Чтобы установить, является ли поток демоном, используется метод boolean isDaemon(). Если да, то он возвращает значение true, если нет, то — то значение false.

Завершение потоков

Завершение потока Java требует подготовки кода реализации потока. Класс Java Thread содержит метод stop(), но он помечен как deprecated. Оригинальный метод stop() не дает никаких гарантий относительно состояния, в котором поток остановили. То есть, все объекты Java, к которым у потока был доступ во время выполнения, останутся в неизвестном состоянии. Если другие потоки в приложении имели доступ к тем же объектам, то они могут неожиданно «сломаться».

Вместо вызова метода stop() нужно реализовать код потока, чтобы его остановить. Приведем пример класса с реализацией Runnable, который содержит дополнительный метод doStop(), посылающий Runnable сигнал остановиться. Runnable проверит его и остановит, когда будет готов.

public class MyRunnable implements Runnable {
    private boolean doStop = false;
    public synchronized void doStop() {
        this.doStop = true;
    }
    private synchronized boolean keepRunning() {
        return this.doStop == false;
    }
    @Override
    public void run() {
        while(keepRunning()) {
            // keep doing what this thread should do.
            System.out.println("Running");
            try {
                Thread.sleep(3L * 1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Обратите внимание на методы doStop() и keepRunning(). Вызов doStop() происходит не из потока, выполняющего метод run() в MyRunnable.

Метод keepRunning() вызывается внутренней потоком, выполняющим метод run() MyRunnable. Поскольку метод doStop() не вызван, метод keepRunning() возвратит значение true, то есть поток, выполняющий метод run(), продолжит работать.

Например:

public class MyRunnableMain {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
        try {
            Thread.sleep(10L * 1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        myRunnable.doStop();
    }
}

В примере сначала создается MyRunnable, а затем передается потоку и запускает его. Поток, выполняющий метод main() (главный поток), засыпает на 10 секунд и потом вызывает метод doStop() экземпляра класса MyRunnable. Впоследствии поток, выполняющий метод MyRunnable, остановится, потому что после того, как вызван doStop(),  keepRunning() возвратит false.

Обратите внимание, если для реализация Runnable нужен не только метод run() (а например, еще метод stop() или pause()), реализацию Runnable больше нельзя будет создать с помощью лямбда-выражений. Понадобится кастомный класс или интерфейс, расширяющий Runnable, который содержит дополнительные методы и реализуется анонимным классом.

Метод Thread.sleep()

Поток может остановиться сам, вызвав статический метод Thread.sleep(). Thread.sleep() принимает в качестве параметра количество миллисекунд. Метод sleep() попытается заснуть на это количество времени перед возобновлениям выполнения. Thread sleep() не гарантирует абсолютной точности.

Приведем пример остановки потока Java на 10 секунд (10 тысяч миллисекунд) с помощью вызова метода Thread sleep():

try {
    Thread.sleep(10L * 1000L);
} catch (InterruptedException e) {
    e.printStackTrace();
}

Поток, выполняющий код, уснет примерно на 10 секунд.

Метод join()

Метод join() экземпляра класса Thread используется для объединения начала выполнения одного потока с завершением выполнения другого потока. Это необходимо, чтобы один поток не начал выполняться до того, как завершится другой поток. Если метод join() вызывается на Thread, то выполняющийся в этот момент поток блокируется до момента, пока Thread не закончит выполнение.

Метод join() ждет не более указанного количества миллисекунд, пока поток умрет. Тайм-аут 0 (ноль) означает «ждать вечно».

Синтаксис:

public void join()throws InterruptedException

Например:

class TestJoinMethod1 extends Thread{  
 public void run(){  
  for(int i=1;i<=5;i++){  
   try{  
    Thread.sleep(500);  
   }catch(Exception e){System.out.println(e);}  
  System.out.println(i);  
  }  
 }  
public static void main(String args[]){  
 TestJoinMethod1 t1=new TestJoinMethod1();  
 TestJoinMethod1 t2=new TestJoinMethod1();  
 TestJoinMethod1 t3=new TestJoinMethod1();  
 t1.start();  
 try{  
  t1.join();  
 }catch(Exception e){System.out.println(e);}  
 t2.start();  
 t3.start();  
 }  
}

Результат:

1
2
3
4
5
1
1
2
2
3
3
4
4
5
5

Из примера видно, что как только поток t1 завершает выполнение задачи, потоки t2 и t3 начинают выполнять свои задачи.

Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.