Статьи

RAII на Яве

Resource Acquisition Is Initialization ( RAII ) — идея проекта, предложенная Бьярном Страуструпом в C ++ для безопасного управления ресурсами в исключительных ситуациях. Благодаря сборке мусора Java не имеет этой возможности, но мы можем реализовать нечто подобное, используя try-with-resources .

На ферме Сахем (1998) Джон Хаддлс

Проблема, которую RAII решает, очевидна; взгляните на этот код (я уверен, что вы знаете, что такое Semaphore и как он работает в Java):

01
02
03
04
05
06
07
08
09
10
11
class Foo {
  private Semaphore sem = new Semaphore(5);
  void print(int x) throws Exception {
    this.sem.acquire();
    if (x > 1000) {
      throw new Exception("Too large!");
    }
    System.out.printf("x = %d", x);
    this.sem.release();
  }
}

Код довольно примитивен и не делает ничего полезного, но вы, скорее всего, поняли идею: метод print() , при вызове из нескольких параллельных потоков, позволит только пяти из них печатать параллельно. Иногда это не позволяет некоторым из них печатать и выдает исключение, если x больше 1000 .

Проблема с этим кодом — утечка ресурсов . Каждый вызов print() с x больше 1000 получает одно разрешение от семафора и не возвращает его. В пяти вызовах с исключениями семафор будет пустым, и все остальные потоки ничего не будут печатать.

Каково решение? Вот:

01
02
03
04
05
06
07
08
09
10
11
12
class Foo {
  private Semaphore sem = new Semaphore(5);
  void print(int x) throws Exception {
    this.sem.acquire();
    if (x > 1000) {
      this.sem.release();
      throw new Exception("Too large!");
    }
    System.out.printf("x = %d", x);
    this.sem.release();
  }
}

Мы должны отпустить разрешение, прежде чем мы бросим исключение.

Однако возникает еще одна проблема: дублирование кода. Мы отпускаем разрешение в двух местах. Если мы добавим больше инструкций throw нам также придется добавить больше sem.release() .

Очень элегантное решение было введено в C ++ и называется RAII. Вот как это будет выглядеть в Java:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
class Permit {
  private Semaphore sem;
  Permit(Semaphore s) {
    this.sem = s;
    this.sem.acquire();
  }
  @Override
  public void finalize() {
    this.sem.release();
  }
}
class Foo {
  private Semaphore sem = new Semaphore(5);
  void print(int x) throws Exception {
    new Permit(this.sem);
    if (x > 1000) {
      throw new Exception("Too large!");
    }
    System.out.printf("x = %d", x);
  }
}

Посмотрите, насколько красив код внутри метода Foo.print() . Мы просто создаем экземпляр класса Permit и он сразу же получает новое разрешение на семафор. Затем мы выходим из метода print() либо по исключению, либо обычным способом, и метод Permit.finalize() освобождает разрешение.

Элегантно, не правда ли? Да, это так, но это не будет работать в Java.

Это не будет работать, потому что, в отличие от C ++, Java не уничтожает объекты, когда их область видимости закрыта. Объект класса Permit не будет уничтожен при выходе из метода print() . Это будет уничтожено в конце концов, но мы не знаем когда именно. Скорее всего, он будет уничтожен после того, как все разрешения в семафоре будут получены, а мы заблокированы.

В Java тоже есть решение. Он не так элегантен, как в C ++, но работает. Вот:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Permit implements Closeable {
  private Semaphore sem;
  Permit(Semaphore s) {
    this.sem = s;
  }
  @Override
  public void close() {
    this.sem.release();
  }
  public Permit acquire() {
    this.sem.acquire();
    return this;
  }
}
class Foo {
  private Semaphore sem = new Semaphore(5);
  void print(int x) throws Exception {
    try (Permit p = new Permit(this.sem).acquire()) {
      if (x > 1000) {
        throw new Exception("Too large!");
      }
      System.out.printf("x = %d", x);
    }
  }
}

Обратите внимание на блок try и на интерфейс Closeable который теперь реализует класс Permit . Объект p будет «закрыт» при выходе из блока try . Он может выйти либо в конце, либо с помощью операторов return или throw . В любом случае Permit.close() будет вызван: так работает try-with-resources в Java.

Я ввел метод sem.acquire() acquire() и переместил sem.acquire() из конструктора Permit поскольку считаю, что конструкторы должны быть свободны от кода.

Подводя итог, RAII это идеальный дизайн шаблон подход, когда вы имеете дело с ресурсами, которые могут утечь . Несмотря на то, что в Java его нет «из коробки», мы можем реализовать его с помощью try-with-resources и Closeable .

Ссылка: RAII на Java от нашего партнера по JCG Егора Бугаенко в блоге About Programming .