Многопоточность в Java. Обзор Runnable, Thread, Object.

Многоазадачность

Многопоточность в Java представлена двумя базовыми сущностями. Это процессы и потоки. В Java многопоточном програмировании чаще всего используются потоки, хотя в реальной жизни существуют и много-процессные приложение.

Процессы в Java

Процесс — это самодостаточная среда выполнения программы. Каждый процесс имеет свое пространство в памяти. Часто под термином процесс подразумевается целое приложение.

Зачастую коммуникация между процессами производится с помощью IPC ресурсов: пайплайнов или сокетов.

Для того, чтобы создать много-процессное приложение предусмотрен класс ProcessBuilder.

Потоки в Java

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

Обзор интерфейса Runnable

Runnable это интерфейс содержащий метод run. В рамках выполнения инструкций, все операции в методе run будут выполнены в другом потоке.

Обзор класса Thread

Основное предназначение класса — это имплементация нового потока для выполнения инструкций. Класс Thread имплементирует интерефейс Runnable, где все операции выполняются в новом потоке в методе run. В свою очередь класс инкапсулирует множество дополнительных полей, мета данных потока: название, приоритет, флаг выполнения на фоне. Также класс содержит множество методов, которые помогают ожидать завершение потока, приостанавливать, продолжать выполнение инструкций и множество других функциональных методов.

Метод Sleep

Статический нативный метод, который приостанавливает выполнение потока. В аргументе метода указывается количество миллисекунд.

Thread.sleep(1500); 		//Ждет полторы секунды
Thread.sleep(2000, 100);  	//Ждет 2 секунды и 100 наносекунд

Метод Yield

Статический метод, который рекомендует процессору переключиться на выполнение других потоков. Этот метод эффективен при ожидании событий и других случаев, когда текущий поток не требует процессорного времени выполнения.

Метод Join

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

    public final synchronized void join(long millis) throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0L;
        if (millis < 0L) {
            throw new IllegalArgumentException("timeout value is negative");
        } else {
            if (millis == 0L) {
                while(this.isAlive()) {
                    this.wait(0L);
                }
            } else {
                while(this.isAlive()) {
                    long delay = millis - now;
                    if (delay <= 0L) {
                        break;
                    }

                    this.wait(delay);
                    now = System.currentTimeMillis() - base;
                }
            }

        }
    }

Как показывает исходнык код библиотеки, метод join работает на основе wait функционала из Object класса.

Методы класса Object для работы с многопоточностью

Класс Object содержит три метода для работы с потоками. Это wait, notify, notifyAll методы. Далее мы рассмотрим более подробно, как они взаимодействуют.

Метод Wait

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

Метод Notify

Уведомляет один из потоков, что нужно прекратить ожидание.

Метод NotifyAll

Уведомляет все потоки, что нужно прекратить ожидание.

Рассмотрение на практике

В приведенном примере мы видим простое приложение, которое ожидает нотификаций в определенных потока. Результат выполнение может немного удивить, потому, как мы вызываем метод notify вместо notifyAll.

public class Message {
    // поле, с которым будут работать потоки через вызовы геттеров и сеттеров
    private String msg;
     
    public Message(String str){
        this.msg=str;
    }
 
    public String getMsg() {
        return msg;
    }
 
    public void setMsg(String str) {
        this.msg=str;
    }
 
}
public class Waiter implements Runnable{
     
    private Message msg;
     
    public Waiter(Message m){
        this.msg = m;
    }
 
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        synchronized (msg) {
            try{
                System.out.println(name + " ждем вызов метода notify: " + System.currentTimeMillis());
                msg.wait();
            }catch(InterruptedException e){
                e.printStackTrace();
            }
            System.out.println(name + " был вызов метода notify: " + System.currentTimeMillis());
            // обработаем наше сообщение
            System.out.println(name + " : " + msg.getMsg());
        }
    }
}
public class Notifier implements Runnable {
 
    private Message msg;
     
    public Notifier(Message msg) {
        this.msg = msg;
    }
 
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(name + " стартовал");
        try {
            Thread.sleep(1000);
            synchronized (msg) {
                msg.setMsg(name + " поток Notifier отработал");
                msg.notify();
                // msg.notifyAll();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class WaitNotifyTest {
 
    public static void main(String[] args) {
        Message msg = new Message("обработать");
        Waiter waiter = new Waiter(msg);
        new Thread(waiter,"waiter").start();
         
        Waiter waiter1 = new Waiter(msg);
        new Thread(waiter1, "waiter1").start();
         
        Notifier notifier = new Notifier(msg);
        new Thread(notifier, "notifier").start();
        System.out.println("Стартовали все потоки");
    }
 
}
waiter ждем вызов метода notify: 12543137440105
waiter1 ждем вызов метода notify: 1253313744106
Стартовали все потоки
notifier стартовал
waiter был вызов метода notify: 1356318735011
waiter : поток Notifier отработал

Вот какой результат будет при вызове метода notifyAll:

waiter ждем вызов метода notify: 1276114117827
waiter1 ждем вызов метода notify: 1276114117827
Стартовали все потоки
notifier стартовал
waiter1 был вызов метода notify: 1276114117212
waiter1 : поток Notifier отработал
waiter был вызов метода notify: 1276114117212
waiter : поток Notifier отработал

Мы видим, что отработали оба ожидающих потока.

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

Часто в потоках используются циклы с функциональными инструкциями, которые можно завершить установив ключевой флаг. Ниже мы приводим пример простой программы где по включению флага mFinish завершается определенный поток.

class Incremenator extends Thread
{
	private volatile boolean mIsIncrement = true;
	private volatile boolean mFinish = false;

	public void changeAction()	//Меняет действие на противоположное
	{
		mIsIncrement = !mIsIncrement;
	}
	public void finish()		//Инициирует завершение потока
	{
		mFinish = true;
	}

	@Override
	public void run()
	{
		do
		{
			if(!mFinish)	//Проверка на необходимость завершения
			{
				if(mIsIncrement)	
					Program.mValue++;	//Инкремент
				else
					Program.mValue--;	//Декремент
				
				//Вывод текущего значения переменной
				System.out.print(Program.mValue + " ");
			}
			else
				return;		//Завершение потока

			try{
				Thread.sleep(1000);		//Приостановка потока на 1 сек.
			}catch(InterruptedException e){}
		}
		while(true); 
	}
}

public class Program
{
	//Переменая, которой оперирует инкременатор
	public static int mValue = 0;
	
	static Incremenator mInc;	//Объект побочного потока

	public static void main(String[] args)
	{
		mInc = new Incremenator();	//Создание потока
		
		System.out.print("Значение = ");
		
		mInc.start();	//Запуск потока
		
		//Троекратное изменение действия инкременатора
		//с интервалом в i*2 секунд
		for(int i = 1; i <= 3; i++)
		{
			try{
				Thread.sleep(i*2*1000); //Ожидание в течении i*2 сек.
			}catch(InterruptedException e){}
			
			mInc.changeAction();	//Переключение действия
		}
		
		mInc.finish();	//Инициация завершения побочного потока	
	}
}
Консоль:
Значение = 1 2 1 0 -1 -2 -1 0 1 2 3 4

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

Вызывав метод interupt, можно принудительно прервать выполнение потока. В данном случае будет вызвано исключение InterruptedException.

Пример кода прерывающего выполнение потока:

class Incremenator extends Thread
{
	private volatile boolean mIsIncrement = true;

	public void changeAction()	//Меняет действие на противоположное
	{
		mIsIncrement = !mIsIncrement;
	}

	@Override
	public void run()
	{
		do
		{
			if(!Thread.interrupted())	//Проверка прерывания
			{
				if(mIsIncrement) Program.mValue++;	//Инкремент
				else Program.mValue--;			//Декремент
				
				//Вывод текущего значения переменной
				System.out.print(Program.mValue + " ");
			}
			else
				return;		//Завершение потока	

			try{
				Thread.sleep(1000);		//Приостановка потока на 1 сек.
			}catch(InterruptedException e){
				return;	//Завершение потока после прерывания
			}
		}
		while(true); 
	}
}

class Program
{
	//Переменая, которой оперирует инкременатор
	public static int mValue = 0;
	
	static Incremenator mInc;	//Объект побочного потока

	public static void main(String[] args)
	{
		mInc = new Incremenator();	//Создание потока
		
		System.out.print("Значение = ");
		
		mInc.start();	//Запуск потока
		
		//Троекратное изменение действия инкременатора
		//с интервалом в i*2 секунд
		for(int i = 1; i <= 3; i++)
		{
			try{
				Thread.sleep(i*2*1000);		//Ожидание в течении i*2 сек.
			}catch(InterruptedException e){}
			
			mInc.changeAction();	//Переключение действия
		}
		
		mInc.interrupt();	//Прерывание побочного потока
	}
}

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

При выполнении потоков можно устанавливать приоритет выполнения, который определяет количество ресурсов предоставленных для выполнения инструкций. В наличии есть такие значения: MIN_PRIORITY, NORM_PRIORITY и MAX_PRIORITY

Заключение

Java, как и другие языки программирования предоставляет множество возможностей выполнять код одновременно. В данной статье мы рассмотрели основы многопоточности и много-процессорных приложений.

Будем рады здоровой критике в комментариях 🙂 Успехов в написании качественных продуктов 🙂

Related posts

Leave a Comment