Рубріки: Теория

Многопоточность в Java: работа с потоками и полезные методы класса Thread

Ольга Наконечна

Многопоточность в 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 секунд.

Метод yield()

Предотвратить выполнение потока можно методом yield(): предположим, существует три потока t1, t2, and t3. Поток t1 выполняется процессором, а потоки t2 и t3 находятся в состоянии Ready/Runnable. Время выполнения для потока t1 — 5 часов, а для t2 – 5 минут.

Поскольку t1 закончит свое выполнение через 5 часов, t2 придется ждать все это время, чтобы закончить 5-минутную задачу. В таких случаях, когда один поток требует слишком много времени, чтобы завершить свое выполнение, нужен способ приостановить выполнение длинного потока в промежутке, если какая-то важная задача не завершена. Тут и поможет yield ().

По сути, yield() означает, что поток не выполняет ничего особо важного, и если другие потоки или процессы требуют запуска, то их можно запустить.

Использование метода yield() :

  • Когда поток вызывает метод java.lang.Thread.yield, он дает планировщику подсказку, что готов приостановить свое выполнение. Планировщик потока вправе это проигнорировать.
  • Если какой-то поток выполняет метод yield(), планировщик потока проверяет, есть ли поток с таким же или высшим приоритетом. Если процессор найдет такой поток, то изменит состояние выполняющегося в данный момент потока на Ready/Runnable и отдаст процессор другому потоку. Если нет — поток продолжит выполняться.

Синтаксис:

public static native void yield()
// Java program to illustrate yield() method
// in Java
import java.lang.*;
// MyThread extending Thread
class MyThread extends Thread
{
    public void run()
    {
        for (int i=0; i<5 ; i++)
         System.out.println(Thread.currentThread().getName()
                                + " in control");
    }
}
// Driver Class
public class yieldDemo
{
    public static void main(String[]args)
    {
        MyThread t = new MyThread();
        t.start();
        for (int i=0; i<5; i++)
        {
            // Control passes to child thread
            Thread.yield();
            // After execution of child Thread
            // main thread takes over
        System.out.println(Thread.currentThread().getName()
                                + " in control");
        }
    }
}

Результат:

Thread-0 in control
Thread-0 in control
Thread-0 in control
Thread-0 in control
Thread-0 in control
main in control
main in control
main in control
main in control
main in control

Метод 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 начинают выполнять свои задачи.

Приоритеты потоков

У каждого потока есть приоритет. Приоритет обозначается числом от 1 до 10. В большинстве случаев планировщик распределяет потоки относительно их приоритета (другими словами — происходит приоритетное планирование). Но это не гарантированно, поскольку он зависит от того, какое планирование выберет JVM.

Три константы, которые определяются в классе Thread:

1. public static int MIN_PRIORITY (значение равно 1);

2. public static int NORM_PRIORITY (дефолтный приоритет потока);

3. public static int MAX_PRIORITY (значение равно 10).

Пример приоритета потока:

class TestMultiPriority1 extends Thread{  
 public void run(){  
   System.out.println("running thread name is:"+Thread.currentThread().getName());  
   System.out.println("running thread priority is:"+Thread.currentThread().getPriority());  
  
  }  
 public static void main(String args[]){  
  TestMultiPriority1 m1=new TestMultiPriority1();  
  TestMultiPriority1 m2=new TestMultiPriority1();  
  m1.setPriority(Thread.MIN_PRIORITY);  
  m2.setPriority(Thread.MAX_PRIORITY);  
  m1.start();  
  m2.start();  

Результат:

       running thread name is:Thread-0

       running thread priority is:10

       running thread name is:Thread-1

       running thread priority is:1

Некоторые полезные методы класса Thread

  • boolean isAlive() — возвращает true, если myThread() выполняется, и false — если поток еще не был запущен или был завершен.

Синтаксис:

final boolean isAlive(){
    }

Например:

import java.lang.Thread;

class IsThreadAlive extends Thread {
    public void run() {
        try {

            // this thread stops for few miliseconds before 
            // run() method complete its execution
            Thread.sleep(500);

            // Display status of Current Thread is alive 
            System.out.println("Is thread alive in run() method " + Thread.currentThread().isAlive());
        } catch (Exception ex) {
            System.out.println(ex.getMessage());
        }
    }
    public static void main(String[] args) {
        // creating an object of class named 
        IsThreadAlive alive = new IsThreadAlive();

        // Display status of thread is alive before 
        // calling start() method 
        System.out.println("Is thread alive before start() call:" + alive.isAlive());

        alive.start();

        // Display status of thread is alive after 
        // calling start() method  
        System.out.println("Is thread alive after start() call:" + alive.isAlive());
    }
}

Результат:

E:\Programs>javac IsThreadAlive.java

E:\Programs>java IsThreadAlive
Is thread alive before start() call:false
Is thread alive after start() call:true
Is thread alive in run() method true
  • setName(String threadName) – задает имя потока.

Синтаксис:

public final void setName(String a)

Например:

public class SetNameExample extends Thread  
{    
    public void run()  
    {    
        System.out.println("running...");    
    }    
    public static void main(String args[])  
    {    
        // creating two threads  
        SetNameExample t1=new SetNameExample();    
        SetNameExample t2=new SetNameExample();    
        // start of thread   
        t1.start();    
        t2.start();       
        // change the thread name   
        t1.setName("Sonoo Jaiswal");    
        t2.setName("javatpoint");  
        // print the thread after changing   
        System.out.println("After changing name of t1: "+t1.getName());  
        System.out.println("After changing name of t2: "+t2.getName());  
    }    
} 

Результат:

After changing name of t1: Sonoo Jaiswal
running...
running...
After changing name of t2: javatpoint
  • String getName() – получает имя потока.

Имя потока – ассоциированная с ним строка, которая в некоторых случаях помогает понять, какой поток выполняет действие.

Синтаксис:

public final String getName()

Например:

public class GetNameExample extends Thread  
{    
    public void run()  
    {    
        System.out.println("Thread is running...");    
    }    
    public static void main(String args[])  
    {   
        // creating two threads   
        GetNameExample t1=new GetNameExample();    
        GetNameExample t2=new GetNameExample();    
        // return the name of threads  
        System.out.println("Name of t1: "+ t1.getName());    
        System.out.println("Name of t2: "+t2.getName());    
        // start t1  and t2 threads   
        t1.start();    
        t2.start();    
    }    
}

Результат:

Name of t1: Thread-0

Name of t2: Thread-1

Thread is running...

Thread is running...
  • toString() — возвращает строковое представление потока, включая имя, приоритет и группу потока.

Синтаксис:

public String toString()

Например:

public class JavaToStringExp implements Runnable   
{  
    Thread t;  
    JavaToStringExp()   
    {  
        t = new Thread(this);  
        // this will call run() function  
        t.start();  
    }  
    public void run()   
    {  
        // returns a string representation of this thread   
        System.out.println(t.toString());  
    }  
    public static void main(String[] args)   
    {  
        new JavaToStringExp();  
    }  
}

Результат:

Thread[Thread-0,5,main]
  • static Thread Thread.currentThread() — статический метод, возвращающий объект потока, в котором он был вызван.

Синтаксис:

public static Thread currentThread()

Например:

public class CurrentThreadExp extends Thread  
{    
    public void run()  
    {    
        // print currently executing thread   
        System.out.println(Thread.currentThread().getName());    
    }    
    public static void main(String args[])  
    {    

        // creating two thread 

 CurrentThreadExp t1=new CurrentThreadExp();    
        CurrentThreadExp t2=new CurrentThreadExp();    
        // this will call the run() method  
        t1.start();    
        t2.start();    
    }    
}

Результат:

Thread-0

Thread-1
  • long getId() – возвращает идентификатор потока. Идентификатор – уникальное число, присвоенное потоку. Когда поток завершается, его идентификатор можно повторно использовать.

Синтаксис:

long getId(){
    }

Например:

*/import java.lang.Thread;

class GetThreadId extends Thread {
    // Override run() of Thread class
    public void run() {
        System.out.println("The priority of this thread is : " + Thread.currentThread().getPriority());
    }

    public static void main(String[] args) {
        // Creating an object of GetThreadId class
        GetThreadId gt_id = new GetThreadId();

        // Calling start() method with GetThreadId class 
        // object of Thread class
        gt_id.start();

        // Get name of a thread th and display on standard 
        // output stream
        System.out.println("The name of this thread is " + " " + gt_id.getName());

        // Get Id of a thread th and display on standard 
        // output stream
        System.out.println("The Id of this thread is " + " " + gt_id.getId());
    }
}

Результат:

E:\Programs>javac GetThreadId.java

E:\Programs>java GetThreadId
The priority of this thread is :5
The name of this thread is  Thread-0
The Id of this thread is  9

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023