在Java中的并发包中了提供了以下几种类型的原子类来来解决线程安全的问题。分为
- 基本数据类型原子类
- 数组类型原子类
- 引用类型原子类
- 字段类型原子类。
因为其内部原理都差不多一致。这里会对每种类型的原子类抽一个来介绍。
一、原子类的使用方式
public class AtomicTest{
//public static volatile int race=0;//1
public static AtomicInteger race=new AtomicInteger(0);//2
public static void increase(){
race.incrementAndGet();
}
//public static synchronized void increase(){
// race++;
//}
private static final int THREADS_COUNT=20;
public static void main(String[]args)throws Exception{
Thread[]threads=new Thread[THREADS_COUNT];
for(int i=0;i<THREADS_COUNT;i++){
threads[i]=new Thread(new Runnable(){
@Override
public void run(){
for(int i=0;i<10000;i++){
increase();
}
}
});
threads[i].start();
}
//所有线程都执行完毕后,打印结果
while(Thread.activeCount()>1) Thread.yield();//yield在于阻塞当前线程
System.out.println(race);
}
}
- 如果是1处使用的代码volatile,则结果并不是200. 这是由于 volatile只能保证可见性并不能保证 a++自增操作的原子性,多线程在读写时可能出现覆盖
- 把“race++”操作或increase()方法用synchronized同步块包裹起来当然是一个办法,使用synchronized修饰后,increase方法变成了一个原子操作,因此是肯定能得到正确的结果。但这里我们暂时不关注这方面
- 使用 AtomicInteger 代替int后,程序输出了正确的结果 200,一切都要归功于incrementAndGet()方法的原子性。
二、基本数据类型原子类
基本数据类型原子类主要为以下几种:
- AtomicBoolen: boolean类型原子类
- AtomicInteger: int类型原子类
- AtomicLong: long类型原子类
这里我们以AtomicInteger来进行讲解,具体代码如下:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
private static final long VALUE;
private volatile int value;//注意该值用volatile修饰
public AtomicInteger(int initialValue) {
value = initialValue;
}
//以原子的方式将输入的值与ActomicInteger中的值进行相加,
//注意:返回相加前ActomicInteger中的值
public final int getAndAdd(int delta) {
return U.getAndAddInt(this, VALUE, delta);
}
//以原子的方式将输入的值与ActomicInteger中的值进行相加,
//注意:返回相加后的结果
public final int addAndGet(int delta) {
return U.getAndAddInt(this, VALUE, delta) + delta;
}
//以原子方式将当前ActomicInteger中的值加1,
//注意:返回相加前ActomicInteger中的值
public final int getAndIncrement() {
return U.getAndAddInt(this, VALUE, 1);
}
//以原子方式将当前ActomicInteger中的值加1,
//注意:返回相加后的结果
public final int incrementAndGet() {
return U.getAndAddInt(this, VALUE, 1) + 1;
}
//省略部分代码...
}
在上述代码中,我只留了AtomicInteger 类一部分常用的方法。大家在使用其内部方法时一定要注意其返回的结果。例如getAndAdd()与addAndGet()方法之间的返回值的区别。
2.1 原理解析
其实我们在上一章已经有所讲解incrementAndGet
,这里再重述一次getAndAddInt()
过程加深记忆
//AtomicInteger内部会调用其中sun.misc.Unsafe方法中getAndAddInt的方法。
public final int getAndAdd(int delta) {
return U.getAndAddInt(this, VALUE, delta);
}
//sun.misc.Unsafe方法中getAndAddInt方法又会调用jdk.internal.misc.Unsafe的getAndAddInt,具体代码如下:
public final int getAndAddInt(Object o, long offset, int delta) {
return theInternalUnsafe.getAndAddInt(o, offset, delta);
}
//jdk.internal.misc.Unsafe的getAndAddInt()方法的声明如下:
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);//先获取内存中存储的值
} while (!weakCompareAndSetInt(o, offset, v, v + delta));//如果不是期望的结果值,就一直循环
return v;
}
public final boolean weakCompareAndSetInt(Object o, long offset,
int expected,
int x) {//该函数返回值代表CAS操作是否成功
return compareAndSetInt(o, offset, expected, x);//执行CAS操作
}
getAndAddInt()方法在一个无限循环中(也就是CAS的自旋),不断尝试将一个比当前值大delta的新值赋给自己。如果失败了,那说明在执行“获取-设置”操作的时候值已经有了修改,于是再次循环进行下一次操作,直到设置成功为止。
三、数组类型原子类
对于数组类型的原子类,在Java中,主要通过原子的方式更新数组里面的某个元素,数组类型原子类主要有以下几种:
- AtomicIntegerArray:Int数组类型原子类
- AtomicLongArray:long数组类型原子类
- AtomicReferenceArray:引用类型原子类(关于AtomicReferenceArray即引用类型原子类会在下文介绍)
这里我们还是以AtomicIntegerArray为例,因为其内部原理都是循环CAS操作,所以我们这里就描述其使用方式,具体代码如下:
class AtomicDemo {
private int[] value = new int[]{0, 1, 2};
private AtomicIntegerArray mAtomicIntegerArray = new AtomicIntegerArray(value);
private void doAdd() {
for (int i = 0; i < 5; i++) {
int value = mAtomicIntegerArray.addAndGet(0, 1);
System.out.println(Thread.currentThread().getName() + "--->" + value);
}
}
public static void main(String[] args) {
AtomicDemo demo = new AtomicDemo();
new Thread(demo::doAdd, "线程1").start();
new Thread(demo::doAdd, "线程2").start();
}
}
/程序输出结果如下:
线程1--->1
线程1--->2
线程1--->4
线程2--->3
线程1--->5
线程2--->6
线程1--->7
线程2--->8
线程2--->9
线程2--->10
四、引用类型原子类
在Java并发编程之Java CAS操作文章中我们曾经提到过两个问题
- 第一个问题:虽然我们能通过循环CAS操作来完成对一个变量的原子操作,但是对于多个变量进行操作时,自旋CAS操作就不能保证其原子性。
- 第二个问题:ABA问题,因为CAS在操作值的时候,需要检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现她的值并没有发生变化。那么会导致程序出问题。
为了解决上述提到的两个问题,Java为我们提供了AtomicReference等系列引用类型原子类,来保证引用对象之间的原子性,即可以把多个变量放在一个对象里来进行CAS操作与ABA问题。主要类型原子类如下:
- AtomicReference:
- AtomicReferenceFieldUpdater:
- AtomicMarkableReference:
- AtomicStampedReference:
4.1 多个变量的CAS操作
关于引用类型的原子类,内部都调用的是compareAndSwapObject()方法来实现CAS操作的。
这里我们先解决第一个问题,关系多个变量的CAS操作,我们先以AtomicReference来进行讲解,具体代码如下所示:
class AtomicDemo {
Person mPerson = new Person("红红", 1);
private AtomicReference<Person> mAtomicReference = new AtomicReference<>(mPerson);
private class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
}
private void updatePersonInfo(String name, int age) throws Exception {
System.out.println(Thread.currentThread().getName() + "更新前--->" + mAtomicReference.get().name + "---->" + mAtomicReference.get().age);
mAtomicReference.getAndUpdate(person -> new Person(name, age));
}
public static void main(String[] args) {
AtomicDemo demo = new AtomicDemo();
new Thread(() -> demo.updatePersonInfo("蓝蓝", 2), "线程1").start();
Thread.sleep(1000);
System.out.println("暂停一秒--->" + demo.mAtomicReference.get().name + "---->" + demo.mAtomicReference.get().age);
System.out.println("更新后---->" + demo.mAtomicReference.get().name + "---->" + demo.mAtomicReference.get().age);
}
}
//输出结果
线程1更新前--->红红---->1
暂停一秒--->蓝蓝---->2
更新后---->蓝蓝---->2
上述代码中创建了Person 类,且当前AtomicReference传入的是当前 mPerson =new Person(“红红”, 1),在Main方法中创建线程1使其调用mAtomicReference.getAndUpdate(new Person(“蓝蓝”,2))来更新Person信息。
主线程中休眠一秒后,获取更新结果并打印。从结果上来看,的确是对多个变量进行了更新的操作。
4.2 ABA问题
class AtomicDemo {
Person mPerson = new Person("红红", 1);
private AtomicStampedReference<Person> mAtomicReference = new AtomicStampedReference<>(mPerson, 1);
private class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
}
/**
* 更新信息
*
* @param name 名称
* @param age 年龄
* @param oldStamp CAS操作比较的旧的版本
* @param newStamp 希望更新后的版本
*/
private void updatePersonInfo(String name, int age, int oldStamp, int newStamp) {
System.out.println(Thread.currentThread().getName() + "更新前--->" + mAtomicReference.getReference().name + "---->" + mAtomicReference.getReference().age);
mAtomicReference.compareAndSet(mPerson, new Person(name, age), oldStamp, newStamp);
}
public static void main(String[] args) throws Exception {
AtomicDemo demo = new AtomicDemo();
new Thread(() -> demo.updatePersonInfo("蓝蓝", 2, 1, 2), "线程1").start();
Thread.sleep(1000);
System.out.println("暂停一秒--->" + demo.mAtomicReference.getReference().name + "---->" + demo.mAtomicReference.getReference().age);
new Thread(() -> demo.updatePersonInfo("花花", 3, 1, 3), "线程2").start();
Thread.sleep(1000);
System.out.println("更新后---->" + demo.mAtomicReference.getReference().name + "---->" + demo.mAtomicReference.getReference().age);
}
}
//输出结果
线程1更新前--->红红---->1
暂停一秒--->蓝蓝---->2
线程2更新前--->蓝蓝---->2
更新后---->蓝蓝---->2
在上述代码中,我们使用AtomicStampedReference类,其中在使用该类的时候,需要传入一个类似于版本(你也可以叫做邮戳,时间戳等,随你喜欢)的int类型的属性。
在Main方法中我们分别创建了2个线程来进行CAS操作,其中线程1想做的操作是将版本为1的mPerson(“红红”,1)修改为版本为2的Person(“蓝蓝,2”)。当线程1执行完毕后,紧接着线程2开始执行,线程2想做的操作是将版本为1的mPerson(“红红”,1)修改为版本3的Person(“花花”,3)。从程序输出结果可以看出,线程2的操作是没有执行的。
4.3 字段类型原子类
如果需要更新某个类中的某个字段,在Actomic系列中,Java提供了以下几个类来实现:
- AtomicIntegerFieldUpdater:int类型字段原子类
- AtomicLongFieldUpdater:long类型字段原子类
- AtomicReferenceFieldUpdater:引用型字段原子类
上面所说的三个类原理都差不多,这里我们以AtomicIntegerFieldUpdate类来讲解,具体代码如下:
lass AtomicDemo {
Person mPerson = new Person("红红", 1);
private AtomicIntegerFieldUpdater<Person> mFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(Person.class, "age");
private class Person {
String name;
volatile int age;//使用volatile修饰
Person(String name, int age) {
this.name = name;
this.age = age;
}
}
/**
* 更新信息
*
* @param age 年龄
*/
private void updatePersonInfo(int age) {
System.out.println("更新前--->" + mPerson.age);
mFieldUpdater.addAndGet(mPerson, age);
}
private int getUpdateInfo() {
return mFieldUpdater.get(mPerson);
}
public static void main(String[] args) throws Exception {
AtomicDemo demo = new AtomicDemo();
new Thread(() -> demo.updatePersonInfo(12), "线程1").start();
Thread.sleep(1000);
System.out.println("更新后--->" + demo.getUpdateInfo());
}
}
//输出结果
更新前--->1
更新后--->13
这里对AtomicIntegerFieldUpdate不在进行过多的描述,大家需要主要的是在使用字段类型原子类的时候,需要进行更新的字段,需要通过volatile来修饰。