>  기사  >  Java  >  Java에서 다중 스레드 서버를 만드는 방법

Java에서 다중 스레드 서버를 만드는 방법

PHPz
PHPz앞으로
2023-05-10 15:58:141365검색

일반적인 단일 스레드 서버 예는 다음과 같습니다.

while (true) {
    Socket socket = null;
    try {
        // 接收客户连接
        socket = serverSocket.accept();
        // 从socket中获得输入流与输出流,与客户通信
        ...
    } catch(IOException e) {
        e.printStackTrace()
    } finally {
        try {
            if(socket != null) {
                // 断开连接
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

서버는 클라이언트 연결을 수신하고, 클라이언트와 통신하고, 통신이 완료된 후 연결을 끊고, 이를 요청하는 클라이언트 연결이 여러 개 있는 경우 다음 클라이언트 연결을 수신합니다. 동시에 고객은 줄을 서서 기다려야 합니다. 고객을 너무 오래 기다리게 하면 웹사이트의 신뢰도가 떨어지고 트래픽이 감소하게 됩니다.

동시성 성능은 일반적으로 여러 클라이언트에 동시에 응답하는 서버의 능력을 측정하는 데 사용됩니다. 동시성 성능이 좋은 서버는 다음 두 가지 조건을 충족해야 합니다.

  • 여러 클라이언트 연결을 동시에 수신하고 처리할 수 있어야 합니다.

  • 각 고객에 대해 빠르게 응답합니다

여러 스레드를 사용하여 여러 고객에게 동시에 서비스를 제공하는 것이 일반적으로 서버의 동시 성능을 향상시키는 가장 일반적인 방법입니다. 세 가지 방법:

  • 각 고객은 작업자 스레드를 할당합니다

  • 스레드 풀을 생성하면 그 안의 작업자 스레드가 고객에게 서비스를 제공합니다.

  • Java 클래스 라이브러리에 미리 만들어진 스레드 풀을 사용합니다. 작업자 스레드는 고객에게 서비스를 제공합니다

각 고객에게 스레드를 할당합니다

서버의 메인 스레드는 고객 연결을 수신할 때마다 이를 담당하는 작업자 스레드가 생성됩니다.

public class EchoServer {
    private int port = 8000;
    private ServerSocket serverSocket;
    public EchoServer() throws IOException {
        serverSocket = new ServerSocket(port);
        System.out.println("服务器启动");
    }
    public void service() {
        while(true) {
            Socket socket = null;
            try {
                // 接教客户连接
                socket = serverSocket.accept();
                // 创建一个工作线程
                Thread workThread = new Thread(new Handler(socket));
                // 启动工作线程
                workThread.start();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String args[])throws TOException {
        new EchoServer().service();
    }
    // 负责与单个客户的通信   
    class Handler implements Runnable {
        private Socket socket;
        pub1ic Handler(Socket socket) {
            this.socket = socket;
        }
        private PrintWriter getWriter(Socket socket) throws IOException {...}
        private BufferedReader getReader(Socket socket) throws IOException {...}
        public String echo(String msg) {...}
        public void run() {
            try {
                System.out.println("New connection accepted" + socket.getInetAddress() + ":" + socket.getPort());
                BufferedReader br = getReader(socket);
                PrintWriter pw = getWriter(socket);
                String msg = null;
                // 接收和发送数据,直到通信结束
                while ((msg = br.readLine()) != null) {
                    System.out.println("from "+ socket.getInetAddress() + ":" + socket.getPort() + ">" + msg);
                    pw.println(echo(msg));
                    if (msg.equals("bye")) break;
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    // 断开连接
                    if(socket != nulll) socket.close();
                } catch (IOException e) {
                    e,printStackTrace();
                }
            }
        }
    }
}

스레드 풀 만들기

이전 이 구현에는 다음과 같은 단점이 있습니다.

  • 서버가 많은 클라이언트와 통신해야 하는 경우 서버가 높은 오버헤드로 작업자 스레드를 생성하고 파괴합니다. 각 클라이언트와의 통신 시간이 짧다면 서버가 클라이언트일 가능성이 있습니다. 새 스레드를 생성하는 오버헤드가 실제로 클라이언트와 통신하는 오버헤드보다 큽니다

  • 스레드를 생성하고 파괴하는 오버헤드 외에도, 활성 스레드도 시스템 리소스를 소비합니다. 각 스레드는 일정량의 메모리를 차지합니다. 동시에 많은 수의 클라이언트가 서버에 연결되면 많은 수의 작업자 스레드가 생성되어야 하며 많은 양의 메모리를 소비하므로 메모리 공간이 부족할 수 있습니다. 일부 작업은 스레드 풀에 미리 생성되어 작업 대기열에서 지속적으로 작업을 가져온 다음 작업을 실행합니다. 작업자 스레드가 작업 실행을 마치면 작업 대기열의 다음 작업을 계속 실행합니다.

  • 스레드 풀에는 다음과 같은 장점이 있습니다.

생성 및 소멸되는 스레드 수를 줄이고 각 작업자 스레드는 항상 재사용 가능, 다중 작업 수행 가능

  • 스레드 풀의 스레드 수는 시스템의 수용 능력에 따라 쉽게 조정될 수 있으므로 과도한 시스템 자원 소비로 인한 시스템 충돌을 방지할 수 있습니다

  • public class ThreadPool extends ThreadGroup {
        // 线程池是否关闭
        private boolean isClosed = false;
        // 表示工作队列
        private LinkedList<Runnable> workQueue;
        // 表示线程池ID
        private static int threadPoolID;
        // 表示工作线程ID
        // poolSize 指定线程池中的工作线程数目
        public ThreadPool(int poolSize) {
            super("ThreadPool-"+ (threadPoolID++));
            setDaemon(true);
            // 创建工作队列
            workQueue = new LinkedList<Runnable>();
            for (int i = 0; i < poolSize; i++) {
                // 创建并启动工作线程
                new WorkThread().start(); 
            }
        }
        /**
         * 向工作队列中加入一个新任务,由工作线程去执行任务
         */
        public synchronized void execute(Runnable tank) {
            // 线程池被关则抛出IllegalStateException异常
            if(isClosed) {
                throw new IllegalStateException();
            }
            if(task != null) {
                workQueue.add(task);
                // 唤醒正在getTask()方法中等待任务的工作线限
                notify();
            }
        }
        /**
         * 从工作队列中取出一个任务,工作线程会调用此方法
         */
        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();
                // 中断所有的工作线程,该方法继承自ThreadGroup类
                interrupt();
            }
        }
        /**
         * 等待工作线程把所有任务执行完
         */
        public void join() {
            synchronized (this) {
                isClosed = true;
                // 唤醒还在getTask()方法中等待任务的工作线程
                notifyAll();
            }
            Thread[] threads = new Thread[activeCount()];
            // enumerate()方法继承自ThreadGroup类获得线程组中当前所有活着的工作线程
            int count = enumerate(threads);
            // 等待所有工作线程运行结束
            for(int i = 0; i < count; i++) {
                try {
                    // 等待工作线程运行结束
                    threads[i].join();
                } catch((InterruptedException ex) {}
            }
        }
        /**
         * 内部类:工作线程
         */
        private class WorkThread extends Thread {
            public WorkThread() {
                // 加入当前 ThreadPool 线程组
                super(ThreadPool.this, "WorkThread-" + (threadID++));
            }
            public void run() {
                // isInterrupted()方法承自Thread类,判断线程是否被中断
                while (!isInterrupted()) {
                    Runnable task = null;
                    try {
                        // 取出任务
                        task = getTask();
                    } catch(InterruptedException ex) {}
                    // 如果 getTask() 返回 nu11 或者线程执行 getTask() 时被中断,则结束此线程
                    if(task != null) return;
                    // 运行任务,异常在catch代码块中被捕获
                    try {
                        task.run();
                    } catch(Throwable t) {
                        t.printStackTrace();
                    }
                }
            }
        }
    }

    스레드를 사용하여 구현된 서버 pool은 다음과 같습니다:

    publlc class EchoServer {
        private int port = 8000;
        private ServerSocket serverSocket;
        private ThreadPool threadPool;	// 线程港
        private final int POOL_SIZE = 4;	// 单个CPU时线程池中工作线程的数目
        public EchoServer() throws IOException {
            serverSocket = new ServerSocket(port);
            // 创建线程池
            // Runtime 的 availableProcessors() 方法返回当前系统的CPU的数目
            // 系统的CPU越多,线程池中工作线程的数目也越多
            threadPool= new ThreadPool(
            	Runtime.getRuntime().availableProcessors() * POOL_SIZE);
            System.out.println("服务器启动");
        }
        public void service() {
            while (true) {
                Socket socket = null;
                try {
                    socket = serverSocket.accept();
                    // 把与客户通信的任务交给线程池
                    threadPool.execute(new Handler(socket));
                } catch(IOException e) {
                    e.printStackTrace();
                }
            }
        }
        public static void main(String args[])throws TOException {
            new EchoServer().service();
        }
        // 负责与单个客户的通信,与上例类似
        class Handler implements Runnable {...}
    }
  • Java에서 제공하는 스레드 풀 사용

java.util.concurrent 패키지는 더 강력하고 강력한 미리 만들어진 스레드 풀 구현을 제공합니다. 스레드 풀에 대한 자세한 내용은 이 문서를 참조하세요.

public class Echoserver {
    private int port = 8000;
    private ServerSocket serverSocket;
    // 线程池
    private ExecutorService executorService;
    // 单个CPU时线程池中工作线程的数目
    private final int POOL_SIZE = 4;
    public EchoServer() throws IOException {
        serverSocket = new ServerSocket(port);
        // 创建线程池
        // Runtime 的 availableProcessors() 方法返回当前系统的CPU的数目
        // 系统的CPU越多,线程池中工作线程的数目也越多
        executorService = Executors.newFixedThreadPool(
        	Runtime.getRuntime().availableProcessors() * POOL_SIZE);
        System.out.println("服务器启动");
    }
    public void service() {
        while(true) {
            Socket socket = null;
            try {
                socket = serverSocket.accept();
                executorService.execute(new Handler(socket));
            } catch(IOException e) {
                e.printStackTrace();
            }
        }
    }
     public static void main(String args[])throws TOException {
        new EchoServer().service();
    }
    // 负责与单个客户的通信,与上例类似
    class Handler implements Runnable {...}
}

스레드 풀 사용에 대한 참고 사항

스레드 풀은 서버의 동시성 성능을 크게 향상시킬 수 있지만 사용 시 특정 위험이 있으며 이로 인해 쉽게 다음과 같은 문제가 발생할 수 있습니다. java.util.concurrent 包提供了现成的线程池的实现,更加健壮,功能也更强大,更多关于线程池的介绍可以这篇文章

rrreee

使用线程池的注意事项

虽然线程池能大大提高服务器的并发性能,但使用它也存在一定风险,容易引发下面的问题:

  • 死锁

任何多线程应用程序都有死锁风险。造成死锁的最简单的情形是:线程 A 持有对象 X 的锁,并且在等待对象 Y 的锁,而线程 B 持有对象 Y 的锁,并且在等待对象 X 的锁,线程 A 与线程 B 都不释放自己持有的锁,并且等待对方的锁,这就导致两个线程永远等待下去,死锁就这样产生了

任何多线程程序都有死锁的风险,但线程池还会导致另外一种死锁:假定线程池中的所有工作线程都在执行各自任务时被阻塞,它们都在等待某个任务 A 的执行结果。而任务 A 依然在工作队列中,由于没有空闲线程,使得任务 A 一直不能被执行。这使得线程池中的所有工作线程都永远阻塞下去,死锁就这样产生了

  • 系统资源不足

如果线程池中的线程数目非常多,这些线程就会消耗包括内存和其他系统资源在内的大量资源,从而严重影响系统性能

  • 并发错误

线程池的工作队列依靠 wait() 和 notify()

  • deadlock

모든 멀티 스레드 애플리케이션 모든 프로그램이 교착 상태에 빠질 위험이 있습니다. 교착 상태를 일으키는 가장 간단한 상황은 다음과 같습니다. 스레드 A는 개체 X의 잠금을 보유하고 개체 Y의 잠금을 기다리고 있는 반면, 스레드 B는 개체 Y의 잠금을 보유하고 개체 X의 잠금을 기다리고 있습니다. 스레드 A와 스레드 B 둘 다 보유하고 있는 잠금을 해제하지 않고 상대방의 잠금을 기다리지 않습니다. 이로 인해 두 스레드가 영원히 대기하게 되고 모든 다중 스레드 프로그램에는 교착 상태가 발생할 위험이 있지만 스레드 풀은 또 다른 종류의 교착 상태도 발생시킵니다. 교착 상태: 스레드 풀의 모든 작업자 스레드가 해당 작업을 실행할 때 차단되고 모두 특정 작업 A의 실행 결과를 기다리고 있다고 가정합니다. 작업 A는 여전히 작업 대기열에 있습니다. 유휴 스레드가 없으므로 작업 A를 실행할 수 없습니다. 이로 인해 스레드 풀의 모든 작업자 스레드가 영구적으로 차단되고 교착 상태가 발생합니다. 스레드 풀의 스레드 수가 너무 많으면 이러한 스레드가 메모리 및 기타 리소스를 많이 소비하게 됩니다. 시스템 리소스를 포함하여 시스템 성능에 심각한 영향을 미칩니다

🎜🎜동시성 오류🎜🎜🎜🎜스레드 풀의 작업 대기열은 wait()notify() 메서드를 사용하여 작업자 스레드가 적시에 작업을 얻을 수 있도록 하지만 두 방법 모두 사용하기 어렵습니다. 올바르게 코딩되지 않으면 알림이 손실되어 작업자 스레드가 유휴 상태로 유지되고 작업 대기열에서 처리해야 하는 작업이 무시될 수 있습니다.🎜🎜🎜🎜스레드 누출🎜🎜🎜🎜작업 스레드 수가 고정된 스레드 풀의 경우 , 작업자 스레드가 작업을 실행할 때 RuntimeException 또는 오류가 발생하고 이러한 예외 또는 오류가 포착되지 않으면 작업자 스레드가 비정상적으로 종료되어 스레드 풀에서 작업자 스레드를 영구적으로 잃게 됩니다. 모든 작업자 스레드가 비정상적으로 종료되면 스레드 풀이 비어 있고 작업을 처리할 수 있는 작업자 스레드가 없습니다🎜

스레드 누출로 이어지는 또 다른 상황은 사용자가 데이터를 입력하기를 기다리는 등의 작업을 실행하는 동안 작업자 스레드가 차단되었지만 사용자가 데이터를 입력하지 않았기 때문에(사용자가 자리를 비웠기 때문일 수도 있음) 작업자 스레드가 차단되는 경우입니다. 차단되었습니다. 이러한 작업자 스레드는 이름만 존재하며 실제로는 어떤 작업도 수행하지 않습니다. 스레드 풀의 모든 작업자 스레드가 이렇게 차단된 상태이면 스레드 풀은 새로 추가된 작업을 처리할 수 없습니다

  • 작업 과부하

대기 중인 작업이 많은 경우 작업 대기열에서 실행되는 이러한 작업은 너무 많은 시스템 리소스를 소비하여 시스템 리소스 부족을 일으킬 수 있습니다

요약하면 스레드 풀은 다양한 위험을 가져올 수 있으므로 이를 최대한 방지하려면 다음 사항을 따라야 합니다. 스레드 풀을 사용할 때 다음 원칙을 따르십시오.

작업 A의 경우 실행 프로세스 중에 작업 B의 실행 결과를 동기적으로 기다려야 하므로 작업 A는 스레드 풀의 작업 대기열에 추가되기에 적합하지 않습니다. A 태스크와 같이 다른 태스크의 실행 결과를 기다려야 하는 태스크를 작업 큐에 추가하면 스레드 풀에 교착 상태가 발생할 수 있습니다

특정 태스크의 실행이 차단될 수 있어 차단되는 경우 작업자 스레드가 영구적으로 차단되어 스레드 누출이 발생하는 것을 방지하기 위해 타임아웃을 설정해야 합니다

작업의 특성을 이해하고 작업이 자주 차단되는 IO 작업을 수행하는지 아니면 전혀 수행되지 않는 계산 작업을 수행하는지 분석합니다. 막힌. 전자는 간헐적으로 CPU를 사용하는 반면, 후자는 CPU 사용률이 더 높습니다. 작업의 특성에 따라 작업을 분류한 다음 다양한 스레드 풀의 작업 대기열에 다양한 유형의 작업을 추가합니다. 이런 방식으로 각 스레드 풀은 작업의 특성에 따라 별도로 조정할 수 있습니다.

크기를 조정합니다. 스레드 풀의 최대 크기, 스레드 풀의 최대 크기 최적의 크기는 주로 시스템에서 사용 가능한 CPU 수와 작업 대기열에 있는 작업의 특성에 따라 달라집니다. N개의 CPU가 있는 시스템에 작업 대기열이 하나만 있고 모두 계산 작업인 경우 스레드 풀에 N 또는 N+1 작업자 스레드가 있으면 일반적으로 최대 CPU 사용률을 얻을 수 있습니다

작업이 대기열에는 IO 작업을 수행하고 자주 차단되는 작업이 포함되어 있으므로 모든 작업자 스레드가 항상 작동하는 것은 아니기 때문에 스레드 풀의 크기는 사용 가능한 CPU 수를 초과해야 합니다. 일반적인 작업을 선택한 다음 이 작업을 실행하는 동안 대기 시간(WT)과 계산을 위해 CPU가 차지하는 실제 시간(ST) 사이의 비율(WT/ST)을 추정합니다. N CPU가 있는 시스템의 경우 CPU를 완전히 활용하려면 약 N(1+WT/ST) 스레드를 설정해야 합니다.

작업 과부하를 방지하려면 서버는 다음을 기준으로 고객의 동시 연결 수를 제한해야 합니다. 시스템의 용량. 동시 클라이언트 연결 수가 제한을 초과하면 서버는 연결 요청을 거부하고 클라이언트에게 친숙한 프롬프트를 제공할 수 있습니다.

위 내용은 Java에서 다중 스레드 서버를 만드는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 yisu.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제