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 ...
随机推荐
- 机器学习基础——简单易懂的K邻近算法,根据邻居“找自己”
本文始发于个人公众号:TechFlow,原创不易,求个关注 今天的文章给大家分享机器学习领域非常简单的模型--KNN,也就是K Nearest Neighbours算法,翻译过来很简单,就是K最近邻居 ...
- 复制图片链接和标题生成Markdown文本
写Markdown的时候常常会需要复制图片链接和标题以插入图片,不借助其他工具的话,一般需要先在Markdown文件中输入插入图片的格式,然后在浏览器中复制图片链接和标题将其依次粘贴到Markdown ...
- 何为引用法---细谈C++引用
何为引用...给已有的变量取别名 ; int &a = num;//此处 &不是取地址 而是标明 a是引用变量(a 是 num的别名) 注意: 1.引用必须初始化 2.引用一旦初始化 ...
- C++ Dll中导出一个类
//定义一个头文件,创建MyObject.h的头文件 并打印如下代码 #ifndef _MY_OBJECT_H #define _MY_OBJECT_H #ifndef MYDLL_EXPORTS # ...
- Asp.Net Core 中IdentityServer4 授权原理及刷新Token的应用
一.前言 上面分享了IdentityServer4 两篇系列文章,核心主题主要是密码授权模式及自定义授权模式,但是仅仅是分享了这两种模式的使用,这篇文章进一步来分享IdentityServer4的授权 ...
- HTTP中主要的头字段
HTTP中主要的头字段 头字段类型 含义 备注 通用头:适用于请求和响应消息的头字段 Date 表示请求和响应生成的日期 Pragma 表示数据是否允许缓存的通信选项 Cache-Contro ...
- 代码备份 | 博客侧边栏公告(支持HTML代码)(支持JS代码)
博客侧边栏公告(支持HTML代码)(支持JS代码) <div id='btnList'> <a class="ivu-btn ivu-btn-primary" h ...
- 聊一聊 React 中的 CSS 样式方案
和 Angular,Vue 不同,React 并没有如何在 React 中书写样式的官方方案,依靠的是社区众多的方案.社区中提供的方案有很多,例如 CSS Modules,styled-compone ...
- C++ 类的继承和派生
继承的优点:减少代码的冗余 提高代码的重用性 派生类定义格式: Class 派生类名 : 继承方式 基类名{ //派生类新增的数据成员和成员函数 }; class 子类: 继承方式 父类名{ //子类 ...
- Unity 游戏框架:资源管理神器 ResKit
此篇文章准备了将近两周的时间,写了改,改了删.之前有朋友反馈,上一个文章太冗长了,影响阅读体验,这一讲就走个精简路线.所以只要不是很重要的内容就都删减掉了. 文章分两个部分,第一部分是原理,第二部分是 ...