面临问题
在游戏中我们经常会遇到定时器,例如从大方面来每日的零点,节假日双倍经验,boss刷新,从小方面来说回合制中该回合在多长时间内结束,战斗中buff/debuff状态时间,技能的cd时间等等都涉及到定时器,前一种我们实现方式比较多,例如最常用的crontab,quartz都可以实现,而后者更像一种闹钟,到时候就去唤醒一个事件.我们今天就先讨论了下后面这种”闹钟”如何实现.
设计实现
要使用闹钟我们首先想到的就是Timer,但在java 1.5后全新推出了ScheduledThreadPoolExecutor,我们用它来对Timer进行替代,其次我们需要定义一个间隔周期,让这个定时器每隔多长时间去跑一下,看看有没有什么任务可以执行,这个间隔不能太长,太长会导致某些任务触发不及时,但也不能太短,太短容易加重系统开销,我们暂定50毫秒走一次定时器,伪代码看起来像这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
public class AlarmClock { private final static Logger LOG = LoggerFactory.getLogger(AlarmClock.class); // 默认间隔50ms private final static int INTERVAL = 50; private static AlarmClock clock; private volatile boolean active = false; private ScheduledThreadPoolExecutor executor; private AlarmClock() { executor = new ScheduledThreadPoolExecutor(DEFAULT_THREADS); } public synchronized static AlarmClock getInstance() { if (clock == null) { clock = new AlarmClock(); } return clock; } public void start() { if (active) { return; } active = true; executor.scheduleWithFixedDelay(new ScheduledTask(), 0, INTERVAL, TimeUnit.MILLISECONDS); } // 定时任务 class ScheduledTask implements Runnable { @Override public void run() { // TODO 需要触发的任务 } } } |
有了AlarmClock后,我们自然需要定义ScheduledTask中需要触发的方法,那这里我们就需要统一的Alarm事件接口,这里我们为简单使用就直接设计了AlarmEvent,代码就改成这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
public class AlarmClock { private final static Logger LOG = LoggerFactory.getLogger(AlarmClock.class); // 默认间隔50ms private final static int INTERVAL = 50; private static AlarmClock clock; private volatile boolean active = false; private CopyOnWriteArrayList<AlarmEvent> list; private ScheduledThreadPoolExecutor executor; private AlarmClock() { list = new CopyOnWriteArrayList<AlarmEvent>(); executor = new ScheduledThreadPoolExecutor(DEFAULT_THREADS); } public synchronized static AlarmClock getInstance() { if (clock == null) { clock = new AlarmClock(); } return clock; } public void start() { if (active) { return; } active = true; executor.scheduleWithFixedDelay(new ScheduledTask(), 0, INTERVAL, TimeUnit.MILLISECONDS); } public void fire(long currentTime) { for (AlarmEvent alarmEvent : list) { // 判断任务是否已经完成 if (alarmEvent.getCount() == 0) { list.remove(alarmEvent); } else if (currentTime >= alarmEvent.getNextTime()) { // 判断任务时间是否已到 alarmEvent.fire(currentTime); } } } public void addEvent(AlarmEvent event) { list.add(event); } public void removeEvent(AlarmEvent event) { list.remove(event); } // 定时任务 class ScheduledTask implements Runnable { @Override public void run() { try { fire(System.currentTimeMillis()); } catch (Exception e) { LOG.error("alarm clock fire error", e); } } } } |
根据上面AlarmEvent的操作,我们可以进行如下扩展,例如增加AlarmListener接口,用来实现onTime方法,同时我们还可以增加parameter属性这样AlarmEvent就能完成很多不同的任务.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
public class AlarmEvent { private final static Logger LOG = LoggerFactory.getLogger(AlarmEvent.class); private AlarmListener listener = null; private Object parameter = null; private int intervalTime = 0; private int count = 0; private int initTime = 0; private long currentTime = 0L; private long startTime = 0L; private long nextTime = 0L; private boolean absolute = false; /** * 构造方法 * * @param listener 侦听 * @param parameter 参数 * @param intervalTime 间隔时间 */ public AlarmEvent(AlarmListener listener, Object parameter, int intervalTime) { this(listener, parameter, intervalTime, -1, 0, false); } /** * 构造方法 * * @param listener 侦听 * @param parameter 参数 * @param intervalTime 间隔时间 * @param absolute 是否是绝对时间 */ public AlarmEvent(AlarmListener listener, Object parameter, int intervalTime, boolean absolute) { this(listener, parameter, intervalTime, -1, 0, absolute); } /** * 构造方法 * * @param listener 侦听 * @param parameter 参数 * @param intervalTime 间隔时间 * @param count 执行次数 */ public AlarmEvent(AlarmListener listener, Object parameter, int intervalTime, int count) { this(listener, parameter, intervalTime, count, 0, false); } /** * 构造方法 * * @param listener 侦听 * @param parameter 参数 * @param intervalTime 间隔时间 * @param count 执行次数 * @param absolute 是否是绝对时间 */ public AlarmEvent(AlarmListener listener, Object parameter, int intervalTime, int count, boolean absolute) { this(listener, parameter, intervalTime, count, 0, absolute); } /** * 构造方法 * * @param listener 侦听 * @param parameter 参数 * @param intervalTime 间隔时间 * @param count 执行次数 * @param initTime 初始时间后执行 */ public AlarmEvent(AlarmListener listener, Object parameter, int intervalTime, int count, int initTime) { this(listener, parameter, intervalTime, count, initTime, false); } public AlarmEvent(AlarmListener listener, Object parameter, int intervalTime, int count, int initTime, long startTime) { this(listener, parameter, intervalTime, count, initTime, startTime, false); } public AlarmEvent(AlarmListener listener, Object parameter, int intervalTime, int count, int initTime, boolean absolute) { if (listener == null) { throw new IllegalArgumentException(getClass().getName() + " listener is null"); } this.listener = listener; if (parameter == null) { throw new IllegalArgumentException(getClass().getName() + " parameter is null"); } this.parameter = parameter; this.intervalTime = intervalTime; this.count = count; this.initTime = initTime; this.absolute = absolute; this.startTime = System.currentTimeMillis(); this.nextTime = (this.startTime + initTime); } public AlarmEvent(AlarmListener listener, Object parameter, int intervalTime, int count, int initTime, long startTime, boolean absolute) { if (listener == null) { throw new IllegalArgumentException(getClass().getName() + " listener is null"); } this.listener = listener; if (parameter == null) { throw new IllegalArgumentException(getClass().getName() + " parameter is null"); } this.parameter = parameter; this.intervalTime = intervalTime; this.count = count; this.initTime = initTime; this.absolute = absolute; if (startTime == 0) { this.startTime = System.currentTimeMillis(); } else { this.startTime = startTime; } this.nextTime = (this.startTime + initTime); } public void fire(long currentTime) { this.count -= 1; this.currentTime = currentTime; try { listener.onTimer(this); } catch (Throwable e) { LOG.error("process alarm event error!", e); } this.nextTime = (absolute ? nextTime + intervalTime : currentTime + intervalTime); } // setter getter toString... } |
基本上这定时器就已经实现完毕了,基本上这个闹钟已经能完成很多工作,如果还有特殊需要可以自行扩展AlarmEvent,下面我们来看一个最简单的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Service public class AppListener implements AlarmListener { private final static Logger LOG = LoggerFactory.getLogger(AppListener.class); @Override public void onTimer(AlarmEvent alarmEvent) { LOG.debug("event:{}", alarmEvent); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@Service public class AppService { private final static Logger LOG = LoggerFactory.getLogger(AppService .class); @Autowired private AppListener appListener; public void doit() { AlarmClock.getInstance().start(); // 每10秒触发一次 参数是abc AlarmEvent event = new AlarmEvent(appListener, "abc", 10000); AlarmClock.getInstance().addEvent(event); } } |