В предыдущей статье мы дали определение потокам в Java, научились определять потоки с помощью расширения класса java.lang.Thread и реализации интерфейса java.lang.Runnable, инстанцировать потоки и запускать их.
Также мы рассмотрели как выполняется несколько потоков, немного поговорили о планировщике потоков и закончили первую часть рассмотрением состояний потоков и переходов между их состояниями.
В этой статье мы продолжим рассматривать тему потоков в Java. А именно, мы рассмотрим каким образом можно приостановить выполнение потока, разберемся с приоритетами, научимся соединять потоки в цепочку (join), и рассмотрим большую и важную тему – синхронизацию кода. Начнем.
Приостановка выполнения потока
Существует несколько способов приостановить выполнение потока. Рассмотрим в этом разделе три способа.
Отправка в сон. Sleeping
Метод sleep() – это статический метод класса Thread. Метод sleep() вызывает засыпание текущего потока на заданное время (в миллисекундах).
Какие могут быть причины для переключения потока в состояние сна? Вы можете решить что поток выполняется слишком быстро и таким образом притормозить ход его выполнения. Или может потребоваться создать какой-то механизм, который будет заставлять выполняться ваши потоки по очереди. Или, например, ваш поток непрерывно что-то обновляет через сеть и вы хотите уменьшить трафик и нагрузку на сервер путем перевода потока в состояние сна на определенное время после каждой порции обновлений.
Сделать это можно путем вызова метода Thread.sleep(), передав ему время в миллисекундах, следующим образом:
try { Thread.sleep(5*60*1000); // Спать 5 минут } catch (InterruptedException ex) { }
Обратите внимание на то, что метод sleep() может выбрасывать исключение типа InterruptedException, поэтому необходимо как-то обрабатывать это исключение.
Давайте изменим пример с несколькими потоками из первой части так, чтобы в нем использовался метод sleep():
package ru.topcode.threadomaniac; public class Starter { public static void main(String[] args) { NameRunnable nr = new NameRunnable(); Thread one = new Thread(nr); one.setName("Первый"); Thread two = new Thread(nr); two.setName("Второй"); Thread three = new Thread(nr); three.setName("Третий"); one.start(); two.start(); three.start(); } } class NameRunnable implements Runnable { public void run() { for (int x = 1; x <= 4; x++) { System.out.println("Запущен " + Thread.currentThread().getName() + ", x равен " + x); try { Thread.sleep(1000); } catch (InterruptedException ex) { } } } }
Запустив у себя этот пример, я получил следующий вывод в консоль:
Запущен Первый, x равен 1 Запущен Третий, x равен 1 Запущен Второй, x равен 1 Запущен Первый, x равен 2 Запущен Третий, x равен 2 Запущен Второй, x равен 2 Запущен Первый, x равен 3 Запущен Третий, x равен 3 Запущен Второй, x равен 3 Запущен Первый, x равен 4 Запущен Второй, x равен 4 Запущен Третий, x равен 4
Порядок выполнения потоков в этом примере так же не предсказуем. Мы не можем узнать как долго поток будет реально работать пока не уснет, а также не можем заведомо знать какой из потоков запустится когда один из них уйдет спать. Однако, метод sleep() является лучшим способом предоставить возможность выполниться всем трем потокам. Или, по крайней мере, можно гарантировать, что один поток не будет выполняться все время до окончания метода run(). Если поток по каким-то причинам будет прерван до его пробуждения, то возникнет исключительная ситуация InterruptedException.
То что время сна потока истекло и поток пробудился еще не означает что он сразу же перейдет в состояние running! Когда поток просыпается, он просто переходит в состояние runnable, а значит становится доступным для планировщика потоков. Поэтому, время, переданное в метод sleep() – это минимальное время в течение которого поток не будет работать.
Также всегда необходимо помнить что sleep() – это статический метод, поэтому не обманывайте себя, думая что один поток может уложить спать другой поток. Вы можете вызывать sleep() из любого места программы, но в результате ляжет спать только текущий поток.
Приоритеты потока и метод yield()
Чтобы разобраться как работает метод yield(), нужно сначала разобраться в концепции приоритетов потоков. Потоки всегда запускаются с некоторым приоритетом, который представляется в виде числа от 1 до 10.
Планировщик может быть двух видов: с приоритетом и без.
Планировщик с приоритетом предоставляет определённый отрезок времени для всех потоков, которые запущены в системе. Планировщик решает, какой поток запустится следующим или возобновит работу через некоторый постоянный период времени. Когда поток запустится, через этот определённый промежуток времени, то выполняющийся поток будет приостановлен и следующий поток возобновит свою работу.
Планировщик без приоритета решает, какой поток должен запуститься и выполняться до того, пока не закончит свою работу. Поток имеет полный контроль над системой столь долго, сколько ему захочется.
В большинстве JVM планировщик осуществляет планирование на основе приоритетов. Это означает, что если запускается поток с приоритетом выше, чем у текущего, то планировщик даст возможность выполниться этому потоку, отправив текущий в состояние runnable. Однако полагаться на приоритеты чтобы гарантировать предсказуемое поведение многопоточной программы тоже нельзя!
По умолчанию, значение приоритета потоку устанавливается таким же, какое имеет создавший его поток. Например, в коде
public class TestThreads { public static void main (String [] args) { MyThread t = new MyThread(); } }
поток с ссылкой t будет иметь такой же приоритет как у главного потока. Вы также можете установить приоритет явно с помощью вызова метода setPriority() экземпляра Thread.
FooRunnable r = new FooRunnable(); Thread t = new Thread(r); t.setPriority(8); t.start();
Значение приоритета по умолчанию равно 5, а класс Thread имеет три константы, определяющие диапазон приоритетов:
Thread.MIN_PRIORITY (1) Thread.NORM_PRIORITY (5) Thread.MAX_PRIORITY (10)
Итак, что же делает статический метод Thread.yield()? Метод yield() можно использовать для того чтобы принудить планировщик выполнить другой поток, который ожидает своей очереди. Тут опять же не предоставляется ни каких гарантий что один поток уступит другому. Метод yield() не пытается отправить поток в состояние waiting/sleeping/blocking, он пытается перевести его из состояния running в runnable и вызов этого метода может не возыметь ни какого эффекта.
Метод join()
Нестатический метод join() класса Thread позволяет один поток присоединить к другому потоку. Например, если у вас есть поток B, который по логике не может выполнить свою работу пока не завершится другой поток, то вы можете присоединить поток B к потоку A. Это значит что поток B не станет runnable пока поток A не завершится (пока не станет мертвым).
Thread t = new Thread(); t.start(); t.join();
В приведенном коде берется текущий запущенный поток (если этот код вставить в метод main(), то текущим потоком будет main thread) и присоединяет его в конец потока на который ссылается t. Это блокирует текущий поток до того времени пока поток t не умрет. Другими словами, код t.join() означает “присоединить текущий поток в конец t, таким образом, чтобы текущий поток заблокировался пока t не умрет”. Вы также можете вызвать одну из перегруженных версий метода join(), которая принимает значение таймаута, которое, в свою очередь, означает “ждать, пока поток t не умрет, но если это займет больше чем 5000 миллисекунд, то прервать ожидание и перевести ждущий поток в состояние runnable”.
На следующем рисунке этот процесс показан наглядно:
Картинка взята из книги SCJP Sun Certified Programmer for Java 6 Study Guide (Exam 310-065).
На данный момент мы рассмотрели три способа приостановить выполнение потока:
- Вызов sleep(). Гарантированно прекращает выполнение текущего потока как минимум на указанный отрезок времени.
- Вызов yield(). Ничего не гарантирует, но делает попытку перевести текущий поток из состояния running в состояние runnable чтобы поток с таким же приоритетом имел шанс поработать.
- Вызов join(). Гарантированно останавливает выполнение текущего потока до завершения выполнения потока к которому он прицеплен.
Помимо этих трех, есть еще способы остановить выполнение потока:
- Дать потоку закончить свое выполнение
. - Вызвать метод wait().
- Поток не может получить блокировку на объект, метод которого пытается выполниться.
Синхронизация кода
Представьте себе хаос, который наступит если два разных потока будут иметь доступ к одному объекту и оба эти потока будут вызывать методы, которые изменяют состояние объекта. Иными словами, что произойдет если два разных потока вызовут, например, мутатор (сеттер, метод set…) из одного объекта. Это даже страшно представить!
Насладившись ужасом, давайте посмотрим на пример того, что может произойти. Следующий код показывает что происходит когда два различных потока имеют доступ к данным одной учетной записи. Представьте себе два человека, каждый имеет свою чековую книжку для одного расчетного счета (или два человека с разными кредитными картами, которые привязаны к одному банковскому счету).
В данном примере у нас есть класс Account, который представляет банковский счет. На счету лежит 50 рублей и этот счет может быть использован только для снятия. Вывод средств будет осуществлен даже если на счету недостаточно средств. Счет просто уменьшается на снимаемую сумму:
Теперь начинается самое интересное. Представьте себе пару – Федя и Люся – оба имеют доступ к счету и оба хотят снять денег, но оба не хотят чтобы баланс был отрицательным, так как банк сдерет с них за это дикие проценты. Поэтому, перед тем как снять определенную сумму, они проверяют, есть ли на счете такая сумма и только потом приступают к операции вывода средств. Кроме того, разовый вывод ограничен суммой в 10 рублей, поэтому для совершения операции на счету должно быть не менее 10 рублей. Процедура вывода происходит в два этапа:
- Проверка баланса.
- Если денег на счету достаточно, то осуществить вывод.
Что произойдет, если Нечто отделит шаг 1 от шага 2? Например, Люся проверила баланс и видит что на счету как раз 10 рублей. Но прежде чем она успеет вывести эти деньги, Федя проверяет баланс и видит что там есть 10 рублей. А значит, Федя видит уже “плохие” данные. Теперь Люся снимает 10 рублей и кажется что все хорошо. но Федя уже тоже проверил баланс и считает что деньги на счету есть, а поэтому снимает 10 рублей. В итоге, семья попала в рабство банку.
Ниже мы рассмотрим пример реализации этой ситуации в коде. логика следующая:
- Объект Runnable содержит ссылку на одну учетную запись (Account).
- Стартуют два потока, представляющих Люсю и Федю, соответственно в конструктор Thread для обоих передается один и тот же объект Runnable.
- Первоначальный баланс счета – 50 рублей, вывод осуществляется порциями по 10 рублей за раз.
-
В методе run() мы выполняем пять проходов цикла, а в каждом проходе мы
- делаем вывод если на счете достаточно средств
- печатаем отчет в том случае, если баланс опустился ниже нуля, но такое никогда не должно произойти так как мы проверяем баланс перед снятием.
-
Метод makeWithdrawal() в тестовом классе будет делать следующее:
- Проверяет баланс на достаточное количество средств.
- Если денег достаточно, то печатаем имя того, кто сделал вывод средств.
- Отправляет поток спать на 500 миллисекунд – этого достаточно для того, чтобы дать другому потоку тоже проверить счет.
- Проснувшись, поток снимает средства и печатает отчет об этом.
- Если средств не хватает, печатает соответствующий отчет.
Вот код:
Класс Account:
package ru.topcode.threadomaniac; class Account { private int balance = 50; public int getBalance() { return balance; } public void withdraw(int amount) { balance = balance - amount; } }
Класс AccountDanger:
package ru.topcode.threadomaniac; public class AccountDanger implements Runnable { private Account acct = new Account(); public static void main(String[] args) { AccountDanger r = new AccountDanger(); Thread one = new Thread(r); Thread two = new Thread(r); one.setName("Федя"); two.setName("Люся"); one.start(); two.start(); } public void run() { for (int x = 0; x < 5; x++) { makeWithdrawal(10); if (acct.getBalance() < 0) { System.out.println("Баланс меньше 0!"); } } } private void makeWithdrawal(int amt) { if (acct.getBalance() >= amt) { System.out.println("Пользователь " + Thread.currentThread().getName() + " совершил проверку счета"); try { Thread.sleep(500); } catch (InterruptedException ex) { } acct.withdraw(amt); System.out.println(Thread.currentThread().getName() + " вывел средства"); } else { System.out.println("Недостаточно средств на счете для клиента с именем " + Thread.currentThread().getName() + ". Доступно для вывода: " + acct.getBalance()); } } }
У меня получился следующий вывод:
Пользователь Федя совершил проверку счета Пользователь Люся совершил проверку счета Федя вывел средства Пользователь Федя совершил проверку счета Люся вывел средства Пользователь Люся совершил проверку счета Федя вывел средства Пользователь Федя совершил проверку счета Люся вывел средства Пользователь Люся совершил проверку счета Федя вывел средства Недостаточно средств на счете для клиента с именем Федя. Доступно для вывода: 0 Недостаточно средств на счете для клиента с именем Федя. Доступно для вывода: 0 Люся вывел средства Баланс меньше 0! Недостаточно средств на счете для клиента с именем Люся. Доступно для вывода: -10 Баланс меньше 0! Недостаточно средств на счете для клиента с именем Люся. Доступно для вывода: -10 Баланс меньше 0!
Из этого вывода видно, что уже в самом начале выполнения, Федя совершил проверку счета, но не успел снять средства, потому что сразу же за ним это сделала и Люся. Ну а дальше все пошло наперекосяк.
На следующем рисунке показан таймлайн, на котором видно что происходит, когда два потока обращаются к одному объекту.
Проблема, когда несколько потоков могут получить доступ к одному ресурсу и изменить его данные до того как логически атомарная операция будет завершена другим потоком, известна как “состязание потоков”.
Так как же избежать проблемы в нашем примере? Решение, на самом деле, довольно простое. Мы должны гарантировать, что проверка баланса и вывод средств будут выполняться как одна операция и никогда не будут разделяться. Такая операция называется “атомарной”.
Вы не можете гарантировать, что один единственный поток будет работать все время, которое требуется на выполнение атомарной операции, но вы можете гарантировать, что если даже работающий поток ушел из состояния running в какое-либо другое, то другие запущенные потоки никогда не увидят данные, находящиеся в промежуточном состоянии.
Иными словами, если Люся заснет после проверки баланса, то Федя сможет проверить баланс только после того как Люся проснется и завершит операцию вывода средств со счета.
Так как же защитить данные? Для этого нужно сделать две вещи:
- Установить для переменных уровень доступа private.
- Синхронизировать код, который изменяет переменные.
Мы можем решить все проблемы Феди и Люси, добавив в программу всего одно ключевое слово. Применим ключевое слово synchronized к методу makeWithdrawal():
private synchronized void makeWithdrawal(int amt) { if (acct.getBalance() >= amt) { System.out.println("Пользователь " + Thread.currentThread().getName() + " совершил проверку счета"); try { Thread.sleep(500); } catch (InterruptedException ex) { } acct.withdraw(amt); System.out.println(Thread.currentThread().getName() + " вывел средства"); } else { System.out.println("Недостаточно средств на счете для клиента с именем " + Thread.currentThread().getName() + ". Доступно для вывода: " + acct.getBalance()); } }
Теперь мы можем гарантировать что если один поток (Люся или Федя) начал процесс вывода средств со счета (вызов метода makeWithdrawal()), то другой поток не сможет войти в этот метод пока первый не выйдет из него. Новый консольный вывод подтверждает это:
Пользователь Федя совершил проверку счета Федя вывел средства Пользователь Федя совершил проверку счета Федя вывел средства Пользователь Федя совершил проверку счета Федя вывел средства Пользователь Федя совершил проверку счета Федя вывел средства Пользователь Федя совершил проверку счета Федя вывел средства Недостаточно средств на счете для клиента с именем Люся. Доступно для вывода: 0 Недостаточно средств на счете для клиента с именем Люся. Доступно для вывода: 0 Недостаточно средств на счете для клиента с именем Люся. Доступно для вывода: 0 Недостаточно средств на счете для клиента с именем Люся. Доступно для вывода: 0 Недостаточно средств на счете для клиента с именем Люся. Доступно для вывода: 0
Синхронизация и блокировки
Синхронизация происходит за счет блокировок. Каждый объект в Java имеет встроенную блокировку, которая вступает в игру только в том случае, если объект имеет синхронизированные методы. Когда мы входим в синхронизированный нестатический метод, мы автоматически получаем блокировку, связанную с текущим экземпляром класса (текущий – это тот, код которого выполняется в данный момент).
Поскольку существует только одна блокировка на объект, то если один поток захватил блокировку, ни один другой поток не может ее захватить, пока первый не отпустит ее. Это означает, что ни какой другой поток не может войти в синхронизированный код пока блокировка не будет освобождена. Запомните следующие ключевые моменты блокировки и синхронизации:
- Только методы (или блоки) могут быть synchronized.
- Каждый объект имеет только одну блокировку.
- Не все методы в классе нуждаются в синхронизации. Класс может иметь как синхронизированные методы, так и не синхронизированные.
- Если два потока собираются выполнить синхронизированный метод в классе, и оба потока используют один и тот же экземпляр класса для вызова метода, то только один поток сможет выполнить метод. Другой поток будет ждать пока первый не закончит выполнение метода. Иными словами, как только поток получает блокировку на объект, ни один другой поток не может войти ни в один из синхронизированных методов класса (для этого объекта).
- Если класс имеет и синхронизированные и не синхронизированные методы, то несколько потоков могут получить доступ к не синхронизированным методам класса. Если ваш метод не имеет доступа к данным, которые вы хотите защитить, то вам не нужно объявлять этот метод как синхронизированный. В некоторых случаях синхронизация может привести к взаимной блокировке, поэтому не стоит ей злоупотреблять.
- Если поток уходит спать, он удерживает все свои блокировки, а не освобождает их.
- Поток может захватить несколько блокировок. Например, поток может войти в синхронизированный метод, таким образом захватив блокировку, а затем сразу же вызвать синхронизированный метод другого объекта, получив также и его блокировку.
- Можно синхронизировать не весь метод, а только блок кода.
Поскольку синхронизация “вредит” параллелизму, необходимо синхронизировать наименьшее количество кода, которого достаточно для защиты данных. Уменьшить масштаб синхронизации можно использовать синхронизированный блок:
class SyncTest { public void doStuff() { System.out.println("не синхронизированный"); synchronized (this) { System.out.println("синхронизированный"); } } }
Когда поток вызывает код из синхронизированного блока, включая любые вызовы методов из синхронизированного блока, то говорят что код выполняется в синхронизированном контексте. Может возникнуть вопрос, а что синхронизируется или какой объект получит блокировку?
Когда вы синхронизируете метод, объект используемый для вызова этого метода получает блокировку. Но когда вы синхронизируете блок кода, вы должны указать объект, который вы хотите использовать в качестве блокировки, поэтому возможно, например, использовать некий сторонний объект в качестве блокировки для этого куска кода. Это дает вам возможность иметь больше одной блокировки для синхронизированного кода в рамках одного объекта.
Можно использовать для синхронизации текущий экземпляр (this), как показано в примере ниже. Это означает что мы всегда можем заменить синхронизированные методы на не синхронизированные, но содержащие синхронизированный блок. Вот пример:
public synchronized void doStuff() { System.out.println("synchronized"); }
эквивалентен
public void doStuff() { synchronized (this) { System.out.println("synchronized"); } }
В практическом плане эти методы полностью эквивалентны. Первая форма записи более короткая, однако вторая может оказаться более гибкой.
Что насчет статических методов? Они могут быть synchronized?
Статические методы также могут быть синхронизированы. Существует только одна копия статических данных, которую нужно защитить, поэтому необходима только одна блокировка на класс для синхронизации статических методов.
А что же используется в качестве блокировки при синхронизации статического метода? Каждый загруженный класс в Java имеет соответствующий экземпляр класса java.lang.Class, представляющий этот класс. Именно этот объект и используется для защиты статических синхронизированных методов класса.
static int count; public static synchronized int getCount() { return count; }
Этот метод также можно переписать с помощью синхронизированного блока. Предположим, что метод getCount() находится в классе MyClass, тогда:
public static int getCount() { synchronized (MyClass.class) { return count; } }
или так:
public static void classMethod() { Class cl = Class.forName("MyClass"); synchronized (cl) { // какой-то код } }
Что происходит если поток не может получить блокировку?
Если поток пытается войти в синхронизированный метод, а блокировка уже захвачена, то, по сути, поток переходит в некий пул для конкретного объекта и сидит там пока блокировка не снимется. Когда блокировка освободится, этот поток снова перейдет в состояние runnable/running. То, что блокировка освобождена еще не означает, что ее получит какой-то конкретный поток. Одну блокировку могут ожидать, например, три потока и нет ни какой гарантии, что поток, который ждал дольше всех, получит блокировку первым.
Думая о блокировке, важно обращать внимание на то, какие объекты используются для блокировки:
- Потоки, вызывающие нестатические синхронизированные методы в одном классе будут блокировать другие потоки если они вызываются с помощью того же экземпляра класса.
- Потоки, вызывающие статические синхронизированные методы в одном классе будут всегда блокировать друг друга если все они используют один и тот же экземпляр Class.
- Статические синхронизированные методы и нестатические синхронизированные методы не будет блокировать друг друга, никогда. Статические методы блокируются на экземпляре класса Class в то время как нестатические методы блокируются на текущем экземпляре (this). Эти действия не мешают друг другу.
- При использовании синхронизированных блоков, необходимо обращать внимание на то, какой объект используется для блокирования. Потоки, которые синхронизируются на том же объекте будут блокировать друг друга. Потоки, которые синхронизируются на разных объектах, не будут блокировать друг друга.
В следующей таблице приведены методы, сгруппированные по признаку “отказывается ли поток от блокировки в результате вызова метода”.
| Отказывается от блокировок | Сохраняет блокировки | Класс, в котором находится метод |
|---|---|---|
| wait() | notify() | java.lang.Object |
| join() | java.lang.Thread | |
| sleep() | java.lang.Thread | |
| yield() | java.lang.Thread |
Потоко-безопасные классы
Когда класс тщательно синхронизирован, говорят что он является “потоко-безопасным” (“thread-safe”). Многие классы в Java API являются потоко-безопасными. Например, StringBuffer и StringBuilder практически идентичные классы, однако все методы в StringBuffer синхронизированы, а в StringBuilder – нет, поэтому использование StringBuffer в многопоточной среде безопасно, а StringBuilder – нет. Тем не менее, не всегда можно полагаться на то, что класс обеспечивает необходимую защиту. Вам все еще необходимо тщательно продумать как использовать эти классы. В качестве примера рассмотрим следующий класс.
package ru.topcode.threadomaniac; import java.util.Collections; import java.util.LinkedList; import java.util.List; public class NameList { private List names = Collections.synchronizedList( new LinkedList()); public void add(String name) { names.add(name); } public String removeFirst() { if (names.size() > 0) return (String) names.remove(0); else return null; } }
Метод Collections.synchronizedList() возвращает потоко-безопасный список, методы которого синхронизированы. Вопрос в том, может ли класс NameList использоваться безопасно с множеством потоков? На первый взгляд – может, поскольку данные в names защищены синхронизацией методов списка. Однако это не всегда так. Метод removeFirst() может выбросить исключение NoSuchElementException. В чем же проблема? Мы же проверяем size() списка names до удаления элемента, тогда почему код выполняется неправильно и бросает исключение? Давайте попробуем использовать NameList так:
public static void main(String[] args) { final NameList nl = new NameList(); nl.add("Ozymandias"); class NameDropper extends Thread { public void run() { String name = nl.removeFirst(); System.out.println(name); } } Thread t1 = new NameDropper(); Thread t2 = new NameDropper(); t1.start(); t2.start(); }
Может случиться так, что один из потоков будет удалять одно имя и распечатывать его, тогда другой попытается удалить имя и получит null. Это будет справедливо если думать о вызовах names.size() и names.get(0) в следующем порядке:
- t1 выполняет names.size(), который возвращает 1.
- t1 выполняет names.remove(0), который возвращает Ozymandias.
- t2 выполняет names.size(), который возвращает 0.
- t2 не вызывает names.remove(0).
Получаем следующий вывод в консоль:
Ozymandias null
Однако может произойти следующая ситуация:
- t1 выполняет names.size(), который возвращает 1.
- t2 выполняет names.size(), который возвращает 1.
- t1 выполняет names.remove(0), который возвращает Ozymandias.
- t2 выполняет names.remove(0), который выбрасывает исключительную ситуацию потому что список в настоящий момент пуст.
Необходимо понимать то, что в списке все методы синхронизированы, а это всего лишь означает что метод names.size() выполнится как транзакция, метод names.remove(0) тоже выполнится как атомарная операция, но ни что не мешает другому потоку влезть между двумя этими вызовами. Необходимо синхронизировать свой код. Вот пример без использования synchronizedList():
package ru.topcode.threadomaniac; import java.util.LinkedList; import java.util.List; public class NameList { private List names = new LinkedList(); public synchronized void add(String name) { names.add(name); } public synchronized String removeFirst() { if (names.size() > 0) return (String) names.remove(0); else return null; } }
Теперь метод removeFirst() синхронизирован и только один поток сможет войти в него. Мораль в том, что то, что класс определен как потоко-безопасный, еще не означает что он всегда потоко-безопасный. Синхронизации отдельных методов может быть недостаточно, может понадобиться синхронизация на более высоком уровне. Как только вы синхронизируете свой код, синхронизируемый список, который возвращается методом Collections.synchronizedList() может оказаться лишним.
Взаимная блокировка потоков (Deadlock)
Самое страшное что может случиться в Java-программе – это deadlock. Deadlock – это ситуация, когда два потока блокируются, при этом каждый ждет освобождения блокировки, которую захватил другой поток. Это означает, что потоки не получат ожидаемых блокировок никогда.
Простейший случай deadlock — это когда поток A получает блокировку ресурса AR1, затем пытается получить доступ к ресурсу AR2, который в это время уже захвачен потоком B, который в свою очередь пытается получить доступ к AR1.
Следующий пример демонстрирует deadlock:
package ru.topcode.threadomaniac; public class DeadlockRisk { private static class Resource { public int value; } private Resource resourceA = new Resource(); private Resource resourceB = new Resource(); public int read() { synchronized (resourceA) { // deadlock может быть здесь synchronized (resourceB) { return resourceB.value + resourceA.value; } } } public void write(int a, int b) { synchronized (resourceB) { // deadlock может быть здесь synchronized (resourceA) { resourceA.value = a; resourceB.value = b; } } } }
Предположим, что read() запускается первым потоком, а write() вторым. Если есть два разных потока значит чтение и запись могут вызываться независимо друг от друга, а следовательно в прокомментированных строках может возникнуть deadlock. Поток-читатель захватит resourceA, писатель – resourceB и оба остановятся в ожидании друг друга.
Приведенный пример легко исправить просто переставив местами строки synchronized(resourceB) и synchronized(resourceA) в методе write(), однако это всего лишь простой пример. Может возникнуть более сложная ситуация, которая потребует много времени для решения.
Продолжение читайте в статье “Потоки в Java. Часть 3“.
Дополнительная информация
- Теория и практика Java: Введение в неблокирующие алгоритмы
- Отчет о DeadLock’ах в работающем приложении
- Deadlocks
- Kathy Sierra, Bert Bates. SCJP Sun Certified Programmer for Java 6 Exam 310-065.





Pingback: Потоки в Java. Часть 1. | Java EE Dev
#1 by Alex on 16 Апрель 2010 - 13:13
Quote
За последнее время, что перечитал по работе с потоками, эта статья написана наиболее понятна и приятна для чтения.
но есть пару вопросов, почему упущен класс TimeUnit.
И, по-моему, sleep() не освобождает ресурсы занятые потоком, а yield() делает это. Поправте, если я не прав.
И почему ничего не рассказали про метод wait() и notify()?
#2 by Дмитрий Леонтьев on 16 Апрель 2010 - 16:40
Quote
sleep() действительно не освобождает ресурсы, а yield() просит поток освободить ресурс, но то что это произойдет сразу после вызова этого метода – не гарантируется. Все решает планировщик.
С TimeUnit я пока сам не разобрался, поэтому и не написал о нем.
Сейчас пишу “Часть 3″, скоро опубликую, там как раз все про методы из класса Object (wait(), notify() и т.д.).
Есть еще пакет java.util.concurrent про который я ничего не рассказал, но там много интересных вещей. В ближайшее время постараюсь заполнить пробелы.
#3 by Alex on 17 Апрель 2010 - 0:57
Quote
Вот кстати про java.util.concurrent будет классно на самом деле, потому что я очень часто с ним сталкиваюсь в многопоточности, очень удобно и быстро, и голова не болит за доступ к коллекция. Но вот как-то не хватает времени полностью сесть и от и до прочитать. Было бы классно, если этот рассказ был бы выдержан в духе этих 2 статей. Так же четко, коротко и по факту. А тебе на сама деле огромный респект, что проводишь свет в массы. не знаю правда, что является для тебя лучшим поощрением: посещаемость или же благодарность читателей.
#4 by Дмитрий Леонтьев on 17 Апрель 2010 - 13:06
Quote
Спасибо, Alex
Лучшим поощрением для меня является благодарность читателей. Хорошо если кому-то помог.
#5 by Anton on 18 Апрель 2010 - 19:25
Quote
Приятно читать ваши статьи – написаны простым понятным языком, и тем не менее сохраняется достаточная глубина сведений.
#6 by Дмитрий Леонтьев on 21 Апрель 2010 - 22:12
Quote
Anton, спасибо за теплые отзывы. Рад что мой блог оказался полезен вам )
#7 by Сергей on 22 Апрель 2010 - 17:40
Quote
я новичек в программировании и скажу, что Ваши статьи более понятны, чем чтение занудных книг, пасиб:)
Pingback: Потоки в Java. Часть 3. | Java EE Dev
#8 by Васильевич on 26 Апрель 2010 - 15:17
Quote
Не мешало бы в раздел “Дополнительная информация” добавить книгу, откуда вы взяли оригинальный текстЮ, код и картинки (книга Кати Сиерры и Берта Бейтса).
#9 by Дмитрий Леонтьев on 27 Апрель 2010 - 9:43
Quote
Здравствуйте, Васильевич. Добавил
Спасибо.
#10 by Васильевич on 27 Апрель 2010 - 13:15
Quote
Спасибо !
#11 by Sergey on 8 Июнь 2010 - 10:42
Quote
Дмитрий, на мой взгляд, вам необходимо писать более строго. Думаю, некоторые вольности изложения (особенно в разделе про блокировки) могут запросто сбить с толку человека, который не знаком с .
#12 by Sergey on 8 Июнь 2010 - 10:45
Quote
Дмитрий, на мой взгляд, вам необходимо писать более строго. Некоторые вольности изложения (особенно в разделе про блокировки) просто сбивают с толку.
#13 by Дмитрий Леонтьев on 13 Сентябрь 2010 - 14:31
Quote
Sergey, более строго можно почитать в JavaDoc и в книгах, которые в большом количестве продаются в магазинах как на английском, так и на русском языках.
Может быть мои примеры не всегда строги и местами являются надуманными, но каждый пример – это всего лишь определенная абстракция.
Спасибо за Ваше мнение.
#14 by simetrix on 2 Октябрь 2010 - 3:22
Quote
интересно все расписано, только осталось с TimeUnit разобраться
#15 by Alexey on 11 Октябрь 2010 - 19:17
Quote
Отличная статья, спасибо.
#16 by Pawel on 8 Март 2011 - 20:42
Quote
По-поводу метода join(). “Гарантированно останавливает выполнение текущего потока до завершения выполнения потока к которому он прицеплен.” Если я правильно понял, все остальные потоки приложения, неважно когда запущенные, должны приостановить свою работу, когда на одном из них вызывается join()? Поясните этот момент подробнее, пожалуйста! Я бы тут привел непонятный мне код, да места маловато:).
#17 by Артём Зцаринный on 19 Март 2011 - 19:10
Quote
Pawel, Вы почти правильно все поняли, за исключением того, что join() останавливает не все потоки приложения, а только тот – в котором вызван join(). Все довольно просто
#18 by Артём Зацаринный on 20 Март 2011 - 2:08
Quote
Еще один вариант решения проблемы с потокобезопасным List’ом:
#19 by Alexey on 27 Апрель 2011 - 13:28
Quote
Отличные статьи! Огромное спасибо, наконец то более отчетливо понимаю механизм потоков в Java.
#20 by Sergey on 25 Май 2011 - 23:53
Quote
Молодец! Очень хорошие статьи!
#21 by Сергей on 4 Сентябрь 2011 - 16:33
Quote
Огромное спасибо за статью, действительно легко и понятно написана! Автор – молодец! Юху!
#22 by Vasya on 1 Октябрь 2011 - 12:42
Quote
“Никакой” пишется вместе.
#23 by Оля on 16 Октябрь 2011 - 17:17
Quote
Спасибо за статью. Очень доступно объясняете.
#24 by Игорь on 10 Ноябрь 2011 - 16:51
Quote
Я немного не уловил – если поток получает блокировку на статический синхронизированный метод класса, то остальные потоки не могут войти ни в один стат. синх. метод этого класса, так?
Насчет изложения – тоже нравится, по-человечьи
#25 by ikozlov on 16 Декабрь 2011 - 19:47
Quote
>>Это будет справедливо если думать о вызовах names.size() и names.get(0) в следующем порядке:
Опечатка вместо “names.get(0)” надо “names.remove(0)”