Сложные вопросы на собеседовании для тех, кто 7 лет работал с Java. Часть 2

Продолжаем разбирать вопросы, приводя подробные объяснения и примеры.

Первая часть статьи.

6. В чем разница между обычной и реентрабельной блокировками считывания/записи в Java? Какая из них гибче?

Блокировка считывания/записи

При блокировке считывания/записи считывание общего ресурса выполняется единовременно несколькими потоками, а запись в него  —  лишь одним. При выполнении записи в ресурс, прежде чем захватить блокировку, поток дожидается завершения считывания всеми «читателями».

Блокировка считывания/записи нереентерабельна. То есть блокировка для записи захватывается потоком только после освобождения удерживаемой им блокировки для считывания. Аналогично блокировка для считывания захватывается потоком только после освобождения удерживаемой им блокировки для записи.

Реентрабельная блокировка считывания/записи

А эта реализация блокировки считывания/записи гибче: блокировка для считывания захватывается несколькими потоками одновременно, а блокировка для записи захватывается потоком без освобождения удерживаемой им блокировки для считывания. Так поток переходит от блокировки для считывания к блокировке для записи.

Реентрабельность этой блокировки и выражается в том, что блокировка для считывания или записи захватывается потоком повторно без освобождения удерживаемой им блокировки.

Реентрабельная блокировка считывания/записи в целом гибче обычной, но сложнее и при неправильном применении чревата взаимоблокировками. Она обычно рекомендуется для детализированного контроля над блокировкой, а обычная  —  когда предпочтение отдается простоте.

7. На компьютере имеется два диска с вложенными папками, и в некоторых папках содержится файл. Напишите на Java программу поиска этого файла. Как минимизировать время поиска файла?

Для поиска файла воспользуемся многопоточностью, реализуем это кодом на Java:

import java.io.File;
class FileSearchThread extends Thread {

// Название и каталог файла для каждого потока
private final String fileName;
private final File directory;
// Конструктор
public FileSearchThread(String fileName, File directory) {
this.fileName = fileName;
this.directory = directory;
}

// Запускаем метод, которым вызывается метод для поиска файла.
@Override
public void run() {
boolean result = searchFile(fileName, directory);
if(result) {
System.out.println(“File Found. Location — “ + directory.toString());
}
}
private boolean searchFile(String fileName, File directory) {
// Поиск в каталоге
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
// Рекурсивный поиск во вложенных каталогах
searchFile(fileName, file);
} else if (file.getName().equalsIgnoreCase(fileName)) {
System.out.println(“File found at: “ + file.getAbsolutePath());
return true;
}
}
}
return false;
}
}
public class FileSearch {
public static void main(String[] args) {
String fileNameToSearch = “xyz.txt”;
// Для каждого диска и папки создаем отдельный поток
for (char drive = ‘C’; drive <= ‘D’; drive++) {
String rootPath = drive + “:\\”;
File[] directories = new File(rootPath).listFiles(File::isDirectory);
if (directories != null) {
for (File directory : directories) {
new FileSearchThread(fileNameToSearch, directory).start();
}
}
}
}
}

В этой программе сначала определяем класс FileSearchThread, которым расширяется поток Thread, а в его конструкторе принимается параметр fileName и directory. Чтобы выполнить рекурсивный поиск файла в заданном каталоге и его вложенных каталогах, в методе run вызываем метод searchFile. Если файл найден, выводим его абсолютный путь и возвращаемся из метода.

В классе FileSearch, перебирая каталоги дисков C и D, для каждого диска и папки создаем отдельный поток. Передаем fileNameToSearch и текущий каталог конструктору FileSearchThread и запускаем поток.

Создав для каждого диска и папки отдельный поток, выполняем параллельный поиск файла, минимизируя время поиска.

8. В чем разница между драйверами JDBC «Type 1» и «Type 4»? Какой из них предпочтительнее?

Драйверы JDBC используются для соединения Java-приложений с базами данных. Типов таких драйверов несколько, самые распространенные  —  Type 1 и Type 4.

Вот разница между ними.

Драйвер «Type 1»

Это драйвер-мост JDBC-ODBC между интерфейсом соединения Java-приложений с БД и интерфейсом открытой связи с БД. Для соединения с БД используется драйвер ODBC, устанавливаемый на клиентском компьютере.

Драйвер «Type 1» прост в использовании с любой БД, в которой имеется доступный драйвер ODBC. Однако добавление дополнительного уровня взаимодействия между Java-приложением и БД сказывается на его производительности.

Драйвер «Type 4»

Это так называемый «чистый Java-драйвер», то есть полностью на Java, который взаимодействует с БД по собственному протоколу напрямую. Ему не требуется никаких внешних библиотек или драйверов, это самый распространенный тип драйвера в Java-приложениях.

Производительность Type 4 выше, чем у Type 1, ведь дополнительные уровни взаимодействия между Java-приложением и БД не добавляются. Выше у него и безопасность с платформонезависимостью: от внешних библиотек он не зависит.

За счет всего этого Type 4 для Java-приложений оказывается предпочтительнее драйвера Type 1.

9. Как в JDBC реализуется оптимистическая блокировка?

Оптимистическая блокировка  —  это техника предотвращения конфликтов данных в многопользовательской среде. В JDBC она реализуется так.

  • Когда пользователь начинает редактировать запись, номер текущей версии записи извлекается из БД и сохраняется в переменной.
  • Когда редактирование записи завершено, запись в БД обновляется свежими значениями и номер версии увеличивается.
  • Если обновление успешно, транзакция фиксируется; если нет  —  откатывается.
  • Прежде чем обновлять запись в БД, номер текущей версии сравнивается с номером версии в переменной. Если значения совпадают, запись обновляется; если нет  —  значит, другой пользователь обновил запись после извлечения ее первым. В этом случае либо прерывается транзакция, либо пользователю предлагается извлечь последнюю версию записи, либо внесенные обоими пользователями изменения объединяются.

Вот пример фрагмента кода с реализацией оптимистической блокировки в JDBC посредством PreparedStatement:

try {
// Извлекаем номер текущей версии записи
PreparedStatement selectStmt = conn.prepareStatement(“SELECT version FROM table WHERE id = ?”);
selectStmt.setInt(1, id);
ResultSet rs = selectStmt.executeQuery();
int currentVersion = 0;
if (rs.next()) {
currentVersion = rs.getInt(1);
}
// Обновляем запись и увеличиваем номер версии
PreparedStatement updateStmt = conn.prepareStatement(“UPDATE table SET column1 = ?, column2 = ?, version = ? WHERE id = ? AND version = ?”);
updateStmt.setString(1, newValue1);
updateStmt.setString(2, newValue2);
updateStmt.setInt(3, currentVersion + 1);
updateStmt.setInt(4, id);
updateStmt.setInt(5, currentVersion);
int rowsUpdated = updateStmt.executeUpdate();
// Проверяем, прошло ли обновление
if (rowsUpdated == 1) {
conn.commit();
} else {
conn.rollback();
}
} catch (SQLException e) {
conn.rollback();
e.printStackTrace();
}

В этом примере сначала оператором SELECT мы извлекаем номер текущей версии записи.

Затем обновляем запись, используя PreparedStatement, с включением номера текущей версии в предложении WHERE: запись обновляется, только если не изменена другим пользователем.

Проверяем количество обновленных оператором UPDATE строк, транзакция фиксируется или откатывается.

10. Каково назначение класса «Exchanger» в Java, как и в каком сценарии он используется?

Класс Exchanger  —  средство синхронизации для обмена объектами между двумя потоками в условиях блокировки. Это простой способ обмена данными между двумя потоками в сценарии «отправитель/получатель», где одним потоком данные отправляются, а другим  —  получаются.

Класс Exchanger  —  часть пакета java.util.concurrent с единственным методом exchange(). Этот метод блокируется, пока оба потока его не вызовут для обмена объектами.

Вот пример использования класса Exchanger:

import java.util.concurrent.Exchanger;
public class ExchangerExample {
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<>();
Thread producerThread = new Thread(() -> {
try {
String data = “Hello from producer thread”;
System.out.println(“Producer thread is sending: “ + data);
String receivedData = exchanger.exchange(data);
System.out.println(“Producer thread received: “ + receivedData);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread consumerThread = new Thread(() -> {
try {
String data = “Hello from consumer thread”;
System.out.println(“Consumer thread is sending: “ + data);
String receivedData = exchanger.exchange(data);
System.out.println(“Consumer thread received: “ + receivedData);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producerThread.start();
consumerThread.start();
}
}

В этом примере создаются объект Exchanger и два потока: отправитель и получатель. Потоком-отправителем потоку-получателю с помощью метода exchange() отправляется строковое сообщение. То же происходит и в обратном направлении. Оба потока блокируются, пока не вызывается exchange() для обмена их объектами, и каждым потоком выводится полученное сообщение.

Класс Exchanger применяется в сценариях взаимодействия и обмена данными двух потоков. Им упрощают координацию потоков, избегают возникновения «состояния гонок» и других проблем синхронизации.

Заключение

Мы рассмотрели некоторые каверзные вопросы для собеседования по Java.

Надеемся, вы увеличили багаж знаний и навыков, повысив свои шансы на успех в собеседовании.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи AlishaS: Tricky Java interview questions for 7 years of Experience

Предыдущая статьяУправляем зависимостями: возможности каталога версий и convention-плагина
Следующая статьяПервые шаги в JavaScript: создание калькулятора