Статьи

Стажировка объектов Java

Java хранит строковые константы, появляющиеся в исходном коде, в пуле. Другими словами, когда у вас есть такой код:

1
2
String a = "I am a string";
String b = "I am a string";

переменные a и b будут иметь одинаковое значение. Не просто две одинаковые строки, а одна и та же строка. В Java слова a == b будут истинными. Однако это работает только для строк и небольших целых и длинных значений. Другие объекты не интернированы, поэтому, если вы создаете два объекта, которые содержат точно такие же значения, они обычно не совпадают. Они могут и, вероятно, быть равными, но не одинаковыми объектами. Это может быть неприятно некоторое время. Вероятно, когда вы выбираете какой-то объект из какого-либо хранилища. Если вам случается получить один и тот же объект более одного раза, вы, вероятно, захотите получить один и тот же объект вместо двух копий. Другими словами, я также могу сказать, что вы хотите иметь только одну копию в памяти одного объекта в постоянстве. Некоторые постоянные слои делают это для вас. Например, реализации JPA следуют этому шаблону. В других случаях вам может потребоваться выполнить кэширование самостоятельно.

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

Пул объектов

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

Есть две основные проблемы, с которыми мы должны столкнуться во время реализации:

  • Вывоз мусора
  • Многопоточная среда

Когда объект больше не нужен, его нужно удалить из пула. Удаление может быть сделано приложением, но это будет полностью устаревший и старый подход. Одним из главных преимуществ Java над C ++ является сборка мусора. Мы можем позволить GC собирать эти объекты. Для этого у нас не должно быть сильных ссылок в пуле объектов на объединенные объекты.

Ссылка

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

Вы можете заметить, что я не просто сказал «ссылки», но я сказал «сильные ссылки». Если вы узнали, что GC собирает объекты, когда нет ссылок на объект, то это было не совсем правильно. Дело в том, что это сильная ссылка, необходимая для GC для обработки неприкасаемого объекта. Чтобы быть еще более точным, сильная ссылка должна быть достижимой, путешествуя по другим сильным ссылкам из локальных переменных, статических полей и аналогичных вездесущих мест. Другими словами: (сильные) ссылки, которые указывают от одного мертвого объекта на другой, не учитываются, они вместе будут удалены и собраны.

Так что, если это сильные ссылки, то, вероятно, есть не такие сильные ссылки, как вы думаете. Вы правы. Существует класс с именем java.lang.ref.Reference и есть три других класса, расширяющих его. Классы:

  1. PhantomReference
  2. WeakReference и
  3. SoftReference

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

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

WeakHashMap

Слабая ссылка не тот класс, который мы должны использовать напрямую. Существует класс с именем WeakHashMap который ссылается на ключевые объекты, используя мягкие ссылки. Это на самом деле то, что нам нужно. Когда мы интернируем объект и хотим увидеть, находится ли он уже в пуле, мы ищем все объекты, чтобы увидеть, есть ли какой-либо из них, равный фактическому. Карта — это как раз то, что реализует эту возможность поиска. Удержание ключей в слабых ссылках позволит GC собирать ключевой объект, когда он никому не нужен.

Мы можем искать пока, что хорошо. Используя карту, мы также должны получить некоторую ценность. В этом случае мы просто хотим получить тот же объект, поэтому мы должны поместить объект на карту, когда его там нет. Однако помещение туда самого объекта разрушило бы то, что мы получили, оставив только слабые ссылки на тот же объект в качестве ключа. Мы должны создать и поставить слабую ссылку на объект в качестве ключа.

WeakPool

После этого объяснения вот код. Он просто говорит, что если существует объект, равный фактическому, тогда get(actualObject) должен вернуть его. Если его нет, get(actualObject) вернет null. Метод put(newObject) поместит новый объект в пул, и, если был найден новый, он заменит место старого на новый.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public class WeakPool<T> {
  private final WeakHashMap<T, WeakReference<T>> pool = new WeakHashMap<T, WeakReference<T>>();
  public T get(T object){
      final T res;
      WeakReference<T> ref = pool.get(object);
      if (ref != null) {
          res = ref.get();
      }else{
          res = null;
      }
      return res;
  }
  public void put(T object){
      pool.put(object, new WeakReference<T>(object));
  }
}

InternPool

Окончательное решение проблемы — внутренний пул, который очень легко реализовать с помощью уже имеющегося WeakPool . InternPool имеет слабый пул внутри, и в нем есть один единственный синхронизированный метод intern(T object) .

01
02
03
04
05
06
07
08
09
10
11
public class InternPool<T> {
  private final WeakPool<T> pool = new WeakPool<T>();
  public synchronized T intern(T object) {
    T res = pool.get(object);
    if (res == null) {
        pool.put(object);
        res = object;
    }
    return res;
  }
}

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

Многопотоковый

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

Гонки со сборщиком мусора

Несмотря на то, что различные потоки Java-приложения, использующие пул, не могут столкнуться с проблемами при использовании пула, мы все равно должны рассмотреть его, если есть какие-либо помехи потоку сборщика мусора.

Может случиться, что ссылка вернется к нулю при вызове метода get слабой ссылки. Это происходит, когда ключевой объект восстанавливается сборщиком мусора, но слабая хеш-карта в реализации слабого опроса все еще не удаляет запись. Даже если слабая реализация карты проверяет существование ключа всякий раз, когда карта запрашивается, это может произойти. Сборщик мусора может вставлять между вызовом get() для слабой хэш-карты и вызовом get() для возвращенной слабой ссылки. Хэш-карта вернула ссылку на объект, который существовал к моменту его возвращения, но, поскольку ссылка слабая, она была удалена до тех пор, пока выполнение нашего java-приложения не дошло до следующего оператора.

В этой ситуации реализация WeakPool возвращает WeakPool . Нет проблем. InternPool от этого тоже не страдает.

Если вы посмотрите на другие коды в вышеупомянутых темах stackoverflow , вы можете увидеть код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class InternPool<T> {
 
    private WeakHashMap<T, WeakReference<T>> pool =
        new WeakHashMap<T, WeakReference<T>>();
 
    public synchronized T intern(T object) {
        T res = null;
        // (The loop is needed to deal with race
        // conditions where the GC runs while we are
        // accessing the 'pool' map or the 'ref' object.)
        do {
            WeakReference<T> ref = pool.get(object);
            if (ref == null) {
                ref = new WeakReference<T>(object);
                pool.put(object, ref);
                res = object;
            } else {
                res = ref.get();
            }
        } while (res == null);
        return res;
    }
}

В этом коде автор создал бесконечный цикл для обработки этой ситуации. Не слишком привлекательно, но это работает. Маловероятно, что цикл будет выполняться бесконечное количество времени. Вероятно, не более чем в два раза. Конструкция сложна для понимания, сложна. Мораль: принцип единой ответственности. Сосредоточьтесь на простых вещах, разложите ваше приложение на простые компоненты.

Вывод

Несмотря на то, что Java выполняет интернирование только для String и некоторых объектов, к которым прикреплены примитивные типы, это возможно, а иногда желательно сделать интернирование. В этом случае стажировка не является автоматической, приложение должно явно выполнить ее. Два простых класса, перечисленные здесь, могут быть использованы для этого, используя команду copy paste в вашу базу кода, или вы можете:

1
2
3
4
5
<dependency>
  <groupId>com.javax0</groupId>
  <artifactId>intern</artifactId>
  <version>1.0.0</version>
</dependency>

импортировать библиотеку как зависимость от центрального плагина maven. Библиотека минимальна, содержит только эти два класса и доступна под лицензией Apache. Исходный код библиотеки находится на GitHub .

Ссылка: Java Object Interning от нашего партнера JCG Питера Верхаса в блоге Java Deep .