BUAA-OO:第二单元总结
目录
前言
在第二单元中,我们开发了一个基于Java多线程的电梯系统。首先,我们学习了Java多线程的基本用法和线程安全问题的解决方法,实现了电梯的基础功能。接着,为了支持更多的功能,我们引入了更复杂的线程同步和互斥机制。同时,我们也不断地优化电梯的调度策略,以提升电梯系统的性能。虽然这听起来有些复杂,但其实主要的挑战在于处理线程之间的同步和互斥问题,并设计出一个更高效的电梯调度策略。接下来,我将简单总结这一单元的三次作业,主要从线程安全的解决方案、线程间的交互关系,以及电梯的调度策略这几个方面进行分析。
一,第一次作业
在这个项目中,我们的目标是模拟一个类似于北京航空航天大学新主楼的电梯系统。这个系统中,多部电梯可以在1到11层之间运行。系统的工作方式是通过标准输入读取乘客的请求信息,这些信息包括乘客的起始楼层和目的楼层。一旦接收到请求,调度器会根据当前电梯的位置和运行方向来决定将请求分配给哪部电梯。被选中的电梯将执行一系列操作,如上下行、开关门以及让乘客进入或离开,以确保乘客从起点被安全送达到终点。为了实现这一过程,我们需要定时地从我们提供的输入接口接收请求。具体的接口细节和如何使用这些接口的信息可以在项目的第三部分和相关文档中找到。在电梯的运行策略方面,我们可以自由地设计电梯的行为,比如选择何时上下行以及在哪一层开关门。关键是要确保在不超过项目要求的最大运行时间内,所有乘客都能被送达到他们的目的地。
UML 类图:
CLASS Metrcis:
MethodsMetrics :
同步块的设置和锁:
在你实现的电梯调度系统中,使用了多个同步块和锁来确保线程安全和避免竞争条件。以下是一些具体的同步块设置和锁的选择:
-
Elevator类中的同步块:
-
handlePassengers
方法:private synchronized void handlePassengers() { // ... }
使用
synchronized
关键字修饰方法,表示整个方法是一个同步块。这样可以防止多个线程同时处理同一个电梯的乘客。
-
-
RequestDispatcher
类中的同步块:-
run方法:
@Override public void run() { while (running || !endOfRequests) { // ... synchronized (requestQueue) { // ... } // ... synchronized (this) { // ... } } }
-
在run方法中,使用了
synchronized (requestQueue)
来锁定每个电梯的请求队列,确保一次只有一个线程能够处理该队列。同时,使用synchronized (this)
来锁定整个RequestDispatcher
对象,以便在等待新请求时进行同步。 -
addRequest
方法:public synchronized void addRequest(PersonRequest request, int elevatorId) { // ... }
使用
synchronized
关键字修饰方法,确保一次只有一个线程能够向特定电梯的请求队列中添加新的请求。
-
这些同步块的设置和锁的选择有效地解决了多线程并发访问共享资源时可能出现的竞争条件和数据不一致问题。通过适当地锁定关键的代码块和数据结构,确保了电梯调度系统的线程安全性和正确性。
同时,在选择锁的粒度时,也进行了权衡。例如,在Elevator类的handlePassengers
方法中,直接锁定了整个方法,因为该方法的操作相对简单,锁定整个方法不会显著影响性能。
调度器及调度策略:
在这个电梯调度系统项目中,调度器的主要组成部分是RequestDispatcher类,它协同工作来实现高效的电梯调度策略。
-
RequestDispatcher(请求调度器):
-
RequestDispatcher是一个独立的线程,负责将新的乘客请求分配给合适的电梯。
-
它维护了每个电梯的请求队列,并不断地从这些队列中取出请求进行处理。
-
RequestDispatcher通过调用RequestAssigner的方法来找到最适合处理给定请求的电梯。
-
一旦找到合适的电梯,RequestDispatcher将请求添加到该电梯的乘客队列中,并通知电梯线程有新的请求需要处理。
-
RequestDispatcher还负责处理未被分配的请求,确保所有的请求都能得到及时处理
-
仿伪的代码实现:
/**
* Constructs a RequestDispatcher instance responsible for distributing person requests to the appropriate elevators.
* This constructor initializes the list of elevators, the elevator system, and the request queues for each elevator.
* It also sets the dispatcher's running state to true, indicating that it is ready to start processing requests.
*
* @param elevators A list of Elevator objects that the dispatcher will manage.
* @param elevatorSystem The ElevatorSystem instance that this dispatcher is part of.
*/
public RequestDispatcher(List<Elevator> elevators, ElevatorSystem elevatorSystem) {
// Initialize the dispatcher with the provided elevators and elevator system, and set up the request queues and running state
// Initialize request queues for each elevator
}
/**
* The main execution method for the RequestDispatcher thread.
* Continuously checks for new person requests and dispatches them to the appropriate elevators.
* To prevent excessive CPU usage, the dispatcher sleeps for a short interval after each dispatch cycle.
*/
@Override
public void run() {
while (running) {
// Dispatch requests to elevators
// Sleep for a short interval to avoid excessive CPU usage
}
}
/**
* Adds a new person request to the appropriate elevator's request queue.
* This method determines the correct elevator based on the elevator ID specified in the request,
* then synchronizes on the elevator's request queue to add the new request and notifies any waiting threads.
*
* @param request The {@link PersonRequest} to be added to an elevator's queue.
*/
public void addRequest(PersonRequest request) {
}
/**
* Dispatches requests to the appropriate elevators.
* <p>
* This method iterates through each elevator, checking if there are any pending
* requests in the corresponding request queue. If a request is found, it is removed
* from the queue and added to the elevator's passenger queue. This operation is
* synchronized on both the request queue and the elevator's passenger queue to ensure
* thread safety.
*/
private void dispatchRequests() {
}
// Renamed from stop() to shutdown() to avoid conflict with Thread's stop() method
public void shutdown() {
}
二,第二次作业:
在本项目中,我们将模拟一个类似于北京航空航天大学新主楼的电梯系统。这个系统内部配备了多部电梯,能够在1至11层之间运行。系统的核心功能是通过标准输入接收乘客的请求信息,包括乘客的起始楼层和目的楼层。
系统中的请求调度器会根据当前电梯的位置和运行方向来决定如何分配这些请求。一旦请求被分配给特定的电梯,该电梯将执行一系列操作,包括上下移动、开关门以及让乘客进出,从而将乘客从起始楼层安全送达目的楼层。所有这些请求都是通过我们提供的输入接口定时接收的,具体的接口使用细节可以在项目文档的第三部分找到。
在调度策略方面,没有固定的要求,可以自由设计电梯的运行策略,包括决定电梯的上下移动,门的开关,以及如何分配乘客到各个电梯。关键是要确保在不超过系统允许的最大运行时间内,所有乘客都能被送达到他们的目的地。
此外,每个乘客请求的处理结果需要通过输出接口进行反馈。具体来说,需要控制电梯的上下行动、门的开关以及乘客的进出,以确保乘客能从起始楼层到达目的楼层。由于系统中有多部电梯,合理的调度这些电梯将是提高系统性能的关键。
电梯在运行一段时间后,其性能参数(如满载人数、移动时间)可能会发生变化。当接收到重置指令后,相关电梯需要尽快停靠并完成重置动作,然后再次投入运行。出于安全考虑,进行重置时电梯内不能有乘客,并且重置过程需要大约1.2秒的时间。本做加了 RESET 和 RECEIVE 功能。
本次作业的UML类图如下:
Classes Metrics:
Methods Metrics:
1- 同步块及锁
在你实现的电梯调度系统中,使用了多个同步块和锁来确保线程安全和避免竞争条件。以下是一些具体的同步块设置和锁的选择:
-
Elevator类中的同步块:
-
reset方法:
public void reset(int newCapacity, double newMoveTimePerFloor) throws InterruptedException { synchronized (this) { // ... } }
这里使用了
synchronized (this)
来锁定整个Elevator对象,确保在重置操作期间,其他线程无法访问或修改电梯的状态。 -
handlePassengers
方法:private synchronized void handlePassengers() { // ... }
使用
synchronized
关键字修饰方法,表示整个方法是一个同步块。这样可以防止多个线程同时处理同一个电梯的乘客。
-
-
RequestAssigner
类中的同步块:-
addRequest
方法:public synchronized void addRequest(PersonRequest request) { // ... }
使用
synchronized
关键字修饰方法,确保一次只有一个线程能够添加新的请求到RequestAssigner中。 -
findAvailableElevator
方法:public synchronized int findAvailableElevator(PersonRequest request) { // ... }
使用
synchronized
关键字修饰方法,确保在查找可用电梯时,不会有其他线程同时修改电梯的状态。 -
assignRequestToElevator
方法:public synchronized void assignRequestToElevator(PersonRequest request, int elevatorId) { // ... synchronized (elevator.getPassengers()) { // ... } }
除了使用
synchronized
关键字修饰整个方法外,还在方法内部使用了synchronized (elevator.getPassengers())
来锁定特定电梯的乘客队列,确保在分配请求时,不会有其他线程同时修改该电梯的乘客队列。
-
-
RequestDispatcher
类中的同步块:-
run方法:
@Override public void run() { while (running || !endOfRequests) { // ... synchronized (requestQueue) { // ... } // ... synchronized (this) { // ... } } }
在run方法中,使用了
synchronized (requestQueue)
来锁定每个电梯的请求队列,确保一次只有一个线程能够处理该队列。同时,使用synchronized (this)
来锁定整个RequestDispatcher对象,以便在等待新请求时进行同步。 -
addRequest方法:
public synchronized void addRequest(PersonRequest request, int elevatorId) { // ... }
使用
synchronized
关键字修饰方法,确保一次只有一个线程能够向特定电梯的请求队列中添加新的请求。
-
这些同步块的设置和锁的选择有效地解决了多线程并发访问共享资源时可能出现的竞争条件和数据不一致问题。通过适当地锁定关键的代码块和数据结构,确保了电梯调度系统的线程安全性和正确性。
同时,在选择锁的粒度时,也进行了权衡。例如,在Elevator类的handlePassengers方法中,直接锁定了整个方法,因为该方法的操作相对简单,锁定整个方法不会显著影响性能。而在RequestAssigner类的assignRequestToElevator方法中,除了锁定整个方法外,还额外锁定了特定电梯的乘客队列,以实现更细粒度的同步控制。
2- 调度器及调度策略
在这个电梯调度系统项目中,调度器的主要组成部分是RequestDispatcher类和RequestAssigner类,它们协同工作来实现高效的电梯调度策略。
-
RequestDispatcher(请求调度器):
-
RequestDispatcher是一个独立的线程,负责将新的乘客请求分配给合适的电梯。
-
它维护了每个电梯的请求队列,并不断地从这些队列中取出请求进行处理。
-
RequestDispatcher通过调用RequestAssigner的方法来找到最适合处理给定请求的电梯。
-
一旦找到合适的电梯,RequestDispatcher将请求添加到该电梯的乘客队列中,并通知电梯线程有新的请求需要处理。
-
RequestDispatcher还负责处理未被分配的请求,确保所有的请求都能得到及时处理。
-
-
RequestAssigner(请求分配器):
-
RequestAssigner负责根据特定的调度策略将乘客请求分配给最合适的电梯。
-
它维护了请求与电梯之间的映射关系,以及未处理请求的优先级队列。
-
RequestAssigner提供了多个方法,如findAvailableElevator和assignRequestToElevator,用于找到可用的电梯并将请求分配给它们。
-
当一个电梯重置时,RequestAssigner会将分配给该电梯的请求重新分配给其他可用的电梯,以避免请求被长时间搁置。
-
调度策略:
-
基于距离的调度策略:
-
在findAvailableElevator方法中,RequestAssigner会优先选择与请求方向一致且距离请求楼层最近的电梯。
-
通过计算电梯当前楼层与请求楼层之间的距离,并考虑电梯的运行方向,RequestAssigner可以找到最合适的电梯来处理请求。
-
这种策略可以最小化电梯的移动距离,减少乘客的等待时间。
-
-
基于负载均衡的调度策略:
-
在assignRequestToElevator方法中,RequestAssigner会考虑电梯的当前负载情况。
-
如果多个电梯都满足距离要求,RequestAssigner会选择当前乘客数量最少的电梯来处理请求。
-
通过均衡电梯的负载,可以避免某些电梯过于繁忙而其他电梯闲置的情况,提高了系统的整体效率。
-
-
优先级调度策略:
-
Elevator类中的moveToNextTargetFloor方法使用了两个优先级队列来处理分配给电梯的请求。
-
一个优先级队列用于接送请求,另一个用于送达请求。
-
根据电梯当前的运行方向,Elevator会优先处理与运行方向一致的请求,以提高运输效率。
-
对于静止状态的电梯,它会优先处理最近的接送请求。
-
-
电梯重置策略:
-
当电梯收到重置请求时,它会立即停止当前的运行,并进入重置状态。
-
在重置过程中,电梯会将所有乘客送达目的地,然后返回初始楼层。
-
RequestAssigner会将分配给该电梯的请求重新分配给其他可用的电梯,以确保请求能够继续得到处理。
-
重置完成后,电梯会恢复正常运行,并开始处理新的请
-
三,第三次作业
在本次作业中,加了第二类重置请求的引入,使得电梯可以转变为双轿厢电梯。这种重置不仅涉及到电梯ID的指定,还包括了换乘楼层的设置以及两个轿厢的运行参数,如每移动一层的时间和最大载人数。完成重置后,轿厢A将默认停在换乘楼层下面一层,而轿厢B则停在换乘楼层上面一层。
双轿厢电梯的运行机制
双轿厢电梯设计中,两个独立的轿厢在同一电梯井道内运行,与传统的单轿厢电梯形成对比。为了避免两个轿厢在井道中相互碰撞,我们将楼层划分为上区、下区和换乘楼层。上区包括换乘楼层以上的所有楼层,而下区则是换乘楼层以下的楼层,两者都不包括换乘楼层本身。在运行策略上,轿厢A仅在下区和换乘楼层之间运行,而轿厢B则服务于上区和换乘楼层。此外,为了安全考虑,两个轿厢不得同时停靠在换乘楼层。
双轿厢电梯的优势与调度策略
双轿厢电梯的设计不仅可以提高运输效率,还有助于节能。由于两个轿厢可以独立运行,它们可以根据实际的乘客流量和目的地需求进行优化调度,从而减少等待时间和空驶率。在设计调度方案时,我们需要考虑如何最大化这些优势。例如,可以设置轿厢A优先处理下区的高峰流量,而轿厢B则处理上区的需求。此外,通过智能调度系统,可以实时调整轿厢的运行计划,以应对不同时间段的乘客流变化,确保电梯系统的高效和节能运行。
UML 类图:
同步块及锁的应用
在本次作业中,我们的重点放在了调度策略的实现上。尽管如此,同步块和锁的使用仍然至关重要,以确保线程之间的正确协作和数据的一致性。在CountController中,我们特别注意了线程互斥的严格执行,以避免可能的并发问题。此外,在调整调度策略时,我们也特别保护了waitmap和waitlist这两个关键数据结构,确保在多线程环境下它们的安全访问。
调度器及调度策略的优化
在这次作业中,我对调度器的策略进行了重大的修改。对于无需换乘的情况,我仍然采用了之前的策略,即优先考虑最近且方向相同的电梯。但是,我在这个基础上考虑了更多的细节:
-
当电梯内没有乘客时,如果有乘客请求的方向与电梯的运行方向一致,电梯只有在乘客的上下楼层都位于电梯当前位置和即将前往接送乘客的位置之间时,才会顺路捎带该乘客。
-
同样地,如果电梯内没有乘客而乘客请求的方向与电梯运行方向相反,我们会考虑乘客的目的方向是否与电梯即将接送的乘客的方向一致。如果一致,并且乘客上电梯的楼层在电梯即将接送的乘客的楼层之后,那么这部电梯可以作为备选电梯。在这种情况下,我们会计算电梯到达接送乘客的距离以及接送乘客到达请求乘客上电梯位置的总距离,以此作为选择备选电梯的依据。
通过这些细致的策略调整,我们的电梯调度系统将更加智能和高效,能够更好地满足乘客的需求,同时提高电梯的运行效率。
四,总结
完成这个电梯调度系统项目后,我有以下几点总结和收获:
-
深入理解多线程编程:通过实现电梯、请求分配器和请求调度器等组件,我深入理解了线程间的通信、同步和协作机制。我学会了如何使用锁、同步块和等待-通知机制来解决多线程并发访问共享资源时的竞争条件和数据一致性问题。
-
掌握复杂调度算法的设计与实现:我考虑了电梯的状态、请求的优先级和乘客的需求,设计了基于距离和负载均衡的调度策略。我学会了如何使用优先队列和其他数据结构来优化调度过程,提高系统的效率和响应速度。
-
加强设计模式的应用:在项目中,我合理地运用了单例模式、观察者模式和生产者-消费者模式等,使系统结构更清晰、模块化和可扩展。通过将复杂功能划分为不同的类和组件,提高了代码的可读性和可维护性。
-
积累项目开发和管理经验:通过合理划分任务、估算工作量和安排进度,我提高了项目推进的效率。我学会了如何与团队成员沟通协作,解决开发过程中的问题,并及时调整和优化方案。
对于后续的项目,我有以下几点期望:
-
探索更高级的并发编程技术:我希望应用并发集合、线程池和异步编程框架等技术,以应对更大规模和更高并发量的场景。
-
引入智能化调度算法:我计划尝试基于机器学习或人工智能的方法,实现更精准和动态的请求分配和资源调度。
-
扩展为分布式架构:我考虑将系统扩展为支持多个电梯调度子系统协同工作的分布式架构,以提高系统的可扩展性和容错性。
总之,通过这个项目的实践,我对并发编程、调度算法和系统设计有了更全面和深入的认识。同时也意识到,软件开发是一个持续学习和迭代优化的过程,需要不断探索新的技术和方法,并根据实际场景和需求进行创新和改进。我期待在后续的项目中继续挑战自己,不断提升技术能力和问题解决能力,为用户提供更优质的软件产品和服务。