您的位置:首页 > 编程语言 > Java开发

《Java并发编程的艺术》笔记四——Java如何实现原子操作.md

2017-08-13 20:54 471 查看
在Java中可以通过锁和循环CAS的方式实现原子操作。

注:CAS(比较与交换,Compare and swap) 是一种有名的无锁算法。CAS的语义是“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少”,CAS是一种 乐观锁 技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

CAS是以原子操作为基础,采用事务->提交->提交失败->重试这样特定编程手法的机制,它使得正在访问共享资源的线程不依赖于任何其它线程的调度和执行,并且能够在有限的步骤内完成。

1.使用循环CAS实现原子操作。

JVM中的CAS操作是利用了处理器提供的CMPXCHG指令实现的。自旋CAS的基本思路就是循环进行CAS操作,直到成功为止。

以下代码实现了一个基于CAS的线程安全的计数器和一个非线程安全的计数器:

package wha.thread;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
* 使用锁和循环CAS实现原子操作
*/
public class Counter {
private int i = 0;
private AtomicInteger atomicI = new AtomicInteger(0);

public void count(){
i++;
}

public void safeCount(){
while(true){
int j = atomicI.get();
boolean suc = atomicI.compareAndSet(j, ++j);
if(suc) break;
}
}

public static void main(String[] args) {
final Counter cas = new Counter();
List<Thread> ts = new ArrayList<>(600);
long start = System.currentTimeMillis();
for (int k=0; k<100; k++){
Thread t = new Thread(new Runnable() {
@Override
public void run() {
cas.count();
cas.safeCount();
}
});
ts.add(t);
//            Thread t = new Thread(()->{//lambda
//                for (int m=0; m<10000;m++){
//                    cas.count();
//                    cas.safeCount();
//                }
//            });
//            ts.add(t);
}

ts.forEach(thread -> thread.start());
for (Thread t: ts){
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(cas.i);
System.out.println(cas.atomicI.get());
System.out.println("time:"+(System.currentTimeMillis()-start));
}
}
}

运行的结果:

...

978173

982162

time:177

...

996010

1000000

time:184


从JDK1.5版本开始,JDK的并发包中提供了以Atomic开头的类,来支持原子操作,比如AtomicBoolean,AtomicInteger,AtomicLong等。

2.CAS实现原子操作的3大问题

CAS虽然很高效的解决了原子操作,但是依然存在几个问题

1.ABA问题

因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新。但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查的时候会发现他的值没有变化,但实际上变化了。ABA问题的解决思路是,使用版本号。在变量前面追加版本号,每次变量更新的时候把版本号+1,这样A->B->A,就会变成1A->2B->3A。

2.循环时间长开销大

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

3.只能保证一个共享变量的原子操作

当CAS对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候可以用锁。还有一个取巧的方法,就是把多喝共享变量合并成一个共享变量来操作。例如,两个共享变量i=1,j=a,合并一下ij=1a,然后用CAS来操作ij。

从Java1.5开始,JDK提供了AtomicReference类来保证应用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

3.使用锁机制来实现原子操作

锁机制保证了线程只有在获得锁之后才能操作锁定的内存区域。JVM内部实现了很多种锁机制,有偏向锁,轻量级锁和互斥锁。有意思的是除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获得锁,当它退出同步块的时候使用循环CAS释放锁。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: