在java中可以通过锁和CAS操作来实现原子操作
CAS实现原子操作
CAS自旋的基本思路是:循环进行CAS操作,直到成功为止
下面代码演示了非线程安全的计数器和采用CAS操作的线程安全计数器
public class Counter {
private AtomicInteger actomicI = new AtomicInteger(0);
private int i = 0;
/**
* 使用CAS操作实现线程安全的计数器
*/
private void safeCount(){
for(;;){
int k = actomicI.get();
boolean suc = actomicI.compareAndSet(k,++k);
if(suc){
break;
}
}
}
/**
* 非线程安全的计数器
*/
private void unsafeCount(){
i++;
}
public static void main(String[] args) {
final Counter counter = new Counter();
List<Thread> list = new ArrayList<>(600);
long start = System.currentTimeMillis();
for (int j = 0; j < 100; j++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int l = 0; l < 10000; l++) {
counter.safeCount();
counter.unsafeCount();
}
}
});
list.add(t);
}
//启动所有线程
for (Thread thread : list) {
thread.start();
}
//等待所有线程执行完成
for (Thread thread : list) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("非线程安全计数结果:"+counter.i);
System.out.println("CAS操作计数结果:"+counter.actomicI.get());
System.out.println("执行时间:");
System.out.println(System.currentTimeMillis()-start);
}
}
第一次执行结果:
非线程安全计数结果:991053
CAS操作计数结果:1000000
执行时间:
71
第二次执行结果:
非线程安全计数结果:988024
CAS操作计数结果:1000000
执行时间:
77
通过对比可知,非线程安全计数的结果是不一样的!
但是通过CAS操作保证原子性也会有三大问题:
1. 问题一:ABA问题
CAS需要操作值的时候,先检查值是否发生变化,如果没有变化,则更新。如果一个值原来是A,先被更新为B,然后又更新为A,那么CAS检查时会认为值没有变化,但实际上是变化的。
ABA解决的思路是在变量的前面追加版本号,每次更新的时候,把版本号加1,那么A->B->A就变成了1A->2B->3A
AtomicStampedReference类提供了一个compareAndSet方法来解决ABA问题:
public boolean compareAndSet(V expectedReference,//预期引用
V newReference,//更新后的引用
int expectedStamp,//预期标志
int newStamp) {//更新后的标志
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
首先检查预期引用是否等于当前引用,再检查预期标志是否等于当前标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值
2. 循环时间过长
循环时间过长,则会长期占用cpu资源。如果jvm支持处理器的pause指令,那么性能会有所提升。
pause指令主要干了两件事:
- 延迟流水线执行指令,使cpu不会消耗过多的执行资源
- 避免退出循环因内存顺序冲突而导致cpu流水线被清空
3. 只能保证一个共享变量的原子操作
取巧的方法:
把多个共享变量合并成一个共享变量来操作
jdk提供了AtomicReference来保证引用对象之间的原子性,可以把多个共享变量放在一个对象里来进行CAS操作!
使用锁机制实现原子操作
锁机制保证了只有获得锁的线程才能进入被锁住的内存区域进行操作。处理偏向锁,JVM实现锁的方式都使用了CAS循环操作,即当一个线程要进入同步块的时候,通过CAS循环操作来获得锁,要退出同步快时,通过CAS循环操作来释放锁。