BUAA_OO_2021_ 第二單元 - 難度巔峯之多線程電梯

BUAA_OO_2021_ 第二單元 - 難度巔峯之多線程電梯

寫在前面

早就耳聞了面向對象課程第二單元的難度,在面臨一個全新的領域——多線程時,或多或少都會手足無措吧。對於一個普普統統的計算機專業的學生來講,沒有大佬們對於代碼強大的理解與拓展能力,只能看着入門教程一點點自學,十分痛苦。多虧了廖雪峯老師網站的java多線程入門,我纔對多線程思想有了些許體會。但俗話說「師傅領進門,修行在我的」,對於這個單元的做業,最最重要也是最最基礎的就是 wait()notifyAll() 的使用來對線程進行調度;另外一個最最重要也是最最基礎的就是 synchronized() ,對於鎖的使用仍是十分抽象的。但通過了這三次做業的摸索,我對這兩者有了一個較深的理解。java

oo上機時的實驗代碼也給我帶來了非非很是大的幫助(雖而後來被錘有bug)上課時老師給的多線程的例子不免很是簡單, 而實驗代碼清晰的架構以及對多線程的操做的示例都給我前兩次做業帶來了很大的啓示。若是學弟學妹們無心中刷到了這篇博客,聽學長一句勸:不要死磕本身醜到爆的架構,在入門的時候借鑑借鑑優秀代碼,會事半功倍。算法

1、同步塊的設置與鎖的選擇

三次做業使用的同步鎖是synchronized() ,而未使用Lock編程

synchronized()可修飾的對象有:安全

  • 修飾一個代碼塊:被修飾的代碼塊稱爲語句同步塊,其做用範圍是使用大括號{}括起來的代碼,做用的對象是synchronized(object) 中的object。
  • 修飾一個方法:被修飾的方法稱爲同步方法,其做用範圍是整個方法,做用的對象是調用這個方法的對象。
  • 修飾一個靜態方法:做用範圍是整個靜態方法,做用對象是這個類的全部對象。
  • 修飾一個類:做用範圍是整個類中全部方法,做用對象是這個類的全部對象。

三次做業的同步塊統一採用的是使用 synchronized() 鎖住代碼塊,而非給方法加鎖,更沒有給類加鎖。一是爲了性能考慮,畢竟加鎖的代碼塊越小越好,二是我想鍛鍊一下本身對於synchronized() 的理解,給小塊代碼加鎖,雖然對於互斥問題的解決更加複雜,但這也正是多線程編程的精髓。多線程

整體而言,這三次中都存在的電梯安全問題是輸入線程和電梯線程對於等待隊列的讀寫操做。架構

在三次做業的電梯類中,我採用的是Look算法進行調度,所以在每次有新請求進入電梯後,都要對要去的最高(最低樓層)進行更新,但整個過程都是對於本部電梯內部屬性的操做,並無致使線程衝突。只有在第七次做業中加入了換乘操做,才致使了不一樣電梯之間的線程安全問題。dom

下面介紹具體到每一次做業中的呈現形式:性能

第五次做業

本次做業架構比較簡單,只有一部電梯所以共享對象並很少。測試

共享對象有:waitQueue(等待隊列)優化

waitQueue的同步問題主要涉及如下幾點:

  • inputThread線程在輸入後須要將需求 寫入 waitQueue,並喚醒電梯

    synchronized (waitQueue) {
                    if (request == null) {
                        waitQueue.close();
                        waitQueue.notifyAll();
                        try {
                            elevatorInput.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                        return;
                    } else {
                        waitQueue.addRequest(request);
                        waitQueue.notifyAll();
                    }
                }
  • elevator線程須要讀取 waitQueue中是否有人,若沒人則須要wait()

    synchronized (waitQueue) {
                    if (elevatorRequests.isEmpty() && waitQueue.noWaiting() && waitQueue.isEnd()) {
                        return;
                    }
                    if (waitQueue.noWaiting()) {
                        try {
                            waitQueue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
  • elevator線程須要讀取 waitQueue在某一層的人,若是有知足上電梯條件的請求,則還須要將請求寫出 waitQueue

    因爲對於waitQueue的讀寫操做涉及算法核心,所以不附代碼

第六次做業

本次做業增長了多部電梯,所以增長了Scheduler調度器類,來對各個請求進行分配,架構與前一次單部電梯相比複雜了很多,也形成了更多的線程安全問題。

共享對象有:totalQueue(請求總隊列),waitQueue(每部電梯單獨的等待隊列)

全部同步問題如圖:

image

從圖中可知各個線程對於總等待隊列和各個電梯的等待隊列的線程安全衝突問題,所以在每次讀寫操做時,都要使用synchronized() 解決同步問題。如:

//Scheduler調度器線程中:
synchronized (totalQueue) {
                if (totalQueue.isEnd() && totalQueue.noWaiting()) {
                    for (Elevator elevator : elevators) {
                        WaitQueue waitQueue = elevator.getWaitQueue();
                        synchronized (waitQueue) {
                            waitQueue.notifyAll();
                        }
                    }
                    return;
                }
                if (totalQueue.noWaiting()) {
                    try {
                        totalQueue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } //解除總等待隊列的鎖,能夠繼續進行輸入
//Elevator電梯線程中:
synchronized (waitQueue) {
                if (elevatorRequests.isEmpty() && waitQueue.noWaiting()
                        && totalQueue.isEnd() && totalQueue.noWaiting()) {
                    return;
                }
                if (waitQueue.noWaiting() || (type == 1 && !totalQueue.isEnd())) {
                    try {
                        direction = 0;
                        waitQueue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                direction = waitQueue.getRequest(0).getToFloor()
                        - waitQueue.getRequest(0).getFromFloor();
            }//解除電梯等待隊列的鎖,調度器能夠繼續進行分配

第七次做業

本次做業增長了電梯的種類屬性,爲了優化性能,所以增長了換乘策略,形成了不一樣電梯間互相改變等待隊列的線程安全問題。

共享對象有:totalQueue(請求總隊列),waitQueue(每部電梯單獨的等待隊列)

全部同步問題如圖:

image

從圖中能夠看出,因爲增長了A類電梯和B類電梯之間的換乘策略,致使了在不一樣電梯之間也會產生線程安全問題,所以除了上文給出的第六次做業中的代碼外,還會出現如下形式的同步塊與鎖的設置:

//換乘操做
int toFloor = request.getToFloor();
int fromFloor = floor;
int id = request.getPersonId();
PersonRequest newRequest = new PersonRequest(fromFloor, toFloor, id);
elevatorRequests.remove(request);
TimableOutput.println("OUT-" + request.getPersonId() + "-" + floor + "-" + idNumber);
i--;
synchronized (totalQueue) {
	totalQueue.getRequests().add(0, newRequest);
	totalQueue.notifyAll();
}

2、調度器設計

第五次做業

「就一部電梯,調度器有啥用呢?」

開始作第五次做業以前,我就是這樣想的。因而把已經建好的Scheduler類默默刪掉了。

事實也是這樣,一部電梯的確不須要調度,而此次做業的核心應當是對於單部電梯調度算法的設計,好比指導書給出的als算法,以及最經常使用,性能也不錯,容易實現的look算法(這三次做業的Random模式使用的都是look算法,真香!)而調度器這東西,雖然知道在後面必定會再加上,也知道在第一次添上調度器的話有利於以後做業的拓展,但俗話說:「一口吃不成胖子。」我知道本身水平咋樣,就不如剛剛接觸多線程的時候,整個簡單點的架構吧~

因而第五次做業並無調度器,全部輸入的請求直接進入等待隊列,等待電梯的臨幸。

下面介紹一下三種模式下電梯的運行策略:

  • Night

Night 模式的兩大特色:

  • 目的樓層肯定(都是一樓)
  • 全部乘客同時到達

所以能夠將其看做靜態請求。對於處理靜態請求,總會有一個最優解,而我採起的策略是

電梯從高處往低處接,每次接六個,若是不足六個則接完就結束

這個策略十分簡單,性能也還不錯,然而對於Night 模式仍是有別的最優解的,只是代碼會複雜不少,而在我能力範圍以內,這種策略的性價比很高。

  • Morning

Morning模式的最大問題是,人並非一口氣都到一樓,而是動態添加的。

因爲是動態模型,不一樣人的算法可能會形成很大的性能差別,而我採起了一種比較穩妥地方法:

等人

只有當來了六我的,電梯滿員時,纔會發車,不然就一直等着,除非識別到已經中止輸入了。這種方法帶來的一個玄學問題是:沒法保證在等人的過程當中是否是有時間可以把電梯上的人先送上去。但若是要實現這樣的機制的話,代碼量應該是十分驚人的,我也就沒有再深究。

  • Random

我採用了傳統電梯搭載的look 算法,並在其基礎上進行了些許優化。其運行策略以下:

一、獲取等待隊列中全部上行請求的最低層 和全部下行請求的最高層 ,並判斷電梯當前所處位置離誰更近,而後去往更近的那個樓層,並改變電梯的運行方向次序。

二、電梯按照選擇好的運行次序運行,並把目標樓層設定爲電梯中全部請求的目的樓層的最高層。

三、電梯在每一層進行遍歷:首先遍歷電梯中的請求有沒有目的樓層是該樓層的,並處理出電梯的請求;而後遍歷等待隊列中有沒有前往樓層方向與電梯運行方向相同的,並在該樓層上電梯的請求,如有,則上電梯;若請求的目的樓層要高於目前電梯運行要去的最高樓層,則將電梯的最高樓層屬性更新。

四、當電梯到達目標樓層(即全部請求的最高樓層或最低樓層)後,電梯掉頭,重複上述行爲。

這種算法與平常生活中的電梯十分類似,也很好理解,通過實踐,性能也說得過去。

第六次做業

第五次做業欠下的債,遲早都要還的。

第六次做業要把請求們按照必定的策略分給多部電梯了,所以須要設計調度器以及調度器算法,來實現請求的調度。

調度器的遇到的難點以下:

​ 一、如何制定合理的分人策略,是影響電梯性能的最關鍵因素

​ 二、如何處理好調度器線程與電梯線程、調度器線程與輸入線程之間的矛盾,是電梯可否順利運行的關鍵因素

制定的分人策略以下:

  • Night

Night模式的分配策略比較容易想到,畢竟是靜態請求。

​ 一、將總請求隊列中的人從高往低排序

​ 二、按照每六個爲一組,輪流分給各部電梯

這樣作也會遇到些許問題,好比沒法統一處理高層請求,對於人數較少的狀況沒法調動全部電梯。但我認爲這些問題致使的性能差別微乎其微,便沒有再進行優化。

  • Morning

實在沒有想出完美的解決辦法,因而採起將請求均分給全部電梯的調度策略。

​ 一、每次提取出總等待隊列的第一個請求,進行處理。

​ 二、遍歷全部的電梯的等待隊列,選擇等待請求最少的電梯

​ 三、將提取出的請求分配給所選擇的電梯

​ 四、重複上述操做,直到總請求隊列爲空

這樣的策略有顯而易見的性能問題,好比每部電梯爲了等夠6我的,等待的時間特別長。

  • Random

Random模式的調度策略比較複雜,我整體按照的是「順路原則」進行分配。具體分配過程如圖:

image

雖然並不能保證這樣的算法是最優的,但能夠保證順路原則的的確確提升了不少性能。

第七次做業

換乘!換乘!換乘!

隨着換乘算法的加入,第二單元做業難度達到了高潮。

難道非換乘不可嗎?固然不是,最簡單的調度方法即是:

​ 一、全部符合高樓層條件的請求一概給C類電梯

​ 二、全部奇數層到奇數層的請求一概給B類電梯

​ 三、其它全部請求一概給A類電梯

這樣,第七次做業的調度器就更改完畢啦!

我按照這樣的調度策略,寫了第七次做業的原始版本,並獲得了不換乘的性能時間,而後便開始設計換乘調度策略。

​ 一、全部符合C類電梯的請求,直接分配給請求最少的C類電梯,不換乘

​ 二、全部奇數層到奇數層的請求,直接分配給請求最少的B類電梯,不換乘

​ 三、全部偶數層到偶數層的請求,直接分配給請求最少的A類電梯,不換乘

​ 四、全部偶數層到奇數層的請求,先分配給A類電梯,運送到最鄰近的奇數樓層後,換乘給B類電梯

​ 五、全部奇數層到偶數層的請求,先分配給B類電梯,運送到最鄰近目的地的偶數樓層後,換乘給A類電梯

哈哈,換乘也不過如此嘛!

真的不過如此嗎?

不是!且不說這種換成策略的性能問題,單看整個換乘過程,明明是爲了換乘而換乘啊!我提交了「優化後」的這個版本,果真不出我所料,這個版本的性能比不優化的原始版本還要差...沒錯,個人換乘策略是負優化。又考慮到第一單元就是因爲各類優化算法才致使了強測出鍋,我獲得了一個結論:若是能力不足,謹慎優化!

3、從功能設計與性能設計的平衡方面,分析和總結本身第三次做業架構設計的可擴展性

第七次做業UML圖

image

第七次做業UML協做圖

image

本次的架構設計爲「消費者——生產者模型」,生產者爲InputThread 線程,消費者爲Elevator 線程,並不複雜。

關於可擴展性,在這樣的架構之下,分配調度算法與運行算法分離,必定程度上提高了可擴展性,但總的來講,可擴展性仍是不高的,畢竟在寫程序的過程當中有着「反正寫完這三次就沒了」的內心,沒有太多考慮程序的可擴展性。

4、分析本身程序的BUG

第五次做業

未發現本身程序bug,也未被hack,而且代碼提交一次經過,沒進行debug。(因此下次做業就飄了

第六次做業

BUG警告! 本次做業強測稍炸,出現的鍋是因爲morning算法的進程結束問題,致使4個morning模式的測試樣例線程沒能正常結束,從而致使rtle。這個bug是我測試的時候樣例過弱致使的,很明顯也很容易de的一個bug,居然被放生到強測中,真的不該該。

第七次做業

未發現本身程序bug,也未被hack,但在寫程序中出現過輪詢致使的tle的bug,緣由是wait() 方法調用條件有誤形成程序沒有正常進行等待。

在debug過程當中,的確出現過死鎖的問題。出現了鎖的嵌套,而且同步塊過大,就十分有可能出現死鎖。並且有的死鎖居然很難復現,這給個人debug形成了必定的困擾。好在我及時定位到了bug,並絞盡腦汁地把嵌套的鎖拆開,不惜犧牲一些性能,也要保證線程安全。

5、分析發現別人程序bug所採用的策略

因爲本單元做業的輸入是隨機投放的,這就致使只要不是C組,想要hack別人,就只能藉助評測機了。多是因爲評測機生成的樣例太弱,這三次做業都沒能成功hack別人。

我也曾經嘗試過肉眼debug,但因爲別人的代碼架構和個人代碼或多或少有些區別,並且也沒有註釋,致使肉眼debug變得十分困難乃至不可能完成。我也着重查看了鎖嵌套的狀況,並思考有沒有死鎖狀況的發生,但同窗們的代碼都很強,並無發現有什麼異常。

遙想上個單元,三次做業都沒使用評測機,只是經過本身的火眼金睛,即便在A組,都能成功hack別人,但在多線程這個單元,肉眼不太管用了,甚至評測機的效率都不過高了...

6、心得體會

  • 線程安全問題是多線程編程最重要的問題之一,線程不安全的多線程是沒有意義的。固然,若是把全部的類都用synchronized() 鎖起來,線程也就百分百安全了,但也就由多線程退化成單線程了...如何在儘量優化性能的前提下保證線程安全,是一件值得琢磨的事,這也許就是多線程的魅力吧。
  • 層次化設計在多線程中是必須的,畢竟要把每個層次抽象出來,每個線程也都分工明確。一個好的架構對於多線程而言是前提,多學幾個架構也沒啥壞處。但把模板架構與具體任務結合起來的時候,就要花一點心思了。
  • 本單元做業又跟上一單元同樣,在第二次做業翻車,確定是有點遺憾的,不過還有兩個單元呢,但願以後的單元別再翻車了嗚嗚嗚。
相關文章
相關標籤/搜索