博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java并发| Atomic包下的原子操作类使用与原理解析
阅读量:4186 次
发布时间:2019-05-26

本文共 12384 字,大约阅读时间需要 41 分钟。

我们为什么一定要学习 Atomic 包下的这些原子操作类呢? 下面告诉你原因。

Java中有那么一些类,是以Atomic开头的。这一系列的类我们称之为原子操作类。以最简单的类AtomicInteger为例。它相当于一个int变量,我们执行Int的 i++ 的时候并不是一个原子操作。而使用AtomicInteger的incrementAndGet却能保证原子操作。 更新变量这种场景下效果和 synchronized 相同,却要简单高效的多。

这篇文章主要介绍了 Java并发包下 Atomic包中的原子操作类具体有什么?如何使用?以及如何实现的?

大家带着这三个问题来读这篇文章,相信你会有所收获的!

在这里插入图片描述

同时文章中的示例代码,我们要尽量多动手去实践操作,自己写出来和仅仅看懂是不一样的。上周末去参加《腾讯课堂2020讲师大赛决赛》,在比赛中李涛(中国PS第一人)老师作为评委点评说:

知识和技能是不一样的,看会的那叫知识,动手实践掌握的才叫技能。

在这里插入图片描述

看完上面的图,我们会发现原子操作类分为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() 方法的使用。

@Testpublic 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值

@Testpublic 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 + '}'; }}
运行结果:1212

思考一个问题

当我们看了 Unsafe 类后,发现他只提供了三种CAS方法:

compareAndSwapObjectcompareAndSwapIntcompareAndSwapLong
但是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 源码来看看:

// AtomicBooleanpublic 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变量也可以用类似的思路来实现。

// AtomicDoublepublic final boolean compareAndSet(double expect, double update) {
return updater.compareAndSet(this,Double.doubleToRawLongBits(expect),Double.doubleToRawLongBits(update));}

至此,今天这篇文章想写的Java原子操作类就基本差不多了,给大家小结一下:

  1. JDK中从1.5 开始提供了Atomic包下的原子操作类,他们都是 Atomic 开头的,可以便捷、高效、安全的使用在需要更新变量的场景;
  2. Atomic 包下提供的原子操作类涵盖:原子更新基本类型、原子更新数组、原子更新引用、原子更新属性;
  3. 原子更新操作类都是使用Unsafe提供的三个CAS方法结合死循环实现的,也就是乐观锁。

今天是国庆&中秋假期前的最后一天了,七哥已经请假了,发完这篇文章,就要奔赴西安和家人朋友团聚了。

祝大家假期愉快,在玩耍的同时也要记着充电哦,不知道学什么的可以看看七哥推荐的 《Java必读书籍》哈。

个人公众号

在这里插入图片描述

  • 觉得写得还不错的小伙伴麻烦动手点赞&关注
  • 文章如果存在不正确的地方,麻烦指出,非常感谢您的阅读;
  • 推荐大家关注我的公众号,会为你定期推送原创干货文章,拉你进优质学习社群;
  • github地址:

转载地址:http://zuioi.baihongyu.com/

你可能感兴趣的文章
实战DDD(Domain-Driven Design领域驱动设计:Evans DDD)
查看>>
SSH中各个框架的作用以及Spring AOP,IOC,DI详解
查看>>
openstack juno 配置vmware(vcenter、vsphere)
查看>>
远程debug调试(eclipse)之openstack windows
查看>>
PAAS平台对比:OpenShift VS CloudFoundry【51CTO调研报告】
查看>>
JAX-RS(java restful实现讲解)(转)
查看>>
Spring MVC与JAX-RS比较与分析
查看>>
openstack官方docker介绍
查看>>
头痛与早餐
查看>>
[转]在ASP.NET 2.0中操作数据::创建一个数据访问层
查看>>
Linux命令之chmod详解
查看>>
【java小程序实战】小程序注销功能实现
查看>>
webkit入门准备
查看>>
在Ubuntu 12.04 64bit上配置,安装和运行go程序
查看>>
ATS程序功能和使用方法详解
查看>>
常用Linux命令总结
查看>>
Tafficserver旁路接入方案综述
查看>>
在CentOS 6.3 64bit上如何从源码生成rpm包?
查看>>
利用lua中的string.gsub来巧妙实现json中字段的正则替换
查看>>
ATS名词术语(待续)
查看>>