北航2022面向对象第二单元:多线程控制

内容概括

  • 单元重点
  • 作业设计思路
  • 作业实现和分析
  • 作业的问题
  • 总结

1. 单元重点

1.1 多线程概念

如果所有程序都操作的是不同的对象,彼此之间没有干扰,那么多线程和单线程差不多。

多线程麻烦在于不同的线程操作同一个对象时,由于线程调度是内置的逻辑决定,线程切换可能在任意时刻进行。这导致外层必须要增加控制,使得操作同一个对象的逻辑要符合实际。

1.2 Java多线程

  • Java从语言支持多线程
    最常用的控制方法是synchronized关键字。它可以给对象和类上锁,使得多个线程遇到这段语句块时,只有一个线程能执行。这样就保证了实际上的执行是串行执行的,不会有执行中途被其他线程打断的问题。
    除了synchronized,Java还提供了ReentrantLock类来帮助控制多线程。相比于synchronizedReentrantLock更加灵活。比如可以控制如果1秒钟没有拿到锁,就执行一些处理,而不是一直等待。可以减少死锁的情况。
  • 关于死锁
    死锁很难有特别好的办法完全避免,避免死锁的办法通常会有其他方面的开销。比如:
  1. 线程申请公共资源时,一次把需要用到的资源全部加锁。方法简单,但占有了大量资源,可能使其他线程拿不到资源而空等。
  2. 如果占有了一部分资源,没有拿到其他资源时,将已有的资源释放,再重新申请。但是这样加锁和释放锁的开销很大
  3. 将资源编号,如果占有了编号为i的资源,下次申请的资源必须必i大。但是这样可能在没必要的时候拒绝申请,因为实际操作资源的逻辑不一定和编号大小有什么关系。

应付本单元作业,尤其是因为第三次作业的分配涉及到等待的乘客和电梯乘客的信息,需要获得当前等待的所有乘客,加上对速度要求不是特别高,直接采用第一个方案,把所有资源都加锁,就能简单地实现资源共享。

1.3 设计模式

  • 生产者-消费者模式
    生产者和消费者同时控制一个队列,生产者每次往队列里添加产品,同时通知消费者。消费者每次从队列中拿出产品消费,如果没有产品了,就执行wait(),等待生产者添加之后唤醒。
  • 流水线模式
    将一个产品分成几个部分完成。生产者线程根据目前未完成的步骤向队列中添加。消费者从队列中获取自身能完成的请求进行处理,并标记我这个步骤已经完成。如果仍有步骤未处理完,还是通过生产者将产品放回队列。

上机实验介绍了两个模式,其实可以把两个模式结合起来看。比如有很多步骤,每个步骤都有很多工序。转发器每次往大的步骤里放产品,就类似于生产者添加。在每个步骤里,各个工种的线程从队列中取得自己能处理的工序进行处理,处理完后放回该队列,等待别的线程处理,类似流水线。全部处理完后,给转发器转给下个步骤。这样既可以表示先后顺序关系,也可以表示各部分可以同时开工的情况。

2. 作业设计思路

2.1 第一、二次作业

这两次作业的横纵向请求没有联系,只需要分别处理即可。我维护了一个所有请求的表格,将横向请求给横向电梯,纵向请求给纵向电梯即可。对于电梯的分配策略,尝试了抢夺型和平均分配型,这两种各有优劣,没有方法是适合于所有的情况的。

2.2 第三次作业

这次作业增加了跨越横纵向的请求。我的处理是增加一个类Person继承PersonRequest类。增加了当前位置和下一次的位置两种属性,每次转发器分派请求的时候,根据当前位置和最终位置确定下一次位置,判断下一次需要横向还是纵向请求,分给相应类型的电梯。每次电梯处理请求之后,更新当前位置。如果当前位置不是最终位置,则再加入分派请求,否则就处理结束。

2.3 注意的细节

  • 如何退出
    在第一、二次作业中,电梯只要判断等待表中没有请求在等待、输入线程输入完成、本电梯为空,就能确定处理结束,线程退出。
    第三次作业中,输入有两个来源,一是输入线程,二是处理到一半的请求要加回输入。可以增加一个待处理的数量这个属性,在输入时判断请求的状态,如果是第一次加入,则待处理数量增加。如果请求最终结束,待处理数量减少。将输入线程完成这个条件改为,输入线程完成,且待处理数量为零,就能合理退出。
  • 读写统一
    此时要获得电梯的乘客信息,同时还要获得这类电梯同一楼(层)的信息,以及这类电梯所有楼(层)的信息。因此需要给所有的资源上锁。而且判断要不要进乘客,和最终得到进入的乘客列表,这两个步骤要同时完成。否则在这两步的空档,乘客可能被另一台电梯接走了,这台电梯就会判断要进乘客,但是获得的乘客列表是空的。如果上一步对乘客表进行了修改操作,会导致更严重的问题。

3. 作业实现和分析

3.1 类图

  • 第一、二次作业的逻辑比较相近,都是生产者-消费者模型,输入的线程为生产者,电梯为消费者。以第二次作业的类图为例。
  • 第三次作业中,电梯在交还未完成的请求时,可以看成生产者,在处理请求时是消费者。类图如下。

3.2 UML协作图

作业的线程关系是相同的,主体上都是生产者-消费者模式,以第三次作业为例。前两次作业电梯线程中没有第7步。

多线程监控信息 多线程控制_加锁

4. 作业的问题

我猜想其他同学的问题应该主要是线程安全问题,或者是分派请求的逻辑,如何分派能处理得更快。此处我的问题并不是大多数人的问题,不足以作为典型情况。

  • 第二次作业
    由于处理横向电梯进人的逻辑出错,导致实际测试时有乘客会卡在电梯里。
  • 第三次作业
    在横向电梯进人时,没有判断该电梯对于乘客的出发地和下一个目的地能否开门。这导致电梯会在不能开门的地方开门下客。问题是我在课下debug的时候没有出过这种问题。经过课程的debug,发现分派乘客的时候没问题,但是有两个电梯抢乘客的时候,有可能进电梯能开门,但是出电梯不能开门,结果乘客被这样的电梯抢到了。

5. 心得体会

  • 多线程
    之前也接触过一些多线程的情况,比如Unix下的锁控制。但是没有像这样专门训练过多线程。我认为完全的多线程如果要保证无误是不可能的,因为程序员不能控制线程切换。只能通过一些原子操作,比如加锁和释放锁,将多线程转成串行程序执行。一个线程执行中,其他线程只能等待或者做其他事情。
  • 架构设计
    在很大的代码中,对数据进行必要的筛选是重要的。比如说传入的参数是否合法,传入的对象有没有可能为空指针,或者该对象是否满足我的要求。如果我在第三次作业中被调用的函数做了判断,那就不可能发生这种低级错误。这方面好像涉及到了第三单元的内容,对函数处理的参数进行合理的规定,被调用函数要处理哪些异常情况等。
  • 其他感受
    我在做第二、三次作业的时候,正好赶上其他的事情也非常多,导致消耗很大,精神很不好,以至于作业的分数非常低,几乎到及格边缘。但是回过来看,出的问题都是很简单的问题,在正常情况下应该不会犯这些错误。事实证明,在精神状态不佳的时候,做任何事都做不好,包括其他课程的作业、考试,还有其他的活动。我佩服一些同学有很好的体力能长时间地保持精神集中。但是我没有这种体质,稍微熬夜都会打乱生物钟。我得到的教训是合理安排,劳逸结合
  1. 精神不好的时候不要持续的学习,休息一下,小睡一会再继续,效果会更好。
  2. 尽量在精神好的时候做需要集中精力的事情,比如编程和debug。在精神欠佳的时候,可以做些轻松工作,比如写文档。
  3. 零碎时间可以做一些轻松的事,不要把应该集中精力的事情拆开做,因为集中精力需要一段时间。
  4. 注意锻炼身体,但是在很累的时候不要做剧烈的运动,否则消耗精力,而且没有锻炼效果。

身体是革命的本钱!