总结分析三次作业中同步块的设置和锁的选择,并分析锁与同步块中处理语句之间的关系
同步块是指可能有多个线程同时访问的数据结构。在第二单元的电梯实现中,同步块主要指总请求请求队列中由synchronized修饰的代码块或方法。总请求队列是由主线程创建的、由输入线程或电梯线程向其中添加新请求或未完成的请求的、为调度线程提供调度任务的CopyOnWriteList< Request >。
关于同步块的设置,大概分为两部分,其一是向总请求队列中添加请求,其二是从总请求队列中取出请求。
设计添加请求方法时要确保同一时间最多只有一个线程调用添加请求的方法,因此用sychronized修饰方法名,并在方法最后notifyAll()进行解锁。
public synchronized void add(Request pr) {
this.requests.add(pr);
this.notifyAll();
}
设计取出请求的方法时除了要确保同一时间最多只有一个线程调用该方法, 还要求如果总请求队列为空,当前线程阻塞在此处等待,防止轮询占用处理器资源。因此设计waitForAWhile()方法使其等待,等添加请求的方法执行到notify语句是当前线程继续执行。
在以上过程中,锁保护了临界资源,确保其同一时间最多被一个线程占用,这样保证了所有线程对总请求队列的add或remove等修改有效,数据实时更新。
public synchronized Request getFirstAndRemove() {
waitForAWhile();
if (this.requests.isEmpty()) {
return null;
}
Request pr = this.requests.get(0);
this.requests.remove(0);
notifyAll();
return pr;
}
总结分析三次作业中的调度器设计,并分析调度器如何与程序中的线程进行交互
调度器线程(Schedule)的作用类似于操作系统,是为总请求队列中的任务分配处理器资源(也就是电梯线程)的。因此,调度器可以控制的电梯类的对象必须作为调度器类的属性,由掉度器向电梯对象的请求队列中添加请求,笔者采用轮流分配的调度策略,为电梯对象分配任务。
需要特别注意的是调度器的停止条件。
在第五次作业中,笔者为请求队列设置了end属性,当输入结束时,将end设置为true。在调度器线程中,如果总请求队列为空并且end为true,调度器线程结束。但是这样的设计遇到reset指令时就显露出局限性。
在第六次作业中,由于电梯接到reset指令后要将电梯中的人全部放出,这时就需要将未完成的请求返回总请求队列,但是如果此时由于输入已经结束并且总请求队列为空调度器线程已经结束,那么返回总请求队列的请求无法得到调度。这样一来,就不能让调度器线程太早结束。因此,笔者加入了RequestCount类记录尚未完成的请求的数量,只有尚未完成的请求的数量为0才能结束调度器。
然而,上面的做法仍然不够充分,存在问题。先前当总请求队列为空时,调度器会一直等待,直到队列中出现新的指令或者输入线程为队列setend。但此时的输入线程已经结束了,调度器还未结束,因此当队列为空时,调度器会一直等待,造成程序无法结束。这样,就需要请求队列类增加一个stop方法,为调度器notify。下面是笔者实现的RequestCount类。
public class RequestCount {
private int cnt;
public RequestCount() {
this.cnt = 0;
}
public synchronized void push() {
this.cnt++;
}
public synchronized void pop() {
this.cnt--;
}
public synchronized boolean completed() {
return this.cnt == 0;
}
public synchronized int getCnt() {
return this.cnt;
}
}
三次作业架构设计的逐步变化和未来扩展能力画UML类图
第七次作业
第七次主要是双轿厢电梯的实现调度,笔者采用两个轿厢两个线程一个调度器的方法。
第六次作业
第六次作业主要增加了RequestCount类用来记录未完成的请求数量,控制调度器的结束时机。
第五次作业
第五次作业的核心在于elevator的实现,这里电梯类的作用类似于CPU,都是一个有限状态机,在有限个状态之间来回转换,执行来自调度器的指令,输出相关信息。
分析自己在第三次作业中是如何实现双轿厢的两个轿厢不碰撞的
两个轿厢要想不碰撞,必须保证同一时间,至多有一个轿厢即将到达换乘楼层。笔者增加了一个TransFloorOccupied类,用来记录换乘楼层是否有轿厢或者是否有轿厢即将到达换乘楼层。任何一个轿厢要想到达换乘楼层,必须申请,申请时会检查当前轿厢线程是否具备到达换乘楼层的条件,如果不具备条件,就要阻塞当前线程在此处,等待条件。
public class TransFloorOccupied {
private boolean transFloorOccupied;
public TransFloorOccupied() {
this.transFloorOccupied = false;
}
public synchronized void setTransFloorOccupied(boolean in) {
this.transFloorOccupied = in;
notifyAll();
}
public synchronized boolean getTransFloorOccupied() {
notifyAll();
return this.transFloorOccupied;
}
public synchronized void apply() {
if (this.transFloorOccupied) {
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
this.setTransFloorOccupied(true);
}
}
分析自己程序出现过的bug
一段时间内所有的新请求全部被分配至同一电梯线程,致使程序运行超时。
这是因为调度器只能给不在重置中的电梯线程分配任务,而不能给重置中的电梯分配请求。如果某一段时间5部电梯同时处于重置中,只有一部电梯可以接受指令,这段时间又涌入大量指令,就会被调度器分配给同一部电梯。
因此,只有在可以接受任务的电梯达到一定数量时,才可以分配任务。笔者新增AvailableEle类,用来记录和更新可以接受任务的电梯的数量。
public class AvailableEle {
private int cnt;
public AvailableEle() {
this.cnt = 6;
}
public synchronized void resetBegin() {
this.cnt--;
notifyAll();
}
public synchronized void resetEnd() {
this.cnt++;
notifyAll();
}
public synchronized int getNum() {
return this.cnt;
}
}
并在调度器中获取AvailableEle类型的值,判断是否可以分配指令。
if (request instanceof PersonRequest) {
while (this.ae.getNum() < 4) {
synchronized (this.ae) {
try {
this.ae.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
do {
temp = pollElevator(temp);
} while (elevators.get(temp - 1).getReset());
dispatch((PersonRequest)request,temp);
}
其中pollElevator是用来轮流获取电梯ID的方法,dispatch是分配的过程。
private synchronized int pollElevator(int lastId) {
if (lastId == 6) {
return 1;
}
return lastId + 1;
}
public synchronized void dispatch(PersonRequest request,int temp) {
if (this.elevators.get(temp - 1).getDce()) {
TimableOutput.println("RECEIVE-" + request.getPersonId() + "-" + temp);
Elevator elevator = this.elevators.get(temp - 1);
elevator.getWaitQueue().add(request);
} else {
Elevator elevator = this.elevators.get(temp - 1);
elevator.getDoubleCarSchedule().getDoubleCarWaitQueue().add(request);
}
}
心得体会
关于线程安全
锁归属于共享对象,所以锁的行为必须由共享对象的类进行定义,而不应该由访问共享对象的线程自己决定,否则会出现意想不到的错误。具体来说,加锁的synchronized和解锁的notify、notifyAll都全部出现在共享对象的类中,不应该出现在线程类中。
关于层细化设计
对于复杂程序的设计,必须做好层次的划分。比如,一台计算机就要分为体系结构层次、系统软件层次、应用软件层次,每一个层次都是独立的个体,由其自身的运作模式,不同层次之间互相协作,共同发挥作用。具体来说,电梯类就是在模拟电梯的体系结构,调度类就是在模拟操作系统,请求类就是在模拟应用软件。整个程序就是模拟一个电梯系统的运行。这样的设计具有清晰的逻辑和较强的可扩展性,值得被我们学习和借鉴。