Программа завершается когда завершается последний ее поток
Примеры потоков ранее представляли поток как последовательный набор операций. После выполнения последней операции завершался и поток. Однако нередко имеет место и другая организация потока в виде бесконечного цикла. Например, поток сервера в бесконечном цикле прослушивает определенный порт на предмет получения данных. И в этом случае мы также можем предусмотреть механизм завершения потока.
Завершение потока
Распространенный способ завершения потока представляет опрос логической переменной. И если она равна, например, false, то поток завершает бесконечный цикл и заканчивает свое выполнение.
Определим следующий класс потока:
class MyThread implements Runnable < private boolean isActive; void disable()< isActive=false; >MyThread() < isActive = true; >public void run() < System.out.printf("%s started. \n", Thread.currentThread().getName()); int counter=1; // счетчик циклов while(isActive)< System.out.println("Loop " + counter++); try< Thread.sleep(400); >catch(InterruptedException e) < System.out.println("Thread has been interrupted"); >> System.out.printf("%s finished. \n", Thread.currentThread().getName()); > >
Переменная isActive указывает на активность потока. С помощью метода disable() мы можем сбросить состояние этой переменной.
Теперь используем этот класс:
public static void main(String[] args) < System.out.println("Main thread started. "); MyThread myThread = new MyThread(); new Thread(myThread,"MyThread").start(); try< Thread.sleep(1100); myThread.disable(); Thread.sleep(1000); >catch(InterruptedException e) < System.out.println("Thread has been interrupted"); >System.out.println("Main thread finished. "); >
Итак, вначале запускается дочерний поток: new Thread(myThread,»MyThread»).start() . Затем на 1100 миллисекунд останавливаем Main thread и потом вызываем метод myThread.disable() , который переключает в потоке флаг isActive. И дочерний поток завершается.
Метод interrupt
Еще один способ вызова завершения или прерывания потока представляет метод interrupt() . Вызов этого метода устанавливает у потока статус, что он прерван. Сам метод возвращает true, если поток может быть прерван, в ином случае возвращается false.
При этом сам вызов этого метода НЕ завершает поток, он только устанавливает статус: в частности, метод isInterrupted() класса Thread будет возвращать значение true. Мы можем проверить значение возвращаемое данным методом и прозвести некоторые действия. Например:
class JThread extends Thread < JThread(String name)< super(name); >public void run() < System.out.printf("%s started. \n", Thread.currentThread().getName()); int counter=1; // счетчик циклов while(!isInterrupted())< System.out.println("Loop " + counter++); >System.out.printf("%s finished. \n", Thread.currentThread().getName()); > > public class Program < public static void main(String[] args) < System.out.println("Main thread started. "); JThread t = new JThread("JThread"); t.start(); try< Thread.sleep(150); t.interrupt(); Thread.sleep(150); >catch(InterruptedException e) < System.out.println("Thread has been interrupted"); >System.out.println("Main thread finished. "); > >
В классе, который унаследован от Thread, мы можем получить статус текущего потока с помощью метода isInterrupted() . И пока этот метод возвращает false, мы можем выполнять цикл. А после того, как будет вызван метод interrupt, isInterrupted() возвратит true, и соответственно произойдет выход из цикла.
Возможный консольный вывод:
Main thread started. JThread started. Loop 1 Loop 2 Loop 3 Loop 4 JThread finished. Main thread finished.
Если основная функциональность заключена в классе, который реализует интерфейс Runnable, то там можно проверять статус потока с помощью метода Thread.currentThread().isInterrupted() :
class MyThread implements Runnable < public void run()< System.out.printf("%s started. \n", Thread.currentThread().getName()); int counter=1; // счетчик циклов while(!Thread.currentThread().isInterrupted())< System.out.println("Loop " + counter++); >System.out.printf("%s finished. \n", Thread.currentThread().getName()); > > public class Program < public static void main(String[] args) < System.out.println("Main thread started. "); MyThread myThread = new MyThread(); Thread t = new Thread(myThread,"MyThread"); t.start(); try< Thread.sleep(150); t.interrupt(); Thread.sleep(150); >catch(InterruptedException e) < System.out.println("Thread has been interrupted"); >System.out.println("Main thread finished. "); > >
Однако при получении статуса потока с помощью метода isInterrupted() следует учитывать, что если мы обрабатываем в цикле исключение InterruptedException в блоке catch, то при перехвате исключения статус потока автоматически сбрасывается, и после этого isInterrupted будет возвращать false.
Например, добавим в цикл потока задержку с помощью метода sleep:
public void run() < System.out.printf("%s started. \n", Thread.currentThread().getName()); int counter=1; // счетчик циклов while(!isInterrupted())< System.out.println("Loop " + counter++); try< Thread.sleep(100); >catch(InterruptedException e) < System.out.println(getName() + " has been interrupted"); System.out.println(isInterrupted()); // false interrupt(); // повторно сбрасываем состояние >> System.out.printf("%s finished. \n", Thread.currentThread().getName()); >
Когда поток вызовет метод interrupt, метод sleep сгенерирует исключение InterruptedException, и управление перейдет к блоку catch. Но если мы проверим статус потока, то увидим, что метод isInterrupted возвращает false. Как вариант, в этом случае мы можем повторно прервать текущий поток, опять же вызвав метод interrupt(). Тогда при новой итерации цикла while метода isInterrupted возвратит true, и поизойдет выход из цикла.
Либо мы можем сразу же в блоке catch выйти из цикла с помощью break:
while(!isInterrupted()) < System.out.println("Loop " + counter++); try< Thread.sleep(100); >catch(InterruptedException e) < System.out.println(getName() + " has been interrupted"); break; // выход из цикла >>
Если бесконечный цикл помещен в конструкцию try. catch, то достаточно обработать InterruptedException:
public void run() < System.out.printf("%s started. \n", Thread.currentThread().getName()); int counter=1; // счетчик циклов try< while(!isInterrupted())< System.out.println("Loop " + counter++); Thread.sleep(100); >> catch(InterruptedException e) < System.out.println(getName() + " has been interrupted"); >System.out.printf("%s finished. \n", Thread.currentThread().getName()); >
После завершения основного потока программы, JVM продолжает выполянть непонятные потоки
Использую proprietary библиотеку. В классе main() создаю экземляр Thread-safe configurated класса из этой библиотеки. Затем, в мультитрейдинге, используя этот единственный экземляр, запускаю методы этого класса с разными параметрами. Каждому созданному потоку делаю join() . По окончанию этих методов и метода main() имею необходимый результат. Однако даже после окончания метода main() в командной строке продолжает мигать курсор, говорящий о том что программа продолжает выполняться. Полагаю, что это потоки, запущенные методами этого класса (хотя сами методы завершили работу!). Как я могу программно завершить работу этих потоков? К исходникам доступа не имею, от этой библиотеки отказаться не могу.
Отслеживать
76.7k 6 6 золотых знаков 54 54 серебряных знака 120 120 бронзовых знаков
задан 28 мар 2016 в 12:18
121 9 9 бронзовых знаков
так как библиотека неизвестна, то рекомендую почитать документацию к ней и поспрашивать разработчиков этой библиотеки. Если и это не поможет, то посмотреть отладчиком/профайлером/дизамссемблером и разобраться. Если и это не поможет — написать библиотеку самому. В случае, если и после этого решение не будет найдено — пойти в другую компанию.
28 мар 2016 в 12:25
Если вам дан исчерпывающий ответ, отметьте его как верный (галка напротив выбранного ответа).
29 мар 2016 в 6:32
1 ответ 1
Сортировка: Сброс на вариант по умолчанию
Краткая справка о потоках в Java. Потоки бывают двух видов:
- Daemon (демон) — служебный поток
- Основной поток (не-демон)
JVM завершает работу программы, когда все ее потоки, не являющиеся демонами, завершены. Как только завершается последний не-демон-поток, все демоны сразу уничтожаются. Поэтому, чтобы гарантированно завершить все потоки после завершения вашего основного потока, нужно, чтобы они были демонами. И вот тут есть удобная штука: демон-поток может порождать только демоны. Поэтому вам нужно в мейне создать новый поток, сделать его демоном, и уже в нём запускать сторонние библиотеки. Когда ваш главный поток отработает, все демоны тут же завершатся. Пример, как это сделал я, когда столкнулся с такой же проблемой:
Thread daemon = new Thread() < @Override public void run() < bot.init(log); //в этом методе запускаются потоки сторонних библиотек >>; daemon.setDaemon(true); //делаем поток демоном daemon.start(); //запускаем. Все потоки, порожденные внутри него, //будут тоже демонами, и не будут мешать завершению программы
Только необходимо учитывать, что как только ваш main завершит работу, все демоны будут завершены (если нет других не-демонов). Советую основательно подумать, как лучше разделить потоки на основные и служебные, в случае крайней необходимости можно подождать завершения демона с помощью известного вам join() .
Жизненный цикл потока в Java
Эта статья направлена на объяснение различных состояний потока в мире Java. Если вы новичок в области многопоточного программирования, попробуйте сначала почитать про потоки что-нибудь базовое.
Согласно Sun Microsystems, существует четыре состояния жизненного цикла потока Java. Вот они:
- New — поток находится в состоянии New , когда создается экземпляр объекта класса Thread , но метод start не вызывается.
- Runnable — когда для объекта Thread был вызван метод start . В этом состоянии поток либо ожидает, что планировщик заберет его для выполнения, либо уже запущен. Назовем состояние, когда поток уже выбран для выполнения, “работающим” ( running).
- Non-Runnable(Blocked , Timed-Waiting) — когда поток жив, то есть объект класса Thread существует, но не может быть выбран планировщиком для выполнения. Он временно неактивен.
- Terminated — когда поток завершает выполнение своего метода run , он переходит в состояние terminated (завершен). На этом этапе задача потока завершается.
Ниже дано схематическое представление жизненного цикла потока в Java:
Минуточку! Звучит замечательно, но что это за “планировщик” такой? Я вызвал метод start , так почему бы системе просто не запустить мой поток, а не ждать в рабочем состоянии, пока планировщик его подберет?
Хороший вопрос! Планировщик — это программное обеспечение, которое используется для отслеживания задач компьютера. Он отвечает за назначение задач ресурсам, которые могут совершать работу. Мы не будем углубляться в логику, которую реализует планировщик. На данный момент достаточно знать, что планировщик имеет контроль над тем, какая задача должна быть назначена какому аппаратному ресурсу и когда, исходя из доступности ресурса и состояния задачи.
Выходит, планировщик решает, когда назначить задачу требуемому ресурсу. Но как задача переходит в нерабочее состояние? Как и почему поток должен отказаться от процессорного времени и приостановить выполнение? Это происходит по выбору или вынужденно?
Что же. Поток может находиться в нерабочем состоянии по разным причинам — иногда принудительно, иногда по собственному выбору. Вынужденные причины могут заключаться в том, что он ожидает операции ввода-вывода, например получения сообщения по порту, или он может ждать объект, который удерживается другим потоком. Последний сценарий приводит к появлению синхронизированного объекта. Когда поток обращается к синхронизированному объекту, он создает блокировку на этом объекте. Блокировка — это что-то вроде временного контракта между потоком и объектом, который дает потоку эксклюзивный доступ к объекту, запрещая доступ любому другому потоку. Для обеспечения этого контракта Java связывает с каждым объектом монитор. Поток также может быть перемещен планировщиком в нерабочее состояние (спящий режим) на основе логики совместного использования ресурсов планировщика.
Потоки могут перейти в нерабочее состояние по выбору. То есть по выбору программиста. Программист может написать метод потока ( run или любой другой метод, который вызывается внутри run ) таким образом, чтобы тот намеренно уступал процессорное время. Так делается, чтобы получить максимальную отдачу от доступных вычислительных мощностей или вызвать задержки после выполнения определенной части потока. Давайте посмотрим, какие методы добровольного отказа от процессорного времени нам доступны:
- sleep(long millis) — этот метод инициирует у вызывающего потока спящий режим на время, указанное в качестве параметра. Важно отметить, что при вызове sleep поток отдает процессорное время, но блокировка объектов не отменяются. После выхода из спящего режима поток возвращается в рабочее состояние, ожидая, пока планировщик заберет его для выполнения. Это обычно применяется для вызова задержки в части выполнения потока.
- wait() или wait(long timeout) — этот метод заставляет поток отказаться от процессорного времени, а также снять любые блокировки объектов. Он может быть вызван с параметром timeout . При вызове без тайм-аута поток остается в неработающем состоянии бесконечно, пока другой поток не вызовет метод notify() или notifyAll() . При вызове с параметром timeout поток ожидает не более продолжительности тайм-аута, а затем автоматически переходит в состояние runnable . Этот метод необходим в ситуациях, когда несколько потоков должны работать синхронно.
- yield() — этот метод представляет собой своего рода уведомление планировщика о том, что поток готов отказаться от выполнения. Затем планировщик, основываясь на других имеющихся потоках и их приоритетах, решает: хочет ли он переместить вызывающий поток в состояние runnable и предоставить процессорное время другим потокам или продолжать выполнять существующий поток. Лично мне этот метод кажется весьма полезным. Если мы знаем, что выполнение метода/функции займет много времени и что задача не является срочной, мы можем написать ее со стратегически расположенными вызовами метода yield , чтобы планировщик мог использовать процессор для выполнения потоков с более высоким приоритетом и более коротким временем выполнения.
- join() — вызывается для приостановки выполнения программы до тех пор, пока поток, вызывающий метод join , не будет завершен.
Время запачкать руки! Напишем-ка небольшой код, чтобы создать пару потоков и проверить их состояние на протяжении всего выполнения.
Простое выполнение единственного потока
package multithreadingPackage;
class Thread1 implements Runnable @Override
public void run() System.out.println("We are inside the run function."
+ "The thread is in the \"" + Thread.currentThread().getState() + "\" state.");
>
>
public class BasicThreadLifeCycleDemo public static void printThreadState(Thread threadToCheck) System.out.println("The thread is in the \"" + threadToCheck.getState() + "\" state.");
>
public static void main(String[] args) Thread t1 = new Thread(new Thread1());
printThreadState(t1);
t1.start();
printThreadState(t1);
for(int i=0; i <=10000; i++) for(int j=0; j <=10000; j++)
>
>
printThreadState(t1);
>
>
Этот код выведет следующее:
Давайте попробуем понять, что здесь происходит.
Функция printThreadState выводит текущее состояние потока. Впервые она вызывается после создания экземпляра объекта Thread , и ее вывод соответствует тому, что мы узнали недавно. Поток оказался в состоянии new . Теперь внимательно наблюдайте за выводом.
После метода start мы выполнили метод printThreadState , и поток перешел в состояние runnable . Это происходит до того, как планировщик передал поток для выполнения, потому что если бы поток уже был запущен, мы получили бы в выводе инструкцию print , написанную внутри метода run класса Thread1 . Отсюда можно видеть, что поток выполняется.
Обратите внимание, что поток так и находится в состоянии runnable , поскольку, как упоминалось в начале этой статьи, Java определяет только четыре состояния в жизненном цикле потока. Состояние running фигурирует в этой статье только для упрощения понимания.
Наконец, после завершения выполнения метода run , поток тоже завершается, а затем уничтожается. Циклы for в программе существуют только затем, чтобы дать потоку достаточно времени для завершения выполнения.
2. Выполнение нескольких потоков
1. Влияние синхронизации
Теперь давайте рассмотрим сценарии с синхронизированными блоками кода. Мы напишем программу для запуска двух потоков, которые пытаются получить доступ к одному и тому же экземпляру класса. Мы запустим программу дважды: один раз без ключевого слова synchronized в объявлении метода и один раз с ключевым словом synchronized .
package multithreadingPackage.objectsForDemo;
public class Person implements Runnable private String name;
private String job;
private String address;
public Person(String name, String job, String addr) this.name = name;
this.job = job;
this.address = addr;
>
/*Non-synchronized version.
To convert this to synchronized block, replace method declaration with
public synchronized void printPersonDetails() < */
public void printPersonDetails() String threadName = Thread.currentThread().getName();
System.out.println("-------------------------------");
System.out.println(threadName + " holds lock?- " + Thread.currentThread().holdsLock(this));
System.out.println(threadName + " Name - " + this.name);
System.out.println(threadName + " Job - " + this.job);
System.out.println(threadName + " Address - " + this.address);
System.out.println("-------------------------------");
>
@Override
public void run() printPersonDetails();
>
>package multithreadingPackage;
import multithreadingPackage.objectsForDemo.Person;
public class SynchronizationDemo public static void printThreadState(Thread threadToCheck) System.out.println("Thread \"" + threadToCheck.getName() + "\" is in the \"" + threadToCheck.getState() + "\" state.");
>
public static void main(String[] args) Person person = new Person("Rajat", "Blogger", "Ireland");
Thread t1 = new Thread(person);
t1.setName("FirstThread");
Thread t2 = new Thread(person);
t2.setName("SecondThread");
t1.start();
t2.start();
>
>
На рис. 3 показан вывод без ключевого слова synchronized, а на рис. 4 — с ключевым словом synchronized.
Почему вывод такой рандомный?
Потому что два потока выполняются параллельно. Обратите внимание: статус блокировки потоков четко false , поэтому любой поток может получить доступ к объекту в одно время.
А поскольку мы использовали ключевое слово synchronized для получения вывода на рис.4, то первый поток, получивший доступ к объекту, удерживал его блокировку. Это преграждало другому потоку доступ к объекту до завершения выполнения первого потока.
2. Методы “sleep” и “yield”
public void printPersonDetails() String threadName = Thread.currentThread().getName();
System.out.println("-------------------------------");
System.out.println(threadName + " holds lock?- " + Thread.currentThread().holdsLock(this));
if(threadName.equals("FirstThread")) try Thread.currentThread().sleep(2000);
> catch (InterruptedException e) e.printStackTrace();
>
>
System.out.println(threadName + " Name - " + this.name);
System.out.println(threadName + " Job - " + this.job);
System.out.println(threadName + " Address - " + this.address);
System.out.println("-------------------------------");
>public static void main(String[] args) Person person = new Person("Rajat", "Blogger", "Ireland");
Thread t1 = new Thread(person);
t1.setName("FirstThread");
Thread t2 = new Thread(person);
t2.setName("SecondThread");
t1.start();
t2.start();
for(int i=0; i <=10000; i++) for(int j=0; j <=10000; j++)
>
>
printThreadState(t1);
>
Измените код в файлах SynchronizationDemo.java и Persons.java , как показано в вышеприведенном фрагменте кода. Запустите код как в синхронизированном, так и в несинхронизированном режимах. Мы переводим первый поток в спящий режим на две секунды, как только начинается его выполнение.
Как и ожидалось, в несинхронизированном режиме оба потока не удерживают никакой блокировки на объекте. FirstThread переходит в Timed_Waiting при вызове метода sleep . Второй поток продолжает выполнение, так как нет блокировки его доступа к объекту.
В синхронизированном режиме потоки удерживают блокировки, и поэтому, даже когда первый поток находится в состоянии Timed_Waiting , второй поток все равно не может получить доступ к объекту.
Теперь замените метод sleep на yield и удалите блок try-catch в файле Persons.java . Результат будет таким же, как и в разделе 2.1. Этот метод будет полезен для задач, требующих больших вычислительных мощностей, но меньшей срочности.
3. Методы “wait” и “notify”
Что делать, если мы не хотим, чтобы поток заканчивал выполнение, пока какой-то другой поток не скажет ему продолжить?
Допустим, у нас есть учетная запись у поставщика широкополосных сетевых услуг и мы хотим добавить на баланс еще данных. Если у нашей учетной записи недостаточно средств, мы не сможем пополнить счет для передачи данных. Таким образом, поставщик услуг будет ждать, пока мы добавим на аккаунт средства, чтобы завершить дозакупку. Давайте взглянем на код.
package multithreadingPackage;
class BroadbandAccount private double dataInGb;
private double moneyInEuros;
public BroadbandAccount(double data, double money) this.dataInGb = data;
this.moneyInEuros = money;
>
public synchronized void addData() System.out.println("Current balance = " + this.moneyInEuros);
if(moneyInEuros <20) System.out.println("You don't have enough balance. Waiting for you to add money");
try wait();
> catch (InterruptedException e) e.printStackTrace();
>
>
System.out.println("Adding data and deducting money");
this.moneyInEuros -= 20;
this.dataInGb += 20;
System.out.println("Remaining Balance = " + this.moneyInEuros + ". Remaining data = " + this.dataInGb);
>
public synchronized void addMoney() System.out.println("Adding 20 Euros");
this.moneyInEuros += 20;
System.out.println("Added. New balance = " + this.moneyInEuros);
notify();
>
>
public class WaitNotifyImplementationDemo
public static void main(String[] args) BroadbandAccount account = new BroadbandAccount(1, 10);
new Thread() public void run() account.addData();
>
>.start();
new Thread() public void run() account.addMoney();
>
>.start();
>
>
Обратите внимание, что методы потока синхронизированы, поэтому только один поток единовременно может получить доступ к объекту BroadBandAccount . Вывод этого кода будет зависеть от того, какой поток был выбран планировщиком первым. Если второй поток, то есть поток добавления денег, будет выполнен первым, то денег будет достаточно для пополнения счета и следующему потоку в мониторе объекта, то есть потоку пополнения, не придется ждать (переходить в нерабочее состояние). Однако, если поток add data выполняется первым, он будет переведен в нерабочее состояние из-за недостаточного баланса и будет ждать завершения выполнения потока add money и вызова метода notify . Как думаете, что произойдет, если вы не вызовете метод notify в последнем сценарии? Попробуйте разобраться сами .
Кроме того, вы заметили другой стиль создания экземпляра объекта Thread ? Это происходит потому, что в классе может быть только один метод run . Я хотел выполнить два разных метода того же класса, что и потоки, поэтому динамически определил разные методы запуска для обоих потоков.
4.Метод “join”
Метод join используется, когда мы хотим остановить выполнение программы до тех пор, пока вызывающий поток не завершит выполнение. Можете ли вы представить себе такой сценарий?
Рассмотрим объект Person , который мы приводили как пример выше. Допустим, Job и Address — это не просто строковые значения, а объекты. Что если у них есть свои собственные таблицы базы данных и их значения оттуда нужно извлечь, чтобы отобразить фрагменты информации о человеке Person ? У нас может быть два потока, которые создают связи с каждой таблицей — работы и адресы, — извлекают записи в соответствии с конкретным Person , а затем отображают полные сведения о человеке. Но здесь нужна осторожность. Если мы извлекаем данные с помощью потоков, а затем создаем экземпляр объекта Person с этими значениями, нет гарантии, что потоки извлекут данные к моменту, когда выполнится команда создания экземпляра объекта. Вот тут-то на сцене и появляется join . Взгляните на приведенный ниже код:
package multithreadingPackage;
class Address implements Runnable private String address;
public String getAddress() return address;
>
public Address()
>
@Override
public void run() this.address = "Dublin";
>
>
class Job implements Runnable <
private String job;
public String getJob() return job;
>
public Job()
>
@Override
public void run() try Thread.currentThread().sleep(2000);
> catch (InterruptedException e) // TODO Auto-generated catch block
e.printStackTrace();
>
this.job = "Software Developer";
>
>
class Person private String name;
@Override
public String toString() return "Person [name=" + name + ", job=" + job.getJob() + ", address=" + address.getAddress() + "]";
>
private Job job;
private Address address;
public Person(String name, Address address, Job job) this.name = name;
this.address = address;
this.job = job;
>
>
public class JoinImplementationDemo
public static void main(String[] args) Address address = new Address();
Thread tAddress = new Thread(address);
Job job = new Job();
Thread tJob = new Thread(job);
tJob.start();
tAddress.start();
try tAddress.join();
tJob.join();
> catch (InterruptedException e) e.printStackTrace();
>
Person person = new Person("Rajat", address, job);
System.out.println(person.toString());
>
>
Вывод без join :
Person [name=Rajat, job=null, address=Dublin]
Вывод с join :
Person [name=Rajat, job=Software Developer, address=Dublin]
Обратите внимание на результаты и сравните их. Я добавил метод sleep в поток Job , чтобы увеличить время выполнения и более наглядно продемонстрировать результаты. Без вмешательства join , выполнение не ждет, когда потоки завершат выполнение, и создает объект Person . В результате объект содержит значение null в атрибуте job . Тогда как, когда мы использовали метод join , все отработало гладко. Проще говоря, распараллеливание задач, требующих большой вычислительной мощности, сокращает общее время выполнения, но надо быть осторожным, чтобы в процессе не потерять какую-то информацию. Метод join — один из лучших инструментов для этого.
Занимаясь многопоточным программированием, я провел много лет в поисках идеального способа реализации, и это было трудно. Итак, я попытался объяснить вам многопоточное программирование. Рекомендую поиграть с кодом из этой статьи, чтобы исследовать его глубже и укрепить свое понимание предмета.
Все, что вам нужно, — практика.
- Функции Java 15: скрытые и запечатанные классы, сопоставление шаблонов и текстовые блоки
- Учимся избегать null-значений в современном Java. Часть 1
- Фреймворк Executor в Java
Многопоточность в 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("Значение java">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("Значение java">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. Если эта статья окажется полезной, то в следующей я расскажу о проблемах совместного доступа потоков к ресурсам и о методах их решения.
- Java
- многопоточное программирование
- Программирование
- Java
- Параллельное программирование