为什么要使用计数器?

在游戏程序的开发中,我们会遇到很多跟计数相关的需求,比如玩家领取了多少次奖励、成就的任务进度、一场比赛中的得分等等。然而在很多的API里,很少提供我们不用关心边界值或中间操作的计数器,特别是对于服务器来讲,基本会使用有键值对的计数器,因为我们管理的是一群玩家,并不是一个,当然可能还会有更多的层级,比如一群玩家的任务进度计数,就是一个3维数组。要实现这种看似简单的功能,我们就会想到Java里Map这个东西,但是很遗憾的是,Map的处理太过多余了,他并不是为计数而生,我们需要改造一下,这里先从简单计数器说起。

AtomicInteger

这个AtomicInteger好啊,又是线程安全的,又有方便的计数API,我们就从他出发吧。

计数器的API总共就几个,获取当前值(get),直接设置当前值(set),增加多少值(getAndAdd和addAndGet),其实就相当于操作符++,一个是a++,一个是++a。在增加值的基础上再提供++1、- -1、1- -、1++这种操作,基本上就够用了,对于AtomicInteger的原理这里就不赘述。

IntCounter

既然有线程安全的了,为啥要有一个非安全的,嘛。。毕竟线程安全的有那么点点以牺牲性能为代价。这里提一下游戏服务器里的线程模型,假设是一个以地图为基础的RPG游戏,通常来讲,我们会以地图来分线程,保证同一个地图的玩家是在同一个线程上的(这就是为什么有些游戏交易必须同地图,甚至更老的游戏,功能都绑定在了NPC身上,题外话不多说)。如果该功能对于玩家是单机性质(自己的操作不影响他人)的或者说即使交互(与其他玩家发生行为)也是同地图玩家的交互,计数操作是没必要线程安全的。RPG游戏是高反馈低延时的游戏,所以扣点性能也没什么大惊小怪的(虽然现在硬件已经很变态了)。

IntCounter的实现其实非常简单,就是对int的再次封装(LongCounter同理),想必我这里不说,大家都明白怎么写这个代码了。举个栗子,代码没什么好讲的,大家可以实现更多方便的API,虽然这些API总共就没几行,但是积少成多,对于项目的开大有着很大的帮助,不要小看他了。

private int count;

/*** 归零*/

public void zero() {

setCount(0);

}

/*** 设置为最大值*/

public void setHigh() {

setCount(high());

}

/*** 设置为最小值*/

public void setLow() {

setCount(low());

}

/*** 最大限制** @return*/

public int high() {

return Integer.MAX_VALUE;

}

/*** 最小限制** @return*/

public int low() {

return 0;

}

/*** 获取当前数值** @return*/

public int getCount() {

return this.count;

}

/*** 直接设置当前值** @param value* @return*/

protected int setCount(int value) {

return this.count = GameMathUtil.fixedBetween(value, low(), high());

}

/*** +1并获取** @return*/

public int incrementAndGet() {

return addAndGet(1);

}

/*** -1并获取** @return*/

public int decrementAndGet() {

return addAndGet(-1);

}

/*** 增加并获取** @param delta* @return*/

public int addAndGet(int delta) {

return setCount(getCount() + delta);

}

/*** 获取并+1** @return*/

public int getAndIncrement() {

return getAndAdd(1);

}

/*** 获取并-1** @return*/

public int getAndDecrement() {

return getAndAdd(-1);

}

/*** 获取并增加** @param delta* @return*/

public int getAndAdd(int delta) {

int old = getCount();

setCount(GameMathUtil.safeAdd(old, delta));

return old;

}

键值对Map类型计数器的包装

刚才说了,不管是对于服务器本身的性质也好还是对于需求本身也好,都会存在同类型复数个计数器,我们就自然想到了Map(2维映射)类型甚至Table(3维映射)来处理这个事情。由于原本自带的Map并不是专门做这种事情的,所以我们针对计数器的需求特别优化一下API的友好度。

如果我们直接使用Map的话,我们必须要处理

1.不管在放入计数或者是获取计数的时候是否存在一个键值对,如果不存在我们会初始化他

2.如果大部分的计数在常规状态下都为初始值(这里假设为0),那么我们会初始化一堆没有用的数据

3.每次计数改变的操作,都会先取出数据(取出的时候还要做第1步的检查),然后更改数值再放回,这些代码重复太多会让写代码的人不能直接关注需求本身而产生混乱从而导致很多BUG。

下面的代码简单的演示下上面的痛处

//声明一个MapMap tasks = new HashMap<>();

//现在获取任务"kill monster"的进度String taskName = "kill monster";

Integer taskProccess = tasks.get(taskName);

//如果任务进度为空则初始化任务进度为0if(taskProccess == null){

taskProccess = 0;

tasks.put(taskName,taskProccess);

}

//任务进度+1taskProccess+=1;

//这里由于惯性思维,在写很多复杂逻辑的时候很有可能会不做put操作而导致bugtasks.put(taskName,taskProccess);

上面的操作简直让人蛋疼无比!那么我们先看看经过优化过的IntMap是怎么写代码的吧!

IntMap tasksNew = IntMap.empty();

tasksNew.incrementAndGet(taskName);

两行代码解决,是不是轻松多了,不算上声明,1行代码解决,本来计数这种简单操作就应该是一行代码操作的事情,对吧。

针对上面的伤痛,我们看看是怎么实现一个自己的IntMap。计数器的API都是大同小异的

/*** 获取计数** @param key* @return*/

int getCount(K key);

/*** 设置计数** @param key* @param newValue* @return*/

int putCount(K key, int newValue);

/*** 总数** @return*/

int sum();

/*** 加1并获取** @param key* @return*/

int incrementAndGet(K key);

/*** 减1并获取** @param key* @return*/

int decrementAndGet(K key);

/*** 增加并获取** @param key* @param delta* @return*/

int addAndGet(K key, int delta);

/*** 获取并加1** @param key* @return*/

int getAndIncrement(K key);

/*** 获取并减1** @param key* @return*/

int getAndDecrement(K key);

/*** 获取并增加** @param key* @param delta* @return*/

int getAndAdd(K key, int delta);

我们使用Java8中Map的新API(Map.compute)可以非常方便的实现刚才Map中冗余的操作

/*** 获取并更新** @param key* @param updaterFunction* @return*/

private int getAndUpdate(K key, IntUnaryOperator updaterFunction) {

AtomicInteger holder = new AtomicInteger();

map.compute(key, (k, value) -> {

// 如果获取key的value为空,则直接返回0 int oldValue = (value == null) ? 0 : value;

holder.set(oldValue);

return updaterFunction.applyAsInt(oldValue);

});

return holder.get();

}

获取并更新实现了,更新并获取就大同小异了,其余的API也只是对这个基础方法进行包装而已

键值对更特殊的优化-枚举计数器EnumIntCounter

枚举是个非常好的东西,让代码看起来非常简洁,不混乱,有明确定义,主要是有一种限定作用,避免产生参数值的错误。我们来想象一个需求,统计星期1-7当中,哪个星期玩家杀怪的数量最多,这里我们就可以把星期1-7做成一个枚举

public enum WEEK{

W_1,

W_2,

W_3,

W_4,

W_5,

W_6,

W_7,

}

这里不用星期的英文是因为我懒得去查了(属于说得出来拼不出来,看见又认识的那种,哈哈,野生英语水平)。既然枚举叫枚举,那在代码运行期间,他的数量肯定是一定的,所以我们在表示这种结构的时候不会像Map那样复杂,单纯的用一个int数组(int[])就行了,至于大小,刚才不是说了吗,枚举是固定的,所以我们就这样声明

private int[] counts;

public static > EnumIntCounter create(Class enumClass) {

return new EnumIntCounter<>(enumClass);

}

public EnumIntCounter(Class enumClass) {

counts = new int[EnumUtil.length(enumClass)];

}

这里获取枚举长度用的EnumUtil.length其实就是c.getEnumConstants().length,c是枚举Class。

实现get和put对于数组来说就非常简单了,只需要提供数据的index去做更改就行了。至于默认值为0,数据本身new出来就全部默认是0了。有时候还是需要通过枚举的编号去获取计数的,所以我们还得为get和put分别提供一个传int编号过来查找计数的重载方法

/*** 获取计数** @param e* @return*/

public int getCount(E e) {

return getCount(e.ordinal());

}

private int getCount(int ordinal) {

if (ordinal > counts.length - 1 || ordinal < 0) {

return 0;

}

return counts[ordinal];

}

/*** 放置计数** @param e* @param value* @return*/

public int putCount(E e, int value) {

return putCount(e.ordinal(), value);

}

private int putCount(int ordinal, int value) {

// 容错 if (ordinal > counts.length - 1) {

int[] temp = new int[ordinal + 1];

System.arraycopy(counts, 0, temp, 0, counts.length);

counts = temp;

}

int old = counts[ordinal];

counts[ordinal] = value;

return old;

}

同样的,实现了get和put,什么增加、减少、加一、减一我相信你自己就能搞定,就不赘述了。总之,这些东西虽然看起来非常简单,感觉人人都能想到,但是真正跑过去优化的人不多,自己总是抱怨写起烦躁,但是就是不动手搞一搞。我在项目上用到的这3种计数器让我写复杂逻辑的时候不再关心如何计数,身心健康,心旷神怡,再也不再想锤策划人员一顿了~好处就是这么多。

如果你也实现完了,我们来看看怎么用吧,巨简单!

EnumIntCounter tasksNewAgain = new EnumIntCounter<>(WEEK.class);

tasksNewAgain.incrementAndGet(WEEK.W_1);

哎呀!我去,舒服!

告辞!

java 中counter什么意思_方便适用的计数器Counter相关推荐

  1. java中execution的作用_一文初步了解Java虚拟机

    大家都知道,Java中JVM的重要性,学习了JVM你对Java的运行机制.编译过程和如何对Java程序进行调优相信都会有一个很好的认知. 什么是JVM? JVM(Java Virtual Machin ...

  2. 谈谈对java中分层的理解_让我们谈谈网页设计中的卡片设计

    谈谈对java中分层的理解 "I want a card", this is the first demand point that the customer said in th ...

  3. java中next的用法_关于java iterator的next()方法的用法

    UYOU next()是java迭代器类(Iterator)的方法,获得当前游标指向的下一个元素,详细说明和应用如下:1.迭代器(Iterator)介绍 迭代器是一种设计模式,它是一个对象,它可以遍历 ...

  4. java中打开文件显示_从java程序中打开任何文件

    在 java中打开文件似乎有点棘手 – 对于.txt文件,必须将File对象与Scanner或BufferedReader对象结合使用 – 对于图像IO,必须使用 ImageIcon类 – 如果要打开 ...

  5. java中字符串的创建_【转载】 Java中String类型的两种创建方式

    本文转载自 https://www.cnblogs.com/fguozhu/articles/2661055.html Java中String是一个特殊的包装类数据有两种创建形式: String s ...

  6. java中字符流 字节流_理解Java中字符流与字节流的区别

    1. 什么是流 Java中的流是对字节序列的抽象,我们可以想象有一个水管,只不过现在流动在水管中的不再是水,而是字节序列.和水流一样,Java中的流也具有一个"流动的方向",通常可 ...

  7. JAVA中流水账的实现_流水账式java基础Summary

    来一篇java基础知识的小总结,采用流水账式,不是特意的,而是不知道用什么形式把这些东西联系起来,那就想起什么写些什么吧 关键字:java中赋予特殊含义,具有专门用途的的单词,class.public ...

  8. java中的最终变量_在lambda表达式中使用的变量应该是最终变量或有效的最终变量。...

    A final变量意味着它只能被实例化一次.在Java中,您不能在lambda和匿名内部类中使用非最终变量. 您可以使用旧的for-each循环重构代码:private TimeZone extrac ...

  9. java 中如何实现多进程_在Java中可以使用哪些方法来实现Java的多进程运行模式?...

    在Java中我们可以使用两种方法来实现这种要求.最简单的方法就是通过Runtime中的exec方法执行java classname.如果执行成功,这个方法返回一个Process对象,如果执行失败,将抛 ...

  10. java中方法的具体化_我为什么要关心Java没有具体化的泛型?

    问题 这是我最近在一次采访中提出的一个问题,即候选人希望看到添加到Java语言中的问题.它通常被认为是Java没有的痛苦reified generics但是,当被推动时,候选人实际上无法告诉我他在那里 ...

最新文章

  1. ipv6的rip配置
  2. sqlite java excel,Android将Excel表数据导入SQLite数据库
  3. python easygui_EasyGUI是python的一个超级简单的GUI工具介绍(一)
  4. 解决修改/etc/sudoers文件后:报 语法错误 near line 23
  5. 安卓之上传文件,即HTTP提交表单
  6. Spaly_Tree 模版
  7. 自写sonar 插件加载到sonar 服务中的展示信息
  8. 窥探Swift编程之强大的Switch
  9. CF1556E-Equilibrium【栈,树状数组】
  10. 人这辈子没法做太多的事情
  11. 模块设计之“模块”与“模块化”
  12. 通过样式class也是可以控制超链接是否弹窗
  13. 字典树 之 hdu 1247
  14. 【信号处理第十二章】转置卷积
  15. DDK_HelloWorld卸载例程细化(驱动学习笔记四)
  16. Xcode12 兼容iOS14 及下载链接
  17. PMP培训机构哪家好,求推荐?
  18. 淘宝客搜索链接组成详解
  19. 来电弹屏--线程间操作无效: 从不是创建控件的线程访问它
  20. 跨namespace通信

热门文章

  1. java http 401_java HttpClient模拟登陆一直401
  2. 阿里云网站备案时变更备案的问题解决总结 满满干货
  3. inputBox 与 Application.inputBox 的用法与区别。
  4. after meet KeyNi liu
  5. android 6.0小游戏,宝宝汽车小游戏
  6. 银行卡卡号基于Luhn算法的格式校验
  7. pandas读取xls文件
  8. php 音频转换 WAV转MP3
  9. Codeforces894A QAQ
  10. 认知升级:从首席架构师到CTO