08 交通燈管理系統

筆者觀看了張老師關於交通燈管理系統的視頻講解,按照要求自己重新編寫了程序。

1. 項目需求

模擬實現十字路口的交通燈管理系統邏輯,具體需求如下:
(1)異步隨機生成按照各個路線行駛的車輛。
例如:
       由南向而來去往北向的車輛 ---- 直行車輛
       由西向而來去往南向的車輛 ---- 右轉車輛
       由東向而來去往南向的車輛 ---- 左轉車輛
       。。。
(2)信號燈忽略黃燈,只考慮紅燈和綠燈;
(3)應考慮左轉車輛控制信號燈,右轉車輛不受信號燈控制;
(4)具體信號燈控制邏輯與現實生活中普通交通燈控制邏輯相同,不考慮特殊情況下的控制邏輯。注:南北向車輛與東西向車輛交替放行,同方向等待車輛應先放行直行車輛而後放行左轉車輛;
(5)每輛車通過路口時間爲1秒(提示:可通過線程Sleep的方式模擬);
(6)隨機生成車輛時間間隔以及紅綠燈交換時間間隔自定,可以設置;
(7)不要求實現GUI,只考慮系統邏輯實現,可通過Log方式展現程序運行結果。

2. 需求分析

(1)點:異步生成要求我們使用多線程,每條車流都要有自己的線程。車輛生成間隔可以用Thread.sleep(new Random()...)來模擬;
(2)點:不考慮黃燈,說明信號燈總體上只有兩個狀態:放行和不放行;
(3)點:右轉車輛不受信號燈控制,因而右轉車輛的控制可以省略,實際上只需考慮8條車流;
(4)點:這個要求規定了信號燈的有限狀態機。按照日常生活經驗,左轉燈要單獨列出。這樣每個方向上的燈都需顯示三種信號:直行,左轉,停車。狀態機採取南北直行,全向左轉,東西直行的循環,以保證任一條道路上沒有對向駛來的車流;因爲信號燈需要定時轉換信號,所以需要單列一個信號燈線程,使用線程休眠方式來定時;
(5)點:車輛駛出道路需要1秒,需要創建放行線程定時放行車輛並刷新車輛數;
(6) 點:時間間隔通過相應線程的休眠時間來控制;
(7)點:爲了打印Log,需要單列一個監視器線程,監視8條車流的情況。無論是新車出現還是舊車放行導致剩餘車輛數有變化,都需要打印變化情況。
綜上,這個項目的難點是同時運行的線程較多,而且不同線程的打印語句有可能互相干擾,導致LOG上輸出的語句順序混亂。張老師用到了java.util.concurrent包中的併發線程工具。這些工具相比於Thread類中的方法,更爲簡潔和安全。不過筆者還是想用自學視頻中講到的多線程基本語句來寫。這樣做在代碼上更繁瑣一些,但是卻更熟悉。

3. 模塊分析

整個程序涉及到一個接口(常數標籤)和五個類(main類,信號燈類,車流類,放行類和監視器類)。

3.1 接口

將一些常數寫進接口裏,便於後面的模塊共享裏面的常數。
import java.util.*;

//用常數代替字符串作爲標籤
interface Tag
{
    //直行信號
    public final int PASS = -1;
    //左轉信號
    public final int LEFT_PASS = -2;
    //停車信號
    public final int STOP = -3;
    //信號間隔(秒)
    public final long LAMP_PERIOD = 3;
    //任一道路上的車輛出現間隔(最小值,秒)
    public final double CAR_PERIOD_MIN = 2;
    //最大值(秒)
    public final double CAR_PERIOD_MAX = 3;
}

3.2 信號燈類

信號燈類是一個線程類,作用是按照固定的狀態機定時切換信號,並打印切換時間和新的信號。
//信號燈類包括東西南北四個方向的信號燈,每個燈有PASS, LEFT_PASS, STOP三種信號
class Light implements Tag, Runnable
{
    int N = PASS;
    int S = PASS;
    int W = STOP;
    int E = STOP;

    //鎖,用來規範顯示順序
    Object lock;

    public Light(Object lock)
    {
        this.lock = lock;
    }

    //將信號燈狀態的常數標籤轉換回字符串
    public String translate(int signal)
    {
        if(signal == PASS)
            return "PASS";
        else if(signal == LEFT_PASS)
            return "LEFT_PASS";
        else
            return "STOP";
    }

    //信號燈變化時打印新的信號
    public void printLight()
    {
         System.out.println("N = " + translate(N) + ", S = " + translate(S) + ", W = " + translate(W) + ", E = " + translate(E));
    }

    //信號燈的有限狀態機
    @Override
    public void run()
    {
        long time = System.currentTimeMillis();
        while(true)
        {
            if(N == PASS)
            {
                //改變信號
                N = LEFT_PASS;
                S = LEFT_PASS;
                W = LEFT_PASS;
                E = LEFT_PASS;
            }
            else if(N == LEFT_PASS)
            {
                N = STOP;
                S = STOP;
                W = PASS;
                E = PASS;
            }
            else if(N == STOP)
            {
                N = PASS;
                S = PASS;
                W = STOP;
                E = STOP;
            } 
          
            //使用鎖來保證三行顯示語句連貫輸出
            //顯示信號燈變化的時刻和變化後的信號
            synchronized(lock)
            {
                System.out.println("");
                System.out.println((double)(System.currentTimeMillis() - time)/1000 + "s");
                printLight();
            }
            
            try
            {
                //信號燈定時切換信號
                Thread.sleep(LAMP_PERIOD * 1000);
            }
            catch(Exception e){}
        }
    }
}

3.3 車流類

車流類也是線程類。因爲要考慮東西南北道路的直行和左轉,實際上有8條車流,也就是8個併發的車流類線程。車流類的作用是按照一定範圍隨機產生新車,並儲存剩餘車輛數。
//每條路都是一個獨立線程,線程內隨機生成車輛並儲存該路上當前的車輛數
class Road implements Tag, Runnable
{
    //當前車輛數
    int total = 0;

    @Override
    public void run()
    {
        while(true)
        {
            //新出現一輛車
            total++;
            //等待一段隨機的時間
            try
            {
                Thread.sleep((long) (1000 * (new Random().nextDouble() * (CAR_PERIOD_MAX - CAR_PERIOD_MIN) + CAR_PERIOD_MIN)));
            }
            catch(Exception e){}
        }
    }
}

3.4 放行類

放行類也是線程類。放行類的作用是根據車輛通過路口所需時間和信號燈指示,定時放行有關道路上的車流。前面的車流類會生成新車從而增加剩餘車輛數。這裏的放行類會放行舊車從而減少剩餘車輛數。
//控制系統,放行車輛,並打印放行結束後各條路的車輛數
class Control implements Tag, Runnable
{
    //由於向右轉無需交通燈控制,故關注4個方向的8條路,每個方向有直行和左轉兩條路
    //北邊的直行路
    Road N_road;
    //北邊的左轉路
    Road NL_road;
    Road S_road;
    Road SL_road;
    Road W_road;
    Road WL_road;
    Road E_road;
    Road EL_road;
    Light light;

    public Control(Road N_road, Road NL_road, Road S_road, Road SL_road,  Road W_road, Road WL_road, Road E_road, Road EL_road, Light light)
    {
        this.N_road = N_road;
        this.NL_road = NL_road;
        this.S_road = S_road;
        this.SL_road = SL_road;
        this.W_road = W_road;
        this.WL_road = WL_road;
        this.E_road = E_road;
        this.EL_road = EL_road;
        this.light = light;
    }
    
    //放行車輛
    @Override
    public void run()
    {
       while(true)
       {
            try
            {
                //車輛駛出道路需要1秒
                Thread.sleep(1000);
            }
            catch(Exception e){}
            
            //放行後更改剩餘車輛數
            if(light.N == LEFT_PASS)
            {
                NL_road.total = (NL_road.total > 0) ? NL_road.total - 1 : 0;
                SL_road.total = (SL_road.total > 0) ? SL_road.total - 1 : 0;
                WL_road.total = (WL_road.total > 0) ? WL_road.total - 1 : 0;
                EL_road.total = (EL_road.total > 0) ? EL_road.total - 1 : 0;
            }
            else if(light.N == STOP)
            {
                W_road.total = (W_road.total > 0) ? W_road.total - 1 : 0;
                E_road.total = (E_road.total > 0) ? E_road.total - 1 : 0;
            }
            else if(light.N == PASS)
            {
                N_road.total = (N_road.total > 0) ? N_road.total - 1 : 0;
                S_road.total = (S_road.total > 0) ? S_road.total - 1 : 0;
            } 
       }
    }
}

3.5 監視器類

監視器類也是線程類。它獲取車流類的剩餘車輛數信息,只要該信息有變化就將變化時間和變化情況打印出來。
//監視器,當任何道路的車輛數因爲新車出現或舊車放行而改變時,顯示變化時間和變化後車輛數
class Monitor implements Tag, Runnable
{
    Road N_road;
    Road NL_road;
    Road S_road;
    Road SL_road;
    Road W_road;
    Road WL_road;
    Road E_road;
    Road EL_road;
    
    int N_old = 0;
    int NL_old = 0;
    int S_old = 0;
    int SL_old = 0;
    int W_old = 0;
    int WL_old = 0;
    int E_old = 0;
    int EL_old = 0;
    
    Object lock;

    //換行標誌
    boolean changeLine = false;

    public Monitor(Road N_road, Road NL_road, Road S_road, Road SL_road,  Road W_road, Road WL_road, Road E_road, Road EL_road, Object lock)
    {
        this.N_road = N_road;
        this.NL_road = NL_road;
        this.S_road = S_road;
        this.SL_road = SL_road;
        this.W_road = W_road;
        this.WL_road = WL_road;
        this.E_road = E_road;
        this.EL_road = EL_road;
        this.lock = lock;
    }

    @Override
    public void run()
    {
        long time = System.currentTimeMillis();
        while(true)
        {
            if(N_road.total == N_old && NL_road.total == NL_old 
                && S_road.total == S_old && SL_road.total == SL_old 
                && W_road.total == W_old && WL_road.total == WL_old 
                && E_road.total == E_old && EL_road.total == EL_old);
            else
            {
                //用鎖保證下列語句是連貫輸出的
                synchronized(lock)
                {
                    changeLine = false;
                    //打印車輛數發生變化的時間
                    System.out.println((double)(System.currentTimeMillis() - time)/1000 + "s");
                    //若是放行導致的,打印放行情況
                    if(N_road.total < N_old)    {System.out.print("Go N, ");    changeLine = true;}
                    if(NL_road.total < NL_old)  {System.out.print("Go NL, ");   changeLine = true;}
                    if(S_road.total < S_old)    {System.out.print("Go S, ");    changeLine = true;}
                    if(SL_road.total < SL_old)  {System.out.print("Go SL, ");   changeLine = true;}
                    if(W_road.total < W_old)    {System.out.print("Go W, ");    changeLine = true;}
                    if(WL_road.total < WL_old)  {System.out.print("Go WL, ");   changeLine = true;}
                    if(E_road.total < E_old)    {System.out.print("Go E, ");    changeLine = true;}
                    if(EL_road.total < EL_old)  {System.out.print("Go EL, ");   changeLine = true;}
                    //打印變化後的車輛數
                    if(changeLine)
                        System.out.println("\nN   S   W   E   NL  SL  WL  EL");
                    else
                        System.out.println("N   S   W   E   NL  SL  WL  EL");
                    System.out.println(N_road.total + "   " + S_road.total + "   " + W_road.total + "   " + E_road.total + "   " + NL_road.total + "   " + SL_road.total + "   " + WL_road.total + "   " + EL_road.total);
                }
                
                //更新車輛數
                N_old = N_road.total;
                NL_old = NL_road.total;
                S_old = S_road.total;
                SL_old = SL_road.total;
                W_old = W_road.total;
                WL_old = WL_road.total;
                E_old = E_road.total;
                EL_old = EL_road.total;
            }
        }
    }
}

3.6 main類

main類負責創建並啓動所有線程。
public class Test implements Tag
{
    public static void main(String[] args)
    {
        //生成鎖用來規範顯示順序
        Object lock = new Object();

        Light light = new Light(lock);
        Road N_road = new Road();
        Road S_road = new Road();
        Road W_road = new Road();
        Road E_road = new Road();
        Road NL_road = new Road();
        Road SL_road = new Road();
        Road WL_road = new Road();
        Road EL_road = new Road();

        //開啓交通燈線程,交通燈開始工作
        new Thread(light).start();
        //開啓所有路的線程,開始生成車輛
        new Thread(N_road).start();
        new Thread(S_road).start();
        new Thread(W_road).start();
        new Thread(E_road).start();
        new Thread(NL_road).start();
        new Thread(SL_road).start();
        new Thread(WL_road).start();
        new Thread(EL_road).start();
        //開啓控制系統線程,車輛開始放行
        new Thread(new Control(N_road, NL_road, S_road, SL_road, W_road, WL_road, E_road, EL_road, light)).start();
        //開啓監控器線程,打印日誌
        new Thread(new Monitor(N_road, NL_road, S_road, SL_road, W_road, WL_road, E_road, EL_road, lock)).start();
    }
}

4. 運行結果


上面的日誌顯示了信號燈的變化時間,變化情況,車流的變化時間和變化情況,以及剩餘車輛數。

5. 總結

通過做這個項目,複習了javaSE的多線程基礎知識,提高了建模能力。從代碼上看,筆者的程序比張老師的繁瑣很多,不過日誌輸出的更詳細一些。這個項目的難點在於諸多線程之間容易互相干擾,尤其在打印日誌的時候,這時需要專門的鎖來鎖定輸出語句以防順序混亂。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章