JavaRush /Java блог /Random UA /Топ-50 Java Core питань та відповідей на співбесіді. Част...
Roman Beekeeper
35 рівень

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

Стаття з групи Random UA
Топ-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
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ