大家好,我是一天喝 3 升水的七哥,今天继续卷。
我们为什么一定要学习 Atomic 包下的这些原子操作类呢?下面告诉你原因。
Java中有那么一些类,是以Atomic开头的。这一系列的类我们称之为原子操作类。
以最简单的类AtomicInteger为例,它相当于一个int变量,我们执行Int的 i++ 的时候并不是一个原子操作。而使用AtomicInteger的incrementAndGet却能保证原子操作。更新变量这种场景下效果和 synchronized 相同,却要简单高效的多。
这篇文章大家带着下面的三个问题出发,相信你会有所收获的。
-
Atomic包中的原子操作类具体有什么? -
如何使用? -
如何实现的?
文章中的示例代码,我们要尽量多动手去实践操作,自己写出来和仅仅看懂是不一样的。知识和技能是不一样的,看会的那叫知识,动手实践掌握的才叫技能。
看完上面的图,我们会发现原子操作类分为4种类型的原子更新方式,那为什么要提供这么多种分类呢?显而易见,是因为我们Java中的变量有很多种类型,Atomic包是为了让我们在不同的场景选择更加实用的原子操作类。
原子更新基本类型
我们Java中的基本数据类型是用的非常多的,比如说计算 人数、金额、更新条件变量布尔值等。Atomic包针对基本数据类型提供了3个类,想知道他们具体是什么,如何使用?那么接着往下看,你会有所收获。
-
AtomicBoolean -
AtomicInteger -
AtomicLong
以上三个类,看名字就能发现它们是分别用来原子更新布尔、整形、Long整形的。具体该如何使用呢?我们先来看下它们提供的方法。
我们只要知道了原子更新基本数据类型的类,然后点进去看,通过名字和代码注释就知道怎么用了。除去一些 getter/setter等基础方法,常用的也不多,这里给大家列举下。
由于Atomic包下针对基本数据类型提供的这三个类方法基本上都是一样的,名称都一样,知道一个的意思,其他的套用即可。
我这里列举 AtomicInteger 这个最常用的类来展示其提供的常用方法以及含义:
-
以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果
/**
* Atomically adds the given value to the current value.
* 原子地将给定值添加到当前值。
* @param delta the value to add
* @return the updated value
*/
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
-
如果输入的数值等于预期值,则以原子方 式将该值设置为输入的值
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
-
以原子的方式将当前值+1,注意,这里是返回增加之前的原有值,其实看方法名就可以知道, getAndIncrement
是要先get
然后再increment
/**
* Atomically increments by one the current value.
* 将当前值原子递增1。
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
-
懒设置,即最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值,这是基于性能方面考虑,因为在多个CPU缓存间同步一个内存的代价是很昂贵的。
/**
* Eventually sets to the given value.
* 最终设置为给定值。
* @param newValue the new value
* @since 1.6
*/
public final void lazySet(int newValue) {
unsafe.putOrderedInt(this, valueOffset, newValue);
}
-
以原子方式设置为newValue的值,并返回旧值,也是通过名字就知道是先 get
,所以返回时设置新值之前的值
/**
* Atomically sets to the given value and returns the old value.
*
* @param newValue the new value
* @return the previous value
*/
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
上面这些常见的方法,使用也很简单。在需要原子操作的场景比使用 synchronized 可简单、高效很多的,下面就来看看具体的使用场景。
问题:假设我们有两个线程,分别将全局整形变量 i 进行加1。每个线程执行5000次,按照传统的int使用方式,代码如下:
public static void main(String[] args) throws InterruptedException
CountDownLatch cdl = new CountDownLatch(2);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 5000; j++) {
m++;
}
cdl.countDown();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 5000; j++) {
m++;
}
cdl.countDown();
}
});
t1.start();
t2.start();
cdl.await();
System.out.println("result=" + m);
}
当我运行的时候,结果却不是10000,你知道为什么?
原因就在于 m++, 因为 m++ 不是一个原子操作, 它是要先读取再自增,然后赋值,在读取自增未赋值的时候就会产生线程安全问题,如果你没想明白,这里为什么线程不安全,欢迎给我留言。
那这个问题要怎么解决呢?我们最容易想到的就是,线程安全问题,那就上大杀器 synchronized
,没有解决不了的并发问题。当然这个没有错了,只要在 m++ 这行代码加上关键字 synchronized就能解决,不过 synchronized 过于沉重,今天我们要用原子操作类来解决,我看来看下使用原子操作类如何解决。
public static void main(String[] args) throws InterruptedException {
CountDownLatch cdl = new CountDownLatch(2);
AtomicInteger i = new AtomicInteger(0);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 5000; j++) {
i.incrementAndGet();
}
cdl.countDown();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 5000; j++) {
i.incrementAndGet();
}
cdl.countDown();
}
});
t1.start();
t2.start();
cdl.await();
System.out.println("result=" + i.get());
}
现在我们无论执行多少次,结果总是10000。
上面我们已经看了 AtomicInteger 可以在并发场景中保证变量更新是线程安全的,接下来我们再看下 getAndIncrement()
方法的使用。
@Test
public void testAtomicIntegerTest() {
AtomicInteger atomicInteger = new AtomicInteger(0);
int andIncrement = atomicInteger.getAndIncrement();
System.out.println(andIncrement);
System.out.println(atomicInteger.get());
}
通过上面的学习,你先看下上面的代码,思考下输出值是多少呢?
答案我就不贴了,动手验证下,你会掌握的更加牢固,明白 incrementAndGet()
与 getAndIncrement()
之间的差异点。
还是忍不住告诉你答案吧:
实现原理
通过上面的示例代码以及练习,你是否会思考这样一个问题,没错,就是 实现原理!
接下来我们就拿上面刚刚练习的 getAndIncrement()
来一起分析下实现原理。
看源码呢,这里七哥给大家分享下我的方法,不是直接去看对应类的所有成员变量和方法,而是直接进入我们使用的方法,因为我们研究原理的前提肯定是已经会用了,通过写的示例代码,直接进入对应方法查看,比如 getAndIncrement()
, 进入后源代码如下:
/**
* Atomically increments by one the current value.
* 将当前值原子递增1。
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
发现没有,居然没有逻辑,就一行代码,使用了 Unsafe 来实现,这个类是JDK提供的一个工具类,里面有很多 native 方法,即本地方法,是 JVM 使用 C 帮我们实现的,我们只要记住他提供了一些安全的操作方法,是直接操作内存的。
不过我们发现, unsafe.getAndAddInt(this,valueOffset,1)
方法中的三个参数,第一个this不用多说,就是指我们当前操作的 AtomicInteger 对象, 第3个参数是1,这个也好理解,因为我们这个方法就是用来自增的嘛,每次增加1,是增量值。重要在于第二个参数 valueOffset,这是一个成员变量,我们看下其在类中的定义:
// value成员属性的内存地址相对于对象内存地址的偏移量
private static final long valueOffset;
我已经写了注释,相信不难理解,这里我们会发现他定义修饰为常量,使用了 static final 描述,这又是为什么呢?要解答这个问题,我们要看下 valueOffset 赋值的地方:
static {
try {
//初始化valueOffset,通过unsafe.objectFieldOffset方法获取成员属性value内存地址相对于对象内存地址的偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
在静态代码块中赋值的,这里就要考验大家的基本功了,静态代码块的执行时机是在什么时候呢?
这也是面试常考的知识点,通常会和子类继承父类的场景结合在一起,考验我们的基本功,我梳理了一张图,分享给大家,这个绝对值得保存,因为我不相信你不会忘。
通过上图我们知道了 在我们创建 AtomicInteger 对象时,会先执行静态代码块,为 valueOffset 进行赋值,调用的是 unsafe.objectFieldOffset
方法,这个方法也是 native 的,上面说了 native 方法是 JVM 调用 C 函数实现的,我们这里只要清楚他是用来获取 AtomicInteger
这个 对象中 value
属性 的内存偏移量的,这样就能根据对象的内存地址找到属性值的内存地址。
搞清楚了 unsafe.getAndAddInt(this, valueOffset, 1)
方法参数的含义,我们接着往里看:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// 通过对象和字段偏移量找到当前的预期值
var5 = this.getIntVolatile(var1, var2);
/**
* CAS操作,现代CPU已广泛支持,是一种原子操作;
* 简单地说,当期待值var5与valueOffset地址处的值相等时,设置为预期值+1,完成自增
*/
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
注释我也写的很明白了,要补充一点就是,这里使用了 do-while 循环,其实也就是乐观锁,非阻塞的同步方式,一直尝试进行原子操作更新,直到成功。
讲到这里,原理就了解的差不多了,其他类似 AtomicLong、AtomicBoolean 都是利用 Unsfae实现的,按照我上面讲的方法你可以自行查看。一定要自己多去看看,动手操作下,我们是程序员,这个自觉性一定要有。
掌握了原理,我们在一起看看 其他的原子操作类的方法和使用。
原子更新数组
我们接着来看 Atomic 包下提供的原子更新类。
通过原子的方式更新数组里的某个元素,即操作数组元素是线程安全的,Atomic包提供了以下3个类:
-
AtomicIntegerArray:原子更新整型数组里的元素
-
AtomicLongArray:原子更新长整型数组里的元素
-
原子更新引用类型数组里的元素
上述几个类提供的方法几乎一样,我们以AtomicIntegerArray类为例讲解,其常用方法如下:
-
int addAndGet(int i , int delta):以原子方式将输入值与数组中索引 i 的元素相加
-
boolean compareAndSet(int i , int expect , int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值
@Test
public void testAtomicIntegerArray() {
int[] value = new int[]{1,2};
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(value);
int andSet = atomicIntegerArray.getAndSet(0, 3);
// 以原子方式设置为newValue的值,并返回旧值
Assert.assertEquals(4,atomicIntegerArray.addAndGet(0,1));
Assert.assertEquals(1,andSet);
Assert.assertEquals(4,atomicIntegerArray.get(0));
// AtomicIntegerArray会将传入的数组复制一份,不会影响原数组
Assert.assertEquals(1,value[0]);
}
上面的测试都是通过的,你能看懂就说明掌握了对应方法的使用。
需要注意的是,数组value通过构造方法传递进去,然后 AtomicIntergerArray
会将当前数组复制一份,所以当 AtomicIntergerArray
对内部的数组元素进行修改时,不会影响传入的数组。
构造方法:
/**
* Creates a new AtomicIntegerArray with the same length as, and
* all elements copied from, the given array.
*
* @param array the array to copy elements from
* @throws NullPointerException if array is null
*/
public AtomicIntegerArray(int[] array) {
// Visibility guaranteed by final field guarantees
this.array = array.clone();
}
原子更新引用
上面我们学会了如何原子更新基本类型,可是平时工作中复杂的业务场景使用最多的还是应用类型,这个要如何更新呢?
这就需要使用原子更新引用类型提供的类,Atomic包提供了以下3个类:
-
AtomicReference
:原子更新引用类型 -
AtomicReferenceFieldUpdater
:原子更新引用类型里的字段 -
AtomicMarkableReference
:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,boolean initialMark)
以上几个类提供的方法几乎一样,所以此处我们仅以AtomicReference为例进行讲解,AtomicReference的使用示例代码如下:
public static void main(String[] args) {
User user = new User("张三", 20);
atomicReference.set(user);
User updateUser = new User("李四",25);
atomicReference.compareAndSet(user,updateUser);
System.out.println(atomicReference.get());
}
static class User{
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + ''' +
", age=" + age +
'}';
}
}
运行结果:
User{name='李四', age=25}
其实现原理是依靠了 unsafe.compareAndSwapObject
方法。
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
原子更新属性(字段)
如果需原子地更新某个类里的某个字段时,就需要使用原子更新字段类,Atomic包提供了以下3个类进行原子字段更新:
-
AtomicIntegerFieldUpdater:原子更新整型的字段的更新器
-
AtomicLongFieldUpdater:原子更新长整型字段的更新器
-
AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题
要想原子地更新字段类需要两步。第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新类的字段.
以上3个类提供的方法几乎一样,此处仅以 AtomicIntegerFieldUpdater
为例进行讲解,AtomicIntegerFieldUpdater的
示例代码如下:
private static AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
public static void main(String[] args) {
User user = new User("张三",10);
System.out.println(updater.addAndGet(user,2));
System.out.println(updater.get(user));
}
static class User{
private String name;
public volatile int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + ''' +
", age=" + age +
'}';
}
}
运行结果:
12
12
思考一个问题
当我们看了 Unsafe 类后,发现他只提供了三种CAS方法:compareAndSwapObject
、compareAndSwapInt
和 compareAndSwapLong
。但是Java的基本类型里还有char、float和double等。那么问题来了,如何原子的更新其他的基本类型呢?
/**
* 如果当前数值是expected,则原子的将Java变量更新成x
* @return 如果更新成功则返回true
*/
public final native boolean compareAndSwapObject(Object o,long offset , Object expected , Object x );
public final native boolean compareAndSwapInt(Object o , long offset , int expected, int x );
public final native boolean compareAndSwapLong(Object o , long offset , long expected ,long x );
这个问题,我们直接通过 AtomicBoolean 源码来看看:
// AtomicBoolean
public final boolean compareAndSet(boolean expect, boolean update) {
int e = expect ? 1 : 0;
int u = update ? 1 : 0;
return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}
发现没?它是先把Boolean转换成整型,再使用 compareAndSwapInt 进行CAS,所以原子更新char、float和double变量也可以用类似的思路来实现。
// AtomicDouble
public final boolean compareAndSet(double expect, double update) {
return updater.compareAndSet(this,Double.doubleToRawLongBits(expect),Double.doubleToRawLongBits(update));
}
至此,今天这篇文章想写的Java原子操作类就基本差不多了,给大家小结一下:
-
JDK中从1.5 开始提供了Atomic包下的原子操作类,他们都是 Atomic 开头的,可以便捷、高效、安全的使用在需要更新变量的场景; -
Atomic 包下提供的原子操作类涵盖:原子更新基本类型、原子更新数组、原子更新引用、原子更新属性; -
原子更新操作类都是使用Unsafe提供的三个CAS方法结合死循环实现的,也就是乐观锁。
作者介绍: 七哥,非科班转行程序员,靠努力学习一步步成功,写文章也经常拍视频,专注编程技术与个人成长干货,愿望是陪家人平淡快乐的度过一生!
留个个人微信二维码
欢迎做个点赞之交
原文始发于微信公众号(七哥聊编程):并发编程 11:Atomic原子操作类总结
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/37104.html