共计 4658 个字符,预计需要花费 12 分钟才能阅读完成。
作者: 雅各布·詹科夫
原文: http://tutorials.jenkov.com/j…
翻译: 潘深练 如您有更好的翻译版本,欢送 ❤️ 提交 issue 或投稿哦~
更新: 2022-02-24
CAS
(compare and swap) 是并发算法设计时应用的一种技术。基本上,CAS
是将变量的值与期望值进行比拟,如果值相等,则将变量的值替换设置为新值。CAS
可能听起来有点简单,但一旦你了解它实际上相当简略,所以让我进一步具体阐明这个主题。
顺便说一句,compare and swap 有时是 CAS
的缩写,所以如果你看到一些对于并发的文章或视频提到 CAS
,它很有可能是指 compare and swap(比拟并替换)操作。
CAS 教程视频
如果您喜爱视频,我在这里有这个 CAS
的视频教程版本:(绿色上网)
CAS 视频教程
CAS 的应用场景(Check Then Act)
并发算法中常见的模式是先查看后执行(check then act
)模式。当代码首先查看变量的值而后依据该值进行操作时,就会呈现先查看后执行(check then act
)模式。这是一个简略的例子:
public class ProblematicLock {
private volatile boolean locked = false;
public void lock() {while(this.locked) {// 忙期待 - 直到 this.locked == false}
this.locked = true;
}
public void unlock() {this.locked = false;}
}
此代码不是多线程锁的 100%
正确实现。这就是我给它命名的起因 ProblematicLock
(问题锁)。然而,我创立了这个谬误的实现来阐明如何通过 CAS
性能来解决它的问题。
该 lock()
办法首先查看成员变量是否 locked
等于 false
。这是在while-loop
外部实现的。如果 locked
变量是 false
,则该lock()
办法来到 while
循环并设置 locked
为true
。换句话说,该 lock()
办法首先查看变量的值locked
,而后依据该查看进行操作。先查看,再执行。
如果多个线程简直同时刻拜访同一个 ProblematicLock
实例,那以上的 lock()
办法将会有一些问题,例如:
如果线程 A 查看 locked
的值为 false
(预期值),它将退出 while-loop
循环执行后续的逻辑。如果此时有个线程 B 在线程 A 将 locked
值设置为 true
之前也查看了 locked
的值,那么线程 B 也将退出 while-loop
循环执行后续的逻辑。这是一个典型的资源竞争问题。
先查看后执行(Check Then Act)必须是原子性的
为了在多线程应用程序中失常工作(以防止资源竞争),先查看后执行(Check Then Act
)必须是原子性的。原子性的意思是检查和执行动作都作为原子(不可分割的)代码块执行。任何开始执行该块的线程都将实现该块的执行,而不受其余线程的烦扰。不容许其余线程在同一时刻执行雷同原子块。
使 Java
代码块具备原子性的一种简略办法是应用 Java
的synchronized
关键字对其进行标记。能够参阅 对于 synchronized 的内容。这是 ProblematicLock
之前应用 synchronized
关键字将 lock()
办法转换为原子代码块的办法:
public class MyLock {
private volatile boolean locked = false;
public synchronized void lock() {while(this.locked) {// 忙期待 - 直到 this.locked == false}
this.locked = true;
}
public void unlock() {this.locked = false;}
}
当初办法 lock()
已申明同步,因而同一实例的 lock()
办法在同一时刻只容许被一个线程拜访执行。相当于 lock()
办法是原子性的。
阻塞线程的代价很大
当两个线程试图同时进入 Java
中的一个同步块时,其中一个线程将被阻塞,而另一个线程将被容许进入同步块。当进入同步块的线程再次退出该块时,期待中的线程才会被容许进入该块。
如果线程被容许拜访执行,那么进入一段同步代码块的代价并不大。然而如果因为已有一个线程在同步块中执行导致另一个线程被迫等阻塞,那么这个阻塞线程的代价就很大。
此外,当同步块再次闲暇时,您无奈精确地确定何时能解除阻塞的线程 。这通常取决于 操作系统
或执行平台
来 协调 阻塞线程的 阻塞解除。当然,在阻塞线程被解除阻塞并容许进入之前不会破费几秒钟或几分钟,然而可能会节约一些工夫用于阻塞线程,因为它原本能够访问共享数据结构的。这在此处进行了阐明:
硬件提供的原子性 CAS 操作
古代 CPU
内置了对 CAS
的原子性操作的反对。在某些状况下,能够应用 CAS
操作来代替同步块或其余阻塞数据结构。CPU
保障一次只有一个线程能够执行 CAS
操作,即便跨 CPU
内核也是如此。稍后在代码中有示例。
当应用硬件或 CPU
提供的 CAS
性能而不是操作系统或执行平台提供的 synchronized
、lock
、mutex
(互斥锁)等时,操作系统或执行平台不须要解决线程的阻塞和解除阻塞。这使得应用 CAS
的线程期待执行操作的工夫更短,并且领有更少的拥塞和更高的吞吐量。如下图所示:
如您所见,试图进入共享数据结构的线程永远不会被齐全阻塞。它一直尝试执行 CAS
操作,直到胜利,并被容许访问共享数据结构。这样线程能够进入共享数据结构之前的提早被最小化。
当然,如果线程在反复执行 CAS
的过程中期待很长时间,可能会节约大量的 CPU
周期,而这些 CPU
周期原本能够用在其余工作(其余线程)上。但在许多状况下,状况并非如此。这取决于共享数据结构被另一个线程应用多长时间。实际上,共享数据结构的应用工夫不长,因而上述情况不应该常常产生。但同样这取决于具体情况、代码、数据结构、尝试拜访数据结构的线程数、零碎负载等。相同,阻塞的线程基本不应用CPU
。
Java 中的 CAS
从 Java 5
开始,您能够通过 java.util.concurrent.atomic
包中的一些新的原子类拜访 CPU
级别的 CAS
办法。这些类有:
- AtomicBoolean
- AtomicInteger
- AtomicLong
- AtomicReference
- AtomicStampedReference
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
应用 Java 5+
附带的 CAS
性能而不是本人实现的劣势在于,Java 5+
中内置的 CAS
性能容许您的应用程序利用 CPU
的底层能力执行 CAS
操作。这使您的 CAS
实现代码更快。
CAS 的保障性
CAS
性能可用于爱护临界区(Critical Section
),从而避免多个线程同时执行临界区。
?> critical section 是每个线程中拜访临界资源的那段代码,不论是硬件临界资源,还是软件临界资源,多个线程必须互斥地对它进行拜访。每个线程中拜访临界资源的那段代码称为临界区(Critical Section
)。每个线程中拜访临界资源的那段程序称为临界区(Critical Section
)(临界资源是一次仅容许一个线程应用的 共享资源)。每次只准许一个线程进入临界区,进入后不容许其余线程进入。
上面的一个示例,展现了如何应用 AtomicBoolean
类的 CAS
性能来实现后面显示的 lock()
办法并因而起到保障作用(一次只有一个线程能够退出该 lock()
办法)。
public class CompareAndSwapLock {private AtomicBoolean locked = new AtomicBoolean(false);
public void unlock() {this.locked.set(false);
}
public void lock() {while(!this.locked.compareAndSet(false, true)) {// busy wait - until compareAndSet() succeeds
}
}
}
留神这个 locked
变量不再是一个布尔类型而是 AtomicBoolean
类型,此类有一个 compareAndSet()
办法,会把实例的值(变量 locked)与第一个参数(false
)进行比拟,如果比拟后果雷同(即 locked 的值等于第一个参数 false),那么会将实例的值 locked
与期望值 true
替换(即把 locked 变量设置为 true,示意锁住了)。如果替换胜利则 compareAndSet()
办法会返回 true
,如果没有替换胜利则返回 false
。
在下面的例子中,compareAndSet()
办法调用比拟了 locked
变量值与 false
值,如果 locked
变量值的后果值就是 false
,那么就是设置locked
值为true
。
因为一次只能容许一个线程执行该 compareAndSet()
办法,因而只有一个线程可能看到 AtomicBoolean 实例值为 false
,从而将其替换为true
。因而,每次只有一个线程可能退出while-loop
(while 循环),通过调用 unlock()
办法设置 locked
为 false
使得每次只有一个线程的 CompareAndSwapLock
是解锁状态的。
CAS 实现乐观锁
也能够应用 CAS
性能作为乐观锁机制。乐观锁机制容许多个线程同时进入临界区,但只容许其中一个线程在临界区完结时提交其工作。
上面是一个应用乐观锁策略的并发计数器类示例:
public class OptimisticLockCounter{private AtomicLong count = new AtomicLong();
public void inc() {
boolean incSuccessful = false;
while(!incSuccessful) {long value = this.count.get();
long newValue = value + 1;
incSuccessful = this.count.compareAndSet(value, newValue);
}
}
public long getCount() {return this.count.get();
}
}
请留神 inc()
办法是如何从 AtomicLong
实例变量 count
中获取现有计数值的。而后依据旧值计算出新值。最初,inc()
办法尝试通过调用 AtomicLong
实例的 compareAndSet()
办法来设置新值。
如果 AtomicLong
实例值 count
在比拟时依然领有与上次获取时(long value = this.count.get()
)的值雷同,那么 compareAndSet()
会执行胜利。然而如果有另一个线程在同一时刻曾经调用减少了 AtomicLong
实例值(指有一个线程在之前曾经调用胜利 compareAndSet()
办法了,个别认为是 资源竞争 ),则compareAndSet()
调用将失败,因为预期值 value
不再等于存储在中的值 AtomicLong
(原值曾经被前一个线程更改过)。在这种状况下,inc()
办法将在 while-loop
(while 循环)中进行另外一次迭代并尝试再次减少 AtomicLong
值。
(本篇完)
原文: http://tutorials.jenkov.com/j…
翻译: 潘深练 如您有更好的翻译版本,欢送 ❤️ 提交 issue 或投稿哦~