25

加载中......

 4 years ago
source link: https://code.zhidu.tech/code/reader/index.html?postId=42
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

使用Akka Actor来替换Java的Synchronized同步代码

2020年6月7日

synchronized

主要语言:Java

爱编程,爱技术

Synchronized是Java非常常见的一种并发机制。哪怕我们不见得会直接用到,synchronzied仍在很多的公用库里会用到。使用Synchronized会有一些,其中的一个问题是,Synchronized是一种阻塞操作,带来了复杂性。本文将通过一种简单的方式来说明此问题,并说明选择Akka Actor获得更好、更易维护的并发性代码的理由。
考虑以下样例代码:
int x; if (x > 0) { return true; } else { return false; }
如果x为正数,则返回true 。很简单的一段代码。再考虑一下下面的计算器的代码:
以上代码都很简单,但如果在多线程环境下,代码可能会有很大的问题。
在第一个示例中,true或false不是由x的值确定的,而是由if判断确定。因此,如果在第一个线程通过if判断之后另一个线程将x更改为负数,即使x不再为正数,我们仍然会变为true。
第二个例子很具有欺骗性。尽管只是一行代码,但实际上有三个操作:读取x,对其进行递增并返回更新后的值。如果两个线程恰好同时运行,则更新的值可能会丢失。
当不同的线程同时访问和修改同一个变量的时候,就出现了竞争条件。如果我们仅只是想构建一个计数器,则Java提供了线程安全的Atomic变量,其中包括Atomic Integer,我们可以将其用于此目的。但是,Atomic仅适用于单个变量。如何使多个操作原子化?
第一反应是通过使用Synchronized块。看一个更详细的例子:
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; } } }
同步块的想法很简单。一个线程进入并锁定,其它线程必须等待。锁是一个对象,在我们的例子中是this。进入同步块的代码执行完成后,将锁释放并传递给另一个线程,然后该线程将执行相同的操作。另,请注意需要使用关键字volatile,以防止线程使用变量x的本地CPU缓存。
加入同步块后,哪怕在多线程复杂的执行环境下,银行也不会意外提供多次提款。但是,当有越来越多的同步块和并发锁存在的时候,这种代码结构往往带来复杂的代码逻辑,而处理多个同步锁的过程也容易引发错误。多个同步块可能会在不经意间互相持有同步锁并锁定整个应用。还有一个非常重要的情况是,Synchronized同步块在很多情况下存在效率问题:当一个线程运行的时候,所有其它线程都需要等待。
和上面的同步块类似的是队列,可以考虑使用队列来实现相同的功能。想象一个电子邮件系统,发送电子邮件时,会将电子邮件拖放到收件人的邮箱中,不必等到接收方阅读就直接返回。Actor模型和Akka框架就是基于此。
Actor封装状态和行为。但是,与OOP的封装不同,actor根本不公开其状态和行为。actor相互交流的唯一方法是交换消息。传入邮件将被放入邮箱中,并按照先进先出的顺序进行进行处理。
下面代码是Akka和Scala中重写的示例:
case class Withdraw(deduct: Int) class SlaveActor extends Actor { var x = 10; def receive: Receive = { case Withdraw(deduct) => val r = withdraw(deduct) } } class BossActor extends Actor { var slave = context.actorOf(Props[SlaveActor]) slave ! Withdraw(6) slave ! Withdraw(9) }
SlaveActor负责具体的工作,而BossActor负责向SlaveActor发送命令。sign(tell)是一个参与者将消息异步发送到另一个参与者的两种方法之一(另一种是ask)。tell在执行时不等待答复。因此,BossActor告诉SlaveActor做两次撤单。这些消息到达slave的接收器,其中的每个消息都会被相应的处理程序处理。这种情况下,Withdraw执行withdraw执行金额扣除操作。操作完成后,将前行到队列中的下一条消息。
以上代码改动带来了什么优势?首先,不需要担心锁和使用原子/并发类型带来的线程安全性问题。Actor的封装和排队机制已经保证了线程安全性。线程只是发送消息就返回,也不需要再等待。结果稍后通过Ask或Tell传递,模型很简单,也很有效。
Akka基于JVM,在Scala和Java中均可使用。本文并未对Java与Scala语言进行对比,但Scala的模式匹配和函数式编程在管理Actor的数据消息传递方面很有用,可以避免Java的方括号和分号,可以编写出较短、但同样有效的代码。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK