Netty为什么不直接用AtomicXXX,而要用AtomicXXXFieldUpdater去更新变量呢?

更多技术分享可关注我
前言
如果仔细阅读过Netty的线程调度模型的源码,或者NIO线程对象及其线程池的创建源码,那么肯定会遇到类似“AtomicIntegerFieldUpdater”的身影,不禁想知道——Netty为何不直接使用原子类包装普通的比如计数的变量?
下面带着这个疑问,深入Netty以及JDK源码去窥探一二,顺便学习先进的用法。原文:Netty为什么不直接用AtomicXXX,而要用AtomicXXXFieldUpdater去更新变量呢?
JDK的Atomic原子操作类实现机制
在JDK里,Atomic 开头的原子操作类有很多,涉及到 Java 常用的数字类型的,基本都有相应的 Atomic 原子操作类,如下图所示:
原子操作类都是线程安全的,编码时可以放心大胆的使用。下面以其中常用的AtomicInteger原子类为例子,分析这些原子类的底层实现机制,辅助理解Netty为何没有直接使用原子类。具体使用的demo就不写了,想必Javaer都多少用过或者见过,直接看AtomicInteger类核心源码:
private volatile int value; // 简化了部分非核心源码
// 初始化,简化了部分非核心源码
public AtomicInteger(int initialValue) {
value = initialValue;
}
public final int get() {
return value;
}
// 自增 1,并返回自增之前的值
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
// 自减 1,并返回自增之前的值
public final int getAndDecrement() {
return unsafe.getAndAddInt(this, valueOffset, -1);
}
以上,AtomicInteger可以对int类型的值进行线程安全的自增或者自减等操作。从源码中可以看到,线程安全的操作方法底层都是使用unsafe方法实现,这是一个JDK的魔法类,能实现很多贴近底层的功能,所以并不是Java的实现的,但是能保证底层的这些getAndXXX操作都是线程安全的,关于unsafe具体的用法和细节,可以参考这篇文章Java魔法类:Unsafe应用解析(https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html,可能无法直接打开,复制黏贴到浏览器即可)
题外话:如果AtomicXXX的对象是自定义类型呢?不要慌,Java 也提供了自定义类型的原子操作类——AtomicReference,它操作的对象是个泛型对象,故能支持自定义的类型,其底层是没有自增方法的,操作的方法可以作为函数入参传递,源码如下:
// 对 x 执行 accumulatorFunction 操作
// accumulatorFunction 是个函数,可以自定义想做的事情
// 返回老值
public final V getAndAccumulate(V x,
BinaryOperator<V> accumulatorFunction) {
// prev 是老值,next 是新值
V prev, next;
// 自旋 + CAS 保证一定可以替换老值
do {
prev = get();
// 执行自定义操作
next = accumulatorFunction.apply(prev, x);
} while (!compareAndSet(prev, next));
return prev;
}
JDK的AtomicXXXFieldUpdater原子更新器及其优势
在Java5中,JDK就开始提供原子类了,当然也包括原子的更新器——即后缀为FieldUpdater的类,如下Integer、Long,还有一个自定义类型的原子更新器,共三类:
这些原子更新器常见于各种优秀的开源框架里,而很少被普通的业务程序员直接使用,其实这些原子更新器也可以被用来包装共享变量(必须是volatile修饰的对象属性),来为这些共享变量实现原子更新的功能。这些被包装的共享变量可以是原生类型,也可以是引用类型,那么不禁要问:已经有了原子类,为啥还额外提供一套原子更新器呢?
简单的说有两个原因,以int变量为例,基于AtomicIntegerFieldUpdater实现的原子计数器,比单纯的直接用AtomicInteger包装int变量的花销要小,因为前者只需要一个全局的静态变量AtomicIntegerFieldUpdater即可包装volatile修饰的非静态共享变量,然后配合CAS就能实现原子更新,而这样做,使得后续同一个类的每个对象中只需要共享这个静态的原子更新器即可为对象计数器实现原子更新,而原子类是为同一个类的每个对象中都创建了一个计数器 + AtomicInteger对象,这种开销显然就比较大了。
下面看一个JDK使用原子更新器的例子,即JDK的BufferedInputStream,如下是源码的片段节选:
public class BufferedInputStream extends FilterInputStream {
private static int DEFAULT_BUFFER_SIZE = 8192;
private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;
protected volatile byte buf[];
/**
* Atomic updater to provide compareAndSet for buf. This is
* necessary because closes can be asynchronous. We use nullness
* of buf[] as primary indicator that this stream is closed. (The
* "in" field is also nulled out on close.)
*/
private static final
AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater =
AtomicReferenceFieldUpdater.newUpdater
(BufferedInputStream.class, byte[].class, "buf");
可以看出,每个BufferedInputStream对象都包含了一个buf属性,该属性是对象属性,且被volition修饰,并被原子更新器AtomicReferenceFieldUpdater包装,注意这个引用类型的原子更新器是静态类型的,这意味着不论用户创建了多少个BufferedInputStream对象,在全局都只有这一个原子更新器被创建,这里之所以不用原子类AtomicReference直接包装buf属性,是因为buf是一个byte数组,通常会是一个比较大的对象,如果用原子类直接包装,那么后续每个BufferedInputStream对象都会额外创建一个原子类的对象,会消耗更多的内存,负担较重,因此JDK直接使用了原子更新器代替了原子类,Netty源码中的类似使用也是如出一辙。
另外一个重要原因是使用原子更新器,不会破坏共享变量原来的结构,回到上述JDK的例子,buf对外仍然可以保留buf对象的原生数组属性,只不过多了一个volatile修饰,外界可以直接获取到这个byte数组实现一些业务逻辑,而且在必要的时候也能使用原子更新器实现原子更新,可谓两头不耽误,灵活性较强!
还有一个可能的疑问点需要理解,即原子更新器虽然是静态的,但是其修饰的共享变量确仍然是类的对象属性,即每个类的对象仍然是只包含自己那独一份的共享变量,不会因为原子更新器是静态的,而受到任何影响。
结论:实现原子更新最佳的方式是直接使用原子更新器实现。一方面是更节省内存,另一方面是不破坏原始的共享变量,使用起来更灵活。当然如果是时延要求没有那么高的场景,那么就不需要这么严苛,直接使用原子类就OK,毕竟原子类的编码简单,开发效率高,不易出错。
品Netty源码,学习原子更新的最佳实现方式
前面说了很多理论,下面看一段Netty源码,看Netty是如何优雅的使用原子更新器的。下面是Netty的NIO线程实现类——SingleThreadEventExecutor的部分源码,省略了很多和本次分析无关的代码:
/**
* Abstract base class for {@link OrderedEventExecutor}'s that execute all its submitted tasks in a single thread.
*/
public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor {
private static final int ST_NOT_STARTED = 1;
private static final int ST_STARTED = 2;
private static final int ST_SHUTTING_DOWN = 3;
private static final int ST_SHUTDOWN = 4;
private static final int ST_TERMINATED = 5;
private static final AtomicIntegerFieldUpdater<SingleThreadEventExecutor> STATE_UPDATER;
private static final AtomicReferenceFieldUpdater<SingleThreadEventExecutor, ThreadProperties> PROPERTIES_UPDATER;
private static final long SCHEDULE_PURGE_INTERVAL = TimeUnit.SECONDS.toNanos(1);
static {
AtomicIntegerFieldUpdater<SingleThreadEventExecutor> updater =
PlatformDependent.newAtomicIntegerFieldUpdater(SingleThreadEventExecutor.class, "state");
if (updater == null) {
updater = AtomicIntegerFieldUpdater.newUpdater(SingleThreadEventExecutor.class, "state");
}
STATE_UPDATER = updater;
}
private final Queue<Runnable> taskQueue;
private final Executor executor;
private volatile Thread thread;
private volatile int state = ST_NOT_STARTED;
以上截取了一小片段,并删除了注释,可以清晰的看到Netty封装了JDK的Thread对象,一些标识线程状态的静态常量,线程执行器,异步任务队列,以及标识线程状态的属性state等,其中重点关注state,这个属性是普通的共享变量,由volatile修饰,并且被静态的原子更新器STATE_UPDATER包装。
下面看NIO线程的启动源码:
/**
* NioEventLoop线程启动方法, 这里会判断本NIO线程是否已经启动
*/
private void startThread() {
if (STATE_UPDATER.get(this) == ST_NOT_STARTED) {
if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
doStartThread();
}
}
}
注释写到了,启动NIO线程之前会做一次是否已经启动的判断,避免重复启动,这个判断逻辑就是前面提到的原子更新器实现的,当本NIO线程实例没有启动时,会做一次CAS计算,注意CAS对应操作系统的一个指令,是原子操作,如果是多个外部线程在启动NIO线程,那么同时只有一个外部线程能启动成功一次,后续的线程不会重复启动这个NIO线程。保证在NIO线程的一次生命周期内,外部线程只能调用一次doStartThread()方法,这样可以实现无锁更新,且没有自旋,性能较好,这里之所以不需要自旋,是因为启动线程就应该是一锤子买卖,启动不成功,就说明是已经启动了,直接跳过,无需重试。
在看一个自旋的用法:
在NIO线程被优雅(也可能异常)关闭时,会在死循环里,结合CAS算法,原子更新当前NIO线程的状态为关闭中。。。这里有两个注意事项:
1、和线程安全的启动NIO线程的逻辑不一样,更新线程状态必须成功,不是一锤子买卖,所以需要自旋重试,直到CAS操作成功
2、需要使用局部变量缓存外部的共享变量的旧值,保证CAS操作执行期间该共享变量的旧值不被外部线程修改
3、同样的,每次执行CAS操作之前,必须判断一次旧值,只有符合更新条件,才真的执行CAS操作,否则说明已经被外界线程更新成功,无需重复操作,以提升性能。
Netty这样做也侧面反映Nerty的源码确实很优秀,平时的业务开发,如果有类似场景,那么可以参考学习这两类用法。
总结使用原子更新器的注意事项:
1、包装的必须是被volatile修饰的共享变量
2、包装的必须是非静态的共享变量
3、必须搭配CAS的套路自行实现比较并交换的逻辑
4、自行实现比较并交换的逻辑时需要注意:如果是非一锤子买卖的原子更新操作,那么必须用局部变量缓存外部的共享变量的旧值,具体原因可以参考:Netty的线程调度模型分析(10)《多线程环境下,实例变量转为局部变量的程序设计技巧》,且放在一个循环里操作,以保证最终一致性。
后记
dashuai的博客是终身学习践行者,大厂程序员,且专注于工作经验、学习笔记的分享和日常吐槽,包括但不限于互联网行业,附带分享一些PDF电子书,资料,帮忙内推,欢迎拍砖!

Netty为什么不直接用AtomicXXX,而要用AtomicXXXFieldUpdater去更新变量呢?的更多相关文章
- 只用@property定义一个属性speed,子类不能直接用_speed,需要在interface的成员变量列表里写上_speed
//写法一: @interface Person : NSObject { } @property (nonatomic, strong) NSString *name; @end @implemen ...
- 每日学习笔记:js中可以直接用id名调用的问题?
在JavaScript中,标准的id选择器调用语法是: document.getElementById('myid').style.width = pc + "%"; 但是,今天发 ...
- 直接用<img> 的src属性显示base64转码后的字符串成图片
直接用<img> 的src属性显示base64转码后的字符串成图片 <img src="base64转码后的字符串" ></img> 下面的图片 ...
- 在nginx中配置如何防止直接用ip访问服务器web server及server_name特性讲解
看了很多nginx的配置,好像都忽略了ip直接访问web的问题,不利于SEO优化,所以我们希望可以避免直接用IP访问网站,而是域名访问,具体怎么做呢,看下面. 官方文档中提供的方法: If you d ...
- 直接用Qt写soap
直接用Qt写soap 最近的项目里用到了webservice, 同事用的是`gSoap`来搞的. 用这个本身没什么问题, 但这货生成的代码实非人类可读, 到处都是`__`和`_`, 看得我眼晕.... ...
- 为什么是List list = new ArrayList() 而不直接用ArrayList
为什么是List list = new ArrayList(),而不直接用ArrayList? 编程是要面向对象编程,针对抽象(接口),而非具体.List 是接口,ArrayList是实现. 实现Li ...
- 直接用bat命令对Inno Setup的脚本文件.iss进行编译
直接用bat命令对Inno Setup的脚本文件.iss进行编译 2010-06-17 15:17 qjn0059 | 浏览 2163 次 编程语言外语学习 分享到: 2010-06-29 11: ...
- 查看程序是否启动或者关闭--比如查看Tomcat是否开启!直接用ps命令查看进程就行了啊
1.查看程序是否启动或者关闭--比如查看Tomcat是否开启!直接用ps命令查看进程就行了啊 2.Tomcat服务器和虚拟机的关系,Tomcat启动运行过程要调用系统环境变量的java_home啊,J ...
- (转载)直接用SQL语句把DBF导入SQLServer
告诉大家一个直接用SQL语句把DBF导入SQLServer,以及txt导入Access的方法,大家抛弃BatchMove吧来自:碧血剑告诉你一个最快的方法,用SQLServer连接DBF在SQLSer ...
随机推荐
- display的block、none、inline属性及解释
常会用到display对应值有block.none.inline这三个值 参数: block :块对象的默认值.用该值为对象之后添加新行.之前也添加一行. none :隐藏对象.与visibility ...
- 关于integer overflow错误
前端突然报了integer overflow错误,int类型溢出也就是数字超过了int类型,一看很懵逼,查看后台日期发现是在Math.toIntExact()方法报错 那么我们看下方法内部代码: /* ...
- deepin15.11安装N卡驱动,实测!!!(可解决N卡电脑关机卡屏)
前言:deepin(深度)是一款由武汉深之度公司研发的一款适合国人日常学习的linux系统,其UI精美,美过Mac.它对于中国用户的一个亮点就是QQ微信等国软件傻瓜式安装(类似安卓应用商店安装),如果 ...
- 什么是data:image/png;base64,?一道关于Data URI Scheme的入门级CTF_Web题
一道关于Data URI Scheme的入门级CTF_Web题 0x00 题目描述 这是偶尔遇到的某网安交流群的入群题,题目没有任何的提示,直接给了一个txt文件. 0x01 解题过程 通过给的这个文 ...
- Protocol buffers编写风格指南
原文链接:https://developers.google.com/protocol-buffers/docs/style Style Guide 本文说明了.proto文件的编写风格指南.遵循这些 ...
- 区间DP(力扣1000.合并石头的最低成本)
一.区间DP 顾名思义区间DP就是在区间上进行动态规划,先求出一段区间上的最优解,在合并成整个大区间的最优解,方法主要有记忆化搜素和递归的形式. 顺便提一下动态规划的成立条件是满足最优子结构和无后效性 ...
- Hacker101-CTF | Postbook
Hacker101-CTF | Postbook mirror王宇阳 水平有限,不足之处还望指教 ^_^ 看看这个一大堆英文介绍 With this amazing tool you can writ ...
- KEIL编译出现错误“source file is not valid utf-8”
实际情况是: .h文件一直报错source file is not valid utf-8的错误, 原因就是: 文件中出现了一个中文的“:”导致的.总结就是:如出现此类错误,可能是字符不够标准.
- VsCode代码段添加方法
VsCode代码段添加方法 我们在编写代码的过程中,常常会遇到一些固定的结构或常用的处理方法. 编写耗费时间尽力,这时我们想到了添加代码段功能,帮助我们快速的完成编写. 下面以VsCode为例子: 我 ...
- idea安装 阿里巴巴Java编码准则插件
首先还是打开熟悉的idea 在marketplace 输入 alibaba 我这是已经安装过了 下载完成之后重启idea生效 如果需要那就手动的扫描 当然已经自动的扫描了 如果你的代码不符合阿里的标准 ...