Недостатки Java: Threads

апр 23, 15:21

Когда я говорил о недостатках Java, то упустил одну вещь, а именно – многопоточность.

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

Небольшой ликбез

В яве центральное понятие для мнопоточности – это понятие “монитора”. Монитор можно представить себе как объект специального назначения, который представляют собой набор некоторого кода, процедур, доступ к которым гарантирован по принципу взаимного исключения (mutal-exclusion), или взаимо-исключающему семафору (mutal-exclusion semaphore или просто mutex, мьютекс). Мониторы существуют только на уровне работы JVM, они не доступны как объекты Java.

Основная идея мьютекса – это владение. Только один поток может завладеть мьютексом в одно время. Если другой поток попытается захватить мьютекс, это заблокирует его пока первый поток не отпустит мьютекс.

Если несколько потоков ждут освобождения мьютекса, то только один получит мьютекс, когда тот освободится. Остальные продолжат ожидание.

Т.е другими словами, имея код:


synchronized (this) {
    doSomething();
}

мы можем представить его себе, как:


this.mutex.acquire();
try {
    doSomething();
} finally {
    this.mutex.release();
}

Проблема 1: Каждый объект в Java имеет только один монитор

Чтобы объяснить это, приведу аналогию, которую я прочел в одной из книг по потокам и она мне показалась очень удачной.

Представим себе, что монитор – это туалет на борту самолета. Только один человек (поток, thread) может воспользоваться туалетов в одно время. Все остальные вынуждены ждать пока туалет не освободится, создавая очереди и так далее, и так далее. Пока дверь закрыта – все ждут.

В этом примере, объект – это самолет. Пассажиры – потоки. Туалет – монитор. Замок на двери – мьютекс.

В яве, у каждого объекта есть один и только один монитор. В тоже время, один монитор (туалет) может иметь несколько дверей в него. Каждая дверь представляет собой synchronized блок. Когда пассажир заходит в туалет, то все двери закрываются. Это и есть проблема и ограничение.

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

Можно проиллюстрировать с помощью следующего кода:

public class EveryObjectIsMutex {
    private int count;
    private int anotherCount;

static public void main(String[] args) { final EveryObjectIsMutex mutex = new EveryObjectIsMutex(); new Thread(new Runnable() { public void run() { mutex.setCount(20); } }).start(); System.out.println(mutex.getCount()); System.out.println(mutex.getAnotherCount()); } synchronized public int getCount() { return count; } synchronized public void setCount(int count) { // some long calculations goes here this.count = count; } synchronized public void setAnotherCount(int anotherCount) { this.anotherCount = anotherCount; } synchronized public int getAnotherCount() { return anotherCount; } }

Здесь пока выполняется метод setCount(20) в отдельном потоке, никакой другой методов над объектом mutex выполнить нельзя. Даже getAnotherCount(), хотя тот и использует совсем другие данные, никак не пересекаясь с данными метода setCount().

Выход из этой ситуации делать synchronized над разными объектами внутри setCount() и setAnotherCount() с помощью:


… private Object lock1; pirvate Object lock2;
… public void setCount(int count) { synchronized (lock1) { // some long calculations goes here this.count = count; } }

synchronized public void setAnotherCount(int anotherCount) { synchronized (lock2) { this.anotherCount = anotherCount; } }

wait() и notify()

Wait() и notify() – это еще способ синхронизации. Первый – взаимное исключение с помощью мьютексов – позволяет осуществлять последовательный доступ к общим данным.

Второй – это кооперация с помощью wait() и notify(). Это необходимо, когда один поток зависит от другого потока (например, ждет данных, которые еще не поступили).

С этими методами связано несколько проблем.

Первая – wait() принимает как параметр таймаут, максимальное время, которые нужно ждать, но при возврате из метода wait() нет никакой возможности узнать был ли возврат вызван таймаутом или тем, что другой поток вызвал notify().

Вторая проблема – nested-monitor lockout (не знаю, как перевести по-хорошему на русский). С wait() и notify(), как и с любыми блокирующими операциями, очень легко нарваться на это вид deadlock’a. Проблема заключается в том, что блокирующая функция может быть вызвана из синхронизированного кода. И единственный способ разблокировать ее – вызвать другой синхронизированный метод.

Пример:

public class BlockingObject {
    public synchronized void sleep() {
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

public synchronized void wakeUp() { notify(); } };

public class NestedMonitorLockout { private BlockingObject object = new BlockingObject();

public static void main(String[] args) { final NestedMonitorLockout nestedLock = new NestedMonitorLockout(); new Thread(new Runnable() { public void run() { nestedLock.sleep(); } }).start(); nestedLock.wakeUp(); }

private synchronized void sleep() { object.sleep(); }

private synchronized void wakeUp() { object.wakeUp(); } }

Основное правило, как избежать этого – не делать блокирующих вызовов внутри синхронизированных методов. А если делать, то должен быть не синхронизированный метод, который может достучаться к блокируемому объекту.

Пока все.

На этом пока можно остановится. Уже из этих примеров видно, что многопоточное программирование в яве – нетривиальная задача. Стандартные средства есть, но они несовершенны (wait() мог бы возвращать boolean, индикатор, что возврат был по таймауту или по notify(), synchronized блок не поддерживает таймауту по умолчнанию). И так далее. О других вещах и как обойти эти ограничения – в следующий раз.

Комментарии

 
---