Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Многопоточность.doc
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
378.88 Кб
Скачать

Синхронизация потоков

Рассмотрим концепцию Monitor, основанную на применении ключевого слова synchronized и методов класса Object: wait(), notify() и notifyAll().

Модель синхронизации потоков – монитор – это механизм управления связью между потоками придуманный Хором (Hoare, C.A.R). Монитор можно представить как маленький блок, который содержит только один поток. Как только поток входит в монитор, все другие потоки должны ждать, пока данный не выйдет из монитора. Таким образом, монитор можно использовать для защиты совместно разделяемых ресурсов от управления несколькими потоками одновременно.

Синхронизировать код можно двумя способами. Оба используют ключевое слово synchronized. Первый способ – записать слово synchronized в сигнатуре метода (в разделе модификаторов), второй – создать синхронизированный блок. Например, так

synchronized (object) {

// операторы для синхронизации

}

Объект не должен быть равен null.

В синхронизированном методе или блоке можно использовать методы wait(), notify() и notifyAll(). Все три метода можно вызывать только внутри синхронизированного кода, в противном случае вылетает исключение IllegalMonitorStateException. Метод wait() сообщает вызывающему потоку, что нужно уступить монитор и переходить в режим ожидания (спать), пока некоторый другой поток не войдет в тот же монитор и не вызовет notify(). Метод notifyAll() пробуждает все потоки, которые вызвали wait() и спят. Метод wait() бросает проверяемое исключение InterruptedException.

Рассмотрим на простых примерах правила написания синхронизированных блоков и методов.

Синхронизация выполняется по объекту. В примере программа повиснет. Есть вариант метода wait(timeout) с параметром. В скобках указывается количество миллисекунд, которое объект должен находиться в ожидающем состоянии.

public static void main(String[] args)

throws InterruptedException{

Object obj = new Object();

synchronized (obj) {

obj.wait();

}}

В данном случае обе ссылки ссылаются на один и тот же объект в куче. Ошибок компиляции не будет, так как синхронизация происходит не по ссылке, а по объекту.

public static void main(String[] args)

throws InterruptedException{

Object obj0 = new Object();

Object obj = obj0;

synchronized (obj) {

obj0.wait();

}}

Нижний пример работать не будет (вылетит исключение IllegalMonitorStateException), так как создаются два разных объекта. Метод wait() вызван не для того объекта, по которому синхронизирован блок.

public static void main(String[] args)

throws InterruptedException{

synchronized (new Object) {

new Object.wait();

}}

Нижний фрагмент кода корректный, так как вызов метода wait() происходит в блоке, синхронизированном по объекту obj0. В этой же месте можно вызывать и метод wait() для объекта obj1. В Java можно создавать любое количество вложенных синхронизированных блоков. При этом поток запоминает, в какое количество синхронизированных блоков он вошел.

public static void main(String[] args)

throws InterruptedException{

Object obj0 = new Object();

Object obj1 = new Object();

synchronized (obj0) {

synchronized (obj1) {

obj0.wait();

}

}

}

Следующий код абсолютно корректный. В синхронизированном нестатическом методе f() можно вызвать метод notify(). Помните, что статический метод не имеет ссылки this!

public class Example{

public static void main(String[] args)

throws InterruptedException{

new Example().f();

}

public synchronized void f() {

this.notify();

}

}

Предыдущий код полностью эквивалентен следующему:

public class Example{

public static void main(String[] args)

throws InterruptedException{

new Example().f();

}

public void f() {

synchronized (this) {

this.notify();

}

}

}

Но как быть, если существует необходимость вызвать методы wait(), notify() или notifyAll() в статическом методе. Как известно у статического метода нет ссылки this. В статическом методе данные методы можно вызвать для объекта класса, как это показано ниже:

public class Example{

public static void main(String[] args)

throws InterruptedException{

f();

}

public static synchronized void f() {

Class clazz = Example.class;

clazz.notify();

}

}

Такая запись вполне корректна. Класс является ссылочным типом данных, т.е. объектом для которого может быть вызван метод notify().

Можно видоизменить приведенный выше код, используя синхронизированный блок:

public class Example{

public static void main(String[] args)

throws InterruptedException{

f();

}

public static void f() {

Class clazz = Example.class;

synchronized (clazz) {

clazz.notify();

}

}

}

Рассмотрим пример. Имеется класс BlockedMethodCaller, реализующий интерфейс Runnable. В конструктор данного класса передается ссылка на объект и целое число k. В методе run() для данной ссылки вызывается метод f(), в который и передается целое число k.

Отдельно создается класс BlockedSetExample, имеющий метод f(). Идея состоит в том, чтобы создавать много потоков (в примере их 5), каждый из которых может вызывать синхронизируемый метод f().

ПРИМЕР 3.

public class BlockedMethodCaller implements Runnable {

private final BlockedSetExample ref;

private final int k;

public BlockedMethodCaller (BlockedSetExample ref, int k) {

this.ref = ref;

this.k = k;

}

@Override

public void run() {

try {

ref.f(k);

} catch (InterruptedException e) {

e.printStackTrace();}

}}

public class BlockedSetExample {

public static void main (String[] args) throws InterruptedException {

BlockedSetExample ref = new BlockedSetExample();

for (int k = 0; k < 5; k++) {

new Thread(new BlockedMethodCaller(ref, k)).start();

}}

public synchronized void f(int x) throws InterruptedException {

System.out.println("+"+x);

Thread.sleep(1000);

System.out.println("-"+x);

}}

В программе создается 5 потоков, которые вызывают синхронизируемый метод f() (см.рис). В самом методе f() поток засыпает на 1 сек. Каждый поток имеет свой номер – целое число x.

При входе потока в метод f() на экран выводится его номер со знаком "+", после того как поток проснется, на экран выводится его номер, но уже со знаком "-". Результат работы программы приводится ниже:

+0

-0

+4

-4

+3

-3

+1

-1

+2

-2

Как видно из результата работы программы, потоки входят в метод f() по очереди. Пока в методе спит один поток, никакой другой поток туда зайти не может. Как только, находящийся в методе поток просыпается и покидает его, в метод заходит следующий поток. Таким образом, никакие два и более потоков не могут находиться и одновременно работать в синхронизированной по одной переменной секции. Это свойство называется взаимное исключение (mutual exclusion) или мьютекс.

В примере потоки синхронизируются по одной ссылке ref. В метод f() она передается неявно как ссылка this. Давайте попробуем создавать для каждого потока отдельную ссылку:

public class BlockedSetExample {

public static void main (String[] args) throws InterruptedException {

for (int k = 0; k < 5; k++) {

new Thread(new BlockedMethodCaller(new BlockedSetExample(), k)).start();

}}

public synchronized void f(int x) throws InterruptedException {

System.out.println("+"+x);

Thread.sleep(1000);

System.out.println("-"+x);

}}

В итоге мы не наблюдаем синхронизацию потоков, несмотря на то, что в заголовке метода f() записано слово synchronized.

+2

+0

+1

+3

+4

-0

-2

-4

-3

-1

Э то связано с тем, что теперь мы создаем пять разных объектов и каждый поток вызывает синхронизированный метод f() у своего объекта (см.рис.). Так как у каждого объекта по одному потоку, то конкурировать некому. Потоки одновременно могут зайти каждый в свой метод, поспать 1 сек и проснуться. Поэтому на экране сначала появились все плюсы, а потом минусы. Помните, что синхронизация осуществляется по объектам!!!

Можете поэкспериментировать и убрать из заголовка метода f() слово synchronized. Результат будет следующий:

+0

+3

+2

+4

+1

-0

-4

-2

-3

-1

Поведение потоков изменилось. Обратите внимание, несмотря на то, что стартовали потоки последовательно, начиная с нулевого, добежали до метода f() они в произвольном порядке (и проснулись тоже). Это один из примеров непредсказуемого поведения потоков.

Рассмотрим еще один интересный пример:

ПРИМЕР 4.

public class WaitMethodCaller implements Runnable {

private final WaitSetExample ref;

private final int k;

public WaitMethodCaller (WaitSetExample ref, int k) {

this.ref = ref;

this.k = k;

}

@Override

public void run() {

try {

ref.f(k);

} catch (InterruptedException e) {

e.printStackTrace();}

}}

public class WaitSetExample {

public static void main (String[] args) throws InterruptedException {

WaitSetExample ref = new WaitSetExample();

for (int k = 0; k < 5; k++) {

new Thread(new BlockedMethodCaller(ref, k)).start();

}}

public synchronized void f(int x) throws InterruptedException {

System.out.println("+"+x);

this.wait();

System.out.println("-"+x);

}}

+0

+2

+3

+4

+1

В синхронизируемом методе f() потоки теперь не спять 1 сек., а переводятся в режим ожидания методом wait(). Когда поток в режиме ожидания – разбудить его могут только методы notify() и notifyAll(), которых в программе нет. Как вы можете видеть по результату выполнения программы, все пять потоков смогли зайти в синхронизируемый по одному объекту метод. И каждый из них отдельно повиснул. Как же можно объяснить такой странный результат, ведь выше уже говорилось о том, что одновременно не более одного потока могут находиться и работать в синхронизируемой секции? Правильно работать, но не вызвать метод wait()!!! Единственный способ освободить блокировку синхронизируемой секции – вызвать для потока метод wait(). В этом случае потоки переходят в отдельное множество называемое wait-set.

С любым объектом в Java, находящимся в куче, связано три незримых множества. Схематично это можно представить следующим образом:

- блокировка. Объект может быть либо блокирован, либо не блокирован (в случае, когда в synchronized по этому объекту кто-то вошел, и никто другой уже туда не может войти).

- множество wait-set. Потоки внутри этого множества пассивные, они спят и ждут, когда их разбудят.

- множество blocked-set. Потоки в этом множестве как бы активные, но не работают (не грузят CPU), им что-то мешает работать.

Смоделируем ситуацию с блокировкой в примере 3. Когда в синхронизируемый метод вошел первый поток (№0) и установил блокировку, никакой другой поток туда зайти уже не может, и они все перемещаются во множество blocked-set (рис.а). В этом множестве потоки активные, но их временно приостановила ОС, до тех пор, пока не освободится блокировка. Как только поток №0 завершит работу и освободит блокировку, ОС случайным образом запустит в synchronized метод следующий поток из множества blocked-set (рис b).

Схематично изобразим действия, происходящие в примере 4. Поток захватывает синхронизированный метод и самостоятельно вызывает метод wait() (рис.а). При этом блокировка метода снимается, а сам поток помещается JVM в wait-set. Затем следующий поток ставит блокировку и захватывает метод (рис. b). Процесс повторяется до тех пор, пока все потоки не окажутся в множестве wait-set.