Java并发编程:AtomicInteger&CAS 博客分类: Java多线程 Java并发编程AtomicIntegerCAS
程序员文章站
2024-03-16 14:51:52
...
很多情况下我们只需要一个简单的、高效的、线程安全的递增递减方案,而Java中++i或--i并不是线程安全的,但是java.util.concurrent包中提供原子(Atomic) 操作的类,今天我们就来学习它最基本的AtomicInteger。
以下是本文包含的知识点:
1.什么是原子操作
2.AtomicInteger用法
3.CAS介绍
4.AtomicIntegerArray/AtomicIntegerFieldUpdater<T> 介绍
一、什么是原子操作
通常情况下,在Java里面,++i或者--i不是线程安全的,这里面有三个独立的操作:获得变量当前值,为该值+1/-1,然后写回新的值。在没有额外资源可以利用的情况下,只能使用加锁(synchronized)才能保证读-改-写这三个操作时“原子性”的。
来看下面的例子:
public class AtomicTest { public static int num = 0; public static void increment(){ num++; } public static void main(String[] args) { Thread[] threads = new Thread[20]; for(int i=0; i< threads.length; i++){ threads[i] = new Thread(new Runnable() { public void run() { for(int i=0;i<10000;i++){ increment(); } } }); threads[i].start(); } //等待所有累加线程都结束 while(Thread.activeCount() > 1){ Thread.yield(); } System.out.println("num="+num); } }这段代码发起20个线程,每个线程对变量num进行10000次自增操作,如果这段代码能够正确并发的话,最后输出的结果应该是200000。结果运行之后,发现每次执行它都小于200000,这是为什么呢?
问题就出在自增运算num++中,它其实包含了三个独立的操作:获得变量当前值,为该值+1/-1,然后写回新的值。这三个操作又不具备原子性操作,一个线程在执行的时候,有可能被其它线程打断,这时num的值就不安全,同时被多个线程共享,修改。
要解决这个问题,其实很简单,加锁就可以了,用synchronized或lock都行。只需要给increment()加上锁即可:
public synchronized static void increment(){ num++; }除了使用同步加锁,JDK5以后还提供了内置的API来解决原子性的自增,自减操作,下面我们来看最基本的AtomicInteger的用法。
二、AtomicInteger用法
AtomicInteger是java.util.concurrent.atomic包中最基本的原子操作类,即int类型的自增、自减原子性操作,我们来看用它实现上面的num自增操作:
public class AtomicIntegerTest { private static AtomicInteger num = new AtomicInteger(0); public static void increment(){ num.incrementAndGet(); } public static void main(String[] args) { Thread[] threads = new Thread[20]; for(int i=0; i< threads.length; i++){ threads[i] = new Thread(new Runnable() { public void run() { for(int i=0;i<100000;i++){ increment(); } } }); threads[i].start(); } //等待所有累加线程都结束 while(Thread.activeCount() > 1){ Thread.yield(); } System.out.println("num="+num); } }执行后,每次运行结果都是200000。而且还不用手动加锁,因为它本身实现的就是线程安全的原子性操作。
AtomicInteger除了incrementAndGet()方法,它还提供其它自增,自减的方法:
decrementAndGet()自减,相当于--i,返回修改后的值
getAndIncrement()自增,相当于++i,返回修改前的值
getAndDecrement()自减,相当于--i,返回修改前的值
getAndSet() 设置值,返回修改前的值
compareAndSet()比较赋值,修改成功返回true,否则返回false
还有其它方法,这些方法都是原子性操作,都是线程安全的。那问题来了,AtomicInteger是怎么保证原子性操作的呢,其实它是利用处理器指令比较并交换(Compare-and-Swap,简称CAS)来实现的。
三、CAS介绍
CAS指令需要3个操作数,分别是内存位置(Java中可理解为内存地址,用V表示),旧的预期值(用A表示)和新值(用B表示)。指令执行时,当且仅当V符合旧的预期值A时,处理器用新值B更新V的值,否则不更新。
在JDK5以后,Java程序才可以使用CAS操作,具体由Unsafe类来实现,并且用户程序无法直接调用,我们可以通过Java API来间接使用它,如J.U.C包里的整数原子类AtomicInteger,其中的compareAndSet()和getAndIncrement都是使用了Unsafe类的CAS操作。
我们来看AtomicInteger的incrementAndGet()源码:
/** * Atomically increments by one the current value. * * @return the updated value */ public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } }
incrementAndGet()方法在一个无限循环中,不断尝试将一个比自己大1的值赋给自己,如果失败了,那说明在执行“获取-设置”操作的时候已经有了修改,于是再次循环进行下一次操作,直到设置成功为止。
ABA问题:尽管CAS看起很美,但显然这种操作无法涵盖互斥同步的所有使用场景,并且CAS从语义上说并不完美,存在这样一个漏洞:如果一个变量初次读取的时候是A值,并且在准备赋值检查它是仍然是A值,那我们就说它的值没有被其它线程改变过吗?如果在这段期间它的值曾经改成了B,后来又改回成A,那CAS操作就会误以为它从来都没变过,这个漏洞称为CAS操作的ABA问题。如果要解决ABA问题,请使用互斥同步。
四、AtomicIntegerArray/AtomicIntegerFieldUpdater<T> 介绍
AtomicInteger和AtomicLong、AtomicBoolean、AtomicReference差不多,这里就不介绍了。
AtomicIntegerArray/AtomicLongArray/AtomicReferenceArray的API类似,为数组的原子操作类,以AtomicIntegerArray为例来看下:
int addAndGet(int i, int delta) 以原子方式将给定值与索引 i 的元素相加
boolean compareAndSet(int i, int expect, int update) 如果当前值
==
预期值,则以原子方式将位置 i
的元素设置为给定的更新值
int decrementAndGet(int i)以原子方式将索引 i 的元素减 1
int get(int i)获取位置 i 的当前值
int getAndAdd(int i, int delta)以原子方式将给定值与索引 i 的元素相加
int getAndDecrement(int i)以原子方式将索引 i 的元素减 1
int getAndIncrement(int i) 以原子方式将索引 i 的元素加 1
int getAndSet(int i, int newValue)将位置 i 的元素以原子方式设置为给定值,并返回旧值
int incrementAndGet(int i)以原子方式将索引 i 的元素加 1
其实这些方法与AtomicInteger方法也很类似,而且从方法命名就看出是什么意思,这种通过方法、参数的名称就能够得到函数意义的写法是非常值得称赞的。
AtomicIntegerFieldUpdater<T>/AtomicLongFieldUpdater<T>/AtomicReferenceFieldUpdater<T,V>是基于反射的原子更新字段的值。
相应的API也是非常简单的,但是也是有一些约束的。
1.字段必须是volatile类型的!在后面的章节中会详细说明为什么必须是volatile,volatile到底是个什么东西。
2.字段的描述类型(修饰符public/protected/default/private)是与调用者与操作对象字段的关系一致。也就是说调用者能够直接操作对象字段,那么就可以反射进行原子操作。但是对于父类的字段,子类是不能直接操作的,尽管子类可以访问父类的字段。
3.只能是实例变量,不能是类变量,也就是说不能加static关键字。
4.只能是可修改变量,不能使final变量,因为final的语义就是不可修改。实际上final的语义和volatile是有冲突的,这两个关键字不能同时存在。
5.对于AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long类型的字段,不能修改其包装类型(Integer/Long)。如果要修改包装类型就需要使用AtomicReferenceFieldUpdater。
public class AtomicIntegerFieldUpdaterTest { class DemoData { public volatile int value1 = 1; volatile int value2 = 2; protected volatile int value3 = 3; private volatile int value4 = 4; } AtomicIntegerFieldUpdater<DemoData> getUpdater(String fieldName) { return AtomicIntegerFieldUpdater.newUpdater(DemoData. class, fieldName); } void doit() { DemoData data = new DemoData(); System. out.println("1 ==> " + getUpdater("value1" ).addAndGet(data, 10)); System. out.println("2 ==> " + getUpdater( "value2").incrementAndGet(data)); System. out.println("3 ==> " + getUpdater( "value3").decrementAndGet(data)); System. out.println("value4 ==> " + getUpdater( "value4").compareAndSet(data, 4, 5)); } public static void main(String[] args) { //DemoData的字段value3/value4对于AtomicIntegerFieldUpdaterDemo类是不可见的,因此通过反射是不能直接修改其值的。 AtomicIntegerFieldUpdaterTest demo = new AtomicIntegerFieldUpdaterTest(); demo.doit(); } }执行结果为:
1 ==> 11 2 ==> 3 Exception in thread "main" java.lang.RuntimeException: java.lang.IllegalAccessException : Class org.concurrent.atomic.AtomicIntegerFieldUpdaterTest can not access a protected member of class org.concurrent.atomic.AtomicIntegerFieldUpdaterTest$DemoData using an instance of org.concurrent.atomic.AtomicIntegerFieldUpdaterTest$DemoData看到只改变了value1,value2的值
value3value4对于AtomicIntegerFieldUpdaterDemo类是不可见的,因此通过反射是不能直接修改其值
上一篇: mysqlreport 博客分类: DATABASE mysql数据库调优
下一篇: springMvc直接自动转化对象为json的配置(不需要自己转成json) 博客分类: springmvc springMvc
推荐阅读
-
Java并发编程:AtomicInteger&CAS 博客分类: Java多线程 Java并发编程AtomicIntegerCAS
-
Java并发编程:阻塞队列 博客分类: Java多线程 java阻塞队列
-
Java并发编程:阻塞队列 博客分类: Java多线程 java阻塞队列
-
Java并发编程:Callable、Future和FutureTask 博客分类: Java多线程 CallableFutureFutureTask
-
深入理解Java并发编程之核心原理概念
-
JAVA线程安全概念 博客分类: Java并发编程 线程安全
-
冒泡法排序原理 博客分类: java编程 java算法冒泡排序
-
冒泡法排序原理 博客分类: java编程 java算法冒泡排序
-
java 注解的几大作用及使用方法详解(完) 博客分类: 编程语言 注解 java 注解
-
java 注解的几大作用及使用方法详解(完) 博客分类: 编程语言 注解 java 注解