在多线程并发中,诸如“++”或“--”元素不具备原子性,不是线程安全的,需要使用JDK中的JUC原子类
Atomic 原子操作包
JUC中的 java.util.concurrent.atomic
类的原子类主要分为以下四种:
- 基本原子类:
-
AtomicInteger
:整型原子类。 -
AtomicLong
:长整型原子类。 -
AtomicBoolean
:布尔型原子类。
-
- 数组原子类:
-
AtomicIntegerArray
:整型数组原子类。 -
AtomicLongArray
:长整型数组原子类。 -
AtomicReferenceArray
:引用类型数组原子类。
-
- 引用原子类:
-
AtomicReference
:引用类型原子类。 -
AtomicMarkableReference
:带有更新标记位的原子引用类型。 -
AtomicStampedReference
:带有更新版本号的原子引用类型。
-
- 字段更新原子类:
-
AtomicIntegerFieldUpdater
:原子更新整型字段的更新器。 -
AtomicLongFieldUpdater
:原子更新长整型字段的更新器。 -
AtomicRefernceFieldUpdater
:原子更新引用类型中的字段。
-
基础原子类 AtomicInteger
基础原子类以 AtomicInteger
为例来进行介绍。常用方法如下:
// 获取当前的值
public final int get();
// 获取当前的值,然后设置新的值
public final int getAndSet(int newValue);
// 获取当前的值,然后自增
public final int getAndIncrement();
// 获取当前的值,然后自减
public final int getAndDecrement();
// 获取当前的值,并加上预期的值
public final int getAndAdd(int delta);
// 通过CAS方式设置整数值
boolean compareAndSet(int expect, int update);
下面是一个简单的实践例子:
@Test
public void atomicIntegerTest() {
int tempVal = 0;
// 定义一个整数原子类实例,赋值到变量i
AtomicInteger i = new AtomicInteger(0);
// 取值,然后设置一个新值
tempVal = i.getAndSet(3);
System.out.println("tempVal: " + tempVal + "; i:" + i.get());
// 取值,然后自增
tempVal = i.getAndIncrement();
System.out.println("tempVal: " + tempVal + "; i:" + i.get());
// 取值,然后增加5
tempVal = i.getAndAdd(5);
System.out.println("tempVal: " + tempVal + "; i:" + i.get());
// CAS交换
boolean flag = i.compareAndSet(9, 100);
System.out.println("flag: " + flag + "; i:" + i.get());
}
数组原子类AtomicIntegerArray
以 AtomicIntegerArray
为例介绍数组原子类。包含如下常用方法:
// 获取索引为i的元素
public final int get(int i);
// 获取索引为i的元素,并将其设置为新值 newValue
public final int getAndSet(int i, int newValue);
// 获取索引为i的元素,并让该位置的元素自增
public final int getAndIncrement(int i);
// 获取索引为i的元素,并让该位置的元素自减
public final int getAndDecrement(int i);
// 获取索引为i的元素,并加上预期的值
public final int getAndAdd(int i, int delta);
// 如果输入的值等于预期值,则将位置i的元素设置为输入值update
boolean compareAndSet(int i, int expect, int update);
// 最终将索引为IDE元素设置为newValue
public final void lazySet(int i, int newValue);
下面看一个实践小例子:
@Test
public void testAtomicIntegerArray() {
int tempVal = 0;
int[] array = {1, 2, 3, 4, 5, 6};
// 包装为原子数组
AtomicIntegerArray i = new AtomicIntegerArray(array);
// 获取第0个元素并设置为2
tempVal = i.getAndSet(0, 2);
System.out.println("tempVal: " + tempVal + "; i:" + i);
// 获取第0个元素,然后自增
tempVal = i.getAndIncrement(0);
System.out.println("tempVal: " + tempVal + "; i:" + i);
// 获取第0个元素,然后增加 5
tempVal = i.getAndAdd(0, 5);
System.out.println("tempVal: " + tempVal + "; i:" + i);
}
AtomicInteger 线程安全原理
基础原子类主要通过CAS自旋和volatile实现,下面以 AtomicInteger
源码为例分析一下。
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// Unsafe类实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
// value属性值的地址偏移量
private static final long valueOffset;
static {
try {
// 计算 value 属性值的地址偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// 内部 value值,使用volatile保证线程可见性
private volatile int value;
// 初始化
public AtomicInteger(int initialValue) {
value = initialValue;
}
public AtomicInteger() {
}
// 获取当前 value 值
public final int get() {
return value;
}
// 设置值
public final void set(int newValue) {
value = newValue;
}
// 返回旧值赋予新值
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
// 封装底层CAS操作
// 对比expect和value,不同返回false,相同则将新值赋予给value,并返回true
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
// 安全自增
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
...
可以看到,其中的主要方法都通过 CAS 自旋实现,CAS自旋的主要操作为:如果一次CAS操作失败,获取最新的 value 值,再次进行 CAS 操作,直到成功。
其中的内部成员 value
使用了关键字 volatile
进行修饰,保证任何时刻总能拿到该变量的最新值,其目的在于保障变量值的线程可见性。
引用类型原子类
下面以 AtomicReference
为例来介绍一下引用类型原子类。
创建一个简单的对象类。
public class User implements Serializable {
private String uid;
private String nickName;
public volatile int age;
public User(String uid, String nickName) {
this.uid = uid;
this.nickName = nickName;
}
public String getUid() {
return uid;
}
public void setUid(String uid) {
this.uid = uid;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
@Override
public String toString() {
return "User{" +
"uid='" + uid + '\'' +
", nickName='" + nickName + '\'' +
", age=" + age +
'}';
}
}
然后使用 AtomicReference
修改类的引用。
@Test
public void testAtomicReference() {
AtomicReference<User> userRef = new AtomicReference<User>();
User user = new User("1", "张三");
userRef.set(user);
System.out.println("userRef is:" + userRef.get());
User updateUser = new User("2", "李四");
boolean success = userRef.compareAndSet(user, updateUser);
System.out.println("cas result is:" + success);
System.out.println("after cas, userRef is:" + userRef.get());
}
可以得到输出:
userRef is:User{uid='1', nickName='张三', age=0}
cas result is:true
after cas, userRef is:User{uid='2', nickName='李四', age=0}
需要注意的是,要通过 compareAndSet
方法来修改 AtomicReference
类型的引用包装值。
另外,还要说明,使用原子引用,只能保障对象引用的原子操作,对被包装对象的字段值修改时不能保证原子性。
属性更新原子类
这里以 AtomicIntegerFieldUpdater
为例进行介绍。属性更新原子类保障属性安全更新的流程大致需要两步:
- 更新的对象属性必须使用
public volatile
修饰符。 - 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须调用静态方法
newUpdater
创建一个更新器,并且需要设置想要更新的类和属性。
下面是一个简单的实战小例子。
@Test
public void testAtomicIntegerFieldUpdater() {
// 创建更新器
AtomicIntegerFieldUpdater<User> updater = AtomicIntegerFieldUpdater.newUpdater(User.class,
"age");
User user = new User("1", "张三");
// 更新user的age属性
System.out.println(updater.getAndIncrement(user));
System.out.println(updater.getAndAdd(user, 100));
System.out.println(updater.get(user));
}
ABA问题
CAS操作如果使用不合理,会存在 ABA 问题。
ABA 问题介绍
通过一个例子来说明 ABA 问题。
一个线程A 从内存位置M中取出V1,另一个线程B也取出V1。现在假设线程B进行了一些操作之后将M位置的数据V1变成了V2,然后又有一些操作之后将V2变成了V1。之后,线程A进行CAS操作,发现M位置的数据仍然是V1,然后线程A操作成功。
尽管线程A的CAS操作陈宫,但是A操作的数据V1可能已经不是之前的V1,而是被线程B替换过的V1,这就是ABA问题。
ABA问题解决方案
很多乐观锁的实现版本都是使用版本号(Version)方式来解决ABA问题。乐观锁每次在执行数据的修改操作时都会带上一个版本号,版本号和数据的版本号一致就可以执行修改操作并对版本号执行加1操作,否则执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题。
使用 AtomicStampedReference 解决ABA问题
参考乐观锁的版本号,JDK提供了一个 AtomicStampedReference
类来解决ABA问题。它在CAS的基础上增加了一个Stamp
,用来观察数据是否发生变化。
AtomicStampedReference
的 compareAndSet()
方法首先检查当前的对象引用值是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,就以原子方式将引用值和标志的值更新为给定的更新值。
AtomicStampReference
的构造器有两个参数,具体如下:
AtomicStampedReference(V initialRef, int initialStamp);
包含常用方法如下:
//获取被封装的数据
public V getReference();
// 获取被封装的数据的版本标志
public int getStamp();
CAS 操作定义如下:
public boolean compareAndSet {
V expectedReference, // 预发引用值
V newReference, // 更新后的引用值
int expectedStamp, // 预期标志
int newStamp // 更新后的标志
}
使用 AtomicMarkableReference 解决ABA 问题
AtomicMarkableReference
是 AtomicStampedReference
的简化版,只关心是否修改过,其标记 mark
是 boolean
类型。
AtomicMarkableReference
适用于只要知道对象是否被修改过,而不适用于对象被反复修改的场景。
网友评论