Java 101: Общие сведения о потоках Java, часть 1: Введение в потоки и исполняемые файлы

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

Обратите внимание, что эта статья (часть архивов JavaWorld) была обновлена ​​новыми списками кода и загружаемым исходным кодом в мае 2013 года.

Понимание потоков Java - прочтите всю серию

  • Часть 1. Введение в потоки и исполняемые файлы
  • Часть 2: Синхронизация
  • Часть 3. Планирование потоков и ожидание / уведомление
  • Часть 4. Группы потоков и волатильность

Что такое нить?

Концептуально понятие потока нетрудно понять: это независимый путь выполнения через программный код. Когда выполняется несколько потоков, путь одного потока через один и тот же код обычно отличается от других. Например, предположим, что один поток выполняет байт-код, эквивалентный части оператора if-else if, а другой поток выполняет байт-код, эквивалентный elseчасти. Как JVM отслеживает выполнение каждого потока? JVM предоставляет каждому потоку свой собственный стек вызовов методов. Помимо отслеживания текущей инструкции байтового кода, стек вызовов метода отслеживает локальные переменные, параметры, передаваемые JVM методу, и возвращаемое значение метода.

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

  • Программы, основанные на многопоточном графическом интерфейсе пользователя (GUI), реагируют на запросы пользователей при выполнении других задач, таких как переформатирование или печать документа.
  • Многопоточные программы обычно завершаются быстрее, чем их беспоточные аналоги. Это особенно верно для потоков, работающих на многопроцессорной машине, где каждый поток имеет свой собственный процессор.

Java выполняет многопоточность через свой java.lang.Threadкласс. Каждый Threadобъект описывает один поток выполнения. Это исполнение происходит в Thread«S run()метода. Поскольку run()метод по умолчанию ничего не делает, вы должны создать подкласс Threadи переопределить, run()чтобы выполнить полезную работу. Чтобы получить представление о потоках и многопоточности в контексте Thread, просмотрите листинг 1:

Листинг 1. ThreadDemo.java

// ThreadDemo.java class ThreadDemo { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); for (int i = 0; i < 50; i++) System.out.println ("i = " + i + ", i * i = " + i * i); } } class MyThread extends Thread { public void run () { for (int count = 1, row = 1; row < 20; row++, count++) { for (int i = 0; i < count; i++) System.out.print ('*'); System.out.print ('\n'); } } }

В листинге 1 представлен исходный код приложения, состоящего из классов ThreadDemoи MyThread. Класс ThreadDemoуправляет приложением, создавая MyThreadобъект, запуская поток, который ассоциируется с этим объектом, и выполняет некоторый код для печати таблицы квадратов. В противоположность этому , MyThreadпереопределяет Thread«ы run()метод для печати (на стандартный выходной поток) под прямым углом треугольника , состоящего из звездочки символов.

Планирование потоков и JVM

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

Когда вы вводите java ThreadDemoкоманду для запуска приложения, JVM создает начальный поток выполнения, который выполняет main()метод. При выполнении mt.start ();начальный поток сообщает JVM о необходимости создания второго потока выполнения, который выполняет инструкции байтового кода, составляющие метод MyThreadобъекта run(). Когда start()метод возвращается, начальный поток выполняет свой forцикл для печати таблицы квадратов, а новый поток выполняет run()метод для печати прямоугольного треугольника.

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

Класс Thread

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

Остальные Threadметоды я представлю в следующих статьях, за исключением устаревших методов Sun.

Устаревшие методы

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

Построение потоков

Threadимеет восемь конструкторов. Самые простые из них:

  • Thread(), который создает Threadобъект с именем по умолчанию
  • Thread(String name), который создает Threadобъект с именем, nameуказанным в аргументе

Следующие простейшие конструкторы - это Thread(Runnable target)и Thread(Runnable target, String name). За исключением Runnableпараметров, эти конструкторы идентичны вышеупомянутым конструкторам. Разница: Runnableпараметры идентифицируют объекты за пределами, Threadкоторые предоставляют run()методы. (Вы узнаете о Runnableпозже в этой статье.) Последние четыре Конструкторы похожи Thread(String name), Thread(Runnable target)и Thread(Runnable target, String name); однако финальные конструкторы также включают ThreadGroupаргумент для организационных целей.

Один из последних четырех конструкторов, Thread(ThreadGroup group, Runnable target, String name, long stackSize)интересен тем, что позволяет указать желаемый размер стека вызовов методов потока. Возможность указать этот размер оказывается полезной в программах с методами, использующими рекурсию - технику выполнения, при которой метод неоднократно вызывает себя - для элегантного решения определенных проблем. Явно задав размер стека, вы иногда можете предотвратить StackOverflowErrors. Однако слишком большой размер может привести к OutOfMemoryErrors. Кроме того, Sun считает, что размер стека вызовов методов зависит от платформы. В зависимости от платформы размер стека вызовов методов может меняться. Поэтому тщательно подумайте о последствиях для вашей программы, прежде чем писать вызывающий код Thread(ThreadGroup group, Runnable target, String name, long stackSize).

Заведи свои автомобили

Потоки напоминают транспортные средства: они перемещают программы от начала до конца. Threadи Threadподклассы объекты не являются потоками. Вместо этого они описывают атрибуты потока, такие как его имя, и содержат код (через run()метод), который этот поток выполняет. Когда приходит время для выполнения нового потока run(), другой поток вызывает метод Threadобъекта или его подкласса start(). Например, чтобы запустить второй поток, main()вызывается запускающий поток приложения start(). В ответ код обработки потока JVM работает с платформой, чтобы гарантировать, что поток правильно инициализируется и вызывает метод Threadобъекта или его подкласса run().

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

На графике показаны несколько значимых временных периодов:

  • Инициализация начального потока
  • Момент, когда этот поток начинает выполняться main()
  • Момент, когда этот поток начинает выполняться start()
  • Момент start()создает новый поток и возвращается кmain()
  • Инициализация нового потока
  • В момент начала выполнения нового потока run()
  • Различные моменты завершения каждого потока

Обратите внимание, что инициализация нового потока, его выполнение run()и его завершение происходят одновременно с выполнением начального потока. Также обратите внимание, что после вызовов потока start()последующие вызовы этого метода до выхода из run()метода вызывают start()создание java.lang.IllegalThreadStateExceptionобъекта.

Что в имени?

Во время сеанса отладки полезно отличать один поток от другого удобным для пользователя способом. Чтобы различать потоки, Java связывает имя с потоком. По умолчанию для Threadэтого имени используется дефис и целое число с отсчетом от нуля. Вы можете принять имена потоков Java по умолчанию или выбрать свои собственные. Для размещения настраиваемых имен Threadпредоставляет конструкторы, которые принимают nameаргументы и setName(String name)метод. Threadтакже предоставляет getName()метод, который возвращает текущее имя. В листинге 2 показано, как установить собственное имя через Thread(String name)конструктор и получить текущее имя в run()методе, вызвав getName():

Листинг 2. NameThatThread.java

// NameThatThread.java class NameThatThread { public static void main (String [] args) { MyThread mt; if (args.length == 0) mt = new MyThread (); else mt = new MyThread (args [0]); mt.start (); } } class MyThread extends Thread { MyThread () { // The compiler creates the byte code equivalent of super (); } MyThread (String name) { super (name); // Pass name to Thread superclass } public void run () { System.out.println ("My name is: " + getName ()); } }

Вы можете передать необязательный аргумент MyThreadимени в командной строке. Например, java NameThatThread Xустанавливается Xкак имя потока. Если вы не укажете имя, вы увидите следующий результат:

My name is: Thread-1

При желании вы можете изменить super (name);вызов в MyThread (String name)конструкторе на вызов setName (String name)—as in setName (name);. Этот последний вызов метода достигает той же цели - установление имени потока - что и super (name);. Я оставляю это вам в качестве упражнения.

Именование основной

Java присваивает имя mainпотоку, который запускает main()метод, начальному потоку. Обычно вы видите это имя в Exception in thread "main"сообщении, которое выводит обработчик исключений JVM по умолчанию, когда начальный поток генерирует объект исключения.

Спать или не спать

Later in this column, I will introduce you to animation— repeatedly drawing on one surface images that slightly differ from each other to achieve a movement illusion. To accomplish animation, a thread must pause during its display of two consecutive images. Calling Thread's static sleep(long millis) method forces a thread to pause for millis milliseconds. Another thread could possibly interrupt the sleeping thread. If that happens, the sleeping thread awakes and throws an InterruptedException object from the sleep(long millis) method. As a result, code that calls sleep(long millis) must appear within a try block—or the code's method must include InterruptedException in its throws clause.

Для демонстрации sleep(long millis)я написал CalcPI1приложение. Это приложение запускает новый поток, который использует математический алгоритм для вычисления значения математической константы «пи». Пока новый поток вычисляет, начальный поток останавливается на 10 миллисекунд, вызывая sleep(long millis). После пробуждения начального потока он печатает значение числа Пи, которое новый поток сохраняет в переменной pi. В листинге 3 представлен CalcPI1исходный код:

Листинг 3. CalcPI1.java

// CalcPI1.java class CalcPI1 { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); try { Thread.sleep (10); // Sleep for 10 milliseconds } catch (InterruptedException e) { } System.out.println ("pi = " + mt.pi); } } class MyThread extends Thread { boolean negative = true; double pi; // Initializes to 0.0, by default public void run () { for (int i = 3; i < 100000; i += 2) { if (negative) pi -= (1.0 / i); else pi += (1.0 / i); negative = !negative; } pi += 1.0; pi *= 4.0; System.out.println ("Finished calculating PI"); } }

Если вы запустите эту программу, вы увидите результат, аналогичный (но, вероятно, не идентичный) следующему:

pi = -0.2146197014017295 Finished calculating PI