在谈谈java中的volatile一文中,我们提到过并发包中的原子类可以解决类似num++这样的复合类操作的原子性问题,相比锁机制,使用原子类更精巧轻量,性能开销更小,本章就一起来分析下原子类的实现机理。
我们知道,num++看似简单的一个操作,实际上是由1.读取 2.加一 3.写入 三步组成的,这是个复合类的操作(所以我们之前提到过的volatile是无法解决num++的原子性问题的),在并发环境下,如果不做任何同步处理,就会有线程安全问题。最直接的处理方式就是加锁。
synchronized(this){
num++;
}
使用独占锁机制来解决,是一种悲观的并发策略,抱着一副“总有刁民想害朕”的态势,每次操作数据的时候都认为别的线程会参与竞争修改,所以直接加锁。同一刻只能有一个线程持有锁,那其他线程就会阻塞。线程的挂起恢复会带来很大的性能开销,尽管jvm对于非竞争性的锁的获取和释放做了很多优化,但是一旦有多个线程竞争锁,频繁的阻塞唤醒,还是会有很大的性能开销的。所以,使用synchronized或其他重量级锁来处理显然不够合理。
乐观的解决方案,顾名思义,就是很大度乐观,每次操作数据的时候,都认为别的线程不会参与竞争修改,也不加锁。如果操作成功了那最好;如果失败了,比如中途确有别的线程进入并修改了数据(依赖于冲突检测),也不会阻塞,可以采取一些补偿机制,一般的策略就是反复重试。很显然,这种思想相比简单粗暴利用锁来保证同步要合理的多。
鉴于并发包中的原子类其实现机理都差不太多,本章我们就通过AtomicInteger这个原子类来进行分析。我们先来看看对于num++这样的操作AtomicInteger是如何保证其原子性的。
/**
* 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.先获取当前的value值
2.对value加一
3.第三步是关键步骤,调用compareAndSet方法来来进行原子更新操作,这个方法的语义是:
先检查当前value是否等于current,如果相等,则意味着value没被其他线程修改过,更新并返回true。如果不相等,compareAndSet则会返回false,然后循环继续尝试更新。
compareAndSet调用了Unsafe类的compareAndSwapInt方法
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
此方法是Java的native方法,并不由Java语言实现。
方法的作用是,读取传入对象o在内存中偏移量为offset位置的值与期望值expected(var4)作比较。
相等就把var5的值赋值给offset位置的值。方法返回true。不相等,就取消赋值,方法返回false。
这也是CAS的思想,及比较并交换。用于保证并发时的无锁并发的安全性。
CAS算法是由硬件直接支持来保证原子性的,有三个操作数:内存位置V、旧的预期值A和新值B,当且仅当V符合预期值A时,CAS用新值B原子化地更新V的值,否则,它什么都不做。
CAS的ABA问题:当然CAS也并不完美,它存在"ABA"问题,假若一个变量初次读取是A,在compare阶段依然是A,但其实可能在此过程中,它先被改为B,再被改回A,而CAS是无法意识到这个问题的。CAS只关注了比较前后的值是否改变,而无法清楚在此过程中变量的变更明细,这就是所谓的ABA漏洞。
在JDK7包括7之前,java原子类有12个,图片如下
在JDK8时出现了4个原子操作类,分别是如下图片所示
使用原子的方式更新基本类型,Atomic包提供了以下3个类。
以上3个类提供的方法几乎一模一样,以AtomicInteger为例进行详解,AtomicIngeter的常用方法如下:
代码示例
static AtomicInteger ai =new AtomicInteger(1);
public static void main(String[] args) {
System.out.println(ai.getAndIncrement());
System.out.println(ai.get());
}
输出结果
1
2
下面我们看看getAndIncrement() 是如何实现原子操作的
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
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;
}
代码解析
我们取得了旧值,然后把要加的数传过去,调用getAndAddInt () 进行原子更新操作,实际最核心的方法是 compareAndSwapInt(),使用CAS进行更新。我们Unsafe只提供了3中CAS操作,
另外注意,AtomicBoolean 是把Boolean转成整型,在使用 compareAndSwapInt 进行操作的。
/**
* 如果当前数值是var4,则原子的将java变量更新成var5或var6
* @return 如果更新成功返回true
*/
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
通过原子的方式更新数组里的某个元素,Atomic包提供了以下的3个类:
这三个类的最常用的方法是如下两个方法:
下面以 AtomicReferenceArray 举例如下
static int[] value =new int[]{1,2};
static AtomicIntegerArray ai =new AtomicIntegerArray(value);
public static void main(String[] args) {
ai.compareAndSet(0,1,5);
System.out.println(ai.get(0));
System.out.println(value[0]);
}
输出结果
5
1
需要注意的是:数组value通过构造函数方法传递进去,然后AtomicIntegerArray会将当前的数组复制一份,所以当AtomicIntegerArray对内部数组元素进行修改时,并不会影响传入的数组
public AtomicIntegerArray(int[] array) {
// Visibility guaranteed by final field guarantees
this.array = array.clone();
}
原子更新基本类型的AtomicInteger,只能更新一个值,如果更新多个值,比如更新一个对象里的值,那么就要用原子更新引用类型提供的类,Atomic包提供了以下三个类:
public static AtomicReference ai = new AtomicReference();
public static void main(String[] args) {
User u1 = new User("pangHu", 18);
ai.set(u1);
User u2 = new User("piKaQiu", 15);
ai.compareAndSet(u1, u2);
System.out.println(ai.get().getAge() + ai.get().getName());
}
static class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
输出结果:piKaQiu 15
代码分析
我们把对象放到 AtomicReference 中,然后调用 compareAndSet () 原子操作替换,原理和 AtomicInteger 相同,只是调用的是 compareAndSwapObject() 方法。
如果需要原子的更新类里某个字段时,需要用到原子更新字段类,Atomic包提供了3个类进行原子字段更新:
//创建原子更新器,并设置需要更新的对象类和对象的属性
private static AtomicIntegerFieldUpdater ai = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
public static void main(String[] args) {
User u1 = new User("pangHu", 18);
//原子更新年龄,+1
System.out.println(ai.getAndIncrement(u1));
System.out.println(u1.getAge());
}
static class User {
private String name;
public volatile int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
代码详解
要想原子地更新字段类需要两步。
输出结果:
18
19
下面以 LongAdder 为例介绍一下,并列出使用注意事项
这些类对应把 AtomicLong 等类的改进。比如 LongAccumulator 与 LongAdder 在高并发环境下比 AtomicLong 更高效。
Atomic、Adder在低并发环境下,两者性能很相似。但在高并发环境下,Adder 有着明显更高的吞吐量,但是有着更高的空间复杂度。
LongAdder其实是LongAccumulator的一个特例,调用LongAdder相当使用下面的方式调用LongAccumulator。
sum() 方法在没有并发的情况下调用,如果在并发情况下使用会存在计数不准,下面有代码为例。
LongAdder不可以代替AtomicLong ,虽然 LongAdder 的 add() 方法可以原子性操作,但是并没有使用 Unsafe 的CAS算法,只是使用了CAS的思想。
LongAdder其实是LongAccumulator的一个特例,调用LongAdder相当使用下面的方式调用LongAccumulator,LongAccumulator提供了比LongAdder更强大的功能,构造函数其中accumulatorFunction一个双目运算器接口,根据输入的两个参数返回一个计算值,identity则是LongAccumulator累加器的初始值。
private static ExecutorService executorService = Executors.newFixedThreadPool(5);
public static void main(String[] args) {
for (int i = 1; i <= 100; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
counter.add(2);
}
});
}
System.out.println(counter.sum());
System.out.println(counter);
}
输出结果
如图LongAdder则是内部维护多个变量,每个变量初始化都0,在同等并发量的情况下,争夺单个变量的线程量会减少这是变相的减少了争夺共享资源的并发量,另外多个线程在争夺同一个原子变量时候如果失败并不是自旋CAS重试,而是尝试获取其他原子变量的锁,最后获取当前值时候是把所有变量的值累加后返回的。
//构造函数
LongAdder()
//创建初始和为零的新加法器。
//方法摘要
void add(long x) //添加给定的值。
void decrement() //相当于add(-1)。
double doubleValue() //在扩展原始转换之后返回sum()as double。
float floatValue() //在扩展原始转换之后返回sum()as float。
void increment() //相当于add(1)。
int intValue() //返回sum()作为int一个基本收缩转换之后。
long longValue() //相当于sum()。
void reset() //重置将总和保持为零的变量。
long sum() //返回当前的总和。
long sumThenReset() //等同于sum()后面的效果reset()。
String toString() //返回。的字符串表示形式sum()。