Java網絡編程(三)ServerSocket用法淺學

第三章 ServerSoclet用法解析

前言

在客戶/服務器通信模式中,服務器需要創建監聽特定端口的ServerSocket,它負責接受客戶端的連接請求。本章的學習內容:

  • 1、ServerSocket的構造函數及其區別
  • 2、ServerSocket的成員函數的用法
  • 3、ServerSocket服務器是如何處理與多個客戶通信的問題——多線程
  • 4、java.util.concurrent包中線程池類的用法

3.1 ServerSocket的構造器

構造器 描述
ServerSocket()throws IOException; 無參構造器,不會分配端口綁定和IP綁定,使用需要綁定端口和IP地址
ServerSocket(int port)throws IOException; 綁定指定的端口號port
ServerSocket(int port,int backlog)throws IOException; 綁定指定的端口號,同時指定客戶請求連接隊列長度
ServerSocket(int port,int backlog,InetAddress bindAddr)throws IOException; 綁定指定端口,指定隊列長度,指定IP地址

3.1.1 端口綁定

從ServerSocket的構造器我們可以看出除了無參構造函數以外,其他的構造器都會和一個指定端口綁定。使用參數port指定。 例如:

//創建一個服務器,綁定到80端口
ServerSocket serverSocket = new ServerSocket(80);

創建過程中,如果無法綁定到該端口,則會拋出異常:IOException,更確切地說是拋出BindException,它是IOException的子類。拋出BindException一般是如下原因造成的:

  • 端口已經被其他的服務器進程使用
  • 某些操作系統中,如果沒有以管理員的身份運行服務器程序,不允許服務器綁定到0~1023之間的端口

匿名端口

將port設置爲0時,則表示由操作系統來給服務器隨機分配一個可用的端口。由操作系統分配的端口叫做匿名端口。對於多數服務器一般使用指定的端口,而不是匿名端口,方便客戶端連接。當然,某些場合下匿名端口有特殊用途。

3.1.2 如何設置客戶端的連接請求隊列長度

當服務器運行時,可能監聽到多個客戶的連接。例如:

Socket socket = new Socket("www.baidu.com",80)

每當一個這樣的客戶進程執行上面的代碼,就會與遠程服務器www.baidu.com的80端口連接,也就是說服務器監聽到了一個客戶連接。管理客戶連接請求的任務是由操作系統來完成的,操作系統將這些請求連接存放於一個先進先出隊列裏邊特別的,操作系統限定了連接隊列的長度,一般爲50。當隊列中的連接請求達到了上限,則此時服務器是無法接受新的客戶連接,進而只能拒絕新的連接請求。
我們知道使用accept()方法是從客戶連接請求隊列裏邊取出連接請求,來實現爲該客戶服務的

Socket socket = serverSocket.accept();

該方法可以將連接請求取出,從而給新的客戶連接騰出空位。
對於客戶端,如果發出的連接請求被加入了服務器的請求隊列,也就是連接建立成功。成功返回socket對象。如果連接被拒絕,則會拋出異常:ConnectionException。
設置請求隊列長度
使用ServerSocket構造方法的backlog參數。分三種情況:

  • 1、backlog值介於0到操作系統限定的上限,此時連接請求隊列長度=backlog的值
  • 2、backlog<=0或者backlog>操作系統上限,backlog無效,則連接請求隊列長度=操作系統的上限
  • 3、ServerSocket構造方法沒有設置backlog參數,連接請求隊列長度=操作系統上限

代碼演示
1、客戶端

package SocketProgram;

import java.io.IOException;
import java.net.Socket;

/**
 * 練習使用服務器的連接請求隊列長度backlog指定
 * 創建100次客戶連接,而服務器之監聽3個客戶。
 */

public class Chapter3_1_Client {
    public static void main(String[] ars) throws IOException, InterruptedException {
        final int length = 100;
        String host = "localhost";
        int port = 9090;
        Socket[]sockets = new Socket[length];
        for(int i=0;i<length;i++){
            sockets[i] = new Socket(host,port);//創建100次連接
            System.out.println("第"+(i+1)+"次連接成功!");
        }
        Thread.sleep(3000);//延時3s
        for(int i=0;i<length;i++){
            sockets[i].close();//關閉客戶端
        }
    }
}

2、服務器端

package SocketProgram;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class Chapter3_1_Server {
    private  int port = 9090;
    private ServerSocket serverSocket;
    public Chapter3_1_Server()throws IOException{
        serverSocket = new ServerSocket(port,3);//指定請求連接隊列長度
        System.out.println("服務器啓動...完成!");
    }
    public void server(){
        while(true){
            Socket socket = null;
            try{
                socket = serverSocket.accept();
                System.out.println("new connection accepted"+socket.getInetAddress()+
                        ":"+socket.getPort());
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try{
                    if(socket!=null)
                        socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[]ars) throws InterruptedException, IOException {
        Chapter3_1_Server server = new Chapter3_1_Server();
        Thread.sleep(60000);//睡眠60秒
        //server.server();
    }
}

先運行服務器,在運行客戶端,結果如下:
在這裏插入圖片描述
我們使用了backlog指定了3次連接,就只能三個客戶連接,其他的客戶會被拒絕而拋出異常。

現在去除服務器的main方法中的server.server();註釋,將Thread.sleep(60000)註釋。再運行:
在這裏插入圖片描述
在這裏插入圖片描述
可見accept()方法可以取出連接請求,從而爲新的客戶連接騰出空位。

3.1.3 設定綁定的IP地址

當主機只有一個IP地址,那麼默認情況下,服務器程序就會和這個IP地址綁定。但是當一個主機有多個IP地址時,我們需要顯式指定服務器程序的IP地址。比如一個主機有兩個網卡,一個網卡是連接到Internet,222.66.5.94,一個是連接到局域網192.168.3.4;如果我們的服務器僅僅被局域網的客戶端訪問,則應該使用第四個構造函數:

ServerSocket serverSocket = new ServerSocket(8000,10,InetAddress.getByName(192.168.3.4));//指定局域網的IP

3.1.4 默認構造函數的作用

默認構造函數沒有參數,通過該構造器創建的ServerSocket對象不與任何端口綁定。所以我們接下來還要使用bind()方法將服務器與端口綁定。這個構造器的作用是:運行服務器在綁定到特定的端口之前設置一些ServerSocket選項。因爲如果服務器綁定了特定端口,一些選項就無法改變。
如:

ServerSocket server = new ServerSocket();
server.setReuseAddress(true);//設置是否可以重用
server.bind(new InetSocketAddress(8000));//綁定端口。

此時設置的選項生效。但是如下代碼則不生效:

ServerSocket server = new ServerSocket(8000);
server.setReuseAddress(true);//不生效

3.2 接收和關閉客戶端的連接

1、接收客戶端使用accept():
在這裏插入圖片描述
2、關閉客戶端socket.close()
應該放於finally()塊裏邊,它保證了不管與客戶的通信是否是正常結束還是異常結束都關閉Socket,斷開與客戶的連接。

3.3 關閉ServerSocket

首先我們需要明確:
ServerSocket的close()方法使得服務器釋放佔用的端口,並且斷開和所有客戶連接。當一個服務器程序運行結束時,即使沒有執行close方法,操作系統也會釋放該服務器佔用的端口。因此服務器不一定需要在結束之前執行其close()方法

不同:
有些情況我們需要及時釋放服務器佔用的端口,以便使得其他程序能佔用該端口,則可以顯示調用close方法。

如何判斷ServerSocket已經關閉?
使用isClosed()方法判斷ServerSocket是否已經關閉,只有執行了ServerSocket的close()方法,isClosed()方法才返回true,否則即使ServerSocket還沒有和端口綁定,isClosed()方法也會返回false
如何判斷ServerSocket已經綁定端口
使用isBound()方法,可以判斷。只要ServerSocket已經和一個端口綁定了,則isBound()方法會返回true,即使它已經關閉
如何判斷ServerSocket已經綁定端口而且還沒有關閉

boolean isOpen = serverSocket.isBound()&&!serverSocket.isClosed();

3.4 獲取ServerSocket的信息

兩個get方法可以分別獲取其綁定的IP地址和端口:

  • 1、public InetAddress getInetAddress();返回綁定的IP地址對象
  • 2、public int getLocalPort();返回綁定的端口號,即使是匿名端口也能獲知。

匿名端口的應用場景
匿名端口一般適用於服務器和客戶端之間的臨時通信,通信結束就斷開連接。並且ServerSocket佔用的臨時端口也被釋放。如FTP(文件傳輸協議)就使用了匿名端口。FTP協議用於本地文件系統和遠程文件系統之間傳送文件:在這裏插入圖片描述
FTP使用了兩個TCP連接:

  • 控制連接:用於在客戶和服務器之間發送控制信息,如用戶名和口令、改變遠程目錄命令、上傳和下載文件的命令。TCP服務器在21端口監聽控制連接
  • 數據連接:用於傳輸文件

過程
1、TCP服務器在21端口上監聽到了用戶的需求,就另外創建一個數據連接,通過它來傳輸文件。
2、建立數據連接,其創建方式有兩種:

  • 方式一,創建的數據連接端口是指定的,如TCP服務器在20端口監聽數據連接,TCP客戶主動請求建立和該端口的連接。在這裏插入圖片描述
  • 方式二,先由TCP客戶創建一個監聽匿名端口的ServerSocket,然後,把這個匿名端口通過getLocalPort方法得到,然後發送給TCP服務器,接着TCP服務器主動請求建立和客戶端的連接。在這裏插入圖片描述
    以上的第二種方式就使用了匿名端口,而且是在客戶端使用,用於和服務器建立臨時數據連接。在實際應用中,服務器端也可以使用匿名端口。

3.5 ServerSocket選項

ServerSocket有如下三個選項:

  • 1、SO_TIMEOUT:表示等待客戶連接的超時時間。
  • 2、SO_RESUEADDR:表示是否允許重用端口服務器綁定的IP地址。
  • 3、SO_RCVBUF:表示接收數據的緩衝區大小。

3.5.1 SO_TIMEOUT選項

在這裏插入圖片描述

3.5.2 SO_RESUEADDR選項

在這裏插入圖片描述在這裏插入圖片描述

3.5.3 SO_RCVBUF選項

在這裏插入圖片描述

3.5.4 設定連接時間、延遲、帶寬的相對重要性

public void setPerformancePreferences(int connectionTime,int latency,int bandwidth)

該方法和Socket的setPerformancePreferences()方法作用一致,都是用於設定連接時間、延遲、帶寬的相對重要性。

3.6 創建多線程的服務器

3.6.1 單線程客戶端服務器通信的不足

我們服務器的服務方法如下:

public void server(){
        while(true){
            Socket socket = null;
            try{
                socket = serverSocket.accept();
                System.out.println("new connection accepted"+socket.getInetAddress()+
                        ":"+socket.getPort());
                        //+服務器的服務
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try{
                    if(socket!=null)
                        socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

服務器通過accept方法監聽客戶的連接請求,每次只能服務一個客戶,其他的客戶只能排隊等候。帶來了一個問題:無法同時給多個客戶服務,然後實際的應用需求就是要滿足多客戶服務
。例如HTTP服務器,就是最明顯的例子,任何時刻HTTP服務器都可能接收到大量客戶的請求,每個客戶都希望能快速得到服務器的響應,這個時候單線程毫無疑問無法解決我們的需求。

併發性
可以使用併發性來衡量一個服務器同時響應多個客戶的能力,一個具有好的併發性能服務器應該具備:

  • 同時能接收多個客戶的請求連接並處理請求
  • 對每個客戶的請求都能快速響應

同時處理的客戶數越多,並且對每個客戶響應速度越快,則併發性能越高。而提高服務器的併發性能就是我們需要解決的問題!

如何提高服務器的併發性能?
最常用的手段是多個線程同時爲客戶服務這裏介紹3種方式重新實現上面的服務器程序:

  • 1、爲每個客戶分配一個工作線程
  • 2、創建一個線程池,由其中的工作線程來爲客戶提供服務、
  • 3、利用JDK的java類庫現成的線程池,由它的工作線程來爲客戶服務。

3.6.2 爲每個客戶分配一個線程

這個比較容易:就是沒連接一個客戶,就創建一個線程用於運行對應的服務。
步驟
1、創建一個線程子類實現Runnable接口,然後將服務封裝爲線程任務,以3.1節中的服務器程序爲例:

class Handler implements Runnable{
	private Socket socket;
	public Handler(Socket socket){
		this.socket = socket;
	}
	public void run(){
	//+服務器的服務
	}
	

2、原服務端的server方法修改爲:

public void server(){
        while(true){
            Socket socket = null;
            try{
                socket = serverSocket.accept();
                new Thread(new Handler(socket)).start();//每個客戶都創建一個線程來處理其請求
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try{
                    if(socket!=null)
                        socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

3.6.3 創建線程池

每個客戶分配一個線程的不足之處
在上面的方式裏邊,爲每個客戶都分配一個新的工作線程。當工作線程和客戶通信完畢這個線程就會被銷燬。但是卻有如下的不足

  • 1、服務器創建和銷燬的工作線程開銷(包括所花費的時間和系統資源)很大!比如很多客戶和服務器連接,但是每個客戶的通信時間很短,那麼創建線程很多,並且一會兒通信就結束了。也就是創建線程開銷比與客戶通信的開銷大
  • 2、每個線程的創建都是需要內存的,客戶過多的時候,要是都分配一個線程(一個線程大約1MB),那麼就會大量消耗內存造成系統空間不足。
  • 3、如果線程的數目固定,且每個線程的生命週期都很長,那麼線程在CPU中切換相對固定,不同的系統切換週期不一,大約都在20ms。但是如果頻繁地創建線程,導致了線程的不穩定,CPU需要頻繁切換和銷燬線程,那麼這個時候,CPU處理線程之間的切換週期時間不再固定,進而可能導致切換線程的開銷比創建和銷燬線程的開銷還要大。

解決方案
線程池爲線程的生命週期開銷問題和系統資源不足問題提供瞭解決方案。線程池裏邊預先創建了一些工作線程,它們不斷地從工作隊列裏邊取出任務,執行任務。執行完一個就取一個再執行。

線程池的優點

  • 減少了創建和銷燬線程的次數,每個工作線程可以被一直重用,能執行多個任務。
  • 可以根據系統的負載能力,方便地調整線程池裏邊的線程數目,防止因爲消耗過量的系統資源導致系統崩潰。

ThreadGroup

public class ThreadGroup
extends Object
implements Thread.UncaughtExceptionHandler
/*一個線程組表示一組線程。此外,一個線程組還可以包括其他線程組。線程組形成一個樹,其中每一個線程組除了初始線程組有一個父。 
允許一個線程訪問它自己的線程組的信息,但不允許訪問它的線程組的父線程組或其他任何線程組的信息。

從以下版本開始: 
JDK1.0 */

該類表示線程組,提供了一系列操作和管理線程組中線程的方法。如:

  • interrupt方法相當於調用線程組中所有活着的線程的interrupt方法,對所有的工作線程進行中斷處理
    該方法可能造成一下的影響:1、如果一個工作線程正在執行wait方法而阻塞,則會拋出InterruptedException;2、如果此時一個工作線程正在執行任務而且這個任務不會被阻塞,那麼該線程會執行完這個任務再中斷。

自定義線程池類ThreadPool

package SocketProgram;

import java.util.LinkedList;

/**
 * 爲了解決每個客戶都要分配一個線程來服務所帶來的缺點,我們應該使用線程池對象。
 * public class ThreadGroup
 * extends Object
 * implements Thread.UncaughtExceptionHandler
 * 一個線程組表示一組線程。此外,一個線程組還可以包括其他線程組。線程組形成一個樹,其中每一個線程組除了初始線程組有一個父。
 * 允許一個線程訪問它自己的線程組的信息,但不允許訪問它的線程組的父線程組或其他任何線程組的信息。
 *
 * 從以下版本開始:
 * JDK1.0
 * 下面使用自定義線程池類 ThreadPool
 */

public class Chapter3_6_ThreadPool extends ThreadGroup {
    private boolean isClosed = false;//判斷線程池是否關閉
    private LinkedList<Runnable> workQueue;//表示工作隊列
    private static int threadPoolID;//表示線程池ID
    private static int threadID;//工作線程的ID號

    /**
     *
     * @param poolSize 指定線程池中工作線程的數量
     */
    public Chapter3_6_ThreadPool(int poolSize) {
        super("ThreadPool-"+(threadPoolID++));
        setDaemon(true);//守護線程組,後臺運行
        workQueue = new LinkedList<>();//創建工作隊列
        for(int i=0;i<poolSize;i++){
            new WorkThread().start();//創建並啓動工作線程,該WorkThread是一個內部類,線程子類
        }
    }

    /**
     * 向工作隊列加入一個新任務,由工作線程去執行該任務。
     * @param task 線程任務,Runnable的子類對象
     */
    public synchronized void execute(Runnable task){
        if(isClosed){//如果線程池已經關閉,則拋出異常
            throw new IllegalStateException();
        }
        if(task!=null){
            workQueue.add(task);
            notify();//喚醒正在getTask()方法裏邊等到的工作線程
        }
    }

    /**
     * 從工作隊列中取出一個任務,工作線程會調用該方法。
     * @return 返回執行的工作任務
     * @throws InterruptedException 中斷異常
     */
    protected synchronized Runnable getTask()throws InterruptedException{
        while(workQueue.size()==0){
            if(isClosed)return null;
            wait();//如果線程隊列沒有任務則等待任務
        }
        return workQueue.removeFirst();//執行任務同時工作隊列任務減一。
    }
    /**
     * 關閉線程池
     */
    public synchronized  void close(){
        if(!isClosed){
            isClosed = true;
            workQueue.clear();//清空工作隊列
            interrupt();//線程池終止
        }
    }
    /**
     * 等到工作線程將所有的任務執行完畢
     */
    public void join(){
        synchronized (this){
            isClosed = true;
            notifyAll();//喚醒所有還在getTask方法中等待的工作線程
        }
        Thread[]threads = new Thread[activeCount()];
        //enumerate方法繼承自ThreadGroup類,獲取線程組中當前存活的所有工作線程
        int count = enumerate(threads);
        for(int i=0;i<count;i++){//等待所有的工作線程結束
            try{
                threads[i].join();//等到工作線程結束
            }catch (InterruptedException ignored){}
        }
    }
    /**
     * 內部類:工作線程類
     */
    private class WorkThread extends Thread{
        public WorkThread(){
            //加入到當前的ThreadPool線程組中
            super(Chapter3_6_ThreadPool.this,"WorkThread-"+(threadID++));
        }
        public void run(){
            while(!isInterrupted()){
                Runnable task = null;
                try{
                    task = getTask();
                } catch (InterruptedException ignored) {}

                //如果getTask返回null或者執行時方法被中斷,則結束該線程
                if(task==null) return;
                try{
                    task.run();
                }catch (Throwable t){//Throwable是異常體系的頂層
                    t.printStackTrace();
                }
            }
        }

    }
}

解析:
1、workQueue
在ThreadPool類裏邊,創建了一個LinkedList類型的workQueue成員變量,裏邊的存放的類型都是封裝爲Runnable的任務。它表示工作隊列
2、execute方法
客戶程序只要調用execute方法就能向線程池提交任務,該方法先判斷線程池是否關閉,關閉則不再接收任務,否則就將任務添加到工作隊列。並喚醒正在等待任務的工作線程。
3、構造方法
該類的構造方法只有一個參數——poolSize,表示工作線程的數目。
4、WorkThread
一個內部類,繼承自Thread類,表示工作線程,用於從工作隊列裏邊執行任務,取一個執行一個,執行完再取,如此重複。
5、工作線程如何取任務?
由getTask方法實現,處理邏輯如下

  • 如果隊列爲空且線程池關閉,則返回null,表示沒有線程可以執行
  • 如果隊列爲空而線程池沒有關閉,則等待,直到其他線程將其喚醒或者中斷
  • 如果隊列中有任務,則按照先進先出的原則,取出並返回第一個任務

6、join方法和close方法
join方法用於確保在關閉線程池之前,工作線程能夠把工作隊列的任務都執行完。close()方法則清空隊列,並直接中斷所有的工作線程。

測試自定義的線程池類
我們使用了線程池類,創建了3個工作線程(poolSize=3),5個線程任務(numTask=5),下面演示3個線程如何處理5個任務的

package SocketProgram;

/**
 * 測試自定義線程池類ThreadPool
 */
public class Chapter3_6_ThreadPoolTest {
    public static void main(String[] args) {
        /*if(args.length!=2){
            System.out.println("用法:java ThreadPoolTest numTasks poolSize"+
                    "\n numTask-integer:任務的數目"+
                    "\n numThreads-integer:線程池中線程數目");
            return;
        }*/
        int numTasks = 5;//Integer.parseInt(args[0]);
        int poolSize = 3;//Integer.parseInt(args[1]);
        Chapter3_6_ThreadPool threadPool = new Chapter3_6_ThreadPool(poolSize);//創線程池
        //運行任務
        for(int i=0;i<numTasks;i++){
            threadPool.execute(createTask(i));
        }
        threadPool.join();//必須等所有的線程都完成主線程才能退出。
        //threadPool.close();//關閉線程池

    }

    private static Runnable createTask(final int taskID) {
        return () -> {//lambda表達式
            System.out.println("Task"+taskID+": start!"+"線程"+Thread.currentThread().getName());
            try{
                Thread.sleep(500);//增加執行一個任務的時間

            } catch (InterruptedException ignored){}
            System.out.println("Task"+taskID+": end..."+"線程"+Thread.currentThread().getName());
        };
    }
}

運行結果:在這裏插入圖片描述
可以觀察到,線程1執行了任務1、4,線程2執行了任務2,線程0執行了任務0和任務3.從而解決了每個客戶都分配一個線程來服務帶來的問題。

3.6.4 使用自定義線程池類實現客戶端和服務器的通信

package SocketProgram;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 將服務器的服務用線程池處理
 */

public class Chapter3_6_EchoServer_ThreadPool {
    private int port = 9000;
    private ServerSocket serverSocket;
    private Chapter3_6_ThreadPool threadPool;
    private final int POOL_SIZE = 4;//單CPU時線程池中工作線程的數目

    public Chapter3_6_EchoServer_ThreadPool()throws IOException{
        serverSocket = new ServerSocket(port);
        //創建線程池
        threadPool = new Chapter3_6_ThreadPool(Runtime.getRuntime().availableProcessors()*POOL_SIZE);
        //Runtime.getRuntime().availableProcesses返回當前系統的CPU數目,我們根據該數目創建工作線程數目
        System.out.println("服務器啓動!");
    }
    public void service(){
        while(true){
            Socket socket = null;
            try{
                socket = serverSocket.accept();
                //每來一個線程就執行一次execute方法判斷從而添加線程任務到工作隊列。
                threadPool.execute(new Chapter3_5_Handler(socket));//Handler是將服務器的任務處理封裝爲了對象
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }
    public static void main(String[]args) throws IOException {
        new Chapter3_6_EchoServer_ThreadPool().service();
    }
}

我這裏模擬3個客戶連接,第一個客戶連接上又退出,第二個客戶正常發送信息,第三個客戶是dos命令打開,因爲其是GBK編碼,這裏出現亂碼。第二個客戶第三個客戶都發送了bye,則服務器接受到了兩個bye。通信結束。
在這裏插入圖片描述

3.6.5 使用JDK類庫提供的線程池

java.util.concurrent包提供了現成的線程池實現,它相比上面的線程池實現更加健壯,而且功能更加強大!。體系結構:(圖片來自——點擊鏈接
在這裏插入圖片描述
java.util.concurrent.Executor : 負責線程的使用與調度的根接口

  • ExecutorService:Executor的子接口,線程池的主要接口
    • ThreadPoolExecutor:ExecutorService的實現類
    • ScheduledExecutorService:ExecutorService的子接口,負責線程的調度
      • ScheduledThreadPoolExecutor:繼承了ThreadPoolExecutor實現了ScheduledExecutorService

Executor接口表示線程池,它只有一個方法——execute(Runnable task),用來執行Runnable類的任務,子接口ExceutorService中聲明瞭一些管理線程池的方法,如關閉線程池方法shutdown()等。Executors類中包含一些靜態方法,負責生成各種類型的線程池ExecutorService實例。如圖:在這裏插入圖片描述
現在我們將利用上述的線程池類來實現服務器與客戶通信的任務
對比上面的代碼,只需要修改以下三個地方:

1private Chapter3_6_ThreadPool threadPool;

2//創建線程池
        threadPool = new Chapter3_6_ThreadPool(Runtime.getRuntime().availableProcessors()*POOL_SIZE);

3、 threadPool.execute(new Chapter3_5_Handler(socket));//Handler是將服務器的任務處理封裝爲了對象

修改爲:

1private ExecutorService executorService;
2、executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()*POOL_SIZE);
//創建了固定工作線程的數目的線程池

3、executorService.execute(new Chapter3_5_Handler(socket));//將任務提交給線程池執行

3.6.6 使用線程池的注意事項總結

雖然使用來線程池能提高服務器的併發性能,但也存在一定的風險:

在這裏插入圖片描述在這裏插入圖片描述
也就是大量線程消耗資源,容易導致系統資源不足在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述

所以我們使用線程池需注意:

在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述

3.7 關閉服務器

在服務器裏邊添加一個線程類,用於關閉線程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章