Главная » Java, Советы » Избегайте избыточной синхронизации

0

 

Статья 48 предупреждает об опасностях недостаточной синхронизации. Данная статья посвящена обратной проблеме. В зависимости от ситуации избыточная синхронизация может приводить к снижению производительности приложения, взаимной блокировке потоков или даже к непредсказуемому поведению программы.

для исключения возможности взаимной блокировки (deadlock) никогда не передавайте управление клиенту, если находитесь в синхронизированном методе или блоке. Иными словами, из области синхронизации не следует вызывать открытые или защищенные методы, которые предназначены для переопределения. (Такие методы обычно являются абстрактными, но иногда по умолчанию могут иметь определенную реализацию.) С точки зрения класса, содержащего синхронизированную область, такой метод является чужим. У класса нет сведений о том, что этот метод делает, нет над ним контроля. Клиент может реализовать этот метод таким образом, чтобы он создавал другой поток, выполняющий обратный вызов этого же класса. Затем вновь созданный поток может попытаться получить доступ к области, блокированной первым потоком, что приведет к блокировке нового потока. И если метод, создавший новый поток, ждет его завершения, возникает взаимная блокировка.

для пояснения рассмотрим класс, в котором реализована очередь заданий (work queue). Этот класс позволяет клиентам ставить задания в очередь на асинхронную обработку. Метод enqueue может вызываться столь часто, сколь это необходимо. Конструктор класса запускает фоновый поток, который удаляет из очереди записи в том порядке, в котором они были сделаны, и обрабатывает их, используя метод processItem. Если очередь заданий больше не нужна, клиент вызывает метод stop, чтобы заставить поток изящно остановиться после завершения всех заданий, находящихся в обработке.

public abstract class WorkQueue {

private final List queue = new LinkedList();

private boolean stopped = false; 183

protected WorkQueue() { new WorkerThread().start(); }

public final void enqueue(Object workltem) {

            synchronized (queue) {

                       queue.add(worklt~m);

                       queue.notify(); }

}

 

public final void stop() {

            synchronized (queue) {

            stopped = true;

            queue. notify(); }

}

 

protected abstract void processltem(Object workltem)

            throws InterruptedException;

 

// Ошибка: вызов чужого метода из синхронизированного блока!

 

private class WorkerThread extends Thread {

            public void run() {

                        while (true) {    //Главный цикл

                                   synchronized (queue)  {

                                               try {

                                                           while (queue.isEmpty() && !stopped)

                                                                      queue. wait();

                                               }catch  (InterruptedException е) {

                                                           return; }

                                               if (stopped)

                                                           return;

                                               Object workltem = queue. геmоvе(0);

                                               try {

                                                           processltem(workltem): // Блокировка!

                                               } catch (InterruptedException е) {

                                                           return;  }

                                               }

                                   }

                       }

            }

}

}

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

class DisplayQueue extends WorkQueue {

protected void processltem(Object workltem)

 throws InterruptedException {

System,out.println(workltem);

Thread.sleep(1000);  }

}

Поскольку класс WorkQueue вызывает абстрактный метод рrocessItem из синхронизированного блока, для него может наступить взаимная блокировка. Действительно, работа со следующим подклассом по описанной выше схеме приведет к взаимной блокировке:

 

class DeadlockQueue extends WorkQueue {

protected void processltem(final Object workltem)

 throws InterruptedException {

// Создаем новый поток, который возвращает workltem в очередь

 Thread child = new Thread() {

public void run() { enQueue(workltem); }

};

child. start();

child. join();    //Взаимная блокировка!

}

}

 

Этот пример приведен потому, что здесь нет причин, которые заставили бы метод рrocessItem создавать фоновый поток, однако описываемая проблема абсолютно реальна. Вызовы из синхронизированного блока методов, реализуемых за пределами класса, приводили к множеству взаимных блокировок в таких реальных системах, как программные пакеты с графическим интерфейсом пользователя. К счастью, эта проблема легко устранима. Вынесите вызов метода за пределы синхронизированного блока, как показано в следующем примере:

// Чужой метод за пределами синхронизированного блока

// "открытый вызов"

private class WorkerThread extends Thread {

public void run() {

                      while (true) {            // Главный цикл

Object workltem = null;

 

synchronized (queue) {

      try {

                  while (queue.isEmpty() &&  !stopped)

                             queue.wait();

      } catch (InterruptedException е) {

                  return ; }

      if (stopped)

                  return;

      workItem = queue. remove(0);

}

try {

                  processItem(workItem);  // Блокировки нет

} catch (InterruptedExcepti~n

                   return;  }

                     }

      }

}

Чужой метод, который вызывается за пределами синхронизированной области, называется открытым вызовом (open саll) [LeaOO,2.4.1.3]. Открытые вызовы не только предотвращают взаимную блокировку, но и значительно увеличивают распараллеливание вычислений. Если бы чужой метод вызывался из блокированной области и выполнялся сколь угодно долго, то все это время остальные потоки без всякой на то необходимости получали бы отказ в доступе к совместно используемому объекту.

Правило таково, что в пределах синхронизированной области нужно выполнять как можно меньше работы: заблокируйте ресурс, проверьте совместно используемые данные, преобразуйте их при необходимости и разблокируйте ресурс. Если требуется выполнение операций, отнимающих много времени, найдите способ вынести их за пределы синхронизированной области.       

Вызов чужого метода из синхронизированной области может привести к более серьезным сбоям, чем просто взаимная блокировка, если он происходит в тот момент, когда инварианты, защищаемые синхронизацией, временно недействительны. (В первом примере с очередью заданий этого случиться не может, поскольку при вызове метода рrocessItem очередь находится в непротиворечивом состоянии.) Возникновение таких сбоев не связано с созданием в чужом методе новых потоков. Это происходит, когда чужой метод делает обратный вызов пока что некорректного класса. И поскольку блокировка в языке программирования Java является рекурсивной, подобные обратные вызовы не приведут к взаимной блокировке, как это было бы, если бы вызовы производились из другого потока. Поток, из которого делается вызов, уже заблокировал область, и потому этот поток успешно пройдет через блокировку 186

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

Мы обсудили проблемы параллельности потоков, теперь обратимся к производительности. Хотя с момента появления платформы Java расходы на синхронизацию резко сократились, полностью они не исчезнут никогда. И если часто выполняемую операцию синхронизировать без всякой необходимости, это может существенно сказаться на производительности приложения. Например, рассмотрим классы StringBuffer и BufferedInputStream. Эти классы имеют поддержку много поточности (статья 52), однако почти всегда их использует только один поток, а потому осуществляемая ими блокировка, как правило, оказывается избыточной. Они содержат методы, выполняющие тонкую обработку на уровне отдельных символов или байтов, а потому не только склонны выполнять ненужную работу по блокированию потоков, но имеют тенденцию использовать множество таких блокировок. Это может привести к значительному снижению производительности. В одной из статей сообщалось почти о 20% потерь для каждого реального приложения [Heydon99]. Вряд ли вы столкнетесь со столь существенным падением производительности, обусловленным излишней синхронизацией, однако 5-100/0 потерь вполне возможны.

Можно утверждать, что все это относится к разряду "маленьких усовершенствований", о которых, как говорил Кнут, нам следует забыть (статья 37). Однако если вы пишите абстракцию низкого уровня, которая в большинстве случаев будет работать с одним единственным потоком или как составная часть более крупного синхронизированного объекта, то следует подумать над тем, чтобы отказаться от внутренней синхронизации этого класса. Независимо от того, будете вы синхронизировать класс или нет, крайне важно, чтобы в документации вы отразили его возможности при работе в многопоточном режиме (статья 52).

Не всегда понятно, следует ли в указанном классе выполнять внутреннюю синхронизацию. Перечень, приведенный в статье 52, не дает четкого ответа на вопрос, следует ли поддерживать в классе МН020поточность или же его нужно сделать совместимым с многопоточностью. Приведем несколько рекомендаций, которые помогут вам в выборе.

Если вы пишите’ класс, который будет интенсивно использоваться в условиях, требующих синхронизации, а также в условиях, когда синхронизация не нужна, правильный подход заключается в обеспечении обоих вариантов: с синхронизацией (с поддержкой многопоточности – thread-safe) и без синхронизации (совместимый с многопоточностью – thread-соmраtibIе). Одно из возможных решений – создание класса-оболочки (статья 14), в котором реализован соответствующий этому классу интерфейс, а перед передачей вызова внутреннего объекта соответствующему методу выполняется необходимая синхронизация. Такой подход при меняется в Collections

 

187Framework, а также в классе jауа.util.Random. Второй вариант решения, который можно использовать для классов, не предназначенных для расширения или повторной реализации, заключается в предоставлении класса без синхронизации, а также подкласса, состоящего исключительно из синхронизированных методов, которые вызывают соответствующие методы из суперкласса.

Хорошим поводом для внутренней синхронизации класса является то, что он будет интенсивно использоваться параллельными потоками, а также то, что вы можете добиться большего· параллелизма, выполняя тонкую внутреннюю синхронизацию. Например, можно реализовать хэш-таблицу постоянного размера, в которой доступ к каждому сегменту синхронизируется отдельно. Это обеспечивает более высокую степень распараллеливания, чем в случае блокирования всей таблицы при доступе к одному элементу.

Если класс или статический метод связан с изменяемым статическим полем, он должен иметь внутреннюю синхронизацию, даже если обычно применяется только с одним потоком. В противоположность совместно используемому экземпляру, здесь клиент не имеет возможности произвести внешнюю синхронизацию, поскольку нет никакой гарантии, что другие клиенты будут делать то же самое. Эту ситуацию иллюстрирует статический метод Math.random.

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

 

Источник: Джошуа Блох, Java TM Эффективное программирование, Издательство «Лори»

По теме:

  • Комментарии