这里有新鲜出炉的 Java 函数式编程,程序狗速度看过来!
Java 程序设计语言
java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的 Java 程序设计语言和 Java 平台(即 JavaEE(j2ee), JavaME(j2me), JavaSE(j2se))的总称.
下面小编就为大家带来一篇全面了解 Java 中的 CAS 机制.小编觉得挺不错的,现在就分享给大家,也给大家做个参考.一起跟随小编过来看看吧
前言
在看到 Java 锁机制的时候,无意中看到了 CAS 这个词,然后在百度查找 CAS 看了很多文章始终没有看的太懂,今天又在 Google 上查找了一些资料,才算是真正弄清楚了 CAS 机制.
什么是 CAS
在 jdk 1.5 中增加的一个最主要的支持是 Atomic 类,比如说 AtomicInteger, AtomicLong,这些类可帮助最大限度地减少在多线程中对于一些基本操作(例如,增加或减少多个线程之间共享的值)的复杂性.而这些类的实现都依赖于 CAS(compare and swap)的算法.
乐观锁和悲观锁
cpu 是时分复用的,也就是把 cpu 的时间片,分配给不同的 thread/process 轮流执行,时间片与时间片之间,需要进行 cpu 切换,也就是会发生进程的切换.切换涉及到清空寄存器,缓存数据.然后重新加载新的 thread 所需数据.当一个线程被挂起时,加入到阻塞队列,在一定的时间或条件下,在通过 notify(),notifyAll() 唤醒回来.在某个资源不可用的时候,就将 cpu 让出,把当前等待线程切换为阻塞状态.等到资源 (比如一个共享数据)可用了,那么就将线程唤醒,让他进入 runnable 状态等待 cpu 调度.这就是典型的悲观锁的实现.独占锁是一种悲观锁,synchronized 就是一种独占锁,它假设最坏的情况,并且只有在确保其它线程不会造成干扰的情况下执行,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁.
但是,由于在进程挂起和恢复执行过程中存在着很大的开销.当一个线程正在等待锁时,它不能做任何事,所以悲观锁有很大的缺点.举个例子,如果一个线程需要某个资源,但是这个资源的占用时间很短,当线程第一次抢占这个资源时,可能这个资源被占用,如果此时挂起这个线程,可能立刻就发现资源可用,然后又需要花费很长的时间重新抢占锁,时间代价就会非常的高.
乐观锁思路就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止.某个线程可以不让出 cpu, 而是一直 while 循环,如果失败就重试,直到成功为止.所以,当数据争用不严重时,乐观锁效果更好.比如 CAS 就是一种乐观锁思想的应用.
CAS(Compare and Swap )算法 CAS 中有三个核心参数:
1. 主内存中存放的 V 值,所有线程共享.
2. 线程上次从内存中读取的 V 值 A 存放在线程的帧栈中,每个线程私有.
3. 需要写入内存中并改写 V 值的 B 值.也就是线程对 A 值操作后放入到主存 V 中.
上面说的比较抽象,看下面的这幅图比较容易理解.
如上图中,主存中保存 V 值,线程中要使用 V 值要先从主存中读取 V 值到线程的工作内存 A 中,然后计算后变成 B 值,最后再把 B 值写回到内存 V 值中.多个线程共用 V 值都是如此操作.CAS 的核心是在将 B 值写入到 V 之前要比较 A 值和 V 值是否相同,如果不相同证明此时 V 值已经被其他线程改变,重新将 V 值赋给 A,并重新计算得到 B,如果相同,则将 B 值赋给 V.
如果不使用 CAS 机制,看看存在什么问题,假如 V=1,现在 Thread1 要对 V 进行加 1,Thread2 也要对 V 进行加 1,首先 Thread1 读取 V=1 到自己工作内存 A 中此时 A=1,假设 Thread2 此时也读取 V=1 到自己的工作内存 A 中,分别进行加 1 操作后,两个线程中 B 的值都为 2,此时写回到 V 中时发现 V 的值为 2,但是两个线程分别对 V 进行加处理结果却只加了 1 有问题.
CAS 核心代码
上面的操作是原子操作,现在来看看如果两个线程同时要对 V 进行加 1 操作使用上面的 CAS 机制后能不能获得正确结果.
if (A==V)
{
V = B
return B;
}
else
return V;
①Thread 1 和 Thread2 要对 V 进行加 1,Thread1 和 Thread2 同时读取 v 值并且对 V 执行加 1 操作.
初始值 v=1,A=0, B=0.
②假设 Thread1,Thread 2 先读取 V 值赋给 A,并且对 A 进行加 1,得到 B=2.
V=1,T1_A=1,T1_B=2;T2_A=1
Thread1 要将 T1_B 写入 V 中,先要执行 CAS 操作:
因为 T1_A=1=V,所以执行 V=T1_B=2,此时 V=2.
if (T1_A==V)
{
V = T1_B
return T1_B;
}
else
return V;
③Thread2 也要对 V 执行加操作.执行加操作之后
V=2 ,T2_A=1,T2_B=2,
当 Thread2 要将 T2_B 值写要 V 中之前要执行 CAS 操作,
此时 T2_A=1,V=2, T2_A!=V, 这时候返回 V=2,给 T2_A,T2_A=V=2,然后再次对 T2_A 进行加 1,得到 T2_B,此时 T2_B=3,T2_A=2,比较 T2_A==V,所以将 T2_B 的值赋给 T2_V,T2_V=T2_B=3.最后结果为 3.正确.
if (T2_A==V)
{
V = T2_B
return T2_B;
}
else
return V;
CAS 中的 ABA 问题
如果一开始位置 V 得到的旧值是 A,当进行赋值操作时再次读取发现仍然是 A,并不能说明变量没有被其它线程改变过.有可能是其它线程将变量改为了 B,后来又改回了 A.大部分情况下 ABA 问题不会影响程序并发的正确性,如果要解决 ABA 问题,用传统的互斥同步可能比原子类更高效.
ABA 问题的解决办法
1. 在变量前面追加版本号:每次变量更新就把版本号加 1,则 A-B-A 就变成 1A-2B-3A.
2.atomic 包下的 AtomicStampedReference 类:其 compareAndSet 方法首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用的该标志的值设置为给定的更新值.
以上这篇全面了解 Java 中的 CAS 机制就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持 PHPERZ.
来源: http://www.phperz.com/article/18/0118/353130.html