Пользователь Roman Beskrovnyi
Roman Beskrovnyi
35 уровень

Топ-50 Java Core вопросов и ответов на собеседовании. Часть 3

Статья из группы Java Developer
Топ-50 Java Core вопросов и ответов на собеседовании. Часть 1 Топ-50 Java Core вопросов и ответов на собеседовании. Часть 2

Multithreading

37. Как создать в Java новый тред (поток)?

Так или иначе создание происходит через использование класса Thread. Но здесь могут быть варианты…
  1. Наследуемся от java.lang.Thread
  2. Имплементируем интерфейс java.lang.Runnable, объект которого принимает к себе к конструктор Thread класс
Поговорим о каждом из них.

Наследуемся от Thread класса

Чтобы это заработало, в нашем классе наследуемся от java.lang.Thread. В нем есть метом run(), он как раз нам и нужен. Вся жизнь и логика нового потока будет в этом методе. Это своего рода main метод для нового потока. После этого останется только создать объект нашего класса и выполнить метод start(), который создаст новый поток и запустит записанную в нем логику. Смотрим:

/**
* Пример того, как создавать треды путем наследования {@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
То есть даже здесь мы видим, что выполняются потоки не по очереди, а как JVM рассудила)

Реализуем интерфейс Runnable

Если вы противник наследований и/или уже наследуете какой-то из других классов, можно воспользоваться интерфейсом java.lang.Runnable. Здесь мы в нашем классе реализуем этот интерфейс и имплементируем метод run(), как это было и в том примере. Только нужно будет еще создать объекты Thread. Казалось бы, больше строк и это хуже. Но мы то знаем как пагубно наследование и что его лучше избегать всеми способами ;) Смотрим:

/**
* Пример того, как создавать треды из интерфейса {@link Runnable}.
* Здесь проще простого - реализуем этот интерфейс и потом передаем в конструктор
* экземпляр реализуемого объекта.
*/
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. Какая разница между процессом и потоком?

Топ-50 Java Core вопросов и ответов на собеседовании. Часть 3 - 1Существуют следующие различия между процессом и потоком:
  1. Программа в исполнении называется процессом, тогда как Поток является подмножеством процесса.
  2. Процессы независимы, тогда как потоки являются подмножеством процесса.
  3. Процессы имеют различное адресное пространство в памяти, в то время как потоки содержат общее адресное пространство.
  4. Переключение контекста происходит быстрее между потоками по сравнению с процессами.
  5. Межпроцессное взаимодействие медленнее и дороже, чем межпотоковое взаимодействие.
  6. Любые изменения в родительском процессе не влияют на дочерний процесс, тогда как изменения в родительском потоке могут влиять на дочерний поток.

39. Какие преимущества есть у многопоточности?

Топ-50 Java Core вопросов и ответов на собеседовании. Часть 3 - 2
  1. Многопоточность позволяет приложению / программе всегда реагировать на ввод, даже если она уже выполняется с некоторыми фоновыми задачами;
  2. Многопоточность позволяет быстрее выполнять задачи, поскольку потоки выполняются независимо;
  3. Многопоточность обеспечивает лучшее использование кэш-памяти, поскольку потоки разделяют общие ресурсы памяти;
  4. Многопоточность уменьшает количество требуемого сервера, поскольку один сервер может одновременно выполнять несколько потоков.

40. Каковы состояния в жизненном цикле потока?

Топ-50 Java Core вопросов и ответов на собеседовании. Часть 3 - 3
  1. New: В этом состоянии объект класса Thread создается с использованием оператора new, но поток не существует. Поток не запускается, пока мы не вызовем метод start().
  2. Runnable: В этом состоянии поток готов к запуску после вызова метода start(). Однако он еще не выбран планировщиком потока.
  3. Running: В этом состоянии планировщик потока выбирает поток из состояния готовности, и тот работает.
  4. Waiting/Blocked: в этом состоянии поток не работает, но все еще жив или ожидает завершения другого потока.
  5. Dead/Terminated: при выходе из метода run() поток находится в завершенном или мертвом состоянии.

41. Можно ли запустить тред дважды?

Нет, мы не можем перезапустить поток, так как после запуска и выполнения потока он переходит в состояние 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. Что если вызвать напрямую метод run(), не вызывая метод start()?

Да, вызвать метод run() конечно можно, но это никак не создаст новый поток и не выполнит его как отдельный. В этом случае, это простой объект, который вызывает простой метод. Если мы говорим о методе 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
Видно, что никакой нити не было создано. Все сработало как обычный класс. Вначале отработал метод первого класса, затем второй.

43. Что такое daemon тред?

Топ-50 Java Core вопросов и ответов на собеседовании. Часть 3 - 4Daemon thread (далее — демон-тред) — это тред, который выполняет задачи в фоне по отношению к другому потоку. То есть, его работа заключается в том, чтоб выполнять задачи вспомогательные, которые нужно делать только в привязке другому (основному) потоку. Есть много потоков демонов, работающих автоматически, например Garbage Collector, finalizer и т. д.

Почему Java закрывает демон-поток?

Единственная цель потока демона состоит в том, что он предоставляет сервисы потоку пользователя для фоновой задачи поддержки. Поэтому если основной поток завершился, то runtime закрывает автоматически и все его демон-потоки.

Методы для работы в Thread классе

Класс java.lang.Thread предоставляет два метода для работы с демоном-потоком:
  1. public void setDaemon(boolean status) — указывает, что это будет демон-поток. По умолчанию стоит false, что значит, что будут создаваться не демон-потоки, если не указать это отдельно.
  2. public boolean isDaemon() — по сути это геттер для переменной daemon, который мы устанавливаем предыдущим методом.
Пример:

class DaemonThreadExample extends Thread {

   public void run() {
       // Проверяет, демон ли этот поток или нет
       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() метода можно узнать какой это поток с одной стороны, с другой стороны, если у нас есть ссылка на объект этого потока, мы можем узнать и непосредственно у него. Это дает ту необходимую гибкость в настройке.

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. Что такое shutdownhook?

Shutdownhook — это поток, который неявно вызывается до завершения работы JVM(виртуальная машина Java). Таким образом, мы можем использовать его для очистки ресурса или сохранения состояния, когда виртуальная машина Java выключается нормально или внезапно. Мы можем добавить shutdown hook, используя следующий метод:

Runtime.getRuntime().addShutdownHook(new ShutdownHookThreadExample());
Как показано в примере:

/**
* Программа, которая показывает как запустить 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. Что такое синхронизация (synchronization)?

Синхронизация (Synchronization) в Java — это возможность контролировать доступ нескольких потоков к любому общему ресурсу. Когда несколько потоков пытаются выполнить одну и ту же задачу, существует вероятность ошибочного результата, поэтому для устранения этой проблемы Java использует синхронизацию, благодаря которой будет только один тред сможет работать в один момент. Синхронизация может быть достигнута тремя способами:
  • Синхронизируя метод
  • Синхронизируя определенный блок
  • Статической синхронизацией

Синхронизация метода

Синхронизированный метод используется для блокировки объекта для любого общего ресурса. Когда поток вызывает синхронизированный метод, он автоматически получает блокировку для этого объекта и снимает ее, когда поток завершает свою задачу. Чтоб заработало, нужно добавить ключевое слово synchronized. На примере увидим, как это работает:

/**
* Пример, где мы синхронизируем метод. То есть добавляем ему слово synchronized.
* Есть два писателя, которые хотят использовать один принтер. Они подготовили свои поэмы
* И конечно же не хотят, чтоб их поэмы перемешались, а хотят, чтоб работа была сделана по * * * очереди для каждого из них
*/
class Printer {

   synchronized void print(List<String> wordsToPrint) {
       wordsToPrint.forEach(System.out::print);
       System.out.println();
   }

   public static void main(String args[]) {
       // один объект для двух тредов
       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 Не пишу Не Письмо

Блок синхронизации

Синхронизированный блок может быть использован для выполнения синхронизации на любом конкретном ресурсе метода. Допустим, что в большом методе(да да, такие писать нельзя, но иногда бывает) нужно синхронизировать только небольшую часть, по каким-то причинам. Если вы поместите все коды метода в синхронизированный блок, он будет работать так же, как синхронизированный метод. Синтаксис выглядит так:

synchronized (“объект для блокировки”) {
   // сам код, который нужно защитить
}
Для того, чтоб не повторять пример предыдущий, создадим треды через анонимные классы - то есть сразу реализуя Runnable интерфейс.

/**
* Вот как добавляется блок синхронизации.
* Внутри нужно указать у кого будет взят мьютекс для блокировки.
*/
class Printer {

   void print(List<String> wordsToPrint) {
       synchronized (this) {
           wordsToPrint.forEach(System.out::print);
       }
       System.out.println();
   }

   public static void main(String args[]) {
       // один объект для двух тредов
       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 Не пишу Не Письмо

Статическая синхронизация

Если сделать статический метод синхронизированным, то блокировка будет на классе, а не на объекте. В этом примере мы применяем ключевое слово synchronized к статическому методу для выполнения статической синхронизации:

/**
* Вот как добавляется блок синхронизации.
* Внутри нужно указать у кого будет взят мьютекс для блокировки.
*/
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. Что такое volatile переменная?

Ключевое слово volatile используется в многопоточном программировании для обеспечения безопасности потока, поскольку модификация одной изменяемой переменной видна всем другим потокам, поэтому одна переменная может использоваться одним потоком за раз. При помощи ключевого слова volatile можно гарантировать, что переменная будет потокобезопасна и будет храниться в общей памяти, и потоки не будут ее брать себе в свой кеш. Как это выглядит?

private volatile AtomicInteger count;
Просто добавляем к переменной volatile. Но это не говорит о полной потокобезопасности… Ведь операции могут быть не атомарны над переменной. Но можно использовать Atomic классы, которые делают операцию атомарно, то есть за одно выполнение процессором. Таких классов много можно найти в пакете java.util.concurrent.atomic.

48. Что такое deadlock

Deadlock в Java является частью многопоточности. Взаимная блокировка может возникнуть в ситуации, когда поток ожидает блокировки объекта, полученной другим потоком, а второй поток ожидает блокировки объекта, полученной первым потоком. Таким образом эти два потока ждут друг друга и не будут дальше выполнять свой код. Топ-50 Java Core вопросов и ответов на собеседовании. Часть 3 - 5Рассмотрим Пример, в котором есть класс имплементирующий Runnable. Принимает в конструкторе он два ресурса. Внутри метода run() он по-очереди берет блокировку для них, так вот если создать два объекта этого класса, а ресурсы передать в разном порядке, то легко можно нарваться на блокировку:

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?

Исходя из того, что мы знаем как дедлок возникает, то можно сделать некоторые выводы…
  • Как показано в примере выше, дедлок был из-за того, что была вложенность блокировок. То есть внутри одной блокировки находится еще одна или более. Избежать это можно следующим образом - вместо вложенности нужно добавить новую абстракцию поверх и дать блокировку на более высокий уровень, а вложенные блокировки убрать.
  • Чем больше блокировок, тем больше шансов, что будет дедлок. Поэтому каждый раз добавляя блокировку нужно думать, а точно ли она нужна и можно ли избежать добавление новой.
  • Использования Thread.join(). Дедлок можно сделать также при ожидании одного треда другим. Чтобы избежать этой проблемы, можно подумать над тем, чтобы выставлять ограниченное время на join() метод.
  • Если у нас один поток - дедлока не будет ;)

50. Что такое состояние гонки?

Если в реальных гонках выступают машины, то в гонках терминологии многопоточности в гонках выступают треды. Но почему? Есть же два треда, которые работают и которые могут иметь доступ к одному и тому же объекту. И они могут попытаться обновить состояние в одно и тоже время. Пока все ясно, да? Так работа тредов происходит или реально параллельно(если есть больше одного ядра в процессоре) или условно параллельно, когда процессор выделяет по небольшому промежутку времени. И управлять этими процессами мы не можем, поэтому мы не можем гарантировать, что когда один тред прочитает данные из объекта, он успеет их изменить ДО того, как это сделает какой-то другой тред. Такие проблемы бывают, когда проходит такая комбинация “проверь-и-действуй”. Что это значит? Например у нас есть if выражение, в теле которого изменяется само условие, то есть:

int z = 0;

// проверь
if (z < 5) {
//действуй
   z = z + 5;
}
Так вот может быть ситуация, когда два треда одновременно зайдут в этот блок кода в момент, когда z еще равно нулю и в двоем изменят это значение. И в итоге мы получим не ожидаемое значение 5, а уже 10. Как это избежать? Нужно поставить блокировку до начала выполнения и после. То есть, чтобы первый тред зашел в блок if, выполнил все действия, изменил z и уже потом дал возможность сделать это следующему треду. А вот уже следующий тред не зайдет в блок if, так как z уже будет равно 5:

// получить блокировку для z
if (z < 5) {
   z = z + 5;
}
// выпустить из блокировки z
===================================================

Вместо вывода

Хочу сказать спасибо всем тем, кто дочитал до конца. Это был длинный путь и вы его осилили! Может быть понятно не всё. Это нормально. Я как только начинал изучать джаву, мне никак в голове не умещалось что такое статическая переменная. Но ничего, переспал с этой мыслью, почитал еще несколько источников и таки понял. Подготовка к собеседованию - это скорее академический вопрос, чем практический. Поэтому перед каждым собеседованием нужно повторять и освежать в памяти то, что может не так уж и часто используешь.

И как всегда, полезные ссылки:

Всем спасибо за прочтение, До скорых встреч) Мой профиль на GitHub
Комментарии (10)
Чтобы просмотреть все комментарии или оставить свой,
перейдите в полную версию
Roman Beskrovnyi 35 уровень
11 марта 2021
⚡️UPDATE⚡️ Друзья, создал телеграм-канал 🤓, в котором освещаю свою писательскую деятельность и свою open-source разработку в целом. Не хотите пропустить новые статьи? Присоединяйтесь ✌️
Богдан Зінченко 37 уровень, Харьков
27 января 2021
спасибо, статья слабовата, имхо
Maris 40 уровень
3 декабря 2020
Спасибо! Надеюсь на интервью и будут такие не сложные вопросы, а не какая-нибудь чернь. Особенно понравился вопрос про два класса, которые не наследуются от Object ))))
Евгений 41 уровень, Нижний Новгород Expert
20 июля 2020
Спасибо.
Сергей 6 уровень, Екатеринбург
24 марта 2020
Наверно опечатка в 38 вопросе, в 6 пункте - "Любые изменения в родительском процессе не влияют на дочерний процесс, тогда как изменения в родительском потоке могут влиять на дочерний поток".
Евгений 35 уровень, Москва
20 марта 2020
мне на всех собеседованиях обязательно задавали один и тот-же вопрос: "Расскажите все что знаете о equals и hashcode". Было бы интересно получить какой-то четкий и понятный ответ на этот вопрос. Что хотят услышать интервьюеры в ответ на него?))
Aleksey 9 уровень, Днепр
18 марта 2020
Спасибо огромное!
AlinaAlina 32 уровень, Санкт-Петербург
18 марта 2020
Спасибо за статьи! Подобный материал очень помогает! Причём как подбадривает, так и приземляет🙂