synchronized 代码块测试用例
项目涉及到一个订单重复提交的问题,用一个token验证来解决,
客户端订单页面请求一个token,此token由服务端生成,并加入缓存,客户端提交订单时将token一并传入,服务端验证token,下单时将token置为无效,以此来防止重复提交,因为每个token只有一次真正入库的机会
验证token的过程有两个最主要的程序:
读token,写token,这两个语句必须处在同步中
业务类:(假定为多线程单例)
public class ResourceService { public SpResult buyResource(Map<String,String> map) { String cacheToken = map.get("cacheToken"); SpResult sr = new SpResult(); if(cacheToken == null || "".equals(cacheToken)) { sr.setHeaderMessage("您的token无效,请刷新页面。"); } else { synchronized (this) { Long cacheNow = HandleCache.getCache(cacheToken); if(cacheNow == null) { System.out.println("您已报过名,我们将在24小时之内联系您,请耐心等待。"); } else { Long now = System.currentTimeMillis(); Long minus = now - cacheNow; if(minus > HandleCache.expireTime) { System.out.println("您的token已过期,请刷新页面。"); } else { // resourceAccessor.buyResource(map); System.out.println("报名成功,我们将在24小时之内联系您。"); HandleCache.destroyCache(cacheToken); } } } } sr.setHeaderCode("200"); sr.setBody(null); return sr; } }
token缓存类:
public class HandleCache { protected static Map<String,Long> cache = new ConcurrentHashMap<>(); public static Long expireTime = new Long(1000*60*5); public static void setCache(String token) { Long now = System.currentTimeMillis(); cache.put(token, now); } public static Long getCache(String token) { Long temp = cache.get(token); return temp; } public static void destroyCache(String token) { cache.remove(token); } public static String makeToken() { String base = "abcdefghijklmnopqrstuvwxyz0123456789"; Random random = new Random(); StringBuffer sb = new StringBuffer(); for (int i = 0; i < 20; i++) { int number = random.nextInt(base.length()); sb.append(base.charAt(number)); } return sb.toString(); } public static void main(String[] args) { String cacheToken = HandleCache.makeToken(); HandleCache.setCache(cacheToken); Map<String,String> map = new HashMap<>(); map.put("phone", "13333333333"); map.put("targetId","0"); map.put("cacheToken",cacheToken); long startMili=System.currentTimeMillis(); for(int i = 0; i < 10; i++){ MyThread thread = new MyThread(map); thread.start(); } long endMili=System.currentTimeMillis(); System.out.println("总耗时为:"+(endMili-startMili)+"毫秒"); } }
线程类:
class MyThread extends Thread{ private static ResourceService resourceService = new ResourceService(); private static Map<String,String> map; public MyThread(Map<String,String> _map) { map = _map; } public void run() { resourceService.buyResource(map); } }
总耗时为:55毫秒
报名成功,我们将在24小时之内联系您。
您已报过名,我们将在24小时之内联系您,请耐心等待。
您已报过名,我们将在24小时之内联系您,请耐心等待。
您已报过名,我们将在24小时之内联系您,请耐心等待。
您已报过名,我们将在24小时之内联系您,请耐心等待。
您已报过名,我们将在24小时之内联系您,请耐心等待。
您已报过名,我们将在24小时之内联系您,请耐心等待。
您已报过名,我们将在24小时之内联系您,请耐心等待。
您已报过名,我们将在24小时之内联系您,请耐心等待。
您已报过名,我们将在24小时之内联系您,请耐心等待。
十个线程抢入库,仅有第一次真正入库。这里采用存放token的缓存对象cache和验证过程getCache+destroyCache 双重加锁,耗时55ms
如果拆掉getCache+destroyCache的过程锁,
public SpResult buyResource(Map<String,String> map) {
String cacheToken = map.get("cacheToken");
SpResult sr = new SpResult();
if(cacheToken == null || "".equals(cacheToken)) {
sr.setHeaderMessage("您的token无效,请刷新页面。");
} else {
// synchronized (this) {
Long cacheNow = HandleCache.getCache(cacheToken);
if(cacheNow == null) {
System.out.println("您已报过名,我们将在24小时之内联系您,请耐心等待。");
// sr.setHeaderMessage("您已报过名,我们将在24小时之内联系您,请耐心等待。");
} else {
Long now = System.currentTimeMillis();
Long minus = now - cacheNow;
if(minus > HandleCache.expireTime) {
System.out.println("您的token已过期,请刷新页面。");
// sr.setHeaderMessage("您的token已过期,请刷新页面。");
} else {
// resourceAccessor.buyResource(map);
System.out.println("报名成功,我们将在24小时之内联系您。");
HandleCache.destroyCache(cacheToken);
// sr.setHeaderMessage("报名成功,我们将在24小时之内联系您。");
}
}
// }
}
sr.setHeaderCode("200");
sr.setBody(null);
return sr;
}
总耗时为:46毫秒
报名成功,我们将在24小时之内联系您。
报名成功,我们将在24小时之内联系您。
报名成功,我们将在24小时之内联系您。
报名成功,我们将在24小时之内联系您。
报名成功,我们将在24小时之内联系您。
报名成功,我们将在24小时之内联系您。
报名成功,我们将在24小时之内联系您。
报名成功,我们将在24小时之内联系您。
报名成功,我们将在24小时之内联系您。
报名成功,我们将在24小时之内联系您。
十个全抢到了,经过反复运行,也存在4个抢到,6个没抢到这样的情况,处于随机状态
3.12补充,全程加锁的模式,效率不高,下面参考单例模式,进行双重判断,第一次判断后加锁:
public SpResult buyResource(Map<String,String> map) { String cacheToken = map.get("cacheToken"); SpResult sr = new SpResult(); if(cacheToken == null || "".equals(cacheToken)) { sr.setHeaderMessage("您的token无效,请刷新页面。"); } else { if(HandleCache.getCache(cacheToken) != null) { synchronized (this) { Long cacheNow = HandleCache.getCache(cacheToken); if(cacheNow != null) { Long now = System.currentTimeMillis(); Long minus = now - cacheNow; if (minus > HandleCache.expireTime) { System.out.println("您的token已过期,请刷新页面。"); sr.setHeaderMessage("您的token已过期,请刷新页面。"); } else { // resourceAccessor.buyResource(map); System.out.println("报名成功,我们将在24小时之内联系您。"); HandleCache.destroyCache(cacheToken); sr.setHeaderMessage("报名成功,我们将在24小时之内联系您。"); } } else { System.out.println("您已报过名lock,我们将在24小时之内联系您,请耐心等待。"); sr.setHeaderMessage("您已报过名lock,我们将在24小时之内联系您,请耐心等待。"); } } } else { System.out.println("您已报过名nlock,我们将在24小时之内联系您,请耐心等待。"); sr.setHeaderMessage("您已报过名nlock,我们将在24小时之内联系您,请耐心等待。"); } } sr.setHeaderCode("200"); sr.setBody(null); return sr; }
让一部分的线程不需要阻塞便可以进行下去,输出如下:
报名成功,我们将在24小时之内联系您。
您已报过名nlock,我们将在24小时之内联系您,请耐心等待。
您已报过名lock,我们将在24小时之内联系您,请耐心等待。
您已报过名lock,我们将在24小时之内联系您,请耐心等待。
您已报过名nlock,我们将在24小时之内联系您,请耐心等待。
您已报过名nlock,我们将在24小时之内联系您,请耐心等待。
您已报过名nlock,我们将在24小时之内联系您,请耐心等待。
您已报过名nlock,我们将在24小时之内联系您,请耐心等待。
您已报过名nlock,我们将在24小时之内联系您,请耐心等待。
总耗时为:14毫秒
您已报过名nlock,我们将在24小时之内联系您,请耐心等待。
Process finished with exit code 0
可以看到,有7个线程未阻塞,直接判断到null,证明已经处理掉了,2个线程判断到非null,进尔再加锁处理读写