Использование синхронизированных методов

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

Чтобы понять необходимость синхронизации, давайте начнем с простого примера, который не использует ее, хотя и должен. Следующая программа содержит три простых класса. Первый из них, Callme, имеет единственный метод — call (). Этот метод принимает параметр типа String по имени msg. Этот метод пытается напечатать строку 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, "мир!");
wait for threads to end try {
obi.t.j oin();
ob2.t.join();
ob3.t.join();
}
catch(InterruptedException e) {
System.out.println("Прервано");
}
}
}

Вот вывод, сгенерированный этой программой:

[Добро пожаловать[в синхронизированный[мир!]]]

Как видите, вызывая sleep (), метод call () позволяет переключиться на выполнение другого потока. Это приводит к смешанному выводу трех строк сообщений. В этой программе нет ничего, что предотвращает вызов потоками одного и того же метода на одном и том же объекте в одно и то же время. Это называется состоянием гонок, поскольку три потока соревнуются друг с другом в окончании выполнения метода. Этот пример использует sleep (), чтобы сделать эффект повторяемым и наглядным. В большинстве ситуаций этот эффект более неуловим и менее предсказуем, поскольку вы не можете предвидеть, когда произойдет переключение контекста. Это может привести к тому, что программа один раз отработает правильно, а другой раз — нет.

Чтобы исправить эту программу, вы должны сериализироватъ доступ к call (). То есть вы должны разрешить доступ к этому методу одновременно только одному потоку. Чтобы сделать это, вам нужно просто предварить объявление call () ключевым словом synchronized, как показано ниже:

class Callme {
synchronized void call (String msg)
}

Это предотвратит доступ другим потокам к call (), когда один из них уже использует его. После того как слово synchronized добавлено к call (), результат работы программы будет выглядеть следующим образом:

[Добро пожаловать]
[в синхронизированный]
[мир!]

Всякий раз, когда у вас есть метод, или группа методов, которые манипулируют внутренним состоянием объекта в многопоточной среде, вы должны использовать ключевое слово synchronized, чтобы исключить ситуацию с гонками. Помните, что как только поток входит в любой синхронизированный метод на экземпляре, ни один другой поток не может войти ни в один синхронизированный метод на том же экземпляре. Однако не синхронизированные методы экземпляра по-прежнему остаются доступными для вызова.