现在的位置: 首页 > 综合 > 正文

电影票项目之Worker多线程

2018年04月10日 ⁄ 综合 ⁄ 共 6069字 ⁄ 字号 评论关闭

《项目总结》专栏地址http://blog.csdn.net/column/details/project-summary.html

上期:

 电影电子票项目-项目流程体验
 电影电子票项目-页面控制worker

前一篇项目总结里,说了电影票项目的worker的界面控制。这里来说一下多线程的运行,并且这里当时开发的还出现过一个问题。所以说,多线程,也最容易出问题。

 

场景:取一百部电影近3天的排期信息,创建一个线程池,多线程去请求数据

代码:2个类,一个配置文件,采用模拟方式,构造数据,模拟执行库的update等

错误的方式其实就在这一段代码里:

for (CinemaDetail cinemaDetail : cinemaDetails) {
    for (int i = 1; i <= 3; i++) {// 一次循环取一家影院一天的排期
        System.out.println(Thread.currentThread().getName() + "取影院ID:" + cinemaDetail.getCinemaId() + "第" + i + "天的排期"); 
        cinemaDetail.setIssueDate(i);
        executorService.execute(new getDataTask(cinemaDetail));
        while (executorService.getQueue().size() >= (queueSize - 3)) {
            // 等待队列有空位置,任务先创建corePoolSize大小的线程,再往队列中压,超队列再创建线程直到maxPoolSize
            // executorService.getActiveCount(),由于通过work.isLock判,不在一个线程中,线程对象被创建,不表示马上会调度,使isLock返回TRUE,会有并发问题
            // executorService.getPoolSize(),等待超过空闲,线程回收后才会变化
            System.out.println("取排期有几积压,在排队");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                // 不处理
            }
        }
    }
}

眼尖的同学可能已经看到问题所在了。

先来剖析一下这个主要的业务处理类吧

在Spring初始bean时,先初始一下线程池:

public void init() {
    // 创建一个固定大小的线程池
    executorService = new ThreadPoolExecutor(threadSize, maxThreadSize, idletime, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(queueSize));
}

这里有文档介绍ThreadPoolExecutor

在这个bean创建的时候,静态初始100条影院数据

static {
        System.out.println("开始创建:---------");
        long start = System.currentTimeMillis();
        cds = new ArrayList<CinemaDetail>();
        for (Integer i = 1; i <= 100; i++) {
            CinemaDetail cd = new CinemaDetail();
            cd.setCinemaId(i);
            cd.setCinemaName(i.toString());
            cds.add(cd);
        }
        System.out.println("结束创建,总耗时:" + (System.currentTimeMillis() - start));
    }

我下面展示的所以输出都是把log替代成直接输出到控制台,具体要执行的方法:

public void execute() {    
    if (executorService == null) {
        throw new NullPointerException("executorService is null,please call init()!");
    }
    
    boolean flag = true;
    while (flag) { 
        List<CinemaDetail> cinemaDetails = getAllCinemas(iCinemaId, pageSize);
        
        if (CollectionUtils.isEmpty(cinemaDetails)) {
            flag = false;
            System.out.println(Thread.currentThread().getName() + "无影院,本轮同步结束");
            break;
        }
        // 记录下次循环需要执行的位置
        iCinemaId = iCinemaId + pageSize;
        System.out.println(Thread.currentThread().getName() + "取所有影院排期信息===task===>开始,iCinemaId" + iCinemaId);
        for (CinemaDetail cinemaDetail : cinemaDetails) {
            for (int i = 1; i <= 3; i++) {// 一次循环取影院每一天的排期
                System.out.println(Thread.currentThread().getName() + "取影院ID:" + cinemaDetail.getCinemaId() + "第" + i + "天的排期");
                cinemaDetail.setIssueDate(i);
                executorService.execute(new getDataTask(cinemaDetail));
                while (executorService.getQueue().size() >= (queueSize - 3)) {
                    System.out.println("取排期有几积压,在排队");
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        // 不处理
                    }
                }
            }
        }
        while (executorService.getActiveCount() > 0) {
            // 本次时间任务跑完后,才能处理下次时间任务的调度
            log.debug("取排期等待本次投注任务调度完成");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                // 不处理
            }
        }
    }
    System.out.println("取所有影院排期信息===task===>结束");
}

然而executorService.execute(new getDataTask(cinemaDetail));所执行的就是一个模拟处理,这里只计数和打印信息,看一下代码

public class getDataTask implements Runnable {
    private CinemaDetail cinemaDetail;
    
    public getDataTask(CinemaDetail cinemaDetail) {
        this.cinemaDetail = cinemaDetail;
    }
    
    public void run() {
        executeData(cinemaDetail); 
    }
}

具体执行如下:

private void executeData(CinemaDetail cinemaDetail) {
    count.addAndGet(1);
    System.out.println(Thread.currentThread().getName() + "当前执行数:" + count.get() + "影院ID:" + cinemaDetail.getCinemaId() + "排期信息:第"
            + cinemaDetail.getIssueDate() + "天");
    
    try {
        Thread.sleep(500);//模拟处理业务逻辑
    } catch (InterruptedException e) {
        // 不处理
    }
}


那再来看一下配置文件,具体配了些啥,为了简单点,我就没有用上次说的Scheduler了,直接用随应用启动而启动,启动时间是每59秒一次。

<!--  JOB start -->
<bean id="taskMultiThread" class="com.project.task.multithreading.TaskMultiThread" init-method="init" destroy-method="destroy"/>
   
<bean id="taskMultiThreadTask" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<!-- false表示,当前Worker未完成时,即便到了启动时间点,也不启动新Worker -->
	<property name="concurrent" value="false"></property>
	<property name="targetObject">
		<ref bean="taskMultiThread" />
	</property>
	<property name="targetMethod">
		<value>execute</value>
	</property>
</bean>

<bean id="taskMultiThreadJob" class="org.springframework.scheduling.quartz.CronTriggerBean">
	<property name="jobDetail">
		<ref bean="taskMultiThreadTask" />
	</property>
	<property name="cronExpression">
		<value>0/59 * * * * ?</value>
	</property>
</bean>
<!-- JOB end -->

<bean id="startQuertz" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
	<property name="triggers">
		<list>
			<ref bean="taskMultiThreadJob" />
		</list>
	</property>
</bean>


打包,启动程序,执行结果,一看,咋就不对呢?


全取到的是第三天的结果,就是对第三天的结果进行了三次处理

这里也模拟出来了,需要排队等待的请况:


执行结束时的结果

这里会有quartz worker 2的原因是我把配置文件里的concurrent属性配成了true,请看配置处对此属性的注释


从运行结果来看,基本不会执行取第一天,第二天的数据,全执行的是第三天的数据(这时的脑袋是短路了,根本不会先问题),多线程造成的?

那我们用同步来试试?

synchronized (cinemaDetail) {
    cinemaDetail.setIssueDate(i);
    executorService.execute(new getDataTask(cinemaDetail));
}


如果没有发现问题的本质,你同步也是不行的。请看结果:

冷静下来,再看,你会发现,其实不是线程公用变量之类的事情造成的

for (CinemaDetail cinemaDetail : cinemaDetails) {
    //一次循环,取1家影院近3天排期数据
    for (int i = 1; i <= 3; i++) {
        // 一次循环取一家影院一天的排期
        cinemaDetail.setIssueDate(i);
        executorService.execute(new getDataTask(cinemaDetail));
    }
}

再来仔细看一下这个for循环,这里我去掉了一些不必要的信息,这是两个循环引起的,再认真看一下注释。在第二个循环来,我们还是用的同一个对象(引用)。那我们每次做的set操作,都是针对同一个引用来的。而当多线程去执行的时候,当然只认现在这个引用的状态,在这里也就是当前的属性(最后一次修改为3)。想明白这里之后,改起来应该就方便了。有几种方式可以选择,可以clone一个对象,或者新建一个对象。

这里有文档对clone的介绍:


如果采用clone的话,CinemaDetail这个类需要实现Cloneable接口,如下:

public class CinemaDetail implements Cloneable{
    public CinemaDetail clone() {
        CinemaDetail cd;
        try {
            cd = (CinemaDetail) super.clone();
            return cd;
        } catch (CloneNotSupportedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return null;
    }
}

然后for循环里进行相应的修改:

for (CinemaDetail cinemaDetail : cinemaDetails) {
    //一次循环,取1家影院近3天排期数据
    for (int i = 1; i <= 3; i++) {
        CinemaDetail cinemaDetailClone = cinemaDetail.clone();
        // 一次循环取一家影院一天的排期
        cinemaDetailClone.setIssueDate(i);
        executorService.execute(new getDataTask(cinemaDetailClone));
    }
}


如果采用new对象的话,相应修改如下:

for (CinemaDetail cinemaDetail : cinemaDetails) {
    //一次循环,取1家影院近3天排期数据
    for (int i = 1; i <= 3; i++) {
        // 一次循环取一家影院一天的排期
        CinemaDetail cinemaDetailNew = new CinemaDetail();
        //有多少属性,就复制多少属性。。。如果属性多就麻烦了
        cinemaDetailNew.setCinemaId(cinemaDetail.getCinemaId());
        cinemaDetailNew.setCinemaName("cinemaName");
        cinemaDetailNew.setIssueDate(i);
        executorService.execute(new getDataTask(cinemaDetailNew));
    }
}

这两种方式,都可以正常的运作了。

看一下运行的结果吧

解决了之后,心里还是美滋滋的,项目也正常往下进行着。

总结:

有多线程的使用时,全局变量的使用需要谨慎

Synchronized也并不是万能的,首先应明白原因

一个对象在两重以上的循环里处理时,需要注意

多线程ThreadPoolExecutor创建的使用

对象的clone,需要实现cloneable

SpringQuartz的配置熟练使用

 

总结中,有些是针对这次的问题的,有些是针对这次的内容的。

抱歉!评论已关闭.