第二单元总结
1.同步块设置与锁的选择:
三次作业中基本都遵循生产者-消费者模式
- 第一次作业:因为第一次作业的结构较简单,故没有设置调度器,而是将Input线程直接通过传送带类与电梯相连.传送带类为TaskQueue类,其中有一个容器tasks装着所有的任务同时有end标记结束.put与get方法分别用来获得任务和放置任务,将这两个方法加锁,使得电梯与Input线程同时只有一个能访问修改tasks
- 第二次作业添加了横向电梯与增加电梯的请求.新增调度器线程Dispatch,调度器线程与电梯间共享数据即为TaskQueue类,此类不变.Input线程与Dispatch间通过TaskQueue类作为共享数据,以实现Input线程向Dispatch传输数据.同时在Dispatch中有taskQueues容器.且有put用于启动新的电梯并将对应的TaskQueue加入到容器中.alloc用于向taskQueues中添加任务.put,alloc两个方法需要加锁,即分配任务时不能启动新的电梯.
- 第三次作业的与第二次作业的实现基本相同,只需要在对应的加锁的方法中略作修改即可.
2.调度器如何与程序中的线程进行交互:
- 第一次作业比较简单,故没有设置调度器.
- 第二次作业调度器即Dispatch类为一个线程,通过生产者-消费者模型,InputQueue来从Input线程中获得任务.同时通过put方法来从Input线程中获得添加电梯的请求,同时启动新加入的电梯线程.与各个电梯之间通过生产者-消费者模型,将任务放入共享数据TaskQueue中实现.
- 第三次作业则设计了两级调度器,分别用于调度所有任务和调度某一楼层或楼座的任务.两级调度器中同样通过生产者-消费者模型InputQueue进行交互.
3.结合线程协同的架构模式:
-
第一次作业中没有设计调度器,而是将Input线程与5个电梯直接交互.在电梯运行算法方面采用了ALS算法
-
第二次作业在每一层每一座添加了调度器用于给电梯分发任务.在第一次作业过程中发现ALS算法思路较为复杂,并且性能方面相比look算法也没有多少提升,故在第二次作业中采用了look算法.look算法中比较重要的是在何时更改电梯的方向.对于纵向电梯:
direction == tempStep.getDirection() && tempStep.getDestinationFloor() != floor tempStep.getStartFloor() == floor direction == 1 && tempStep.getStartFloor() > floor direction == -1 && tempStep.getStartFloor() < floor
在以上三种情况下不更改运行方向
对于横向电梯:运行方向取决于内部的第一个任务目的地的最短路径或外部的第一个任务出发地的最短路径
-
第三次作业因为存在中转,故设计了两级调度器,一级调度器不仅接收Input线程的任务,还接收每个电梯中未完成的任务.
在第三次作业中,Task的路径采用了静态规划,即在任务输入时就根据现有的电梯规划好路线,即在哪一楼中转.这种方法的优点在于能够较为简单地规划路径,且性能尚可,但如果在请求之后又新增了其他有更好路径的电梯时则无法再用上.
如果在运行途中乘客的请求发生了变化,或突然有电梯退出,那么重新规划路线可以再次调用Task中的路径规划方法,可以扩展这一类功能.且由于使用了生产者消费者模式,各个线程之间的耦合度较低,每个线程共用的数据与方法较少,故扩展性较好.
-
三次作业中线程直接的交互均采用生产者-消费者模式,设置传送带类,这种方法结构较为清晰,且能够明确哪些部分需要加锁.
4.程序的bug:
-
在第一次作业中,由于采用了ALS算法,采用了主任务的模式,所以在上下电梯时分主任务与捎带任务,在主任务上下时忘记了更改电梯中的人数,导致电梯超载.
-
在第一次作业中,忘记输出包线程不安全,没有在输出时使用static或单例模式,导致输出的时间戳不合理
-
在第二次作业中,由于调度器线程的加入以及将电梯由ALS算法更改为look算法,导致线程无法结束.其原因为在end信号传入传送带时执行了notifyAll语句,但在这之后因为传送带中没有Task而导致线程wait了,使得线程无法进入判断end为真从而结束的语句.解决方法:在wait之前加入特判,end必须为假.
-
第三次作业中,因为添加电梯请求和搭乘请求都采用了生产者消费者模式,导致新添加的电梯不能第一时间启动,又因为在搭乘请求输入时即规划了路线,这会使得新加入的电梯必须等较长一段时间才能启用,影响了性能.故在添加电梯请求时不再使用生产者消费者模式,而是直接将电梯启动并将其队列加入到对应的调度器中.
input中: public void setElevator(ElevatorRequest elevatorRequest) { ... if (elevatorRequest.getType().equals("building")) { BuilElevator temp = new BuilElevator(...); buDispach.get(temp.getBuilding() - 'A').putBuElevator(temp); } ... } dispatch中: public void putBuElevator(BuilElevator builElevator) { synchronized (lock) { if (!endflag) { Thread thread; taskQueue.add(builElevator.getTaskQueue()); thread = new Thread(builElevator); thread.start(); } } }
5.分析bug思路:
- 测试策略:尽量将测试数据覆盖所有的代码的使用,同时将指导书中提及的临界条件进行测试.
- 采用大量不同线程的请求进行测试,着重测试调度器类的实现,因其上接input,下连elevator
- 本单元的测试策略更多的是集中在多个线程的协作与交互上,测试数据需尽可能多的涉及多个线程.而在第一单元的测试中,更多的是进行各种特殊情况,边界条件的测试.
6.心得体会:
- 线程安全:多线程的代码中,共享数据的安全是重点,需要保证共享数据在读写时不出现由于线程访问而产生的逻辑问题.在何处加锁,是关键,共享数据在同一个时候,可以由多个线程读,但不能同时写,或一个读一个写.后面两种情况若存在,则必须要加锁.
- 层次化设计:多个线程直接的协作关系,是有层次的,数据从Input线程到Dispatch再到Elevator.同时,对于不同功能的电梯,不同的传送带类可以采用继承的方式共用一些类似的代码.