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。
相关文章推荐
- JDK提供的原子类原理与使用
- 高并发编程-07-JDK提供的原子类操作及原理
- 短信恢复的原理——使用内容提供者
- iOS酷我音乐导出工具使用说明和原理介绍(提供下载链接及工程源码)
- net控件中数据导到Excel的格式 首先,我们了解一下excel从web页面上导出的原理。当我们把这些数据发送到客户端时,我们想让客户端程序(浏览器)以excel的格式读取它,所以把mime类型设为:application/vnd.ms-excel,当excel读取文件时会以每个cell的格式呈现数据,如果cell没有规定的格式,则excel会以默认的格式去呈现该cell的数据。这样就给我们提供了自定义数据格式的空间,当然我们必须使用excel支持的格式。下面就列出常用的一些格式: 1) 文本
- 【引用】关于ALTERA提供的FIFO核使用原理
- 【引用】关于ALTERA提供的FIFO核使用原理
- 关于ALTERA提供的FIFO核使用原理
- OLED使用测试-参照STM32正点原子和电机控制原理(一)
- ConcurrentModificationException异常以及iterator迭代器的使用原理
- 使用ADO.net转换数据到Excel格式并提供下载
- java 迭代器 iteractor原理及使用误区
- Ubuntu 搭建 NFS服务提供给 SC2440或SC6410开发板使用【要点整理】
- 使用Android系统提供的DownloadManager来下载文件
- SDWebImage使用及原理
- base64原理,使用场景
- Java位运算原理及使用讲解(转载)
- Page_Load Page_Init方法使用原理 Page 添加 事件
- 使用gem安装compass失败了的看过来(淘宝提供的gem国内镜像介绍)
- DelegatingFilterProxy的原理及使用