• ,

Основы Параллелизма: взаимоблокировки и мониторы объектов (раздел 3) (перевод статьи)

Исходная статья: www.javacodegeeks.com/2015/09/concurrency-fundamentals-deadlocks-and-object-monitors.html
Автор: Martin Mois

Первые две части перевода здесь.

Содержание

1. Живучесть
 1.1 Взаимоблокировка
 1.2 Голодание
2. Мониторы объектов совместно с wait() и notify()
 2.1 Вложенные синхронизированные блоки совместно с wait() и notify()
 2.2 Условия в синхронизированных блоках
3. Проектирование для многонитевости
 3.1 Неизменяемый объект
 3.2 Проектирование API
 3.3 Локальное хранилище нити

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

3.1 Неизменный объект
Одним из правил проектирования, являющихся очень важным в данном контексте, является Неизменяемость. Если вы распределяете экземпляры объектов, например, между различными нитями, то должны уделить внимание тому, чтобы две нити не изменяли один и тот же объект одновременно. В подобных ситуациях легко управляться с немодифицируемыми объектами, поскольку вы не можете их изменить. Вы всегда должны создавать новый экземпляр, когда хотите изменить данные. Базовый класс java.lang.String — пример неизменяемого класса. Вы получаете новый экземпляр всякий раз, когда хотите изменить строку:

String str = "abc";

String substr = str.substring(1);


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

Далее приведён список правил, которые необходимо применять, чтобы сделать класс неизменяемым:
  • Все поля должны быть final и private.
  • Должны отсутствовать сеттеры.
  • Класс сам по себе должны быть объявлен как final для того, чтобы предотвратить нарушение принципа неизменяемости подклассом.
  • Если тип поля не является примитивным:
    • Не должно быть геттера, который передавал бы ссылку в открытом виде напрямую вызывающей стороне.
    • Не изменяйте ссылочные объекты (или хотя бы делайте эти изменения невидимыми для клиентов объекта).


Экземпляры следующего класса представляют собой сообщения с темой, телом и несколькими парами ключ/значение:


public final class ImmutableMessage {
  private final String subject;
  private final String message;
  private final Map<String,String> header;

  public ImmutableMessage(Map<String,String> header, String subject, String message) {
    this.header = new HashMap<String,String>(header);
    this.subject = subject;
    this.message = message;
  }

  public String getSubject() {
    return subject;
  }

  public String getMessage() {
    return message;
  }

  public String getHeader(String key) {
    return this.header.get(key);
  }

  public Map<String,String> getHeaders() {
    return Collections.unmodifiableMap(this.header);
  }
}


Класс — неизменяемый, поскольку все его поля определены как final и private. В классе отсутствуют методы, которые могли бы изменить состояние экземпляра после создания. Возврат ссылок на тему и сообщение безопасен, т.к. класс String сам по себе является неизменяемым. Тот, кто вызвал метод и получил ссылку, например, на сообщение не может изменить его непосредственно. А с полем Map, хранящим заголовки, необходимо быть осторожным. Возврат ссылки на объект Map позволил бы вызывающему изменить его содержимое. Следовательно, нам следует возвращать неизменяемый Map, полученный через вызов метода Collections.unmodifiableMap(). Он вернёт «изображение» Map, позволяющее вызывающим читать значения (которые опять же являются строками), но не допускающее изменений. Исключение UnsupportedOperationException будет выброшено при попытке изменить экземпляр объекта Map. В данном примере также безопасно возвращать определённые ключи, как это сделано в методе getHeader(String key), поскольку возвращаемый объект типа String вновь неизменяем. Если объект Map будет содержать объекты, которые сами по себе не являются неизменными, данная операция не будет ните-безопасной.

3.2 Разработка API
При разработке публичных методов класса, т.е. его API, вы можете также приспособить его для использования в многонитевой среде. У вас могут быть методы, которые не должны выполняться, когда объект находится в каком-то определённом состоянии. Одно из простых решений — завести приватный флаг, обозначающий, в каком состоянии мы находимся, и выбрасывать, например, исключение IllegalStateException, когда определённый метод не должен быть вызван:

public class Job {
  private boolean running = false;
  private final String filename;

  public Job(String filename) {
    this.filename = filename;
  }

  public synchronized void start() {
    if(running) {
      throw new IllegalStateException("...");
    }
    ...
  }

  public synchronized List getResults() {
    if(!running) {
      throw new IllegalStateException("...");
    }
    ...
  }
}


Приведённый шаблон часто называют “balking pattern”, поскольку метод уклоняется от выполнения, когда запущен в неправильном состоянии. Но вы можете реализовать тот же функционал, используя статический фабричный метод (шаблон fabric method) без проверки состояния объекта в каждом методе:

public class Job {
  private final String filename;

  private Job(String filename) {
    this.filename = filename;
  }

  public static Job createAndStart(String filename) {
    Job job = new Job(filename);
    job.start();
    return job;
  }

  private void start() {
    ...
  }

  public synchronized List getResults() {
    ...
  }
}


Статический фабричный метод создаёт новый экземпляр объекта Job, используя частный конструктор, и сразу вызывает start() для экземпляра. Возвращаемый по ссылке объект уже находится в подходящем для работы состоянии, поэтому метод getResults() требует только синхронизации, но не требует проверки состояния объекта.

3.3 Локальное хранилище нити
До сих пор мы видели, что нити разделяют одну и ту же память. Это хорошо с позиции производительности. Если бы мы использовали раздельные процессы для того, чтобы выполнять код параллельно, то нам бы потребовались более тяжеловесные методы обмена данными, такие как удалённые вызовы процедур или синхронизация на уровне файловой системы или сети. Но разделение памяти между различными нитями тоже тяжело поддерживать, если синхронизация не реализована должным образом.

Специализированная память, используемая только нашей собственной нитью и никакой другой, обеспечивается в Java посредством класса java.lang.ThreadLocal:

private static final ThreadLocal myThreadLocalInteger = new ThreadLocal();


Тип данных, которые должны быть сохранены в ThreadLocal, задаётся шаблонным параметром T. В примере мы использовали просто Integer, но также могли бы использовать любой другой тип данных. Следующий код демонстрирует использование ThreadLocal:

public class ThreadLocalExample implements Runnable {
  private static final ThreadLocal threadLocal = new ThreadLocal();
  private final int value;

  public ThreadLocalExample(int value) {
    this.value = value;
  }

  @Override
  public void run() {
    threadLocal.set(value);
    Integer integer = threadLocal.get();
    System.out.println("[" + Thread.currentThread().getName() + "]: " + integer);
  }

  public static void main(String[] args) throws InterruptedException {
    Thread threads[] = new Thread[5];
    for (int i = 0; i < threads.length; i++) {
      threads[i] = new Thread(new ThreadLocalExample(i), "thread-" + i);
      threads[i].start();
    }
    for (int i = 0; i < threads.length; i++) {
      threads[i].join();
    }
  }
}


Вы удивитесь, но каждая нить выводит в точности то значение, которое получила из конструктора, не смотря на то, что переменная threadLocal объявлена как static. Внутренняя реализация класса ThreadLocal гарантирует, что каждый раз при вызове set() данное значение сохраняется в области памяти, доступной только данной нити. Поэтому при последующем вызове get() вы получаете значение, которое установили ранее, не смотря на то, что другие нити тем временем могли вызывать set().
Серверы приложений в мире Java EE активно используют возможности ThreadLocal когда у вас есть много параллельных нитей, но каждая нить имеет, например, свой собственный контекст транзакции или безопасности. Если вы не хотите передавать эти объекты при каждом вызове метода, то просто сохраняете их в собственной памяти нити и позже получаете к ним доступ, когда потребуется.

1 комментарий

novant
Хорошо, познал много интересного, особенно про TreadLOcal, буду конечно еще не раз перечитывать, автору за труд зачет
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.