一、Java中的13个原子操作类
在多线程环境下执行i++这个操作,并不能保证变量i的线程安全性。因为i++其实不是一个原子操作,i++是由以下3个步骤组成的:
(1)取出变量i的值。
(2)执行累加操作。
(3)累加后的结果写回变量i。
在多线程竞争环境下,以上3个步骤可能被不同的线程按照不同的顺序执行,因此无法保证在多线程环境下变量i的线程安全。在这种场景下,使用synchronized/lock等加锁方式来保证代码块互斥访问可以实现变量线程安全。
除了加锁方式外,Java提供的13个原子操作类也可以解决上述问题。Java中的13个原子操作类都是基于无锁方案实现的。与加锁的方案相比,原子操作类并没有加锁
、解锁和线程切换的消耗
。
java.util.concurrent.atomic包提供了多类用法简单、性能高效、线程安全的原子操作类。主要包含以下四种类型:
原子更新基本类型
原子更新数组
原子更新引用
原子更新属性(字段)
1. 原子更新基本类型
其中原子更新基本类型主要是如下三个类:
AtomicBoolean
类用于原子性地更新布尔类型。
AtomicInteger
类用于原子性地更新整数类型。
AtomicLong
类用于原子性地更新长整型类型。
以AtomicInteger案例代码:
importjava.util.concurrent.atomic.AtomicInteger;publicclassAtomicIntegerTest{publicstaticvoidmain(String[]args){inttemvalue=0;AtomicIntegeri=newAtomicInteger(0);temvalue=i.getAndSet(3);System.out.println("temvalue:"+temvalue+";i:"+i);//temvalue:0;i:3temvalue=i.getAndIncrement();System.out.println("temvalue:"+temvalue+";i:"+i);//temvalue:3;i:4temvalue=i.getAndAdd(5);System.out.println("temvalue:"+temvalue+";i:"+i);//temvalue:4;i:9}}
2. 原子更新数组
原子更新数组指的是以原子的方式更新数组中的某个索引位置的元素,主要包括以下3个类:
AtomicIntegerArray
类用于原子性地更新整数类型的数组。
AtomicLongArray
类用于原子性地更新长整型类型的数组。
AtomicReferenceArray
类用于原子性地更新引用类型的数组。
其中常用的方法有:
int addAndGet(int i, int delta)
:将输入值和对应索引i位置的元素相加。
boolean compareAndSet(int i, int expect, int update)
:如果当前值等于预期值,则将数组索引为i位置的元素设置为update值。
举个AtomicIntegerArray例子:
importjava.util.concurrent.atomic.AtomicIntegerArray;publicclassDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{AtomicIntegerArrayarray=newAtomicIntegerArray(newint[]{0,0});System.out.println(array);System.out.println(array.getAndAdd(1,2));System.out.println(array);}}输出结果:[0,0]0[0,2]
下面看一下compareAndSet()
的源码实现,它的源码如下:
publicfinalbooleancompareAndSet(inti,intexpect,intupdate){returncompareAndSetRaw(checkedByteOffset(i),expect,update);}privatebooleancompareAndSetRaw(longoffset,intexpect,intupdate){returnunsafe.compareAndSwapInt(array,offset,expect,update);}
最终实现上还是调用了Unsafe的compareAndSwapInt()
。
publicfinalnativebooleancompareAndSwapInt(Objectvar1,longvar2,intvar4,intvar5);
3. 原子更新引用类型
原子更新引用类型包括如下三个类:
AtomicReference
:更新引用类型。
AtomicReferenceFieldUpdate
:更新引用类型中的字段。
AtomicMarkableReference
:更新带有标记位的引用类型,可以更新一个布尔类型的标记位和引用类型。
这三个类提供的方法都差不多,首先构造一个引用对象,然后把引用对象set进Atomic类,然后调用
compareAndSet
等一些方法去进行原子操作,原理都是基于Unsafe实现。
举个AtomicReference例子:
importjava.util.concurrent.atomic.AtomicReference;publicclassAtomicReferenceTest{publicstaticvoidmain(String[]args){//创建两个Person对象,它们的id分别是101和102。Personp1=newPerson(101);Personp2=newPerson(102);//新建AtomicReference对象,初始化它的值为p1对象AtomicReferencear=newAtomicReference(p1);//通过CAS设置ar。如果ar的值为p1的话,则将其设置为p2。ar.compareAndSet(p1,p2);Personp3=(Person)ar.get();System.out.println("p3is"+p3);System.out.println("p3.equals(p1)="+p3.equals(p1));}}classPerson{volatilelongid;publicPerson(longid){this.id=id;}publicStringtoString(){return"id:"+id;}}
结果输出:
p3isid:102p3.equals(p1)=false
结果说明:
新建AtomicReference对象ar时,将它初始化为p1。
紧接着,通过CAS函数对它进行设置。如果ar的值为p1的话,则将其设置为p2。
最后,获取ar对应的对象,并打印结果。p3.equals(p1)的结果为false,这是因为Person并没有覆盖equals()方法,而是采用继承自Object.java的equals()方法;而Object.java中的equals()实际上是调用"=="去比较两个对象,即比较两个对象的地址是否相等。
4. 原子更新字段类
原子更新字段类自然是用于更新某个类中的字段值,主要包含如下四个类:
AtomicIntegerFieldUpdater
:更新整型字段的更新器。
AtomicLongFieldUpdater
:更新长整型字段的更新器。
AtomicStampedFieldUpdater
: 原子更新带有版本号的引用类型。
AtomicStampedReference
:更新带有版本号的引用类型,需要更新版本号和引用类型,主要为了解决ABA问题。
使用步骤为:
第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
第二步,更新类的字段必须使用public volatile修饰。
举个例子:
publicclassTestAtomicIntegerFieldUpdater{publicstaticvoidmain(String[]args){TestAtomicIntegerFieldUpdatertIA=newTestAtomicIntegerFieldUpdater();tIA.doIt();}publicAtomicIntegerFieldUpdater<DataDemo>updater(Stringname){returnAtomicIntegerFieldUpdater.newUpdater(DataDemo.class,name);}publicvoiddoIt(){DataDemodata=newDataDemo();System.out.println("publicVar="+updater("publicVar").getAndAdd(data,2));/**由于在DataDemo类中属性value2/value3,在TestAtomicIntegerFieldUpdater中不能访问**///System.out.println("protectedVar="+updater("protectedVar").getAndAdd(data,2));//System.out.println("privateVar="+updater("privateVar").getAndAdd(data,2));//System.out.println("staticVar="+updater("staticVar").getAndIncrement(data));//报java.lang.IllegalArgumentException/**下面报异常:mustbeinteger**///System.out.println("integerVar="+updater("integerVar").getAndIncrement(data));//System.out.println("longVar="+updater("longVar").getAndIncrement(data));}}classDataDemo{publicvolatileintpublicVar=3;protectedvolatileintprotectedVar=4;privatevolatileintprivateVar=5;publicvolatilestaticintstaticVar=10;//publicfinalintfinalVar=11;publicvolatileIntegerintegerVar=19;publicvolatileLonglongVar=18L;}
再说下对于AtomicIntegerFieldUpdater 的使用稍微有一些限制和约束,约束如下:
字段必须是volatile
类型的,在线程之间共享变量时保证立即可见。
字段的描述类型(修饰符public/protected/default/private
)是与调用者与操作对象字段的关系一致。也就是说调用者能够直接操作对象字段,那么就可以反射进行原子操作。但是对于父类的字段,子类是不能直接操作的,尽管子类可以访问父类的字段。
只能是实例变量,不能是类变量,也就是说不能加static
关键字。
只能是可修改变量,不能使final
变量,因为final的语义就是不可修改。实际上final
的语义和volatile
是有冲突的,这两个关键字不能同时存在。
对于AtomicIntegerFieldUpdater
和AtomicLongFieldUpdater
只能修改int/long类型的字段,不能修改其包装类型(Integer/Long)。如果要修改包装类型就需要使用AtomicReferenceFieldUpdater
。
二、ABA问题
ABA问题发生在多线程环境中,当某线程连续读取同一块内存地址两次,两次得到的值一样,它简单地认为“此内存地址的值并没有被修改过”,然而,同时可能存在另一个线程在这两次读取之间把这个内存地址的值从A修改成了B又修改回了A,这时还简单地认为“没有修改过”显然是错误的。
比如,两个线程按下面的顺序执行:
(1)线程1读取内存位置X的值为A;
(2)线程1阻塞了;
(3)线程2读取内存位置X的值为A;
(4)线程2修改内存位置X的值为B;
(5)线程2修改又内存位置X的值为A;
(6)线程1恢复,继续执行,比较发现还是A把内存位置X的值设置为C;
可以看到,针对线程1来说,第一次的A和第二次的A实际上并不是同一个A。
三、AtomicStampedReference解决ABA问题
AtomicStampedReference
主要维护包含一个对象引用以及一个可以自动更新的整数"stamp"的pair对象来解决ABA问题。
3.1 AtomicStampReference类的属性
/**volatile修饰的pair*/privatevolatilePair<V>pair;
3.2 AtomicStampReference类的构造方法
/***VinitialRef:任意类型的初始引用对象*intinitialStamp:Int类型的初始版本号*/publicclassAtomicStampedReference<V>{privatestaticclassPair<T>{finalTreference;//维护对象引用finalintstamp;//用于标志版本privatePair(Treference,intstamp){this.reference=reference;this.stamp=stamp;}static<T>Pair<T>of(Treference,intstamp){returnnewPair<T>(reference,stamp);}}
3.3 AtomicStampReference的内部类Pair
privatestaticclassPair<T>{//引用finalTreference;//版本号finalintstamp;//构造方法privatePair(Treference,intstamp){this.reference=reference;this.stamp=stamp;}//生成一个Pairstatic<T>Pair<T>of(Treference,intstamp){returnnewPair<T>(reference,stamp);}}
3.4 comparedAndSwap()方法
importjava.util.concurrent.atomic.AtomicIntegerArray;publicclassDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{AtomicIntegerArrayarray=newAtomicIntegerArray(newint[]{0,0});System.out.println(array);System.out.println(array.getAndAdd(1,2));System.out.println(array);}}输出结果:[0,0]0[0,2]0
如果元素值和版本号都没有变化,并且和新的也相同,返回true;
如果元素值和版本号都没有变化,并且和新的不完全相同,就构造一个新的Pair对象并执行CAS更新pair。
可以看到,java中的实现跟我们上面讲的ABA的解决方法是一致的。
首先,使用版本号控制;
其次,不重复使用节点(Pair)的引用,每次都新建一个新的Pair来作为CAS比较的对象,而不是复用旧的;
最后,外部传入元素值及版本号,而不是节点(Pair)的引用。
案例:
importjava.util.concurrent.atomic.AtomicIntegerArray;publicclassDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{AtomicIntegerArrayarray=newAtomicIntegerArray(newint[]{0,0});System.out.println(array);System.out.println(array.getAndAdd(1,2));System.out.println(array);}}输出结果:[0,0]0[0,2]1
四、总结
(1)在多线程环境下使用无锁结构要注意ABA问题;
(2)ABA的解决一般使用版本号来控制,并保证数据结构使用元素值来传递,且每次添加元素都新建节点承载元素值;
(3)AtomicStampedReference
内部使用Pair来存储元素值及其版本号;
AtomicMarkableReference也可以解决ABA问题,它不是维护一个版本号,而是维护一个boolean类型的标记,标记值有修改,了解一下。
参考文献:
Java并发编程-无锁CAS与Unsafe类及其并发包Atomic
作者:清粥为伴&掘金著作权归作者所有。