概述
所谓原子性,就是表示一个获取多个操作,要么全部执行完,要么一个都不执行,不能出现部分成功和部分失败的情况。
在多线程中,如果多个线程同时更新一个共享变量,可能会得到一个意料之外的值。比如 i=1 。 A 线程更新 i+1 、B 线程也更新 i+1。通过两个线程并行操作之后可能 i 的值不等于 3。而可能等于 2。因为 A 和 B 在更新变量 i 的时候拿到的 i 可能都是 1这就是一个典型的原子性问题。
可以使用synchronized或者Lock来解决上面的原子性问题。但是从jdk1.5开始,在JUC包中提供了Atomic包,提供了对于常用的数据结构的原子操作,它提供了简单、高效、以及线程安全地更新一个变量的方式。
CAS
CAS(compare-and-swap)直译即比较并交换,提供原子化的读改写能力。
CAS的思想:三个参数,一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。
类似以下代码:只是java中的cas是通过unsafe类,通过native方法实现的。
public class CASDemo {
volatile static int value = 5;
public static boolean compareAndSwap(int expect,int update){
if (value == expect){
value = update;
System.out.println("修改成功 " + expect + "=>" + update);
return true;
}
System.out.println("修改失败");
return false;
}
public static void main(String[] args) {
compareAndSwap(5,10);
compareAndSwap(5,10);
}
}
结果:
CAS会存在ABA问题,也就是一个线程把值修改成B,另一个线程把值又修改回A,最后一个线程把线程修改成C成功,但是却无法感知值以及被修改过多次,CAS只会根据expect值来判断是否修改。
CAS+自旋能够保证修改的原子性且不会把线程阻塞,只是自旋会消耗CPU的资源,所以CAS+自旋比较适用于原子操作的操作的执行时间很短,并且并发线程数不是特别高,否则会因自旋次数太多而消耗较多的cpu资源,这种情况下可能还没重量级锁性能来的好。
//第一步把5改成10,第二步把10改成5,第三步把5改成7,但是第三步是感知不到值已经被修改多次。
public static void main(String[] args) {
compareAndSwap(5,10);
compareAndSwap(10,5);
compareAndSwap(5,7);
}
但是这个问题要很多情况下不会导致业务的错误。如果实在要感知到修改,jdk也提供了一些实现类来保证,通过版本号或者一个修改标志来使得后面线程能够感知值已经被修改。
根据变量类型,可以分为六大类:
- 原子更新基本类型
AtomicBoolean、 AtomicInteger、 AtomicLong。 - 原子更新数组
AtomicIntegerArray 、 AtomicLongArray 、AtomicReferenceArray - 原子引用更新
AtomicReference 、 AtomicReferenceFieldUpdater 、AtomicMarkableReference - 原子更新字段
AtomicIntegerFieldUpdater、 AtomicLongFieldUpdater、AtomicReferenceFieldUpdater - 原子计算器
DoubleAccumulator、LongAccumulator - 原子累加器
DoubleAdder、LongAdder
原子更新基本类型
对基本类型的操作,这里三个实现的api基本一样,就只介绍AtomicInteger。
比如下面代码,开启20个线程对共享变量num进行+1,但是下面代码是线程不安全的,对num的并发修改没有做到原子性,所以得出的结果不一定等于20。
public class IntegerDemo {
static int num = 0;
static void incr(){
num++;
}
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(20);
for (int i = 0;i<20;i++){
new Thread(()->{
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
incr();
}).start();
latch.countDown();
}
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(num);
}
}
结果:
可以对incr方法进行加锁synchronized达到原子性,但是,对一个数字进行加1,是很快的一个操作,如果要用到synchronized重量级锁,会导致频繁的上下文切换,性能较低。
此时就可以通过原子操作类AtomicInteger,该类封装了一系列对Integer类型的原子更新操作,使用的就是自旋+CAS乐观锁,因为对一个变量进行修改通常是很快的一个操作,所以使用cas可以适当提高并发性能。
对上面的改造:
//main方法没改,只是改成这个。
static AtomicInteger num = new AtomicInteger(0);
static void incr(){
num.getAndIncrement();
}
结果正确:
AtomicIntegerAPI的讲解:
//这个是底层的实际存储的int值,因为cas实际上是没锁的,所以不能像synchronized那样已经保证了变量的可见性,所以要加volatile 保证变量的可见性。
private volatile int value;
//构造方法,上面那个是设置初始值。
public AtomicInteger(int initialValue);
//构造方法,使用默认值0作为初始值。
public AtomicInteger() ;
//普通的返回值,没有加原子操作,但是因为value加了volatile ,所以能够保证可见性。
public final int get();
//设置值,没有加cas保证原子性。但是保证了可见性。
public final void set(int newValue);
//设置新值,并返回旧值。使用cas保证修改操作的原子性。
public final int getAndSet(int newValue);
//比较expect和 原本的值是否相等,相等就设置成功返回true,否则设置失败返回false,具有原子性。
public final boolean compareAndSet(int expect, int update);
//与compareAndSet的效果是一样的,底层源码也是一样的。
public final boolean weakCompareAndSet(int expect, int update);
//相当于i++的 原子操作版,返回加一前的值。
public final int getAndIncrement();
//相当于i--的 原子操作版,返回减一前的值。
public final int getAndDecrement()
//相当于 i + delta的 原子操作版,返回+delta之前的值。
public final int getAndAdd(int delta)
//相当于++i的原子操作版,返回加一之后的值。
public final int incrementAndGet()
//相当于--i的原子操作版。
public final int decrementAndGet()
//相当于 i + delta的 原子操作版,返回+delta之后的值。
public final int addAndGet(int delta)
//获取和更新,这个支持复杂计算的原子操作,下面会演示,jdk1.8之后出现的。
public final int getAndUpdate(IntUnaryOperator updateFunction);
//跟上面的一样,只是这个返回更新后的值,上面返回更新前的值。
public final int updateAndGet(IntUnaryOperator updateFunction)
//也是支持复杂操作的原子性,跟上面的区别是,他可以再传一个参数x。
public final int getAndAccumulate(int x,
IntBinaryOperator accumulatorFunction)
//返回操作后的值,上面是返回操作前的值。
public final int accumulateAndGet(int x,
IntBinaryOperator accumulatorFunction)
static AtomicInteger num = new AtomicInteger(5);
static void testUpdate(){
num.getAndUpdate(new IntUnaryOperator() {
@Override
public int applyAsInt(int operand) {
//传入的就是原子类的值,通过CAS能够保证这个方法的操作的原子性。
//operand就是num的值
//先把他*2
int result = operand*2;
//再-2
result = result-2;
return result;
}
});
}
static void testCaculate(){
num.getAndAccumulate(6, new IntBinaryOperator() {
@Override
public int applyAsInt(int left, int right) {
//left是原子对象的值,而right是传入的参数值6,也就是getAndAccumulate方法第一个参数的值。
//这个方法与getAndUpdate方法的区别是他能够多传一个参数。
//这个方法使用CAS确保具有原子性
//operand就是num的值
int result = left*right;
//再-2
result = result-right;
return result;
}
});
}
原理:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// Unsafe 相当于java开的一个后门,通过本地方法可用于手动分配内存等操作。
private static final Unsafe unsafe = Unsafe.getUnsafe();
//记录value值再内存中的地址偏移量。
private static final long valueOffset;
static {
try {
//初始化偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//AtomicInteger 底层的值,使用volatile 保证可见性和有序性。
//因为CAS并不是真正意义上的锁,不像synchronized那样能保证原子性、可见性、有序性,
//而CAS只能保证原子性,所以要靠volatile关键字来保证可见性和有序性
private volatile int value;
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
//这个方法是UnSafe类的一个方法。
public final int getAndSetInt(Object obj, long valueOffset, int newValue) {
//obj :当前对象。
//valueOffset:value偏移量
//newValue新的值
int var5;
do {
//获取当前内存中最新的值
//这个方法就是通过比较内存中最新的值var5,用于更新成newValue
//通过CAS实现,如果返回false,更新失败,就通过while循环自旋不断尝试更新知道成功退出自旋。
var5 = this.getIntVolatile(obj, valueOffset);
} while(!this.compareAndSwapInt(obj, valueOffset, var5, newValue));
return var5;
}
//这个是AtomicInteger的方法
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
原子引用更新类:
上面的原子基本类型更新,这里的是原子引用更新,是对对象引用修改时,通过CAS使其具备原子性。
AtomicReference
public class AtomicReferenceDemo {
static Person mike = new Person(18,"Mike");
static AtomicReference<Person> personAtomicReference = new AtomicReference<>(mike);
public static void main(String[] args) {
Person kobe = new Person(42,"Kobe Bryant");
Person leBron = new Person(35,"LeBron James");
System.out.println("update " + mike.getName() + " to " + leBron.getName() + " " + personAtomicReference.compareAndSet(mike,leBron));
System.out.println("update " + leBron.getName() + " to " + mike.getName() + " " + personAtomicReference.compareAndSet(leBron,mike));
System.out.println("update " + mike.getName() + " to " + kobe.getName() + " " + personAtomicReference.compareAndSet(mike,kobe));
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Person{
private int age;
private String name;
}
}
上面代码通过CAS原子操作更新对象的引用。同样存在ABA问题。因为上面三个更新都成功了,但是最后一次更新无法感知到前面两次的更新。
API跟原子基本类型类差不多,只是少了一些自增自减的方法。
JUC提供了2个类来解决ABA问题,分别是AtomicMarkableReference、AtomicStampedReference。
还有这两个方法跟AtomicInteger作用差不多。用法也差不多。
public final V getAndAccumulate(V x,
BinaryOperator<V> accumulatorFunction)
public final V getAndUpdate(UnaryOperator<V> updateFunction)
AtomicStampedReference
这个原子引用更新类是通过版本号来解决ABA问题的。
public class AtomicStampedReferenceDemo {
static AtomicReferenceDemo.Person mike = new AtomicReferenceDemo.Person(18,"mike");
static AtomicStampedReference<AtomicReferenceDemo.Person> personAtomicStampedReference = new AtomicStampedReference<>(mike,0);
public static void main(String[] args) {
AtomicReferenceDemo.Person kobe = new AtomicReferenceDemo.Person(42,"Kobe Bryant");
AtomicReferenceDemo.Person leBron = new AtomicReferenceDemo.Person(35,"LeBron James");
int initStamp = 0;
System.out.println("update " + mike.getName() + " to " + leBron.getName() + " " + personAtomicStampedReference.compareAndSet(mike,leBron,initStamp,initStamp+1));
System.out.println("update " + leBron.getName() + " to " + mike.getName() + " " + personAtomicStampedReference.compareAndSet(leBron,mike,initStamp + 1,initStamp+2));
System.out.println("update " + mike.getName() + " to " + kobe.getName() + " " + personAtomicStampedReference.compareAndSet(mike,kobe,initStamp,initStamp+3));
}
}
我更新时,第一个更新使用版本号为0,然后把版本号更新为1,第二个更新使用版本号为1,然后把版本号更新为2,第三个更新使用版本号0,把版本号更新为3.
然后第一第二成功,因为使用的版本号与personAtomicStampedReference对象内部维护的版本号一样,第三个失败,因为版本号对不上。这个原子类使用预期版本号+预期对象引用解决了ABA问题。第三次更新能够感知到前面的更新而导致更新失败。
AtomicMarkableReference
AtomicMarkableReference是通过一个标记来解决ABA问题的,上面的AtomicStampedReference能够根据版本号自增清除记录被修改的次数,而这个只是通过一个布尔值记录是否被修改过。
public class AtomicMarkableReferenceDemo {
static AtomicReferenceDemo.Person mike = new AtomicReferenceDemo.Person(18,"mike");
static AtomicMarkableReference<AtomicReferenceDemo.Person> personAtomicMarkableReference = new AtomicMarkableReference<>(mike,false);
public static void main(String[] args) {
AtomicReferenceDemo.Person kobe = new AtomicReferenceDemo.Person(42,"Kobe Bryant");
AtomicReferenceDemo.Person leBron = new AtomicReferenceDemo.Person(35,"LeBron James");
System.out.println("update " + mike.getName() + " to " + leBron.getName() + " " + personAtomicMarkableReference.compareAndSet(mike,leBron,false,true));
System.out.println("update " + leBron.getName() + " to " + mike.getName() + " " + personAtomicMarkableReference.compareAndSet(leBron,mike,false,true));
}
}
因为第一个更新一集把mark标记从false改为true,所以第二个更新会失败。
原子更新数组
该类型的类用于原子更新数组里面的元素值:
AtomicIntegerArray :原子更新int数组里面的int元素。
AtomicLongArray :原子更新long数组的long引用。
AtomicReferenceArray:原子更新对象数组里面的引用。
AtomicIntegerArray
API跟AtomicInteger差不多,只是多了个下标。
public class AtomicIntegerArrayDemo {
static int[] array = new int[10];
static {
Arrays.fill(array,5);
}
static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(array);
public static void main(String[] args) {
atomicIntegerArray.compareAndSet(2,5,10);
System.out.println(atomicIntegerArray.get(2));
}
}
AtomicReferenceArray
API跟AtomicReference差不多,只是多了个下标。
public class AtomicReferenceArrayDemo {
static AtomicReferenceArray<AtomicReferenceDemo.Person> personAtomicReferenceArray;
static AtomicReferenceDemo.Person mike = new AtomicReferenceDemo.Person(18,"mike");
static AtomicReferenceDemo.Person kobe = new AtomicReferenceDemo.Person(42,"Kobe Bryant");
static AtomicReferenceDemo.Person leBron = new AtomicReferenceDemo.Person(35,"LeBron James");
static {
AtomicReferenceDemo.Person[] people = new AtomicReferenceDemo.Person[4];
people[0] = mike;
people[1] = kobe;
people[2] = leBron;
personAtomicReferenceArray = new AtomicReferenceArray<>(people);
}
public static void main(String[] args) {
AtomicReferenceDemo.Person kevin = new AtomicReferenceDemo.Person(18,"Kevin Durant");
personAtomicReferenceArray.compareAndSet(0,mike,kevin);
System.out.println(personAtomicReferenceArray.get(0));
}
}
原子更新字段
这个类型的类用于原子更新对象中的某个变量。
有以下限制:
- 字段必须是volatile类型的,在线程之间共享变量时保证立即可见.
- 字段的描述类型(修饰符public/protected/default/private)是与调用者与操作对象字段的关系一致。也就是说调用者能够直接操作对象字段,那么就可以反射进行原子操作。但是对于父类的字段,子类是不能直接操作的,尽管子类可以访问父类的字段
- 只能是实例变量,不能是类变量,也就是说不能加static关键字。
- 只能是可修改变量,不能使final变量,因为final的语义就是不可修改。
- 对于AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long类型的字段,不能修改其包装类型(Integer/Long)。如果要修改包装类型就需要使用AtomicReferenceFieldUpdater。
AtomicIntegerFieldUpdater
原子修改对象里面的int类型的成员变量。
API跟AtomicInteger差不多,他是修改对象的一个成员变量,而AtomicInteger是直接修改值。
public class AtomicIntegerFieldUpdaterDemo {
static AtomicReferenceDemo.Person mike = new AtomicReferenceDemo.Person(20,"mike");
//因为AtomicIntegerFieldUpdater是抽象方法,不能用构造器创建对象,使用提供的静态方法创建一个实现类对象AtomicIntegerFieldUpdaterImpl。
//第一个参数传要修改的对象的类型,第二个参数传要修改的成员属性名称。
//泛型变量的值是要修改的对象的类型。
static AtomicIntegerFieldUpdater<AtomicReferenceDemo.Person> personAtomicIntegerFieldUpdater =
AtomicIntegerFieldUpdater.newUpdater(AtomicReferenceDemo.Person.class,"age");
public static void main(String[] args) {
personAtomicIntegerFieldUpdater.compareAndSet(mike,20,30);
System.out.println(personAtomicIntegerFieldUpdater.get(mike));
}
}
AtomicReferenceFieldUpdater
原子修改对象里面引用类型的成员变量。
API跟AtomicReference差不多,他是修改对象的一个成员变量引用,而AtomicReference是直接修改值。
public class AtomicReferenceFieldUpdaterDemo {
static AtomicReferenceDemo.Person mike = new AtomicReferenceDemo.Person(20,"mike");
//也是一个抽象类,需要静态方法进行实例化,类的第一个泛型参数是要修改的对象的类型,第二个是要修改的成员变量的类型。
//静态方法传3个参数,第一个是要修改的对象的Class对象。
//第二个是要修改的成员变量的Class对象。
//第三个是成员变量名称。成员变量也要符合上面说的限制。
static AtomicReferenceFieldUpdater<AtomicReferenceDemo.Person,String> personStringAtomicReferenceFieldUpdater =
AtomicReferenceFieldUpdater.newUpdater(AtomicReferenceDemo.Person.class,String.class,"name");
public static void main(String[] args) {
personStringAtomicReferenceFieldUpdater.compareAndSet(mike,"mike","LeBron James");
System.out.println(personStringAtomicReferenceFieldUpdater.get(mike));
}
}
原子累加器
对一些基本类型进行原子累加,目前支持long和double类型,其实也代表了支持int short float等,虽然高精度往低精度转可能会丢失进度,但是如果要丢失精度,那就没必要转了,直接要高精度得了。
这个比AtomicLong的操作性能更高。因为采用累加单元cell,每个线程操作一个累加单元,最后将结果汇总,这样他们在累加时操作不同的cell变量,因此减少了cas重试失败,提高性能。
LongAdder
//只有一个无参构造器。初始值为0.
public LongAdder() ;
//加上传入的值。添加一个累加单元
public void add(long x)
//自增1,也是添加一个累加单元
public void increment()
//自减1,也是添加一个累加单元。
public void decrement()
//把所有计算结果汇总返回
public long sum()
//复位,就是删除所有累加单元。
public void reset()
//汇总返回并复位,相当于sum+reset。
public long sumThenReset()
public class LongAdderDemo {
static LongAdder longAdder = new LongAdder();
public static void main(String[] args) {
longAdder.add(5);
longAdder.increment();
System.out.println(longAdder.sum());
longAdder.reset();
System.out.println(longAdder.sum());
}
}
原子计算器
保证复杂运算的原子性和较为高效性。
LongAccumulator、DoubleAccumulator 目前有这两个。
//添加计算单元
public void accumulate(long x)
//计算并汇总返回
public long get()
//重置
public void reset()
//计算汇总返回后重置,等于 get + reset
public long getThenReset()
public class LongAccumulatorDemo {
//构造器有两个参数 ,一个是具体操作,一个是初始值
static LongAccumulator longAccumulator = new LongAccumulator(new LongBinaryOperator() {
@Override
public long applyAsLong(long left, long right) {
//具体操作,left为当前LongAccumulator对象的当前值,right为成员方法传入的值。
return left + right;
}
},5);
public static void main(String[] args) {
longAccumulator.accumulate(5);
longAccumulator.accumulate(6);
System.out.println(longAccumulator.get());
longAccumulator.reset();
System.out.println(longAccumulator.get());
}
}