如今框架横行,Spring 已经是非常成熟的容器体系,我们在日常开发 JavaWeb 的工作中,大多已经不需要考虑多线程的问题,这些问题都已经在Spring容器中实现,框架的意义就是让程序员们可以专注于逻辑的实现。然而这种编程工作是非常无趣无味的,如果长期从事这个工作,技术不一定见长,业务知识一定很熟悉!= =但说实在的,我并不喜欢这类工作,因为这种工作大多情况下知识对代码的简单复制,或是简单的一些编写,并没有什么真正的创造性,不会给人成就感。

  需求背景


  我们的项目,是 Mysql+ElasticSearch 做的一个数据库和搜索引擎,项目经理提出需要做一个用于重建 ES 搜索数据的接口,这个任务很光荣的交给了我。

  在功能的编写过程当中,我突然思考这样一个问题,因为我们 Web 项目本身是多线程的,那如果在同一时间段,有多个请求同时发起,那同时发起 ES 的重建,对于 ES 来说,可能会产生一些莫名其妙的问题。

  所以我感到非常高兴,因为这个问题,似乎不是听起来的那么简单。于是乎我想到了,要加入同步锁了。

最开始的思考:


  最开始我只是很简单的想,直接在对应的 Service 层写一个方法,然后直接加一个

synchronized(this)

在整个方法体上。    

 @Override
public synchronized int rebuiltBountyData() throws Exception {
...
}

  可是问题来了:


  但是这个方法很快就联想到了另一个问题:

  我们是希望不要多线程同时重建数据,但是如果排队重建呢?好像也不是我们想要的结果。我希望的是当一个线程在执行重建任务的时候,另一个线程要被拒绝开始任务,而不是等待上一个任务做好后再开始。因为我们 tomcat 是采用线程池的概念,如果所有线程都执行这个方法,最后每个线程都会处于等待状态,结果其他请求就会因为没有空闲的线程可用,而无法正常执行。

  so,我们修改了一下思路:

  在 Service 的这个实现类中,添加一个私有类成员对象  flag = false,当线程进入时,判断 flag 是否为 true,是,则直接抛出异常,结束线程。否,则修改 flag 的值为 true,然后开始执行线程任务,并且,我们对这个 flag 加上一个同步锁,例如:我们在代码中使用时,加入这样一段

synchronized(flag)

  由于 Spring 默认是单例模式,所以这个flag 在多个线程中是共享的,这样就不需要将这个flag 设置为 static 了,因为它在这个局部当中实现了类似 static 的作用。但是这个时候,flag 不能是基础类型,必须是 Boolean 包装类型。那就会产生另一个隐患:包装类的对象仅仅是一个引用,引用是可以被更换了,比如使用了这个 flag 的 set 方法来修改值,但是同步锁取得是引用的锁,而不是引用对应那个实例的锁,锁了引用却没锁实例,但我们实际上却要根据实例的状态来判断,这就会造成一个隐患,可能会使得同步锁失效。

  那使用 this 来获得整个 Service 类的同步锁,貌似可以解决问题(如下面这段代码具体实现),但是如果万一以后这个 Service 还有其他需要用到同步锁的需求怎么办呢?这样就会让两个不想干的业务逻辑因为同步锁的问题产生互相的影响。添加同步锁,要尽可能的缩小同步锁的获取范围,和锁内代码的代码量,这样才能减少冲突和线程获取锁时等待的时间,提高软件的安全性和执行效率。

  而且,我们的需求在这个时候,又有了变化,项目经理说,有两张表都需要做这种功能。就是说两个业务内容,都需要进行ES 的数据重建。所以如果每次增加一个,我就要单独写一个类似下面这段代码,不仅代码的可复用性降低了,而且以后换别人来维护的时候,说不定会写错这些内容。

 @Override
public int rebuiltBountyData() throws Exception {
//锁住资源防止多线程重复发起任务
synchronized (this) {
if (hasThread) {
throw new RebuiltBountyEsException("搜索引擎重建任务已经在执行,请勿重复发起!");
} else {
hasThread = true;
}
}
//获取总数
int count = bountyMapper.countNum(); int pageTotal, pageSize = 1000; if (count % pageSize != 0) {
//若不能整除,则页数加1
pageTotal = count / pageSize + 1;
} else {
pageTotal = count / pageSize;
} try {
for (int pageNum = 1; pageNum <= pageTotal; pageNum++) {
//分页查询数据库的数据
PageHelper.startPage(pageNum, pageSize);
List<Bounty> bountyList = bountyMapper.selectForRebuiltES();
//添加到 ES 引擎
bountyDao.add4List(bountyList);
}
} catch (Exception e) {
throw e;
} finally {
hasThread = false;
}
return count;
}

  那我们该怎么办好呢?


  最好的办法,就是把这个需要“加锁”的逻辑,单独赋予一个对象,让这个锁的范围能够缩小到只针对这个逻辑,这个功能,而不要跟其他的功能混在一起。 然后我们需要对这个功能,进行进一步的抽象。

  我们来好好观察上面这段代码,上面这段代码,算是已经实现了整个功能,从头到尾分解一下这段代码的功能,可以看得出如下:

  1.   单线程检查
  2.   分页处理
  3.   获取数据
  4.   写入 ES

  So,我们可以看到,其实不同业务场景下,线程检查是一模一样的代码,而分页处理中,获取数据总条数会根据不同业务场景而不同,其他代码也都是相同的,至于写入 ES 的部分,如果数据结构跟从数据库中获取的实体对象没有区别的话,这个也是可以看做是相同的而不需要特别的处理,但是我们公司的项目中,因为种种原因,ES 中的数据结构和实体对象是不同的(尽管数据字段都是相同,我表示我不知道怎么跟你们说这个历史遗留的奇葩问题...)。在这里,我们要应用一个设计模式,是模板模式,将固定的流程代码封装起来。再将可变的部分,留给子类实现。

 /**
* 类说明:从 JDBC 中获取重建 ES 的数据
*/
@Service
public abstract class JdbcRebuiltEsService<E> extends BaseService{ protected Logger log = LoggerFactory.getLogger(getClass()); private boolean threadLock = false;//线程锁 @Value("1")
private int startPage;//开始页码 @Value("1000")
private int pageSize;//页面容量 protected abstract int countTotalData() throws Exception; protected abstract Collection<E> loadDataSource(int pageSize, int pageNum) throws Exception; protected abstract void writeToElasticSearch(Collection<E> collection) throws Exception; /**
* 检查线程锁
*
* @throws Exception
*/
private void checkLock() throws Exception { //这段代码需要保证线程安全
synchronized (this) {
if (threadLock) {
//如果已经有线程占用,后续线程进入则抛出异常,因为本接口只允许单线程执行
throw new RebuiltEsTaskExistException("已经有重建任务正在执行,请等待结束后再发起新任务!");
} else {
//如果没有线程占用,则新线程进入后将改成线程占用状态
threadLock = true;
log.info("用户[{}]发起 ES 重建任务!其他重建任务请求将被拒绝!", getUserJid());
}
}
} /**
* 数据重建
*
* @return
* @throws Exception
*/
public int rebuild() throws Exception { checkLock();
log.info("#=== ES 重建任务开始执行"); int totalNum = countTotalData();
log.info("本次重建预计总记录数{}", totalNum); int pageTotal;
int pageNum = this.startPage;
int pageSize = this.pageSize; //根据条目总数计算总页数
if (totalNum % pageSize != 0) {
//若不能整除,则页数加1
pageTotal = totalNum / pageSize + 1;
} else {
pageTotal = totalNum / pageSize;
} long startTime = System.currentTimeMillis();//任务开始计时 try {
while (pageNum <= pageTotal) {
//分页查询数据库的数据并同时发送到 ES
writeToElasticSearch(loadDataSource(pageSize, pageNum));
pageNum++;
}
} catch (Exception e) {
Double progress = (Double) (pageNum * 1.0) / (Double) (pageSize * 1.0);
DecimalFormat decimalFormat = new DecimalFormat("##.00%");
log.info("重建异常中断,当前已重建进度为:{}", decimalFormat.format(progress));
throw e;
} finally {
threadLock = false;//不论是否成功,当线程退出时,都需要将线程状态改为非占用
long endTime = System.currentTimeMillis();//任务结束计时
log.info("#=== ES 重建任务执行结束,耗时:{}毫秒", endTime - startTime);
} return totalNum;
} public int getStartPage() {
return startPage;
} public void setStartPage(int startPage) {
this.startPage = startPage;
} public int getPageSize() {
return pageSize;
} public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
}

  OK,这样就解决了。复写三个容易跟随应用场景不同,而改变的方法,分别是,获取数据源,获取数据总条目,写入 ES。然后暴露 rebuild 方法给外部调用,在 rebuild 方法内部,实现整个运作流程,这样也可以避免以后有人需要做新的实现的时候,修改到这部分有涉及到同步锁的代码,以避免安全隐患。

  实际使用的时候可以这样用,创建一个子类继承这个 JdbcRebuiltEsService

 /**
* 类说明:商品信息 ES 重建所需要实现的具体方法
*/
@Service
public class GoodsRebuiltEsServiceImpl extends JdbcRebuiltEsService<Goods> { @Autowired
private GoodsMapper goodsMapper; @Autowired
private DrawingDAO drawingDAO; @Override
public int countTotalData() throws Exception {
return goodsMapper.countNum();
} @Override
public Collection<Goods> loadDataSource(int pageSize, int pageNum) throws Exception {
PageHelper.startPage(pageNum, pageSize);
return goodsMapper.selectForRebuiltES();
} @Override
public void writeToElasticSearch(Collection<Goods> collection) throws Exception {
drawingDAO.addBatch(collection);
}
}

  这样以后每次使用,都只需要实现一个新的子类,然后这样调用:

   @Autowired
@Qualifier("goodsRebuiltEsServiceImpl")
private JdbcRebuiltEsService<Goods> jdbcRebuiltEsService; /**
* 数据重建
*
* @return
* @throws Exception
*/
@Override
public int rebuiltEsGoodsData() throws Exception {
return jdbcRebuiltEsService.rebuiltd();
}

  这样 rebuilt 方法就很安全的被调用,将程序中不希望被修改的部分,用父类写好,只留下希望被复写的部分,这样就可以很好的保护比较关键的部位,当然了,public 方法也是可以重写的,不过这就超出了我们“以防万一,不小心写错”的初衷了,如果需要重写,那就重写呗。

[多线程] Web 项目中,少有涉及到的一次多线程编程的经验的更多相关文章

  1. 转 web项目中的web.xml元素解析

    转 web项目中的web.xml元素解析 发表于1年前(2014-11-26 15:45)   阅读(497) | 评论(0) 16人收藏此文章, 我要收藏 赞0 上海源创会5月15日与你相约[玫瑰里 ...

  2. (转)关于java和web项目中的相对路径问题

    原文:http://blog.csdn.net/yethyeth/article/details/1623283 关于java和web项目中的相对路径问题 分类: java 2007-05-23 22 ...

  3. 对Java Web项目中路径的理解

    第一个:文件分隔符 坑比Window.window分隔符 用\;unix采用/.于是用File.separator来跨平台 请注意:这是文件路径.在File f = new File(“c:\\hah ...

  4. 一个Web项目中实现多个数据库存储数据并相互切换用过吗?

    最近公司一个项目需要连接多个数据库(A和B)操作,根据不同的业务模块查询不同的数据库,因此需要改造下之前的spring-mybatis.xml配置文件以及jdbc.properties配置文件,项目后 ...

  5. 由web项目中上传图片所引出的路径问题

    我在做javaweb项目的时候,有个项目中需要进行图片的上传,有次我重新部署项目后,发现之前上传的图片不见了,最后找出原因:图片上传在服务器目录上,而不是绝对路径,所以特别想弄清楚javaweb项目中 ...

  6. SpringBoot Web项目中中如何使用Junit

    Junit这种老技术,现在又拿出来说,不为别的,某种程度上来说,更是为了要说明它在项目中的重要性. 凭本人的感觉和经验来说,在项目中完全按标准都写Junit用例覆盖大部分业务代码的,应该不会超过一半. ...

  7. php课程 1-3 web项目中php、html、js代码的执行顺序是怎样的(详解)

    php课程 1-3 web项目中php.html.js代码的执行顺序是怎样的(详解) 一.总结 一句话总结:b/s结构 总是先执行服务器端的先.js是客户端脚本 ,是最后执行的.所以肯定是php先执行 ...

  8. 在普通WEB项目中使用Spring

    Spring是一个对象容器,帮助我们管理项目中的对象,那么在web项目中哪些对象应该交给Spring管理呢? 项目中涉及的对象 ​ 我们回顾一下WEB项目中涉及的对象 Servlet Request ...

  9. 在基于MVC的Web项目中使用Web API和直接连接两种方式混合式接入

    在我之前介绍的混合式开发框架中,其界面是基于Winform的实现方式,后台使用Web API.WCF服务以及直接连接数据库的几种方式混合式接入,在Web项目中我们也可以采用这种方式实现混合式的接入方式 ...

随机推荐

  1. nginx代理tomcat后,tomcat获取真实(非proxy,非别名)nginx服务端ip端口的解决方案

    nginx代理tomcat后,tomcat获取服务端ip端口的解决方案 1.注意修改nginx配置代理,标红地方 #user nginx; worker_processes ; error_log l ...

  2. js正则表达式验证

    有时候会要验证自己写的正则表达式是否正确 所以写了这个小东西: demo:js正则表达式验证 html: <h3>绿色表示匹配,红色表示不匹配</h3> <label&g ...

  3. Unity UGUI —— 无限循环List

    还记得大学毕业刚工作的时候是做flash的开发,那时候看到别人写的各种各样的UI组件就非常佩服,后来自己也慢慢尝试着写,发现其实也就那么回事.UI的开发其实技术的成分相对来说不算多,但是一个好的UI是 ...

  4. iOS 之 socket 与 http

    http连接:短连接,发送一次请求,服务器响应后连接就断开. socket连接:长连接,连接后长期保持连接状态.

  5. Heka 编译安装后 运行报错 panic: runtime error: cgo argument has Go pointer to Go pointer

    Heka 编译安装后 运行报错 panic: runtime error: cgo argument has Go pointer to Go pointer 解决办法: 1.  Start heka ...

  6. Spring mvc 数据验证

    加入jar包 bean-validator.jar 在实体类中加入验证Annotation和消息提示 package com.stone.model; import javax.validation. ...

  7. MySQL锁详解

    一.概述 数据库锁定机制简单来说就是数据库为了保证数据的一致性而使各种共享资源在被并发访问访问变得有序所设计的一种规则.对于任何一种数据库来说都需要有相应的锁定机制,所以MySQL自然也不能例外.My ...

  8. oracle数据库包package小例子

    为了把某一个模块的函数.存储过程等方便查询维护,可以把它们打到一个包里.下面给出一个简单的小例子. 1.创建包头 create or replace package chen_pack is func ...

  9. Word常用实用知识3

    纯手打,可能有错别字,使用的版本是office Word 2013 转载请注明出处 http://www.cnblogs.com/hnnydxgjj/p/6322813.html,谢谢. 分页符分页 ...

  10. python实现二分查找与冒泡排序

    二分查找,代码如下: def binarySearch(l, t): low, high = 0, len(l) - 1 while low < high: 'print low, high' ...