JavaRush /Java блог /Random /Кофе-брейк #214. Как спроектировать класс Singleton — точ...

Кофе-брейк #214. Как спроектировать класс Singleton — точка зрения интервьюера. Пользовательские аннотации в Java

Статья из группы Random

Как спроектировать класс Singleton — точка зрения интервьюера

Источник: Rahulraj Благодаря этой публикации вы узнаете о различных способах проектирования класса Singleton. Кофе-брейк #214. Как спроектировать класс Singleton — точка зрения интервьюера. Пользовательские аннотации в Java - 1Как спроектировать класс Singleton — очень популярный вопрос на собеседованиях по Java. А все потому, что он открывает интервьюеру возможность задать кандидату множество дополнительных вопросов. Можно спросить об основах Java, знании шаблонов проектирования и даже перейти к многопоточности или другим сценариям LLD (Low Level Design, низкоуровневое проектирование). В вопросе проектирования синглтона существует три подхода: базовое решение, потокобезопасное решение и оптимизированное потокобезопасное решение. Кандидату лучше начать с базового решения и переходить к другим вариантам лишь в том случае, если интервьюер вас об этом попросит. Если же вы сразу перейдете к остальным решениям, вполне вероятно, наткнетесь на несколько дополнительных вопросов о многопоточности. Лучший способ — начать объяснение с простого класса:

class Singleton{
  private Singleton(){}
  private static Singleton obj;

  public static Singleton getInstance(){
    if(obj==null){
      obj = new Singleton();
    }
    return obj;
  }
}
Минимальные требования для проектирования класса Singleton:
  • Частный конструктор.
  • Статический объект, который содержит значение одноэлементного объекта (уровень класса).
  • Статический метод для возврата объекта.
Одна из ошибок, которую на этом месте обычно допускают кандидаты, заключается в объяснении использования переменных static в качестве обходного пути для обеспечения безопасности потоков. Обратите внимание, что переменные static — это переменные уровня класса, которые по умолчанию являются общими для всех потоков. Переменные экземпляра не требуют синхронизации, если только они не подвергаются многопоточному сценарию. То есть, переменные static ни в коем случае НЕ являются потокобезопасными. Здесь переменная static используется только для представления одноэлементного объекта, что означает, что один объект доступен на протяжении всего жизненного цикла приложения. Следующий вопрос, который вам зададут, наверняка будет звучать так: “Это потокобезопасно?” Конечно, нет! Наверное, у вас может появиться идея синхронизировать метод, тем самым внедрив внутреннюю блокировку. При таком подходе общее изменяемое состояние становится атомарно доступным, а это именно то, что нам нужно для безопасности потоков:

class Singleton{
  private Singleton(){}
  private static Singleton obj;

  public static synchronized Singleton getInstance(){
    if(obj==null){
      obj = new Singleton();
    }
    return obj;
  }
}
Но знаете, в чем тут может быть проблема? Мы применяем intrinsic lock (внутреннюю блокировку) на весь метод. А это довольно дорогостоящая операция. Мы хотим убедиться, что синхронизируем ровно столько, чтобы охватить все изменяемые состояния. Мы не должны синхронизироваться слишком много или слишком мало. То есть, здесь есть проблема с производительностью. Кроме того, переменная obj может кэшироваться в соответствующих потоках. С таким состоянием obj может возникнуть race conditions, при котором критический раздел (часть программы, в которой осуществляется доступ к общей памяти) одновременно выполняется двумя или более потоками. Это приведет к некорректному поведению программы. Вот оптимизированная версия с учетом проблемы безопасности потоков:

class Singleton{
  private Singleton(){}
  private static volatile Singleton obj;

  public static synchronized Singleton getInstance(){
    if(obj==null){
      synchronized(this){
        if(obj==null){
            obj = new Singleton();
        }
      }
    }
    return obj;
  }
}
Обратите внимание, что использование volatile предназначено для решения проблемы visibility. В отличие от обычных переменных, переменные volatile гарантируют, что изменения помещаются в общую память, а не сохраняются в локальном кэше потока. Это означает, значения этих переменных будут считаны из оперативной памяти (ОЗУ). Иными словами, если один поток изменяет значение obj, то изменения будут видны и для других потоков, пытающихся использовать vobj. Поскольку мы не синхронизируем весь метод, несколько потоков могут одновременно обращаться к методу и проверять, является ли значение obj null или нет. Потоки могут выполнять эту проверку без получения блокировки. Здесь есть следующие возможные сценарии:
  • Поток A приходит и замечает, что значение obj уже установлено. Значение возвращается.
  • Поток A приходит и получает блокировку на obj. Меняем значение, если obj еще не изменено.
  • Поток A приходит и наблюдает, что поток B в настоящее время удерживает блокировку. Поток B завершил выполнение и изменил значение obj. Поток A возобновляет операцию и получает блокировку. Поскольку значение obj уже изменено, просто возвращаем значение.
Тут следует отметить, что ключевое слово volatile НЕ является гарантией безопасности потоков. Наверное, после этого вам зададут встречный: “Тогда почему мы упомянули это как оптимальное решение ранее?”. Правильный ответ состоит в том, что ключевые слова volatile обеспечивают безопасность потоков, когда есть несколько потоков чтения, но только один поток записи! Пока это верно, оптимизированное решение по-прежнему будет потокобезопасным. И наконец, в качестве последней меры безопасности потоков мы можем использовать встроенный потокобезопасный класс из Java: например, AtomicReference. Этот потокобезопасный класс позволяет нам выполнять атомарные составные операции с несколькими изменяемыми состояниями. Мы можем инкапсулировать все изменяемые состояния в объект AtomicReference и использовать его для атомарного доступа или изменения значения. На практике это выглядит следующим образом:

public class Singleton{
  private Singleton(){}
  private static AtomicReference<Singleton> obj;

  public static synchronized SingleTon getInstance(){
    if(obj==null){
      synchronized(SingleTon.class){
        if(obj==null){
          // устанавливаем значение объекта Singleton
          obj = new AtomicReference<>();
        }
      }
    }
    return obj.get();
  }

}
Это все на данный момент. Желаю удачи в написании кода! :)

Пользовательские аннотации в Java

Источник: Medium Представленное здесь руководство даст вам четкое представление о пользовательских аннотациях и о том, как они могут помочь процессу разработки. Кофе-брейк #214. Как спроектировать класс Singleton — точка зрения интервьюера. Пользовательские аннотации в Java - 2Аннотации стали неотъемлемой частью современного программирования. Они позволяют разработчикам добавлять в свой код метаданные, которые затем можно использовать для таких целей, как создание документации, настройка фреймворков и соблюдение ограничений. Аннотации — это мощный инструмент в Java-разработке, но что делать, если вам нужно создать свои собственные аннотации? Несмотря на то, что в Java доступно множество встроенных аннотаций, бывают ситуации, когда вам нужно определить свои аннотации чтобы удовлетворить определенные требования или добавить дополнительные методы в код. Создание пользовательских аннотаций может показаться сложной задачей, но если вы поймете основы, то будете поражены мощью и гибкостью, которые они могут привнести в код.

Определение пользовательских аннотаций

Аннотации похожи на маленькие стикеры, которые можно прикреплять к различным частям кода Java, таким как классы, методы и поля. Они содержат метаданные, предоставляющие дополнительную информацию об этих элементах. Представьте, что вы можете создавать свои собственные заметки с сообщениями, которые соответствуют вашим потребностям. Это именно то, что представляет собой пользовательская аннотация. Пользовательские аннотации — отличный способ добавить в код собственные метаданные. Они позволяют вам четко выразить свое намерение и сделать ваш код более самодокументируемым. Добавляя пользовательские аннотации к коду, вы можете дать важную информацию другим разработчикам, которые могут работать над вашим кодом в будущем. Например, вы можете создать пользовательскую аннотацию, которая помечает метод как устаревший, или с указанием определенного поведения для класса.

Создание пользовательской аннотации

Чтобы создать пользовательскую аннотацию, вам необходимо определить новый тип аннотации.

Синтаксис создания аннотации

Синтаксис создания аннотации аналогичен синтаксису создания интерфейса, где за ключевым словом @interface следует имя типа аннотации. В приведенном ниже примере тип аннотации называется MyAnnotation.

import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnnotation {
    String value();
}

Как указать свойства для аннотации

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

Ограничения характеристик

Существуют некоторые ограничения на типы свойств, которые можно определить в типе аннотации. Например, свойства не могут быть массивами или универсальными типами. Кроме того, типы свойств должны быть одним из следующих: byte, char, short, int, long, float, double, boolean, String, Class, enum или другим типом аннотации.

Правила хранения аннотаций

Правила хранения определяют, как долго хранится аннотация. Java поддерживает три различных политики хранения: RUNTIME, CLASS и SOURCE.

Определение правил хранения

Аннотация @Retention используется для указания правил хранения пользовательской аннотации. Аннотация @Retention принимает один аргумент, который является одним из трех правил хранения: RUNTIME, CLASS, SOURCE.
  • RUNTIME: сохраняется во время выполнения и может запрашиваться через отражение. Это означает, что информация аннотации доступна во время выполнения кода.
  • CLASS: сохраняется в файле класса, но недоступна во время выполнения. Это означает, что информация аннотации недоступна во время выполнения, но может использоваться другими инструментами или библиотеками, анализирующими код.
  • SOURCE: сохраняется только в исходном коде и удаляется при компиляции. Это означает, что информация аннотаций используется только в процессе компиляции, но не включается в скомпилированный код.

Целевые типы аннотаций

Целевые типы указывают, где в коде можно использовать аннотацию. Java поддерживает восемь различных типов целей: TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, ANNOTATION_TYPE и PACKAGE.

Обработчики аннотаций

Обработчики аннотаций (Annotation processors) — это инструменты, которые могут обрабатывать аннотации и генерировать код на их основе. Обработчики аннотаций можно использовать для автоматизации повторяющихся задач, таких как создание стандартного кода. Чтобы создать обработчик аннотаций, нам нужно реализовать интерфейс javax.annotation.processing.Processor. В интерфейсе Processor есть несколько методов, которые мы должны реализовать, в том числе process(). Он вызывается для каждой аннотации, которую мы хотим обработать. Вот пример обработчика аннотаций:

public class CustomAnnotationProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(CustomAnnotation.class)) {
            // обрабатываем аннотированный элемент
        }
        return true;
    }
}
Здесь мы создали обработчик (процессор) аннотаций с именем CustomAnnotationProcessor. Процессор расширяет класс javax.annotation.processing.AbstractProcessor, который предоставляет реализацию по умолчанию для некоторых методов интерфейса Processor.
Комментарии (2)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Dima Makarov Уровень 42
4 апреля 2023
"Но знаете, в чем тут может быть проблема? Мы применяем intrinsic lock (внутреннюю блокировку) на весь метод. А это довольно дорогостоящая операция. Мы хотим убедиться, что синхронизируем ровно столько, чтобы охватить все изменяемые состояния. Мы не должны синхронизироваться слишком много или слишком мало. " - И тут же мы к геттеру с подписью синхронайзд добавляем еще один блок синхронайзд. Почему ? Какая логика? Как это относится к тому, что выше написано?