Java 線程池的原理與實現


 

JAVA線程池原理以及幾種線程池類型介紹

文章分類:Java編程


    在什么情況下使用線程池?

    1.單個任務處理的時間比較短
    2.將需處理的任務的數量大

    使用線程池的好處:

    1.減少在創建和銷毀線程上所花的時間以及系統資源的開銷
    2.如不使用線程池,有可能造成系統創建大量線程而導致消耗完系統內存以及”過度切換”。

線程池工作原理:http://www.ibm.com/developerworks/cn/java/j-jtp0730/
該文章里有個例子,簡單的描述了線程池的內部實現,建議根據里面的例子來了解JAVA 線程池的原理。同時,里面還詳細描述了使用線程池存在的優點和弊端,大家可以研究下,我覺得是篇非常好的文章。

    JDK自帶線程池總類介紹介紹:

    1、newFixedThreadPool創建一個指定工作線程數量的線程池。每當提交一個任務就創建一個工作線程,如果工作線程數量達到線程池初始的最大數,則將提交的任務存入到池隊列中。

    2、newCachedThreadPool創建一個可緩存的線程池。這種類型的線程池特點是:
    1).工作線程的創建數量幾乎沒有限制(其實也有限制的,數目為Interger. MAX_VALUE), 這樣可靈活的往線程池中添加線程。
    2).如果長時間沒有往線程池中提交任務,即如果工作線程空閑了指定的時間(默認為1分鍾),則該工作線程將自動終止。終止后,如果你又提交了新的任務,則線程池重新創建一個工作線程。

    3、newSingleThreadExecutor創建一個單線程化的Executor,即只創建唯一的工作者線程來執行任務,如果這個線程異常結束,會有另一個取代它,保證順序執行(我覺得這點是它的特色)。單工作線程最大的特點是可保證順序地執行各個任務,並且在任意給定的時間不會有多個線程是活動的 。

    4、newScheduleThreadPool創建一個定長的線程池,而且支持定時的以及周期性的任務執行,類似於Timer。(這種線程池原理暫還沒完全了解透徹)

    總結: 一.FixedThreadPool是一個典型且優秀的線程池,它具有線程池提高程序效率和節省創建線程時所耗的開銷的優點。但是,在線程池空閑時,即線程池中沒有可運行任務時,它不會釋放工作線程,還會占用一定的系統資源。

        二.CachedThreadPool的特點就是在線程池空閑時,即線程池中沒有可運行任務時,它會釋放工作線程,從而釋放工作線程所占用的資源。但是,但當出現新任務時,又要創建一新的工作線程,又要一定的系統開銷。並且,在使用CachedThreadPool時,一定要注意控制任務的數量,否則,由於大量線程同時運行,很有會造成系統癱瘓。

        三.就是向各位請教一下,請問各位使用過SingleThreadExecutor嗎?它一般使用在哪些地方?

    剛研究了一下線程池,以上為個人學習過程以及觀點,請勿見笑!下章,將分析一下JDK自帶線程池(ThreadPoolExecutor.java)源碼,如果感興趣,可看看。順便幫忙踩踩,呵呵!

java線程池原理及簡單實例

 

建議:在閱讀本文前,先理一理同步的知識,特別是syncronized同步關鍵字的用法。
關於我對同步的認識,要緣於大三年的一本書,書名好像是 Java 實戰,這本書寫得實在太妙了,真正的從理論到實踐,從截圖分析到.class字節碼分析。哇,我想市場上很難買到這么精致的書了。作為一個Java愛好者,我覺得絕對值得一讀。
我對此書印象最深之一的就是:equal()方法,由淺入深,經典!
還有就是同步了,其中提到了我的幾個編程誤區,以前如何使用同步提高性能等等,通過學習,使我對同步的認識進一步加深。


簡單介紹

    創建線程有兩種方式:繼承Thread或實現Runnable。Thread實現了Runnable接口,提供了一個空的run()方法,所以不論是繼承Thread還是實現Runnable,都要有自己的run()方法。
    一個線程創建后就存在,調用start()方法就開始運行(執行run()方法),調用wait進入等待或調用sleep進入休眠期,順利運行完畢或休眠被中斷或運行過程中出現異常而退出。

wait和sleep比較:
      sleep方法有:sleep(long millis),sleep(long millis, long nanos),調用sleep方法后,當前線程進入休眠期,暫停執行,但該線程繼續擁有監視資源的所有權。到達休眠時間后線程將繼續執行,直到完成。若在休眠期另一線程中斷該線程,則該線程退出。
      wait方法有:wait(),wait(long timeout),wait(long timeout, long nanos),調用wait方法后,該線程放棄監視資源的所有權進入等待狀態;
      wait():等待有其它的線程調用notify()或notifyAll()進入調度狀態,與其它線程共同爭奪監視。wait()相當於wait(0),wait(0, 0)。
      wait(long timeout):當其它線程調用notify()或notifyAll(),或時間到達timeout亳秒,或有其它某線程中斷該線程,則該線程進入調度狀態。
      wait(long timeout, long nanos):相當於wait(1000000*timeout + nanos),只不過時間單位為納秒。



線程池:
    多線程技術主要解決處理器單元內多個線程執行的問題,它可以顯著減少處理器單元的閑置時間,增加處理器單元的吞吐能力。
    
    假設一個服務器完成一項任務所需時間為:T1 創建線程時間,T2 在線程中執行任務的時間,T3 銷毀線程時間。
    
    如果:T1 + T3 遠大於 T2,則可以采用線程池,以提高服務器性能。
                一個線程池包括以下四個基本組成部分:
                1、線程池管理器(ThreadPool):用於創建並管理線程池,包括 創建線程池,銷毀線程池,添加新任務;
                2、工作線程(PoolWorker):線程池中線程,在沒有任務時處於等待狀態,可以循環的執行任務;
                3、任務接口(Task):每個任務必須實現的接口,以供工作線程調度任務的執行,它主要規定了任務的入口,任務執行完后的收尾工作,任務的執行狀態等;
                4、任務隊列(taskQueue):用於存放沒有處理的任務。提供一種緩沖機制。
                
    線程池技術正是關注如何縮短或調整T1,T3時間的技術,從而提高服務器程序性能的。它把T1,T3分別安排在服務器程序的啟動和結束的時間段或者一些空閑的時間段,這樣在服務器程序處理客戶請求時,不會有T1,T3的開銷了。

    線程池不僅調整T1,T3產生的時間段,而且它還顯著減少了創建線程的數目,看一個例子:

    假設一個服務器一天要處理50000個請求,並且每個請求需要一個單獨的線程完成。在線程池中,線程數一般是固定的,所以產生線程總數不會超過線程池中線程的數目,而如果服務器不利用線程池來處理這些請求則線程總數為50000。一般線程池大小是遠小於50000。所以利用線程池的服務器程序不會為了創建50000而在處理請求時浪費時間,從而提高效率。

 

在現代的操作系統中,有一個很重要的概念――線程,幾乎所有目前流行的操作系統都支持線程,線程來源於操作系統中進程的概念,進程有自己的虛擬地址空間以及正文段、數據段及堆棧,而且各自占有不同的系統資源(例如文件、環境變量等等)。與此不同,線程不能單獨存在,它依附於進程,只能由進程派生。如果一個進程派生出了兩個線程,那這兩個線程共享此進程的全局變量和代碼段,但每個線程各擁有各自的堆棧,因此它們擁有各自的局部變量,線程在UNIX系統中還被進一步分為用戶級線程(由進程自已來管理)和系統級線程(由操作系統的調度程序來管理)。

  既然有了進程,為什么還要提出線程的概念呢?因為與創建一個新的進程相比,創建一個線程將會耗費小得多的系統資源,對於一些小型的應用,可能感覺不到這點,但對於那些並發進程數特別多的應用,使用線程會比使用進程獲得更好的性能,從而降低操作系統的負擔。另外,線程共享創建它的進程的全局變量,因此線程間的通訊編程會更將簡單,完全可以拋棄傳統的進程間通訊的IPC編程,而采用共享全局變量來進行線程間通訊。

  有了上面這個概念,我們下面就進入正題,來看一下線程池究竟是怎么一回事?其實線程池的原理很簡單,類似於操作系統中的緩沖區的概念,它的流程如下:先啟動若干數量的線程,並讓這些線程都處於睡眠狀態,當客戶端有一個新請求時,就會喚醒線程池中的某一個睡眠線程,讓它來處理客戶端的這個請求,當處理完這個請求后,線程又處於睡眠狀態。可能你也許會問:為什么要搞得這么麻煩,如果每當客戶端有新的請求時,我就創建一個新的線程不就完了?這也許是個不錯的方法,因為它能使得你編寫代碼相對容易一些,但你卻忽略了一個重要的問題――性能!就拿我所在的單位來說,我的單位是一個省級數據大集中的銀行網絡中心,高峰期每秒的客戶端請求並發數超過100,如果為每個客戶端請求創建一個新線程的話,那耗費的CPU時間和內存將是驚人的,如果采用一個擁有 200個線程的線程池,那將會節約大量的的系統資源,使得更多的CPU時間和內存用來處理實際的商業應用,而不是頻繁的線程創建與銷毀。

  既然一切都明白了,那我們就開始着手實現一個真正的線程池吧,線程編程可以有多種語言來實現,例如C、C++、java等等,但不同的操作系統提供不同的線程API接口,為了讓你能更明白線程池的原理而避免陷入煩瑣的API調用之中,我采用了JAVA語言來實現它,由於JAVA語言是一種跨平台的語言,因此你不必為使用不同的操作系統而無法編譯運行本程序而苦惱,只要你安裝了JDK1.2以上的版本,都能正確地編譯運行本程序。另外JAVA語言本身就內置了線程對象,而且JAVA語言是完全面像對象的,因此能夠讓你更清晰地了解線程池的原理,如果你注意看一下本文的標題,你會發現整個示例程序的代碼只有大約100行。

  本示例程序由三個類構成,第一個是TestThreadPool類,它是一個測試程序,用來模擬客戶端的請求,當你運行它時,系統首先會顯示線程池的初始化信息,然后提示你從鍵盤上輸入字符串,並按下回車鍵,這時你會發現屏幕上顯示信息,告訴你某個線程正在處理你的請求,如果你快速地輸入一行行字符串,那么你會發現線程池中不斷有線程被喚醒,來處理你的請求,在本例中,我創建了一個擁有10個線程的線程池,如果線程池中沒有可用線程了,系統會提示你相應的警告信息,但如果你稍等片刻,那你會發現屏幕上會陸陸續續提示有線程進入了睡眠狀態,這時你又可以發送新的請求了。

  第二個類是ThreadPoolManager類,顧名思義,它是一個用於管理線程池的類,它的主要職責是初始化線程池,並為客戶端的請求分配不同的線程來進行處理,如果線程池滿了,它會對你發出警告信息。

  最后一個類是SimpleThread類,它是Thread類的一個子類,它才真正對客戶端的請求進行處理,SimpleThread在示例程序初始化時都處於睡眠狀態,但如果它接受到了ThreadPoolManager類發過來的調度信息,則會將自己喚醒,並對請求進行處理。
    首先我們來看一下TestThreadPool類的源碼:
 

package com.thread.simple;

import java.io.*;

public class TestThreadPool {

    /**

     * @param args

     */

    public static void main(String[] args) {
        try
        {
            BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
            String s;
            ThreadPoolManager manager = new ThreadPoolManager(10);          

            while((s = br.readLine()) != null)
            {
              manager.process(s);
            }
        }
        catch(IOException e){
        }
    }
}

由於此測試程序用到了輸入輸入類,因此第1行導入了JAVA的基本IO處理包,在第11行中,我們創建了一個名為manager的類,它給 ThreadPoolManager類的構造函數傳遞了一個值為10的參數,告訴ThreadPoolManager類:我要一個有10個線程的池,給我創建一個吧!第12行至15行是一個無限循環,它用來等待用戶的鍵入,並將鍵入的字符串保存在s變量中,並調用ThreadPoolManager類的 process方法來將這個請求進行處理。

  下面我們再進一步跟蹤到ThreadPoolManager類中去,以下是它的源代碼:
 

package com.thread.simple;
 
import java.util.*;
 
public class ThreadPoolManager {

    private int maxThread;

    public Vector vector;
  
    public void setMaxThread(int threadCount)
    {
        this.maxThread = threadCount;      
    }
  
    public ThreadPoolManager(int threadCount)
    {
        this.setMaxThread(threadCount);
      
        System.out.println("Starting thread pool...");
      
        vector = new Vector();

        for(int i=1;i<=10;i++)
        {
            SimpleThread thread = new SimpleThread(i);

            vector.addElement(thread);

            thread.start();
        }
    }
  
    public void process(String argument)
    {
        int i;
        for(i = 0;i<vector.size();i++)
        {
            SimpleThread currentThread = (SimpleThread)vector.elementAt(i);          

            if(!currentThread.isRunning())
            {
                System.out.println("Thread "+(i+1)+" is processing:"+argument);
                currentThread.setArgument(argument);
                currentThread.setRunning(true);
                return;
            }
        }      

        if(i == vector.size())
        {
            System.out.println("pool is full,try in another time.");
        }       
    }
}

我們先關注一下這個類的構造函數,然后再看它的process()方法。第16-24行是它的構造函數,首先它給ThreadPoolManager類的成員變量maxThread賦值,maxThread表示用於控制線程池中最大線程的數量。第18行初始化一個數組vector,它用來存放所有的 SimpleThread類,這時候就充分體現了JAVA語言的優越性與藝術性:如果你用C語言的話,至少要寫100行以上的代碼來完成vector的功能,而且C語言數組只能容納類型統一的基本數據類型,無法容納對象。好了,閑話少說,第19-24行的循環完成這樣一個功能:先創建一個新的 SimpleThread類,然后將它放入vector中去,最后用thread.start()來啟動這個線程,為什么要用start()方法來啟動線程呢?因為這是JAVA語言中所規定的,如果你不用的話,那這些線程將永遠得不到激活,從而導致本示例程序根本無法運行。

  下面我們再來看一下process()方法,第30-40行的循環依次從vector數組中選取SimpleThread線程,並檢查它是否處於激活狀態(所謂激活狀態是指此線程是否正在處理客戶端的請求),如果處於激活狀態的話,那繼續查找vector數組的下一項,如果vector數組中所有的線程都處於激活狀態的話,那它會打印出一條信息,提示用戶稍候再試。相反如果找到了一個睡眠線程的話,那第35-38行會對此進行處理,它先告訴客戶端是哪一個線程來處理這個請求,然后將客戶端的請求,即字符串argument轉發給SimpleThread類的setArgument()方法進行處理,並調用SimpleThread類的setRunning()方法來喚醒當前線程,來對客戶端請求進行處理。

  可能你還對setRunning()方法是怎樣喚醒線程的有些不明白,那我們現在就進入最后一個類:SimpleThread類,它的源代碼如下:

package com.thread.simple;

public class SimpleThread extends Thread {
    private boolean runningFlag;
    private String argument;
   
    public boolean isRunning()
    {
        return runningFlag;
    }
   
    public synchronized void setRunning(boolean flag)
    {
        runningFlag = flag;
       
        if(flag)
            this.notify();
    }
   
    public String getArgument()
    {
        return this.argument;
    }
   
    public void setArgument(String argument)
    {
        this.argument = argument;
    }
   
    public SimpleThread(int threadNumber)
    {
        runningFlag = false;
        System.out.println("Thread "+threadNumber+" started.");
    }
   
    public synchronized void run()
    {
        try
        {
            while(true)
            {
                if(!runningFlag)
                {
                    this.wait();
                }
                else
                {
                    System.out.println("processing "+getArgument()+"...done");
                    sleep(5000);
                    System.out.println("Thread is sleeping...");
                    setRunning(false);
                }
            }
        }
        catch(InterruptedException e)
        {
            System.out.println("Interrupt");
        }
    }//end of run()
   
}//end of class SimpleThread

  如果你對JAVA的線程編程有些不太明白的話,那我先在這里簡單地講解一下,JAVA有一個名為Thread的類,如果你要創建一個線程,則必須要從 Thread類中繼承,並且還要實現Thread類的run()接口,要激活一個線程,必須調用它的start()方法,start()方法會自動調用 run()接口,因此用戶必須在run()接口中寫入自己的應用處理邏輯。那么我們怎么來控制線程的睡眠與喚醒呢?其實很簡單,JAVA語言為所有的對象都內置了wait()和notify()方法,當一個線程調用wait()方法時,則線程進入睡眠狀態,就像停在了當前代碼上了,也不會繼續執行它以下的代碼了,當調用notify()方法時,則會從調用wait()方法的那行代碼繼續執行以下的代碼,這個過程有點像編譯器中的斷點調試的概念。以本程序為例,第38行調用了wait()方法,則這個線程就像凝固了一樣停在了38行上了,如果我們在第13行進行一個notify()調用的話,那線程會從第 38行上喚醒,繼續從第39行開始執行以下的代碼了。

  通過以上的講述,我們現在就不難理解SimpleThread類了,第9-14行通過設置一個標志runningFlag激活當前線程,第 25-29行是SimpleThread類的構造函數,它用來告訴客戶端啟動的是第幾號進程。第31-50行則是我實現的run()接口,它實際上是一個無限循環,在循環中首先判斷一下標志runningFlag,如果沒有runningFlag為false的話,那線程處理睡眠狀態,否則第42-45行會進行真正的處理:先打印用戶鍵入的字符串,然后睡眠5秒鍾,為什么要睡眠5秒鍾呢?如果你不加上這句代碼的話,由於計算機處理速度遠遠超過你的鍵盤輸入速度,因此你看到的總是第1號線程來處理你的請求,從而達不到演示效果。最后第45行調用setRunning()方法又將線程置於睡眠狀態,等待新請求的到來。

  最后還有一點要注意的是,如果你在一個方法中調用了wait()和notify()函數,那你一定要將此方法置為同步的,即 synchronized,否則在編譯時會報錯,並得到一個莫名其妙的消息:“current thread not owner”(當前線程不是擁有者)。

  至此為止,我們完整地實現了一個線程池,當然,這個線程池只是簡單地將客戶端輸入的字符串打印到了屏幕上,而沒有做任何處理,對於一個真正的企業級運用,本例還是遠遠不夠的,例如錯誤處理、線程的動態調整、性能優化、臨界區的處理、客戶端報文的定義等等都是值得考慮的問題,但本文的目的僅僅只是讓你了解線程池的概念以及它的簡單實現,如果你想成為這方面的高手,本文是遠遠不夠的,你應該參考一些更多的資料來深入地了解它。

創建Java程序中線程池詳解

 

線程是Java的一大特性,它可以是給定的指令序列、給定的方法中定義的變量或者一些共享數據(類一級的變量)。在Java中每個線程有自己的堆棧和程序計數器(PC),其中堆棧是用來跟蹤線程的上下文(上下文是當線程執行到某處時,當前的局部變量的值),而程序計數器則用來跟蹤當前線程正在執行的指令。

通常情況,一個線程不能訪問另外一個線程的堆棧變量,而且這個線程必須處於如下狀態之一:

1.排隊狀態(Ready),在用戶創建了一個線程以后,這個線程不會立即運行。當線程中的方法start()被調用時,這個線程就會進行排隊狀態,等待調度程序將它轉入運行狀態(Running)。當一個進程被執行后它也可以進行排隊狀態。如果調度程序允許的話,通過調用方法yield()就可以將進程放入排隊狀態。

2.運行狀態(Running),當調度程序將CPU的運行時間分配給一個線程,這個線程就進入了運行狀態開始運行。

3.等待狀態(Waiting),很多原因都可以導致線程處於等待狀態,例如線程執行過程中被暫停,或者是等待I/O請求的完成而進入等待狀態。

在Java中不同的線程具有不同的優先級,高優先級的線程可以安排在低優先級線程之前完成。如果多個線程具有相同的優先級,Java會在不同的線程之間切換運行。一個應用程序可以通過使用線程中的方法setPriority()來設置線程的優先級,使用方法getPriority()來獲得一個線程的優先級。

線程的生命周期

一個線程的的生命周期可以分成兩階段:生存(Alive)周期和死亡(Dead)周期,其中生存周期又包括運行狀態(Running)和等待狀態(Waiting)。當創建一個新線程后,這個線程就進入了排隊狀態(Ready),當線程中的方法start()被調用時,線程就進入生存周期,這時它的方法isAlive()始終返回真值,直至線程進入死亡狀態。

線程的實現

有兩種方法可以實現線程,一種是擴展java.lang.Thread類,另一種是通過java.lang.Runnable接口。世界經理人電子商務網訊

Thread類封裝了線程的行為。要創建一個線程,必須創建一個從Thread類擴展出的新類。由於在Thread類中方法run()沒有提供任何的操作,因此,在創建線程時用戶必須覆蓋方法run()來完成有用的工作。當線程中的方法start()被調用時,方法run()再被調用。下面的代碼就是通過擴展Thread類來實現線程:


import java.awt.*;
class Sample1{
public static void main(String[] args){
Mythread test1=new Mythread(1);
Mythread test2=new Mythread(2);
test1.start();
test2.start();
}
}
class Mythread extends Thread {
int id;
Mythread(int i)
{ id=i;}
public void run() {
int i=0;
while(id+i==1){
try {sleep(1000);
} catch(InterruptedException e) {}
}
System.out.println(“The id is ”+id);
}
 

通常當用戶希望一個類能運行在自己的線程中,同時也擴展其它某些類的特性時,就需要借助運行Runnable接口來實現。Runnable接口只有一個方法run()。不論什么時候創建了一個使用Runnable接口的類,都必須在類中編寫run()方法來覆蓋接口中的run()方法。例如下面的代碼就是通過Runnable接口實現的線程:世界經理人電子商務網訊


import java.awt.*;
import java.applet.Applet;
public class Bounce extends Applet implements Runnable{
static int r=30;
static int x=100;
static int y=30;
Thread t;
public void init()
{
t = new Thread(this);
t.start();
}
public void run()
{
int y1=+1;
int i=1;
int sleeptime=10;
while(true)
{
y+=(i*y);
if(y-rgetSize().height)
y1*=-1;
try{
t.sleep(sleeptime);
}catch(InterruptedException e){ }
}}
}
 

為什么要使用線程池

在Java中,如果每當一個請求到達就創建一個新線程,開銷是相當大的。在實際使用中,每個請求創建新線程的服務器在創建和銷毀線程上花費的時間和消耗的系統資源,甚至可能要比花在處理實際的用戶請求的時間和資源要多得多。除了創建和銷毀線程的開銷之外,活動的線程也需要消耗系統資源。如果在一個JVM里創建太多的線程,可能會導致系統由於過度消耗內存或“切換過度”而導致系統資源不足。為了防止資源不足,服務器應用程序需要一些辦法來限制任何給定時刻處理的請求數目,盡可能減少創建和銷毀線程的次數,特別是一些資源耗費比較大的線程的創建和銷毀,盡量利用已有對象來進行服務,這就是“池化資源”技術產生的原因。

線程池主要用來解決線程生命周期開銷問題和資源不足問題。通過對多個任務重用線程,線程創建的開銷就被分攤到了多個任務上了,而且由於在請求到達時線程已經存在,所以消除了線程創建所帶來的延遲。這樣,就可以立即為請求服務,使應用程序響應更快。另外,通過適當地調整線程池中的線程數目可以防止出現資源不足的情況。 世界經理人電子商務網訊

創建一個線程池

一個比較簡單的線程池至少應包含線程池管理器、工作線程、任務隊列、任務接口等部分。其中線程池管理器(ThreadPool Manager)的作用是創建、銷毀並管理線程池,將工作線程放入線程池中;工作線程是一個可以循環執行任務的線程,在沒有任務時進行等待;任務隊列的作用是提供一種緩沖機制,將沒有處理的任務放在任務隊列中;任務接口是每個任務必須實現的接口,主要用來規定任務的入口、任務執行完后的收尾工作、任務的執行狀態等,工作線程通過該接口調度任務的執行。下面的代碼實現了創建一個線程池,以及從線程池中取出線程的操作:



//create threads
synchronized(workThreadVector)
{
for(int j = 0; j < i; j )
{
threadNum ;
WorkThread workThread = new WorkThread(taskVector, threadNum);
workThreadVector.addElement(workThread);
}}

線程池適合應用的場合

當一個Web服務器接受到大量短小線程的請求時,使用線程池技術是非常合適的,它可以大大減少線程的創建和銷毀次數,提高服務器的工作效率。但如果線程要求的運行時間比較長,此時線程的運行時間比創建時間要長得多,單靠減少創建時間對系統效率的提高不明顯,此時就不適合應用線程池技術,需要借助其它的技術來提高服務器的服務效率。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM