
Мартин Буди

Синхронизировано является традиционным механизмом параллельности Java. Хотя сегодня это, вероятно, не то, что мы видим часто, оно все еще питает множество библиотек. Проблема в том, что синхронизация одновременно блокируется и сложна. В этой статье я хотел бы просто проиллюстрировать проблему и свои соображения по поводу перехода на Akka Actor для лучшего и более легкого параллельного использования.
Рассмотрим этот простой код:
int x;
if (x > 0) {
return true;
} else {
return false;
}
Итак, верните true if x является положительным. Просто.
Далее рассмотрим этот еще более простой код:
x++;
Да, счетчик. Очень просто, верно?
Однако все эти коды могут потрясающе взорваться в многопоточной среде.
В первом примере значение true либо false не определяется значением x. Фактически это определяется посредством теста if. Итак, если другой поток сменит x на отрицательное сразу после того, как первый поток прошел тест if, мы все равно получим истину, даже если x больше не положительно.
Второй пример достаточно обманчив. Хотя это только одна строка, на самом деле есть три операции: чтение x, увеличивая его и возвращая обновленное значение. Если два потока запускаются одновременно, обновление может быть утрачено.
Когда у нас есть разные потоки, которые одновременно обращаются к переменной и меняют ее, у нас есть условие гонки. Если мы просто хотим построить счетчик, Java предоставляет потокобезопасные атомные переменные, в том числе атомные цели, которые мы можем использовать для этой цели. Однако Atomic Integer работает только с отдельными переменными. Как сделать несколько операций атомарными?
С помощью синхронизированные блокировать. Сначала рассмотрим более подробный пример.
int x;
public int withdraw(int deduct){
int balance = x - deduct;
if (balance > 0) {
x = balance;
return deduct;
} else {
return 0;
}
}
Это очень простой способ снятия наличных денег. Это тоже бывает опасно. Выполняемые одновременно два потока могут привести к тому, что банк выдаст два снятия средств, даже если баланса больше не хватает. Теперь давайте посмотрим, как это работает с синхронизированным блоком:
volatile int x;
public int withdraw(int deduct){
synchronized(this){
int balance = x - deduct;
if (balance > 0) {
x = balance;
return deduct;
} else {
return 0;
}
}
}
Идея синхронизированного блока проста. Один поток входит в него и блокирует его, в то время как другие потоки ждут снаружи. В нашем случае замок – это объект это. После этого блокировка освобождается и передается другому потоку, который затем выполняет то же самое. Также обратите внимание на ключевое слово эзотерика непостоянный который необходим, чтобы поток не использовал локальный кэш ЦП x
Теперь, когда потоки распутаны, банк не будет случайно выдавать незаполненные средства. Однако эта структура, как правило, усложняется с большим количеством блоков и блоков. Работа с несколькими замками особенно рискованна. Блоки могут ненамеренно удерживать ключ друг для друга и в конечном итоге заблокировать всю программу. Кроме того, у нас есть проблема эффективности. Помните, что пока поток работает внутри, все остальные потоки ждут снаружи. И ожидают потоки хорошо… ждут. Они не делают ничего другого, кроме как ждут.
Так что вместо того, чтобы делать такой механизм, почему бы просто не оставить работу в очереди? Чтобы лучше это представить, представьте систему электронной почты. При отправке электронного письма вы бросаете письмо в почтовый ящик получателя. Вы не ждете, пока человек это прочтет.
Это основы модели Actor и фреймворка Akka в целом.
Актер инкапсулирует состояние и поведение. В отличие от инкапсуляции ООП, актеры вообще не разоблачают свое состояние и поведение. Единственный способ для актеров общаться друг с другом – обмен сообщениями. Входящие сообщения сбрасываются в почтовый ящик и перевариваются в порядке «первым пришел – первым вышел». Вот переделанный образец в Akka и Scala.
case class Withdraw(deduct: Int)
class ReplicaActor extends Actor {
var x = 10;
def receive: Receive = {
case Withdraw(deduct) => val r = withdraw(deduct)
}
}
class BossActor extends Actor {
var replica = context.actorOf(Props[ReplicaActor])
replica ! Withdraw(6)
replica ! Withdraw(9)
}
У нас есть ReplicaActor, который выполняет работу, и BossActor, упорядочивающий реплику. Во-первых, обратите внимание на ! знак или рассказать. Это один из двух методов (другой спроси), чтобы актер асинхронно посылал сообщение другому актеру. рассказать в частности, делает это, не дожидаясь ответа. Поэтому босс говорит реплике выполнить два приказа об отзыве и немедленно идет. Эти сообщения поступают в реплику получать где каждый из них появляется и совпадает с подходящим обработчиком. В этом случае, Отозвать выполняет отзывать метод из предыдущего примера и вычитает запрошенную сумму из состояния x. После этого актер переходит к следующему сообщению в очереди.
Итак, что мы здесь получаем? Во-первых, нам больше не нужно беспокоиться о блокировании и работе с атомарными/конкурентными типами. Механизм инкапсуляции и очереди актера уже обеспечивает безопасность потоков. И больше не нужно ждать, поскольку потоки просто сбрасывают сообщения и возвращаются. Результаты можно предоставить позже с помощью спроси или рассказать. Это просто и разумно.
Akka основана на JVM и доступна как в Scala, так и в Java. Хотя эта статья не обсуждает Java против Scala, согласование шаблонов и функциональное программирование Scala были бы очень полезны для управления сообщениями данных Actor. По крайней мере, это может помочь вам писать более короткий код, избегая скобок и точки с запятой Java.