一直使用AtomicInteger?试一试FieldUpdater
1. 背景
在进入正题之前,这里先提出一个问题,如何在多线程中去对一个数字进行+1操作?这个问题非常简单,哪怕是Java的初学者都能回答上来,使用AtomicXXX,比如有一个int类型的自加,那么你可以使用AtomicInteger 代替int类型进行自加。
AtomicInteger atomicInteger = new AtomicInteger(); atomicInteger.addAndGet(1);
如上面的代码所示,使用addAndGet即可保证多线程中相加,具体原理在底层使用的是CAS,这里就不展开细讲。基本上AtomicXXX能满足我们的所有需求,直到前几天一个群友(ID:皮摩)问了我一个问题,他发现在很多开源框架中,例如Netty中的AbstractReferenceCountedByteBuf 类中定义了一个refCntUpdater:
private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> refCntUpdater; static { AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> updater = PlatformDependent.newAtomicIntegerFieldUpdater(AbstractReferenceCountedByteBuf.class, "refCnt"); if (updater == null) { updater = AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, "refCnt"); } refCntUpdater = updater; }
refCntUpdater 是Netty用来记录ByteBuf被引用的次数,会出现并发的操作,比如增加一个引用关系,减少一个引用关系,其retain方法,实现了refCntUpdater的自增:
private ByteBuf retain0(int increment) { for (;;) { int refCnt = this.refCnt; final int nextCnt = refCnt + increment; // Ensure we not resurrect (which means the refCnt was 0) and also that we encountered an overflow. if (nextCnt <= increment) { throw new IllegalReferenceCountException(refCnt, increment); } if (refCntUpdater.compareAndSet(this, refCnt, nextCnt)) { break; } } return this; }
俗话说有因必有果,netty多费力气做这些事必然是有自己的原因的,接下来就进入我们的正题。
2.Atomic field updater
在java.util.concurrent.atomic
包中有很多原子类,比如AtomicInteger,AtomicLong,LongAdder等已经是大家熟知的常用类,在这个包中还有三个类在jdk1.5中都存在了,但是经常被大家忽略,这就是fieldUpdater:
- AtomicIntegerFieldUpdater
- AtomicLongFieldUpdater
- AtomicReferenceFieldUpdater
这个在代码中不经常会有,但是有时候可以作为性能优化的工具出场,一般在下面两种情况会使用它:
- 你想通过正常的引用使用volatile的,比如直接在类中调用
this.variable
,但是你也想时不时的使用一下CAS操作或者原子自增操作,那么你可以使用fieldUpdater。 - 当你使用AtomicXXX的时候,其引用Atomic的对象有多个的时候,你可以使用fieldUpdater节约内存开销。
2.1 正常引用volatile变量
一般有两种情况需要正常引用:
- 当代码中引入已经正常引用,但是这个时候需要新增一个CAS的需求,我们可以将其替换AtomicXXX对象,但是之前的调用都得换成
.get()
和.set()
方法,这样做会增加不少的工作量,并且还需要大量的回归测试。 - 代码更加容易理解,在
BufferedInputStream
中,有一个buf数组用来表示内部缓冲区,它也是一个volatile数组,在BufferedInputStream中大多数时候只需要正常的使用这个数组缓冲区即可,在一些特殊的情况下,比如close的时候需要使用compareAndSet
,我们可以使用AtomicReference,我觉得这样做有点乱,使用fieldUpdater来说更加容易理解,
protected volatile byte buf[]; private static final AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater = AtomicReferenceFieldUpdater.newUpdater (BufferedInputStream.class, byte[].class, "buf"); public void close() throws IOException { byte[] buffer; while ( (buffer = buf) != null) { if (bufUpdater.compareAndSet(this, buffer, null)) { InputStream input = in; in = null; if (input != null) input.close(); return; } // Else retry in case a new buf was CASed in fill() } }
2.2 节约内存
之前说过在很多开源框架中都能看见fieldUpdater的身影,其实大部分的情况都是为了节约内存,为什么其会节约内存呢?
我们首先来看看AtomicInteger类:
public class AtomicInteger extends Number implements java.io.Serializable { private static final long serialVersionUID = 6214790243416807050L; // 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; }
在AtomicInteger成员变量只有一个int value
,似乎好像并没有多出内存,但是我们的AtomicInteger是一个对象,一个对象的正确计算应该是 对象头 + 数据大小,在64位机器上AtomicInteger对象占用内存如下:
-
关闭指针压缩: 16(对象头)+4(实例数据)=20不是8的倍数,因此需要对齐填充 16+4+4(padding)=24
-
开启指针压缩(-XX:+UseCompressedOop): 12+4=16已经是8的倍数了,不需要再padding。
由于我们的AtomicInteger是一个对象,还需要被引用,那么真实的占用为:
- 关闭指针压缩:24 + 8 = 32
- 开启指针压缩: 16 + 4 = 20
而fieldUpdater是staic final
类型并不会占用我们对象的内存,所以使用fieldUpdater的话可以近似认为只用了4字节,这个再未关闭指针压缩的情况下节约了7倍,关闭的情况下节约了4倍,这个在少量对象的情况下可能不明显,当我们对象有几十万,几百万,或者几千万的时候,节约的可能就是几十M,几百M,甚至几个G。
比如在netty中的AbstractReferenceCountedByteBuf,熟悉netty的同学都知道netty是自己管理内存的,所有的ByteBuf都会继承AbstractReferenceCountedByteBuf,在netty中ByteBuf会被大量的创建,netty使用fieldUpdater用于节约内存。
在阿里开源的数据库连接池druid中也有同样的体现,早在2012的一个pr中,就有优化内存的comment:,在druid中,有很多统计数据对象,这些对象通常会以秒级创建,分钟级创建新的,druid通过fieldUpdater节约了大量内存:
3.最后
AtomicFieldUpdater的确在我们平时使用比较少,但是其也值得我们去了解,有时候在特殊的场景下的确可以作为奇技淫巧。
如果大家觉得这篇文章对你有帮助,你的关注和转发是对我最大的支持,O(∩_∩)O:
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Nebula Graph 技术总监陈恒:图数据库怎么和深度学习框架进行结合?
Nebula Graph 的技术总监在 09.24 - 09.30 期间同 开源中国·高手问答 的小伙伴们以「图数据库的设计和实践」为切入点展开讨论,包括:「图数据库的存储设计」、「图数据库的计算设计」、「图数据库的架构设计」等方面内容,本文整理于他和开源中国小伙伴对图数据库的讨论内容~ 嘉宾·陈恒介绍 陈恒,开源的分布式图数据库 Nebula Graph 技术总监,图数据库领域专家 & HBase Committer。北京邮电大学硕士,曾就职于蚂蚁金服、猿题库、网易等公司,一直从事基础设施相关研发工作。 本文目录 图数据库怎么和深度学习框架进行结合? 图数据库它可以被认为是 MySQL 中的一种数据库引擎,具备特殊的查询功能,以及特殊的数据结构? Nebula 和 Neo4j 的图数据库的优势和劣势?为何要新开发使用 Nebula ? 图数据库目前主要用于哪些应用场景? 图数据库和一般数据库结构相比,优势在哪里? Nebula 的实践问题 存储计算分离 Nebula 高度可扩展具体指的是什么?存储层是否还支持其他类型的数据库? 「图数据库」是基于已有数据库衍生出来的产品吗?如...
- 下一篇
线程的来龙去脉,你了解吗?
进程最近有些烦恼,整日愁眉苦脸的,拜访内存的时候也有点心不在焉。 内存是个明眼人,开门见山的问道:“进程啊,最近遇到啥问题了?我看你最近情绪有点低落,有啥问题你就直接说出来嘛,我让大家伙儿来一起帮你想想办法。” 进程叹了口气,说道:“唉,最近不是说 CPU 单核频率到瓶颈了吗?人类就用多核芯来弥补单核处理器性能的不足,咱们的 CPU 不也升级到四核了嘛。” “是啊,这是好事啊,现在最多能并行处理 4 个进程,效率比以前高多了,这还不好吗?”内存疑惑的问。 “好是好,可我每次上 CPU 运行的时候,都忍不住去想,要是单核频率不增加,我总的运行的时间不还是没有什么变化吗?以后的应用程序越来越大,越来越吃 CPU 资源,比如那些大型游戏进程,在短时间内需要进行大量计算,靠单核撑不住怎么办。不谈以后,就说说我自己,我也想能够早点运行完,早点休息啊。” tobe 注:很明显单进程的运行时间是变小了的,不过这里主要强调的是进程占用 CPU 的时间。 内存点点头,赞同道:“这个问题我倒是没想到,多核处理器对单个进程确实不大友好。那咱得想办法让你能够同时使用几个核心。不过我一时间也想不到什么好办法,还...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- MySQL8.0.19开启GTID主从同步CentOS8
- 设置Eclipse缩进为4个空格,增强代码规范
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Hadoop3单机部署,实现最简伪集群