简介
在1.5开始提供了java.util.concurrent.atomic工具包,这个包下的所有类都是基于CAS思想实现的,提供了安全、高效、简单的更新一个变量的方式。
为了适配变量类型,在atomic包下提供了12个类,分属于4中类型的原子更新方式,分别为:原子更新基本类型、原子更新数据、原子更新引用和原子更新属性。其内部基本都是使用Unsafe实现的包装类。
原子更新基本类型
以原子方式更新基本类型,Atomic提供了三个类。分别为AtomicInteger、AtomicBoolean、AtomicLong。这三个类的方法基本相同,此处以AtomicInteger为例:
- int addAndGet():以原子的方式将输入的数字与AtomicInteger里的值相加,并返回结果。
public class AtomicIntegerTest {
private static AtomicInteger atomicInteger = new AtomicInteger(0);
public static Integer addAndGetDemo(int value){
return atomicInteger.addAndGet(value);
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Integer result = addAndGetDemo(i);
System.out.println(result);
}
}
}
- boolean compareAndSet(int expect,int uddate):如果输入的值等于预期值,则以原子方式将该值设为输入的值
- int incrementAndSet():对原值+1,并返回操作后的值,类似于redis中的increment命令。相反的还有decrementAndSet()
- int getAndAdd(int delta):原值加上指定值,并返回修改前的值。
- int getAndSet(int delta):将原值修改为新值,并返回修改前的值。
- int getAndIncrement():原值加1,返回修改前的值。对应的还有getAndDecrement()
原子更新数组
通过原子方式更新数组里的某个元素,Atomic包中提供了三个类,分别为AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray。 以上几个类中的方法基本类似,只是操作的数据类型不一样,以AtomicIntegerArray中的API为例。
//执行加法,第一个参数为数组的下标,第二个参数为增加的数量,返回增加后的结果
int addAndGet(int i, int delta)
//对比修改,参1数组下标,参2原始值,参3修改目标值,成功返回true否则false
boolean compareAndSet(int i, int expect, int update)
//参数为数组下标,将数组对应数字减少1,返回减少后的数据
int decrementAndGet(int i)
// 参数为数组下标,将数组对应数字增加1,返回增加后的数据
int incrementAndGet(int i)
//和addAndGet类似,区别是返回值是变化前的数据
int getAndAdd(int i, int delta)
//和decrementAndGet类似,区别是返回变化前的数据
int getAndDecrement(int i)
//和incrementAndGet类似,区别是返回变化前的数据
int getAndIncrement(int i)
// 将对应下标的数字设置为指定值,第一个参数数组下标,第二个参数为设置的值,返回是变化前的数据
getAndSet(int i, int newValue)
public class AtomicIntegerArrayDemo {
static int[] value = new int[]{1,2,3};
static AtomicIntegerArray ai = new AtomicIntegerArray(value);
public static void main(String[] args) {
System.out.println(ai.getAndSet(2,6));
System.out.println(ai.get(2));
System.out.println(value[2]);
}
}
此时可以看到从AtomicIntegerArray获取的值与原传入数组的值不同。这是因为数组是通过构造方法传递,然后AtomicIntegerArray会将当前传入数组复制一份。因此当AtomicIntegerArray对内部数组元素进行修改时,不会影响原数组。
原子更新引用类型
之前提到的通过CAS只能保证一个共享变量的原子操作,当多个的话,就需要使用锁。对于这个问题,在Atomic包中进行了解决。如果需要更新多个变量,就需要使用Atomic包中的三个类,分别为:AtomicReference(用于原子更新引用类型)、AtomicMarkableReference(用于原子更新带有标记位的引用类型)、AtomicStampedReference(用于原子更新带有版本号的引用类型)。
public class AtomicReferenceTest {
static class User{
private String name;
private int 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;
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
public static AtomicReference<User> atomicReference = new AtomicReference<>();
public static void main(String[] args) {
User u1 = new User("张三",18);
User u2 = new User("李四",19);
atomicReference.set(u1);
atomicReference.compareAndSet(u1,u2);
System.out.println(atomicReference.get().getName());
System.out.println(atomicReference.get().getAge());
}
}
AtomicMarkableReference可以用于解决CAS中的ABA的问题。使用演示如下:
public class AtomicMarkableReferenceDemo {
static class User{
private String name;
private int 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;
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
public static void main(String[] args) throws InterruptedException {
User u1 = new User("张三", 22);
User u2 = new User("李四", 33);
//只有true和false两种状态。相当于未修改和已修改
//构造函数出传入初始化引用和初始化修改标识
AtomicMarkableReference<User> amr = new AtomicMarkableReference<>(u1,false);
//在进行比对时,不仅比对对象,同时还会比对修改标识
//第一个参数为期望值
//第二个参数为新值
//第三个参数为期望的mark值
//第四个参数为新的mark值
System.out.println(amr.compareAndSet(u1,u2,false,true));
System.out.println(amr.getReference().getName());
}
}
AtomicStampedReference会基于版本号思想解决了ABA问题,根据源码可知,其内部维护了一个Pair对象,Pair对象记录了对象引用和时间戳信息,实际使用的时候,要保证时间戳唯一,如果时间戳如果重复,会出现ABA的问题。
AtomicStampedReference中每个引用变量都带上pair.stamp这个时间戳,这样就可以解决CAS中的ABA问题。
使用实例如下:
public class AtomicStampedReferenceDemo {
private static final Integer INIT_NUM = 1000;
private static final Integer UPDATE_NUM = 100;
private static final Integer TEM_NUM = 200;
private static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(INIT_NUM, 1);
public static void main(String[] args) {
new Thread(() -> {
int value = (int) atomicStampedReference.getReference();
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + " : 当前值为:" + value + " 版本号为:" + stamp);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(atomicStampedReference.compareAndSet(value, UPDATE_NUM, stamp, stamp + 1)){
System.out.println(Thread.currentThread().getName() + " : 当前值为:" + atomicStampedReference.getReference() + " 版本号为:" + atomicStampedReference.getStamp());
}else{
System.out.println("版本号不同,更新失败!");
}
}, "线程A").start();
new Thread(() -> {
// 确保线程A先执行
Thread.yield();
int value = (int) atomicStampedReference.getReference();
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + " : 当前值为:" + value + " 版本号为:" + stamp);
System.out.println(Thread.currentThread().getName() +" : "+atomicStampedReference.compareAndSet(atomicStampedReference.getReference(), TEM_NUM, stamp, stamp + 1));
System.out.println(Thread.currentThread().getName() + " : 当前值为:" + atomicStampedReference.getReference() + " 版本号为:" + atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName() +" : "+atomicStampedReference.compareAndSet(atomicStampedReference.getReference(), INIT_NUM, stamp, stamp + 1));
System.out.println(Thread.currentThread().getName() + " : 当前值为:" + atomicStampedReference.getReference() + " 版本号为:" + atomicStampedReference.getStamp());
}, "线程B").start();
}
}
线程A : 当前值为:1000 版本号为:1
线程B : 当前值为:1000 版本号为:1
线程B : true
线程B : 当前值为:200 版本号为:2
线程B : false
线程B : 当前值为:200 版本号为:2
版本号不同,更新失败!
原子更新字段类
当需要原子更新某个类中的字段时,就需要使用原子更新字段类了。Atomic包下又3个类AtomicIntegerFieldUpdater(原子更新整型字段)、AtomicLongFieldUpdater(原子更新长整型字段)、AtomicReferenceFieldUpdater(原子更新引用类型字段)
原子更新字段类都是抽象类,每次使用都必须使用静态方法newUpdate创建一个更新器。原子更新类的字段必须使用public volatile修饰符。
以AtomicIntegerFieldUpdater为例
public class AtomicIntegerFieldUpdaterTest {
static class User{
private String name;
public volatile int 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;
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
private static AtomicIntegerFieldUpdater<User> fieldUpdater = AtomicIntegerFieldUpdater.newUpdater(User.class,"age");
public static void main(String[] args) {
User user = new User("zhangsan",18);
System.out.println(fieldUpdater.getAndIncrement(user));
System.out.println(fieldUpdater.get(user));
}
}
JDK1.8新增原子类
LongAdder:长整型原子类
DoubleAdder:双浮点型原子类
LongAccumulator:类似LongAdder,但要更加灵活(要传入一个函数式接口)
DoubleAccumulator:类似于DoubleAdder,但要更加灵活(要传入一个函数式接口)
以LongAdder为例,其内部提供的API基本上可以替换原先的AtomicLong。
LongAdder类似于AtomicLong是原子性递增或者递减类,AtomicLong已经通过CAS提供了非阻塞的原子性操作,相比使用阻塞算法的同步器来说性能已经很好了,但是JDK开发组并不满足,因为在非常高的并发请求下AtomicLong的性能不能让他们接受,虽然AtomicLong使用CAS但是CAS失败后还是通过无限循环的自旋锁不断尝试。
在高并发下N多线程同时操作一个变量会造成大量线程CAS失败然后处于自旋状态,这大大浪费了CPU资源,降低了并发性,那么既然AtomicLong性能是由于过多线程同时竞争一个变量的更新而降低的,那么如果把一个变量分解为多个变量,让同样多的线程去竞争多个资源那么性能问题不就解决了?是的,JDK1.8的LongAdder就是这个思路。
public class Demo9Compare {
public static void main(String[] args) {
AtomicLong atomicLong = new AtomicLong(0L);
LongAdder longAdder = new LongAdder();
long start = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
//atomicLong.incrementAndGet();
longAdder.increment();
}
}
}).start();
}
while (Thread.activeCount() > 2) {
}
System.out.println(atomicLong.get());
System.out.println(longAdder.longValue());
System.out.println("耗时:" + (System.currentTimeMillis() - start));
}
}
根据测试结论,使用longAdder相对比atomicLong可以进行大幅度的性能优化。当然不同计算机因为CPU、内存等硬件不一样,所以测试的数值也不一样,但是得到的结论都是一样的。
从上结果图可以看出,在并发比较低的时候,LongAdder和AtomicLong的效果非常接近。但是当并发较高时,两者的差距会越来越大。