Как найти поток java

Здравствуйте! В этой статье я вкратце расскажу вам о процессах, потоках, и об основах многопоточного программирования на языке Java.
Наиболее очевидная область применения многопоточности – это программирование интерфейсов. Многопоточность незаменима тогда, когда необходимо, чтобы графический интерфейс продолжал отзываться на действия пользователя во время выполнения некоторой обработки информации. Например, поток, отвечающий за интерфейс, может ждать завершения другого потока, загружающего файл из интернета, и в это время выводить некоторую анимацию или обновлять прогресс-бар. Кроме того он может остановить поток загружающий файл, если была нажата кнопка «отмена».

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

Давайте начнем. Сначала о процессах.

Процессы

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

Для каждого процесса ОС создает так называемое «виртуальное адресное пространство», к которому процесс имеет прямой доступ. Это пространство принадлежит процессу, содержит только его данные и находится в полном его распоряжении. Операционная система же отвечает за то, как виртуальное пространство процесса проецируется на физическую память.

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

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

Потоки

Один поток – это одна единица исполнения кода. Каждый поток последовательно выполняет инструкции процесса, которому он принадлежит, параллельно с другими потоками этого процесса.

Следует отдельно обговорить фразу «параллельно с другими потоками». Известно, что на одно ядро процессора, в каждый момент времени, приходится одна единица исполнения. То есть одноядерный процессор может обрабатывать команды только последовательно, по одной за раз (в упрощенном случае). Однако запуск нескольких параллельных потоков возможен и в системах с одноядерными процессорами. В этом случае система будет периодически переключаться между потоками, поочередно давая выполняться то одному, то другому потоку. Такая схема называется псевдо-параллелизмом. Система запоминает состояние (контекст) каждого потока, перед тем как переключиться на другой поток, и восстанавливает его по возвращению к выполнению потока. В контекст потока входят такие параметры, как стек, набор значений регистров процессора, адрес исполняемой команды и прочее…

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

Вот как это выглядит:

Цветные квадраты на рисунке – это инструкции процессора (зеленые – инструкции главного потока, синие – побочного). Выполнение идет слева направо. После запуска побочного потока его инструкции начинают выполняться вперемешку с инструкциями главного потока. Кол-во выполняемых инструкций за каждый подход не определено.

То, что инструкции параллельных потоков выполняются вперемешку, в некоторых случаях может привести к конфликтам доступа к данным. Проблемам взаимодействия потоков будет посвящена следующая статья, а пока о том, как запускаются потоки в Java…

Запуск потоков

Каждый процесс имеет хотя бы один выполняющийся поток. Тот поток, с которого начинается выполнение программы, называется главным. В языке Java, после создания процесса, выполнение главного потока начинается с метода main(). Затем, по мере необходимости, в заданных программистом местах, и при выполнении заданных им же условий, запускаются другие, побочные потоки.

В языке Java поток представляется в виде объекта-потомка класса Thread. Этот класс инкапсулирует стандартные механизмы работы с потоком.

Запустить новый поток можно двумя способами:

Способ 1

Создать объект класса Thread, передав ему в конструкторе нечто, реализующее интерфейс Runnable. Этот интерфейс содержит метод run(), который будет выполняться в новом потоке. Поток закончит выполнение, когда завершится его метод run().

Выглядит это так:

class SomeThing			//Нечто, реализующее интерфейс Runnable
implements Runnable		//(содержащее метод run())
{
	public void run()		//Этот метод будет выполняться в побочном потоке
	{
		System.out.println("Привет из побочного потока!");
	}
}

public class Program			//Класс с методом main()
{
	static SomeThing mThing;	//mThing - объект класса, реализующего интерфейс Runnable
	
	public static void main(String[] args)
	{
		mThing = new SomeThing();				

		Thread myThready = new Thread(mThing);	//Создание потока "myThready"
		myThready.start();				//Запуск потока

		System.out.println("Главный поток завершён...");
	}
}

Для пущего укорочения кода можно передать в конструктор класса Thread объект безымянного внутреннего класса, реализующего интерфейс Runnable:

public class Program		//Класс с методом main().
{
	public static void main(String[] args)
	{
		//Создание потока
		Thread myThready = new Thread(new Runnable()
		{
			public void run() //Этот метод будет выполняться в побочном потоке
			{
				System.out.println("Привет из побочного потока!");
			}
		});
		myThready.start();	//Запуск потока

		System.out.println("Главный поток завершён...");
	}
}
Способ 2

Создать потомка класса Thread и переопределить его метод run():

class AffableThread extends Thread
{
	@Override
	public void run()	//Этот метод будет выполнен в побочном потоке
	{
		System.out.println("Привет из побочного потока!");
	}
}

public class Program
{
	static AffableThread mSecondThread;
	
	public static void main(String[] args)
	{
		mSecondThread = new AffableThread();	//Создание потока
		mSecondThread.start();					//Запуск потока
		
		System.out.println("Главный поток завершён...");
	}
}

В приведённом выше примере в методе main() создается и запускается еще один поток. Важно отметить, что после вызова метода mSecondThread.start() главный поток продолжает своё выполнение, не дожидаясь пока порожденный им поток завершится. И те инструкции, которые идут после вызова метода start(), будут выполнены параллельно с инструкциями потока mSecondThread.

Для демонстрации параллельной работы потоков давайте рассмотрим программу, в которой два потока спорят на предмет философского вопроса «что было раньше, яйцо или курица?». Главный поток уверен, что первой была курица, о чем он и будет сообщать каждую секунду. Второй же поток раз в секунду будет опровергать своего оппонента. Всего спор продлится 5 секунд. Победит тот поток, который последним изречет свой ответ на этот, без сомнения, животрепещущий философский вопрос. В примере используются средства, о которых пока не было сказано (isAlive() sleep() и join()). К ним даны комментарии, а более подробно они будут разобраны дальше.

class EggVoice extends Thread
{
	@Override
	public void run()
	{
		for(int i = 0; i < 5; i++)
		{
			try{
				sleep(1000);		//Приостанавливает поток на 1 секунду
			}catch(InterruptedException e){}
			
			System.out.println("яйцо!");	
		}
		//Слово «яйцо» сказано 5 раз
	}
}

public class ChickenVoice	//Класс с методом main()
{
	static EggVoice mAnotherOpinion;	//Побочный поток
	
	public static void main(String[] args)
	{
		mAnotherOpinion = new EggVoice();	//Создание потока
		System.out.println("Спор начат...");
		mAnotherOpinion.start(); 			//Запуск потока
		
		for(int i = 0; i < 5; i++)
		{
			try{
				Thread.sleep(1000);		//Приостанавливает поток на 1 секунду
			}catch(InterruptedException e){}
			
			System.out.println("курица!");
		}
		
		//Слово «курица» сказано 5 раз

		if(mAnotherOpinion.isAlive())	//Если оппонент еще не сказал последнее слово
		{
			try{
				mAnotherOpinion.join();	//Подождать пока оппонент закончит высказываться.
			}catch(InterruptedException e){}
			
			System.out.println("Первым появилось яйцо!");
		}
		else	//если оппонент уже закончил высказываться
		{
			System.out.println("Первой появилась курица!");
		}
		System.out.println("Спор закончен!");	
	}
}

Консоль:
Спор начат...
курица!
яйцо!
яйцо!
курица!
яйцо!
курица!
яйцо!
курица!
яйцо!
курица!
Первой появилась курица!
Спор закончен!

В приведенном примере два потока параллельно в течении 5 секунд выводят информацию на консоль. Точно предсказать, какой поток закончит высказываться последним, невозможно. Можно попытаться, и можно даже угадать, но есть большая вероятность того, что та же программа при следующем запуске будет иметь другого «победителя». Это происходит из-за так называемого «асинхронного выполнения кода». Асинхронность означает то, что нельзя утверждать, что какая-либо инструкция одного потока, выполнится раньше или позже инструкции другого. Или, другими словами, параллельные потоки независимы друг от друга, за исключением тех случаев, когда программист сам описывает зависимости между потоками с помощью предусмотренных для этого средств языка.

Теперь немного о завершении процессов…

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

В Java процесс завершается тогда, когда завершается последний его поток. Даже если метод main() уже завершился, но еще выполняются порожденные им потоки, система будет ждать их завершения.

Однако это правило не относится к особому виду потоков – демонам. Если завершился последний обычный поток процесса, и остались только потоки-демоны, то они будут принудительно завершены и выполнение процесса закончится. Чаще всего потоки-демоны используются для выполнения фоновых задач, обслуживающих процесс в течение его жизни.

Объявить поток демоном достаточно просто — нужно перед запуском потока вызвать его метод setDaemon(true);
Проверить, является ли поток демоном, можно вызвав его метод boolean isDaemon();

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

В Java существуют (существовали) средства для принудительного завершения потока. В частности метод Thread.stop() завершает поток незамедлительно после своего выполнения. Однако этот метод, а также Thread.suspend(), приостанавливающий поток, и Thread.resume(), продолжающий выполнение потока, были объявлены устаревшими и их использование отныне крайне нежелательно. Дело в том что поток может быть «убит» во время выполнения операции, обрыв которой на полуслове оставит некоторый объект в неправильном состоянии, что приведет к появлению трудноотлавливаемой и случайным образом возникающей ошибке.

Вместо принудительного завершения потока применяется схема, в которой каждый поток сам ответственен за своё завершение. Поток может остановиться либо тогда, когда он закончит выполнение метода run(), (main() — для главного потока) либо по сигналу из другого потока. Причем как реагировать на такой сигнал — дело, опять же, самого потока. Получив его, поток может выполнить некоторые операции и завершить выполнение, а может и вовсе его проигнорировать и продолжить выполняться. Описание реакции на сигнал завершения потока лежит на плечах программиста.

Java имеет встроенный механизм оповещения потока, который называется Interruption (прерывание, вмешательство), и скоро мы его рассмотрим, но сначала посмотрите на следующую программку:

Incremenator — поток, который каждую секунду прибавляет или вычитает единицу из значения статической переменной Program.mValue. Incremenator содержит два закрытых поля – mIsIncrement и mFinish. То, какое действие выполняется, определяется булевой переменной mIsIncrement — если оно равно true, то выполняется прибавление единицы, иначе — вычитание. А завершение потока происходит, когда значение mFinish становится равно true.

class Incremenator extends Thread
{
	//О ключевом слове volatile - чуть ниже
	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

Взаимодействовать с потоком можно с помощью метода changeAction() (для смены вычитания на сложение и наоборот) и метода finish() (для завершения потока).

В объявлении переменных mIsIncrement и mFinish было использовано ключевое слово volatile (изменчивый, не постоянный). Его необходимо использовать для переменных, которые используются разными потоками. Это связано с тем, что значение переменной, объявленной без volatile, может кэшироваться отдельно для каждого потока, и значение из этого кэша может различаться для каждого из них. Объявление переменной с ключевым словом volatile отключает для неё такое кэширование и все запросы к переменной будут направляться непосредственно в память.

В этом примере показано, каким образом можно организовать взаимодействие между потоками. Однако есть одна проблема при таком подходе к завершению потока — Incremenator проверяет значение поля mFinish раз в секунду, поэтому может пройти до секунды времени между тем, когда будет выполнен метод finish(), и фактическим завершения потока. Было бы замечательно, если бы при получении сигнала извне, метод sleep() возвращал выполнение и поток незамедлительно начинал своё завершение. Для выполнения такого сценария существует встроенное средство оповещения потока, которое называется Interruption (прерывание, вмешательство).

Interruption

Класс Thread содержит в себе скрытое булево поле, подобное полю mFinish в программе Incremenator, которое называется флагом прерывания. Установить этот флаг можно вызвав метод interrupt() потока. Проверить же, установлен ли этот флаг, можно двумя способами. Первый способ — вызвать метод bool isInterrupted() объекта потока, второй — вызвать статический метод bool Thread.interrupted(). Первый метод возвращает состояние флага прерывания и оставляет этот флаг нетронутым. Второй метод возвращает состояние флага и сбрасывает его. Заметьте что Thread.interrupted() — статический метод класса Thread, и его вызов возвращает значение флага прерывания того потока, из которого он был вызван. Поэтому этот метод вызывается только изнутри потока и позволяет потоку проверить своё состояние прерывания.

Итак, вернемся к нашей программе. Механизм прерывания позволит нам решить проблему с засыпанием потока. У методов, приостанавливающих выполнение потока, таких как sleep(), wait() и join() есть одна особенность — если во время их выполнения будет вызван метод interrupt() этого потока, они, не дожидаясь конца времени ожидания, сгенерируют исключение InterruptedException.

Переделаем программу Incremenator – теперь вместо завершения потока с помощью метода finish() будем использовать стандартный метод interrupt(). А вместо проверки флага mFinish будем вызывать метод bool Thread.interrupted();
Так будет выглядеть класс Incremenator после добавления поддержки прерываний:

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();	//Прерывание побочного потока
	}
}

Консоль:
Значение = 1 2 1 0 -1 -2 -1 0 1 2 3 4

Как видите, мы избавились от метода finish() и реализовали тот же механизм завершения потока с помощью встроенной системы прерываний. В этой реализации мы получили одно преимущество — метод sleep() вернет управление (сгенерирует исключение) незамедлительно после прерывания потока.

Заметьте что методы sleep() и join() обёрнуты в конструкции try-catch. Это необходимое условие работы этих методов. Вызывающий их код должен перехватывать исключение InterruptedException, которое они бросают при прерывании во время ожидания.

С запуском и завершением потоков разобрались, дальше я расскажу о методах, использующихся при работе с потоками.

Метод Thread.sleep()

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

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

Несмотря на то, что метод sleep() может принимать в качестве времени ожидания наносекунды, не стоит принимать это всерьез. Во многих системах время ожидания все равно округляется до миллисекунд а то и до их десятков.

Метод yield()

Статический метод Thread.yield() заставляет процессор переключиться на обработку других потоков системы. Метод может быть полезным, например, когда поток ожидает наступления какого-либо события и необходимо чтобы проверка его наступления происходила как можно чаще. В этом случае можно поместить проверку события и метод Thread.yield() в цикл:

//Ожидание поступления сообщения
while(!msgQueue.hasMessages())		//Пока в очереди нет сообщений
{
	Thread.yield();		//Передать управление другим потокам
}

Метод join()

В Java предусмотрен механизм, позволяющий одному потоку ждать завершения выполнения другого. Для этого используется метод join(). Например, чтобы главный поток подождал завершения побочного потока myThready, необходимо выполнить инструкцию myThready.join() в главном потоке. Как только поток myThready завершится, метод join() вернет управление, и главный поток сможет продолжить выполнение.

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

С помощью задания времени ожидания потока можно, например, выполнять обновление анимированной картинки пока главный (или любой другой) поток ждёт завершения побочного потока, выполняющего ресурсоёмкие операции:

Thinker brain = new Thinker(); 	//Thinker - потомок класса Thread.
brain.start();		//Начать "обдумывание".

do
{
	mThinkIndicator.refresh();		//mThinkIndicator - анимированная картинка.

	try{
		brain.join(250);				//Подождать окончания мысли четверть секунды.
	}catch(InterruptedException e){}
}
while(brain.isAlive());	//Пока brain думает...

//brain закончил думать (звучат овации).

В этом примере поток brain (мозг) думает над чем-то, и предполагается, что это занимает у него длительное время. Главный поток ждет его четверть секунды и, в случае, если этого времени на раздумье не хватило, обновляет «индикатор раздумий» (некоторая анимированная картинка). В итоге, во время раздумий, пользователь наблюдает на экране индикатор мыслительного процесса, что дает ему знать, что электронные мозги чем то заняты.

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

Каждый поток в системе имеет свой приоритет. Приоритет – это некоторое число в объекте потока, более высокое значение которого означает больший приоритет. Система в первую очередь выполняет потоки с большим приоритетом, а потоки с меньшим приоритетом получают процессорное время только тогда, когда их более привилегированные собратья простаивают.

Работать с приоритетами потока можно с помощью двух функций:

void setPriority(int priority) – устанавливает приоритет потока.
Возможные значения priority — MIN_PRIORITY, NORM_PRIORITY и MAX_PRIORITY.

int getPriority() – получает приоритет потока.

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

Это практически всё. Напоследок приведу несколько полезных методов работы с потоками.

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

setName(String threadName) – Задает имя потока.
String getName() – Получает имя потока.
Имя потока – ассоциированная с ним строка, которая в некоторых случаях помогает понять, какой поток выполняет некоторое действие. Иногда это бывает полезным.

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

long getId()– возвращает идентификатор потока. Идентификатор – уникальное число, присвоенное потоку.

Заключение

Отмечу, что в статье рассказано далеко не про все нюансы многопоточного программирования. И коду, приведенному в примерах, для полной корректности не хватает некоторых нюансов. В частности, в примерах не используется синхронизация. Синхронизация потоков — тема, не изучив которую, программировать правильные многопоточные приложения не получится. Почитать о ней вы можете, например, в книге «Java Concurrency in Practice» или здесь (всё на английском).

В статье были рассмотрены основные средства работы с потоками в Java. Если эта статья окажется полезной, то в следующей я расскажу о проблемах совместного доступа потоков к ресурсам и о методах их решения.

Всех благ.

#статьи

  • 19 окт 2022

  • 0

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

Иллюстрация: Merry Mary для Skillbox Media

Лев Сергеев

Программист, музыкант. Знает Java, C# и Unity3D, но не собирается останавливаться на достигнутом.

В многоядерных процессорах все ядра параллельно выполняют свои наборы машинных инструкций. Поэтому современные компьютеры довольно быстро справляются со сложными вычислительными задачами.

Но и одноядерную систему можно настроить так, чтобы она работала над несколькими наборами инструкций как бы одновременно — то есть переключалась между ними очень быстро и незаметно для пользователя. При этом за каждую подзадачу будет отвечать своё «виртуальное» ядро, или поток (его ещё называют thread, то есть «нить»). Это и есть многопоточность. Разберёмся, что это такое и как её настроить.

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

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

Любой процесс имеет минимум один поток, который называют главным. Он запускается в первую очередь, а остальные идут параллельно. Например, при запуске программы на Java процесс — это её среда исполнения (JRE).

Обычные программы на Java работают синхронно: строчки кода выполняются одна за другой в главном потоке. Но можно создать несколько тредов и управлять ими. Допустим, один может просто ждать, пока выполнится другой, а может в это время что-то вычислять.

Кроме «виртуальных» тредов, есть аппаратные, о которых говорилось в начале статьи. Они представляют собой «среды исполнения» программных тредов вашего кода. Когда код на Java заточен под несколько тредов, система задействует столько же реальных потоков процессора.

А если программа использует больше тредов, чем есть ядер у компьютера? Вам не стоит беспокоиться: за это отвечает планировщик ОС, который сам распределит ресурсы в угоду производительности.

Чтобы увидеть, как работает поток, выполните в режиме отладки следующий код, расставив брейкпоинты на каждой строчке:

public class Main{
    public static void main(String[] args){
        System.out.print("Hello");
        System.out.print(" ");
        System.out.print("World");
    }
}

Команды будут выполняться последовательно: сначала на экране появится слово Hello, затем пробел, а в конце — World. Это обычное поведение программы, но его можно изменить с помощью класса Thread и интерфейса Runnable из стандартной библиотеки java.lang.

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

public class Main{
    public static void main(String[] args){
        System.out.println(Thread.currentThread());
    }
}
Вывод: Thread[main,5,main]

В квадратных скобках первым параметром указано имя потока main, о котором говорилось выше. Второй параметр — это приоритет (по умолчанию он равен 5), третий — имя группы потоков.

Отдельно имя можно получить с помощью метода getName():

    System.out.println(Thread.currentThread().getName());
Вывод: main

Для этого есть класс Thread — это поток, только на уровне кода. Создать их можно сколько угодно, но одновременно будет выполняться столько, сколько поддерживает ваша система.

Интерфейс Runnable — это задача, которую выполняет поток, то есть код. Интерфейс содержит основной метод run() — в нём и находится точка входа и логика исполняемого потока.

Создать поток в Java можно двумя способами.

Первый способ:

  • Определить класс — наследник класса Thread и переопределить метод run().
  • Создать экземпляр своего класса и вызвать метод start().

class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println("Hello, I’m " + Thread.currentThread());
    }
}
public class Main{
    public static void main(String[] args){
        MyThread myThread = new MyThread();
        myThread.start();
    }
}
Вывод: Hello, I’m Thread[Thread-0,5,main]

Обратите внимание: если на экземпляре класса Thread вместо метода start() вызвать run(), то код, написанный для другого потока, отлично выполнится, но выполнит его тот же тред, который и вызвал этот метод, а новый запущен не будет! Поэтому нужно пользоваться методом start().

Второй способ:

  • Реализовать интерфейс Runnable и метод run().
  • Создать экземпляр Thread и передать в конструктор свой Runnable (экземпляр класса, реализующий этот интерфейс).

class MyThread implements Runnable{
    @Override
    public void run(){
        System.out.print("Hello, I’m " + Thread.currentThread().getName());
    }
}

public class Main{
    public static void main(String[] args){
        // Первый параметр: экземпляр Runnable
        // Второй параметр: своё имя (необязательно) 
        Thread myThread = new Thread(new MyThread(), "Leo");
        myThread.start();
    }
}
Вывод: Hello, I’m Leo

Второй вариант лучше — он более гибкий. Например, если бы MyThread уже наследовал какой-либо класс, то было бы невозможно пойти первым путём, так как Java не поддерживает множественное наследование.

Напишем небольшую консольную игру, в которой будут драться… коты. Обещаем: в процессе написания программы ни один кот не пострадает, зато вы увидите, как «нити» конкурируют между собой.

В коде будут новые ключевые слова и класс, которых вы раньше, скорее всего, не встречали:

  • synchronized перед методом означает, что он синхронизирован. Поток, вызвавший синхронизированный метод, запрещает другим нитям к нему обращаться, пока сам не выйдет из метода.
  • volatile нужен, когда одну переменную используют разные потоки, во избежание некорректных результатов.
  • Класс CopyOnWriteArrayList — это тот же ArrayList, только потокобезопасный, то есть оптимизированный под использование нескольких потоков. Он находится в библиотеке java.util.concurrent.

Программа состоит из двух классов — CatFightsConsole и Cat. Вначале создаётся N боевых «котов», а когда они запускаются, они начинают драться друг с другом. Каждый кот стремится первым вызвать метод Cat.attack(), чтобы атаковать случайного из оставшихся в живых конкурентов и отнять у него жизнь. Когда жизнь кота становится равна 0, его поток завершает свою работу. Бой идёт до последнего кота.

Класс CatFightsConsole содержит метод main(String[] args), в котором производится начальная настройка программы: создание и настройка объектов, запуск потоков. Далее главный поток натыкается на метод join(), который говорит ему остановиться на этой строчке, пока не завершится поток, на котором был вызван метод. Когда все «кототреды» завершат свою работу, в консоль выведется сообщение о последнем коте — победителе.

// Главный класс
public class CatFightsConsole {
   // Точка входа в программу, Main Thread
   public static void main(String[] args){
       // Title
       System.out.println("Cat Fights Console");

       // Создаём контейнер с котами
       List<Cat> catThreads = new ArrayList<>();
       // Жизни котов
       int life = 9;

       // Создаём и настраиваем классы-потоки котов, добавляя их в контейнер
       Collections.addAll(catThreads,
               new Cat("Tom", life, "Thread Tom"),
               new Cat("Cleocatra", life, "Thread Cleocatra"),
               new Cat("Dupli", life, "Thread Dupli"),
               new Cat("Toodles", life, "Thread Toodles"));

       // Запускаем котов
       for(Cat cat : catThreads)
           cat.getThread().start();

       // Ждём, пока завершатся все, кроме главного
       for(Cat cat : catThreads){
           try{
     // Поток, который вызвал метод join(), приостанавливается на этой строчке
               cat.getThread().join();   
               // Пока поток, на котором вызван метод, не завершит работу, Main ждёт остальных
           }catch (InterruptedException e){
               e.printStackTrace();
           }
       }

       // Последний выживший — первый элемент cats
       System.out.println(String.format("Кот-победитель: %s!!!", Cat.cats.get(0)));
   }
}

У класса Cat есть имя (String name), количество жизней (int life), личный поток и статический список cats со ссылками на все объекты Cat. Он реализует интерфейс Runnable, а значит, основной цикл работы потока происходит в методе run().

При запуске потока, пока объектов Cat более одного и пока у них есть жизни, вызывается синхронизированный статический метод Cat.attack(), который декрементирует переменную life с помощью метода decrementLife() у второго переданного в него объекта. Если после этого значение life равно нулю, то у этого же объекта вызывается метод getThread(), а на нём interrupt() — функция, прерывающая работу потока.

// Это класс «Кот»
class Cat implements Runnable{

   // Статический контейнер всех созданных «кототредов»
   // Класс CopyOnWriteArrayList — тот же ArrayList, только потокобезопасный
   public static final List<Cat> cats = new CopyOnWriteArrayList<>();

   // Имя и количество жизней
   private String name;
   private volatile int life;
   // Личный поток
   private Thread thread;

   // Конструктор: задаём параметры и добавляем объект в статический список
   public Cat(String name, int life, String threadName) {

       this.name = name;           // Имя
       this.life = life;           // Количество жизни
       Cat.cats.add(this);         // Добавляем себя в List<Cat> cats
       thread = new Thread(this, threadName);   // Создаём поток этого кота и передаём ему ссылку на себя

       System.out.println(String.format("Кот %s создан. HP: %d", this.name, this.life));
   }

   // Атака. Принимает текущего кота и кота-противника. Метод синхронизирован
   public static synchronized void attack(Cat thisCat, Cat enemyCat) {

       // Дополнительная проверка жизни — во избежание конфликта (у кота может не быть жизней)
       if (thisCat.getLife() <= 0) { return; }

       // Если противник имеет жизни
       if (enemyCat.getLife() > 0) {
           // Отнимаем жизнь противника
           enemyCat.decrementLife();
           System.out.println(String.format("Кот %s атаковал кота %s. Жизни %<s: %d", thisCat.getName(), enemyCat.getName(), enemyCat.getLife()));

           // Если противник не имеет жизней
           if (enemyCat.getLife() <= 0) {
               // Удаляем противника из списка котов
               Cat.cats.remove(enemyCat);

               System.out.println(String.format("Кот %s покидает бой.", enemyCat.getName()));
               System.out.println(String.format("Оставшиеся коты: %s", Cat.cats));
               System.out.println(String.format("%s завершает свою работу.", enemyCat.getThread().getName()));
               // interrupt() — прервать работу треда
               enemyCat.getThread().interrupt();
           }
       }
   }

   // Точка входа в поток
   @Override
   public void run() {
       System.out.println(String.format("Кот %s идёт в бой.", name));

       // Пока котов больше 1
       while (Cat.cats.size() > 1){
           // Атакуем произвольного кота из оставшихся, кроме себя
           Cat.attack(this, getRandomEnemyCat(this));
       }
   }

   // Возвращает произвольный объект Cat из cats, кроме самого себя
   private Cat getRandomEnemyCat(Cat deleteThisCat) {

       // Создаём лист-копию из основного листа cats
       List<Cat> copyCats = new ArrayList<>(Cat.cats);
       // Удаляем текущего кота, чтобы он не выпал в качестве противника
       copyCats.remove(deleteThisCat);
       // Возвращаем произвольного кота из оставшихся с помощью класса util.java.Random
       return copyCats.get(new Random().nextInt(copyCats.size()));
   }

   // Декремент жизней
   public synchronized void decrementLife() { life--; }

   // Нужен для корректного вывода 
   @Override
   public String toString() { return name; }

   // Геттеры и сеттеры
   public String getName() { return name; }
   public int getLife() { return life; }
   public Thread getThread() { return thread; }
}
 Вывод:   Оставшиеся коты: [Tom, Dupli, Toodles]
          Cleocatra завершает свою работу.
          Кот Dupli атаковал кота Toodles. Жизни Toodles: 0
          Кот Toodles покидает бой.
          Оставшиеся коты: [Tom, Dupli]
          Toodles завершает свою работу.
          Кот Dupli атаковал кота Tom. Жизни Tom: 2
          Кот Dupli атаковал кота Tom. Жизни Tom: 1
          Кот Dupli атаковал кота Tom. Жизни Tom: 0
          Кот Tom покидает бой.
          Оставшиеся коты: [Dupli]
          Tom завершает свою работу.
          Кот-победитель: Dupli!!!

          Process finished with exit code 0

Изучите код и запустите его на своём компьютере. Из-за постоянной гонки потоков результаты будут различаться при каждом запуске программы.

Посмотрите, как поведут себя потоки, если дать «котам» 1 или 100 000 жизней. Проверьте, не происходит ли ошибок в вычислениях здоровья и успешно ли завершаются треды.

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

Поток, как и любой другой объект, имеет цикл жизни: он рождается, живёт (готов к выполнению или выполняется), спит (находится в ожидании), и умирает (завершает свою работу).

Получить текущее состояние потока позволяет метод getState(). Он возвращает одно из значений перечисления State, которое содержит набор из 6 констант:

  • ‌NEW — новый, только что созданный поток. Это состояние присваивается, когда выделяется память для объекта.

Thread myThread = new Thread();

  • ‌RUNNABLE — вызывая метод start(), поток становится готовым к выполнению, а затем выполняемым.
  • ‌BLOCKED / WAITING / TIME_WAITING — данные состояния означают, что поток находится в ожидании своего выполнения.
  • ‌TERMINATED — после завершения работы поток уничтожается.

В следующем примере на экран выводится несколько состояний потока. Внимательно изучите код и комментарии к нему:

class MyThread implements Runnable{
    @Override
    public void run(){
        // Здесь прописана логика объекта
    }
}

public class Main {
    public static void main(String[] args) {
        // При создании объект имеет состояние NEW
        Thread myThread = new Thread(new MyThread());
        System.out.println(myThread.getState());

        // Нить запускается и переходит в состояние RUNNABLE
        myThread.start();
        System.out.println(myThread.getState());

        // main переходит в состояние WAITING
        try{
            myThread.join(); // main на этой строчке приостановится, чтобы подождать, пока myThread завершит свою работу в методе run(), и только потом код будет выполняться дальше
        }catch(InterruptedException e) {
             e.printStackTrace();
        }

        // Объект завершил свою работу и получил статус TERMINATED
        System.out.println(myThread.getState());

        // После выполнения всех инструкций нить main также становится TERMINATED
    }
}
Вывод: NEW
       RUNNABLE
       TERMINATED

Как говорилось ранее, когда запускается новый поток, старый продолжает работу. Вызвав метод join(), главный поток перейдёт на время в состояние WAITING, а затем снова станет RUNNABLE. По завершении работы программы все потоки перейдут в состояние TERMINATED.

Есть ещё метод isAlive(), который позволяет узнать, жив поток или нет. Он возвращает логическое значение true или false.

public class Main{
   public static void main(String[] args){
       // Главный поток сейчас живой и выполняется, поэтому выведет true
       System.out.println("main thread: " + Thread.currentThread().isAlive());

       // Новый поток создан, но ещё не запущен (не живой), поэтому вывод будет false
       System.out.println("new thread: " + new Thread().isAlive());
   }
}
Вывод: main thread: true
       new thread: false

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

  • ‌Потоки — это виртуальные сущности, которые последовательно выполняют код. Они протекают в процессах, где процесс — это программа, которая выполняется.
  • ‌Поток можно создать двумя способами: унаследовать класс Thread или реализовать интерфейс Runnable.
  • ‌Вся логика нового треда выполняется в методе run(), а запускается он методом start().
  • ‌Поток имеет свой жизненный цикл и шесть состояний, описанных в перечислении ‌State.
  • State — это свойство класса Thread, которое содержит состояния потока, а получить его можно с помощью метода getState().
  • Метод join() переводит в ожидание текущий поток, а interrupt() прерывает его работу.

Многопоточность — важная тема в программировании. Все современные системы используют много потоков для увеличения производительности, а также выполнения нескольких задач одновременно. Поэтому важно понимать, как многопоточность работает.

Научитесь: Профессия Java-разработчик
Узнать больше

Афоризм

Не знаю, кто пишет сценарий моей жизни, но у него присутствует чувство юмора.

Поддержка проекта

Если Вам сайт понравился и помог, то будем признательны за Ваш «посильный» вклад в его поддержку и развитие

 • Yandex.Деньги
  410013796724260

 • Webmoney
  R335386147728
  Z369087728698

Многопоточное программирование позволяет разделить представление и обработку информации на несколько «легковесных»
процессов (light-weight processes), имеющих общий доступ как к методам различных объектов приложения, так и к их полям.
Многопоточность незаменима в тех случаях, когда графический интерфейс должен реагировать на действия пользователя при
выполнении определенной обработки информации. Потоки могут взаимодействовать друг с другом через основной «родительский»
поток, из которого они стартованы.

В качестве примера можно привести некоторый поток, отвечающий за представление информации в интерфейсе, который ожидает
завершения работы другого потока, загружающего файл, и одновременно отображает некоторую анимацию или обновляет
прогресс-бар. Кроме того этот поток может остановить загружающий файл поток при нажатии кнопки «Отмена».

Создатели Java предоставили две возможности создания потоков: реализация (implementing) интерфейса Runnable и
расширение(extending) класса Thread. Расширение класса – это путь наследования
методов и переменных класса родителя. В этом случае можно наследоваться только от одного родительского класса Thread.
Данное ограничение внутри Java можно преодолеть реализацией интерфейса Runnable,
который является наиболее распространённым способом создания потоков.

Преимущества потоков перед процессами

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

Главный поток

Каждое java приложение имеет хотя бы один выполняющийся поток. Поток, с которого начинается выполнение программы,
называется главным. После создания процесса, как правило, JVM начинает выполнение главного потока с метода main(). Затем,
по мере необходимости, могут быть запущены дополнительные потоки. Многопоточность — это два и более потоков,
выполняющихся одновременно в одной программе. Компьютер с одноядерным процессором может выполнять только один поток,
разделяя процессорное время между различными процессами и потоками.

Класс Thread

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

Конструкторы класса Thread

Thread();
Thread(Runnable target);
Thread(Runnable target, String name);
Thread(String name);
Thread(ThreadGroup group, Runnable target);
Thread(ThreadGroup group, Runnable target, String name);
Thread(ThreadGroup group, String name);

где :

  • target – экземпляр класса реализующего интерфейс Runnable;
  • name – имя создаваемого потока;
  • group – группа к которой относится поток.

Пример создания потока, который входит в группу, реализует интерфейс Runnable и имеет свое уникальное название :

Runnable    r  = new MyClassRunnable();
ThreadGroup tg = new ThreadGroup();
Thread      t  = new Thread(tg, r, "myThread");

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

Несмотря на то, что главный поток создаётся автоматически, им можно управлять. Для этого необходимо создать объект класса
Thread вызовом метода currentThread().

Методы класса Thread

Наиболее часто используемые методы класса Thread для управления потоками :

  • long getId() – получение идентификатора потока;
  • String getName() – получение имени потока;
  • int getPriority() – получение приоритета потока;
  • State getState() – определение состояния потока;
  • void interrupt() – прерывание выполнения потока;
  • boolean isAlive() – проверка, выполняется ли поток;
  • boolean isDaemon() – проверка, является ли поток «daemon»;
  • void join() – ожидание завершения потока;
  • void join(millis) – ожидание millis милисекунд завершения потока;
  • void notify() – «пробуждение» отдельного потока, ожидающего «сигнала»;
  • void notifyAll() – «пробуждение» всех потоков, ожидающих «сигнала»;
  • void run() – запуск потока, если поток был создан с использованием интерфейса Runnable;
  • void setDaemon(bool) – определение «daemon» потока;
  • void setPriority(int) – определение приоритета потока;
  • void sleep(int) – приостановка потока на заданное время;
  • void start() – запуск потока.
  • void wait() – приостановка потока, пока другой поток не вызовет метод notify();
  • void wait(millis) – приостановка потока на millis милисекунд или пока другой поток не вызовет метод
    notify();

Жизненный цикл потока

При выполнении программы объект Thread может находиться в одном из четырех основных состояний: «новый», «работоспособный»,
«неработоспособный» и «пассивный». При создании потока он получает состояние «новый» (NEW) и не выполняется. Для перевода потока
из состояния «новый» в «работоспособный» (RUNNABLE) следует выполнить метод start(), вызывающий метод run().

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

NEW — поток создан, но еще не запущен;
RUNNABLE — поток выполняется;
BLOCKED — поток блокирован;
WAITING — поток ждет окончания работы другого потока;
TIMED_WAITING — поток некоторое время ждет окончания другого потока;
TERMINATED — поток завершен.

Пример использования Thread

В примере ChickenEgg рассматривается параллельная работа двух потоков (главный поток и поток Egg), в которых идет спор,
«что было раньше, яйцо или курица?». Каждый поток высказывает свое мнение после небольшой задержки, формируемой методом
ChickenEgg.getTimeSleep(). Побеждает тот поток, который последним говорит свое слово.

package example;

import java.util.Random;

class Egg extends Thread
{
    @Override
    public void run()
    {
        for(int i = 0; i < 5; i++) {
            try {
                // Приостанавливаем поток
                sleep(ChickenEgg.getTimeSleep());
                System.out.println("Яйцо");
            }catch(InterruptedException e){}
        }
    }
}
public class ChickenEgg
{
    public static int getTimeSleep()
    {
        final Random random = new Random();
        int tm = random.nextInt(1000);
        if (tm < 10)
            tm *= 100;
        else if (tm < 100)
            tm *= 10;
        return tm;
    }
    public static void main(String[] args)
    {
        Egg egg = new Egg (); // Создание потока
        System.out.println(
                  "Начинаем спор : кто появился первым ?");

        egg.start(); // Запуск потока
        for(int i = 0; i < 5; i++) {
            try {
                // Приостанавливаем поток
                Thread.sleep(ChickenEgg.getTimeSleep());
                System.out.println("Курица");	
            }catch(InterruptedException e){}
        }
        if(egg.isAlive()) {
            // Cказало ли яйцо последнее слово?
            try {
                // Ждем, пока яйцо закончит высказываться
                egg.join();
            } catch (InterruptedException e){}

            System.out.println("Первым появилось яйцо !!!");
        } else {
            //если оппонент уже закончил высказываться
            System.out.println("Первой появилась курица !!!");
        }
        System.out.println("Спор закончен");
    }
}

При выполнении программы в консоль было выведено следующее сообщение.

Начинаем спор : кто появился первым ?
Курица
Курица
Яйцо
Курица
Яйцо
Яйцо
Курица
Курица
Яйцо
Яйцо
Первым появилось яйцо !!!
Спор закончен
 

Невозможно точно предсказать, какой поток закончит высказываться последним. При следующем запуске «победитель»
может измениться. Это происходит вследствии так называемого «асинхронного выполнения кода». Асинхронность
обеспечивает независимость выполнения потоков. Или, другими словами, параллельные потоки независимы друг от друга,
за исключением случаев, когда бизнес-логика зависимости выполнения потоков определяется предусмотренными для этого
средств языка.

Интерфейс Runnable

Интерфейс Runnable содержит только один метод run() :

interface Runnable
{
    void run();
}

Метод run() выполняется при запуске потока. После определения объекта Runnable он передается в один из
конструкторов класса Thread.

Пример класса RunnableExample, реализующего интерфейс Runnable

package example;

class MyThread implements Runnable
{
    Thread thread;
    MyThread() {
        thread = new Thread(this, "Дополнительный поток");
        System.out.println("Создан дополнительный поток " + 
                                                 thread);
        thread.start();
    }
    @Override
    public void run() {
        try {
            for (int i = 5; i > 0; i--) {
                System.out.println(
                           "tдополнительный поток: " + i);
                Thread.sleep(500);
            }
        } catch (InterruptedException e) {
            System.out.println(
                         "tдополнительный поток прерван");
        }
        System.out.println(
                        "tдополнительный поток завершён");
    }
}
public class RunnableExample
{
    public static void main(String[] args)
    {
        new MyThread();
        try {
            for (int i = 5; i > 0; i--) {
                System.out.println("Главный поток: " + i);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            System.out.println("Главный поток прерван");
        }
        System.out.println("Главный поток завершён");
    }
}

При выполнении программы в консоль было выведено следующее сообщение.

Создан дополнительный поток Thread[Дополнительный поток,5,main]
Главный поток: 5
	дополнительный поток: 5
	дополнительный поток: 4
Главный поток: 4
	дополнительный поток: 3
	дополнительный поток: 2
Главный поток: 3
	дополнительный поток: 1
	дополнительный поток завершён
Главный поток: 2
Главный поток: 1
Главный поток завершён
 

Синхронизация потоков, synchronized

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

package example;

class CommonObject
{
    int counter = 0;
}

class CounterThread implements Runnable
{
    CommonObject res;
    CounterThread(CommonObject res)
    {
        this.res = res;
    }
    @Override
    public void run()
    {
//      synchronized(res) {
            res.counter = 1;
            for (int i = 1; i < 5; i++){
                System.out.printf("'%s' - %dn",
                        Thread.currentThread().getName(),
                                            res.counter);
                res.counter++;
                try {
                    Thread.sleep(100);
                }
                catch(InterruptedException e){}
            }
//      }
    }
}
public class SynchronizedThread
{
    public static void main(String[] args) {
        CommonObject commonObject= new CommonObject();
        for (int i = 1; i < 6; i++) {
            Thread t;
            t = new Thread(new CounterThread(commonObject));
            t.setName("Поток " + i);
            t.start();
        }
    }
}

В примере определен общий ресурс в виде класса CommonObject, в котором имеется целочисленное поле counter. Данный ресурс
используется внутренним классом, создающим поток CounterThread для увеличения в цикле значения counter на
единицу. При старте потока полю counter присваивается значение 1. После завершения работы потока значение res.counter должно
быть равно 4.

Две строчки кода класса CounterThread закомментированы. О них речь пойдет ниже.

В главном классе программы SynchronizedThread.main запускается пять потоков. То есть, каждый поток должен в цикле
увеличить значение res.counter с единицы до четырех; и так пять раз. Но результат работы программы, отображаемый в консоли,
будет иным :

'Поток 4' - 1
'Поток 2' - 1
'Поток 1' - 1
'Поток 5' - 1
'Поток 3' - 1
'Поток 2' - 6
'Поток 4' - 7
'Поток 3' - 8
'Поток 5' - 9
'Поток 1' - 10
'Поток 2' - 11
'Поток 4' - 12
'Поток 5' - 13
'Поток 3' - 13
'Поток 1' - 15
'Поток 4' - 16
'Поток 2' - 16
'Поток 3' - 18
'Поток 5' - 18
'Поток 1' - 20
 

То есть, с общим ресурсов res.counter работают все потоки одновременно, поочередно изменяя значение.

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

Блокировка на уровне объекта

Блокировать общий ресурс можно на уровне объекта, но нельзя использовать для этих целей примитивные типы. В примере следует
удалить строчные комментарии в классе CounterThread, после чего общий ресурс будет блокироваться как только его захватит один
из потоков; остальные потоки будут ждать в очереди освобождения ресурса. Результат работы программы при синхронизации доступа
к общему ресурсу резко изменится :

'Поток 1' - 1
'Поток 1' - 2
'Поток 1' - 3
'Поток 1' - 4
'Поток 5' - 1
'Поток 5' - 2
'Поток 5' - 3
'Поток 5' - 4
'Поток 4' - 1
'Поток 4' - 2
'Поток 4' - 3
'Поток 4' - 4
'Поток 3' - 1
'Поток 3' - 2
'Поток 3' - 3
'Поток 3' - 4
'Поток 2' - 1
'Поток 2' - 2
'Поток 2' - 3
'Поток 2' - 4
 

Следующий код демонстрирует порядок использования оператора synchronized для блокирования доступа к объекту.

synchronized (оbject) {
    // other thread safe code
}

Блокировка на уровне метода и класса

Блокировать доступ к ресурсам можно на уровне метода и класса. Следующий код показывает, что если во время выполнения
программы имеется несколько экземпляров класса DemoClass, то только один поток может выполнить метод demoMethod(), для других
потоков доступ к методу будет заблокирован. Это необходимо когда требуется сделать определенные ресурсы потокобезопасными.

public class DemoClass
{
    public synchronized static void demoMethod(){
        // ...
    }
}
// или
public class DemoClass
{
    public void demoMethod(){
        synchronized (DemoClass.class) {
            // ...
        }
    }
}

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

Некоторые важные замечания использования synchronized

  1. Синхронизация в Java гарантирует, что два потока не могут выполнить синхронизированный метод одновременно.
  2. Оператор synchronized можно использовать только с методами и блоками кода, которые могут быть как
    статическими, так и не статическими.
  3. Если один из потоков начинает выполнять синхронизированный метод или блок, то этот метод/блок блокируются.
    Когда поток выходит из синхронизированного метода или блока JVM снимает блокировку. Блокировка снимается,
    даже если поток покидает синхронизированный метод после завершения из-за каких-либо ошибок или исключений.
  4. Синхронизация в Java вызывает исключение NullPointerException, если объект, используемый в синхронизированном
    блоке, не определен, т.е. равен null.
  5. Синхронизированные методы в Java вносят дополнительные затраты на производительность приложения. Поэтому
    следует использовать синхронизацию, когда она абсолютно необходима.
  6. В соответствии со спецификацией языка нельзя использовать synchronized в конструкторе, т.к. приведет
    к ошибке компиляции.

Примечание : для синхронизации потоков можно использовать объекты синхронизации
Synchroniser’s пакета java.util.concurrent.

Взаимная блокировка

С использованием блокировок необходимо быть очень внимательным, чтобы не создать «взаимоблокировку», которая
хорошо известна разработчикам. Этот термин означает, что один из потоков ждет от другого освобождения заблокированного
им ресурса, в то время как сам также заблокировал один из ресурсов, доступа к которому ждёт второй поток. В данном
процессе могут участвовать два и более потоков.

Основные условия возникновения взаимоблокировок в многопотоковом приложении :

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

Взаимодействие между потоками в Java, wait и notify

При взаимодействии потоков часто возникает необходимость приостановки одних потоков и их последующего извещения
о завершении определенных действий в других потоков. Так например, действия первого потока зависят от результата
действий второго потока, и надо каким-то образом известить первый поток, что второй поток произвел/завершил
определенную работу. Для подобных ситуаций используются методы :

  • wait() – освобождает монитор и переводит вызывающий поток в состояние ожидания до тех пор, пока другой поток
    не вызовет метод notify();
  • notify() – продолжает работу потока, у которого ранее был вызван метод wait();
  • notifyAll() – возобновляет работу всех потоков, у которых ранее был вызван метод wait().

Все эти методы вызываются только из синхронизированного контекста (синхронизированного блока или метода).

Рассмотрим пример «Производитель-Склад-Потребитель» (Producer-Store-Consumer). Пока производитель не поставит на
склад продукт, потребитель не может его забрать. Допустим производитель должен поставить 5 единиц определенного товара.
Соответственно потребитель должен весь товар получить. Но, при этом, одновременно на складе может находиться не более
3 единиц товара. При реализации данного примера используем методы wait() и notify().

Листинг класса Store

package example;

public class Store
{
    private int counter = 0;
    public synchronized void get()
    {
        while (counter < 1) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        counter--;
        System.out.println("-1 : товар забрали");
        System.out.println(
             "tколичество товара на складе : " + counter);
        notify();
    }
    public synchronized void put() {
        while (counter >= 3) {
            try {
                wait();
            }catch (InterruptedException e) {} 
        }
        counter++;
        System.out.println("+1 : товар добавили");
        System.out.println(
             "tколичество товара на складе : " + counter);
        notify();
    }
}

Класс Store содержит два синхронизированных метода для получения товара get() и для добавления товара
put(). При получении товара выполняется проверка счетчика counter. Если на складе товара нет, то есть counter
< 1, то вызывается метод wait(), который освобождает монитор объекта Store и блокирует выполнение метода
get(), пока для этого монитора не будет вызван метод notify().

При добавлении товара также выполняется проверка количества товара на складе. Если на складе больше 3 единиц товара,
то поставка товара приостанавливается и вызывается метод notify(), который передает управление методу get()
для завершения цикла while().

Листинги классов Producer и Consumer

Классы Producer и Consumer реализуют интерфейс Runnable, методы run() у них переопределены. Конструкторы
этих классов в качестве параметра получают объект склад Store. При старте данных объектов в виде отдельных потоков в
цикле вызываются методы put() и get() класса Store для «добавления» и «получения» товара.

package example;

public class Producer implements Runnable
{
    Store store;    
    Producer(Store store) {
       this.store=store; 
    }
    @Override
    public void run() {
        for (int i = 1; i < 6; i++) {
            store.put();
        }
    }
}

public class Consumer implements Runnable
{
    Store store;
    Consumer(Store store) {
        this.store=store; 
    }
    @Override
    public void run(){
        for (int i = 1; i < 6; i++) {
            store.get();
        }
    }
}

Листинг класса Trade

В главном потоке класса Trade (в методе main) создаются объекты Producer-Store-Consumer и стартуются потоки
производителя и потребителя.

package example;

public class Trade
{
    public static void main(String[] args) 
    {
        Store     store    = new Store();
        Producer  producer = new Producer(store);
        Consumer  consumer = new Consumer(store);
        new Thread(producer).start();
        new Thread(consumer).start();
    }
}

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

+1 : товар добавили
	количество товара на складе : 1
+1 : товар добавили
	количество товара на складе : 2
+1 : товар добавили
	количество товара на складе : 3
-1 : товар забрали
	количество товара на складе : 2
-1 : товар забрали
	количество товара на складе : 1
-1 : товар забрали
	количество товара на складе : 0
+1 : товар добавили
	количество товара на складе : 1
+1 : товар добавили
	количество товара на складе : 2
-1 : товар забрали
	количество товара на складе : 1
-1 : товар забрали
	количество товара на складе : 0
 	

Поток-демон, daemon

Java приложение завершает работу тогда, когда завершает работу последний его поток. Даже если метод main() уже
завершился, но еще выполняются порожденные им потоки, система будет ждать их завершения.

Однако это правило не относится к потоков-демонам (daemon). Если завершился последний обычный поток процесса,
и остались только daemon потоки, то они будут принудительно завершены и выполнение приложения закончится. Чаще
всего daemon потоки используются для выполнения фоновых задач, обслуживающих процесс в течение его жизни.

Объявить поток демоном достаточно просто. Для этого нужно перед запуском потока вызвать его метод setDaemon(true).
Проверить, является ли поток daemon‘ом можно вызовом метода isDaemon(). В качестве примера использования
daemon-потока можно рассмотреть класс Trade, который принял бы следующий вид :

package example;

public class Trade
{
    public static void main(String[] args) 
    {
        Producer  producer = new Producer(store);
        Consumer  consumer = new Consumer(store);
		
//		new Thread(producer).start();
//		new Thread(consumer).start();
		
        Thread  tp = new Thread(producer);
        Thread  tc = new Thread(consumer);

        tp.setDaemon(true);
        tc.setDaemon(true);
		
        tp.start();
        tc.start();

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {}
		
        System.out.println("nГлавный поток завершенn");
        System.exit(0);
    }
}

Здесь можно самостоятельно поэкспериментировать с определением daemon-потока для одного из классов (producer,
consumer) или обоих классов, и посмотреть, как система (JVM) будет вести себя.

Thread и Runnable, что выбрать ?

Зачем нужно два вида реализации многопоточности; какую из них и когда использовать? Ответ несложен. Реализация
интерфейса Runnable используется в случаях, когда класс уже наследует какой-либо родительский класс и не
позволяет расширить класс Thread. К тому же хорошим тоном программирования в java считается реализация
интерфейсов. Это связано с тем, что в java может наследоваться только один родительский класс. Таким образом,
унаследовав класс Thread, невозможно наследовать какой-либо другой класс.

Расширение класса Thread целесообразно использовать в случае необходимости переопределения других методов
класса помимо метода run().

Приоритеты выполнения и голодание

Иногда разработчики используют приоритеты выполнения потока. В Java есть планировщик потоков (Thread Scheduler),
который контролирует все запущенные потоки и решает, какие потоки должны быть запущены и какая строка кода должна
выполняться. Решение основывается на приоритете потока. Поэтому потоки с меньшим приоритетом получают меньше процессорного
времени по сравнению с потоками с бо́льшим приоритет. Данное разумное решением может стать причиной проблем при
злоупотреблении. То есть, если бо́льшую часть времени исполняются потоки с высоким приоритетом, то низкоприоритетные
потоки начинают «голодать», поскольку не получают достаточно времени для того, чтобы выполнить свою работу должным образом.
Поэтому рекомендуется задавать приоритет потока только тогда, когда для этого имеются веские основания.

Неочевидный пример «голодания» потока даёт метод finalize(), предоставляющий возможность выполнить код перед тем,
как объект будет удалён сборщиком мусора. Однако приоритет финализирующего потока невысокий. Следовательно, возникают
предпосылки для потокового голодания, когда методы finalize() объекта тратят слишком много времени (большие задержки)
по сравнению с остальным кодом.

Другая проблема со временем исполнения может возникнуть от того, что не был определен порядок прохождения потоком блока
synchronized. Когда несколько параллельных потоков должны выполнить некоторый код, оформленный блоком
synchronized, может получиться так, что одним потокам придётся ждать дольше других, прежде чем войти в блок.
Теоретически они могут вообще туда не попасть.

Скачать примеры

Рассмотренные на странице примеры многопоточности и синхронизации потоков в виде проекта Eclipse можно скачать
здесь (14Кб).

Глава 11

Многопоточное программирование

Основные навыки и понятия

  • Общее представление о многопоточной обработке
  • Класс Thread и интерфейс Runnable
  • Создание потока
  • Создание нескольких потоков
  • Определение момента завершения потока
  • Использование приоритетов потоков
  • Представление о синхронизации потоков
  • Применение синхронизированных блоков
  • Взаимодействие потоков
  • Приостановка, возобновление и остановка потоков

Среди многих замечательных свойств языка Java особое место принадлежит поддержке многопоточного программирования. Многопоточная программа состоит из двух или более частей, выполняемых параллельно. Каждая часть такой программы называется потоком и определяет отдельный путь выполнения команд. Таким образом, многопоточная обработка является особой формой многозадачности.

Общее представление о многопоточной обработке

Различают две разновидности многозадачности: на основе процессов и на основе потоков. В связи с этим важно понимать отличия между ними. Процесс фактически представляет собой исполняемую программу. Поэтому многозадачность на основе процессов — это средство, благодаря которому на компьютере могут параллельно выполняться две программы или более. Так, многозадачность на основе процессов позволяет одновременно выполнять программы текстового редактора, электронных таблиц и просмотра содержимого в Интернете. При организации многозадачности на основе процессов программа является наименьшей единицей кода, выполнение которой может координировать планировщик задач.

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

Главное преимущество многопоточной обработки заключается в том, что она позволяет писать программы, которые работают очень эффективно благодаря возможности выгодно использовать время простоя, неизбежно возникающее в ходе выполнения большинства программ. Как известно, большинство устройств ввода-вывода, будь то устройства, подключенные к сетевым портам, накопители на дисках или клавиатура, работают намного медленнее, чем центральный процессор (ЦП). Поэтому большую часть своего времени программе приходится ожидать отправки данных на устройство ввода-вывода или приема информации из него. А благодаря многопоточной обработке программа может решать какую-нибудь другую задачу во время вынужденного простоя. Например, в то время как одна часть программы отправляет файл через соединение с Интернетом, другая ее часть может выполнять чтение текстовой информации, вводимой с клавиатуры, а третья — осуществлять буферизацию очередного блока отправляемых данных.

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

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

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

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

Класс Thread и интерфейс Runnable

В основу системы многопоточной обработки в Java положены класс Thread и интерфейс Runnable, входящие в пакет java. lang. Класс Thread инкапсулирует поток исполнения. Для того чтобы образовать новый поток, нужно создать класс, являющийся подклассом Thread или реализующий интерфейс Runnable.

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

Метод Описание
final String getName() Получает имя потока
final int getPriority() Получает приоритет потока
final boolean isAliveO Определяет, выполняется ли поток
final void join() Ожидает завершения потока
void run() Определяет точку входа в поток
static void sleep(long миллисекунд) Приостанавливает исполнение потока на указанное число миллисекунд
void start() Запускает поток, вызывая его метод run()

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

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

Создание потока

Для того чтобы создать поток, нужно построить объект типа Thread. Класс Thread инкапсулирует объект, который может стать исполняемым. Как пояснялось ранее, пригодные для исполнения объекты можно создавать в Java двумя способами:

  • реализуя интерфейс Runnable;
  • создавая подкласс класса Thread.

В большинстве примеров, представленных в этой главе, будет применяться первый способ. Хотя в примере для опробования 11.1 будет продемонстрировано, каким образом поток реализуется путем расширения класса Thread. Но независимо от выбранного способа создание экземпляра потока, организация доступа к нему и управление потоком осуществляется средствами класса Thread. Единственное отличие обоих способов состоит в том, как создается класс, активизирующий поток.

Интерфейс Runnable дает абстрактное описание единицы исполняемого кода. Для формирования потока подходит любой объект, реализующий этот интерфейс. В интерфейсе Runnable объявлен только один метод, run():

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

После создания класса, реализующего интерфейс Runnable, следует создать экземпляр объекта типа Thread на основе объекта данного класса. В классе Thread определен ряд конструкторов. В дальнейшем будет использоваться следующий конструктор:

Thread(Runnable threadOb)

В качестве параметра threadOb этому конструктору передается экземпляр класса, реализующего интерфейс Runnable. Благодаря этому определяется место для исполнения потока.

Созданный поток не начнет исполнение до тех пор, пока не будет вызван метод start(), объявленный в классе Thread. По существу, единственным назначением метода start() является вызов метода run(). А объявляется метод start() следующим образом:

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

// Создание потока путем реализации интерфейса Runnable,
class MyThread implements Runnable {
    String thrdName;

    // Объекты типа MyThread выполняются в отдельных потоках, так как
    // класс MyThread реализует интерфейс Runnable.
    MyThread(String name) {
        thrdName = name;
    }

    // Точка входа в поток,
    public void run() {
        // Здесь начинают исполняться потоки.
        System.out.println(thrdName + " starting.");
        try {
            for(int count=0; count < 10; count++) {
                Thread.sleep(400);
                System.out.println("In " + thrdName +
                                   ", count is " + count);
            }
        }
        catch(InterruptedException exc) {
            System.out.println(thrdName + " interrupted.");
        }
        System.out.println(thrdName + " terminating.");
    }
}

class UseThreads {
    public static void main(String args[]) {
        System.out.println("Main thread starting.");
        // сначала построить объект типа MyThread
        MyThread mt = new MyThread("Child #1"); // Создание исполняемого объекта.
        // далее сформировать поток из этого объекта
        Thread newThrd = new Thread(mt); // Формирование потока из этого объекта.
        // и, наконец, начать исполнение потока

        newThrd.start О; // Начало исполнения потока.
        for(int i=0; i<50; i++) {
            System.out.print(".") ;
            try {
                Thread.sleep(100) ;
            }
            catch(InterruptedException exc) {
                System.out.println("Main thread interrupted.");
            }
        }
        System.out.println("Main thread ending.");
    }
}

Рассмотрим исходный код приведенной выше программы более подробно. Как видите, класс MyThread реализует интерфейс Runnable. Это означает, что объект типа MyThread подходит для использования в качестве потока, а следовательно, его можно передать конструктору класса Thread.

В теле метода run() присутствует цикл, в котором производится отсчет от 0 до 9. Обратите внимание на вызов метода sleep(). Этот метод приостанавливает поток, из которого он был вызван на указанное число миллисекунд. Ниже приведена общая форма объявления данного метода.

static void sleep(long миллисекунд) throws InterruptedException

Единственный параметр метода sleep() задает время задержки, определяемое числом миллисекунд. Как следует из объявления этого метода, в нем может быть сгенерировано исключение InterruptedException. Следовательно, его нужно вызывать в блоке try. Имеется и другой вариант метода sleep(), позволяющий точнее указывать время задержки в миллисекундах и дополнительно в наносекундах. Когда метод sleep() вызывается в методе run(), исполнение потока приостанавливается на 400 миллисекунд на каждом шаге цикла. Благодаря этому поток исполняется достаточно медленно, чтобы можно проследить за ним.

В методе main() создается новый объект типа Thread. Для этой цели служит приведенная ниже последовательность операторов.

// сначала построить объект типа MyThread
MyThread mt = new MyThread("Child #1");
// далее сформировать поток из этого объекта
Thread newThrd = new Thread(mt);
// и, наконец, начать исполнение потока
newThrd.start();

Как видите, сначала создается объект типа MyThread, а затем он используется для построения объекта типа Thread. Его можно передать конструктору класса Thread в качестве параметра, поскольку класс MyThread реализует интерфейс Runnable. И наконец, начинается исполнение нового потока, для чего вызывается метод start(), что приводит к вызову метода run() из порожденного потока. После вызова метода start() управление возвращается к методу main(), где начинается выполнение цикла for. Этот цикл повторяется 50 раз, приостанавливая на 100 миллисекунд исполнение потока на каждом своем шаге. Оба потока продолжают исполняться, разделяя ресурсы

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

Main thread starting.
.Child #1 starting.
....In Child #1, count is 0
.....In Child #1, count is 1
.....In Child #1, count is 2
.....In Child #1, count is 3
.....In Child #1, count is 4
.....In Child #1, count is 5
.....In Child #b count is 6
.....In Child #1, count is 7
.....In Child #1, count is 8
.....In Child #1 count is 9
Child #1 terminating.
    Main thread ending

В рассматриваемом здесь первом примере организации многопоточной обработки любопытно также отметить следующее обстоятельство: для демонстрации того факта, что основной и порожденный потоки исполняются одновременно, необходимо задержать завершение метода main() до тех пор, пока не окончится порожденный поток mt. В данном примере это достигается благодаря отличиям во временных характеристиках обоих потоков. Вызовы метода sleep() из цикла for в методе main() приводят в итоге к задержке на 5 секунд (50 шагов цикла х 100 миллисекунд), тогда как общая задержка с помощью того же самого метода в аналогичном цикле в методе run() составляет лишь 4 секунды (10 шагов цикла х 400 миллисекунд). Поэтому метод run() завершится приблизительно на 1 секунду раньше, чем метод main(). В итоге основной и порожденный потоки будут выполняться параллельно до тех пор, пока не завершится порожденный поток mt. А приблизительно через одну секунду завершится и основной поток в методе main().

Отличий во временнь/х характеристиках обоих потоков в данном и ряде последующих простых примеров оказывается достаточно для того, чтобы основной поток в методе main() завершился последним, но на практике этого, как правило, оказывается недостаточно. В Java предоставляются намного более совершенные способы, позволяющие организовать ожидание завершения потока. Далее в этой главе будет продемонстрирован более совершенный способ организации ожидания одним потоком завершения другого.

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

Несложные усовершенствования многопоточной программы

Рассмотренная выше многопоточная программа вполне работоспособна, тем не менее ей не помешает небольшая доработка, повышающая ее эффективность. Во- первых, можно сделать так, чтобы поток начинал исполняться сразу после создания. Эта цель достигается созданием экземпляра объекта типа Thread в конструкторе класса MyThread. И во-вторых, нет никакой нужды хранить в объекте типа MyThread имя потока, но присвоить имя потоку при его создании. Эту задачу позволяет решить следующий вариант конструктора Thread:

Thread(Runnable threadOb, String имя)

где имя обозначает конкретное наименование потока.

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

В приведенной ниже программе имя присваивается потоку после его создания с помощью метода setName(). И хотя в этом нет особой необходимости, такое решение выбрано лишь для того, чтобы продемонстрировать возможности класса Thread. Объявление метода setName() имеет следующий вид:

final void setName(String имя_потока)

где имя_потока обозначает имя, которое присваивается потоку.

Ниже приведена видоизмененная версия предыдущей программы.

// Видоизменение класса MyThread.
class MyThread implements Runnable {
    Thread thrd; // В этой переменной хранится ссылка на поток.

    // построить новый поток
    MyThread(String name) {
        thrd = new Thread(this, name); // Поток именуется при его создании,
        thrd.start() ; // Начало исполнения потока.
    }

    // начать исполнение нового потока
    public void run()   {
        System.out.println(thrd.getName() + " starting.");
        try {
            for (int count=0; countclO; count++) {
                Thread.sleep(400);
                System.out.println("In " + thrd.getName() +
                                   ", count is " + count)';
            }
        }
        catch(InterruptedException exc) {
            System.out.println(thrd.getName() + " interrupted.");
        }
        System.out.println(thrd.getName() + " terminating.");
    }
}

class UseThreadsImproved {
    public static void main(String args[]) {
        System.out.println("Main thread starting.");

        // Теперь поток начинается при его создании.
        MyThread mt = new MyThread("Child #1");

        for (int i=0; i < 50; i++) {
            System.out.print(".") ;
            try {
                Thread.sleep(100) ;
            }
            catch(InterruptedException exc) {
                System.out.println("Main thread interrupted.");
            }
        }
        System.out.println("Main thread ending.");
    }
}

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

Пример для опробования 11.1.
Расширение класса Thread

Реализация интерфейса Runnable — это лишь один из способов получения экземпляров потоковых объектов. Другой способ состоит в создании подкласса, производного от класса Thread. В этом проекте будет продемонстрировано, каким образом расширение класса Thread позволяет реализовать такие же функциональные возможности, как и у рассмотренной выше программы UseThreadsImproved.

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

Последовательность действий

  1. Создайте файл ExtendThread.java. Скопируйте в этот файл исходный код второго рассмотренного ранее примера программы (файл UseThreadsImproved. java).
  2. Измените объявление класса MyThread. Теперь он должен быть подклассом, производным от класса Thread, как показано ниже.
    class MyThread extends Thread {
    
  3. Удалите следующую строку кода:

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

  4. Внесите в конструктор класса Thread следующие изменения:
    // построить новый поток.
    MyThread(String name) {
        super(name); // присвоить потоку имя
        start(); // начать поток
    }
    

    Как видите, в данном конструкторе присутствует ключевое слово super, которое используется для вызова следующего варианта конструктора Thread:

    где имя обозначает присваиваемое потоку конкретное имя.

  5. Внесите приведенные ниже изменения в метод run(), чтобы он вызывал метод getName() непосредственно, не предваряя его именем переменной thrd.
    // начать исполнение нового метода
    public void run()  {
        System.out.println(getName() + " starting.");
        try {
            for(int count=0; count < 10; count++) {
                Thread.sleep(400);
                System.out.println("In " + getName() +
                                   ", count is " + count);
            }
        }
        catch(InterruptedException exc) {
            System.out.println(getName() + " interrupted.");
        }
    
        System.out.println(getName() + " terminating.");
    }
    
  6. Ниже приведен весь исходный код программы, в которой вместо реализации интерфейса Runnable используется подкласс, производный от класса Thread. Выполнение этой программы дает такой же результат, как и предыдущие ее версии.
    /*
    Пример для опробования 11.1.
    Расширение класса Thread.
    */
    class MyThread extends Thread {
        // построить новый поток
        MyThread(String name) {
            super(name); // присвоить потоку  имя
            start(); // начать поток
        }
    
        // начать исполнение нового потока
        public void run() {
            System.out.println(getName() + " starting.");
            try {
                for(int count=0; count < 10; count++) {
                    Thread.sleep(400);
                    System.out.println("In " + getName() +
                                       ", count is " + count);
                }
            }
            catch(InterruptedException exc) {
                System.out.println(getName() + " interrupted.");
            }
            System.out.println(getName() + " terminating.");
        }
    }
    
    class ExtendThread {
        public static void main(String args[])  {
            System.out.println("Main thread starting.");
            MyThread mt = new MyThread("Child #1");
            for(int i=0; i < 50; i++) {
                System.out.print(".");
                try {
                    Thread.sleep(100);
                }
                catch(InterruptedException exc) {
                    System.out.println("Main thread interrupted.");
                }
            }
            System.out.println("Main thread ending.");
        }
    }
    

Создание нескольких потоков

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

// Создание нескольких потоков.
class MyThread implements Runnable {
    Thread thrd;
    // построить новый поток
    MyThread(String name) {
        thrd = new Thread(this, name);

        thrd.start(); // начать поток
    }
    // начать исполнение нового потока
    public void run() {
        System.out.println(thrd.getName() + " starting.");
        try {
            for(int count=0; count < 10; count++) {
                Thread.sleep(400);
                System.out.println("In " + thrd.getName() +
                                   ", count is " + count);
            }
        }
        catch(InterruptedException exc) {
            System.out.println(thrd.getName() + " interrupted.");
        }
        System.out.println(thrd.getName() + " terminating.");
    }
}

class MoreThreads {
    public static void main(String args[]) {
        System.out.println("Main thread starting.");

        // Создание и запуск на исполнение трех потоков.
        MyThread mtl = new MyThread("Child #1");
        MyThread mt2 = new MyThread("Child #2");
        MyThread mt3 = new MyThread("Child #3");

        for (int i=0;   i   < 50; i++) {
            System.out.print(".");
            try {
                Thread.sleep(100);
            }
            catch(InterruptedException exc) {
                System.out.println("Main thread interrupted.");
            }
        }
        System.out.println("Main thread ending.");
    }
}

Ниже приведен результат выполнения данной программы.

Main thread starting.
Child #1 starting.
.Child #2 starting.
Child #3 starting.
...In Child #3, count is О
In Child #2, count is 0
In Child #1, count is 0
....In Child #1, count is 1
In Child #2, count is 1
In Child #3, count is 1
....In Child #2, count is 2
In Child #3, count is 2
In Child #1, count is 2
...In Child #1, count is 3
In Child #2, count is 3
In Child #3, count is 3
....In Child #1, count is 4
In Child #3, count is 4
In Child #2, count is 4
....In Child #1, count is 5
In Child #3, count is 5
In Child #2, count is 5
...In Child #3, count is 6
.In Child #2, count is 6
In Child #1, count is 6
...In Child #3, count is 7
In Child #1, count is 7
In Child #2, count is 7
....In Child #2, count is 8
In Child #1, count is 8
In Child #3, count is 8
....In Child #1, count is 9
Child #1 terminating.
In Child #2, count is 9
Child #2 terminating.
In Child #3, count is 9
Child #3 terminating.
    Main thread ending.

Как видите, после запуска на исполнение все три потока совместно используют ресурсы ЦП. Следует иметь в виду, что потоки в данном примере запускаются на исполнение в том порядке, в каком они были созданы. Но так происходит не всегда. Исполняющая система Java сама планирует исполнение потоков. Вследствие отличий в вычислительных средах у вас может получиться несколько иной результат.

Определение момента завершения потока

Нередко требуется знать, когда завершится поток. Так, в приведенных выше примерах ради большей наглядности нужно было поддерживать основной поток действующим до тех пор, пока не завершатся остальные потоки. Для этой цели основной поток переводился в состояние ожидания на более продолжительное время, чем порожденные им потоки. Но такое решение вряд ли можно считать удовлетворительным или общеупотребительным!

Правда, в классе Thread предусмотрены два средства, позволяющие определить, завершился ли поток. Первым из них является метод is Alive(), объявление которого приведено ниже.

Этот метод возвращает логическое значение true, если поток, для которого он вызывается, все еще исполняется. В противном случае он возвращает логическое значение false. Для того чтобы опробовать метод isAlive() на практике, замените в предыдущей программе класс MoreThreads новой версией, исходный код которой приведен ниже.

// Применение метода isAlive().
class MoreThreads {
    public static void main(String args[])  {
        System.out.println("Main thread starting.");

        MyThread mtl = new MyThread("Child #1");
        MyThread mt2 = new MyThread("Child #2");
        MyThread mt3 = new MyThread("Child #3");

        do {
            System.out.print(" . ") ;
            try {
                Thread.sleep(100);
            }
            catch(InterruptedException exc) {
                System.out.println("Main thread interrupted.");
            }
            // Ожидание завершения потоков.
        } while (mtl.thrd.isAlive() ||
                 mt2.thrd.isAlive() ||
                 mt3.thrd.isAlive());
        System.out.println("Main thread ending.");
    }
}

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

final void join() throws InterruptedException

Этот метод ожидает завершения потока, для которого он был вызван. Его имя join выбрано потому, что вызывающий поток ожидает, когда указанный поток присоединится (англ.уши) к нему. Имеется и другой вариант метода j oin(), позволяющий указать максимальное время ожидания момента, когда поток завершится.

В приведенном ниже примере программы наличие метода join() гарантирует, что основной поток завершит работу последним.

// Применение метода join().
class MyThread implements Runnable {
    Thread thrd;

    // построить новый поток
    MyThread(String name) {
        thrd = new Thread(this, name);
        thrd.start(); // начать поток
    }

    // начать исполнение нового потока
    public void run() {
        System.out.println(thrd.getName() + " starting.");
        try {
            for(int count=0; count < 10; count++) {
                Thread.sleep(400);
                System.out.println("In " + thrd.getName() +
                                   ", count is " + count);
            }
        }
        catch(InterruptedException exc) {
            System.out.println(thrd.getName() + " interrupted.");
        }
        System.out.println(thrd.getName() + " terminating.");
    }
}

class JoinThreads {
    public static void main(String args[]) {
        System.out.println("Main thread starting.");

        MyThread mtl = new MyThread("Child #1");
        MyThread mt2 = new MyThread("Child #2");
        MyThread mt3 = new MyThread("Child #3");

        try {
            // Ожидание до тех nop, пока указанный метод не завершится.
            mtl.thrd.join();
            System.out.println("Child #1 joined.");
            mt2.thrd.join() ;
            System.out.println("Child #2 joined.");
            mt3.thrd.join();
            System.out.println("Child #3 joined.");
        }
        catch(InterruptedException exc) {
            System.out.println("Main thread interrupted.");
        }
        System.out.println("Main thread ending.");
    }
}

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

Main thread starting.
Child #1 starting.
Child #2 starting.
Child #3 starting.
In Child #2, count is 0
In Child #1, count is 0
In Child #3, count is 0
In Child #2, count is 1
In Child #3, count is 1
In Child #1, count is 1
In Child #2, count is 2
In Child #1, count is 2
In Child #3, count is 2
In Child #2, count is 3
In Child #3, count is 3
In Child#1, count is 3
In Child #3, count is 4
In Child #2, count is 4
In Child #1, count is 4
In Child #3, count is 5
In Child #1, count is 5
In Child #2, count is 5
In Child #3, count is 6
In Child #2, count is 6
In Child #1, count is 6
In Child #3, count is 7
In Child #1, count is 7
In Child #2, count is 7
In Child #3, count is 8
In Child #2, count is 8
In Child #1, count is 8
In Child #3, count is 9
Child #3 terminating.
In Child #2, count is 9
Child #2 terminating.
In Child #1, count is 9
Child #1 terminating.
Child #1 joined.
Child #2 joined.
Child #3 joined.
Main thread ending.

Как видите, после того как вызываемый метод j oin() возвращает управление, исполнение потока прекращается.

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

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

Следует иметь в виду, что, помимо приоритета, на частоту доступа потока к ЦП оказывают влияние и другие факторы. Так, если высокоприоритетный поток ожидает доступа к некоторому ресурсу, например для ввода с клавиатуры, он блокируется, а вместо него исполняется низкоприоритетный поток. Но когда высокоприоритетный поток получит доступ к ресурсам, он прервет низкоприоритетный поток и возобновит свое исполнение. На планирование работы потоков оказывает также влияние способ, посредством которого в операционной системе поддерживается многозадачность (см. врезку “Обращение к знатоку” в конце этого раздела). Следовательно, если один поток имеет более высокий приоритет, чем другой поток, это еще не означает, что первый поток будет исполняться быстрее второго. Высокий приоритет потока лишь означает, что потенциально он может получить больше времени ЦП.

При запуске порожденного потока его приоритет устанавливается равным приоритету родительского потока. Изменить приоритет можно, вызвав метод setPriority() из класса Thread. Ниже приведено объявление этого метода,

final void setPriority(int уровень)

В качестве параметра уровень данному методу передается новый приоритет для потока. Значение параметра уровень должно находиться в пределах от MIN PRIORITY до MAX PRIORITY. В настоящее время этим константам соответствуют числовые значения от 1 до 10. Для того чтобы восстановить приоритет потока по умолчанию, следует указать значение 5, которому соответствует константа N0RM PRI0RITY. Константы, определяющие приоритеты потоков, определены как static final в классе Thread.

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

Ниже приведен пример программы, демонстрирующий использование двух потоков с разными приоритетами. Потоки создаются как экземпляры класса Priority. В методе run() содержится цикл, отсчитывающий число своих шагов. Этот цикл завершает работу, когда значение счетчика достигает 10000000 или же когда статическая переменная stop принимает логическое значение true. Первоначально переменной stop присваивается логическое значение false, но первый же поток, заканчивающий отсчет, устанавливает в ней логическое значение true. В результате второй поток завершится, как только ему будет выделен квант времени. В цикле производится проверка символьной строки в переменной currentName на совпадение с именем исполняемого потока. Если они не совпадают, это означает, что произошло переключение задач. При этом отображается имя нового потока, которое присваивается переменной currentName. Это дает возможность следить за тем, насколько часто каждый поток получает время ЦП. После остановки обоих потоков выводится число шагов, выполненных в каждом цикле.

// Демонстрация потоков с разными приоритетами.
class Priority implements Runnable {
    int count;
    Thread thrd;

    static boolean stop = false;
    static String currentName;

    /* Построение нового потока. Обратите внимание на то,
       что конструктор не запускает поток на исполнение. */
    Priority(String name) {
        thrd = new Thread(this, name);
        count = 0;
        currentName = name;
    }

    // начать исполнение нового потока
    public void run() {
        System.out.println(thrd.getName() + " starting.");
        do {
            count++;

            if(currentName.compareTo(thrd.getName())    !=  0)  {
                currentName = thrd.getName();
                System.out.println("In " + currentName);
            }
        // Первый же поток, в котором достигнуто значение 10000000,
        // завершает остальные потоки.
        } while(stop == false && count < 10000000);
        stop = true;

        System.out.println("n" + thrd.getName() +
                           " terminating.");
    }
}

class PriorityDemo {
    public static void main(String args[]) {
        Priority mtl = new Priority("High Priority");
        Priority mt2 = new Priority("Low Priority");

        // задать приоритеты
        // Поток mtl получает более высокий приоритет, чем поток mt2.
        mtl.thrd.setPriority(Thread.NORM_PRIORITY+2);
        mt2.thrd.setPriority(Thread.NORM_PRIORITY-2);

        // запустить потоки на исполнение
        mtl.thrd.start();
        mt2.thrd.start();

        try {
            mtl.thrd.join();
            mt2.thrd.join();
        }
        catch(InterruptedException exc) {
            System.out.println("Main thread interrupted.");
        }
        System.out.println("nHigh priority thread counted to " +
        mtl.count);
        System.out.println("Low priority thread counted to " +
        mt2.count);
    }
}

Результат выполнения данной программы выглядит следующим образом:

High Priority starting.
In High Priority
Low Priority starting.
In Low Priority
In High Priority

High Priority terminating.

Low Priority terminating.

High priority thread counted to 10000000
Low priority thread counted to 8183

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

Синхронизация

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

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

У каждого объекта в Java имеется свой монитор. Этот механизм встроен в сам язык. Следовательно, все объекты поддаются синхронизации. Для поддержки синхронизации в Java предусмотрено ключевое слово synchronized и ряд вполне определенных методов у каждого из объектов. А поскольку средства синхронизации встроены в язык, то пользоваться ими на практике очень просто — гораздо проще, чем может показаться на первый взгляд. Для многих программ средства синхронизации объектов по сути прозрачны.

Синхронизировать код можно двумя способами. Оба способа рассматриваются ниже, и в обоих используется ключевое слово synchronized.

Применение синхронизированных методов

Для того чтобы синхронизировать метод, в его объявлении следует указать ключевое слово synchronized. Когда такой метод получает управление, вызывающий поток активизирует монитор, что приводит к блокированию объекта. Если объект блокирован, он недоступен из другого потока, а кроме того, его нельзя вызвать из других синхронизированных методов, определенных в классе данного объекта. Когда выполнение синхронизированного метода завершается, монитор разблокирует объект, что позволяет другому потоку использовать этот метод. Таким образом, для достижения синхронизации программирующему на Java не приходится прилагать каких-то особых усилий.

Ниже приведен пример программы, демонстрирующий контролируемый доступ к методу sumArray(). Этот метод суммирует элементы целочисленного массива.

// Применение ключевого слова synchronize для управления доступом.
class SumArray {
    private int sum;

    // Метод sumArray() синхронизирован.
    synchronized int sumArray(int nums[]) {
        sum = 0; // обнулить сумму

        for(int i=0; i<nums.length; i++) {
            sum += nums[i];
            System.out.println("Running total for " +
                                Thread.currentThread().getName() +
                                " is " + sum);
            try {
                Thread.sleep(10); // разрешить переключение задач
            }
            catch(InterruptedException exc) {
                System.out.println("Main thread interrupted.");
            }
        }
        return sum;
    }
}

class MyThread implements Runnable {
    Thread thrd;
    static SumArray sa = new SumArray();
    int a[];
    int answer;

    // построить новый поток
    MyThread(String name, int nums[]) {
        thrd = new Thread(this, name);
        a = nums;.
        thrd.start(); // начать поток
    }

    // начать исполнение нового потока
    public void run() {
        int sum;

        System.out.println(thrd.getName() + " starting.");

        answer = sa.sumArray(a);
        System.out.println("Sum for " + thrd.getName() +
                           " is " + answer);

        System.out.println(thrd.getName() + " terminating.");
    }
}

class Sync {
    public static void main(String args[]) {
        int a[] = {1, 2, 3, 4, 5};

        MyThread mtl = new MyThread("Child #1", a);
        MyThread mt2 = new MyThread("Child #2", a);
    }
}

Выполнение этой программы дает следующий результат:

Child #1 starting.
Running total for Child #1 is 1
Child #2 starting.
Running total for Child #1 is 3
Running total for Child #1 is 6
Running total for Child #1 is 10
Running total for Child #1 is 15
Sum for Child #1 is 15
Child #1 terminating.
Running total for Child #2 is 1
Running total for Child #2 is 3
Running total for Child #2 is 6
Running total for Child #2 is 10
Running total for Child #2 is 15
Sum for Child #2 is 15
Child #2 terminating.

Рассмотрим подробнее эту программу. В ней определены три класса. Имя первого — SumArray. В нем содержится метод sumArray(), вычисляющий сумму элементов целочисленного массива. Во втором классе MyThread используется статический объект sa типа SumArray для получения суммы элементов массива. А поскольку он статический, то все экземпляры класса MyThread используют одну его копию. И наконец, в классе Sync создаются два потока, в каждом из которых должна вычисляться сумма элементов массива.

В методе sumArray() вызывается метод sleep(). Он нужен лишь для того, чтобы обеспечить переключение задач. Метод sumArray() синхронизирован, и поэтому в каждый момент времени он может использоваться только одним потоком. Следовательно, когда второй порожденный поток начинает свое исполнение, он не может вызвать метод sumArray() до тех пор, пока этот метод не завершится в первом потоке. Благодаря этому обеспечивается правильность получаемого результата.

Для того чтобы лучше понять эффект от использования ключевого слова synchronized, попробуйте удалить его из объявления метода sumArray(). В итоге метод sumArray() потеряет синхронизацию и может быть использован в нескольких потоках одновременно. Это приведет к затруднению в связи с тем, что результат расчета суммы сохраняется в переменной sum, значение которой изменяется при каждом вызове метода sumArray() для статического объекта sa. Так, если в двух потоках одновременно сделать вызов sa. sumArray(), расчет суммы окажется неверным, поскольку в переменной sum накапливаются результаты суммирования, выполняемого одновременно в двух потоках. Ниже приведен результат выполнения той же самой программы, где из объявления метода sumArray() удалено ключевое слово synchronized. (Вследствие отличий в вычислительных средах у вас может получиться несколько иной результат.)

Child #1 starting.
Running total for Child #1 is 1
Child #2 starting
Running total for Child #2 is 1
Running total for Child #1 is 3
Running total for Child #2 is 5
Running total for Child #2 is 8
Running total for Child #1 is 11
Running total for Child #2 is 15
Running total for Child #1 is 19
Running total for Child #2 is 24
Sum for Child #2 : Is 24
Child #2 terminating.
Running total for Child #1 is 29
Sum for Child #1 : Ls 29
Child #1 terminating.

Нетрудно заметить, что вследствие одновременного вызова sa. sumArray() из разных потоков результат искажается.

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

  • Синхронизированный метод создается путем указания ключевого слова synchronized в его объявлении.
  • Как только синхронизированный метод любого объекта получает управление, объект блокируется и ни один синхронизированный метод этого объекта не может быть вызван другим потоком.
  • Потоки, которым требуется синхронизированный метод, используемый другим потоком, ожидают до тех пор, пока не будет разблокирован объект, для которого он вызывается.
  • Когда синхронизированный метод завершается, разблокируется объект, для которого он вызывается.

Синхронизированные блоки

Несмотря на то что создание синхронизированных методов в классах — простой и эффективный способ управления потоками, такой способ оказывается пригодным далеко не всегда. Иногда возникает потребность синхронизировать доступ к методам, в объявлении которых отсутствует ключевое слово synchronized. Подобная ситуация часто возникает при использовании классов, которые были созданы независимыми разработчиками и исходный код которых недоступен. В таком случае ввести в объявление нужного метода ключевое слово synchronized вряд ли удастся. Как же тогда синхронизировать объект класса, содержащего этот метод? К счастью, данное затруднение разрешается очень просто. Достаточно ввести вызов метода в синхронизированный кодовый блок типа synchronized.

Синхронизированный блок определяется следующим образом:

synchronized{ссылка_на_объект) {
    // синхронизируемые операторы
}

где ссылка_на_объект обозначает ссылку на конкретный объект, который должен быть синхронизирован. Как только содержимое синхронизированного блока получит управление, ни один другой поток не сможет вызвать метод для объекта, на который делается ссылка_на_объект9 до тех пор, пока этот кодовый блок не завершится.

Следовательно, обращение к методу sumArray() можно синхронизировать, вызвав его из синхронизированного блока. Такой способ демонстрируется в приведенной ниже переделанной версии предыдущей программы.

// Применение синхронизированного блока
// для управления доступом к методу sumArray().
class SumArray {
    private int sum;

    // Здесь метод sumArray() не синхронизирован.
    int sumArray(int nums[]) {
        sum =0; // обнулить сумму
        for(int i=0; icnums.length; i++) {
            sum += nums[i];
            System.out.println("Running total for " +
                               Thread.currentThread().getName() +
                               " is " + sum);
            try {
                Thread.sleep(10); // разрешить переключение задач
            }
            catch(InterruptedException exc) {
                System.out.println("Main thread interrupted.");
            }
        }
        return sum;
    }
}

class MyThread implements Runnable {
    Thread thrd;
    static SumArray sa = new SumArray();
    int a[];
    int answer;

    // построить новый поток
    MyThread(String name, int nums[]) {
        thrd = new Thread(this, name);
        a = nums;
        thrd.start(); // начать поток
    }

    // начать исполнение нового потока
    public void run() {
        int sum;

        System.out.println(thrd.getName() + " starting.");

        // Здесь вызовы метода sumArray() для объекта sa синхронизированы.
        synchronized(sa) {
            answer = sa.sumArray(a);
        }
        System.out.println("Sum for " + thrd.getName() +
                           " is " + answer);
        System.out.println(thrd.getName() + " terminating.");
    }
}

class Sync {
    public static void main(String args[]) {
        int a [] = {1, 2, 3, 4, 5};

        MyThread mtl = new MyThread("Child #1", a);
        MyThread mt2 = new MyThread("Child #2", a);

        try {
            mtl.thrd.join();
            mt2.thrd.join();
        } catch (InterruptedException exc) {
            System.out.println("Main thread interrupted.");
        }
    }
}

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

Организация взаимодействия потоков с помощью методов notify(), wait() и notifyAll()

Рассмотрим для примера следующую ситуацию. В потоке Т выполняется синхронизированный метод, которому необходим доступ к ресурсу R. Этот ресурс временно недоступен. Что должен предпринять поток т? Если он будет ожидать в цикле освобождения ресурса R, объект будет по-прежнему заблокирован и другие потоки не смогут обратиться к нему. Такое решение малопригодно, поскольку оно сводит на нет все преимущества программирования в многопоточной среде. Намного лучше, если поток Т временно разблокирует объект и позволит другим потокам воспользоваться его методами. Когда ресурс R станет доступным, поток т получит об этом уведомление и возобновит свое исполнение. Но для того чтобы такое решение можно было реализовать, необходимы средства взаимодействия потоков, с помощью которых один поток мог бы уведомить другой поток о том, что он приостановил свое исполнение, а также получить уведомление о том, что его исполнение может быть возобновлено. Для организации подобного взаимодействия потоков в Java предусмотрены методы wait(), notify() и notifyAll().

Эти методы реализованы в классе Object, поэтому они доступны для любого объекта. Но обратиться к ним можно только из синхронизированного контекста. А применяются они следующим образом. Когда поток временно приостанавливает свое исполнение, он вызывает метод wait(). При этом поток переходит в состояние ожидания и монитор данного объекта освобождается, позволяя другим потокам использовать объект. Впоследствии ожидающий поток возобновит свое выполнение, когда другой поток войдет в тот же самый монитор и вызовет метод notify() или notifyAll().

В классе Object определены различные формы объявления метода wait(), как показано ниже.

final void wait() throws InterruptedException
final void wait(long миллисекунд) throws InterruptedException
final void wait(long миллисекунд, int наносекунд) throws InterruptedException

В первой своей форме метод wait() переводит поток в режим ожидания до поступления уведомления. Во второй форме метода организуется ожидание уведомления или до тех пор, пока не истечет указанный период времени. А третья форма позволяет точнее задавать период времени в наносекундах.

Ниже приведены общие формы объявления методов notify() и notifyAll().

final void notifyO
final void notifyAll()

При вызове метода notify() возобновляется исполнение одного ожидающего потока. А метод notifyAll() уведомляет все потоки об освобождении объекта, и тот поток, который имеет наивысший приоритет, получает доступ к объекту.

Прежде чем рассматривать конкретный пример, демонстрирующий применение метода wait(), необходимо сделать важное замечание. Несмотря на то что метод wait() должен переводить поток в состояние ожидания до тех пор, пока не будет вызван метод notify() или notifyAll(), иногда поток выводится из состояния ожидания вследствие так называемой ложной активизации. Условия для ложной активизации сложны, возникают редко, а их обсуждение выходит за рамки этой книги. Но в компании Oracle рекомендуют учитывать вероятность проявления ложной активизации и помещать вызов метода wait() в цикл. В этом цикле должно проверяться условие, по которому поток переводится в состояние ожидания. Именно такой подход и применяется в рассматриваемом ниже примере.

Пример применения методов wait() и notify()

Для того чтобы стала понятнее потребность в применении методов wait() и notify() в многопоточном программировании, рассмотрим пример программы, имитирующей работу часов и выводящей на экран слова “Tick” (Тик) и “Тоск” (Так). Для этой цели создадим класс TickTock, который будет содержать два метода: tick() и tock(). Метод tick() выводит слово “Tick”, а метод tock() — слово “Тоск”. При запуске программы, имитирующей часы, создаются два потока: в одном из них вызывается метод tick(), а в другом — метод tock(). В результате взаимодействия двух потоков на экран будет выводиться набор повторяющихся сообщений “Tick Tock”, т.е. после слова “Tick”, обозначающего один такт, должно следовать слово “Тоск”, обозначающее другой такт часов.

// Применение методов wait() и notifyO для имитации часов,
class TickTock {
    String state; // содержит сведения о состоянии часов
    synchronized void tick(boolean running) {
        if (!running) { // остановить часы
            state = "ticked";
            notifyO; // уведомить ожидающие потоки
            return;
        }

        System.out.print("Tick ");

        state = "ticked"; // установить текущее состояние после такта "тик"
        notify();         // Метод tick() уведомляет метод tock()
                          // о возможности продолжить выполнение.
        try {
            while(!state.equals("tocked") )
                wait();// Метод tick() ожидает завершения метода tock().
        }
        catch(InterruptedException exc) {
            System.out.println("Thread interrupted.");
        }
    }

    synchronized void tock(boolean running) {
        if(!running) { // остановить часы
            state = "tocked";
            notifyO; // уведомить ожидающие потоки
            return;
        }

        System.out.println("Tock");

        state = "tocked"; // установить текущее состояние после такта "так"
        notifyO; // Метод tock() уведомляет метод tick()
                 // возможности продолжить выполнение.
        try {
            while(!state.equals("ticked") )
                wait(); // Метод tock() ожидает завершения метода tick().
        }
        catch(InterruptedException exc) {
            System.out.println("Thread interrupted.");
        }
    }
}

class MyThread implements Runnable {
    Thread thrd;
    TickTock ttOb;

    // построить новый поток
    MyThread.(String name, TickTock tt) {
        thrd = new Thread(this, name);
        ttOb = tt;
        thrd.start(); // начать поток
    }

    // начать исполнение нового потока
    public void run() {
        if(thrd.getName().compareTo("Tick") == 0) {
            for(int i=0; i<5; i++) ttOb.tick(true);
                ttOb.tick(false);
        }
        else {
            for(int i=0; i<5; i++) ttOb.tock(true);
                ttOb.tock(false);
        }
    }
}

class ThreadCom {
    public static void main(String args[]) {
        TickTock tt = new TickTock();
        MyThread mtl = new MyThread("Tick", tt);
        MyThread mt2 = new MyThread("Tock", tt);

        try {
            mtl.thrd.join();
            mt2.thrd.join();
        } catch(InterruptedException exc) {
            System.out.println("Main thread interrupted.");
        }
    }
}

В результате выполнения этой программы на экране появляются следующие сообщения:

Tick Tock
Tick Tock
Tick Tock
Tick Tock
Tick Tock

Рассмотрим более подробно исходный код программы, имитирующей работу часов. В ее основу положен класс TickTock. В нем содержатся два метода tick() и tock(), которые взаимодействуют друг с другом. Это взаимодействие организовано таким образом, чтобы за словом “Tick” всегда следовало слово “Tock”, затем слово “Tick” и т.д. Обратите внимание на переменную state. В процессе работы имитатора часов в данной переменной хранится строка “ticked” или “tocked”, определяющая текущее состояГлава 1 1. Многопоточное программирование 41.1 ние часов после такта “тик” или/‘так” соответственно. В методе main() создается объект tt типа TickTock, используемый для запуска двух потоков на исполнение.

Потоки строятся на основе объектов типа MyThread. Конструктору MyThread() передаются два параметра. Первый из них задает имя потока (в данном случае — “Tick” или “Тоск”), а второй — ссылку на объект типа TickTock (в данном случае — объект tt). В методе run() из класса MyThread вызывается метод tick(), если поток называется “Tick”, или же метод tock(), если поток называется “Тоск”. Каждый из этих методов вызывается пять раз с параметром, принимающим логическое значение true. Работа имитатора часов продолжается до тех пор, пока методу передается параметр с логическим значением true. Последний вызов каждого из методов с параметром, принимающим логическое значение false, останавливает имитатор работы часов.

Самая важная часть программы находится в теле методов tick() и tock() из класса TickTock. Начнем с метода tick(). Для удобства анализа ниже представлен исходный код этого метода.

synchronized void tick(boolean running) {
    if(!running) { // остановить часы
        state = "ticked";
        notifyO; // уведомить ожидающие потоки
        return;
    }

    System.out.print("Tick ");

    state = "ticked"; // установить текущее состояние после такта "тик"
    notify(); // уведомить метод tock() о возможности продолжить выполнение
    try {
        while(!state.equals("tocked") )
            wait(); // ожидать завершения метода tock()
    }
    catch(InterruptedException exc) {
        System.out.println("Thread interrupted.");
    }
}

Прежде всего обратите внимание на то, что в объявлении метода tick() присутствует ключевое слово synchronized, указываемое в качестве модификатора доступа. Как пояснялось ранее, действие методов wait() и notify() распространяется только на синхронизированные методы. В начале метода tick() проверяется значение параметра running. Этот параметр служит для корректного завершения программы, имитирующей работу часов. Если он принимает логическое значение false, имитатор работы часов должен быть остановлен. Если же параметр running принимает логическое значение true, а переменная state — значение “ticked”, вызывается метод notify(), разрешающий ожидающему потоку возобновить свое исполнение. Мы еще вернемся к этому вопросу несколько ниже.

По ходу работы имитируемых часов в методе tick() выводится слово “Tick”, переменная state принимает значение “ticked”, а затем вызывается метод notify(). Вызов метода notify() возобновляет исполнение ожидающего потока. Далее в цикле while вызывается метод wait(). В итоге выполнение метода tick() будет приостановлено до тех пор, пока другой поток не вызовет метод notify(). Таким образом, очередной шаг цикла не будет выполнен до тех пор, пока другой поток не вызовет метод notify() для того же самого объекта. Поэтому когда вызывается метод tick(), на экран выводится слово “Tick” и другой поток получает возможность продолжить свое исполнение, а затем выполнение этого метода приостанавливается.

В том цикле while, в котором вызывается метод wait(), проверяется значение переменной state. Значение “tocked”, означающее завершение цикла, будет установлено только после выполнения метода tock(). Этот цикл предотвращает продолжение исполнения потока в результате ложной активизации. Если по окончании ожидания в переменной state не будет присутствовать значение “tocked”, значит, имела место ложная активизация, и метод wait() будет вызван снова.

Метод tock() является почти точной копией метода tick(). Его отличие состоит лишь в том, что он выводит на экран слово “Tock” и присваивает переменной state значение “tocked”. Следовательно, когда метод tock() вызывается, он выводит на экран слово “Tock”, вызывает метод notify(), а затем переходит в состояние ожидания. Если проанализировать работу сразу двух потоков, то станет ясно, что за вызовом метода tick() тотчас следует вызов метода tock(), после чего снова вызывается метод tick(), и т.д. В итоге оба метода синхронизируют друг друга.

При остановке имитатора работы часов вызывается метод not if у (). Это нужно для того, чтобы возобновить исполнение ждущего потока. Как упоминалось выше, в обоих методах, tick() и tock(), после вывода сообщения на экран вызывается метод wait(). В результате при остановке имитатора работы часов один из потоков обязательно будет находиться в состоянии ожидания. Следовательно, последний вызов метода notify() необходим. В качестве эксперимента попробуйте удалить вызов метода notify() и посмотрите, что при этом произойдет. Вы увидите, что программа зависнет, и вам придется завершить ее нажатием комбинации клавиш <Ctrl+C>. Дело в том, что когда метод tock() в последний раз получает управление, он вызывает метод wait(), после чего не происходит вызов метода not if у (), позволяющего завершиться методу tock(). В итоге метод tock() остается в состоянии бесконечного ожидания.

Если у вас еще остаются сомнения по поводу того, что методы wait() и notify() необходимы для организации нормального выполнения программы, имитирующей работу часов, замените в ее исходном коде класс TickTock приведенным ниже его вариантом. Он отличается тем, что в нем удалены вызовы методов wait() и notify().

// В этой версии вызовы методов wait() и notify() отсутствуют,
class TickTock {

    String state; // содержит сведения о состоянии часов

    synchronized void tick(boolean running) {
        if(!running) { // остановить часы
            state = "ticked";
            return;
        }

        System.out.print("Tick ");

        state = "ticked"; // установить текущее состояние после такта "тик"
    }

    synchronized void tock(boolean running) {
        if(!running) { // остановить часы
            state = "tocked";
            return;
        }

        System.out.println("Tock") ;

        state = "tocked"; // установить текущее состояние после такта "так"
    }
}

Теперь программа выводит на экран следующие сообщения:

Tick Tick Tick Tick Tick Tock
Tock
Tock
Tock
Tock

Это происходит потому, что методы tick() и tock() не взаимодействуют друг с другом.

Приостановка, возобновление и остановка потоков

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

Механизмы приостановки, возобновление и остановки потоков менялись в разных версиях Java. До появления версии Java 2 для этих целей применялись методы suspend(), resume() и stop(), определенные в классе Thread. Ниже приведены общие формы их объявления.

final void resume()
final void suspend()
final void stop()

На первый взгляд кажется, что упомянутые выше методы удобны для управления потоками, но пользоваться ими все же не рекомендуется по следующим причинам. При выполнении метода suspend() иногда возникают серьезные осложнения, приводящие к взаимоблокировке. Метод resume() сам по себе безопасен, но применяется только в сочетании с методом suspend(). Что же касается метода stop() из класса Thread, то и он не рекомендуется к применению, начиная с версии Java 2, поскольку может вызывать порой серьезные осложнения в работе многопоточных программ.

Если методы suspend(), resume() и stop() нельзя использовать для управления потоками, то может показаться, что приостановить, возобновить и остановить поток вообще нельзя. Но это, к счастью, не так. Поток следует разрабатывать таким образом, чтобы в методе run() периодически осуществлялась проверка, следует ли приостановить, возобновить или остановить поток. Обычно для этой цели используются две флаговые переменные: одна — для приостановки и возобновления потока, другая — для остановки потока. Если флаговая переменная, управляющая приостановкой потока, установлена в состояние исполнения, то метод run() должен обеспечить продолжение исполнения потока. Если же эта флаговая переменная находится в состоянии приостановки, в работе потока должна произойти пауза. А если переменная, управляющая остановкой потока, находится в состоянии остановки, исполнение потока должно прекратиться.

Следующий пример программы демонстрирует один из способов реализации собственных версий методов suspend(), resume() и stop().

// Приостановка, возобновление и остановка потока.
class MyThread implements Runnable {
    Thread thrd;

    // Если эта переменная принимает логическое значение
    // true, исполнение потока приостанавливается.
    volatile boolean suspended;
    // Если эта переменная принимает логическое значение
    // true, исполнение потока прекращается.
    volatile boolean stopped;

    MyThread(String name) {
        thrd = new Thread(this, name);
        suspended = false;
        stopped = false;
        thrd.start();
    }

    // Точка входа в поток
    public void run() {
        System.out.println(thrd.getName() + " starting.");
        try {
            for(int i = 1; i < 1000; i++) {
                System.out.print(i + " ");
                if((i %10)==0) {
                    System.out.println() ;
                    Thread.sleep(250) ;
                }

                // Для проверки условий приостановки и остановки потока
                // используется следужхций синхронизированный блок.
                synchronized(this) {
                    while(suspended) {
                        wait();
                    }
                    if(stopped) break;
                }
            }
        } catch (InterruptedException exc) {
            System.out.println(thrd.getName() + " interrupted.");
        }
        System.out.println(thrd.getName() + " exiting.");
    }

    // остановить поток
    synchronized void mystopO {
        stopped = true;

        // Следующие операторы обеспечивают полную
        // остановку приостановленного потока,
        suspended = false;
        notify();
    }

    // приостановить поток
    synchronized void mysuspend() {
        suspended = true;
    }

    // возобновить поток
    synchronized void myresume() {
        suspended = false;
        notify();
    }
}

class Suspend {
    public static void main(String args[]) {
        MyThread obi = new MyThread("My Thread");

        try {
            Thread.sleep(1000); // позволить потоку оЫ начать исполнение

            obi.mysuspend();
            System.out.println("Suspending thread.");
            Thread.sleep(1000);

            obi.myresume();
            System.out.println("Resuming thread.");
            Thread.sleep(1000);

            obi.mysuspend();
            System.out.println("Suspending thread.");
            Thread.sleep(1000);

            obi.myresume();
            System.out.println("Resuming thread.") ;
            Thread.sleep(1000);

            obi.mysuspend() ;
            System.out.println("Stopping thread.");
            obi.mystop();
        } catch (InterruptedException e) {
            System.out.println("Main thread Interrupted");
        }
        // ожидать завершения потока
        try {
            obi.thrd.join() ;
        } catch (InterruptedException e) {
            System.out.println("Main thread Interrupted");
        }
        System.out.println("Main thread exiting.");
    }
}

Ниже приведен результат выполнения данной программы.

My Thread starting.
123456789 10
11 12   13  14  15  16  17  18  19  20
21 22   23  24  25  26  27  28  29  30
31 32   33  34  35  36  37  38  39  40
Suspending thread.
Resuming thread.
41 42   43  44  45  46  47  48  49  50
51 52   53  54  55  56  57  58  59  60
61 62 63 64 65 66 67 68 69 70
71 72 73 74 75 76 77 78 79 80
Suspending thread.
Resuming thread.
81 82 83 84 85 86 87 88 89 90
91 92 93 94 95 96 97 98 99 100
101 102 103 104 105 106 107 108 109 110
111 112 113 114 115 116 117 118 119 120
Stopping thread.
My Thread exiting.
Main thread exiting.

Эта программа работает следующим образом. В классе потока MyThread определены две логические переменные, suspended и stopped, управляющие временной и полной остановкой потока. В конструкторе этого класса обеим переменным присваивается логическое значение false. Метод run() содержит синхронизированный блок, в котором проверяется состояние переменной suspended. Если эта переменная принимаетлогическое значение true, вызывается метод wait(), приостанавливающий исполнение потока. Логическое значение true присваивается переменной suspended в методе mysuspend(), и поэтому данный метод следует вызвать для приостановки потока. Для возобновления потока служит метод myresume(), в котором переменной suspended присваивается логическое значение false и вызывается метод not if у ().

Для остановки потока следует вызвать метод my stop(), в котором переменной stopped присваивается логическое значение true. Кроме того, в методе mystop() переменной suspended присваивается логическое значение false и вызывается метод notify(). Это необходимо для прекращения работы потока, исполнение которого ранее было приостановлено.

В отношении рассматриваемой здесь программы нужно сделать еще одно, последнее замечание. В объявлении переменных suspended и stopped используется ключевое слово volatile. Этот модификатор подробно описывается в главе 14, а до тех пор вкратце поясним его назначение. Он сообщает компилятору о том, что значение переменной может быть неожиданно изменено другими частями программы, в том числе и другим потоком.

Пример для опробования 11.2.
Применение основного потока

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

Последовательность действий

  1. Создайте файл UseMain.java.
  2. Для доступа к основному потоку нужно получить ссылающийся на него объект типа Thread. Для этого следует вызвать метод currentThread(), являющийся статическим членом класса Thread. Ниже приведено объявление этого метода.
    static Thread currentThread()
    

    Метод currentThread() возвращает ссылку на тот поток, из которого он вызывается. Так, если вызвать метод currentThread() из основного потока, можно получить ссылку на этот поток. А имея ссылку на основной поток, можно управлять им.

  3. Введите в файл UseMain. j ava приведенный ниже исходный код программы. В процессе ее выполнения сначала извлекается ссылка на основной поток, затем определяется и устанавливается имя и приоритет потока.
    /*
    Пример для опробования 11.2.
    
    Управление основным потоком.
    */
    class UseMain {
        public static void main(String args[]) {
            Thread thrd;
    
            // получить основной поток
            thrd = Thread.currentThread();
    
            // отобразить имя основного потока
            System.out.println("Main thread is called: " +
                               thrd.getName());
    
            // отобразить приоритет основного потока
            System.out.println("Priority: " +
                               thrd.getPriority());
    
            System.out.println();
    
            // установить имя и приоритет основного потока
            System.out.println("Setting name and priority.n");
            thrd.setName("Thread #1");
            thrd.setPriority(Thread.NORM_PRI0RITY+3);
    
            System.out.println("Main thread is now called: " +
                               thrd.getName());
    
            System.out.println("Priority is now: " +
                               thrd.getPriority());
        }
    }
    
  4. Ниже приведен результат выполнения данной программы.
    Main thread is called: main
    Priority: 5
    
    Setting name and priority.
    
    Main thread is now called: Thread #1
    Priority is now: 8
    
  5. Выполняя операции над основным потоком, необходимо соблюдать осторожность. Так, если добавить в конце метода main() приведенный ниже код, программа никогда не завершится, потому что будет ожидать завершения основного потока!
    try {
        thrd.join();
    } catch(InterruptedException exc) {
        System.out.println("Interrupted");
    }
    

Упражнение для самопроверки по материалу главы 11

  1. Каким образом имеющиеся в Java средства многопоточного программирования позволяют писать более эффективные программы?
  2. Для поддержки многопоточного программирования в Java предусмотрен класс и интерфейс .
  3. В каких случаях следует отдать предпочтение расширению класса Thread над реализацией интерфейса Runnable?
  4. Покажите, как с помощью метода j oin() можно организовать ожидание завершения потокового объекта MyThrd.
  5. Покажите, как установить приоритет потока MyThrd на три уровня выше нормального приоритета.
  6. Что произойдет, если в объявлении метода указать ключевое слово synchronized?
  7. Методы wait() и notify() служат для ____________ .
  8. Внесите в класс TickTock изменения для организации настоящего отчета времени. Первую половину секунды должен занимать вывод на экран слова “Tick”, а вторую — вывод слова “Tock”. Таким образом, сообщение “Tick-Tock” должно соответствовать одной секунде отсчитываемого времени. (Время переключения контекстов можно не учитывать.)
  9. Почему в новых программах на Java не следует применять методы suspend(), resume() и stop()?
  10. С помощью какого метода из класса Thread можно получить имя потока?
  11. Какое значение возвращает метод isAlive() ?
  12. Попытайтесь самостоятельно реализовать средства синхронизации в классе Queue, разработанном в предыдущих главах. В результате доработки класс должен действовать правильно, когда он используется для многопоточной обработки.

Цикл статей «Учебник Java 8».

Следующая статья — «Java 8 среда платформы».
Предыдущая статья — «Java 8 файлы (NIO.2)».

Каждый поток ассоциирован с классом
java.lang.Thread. Есть два основных способа использования объектов
Thread  в многопоточном программировании:

  • Прямое создание и управление потоками с помощью создания экземпляров класса
    Thread.
  • Абстрагирование от управления потоками и передача задач в executor.

Содержание

java.lang.Thread
— Объявление и запуск потока
— Приостанавливаем исполнение с помощью метода sleep
— Прерывание потока
— Соединение
— Простой пример
Синхронизация
— Вмешательство в поток (thread interference)
— Ошибки консистентности памяти (memory consistency errors)
— Синхронизированные (synchronized) методы
— Внутренние блокировки и синхронизация
— Атомарный доступ
Живучесть (Liveness)
— Взаимная блокировка (Deadlock)
— Голодание (starvation)
— Активная блокировка (livelock)
Защищённые блокировки (guarded blocks)
Неизменяемые объекты (immutable objects)
— Пример синхронизированного класса
— Как определять неизменяемые объекты (immutable objects)
Высокоуровневые объекты для многопоточного приложения
— Объекты Lock
— Executors
— — java.util.concurrent.Executor
— — java.util.concurrent.ExecutorService
— — java.util.concurrent.ScheduledExecutorService
— Пулы потоков
— — Fork/Join Framework
— Атомарные переменные

java.lang.Thread

Объявление и запуск потока

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

  • Предоставить экземпляр класса, реализующего интерфейс
    java.lang.Runnable. Этот класс имеет один метод
    run(), который должен содержать код, который будет выполняться в отдельном потоке. Экземпляр класса
    java.lang.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  сам реализует интерфейс
    java.lang.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() для запуска нового потока. Именно он запускает отдельный поток. Если просто вызывать метод
run(), то код будет выполняться в том же потоке, отдельный поток создаваться не будет.

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

Приостанавливаем исполнение с помощью метода sleep

Метод
sleep  класса
Thread  останавливает выполнение текущего потока на указанное время. Он используется, когда нужно освободить процессор, чтобы он занялся другими потоками или процессами, либо для задания интервала между какими-нибудь действиями.

Есть два варианта метода
sleep: первый принимает в качестве параметра количество миллисекунд, на которое нужно остановить текущий поток, второй дополнительно принимает второй параметр, в котором указывается количество наносекунд, на которые нужно дополнительно остановить поток.

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

Пример:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

public class SleepMessages {

    public static void main(String args[])

        throws InterruptedException {

        String importantInfo[] = {

            “Mares eat oats”,

            “Does eat oats”,

            “Little lambs eat ivy”,

            “A kid will eat ivy too”

        };

        for (int i = 0;

             i < importantInfo.length;

             i++) {

            // Ждём 4 секунды

            Thread.sleep(4000);

            // Выводим сообщение

            System.out.println(importantInfo[i]);

        }

    }

}

Обратите внимание, что метод
main  объявляет, что он
throws InterruptedException . Это исключение бросается методом
sleep, если поток прерывается во время ожидания внутри
sleep. Так как эта программа не объявила никаких других потоков, которые могут прерывать текущий, то ей вовсе не обязательно обрабатывать это исключение.

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

Прерывание (interrupt) — это сигнал для потока, что он должен прекратить делать то, что он делает сейчас, и делать что-то другое. Что должен делать поток в ответ на прерывание, решает программист, но обычно поток завершается.

Поток отправляет прерывание вызывая метод
public void interrupt()  класса
Thread. Для того чтобы механизм прерывания работал корректно, прерываемый поток должен поддерживать возможность прерывания своей работы.

Как поток должен поддерживать прерывание своей работы? Это зависит от того, что он сейчас делает. Если поток часто вызывает методы, которые могут бросить
InterruptedException, то он просто вызывает
return  при перехвате подобного исключения. Пример:

for (int i = 0; i < importantInfo.length; i++) {

    // Пауза 4 секунды

    try {

        Thread.sleep(4000);

    } catch (InterruptedException e) {

        // Ожидание было прервано! Больше не нужно сообщений.

        return;

    }

    // Пишем сообщение

    System.out.println(importantInfo[i]);

}

Многие методы, которые бросают
InterruptedException, например методы
sleep, останавливают своё выполнение и возвращают управление в вызвавший их код при получении прерывания (interrupt).

Что если поток выполняется длительное время без вызова методов, которые бросают исключение
InterruptedException? Тогда он может периодически вызывать метод
Thread.interrupted(), который возвращает
true, если получен сигнал о прерывании. Например:

for (int i = 0; i < inputs.length; i++) {

    heavyCrunch(inputs[i]);

    if (Thread.interrupted()) {

        // Мы были прерваны: no more crunching.

        return;

    }

}

В этом примере код просто проверяет на наличие сигнала о прерывании, и выходит из потока, если сигнал есть. В более сложных приложениях имеет смысл бросить исключение
InterruptedException:

if (Thread.interrupted()) {

    throw new InterruptedException();

}

Это позволяет располагать код обработки прерывания потока в одной клаузе
catch.

Механизм прерывания реализован с помощью внутреннего флага, известного как статус прерывания (interrupt status). Вызов
Thread.interrupt()  устанавливает этот флаг. Когда поток проверяет наличие прерывания вызовов
Thread.interrupted(), то флаг статуса прерывания сбрасывается. Нестатический метод
isInterrupted(), который используется одним потоком для проверки статуса прерывания другого потока, не меняет флаг статуса прерывания.

По соглашению любой метод, который прерывает свою выполнение бросая исключение
InterruptedException, очищает флаг статуса прерывания, когда он бросает это исключение. Однако есть вероятность, что флаг статуса прерывания будет сразу же установлен ещё раз, если другой поток вызовет
interrupt().

Соединение

Метод
join  позволяет одному потоку ждать завершения другого потока. Если
t  является экземпляром класса
Thread, чей поток в данный момент продолжает выполняться, то

t.join();

приведёт к приостановке выполнения текущего потока до тех пор, пока поток
t  не завершит свою работу. Метод
join()  имеет варианты с параметрами:

public final void join(long millis)

                throws InterruptedException

public final void join(long millis,

                       int nanos)

                throws InterruptedException

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

Как и методы
sleep, методы
join  отвечают на сигнал прерывания, останавливая процесс ожидания и бросая исключение
InterruptedException.

Простой пример

Пример состоит из двух потоков. Первый поток является главным потоком приложения, который имеет каждая программа на Java. Главный поток создаёт новый поток и ждёт его завершения. Если второй поток выполняется слишком долго, то главный поток прерывает его.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

public class SimpleThreads {

    // Выводим сообщение

    // с именем текущего потока в начале.

    static void threadMessage(String message) {

        String threadName =

            Thread.currentThread().getName();

        System.out.format(“%s: %s%n”,

                          threadName,

                          message);

    }

    private static class MessageLoop

        implements Runnable {

        public void run() {

            String importantInfo[] = {

                “Mares eat oats”,

                “Does eat oats”,

                “Little lambs eat ivy”,

                “A kid will eat ivy too”

            };

            try {

                for (int i = 0;

                     i < importantInfo.length;

                     i++) {

                    // Ждём 4 секунды

                    Thread.sleep(4000);

                    // Пишем сообщение

                    threadMessage(importantInfo[i]);

                }

            } catch (InterruptedException e) {

                threadMessage(“I wasn’t done!”);

            }

        }

    }

    public static void main(String args[])

        throws InterruptedException {

        // Задержка в миллисекундах

        // перед тем как мы прерываем MessageLoop

        // (по умолчанию один час).

        long patience = 1000 * 60 * 60;

        // Если есть аргумент командной строки,

        // то он указывает ожидание в секундах.

        if (args.length > 0) {

            try {

                patience = Long.parseLong(args[0]) * 1000;

            } catch (NumberFormatException e) {

                System.err.println(“Argument must be an integer.”);

                System.exit(1);

            }

        }

        threadMessage(“Starting MessageLoop thread”);

        long startTime = System.currentTimeMillis();

        Thread t = new Thread(new MessageLoop());

        t.start();

        threadMessage(“Waiting for MessageLoop thread to finish”);

        // ждём пока MessageLoop

        // существует

        while (t.isAlive()) {

            threadMessage(“Still waiting…”);

            // Ждём максимум 1 секунду

            // завершения потока MessageLoop

            t.join(1000);

            if (((System.currentTimeMillis() startTime) > patience)

                  && t.isAlive()) {

                threadMessage(“Tired of waiting!”);

                t.interrupt();

                // Должно быть недолго теперь.

                // — Ждём до конца

                t.join();

            }

        }

        threadMessage(“Finally!”);

    }

}

Синхронизация

Потоки общаются в основном разделяя свои поля и поля объектов между собой. Эта форма общения очень эффективна, но делает возможным два типа ошибок: вмешательство в поток (thread interference) и ошибки консистентности памяти (memory consistency errors). Для того чтобы предотвратить эти ошибки, нужно использовать синхронизацию потоков.

Однако синхронизация может привести к конкуренции потоков (thread contention), которая возникает, когда два или более потока пытаются получить доступ к одному и тому же ресурсу одновременно, что приводит к тому, что среда выполнения Java выполняет один или более этих потоков более медленно или даже приостанавливает их выполнение. Голодание (starvation) и активная блокировка (livelock) — это формы конкуренции потоков. Смотрите пункт «Живучесть (Liveness)».

Вмешательство в поток (thread interference)

Рассмотрим простой класс Counter:

class Counter {

    private int c = 0;

    public void increment() {

        c++;

    }

    public void decrement() {

        c;

    }

    public int value() {

        return c;

    }

}

Counter  спроектирован так, что каждый вызов метода
increment  добавляет 1 к
c, а каждый вызов
decrement  вычитает 1 из
c. Однако если объект
Counter  используется несколькими потоками, то вмешательство в поток может помешать этому коду работать как ожидалось.

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

Может показаться, что операции над экземплярами
Counter  не могут перекрываться, так как все операции над
c  являются одиночными простыми инструкциями. Однако даже простые инструкции могут транслироваться виртуальной машиной в несколько шагов. Выражение
c++  может быть разложено на три шага:

  1. Получить текущее значение
    c.
  2. Увеличить полученное значение на 1.
  3. Сохранить увеличенное значение в
    c.

Предположим, что поток A вызывает
increment, и в то же самое время поток B вызывает
decrement. Начальное значение
c  равно 0, их пересечённые действия могут породить следующую последовательность шагов:

  1. Поток A получает
    c.
  2. Поток B получает
    c.
  3. Поток A увеличивает полученное значение, в результате получает 1.
  4. Поток B уменьшает полученное значение, в результате получает -1.
  5. Поток A сохраняет результат 1 в
    c.
  6. Поток B сохраняет результат -1 в
    c.

Результат потока A потерян, он был перезаписан потоком B. Такое частичное перекрытие действий — это только одна из возможностей. В некоторых других ситуациях может оказаться, что результат потока B будет потерян, либо ошибок не будет совсем. Из-за этого ошибки вмешательства в поток трудно обнаруживать и исправлять.

Ошибки консистентности памяти (memory consistency errors)

Ошибки консистентности памяти (memory consistency errors) возникают, когда разные потоки имеют несовместимое представление о том, что должно быть общими данными. Причины ошибок консистентности памяти сложны и выходят за рамки этой статьи, но вам достаточно будет знать стратегию избегания подобных ошибок.

Ключ к исключению ошибок консистентности памяти — это пониманию связи происходит-до (happens-before). Эта связь гарантирует, что данные, записанные в память одной инструкцией, видимы в другой. Рассмотрим следующий пример. Предположим, что поле типа
int  объявлено и инициализировано:

Поле
counter  используется совместно двумя потоками A и B. Предположим, что поток A увеличивает
counter:

сразу же после этого поток B выводит в консоль значение
counter:

System.out.println(counter);

Если бы обе инструкции были выполнены одним потоком, то можно было бы смело предположить, что в консоль выведется число 1. Но если две инструкции выполняются разными потоками, то может быть выведено 0, так как нет гарантии, что изменение
counter  потоком A будет видимо потоком B, до тех пор пока программист не обеспечит связь происходит-до (happens-before) между этими инструкциями.

Есть разные способы создания связи происходит-до (happens-before). Один из них — это синхронизация, она будет расписана в следующих пунктах.

Мы уже видели два действия, которые порождают связь происходит-до (happens-before):

  • Когда инструкция вызывает
    Thread.start, каждая инструкция, которая имеет связь происходит-до (happens-before) с этой инструкцией, также имеет связь происходит-до (happens-before) с каждой инструкцией, выполняемой новым потоком. Все последствия действий кода, который был выполнен до создания нового потока, видимы новым потоком.
  • Когда поток завершается и приводит
    Thread.join  другого потока к возврату выполнения, то все инструкции, которые были выполнены завершённым потоком, имеют связь происходит-до (happens-before) со всеми инструкциями, которые следуют за успешным соединением потока. Все последствия действий кода в потоке теперь видимы потоком, который осуществил соединение.

Синхронизированные (synchronized) методы

Язык программирования Java предоставляет два базовых способа синхронизации: синхронизированные методы (synchronized methods) и синхронизированные инструкции (synchronized statements). Есть другие, более сложные, способы синхронизации, они будут рассмотрены в дальнейшем.

Чтобы сделать метод синхронизированным (synchronized), просто добавьте ключевое слово
synchronized  к его объявлению:

public class SynchronizedCounter {

    private int c = 0;

    public synchronized void increment() {

        c++;

    }

    public synchronized void decrement() {

        c;

    }

    public synchronized int value() {

        return c;

    }

}

Если
count  является экземпляром класса
SynchronizedCounter, то синхронизированные методы имеют два эффекта:

  • Во-первых, два вызова синхронизированных метода на одном и том же объекте не могут пересекаться. Когда один поток выполняет синхронизированный метод объекта, то другие потоки, которые вызывают синхронизированные методы того же самого объекта, блокируются (приостанавливают своё выполнение) до тех пор, пока первый поток не завершит работу с объектом.
  • Во-вторых, когда синхронизированный метод завершает своё выполнение, то он автоматически делает связь происходит-до (happens-before) со всеми последующими вызовами синхронизированных методов того же самого объекта. Это гарантирует, что изменения состояния объекта будут видимы для других потоков.

Заметьте, что конструкторы не могут быть синхронизированными. Использование ключевого слова
synchronized  для конструктора приведёт к ошибке компиляции. Синхронизированные конструкторы не имеют смысла, так как только один поток, который создаёт объект, должен иметь доступ к нему во время создания.

Предупреждение: Когда создаёте объект, который будет совместно использоваться разными потоками, то будьте очень осторожны, чтобы ссылка на объект не «утекла» раньше времени. Например, предположим, что вы хотите сделать список
List, который содержит экземпляры каждого классы. Вы можете захотеть добавить следующую строку в ваш конструктор:

Но тогда другие потоки смогут использовать
instances  для получения доступа к объекту до того, как его создание будет завершено.

Синхронизированные методы — простая стратегия для предотвращения вмешательства в поток (thread interference) и ошибок консистентности памяти (memory consistency errors): Если объект видим более чем одному потоку, то все чтения и записи полей объекта должны происходить через синхронизированные методы. (Важное исключение: поля с модификатором
final, которые не могут быть изменены после создания экземпляра объекта, могут безопасно читаться из несинхронизированных методов после создания конструктора) Эта стратегия эффективна, но может содержать проблемы с живучестью (liveness).

Внутренние блокировки и синхронизация

Синхронизация построена вокруг внутренней сущности, известной как внутренняя блокировка или блокировка монитора. Внутренняя блокировка играет роль в обоих аспектах синхронизации: обеспечивает эксклюзивный доступ к внутреннему состоянию объекта и обеспечивает связь происходит-до (happens-before).

Каждый объект имеет внутреннюю блокировку (монитор), который с ним связан. По соглашению поток, которому требуется эксклюзивный и согласованный доступ к полям объекта, должен получить внутреннюю блокировку объекта перед доступом к ним и освободить внутреннюю блокировку объекта после совершения необходимых действий с ними. Поток владеет блокировкой объекта между временем получения и временем освобождения блокировки. Пока поток держит внутреннюю блокировку (внутренний монитор) никакой другой объект не может получить ту же самую блокировку. Другой поток будет блокирован (приостановлен) при попытке получить эту блокировку.

Когда поток вызывает синхронизированный метод, то он автоматически получает внутреннюю блокировку этого объекта и освобождает её по завершении метода. Освобождение блокировки происходит даже при возникновении неперехваченного исключения.

Если вызывается статический синхронизированный метод, то поток получает внутреннюю блокировку объекта
Class, связанного с этим классом. Таким образом доступ к статическим полям контролируется другой блокировкой, отличной от блокировки любого из экземпляров класса.

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

public void addName(String name) {

    synchronized(this) {

        lastName = name;

        nameCount++;

    }

    nameList.add(name);

}

В этом примере метод
addName  должен синхронизировать изменение
lastName  и
nameCount, но он также должен избежать синхронизированных вызовов методов других объектов. (Вызов методов других объектов из синхронизированного кода может привести к проблемам, описанным в пункте «Живучесть (Liveness)» Без синхронизированных инструкций это мог бы быть отдельный несинхронизируемый метод для единственного вызова
nameList.add.

Синхронизированные инструкции также полезны для улучшения многопоточности с небольшими блокировками. Например, класс
MsLunch  имеет два поля экземпляров,
c1  и
c2, которые никогда не используются вместе. Все изменения этих полей должны быть синхронизированы, но нет никакого смысла запрещать изменение
c1  при изменении только
c2. Вместо использования синхронизированных методов или использования блокировки
this  мы создадим два объекта, которые будут предоставлять блокировки:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

public class MsLunch {

    private long c1 = 0;

    private long c2 = 0;

    private Object lock1 = new Object();

    private Object lock2 = new Object();

    public void inc1() {

        synchronized(lock1) {

            c1++;

        }

    }

    public void inc2() {

        synchronized(lock2) {

            c2++;

        }

    }

}

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

Поток не может получить блокировку, которой владеет другой поток. Но поток МОЖЕТ получить блокировку, которой он уже владеет. Возможность потоков получать одну и ту же блокировку несколько раз называется повторная синхронизация (reentrant synchronization). Это может быть, например, ситуация, когда синхронизированный код напрямую или ненапрямую вызывает метод, который тоже содержит синхронизированный код, и оба кода используют ту же самую блокировку. Без reentrant synchronization синхронизированному коду пришлось бы использовать много предосторожностей, чтобы исключить блокировку потоком самого себя.

Атомарный доступ

В программировании атомарное действие — это действие которое происходит полностью и сразу. Атомарное действие не может остановиться посередине: оно либо завершается полностью, либо не происходит совсем. Никаких эффектов от атомарного действия не видно снаружи до тех пор, пока действие не завершится.

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

  • Чтения и запись атомарны для ссылочных переменных и большинства примитивных типов (все типы кроме
    long  и
    double )
  • Чтение и запись атомарны для всех переменных, объявленных как
    volatile  (включая
    long  и
    double ).

Атомарные действия не могут пересекаться, и они могут использоваться без опасений о вмешательстве в поток. Однако это не это не устраняет все потребности синхронизации атомарных действий, так как ошибки консистенции памяти всё ещё возможны. Использование
volatile-переменных уменьшает риск ошибок консистенции памяти, потому что любая запись в
volatile-переменную делает связь происходит-до (happens-before) для последующих чтений из этой переменной. Это означает, что изменения
volatile-переменных всегда видны для других потоков. Это также означает, что когда поток читает
volatile-переменную, он видит не только последнее изменение, но и все побочные эффекты кода, которые приводят к этому изменению.

Использование простого атомарного доступа к переменным более эффективно, чем доступ к этим переменным из синхронизированного кода, но он требует большей внимательности от программиста, чтобы исключить ошибки консистентности памяти.

Живучесть (Liveness)

Взаимная блокировка (Deadlock)

Взаимная блокировка (deadlock) описывает ситуацию, когда два или более потока блокируются навсегда, каждый ожидая другого. Вот пример.

Алиса и Боб — друзья и большие приверженцы вежливости. Строгое правило вежливости: когда вы кланяетесь другу вы должны оставаться в поклоне до тех пор, пока ваш друг тоже не поклонится вам. Однако это правило не учитывает возможность, когда оба друга кланяются одновременно:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

public class Deadlock {

    static class Friend {

        private final String name;

        public Friend(String name) {

            this.name = name;

        }

        public String getName() {

            return this.name;

        }

        public synchronized void bow(Friend bower) {

            System.out.format(“%s: %s”

                + ”  has bowed to me!%n”,

                this.name, bower.getName());

            bower.bowBack(this);

        }

        public synchronized void bowBack(Friend bower) {

            System.out.format(“%s: %s”

                + ” has bowed back to me!%n”,

                this.name, bower.getName());

        }

    }

    public static void main(String[] args) {

        final Friend alice =

            new Friend(“Alice”);

        final Friend bob =

            new Friend(“Bob”);

        new Thread(new Runnable() {

            public void run() { alice.bow(bob); }

        }).start();

        new Thread(new Runnable() {

            public void run() { bob.bow(alice); }

        }).start();

    }

}

Когда этот пример запустится, то наиболее вероятно, что каждый из потоков будет заблокирован во время попытки вызова
bowBack. Ни одна из блокировок никогда не закончится, потому что каждый поток ожидает выхода другого потока из метода
bow.

Голодание (starvation)

Голодание (starvation) описывает ситуацию, когда поток не может получить доступ к совместно используемым ресурсам и не может продвинуться в своём выполнении дальше. Это возникает, когда совместно используемый ресурс делается недоступным на долгое время «жадными» потоками. Например, предположим, что объект предоставляет синхронизированный метод, который обычно выполняется достаточно долго. Если один поток вызывает этот метод часто, то другие потоки, которым тоже нужен частый синхронизированный доступ к тому же самому объекту, будут часто блокироваться.

Активная блокировка (livelock)

Поток часто реагирует на события из другого потока. Если действие другого потока тоже является ответом на событие из другого потока, то может произойти активная блокировка (livelock). Как и взаимная блокировка (deadlock), активно заблокированные потоки не могут продвинуться дальше в своём выполнении. Однако эти потоки не заблокированы — они просто слишком заняты, отвечая друг другу, чтобы вернуться к работе. Это можно сравнить с двумя людьми, которые пытаются пройти через друг друга в коридоре: Алиса двигается влево, чтобы Боб мог пройти, в это же время Боб двигается вправо, чтобы Алиса могла пройти. Видя, что они всё ещё блокируют друг друга, Боб двигается влево, а Алиса вправо, но они всё ещё блокируют друг друга.

Защищённые блокировки (guarded blocks)

Потокам зачастую приходится согласовывать свои действия. Наиболее часто используемый способ согласования — защищённые блокировки (guarded blocks). Такой блок начинается с выбора условия, которое должно быть
true, перед тем как может осуществиться блокировка. Есть несколько шагов, которые нужно выполнить, чтобы осуществить блокировку правильно.

Предположим, что
guardedJoy  — это метод, который не должен выполняться до тех пор, пока разделяемая между потоками переменная
joy  не будет установлена другим потоком. Такой метод теоретически должен просто выполнять цикл, пока условие не выполниться, но это было бы расточительно, так как это выполняется в течение всего времени ожидания.

public void guardedJoy() {

    // Простой цикл. Тратит процессорное время

    // не делайте так!

    while(!joy) {}

    System.out.println(“Joy has been achieved!”);

}

Наиболее эффективно использовать
Object.wait(), чтобы приостановить работу текущего потока. Вызов метода
wait  не возвращает управление до тех пор, пока другой поток не обработает уведомление о том, что произошло некоторое специальное событие, однако не имеет значения, какое событие ожидает поток:

public synchronized void guardedJoy() {

    // Этот цикл выполняется только один раз

    // для каждого специального события,

    // которое может быть событием, которое мы ожидаем.

    while(!joy) {

        try {

            wait();

        } catch (InterruptedException e) {}

    }

    System.out.println(“Joy and efficiency have been achieved!”);

}

Замечание: всегда вызывайте
wait  внутри цикла, который проверяет условие, которое ожидается. Не предполагайте, что прерывание было вызвано конкретным условием, которое нам нужно, или что это условие до сих пор выполняется.

Как и многие другие методы, которые приостанавливают выполнение,
wait  может бросить
InterruptedException. В этом примере мы просто игнорируем это исключение, мы беспокоимся только о значении переменной
joy.

Почему эта версия
guardedJoy  синхронизирована (
synchronized )? Предположим, что
d  — это объект, который мы используем для вызова
wait. Когда поток вызывает
d.wait, то он должен обладать внутренней блокировкой для
d, иначе бросится исключение. Вызов
wait  внутри синхронизированного метода — это наиболее простой способ получить внутреннюю блокировку.

При вызове метода
wait  поток освобождает блокировку и приостанавливает выполнение. Затем, спустя время, другой поток получает ту же самую блокировку и вызывает
Object.notifyAll, сообщая всем ожидающим потокам, что произошло что-то существенное:

public synchronized notifyJoy() {

    joy = true;

    notifyAll();

}

Спустя какое-то время второй поток освобождает блокировку, первый поток снова получает блокировку и возвращается из вызова
wait.

Замечание: Есть второй метод уведомления —
notify, который пробуждает только один поток, но так как он не позволяет указать, какой поток пробудить, то он полезен только в программах, которые используют большое количество потоков, и каждый поток выполняет похожую работу. В таком приложении вам не нужно беспокоиться о том, какой поток будет пробуждён.

Давайте сделаем приложение поставщик-потребитель, используя защищённые блокировки. Этот тип приложений разделяет данные между двумя потоками: поставщик, который создаёт данные, и потребитель, который делает что-либо с ними. Два потока общаются с помощью общего объекта. Согласование их действий очень важно: поток потребителя не должен пытаться получать данные до того, как поставщик доставит их, и поток поставщика не должен пытаться доставить новые данные до того, пока потребитель не получил старые данные.

В этом примере данными являются последовательность текстовых сообщений, которые разделяются с помощью объекта класса
Drop:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

public class Drop {

    // Сообщение, отправленное от поставщика потребителю.

    private String message;

    // True, если потребитель должен ждать поставщика,

    // пока тот не отправит сообщение.

    // false если поставщик должен ждать, пока потребитель

    // не получит сообщение

    private boolean empty = true;

    public synchronized String take() {

        // Ждём, пока нет сообщения.

        while (empty) {

            try {

                wait();

            } catch (InterruptedException e) {}

        }

        // Меняем статус

        empty = true;

        // Уведомляем поставщика, что статус был изменён.

        notifyAll();

        return message;

    }

    public synchronized void put(String message) {

        // Ждём, пока сообщение не доставлено.

        while (!empty) {

            try {

                wait();

            } catch (InterruptedException e) {}

        }

        // Переключаем статус.

        empty = false;

        // Сохраняем сообщение.

        this.message = message;

        // Уведомляем потребителя, что статус был изменён.

        notifyAll();

    }

}

Поток поставщика отправляет несколько сообщений. Строка
“DONE”  обозначает, что все сообщения были успешно отправлены. Чтобы имитировать непредсказуемое поведение реального приложения, поток поставщика делает случайную паузу между сообщениями.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

import java.util.Random;

public class Producer implements Runnable {

    private Drop drop;

    public Producer(Drop drop) {

        this.drop = drop;

    }

    public void run() {

        String importantInfo[] = {

            “Mares eat oats”,

            “Does eat oats”,

            “Little lambs eat ivy”,

            “A kid will eat ivy too”

        };

        Random random = new Random();

        for (int i = 0;

             i < importantInfo.length;

             i++) {

            drop.put(importantInfo[i]);

            try {

                Thread.sleep(random.nextInt(5000));

            } catch (InterruptedException e) {}

        }

        drop.put(“DONE”);

    }

}

Поток потребителя просто получает сообщения и выводит их, пока не получит сообщение
“DONE”. Он тоже делает случайную паузу между сообщениями.

Главный поток запускает поток поставщика и поток потребителя:

public class ProducerConsumerExample {

    public static void main(String[] args) {

        Drop drop = new Drop();

        (new Thread(new Producer(drop))).start();

        (new Thread(new Consumer(drop))).start();

    }

}

Замечание: Класс
Drop  написан только для демонстрации потоков. Постарайтесь не изобретать колесо, посмотрите существующие структуры данных в Java Collection Framework, перед тем как пытаться написать свой собственный объект для разделения данных между несколькими потоками.

Неизменяемые объекты (immutable objects)

Объект считается неизменяемым, если его внутреннее состояние не может быть изменено после создания. Использование неизменяемых объектов — широко распространённая стратегия для создания простого и надёжного кода.

Неизменяемые объекты особенно полезны в многопоточных приложениях. Так как они не могут менять своего внутреннего состояния, то они не могут быть испорчены вмешательством в поток (thread interference) или прочитаны в некорректном состоянии.

Программисты часто ленятся использовать неизменяемые объекты, так как они беспокоятся о цене создания нового объекта вместо изменения старого. Влияние создания экземпляров объектов часто переоценивается и может быть компенсировано преимуществами использования неизменяемых объектов.

Пример синхронизированного класса

Класс
SynchronizedRGB  определяет объекты, которые хранят цвет. Каждый объект содержит три числа (красный, зелёный, синий) и имя цвета.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

public class SynchronizedRGB {

    // Values must be between 0 and 255.

    private int red;

    private int green;

    private int blue;

    private String name;

    private void check(int red,

                       int green,

                       int blue) {

        if (red < 0 || red > 255

            || green < 0 || green > 255

            || blue < 0 || blue > 255) {

            throw new IllegalArgumentException();

        }

    }

    public SynchronizedRGB(int red,

                           int green,

                           int blue,

                           String name) {

        check(red, green, blue);

        this.red = red;

        this.green = green;

        this.blue = blue;

        this.name = name;

    }

    public void set(int red,

                    int green,

                    int blue,

                    String name) {

        check(red, green, blue);

        synchronized (this) {

            this.red = red;

            this.green = green;

            this.blue = blue;

            this.name = name;

        }

    }

    public synchronized int getRGB() {

        return ((red << 16) | (green << 8) | blue);

    }

    public synchronized String getName() {

        return name;

    }

    public synchronized void invert() {

        red = 255 red;

        green = 255 green;

        blue = 255 blue;

        name = “Inverse of “ + name;

    }

}

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

SynchronizedRGB color =

    new SynchronizedRGB(0, 0, 0, “Pitch Black”);

...

int myColorInt = color.getRGB();      // инструкция 1

String myColorName = color.getName(); // инструкция 2

Если другой поток вызывает
color.set  после инструкции 1, но перед инструкцией 2, то значение
myColorInt  не будет совпадать со значением
myColorName. Чтобы предотвратить это, две инструкции должны быт связаны вместе:

synchronized (color) {

    int myColorInt = color.getRGB();

    String myColorName = color.getName();

}

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

Как определять неизменяемые объекты (immutable objects)

Ниже перечислены правила определения неизменяемых объектов. Не все классы, документированные как «неизменяемые», следуют этим правилам. Это не обязательно значит, что создатели этих классов были идиотами — они могли иметь веские причины считать, что экземпляры их объектов никогда не будут меняться после создания. Однако такие стратегии требуют сложного анализа и не подходят для начинающих.

  1. Не создавайте методов установки значений (setter-ов, которые меняют значения полей или объектов, на которые ссылаются эти поля).
  2. Делайте все поля
    final  и
    private.
  3. Не позволяйте дочерним классам переопределять методы. Самый просто способ добиться этого — объявить класс как
    final. Более сложный способ — это сделать конструктор приватным и создавать экземпляры класса с помощью методов фабрик.
  4. Если поля экземпляров ссылаются на изменяемые объекты, то не позволяйте менять состояние этих объектов: не предоставляйте методов, которые меняют внутреннее состояние изменяемых объектов, не позволяйте стороннему коду получить ссылки на изменяемые объекты (возвращайте копии этих объектов), не используйте те объекты, которые были переданы в конструктор (создавайте их копии, если нужно).

Применение этих правил к
SynchronizedRGB  состоит из следующих шагов:

  1. Есть два метода установки значений. Первый произвольно меняет объект, и его нужно убрать из неизменяемой версии класса. Второй метод
    invert  может быть адаптирован так, что он будет создавать новый объект, вместо изменения существующего.
  2. Все поля сделать
    final, они уже приватные.
  3. Сам класс объявить как
    final.
  4. Только одно поле ссылается на объект, и этот объект уже неизменяемый, поэтому других предосторожностей не нужно.

Результат:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

final public class ImmutableRGB {

    // Values must be between 0 and 255.

    final private int red;

    final private int green;

    final private int blue;

    final private String name;

    private void check(int red,

                       int green,

                       int blue) {

        if (red < 0 || red > 255

            || green < 0 || green > 255

            || blue < 0 || blue > 255) {

            throw new IllegalArgumentException();

        }

    }

    public ImmutableRGB(int red,

                        int green,

                        int blue,

                        String name) {

        check(red, green, blue);

        this.red = red;

        this.green = green;

        this.blue = blue;

        this.name = name;

    }

    public int getRGB() {

        return ((red << 16) | (green << 8) | blue);

    }

    public String getName() {

        return name;

    }

    public ImmutableRGB invert() {

        return new ImmutableRGB(255 red,

                       255 green,

                       255 blue,

                       “Inverse of “ + name);

    }

}

Высокоуровневые объекты для многопоточного приложения

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

Объекты Lock

Синхронизированный код полагается на простой тип reentrant lock (блокировка, которую можно брать несколько раз). Этот тип легко использовать, но он имеет определённые ограничение. Более сложные способы блокировки поддерживаются пакетом java.util.concurrent.locks. Этот пакет имеет довольно много классов, но здесь будет рассмотрен его наиболее базовый интерфейс Lock.

Объекты
Lock  работают очень похоже на внутренние блокировки, используемые синхронизированным кодом. Так же как и для внутренних блокировок только один поток может держать блокировку объекта
Lock  в одно время. Объекты
Lock  также поддерживают механизм
wait /
notify  через ассоциированные с ними объекты Condition.

Преимущество объектов
Lock  над внутренними блокировками в том, что они могут отказаться от участия в попытке приобрести блокировку. Метод
tryLock  сразу же завершается, если блокировка недоступна сразу же, либо после истечения указанного времени (если время указано). Метод
lockInterruptibly  отказывается от попытки получить блокировку, если другой поток отправляет
interrupt  до получения блокировки.

Используем объекты
Lock, чтобы решить проблему взаимной блокировки, которую мы видели в «Живучесть (Liveness)». Алиса и Боб научили себя замечать, когда другой собирается сделать поклон. Мы смоделируем это улучшение, добавив необходимость для наших объектов
Friend  получить блокировку на обоих участниках, перед тем как сделать поклон. Чтобы продемонстрировать универсальность этого способа, мы предположим, что Алиса и Боб так воодушевлены своей возможностью кланяться безопасно, что они не могут остановиться:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

import java.util.concurrent.locks.Lock;

import java.util.concurrent.locks.ReentrantLock;

import java.util.Random;

public class Safelock {

    static class Friend {

        private final String name;

        private final Lock lock = new ReentrantLock();

        public Friend(String name) {

            this.name = name;

        }

        public String getName() {

            return this.name;

        }

        public boolean impendingBow(Friend bower) {

            Boolean myLock = false;

            Boolean yourLock = false;

            try {

                myLock = lock.tryLock();

                yourLock = bower.lock.tryLock();

            } finally {

                if (! (myLock && yourLock)) {

                    if (myLock) {

                        lock.unlock();

                    }

                    if (yourLock) {

                        bower.lock.unlock();

                    }

                }

            }

            return myLock && yourLock;

        }

        public void bow(Friend bower) {

            if (impendingBow(bower)) {

                try {

                    System.out.format(“%s: %s has”

                        + ” bowed to me!%n”,

                        this.name, bower.getName());

                    bower.bowBack(this);

                } finally {

                    lock.unlock();

                    bower.lock.unlock();

                }

            } else {

                System.out.format(“%s: %s started”

                    + ” to bow to me, but saw that”

                    + ” I was already bowing to”

                    + ” him.%n”,

                    this.name, bower.getName());

            }

        }

        public void bowBack(Friend bower) {

            System.out.format(“%s: %s has” +

                ” bowed back to me!%n”,

                this.name, bower.getName());

        }

    }

    static class BowLoop implements Runnable {

        private Friend bower;

        private Friend bowee;

        public BowLoop(Friend bower, Friend bowee) {

            this.bower = bower;

            this.bowee = bowee;

        }

        public void run() {

            Random random = new Random();

            for (;;) {

                try {

                    Thread.sleep(random.nextInt(10));

                } catch (InterruptedException e) {}

                bowee.bow(bower);

            }

        }

    }

    public static void main(String[] args) {

        final Friend alice =

            new Friend(“Alice”);

        final Friend bob =

            new Friend(“Bob”);

        new Thread(new BowLoop(alice, bob)).start();

        new Thread(new BowLoop(bob, alice)).start();

    }

}

Executors

java.util.concurrent.Executor

Интерфейс java.util.concurrent.Executor предоставляет один метод
execute, который является заменой обычного создания потока. Если
r  реализует интерфейс
Runnable, а
e  реализует интерфейс
Executor, то вы можете заменить

следующим кодом:

Однако определение метода
execute  несколько отличается. Низкоуровневый вариант создаёт новый поток и сразу же его запускает. В зависимости от реализации
Executor-а метод
execute  может делать то же самое, но обычно он использует уже существующий рабочий поток для запуска
r, либо
r  помещается в очередь, где дожидается освобождения рабочего потока.

Реализации executor-ов в
java.util.concurrent  созданы для использования с более продвинутыми интерфейсами
ExecutorService  и
ScheduledExecutorService, но они также работают и с интерфейсом
Executor.

java.util.concurrent.ExecutorService

Интерфейс java.util.concurrent.ExecutorService расширяет интерфейс Executor, добавляя множество новых методов. Основной метод — метод
submit, который принимает как
Runnable, так и интерфейс
java.util.concurrent.Callable<V>  с единственным методом
V call() , который позволяет заданиям возвращать значение. Метод
submit  возвращает интерфейс
java.util.concurrent.Future, который используется для получения результата и контролирования состояния потока.

java.util.concurrent.ScheduledExecutorService

Интерфейс java.util.concurrent.ScheduledExecutorService расширяет интерфейс
java.util.concurrent.ExecutorService  и добавляет методы
schedule*, которые позволяют запланировать выполнение задания.

Пулы потоков

Многие реализации executor-ов из пакета
java.util.concurrent  используют пул потоков, который состоит из работающих потоков. Такой тип потока существует отдельно от
Runnable  и
Callable, который он выполняет, и часто используется для выполнения нескольких заданий.

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

Наиболее часто использующийся тип пула потоков — это фиксированный пул потоков. Этот тип пула всегда держит работающими указанное количество потоков. Если поток как-нибудь завершается в то время, пока он всё ещё используется, то он автоматически заменяется вновь созданным потоком. Задачи отправляются в пул через внутреннюю очередь, которая хранит дополнительные задачи, если их больше, чем потоков.

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

Самый простой способ создать executor, который использует фиксированный пул потоков, — это вызвать
newFixedThreadPool  у класса
java.util.concurrent.Executors:

public static ExecutorService newFixedThreadPool(int nThreads)

Класс Executors также имеет следующие фабричные методы:

public static ExecutorService newCachedThreadPool()

Создаёт новые потоки по мере надобности. Повторно использует предыдущие потоки, если они свободны.

public static ExecutorService newSingleThreadExecutor()

Пул потоков, состоящий из одного потока.

Если ни один из стандартных executor-ов не удовлетворяет вашим потребностям, то вы можете создать экземпляры java.util.concurrent.ThreadPoolExecutor или java.util.concurrent.ScheduleThreadPoolExecutor.

Fork/Join Framework

Fork/Join Framework является реализацией интерфейса
ExecutorService, который помогает вам получить преимущество при использовании мультипроцессорной системы. Он спроектирован для такой работы, которая может быть разбита рекурсивно на множество маленьких частей. Цель фреймворка — использовать всю доступную мощь, чтобы увеличить производительность вашего приложения.

Как и с любой реализацией
ExecutorService  fork/join framework распределяет задачи между рабочими потоками в пуле потоков. Fork/join фрейморк отличается тем, что он использует алгоритм воровства работы. Рабочие потоки, для которых кончилась работа, могут воровать задачи других потоков, которые всё ещё заняты.

Основным классом fork/join фреймворка является класс java.util.concurrent.ForkJoinPool, который расширяет класс java.util.concurrent.AbstractExecutorService. Класс ForkJoinPool реализует основной алгоритм воровства работы и может выполнять задачи java.util.concurrent.ForkJoinTask.

Первый шаг использования fork/join framework — это написать код, который выполняет кусок работы. Ваш код должен быть похож на следующий псевдокод:

если моя порция работы слишком мала

    сделать работу напрямую

иначе

    разделить работу на два куска

    вызывать два куска и подождать результата

Оберните этот код в какой-нибудь дочерний класс от
ForkJoinTask, обычно используя один из более специализированных типов java.util.concurrent.RecursiveTask (который может вернуть результат) либо java.util.concurrent.RecursiveAction.

Когда ваш
ForkJoinTask  будет готов, создайте объект, который представляет из себя всю работу и передайте его в метод
invoke()  экземпляра
ForkJoinPool.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

public class ForkBlur extends RecursiveAction {

    private int[] mSource;

    private int mStart;

    private int mLength;

    private int[] mDestination;

    // Размер окна. Должен быть нечётным

    private int mBlurWidth = 15;

    public ForkBlur(int[] src, int start, int length, int[] dst) {

        mSource = src;

        mStart = start;

        mLength = length;

        mDestination = dst;

    }

    protected void computeDirectly() {

        int sidePixels = (mBlurWidth 1) / 2;

        for (int index = mStart; index < mStart + mLength; index++) {

            // вычисляем средний цвет.

            float rt = 0, gt = 0, bt = 0;

            for (int mi = sidePixels; mi <= sidePixels; mi++) {

                int mindex = Math.min(Math.max(mi + index, 0),

                                    mSource.length 1);

                int pixel = mSource[mindex];

                rt += (float)((pixel & 0x00ff0000) >> 16)

                      / mBlurWidth;

                gt += (float)((pixel & 0x0000ff00) >>  8)

                      / mBlurWidth;

                bt += (float)((pixel & 0x000000ff) >>  0)

                      / mBlurWidth;

            }

            // создаём заново конечный пиксель.

            int dpixel = (0xff000000     ) |

                   (((int)rt) << 16) |

                   (((int)gt) <<  8) |

                   (((int)bt) <<  0);

            mDestination[index] = dpixel;

        }

    }

...

Теперь мы реализуем абстрактный метод compute(), который реализует размывание напрямую, либо делит на мелкие задачи.

protected static int sThreshold = 100000;

protected void compute() {

    if (mLength < sThreshold) {

        computeDirectly();

        return;

    }

    int split = mLength / 2;

    invokeAll(new ForkBlur(mSource, mStart, split, mDestination),

              new ForkBlur(mSource, mStart + split, mLength split,

                           mDestination));

}

Пример запуска:

// source image pixels are in src

// destination image pixels are in dst

ForkBlur fb = new ForkBlur(src, 0, src.length, dst);

ForkJoinPool pool = new ForkJoinPool();

pool.invoke(fb);

Атомарные переменные

Пакет java.util.concurrent.atomic содержит классы, которые поддерживают атомарные операции над простыми переменными. Все классы имеют методы
get  и
set, которые работают так же, как чтение и запись
volatile  переменных, то есть 
set  имеет связь произошло-до (happens-before) с любым последующим
get  над той же самой переменной. Атомарный метод
compareAndSet  также имеет ту же особенность.

Чтобы посмотреть класс в деле давайте посмотрим класс
Counter:

Один из способов избавить
Counter  от вмешательства в поток (thread interference) — это сделать его синхронизированным:

class SynchronizedCounter {

    private int c = 0;

    public synchronized void increment() {

        c++;

    }

    public synchronized void decrement() {

        c;

    }

    public synchronized int value() {

        return c;

    }

}

Для такого простого класса это вполне приемлемое решение. Но для более сложных классов, мы хотим ибежать живучести (liveness). Замена
int  на
AtomicInteger  позволит защититься от вмешательства в поток (thread interference) без
synchronized :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

import java.util.concurrent.atomic.AtomicInteger;

class AtomicCounter {

    private AtomicInteger c = new AtomicInteger(0);

    public void increment() {

        c.incrementAndGet();

    }

    public void decrement() {

        c.decrementAndGet();

    }

    public int value() {

        return c.get();

    }

}

Более подробно использование атомарных классов описано в статье про AtomicInteger.

Цикл статей «Учебник Java 8».

Следующая статья — «Java 8 среда платформы».
Предыдущая статья — «Java 8 файлы (NIO.2)».

Добавить комментарий