JavaRush /جاوا بلاگ /Random-UR /Топ-50 Java Core вопросов и ответов на собеседовании. Час...

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

گروپ میں شائع ہوا۔
Топ-50 Java Core вопросов и ответов на собеседовании. Часть 1 Топ-50 Java Core вопросов и ответов на собеседовании. Часть 2

Multithreading

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

Так or иначе создание происходит через использование класса Thread. Но здесь могут быть варианты…
  1. Наследуемся от java.lang.Thread
  2. Имплементируем интерфейс 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. Какая разница между процессом и потоком?

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

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

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

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

Топ-50 Java Core вопросов и ответов на собеседовании. Часть 3 - 3
  1. New: В этом состоянии an object класса Thread создается с использованием оператора new, но поток не существует. Поток не запускается, пока мы не вызовем метод start().
  2. Runnable: В этом состоянии поток готов к запуску после вызова метода start(). Однако он еще не выбран планировщиком потока.
  3. Running: В этом состоянии планировщик потока выбирает поток из состояния готовности, и тот работает.
  4. Waiting/Blocked: в этом состоянии поток не работает, но все еще жив or ожидает завершения другого потока.
  5. 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 тред?

Топ-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() {
       // Проверяет, демон ли этот поток 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. Топ-50 Java Core вопросов и ответов на собеседовании. Часть 3 - 5Рассмотрим Пример, в котором есть класс имплементирующий 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 всегда, полезные ссылки:

Всем спасибо за прочтение, До скорых встреч) Мой профиль на GitHub
تبصرے
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION