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

Уровень 17

1. Проблемы многопоточности: обращение к общему ресурсу

- Привет, Амиго! Вчера мы обсудили преимущества и удобства, которые несет с собой многонитиевость (multithreading). Теперь пора взглянуть и на минусы. А они, к сожалению, не маленькие.

Раньше мы смотрели на программу, как на набор объектов, которые вызывают методы друг друга. Теперь все стало немного сложнее. Программа – это скорее набор объектов, по которым лазает несколько «маленьких роботиков» – нитей – и выполняют команды, содержащиеся в методах.

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

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

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

Код первой нитиКод второй нити
System.out.print ("Коле");
System.out.print ("");
System.out.print ("15");
System.out.print ("");
System.out.print ("лет");
System.out.println ();
System.out.print ("Лене");
System.out.print ("");
System.out.print ("21");
System.out.print ("");
System.out.print ("год");
System.out.println ();
Ожидаемый вывод на консоль
Коле 15 лет
Лене 21 год
Итоговый порядокКод первой нитиКод второй нити
System.out.print ("Коле");
System.out.print ("Лене");
System.out.print (" ");

System.out.print (" ");
System.out.print ("15");

System.out.print ("21");
System.out.print (" ");

System.out.print (" ");
System.out.print ("лет");
System.out.println ();

System.out.print ("год");
System.out.println ();
System.out.print ("Коле");
исполняется другая нить
исполняется другая нить

System.out.print (" ");
System.out.print ("15");

исполняется другая нить
исполняется другая нить

System.out.print (" ");
System.out.print ("лет");
System.out.println ();

исполняется другая нить
исполняется другая нить
исполняется другая нить
System.out.print ("Лене");
System.out.print (" ");

исполняется другая нить
исполняется другая нить

System.out.print ("21");
System.out.print (" ");

исполняется другая нить
исполняется другая нить
исполняется другая нить

System.out.print ("год");
System.out.println ();
Реальный вывод на консоль
Коле Лене 15 21 лет
год

Или вот еще пример:

КодОписание
class MyClass
{
 private String name1 = "Оля";
 private String name2 = "Лена";
  public void swap()
  {
   String s = name1;
   name1 = name2;
   name2 = s;
 }
}
Метод swap меняет местами значения переменных name1 & name2.

Что же будет если его вызывать из двух нитей одновременно?
Итоговый порядокКод первой нитиКод второй нити
String s1 = name1; //Оля
name1 = name2; //Лена

String s2 = name1; //Лена(!)
name1 = name2; //Лена
name2 = s1; //Оля
name2 = s2; //Лена
String s1 = name1;
name1 = name2;

исполняется другая нить
исполняется другая нить

name2 = s1;
исполняется другая нить
исполняется другая нить
исполняется другая нить

String s2 = name1;
name1 = name2;

исполняется другая нить
name2 = s2;
Итог
Обе переменных имеют значение «Лена».
Объект «Оля» был перезатерт и потерян.

- Кто бы мог подумать, что при элементарном присваивании возможны такие ошибки?

- Да, но для этой проблемы есть решение. Но об этом немного позже – у меня горло пересохло.

2. Задачи на общий ресурс - вывод в консоль..

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

Задачи
1. Заметки

1. Класс Note будет использоваться нитями.
2. Создай public static нить NoteThread (Runnable не является нитью), которая в методе run 1000 раз (index = 0-999) сделает следующие действия:
2.1. используя метод addNote добавит заметку с именем [getName() + "-Note" + index], например, при index=4
"Thread-0-Note4"
2.2. используя метод removeNote удалит заметку
2.3. в качестве первого параметра в removeNote передай имя нити - метод getName()
2. Вместе быстрее? Ща проверим :)

1. Разберись, что и как работает
2. Создай public static нить SortThread, которая в методе run отсортирует статический массив testArray используя метод sort

3. synchronized

- Привет, Амиго! У нас есть панацея – лекарство от всех болезней. Как мы уже успели убедиться – неконтролируемое переключение нитей – это проблема.

- А почему бы нитям самим не решать, когда переключиться на следующую? Сделала все важные дела и «маякует», я – все!

- Разрешать нитям самим управлять своим переключением – еще большая проблема. Вдруг какой-то код не очень красиво написан, и нить никогда сама не отдаст свое «процессорное время». Давным-давно так и было – и это был тихий ужас.

- Ладно. И какое же решение есть?

- Блокировка нитей. И вот как это работает.

Было выяснено, что нити мешают друг другу, когда пытаются сообща работать с общими объектами и/или ресурсами. Как в примере с выводом на консоль: консоль одна, а выводят на нее все нити. Непорядок.

Поэтому был придуман специальный объект – мютекс. Это как табличка на двери туалета «свободно» «занято». Он имеет два состояния – объект свободен и объект занят, или их еще называют заблокирован и разблокирован.

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

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

- А как работать с этим мютексом. Надо создавать специальные объекты?

- Все намного проще. Разработчики Java встроили этот мютекс в класс Object. Тебе даже создавать его не придется. Он есть у каждого объекта. Вот как это все работает:

КодОписание
class MyClass
{
 private String name1 = "Оля";
 private String name2 = "Лена";

 public void swap()
 {
  synchronized (this)
  {
   String s = name1;
   name1 = name2;
   name2 = s;
  }
 }
}
Метод swap меняет местами значения переменных name1 & name2.

Что же будет если его вызывать из двух нитей одновременно?
Итоговый порядокКод первой нитиКод второй нити
String s1 = name1; //Оля
name1 = name2; //Лена
name2 = s1; //Оля


String s2 = name1; //Лена
name1 = name2; //Оля
name2 = s2; //Лена
String s1 = name1;
name1 = name2;

исполняется другая нить
name2 = s1;
нить ждет, пока освободится мютекс

String s2 = name1;
name1 = name2;

исполняется другая нить
исполняется другая нить

name2 = s2;
Итог
Значения переменных были дважды обменяны местами и вернулись на первоначальное место.

Обрати внимание на ключевое слово synchronized.

- Да, а что оно значит?

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

Если же мютекс был занят, то наша нить будет стоять на месте и ждать когда он освободится.

- Так просто и так элегантно. Красивое решение.

- Ага. А как ты думаешь, что будет в этом случае?

КодОписание
class MyClass
{
 private String name1 = "Оля";
 private String name2 = "Лена";

 public void swap()
 {
  synchronized (this)
  {
   String s = name1;
   name1 = name2;
   name2 = s;
  }
 }

 public void swap2()
 {
  synchronized (this)
  {
   String s = name1;
   name1 = name2;
   name2 = s;
  }
 }
}
Методы swap и swap2 имеют один и тот же мютекс – объект this.

Что будет, если одна нить вызовет метод swap, а другая – метод swap2?

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

- Молодец, Амиго! Верное решение!

Хотелось бы обратить твое внимание на то, что словом synchronized может быть помечен как кусок кода, так и метод. Вот что это значит:

КодЧто происходит на самом деле
class MyClass
{
 private static String name1 = "Оля";
 private static String name2 = "Лена";

 public synchronized void swap()
 {
  String s = name1;
  name1 = name2;
  name2 = s;
 }

 public static synchronized void swap2()
 {
  String s = name1;
  name1 = name2;
  name2 = s;
 }
}
class MyClass
{
 private static String name1 = "Оля";
 private static String name2 = "Лена";

 public void swap()
 {
  synchronized (this)
  {
   String s = name1;
   name1 = name2;
   name2 = s;
  }
 }

 public static void swap2()
 {
  synchronized (MyClass.class)
  {
   String s = name1;
   name1 = name2;
   name2 = s;
  }
 }

4. Задачи на synchronized

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

Задачи
1. Синхронизированные заметки

1. Класс Note будет использоваться нитями. Поэтому сделай так, чтобы обращения к листу notes блокировали мютекс notes, не this
2. Все System.out.println не должны быть заблокированы (синхронизированы), т.е. не должны находиться в блоке synchronized
2. Синхронизированные заметки 2

Класс Note будет использоваться нитями. Поэтому сделай так, чтобы ве методы были синхронизированы
3. Сад-огород

1. Создайте метод public void addFruit(int index, String fruit) - который добавляет параметр fruit в лист fruits на позицию index
2. Создайте метод public void removeFruit(int index) - который удаляет из fruits элемент с индексом index
3. Создайте метод public void addVegetable(int index, String vegetable) - который добавляет параметр vegetable в лист vegetables на позицию index
4. Создайте метод public void removeVegetable(int index) - который удаляет из vegetables элемент с индексом index
5. Класс Garden будет использоваться нитями. Поэтому сделай так, чтобы все методы блокировали мютекс this
6. Реализуй это минимальным количеством кода
4. Синхронизированный президент

И снова Singleton паттерн - синхронизация в статическом блоке
Внутри класса OurPresident в статическом блоке создайте синхронизированный блок.
Внутри синхронизированного блока инициализируйте president.
5. МВФ

Singleton паттерн - синхронизация в методе
IMF - это Международный Валютный Фонд
Внутри метода getFund создайте синхронизированный блок
Внутри синхронизированного блока инициализируйте переменную imf так, чтобы метод getFund всегда возвращал один и тот же объект

5. Кэш. проблема многопоточности - локальный кэш. volatile

- Привет, Амиго! Помнишь, Элли тебе рассказывала про проблемы при одновременном доступе нескольких нитей к общему (разделяемому) ресурсу?

- Да.

- Так вот – это еще не все. Есть еще небольшая проблема.

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

Чтобы ускорить свою работу, процессор копирует самые часто используемые переменные и области памяти в свой кэш и все изменения с ними производит в этой быстрой памяти. А после – копирует обратно в «медленную» память. Медленная память все это время содержит старые(!) (неизмененные) значения переменных.

И тогда может возникнуть проблема. Одна нить меняет переменную, такую как isCancel или isInterrupted из примера выше, а вторая нить «не видит» этого изменения, т.к. оно было совершено в быстрой памяти. Это следствие того, что нити не имеют доступа к кэшу друг друга. (Процессор часто содержит несколько независимых ядер и нити физически могут исполняться на разных ядрах.)

Вспомним вчерашний пример:

КодОписание
class Clock implements Runnable
{
 private boolean isCancel = false;

 public void cancel()
 {
  this.isCancel = true;
 }

 public void run()
 {
  while (!this.isCancel)
  {
   Thread.sleep(1000);
   System.out.println("Tik");
  }
 }
}
Нить «не знает» о существовании других нитей.

В методе run переменная isCancel при первом использовании будет помещена в кэш дочерней нити. Эта операция эквивалентна коду:

public void run()
{
 boolean isCancelCached = this.isCancel;
 while (!isCancelCached)
 {
  Thread.sleep(1000);
  System.out.println("Tik");
 }
}


Вызов метода cancel из другой нити поменяет значение переменной isCancel в обычной (медленной) памяти, но не в кэше остальных нитей.
public static void main(String[] args)
{
 Clock clock = new Clock();
 Thread clockThread = new Thread(clock);
 clockThread.start();

 Thread.sleep(10000);
 clock.cancel();
}

- Ничего себе! А для этой проблемы тоже придумали красивое решение, как в случае с synchronized?

- Ты не поверишь!

Сначала думали отключить работу с кэшем, но потом оказалось, что из-за этого программы работают в разы медленнее. Тогда придумали другое решение.

Было придумано специальное ключевое слово volatile. Помещение его перед определением переменной запрещало помещать ее значение в кэш. Вернее не запрещало помещать в кэш, а просто принудительно всегда читало и писало ее только в обычную (медленную) память.

Вот как нужно исправить наше решение, чтобы все стало отлично работать:

КодОписание
class Clock implements Runnable
{
 private volatile boolean isCancel = false;

 public void cancel()
 {
  this.isCancel = true;
 }

 public void run()
 {
  while (!this.isCancel)
  {
   Thread.sleep(1000);
   System.out.println("Tik");
  }
 }
}
Из-за модификатора volatile чтение и запись значения переменной всегда будут происходить в обычной, общей для всех нитей, памяти.
public static void main(String[] args)
{
 Clock clock = new Clock();
 Thread clockThread = new Thread(clock);
 clockThread.start();

 Thread.sleep(10000);
 clock.cancel();
}

- И все?

- Да. Просто и красиво.

6. Задачи на volatile

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

Задачи
1. Заметки для всех

Класс Note будет использоваться нитями.
Поэтому сделай так, чтобы лист notes находился в общей памяти
2. Предложения

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

7. yield - пропуск хода

- Привет, Амиго! У нас сегодня будет небольшой и интересный урок. Я расскажу тебе про yield – статический метод класса Thread.

Элли тебе уже рассказывала, что процессор постоянно переключается между нитями. Каждой нити выделяется небольшой кусочек процессорного времени, называемый квантом. Когда это время истекает – процессор переключается на другую нить и начинает выполнять ее команды. Вызов метода Thread.yield() позволяет досрочно завершить квант времени текущей нити или, другими словами, переключает процессор на следующую нить.

- А зачем нити может понадобиться уступить свое время другой нити?

- Необходимость в этом возникает не часто. Вызов yield приводит к тому, что «наша нить досрочно завершает ход», и что следующая за yield команда начнется с полного кванта времени. Значит шансы, что ее прервут – меньше. Особенно, если она небольшая (по времени). Такой подход можно использовать при оптимизации некоторых процессов.

Еще могу добавить, что метод Thread.sleep(0) работает фактически так же. Думаю, ты вначале будешь использовать метод yield не очень часто, но знать о нем - полезно.

8. Ссылка на вики, synchronized, volatile, yield

- У меня есть для тебя новый материал по критическим секциям и модификатору synchronized. Там довольно много нового и сложного материала. Не стоит сильно вдаваться в детали. Смотри на него пока как на дополнительный взгляд на материал нынешнего уровня. В дальнейшем мы будем глубоко погружаться в эту тему, но пока тебе это не нужно. Почитай вот:

Ссылка на вики, synchronized, volatile, yield

9. Хулио

- Привет, Амиго! Ты сегодня хорошо решал задачи. Садись смотреть видео.

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

10. Домашние и бонусные задания

- Здорово, боец!

- Здравия желаю, товарищ капитан!

- У меня для тебя шикарная новость. Вот тебе задания для закрепления полученных навыков. Выполняй их каждый день, и твои навыки будут расти с неимоверной скоростью. Они специально разработаны для выполнения их в Intellij IDEA.

Дополнительные задания для выполнения в Intellij Idea
1. Общий список

1. Изменить класс Solution так, чтобы он стал списком. (Необходимо реализовать интерфейс java.util.List).
2. Список Solution должен работать только с целыми числами Long.
3. Воспользуйтесь полем original.
4. Список будет использоваться нитями, поэтому позаботьтесь, чтобы все методы были синхронизированы.
2. Comparable

Реализуйте интерфейс Comparable<Beach> в классе Beach, который будет использоваться нитями.
3. Аптека

Реализуй интерфейс Runnable в классах Apteka и Person.
Все нити должны работать пока не isStopped
Логика для Apteka: drugsController должен сделать закупку случайного лекарства (getRandomDrug) в количестве (getRandomCount) и подождать 300 мс
Логика для Person: drugsController должен сделать продажу случайного лекарства (getRandomDrug) в количестве (getRandomCount) и подождать 100 мс
Расставь synchronized там, где это необходимо
4. Синхронизированные методы

Установить модификатор synchronized только тем методам, которым необходимо.
Объект класса Solution будет использоваться нитями.
5. Лишняя синхронизация

synchronized существенно замедляет программу, поэтому убери избыточность synchronized внутри методов
6. Глажка

И снова быт...
Поставьте один synchronized, чтобы diana и igor гладили по-очереди, ведь утюг всего один!

Подсказка: использовать блокировку на уровне класса.
7. ApplicationContext

ApplicationContext будет доступен множеству нитей.
Сделать так, чтобы данные не терялись: подумай, какое ключевое слово необходимо поставить и где.
8. Банкомат

Разберись, как работает программа
Во время тестирования лог содержит следующее:
.....
Добавляем 100, на счету 1100
Добавляем 100, на счету 1200
Тратим 1000, на счету 100
Недостаточно денег
.....

Создан баг: При списании денег со счета теряются деньги
Найти и исправить ошибку
9. Транзакционность

Сделать метод joinData транзакционным, т.е. если произошел сбой, то данные не должны быть изменены.
1. Считать с консоли 2 имени файла
2. Считать построчно данные из файлов. Из первого файла - в allLines, из второго - в forRemoveLines
В методе joinData:
3. Если список allLines содержит все строки из forRemoveLines, то удалить из списка allLines все строки, которые есть в forRemoveLines
4. Если список allLines НЕ содержит каких-либо строк, которые есть в forRemoveLines, то
4.1. выбросить исключение CorruptedDataException
4.2. очистить allLines от данных
Сигнатуру метода main не менять
10. Посчитаем

1. Сделай так, чтобы результат успел посчитаться для всех элементов массива values НЕ используя Thread.sleep
2. Исправь synchronized блок так, чтобы массив values заполнился значением 1

- Те задания были для духов. Для дедушек я добавил бонусные задания повышенной сложности. Только для старослужащих.

1. CRUD

Задача: CrUD - Create, Update, Delete
Программа запускается с одним из следующих наборов параметров:
-c name sex bd
-u id name sex bd
-d id
-i id
Значения параметров:
name - имя, String
sex - пол, "м" или "ж", одна буква
bd - дата рождения в следующем формате 15/04/1990
-с - добавляет человека с заданными параметрами в конец allPeople, выводит id (index) на экран
-u - обновляет данные человека с данным id
-d - производит логическое удаление человека с id
-i - выводит на экран информацию о человеке с id: name sex (м/ж) bd (формат 15-Apr-1990)

id соответствует индексу в списке
Все люди должны храниться в allPeople
Используйте Locale.ENGLISH в качестве второго параметра для SimpleDateFormat

Пример параметров: -c Миронов м 15/04/1990
2. CRUD 2

Задача: CrUD Batch - multiple Creation, Updates, Deletion
Программа запускается с одним из следующих наборов параметров:
-c name1 sex1 bd1 name2 sex2 bd2 ...
-u id1 name1 sex1 bd1 id2 name2 sex2 bd2
-d id1 id2 id3 id4 ...
-i id1 id2 id3 id4 ...
Значения параметров:
name - имя, String
sex - пол, "м" или "ж", одна буква
bd - дата рождения в следующем формате 15/04/1990
-с - добавляет всех людей с заданными параметрами в конец allPeople, выводит id (index) на экран в соответствующем порядке
-u - обновляет соответствующие данные людей с заданными id
-d - производит логическое удаление всех людей с заданными id
-i - выводит на экран информацию о всех людях с заданными id: name sex bd

id соответствует индексу в списке
Формат вывода даты рождения 15-Apr-1990
Все люди должны храниться в allPeople
Порядок вывода данных соответствует вводу данных
Обеспечить корректную работу с данными для множества нитей (чтоб не было затирания данных)
Используйте Locale.ENGLISH в качестве второго параметра для SimpleDateFormat
3. Ресторан

Задача: 1.Разберись, что делает программа. Официант почему-то не относит приготовленные блюда назад к столам :(
2.Исправь ошибку.

Подсказка: это одна строчка