码迷,mamicode.com
首页 > 编程语言 > 详细

CASJAVA一些理解

时间:2018-03-19 13:31:14      阅读:283      评论:0      收藏:0      [点我收藏+]

标签:安全   JD   native   记录   nbsp   ++i   nal   情况   asc   

如果不用锁机制如何实现共享数据访问。(不要用锁,不要 用sychronized  块或者方法,也不要直接使用 jdk  提供的线程安全
的数据结构,需要自己实现一个类来保证多个线程同时读写这个类中的共享数据是线程安全的,怎么 办 ?)

 

 

 

无锁化编程的常用方法 :件 硬件 CPU  同步原语 CAS(Compare a
nd Swap),如无锁栈,无锁队列(ConcurrentLinkedQueue)等等。现在
几乎所有的 CPU 指令都支持 CAS 的原子操作,X86 下对应的是 CMPXCHG 汇
编指令,处理器执行 CMPXCHG 指令是一个原子性操作。有了这个原子操作,
我们就可以用其来实现各种无锁(lock free)的数据结构。
CAS 实现了区别于 sychronized 同步锁的一种乐观锁,当多个线程尝试使
用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线
程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再
次尝试。CAS 有 3 个操作数,内存值 V,旧的预期值 A,要修改后的新值 B。
当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则什么都不做。
其实 CAS 也算是有锁操作,只不过是由 CPU 来触发,比 synchronized 性能
好的多。CAS 的关键点在于,系统 在硬件层面保证了比较并交换操作的原子性,
处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操
作。CAS 是非阻塞算法的一种常见实现。

 

CAS 是非阻塞算法的一种常见实现。
一个线程间共享的变量,首先在主存中会保留一份,然后每个线程的工作
内存也会保留一份副本。这里说的预期值,就是线程保留的副本。当该线程从
主存中获取该变量的值后,主存中该变量可能已经被其他线程刷新了,但是该
线程工作内存中该变量却还是原来的值,这就是所谓的预期值了。当你要用 CAS
刷新该值的时候,如果发现线程工作内存和主存中不一致了,就会失败,如果
一致,就可以更新成功。
Atomic  包提供了一系列原子类。这些类可以保证多线程环境下,当某个
线程在执行 atomic 的方法时,不会被其他线程打断,而别的线程就像自旋锁一
样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个线程执行。
Atomic 类在软件层面上是非阻塞的,它的原子性其实是在硬件层面上借助相关
的指令来保证的。
AtomicInteger 是一个支持原子操作的 Integer 类,就是保证对
AtomicInteger 类型变量的增加和减少操作是原子性的,不会出现多个线程下
的数据不一致问题。如果不使用 AtomicInteger,要实现一个按顺序获取的

ID,就必须在每次获取时进行加锁操作,以避免出现并发时获取到同样的 ID
的现象。Java 并发库中的 AtomicXXX 类均是基于这个原语的实现,拿出
AtomicInteger 来研究在没有锁的情况下是如何做到数据正确性的:
来看看++i 是怎么做到的。
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
在这里采用了 CAS 操作,每次从内存中读取数据然后将此数据和+1 后的
结果进行 CAS 操作,如果成功就返回结果,否则重试直到成功为止。
而 compareAndSet 利用 JNI 来完成 CPU 指令的操作,非阻塞算法。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect,
update);
}
其中,unsafe.compareAndSwapInt()是一个 native 方法,正是调用
CAS 原语完成该操作。
首先假设有一个变量 i,i 的初始值为 0。每个线程都对 i 进行+1 操作。CAS
是这样保证同步的:
假设有两个线程,线程 1 读取内存中的值为 0,current = 0,next = 1,然
后挂起,然后线程 2 对 i 进行操作,将 i 的值变成了 1。线程 2 执行完,回到线
程 1,进入 if 里的 compareAndSet 方法,该方法进行的操作的逻辑是,(1)
如果操作数的值在内存中没有被修改,返回 true,然后 compareAndSet 方法
返回 next 的值(2)如果操作数的值在内存中被修改了,则返回 false,重新
进入下一次循环,重新得到 current 的值为 1,next 的值为 2,然后再比较,
由于这次没有被修改,所以直接返回 2。
那么,为什么自增操作要通过 CAS 来完成呢?仔细观察
incrementAndGet()方法,发现自增操作其实拆成了两步完成的:
int current = get();
int next = current + 1;
由于 volatile 只能保证读取或写入的是最新值,那么可能出现以下情况:
1.A 线程执行 get()操作,获取 current 值(假设为 1)

2.B 线程执行 get()操作,获取 current 值(为 1)
3.B 线程执行 next = current + 1 操作,next = 2
4.A 线程执行 next = current + 1 操作,next = 2
这样的结果明显不是我们想要的,所以,自增操作必须采用 CAS 来完成。
CAS  的优缺点
CAS 由于是在硬件层面保证的原子性,不会锁住当前线程,它的效
率是很高的。
CAS 虽然很高效的实现了原子操作,但是它依然存在三个问题。
1、ABA 问题。CAS 在操作值的时候检查值是否已经变化,没有变化的情况下
才会进行更新。但是如果一个值原来是 A,变成 B,又变成 A,那么 CAS 进行
检查时会认为这个值没有变化,操作成功。ABA 问题的解决方法是使用版本号。
在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么 A-B-A
就变成 1A-2B-3A。从 Java1.5 开始 JDK 的 atomic 包里提供了一个类
AtomicStampedReference 来解决 ABA 问题。从 Java1.5 开始 JDK 的
atomic 包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。这
个类的 compareAndSet 方法作用是首先检查当前引用是否等于预期引用,并
且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标
志的值设置为给定的更新值。
CAS 算法实现一个重要前提是需要取出内存中某时刻的数据,而在下一时
刻把取出后的数据和内存中原始数据比较并替换,那么在这个时间差内会导致
数据的变化。
比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从
内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数
据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操
作成功。尽管线程 one 的 CAS 操作成功,但是不代表这个过程就是没有问题
的。如果链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。
因此前面提到的原子操作
AtomicStampedReference/AtomicMarkableReference 就很有用了。这允
许一对变化的元素进行原子操作。

 ABA 问题带来的隐患,各种乐观锁的实现中通常都会用版本
号 version 来对记录或对象标记,避免并发操作带来的问题。在 Java 中,
AtomicStampedReference<E>也实现了这个作用,它通过包装[E,Integer]
的元组来对对象标记版本戳 stamp,从而避免 ABA 问题。
2、循环时间长开销大。自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的
执行开销。因此 CAS 不适合竞争十分频繁的场景。
3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可
以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环
CAS 就无法保证操作的原子性,这个时候就可以用锁。
这里粘贴一个,模拟 CAS 实现的计数器:
public class CASCount implements Runnable {
private SimilatedCAS counter = new SimilatedCAS();
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println(this.increment());
}
}

public int increment() {
int oldValue = counter.getValue();
int newValue = oldValue + 1;
while (!counter.compareAndSwap(oldValue, newValue)) { //
如果 CAS 失败,就去拿新值继续执行 CAS
oldValue = counter.getValue();
newValue = oldValue + 1;
}
return newValue;
}
public static void main(String[] args) {
Runnable run = new CASCount();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
}
class SimilatedCAS {
private int value;
public int getValue() {
return value;
}
// 这里只能用 synchronized 了,毕竟无法调用操作系统的 CAS
public synchronized boolean compareAndSwap(int expectedValue,
int newValue) {
if (value == expectedValue) {
value = newValue;
return true;
}
return false;
}
}

CASJAVA一些理解

标签:安全   JD   native   记录   nbsp   ++i   nal   情况   asc   

原文地址:https://www.cnblogs.com/panxuejun/p/8601085.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!