一、简介
在多线程环境下,如果多个线程需要对共享资源同时进行操作的话,很容易出现数据不一致的情况。通常情况下我们会使用synchronized同步方式来保证线程安全。
从JDK 1.5开始提供了java.util.concurrent.atomic包,这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。
在java.util.concurrent.atomic包下面主要有以下一些类:
- AtomicBoolean:原子更新布尔类型的变量
- AtomicInteger:原子更新整型的变量
- AtomicIntegerArray:原子更新整型数组里的元素
- AtomicIntegerFieldUpdater:原子更新整型的字段的更新器
- AtomicLong:原子更新长整型的变量
- AtomicLongArray:原子更新长整型数组里的元素
- AtomicLongFieldUpdater:原子更新长整型字段的更新器
- AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型
- AtomicReference:原子更新引用类型的变量
- AtomicReferenceArray:原子更新引用类型数组里的元素
- AtomicReferenceFieldUpdater:原子更新引用类型里的字段
- AtomicStampedReference:原子更新带有版本号的引用类型
下面我们以AtomicInteger为例进行详细讲解,其他的原子类原理都类似,具体也可以参考官网https://docs.oracle.com/javase/8/docs/api/index.html进行学习。
二、AtomicInteger
AtomicInteger:一个可以自动更新的整型值,用于描述原子变量的属性。AtomicInteger用于原子递增的计数器等应用程序,不能用作整数的替代。但是,这个类扩展了Number,允许处理基于数字的类的工具和实用程序进行统一访问。
构造方法:
- AtomicInteger():创建一个初始值为0的新AtomicInteger;
- AtomicInteger(int initialValue):使用给定的初始值创建一个新的AtomicInteger;
三、AtomicInteger常见API
返回值 |
方法名称、描述 |
int |
addAndGet(int delta) 原子地将给定值添加到当前值 |
boolean |
compareAndSet(int expect, int update) 如果当前值==期望值,则自动将该值设置为给定的更新值。 返回true表示成功,false表示实际值不等于期望值。 |
int |
原子性地使当前值递减1 |
double |
在扩展原语转换之后,将此AtomicInteger的值作为双精度值返回 |
float |
在扩展原语转换之后,将此AtomicInteger的值作为浮点数返回 |
int |
get() 获取当前值 |
int |
getAndAccumulate(int x, IntBinaryOperator accumulatorFunction) 使用将给定函数应用于当前值和给定值的结果自动更新当前值,并返回以前的值 |
int |
getAndAdd(int delta) 原子地将给定值添加到当前值 |
int |
原子性地使当前值递减1 |
int |
原子地增加一个当前值 |
int |
getAndSet(int newValue) 自动设置为给定值并返回旧值 |
int |
getAndUpdate(IntUnaryOperator updateFunction) 使用应用给定函数的结果自动更新当前值,并返回以前的值 |
int |
原子地增加一个当前值 |
int |
intValue() 返回这个AtomicInteger的整型值 |
void |
lazySet(int newValue) 最终设置为给定的值 |
long |
将此AtomicInteger的值作为扩展原语转换后的long |
void |
set(int newValue) 设置为给定的值 |
toString() 返回当前值的字符串表示形式 |
|
int |
updateAndGet(IntUnaryOperator updateFunction) 使用应用给定函数的结果自动更新当前值,并返回更新后的值 |
boolean |
weakCompareAndSet(int expect, int update) 如果当前值==期望值,则自动将该值设置为给定的更新值 |
四、AtomicInteger示例
下面通过一个简单的多线程操作count++的示例来说明AtomicInteger的用法。
在这之前,我们先看下用synchronized方式实现同步的代码:
public class T14_TestVolatile {
int count = 0;
//synchronized既保证了可见性又保证了原子性,而volatile只保证了可见性
private synchronized void m() {
for (int i = 0; i < 10000; i++) {
count++;
}
}
public static void main(String[] args) {
T14_TestVolatile t02_testVolatile = new T14_TestVolatile();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10; i++) {
threads.add(new Thread(t02_testVolatile::m, "thread" + i));
}
threads.forEach(t -> t.start());
threads.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t02_testVolatile.count);
}
}
运行结果:
count = 100000
我们都知道count++是一个非原子性的操作,下面我们使用AtomicInteger实现原子操作:
public class T15_TestVolatile {
//原子性操作的Integer
//但不能保证多个方法连续调用具有原子性
//指定初始值为0
AtomicInteger count = new AtomicInteger(0);
void m() {
for (int i = 0; i < 10000; i++) {
//原子操作
count.incrementAndGet();
}
}
public static void main(String[] args) throws InterruptedException {
T15_TestVolatile t15_testVolatile = new T15_TestVolatile();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10; i++) {
threads.add(new Thread(t15_testVolatile::m, "thread" + i));
}
threads.forEach(t -> t.start());
threads.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("count = " + t15_testVolatile.count);
}
}
运行结果:
count = 100000
注意:AtomicInteger只能保证一个操作具有原子性,并不能保证多个方法连续调用具有原子性。
五、AtomicInteger源码分析
以上面示例中使用到的count.incrementAndGet()方法为例大概说明一下底层是怎么保证原子性的。
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int decrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}
//省略....
查看源码,AtomicInteger底层都是
private static final Unsafe unsafe = Unsafe.getUnsafe()
这个unsafe对应的方法来保证原子性的。
然后我们去看一下Unsafe的源码,下面是部分源码
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
继续追踪,发现Unsafe只提供了3种CAS方法:compareAndSwapObject、compareAndSwapInt、compareAndSwapLong:
public final native boolean compareAndSwapObject(Object o,long offset,Object expected,Object x);
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);
以上三个方法都是native本地方法,说明它并不是使用Java来实现的,其底层是使用CAS(Compare-And-Swap)算法来保证原子性的,CAS 是硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。
下面我们介绍一下CAS算法。
六、CAS算法
CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS包含三个操作数:
- 一个内存地址V
- 一个预估值(期望值)A
- 一个更新值(新值)B
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为更新值B,否则就什么都不做。整个比较并替换的操作是一个原子操作.
优点:
- 可以避免优先级倒置和死锁等问题;
- 允许更高程度的并行机制;
缺点:
CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题:
- 循环时间长开销很大:
如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
- 只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
- ABA问题:
如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?
如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。
七、总结
本文介绍了JUC并发包中常用的原子变量,并且介绍了其中用到的CAS原理。实际项目中如果在多线程中存在i++或者++i等需要保证原子性操作的时候可以考虑使用原子变量。