Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология
Образование Политология Производство Психология Стандартизация Технологии


Избегайте избыточной синхронизации



 

Статья 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;  }

                                          }

                              }

                  }

       }

}

}

 

184

 

Чтобы воспользоваться этим классом, вы должны создать для него подкласс с тем, чтобы предоставить реализацию абстрактного метода 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;

 

185

 

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

 

187

 

 

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

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

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

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

 


Поделиться:



Последнее изменение этой страницы: 2019-04-11; Просмотров: 243; Нарушение авторского права страницы


lektsia.com 2007 - 2024 год. Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав! (0.026 с.)
Главная | Случайная страница | Обратная связь