Привет, Хабр!
Вы когда-нибудь замечали, как котики, лениво потягиваясь и сворачиваясь клубком, экономят энергию и действуют только тогда, когда это действительно необходимо?
Как и наши хвостатые друзья, существует такой паттерн как Lazy Loading, который позволяет экономить ресурсы, инициализируя объекты только тогда, когда они действительно нужны.
Рассмотрим, как мы можем применить этот котиковый подход в Java. Будем как котики — умными, экономными и эффективными!
Реализация
В Java существует несколько основных подходов к реализации Lazy Loading: Lazy Initialization, Proxy и Holder.
Lazy Initialization
Lazy Initialization предполагает отложенную инициализацию объекта до первого вызова, при котором он необходим. Это один из самых базовых способов реализации Lazy Loading:
public class LazyInitializedSingleton {
private static LazyInitializedSingleton instance;
private LazyInitializedSingleton() {
// private constructor
}
public static LazyInitializedSingleton getInstance() {
if (instance == null) {
instance = new LazyInitializedSingleton();
}
return instance;
}
public void displayMessage() {
System.out.println("Lazy Initialization Singleton instance.");
}
}
public class Main {
public static void main(String[] args) {
LazyInitializedSingleton instance = LazyInitializedSingleton.getInstance();
instance.displayMessage();
}
}
Объект LazyInitializedSingleton
создается только при первом вызове метода getInstance()
. Хоть выглядит и просто, но по сути это не является потокобезопасным.
Для потокобезопасности можно использовать синхронизацию:
public class ThreadSafeLazyInitializedSingleton {
private static ThreadSafeLazyInitializedSingleton instance;
private ThreadSafeLazyInitializedSingleton() {
// private constructor
}
public static synchronized ThreadSafeLazyInitializedSingleton getInstance() {
if (instance == null) {
instance = new ThreadSafeLazyInitializedSingleton();
}
return instance;
}
public void displayMessage() {
System.out.println("Thread-Safe Lazy Initialization Singleton instance.");
}
}
Proxy
Паттерн Proxy позволяет контролировать доступ к объекту, отложив его создание до момента первого обращения. В Java можно использовать динамические прокси или вручную реализовать прокси-классы. Например, с динамическим прокси:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
interface Image {
void display();
}
class RealImage implements Image {
private String filename;
public RealImage(String filename) {
this.filename = filename;
loadImageFromDisk();
}
private void loadImageFromDisk() {
System.out.println("Loading " + filename);
}
public void display() {
System.out.println("Displaying " + filename);
}
}
class ImageProxyHandler implements InvocationHandler {
private String filename;
private Image realImage;
public ImageProxyHandler(String filename) {
this.filename = filename;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (realImage == null) {
realImage = new RealImage(filename);
}
return method.invoke(realImage, args);
}
}
public class Main {
public static void main(String[] args) {
Image imageProxy = (Image) Proxy.newProxyInstance(
Image.class.getClassLoader(),
new Class[]{Image.class},
new ImageProxyHandler("test.jpg"));
imageProxy.display(); // изображение загружается и отображается
}
}
ImageProxyHandler
откладывает создание объекта RealImage
до первого вызова метода display()
.
Holder
Подход Holder реализует ленивую инициализацию с использованием вложенного статического класса. Веьсма потокобезопасно и обеспечивает ленивую инициализацию без необходимости синхронизации:
public class HolderSingleton {
private HolderSingleton() {
// private constructor
}
private static class Holder {
private static final HolderSingleton INSTANCE = new HolderSingleton();
}
public static HolderSingleton getInstance() {
return Holder.INSTANCE;
}
public void displayMessage() {
System.out.println("Holder Singleton instance.");
}
}
public class Main {
public static void main(String[] args) {
HolderSingleton instance = HolderSingleton.getInstance();
instance.displayMessage();
}
}
Класс Holder
содержит статическое поле INSTANCE
, которое инициализируется только при первом вызове метода getInstance()
.
Lazy Loading в библиотеках и фреймворках
Hibernate
В Hibernate, Lazy Loading можно настроить с помощью аннотации @ManyToOne
, @OneToMany
, @OneToOne
, @ManyToMany
и указания атрибута fetch = FetchType.LAZY
:
@Entity
public class Company {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "company", fetch = FetchType.LAZY)
private List<Employee> employees;
// getters and setters
}
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "company_id")
private Company company;
// getters and setters
}
При загрузке компании, связанные с ней сотрудники не будут загружены сразу, а будут загружены только при первом доступе к полю employees
.
Могут возникнуть некоторые ошибки при работе с Lazy в Hibernate:
LazyInitializationException возникает, когда ленивые данные пытаются быть загружены за пределами сессии.
Решение:
Использование @Transactional: обеспечивает, что сессия Hibernate активна при доступе к ленивым коллекциям.
@Service
public class CompanyService {
@Autowired
private CompanyRepository companyRepository;
@Transactional
public Company getCompanyWithEmployees(Long companyId) {
Company company = companyRepository.findById(companyId).orElseThrow();
// доступ к ленивой коллекции
company.getEmployees().size();
return company;
}
}
Инициализация внутри транзакции: загружать ленивые данные в пределах активной транзакции.
@EntityGraph(attributePaths = {"employees"})
@Query("SELECT c FROM Company c WHERE c.id = :id")
Optional<Company> findByIdWithEmployees(@Param("id") Long id);
@Lazy в Spring
Spring предоставляет аннотацию @Lazy
для ленивой инициализации бинов. В основном юзают для уменьшения времени старта приложения и оптимизации использования ресурсов.
Пример:
@Configuration
public class AppConfig {
@Bean
@Lazy
public ServiceBean serviceBean() {
return new ServiceBean();
}
}
@Component
public class ClientBean {
private final ServiceBean serviceBean;
@Autowired
public ClientBean(@Lazy ServiceBean serviceBean) {
this.serviceBean = serviceBean;
}
public void doSomething() {
serviceBean.performAction();
}
}
Бин ServiceBean
будет инициализирован только при первом доступе к нему через ClientBean
.
Примеры конфигураций:
Конфигурация контекста:
@Lazy
@Configuration
@ComponentScan(basePackages = "com.example.lazy")
public class LazyConfig {
@Bean
public MainService mainService() {
return new MainService();
}
@Bean
@Lazy
public SecondaryService secondaryService() {
return new SecondaryService();
}
}
Тестирование ленивой инициализации:
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = LazyConfig.class)
public class LazyInitializationTest {
@Autowired
private ApplicationContext context;
@Test
public void testLazyInitialization() {
assertFalse(context.containsBean("secondaryService"));
MainService mainService = context.getBean(MainService.class);
mainService.callSecondaryService();
assertTrue(context.containsBean("secondaryService"));
}
}
В тесте проверяется, что бин secondaryService
не создается при старте контекста, но создается при первом доступе через метод callSecondaryService
.
Lazy loading следует применять в тех случаях, когда требуется отложенная загрузка ресурсов или данных для улучшения скорости загрузки.
Однако, не стоит злоупотреблять lazy loading, так как это может привести к нежелательным задержкам и проблемам с производительностью. Например, если объекты часто запрашиваются и необходимы сразу после инициализации, lazy loading может привести к излишней нагрузке на систему.
В завершение приглашаю Java-разработчиков на открытые уроки от Otus:
11 июня: Применение batch-операций в Jdbc. Научимся максимально быстро и эффективно сохранить в базу данных сотни строк сразу. Регистрация по ссылке
25 июня: Redis и Java приложения. Посмотрим, как в java приложениях можно
использовать Redis в качестве in-memory кеша, для каких задач это может быть полезно. Регистрация по ссылке
Комментарии (5)
m_chrom
06.06.2024 06:25Делать весь метод
getInstance()
вThreadSafeLazyInitializedSingleton
synchronized все-таки несколько расточительно. Lock монитора не бесплатный, а нужен он ровно для одного вызова, который инициализирует переменную.
Еще, кажется, будут проблемы с тем, что полеinstance
не volatile, а значит может быть закэшировано тредом. Но тут не уверен, надо перечитать доку что там гарантирует JMM в этих случаях.
PS: не покритиковать ради, а из заботы о новичках. Люди могут потащить код из примеров в проекты и подложить себе мин замедленного действия. А так материал крутой и полезный.
GospodinKolhoznik
А где в первом примере лень? Обычный синглтон, ни разу не ленивый. getInstance() вызывает конструктор объекта.
Лень была бы если бы getInstance не запускала конструктор, а давала бы некоторый дополнительный объект, который содержал бы в себе все необходимые методы исходного объекта и в каждом методе сделана проверка того, а создан ли исходный объект, если нет то запускается его конструктор, а потом вызывается метод исходного объекта.
Да, это муторно, зато это будет настоящая лень.
AstarothAst
Вроде в том и смысл, что пока не вызван getInstance() - объект не существует. Если не будет вызван ни разу - объект лениво никогда не создастся. Ваш вариант отличается только тем, что вызов конструктора перемещается в метод по сути проксика. Смысл это имеет только в одном случае - если мы допустим, что getInstance() вызывается в холостую и целевой объект нам не нужен, но зачем такое допускать? Если такое произойдет значит это облажался программист, пусть идет и исправляет.
GospodinKolhoznik
В строчке `LazyInitializedSingleton instance = LazyInitializedSingleton.getInstance();` конструктор зачем-то вызывается, объект инициализируется, но при этом потребности в нём ещё нет. Потребность появляется только на следующей строчке. А могла бы и не возникнуть.
И что значит программист облажался? Почти всегда доподленно неизвестно, будет ли instance вообще использоваться хоть раз или нет. В приведенном коде очевидно, что instance нужен, но если бы instance использовался где то в другой функции, куда бы он передавался как аргумент, то зачастую очень сложно понять - а дейсвительно ли он нужен будет той функции, или в процессе исполнения функция обойдется без него?
Поэтому я и написал, что LazyInitializedSingleton вовсе не ленивый - его невозможно передать какой либо функции в качестве аргумента, не запустив конструктор. Невозможно положить его в хэш-мапу вместе с другими ленивыми объектами, чтобы дергать их оттуда по мере необходимости. Неизбежно потребуется создавать класс-обёртку, или что-то ещё городить, насколько фантазия позволит.
И конечно в реальности я бы не стал городить такой класс, где проверка на инициализацию сидит в каждом методе, гораздо проще сделать фабрику объекта с ленивой инициализацией. Но фабрика объекта и сам объект это разые типы данных. И даже если фабрика лениво инициализирует объект, то сам то объект всё равно не ленивый.