最近项目中经常用到Atomic原子类,撰写此贴以记录
原文链接:https://blog.csdn.net/weixin_43849277/article/details/108377793
我添加了小部分内容并做了展示优化
说明:
- 在JDK1.5版本之前,多行代码的原子性主要通过synchronized关键字进行保证。
- 在JDK1.5版本,Java提供了原子类型专门确保变量操作的原子性
- 原子类型位于java.util.concurrent.atomic包下,其主要类如下:
首先,以
AtomicInteger
类源码为例简要说明Atomic原子类为什么是线程安全的
// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替 换”的作用)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
private volatile int value;
AtomicInteger
类主要利用CAS
(compare and swap
) + volatile
和 native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升。
CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。 UnSafe类的objectFieldOffset()
方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址。另外value是一个volatile
变量,在内存中可见,因此JVM可以保证任何时刻任何线程总能拿到该变量的最新值。
Atomic原子类常见分类及常用方法
1. 基本类型:使用原子的方式更新基本类型
- AtomicInteger
- AtomicLong
- AtomicBoolean
三种类的方法基本一样,下面就以AtomicInteger 为例说明常用API:
public final int set() //设一个值
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果当前值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue) //最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
2. 数组类型:使用原子的方式更新数组中某个元素
- AtomicIntegerArray
- AtomicLongArray
AtomicReferenceArray
(说明:引用类型数组原子类,即对应数组中存放的元素为对象形式)
三种类的方法基本一样,下面就以AtomicIntegerArray 为例说明常用API:
public final int get(int i) //获取 index=i 位置元素的值
public final int set(int i, int newValue) //为 index=i 位置元素设新值
public final int getAndSet(int i, int newValue) //返回 index=i 位置的当前的值,并将其设置为新值:newValue
public final int getAndIncrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自增
public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减
public final int getAndAdd(int i, int delta) //获取 index=i 位置元素的值,并加上预期的值
boolean compareAndSet(int i, int expect, int update) //如果index=i 位置的值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update)
public final void lazySet(int i, int newValue) //最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
3. 引用类型:使用原子的方式更新某个对象
-
AtomicReference:引用类型原子类
-
AtomicStampedReference:AtomicReference的扩展版,增加了一个参数stamp标记,这里是为了解决了AtomicInteger和AtomicLong的操作会出现ABA问题。
-
AtomicMarkableReference
:与AtomicStampedReference差不多,只不过第二个参数不是用的int作为标志,而用boolean类型做标记,具体用法看后面讲解。
AtomicReference
用法示例:
public class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return this.name;
}
public void setName(final String name) {
this.name = name;
}
public int getAge() {
return this.age;
}
public void setAge(final int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class AtomicReferenceTest {
public static void main(String[] args) {
AtomicReference<User> u = new AtomicReference<>();
User user1 = new User("金厂长",35);
User user2 = new User("木易阿婆",26);
User user3 = new User("快乐的小lau",22);
u.set(user1);
// 查看当前对象并设为新对象user2
System.out.println("当前对象为:"+u.getAndSet(user2)+",并设置新对象为user2");
System.out.println("当前对象为:"+u.get());
System.out.println("如果当前对象为user2,就把当前对象设为user3,否则不操作");
u.compareAndSet(user2,user3); //如果当前对象为user2则把当前对象设为user3
System.out.println("当前对象为:"+u.get());
}
}
结果:
AtomicStampedReference
与AtomicMarkableReference
类用法:
说明:
ABA问题
:简单讲就是多线程环境,2次读写中一个线程修改A->B,然后又B->A,另一个线程看到的值未改变,又继续修改成自己的期望值。当然我们如果不关心过程,只关心结果,那么这个就是无所谓的ABA问题。
为了解决ABA问题,伟大的java为我们提供了AtomicMarkableReference和AtomicStampedReference类,为我们解决了问题
AtomicStampedReference是利用版本戳的形式记录了每次改变以后的版本号,这样的话就不会存在ABA问题了
代码示例:
public class AtomicStampedReferenceTest {
public static void main(String[] args) throws InterruptedException{
final Integer init_Ref = 0, init_Stamp = 0;
AtomicStampedReference<Integer> asr = new AtomicStampedReference<>(init_Ref,init_Stamp);
System.out.println("init_Ref为:"+asr.getReference() + " ====== init_Stamp为:"+asr.getStamp());
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
Integer ref = asr.getReference();
Integer stamp = asr.getStamp();
//与前面AtomicReference的compareAndSet不同的是,增加了一个stamp标记比较,ref与stamp同时与
// 当前的ref、stamp相同时才进行 + 操作
System.out.println(ref + " ====== " + stamp + " ====== "
+ asr.compareAndSet(ref, ref + 10, stamp, stamp + 1));
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
Integer ref = asr.getReference();
// 当前的ref相同,但此时版本号不同,操作不执行返回false
System.out.println(ref + " ====== " + init_Stamp + " ====== "
+ asr.compareAndSet(ref, ref + 10, init_Stamp, init_Stamp + 1));
}
});
t1.start();
t1.join(); //只是为了让代码有序执行
t2.start();
t2.join();
System.out.println("最终的结果为:"+ asr.getReference() + " ====== " + asr.getStamp());
}
}
结果:
说明:
可以看出,第一次ref、stamp都与输入值相等,因此执行ref+10,和stamp+1,此时ref=10,stamp=1。第二个线程中输入的ref与当前ref值相同,但是init_Stamp=0
与当前stamp=1 不等,因此不执行。总的来说就是除了对比ref,又增加了一个stamp来判断到底操不操作。
AtomicMarkableReference
与AtomicStampedReference
不同的是将int stamp改为了boolean类型的mark做标记。同样的例子:
public class AtomicMarkableReferenceTest {
public static void main(String[] args) throws InterruptedException{
final Integer init_Ref = 0;
final Boolean init_Mark = false;
AtomicMarkableReference<Integer> amr = new AtomicMarkableReference<>(init_Ref,init_Mark);
System.out.println("init_Ref为:"+amr.getReference() + " ====== init_Mark为:"+amr.isMarked());
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
Integer ref = amr.getReference();
Boolean mark = amr.isMarked();
//与前面AtomicReference的compareAndSet不同的是,增加了一个stamp标记比较,ref与stamp同时与
// 当前的ref、stamp相同时才进行 + 操作
System.out.println(ref + " ====== " + mark + " ====== "
+ amr.compareAndSet(ref, ref + 10, mark, true));
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
Integer ref = amr.getReference();
// 当前的ref相同,但此时版本号不同,操作不执行返回false
System.out.println(ref + " ====== " + init_Mark + " ====== "
+ amr.compareAndSet(ref, ref + 10, init_Mark, true));
}
});
t1.start();
t1.join(); //只是为了让代码有序执行
t2.start();
t2.join();
System.out.println("最终的结果为:"+ amr.getReference() + " ====== " + amr.isMarked());
}
}
结果:
4. 对象的属性修改类型:使用原子的方式更新某个类中某个字段
-
AtomicIntegerFieldUpdater:原子更新整形字段的更新器
-
AtomicLongFieldUpdater:原子更新长整形字段的更新器
-
AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器
以AtomicIntegerFieldUpdater 为例介绍一下简单使用方法:
public class User {
private String name;
volatile int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return this.name;
}
public void setName(final String name) {
this.name = name;
}
public int getAge() {
return this.age;
}
public void setAge(final int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class AtomicIntegerFieldUpdaterTest {
public static void main(String[] args) {
User user = new User("菜鸡",28);
AtomicIntegerFieldUpdater<User> u = AtomicIntegerFieldUpdater.newUpdater(User.class,"age");
u.addAndGet(user,5);
System.out.println(user.toString());
}
}
这里值得注意的是:使用AtomicIntegerFieldUpdater.newUpdater修改属性时:
被修改的属性必须是volatile类型的,在线程之间共享变量时保证立即可见,
属性的修饰符(public/protected/default/private)要保证当前操作对该属性可以直接进行,比如当我们用private
volatile int age 时就会报错,因为private修饰时,外部无法访问也无法修改。
只能是实例变量,不能是类变量,也就是说不能加static关键字。 只能是可修改变量,不能使final变量,因为final的语义就是不可修改。
对于AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long类型的字段,不能修改其包装类型(Integer/Long)。如果要修改包装类型就需要使用AtomicReferenceFieldUpdater。
5. 原子累加器:JDK1.8之后出现, AtomicLong和AtomicDouble的升级类型,专门用于数据统计,性能更高