Когда два или более потоков имеют доступ к одному совместно используемому ресурсу, они нуждаются в гарантии, что ресурс будет использован только одним потоком в одно и то же время. Процесс обеспечения этого называется синхронизацией.
Скачать исходники для статьи можно ниже
Ключом к синхронизации является концепция монитора. Монитор — это объект, который используется, как взаимоисключающая блокировка (mutually exclusive lock — mutex), или мьютекс.
Только один поток может в одно и то же время владеть монитором. Когда поток запрашивает блокировку, говорят, что он входит в монитор.
Все другие потоки, которые пытаются войти в заблокированный монитор,
будут приостановлены до тех пор, пока первый поток не выйдет из монитора.
Обо всех прочих потоках говорят, что они ожидают монитора. Поток, который владеет монитором, может повторно войти в него, если пожелает.
Использование синхронизированных методов.
Синхронизация в Java проста, поскольку объекты имеют собственные, ассоциированные с ними неявные мониторы. Чтобы войти в монитор объекта, следует просто вызвать метод, модифицированный ключевым словом synchronized.
Когда поток находится внутри синхронизированного метода, все другие потоки, которые пытаются вызвать его (или любые другие синхронизированные методы) в том же экземпляре, должны ожидать. Чтобы выйти из монитора и передать управление объектом другому ожидающему потоку, владелец монитора просто возвращает управление из синхронизированного метода.
Чтобы понять необходимость синхронизации, давайте начнем с простого примера, который не использует ее, хотя и должен.
Следующая программа содержит три простых класса. Первый из них, Callme, имеет единственный метод — call().
Этот метод принимает параметр msg класса String и пытается вывести строку msg внутри квадратных скобок.
Интересно отметить, что после того, как метод call() выводит открывающую скобку и строку msg, он вызывает метод Thread.sleep(1000), который приостанавливает текущий поток на одну секунду.
Конструктор следующего класса, Caller, принимает ссылку на экземпляры
классов Callme и String, которые сохраняются соответственно в переменных target и msg.
Конструктор также создает новый поток, который вызовет метод
run() объекта. Поток стартует немедленно.
Метод run() класса Caller вызывает метод call() для экземпляра target класса Callme, передавая ему строку msg.
Наконец, класс Synch начинает с создания единственного экземпляра класса Callme и трех экземпляров класса Caller, каждый с уникальной строкой сообщения.
Один экземпляр класса Callme передается каждому конструктору Caller():
// Эта программа не синхронизирована, class Callme { void call(String msg) { System.out.print("[" + msg); try { Thread.sleep(1000); } catch(InterruptedException e) { System.out.println("Прервано"); } System.out.println("]"); } } class Caller implements Runnable { String msg; Callme target; Thread t; public Caller(Callme targ, String s) { target = targ; msg = s; t = new Thread(this); t. start(); } public void run() { target.call(msg); } } class Synch { public static void main(String args[]) { Callme target = new Callme(); Caller obi = new Caller(target, "Добро пожаловать"); Caller ob2 = new Caller(target, "в синхронизированный") Caller ob3 = new Caller(target, "мир!"); // ожидание завершения потока try { obi.t.j oin() ; ob2.t.join(); ob3.t.join(); } catch(InterruptedException e) { System.out.println("Прервано"); } } }
Вот вывод этой программы.
[Добро пожаловать[в синхронизированный[мир!]
]
]
Как видите, вызывая метод sleep(), метод call() позволяет переключиться на выполнение другого потока. Это приводит к смешанному выводу трех строк сообщений.
В этой программе нет ничего, что предотвращает вызов потоками одного
и того же метода в одном и том же объекте в одно и то же время.
Это называется состоянием гонок (race condition) или конфликтом, поскольку три потока соревнуются друг с другом в окончании выполнения метода.
Этот пример использует метод sleep(), чтобы сделать эффект повторяемым и наглядным.
В большинстве ситуаций этот эффект менее заметен и менее предсказуем, поскольку вы не можете предвидеть, когда произойдет переключение контекста. Это может привести к тому, что программа один раз отработает правильно, а другой раз — нет.
Чтобы исправить эту программу, следует cинхронизировать доступ к методу call(). То есть в одно и то же время вы должны разрешить доступ к этому методу только одному потоку. Чтобы сделать это, вам нужно просто предварить объявление метода call() ключевым словом synchronized, как показано ниже:
class Callme { synchronized void call(String msg) {
Это предотвратит доступ другим потокам к методу call(), когда один из них уже использует его. После того как слово synchronized добавлено к методу call(), результат работы программы будет выглядеть следующим образом: [Добро пожаловать] [в синхронизированный] [мир!]
Всякий раз, когда у вас есть метод или группа методов, которые манипулируют внутренним состоянием объекта в многопоточной среде, следует использовать ключевое слово synchronized, чтобы исключить ситуацию с гонками.
Помните, что как только поток входит в любой синхронизированный метод экземпляра, ни один другой поток не может войти ни в один синхронизированный метод того же экземпляра.
Однако несинхронизированные методы экземпляра по-прежнему остаются доступными для вызова.
Хотя создание синхронизированных методов в ваших классах — простой и эффективный способ синхронизации, все же он работает не во всех случаях.
Чтобы понять почему, рассмотрим следующее.
Предположим, что вы хотите синхронизировать доступ к объектам классов, которые не были предназначены для многопоточного доступа. То есть класс не использует синхронизированных методов.
Более того, класс был написан не вами, а независимым разработчиком, и у вас нет доступа к его исходному коду. Значит, вы не можете добавить слово synchronized к объявлению соответствующих методов класса.
Как может быть синхронизирован доступ к объектам такого класса? К счастью, существует довольно простое решение этой проблемы: вы просто заключаете вызовы методов этого класса в блок synchronized.
Вот общая форма оператора synchronized:
synchroni zed(объект) { // операторы, подлежащие синхронизации }
Здесь объект — это ссылка на синхронизируемый объект. Блок synchronized гарантирует, что вызов метода объекта произойдет только тогда, когда текущий поток успешно войдет в монитор объекта.
Ниже показана альтернативная версия предыдущего примера с использованием синхронизированного блока внутри метода run().
// Эта программа использует синхронизированный блок, class Callme { void call(String msg) { System.out.print("[" + msg); try { Thread.sleep(1000); } catch (InterruptedException e) { System.out.println("Interrupted"); } System.out.println("]"); } } class Caller implements Runnable { String msg; Callme target; Thread t; public Caller(Callme targ, String s) { target = targ; msg = s; t = new Thread(this); t.start(); } // синхронизированные вызовы call() public void run() { synchronized(target) { // синхронизированный блок target.call(msg); } } } class Synch1 { public static void main(String args[]) { Callme target = new Callme(); Caller obi = new Caller(target, "Добро пожаловать"); Caller ob2 = new Caller(target, "в синхронизированный"); Caller ob3 = new Caller(target, "мир!"); // ожидание завершения потока try { obi.t.join(); ob2.t.join(); ob3.t.join(); } catch(InterruptedException e) { System.out.println("Прервано"); } } }
Здесь метод call() не модифицирован словом synchronized. Вместо этого
используется оператор synchronized внутри метода run() класса Caller. Это позволяет получить тот же корректный результат, что и в предыдущем примере, поскольку каждый поток ожидает окончания выполнения своего предшественника.
У меня код все равно выходит в неправильно порядке:
[Добро пожаловать]
[мир!]
[в синхронизированный]
– как это исправить?