Топ-50 Java Core вопросов и ответов на собеседовании. Часть 1 Топ-50 Java Core вопросов и ответов на собеседовании. Часть 2
Multithreading
37. Как создать в Java новый тред (поток)?
Так or иначе создание происходит через использование класса Thread. Но здесь могут быть варианты…- Наследуемся от
java.lang.Thread
- Имплементируем интерфейс
java.lang.Runnable
, an object которого принимает к себе к конструкторThread
класс
Наследуемся от Thread класса
Whatбы это заработало, в нашем классе наследуемся отjava.lang.Thread
. В нем есть метом run()
, он How раз нам и нужен. Вся жизнь и логика нового потока будет в этом методе. Это своего рода main
метод для нового потока. После этого останется только создать an object нашего класса и выполнить метод start()
, который создаст новый поток и запустит записанную в нем логику. Смотрим:
/**
* Пример того, How создавать треды путем наследования {@link Thread} класса.
*/
class ThreadInheritance extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
ThreadInheritance threadInheritance1 = new ThreadInheritance();
ThreadInheritance threadInheritance2 = new ThreadInheritance();
ThreadInheritance threadInheritance3 = new ThreadInheritance();
threadInheritance1.start();
threadInheritance2.start();
threadInheritance3.start();
}
}
Вывод в консоль будет такой:
Thread-1
Thread-0
Thread-2
То есть даже здесь мы видим, что выполняются потоки не по очереди, а How JVM рассудила)
Реализуем интерфейс Runnable
Если вы противник наследований и/or уже наследуете Howой-то из других классов, можно воспользоваться интерфейсомjava.lang.Runnable
. Здесь мы в нашем классе реализуем этот интерфейс и имплементируем метод run()
, How это было и в том примере. Только нужно будет еще создать an objectы Thread
. Казалось бы, больше строк и это хуже. Но мы то знаем How пагубно наследование и что его лучше избегать всеми способами ;) Смотрим:
/**
* Пример того, How создавать треды из интерфейса {@link Runnable}.
* Здесь проще простого - реализуем этот интерфейс и потом передаем в конструктор
* экземпляр реализуемого an object.
*/
class ThreadInheritance implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
ThreadInheritance runnable1 = new ThreadInheritance();
ThreadInheritance runnable2 = new ThreadInheritance();
ThreadInheritance runnable3 = new ThreadInheritance();
Thread threadRunnable1 = new Thread(runnable1);
Thread threadRunnable2 = new Thread(runnable2);
Thread threadRunnable3 = new Thread(runnable3);
threadRunnable1.start();
threadRunnable2.start();
threadRunnable3.start();
}
}
И результат выполнения:
Thread-0
Thread-1
Thread-2
38. Какая разница между процессом и потоком?
Существуют следующие различия между процессом и потоком:- Программа в исполнении называется процессом, тогда How Поток является подмножеством процесса.
- Процессы независимы, тогда How потоки являются подмножеством процесса.
- Процессы имеют различное addressное пространство в памяти, в то время How потоки содержат общее addressное пространство.
- Переключение контекста происходит быстрее между потоками по сравнению с процессами.
- Межпроцессное взаимодействие медленнее и дороже, чем межпотоковое взаимодействие.
- Любые изменения в родительском процессе не влияют на дочерний процесс, тогда How изменения в родительском потоке могут влиять на дочерний поток.
39. Какие преимущества есть у многопоточности?
- Многопоточность позволяет приложению / программе всегда реагировать на ввод, даже если она уже выполняется с некоторыми фоновыми задачами;
- Многопоточность позволяет быстрее выполнять задачи, поскольку потоки выполняются независимо;
- Многопоточность обеспечивает лучшее использование кэш-памяти, поскольку потоки разделяют общие ресурсы памяти;
- Многопоточность уменьшает количество требуемого serverа, поскольку один server может одновременно выполнять несколько потоков.
40. Каковы состояния в жизненном цикле потока?
- New: В этом состоянии an object класса
Thread
создается с использованием оператора new, но поток не существует. Поток не запускается, пока мы не вызовем методstart()
. - Runnable: В этом состоянии поток готов к запуску после вызова метода
start(). Однако он еще не выбран планировщиком потока. - Running: В этом состоянии планировщик потока выбирает поток из состояния готовности, и тот работает.
- Waiting/Blocked: в этом состоянии поток не работает, но все еще жив or ожидает завершения другого потока.
- Dead/Terminated: при выходе из метода
run()
поток находится в завершенном or мертвом состоянии.
41. Можно ли запустить тред дважды?
Нет, мы не можем перезапустить поток, так How после запуска и выполнения потока он переходит в состояние Dead. Поэтому, если мы попытаемся запустить поток дважды, он выдаст исключение runtimeException "java.lang.IllegalThreadStateException". Смотрим:
class DoubleStartThreadExample extends Thread {
/**
* Имитируем работу треда
*/
public void run() {
// что-то происходит. Для нас не существенно на этом этапе
}
/**
* Запускаем тред дважды
*/
public static void main(String[] args) {
DoubleStartThreadExample doubleStartThreadExample = new DoubleStartThreadExample();
doubleStartThreadExample.start();
doubleStartThreadExample.start();
}
}
Как только работа дойдет до выполнения второго старта одного и того же треда - тогда и будет исключение. Попробуйте сами ;) лучше один раз увидеть, чем сто раз услышать.
42. What если вызвать напрямую метод run(), не вызывая метод start()?
Да, вызвать методrun()
конечно можно, но это ниHow не создаст новый поток и не выполнит его How отдельный. В этом случае, это простой an object, который вызывает простой метод. Если мы говорим о методе start()
, то там другое дело. Запуская этот метод, runtime
запускает новый потом и он уже, в свою очередь, дергает наш метод ;) Не верите — вот, попробуйте:
class ThreadCallRunExample extends Thread {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.print(i);
}
}
public static void main(String args[]) {
ThreadCallRunExample runExample1 = new ThreadCallRunExample();
ThreadCallRunExample runExample2 = new ThreadCallRunExample();
// просто будут вызваны в потоке main два метода, один за другим.
runExample1.run();
runExample2.run();
}
}
И вывод в консоль будет такой:
0123401234
Видно, что ниHowой нити не было создано. Все сработало How обычный класс. Вначале отработал метод первого класса, затем второй.
43. What такое daemon тред?
Daemon thread (далее — демон-тред) — это тред, который выполняет задачи в фоне по отношению к другому потоку. То есть, его работа заключается в том, чтоб выполнять задачи вспомогательные, которые нужно делать только в привязке другому (основному) потоку. Есть много потоков демонов, работающих автоматически, например Garbage Collector, finalizer и т. д.Почему Java закрывает демон-поток?
Единственная цель потока демона состоит в том, что он предоставляет сервисы потоку пользователя для фоновой задачи поддержки. Поэтому если основной поток завершился, то runtime закрывает автоматически и все его демон-потоки.Методы для работы в Thread классе
Классjava.lang.Thread
предоставляет два метода для работы с демоном-потоком:
public void setDaemon(boolean status)
— указывает, что это будет демон-поток. По умолчанию стоитfalse
, что значит, что будут создаваться не демон-потоки, если не указать это отдельно.public boolean isDaemon()
— по сути это геттер для переменнойdaemon
, который мы устанавливаем предыдущим методом.
class DaemonThreadExample extends Thread {
public void run() {
// Проверяет, демон ли этот поток or нет
if (Thread.currentThread().isDaemon()) {
System.out.println("daemon thread");
} else {
System.out.println("user thread");
}
}
public static void main(String[] args) {
DaemonThreadExample thread1 = new DaemonThreadExample();
DaemonThreadExample thread2 = new DaemonThreadExample();
DaemonThreadExample thread3 = new DaemonThreadExample();
// теперь thread1 - поток-демон.
thread1.setDaemon(true);
System.out.println("демон?.. " + thread1.isDaemon());
System.out.println("демон?.. " + thread2.isDaemon());
System.out.println("демон?.. " + thread3.isDaemon());
thread1.start();
thread2.start();
thread3.start();
}
}
Вывод в консоль:
демон?.. true
демон?.. false
демон?.. false
daemon thread
user thread
user thread
Из вывода мы видим, что внутри самого потока при помощи статического currentThread()
метода можно узнать Howой это поток с одной стороны, с другой стороны, если у нас есть link на an object этого потока, мы можем узнать и непосредственно у него. Это дает ту необходимую гибкость в настройке.
44. Можно ли сделать поток демоном уже после его создания?
Нет. Если вы сделаете это, он выдаст исключениеIllegalThreadStateException
. Следовательно, мы можем создать поток демона только до его запуска. Пример:
class SetDaemonAfterStartExample extends Thread {
public void run() {
System.out.println("Working...");
}
public static void main(String[] args) {
SetDaemonAfterStartExample afterStartExample = new SetDaemonAfterStartExample();
afterStartExample.start();
// здесь будет выброшено исключение
afterStartExample.setDaemon(true);
}
}
Вывод в консоль:
Working...
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.setDaemon(Thread.java:1359)
at SetDaemonAfterStartExample.main(SetDaemonAfterStartExample.java:14)
45. What такое shutdownhook?
Shutdownhook — это поток, который неявно вызывается до завершения работы JVM(виртуальная машина Java). Таким образом, мы можем использовать его для очистки ресурса or сохранения состояния, когда виртуальная машина Java выключается нормально or внезапно. Мы можем добавитьshutdown hook
, используя следующий метод:
Runtime.getRuntime().addShutdownHook(new ShutdownHookThreadExample());
Как показано в примере:
/**
* Программа, которая показывает How запустить shutdown hook тред,
* который выполнится аккурат до окончания работы JVM
*/
class ShutdownHookThreadExample extends Thread {
public void run() {
System.out.println("shutdown hook задачу выполнил");
}
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new ShutdownHookThreadExample());
System.out.println("Теперь программа засыпает, нажмите ctrl+c чтоб завершить ее.");
try {
Thread.sleep(60000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Вывод в консоль:
Теперь программа засыпает, нажмите ctrl+c чтоб завершить ее.
shutdown hook задачу выполнил
46. What такое синхронизация (synchronization)?
Синхронизация (Synchronization) в Java — это возможность контролировать доступ нескольких потоков к любому общему ресурсу. Когда несколько потоков пытаются выполнить одну и ту же задачу, существует вероятность ошибочного результата, поэтому для устранения этой проблемы Java использует синхронизацию, благодаря которой будет только один тред сможет работать в один момент. Синхронизация может быть достигнута тремя способами:- Синхронизируя метод
- Синхронизируя определенный блок
- Статической синхронизацией
Синхронизация метода
Синхронизированный метод используется для блокировки an object для любого общего ресурса. Когда поток вызывает синхронизированный метод, он автоматически получает блокировку для этого an object и снимает ее, когда поток завершает свою задачу. Whatб заработало, нужно добавить ключевое слово synchronized. На примере увидим, How это работает:
/**
* Пример, где мы синхронизируем метод. То есть добавляем ему слово synchronized.
* Есть два писателя, которые хотят использовать один принтер. Они подготовor свои поэмы
* И конечно же не хотят, чтоб их поэмы перемешались, а хотят, чтоб работа была сделана по * * * очереди для каждого из них
*/
class Printer {
synchronized void print(List<String> wordsToPrint) {
wordsToPrint.forEach(System.out::print);
System.out.println();
}
public static void main(String args[]) {
// один an object для двух тредов
Printer printer = new Printer();
// создаем два треда
Writer1 writer1 = new Writer1(printer);
Writer2 writer2 = new Writer2(printer);
// запускаем их
writer1.start();
writer2.start();
}
}
/**
* Писатель номер 1, который пишет свою поэму.
*/
class Writer1 extends Thread {
Printer printer;
Writer1(Printer printer) {
this.printer = printer;
}
public void run() {
List<string> poem = Arrays.asList("Я ", this.getName(), " Пишу", " Письмо");
printer.print(poem);
}
}
/**
* Писатель номер 2, который пишет свою поэму.
*/
class Writer2 extends Thread {
Printer printer;
Writer2(Printer printer) {
this.printer = printer;
}
public void run() {
List<String> poem = Arrays.asList("Не Я ", this.getName(), " Не пишу", " Не Письмо");
printer.print(poem);
}
}
И вывод в консоль:
Я Thread-0 Пишу Письмо
Не Я Thread-1 Не пишу Не Письмо
Блок синхронизации
Синхронизированный блок может быть использован для выполнения синхронизации на любом конкретном ресурсе метода. Допустим, что в большом методе(да да, такие писать нельзя, но иногда бывает) нужно синхронизировать только небольшую часть, по Howим-то причинам. Если вы поместите все codeы метода в синхронизированный блок, он будет работать так же, How синхронизированный метод. Синтаксис выглядит так:
synchronized (“an object для блокировки”) {
// сам code, который нужно защитить
}
Для того, чтоб не повторять пример предыдущий, создадим треды через анонимные классы - то есть сразу реализуя Runnable интерфейс.
/**
* Вот How добавляется блок синхронизации.
* Внутри нужно указать у кого будет взят мьютекс для блокировки.
*/
class Printer {
void print(List<String> wordsToPrint) {
synchronized (this) {
wordsToPrint.forEach(System.out::print);
}
System.out.println();
}
public static void main(String args[]) {
// один an object для двух тредов
Printer printer = new Printer();
// создаем два треда
Thread writer1 = new Thread(new Runnable() {
@Override
public void run() {
List<String> poem = Arrays.asList("Я ", "Writer1", " Пишу", " Письмо");
printer.print(poem);
}
});
Thread writer2 = new Thread(new Runnable() {
@Override
public void run() {
List<String> poem = Arrays.asList("Не Я ", "Writer2", " Не пишу", " Не Письмо");
printer.print(poem);
}
});
// запускаем их
writer1.start();
writer2.start();
}
}
}
и вывод в консоль
Я Writer1 Пишу Письмо
Не Я Writer2 Не пишу Не Письмо
Статическая синхронизация
Если сделать статический метод синхронизированным, то блокировка будет на классе, а не на an objectе. В этом примере мы применяем ключевое слово synchronized к статическому методу для выполнения статической синхронизации:
/**
* Вот How добавляется блок синхронизации.
* Внутри нужно указать у кого будет взят мьютекс для блокировки.
*/
class Printer {
static synchronized void print(List<String> wordsToPrint) {
wordsToPrint.forEach(System.out::print);
System.out.println();
}
public static void main(String args[]) {
// создаем два треда
Thread writer1 = new Thread(new Runnable() {
@Override
public void run() {
List<String> poem = Arrays.asList("Я ", "Writer1", " Пишу", " Письмо");
Printer.print(poem);
}
});
Thread writer2 = new Thread(new Runnable() {
@Override
public void run() {
List<String> poem = Arrays.asList("Не Я ", "Writer2", " Не пишу", " Не Письмо");
Printer.print(poem);
}
});
// запускаем их
writer1.start();
writer2.start();
}
}
и вывод в консоль:
Не Я Writer2 Не пишу Не Письмо
Я Writer1 Пишу Письмо
47. What такое volatile переменная?
Ключевое словоvolatile
используется в многопоточном программировании для обеспечения безопасности потока, поскольку модификация одной изменяемой переменной видна всем другим потокам, поэтому одна переменная может использоваться одним потоком за раз. При помощи ключевого слова volatile
можно гарантировать, что переменная будет потокобезопасна и будет храниться в общей памяти, и потоки не будут ее брать себе в свой кеш. Как это выглядит?
private volatile AtomicInteger count;
Просто добавляем к переменной volatile
. Но это не говорит о полной потокобезопасности… Ведь операции могут быть не атомарны над переменной. Но можно использовать Atomic
классы, которые делают операцию атомарно, то есть за одно выполнение процессором. Таких классов много можно найти в пакете java.util.concurrent.atomic
.
48. What такое deadlock
Deadlock в Java является частью многопоточности. Взаимная блокировка может возникнуть в ситуации, когда поток ожидает блокировки an object, полученной другим потоком, а второй поток ожидает блокировки an object, полученной первым потоком. Таким образом эти два потока ждут друг друга и не будут дальше выполнять свой code. Рассмотрим Пример, в котором есть класс имплементирующий Runnable. Принимает в конструкторе он два ресурса. Внутри метода run() он по-очереди берет блокировку для них, так вот если создать два an object этого класса, а ресурсы передать в разном порядке, то легко можно нарваться на блокировку:
class DeadLock {
public static void main(String[] args) {
final Integer r1 = 10;
final Integer r2 = 15;
DeadlockThread threadR1R2 = new DeadlockThread(r1, r2);
DeadlockThread threadR2R1 = new DeadlockThread(r2, r1);
new Thread(threadR1R2).start();
new Thread(threadR2R1).start();
}
}
/**
* Класс, который принимает два ресурса.
*/
class DeadlockThread implements Runnable {
private final Integer r1;
private final Integer r2;
public DeadlockThread(Integer r1, Integer r2) {
this.r1 = r1;
this.r2 = r2;
}
@Override
public void run() {
synchronized (r1) {
System.out.println(Thread.currentThread().getName() + " захватил ресурс: " + r1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (r2) {
System.out.println(Thread.currentThread().getName() + " захватил ресурс: " + r2);
}
}
}
}
Вывод в консоль:
Первый тред захватил первый ресурс
Второй тред захватывает второй ресурс
49. Как избежать deadlock?
Исходя из того, что мы знаем How дедлок возникает, то можно сделать некоторые выводы…- Как показано в примере выше, дедлок был из-за того, что была вложенность блокировок. То есть внутри одной блокировки находится еще одна or более. Избежать это можно следующим образом - instead of вложенности нужно добавить новую абстракцию поверх и дать блокировку на более высокий уровень, а вложенные блокировки убрать.
- Чем больше блокировок, тем больше шансов, что будет дедлок. Поэтому каждый раз добавляя блокировку нужно думать, а точно ли она нужна и можно ли избежать добавление новой.
- Использования
Thread.join()
. Дедлок можно сделать также при ожидании одного треда другим. Whatбы избежать этой проблемы, можно подумать над тем, чтобы выставлять ограниченное время наjoin()
метод. - Если у нас один поток - дедлока не будет ;)
50. What такое состояние гонки?
Если в реальных гонках выступают машины, то в гонках терминологии многопоточности в гонках выступают треды. Но почему? Есть же два треда, которые работают и которые могут иметь доступ к одному и тому же an objectу. И они могут попытаться обновить состояние в одно и тоже время. Пока все ясно, да? Так работа тредов происходит or реально параллельно(если есть больше одного ядра в процессоре) or условно параллельно, когда процессор выделяет по небольшому промежутку времени. И управлять этими процессами мы не можем, поэтому мы не можем гарантировать, что когда один тред прочитает данные из an object, он успеет их изменить ДО того, How это сделает Howой-то другой тред. Такие проблемы бывают, когда проходит такая комбинация “проверь-и-действуй”. What это значит? Например у нас естьif
выражение, в теле которого изменяется само condition, то есть:
int z = 0;
// проверь
if (z < 5) {
//действуй
z = z + 5;
}
Так вот может быть ситуация, когда два треда одновременно зайдут в этот блок codeа в момент, когда z еще равно нулю и в двоем изменят это meaning. И в итоге мы получим не ожидаемое meaning 5, а уже 10. Как это избежать? Нужно поставить блокировку до начала выполнения и после. То есть, чтобы первый тред зашел в блок if
, выполнил все действия, изменил z
и уже потом дал возможность сделать это следующему треду. А вот уже следующий тред не зайдет в блок if
, так How z
уже будет равно 5:
// получить блокировку для z
if (z < 5) {
z = z + 5;
}
// выпустить из блокировки z
===================================================
Вместо вывода
Want сказать спасибо всем тем, кто дочитал до конца. Это был длинный путь и вы его осorли! Может быть понятно не всё. Это нормально. Я How только начинал изучать джаву, мне ниHow в голове не умещалось что такое статическая переменная. Но ничего, переспал с этой мыслью, почитал еще несколько источников и таки понял. Подготовка к собеседованию - это скорее академический вопрос, чем практический. Поэтому перед каждым собеседованием нужно повторять и освежать в памяти то, что может не так уж и часто используешь.И How всегда, полезные ссылки:
- Exceptions in Java
- Default methods in java
- Подробный разбор класса ArrayList
- Подписывайтесь на мой GitHub аккаунт. Я туда выкладываю все то, чем занимаюсь. Все, что учу. Например, недавно разбирался с Drools RuleEngine.
- Checked Unchecked exceptions
- try-with-resources
- final keyword
- Пока готовил материал, мне очень понравился сайт https://www.geeksforgeeks.org. Вот там реально много крутой информации.
GO TO FULL VERSION