死磕 java并发包之AtomicInteger源码分析
问题
(1)什么是原子操作?
(2)原子操作和数据库的ACID有啥关系?
(3)AtomicInteger是怎么实现原子操作的?
(4)AtomicInteger是有什么缺点?
简介
AtomicInteger是java并发包下面提供的原子类,主要操作的是int类型的整型,通过调用底层Unsafe的CAS等方法实现原子操作。
还记得Unsafe吗?点击链接直达【死磕 java魔法类之Unsafe解析】
原子操作
原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何线程上下文切换。
原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分,将整个操作视作一个整体是原子性的核心特征。
我们这里说的原子操作与数据库ACID中的原子性,笔者认为最大区别在于,数据库中的原子性主要运用在事务中,一个事务之内的所有更新操作要么都成功,要么都失败,事务是有回滚机制的,而我们这里说的原子操作是没有回滚的,这是最大的区别。
源码分析
主要属性
// 获取Unsafe的实例 private static final Unsafe unsafe = Unsafe.getUnsafe(); // 标识value字段的偏移量 private static final long valueOffset; // 静态代码块,通过unsafe获取value的偏移量 static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } // 存储int类型值的地方,使用volatile修饰 private volatile int value;
(1)使用int类型的value存储值,且使用volatile修饰,volatile主要是保证可见性,即一个线程修改对另一个线程立即可见,主要的实现原理是内存屏障,这里不展开来讲,有兴趣的可以自行查阅相关资料。
(2)调用Unsafe的objectFieldOffset()方法获取value字段在类中的偏移量,用于后面CAS操作时使用。
compareAndSet()方法
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } // Unsafe中的方法 public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
调用Unsafe.compareAndSwapInt()方法实现,这个方法有四个参数:
(1)操作的对象;
(2)对象中字段的偏移量;
(3)原来的值,即期望的值;
(4)要修改的值;
可以看到,这是一个native方法,底层是使用C/C++写的,主要是调用CPU的CAS指令来实现,它能够保证只有当对应偏移量处的字段值是期望值时才更新,即类似下面这样的两步操作:
if(value == expect) { value = newValue; }
通过CPU的CAS指令可以保证这两步操作是一个整体,也就不会出现多线程环境中可能比较的时候value值是a,而到真正赋值的时候value值可能已经变成b了的问题。
getAndIncrement()方法
public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } // Unsafe中的方法 public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
getAndIncrement()方法底层是调用的Unsafe的getAndAddInt()方法,这个方法有三个参数:
(1)操作的对象;
(2)对象中字段的偏移量;
(3)要增加的值;
查看Unsafe的getAndAddInt()方法的源码,可以看到它是先获取当前的值,然后再调用compareAndSwapInt()尝试更新对应偏移量处的值,如果成功了就跳出循环,如果不成功就再重新尝试,直到成功为止,这可不就是(CAS+自旋)的乐观锁机制么^^
AtomicInteger中的其它方法几乎都是类似的,最终会调用到Unsafe的compareAndSwapInt()来保证对value值更新的原子性。
总结
(1)AtomicInteger中维护了一个使用volatile修饰的变量value,保证可见性;
(2)AtomicInteger中的主要方法最终几乎都会调用到Unsafe的compareAndSwapInt()方法保证对变量修改的原子性。
彩蛋
(1)为什么需要AtomicInteger?
让我们来看一个例子:
public class AtomicIntegerTest { private static int count = 0; public static void increment() { count++; } public static void main(String[] args) { IntStream.range(0, 100) .forEach(i-> new Thread(()->IntStream.range(0, 1000) .forEach(j->increment())).start()); // 这里使用2或者1看自己的机器 // 我这里是用run跑大于2才会退出循环 // 但是用debug跑大于1就会退出循环了 while (Thread.activeCount() > 1) { // 让出CPU Thread.yield(); } System.out.println(count); } }
这里起了100个线程,每个线程对count自增1000次,你会发现每次运行的结果都不一样,但它们有个共同点就是都不到100000次,所以直接使用int是有问题的。
那么,使用volatile能解决这个问题吗?
private static volatile int count = 0; public static void increment() { count++; }
答案是很遗憾的,volatile无法解决这个问题,因为volatile仅有两个作用:
(1)保证可见性,即一个线程对变量的修改另一个线程立即可见;
(2)禁止指令重排序;
这里有个很重要的问题,count++实际上是两步操作,第一步是获取count的值,第二步是对它的值加1。
使用volatile是无法保证这两步不被其它线程调度打断的,所以无法保证原子性。
这就引出了我们今天讲的AtomicInteger,它的自增调用的是Unsafe的CAS并使用自旋保证一定会成功,它可以保证两步操作的原子性。
public class AtomicIntegerTest { private static AtomicInteger count = new AtomicInteger(0); public static void increment() { count.incrementAndGet(); } public static void main(String[] args) { IntStream.range(0, 100) .forEach(i-> new Thread(()->IntStream.range(0, 1000) .forEach(j->increment())).start()); // 这里使用2或者1看自己的机器 // 我这里是用run跑大于2才会退出循环 // 但是用debug跑大于1就会退出循环了 while (Thread.activeCount() > 1) { // 让出CPU Thread.yield(); } System.out.println(count); } }
这里总是会打印出100000。
(2)说了那么多,你知道AtomicInteger有什么缺点吗?
当然就是著名的ABA问题啦,我们下章接着聊^^
欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Jenkins 插件开发之旅:两天内从 idea 到发布(上篇)
本文首发于:Jenkins 中文社区 本文介绍了笔者首个 Jenkins 插件开发的旅程, 包括从产生 idea 开始,然后经过插件定制开发, 接着申请将代码托管到 jenkinsci GitHub 组织, 最后将插件发布到 Jenkins 插件更新中心的过程。 鉴于文章篇幅过长,将分为上下两篇进行介绍。 从一个 idea 说起 前几天和朋友聊天时,聊到了 Maven 版本管理领域的 SNAPSHOT 版本依赖问题, 这给他带来了一些困扰,消灭掉历史遗留应用的 SNAPSHOT 版本依赖并非易事。 类似问题也曾经给笔者带来过困扰,在最初没能去规避问题, 等到再想去解决问题时却发现困难重重,牵一发而动全身, 导致这个问题一直被搁置,而这也给笔者留下深刻的印象。 等到再次制定 Maven 规范时,从一开始就考虑 强制禁止 SNAPSHOT 版本依赖发到生产环境。 这里是通过在 Jenkins 构建时做校验实现的。 因为没有找到提供类似功能的 Jenkins 插件, 目前这个校验通过 shell 脚本来实现的, 具体的做法是在 Jenkins 任务中 Maven 构建之前增加一个 Execu...
- 下一篇
Kafka ProducerConfig和ConsumerConfig 释义
Kafka客户端开发中有一个ProducerConfig和ConsumerConfig,熟悉这两个文件内容的含义对我们使用,调优Kafka是非常有帮助的 生产者配置参数释义 1.bootstrap.servers 指定Kafka集群所需的broker地址清单,默认 "" 2.metadata.max.age.ms 强制刷新元数据时间,毫秒,默认300000,5分钟 3.batch.size 指定ProducerBatch内存区域的大小,默认16kb 4.acks 指定分区中必须有多少个副本收到这条消息,才算消息发送成功,默认值1,字符串类型 5.linger.ms 指定ProducerBatch在延迟多少毫秒后再发送,但如果在延迟的这段时间内batch的大小已经到了batch.size设置的大小,那么消息会被立即发送,不会再等待,默认值0 6.client.id 用户设定,用于跟踪记录消息,默认 "" 7.send.buffer.bytes Socket发送缓冲区大小,默认128kb,-1将使用操作系统的设置 8.receive.buffer.bytes Socket接收缓冲区大小,默...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8安装Docker,最新的服务器搭配容器使用
- Linux系统CentOS6、CentOS7手动修改IP地址
- 设置Eclipse缩进为4个空格,增强代码规范
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- Hadoop3单机部署,实现最简伪集群
- CentOS8编译安装MySQL8.0.19
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池