月度归档:2013年11月

Java自动装箱/拆箱中的坑

Java的装箱/拆箱发生在包装类与其对应的基本类型之间,比如:

1
2
3
int int1 = 1;
Integer integer2 = new Integer(int1); //装箱
int int3 = integer2.intValue(); //拆箱

自JavaSE 5.0之后, Java支持了自动装箱/拆箱,所谓自动装箱/拆箱是指:

1
2
3
int int1 = 1;
Integer integer2 = int1; //自动装箱
int int3 = integer2;  //自动拆箱

以上来自维基。要说明的是,自动装箱/拆箱中的“自动”是编译器帮忙做的,以Integer类和int类型的转化为例,编译器会在int类型转Integer包装类时调用Integer的构造函数来进行实例化,会在Integer包装类转int类型时调用intValue()方法进行拆箱。

当我开始学习Java的时候早已是Java6的时代了,没有经历过装箱/拆箱的变迁,对自动装箱/拆箱的理解停留在理论上,所以实际中踩了坑也就不足为怪了。以下是我亲身经历的两次踩坑过程:

1. if(!null)表达式。
有次我写了段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Boolean check(){
    try{
        if(...){
            return true;
        }else{
            return false;
        }
    catch(Exception e){
    }
    return null;
}
 
public void doSth(){
    if(!check()){
        logger.error("error");
    }else{
         ...
    }
}

正常流程下check()方法返回true和false,但是偏偏我又想处理出现异常的情况,所以机智地在程序异常时返回null,试图用一个Boolean包装类来表示true,false和null三种状态。可能受C语言系语法影响吧,在外层doSth()方法的if语句中写下了!check()表达式,结果某天程序抛出一个运行时异常NullPointerException。因为编译器的自动拆箱机制,if语句处理后的代码变成if(!check().booleanValue())。如果check()方法返回null,if语句就会执行!null.booleanValue(),对一个null值调用任何方法显然都会得到一个NPE。

2. 包装类与基本类型进行关系运算。
不久前我大概写了以下代码:

1
2
3
4
5
6
JSONObject json = getHttpResponse(); // HTTP response text。
if (json.getInteger("code") != 112) {
    // error
    return;
}
...

getHttpResponse()方法调用一个HTTP restful接口,返回值是个JSONObject类型(fastjson),如果返回的json中有code属性,并且code值为112,则表示出现错误,否则,可以认为此次接口调用成功。实际上,上述程序在接口返回正确的情况下根本执行不下去,if语句中会抛出NullPointerException运行时异常。原因也是与编译器的自动拆箱有关,编译器拆箱后的if语句变为if (json.getInteger("code").intValue() != 112),显然,当HTTP接口返回值中不包含code属性时,json.getInteger("code")返回null,对null值调用intValue()会抛NPE异常。所以正确的做法是先判断json.getInteger("code")的返回值是否为null,如果不为null再去与112进行比较。

再细究下去,Java装箱/拆箱中还有一个坑,如果不知觉中写出了以下代码,那么实际上已经掉坑里了:

1
2
3
4
5
6
7
Integer i1 = getInteger1();
Integer i2 = getInteger2();
if(i1 == i2){
    System.out.println("equals");
}else{
    System.out.println("not equals");
}

其一,当==运算符的两个操作数都是对象时,它比较的是两个对象在内存中的地址。通常来说,两个对象的地址不会相同,所以要比较两个对象的值需要调用equals()方法。上述程序中编译器不会调用intValue()方法将i1和i2分别拆箱后再进行比较。其二,Java对包装类进行过一定优化,默认情况下,值为[-128,128)之间的Integer对象会缓存在内存中,也就是说,每个[-128,128)之间的Integer对象,如果它们的值相同,那么它们都是同一个对象(地址相同)。所以,上述程序中打印equals还是not equals完全视getInteger1()和getInteger2()方法的返回值而定。

--EOF--

如何用Spring实现集群环境下的定时任务

定时任务的实现方式有多种,例如JDK自带的Timer+TimerTask方式,Spring 3.0以后的调度任务(Scheduled Task),Quartz等。

Timer+TimerTask是最基本的解决方案,但是比较远古了,这里不再讨论。Spring自带的Scheduled Task是一个轻量级的定时任务调度器,支持固定时间(支持cron表达式)和固定时间间隔调度任务,支持线程池管理。以上两种方式有一个共同的缺点,那就是应用服务器集群下会出现任务多次被调度执行的情况,因为集群的节点之间是不会共享任务信息的,每个节点上的任务都会按时执行。Quartz是一个功能完善的任务调度框架,特别牛叉的是它支持集群环境下的任务调度,当然代价也很大,需要将任务调度状态序列化到数据库。Quartz框架需要10多张表协同,配置繁多,令人望而却步...

经过折中考虑,还是选择了Spring的Scheduled Task来实现定时任务。如下:

1. Spring配置文件application-context.xml中添加task命名空间和描述。

1
2
3
4
5
6
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:task="http://www.springframework.org/schema/task"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans.xsd
	http://www.springframework.org/schema/task
	http://www.springframework.org/schema/task/spring-task.xsd">

2. 添加调度器和线程池声明。

1
2
<task:executor id="taskExecutor" pool-size="10" />
<task:annotation-driven executor="taskExecutor" />

3. 实现调度方法。基本结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
package com.netease.yx.service;
 
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
 
@Service
public class ScheduledService {
    @Scheduled(cron = "0 0 5 * * *")
    public void build() {
       System.out.println("Scheduled Task");
    }
}

@Scheduled注解支持秒级的cron表达式,上述声明表示每天5点执行build任务。

前文已经提过,这种方式在单台应用服务器上运行没有问题,但是在集群环境下,会造成build任务在5点的时候运行多次,遗憾的是,Scheduled Task在框架层面没有相应的解决方案,只能靠程序员在应用级别进行控制。

如何控制?

1. 无非是一个任务互斥访问的问题,声明一把全局的“锁”作为互斥量,哪个应用服务器拿到这把“锁”,就有执行任务的权利,未拿到“锁”的应用服务器不进行任何任务相关的操作。
2.这把“锁”最好还能在下次任务执行时间点前失效。

在项目中我将这个互斥量放在了redis缓存里,1小时过期,这个过期时间是由任务调度的间隔时间决定的,只要小于两次任务执行时间差,大于集群间应用服务器的时间差即可。

完整定时任务类如下:

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
package com.netease.yx.service;
 
import javax.annotation.Resource;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import com.netease.yx.service.ICacheService;
 
@Service
public class ScheduledService {
    @Resource
    private ICacheService cache = null;
 
    private static String CACHE_LOCK = "cache_lock";
 
    private static int EXPIRE_PERIOD = (int)DateUtils.MILLIS_PER_HOUR / 1000;
 
    @Scheduled(cron = "0 0 5 * * *")
    public void build() {
        if (cache.get(CACHE_LOCK) == null) {
            cache.set(CACHE_LOCK, true, EXPIRE_PERIOD);
            doJob();
        }
    }
}

--EOF--

一种"无状态"的竞猜游戏设计

场景如下:某个电视节目,共6个选手,每期节目有一个或多个选手为获胜者。现要给这个节目开发一个与节目同步的竞猜游戏,观众参与竞猜谁是节目的获胜者,答对的有红包奖励。节目每周一期,周六21点播出,22:30结束。周四10点至周六22点为观众竞猜时间,周六22点至22:30为等待开奖时间,周六22:30至下个周四10点为领奖时间。然后下个节目周期开始,依此循环。

上述节目有三个时间点,开始竞猜时间(结束领奖时间,周四10点),停止竞猜时间(周六22点),开奖时间(周六22:30),这三个时间点前后会有较大的访问量,其余时间则较为平缓。一种设计方法是给每个用户保存一个状态,定时器在上述三个时间点修改用户的状态,这就是一种有状态的实现方式,优点是直接,用户访问页面后查一次数据库即可知道状态,显示对应的视图即可。缺点是三个时间点附近有大量的数据库读写,特别是开奖时间前后会涌入大量用户,应用服务器一边要等待数据库IO,处理缓存更新,一边要处理用户请求,会给响应时间带来一定影响。而且定时更新状态的任务在应用服务器集群下还要附加额外逻辑,避免不同服务器更新多次用户状态。

另外有一种无状态的设计方式,这里的无状态是指不用将用户的当前状态存储到数据库中。在用户访问应用之前,他的状态是未知的,当用户访问页面后,服务器根据一些参照值实时运算出该用户的当前状态。这种实现方式避免了第一种实现方式的两个缺点,首先三个时间点上不再有涉及全表的数据库操作,它将集中的状态更新操作平摊到每次用户访问页面时,其次应用支持简单的水平扩展。由于需要实时运算用户状态,这种实现方式会额外消耗CPU时间。从业务场景的特点来看,这种交换是值得的。

主要的实现方式比较简单:

1. 定义状态类型,定义以下8种状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 领奖期间的状态。周六22:30至下个周四10:00。
public static final int STATUS_PRIZE_NO_PARTI = 0; // 未参与
public static final int STATUS_PRIZE_PARTI_WIN_DRAW = 1; // 参与,压中,已领奖
public static final int STATUS_PRIZE_PARTI_WIN_NO_DRAW = 2; // 参与,压中,未领奖
public static final int STATUS_PRIZE_PARTI_NO_WIN = 3; // 参与,未压中
 
// 竞猜阶段的状态。周四10:00至周六22:00。
public static final int STATUS_GUESSING_CHOOSE = 4; // 已选择
public static final int STATUS_GUESSING_NO_CHOOSE = 5; // 未选择
 
// 节目播放阶段的状态。周六22:00至周六22:30。
public static final int STATUS_BROADCAST_CHOOSE = 6; // 已选择
public static final int STATUS_BROADCAST_NO_CHOOSE = 7; // 未选择

2. 用户访问页面后,以当期节目的播出时间为参照点,根据相对时间进行运算,确定3个时间点。确定当前时间所在的区间,再根据其他信息确定当前用户的状态,返回即可。

--EOF--