Пользователь
0,0
рейтинг
31 декабря 2012 в 18:27

Разработка → Многопоточность в Java из песочницы tutorial

Здравствуйте! В этой статье я вкратце расскажу вам о процессах, потоках, и об основах многопоточного программирования на языке 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. Если эта статья окажется полезной, то в следующей я расскажу о проблемах совместного доступа потоков к ресурсам и о методах их решения.

Всех благ.
Сухоруков Иван @Pe3oHaHc
карма
7,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (96)

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

    Еще можно через новый интерфейс java.util.concurrent.Callable

    И еще один момент — InterruptedException нежелательно поглощать без обработки (пустой catch блок), его нужно либо повторно генерировать, либо заново выставить флаг методом interrupt(), чтобы еще «прервать» текущий поток. Иначе другие методы в стеке вызовов не узнают, что было прерывание.
    • +1
      Да, про Callable я забыл, а если честно то и вовсе не знал) Насколько я понимаю возвращение результата работы потока используется не слишком часто, но, все равно, напишу об этом в следующей статье.

      Насчет исключения — честно говоря я не очень понимаю зачем другим методам в стеке знать о возникнувшем исключении, если после его возникновения требуется просто продолжить выполнение вызвавшего метода. Может поясните?
      • 0
        Насколько я понимаю возвращение результата работы потока используется не слишком часто

        Чаще используется возможность выбросить исключение.

        честно говоря я не очень понимаю зачем другим методам в стеке знать о возникнувшем исключении,

        Допустим вы в рамках одного потока поймали InterruptedException, например, в методе sleep и поглотили его без выставления флага. Если дальше по коду (или выше по стеку) стоят проверки флага Interrupted — то они не сработают
        • 0
          Допустим вы в рамках одного потока поймали InterruptedException, например, в методе sleep и поглотили его без выставления флага. Если дальше по коду (или выше по стеку) стоят проверки флага Interrupted — то они не сработают


          Ну, это было бы косяком если бы кто-то собирался прерывать потоки из примеров) В примере с объяснением прерываний обработка исключения выполняется (а именно вызывается return), в других примерах особого смысла раздувать код не вижу. Однако в более «жизненных» ситуациях работать с исключениями, конечно, надо по-другому.
    • +2
      Вы, наверное, имеете большой опыт проведения собеседований по Java Concurrency…
      • 0
        Есть такое )
  • +2
    Статья освежила память, спасибо.
    Но вот за
    class SomeThing			//Нечто, реализующее интерфейс Runnable
    implements Runnable		//(содержащее метод run())

    мне бы за такое руки оторвали в универе.
    • +1
      *спрятал руки за спину* хыхы)
    • +2
      Освежила память? Мне казалось это основы, которые должны после недельного запоя от зубов отскакивать :)
      • +2
        Порой после длительного перерыва, даже основы забываешь, к сожалению.
    • +1
      Простите, а что здесь не так? (без сарказма)
    • +1
      а что не так с этим кодом?
      • 0
        Наверное речь о комментариях.
  • 0
    Разделение на главный/побочный потоки возникает лишь в голове программиста, на уровне JVM таких разделений нет.
    • 0
      Согласен, в Java потоки равноправны.
      • 0
        Не совсем, точнее даже так, не для каждой JVM. У треда как минимум может быть приоритет, который поддерживается не всеми ОС. Но так же тред может быть помечен как daemon, т.е. он не будет для JVM считаться полноценным тредом и и она, например, может его попросту убить, если все нормальные (т.е. не daemon) треды будут убиты или закончат свое исполнение
  • +4
    private boolean mFinish = false;
    Обязательно volatile, иначе другой поток может не увидеть изменений данной переменной, сделанных в методе finish() другим потоком (при опции jit компилятора -server).
    • –1
      Хм, спасибо. Думаю в данном примере это не критично, но о volatile расскажу в следующей статье.
      • +14
        Ничего себе «не критично». Вы не объявили переменную volatile, не используете synchronized блок, пишите в неё из одного потока, а читаете с другого — по спецификации Вам не гарантируется, что изменения в переменной другой поток увидит даже к завтра. И Вы хотите учить других такой статьей?
        • –5
          Я понимаю ваше негодование, но формат статьи не позволяет описать все и сразу. В нормальном режиме компиляции программа работает. На данном этапе не вижу смысла требовать большего. Никто не ринется писать многопоточные программы, сразу после прочтения данной статьи, пока не разберется что к чему. Статья служит точкой старта а не полным руководством. О volatile, syncronized и разделении ресурсов между потоками будет рассказано в следующей статье.
          • +8
            А причём тут другая статья? Код в этой статье написано неправильно, он вполне может повиснуть на одной из реализаций виртуальной машины, и это будет валидное поведение. Я бы сказал, что это даже более вероятно, и если оно где-то работает, то это больше чудо в следствии недоработанного оптимизатора.
            Вам лучше исправить это, а не рассказывать, что это «не критично» да про какой-то «нормальный режим компиляции».
            • +1
              Хорошо, согласен, исправил
          • +1
            зато формат статьи позволяет написать правильный код c volatile, сказав, что про это слово будет следующая статья.
      • 0
        А смысл, зачем еще один пересказ? Об этом писали уже сто раз, в том числе и на хабре, можно просто ссылку оставить.
        • 0
          по ссылке хорошая статья, но более серьёзная. Тут статья детская совсем. Так что это не повтор.
  • 0
    Спасибо большое автору за пост, посоветуйте пожалуйста еще статьи и книги о потоках, желательно из реальной жизни, а то большинство материала довольно абстрактно или устарело. Было бы замечательно увидить еще посты на данную тематику.
    • 0
      По Java есть отличная книжка «Эккель Б. — Философия Java». Пережила четыре издания и имеет хорошие отзывы. Почти все мои знания по Java — из неё. О потоках же я в основном читал из документации Oracle. Конкретно о потоках — здесь.
      • 0
        Для начала сойдет, но как сказано документация и исходники — вот основа знаний о Java.
        Касательно многопоточности могу посоветовать след. книгу — «Concurrency in practice»(если не ошибаюсь только на англ. языке).
      • 0
        А еще Java 2: наиболее полное руководство. Шилдт
        • 0
          Шилдт пишет обо всем подряд, я не очень доверяю таким авторам. Хотя опять же для начинающего сойдет.
          • 0
            Шилдт человек с мировым именем, Ему тяжело не доверять. Да и манера изложения у него приятная. Так что подойдет не только новичкам. А для профи все же рекомендую читать доки oracle.
            • 0
              Прошу прощения за немного сумбурный комментарий.
        • 0
          +1 к Шилдту. Буквально час назад читал в нём про Fork/Join Framework.
    • +4
      читайте Java Concurrency in Practice. Там всё есть. И без грубых ошибок, как в этой статьи.
      • 0
        Спасибо за советы и названия книг, Эккеля читал, но слишком много описания классов и мало понятно, как все это в итоге использовать. Про Шилдта забыл, честно говоря, хотя самая пожалуй очевидная книга, которую надо было смотреть. Java Concurrency in Practice — много о ней слышал, надо будет купить. Мне больше всего понравился подход в книге — Java 7 Recipes A Problem-Solution Approach, там то, как раз показано на примерах, как применять многопоточность и как распараллеливать алгоритмы, но примеров к сожалению не так много.

        Основная моя проблема и моих коллег по работе, в том, что имея более пяти лет опыта разработки на java никто из нас ни разу не использовал многопоточность. Для меня до сих пор не вполне ясно, какие проблемы и как я могу решить используя multi-threading. Хотелось бы простого объяснения вида: проблема -> решение +объяснение подводных камней, а не только описания всех классов пакета, с тоннами кода.
        • +1
          Основная моя проблема и моих коллег по работе, в том, что имея более пяти лет опыта разработки на java никто из нас ни разу не использовал многопоточность.

          так может, оно Вам и не надо? Может, это и не проблема?

          Для меня до сих пор не вполне ясно, какие проблемы и как я могу решить используя multi-threading

          дык зачем Вам многопоточность тогда?

          Хотелось бы простого объяснения вида: проблема -> решение +объяснение подводных камней, а не только описания всех классов пакета, с тоннами кода.

          читайте Java Concurrency in Practice
        • +2
          Нет проблемы — это лучшее состояние!

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

          Особая тема — многопоточность и GUI. Но это совсем отдельная песня.
          • 0
            Интересное мнение и точка зрения, обязательно учту. Область деятельности у меня не связана с промышленной разработкой, а связана с академической и научной деятельностью и я, как и окружающии меня люди, не привык думать о том как распараллелить алгоритм и меня этому никто не учил и спросить тоже неукого. Вот, если к примеру, я анализирую объекты при помощи reflection и объектов десятки тысяч, то думаю для такого рода алгоритмов можно использовать многопоточность и это даст определенный прирост в производительности.

            Именно об этом и хотелось бы прочесть, как и зачем использовать многопоточность и будет ли игра стоить свеч. Да и скилл программирования тоже хочется улучшить.
            • 0
              Основная моя проблема и моих коллег по работе, в том, что имея более пяти лет опыта разработки на java никто из нас ни разу не использовал многопоточность.
              Я думаю следует сказать спасибо всем тем, кто позволил большинству программистов даже не задумываться о том, что они в 99.9% случаев используют многопоточность в решении своих проблем. Остальные 0.089 — это студенты которые решают всевозможные академические задачи, и оставшиеся — люди, которые понимают почему они отказываются от многопоточности.

              Хотелось бы простого объяснения вида: проблема -> решение +объяснение подводных камней, а не только описания всех классов пакета, с тоннами кода.
              Проблема: игровой сервер, способный тянуть на себе несколько тысяч пользователей на одной ноде. Соединения постоянные, игроки всю игру взаимодействуют друг с другом (пачками по 2-9 человек).
              Решение: netty в для обработки клиент-серверного и сервер-серверного (серверов то может быть много) взаимодействия, что бы не писать всю низкоуровневую мишуру с соединениями вручную. Остальное — многопоточность.

              Объяснение: все, что есть сейчас в мире java для EE не подходит, потому что потребуется либо 20+ серверов, что экономически не выгодного. Либо попросту не будет справляться с такими нагрузками. Ну и главный аргумент — оно все http, что нам категорически не подходит.
      • 0
        Прямо-таки грубых?)
        • +2
          Да что вы, какая же это грубая ошибка, так, мелочь: подумаешь в одном случае другие потоки не смогут увидеть изменения переменной (отсутствие volatile), а в другом так и вовсе в гонке мы можем потерять несколько инкрементов\декрементов (отсутствие синхронизации).

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

          И да, вы хотя бы посмотрели исходники прежде чем писать глупости про Thread.sleep() например.
          • 0
            подписываюсь
          • 0
            Volatile добавил, про необходимость синхронизации написал в заключении. Что не так с методом Thread.sleep()?
            • 0
              То есть ты хочешь, что бы я сюда скопировал код данного метода?
              • 0
                Лучше скажите в каком разделе ошибка и в чем она.
                • 0
                  Хорошо, если вы сами не в состоянии, кину код сюда:

                      /**
                       * Causes the currently executing thread to sleep (cease execution) 
                       * for the specified number of milliseconds plus the specified number 
                       * of nanoseconds, subject to the precision and accuracy of system 
                       * timers and schedulers. The thread does not lose ownership of any 
                       * monitors.
                       *
                       * @param      millis   the length of time to sleep in milliseconds.
                       * @param      nanos    0-999999 additional nanoseconds to sleep.
                       * @exception  IllegalArgumentException  if the value of millis is 
                       *             negative or the value of nanos is not in the range 
                       *             0-999999.
                       * @exception  InterruptedException if any thread has interrupted
                       *             the current thread.  The <i>interrupted status</i> of the
                       *             current thread is cleared when this exception is thrown.
                       * @see        Object#notify()
                       */
                      public static void sleep(long millis, int nanos) 
                      throws InterruptedException {
                  	if (millis < 0) {
                              throw new IllegalArgumentException("timeout value is negative");
                  	}
                  
                  	if (nanos < 0 || nanos > 999999) {
                              throw new IllegalArgumentException(
                  				"nanosecond timeout value out of range");
                  	}
                  
                  	if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
                  	    millis++;
                  	}
                  
                  	sleep(millis);
                      }
                  
                  • 0
                    Хм, получается время ожидания все-равно округляется до миллисекунд?
                  • 0
                    исправил
                    • +1
                      Вообще можно было не исправлять. У Вас же в статье нигде не говорится, что речь идёт про конкретную реализацию VM, тем более от Oracle. Oracle может и округлили (для совместимости между различными ОС, т.к. в венде с наносекундами имплементировать паузу сложно), а другие могут и не округлять. Например, Google в своей Dalvik VM использует эти наносекунды без округлений.
                      • 0
                        Вам нравится меня мучать? :) Вернул всё как было.
                        • 0
                          Ну теперь там есть фраза «При вызове этого метода JVM честно передает системе наносекунды, если система это поддерживает», а это неверно. Linux всё поддерживает, но Oracle JVM это не юзает. Нельзя же говорить за все реализации JVM.
                          • 0
                            Я вернул все назад, первый вариант был самым лояльным.

                            p.s. а линукс разве поддерживает интервалы прерываний меньше 1мс? Пока я искал информацию по методу sleep я натыкался на высказывания по поводу 10мс у windows и 1мс у линукса. Или я в чем-то заблуждаюсь?
                            • 0
                              Как-то плохо Вы гуглили. Первые ссылки в гугле ведут на пост в блоге разработчика Oracle VM, где есть фраза: «On Solaris and Linux these calls take time structures that allow microsecond, or nanosecond, values to be passed — but there is no guarantee of that resolution being achieved». В Linux точность таймера очень сильно зависит от того, с какими параметрами скомпилировано было ядро.
    • 0
      Java Concurrency Guidelines
      • 0
        Отличное чтиво, но, как я помню там совершено не раскрывается тема, а почему же, например нельзя просто использовать boolean флаг и «атомарно» переключать его без атомиков и локов, так, пара предложений что это далеко не атомарно. Отличное чтиво, но начинать с него не самая лучшая затея.
  • 0
    Зря ничего не написано про синхронизацию потоков прекрасными методами wait и notify.
    • 0
      Синхронизация — довольно большая тема, я не хотел растягивать статью. О синхронизации я напишу в следующей статье.
    • 0
      Не знаю, к счастью или сожалению, но после того, что предлагает пакет java.util.concurrent методы wait и notify ставновятся уже не такими прекрасными. Про опыты практического примеренния дуамаю даже не стоит и вспоминать
      • 0
        подписываюсь
      • 0
        Ну почему, есть задачи, которые через synchronized/wait/notify решаются несколько лаконичнее, чем через аналогичные ReentrantLock/lock/unlock из java.util.concurrent
        • 0
          Хотелось бы увидеть пример задачи. Особенно с wait/notify. Вот по поводу synchronized — я еще согласен, написал его в объявлении метода и можно спать спокойно.
          • 0
            Пример задачи. Поток каждые 60 секунд сканирует папку на наличие изменений:

            public void loop()
            {
                for (;;)
                {
                    scan();
                    synchronized(lock)
                    {
                        lock.wait(60, TimeUnit.SECONDS);
                    }
                }
            }
            


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

            public void forceScan()
            {
                synchronized(lock)
                {
                    lock.notify();
                }
            }
            


            С ReentrantLock все было бы то же самое, но чуть длиннее
      • 0
        Кстати, насчет wait/notify. Посмотрите класс Monitor из библиотеки Guava. Это более простая и удобная альтернатива стандартным Lock/Condition. Там есть несколько принципиальных отличий: не нужно вызывать методы signal/signalAll, не нужно проверять условие в цикле. В общем, классная вещь, но к сожалению, пока только Beta.
        • 0
          Если я не ошибаюсь, то Monitor это просто сильно упрощенный ReentrantLock, его же и использующий
          • 0
            Ну да, он более упрощенный. Но от этого не менее гибкий. Задачи позволяет решать те же самые.
    • +1
      Про синхронизацию «прекрасными» методами wait, notify/notifyAll, join, yield следует написать следующее: «используйте только в крайнем случае, например, при решении задач на собеседованиях, если вам явно указали что следует использоваться только данные примитивы»
    • +1
      не зря. Ничего замечательного в них нету. С Java5 приехало много высокоуровневых примитивов. Насколько я помню, сейчас рекомендуется использовать именно их, а wait-notify только в двух случаях:
      1. иначе никак
      2. legacy code

      Хотя для первого случая есть мозг, а для второго — рефакторинг.
      • 0
        Есть ещё третий случай, когда использование низкоуровневых средств оправдано требованием быстродействия.
        • 0
          ооо, а можно пример в студию?
          • 0
            Те «высокоуровневые примитивы» построены обычно как обёртки над низкоуровневыми, т.е. быстрее быть не могут по определению. Потому иногда имеет смысл написать свой велосипед, не такой универсальный как стандартный, а заточенный под конкретную задачу. Пример такого велосипеда приводить не вижу смысла.
            • 0
              Те «высокоуровневые примитивы» построены обычно как обёртки над низкоуровневыми,

              не всегда. Всё чаще это Unsafe, быстрее которого сделать сложно.

              т.е. быстрее быть не могут по определению.

              а нативно?

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


              ну если вы — твиттер, гугл, фейсбук или дойчбанк, то да, безусловно. В остальных случаях — не уверен.
              • 0
                > а нативно?
                Нативно реализовать на Си? Ну оно то будет быстрее, но там Вы точно не будете юзать те «высокоуровневые примитивы» о которых говорили. ;)

                >ну если вы — твиттер, гугл, фейсбук или дойчбанк
                Вот как раз они могут себе позволить купить лишний сервер. И потому иногда не заморачиваются, а отдают приоритет надёжности и проверенности. А нам, простым смертным, приходится извращаться.
                • 0
                  с нативностью — это вообще отдельная история, давайте забудем об этом.

                  Я уточню свою мысль: высокоуровневыми примитивами из java.uril.concurrent можно обойтись в 99 процентах случаев.

                  Что касается крупных компаний — речь идёт не о лишем сервере, а о лишних десятках, сотнях и даже тысячах серверов. Поэтому им имеет смысл поинженерить. Тот случай, когда пара инженеромесяцев выйдут дешевле закупки сотни серверов. А вот для мелких компаний — дешевле часто купить сервер, чем тратить инженеро-месяцы на решение задач оптимизации.
                  • 0
                    Свой велосипед просто может глючить по началу, крупным компаниям такое не к лицу, потому в некоторых случаях предпочтительнее использовать проверенные средства.
                    А вот для мелких точно так же может оказаться дешевле потратить месяц работы программиста с окладом 1000 долларов, чем купить новый сервер ценой в 10000 долларов, особенно если их просто нет. Пускай даже потом придётся 3 месяца ещё вылавливать баги.
                    Ну… это уже экономика, думаю её тоже можно оставить в покое.
                • 0
                  ну и насчёт
                  «высокоуровневые примитивы» построены обычно как обёртки над низкоуровневыми


                  Загляните в код AtomicInteger, что ли… Или в Semaphore. Найдите там хоть один wait или notify…
                  • 0
                    Да, соглашусь, в данном случае это обёртки не над wait/notify.

                    Но AtomicInteger — это вообще из другой оперы… там и не надо wait/notify. Если уж сравнивать, то с классом, предоставляющим аналогичные возможности, там Condition, например. Вот если дойдут руки попробую даже как-то сравнить количественно разность в скорости работы для типовой задачи.
                    • 0
                      если дойдут руки попробую даже как-то сравнить количественно разность в скорости работы для типовой задачи.


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

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


                      у него две реализации: ConditionObject в AbstractQueuedSynchronizer и в AbstractQueuedLongSynchronizer. Через первый работает куча java.util,concurrent-примитивов: Semaphore, CountDownLatch, ReetrantLock, ThreadpoolExecutor и пр. Второй — вариант AbstractQueuedSynchronizer для специального случая.

                      AtomicInteger — это вообще из другой оперы… там и не надо wait/notify

                      Речь о том, что есть целый класс высокоуровневых примитивов, которым wait/notify вообще ни в какое место не впились.
        • 0
          Это когда JIT вырезает синхронизацию посредством synchronized?
          • 0
            Вы имеете ввиду случай, когда JIT оптимизирует вход/выход в монитор? Ну это тоже — модный ReentrantLock, который использует CAS-инструкции и должен быть теоретически куда быстрее стандартного явовского монитора (блоки synchronized), оказывается очень часто медленнее. :)
            • +1
              Я имею в виду то, что JIT умеет вырезать полностью всю синхронизацию если она не требуется. По поводу стандартного монитора могу сказать вот что…
              Во-первых это три типа синхронизации: biased, thin и fat, а не одна.
              biased — самый простой и быстрый тип блокировки, при помощи cas-а (как я помню единственного) биндится к одному и тому же потоку, если он постоянно захватывает монитор. Накладные расходы минимальны. Если в дело вступает другой поток, то тип меняется на следующий
              thin — перепривязка монитора происходит уже каждый раз с cas-ом. Происходит до тех пор пока множество потоков не начинает биться за ресурс и дальше мы спускаемся выше, к fat
              fat — тут уже используются примитивы ос, что весьма и весьма затратно.

              Собственно я думаю я ответил на вопрос почему non-fair* RL часто медленнее стандартного монитора (много оптимизаций со стороны JVM, в то время как RL не трогается)

              *fair по своей сути будет в тысячи раз медленнее.
  • +1
    Будете писать про синхронизацию, не забудьте вспомнить не только про volatile переменные, но и про ReentrantLock-и и всякого типа барьеры.
    • 0
      это камент из серии «А я вот ещё про ReentrantLock читал!». Стыдоба.
      • 0
        Я вам чем то не угодил? Вашу позицию я понял — кг/ам. Хотите и знаете чем сделать статью лучше и что в ней не правильно — с удовольствием почитаю и исправлю. Не хотите/не знаете — перестаньте, пожалуйста, писать бессмысленные комментарии.
        • +2
          1. вообще-то, это было не Вам :) К Вам были конкретные претензии, вполне раскрытые юзером ashofthedream. Кажется, Вы многое поправили, и это правильно.

          2. я сам разберусь, что мне писать, а что нет.
          • 0
            Это было обращено к тому, кто хотел помочь, вместо того чтобы выделываться. Никаких претензий к комментарию aleks_pingvin'a не имею и в плане на статью добавил пометку на ReentrantLock и барьеры. В чем смысл вашего комментария я понять не могу… Унизить? Выпендриться? Или одно другого не исключает?
            • 0
              если Вы хотите выяснить отношения — пишите в личку. На мой взгляд, здесь не место подобным разговорам.
      • 0
        Троль детектид? Вы считаете, что синхронизация потоков ограничивается synchronized блоками или методами и wait/notify? Тогда стыдно должно быть вам. Без умения использовать java.util.concurrent.locks.*, барьеров (например они нужны — реализация синхронного выполнения команд при клиент-серверном взаимодействии ) о каком-либо «скиле» в многопоточном программировании не может быть и речи. Я уж молчу, про пулы потоков и т.п.
        • 0
          Вы меня раскусили!
  • +7
    > В частности, в примерах не используется синхронизация

    Это сильно, да :)

    Вообще в современном мире Java есть гораздо более адекватные способы работы с потоками, их пулами, асинхронным выполнением и прочим. Не в обиду автору — но если кто серьезно собирается «черпать знания» из статьи где автор не понимает зачем volatile нужен в проверке условия выхода из потока, то я таким настоятельно советую поискать более другие источники знаний.
    • 0
      Сильно то, что не используется синхронизация или сильно то, что в фразе не правильный смысл?)

      Volatile — добавил. Зачем он нужен — понял. Есть еще замечания?

      Да, и я совсем забыл про то, как называл себя в статье великим знатоком многопоточного программирования, несущим знания в народ… хотя подождите, я же этого не писал. Хм, наверное потому что я так не думаю. Вот ведь интересно, с чего все остальные взяли обратнное? Наверное потому же почему люди верят в то, что по телеку вещают только величайшие умы современности, просто не способные ошибаться из-за своей безграничной крутости и приближенности к богу.
      • +1
        Строго говоря, к автору статьи у меня вопросов нет, и на вопрос " Есть еще замечания?" я не стану повторять многое из того, что критики до меня сообщили. Замечу, что если бы мне на интервью показали такое понимание работы в многопоточной среде, то я бы… не удовлетворился.

        Но речь не о том. А речь о том, что я с удивлением обнаружил, что подобного рода статья от ну очень уж «не знатока многопоточного программирования» вызывает живой народный интерес, и прочитав ее он (народ) действительно проникнется странной идеей, что «Наиболее очевидная область применения многопоточности – это программирование интерфейсов». А после этого начнет эту многопоточность в интерфейсы вставлять. Что само по себе очень скользкая область, да и к тому же полностью обойденные вниманием проблемы синхронизации, конфликтов/гонок и вся специфика работы с muttable сущностями в конкурентной среде преподнесут массу неприятных и неожиданных сюрпризов. Ну и не говоря о том, что так, как описано в примерах, на таком уровне ручного управления Threads, в реальной жизни стараются не пользоваться, т.к. есть более адекватные и управляемы средства.

        Всем, кому интерсно мое мнение, я советую вдумчиво почитать Java Concurrency in Practice. Там все ясно, доступно и разумно рассказано.
  • +1
    Прочитал всё, сначала копировал участки статьи которые требуют отдельных комментариев, потом понял что это бесполезное дело и из-за того, что «в интернете кто-то не прав» у меня будет убит весь вечер. Короче пост в духе «позавчера сел разбираться и сегодня решил написать статью», очевидно, что вышло не очень.

    … ну ладно по быстрому: notify() — ничего не сказано, для sleep() — это тоже самое что для минуса плюс. Кстати и про yield() забыли.

    взаимодействие между процессами осуществляется с помощью специальных средств (например, сокетов)

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

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

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

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

    билибирда написана: создание процесса и есть загрузка кода и данных вместе с выделением адресного пространства.

    mSecondThread = new AffableThread();

    префиксы «m» выглядят подураци. Вообще не могу понять зачем их использовать для переменных. Разве в коде, который полностью написан вами могут быть не ваши переменные? Используйте комментарии во время объявления и не уродуйте код. Короче чек стайл не пройден.

    примеры типа
    if(mAnotherOpinion.isAlive()) //Если оппонент еще не сказал последнее слово
    {
    try{
    mAnotherOpinion.join(); //Подождать пока оппонент закончит высказываться.
    }catch(InterruptedException e){}

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

    вообще классический говнокод

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

    взаимоисключающие параграфы

    Заметьте что Thread.interrupted() — статический метод класса Thread, и его вызов возвращает значение флага прерывания того потока, из которого он был вызван. Поэтому этот метод вызывается только изнутри потока и позволяет потоку проверить своё состояние прерывания.

    интересно зачем потоку самому вызывать этот метод?! Очевидно что когда он живой результат всегда false, а когда он прерван, то соответственно никакой код не выполняется. 1) это явно не api для прикладного использования 2) вы что-то не дописали.

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

    ээм, да так про что угодно можно написать.
    • +1
      > интересно зачем потоку самому вызывать этот метод?! Очевидно что когда он живой результат всегда false
      А вот тут Вы сильно ошибаетесь, interrupted — это просто флаг. Поток может быть живым, а interrupted == true. Обычно этот флаг устанавливают из другого потока в целях уведомить данный поток о чём-то, например, что пора завершаться. А в самом потоке Вы уже проверяете этот флаг в нужном месте (или просто ловите исключение) и корректно завершаете работу. Но теоретически можно использовать этот флаг и в других целях.
  • 0
    … ну ладно по быстрому: notify() — ничего не сказано, для sleep() — это тоже самое что для минуса плюс. Кстати и про yield() забыли.


    Насколько я знаю notify() работает в паре с wait(), а не с sleep().
    Про yield() написал, спасибо.

    Про сокеты убрал, пример, и правда, не удачный.

    Объяснение про процессы много места не заняло. Оно дано для общей информации и без особого смысла. Без него параграф про процессы был бы суховатым.

    Неточность в фразе про создание процесса исправил, еще раз спасибо.

    Префикс «m» в имени переменной значит «member», а именно — член класса. Этот префикс служит для отличения переменных класса от переменных метода, в которых префикс не указывается. Вероятно вы подумали что он значит «my»…

    Исправил текст про предугадание выполнения программы.

    Насчет interrupted() я не очень понял. Именно этот метод вызавается потоком для проверки того, прерван он или нет. Метод interrupt() не убивает поток, а выставляет флаг прерывания, который потом проверяется методом interrupted().

    Предложение в параграфе приоритета потоков убрал.
  • 0
    Насколько я знаю notify() работает в паре с wait(), а не с sleep().

    да, так и есть, я «дал маху».

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

    Префикс «m» в имени переменной значит «member», а именно — член класса. Этот префикс служит для отличения переменных класса от переменных метода, в которых префикс не указывается. Вероятно вы подумали что он значит «my»…

    по-моему это излишне и ещё хуже чем «венгерская нотация». Я например для этого просто настроил IDE и эти переменные у меня разного цвета. Префиксы должны быть семантическими.
  • 0
    Для пущего укорочения кода

    Можно еще более укоротить код использовав анонимный внутренний класс

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

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