Страницы

Поиск по вопросам

Показаны сообщения с ярлыком синхронизация. Показать все сообщения
Показаны сообщения с ярлыком синхронизация. Показать все сообщения

вторник, 31 марта 2020 г.

Чем std::unique_lock отличается от std::lock_guard?

#cpp #синхронизация #cpp14


Чем std::unique_lock отличается от std::lock_guard? В каких ситуациях лучше применять
один класс, а в каких другой?
    


Ответы

Ответ 1



lock_guard и unique_lock - почти то же самое. Разница в том, что вы можете заблокировать и разблокировать std :: unique_lock. std :: lock_guard будет заблокирован только один раз при построении и разблокирован при уничтожении. lock_guard: когда объект создан, он пытается получить мьютекс (вызывая lock()), а когда объект уничтожен, он автоматически освобождает мьютекс (вызывая unlock()). unique_lock: также поддерживает отложенную блокировку, временную блокировку, рекурсивную блокировку и использование условных переменных. Источник

четверг, 13 февраля 2020 г.

Автоматическая авторизация

#linux #ubuntu #администрирование #синхронизация #rsync


Привет всем!
В общем, есть Ubuntu Server, куда я заливаю файлы. Также установлен и настроен rsync.
Есть Ubuntu-клиент, куда я синхронизирую файлы с сервера.

Ввожу на клиенте команду: 

rsync -urlogt --delete-after user@192.168.56.10:/home/user/public_html
/testsite/upload /home/user/Рабочий\ стол/


запрашивает пароль, и все синхронизируется.

Далее создал файл с паролем /etc/rsyncd.scrt

После чего при вводе команды: 

rsync -urlogt --delete-after --password-file=/etc/rsyncd.scrt
user@192.168.56.10:/home/user/public_html/testsite/upload
/home/user/Рабочий\ стол/backup


выводит сообщение 


  the password-file option may only be used when accessing an rsync
  daemon


Как решить эту проблему?
    


Ответы

Ответ 1



Разобрался. Оказывается в файле конфигурации демона имя модуля [upload] было написано неверно. Использовал команду: rsync -urlogt --delete-after --password-file=/etc/rsyncd.scrt adam@192.168.56.10::upload /home/adam/Рабочий\ стол/upload В итоге все повесил на cron: */1 * * * * root rsync -urlogt --delete-after --password-file=/etc/rsyncd.scrt adam@192.168.56.10::upload /home/adam/Рабочий\ стол/upload

Ответ 2



приведённая команда использует возможности openssh для подключения к 192.168.56.10. чтобы подключаться, используя openssh без ввода пароля, воспользуйтесь аутентификацией по ключу. на машине, где выполняете приведённую команду, сгенерируйте, если ещё нет (проверить можно по наличию/отсутствию файла ~/.ssh/id_rsa), openssh-ключ: $ ssh-keygen -t rsa а затем скопируйте его в учётную запись user@192.168.56.10: $ ssh-copy-id user@192.168.56.10

суббота, 8 февраля 2020 г.

Как правильно сделать синхронизацию?

#java #синхронизация


Практикуюсь работать с синхронизацией потоков и не получается сделать так, как придумал.
У меня есть аккаунт в банке, муж и жена кладут туда деньги. Я хочу сделать так, чтоб
они по очереди это делали. Я делаю это так:

public class Main {
    public static void main(String[] args) {
        new Wife().start();
        new Husband().start();
    }
}

public class Husband extends Thread{
    @Override
    public void run() {
        super.run();
        for (int i = 0; i < 10; i++) {
            System.out.println("Husband " + i + " " +   Account.deposit());
        }
    }
}

public class Wife extends Thread{
    @Override
    public void run() {
        super.run();
        for (int i = 0; i < 10; i++) {
            System.out.println("Wife " + i + " " + Account.deposit());
        }
    }
}

public class Account {
    public synchronized static int deposit(){
        account += 300;
        return account;
    }
}


Но все равно получается бардак. Может вместо synchronized нужно семафоры использовать?
Чтоб была правильная очередность.
    


Ответы

Ответ 1



Сейчас нет возможности обдумать элегантное решение, но я бы приоритеты задавал. Вот решение в лоб: public class App { public static void main(String[] args) { Account account = new Account(0); new Wife(account).start(); new Husband(account).start(); } public static abstract class FamilyMember extends Thread { protected Account mAccount; protected String mId; public FamilyMember(Account account) { mAccount = account; } @Override public void run() { super.run(); startMe(); } protected abstract void startMe(); } public static class Husband extends FamilyMember { public Husband(Account account) { super(account); mId = getClass().getName(); mAccount.registerMember(getClass().getName()); } @Override protected void startMe(){ for (int i = 0; i < 10; i++) { try { while(!mAccount.CheckPriority(mId)) Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Husband " + i + " " + mAccount.deposit(mId)); } } } public static class Wife extends FamilyMember { public Wife(Account account) { super(account); mId = getClass().getName(); mAccount.registerMember(getClass().getName()); } @Override protected void startMe(){ for (int i = 0; i < 10; i++) { try { while(!mAccount.CheckPriority(mId)) Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Wife " + i + " " + mAccount.deposit(mId)); } } } public static class Account { private int mAccount = 0; private List mMembers = new ArrayList(); /** * проверяем имеет ли член семьи максимальный приоритет * @param id * @return */ public synchronized boolean CheckPriority(String id){ Member member = mMembers.stream().max(Comparator.comparingInt(Member::getPriority)).get(); return member.getId().equals(id); } public void registerMember(String id){ mMembers.add(new Member(id, 0)); } public Account(int account) { mAccount = account; } public synchronized int deposit(String id) { mAccount += 300; // понижаем приоритет mMembers.stream().filter(member -> member.mId.equals(id)).findFirst().get().decPriority(); return mAccount; } public static class Member{ public String mId; public int mPriority; public Member(String id, int priority){ mId = id; mPriority = priority; } public void decPriority(){ mPriority--; } public int getPriority(){ return mPriority; } public String getId(){ return mId; } } } } Выведет: Wife 0 300 Husband 0 600 Wife 1 900 Husband 1 1200 Wife 2 1500 Husband 2 1800 Wife 3 2100 Husband 3 2400 Wife 4 2700 Husband 4 3000 Wife 5 3300 Husband 5 3600 Wife 6 3900 Husband 6 4200 Wife 7 4500 Husband 7 4800 Wife 8 5100 Husband 8 5400 Wife 9 5700 Husband 9 6000 Потоки, перед тем как внести деньги на счёт, проверяют свой приоритет с помощью CheckPriority. Если текущий поток имеет больший приоритет, то вносятся деньги mAccount.deposit(mId) и понижается текущий приоритет mMembers.stream().filter(member -> member.mId.equals(id)).findFirst().get().decPriority(); Иначе засыпает на 1мс.

Ответ 2



Пример синхронизации с использованием wait() / notify(): public class Main { public static void main(String[] args) { Account account = new Account(); new Thread(new Wife(account)).start(); new Thread(new Husband(account)).start(); } } public class Account { private int account; public boolean turn; public synchronized int deposit() { turn = !turn; account += 300; return account; } } public abstract class FamilyMember implements Runnable { private final String name; protected final Account account; public FamilyMember(String name, Account account) { this.name = name; this.account = account; } public abstract boolean isMyTurn(); public void run() { for (int i = 0; i < 10; i++) { synchronized (account) { while (!isMyTurn()) { try { account.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(name + " " + i + " " + account.deposit()); account.notify(); } } } } public class Wife extends FamilyMember { public Wife(Account account) { super("Wife", account); } @Override public boolean isMyTurn() { return !account.turn; } } public class Husband extends FamilyMember { public Husband(Account account) { super("Husband", account); } @Override public boolean isMyTurn() { return account.turn; } } В счете храниться флажок turn, который определяет, чья очередь класть деньги. Общий код переместился в класс FamilyMember, муж и жена ждут своей очереди с помощью метода isMyTurn(). Конечно, коряво получилось, но представление об использовании wait() и notify() дает. Это решение похоже на решение Suvitruf'а, только вместо Thread.sleep() для ожидания, пока условие не выполниться, используется wait(), что эффективнее, так как программа крепче спит. Метод notify() уведомляет об изменении условия и пробуждает ожидающий поток. wait() нужно использовать в цикле, потому что поток может проснуться и без вызова notify(). Вместо флага account.turn, наверное, можно использовать значение счета. Если сумма на счете изменилась, то очередь класть деньги переходит к другому. Для этого нужно будет добавить синхронизированный метод Account.get(), который будет возвращать текущую сумму на счете. Муж/жена запоминают сумму на счете, после того как доложили деньги, и ждут, пока сумма изменится.

Ответ 3



Можно и семафоры. Если с synchronized то нужно использовать конструкцию wait()/notify(): муж делает вклад - жена "спит", жена делает вклад - "засыпает" муж.

воскресенье, 2 февраля 2020 г.

Методы wait и notify

#java #многопоточность #синхронизация


В интернете нашел пример использования wait и notify:

public class ThreadsApp {
    public static void main(String[] args) {

        Store store = new Store();
        Producer producer = new Producer(store);
        Consumer consumer = new Consumer(store);
        new Thread(producer).start();
        new Thread(consumer).start();
    }
}

// Класс Магазин, хранящий произведенные товары
class Store {
    private int product = 0;
    private boolean available = false;

    public synchronized void get() {
        while (product < 1) {
            try {
                wait();
            } catch (InterruptedException e) {
            }
        }
        product--;
        System.out.println("Покупатель купил 1 товар");
        System.out.println("Товаров на складе: " + product);
        notify();
    }

    public synchronized void put() {
        while (product >= 3) {
            try {
                wait();
            } catch (InterruptedException e) {
            }
        }
        product++;
        System.out.println("Производитель добавил 1 товар");
        System.out.println("Товаров на складе: " + product);
        notify();
    }
}

// класс Производитель
class Producer implements Runnable {

    Store store;

    Producer(Store store) {
        this.store = store;
    }

    public void run() {
        for (int i = 1; i < 6; i++) {
            store.put();
        }
    }
}

// Класс Потребитель
class Consumer implements Runnable {

    Store store;

    Consumer(Store store) {
        this.store = store;
    }

    public void run() {
        for (int i = 1; i < 6; i++) {
            store.get();
        }
    }
}


Вопрос:


В реализации методов get и put есть метод notify, который будит потоки, спящие на
мониторе store, и это происходит каждый раз при их вызове, т.е. когда поток покупатель
купил  товар и вызвал notify в методе get, поток производитель на этом же мониторе
УЖЕ ПРОСНУЛСЯ И ЖДЕТ ОСВОБОЖДЕНИЯ МОНИТОРА?
Так как вызывается несколько раз notify, потому что вызывается несколько раз get
и put, столько же раз происходит пробуждение потоков на мониторе, получается поток
уже проснулся, и его еще 2 раза будят - как это с точки зрения правильности?

    


Ответы

Ответ 1



notify - будит только 1 поток и не известно какой именно. Если никто не ждет, то notify ничего не делает. Попробую пояснить подробно. Поток А захватывает lock и входит в синхронизированный код. Поток А вызывает wait, засыпает и освобождает lock. Поток Б захватывает lock и входит в синхронизированный код. Поток Б вызывает notify. Возможно (так как не известно какой поток будет разбужен) поток А просыпается становится в очередь на захват lock. Поток Б освобождает lock и выходит из синхронизированного кода. Поток А захватывает lock и продолжает со следующего после wait выражения.

суббота, 4 января 2020 г.

Возможно ли комбинировать несколько способов синхронизации?

#java #многопоточность #синхронизация #concurrency


Здравствуйте.
Изучаю синхронизацию потоков в Java, и, как я понял из книги, существует несколько
способов синхронизации: через ключевое слово synchronized, через использование блокировок
ReentrantLock и через методы wait/notify.


Правильно ли я выделил эти способы?
Эти способы самодостаточны? Можно ли каждый из них использовать, не прибегая к остальным?
Комбинация этих способов невозможна/нежелательна/допустима/желательна?

    


Ответы

Ответ 1



Давайте по пунктам: Нет, не верно. пара методов wait()/notify() работают только в synchronized блоке. Следовательно, они не могут быть выделены в отдельную категорию. Для разграничения доступа к разделяемому ресурсу используют: synchronized блок, или метод. некоторые абстракции, в виде реализации интерфейса java.util.concurrent.Lock, различные семафоры, барьеры и пр. Их типы с избытком представлены в пакете java.util.concurrent. Впрочем, никто не запрещает написать какую нибудь свою реализацию. 2 и 3. Способы самодостаточны и их вполне, можно использовать не смешивая. Но как правило, в многопоточных программах, могут встретиться несколько типов синхронизации. Все сильно зависит от логики и условий использования. Приведу лишь несколько примеров: если к ресурсу обращаются не так часто и не много потоков, то производительнее будет использовать блокировку основанную на CAS'ах, т.к. это позволяет покрутившись в цикле избежав парковки треда, захватить ресурс, пускай и затратив некоторое процессорное время. если у ресурса есть два режима: чтение и запись и относительно мало потоков, которые в него пишут, то используется java.util.concurrent.ReadWriteLock. Это позволяет нескольким потокам-читателям заходить в критические области. Но если появляется писатель, то читатели ждут, когда завершится чтение.

Ответ 2



synchronized, wait, notify и notifyAll работают в связке - это примитивы. ReentrantLock - это высокоуровневая конструкция и реализована она через примитивы. Вы можете сами создать высокоуровневые конструкции из примитивов, но придется хорошо продумать архитектуру. Обычно используют высокоуровневые конструкции, а не приметивы, но вы можете их смешивать, если это необходимо.

среда, 11 декабря 2019 г.

Распределенные демоны

#алгоритм #синхронизация #распараллеливание


Всем привет!

Есть вопрос, какие есть способы реализовать отказоустойчивые демоны, в которых в
один момент времени может работать всего один экземпляр?

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


Каждый час генерировать метрику для проекта
Каждые 10 минут выполнять обновление записей в таблице
Каждые 15 минут удалять записи из таблицы
Каждые 20 минут делать увеличение какого-то числа в определенных записях в таблицах


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

Задачи, являются минимальной сущностью и не требуют распараллеливания. Предположим,
генерация метрики это выполнение десятков запросов и агрегирование результатов по определенным
формулам, но все это происходит за 1-2 секунды. Это можно распараллелить, но в этом
нет необходимости.

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

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

В идеале, я представляю себе решение, когда есть минимум демон в двух экземплярах
на разных серверах, который ставят задачи при наступлении времени в какую-нибудь очередь
типа AMQP, а n-демонов воркеров на других серверах берут их из очереди и выполняют.
Тут есть проблема, как синхронизировать 2 демона, которые ставят эти задачи, чтобы
они не поставили их два раза..



Я так понимаю есть следующие способы синхронизации процессов на разных серверах и
дата-центрах:

1) Синхронизация конкурирующих процессов через какое-то кластерное хранилище, например
Redis. Ставится блокировка, другой демон (или задача) не запускается, пока установлен
флаг блокировки. Читал еще про способы блокировки демонов в кластере RabbitMQ, когда
процесс при запуске ставит эксклюзивный worker с определенным названием на очередь
для подобных действий и другой демон пытается установить свою блокировку, и если не
получается, закрывается до лучших времен.

2) Автоматический запуск другого экземпляра демона или задачи через систему мониторинга
в случае падения основного демона.

3) Явная проверка существования другого демона (задачи) по открытому порту и распределение
весов. В демон прописываем открытый порт у другого демона и смотрим, работает ли он
сейчас и отвечает на запросы? Если нет, забираем корону и выполняем задачи(у). Такой
аналог VRRP.

Есть ли более элегантные способы решения задачи? Или какой из этих способов наиболее
оптимален и чаще всего используется по вашему опыту? 
Может есть какие-то распределенные планировщики? А может есть какие-нибудь книги
по архитектуре подобных демонов?
    


Ответы

Ответ 1



TL; DR: избежать дупликации работы нельзя, поэтому необходимо запускать идемпотентные задачи. Если необходима синхронизация работы, необходимо брать распределенное решение, позволяющее достигать консенсуса, например etcd, consul, riak, cassandra, zookeeper. Redis и RabbitMQ использовать нельзя, потому что они к консенсусу не имеют абсолютно никакого отношения. Вероятнее всего, если вы имеете дело с несколькими узлами, у вас либо кто-то назначает один узел главным (и тогда просто надо выполнять все задачи на нем), либо у вас есть какой-то оркестратор, в котором вероятнее всего есть понятие one-time job и который стоит использовать, чтобы не набивать шишки самостоятельно. Short story long Перед ответом на этот вопрос сначала стоит обозначить, с чем вообще планируется борьба. В распределенных системах есть несколько основных проблем, которые всплывают практически всегда. Здесь упоминается "падение сервера" и "работа двух воркеров над одной задачей одновременно". Первая на самом деле гораздо шире и называется разделением сети (network partition) и обозначает просто то, что связь с узлом распределенной системы потеряна. Это не значит, что узел прекратил работу или прекратил работу насовсем - это может быть перезагрузка сетевого оборудование, сборка мусора в одном из передаточных звеньев или на узле или просто товарищ майор не выдержал просмотра трафика в реалтайме и вышел пока покурить. При этом разделение сети может носить совсем причудливые формы, вроде того, что трафик от А к Б не идет, а наоборот - идет, или что А и Б свободно общаются с В, но друг с другом - никак. Ровно здесь все три предлагаемых варианта отваливаются - если работающий воркер не может продлить блокировку, если сервис мониторинга не может увидеть рабочий процесс, если другой рабочий процесс не может увидеть первый рабочий процесс - это все не значит, что работа не продолжается. И не значит, что она не оборвалась. Это просто состояние неизвестности. Вторая проблема обычно характеризуется как "византийская проблема" - возможность одного из узлов распределенной системы отдавать некорректную информацию, преднамеренно или случайно, конкретно в данном случае это будет означать выдачу двух одинаковых задач (в то время как система не имеет права этого делать). Кроме вышеописанных проблем необходимо еще упомянуть типы доставки сообщений. В идеальном мире существует три режима доставки - at-most-once, exactly-once, at-least-once. К сожалению, процесс приема и подтверждения сообщения - это две операции, которые не могут сложиться в одну атомарную операцию, и из-за этого режим exactly-once технически невозможен даже на холостом ходу: разделение сети всегда может произойти в тот момент, когда узел принял сообщение, но не подтвердил его (при этом не имеет значения, подтвердил он просто прием сообщения, или провел все необходимую обработку над ним - важно то, что система теперь в состоянии коробки Шредингера, обработка либо запущена, либо нет, но об этом нельзя узнать до восстановления сети, а время этого восстановления неизвестно). Поэтому распределенная система может либо доставить сообщение не более одного раза, либо доставлять его до тех пор, пока не получит подтверждение. Здесь уже должно быть видно, что идеальную ситуацию - когда ровно один воркер получает сообщение, отрабатывает его, и успешно завершает его - реализовать технически невозможно. Пока воркер работает исправно, все хорошо; как только он падает в процессе выполнения задачи - система повисла в неконсистентном состоянии. В этот момент никто не знает, была ли выполнена задача (может, воркер просто не смог отправить подтверждение, либо наоборот, рухнул сразу после приема), поэтому остается только либо оставить все как есть, либо отправить задачу на новое выполнение. При этом если с встреча с разделением сети - это не самая частая штука, которую большинство компаний готовы перетерпеть, то выбрасывающий при обработке задачи исключение воркер - это повседневная реальность недоработок, в которой придется залезать внутрь и править состояние системы руками. Но, конечно, решение у этой проблемы есть. Если задача будет идемпотентна и атомарна, либо будет состоять из последовательности атомарных шагов, то ее можно будет запускать произвольное количество раз, не опасаясь за результат. Фактически, это просто следование ACI(D) 2.0. Если же задачу невозможно сделать атомарной - например, если один шаг не может включать в себя менее двух сайд-эффектов (нельзя одновременно отправить письмо и записать подтверждение об отправке в базу - всегда есть вероятность, что одна операция провалится, а вторая нет) - то можно попробовать минимизировать эти последствия. Проще всего это будет пояснить на примере. Можно развить задачу из самого вопроса: пусть сервис в качестве рекламной акции в конце каждого месяца выбирает двадцать случайных пользователей и возвращает им стоимость самой дорогой покупки. Конечно, финансовая сторона проекта будет не в восторге, если внезапно компенсируется в два раза больше, чем планировалось, поэтому необходимо гарантировать однократное выполнение задачи. В этом случае задачу можно разбить на следующие этапы: Выборка двадцати случайных пользователей с историей покупок Сохранение этого результата в базе данных по заранее известному первичному ключу / ключам. Если ключи существуют - работает кто-то другой, прекратить работу, либо работать в параллель. Открытие транзакции на возврат денег для каждого пользователя и запись идентификатора этой транзакции в ту же базу данных. Если идентификатор уже существует - кто-то работает в параллель, отменить транзакцию. Итерация по всем пользователям и закрытие каждой незакрытой транзакции Вне зависимости от того, сколько воркеров работают параллельно, пока они заранее знают нужные ключи в базе данных, они не могут помешать друг другу - ну, до тех пор, пока алгоритм не подвержен византийским ошибкам. Фактически, это реализация примитивной журналируемой системы - если приложение такое позволяет, можно писать идентификаторы транзакций до их создания и просто делить такую задачу на "план" и "исполнение". Как было сказано выше, такое не всегда возможно - например, та же рассылка писем. В этом случае имеет смысл уменьшить количество работы, разбивая пользователей на N шардов, обрабатывая каждый шард как отдельный таск. В этом случае максимальная цена ошибки - размер шарда, отталкиваясь от которого и стоит проектировать задачу. Конечно, для этого всего бывают нужны внешние средства синхронизации. Обычно можно использовать ту же базу данных, в которую пишет приложение - большинство решений, даже распределенных, поддерживают CAS-операции, на основе которых можно построить ограниченную по времени блокировку (пока воркер жив, он постоянно обновляет запись блокировки с указанием нового времени истечения). Самые известные инструменты, которые могут позволить это решить - это Riak, Cassandra, Zookeeper, Consul, Etcd. Что до реплики по поводу других означенных инструментов: RabbitMQ - это сервис очередей, и в то время как он действительно поддерживает работу в режиме кластера и действительно на нем можно реализовать забавную схему блокировки (до тех пор, пока сообщение-синхронизатор не будет проглочено упашим приложением), он на самом деле не дает никаких гарантий по поводу консенсуса и без особых проблем может несколько раз доставить одно и то же сообщение, разработчики прямо предупреждают о split-brain. Кроме того, остается проблема с двумя режимами доставки (at-least-once / at-most-once), которые не могут гарантировать, что сообщение получит ровно один узел, поэтому сам по себе - без атомизации/идемпотентизации задач - он никак делу не поможет. Доставить до воркеров сообщение о том, что задача готова к исполнению - это легко. Redis - это невероятный переоцененный инструмент, получивший свою популярность просто за счет удобного интерфейса, являющийся едва ли не худшим инфраструктурным выбором среди всех своих аналогов. Помимо всех остальных проблем, у него так же прямо прописано в документации There is always a window of time when it is possible to lose writes during partitions, что, в общем, является не единственным способом потерять данные в этой волшебной системе, а потеря данных в данном кейсе ведет к дубликации ответственных за задачу хостов и самой работы. И, наконец, то, с чего стоило начинать ответ. Если вы добрались до распределенных систем, то у вас с большой вероятностью уже есть либо инфраструктура, выбирающая мастер-сервер, либо некоторый оркестратор (e.g. Kubernetes), в котором наверняка есть понятие one-time job в этом случае все проблемы по поводу консенсуса стоит переложить на плечи этих инструментов. В то же время, оркестратор сам будет следить за тем, чтобы процессы ваших воркеров были запущены. Это, конечно, не спасет от невозможности гарантии однократного выполнения задачи, но возведение инфраструктуры просто для запуска задач - тоже тот еще геморрой. Если же вы все-таки хотите управлять ставящими задачи демонами самостоятельно, то вас спасет вышеупомянутая блокировка через consul/etcd/etc. Мне правда надо это все держать в голове перед разработкой приложения? Следует. На самом деле, как и было сказано, то же разделение сети - штука, с которой вы обязательно встретитесь, но не обязательно большой число раз. Если это экономически невыгодно - следует проанализировать вместе с руководителями возможные риски и уменьшить объем работы. Тем не менее, провал фонового обработчика по вине разработчика - практически ежедневная ситуация, поэтому я бы советовал все-таки держаться зеленой дорожки. Если по каким-то причинам не хотите этого делать - можно раздавать задачи через брокер сообщений (RabbitMQ в нормальных условиях должен корректно отрабатывать однократную посылку сообщения, пока держится TCP-коннект к клиенту) и делать дополнительную синхронизацию на выполнение задачи через один из вышеописанных инструментов.

Ответ 2



На мой взгляд вам подойдёт композиция publisher-subscriber, которая есть у большинства систем сообщения, как то Akka, 0mq и др. В вашем случае придётся прописать немного другую логику для основного (primary) рабочего от вторичного (secondary) если система не позволяет создавать механизмы failsafe или broadcasting. Доступность рабочих будет делаться самой системой, вам останется только уведомлять контролирующий процесс о прогрессе работы, на который он будет реагировать, и в случае определённых условий обращаться с запросом работы ко вторичному рабочему.

вторник, 10 декабря 2019 г.

В чем смысл вложенных друг в друга инструкций synchronized?

#java #синхронизация


Встретил вот такой пример (стр 422)

class Test {
    public static void main(String[] args) {
        Test t = new Test();
        synchronized(t) {
            synchronized(t) {
                System.out.printIn("made it!");
            }
        }
    }
}


Далее, видимо, объяснение на мой вопрос, но для меня не вполне очевидное.


  Вывод этой программы имеет вид

made it!

  
  Обратите внимание, что эта программа оказалась бы в состоянии
  взаимоблокировки, если бы один поток не мог многократно блокировать
  монитор.


Инструкция synchronized понятна, захватывает блокировку, выполняет тело инструкции,
освобождает блокировку. 

А в чем смысл вложенных друг в друга инструкций synchronized? Как происходит их (инструкций)
взаимодействие между собой?
    


Ответы

Ответ 1



Данный пример призван показать, что один поток может делать сколько угодно вложенных synchronized и не зависнет при этом. О чем и говорит выведенное сообщение. Если бы это не работало, то программа бы подвисла на втором synchronized пока не освободится t, но так как он может освободиться только выйдя из первого - то получилась бы блокировка самого себя

суббота, 7 декабря 2019 г.

“тонкая синхронизация” в односвязном списке

#c #многопоточность #синхронизация


Добрый день!
Недавно подсмотрела в курсах интересный вариант работы со списками в многопоточной
среде, обозвали эту модель там "тонкой синхронизацией". Суть состоит в том, что во
время работы со списком блокируется доступ только к текущему и предыдущему элементу
списка при любых операциях типа удаления/вставки, и при этом коллизий не возникает.
Кто-нибудь использовал этот способ синхронизации на реальных задачах?
Вот приведенный в курсах пример добавления элемента к связному списку на псевдокоде : 
  Node prev = head; prev.lock();
  Node curr = prev.next; curr.lock();

  try{
    while(curr.key < key){
    prev.unlock(); prev = curr;
    curr = curr.next; curr.lock();
    }
    if(key == curr.key) return false;
    else{
      node = new Node(key,item);
      node.next = curr;
      pred.next = node;
      return true;
    }
  } finally{curr.unlock();pred.unlock();}

Если переложить этот пример на С, то я должна создать список, элементами которого
будут экземпляры некой структуры, содержащей мьютекс. Вот на этом месте у меня возникает
вопрос, как реализовать хотя бы первую строку данного кода без риска поймать segfault
в случае, когда: 

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


Ответы

Ответ 1



Насколько я понимаю, этот код исходит из того, что голова списка фиктивна и неизменна. Это часто используемая техника, позволяющая не выделять специальный случай пустого списка. В таких предположениях код, кажется, верен. Инвариант алгоритма таков: для работы с элементом необходима блокировка как самого Node, так и его предшественника. Если у вас есть блокировка на Node или на его предшественника, ваш Node гарантированно жив. Я бы, однако, не стал использовать обработку исключений для анализа достижения конца списка (тем более, что в данном коде не проверяется head.next), и оставил исключения для исключительных ситуаций.

четверг, 11 апреля 2019 г.

Как правильно сделать синхронизацию?

Практикуюсь работать с синхронизацией потоков и не получается сделать так, как придумал. У меня есть аккаунт в банке, муж и жена кладут туда деньги. Я хочу сделать так, чтоб они по очереди это делали. Я делаю это так:
public class Main { public static void main(String[] args) { new Wife().start(); new Husband().start(); } }
public class Husband extends Thread{ @Override public void run() { super.run(); for (int i = 0; i < 10; i++) { System.out.println("Husband " + i + " " + Account.deposit()); } } }
public class Wife extends Thread{ @Override public void run() { super.run(); for (int i = 0; i < 10; i++) { System.out.println("Wife " + i + " " + Account.deposit()); } } }
public class Account { public synchronized static int deposit(){ account += 300; return account; } }
Но все равно получается бардак. Может вместо synchronized нужно семафоры использовать? Чтоб была правильная очередность.


Ответ

Сейчас нет возможности обдумать элегантное решение, но я бы приоритеты задавал. Вот решение в лоб:
public class App { public static void main(String[] args) { Account account = new Account(0); new Wife(account).start(); new Husband(account).start(); }
public static abstract class FamilyMember extends Thread {
protected Account mAccount; protected String mId;
public FamilyMember(Account account) { mAccount = account; }
@Override public void run() { super.run(); startMe(); }
protected abstract void startMe(); }
public static class Husband extends FamilyMember {
public Husband(Account account) { super(account); mId = getClass().getName(); mAccount.registerMember(getClass().getName()); }
@Override protected void startMe(){ for (int i = 0; i < 10; i++) { try { while(!mAccount.CheckPriority(mId)) Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Husband " + i + " " + mAccount.deposit(mId)); } } }
public static class Wife extends FamilyMember {
public Wife(Account account) { super(account); mId = getClass().getName(); mAccount.registerMember(getClass().getName()); }
@Override protected void startMe(){ for (int i = 0; i < 10; i++) { try { while(!mAccount.CheckPriority(mId)) Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Wife " + i + " " + mAccount.deposit(mId)); } } }
public static class Account {
private int mAccount = 0;
private List mMembers = new ArrayList();
/** * проверяем имеет ли член семьи максимальный приоритет * @param id * @return */ public synchronized boolean CheckPriority(String id){ Member member = mMembers.stream().max(Comparator.comparingInt(Member::getPriority)).get(); return member.getId().equals(id); }
public void registerMember(String id){ mMembers.add(new Member(id, 0)); }
public Account(int account) { mAccount = account; }
public synchronized int deposit(String id) { mAccount += 300; // понижаем приоритет mMembers.stream().filter(member -> member.mId.equals(id)).findFirst().get().decPriority(); return mAccount; }
public static class Member{
public String mId; public int mPriority;
public Member(String id, int priority){ mId = id; mPriority = priority; }
public void decPriority(){ mPriority--; }
public int getPriority(){ return mPriority; }
public String getId(){ return mId; } } } }
Выведет:
Wife 0 300 Husband 0 600 Wife 1 900 Husband 1 1200 Wife 2 1500 Husband 2 1800 Wife 3 2100 Husband 3 2400 Wife 4 2700 Husband 4 3000 Wife 5 3300 Husband 5 3600 Wife 6 3900 Husband 6 4200 Wife 7 4500 Husband 7 4800 Wife 8 5100 Husband 8 5400 Wife 9 5700 Husband 9 6000
Потоки, перед тем как внести деньги на счёт, проверяют свой приоритет с помощью CheckPriority. Если текущий поток имеет больший приоритет, то вносятся деньги mAccount.deposit(mId) и понижается текущий приоритет
mMembers.stream().filter(member -> member.mId.equals(id)).findFirst().get().decPriority();
Иначе засыпает на 1мс.

вторник, 29 января 2019 г.

Возможно ли комбинировать несколько способов синхронизации?

Здравствуйте. Изучаю синхронизацию потоков в Java, и, как я понял из книги, существует несколько способов синхронизации: через ключевое слово synchronized, через использование блокировок ReentrantLock и через методы wait/notify
Правильно ли я выделил эти способы? Эти способы самодостаточны? Можно ли каждый из них использовать, не прибегая к остальным? Комбинация этих способов невозможна/нежелательна/допустима/желательна?


Ответ

Давайте по пунктам:
Нет, не верно. пара методов wait()/notify() работают только в synchronized блоке. Следовательно, они не могут быть выделены в отдельную категорию. Для разграничения доступа к разделяемому ресурсу используют:
synchronized блок, или метод. некоторые абстракции, в виде реализации интерфейса java.util.concurrent.Lock, различные семафоры, барьеры и пр. Их типы с избытком представлены в пакете java.util.concurrent. Впрочем, никто не запрещает написать какую нибудь свою реализацию.
2 и 3. Способы самодостаточны и их вполне, можно использовать не смешивая. Но как правило, в многопоточных программах, могут встретиться несколько типов синхронизации. Все сильно зависит от логики и условий использования. Приведу лишь несколько примеров:
если к ресурсу обращаются не так часто и не много потоков, то производительнее будет использовать блокировку основанную на CAS'ах, т.к. это позволяет покрутившись в цикле избежав парковки треда, захватить ресурс, пускай и затратив некоторое процессорное время. если у ресурса есть два режима: чтение и запись и относительно мало потоков, которые в него пишут, то используется java.util.concurrent.ReadWriteLock. Это позволяет нескольким потокам-читателям заходить в критические области. Но если появляется писатель, то читатели ждут, когда завершится чтение.

вторник, 16 октября 2018 г.

Распределенные демоны

Всем привет!
Есть вопрос, какие есть способы реализовать отказоустойчивые демоны, в которых в один момент времени может работать всего один экземпляр?
Предположим, есть набор задач, который запускается в определенное время:
Каждый час генерировать метрику для проекта Каждые 10 минут выполнять обновление записей в таблице Каждые 15 минут удалять записи из таблицы Каждые 20 минут делать увеличение какого-то числа в определенных записях в таблицах
Это какой-то абстрактный набор задач, который выполняется в фоновом режиме на проекте. Указанными задачи управляет главный поток, который решает когда им запускаться. Можно считать это обычным кроном.
Задачи, являются минимальной сущностью и не требуют распараллеливания. Предположим, генерация метрики это выполнение десятков запросов и агрегирование результатов по определенным формулам, но все это происходит за 1-2 секунды. Это можно распараллелить, но в этом нет необходимости.
Предположим, данные воркеры работают на одном сервере, но вдруг данный сервер может упасть ночью, когда все администраторы и разработчики будут спать. Естественно системы мониторинга начнут всех будить, но хотелось бы не нарушать сон людей и чтобы вычисления моментально перешли на другой сервер, если один сервер упал.
Естественно, задачи не должны запускаться одновременно на двух серверах, так как будут проблемы с согласованностью данных, например при выполнении задачи 4.
В идеале, я представляю себе решение, когда есть минимум демон в двух экземплярах на разных серверах, который ставят задачи при наступлении времени в какую-нибудь очередь типа AMQP, а n-демонов воркеров на других серверах берут их из очереди и выполняют. Тут есть проблема, как синхронизировать 2 демона, которые ставят эти задачи, чтобы они не поставили их два раза..

Я так понимаю есть следующие способы синхронизации процессов на разных серверах и дата-центрах:
1) Синхронизация конкурирующих процессов через какое-то кластерное хранилище, например Redis. Ставится блокировка, другой демон (или задача) не запускается, пока установлен флаг блокировки. Читал еще про способы блокировки демонов в кластере RabbitMQ, когда процесс при запуске ставит эксклюзивный worker с определенным названием на очередь для подобных действий и другой демон пытается установить свою блокировку, и если не получается, закрывается до лучших времен.
2) Автоматический запуск другого экземпляра демона или задачи через систему мониторинга в случае падения основного демона.
3) Явная проверка существования другого демона (задачи) по открытому порту и распределение весов. В демон прописываем открытый порт у другого демона и смотрим, работает ли он сейчас и отвечает на запросы? Если нет, забираем корону и выполняем задачи(у). Такой аналог VRRP.
Есть ли более элегантные способы решения задачи? Или какой из этих способов наиболее оптимален и чаще всего используется по вашему опыту? Может есть какие-то распределенные планировщики? А может есть какие-нибудь книги по архитектуре подобных демонов?


Ответ

TL; DR: избежать дупликации работы нельзя, поэтому необходимо запускать идемпотентные задачи. Если необходима синхронизация работы, необходимо брать распределенное решение, позволяющее достигать консенсуса, например etcd, consul, riak, cassandra, zookeeper. Redis и RabbitMQ использовать нельзя, потому что они к консенсусу не имеют абсолютно никакого отношения. Вероятнее всего, если вы имеете дело с несколькими узлами, у вас либо кто-то назначает один узел главным (и тогда просто надо выполнять все задачи на нем), либо у вас есть какой-то оркестратор, в котором вероятнее всего есть понятие one-time job и который стоит использовать, чтобы не набивать шишки самостоятельно.
Short story long
Перед ответом на этот вопрос сначала стоит обозначить, с чем вообще планируется борьба.
В распределенных системах есть несколько основных проблем, которые всплывают практически всегда. Здесь упоминается "падение сервера" и "работа двух воркеров над одной задачей одновременно". Первая на самом деле гораздо шире и называется разделением сети (network partition) и обозначает просто то, что связь с узлом распределенной системы потеряна. Это не значит, что узел прекратил работу или прекратил работу насовсем - это может быть перезагрузка сетевого оборудование, сборка мусора в одном из передаточных звеньев или на узле или просто товарищ майор не выдержал просмотра трафика в реалтайме и вышел пока покурить. При этом разделение сети может носить совсем причудливые формы, вроде того, что трафик от А к Б не идет, а наоборот - идет, или что А и Б свободно общаются с В, но друг с другом - никак. Ровно здесь все три предлагаемых варианта отваливаются - если работающий воркер не может продлить блокировку, если сервис мониторинга не может увидеть рабочий процесс, если другой рабочий процесс не может увидеть первый рабочий процесс - это все не значит, что работа не продолжается. И не значит, что она не оборвалась. Это просто состояние неизвестности.
Вторая проблема обычно характеризуется как "византийская проблема" - возможность одного из узлов распределенной системы отдавать некорректную информацию, преднамеренно или случайно, конкретно в данном случае это будет означать выдачу двух одинаковых задач (в то время как система не имеет права этого делать).
Кроме вышеописанных проблем необходимо еще упомянуть типы доставки сообщений. В идеальном мире существует три режима доставки - at-most-once, exactly-once, at-least-once. К сожалению, процесс приема и подтверждения сообщения - это две операции, которые не могут сложиться в одну атомарную операцию, и из-за этого режим exactly-once технически невозможен даже на холостом ходу: разделение сети всегда может произойти в тот момент, когда узел принял сообщение, но не подтвердил его (при этом не имеет значения, подтвердил он просто прием сообщения, или провел все необходимую обработку над ним - важно то, что система теперь в состоянии коробки Шредингера, обработка либо запущена, либо нет, но об этом нельзя узнать до восстановления сети, а время этого восстановления неизвестно). Поэтому распределенная система может либо доставить сообщение не более одного раза, либо доставлять его до тех пор, пока не получит подтверждение.
Здесь уже должно быть видно, что идеальную ситуацию - когда ровно один воркер получает сообщение, отрабатывает его, и успешно завершает его - реализовать технически невозможно. Пока воркер работает исправно, все хорошо; как только он падает в процессе выполнения задачи - система повисла в неконсистентном состоянии. В этот момент никто не знает, была ли выполнена задача (может, воркер просто не смог отправить подтверждение, либо наоборот, рухнул сразу после приема), поэтому остается только либо оставить все как есть, либо отправить задачу на новое выполнение. При этом если с встреча с разделением сети - это не самая частая штука, которую большинство компаний готовы перетерпеть, то выбрасывающий при обработке задачи исключение воркер - это повседневная реальность недоработок, в которой придется залезать внутрь и править состояние системы руками.
Но, конечно, решение у этой проблемы есть. Если задача будет идемпотентна и атомарна, либо будет состоять из последовательности атомарных шагов, то ее можно будет запускать произвольное количество раз, не опасаясь за результат. Фактически, это просто следование ACI(D) 2.0. Если же задачу невозможно сделать атомарной - например, если один шаг не может включать в себя менее двух сайд-эффектов (нельзя одновременно отправить письмо и записать подтверждение об отправке в базу - всегда есть вероятность, что одна операция провалится, а вторая нет) - то можно попробовать минимизировать эти последствия.
Проще всего это будет пояснить на примере. Можно развить задачу из самого вопроса: пусть сервис в качестве рекламной акции в конце каждого месяца выбирает двадцать случайных пользователей и возвращает им стоимость самой дорогой покупки. Конечно, финансовая сторона проекта будет не в восторге, если внезапно компенсируется в два раза больше, чем планировалось, поэтому необходимо гарантировать однократное выполнение задачи. В этом случае задачу можно разбить на следующие этапы:
Выборка двадцати случайных пользователей с историей покупок Сохранение этого результата в базе данных по заранее известному первичному ключу / ключам. Если ключи существуют - работает кто-то другой, прекратить работу, либо работать в параллель. Открытие транзакции на возврат денег для каждого пользователя и запись идентификатора этой транзакции в ту же базу данных. Если идентификатор уже существует - кто-то работает в параллель, отменить транзакцию. Итерация по всем пользователям и закрытие каждой незакрытой транзакции
Вне зависимости от того, сколько воркеров работают параллельно, пока они заранее знают нужные ключи в базе данных, они не могут помешать друг другу - ну, до тех пор, пока алгоритм не подвержен византийским ошибкам. Фактически, это реализация примитивной журналируемой системы - если приложение такое позволяет, можно писать идентификаторы транзакций до их создания и просто делить такую задачу на "план" и "исполнение".
Как было сказано выше, такое не всегда возможно - например, та же рассылка писем. В этом случае имеет смысл уменьшить количество работы, разбивая пользователей на N шардов, обрабатывая каждый шард как отдельный таск. В этом случае максимальная цена ошибки - размер шарда, отталкиваясь от которого и стоит проектировать задачу.
Конечно, для этого всего бывают нужны внешние средства синхронизации. Обычно можно использовать ту же базу данных, в которую пишет приложение - большинство решений, даже распределенных, поддерживают CAS-операции, на основе которых можно построить ограниченную по времени блокировку (пока воркер жив, он постоянно обновляет запись блокировки с указанием нового времени истечения). Самые известные инструменты, которые могут позволить это решить - это Riak, Cassandra, Zookeeper, Consul, Etcd. Что до реплики по поводу других означенных инструментов:
RabbitMQ - это сервис очередей, и в то время как он действительно поддерживает работу в режиме кластера и действительно на нем можно реализовать забавную схему блокировки (до тех пор, пока сообщение-синхронизатор не будет проглочено упашим приложением), он на самом деле не дает никаких гарантий по поводу консенсуса и без особых проблем может несколько раз доставить одно и то же сообщение, разработчики прямо предупреждают о split-brain. Кроме того, остается проблема с двумя режимами доставки (at-least-once / at-most-once), которые не могут гарантировать, что сообщение получит ровно один узел, поэтому сам по себе - без атомизации/идемпотентизации задач - он никак делу не поможет. Доставить до воркеров сообщение о том, что задача готова к исполнению - это легко.
Redis - это невероятный переоцененный инструмент, получивший свою популярность просто за счет удобного интерфейса, являющийся едва ли не худшим инфраструктурным выбором среди всех своих аналогов. Помимо всех остальных проблем, у него так же прямо прописано в документации There is always a window of time when it is possible to lose writes during partitions, что, в общем, является не единственным способом потерять данные в этой волшебной системе, а потеря данных в данном кейсе ведет к дубликации ответственных за задачу хостов и самой работы.
И, наконец, то, с чего стоило начинать ответ. Если вы добрались до распределенных систем, то у вас с большой вероятностью уже есть либо инфраструктура, выбирающая мастер-сервер, либо некоторый оркестратор (e.g. Kubernetes), в котором наверняка есть понятие one-time job в этом случае все проблемы по поводу консенсуса стоит переложить на плечи этих инструментов. В то же время, оркестратор сам будет следить за тем, чтобы процессы ваших воркеров были запущены. Это, конечно, не спасет от невозможности гарантии однократного выполнения задачи, но возведение инфраструктуры просто для запуска задач - тоже тот еще геморрой.
Если же вы все-таки хотите управлять ставящими задачи демонами самостоятельно, то вас спасет вышеупомянутая блокировка через consul/etcd/etc.
Мне правда надо это все держать в голове перед разработкой приложения?
Следует. На самом деле, как и было сказано, то же разделение сети - штука, с которой вы обязательно встретитесь, но не обязательно большой число раз. Если это экономически невыгодно - следует проанализировать вместе с руководителями возможные риски и уменьшить объем работы. Тем не менее, провал фонового обработчика по вине разработчика - практически ежедневная ситуация, поэтому я бы советовал все-таки держаться зеленой дорожки. Если по каким-то причинам не хотите этого делать - можно раздавать задачи через брокер сообщений (RabbitMQ в нормальных условиях должен корректно отрабатывать однократную посылку сообщения, пока держится TCP-коннект к клиенту) и делать дополнительную синхронизацию на выполнение задачи через один из вышеописанных инструментов.

понедельник, 15 октября 2018 г.

В чем смысл вложенных друг в друга инструкций synchronized?

Встретил вот такой пример (стр 422)
class Test { public static void main(String[] args) { Test t = new Test(); synchronized(t) { synchronized(t) { System.out.printIn("made it!"); } } } }
Далее, видимо, объяснение на мой вопрос, но для меня не вполне очевидное.
Вывод этой программы имеет вид
made it! Обратите внимание, что эта программа оказалась бы в состоянии взаимоблокировки, если бы один поток не мог многократно блокировать монитор.
Инструкция synchronized понятна, захватывает блокировку, выполняет тело инструкции, освобождает блокировку.
А в чем смысл вложенных друг в друга инструкций synchronized? Как происходит их (инструкций) взаимодействие между собой?


Ответ

Данный пример призван показать, что один поток может делать сколько угодно вложенных synchronized и не зависнет при этом. О чем и говорит выведенное сообщение.
Если бы это не работало, то программа бы подвисла на втором synchronized пока не освободится t, но так как он может освободиться только выйдя из первого - то получилась бы блокировка самого себя

четверг, 11 октября 2018 г.

“тонкая синхронизация” в односвязном списке

Добрый день! Недавно подсмотрела в курсах интересный вариант работы со списками в многопоточной среде, обозвали эту модель там "тонкой синхронизацией". Суть состоит в том, что во время работы со списком блокируется доступ только к текущему и предыдущему элементу списка при любых операциях типа удаления/вставки, и при этом коллизий не возникает. Кто-нибудь использовал этот способ синхронизации на реальных задачах? Вот приведенный в курсах пример добавления элемента к связному списку на псевдокоде : Node prev = head; prev.lock(); Node curr = prev.next; curr.lock();
try{ while(curr.key < key){ prev.unlock(); prev = curr; curr = curr.next; curr.lock(); } if(key == curr.key) return false; else{ node = new Node(key,item); node.next = curr; pred.next = node; return true; } } finally{curr.unlock();pred.unlock();} Если переложить этот пример на С, то я должна создать список, элементами которого будут экземпляры некой структуры, содержащей мьютекс. Вот на этом месте у меня возникает вопрос, как реализовать хотя бы первую строку данного кода без риска поймать segfault в случае, когда: первый поток получил указатель на головной элемент списка, управление получил второй поток, и удалил головной элемент списка, управление вернулось к первому потоку, он пытается захватить мьютекс головного элемента списка, которого уже нет в живых... ?


Ответ

Насколько я понимаю, этот код исходит из того, что голова списка фиктивна и неизменна. Это часто используемая техника, позволяющая не выделять специальный случай пустого списка. В таких предположениях код, кажется, верен. Инвариант алгоритма таков: для работы с элементом необходима блокировка как самого Node, так и его предшественника. Если у вас есть блокировка на Node или на его предшественника, ваш Node гарантированно жив. Я бы, однако, не стал использовать обработку исключений для анализа достижения конца списка (тем более, что в данном коде не проверяется head.next), и оставил исключения для исключительных ситуаций.