面向对象程序设计第二单元总结
OO 第二单元总结
一、同步块的设置和锁的选择
第五次作业
在第五次作业中,我编写了一个RequestQueue
类。这个类作为调度器和电梯线程之间的桥梁:调度器Scheduler
可以向这个类中传入请求(用Req
类表示,下面相同),而电梯线程类也可以从每个线程自身的请求队列中读取请求并进行处理。
由于调度器和电梯线程都会使用这个类,并分别进行写/读操作,因此RequestQueue
类需要上锁并进行相应的代码处理。在第五次作业中,每个RequestQueue
会出现在两个地方:第一次是从读取线程InputThread
中读取投喂器发送的请求;第二次是为每个电梯线程存储请求。
在保证线程安全方面,我对每个RequestQueue
上锁的方案为:
- 使每个方法都成为
synchronized
的 - 在方法结束的时候,调用
notifyAll()
- 线程调用
waitForReq
以等待一个新的请求的时候,调用wait()
这样就保证了两个线程(调度器,电梯本身)共同访问RequestQueue
时的线程安全。
另外,为了使输出的时间戳是递增的,我在每个类中传入了一个共享对象Object
,并在输出的时候将输出语句放在同步块中 , 如:
synchronized (obj) {
TimableOutput.println(String.format("OPEN-%c-%d-%d", buildingName, floor, buildingId));
}
第六次作业
第六次作业中,由于电梯与调度器的交互行为没有太大的变动,因此在同步块和上锁方面与第五次作业相同,主要在RequestQueue
中进行上锁和处理。
在本次作业中,为了保证输出的线程安全,我不再将共享对象传入到线程类中,而是使用单例模式,为所有类创建一个可以共同看见的类Lock
,并在输出的时候给Lock
类进行上锁。
第七次作业
在第七次作用中,我发现RequestQueue
这个类的设计存在一些缺陷,它在输入线程与调度器;调度器与电梯线程这两个地方都被使用,但是在这两处中表现的行为却有所不同,在电梯线程处,它还需要承担筛选请求的责任。
因此在第七次作业中,我将输入线程与调度器之间的,保存所有请求的队列设计为一个新的类AllRequestQueue
,并使用单例模式。 而原来的RequestQueue
删除了一些函数,来使其行为恰好对应电梯的需求。这样将两者的耦合解开,使得架构更加简明清晰。
对于第七次作业的两个需要保证线程安全的类AllRequestQueue
和RequestQueue
,我仍然采用的是wait-notify
的设计思路,在方法的定义上将其设置为synchornized
的,在方法内部加入wait
, notifyAll
语句来使得线程的行为安全。
二、调度器设计
在我的设计模式中,调度器(Scheduler类)的作用是:从投喂器获取原始请求,根据一定的规则,将请求分发给各个处理线程的请求队列类RequestQueue
中。
第五次作业
调度器是一个运行的线程,它的运行按照以下模式:
- 如果收到终止信号,则向所有请求队列(即电梯)下发终止信号。
- 等待下一个来自投喂器的请求(使用阻塞等待)
- 将最新的请求放到恰当的电梯线程中,回到1.
调度器会与两类线程进行交互:
首先是输入线程,调度器尝试获取下一个请求,如果请求队列非空则立刻返回;否则调度器线程则会使用wait
方法等待输入;
在调度器获得请求之后,它会将请求发放给电梯的线程对应的RequestQueue
中。实际上电梯线程也有等待请求输入的状态,因此此时可能存在交互:调度器将请求放进电梯的请求队列中,并调用notifyAll
,唤醒电梯的线程,并使得电梯查看到这个请求,接着开始处理该请求。
第六次作业
第六次作业中调度器仍然是遵循上述的,等待-下发-等待……这样的规则运行。
在这一次作业中涉及到了一些更复杂的选择问题,例如可能收到两种(横向和纵向的)请求,每种请求又需要不同的处理和分配方法。
在我的设计中,我在调度器类中添加了一些方法,使得调度器自身保存“当前接受了多少个请求”的状态,并根据此状态将请求分发到特定的电梯线程中。总之,调度器自身获得了一部分“调度”的功能,它可以将请求发给电梯,而具体如何处理请求则由电梯自身进行处理。
第七次作业
第七次作业中对调度器做了比较大的修改,主要在以下两点
- 关于如何控制线程结束
- 如何进行请求调度
在前两次作业中,调度器只需要将请求放进每个电梯的请求队列中就完成任务了,当接受到来自输入线程的结束输入信号的时候就可以下发结束信号,并结束自身。但这一次作业中,因为存在电梯中转的缘故,调度器不能很快的结束,需要等待所有请求被执行完毕之后才能结束。
在本次作业中,我采用了“流水线”和“计数器”的设计方法,在请求完成一部分后,它会被处理并再次放入调度器中,之后调度器继续正常工作,而当请求完全地被解决后,会在计数器(MyCOunter
类,使用单例模式实现)中标记减1。当计数器的值为0后,便可知所有请求都已经被解决,可以结束调度器,并下发结束信号。
另外在本次作业中,由于可能出现不同楼层,不同建筑之间的中转,调度器的“分配”逻辑变得更加复杂,在我的架构中,这样的逻辑被收纳于一个函数中进行规划,而在外部调度器的结构不变,仍然是“先等待-下发-重新等待”的规则。
三、结构的UML图
第五次作业
第六次作业
第七次作业
时序图
四、程序Bug
我的Bug
第五次作业中,由于读题不仔细,没有在移动的时候在每一层输出Arrive信息,而是只在起始层和到达层输出;另外在程序中出现了逻辑判断的问题,导致运载的时候不能正常上下乘客。
第七次作业中:我设计了一些冗余的变量,但调整这些变量的时候没有及时、完整地赋值,最后这些变量在条件判断时出现了问题,导致有些乘客会在横向电梯不能到达某些建筑物的时候也能上电梯;在电梯运行策策略中,我尝试去发现一个比较好的横向捎带运行策略,但是最后发现这个策略的效果非常差,一个简单而有效的策略就是使横向电梯朝一个方向运行;最后线程结束的时候也出现了问题,我尝试去调用线程自身的方法来作为线程是否应该结束的判据,但是在评测机负载比较大的时候会使得线程随机地无法结束,最后重构并采用计数器的方法才通过。
互测
秉持着和平共处的原则,我没有Hack其他同学的程序。
五、心得体会
我觉得自己在电梯这一章的表现比较差,主要有几个原因:
- 没有足够重视:虽然听说过电梯这几次作业比较难,但由于各个方面的原因,我没有投入足够的时间和精力,表现在以下几点:
- 没有预习Java多线程的编写模式
- 在编写程序的时候,往往是自己构思出一个想法,一意孤行,却没有和身边的同学们交换意见,讨论出比较好的解法。这个问题在运行策略时的尤为严重,我自己的想法在有一个强测点中一直超时,但经过室友的指点很快就通过了。
- 过于依赖中测:我以为中测已经能够提供足够的正确性保证,但实际上却并不是……第五次和第七次作业中,我都很快通过了中测并认为没有问题,但在强测中发现了自己的很多Bug。
- 没有完整的测试自己的程序:只是阅读了自己的源码,但是没有写程序去测试,这也和自己的时间安排比较紧有关……
线程安全
- 并发和线程安全是应用开发最重要的几个问题之一,其核心问题是多个线程可以共享地看到的对象,在实现类的时候需要思考:这个类会被哪些线程所看到,由此来对不同的类加锁。
wait
和notifyAll
是多线程中常用的加锁手段,但是wait
之后要明确在什么情况下会notify
,否则很容易出现死锁、线程不结束的问题。
层次化设计
- 电梯单元中,结构显得尤为重要,理清线程和线程之间的关系可以使得编写程序顺利很多。
- 我觉得我在电梯单元中使用的架构大体上是没有问题的,但有一个可以优化的地方:不应该在Scheduler中把请求分发给电梯,可以让同一类(同一建筑/同一层)的电梯看到所有的乘客并自由竞争。
希望接下来的两章能够更好的安排好时间,做得更好。