導航:首頁 > 編程語言 > java多線程安全

java多線程安全

發布時間:2023-10-18 18:10:53

1. java多線程程序設計詳細解析


一、理解多線程
多線程是這樣一種機制,它允許在程序中並發執行多個指令流,每個指令流都稱為一個線程,彼此間互相獨立。
線程又稱為輕量級進程,它和進程一樣擁有獨立的執行控制,由操作系統負責調度,區別在於線程沒有獨立的存儲空間,而是和所屬進程中的其它線程共享一個存儲空間,這使得線程間的通信遠較進程簡單。
多個線程的執行是並發的,也就是在邏輯上「同時」,而不管是否是物理上的「同時」。如果系統只有一個CPU,那麼真正的「同時」是不可能的,但是由於CPU的速度非常快,用戶感覺不到其中的區別,因此我們也不用關心它,只需要設想各個線程是同時執行即可。
多線程和傳統的單線程在程序設計上最大的區別在於,由於各個線程的控制流彼此獨立,使得各個線程之間的代碼是亂序執行的,由此帶來的線程調度,同步等問題,將在以後探討。
二、在Java中實現多線凱液慎程
我們不妨設想,為了創建一個新的線程,我們需要做些什麼?很顯然,我們必須指明這個線程所要執行的代碼,而這就是在Java中實現多線程我們所需要做的一切!
真是神奇!Java是如何做到這一點的?通過類!作為一個完全面向對象的語言,Java提供了類java.lang.Thread來方便多線程編程,這個類提供了大量的方法來方便我們控制自己的各個線程,我們以後的討論都將圍繞這個類進行。
那麼如何提供給 Java 我們要線程執行的代碼呢?讓我們來看一看 Thread 類。Thread 類最重要的方法是run(),它為Thread類的方法start()所調用,提供我們的線程所要執行的代碼。為了指定我們自己的代碼,只需要覆蓋它!
方法一:繼承 Thread 類,覆蓋方法 run(),我們在創建的 Thread 類的子類中重寫 run() ,加入線程所要執行的代碼即可。下面是一個例子:
public class MyThread extends Thread
{
int count= 1, number;
public MyThread(int num)
{
number = num;
System.out.println
("創建線程 " + number);
}
public void run() {
while(true) {
System.out.println
("線程 " + number + ":計數 " + count);
if(++count== 6) return;
}
}
public static void main(String args[])
{
for(int i = 0;
i 〈 5; i++) new MyThread(i+1).start();
}
}
這種方法簡單明了,符合大家的習慣,但是,它也有一個很大的缺點,那就是如果我們的類已經從一個類繼承(如小程序必須繼承自 Applet 類),則無法再繼承 Thread 類,這時如果我們又不想建立一個新的類,應該怎麼辦呢?
我們不妨來探索一種新的方法:我們不創建Thread類的子類,而是直接使用它,那麼我們只能將我們的方法作為參數傳遞給 Thread 類的實例,有點類似回調函數。但是 Java 沒有指針,我們只能傳遞一個包含這個方法的類的實例。
那麼如何限制這個類盯敬必須包含這一方法呢?當然是使用介面!(雖然抽象類也可滿足,但是需要繼承,而我們之所以要採用這種新方法,不就是為了避免繼承帶來的限制嗎?)
Java 提供了介面 java.lang.Runnable 來支持這種方法。
方法二:實現 Runnable 介面
Runnable介面只有一個方法run(),我們聲明自己的類實現Runnable介面並提供這一方法,將我們的線程代碼寫入其中,就完成了這一部分的任務。但是Runnable介面並沒有任何對線程的支持,我們還必須創建Thread類的實例,這一點通過Thread類的構造函數public Thread(Runnable target);來實現。下面埋禪是一個例子:
public class MyThread implements Runnable
{
int count= 1, number;
public MyThread(int num)
{
number = num;
System.out.println("創建線程 " + number);
}
public void run()
{
while(true)
{
System.out.println
("線程 " + number + ":計數 " + count);
if(++count== 6) return;
}
}
public static void main(String args[])
{
for(int i = 0; i 〈 5;
i++) new Thread(new MyThread(i+1)).start();
}
}
嚴格地說,創建Thread子類的實例也是可行的,但是必須注意的是,該子類必須沒有覆蓋 Thread 類的 run 方法,否則該線程執行的將是子類的 run 方法,而不是我們用以實現Runnable 介面的類的 run 方法,對此大家不妨試驗一下。
使用 Runnable 介面來實現多線程使得我們能夠在一個類中包容所有的代碼,有利於封裝,它的缺點在於,我們只能使用一套代碼,若想創建多個線程並使各個線程執行不同的代碼,則仍必須額外創建類,如果這樣的話,在大多數情況下也許還不如直接用多個類分別繼承 Thread 來得緊湊。
綜上所述,兩種方法各有千秋,大家可以靈活運用。
下面讓我們一起來研究一下多線程使用中的一些問題。
三、線程的四種狀態
1. 新狀態:線程已被創建但尚未執行(start() 尚未被調用)。
2. 可執行狀態:線程可以執行,雖然不一定正在執行。CPU 時間隨時可能被分配給該線程,從而使得它執行。
3. 死亡狀態:正常情況下 run() 返回使得線程死亡。調用 stop()或 destroy() 亦有同樣效果,但是不被推薦,前者會產生異常,後者是強制終止,不會釋放鎖。
4. 阻塞狀態:線程不會被分配 CPU 時間,無法執行。
四、線程的優先順序
線程的優先順序代表該線程的重要程度,當有多個線程同時處於可執行狀態並等待獲得 CPU 時間時,線程調度系統根據各個線程的優先順序來決定給誰分配 CPU 時間,優先順序高的線程有更大的機會獲得 CPU 時間,優先順序低的線程也不是沒有機會,只是機會要小一些罷了。
你可以調用 Thread 類的方法 getPriority() 和 setPriority()來存取線程的優先順序,線程的優先順序界於1(MIN_PRIORITY)和10(MAX_PRIORITY)之間,預設是5(NORM_PRIORITY)。
五、線程的同步
由於同一進程的多個線程共享同一片存儲空間,在帶來方便的同時,也帶來了訪問沖突這個嚴重的問題。Java語言提供了專門機制以解決這種沖突,有效避免了同一個數據對象被多個線程同時訪問。
由於我們可以通過 private 關鍵字來保證數據對象只能被方法訪問,所以我們只需針對方法提出一套機制,這套機制就是 synchronized 關鍵字,它包括兩種用法:synchronized 方法和 synchronized 塊。
1. synchronized 方法:通過在方法聲明中加入 synchronized關鍵字來聲明 synchronized 方法。如:
public synchronized void accessVal(int newVal);
synchronized 方法控制對類成員變數的訪問:每個類實例對應一把鎖,每個 synchronized 方法都必須獲得調用該方法的類實例的鎖方能執行,否則所屬線程阻塞,方法一旦執行,就獨占該鎖,直到從該方法返回時才將鎖釋放,此後被阻塞的線程方能獲得該鎖,重新進入可執行狀態。
這種機制確保了同一時刻對於每一個類實例,其所有聲明為 synchronized 的成員函數中至多隻有一個處於可執行狀態(因為至多隻有一個能夠獲得該類實例對應的鎖),從而有效避免了類成員變數的訪問沖突(只要所有可能訪問類成員變數的方法均被聲明為 synchronized)。
在 Java 中,不光是類實例,每一個類也對應一把鎖,這樣我們也可將類的靜態成員函數聲明為 synchronized ,以控制其對類的靜態成員變數的訪問。
synchronized 方法的缺陷:若將一個大的方法聲明為synchronized 將會大大影響效率,典型地,若將線程類的方法 run() 聲明為 synchronized ,由於在線程的整個生命期內它一直在運行,因此將導致它對本類任何 synchronized 方法的調用都永遠不會成功。當然我們可以通過將訪問類成員變數的代碼放到專門的方法中,將其聲明為 synchronized ,並在主方法中調用來解決這一問題,但是 Java 為我們提供了更好的解決辦法,那就是 synchronized 塊。
2. synchronized 塊:通過 synchronized關鍵字來聲明synchronized 塊。語法如下:
synchronized(syncObject)
{
//允許訪問控制的代碼
}
#p#副標題#e#
synchronized 塊是這樣一個代碼塊,其中的代碼必須獲得對象 syncObject (如前所述,可以是類實例或類)的鎖方能執行,具體機制同前所述。由於可以針對任意代碼塊,且可任意指定上鎖的對象,故靈活性較高。
六、線程的阻塞為了解決對共享存儲區的訪問沖突,Java 引入了同步機制,現在讓我們來考察多個線程對共享資源的訪問,顯然同步機制已經不夠了,因為在任意時刻所要求的資源不一定已經准備好了被訪問,反過來,同一時刻准備好了的資源也可能不止一個。為了解決這種情況下的訪問控制問題,Java 引入了對阻塞機制的支持。
阻塞指的是暫停一個線程的執行以等待某個條件發生(如某資源就緒),學過操作系統的同學對它一定已經很熟悉了。Java 提供了大量方法來支持阻塞,下面讓我們逐一分析。
1. sleep() 方法:sleep() 允許 指定以毫秒為單位的一段時間作為參數,它使得線程在指定的時間內進入阻塞狀態,不能得到CPU 時間,指定的時間一過,線程重新進入可執行狀態。典型地,sleep() 被用在等待某個資源就緒的情形:測試發現條件不滿足後,讓線程阻塞一段時間後重新測試,直到條件滿足為止。
2. suspend() 和 resume() 方法:兩個方法配套使用,suspend()使得線程進入阻塞狀態,並且不會自動恢復,必須其對應的resume() 被調用,才能使得線程重新進入可執行狀態。典型地,suspend() 和 resume() 被用在等待另一個線程產生的結果的情形:測試發現結果還沒有產生後,讓線程阻塞,另一個線程產生了結果後,調用 resume() 使其恢復。
3. yield() 方法:yield() 使得線程放棄當前分得的 CPU 時間,但是不使線程阻塞,即線程仍處於可執行狀態,隨時可能再次分得 CPU 時間。調用 yield() 的效果等價於調度程序認為該線程已執行了足夠的時間從而轉到另一個線程。
4. wait() 和 notify() 方法:兩個方法配套使用,wait() 使得線程進入阻塞狀態,它有兩種形式,一種允許 指定以毫秒為單位的一段時間作為參數,另一種沒有參數,前者當對應的 notify() 被調用或者超出指定時間時線程重新進入可執行狀態,後者則必須對應的 notify() 被調用。
初看起來它們與 suspend() 和 resume() 方法對沒有什麼分別,但是事實上它們是截然不同的。區別的核心在於,前面敘述的所有方法,阻塞時都不會釋放佔用的鎖(如果佔用了的話),而這一對方法則相反。
上述的核心區別導致了一系列的細節上的區別。
首先,前面敘述的所有方法都隸屬於 Thread 類,但是這一對卻直接隸屬於 Object 類,也就是說,所有對象都擁有這一對方法。初看起來這十分不可思議,但是實際上卻是很自然的,因為這一對方法阻塞時要釋放佔用的鎖,而鎖是任何對象都具有的,調用任意對象的 wait() 方法導致線程阻塞,並且該對象上的鎖被釋放。
而調用 任意對象的notify()方法則導致因調用該對象的 wait() 方法而阻塞的線程中隨機選擇的一個解除阻塞(但要等到獲得鎖後才真正可執行)。
其次,前面敘述的所有方法都可在任何位置調用,但是這一對方法卻必須在 synchronized 方法或塊中調用,理由也很簡單,只有在synchronized 方法或塊中當前線程才佔有鎖,才有鎖可以釋放。
同樣的道理,調用這一對方法的對象上的鎖必須為當前線程所擁有,這樣才有鎖可以釋放。因此,這一對方法調用必須放置在這樣的 synchronized 方法或塊中,該方法或塊的上鎖對象就是調用這一對方法的對象。若不滿足這一條件,則程序雖然仍能編譯,但在運行時會出現IllegalMonitorStateException 異常。
wait() 和 notify() 方法的上述特性決定了它們經常和synchronized 方法或塊一起使用,將它們和操作系統的進程間通信機製作一個比較就會發現它們的相似性:synchronized方法或塊提供了類似於操作系統原語的功能,它們的執行不會受到多線程機制的干擾,而這一對方法則相當於 block 和wakeup 原語(這一對方法均聲明為 synchronized)。
它們的結合使得我們可以實現操作系統上一系列精妙的進程間通信的演算法(如信號量演算法),並用於解決各種復雜的線程間通信問題。
關於 wait() 和 notify() 方法最後再說明兩點:
第一:調用 notify() 方法導致解除阻塞的線程是從因調用該對象的 wait() 方法而阻塞的線程中隨機選取的,我們無法預料哪一個線程將會被選擇,所以編程時要特別小心,避免因這種不確定性而產生問題。
第二:除了 notify(),還有一個方法 notifyAll() 也可起到類似作用,唯一的區別在於,調用 notifyAll() 方法將把因調用該對象的 wait() 方法而阻塞的所有線程一次性全部解除阻塞。當然,只有獲得鎖的那一個線程才能進入可執行狀態。
談到阻塞,就不能不談一談死鎖,略一分析就能發現,suspend() 方法和不指定超時期限的 wait() 方法的調用都可能產生死鎖。遺憾的是,Java 並不在語言級別上支持死鎖的避免,我們在編程中必須小心地避免死鎖。
以上我們對 Java 中實現線程阻塞的各種方法作了一番分析,我們重點分析了 wait() 和 notify()方法,因為它們的功能最強大,使用也最靈活,但是這也導致了它們的效率較低,較容易出錯。實際使用中我們應該靈活使用各種方法,以便更好地達到我們的目的。
七、守護線程
守護線程是一類特殊的線程,它和普通線程的區別在於它並不是應用程序的核心部分,當一個應用程序的所有非守護線程終止運行時,即使仍然有守護線程在運行,應用程序也將終止,反之,只要有一個非守護線程在運行,應用程序就不會終止。守護線程一般被用於在後台為其它線程提供服務。
可以通過調用方法 isDaemon() 來判斷一個線程是否是守護線程,也可以調用方法 setDaemon() 來將一個線程設為守護線程。
八、線程組
線程組是一個 Java 特有的概念,在 Java 中,線程組是類ThreadGroup 的對象,每個線程都隸屬於唯一一個線程組,這個線程組在線程創建時指定並在線程的整個生命期內都不能更改。
你可以通過調用包含 ThreadGroup 類型參數的 Thread 類構造函數來指定線程屬的線程組,若沒有指定,則線程預設地隸屬於名為 system 的系統線程組。
在 Java 中,除了預建的系統線程組外,所有線程組都必須顯式創建。在 Java 中,除系統線程組外的每個線程組又隸屬於另一個線程組,你可以在創建線程組時指定其所隸屬的線程組,若沒有指定,則預設地隸屬於系統線程組。這樣,所有線程組組成了一棵以系統線程組為根的樹。
Java 允許我們對一個線程組中的所有線程同時進行操作,比如我們可以通過調用線程組的相應方法來設置其中所有線程的優先順序,也可以啟動或阻塞其中的所有線程。
Java 的線程組機制的另一個重要作用是線程安全。線程組機制允許我們通過分組來區分有不同安全特性的線程,對不同組的線程進行不同的處理,還可以通過線程組的分層結構來支持不對等安全措施的採用。
Java 的 ThreadGroup 類提供了大量的方法來方便我們對線程組樹中的每一個線程組以及線程組中的每一個線程進行操作。
九、總結
在本文中,我們講述了 Java 多線程編程的方方面面,包括創建線程,以及對多個線程進行調度、管理。我們深刻認識到了多線程編程的復雜性,以及線程切換開銷帶來的多線程程序的低效性,這也促使我們認真地思考一個問題:我們是否需要多線程?何時需要多線程?
多線程的核心在於多個代碼塊並發執行,本質特點在於各代碼塊之間的代碼是亂序執行的。我們的程序是否需要多線程,就是要看這是否也是它的內在特點。
假如我們的程序根本不要求多個代碼塊並發執行,那自然不需要使用多線程;假如我們的程序雖然要求多個代碼塊並發執行,但是卻不要求亂序,則我們完全可以用一個循環來簡單高效地實現,也不需要使用多線程;只有當它完全符合多線程的特點時,多線程機制對線程間通信和線程管理的強大支持才能有用武之地,這時使用多線程才是值得的。
#p#副標題#e#

2. java 程序中怎麼保證多線程的運行安全

並發編程三要素(線程的安全性問題體現在):

原子性:原子,即一個不可再被分割的顆粒。原子性指的是一個或多個操作要麼 全部執行成功要麼全部執行失敗。

可見性:一個線程對共享變數的修改,另一個線程能夠立刻看到。 (synchronized,volatile)

有序性:程序執行的順序按照代碼的先後順序執行。(處理器可能會對指令進行 重排序)

出現線程安全問題的原因:

3. Java Swing開發中的線程安全

SwingAPI的設計目標是強大 靈活和易用 非凡地 我們希望能讓程序員們方便地建立新的Swing組件 不論是從頭開始還是通過擴展我們所提供的一些組件 出於這個目的 我們不要求Swing組件支持多線程訪問 相反 我們向組件發送請求並在單一慎李線程中執行請求 本文討論線程和Swing組件 目的不僅是為了幫助你以線程安全的方式使用SwingAPI 而且解釋了我們為什麼會選擇現在這樣的線程方案 本文包括以下內容

單線程規則 Swing線程在同一時刻僅能被一個線程所訪問 一般來說 這個線程是事件派發線程 規則的例外 有些操作保證是線程安全的 事件分發 假如你需要從事件處理或繪制代碼以外的地方訪問UI 那麼你可以使用SwingUtilities類的invokeLater要求在事件派發線程中執行某些代碼 這個方法會立即返回 不會等待代碼執行完畢 invokeAndWait行為與invokeLater類似 除了這個方法會等待代碼執行完畢 一般地 你可以用invokeLater來代替這個方法 下面是一些使用這幾個API的例子 請同時參閱《TheJavaTutorial》中的 BINGOexample 尤其是以下幾個類 CardWindow ControlPane Player和OverallStatusPane

使用invokeLater方法你可以從任何線程調用invokeLater方法以請求事件派發線程運行特定代碼 你必須把要運行的代碼放到一個Runnable對象的run方法中 並將此Runnable對象設為invokeLater的參數 invokeLater方法會立即返回 不等待事件派發線程執行指定代碼 這是一個使用invokeLater方法的例子

RunnabledoWorkRunnable=newRunnable };

SwingUtilities invokeLater;使用invokeAndWait方法invokeAndWait方法和invokeLater方法很相似 除了invokeAndWait方法會等事件派發線程執行了指定代碼才返回 在可能的情況下 你應該盡量用invokeLater來代替invokeAndWait 假如你真的要使用invokeAndWait 請確保調用invokeAndWait的線程不會在調用期間持有任何其他線程可能需要的鎖

這是一個使用invokeAndWait的例子

}; 褲滑SwingUtilities invokeAndWait; }

類似地 假設一個線程需要對GUI的狀態進行存取 比如文本域的內容 它的代碼可能類似這樣

voidprintTextField throwsException }; SwingUtilities invokeAndWait; System out println;}

假如你能避免使用線程 最好這樣做 線程可能難於使用 並使得程序的debug更困難 一般來說 對於嚴格意義下的GUI工作 線程是不必要的 比如對組件屬性的更新 不管怎麼說 有時候線程是必要的 下列情況是使用線程的一些典型情況 執行一項費時的任務而不必將事件派發線程鎖定 例子包括執行大量計算的情況 會導致大量類被裝載的情況 和為網路或磁碟I/O而阻塞的情況 重復地執行一項操作 通常在兩次操作間間隔一個預定的時間周期 要等待來自客戶的消息 你可以使用兩個類來幫助你實現線程 SwingWorker 創建一個後台線程來執行費時的操作 Timer 創建一個線程來執行或多次執行某些代碼 在兩次執行間間隔用戶定義的延遲 使用SwingWorker類SwingWorker類在SwingWorker java中實現 這個類並不包寬純遲含在Java的任何發行版中 所以你必須單獨下載它 SwingWorker類做了所有實現一個後台線程所需的骯臟工作 雖然許多程序都不需要後台線程 後台線程在執行費時的操作時仍然是很有用的 它能提高程序的性能觀感

要使用SwingWorker類 你首先要實現它的一個子類 在子類中 你必須實現construct方法還包含你的長時間操作 當你實例化SwingWorker的子類時 SwingWorker創建一個線程但並不啟動它 你要調用你的SwingWorker對象的start方法來啟動線程 然後start方法會調用你的construct方法 當你需要construct方法返回的對象時 可以調用SwingWorker類的get方法 這是一個使用SwingWorker類的例子

//在main方法中 finalSwingWorkerworker=newSwingWorker }; worker start; //在動作事件處理方法中 JOptionPane showMessageDialog)

當程序的main方法調用start方法 SwingWorker啟動一個新的線程來實例化ExpensiveDialogComponent main方法還構造了由一個窗口和一個按鈕組成的GUI 當用戶點擊按鈕 程序將阻塞 假如必要 阻塞到ExpensiveDialogComponent創建完成 然後程序顯示一個包含ExpensiveDialogComponent的模式對話框 你可以在MyApplication java找到整個程序 使用Timer類Timer類通過一個ActionListener來執行或多次執行一項操作 你創建定時器的時候可以指定操作執行的頻率 並且你可以指定定時器的動作事件的監聽者 啟動定時器後 動作監聽者的actionPerformed方法會被調用來執行操作 定時器動作監聽者定義的actionPerformed方法將在事件派發線程中調用 這意味著你不必在其中使用invokeLater方法 這是一個使用Timer類來實現動畫循環的例子

publicvoidstartAnimationelse } publicvoidstopAnimation publicvoidactionPerformed }

在一個線程中執行所有的用戶界面代碼有這樣一些優點 組件開發者不必對線程編程有深入的理解 像ViewPoint和Trestle這類工具包中的所有組件都必須完全支持多線程訪問 使得擴展非常困難 尤其對不精通線程編程的開發者來說 最近的一些工具包如SubArctic和IFC 都採用和Swing類似的設計 事件以可預知的次序派發 invokeLater排隊的runnable對象從滑鼠和鍵盤事件 定時器事件 繪制請求的同一個隊列派發 在一些組件完全支持多線程訪問的工具包中 組件的改變被變化無常的線程調度程序穿插到事件處理過程中 這使得全面測試變得困難甚至不可能 更低的代價 嘗試小心鎖住臨界區的工具包要花費實足的時間和空間在鎖的治理上 每當工具包中調用某個可能在客戶代碼中實現的方法時 工具包都要保存它的狀態並釋放所有鎖 以便客戶代碼能在必要時獲得鎖 當控制權交回到工具包 工具包又必須重新抓住它的鎖並恢復狀態 所有應用程序都不得不負擔這一代價 即使大多數應用程序並不需要對GUI的並發訪問 這是的SubArcticJavaToolkit的對在工具包中支持多線程訪問的問題的描述 我們的基本信條是 當設計和建造多線程應用程序 尤其是那些包括GUI組件的應用程序時 必須保證極端小心 線程的使用可能會很有欺騙性 在許多情況下 它們表現得能夠極好的簡化編成 使得設計 專注於單一任務的簡單自治實體 成為可能 在一些情況下它們的確簡化了設計和編碼 然而 在幾乎所有的情況下 它們都使得調試 測試和維護的困難大大增加甚至成為不可能 無論大多數程序員所受的練習 他們的經驗和實踐 還是我們用來幫助自己的工具 都不是能夠用來對付非決定論的 例如 全面測試在bug依靠於時間時是幾乎不可能的 尤其對於Java來說 一個程序要運行在許多不同類型的機器的操作系統平台上 並且每個程序都必須在搶先和非搶先式調度下都能正常工作 由於這些固有的困難 我們力勸你三思是否絕對有使用線程的必要 盡管如此 有些情況下使用線程是必要的 所以subArctic提供了一個線程安全的訪問機制

lishixin/Article/program/Java/gj/201311/27616

閱讀全文

與java多線程安全相關的資料

熱點內容
newfile命令快捷鍵 瀏覽:567
阿里雲物理伺服器 瀏覽:953
靈狐視頻app哪個好 瀏覽:257
大廠退役程序員自述 瀏覽:252
linux命令watch 瀏覽:889
加密幣哪些平台不撤出中國 瀏覽:553
max加線命令 瀏覽:424
app胖瘦模式哪個好用 瀏覽:724
可以下載源碼的軟體 瀏覽:487
程序員寫一天代碼累嗎 瀏覽:628
ie文件夾禁止訪問 瀏覽:543
百川互聯網程序員 瀏覽:783
linuxpython解釋器 瀏覽:667
興安得力軟體加密狗 瀏覽:494
智能網路攝像頭加密 瀏覽:574
軟體畢業程序員培訓 瀏覽:652
安卓陀螺儀低怎麼辦 瀏覽:247
一級建造師復習題集pdf 瀏覽:904
法理學pdf海默 瀏覽:394
伺服器內存儲器是用什麼的 瀏覽:819