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

JDK1.5提供的原子类原理及使用

2018-02-24 15:15 218 查看

JDK提供的原子类原理及使用

volatile只能保障可见性,不能保障原子性,如value++操作,就不是一个原子性操作,value++共分为以下三步操作(假如value的值是0):1、取出value的值为0;
2、将value的值进行加一操作,得到一个新值为1;
3、将新值1再赋值给变量value。

假如线程1刚执行完了第二步,此时value的值依然为0,得到的新值为1,然后就轮到线程2执行。线程2执行第一步,取出value的值为0,再执行第二步,依然得到的新值为1,再执行第三步,将1赋值给value,执行完毕。此时线程1继续执行,又将1赋值给了value。所以在线程1和线程2执行完毕之后,执行了两次++操作,值本应该为2的value此时的值却为1,这时就出现了线程安全性问题。
解决的办法就是让非原子性的++操作变成一个原子性操作即可,也就是说线程2必须等线程1三个步骤都执行完毕之后才能执行,通常我们都会使用synchronized同步代码块来保障++操作的原子性及内存的可见性
public synchronized int getNext() {
return value++;
}

由于synchronized会导致线程上下文的切换,性能并不高,所以我们可以使用JDK提供的原子类的方式来保障内存的可见性及变量操作的原子性(注:原子类底层是volatile和CAS共同作用的结果)。
public class MainTest {

private AtomicInteger value  = new AtomicInteger(0);

public int getNext() {
// value++
return value.getAndIncrement();
}

public static void main(String[] args) {

MainTest mainTest = new MainTest();

Runnable r = new Runnable() {

@Override
public void run() {
while (true) {

String threadName = Thread.currentThread().getName();
System.out.println(threadName + " " + mainTest.getNext());
try {Thread.sleep(100);} catch (InterruptedException e) {}
}
}
};

new Thread(r).start();
new Thread(r).start();
}
}

原子类源码路径(java.util.concurrent.atomic)


原子类主要有以下的几种类型:原子更新基本类型(AtomicInteger、AtomicBoolean、AtomicLong)
AtomicInteger value  = new AtomicInteger(0);
value.getAndIncrement();

原子更新数组(AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray)
int [] s = {2,1,4,6};
AtomicIntegerArray a = new AtomicIntegerArray(s);
// 将角标为2的值进行加1操作
a.getAndIncrement(2);
// 将角标为2的值进行加10操作
a.getAndAdd(2, 10);

原子更新引用类型(AtomicReference)
// 对user这个引用的get和set,保障对引用的原子性操作
AtomicReference<User> user = new AtomicReference<>();

原子更新字段(AtomicIntegerFieldUpdater,注:更新的字段不能使private,同时字段必须使用volatile进行修饰)
// 对类字段的原子性操作
AtomicIntegerFieldUpdater<User> old =  AtomicIntegerFieldUpdater.newUpdater(User.class, "old");
User user = new User();
System.out.println(old.getAndIncrement(user));


原子类实现原子性操作的原理通过乐观锁(CAS,Compare And Swap)来实现,所谓乐观锁就是每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
假如我们想对一个int型的变量value进行加1操作,总体的流程如下:1、先获取出value的值:int A = get();
2、对value值进行加1操作:int B = A + 1;
3、进行CAS操作:boolean flag = cas(A,B);
4、假如flag为true,则返回 B,假如flag为false,则重复执行第一步操作,直到flag为true

AtomicInteger类中的getAndIncrement代码大致实现如下:
public final int getAndIncrement() {
int A, B;
do {
A = get();
B = A + 1;
} while (!compareAndSet(A, B));
return B
}

compareAndSet函数主要的操作如下:1、将A值和内存中的V值进行比较,若A != V,则返回false;
2、若A == V,则将内存值更新为B,并返回true。

从以上的步骤中我们也可以看出,整个过程的关键在于compareAndSet函数必须是一个原子性的操作,然而CAS也称为比较和交换,从字面的意义上我们可以得知CAS操作至少需要两步才能完成,所以compareAndSet函数也必须通过加锁来保障原子性。
从JDK中的源码我们可以看到,compareAndSet并不是使用synchronized来保障原子性的,因为如果使用了synchronized的话,那么乐观锁将失去其自身的意义了。
compareAndSet函数调用的是unsafe.compareAndSwapInt方法。Unsafe类提供了硬件级别的原子操作,这个原子性的保证并不是靠java本身保证,而是靠一个更底层的与操作系统相关的特性实现(CPU锁),因此,JVM中的CAS的原子性是处理器保障的。
CAS存在着ABA的问题:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
解决的办法就是引入版本号,每修改一次则版本号加1,只有版本号和值都和预期值一直,compareAndSet函数才返回true。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐