Программирование:Лабораторная работа №7: различия между версиями
Ivabus (обсуждение | вклад) Импорт жабка |
Ivabus (обсуждение | вклад) м Ivabus переименовал страницу Программирование. Лабораторная работа №7 в Программирование:Лабораторная работа №7 |
||
(нет различий)
|
Текущая версия от 08:20, 27 июня 2025
Методичка для подготовки к защите лабораторной работы
Раздел 1. Основы многопоточности
1.1. Многопоточность. Класс Thread
, интерфейс Runnable
Что такое многопоточность?
- Ответ: Многопоточность — это способ выполнения программы, при котором несколько потоков (threads) исполняются одновременно (или псевдо-одновременно на одноядерном процессоре) в рамках одного процесса. У всех потоков одного процесса общая память (heap), что позволяет им легко обмениваться данными, но также создаёт проблемы с синхронизацией доступа к этим данным.
Для чего она нужна?
- Ответ:
- Повышение производительности: На многоядерных системах задачи можно распараллелить и выполнять действительно одновременно, что ускоряет вычисления.
- Улучшение отклика интерфейса (Responsiveness): В GUI-приложениях (например, Swing, Android) долгие операции (загрузка файла, сетевой запрос) выносятся в фоновый поток, чтобы основной поток (UI Thread) не “зависал” и продолжал обрабатывать действия пользователя.
- Эффективное использование ресурсов: Пока один поток ждёт ответа от сети или диска (I/O operation), процессор может выполнять другой поток.
Как создать поток в Java?
- Ответ: Есть два основных способа:
- Наследование от класса
Thread
:- Создать класс, который наследуется от
java.lang.Thread
. - Переопределить его метод
run()
, в котором будет содержаться логика потока. - Создать экземпляр этого класса и вызвать его метод
start()
. Важно: именноstart()
, а неrun()
. Вызовrun()
просто выполнит код в текущем потоке, аstart()
создаст новый системный поток и в нём уже вызоветrun()
.
class MyThread extends Thread { @Override public void run() { System.out.println("Поток, созданный наследованием, запущен: " + Thread.currentThread().getName()); } } // Запуск: MyThread myThread = new MyThread(); myThread.start();
- Создать класс, который наследуется от
- Реализация интерфейса
Runnable
:- Создать класс, который реализует интерфейс
java.lang.Runnable
. - Реализовать его единственный метод
run()
. - Создать экземпляр этого класса, передать его в конструктор класса
Thread
и вызвать у объектаThread
методstart()
.
class MyRunnable implements Runnable { @Override public void run() { System.out.println("Поток, созданный через Runnable, запущен: " + Thread.currentThread().getName()); } } // Запуск: MyRunnable myRunnable = new MyRunnable(); Thread thread = new Thread(myRunnable); thread.start(); // Или с помощью лямбда-выражения (с Java 8): Thread lambdaThread = new Thread(() -> System.out.println("Лямбда-поток запущен!")); lambdaThread.start();
- Создать класс, который реализует интерфейс
- Наследование от класса
Какой способ лучше и почему?
- Ответ: Реализация
Runnable
является предпочтительным способом.- Причина 1: Гибкость и композиция. Java не поддерживает множественное наследование классов. Если ваш класс уже от чего-то наследуется, вы не сможете сделать его потоком через
extends Thread
. Интерфейсы же можно реализовывать в любом количестве. Это соответствует принципу “предпочитайте композицию наследованию”. - Причина 2: Разделение ответственности. Класс
Thread
отвечает за управление потоком (его запуск, прерывание, состояние). ИнтерфейсRunnable
отвечает только за задачу (task
), которую нужно выполнить. Разделяя задачу от исполнителя, мы делаем код более чистым и гибким. Одну и ту же задачу (Runnable
) можно передать разным исполнителям (например, в пул потоков).
- Причина 1: Гибкость и композиция. Java не поддерживает множественное наследование классов. Если ваш класс уже от чего-то наследуется, вы не сможете сделать его потоком через
1.2. Модификатор synchronized
Что такое “состояние гонки” (race condition)?
- Ответ: Это ситуация в многопоточной среде, когда результат выполнения кода зависит от того, в какой последовательности потоки получат доступ к общему ресурсу. Обычно это приводит к непредсказуемым и ошибочным результатам. Классический пример — инкремент общего счётчика.
Как synchronized
решает эту проблему?
- Ответ: Модификатор
synchronized
обеспечивает взаимное исключение (mutual exclusion). Он гарантирует, что только один поток может выполнять защищённый (synchronized
) блок кода в один момент времени для одного и того же объекта-монитора.
Как он работает “под капотом”?
- Ответ: Каждый объект в Java имеет ассоциированный с ним внутренний замок (intrinsic lock), или монитор.
- Когда поток входит в
synchronized
метод экземпляра, он пытается захватить монитор объектаthis
. - Когда поток входит в
static synchronized
метод, он пытается захватить монитор объектаClassName.class
. - Когда поток входит в
synchronized
блок, он пытается захватить монитор объекта, указанного в скобках.
- Когда поток входит в
Где можно использовать synchronized
?
1. Методы экземпляра:
public synchronized void increment() { // Код, защищённый монитором объекта 'this' counter++; }
2. Статические методы:
public static synchronized void staticIncrement() { // Код, защищённый монитором объекта 'MyClass.class' staticCounter++; }
3. Блоки кода:
private final Object lock = new Object(); // Явный объект для блокировки public void doWork() { // ... какой-то код, не требующий синхронизации ... synchronized (lock) { // Захват монитора объекта 'lock' // Критическая секция: только один поток одновременно } // ... другой код ... }
Почему synchronized(this)
может быть плохой практикой?
- Ответ: Если вы используете
synchronized(this)
илиsynchronized
метод, вы выставляете монитор вашего объекта наружу. Любой внешний код может также синхронизироваться по вашему объекту и вызвать взаимную блокировку (deadlock) или проблемы с производительностью. Использование приватного финального объекта (private final Object lock = new Object();
) является более безопасной и инкапсулированной практикой.
Раздел 2. Механизмы координации потоков
2.1. Методы wait()
, notify()
, notifyAll()
Для чего нужны эти методы, если уже есть synchronized
?
- Ответ:
synchronized
решает проблему взаимного исключения (кто сейчас работает с ресурсом). Методыwait()
,notify()
,notifyAll()
решают проблему координации или взаимодействия потоков (когда один поток должен дождаться определённого условия, которое создаст другой поток). Классический пример — паттерн “Производитель-Потребитель”.
Как они работают?
wait()
: Поток, вызвавшийwait()
на объекте-мониторе, немедленно освобождает замок этого монитора и переходит в состояние ожидания (WAITING). Он будет “спать”, пока другой поток не вызоветnotify()
илиnotifyAll()
на этом же самом мониторе.notify()
: “Будит” один случайный поток, ожидающий на этом мониторе. Пробуждённый поток не начинает выполняться сразу, а переходит в состояние BLOCKED и пытается снова захватить замок.notifyAll()
: “Будит” все потоки, ожидающие на этом мониторе. Они все начинают бороться за замок, но только один его получит и продолжит выполнение.
Ключевое правило: Методы wait()
, notify()
, notifyAll()
обязаны вызываться только из synchronized
блока (или метода), который синхронизирован по тому же объекту. Иначе будет выброшено исключение IllegalMonitorStateException
.
Почему это правило существует?
- Ответ: Чтобы избежать “потерянного пробуждения” (lost wakeup). Представьте:
- Поток A проверяет условие (например,
while (list.isEmpty())
) и решает, что нужно ждать. - В этот момент планировщик переключает контекст на Поток B.
- Поток B добавляет элемент в список и вызывает
notify()
. - Планировщик возвращает управление Потоку A.
- Поток A вызывает
wait()
и засыпает навсегда, так как сигналnotify()
уже был отправлен и потерян.
synchronized
блок гарантирует, что проверка условия и вызовwait()
будут атомарной операцией относительно изменения этого условия и вызоваnotify()
. - Поток A проверяет условие (например,
Почему wait()
всегда нужно использовать в цикле while
, а не в if
?
synchronized (monitor) {
while (conditionIsNotMet) { // ПРАВИЛЬНО
monitor.wait();
}
// ... делать работу ...
}
// if (conditionIsNotMet) { monitor.wait(); } // НЕПРАВИЛЬНО
- Ответ: Из-за феномена “ложного пробуждения” (spurious wakeup). Спецификация Java допускает, что поток может проснуться из
wait()
без вызоваnotify()
/notifyAll()
. Это редкое, но возможное явление. Циклwhile
гарантирует, что после пробуждения поток повторно проверит условие. Если пробуждение было ложным, он просто снова уснёт.
notify()
vs notifyAll()
: что выбрать?
- Ответ:
notifyAll()
является более безопасным и предпочтительным выбором в большинстве случаев.notify()
можно использовать для оптимизации, только если вы абсолютно уверены, что:- Все ожидающие потоки взаимозаменяемы (любой из них может продолжить работу).
- Только одно условие изменилось, и его хватит только для одного потока. В остальных случаях использование
notify()
может привести к тому, что “проснётся” не тот поток, и система “зависнет”.
2.2. Интерфейсы Lock
и Condition
Зачем нужны Lock
и Condition
, если есть synchronized
и wait
/notify
?
- Ответ:
java.util.concurrent.locks.Lock
— это более гибкая и мощная альтернативаsynchronized
.- Возможность попробовать захватить замок: Метод
tryLock()
пытается захватить замок и немедленно возвращаетtrue
илиfalse
, не блокируя поток. Есть и версия с таймаутомtryLock(long time, TimeUnit unit)
. - Прерываемые блокировки: Метод
lockInterruptibly()
позволяет прервать поток, пока он ждёт захвата замка.synchronized
блок не может быть прерван. - Честность (Fairness): Конструктор
ReentrantLock
(основная реализацияLock
) принимает флагfair
. В “честном” режиме замок будет отдан потоку, который дольше всех ждёт.synchronized
не гарантирует честности. - Несколько условий: Один
Lock
может быть ассоциирован с несколькими объектамиCondition
. Это аналогwait/notify
, но позволяет группировать ожидающие потоки по разным условиям. Уsynchronized
только один набор ожидающих потоков на монитор.
- Возможность попробовать захватить замок: Метод
Как правильно использовать Lock
?
Ответ: Всегда использовать конструкцию
try...finally
. Это критически важно, потому что в отличие отsynchronized
, замок не освободится автоматически при исключении.private final Lock lock = new ReentrantLock(); public void doWork() { lock.lock(); // Захват замка try { // Критическая секция } finally { lock.unlock(); // Гарантированное освобождение замка в блоке finally } }
Как использовать Condition
?
Ответ:
Condition
привязывается кLock
. Его методыawait()
(аналогwait()
),signal()
(аналогnotify()
),signalAll()
(аналогnotifyAll()
) работают так же, но для конкретного условия.class BoundedBuffer { final Lock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); // Условие: буфер не полон final Condition notEmpty = lock.newCondition(); // Условие: буфер не пуст final Object[] items = new Object[100]; int putptr, takeptr, count; public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); // Ждем, пока появится место items[putptr] = x; if (++putptr == items.length) putptr = 0; ++count; notEmpty.signal(); // Сигнализируем, что буфер больше не пуст } finally { lock.unlock(); } } // ... метод take() с await() на notEmpty и signal() на notFull ... }
Раздел 3. Утилиты из java.util.concurrent
3.1. Классы-синхронизаторы
Что это такое и зачем они нужны?
- Ответ: Это готовые высокоуровневые классы для решения типовых задач синхронизации, которые сложны в реализации через
wait/notify
илиLock
. Semaphore
(Семафор):- Аналогия: Парковка с ограниченным числом мест или клуб со строгим фейс-контролем, пропускающим внутрь не более N человек.
- Что делает: Ограничивает количество потоков, которые могут одновременно получить доступ к некоторому ресурсу.
- Методы:
acquire()
— запрашивает разрешение (если нет, блокируется),release()
— освобождает разрешение. - Пример использования: Ограничение количества одновременных подключений к базе данных.
CountDownLatch
(Защёлка обратного отсчёта):- Аналогия: Стартовые ворота на скачках. Они не откроются, пока все жокеи не будут готовы. Или судья на финише, который ждёт всех бегунов.
- Что делает: Позволяет одному или нескольким потокам ждать, пока не завершится N операций, выполняемых в других потоках. Защёлка одноразовая — после того, как счётчик дошёл до нуля, её нельзя использовать повторно.
- Методы:
countDown()
— уменьшает счётчик на 1,await()
— блокирует поток, пока счётчик не станет равен 0. - Пример использования: Основной поток ждёт, пока все рабочие потоки не завершат инициализацию.
CyclicBarrier
(Циклический барьер):- Аналогия: Группа туристов, договорившихся встретиться у достопримечательности, чтобы продолжить путь вместе. Никто не уходит, пока все не соберутся.
- Что делает: Позволяет группе потоков ждать друг друга в определённой точке (барьере) перед тем, как продолжить выполнение. Барьер можно использовать повторно (
cyclic
). - Метод:
await()
— поток сообщает, что он достиг барьера, и блокируется, пока все остальные потоки не вызовутawait()
. Последний пришедший поток “сбрасывает” барьер и все продолжают работу. - Пример использования: Распараллеливание сложного вычисления, где каждый этап зависит от результатов всех потоков на предыдущем этапе.
Phaser
(Фазер):- Что делает: Более гибкая и мощная версия
CyclicBarrier
. Позволяет динамически изменять количество участвующих в синхронизации потоков (сторон). - Пример использования: Сложные многофазные вычисления с переменным количеством участников на каждом этапе.
- Что делает: Более гибкая и мощная версия
Разница между CountDownLatch
и CyclicBarrier
?
CountDownLatch
— это “один ко многим” (один или несколько потоков ждут завершения N других операций). Он одноразовый.CyclicBarrier
— это “многие к многим” (N потоков ждут друг друга). Он многоразовый (циклический).
3.2. volatile
и Атомарные типы
Что гарантирует модификатор volatile
?
- Ответ:
volatile
решает проблему видимости (visibility) переменных между потоками. Он гарантирует две вещи:- Видимость: Любая запись в
volatile
переменную одним потоком становится немедленно видна всем другим потокам, которые читают эту переменную. Это предотвращает ситуацию, когда поток работает с устаревшим значением переменной из своего локального кэша процессора. - Упорядочивание (Happens-Before): Запись в
volatile
переменную устанавливает отношение “происходит-до” (happens-before). Все операции, которые были до записи вvolatile
переменную в одном потоке, будут видны другому потоку после чтения из этой жеvolatile
переменной.
- Видимость: Любая запись в
Чего volatile
НЕ гарантирует?
- Ответ: Атомарности для составных операций (таких как инкремент
i++
). Операцияi++
состоит из трёх шагов: чтение старого значения, вычисление нового, запись нового. Между этими шагами другой поток может изменить значениеi
, и результат будет некорректным.volatile
гарантирует только, что каждый из этих шагов будет виден другим потокам, но не то, что вся троица выполнится как единое целое.
Для чего тогда он нужен?
- Ответ: Для простых флагов и указателей, где запись не зависит от предыдущего значения.
- Пример: Флаг для остановки потока.
private volatile boolean running = true; public void run() { while (running) { // делать работу } } public void stopThread() { running = false; // Эта запись будет видна потоку в цикле while }
Что такое атомарные типы и как они решают проблему?
- Ответ: Атомарные типы — это классы из пакета
java.util.concurrent.atomic
(например,AtomicInteger
,AtomicLong
,AtomicBoolean
), которые предоставляют методы для выполнения атомарных операций над переменными. - Они решают проблему атомарности для операций типа “проверить-и-изменить”, как инкремент.
- Как работают: “Под капотом” они используют низкоуровневые, аппаратно-поддерживаемые инструкции CAS (Compare-And-Swap).
- CAS-операция — это атомарная инструкция, которая принимает три аргумента: адрес в памяти (V), ожидаемое старое значение (A) и новое значение (B). Она атомарно обновляет значение в V на B, только если текущее значение в V равно A. В противном случае ничего не происходит.
- Методы вроде
incrementAndGet()
уAtomicInteger
работают в цикле: читают текущее значение, вычисляют новое, а затем с помощью CAS пытаются записать новое значение, если старое не изменилось. Если изменилось — повторяют операцию. Это называется lock-free или optimistic locking, так как не используется блокировка потока.
// НЕ ПОТОКОБЕЗОПАСНО private int counter = 0; public void increment() { counter++; } // ПОТОКОБЕЗОПАСНО private AtomicInteger atomicCounter = new AtomicInteger(0); public void increment() { atomicCounter.incrementAndGet(); }
3.3. Коллекции из java.util.concurrent
Чем ConcurrentHashMap
лучше Collections.synchronizedMap(new HashMap<>())
?
Collections.synchronizedMap
: Оборачивает обычную карту и делает каждый её методsynchronized
. Это означает, что в любой момент времени с картой может работать только один поток (чтение или запись). Это блокировка на всю коллекцию, что сильно снижает производительность при большом количестве потоков.ConcurrentHashMap
: Использует более сложный механизм блокировки по сегментам (или бакетам в новых версиях). Чтение в большинстве случаев вообще не требует блокировок. Операции записи блокируют только ту часть карты (сегмент), которая затрагивается, а не всю карту целиком. Это позволяет множеству потоков одновременно работать с разными частями карты, обеспечивая высокую степень параллелизма.
Какие ещё полезные concurrent-коллекции существуют?
CopyOnWriteArrayList
:- Принцип работы: Любая операция изменения (
add
,remove
,set
) создаёт полную копию внутреннего массива, вносит изменения в копию и затем атомарно заменяет ссылку на старый массив новой. - Плюсы: Операции чтения (
get
,iterator
) очень быстрые, так как не требуют никаких блокировок и работают с неизменяемым “снимком” массива. Итераторы никогда не бросятConcurrentModificationException
. - Минусы: Операции записи очень дорогие (копирование всего массива).
- Когда использовать: Когда количество чтений многократно превышает количество записей. Типичный пример — список слушателей (listeners).
- Принцип работы: Любая операция изменения (
- Блокирующие очереди (
BlockingQueue
):- Что это: Это интерфейс для очередей, которые являются потокобезопасными и имеют дополнительные методы, блокирующие поток, если операция не может быть выполнена немедленно.
- Методы:
put(E e)
: добавляет элемент, блокируя поток, если очередь полна.take()
: извлекает элемент, блокируя поток, если очередь пуста.
- Реализации:
ArrayBlockingQueue
(на массиве, ограниченный размер),LinkedBlockingQueue
(на связном списке, опционально ограниченный),SynchronousQueue
(очередь без внутреннего буфера, операцияput
ждётtake
). - Когда использовать: Это ключевой компонент для паттерна “Производитель-Потребитель”. Производители кладут задачи в очередь, потребители забирают их оттуда.
Раздел 4. Управление потоками
4.1. Executor
, ExecutorService
, Callable
, Future
Зачем нужен Executor Framework
, если можно создавать потоки через new Thread().start()
?
- Ответ: Ручное управление потоками — это плохая практика:
- Высокие накладные расходы: Создание и уничтожение системного потока — дорогая операция.
- Неограниченное создание потоков: Если на каждый запрос создавать новый поток, можно легко исчерпать ресурсы системы и вызвать её крах (
OutOfMemoryError
). - Сложность управления: Нет простого способа управлять жизненным циклом потоков, получать результаты их работы или обрабатывать исключения.
Executor Framework
абстрагирует создание и управление потоками. Вы просто передаёте ему задачи (Runnable
илиCallable
), а он сам решает, как и когда их выполнять, переиспользуя потоки.
Расскажите про иерархию интерфейсов.
Executor
: Базовый интерфейс с одним методомexecute(Runnable command)
. Он просто принимает задачу на выполнение.ExecutorService
: РасширяетExecutor
. Добавляет:- Управление жизненным циклом:
shutdown()
(перестаёт принимать новые задачи, но выполняет уже начатые),shutdownNow()
(пытается прервать все активные задачи и возвращает список невыполненных). - Возможность отправки
Callable
:submit(Callable<T> task)
.
- Управление жизненным циклом:
Callable<V>
: АналогRunnable
, но его методcall()
может возвращать результат типаV
и бросать проверяемое исключение.Future<V>
: Объект-заместитель, который представляет результат асинхронной операции.- Методы:
get()
: Блокирует текущий поток и ждёт завершения задачи, после чего возвращает её результат. Если в задаче было исключение,get()
броситExecutionException
.isDone()
: Проверяет, завершена ли задача.cancel(boolean mayInterruptIfRunning)
: Пытается отменить задачу.
- Методы:
4.2. Пулы потоков (Thread Pools)
Что такое пул потоков и зачем он нужен?
- Ответ: Пул потоков — это набор заранее созданных рабочих потоков (
worker threads
), которые готовы выполнять задачи. Когда задача поступает, она передаётся одному из свободных потоков в пуле. После выполнения задачи поток не умирает, а возвращается в пул и ждёт следующей задачи. - Преимущества:
- Сокращение накладных расходов: Потоки переиспользуются, а не создаются заново.
- Контроль над ресурсами: Ограничивается максимальное число одновременно работающих потоков, что предотвращает истощение ресурсов.
- Упрощение управления: Предоставляет готовые механизмы для постановки задач в очередь и управления их выполнением.
Как создать пул потоков?
- Ответ: Через статические фабричные методы класса
java.util.concurrent.Executors
:Executors.newFixedThreadPool(int nThreads)
: Создаёт пул с фиксированным числом потоков. Если все потоки заняты, новые задачи помещаются в неограниченную очередь. Хорош для задач, интенсивно использующих CPU.Executors.newCachedThreadPool()
: Создаёт пул, который переиспользует существующие потоки, а если их нет — создаёт новые. Неиспользуемые потоки удаляются через 60 секунд. Подходит для большого количества короткоживущих задач. Опасно: может создать неограниченное число потоков.Executors.newSingleThreadExecutor()
: Пул из одного потока. Гарантирует, что все задачи будут выполняться последовательно, в порядке их поступления.Executors.newScheduledThreadPool(int corePoolSize)
: Пул для выполнения отложенных или периодических задач.
Важное замечание для защиты: В реальных (production) системах использовать фабрики Executors
не рекомендуется, так как они создают пулы с неограниченными очередями (Fixed
, Single
) или неограниченным числом потоков (Cached
), что может привести к OutOfMemoryError
. Правильным подходом является прямое создание экземпляра ThreadPoolExecutor
, где можно точно настроить все параметры (размер ядра, максимальный размер, тип очереди, política отвержения).
Раздел 5. JDBC (Java Database Connectivity)
5.1. Порядок взаимодействия с БД. DriverManager
, Connection
Опишите стандартный алгоритм работы с БД через JDBC.
- Ответ:
- Загрузка драйвера БД (шаг устарел): Раньше требовался вызов
Class.forName("com.mysql.cj.jdbc.Driver")
. В современных версиях JDBC (4.0+) драйверы загружаются автоматически с помощью механизмаServiceLoader
, если jar-файл драйвера есть в classpath. Но знать об этом шаге нужно. - Получение соединения (
Connection
): Вызвать статический методDriverManager.getConnection(url, user, password)
.url
: строка специального формата, указывающая тип БД, хост, порт и имя базы (например,jdbc:postgresql://localhost:5432/mydatabase
).user
,password
: учётные данные для доступа.
- Создание объекта для выполнения запроса (
Statement
илиPreparedStatement
): Вызвать у объектаConnection
методcreateStatement()
илиprepareStatement()
. - Выполнение SQL-запроса:
- Для
SELECT
:executeQuery()
, возвращаетResultSet
. - Для
INSERT
,UPDATE
,DELETE
:executeUpdate()
, возвращаетint
(количество изменённых строк).
- Для
- Обработка результата (
ResultSet
): Если былSELECT
, обойтиResultSet
в циклеwhile(rs.next())
и извлечь данные. - Закрытие ресурсов: Критически важный шаг! Ресурсы (
ResultSet
,Statement
,Connection
) должны быть закрыты в порядке, обратном их созданию. Лучше всего это делать в блокеfinally
или с помощью конструкцииtry-with-resources
(предпочтительно с Java 7+).
- Загрузка драйвера БД (шаг устарел): Раньше требовался вызов
Пример с try-with-resources
:
String url = "jdbc:...";
String user = "user";
String password = "password";
String sql = "SELECT * FROM users WHERE city = ?";
try (Connection conn = DriverManager.getConnection(url, user, password);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, "Moscow");
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
System.out.println(rs.getString("name") + ", " + rs.getInt("age"));
}
}
} catch (SQLException e) {
e.printStackTrace();
}
Роль DriverManager
и Connection
DriverManager
: Класс-менеджер, который управляет набором зарегистрированных JDBC-драйверов. Его основная задача — выбрать подходящий драйвер для указанного URL и установить с его помощью соединение с БД.Connection
: Интерфейс, представляющий сессию (соединение) с конкретной базой данных. Все SQL-запросы выполняются в контексте этого соединения. ТакжеConnection
отвечает за управление транзакциями (setAutoCommit
,commit
,rollback
).
5.2. Statement
, PreparedStatement
, ResultSet
, RowSet
Statement
vs PreparedStatement
: в чём разница и что лучше?
- Ответ: Это один из самых важных вопросов по JDBC.
PreparedStatement
почти всегда лучшеStatement
.
Характеристика | Statement
|
PreparedStatement
|
---|---|---|
Принцип работы | Каждый раз отправляет и компилирует SQL-запрос на сервере БД. | Запрос предварительно компилируется (pre-compiled) на сервере БД один раз. Затем можно многократно выполнять его с разными параметрами. |
Производительность | Ниже, особенно при многократном выполнении однотипных запросов. | Выше, так как СУБД не тратит время на повторный парсинг и компиляцию SQL-кода. |
Безопасность | Уязвим к SQL-инъекциям. Параметры вставляются в строку запроса конкатенацией, что позволяет злоумышленнику внедрить вредоносный SQL-код. | Защищает от SQL-инъекций. Параметры передаются отдельно от SQL-кода через setXXX() методы. Драйвер сам заботится об их корректном экранировании.
|
Использование | connection.createStatement()
|
connection.prepareStatement("SELECT * FROM users WHERE id = ?")
|
Что такое SQL-инъекция?
Ответ: Это атака, при которой злоумышленник внедряет свой SQL-код в параметры запроса.
Пример с
Statement
:String userId = "105 OR 1=1"; // Ввод от злоумышленника String sql = "SELECT * FROM users WHERE id = " + userId; // Получится "SELECT * FROM users WHERE id = 105 OR 1=1" // Запрос вернёт ВСЕХ пользователей, так как 1=1 всегда истинно.
Как
PreparedStatement
защищает:String userId = "105 OR 1=1"; PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?"); pstmt.setString(1, userId); // Драйвер обработает всю строку "105 OR 1=1" как единое значение для поля id, а не как часть SQL.
Что такое ResultSet
и RowSet
?
ResultSet
:- Представляет результат выполнения
SELECT
запроса в виде таблицы. - По сути, это курсор, который указывает на текущую строку данных. Изначально он находится перед первой строкой. Метод
next()
сдвигает курсор на следующую строку и возвращаетfalse
, когда строки заканчиваются. - По умолчанию
ResultSet
“привязан” кConnection
. Если вы закроете соединение,ResultSet
станет недействительным. - Он однонаправленный (только
next()
) и не обновляемый.
- Представляет результат выполнения
RowSet
:- Это интерфейс, который расширяет
ResultSet
. - Ключевое отличие —
RowSet
может быть “отсоединённым” (disconnected). Он может загрузить все данные изResultSet
в память, после чего соединение с БД можно закрыть.RowSet
продолжит работать. - Является полноценным JavaBean: его можно сериализовать, передавать по сети.
- Может быть прокручиваемым (scrolling) и обновляемым.
- Популярные реализации:
CachedRowSet
(отсоединённый),JdbcRowSet
(присоединённый).
- Это интерфейс, который расширяет
Раздел 6. Шаблоны проектирования (Design Patterns)
Здесь нужно знать несколько ключевых шаблонов, особенно те, которые так или иначе связаны с темами выше.
- Singleton (Одиночка):
- Цель: Гарантировать, что у класса есть только один экземпляр, и предоставить глобальную точку доступа к нему.
- Связь с темами: Может использоваться для
DriverManager
(хотя он и так статический), для пула потоков, если он должен быть один на всё приложение, или для фабрики соединений. - Вопрос на засыпку: Как сделать Singleton потокобезопасным? (Ответ:
synchronized
на методеgetInstance()
, double-checked locking сvolatile
переменной, черезEnum
, через статический инициализатор).
- Factory Method (Фабричный метод):
- Цель: Определяет интерфейс для создания объекта, но позволяет подклассам решать, какой класс инстанцировать.
- Связь с темами: Класс
Executors
использует фабричные методы (newFixedThreadPool
и т.д.) для создания различных реализацийExecutorService
.
- Producer-Consumer (Производитель-Потребитель):
- Цель: Разделить работу между потоками, которые создают данные/задачи (производители), и потоками, которые их обрабатывают (потребители).
- Связь с темами: Это фундаментальный паттерн многопоточности. Идеально реализуется с помощью
BlockingQueue
. Производители вызываютqueue.put()
, потребители —queue.take()
.
- Template Method (Шаблонный метод):
- Цель: Определяет “скелет” алгоритма в методе, оставляя реализацию некоторых шагов подклассам.
- Связь с темами: Весь процесс работы с JDBC — это классический шаблонный метод:
- Получить соединение (может меняться).
- Создать запрос (может меняться).
- Выполнить запрос.
- Обработать результат (может меняться).
- Закрыть соединение. Фреймворки вроде Spring JDBC используют этот паттерн, чтобы скрыть от вас рутину (шаги 1, 5) и позволить сосредоточиться на главном (шаги 2, 4).
- Decorator (Декоратор):
- Цель: Динамически добавлять объекту новую функциональность, оборачивая его в другой объект.
- Связь с темами: Можно создать декоратор для
Connection
, который будет логировать все вызовы SQL-запросов, или дляResultSet
, который будет кэшировать данные.
- DAO (Data Access Object):
- Цель: Абстрагировать и инкапсулировать все детали доступа к источнику данных. Приложение работает с простым интерфейсом DAO, а вся JDBC-логика (создание
Connection
,PreparedStatement
и т.д.) скрыта внутри его реализации. - Связь с темами: Это ключевой паттерн для работы с БД. Он позволяет отделить бизнес-логику от логики персистентности (сохранения данных).
- Цель: Абстрагировать и инкапсулировать все детали доступа к источнику данных. Приложение работает с простым интерфейсом DAO, а вся JDBC-логика (создание