2025_BUAA_OO_Unit2

Cdostan MVP++

OO第二单元的主题是多线程编程,旨在通过生活中的常见例子来让我们理解并掌握多线程编程思想,同时能够编写代码正确实现多线程的同步和互斥,解决线程安全和线程交互问题。
本单元围绕电梯调度这个生活中极为常见的例子开展三次作业,在满足正确性的情况下,我们还需对电梯整体调度进行设计尽可能优化性能。
我完成这三次作业的过程不能用轻松来形容,线程安全和线程交互中出现的问题总是让我头疼,甚至一度认为 多线程编程极不安全 ,但其实仔细思考后会发现线程安全和线程交互的问题都是可以通过设计来解决的,只不过针对于不同的架构和方案,相应的解决方案的复杂程度可能不同。整体来说,三次作业的完成是痛苦的,但收获也是丰富的。

第一次作业

第一次作业的要求较为简单,每个乘客会指定一部电梯乘坐,也就是说我们无需再考虑每一个乘客到底该分配给哪一个电梯,只需单独针对一个电梯来考虑相应的调度策略。

1.1 调度设计

本次作业每个乘客均指定了电梯,因此在调度上可以有两种思想,一种是采用中央调度器,另一种是每个电梯有自己的调度器,通过自己的请求队列来进行调度。由于我的设计是有分配线程存在的,也即当其拿到了一个请求后,在能竞争资源的情况下,会马上将请求分配给对应的电梯,因此我采用的是第二种思想。因此我的调度器其实是电梯线程内部的一个设计,直接在线程内工作,使用电梯请求队列这一资源,这一资源是原子性的,因此能够保证线程安全。

调度策略
有关单部电梯调度,已经有很多算法,比如LOOK算法,ALS算法,SATF算法等,但由于三次作业都加入了乘客的优先级这一因素,因此直接使用这些算法并不能很好的考虑到这一因素,可能会导致最终的性能不理想。因此我设计了自己的调度算法。

我的调度算法是基于SATF算法设计的,同时将LOOK算法和ALS策略考虑的因素也纳入进来。SATF算法是通过对每一个要素分配一个权重,最后根据标准化数据和权重计算每个请求的得分,将得分最高的请求纳入自己的接送队列。针对这三次作业,显然要为乘客的优先级这一因素分配权重,但这还不够,参考LOOK算法和ALS策略后,我将请求离电梯的距离以及相对方向也考虑进来,并为这三个要素分配权重。怎么分配权重又是一个问题,如果简单的为这三个要素指定权重,可能不够严谨,怎样做最严谨呢?根据已有数据来进行分析,采用熵权法计算出每一个要素的权重,这样应该是计算权重最严谨的方法了,但可惜的是似乎并没有这样一份数据包含了乘客的优先级,况且请求离电梯的距离以及相对方向这两个因素是动态的,而不是静态的,因此使用熵权法来计算权重是不现实的,因此我采用层次分析法来计算权重,这样既能保证一定的严谨性,又能保证一定的合理性。

判断矩阵如下:

在根据经验得到判断矩阵后,我计算出了三个因素对应的权重,分别是相对方向:0.5,请求离电梯距离:0.25,优先级:0.25(似乎和直接分配权重没啥区别(流汗))。计算好权重后,就可以计算每个请求的得分了,具体来说,假设请求离电梯的距离为 ,优先级为 ,相对方向为 ,那么其得分为:

电梯运行时,每次从自己的请求队列中选择一个得分最高的请求,若电梯未满,如果电梯为空,则将其放入电梯的接送队列,若电梯非空,则需请求得分大于0.5时才会将其放入电梯接送队列。这样设计能够满足优先级的考虑,同时时间上也会优于LOOK算法,也避免了电梯反复横跳的情况,防止其非正常耗电。

1.2 线程安全

本次作业我参考了课上上机的实现,也就是一个输入线程,一个分配线程和六个电梯线程。

  • 输入线程:负责获取输入并将相关请求存入总的请求队列里,相当于生产者的作用
  • 分配线程从总请求队列里取请求并通过请求指定的电梯号分配给相应的电梯请求队列
  • 电梯线程根据调度策略从电梯请求对列里获取请求并进行相关动作

从设计思想上来看,我的设计中相关的共享资源是总请求队列(输入线程和分配线程),电梯的请求队列(分配线程和电梯线程),也就是说,针对线程安全问题只需要在这两个队列进行原子性设计即可。
我本次作业采用synchronized关键字来实现锁,并主要采用对方法加锁的方式来实现原子性设计。

1
2
3
4
public class Allrequest {
private final ArrayList<PersonRequest> allrequests = new ArrayList<>();
private boolean isEnd = false;
}

针对Allrequest类,对其涉及到设置结束标志和读取结束标志、对请求队列做读写操作的方法全部加上synchronized修饰,保证每次最多一个线程能对该类做操作。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public synchronized PersonRequest getNextRequest() {
if (allrequests.isEmpty() && !isEnd) {
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
if (allrequests.isEmpty()) {
return null;
}
notifyAll();
return allrequests.remove(0);
}
1
2
3
4
public class ElevatorRequest {
private final ArrayList<PersonRequest> personRequests = new ArrayList<>();
private boolean isEnd = false;
}

针对ElevatorRequest类,与Allrequest类完全相同,保证每次最多一个线程对该类的队列及结束标志做操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public synchronized PersonRequest getPersonRequest(int cfl, int load, int status, int edr, int realdr) {
if (!isEmpty()) {
double max = 0;
int index = -1;
double score;
for (PersonRequest pr : personRequests) {
score = Calculate.score(pr, load, cfl, edr, realdr);
if (score > max) {
max = score;
index = personRequests.indexOf(pr);
}
}
if (status != 1) {
if (max <= 0.5) {
notifyAll();
return null;
}
}
notifyAll();
return personRequests.remove(index);
} else {
return null;
}
}

1.3 UML类图

功能说明

  • MainClass:程序入口,同时作为主线程
  • InputThread:输入线程,负责将获取的请求放入总请求队列
  • DispatchThread:分配线程,负责从总请求队列拿取请求并分配到相应的电梯请求队列
  • ElevatorThread:电梯线程,主要负责电梯运行
  • Allrequest:总请求队列,输入线程和分配线程访问
  • ElevatorRequest:电梯请求队列,分配线程和电梯线程访问
  • Calculate:计算类,负责转换楼层及计算请求分数

1.4 UML协作图


第一次作业线程之间的协作关系较为简单,不同线程要访问同一资源时严格保持互斥,同步机制可以通过wait-notify实现。

第二次作业

第二次作业在第一次作业的基础上,删去了乘客指定电梯,同时增加了临时调度这一请求。完成本次作业的过程十分痛苦,因为我想增加中央调度器并让中央调度器使用我的调度策略来进行调度,同时我还想把电梯从电梯线程里面分离出来,因此我决定进行重构,但是重构后出现了大量bug,历经两天的debug,最后仍然有一个及其难以复现的bug(开多个进程跑一个小时只复现出来一例,但课程评测机总能命中),且极为匪夷所思(电梯有时会抽风在接到请求后一直往上或往下走,不去接人,出现一大堆ARRIVE UNKNOWN,并导致RTLE),因此在清明假期的最后一天,我决定放弃该重构代码,直接在第一次作业上进行迭代,同时也将原来的中央调度器算法替换为极为简单的平均分配,导致在性能上有很大的损失。

2.1 调度设计

对于迭代后的代码,没什么好说的,整体思想和第一次作业几乎没有差别,别看好像多了个中央调度器采用平均分配进行调度,但其完成的事情就是第一次作业中分配线程完成的,和第一次作业乘客指定电梯几乎没有差别,而单部电梯仍然采用我设计的调度算法,唯一变化的就是电梯在开始临时调度后,会将自己接送队列里的请求转移到自己的请求队列。

对于我的重构代码,我将设计的调度策略从单部电梯移到了中央调度器上,也即中央调度器根据我的算法来分配请求给电梯,每次得到一个能够分配且分数最高的请求并分配给相应电梯,电梯抛弃了自己的请求队列,只保留接送队列,电梯便不再需要去考虑调度的事情了,电梯开始临时调度后,将接送队列里的所有请求返回给总请求队列,这些请求将被中央调度器再次分配。这其中遇到的一个问题是我的算法中有距离和方向这两个要素,但中央调度器开始计算时,六部电梯可能有的停在楼层,有的在运行,如果不仔细设计,很可能会得到错误的距离和方向,导致电梯接到理应不该接的人,我解决该问题的办法是当电梯开始移动的那一刻就立马更新楼层等信息,而不是在到了之后再更新,这样便可保证正确性。

依稀记得重构代码在中测最后一个点跑出了13秒的好成绩,甚至比某些同学影子电梯的实现更快,改成迭代后的代码后变成了22秒,直接慢了十秒,在性能上损失巨大。

2.2 线程安全

我第二次作业仍然采用synchronized关键字修饰来实现锁,一方面是时间问题担心换成其他锁会造成难以预料的问题,另一方面是我认为我的代码用synchronized足以,换成其他锁之后性能并不会得到太大的提升。本次作业中,我仍然主要采用对方法加锁的方式,整个方法为同步块,除非方法里面有wait,否则在执行完整个方法之后才释放锁,但编写过程中还遇到了一些其他问题,于是又增加了其他机制确保线程安全(见下)。

迭代后的代码和第一次作业在线程安全上的实现大差不差,因为是平均分配,并不会涉及到其他的什么共享资源或互斥变量,同时临时调度开始后,我会让电梯将其接送队列里的请求放入它自己的请求队列,因此也不会对线程安全造成什么影响。

对于我的重构代码,虽然有错误,但我认为那个错误并不是由线程安全导致的,因此也简单聊聊。
我仍然设置了三类线程,输入线程,中央调度线程,电梯线程,其中电梯与电梯线程分离。由于电梯会选择将请求放回总请求队列,因此直观上来看三类线程都需要访问总请求队列,但是这便会产生问题,因为我的电梯线程只想拥有电梯这一个内部属性,也就是说其他属性会放在电梯类里,那申请放回的时候,电梯线程首先占有电梯的锁,随后会尝试占有总请求队列的锁,但这时总请求队列的所可能被中央调度线程占有,且它此刻正在分配,因此也会尝试占有电梯的锁,这便造成了死锁。为了解决这个问题,我新增了一个队列来存储从电梯返回的请求,并在中央调度器中将这一队列的请求添加到总请求队列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Allperequ allrequest = new Allperequ();
Allperequ againrequest = new Allperequ();

public class Elevator {
private final Allperequ againrequest;

public void addRequest() {
//假设要放回请求
againrequest.add(Request);
}
}

public class DispatchThread extends Thread {
private final Allperequ allperequest;
private final Allperequ againperequest;

public void run() {
while (true) {
synchronized(againrequest){
if (!againperequest.isEmpty()) {
allperequest.addAll(againperequest);
againperequest.clear();
}
}
//调度分配
}
}
}

除了死锁的问题之外,我的设计还存在轮询的问题,这也是由于线程通信没做好导致的,具体来说,我的中央调度器在获取下一个可以分配的请求时,有可能请求队列并不为空,但由于电梯的状态导致返回的请求是null,这时便会一直重复while循环直到返回不为null。这个问题也让我十分头疼,因为比较难改,我需要考虑到底在什么时候进行下一次计算,需要考虑如何通知中央调度器进行下一次计算。最后我采用的方法是新增一个锁,如果返回为null,则获取这个锁并进入等待状态,直到电梯完成相关操作后唤醒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Plock {
private static final Object lock = new Object();

public static Object getLock() {
return lock;
}

public static void nfa() {
synchronized (lock) {
lock.notifyAll();
}
}
}


public class Allperequ {
private final ArrayList<PersonInfo> allperequests = new ArrayList<>();

public synchronized PersonInfo getNextRequest(){
//获取请求设为maxrequest
while (maxrequest == null) {
try {
synchronized (Plock.getLock()) {
Plock.getLock().wait();
}
maxrequest = maxRequest(elevators);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}

public class Elevator {
public void action() {
//相关操作
Plock.nfa();
}
}

这样便成功处理了轮询问题。

2.3 UML类图

迭代


功能说明

  • MainClass:程序入口,也是主线程
  • InputThread:输入线程,将获取的请求按类别放入总正常请求队列或总临时调度请求队列
  • DispatchprThread:分配正常请求线程,将总正常请求队列里的请求分配给不同电梯,放入对应的电梯请求队列(平均分配)
  • DispatchschThread:分配临时调度线程,将总临时调度请求队列里的请求分配给对应电梯,放入对应的电梯请求队列
  • ElevatorThread:电梯线程,负责电梯运行
  • Allperequest:总正常请求队列,输入线程和分配正常请求线程访问
  • Allschrequest:总临时调度请求队列,输入线程和分配临时调度线程访问
  • ElevatorRequest:电梯请求队列,电梯线程和两个分配线程访问
  • Calculate:计算类
  • PersonInfo:重写的PersonRequest类,方便修改起始楼层

重构


功能说明

  • MainClass:程序入口,也是主线程
  • InputThread:输入线程,将获取的请求按类别放入总正常请求队列或总临时调度请求队列
  • DispatchprThread:分配正常请求线程,将总正常请求队列里的请求分配给不同电梯,作为中央调度器,按调度策略调度
  • DispatchschThread:分配临时调度线程,将总临时调度请求队列里的请求分配给对应电梯
  • ElevatorThread:电梯线程,电梯在此运行
  • Allperequest:总正常请求队列,输入线程和分配正常请求线程访问
  • Allschrequest:总临时调度请求队列,输入线程和分配临时调度线程访问
  • Elevator:电梯类,负责电梯运行,若有临时调度,会将自己接送队列的请求返回到总正常请求队列
  • Calculate:计算类
  • PersonInfo:重写的PersonRequest类,方便修改起始楼层

2.4 UML协作图

迭代和重构的代码在线程协作上类似,只在某些地方不同,仅用一张图展示。

第三次作业

第三次作业在第二次作业的基础上增加了双轿厢设计,初看觉得很简单,动手时才发现左右为难。本次作业在第二次作业(迭代版)上进行迭代,同样省去了中央调度器的大部分工作。本次作业耗费将近一天半的时间进行调试和debug,过程仍然艰辛。

3.1 调度设计

同第二次作业,仍然采用平均分配,同时在开始临时调度或双轿厢改造时将接送队列里的请求返回到电梯请求队列。值得一提的是,因为双轿厢的设计,一部电梯很可能会收到其无法接到的请求,这时需要将这类请求全部输送给同一电梯井里的另一部电梯,因此本次作业中,我直接让所有电梯线程和分配线程同时共用RequestMap,其是一个HashMap,键为电梯id,值为电梯请求队列。

3.2 双轿厢设计

本次作业的难点无疑是双轿厢的设计,主要难点在于如何确保双轿厢改造同步开始、如何确保电梯运行安全(不碰撞),这其实是在考验我们如何实现不同线程之间的通信。
针对同步开始改造,我新增了一个Elevatorready类,里面存放着一个ready队列,为0表示还没准备好改造,为1表示准备好了

1
2
3
public class Elevatorready {
private ArrayList<Integer> readyElevators;
}

这样一个电梯准备好开始改造后,首先将自己的ready标志置1,然后访问对应的另一部电梯的ready标志,直到另一部电梯的ready标志也为1时,开始进行改造。由于只需输出一次UPDATE BEGIN和UPDATE END,我将输出这个操作交给双轿厢改造中的A电梯来执行。
针对运行安全,我也新增了一个Objoccupy类,存放各部电梯对于分界楼层的占用情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Objoccupy {
private ArrayList<Integer> occupyList;

public synchronized void occupy(int eid, int broid) {
notifyAll();
while (occupyList.get(broid - 1) == 1) {
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
occupyList.set(eid - 1, 1);
}

public synchronized void release(int eid) {
notifyAll();
occupyList.set(eid - 1, 0);
}

public synchronized void waitleave(int eid) {
while (occupyList.get(eid - 1) == 1) {
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}

这样在电梯想要移动到分隔楼层时,会先检查楼层是否被占,直到不再被占才移动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ElevatorThread extends Thread {
public void ableTomove() {
if (shaftdouble == 1) {
if ((pos == 1 && floor == divifloor + 1 && direction == -1) ||
(pos == -1 && floor == divifloor - 1 && direction == 1)) {
occupy.waitleave(broid);
}
}
}
public void move() {
if (shaftdouble == 1 && floor == divifloor) {
occupy.occupy(id, broid);
}
//move
if (shaftdouble == 1 && floor != divifloor) {
occupy.release(id);
}
//modify status
}
}

这样便可保证电梯不会相撞,但我的设计是要保证一个电梯到达了分隔楼层上一层或下一层之后,另一部电梯才能移动到分隔楼层,这样在时间上会有一定损失,但也可以避免通过sleep的方式去调整两部电梯的输出顺序。

除了上述两个问题之外,我认为双轿厢还有一个较为困难的地方,我也是在这个地方修改了多次,出现RTLE的原因全部在此,那就是如何结束。显然不能像前两次作业一样只考虑单部电梯,还得考虑同一电梯井的另一部电梯,如果两部电梯都达到了结束的状态,才能结束。但我在实现的过程中发现总会出现非双轿厢电梯无法结束,或者双轿厢电梯中一部电梯提前结束,而另一部电梯还有要接送的乘客且要使这些乘客到达目的地,但凭其自己是做不到的,因此总会发生程序无法结束的情况。
检查了很久,针对第一个问题,最后发现是我分配线程的设置出了问题,我针对每一种请求设置了一个分配线程,那么结束时,要通过三个线程对电梯请求队列设置三个结束标志,只有三个标志都设置好了电梯请求对列才认为自己结束,而这是极其不安全的。很可能当前电梯检查状态时发现还有一个标着没有设置,接着跳转到正常执行的代码块中等待其他线程对电梯请求队列做操作,而就在跳转的这一过程中最后一个标志也被设置了,最后就导致该电梯僵死在那里,无法结束。我将分配线程改为一个,同时电梯请求队列的结束标志也改为一个之后,便解决了该问题。(这个问题在第二次作业中也有,只不过第二次作业只有两个结束标志,没那么容易触发,也让我苟活过了强测和互测)
针对第二个问题,我发现是我在分配线程中为电梯请求队列设置结束标志时,没有同时获取这六个请求队列的锁导致的,如果能获取这六把锁便可解决该问题,因此这一操作的相关代码中,出现了一个金字塔结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (allrequest.isEmpty() && allrequest.isEnd()) {
ElevatorRequest el1 = requestMap.get(1);
ElevatorRequest el2 = requestMap.get(2);
ElevatorRequest el3 = requestMap.get(3);
ElevatorRequest el4 = requestMap.get(4);
ElevatorRequest el5 = requestMap.get(5);
ElevatorRequest el6 = requestMap.get(6);
synchronized (el1) {
synchronized (el2) {
synchronized (el3) {
synchronized (el4) {
synchronized (el5) {
synchronized (el6) {
for (ElevatorRequest request : requestMap.values()) {
request.setEnd();
}
}
}
}
}
}
}
break;
}

3.3 线程安全

本次作业还是采用synchronized关键字修饰,总的实现和第二次作业基本相同,增加了对ElevatorreadyObjoccupy的原子性实现。

3.4 UML类图


可见电梯线程类包含了大量属性和方法,全是因为我没有设置策略类等来进行更好的层次化设计导致的。

功能说明

  • MainClass:程序入口,同时作为主线程
  • InputThread:输入线程,负责将获取的请求放入总请求队列
  • DispatchThread:分配线程,负责从总请求队列拿取请求并分配到相应的电梯请求队列
  • ElevatorThread:电梯线程,主要负责电梯运行
  • Allrequest:总请求队列,输入线程和分配线程访问
  • ElevatorRequest:电梯请求队列,分配线程和电梯线程访问
  • Calculate:计算类,负责转换楼层及计算请求分数
  • PersonInfo:重写PersonRequest
  • Elevatorready:电梯就绪队列,用于保证双轿厢改造同时开始
  • Objoccupy:分隔楼层占用队列,用于保证电梯运行安全
  • ElevatorStatus:电梯状态队列,用于电梯结束

3.5 UML协作图

迭代总结

稳定内容

我认为本单元三次作业中较为稳定的内容如下:

  1. 电梯调度策略
  2. 电梯基本行为逻辑
  3. 线程功能
  4. 锁的选择

易变内容

我认为本单元三次作业中较为易变的内容如下:

  1. 电梯特殊行为逻辑
  2. 分配新增请求
  3. 实现新增请求
  4. 线程安全的实现
  5. 程序结束的实现

测试

这三次作业我强测和互测都未出现bug,全依赖于完成作业时不断的debug。
本单元我主要通过利用IDEA工具、搭建评测机来debug。
利用IDEA工具主要是针对死锁,可以更快地找到死锁出现的原因并加以修改。
除了死锁之外的错误我主要通过评测机来检查。我搭建了一个小型的评测机(data,checker,test),支持对一个输入,多个程序进行评测,设有自动评测、评测指定输入和只检查输出对于输入是否正确三种模式。
由于中测我很少出现WA的错误,大多是CTLE和RTLE,因此在中测通过前我很少使用自动评测(因为要编写checker),针对CTLE,我一般是直接在程序中添加输出语句来查看轮询情况(一定得及时终止程序,听到风扇声响起直接掐,否则重定向后可能会得到一个巨大无比的文件),然后进行修改。针对RTLE,我一般通过添加输出,开多个进程跑同一程序,也可以直接使用评测机的第二种模式,将同一个程序以不同命名放在评测目录下(因为只关心RTLE,不关心其他错误,所以不需要checker的功能)评测,最后查看相应输出。在中测通过后,我便可以加上checker来自动评测检查其他错误。
编写数据生成比较轻松,但想编写出较强的数据也有难度。我最开始编写的数据生成由于随机机制的原因会导致大量数据出现在最大时间戳,后来修改了随机机制后使得整体生成的数据较为均匀。在hw7中,我进一步增强了数据生成,使其能够在某几个时间戳里生成大量请求。
编写checker较为繁琐,因为错误情况较多,hw5中,我自己编写了checker,能够定位错误类别和出错的输出,hw6中,借助同学编写的checker接入我的评测机,hw7中,在上一版checker基础上进行迭代。成功编写checker后,还需通过评测机来检验checker是否正确,并进行修改。总的来说,整个过程较为繁琐。
正是通过评测机,我才能尽可能使自己的错误减少,最后成功通过强测与互测。在互测中,我也借助评测机检查出其他同学的错误。

心得体会

层次化设计

本单元其实我在这一方面做得并不太好,从UML图就可以看出。由于我整体借鉴的是本单元第一次上机的实现,导致我并没有去设计策略类等,电梯线程类里有一大堆东西,其实现在看来,可以把电梯线程类里面的许多东西提取出来成为策略类。虽然层次化设计没有实现得很好,但我对每个模块的功能仍然清晰,这也是我迭代过程中并没有出现太多结构上的改动而使迭代变得困难的原因,反而是第二次作业的重构花了我很长时间(周二写到周六)最后还没有正确地实现 第二次作业的迭代版只用了清明最后一天的上午两个小时就完成了,而且一遍过中测。但是无论怎样,有一个更好的设计还是会让代码阅读起来更加清晰优雅的。

线程安全

其实本单元感触最深的就是这个方面了。写到这里,我还是会像开头一样认为多线程编程是及其不安全的,但我觉得这个安全不是由于其机制,其机制是安全的,而是由于人的实现。一个小小的考虑不周很可能就会造成死锁、通信间的问题,后果也会十分可怕,况且相关错误由于多线程的执行难以复现,查起来也会十分痛苦。经历了三次作业的磨练后,我也总结了一些自己关于线程安全的设计:

  • 尽量避免在一个上了锁的方法或代码块里去尝试获取另一个对象或类的锁,这样很可能会出现死锁问题。
  • 明确共享资源和临界区,及时上锁和释放,记得唤醒
  • 多模拟几个线程同时在某一时间节点开始运行的情况,这样能检查出很多因为线程通信不合适而造成的问题

在本单元的作业中,我常常使用上面的方法和思想检查出了很多错误。我不敢说我现在再去编写一个多线程程序一定能使其完全安全,但我认为如果不时提醒自己有关上面的设计,我应该能尽可能使其安全。

  • Title: 2025_BUAA_OO_Unit2
  • Author: Cdostan
  • Created at : 2025-04-14 10:05:00
  • Updated at : 2025-04-18 18:18:16
  • Link: https://cdostan.github.io/2025/04/14/OO/oo_Unit2/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments