Вы достигли нового уровня

Уровень 27

1. Циклы (break и return, continue, метки)

- Привет, Амиго!

Сегодня я тебе расскажу про некоторые удобные вещи в работе с циклами.

Первая такая вещь – это ключевое слово break. Если в теле цикла написать такую инструкцию, то при ее выполнении цикл сразу завершится. Пример:

ПримерРезультат работы цикла:
for (int i=0;i<10;i++)
{
 System.out.println(i);
 if (i>5)
  break;
}
0
1
2
3
4
5

- А break можно использовать только в цикле?

- Да. break можно использовать только в цикле. При выполнении команды break цикл тут же завершается.

- Ок. Понятно.

- Отлично. Теперь вторая команда – это ключевое слово continue. Его тоже можно использовать только в цикле. При выполнении этой команды, сразу начинается новая итерация цикла. Другими словами, просто пропускается весь код тела цикла.

Пример:

ПримерРезультат работы цикла:
for (int i=0;i<10;i++)
{
 if (i%2==0)
  continue;
 System.out.println(i);
}
1
3
5
7
9

- Т.е. как только программа доходит до исполнения команды continue внутри цикла, она перестает выполнять код в нем?

- Нет. Смотри. Цикл – это когда мы выполняем один и то же код несколько раз. В примере выше – у нас цикл от 0 до 9 – т.е. тело цикла выполнится 10 раз. Так?

- Да.

- Одно такое исполнение кода тела цикла называется итерацией цикла. Наш цикл состоит из 10 итераций – десяти исполнений кода в его теле.

- Да, это ясно.

- Команда continue преждевременно завершает текущую итерацию - код внутри цикла пропускается и начинается новая итерация.

Вот тебе еще пример:

Пример
ArrayList list = new ArrayList();
for (Object o: list)
{
 if (o==null) continue;
 System.out.println(o.toString());
}

В данном примере мы выводим на экран строковое представление всех объектов, содержащихся в списке list. Но пропускаем все объекты, которые равны null.

- Да, ясно. Очень удобная штука, я смотрю.

- Ага. Еще хочу рассказать тебе про метки. Они редко используются в Java, т.к. часто нарушают красоту логики программы. Но ты можешь встретить их где-то в коде. Так что лучше пусть ты от меня услышишь о них, чем где-то во дворе.

Давным-давно в коде разрешалось прыгать с любой строчки на любую. Для этого использовались метки и оператор goto. Вот как это выглядело:

Ужасный код с метками
       System.out.println("Мир");
label: System.out.println("Труд");
       System.out.println("Май");
       goto label;

В данном примере, после выполнении команды goto label, программа прыгала на строку, обозначенную меткой label.

Потом все дружно прозрели и решили не использовать оператор goto. В Java его до сих пор нет, но ключевое слово goto зарезервировано. Мало ли…

- Т.е. ни goto, ни меток в Java нет?

- Оператора goto нет, а метки есть!

- Это как же это?

- Метки можно использовать в Java вместе с командами continue и break. Они используются, когда у тебя много вложенных циклов.

Например, у тебя 5 вложенных циклов и ты хочешь, при выполнении некоторого условия, выйти из трех из них, но не из всех. Метки позволяют сделать это красиво:

Пример
label1: for (int i=0;i<10;i++)
  label2: for (int j=0;j<10;j++)
   label3: for (int k=0;k<10;k++)
    if (i==j && j==k)
     break label2;

В данном примере, при выполнении команды break, мы выйдем не из цикла с переменной k, а из цикла помеченного меткой label2 – т.е. выйдем сразу из двух циклов k и j.

- И как часто это используется?

- Если честно, то не часто, но мало ли. Может, встретишь где-нибудь. Это основы синтаксиса – это все надо знать!

- Ок. Спасибо, Билаабо.

2. Задачи на break & continue;

- Привет, Амиго!

Задачи
1. Рефакторинг

Перепишите код без использования меток, сохранив при этом логику.
Не оставляйте закомментированный код.
2. Нужный оператор

Вставьте в код единственную строчку - оператор (не break), чтобы выводился треугольник из букв S

3. DeadLock, и его причины

- Привет, Амиго!

Сегодня я тебе расскажу, что такое дедлок (Dead Lock) - смертельный захват.

- Так ты же уже что-то такое рассказывала.

- Ага, было дело. Но сегодня мы рассмотрим эту тему детальнее.

В самом простом случае в дедлоке участвуют две нити и два объекта-мютекса. Взаимная блокировка возникает, когда:

А) Каждой нити в процессе работы нужно захватить оба мютекса.

Б) Первая нить захватила первый мютекс и ждет освобождения второго.

В) Вторая нить захватила второй мютекс и ждет освобождения первого.

Примеры:

Пример
public class Student
{
 private ArrayList<Student> friends = new ArrayList<Student>();

 public synchronized ArrayList<Student> getFriends()
 {
  synchronized(friends)
  {
   return new ArrayList(friends);
  }
 }

 public synchronized int getFriendsCount()
 {
  return friends.size();
 }

 public int addFriend(Student student)
 {
  synchronized(friends)
  {
   friends.add(student)
   return getFriendsCount ();
  }
 }
}

Допустим, первая нить вызвала метод getFriends, тогда она сначала захватит мютекс объекта this, а затем мютекс объекта friends.

Вторая нить при этом вызвала метод addFriend, она сначала захватывает мютекс объекта friends, а затем мютекс объекта this (при вызове getFriendsCount).

Сначала все будет хорошо, но как гласит Закон Мерфи - если неприятность может случиться, она случается. Обязательно возникнет ситуация, когда первая нить успеет захватить только один мютекс, а вторая нить в это время захватит второй. Они так и будут висеть вечно в ожидании, что кто-то из них первым освободит мютекс.

Еще один простой пример, нашел в книге – решил привести:

Пример
class KnightUtil
{
 public static void kill(Knight knight1, Knight knight2)
 {
  synchronized(knight1)
  {
   synchronized(knight2)
   {
    knight2.live = 0;
    knight1.experiance +=100;
   }
  }
 }
}

Есть игра, где два рыцаря сражаются друг с другом. Один рыцарь убивает другого. Это поведение отражено в методе kill. Туда передаются два объекта-рыцаря.

Сначала мы защищаем оба объекта, чтобы никто больше не мог их изменить.

Второй рыцарь умирает (live=0)

Первый рыцарь получает +100 опыта.

Все вроде бы отлично, но иногда может возникнуть ситуация, когда второй рыцарь в это время атакует первого. Для него тоже вызывается этот метод, но рыцари передаются в другом порядке.

- Т.е. нам даже не нужно несколько методов для получения дедлока?

- Ага. Иногда бывает достаточно одного простого метода, в котором уже могут происходить процессы, приводящие к зависанию нитей и всей программы.

- Да, оказывается, это явление встречается чаще, чем я думал. Спасибо, Элли.

4. Задачи по написанию своих дедлоков

- Привет, Амиго!

Задачи
1. Создаем дедлок

Расставьте модификаторы так, чтобы при работе с этим кодом появился дедлок
Метод main порождает deadLock, поэтому не участвует в тестировании
2. Второй вариант дедлока

В методе secondMethod в синхронизированных блоках расставьте локи так,
чтобы при использовании класса Solution нитями образовывался дедлок
3. Модификаторы и дедлоки

Расставьте модификаторы так, чтобы при работе с этим кодом появился дедлок

5. Стратегии избегания DeadLock

- Привет, Амиго!

Хочу рассказать тебе про пару стратегий избегания дедлоков.

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

Иногда, например, блокировкам присваивают уровни, требуя при этом от нити захватывать блокировки в порядке от большего уровня к меньшему, но не в обратном. Так же нельзя захватывать несколько блокировок одного уровня.

Например, в предыдущем примере про рыцарей, каждому рыцарю можно добавить его уникальный номер (id) и потребовать, чтобы блокировки происходили от большего id к меньшему.

Пример
class KnightUtil
{
 public static void kill(Knight knight1, Knight knight2)
 {
  Knight knightMax = knight1.id > knight2.id ? knight1: knight2;
  Knight knightMin = knight1.id > knight2.id ? knight2: knight1;

  synchronized(knightMax)
  {
   synchronized(knightMin)
   {
    knight2.live = 0;
    knight1.experiance +=100;
   }
  }
 }
}

- Красивое решение.

- Это очень простое решение, но мне нравится. Надеюсь, оно тебе пригодится, когда ты будешь думать, как решать возможные проблемы с дедлоками.

- Спасибо, Элли.

6. Задачи по иправлению дедлоков

- Привет, Амиго!

Задачи
1. Убираем deadLock

Используя стратегию избегания deadLock-а сделайте так, чтобы он не возник.
Метод main не участвует в тестировании.
Действуйте аналогично примеру из лекций.
Изменения вносите только в safeMethod.
2. Определяем порядок захвата монитора. Сложная.

Реализуйте логику метода isNormalLockOrder, который должен определять:
соответствует ли порядок synchronized блоков в методе someMethodWithSynchronizedBlocks - порядку передаваемых в него аргументов.
Метод main не участвует в тестировании.
3. Убираем deadLock, используя Открытые вызовы

Синхронизированные методы, которые вызывают внутри себя синхронизированные методы других классов, приводят к dead-lock-у.
1. Перенесите синхронизацию с метода в синхронизированный блок, куда поместите лишь необходимые части кода.
2. Уберите избыточную синхронизацию методов.
3. В стеке вызова методов не должно быть перекрестной синхронизации,
т.е. такого synchronizedMethodAClass().synchronizedMethodBClass().synchronizedMethodAClass()

Этот способ избавления от дэдлока называется Открытые вызовы, о нем читайте в дополнительном материале к лекции.
Метод main не участвует в тестировании.

7. Стратегия "wait-notify-notifyAll"

- Привет, Амиго!

Хочу основательно разобрать с тобой тему wait-notify. Методы wait-notify обеспечивают удобный механизм взаимодействия нитей. Также их можно использовать для построения сложных высокоуровневых механизмов взаимодействия нитей.

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

Во-первых, мы создадим группу нитей-исполнителей, столько же, сколько и количество ядер процессора. Каждая нить сможет работать на своем ядре: нити не будут друг другу мешать, а ядра процессора не будут простаивать.

Во-вторых, создадим объект-очередь, в который будут помещаться полученные от пользователей задания. Разным типам заданий будут соответствовать различные объекты, но все они будут реализовать интерфейс Runnable, чтобы их можно было выполнить.

- А можно пример такого объекта-задания?

- Вот смотри:

Класс вычисляет факториал числа n при вызове метода run()
class Factorial implements Runnable
{
 public int n = 0;
 public long result = 1;

 public Factorial (int n)
 {
  this.n = n;
 }

 public void run()
 {
  for (int i=2;i<=n;i++)
   result*=i;
 }
}

- Пока ясно.

- Отлично. Тогда разберем, как должен выглядеть объект-очередь. Что ты можешь сказать про него?

- Он должен быть thread-safe. В него кладутся объекты-задания (таски) нитью, которая принимает их от пользователей, а забираются задания нитями-исполнителями.

- Ага. А если задания временно закончились?

- Тогда нити-исполнители должны ждать, пока они появятся.

- Верно. Тогда представь, что все это можно встроить в одну очередь. Вот смотри:

Очередь заданий, если задания нет, то нить засыпает и ждет его появления:
public class JobQueue
{
 ArrayList<Runnable> jobs = new ArrayList<Runnable>();

 public synchronized void put(Runnable job)
 {
  jobs.add(job);
  this.notifyAll();
 }

 public synchronized Runnable getJob()
 {
  while (jobs.size()==0)
   this.wait();

  return jobs.remove(0);
 }
}

У нас есть метод getJob, который смотрит, если список работы (jobs) пуст, то нить засыпает (wait), пока в списке что-то не появится.

А есть еще метод put, который позволяет добавить в список jobs новое задание (job). Как только новое задание добавлено, вызывается метод notifyAll. Вызов этого метода пробудит все нити-исполнители, которые заснули внутри метода getJob.

- А можешь напомнить еще раз, как работают методы wait и notify?

- Метод wait вызывается только внутри блока synchronized, у объекта-мютекса. В нашем случае – это this. При этом происходит две вещи:

1) Нить засыпает.

2) Нить временно освобождает мютекс (пока не проснется).

После этого другие нити могу входить в блок synchronized и занимать этот же мютекс.

Метод notifyAll тоже можно вызвать только внутри блока synchronized у объекта-мютекса. В нашем случае – это this. При этом происходит две вещи:

1) Просыпаются все нити, которые заснули на этом же объекте-мютексе.

2) Как только текущая нить выйдет из блока synchronized, одна из проснувшихся нитей захватит мютекс и продолжит свою работу. Когда она освободит мютекс, другая проснувшаяся нить захватит мютекс и т.д.

Очень похоже на автобус. Вы заходите внутрь, хотите передать за проезд, а водителя нет. И вы «засыпаете». Со временем вас набивается целый автобус, но за проезд пока никто не передает – некому. Затем заходит водитель, вы слышите « – Передаем за проезд». И тут начинается...

- Интересное сравнение. А что такое автобус?

- А это Хулио рассказывал. Были такие странные штуки в 21 веке.

8. Нюансы работы

- Привет, Амиго!

И еще пара деталей. Так сказать практических советов.

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

Если коллекция пустая, то ждем
public synchronized Runnable getJob()
{
 if (jobs.size()==0)
  this.wait();

 return jobs.remove(0);
}

В документации по Java очень старательно советуют вызвать метод wait в цикле:

Если коллекция пустая, то ждем
public synchronized Runnable getJob()
{
 while (jobs.size()==0)
  this.wait();

 return jobs.remove(0);
}

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

Грубо говоря, могут быть «ложные побудки». Хороший разработчик должен учитывать это дело.

- Ясно. А не проще ли тогда использовать просто notify?

- А если в списке больше чем одно задание? Notify обычно советуют использовать ради оптимизации. Во всех остальных случаях рекомендуют использовать метод notifyAll.

- Ок.

- Но и это еще не все. Во-первых, может возникнуть ситуация, когда кто-то унаследовался от твоего класса, добавил туда свои методы и тоже использует wait/notifyAll. Т.е. может быть ситуация, когда на одном объекте висят независимые пары wait/notifyAll, которые друг о друге не знают. Поэтому что надо делать?

- Всегда вызывать wait в цикле и проверять, что условие выхода из цикла действительно выполнилось!

- Правильно. А чтобы тебе стало совсем понятно, что от этого никуда не деться, то многие разработчики указывают на то, что иногда нити просыпаются сами. Нити, которые гарантированно никто не может будить случайно. Похоже это побочный процесс оптимизации/ускорения кода в работающей Java-машине.

- Ничего себе. Понял, без цикла перед wait никуда.

9. Задачи на "wait-notify-notifyAll"

- Привет, Амиго!

Задачи
1. CountDownLatch

Дана стандартная реализация методологии wait-notify.
Почитайте про CountDownLatch и перепишите тело метода someMethod используя поле latch.
Весь лишний код удалите из класса.
2. Producer–consumer

В классе TransferObject расставьте вызов методов wait/notify/notifyAll,
чтобы обеспечить последовательное создание и получение объекта.
Метод main не участвует в тестировании
3. Расставьте wait-notify

Расставьте wait-notify.

Пример вывода:
Thread-0 MailServer has got: [Person [Thread-1] has written an email 'AAA'] in 1001 ms after start

10. Другие детали синхронизации и многонитиевости

- Привет, Амиго!

Есть такая здоровенная тема, называется Java Memory Model. В принципе знать ее тебе пока не обязательно, но услышать про это – будет полезно.

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

- И сложнее!

- Да, лучше и сложнее. Это как самолет. Летать на самолете лучше, чем идти пешком, но сложнее. Попробую объяснить тебе новую ситуацию очень упрощенно.

Вот, что было придумано. В код был добавлен механизм синхронизации локальной памяти нитей, названный «happens before» (дословно «случилось перед»). Был придуман ряд правил/условий, при наступлении которых память синхронизируется – обновляется до актуального состояния.

Пример:

ПорядокНить 1Нить 2
1
2

101
102
103
104
105

201
202
203
204
205
public int y = 1;
public int x = 1;

x=2;
synchronized(mutex)
{
 y = 2;
}
нить ждет освобождения мютекса - mutex


synchronized(mutex)
{
 if (y == x)
  System.out.println("YES");
}

Одно из таких условий – это захват освобожденного мютекса. Если мютекс был освобожден и снова захвачен, то перед захватом обязательно выполнится синхронизация памяти. Нить 2 увидит «самые новые» значения переменных x и y, даже если не объявлять их volatile.

- Как интересно. И много таких условий?

- Достаточно, вот некоторые условия синхронизации памяти:

- Т.е. все немного сложнее, чем я думал?

- Да, немного сложнее…

- Спасибо, Риша, буду думать.

- Не заморачивайся сильно на эту тему. Придет время, сам все поймешь. Пока тебе лучше разбираться в основах, чем лезть в дебри внутреннего устройства Java-машины. Вот выйдет Java 8 и опять все поменяется.

- О_о. М-да. Некоторые вещи лучше не знать.

11. Учимся гуглить. (Как получить список файлов по маске)

- Привет, Амиго!

Продолжаем наши уроки – учимся гуглить.

Вот тебе несколько заданий:

 Задания на поиск в интернете:
1 Чем плох оператор goto?
2 Что такое зарезервированные слова в Java?
3 Что произойдет, если вызвать wait не в блоке synchronized?
4 Что такое happens-before?
5 Назначение и методы класса BlockedQueue?
6 Как скомпилировать java-файл из консоли?
7 Как запустить java-файл из консоли?
8 Как запустить программу из нескольких скомпилированнх файлов из консоли?
9 Как создать директорию с поддиректориями: (doc/release/com/javarush/test)?
10 Как получить список файлов в директории по маске (шаблону) «*.doc»?

12. Профессор дает доп. материал

- Привет, Амиго!

Вот тебе дополнительный материал по теме.

Ссылка на дополнительный материал

13. Хулио

- Привет, Амиго!

- Привет, Хулио. А скажи, в честь кого тебя назвали?

- Во времена моей прапрапрапрабабушки был такой известный певец, Иглесиас. Ну, вот теперь мне только осталось научиться петь «Nostalgie».

- А как это, петь?

- Сейчас покажу видео, присаживайся.

Оригинал видео на YouTube

14. Вопросы к собеседованию по этой теме

- Привет, Амиго!

 Вопросы к собеседованиям
1 Что такое дедлок?
2 Какие вы знаете стратегии, предотвращающие появление дедлоков?
3 Могут ли возникнуть дедлоки при использовании методов wait-notify?
4 Что чаще используется: notify или notifyAll?
5 Метод wait рекомендуется использовать с конструкциями if или while?
6 Что происходит после вызова метода notifyAll?
7 Какие выгоды получает объект, если он immutable?
8 Что такое «thread-safe»?
9 Что такое "happens-before"?
10 Что такое JMM?
11 Какое исключение вылетит, если вызвать wait не в блоке synchronized?

15. Большая задача

- Привет, Амиго!

Сегодня ты начинаешь работу над супер современной и полезной программой! Это – электронное меню. Вот:

- Круто! А для чего это?

- Что-то ты много вопросов задаешь! Вот сделаешь, потом поговорим. Иди к секретному агенту, он даст тебе все необходимые инструкции.

- Товарищ Капитан, я не умею так красиво рисовать!

- Запомни, тебе нужно реализовать необходимую бизнес логику, а картинки будет рисовать дизайнер. Иди к секретному агенту, по ходу дела разберешься.

- Есть разбираться!