当前位置:

首页 > 编程开发 > Java多线程服务器的实现方法

Java多线程服务器的实现方法

一个典型的单线程服务器示例如下:while(true){Socketsocket=null;try{//接收客户连接socket=serverSocket.accept();//从socket中获得输入流与输出流,与客户通信...}catch(IOExceptione){e.printStackTrace()}finally{try{if(socket!=null){//断开连接socket.close();}catch(IOExceptione){e.printStackTrace();}}}}服务端接

本文目录

    一个典型的单线程服务器示例如下:

    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 workQueue;
        // 表示线程池ID
        private static int threadPoolID;
        // 表示工作线程ID
        // poolSize 指定线程池中的工作线程数目
        public ThreadPool(int poolSize) {
            super("ThreadPool-"+ (threadPoolID++));
            setDaemon(true);
            // 创建工作队列
            workQueue = new LinkedList();
            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();
                    }
                }
            }
        }
    }

    使用线程池实现的服务器如下:

    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 {...}
    }

    使用线程池的注意事项

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

    • 死锁

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

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

    • 系统资源不足

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

    • 并发错误

    线程池的工作队列依靠 wait() 和 notify() 方法来使工作线程及时取得任务,但这两个方法都难以使用。如果编码不正确,就可能会丢失通知,导致工作线程一直保持空闲状态,无视工作队列中需要处理的任务

    • 线程泄漏

    对于工作线程数目固定的线程池,如果工作线程在执行任务时抛出 RuntimeException 或 Error,并且这些异常或错误没有被捕获,那么这个工作线程就会异常终止,使得线程池永久地失去了一个工作线程。如果所有的工作线程都异常终止,线程池变为空,没有任何可用的工作线程来处理任务

    导致线程泄漏的另一种情形是,工作线程在执行一个任务时被阻塞,比如等待用户的输入数据,但是由于用户一直不输入数据(可能是因为用户走开了),导致这个工作线程一直被阻塞。这样的工作线程名存实亡,它实际上不执行任何任务了。假如线程池中所有的工作线程都处于这样的阻塞状态,那么线程池就无法处理新加入的任务了

    • 任务过载

    当工作队列中有大量排队等候执行的任务,这些任务本身可能会消耗太多的系统资源而引起系统资源缺乏

    综上所述,线程池可能会带来种种风险,为了尽可能避免它们,使用线程池时需要遵循以下原则:

    如果任务 A 在执行过程中需要同步等待任务 B 的执行结果,那么任务 A 不适合加入线程池的工作队列中。如集把像任务 A 一样的需要等待其他任务执行结果的任务加入工作队列中,就可能会导致线程池的死锁

    如果执行某个任务时可能会阻塞,并且是长时间的阻塞,则应该设定超时时间避免工作线程永久地阻塞下去而导致线程泄漏

    了解任务的特点,分析任务是执行经常会阻塞的 IO 操作,还是执行一直不会阻塞的运算操作。前者时断时续地占用 CPU,而后者对 CPU 具有更高的利用率。根据任务的特点,对任务进行分类,然后把不同类型的任务分别加入不同线程池的工作队列中,这样可以根据任务的特点分别调整每个线程池

    调整线程池的大小,线程池的最佳大小主要取决于系统的可用 CPU 的数目以及工作队列中任务的特点。假如在一个具有 N 个 CPU 的系统上只有一个工作队列并且其中全部是运算性质的任务,那么当线程池具有 N 或 N+1 个工作线程时,一般会获得最大的 CPU 利用率

    如果工作队列中包含会执行 IO 操作并经常阻塞的任务,则要让线程池的大小超过可用 CPU 的数目,因为并不是所有工作线程都一直在工作。选择一个典型的任务,然后估计在执行这个任务的过程中,等待时间(WT)与实际占用 CPU 进行运算的时间(ST)之间的比:WT/ST。对于一个具有 N 个 CPU 的系统,需要设置大约 N(1+WT/ST) 个线程来保证 CPU 得到充分利用

    避免任务过载,服务器应根据系统的承受能力,限制客户的并发连接的数目。当客户的并发连接的数目超过了限制值,服务器可以拒绝连接请求,并给予客户友好提示。

    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发 Java 服务器
    相关文章 更多
    Java单元测试入门:使用JUnit 5编写第一个测试用例
    Java单元测试入门:使用JUnit 5编写第一个测试用例

    面向初学者的JUnit 5实用入门指南。从测试概念、Maven依赖配置,到手把手编写首个单元测试用例及断言验证,助你掌握Java单元测试的核心流程。

    Java变量未初始化报错解析:局部变量与成员变量默认值差异
    Java变量未初始化报错解析:局部变量与成员变量默认值差异

    深入解析Java中局部变量未初始化编译报错与成员变量自动赋予默认值的机制差异,涵盖JVM内存分配、编译期确定性赋值检查与实际排查修复方案。

    Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat
    Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat

    详细讲解在Java中获取当前系统时间并进行格式化输出的两种核心方案:基于Java 8全新日期时间API的DateTimeFormatter与传统遗留的SimpleDateFormat,包含完整代码示例、线程安全机制对比及生产选型建议。

    Java中final与static final常量的定义及初始化规则
    Java中final与static final常量的定义及初始化规则

    深入解析Java中final变量与static final常量的定义、内存语义、初始化时机及合法赋值路径,涵盖编译期常量优化与实战避坑指南。

    Java抽象方法与接口方法的区别及选型建议
    Java抽象方法与接口方法的区别及选型建议

    深入对比Java中抽象类抽象方法与接口方法的语法演进、设计意图及运行期差异,并提供清晰的架构选型决策流程与实战范例。

    Java实现HTTP请求与JSON解析的完整代码示例
    Java实现HTTP请求与JSON解析的完整代码示例

    本文提供Java标准库HttpClient发起HTTP GET/POST请求并结合Jackson进行JSON数据反序列化与序列化的完整代码示例与工程实践指南。

    Java递归出现StackOverflowError的原因分析及栈空间优化方案
    Java递归出现StackOverflowError的原因分析及栈空间优化方案

    详细分析Java递归调用出现StackOverflowError的原因,结合代码说明线程栈、栈帧和递归深度之间的关系,并介绍终止条件排查、迭代改写、减少栈帧开销以及通过-Xss调整JVM栈空间的方法。

    Java空指针异常(NPE)的代码排查与修复实例
    Java空指针异常(NPE)的代码排查与修复实例

    针对Java中常见的NullPointerException(NPE),本文从堆栈日志定位、典型代码触发场景、具体修复代码对比到防御体系进行系统讲解,提供实用且易上手的排查与修复方案。

    selenium上传文件教程本地文件路径设置方法
    selenium上传文件教程本地文件路径设置方法

    深入解析Selenium处理文件上传的技术细节,涵盖标准HTML控件的路径直接注入法与非标准控件的系统级模拟方案,提供Windows与Linux环境下的路径设置最佳实践及常见异常排查。

    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景
    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景

    针对大数据工程场景,系统对比Java、Scala与Python在执行性能、生态结合、开发效率和维护成本上的核心差异,给出清晰的架构选型依据与落地方案。

    查看更多
    精品专题 更多
    装机必备
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows
    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程
    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    Mac软件 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    Aperty macOS版
    Aperty macOS版
    macOS

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    WINDOWS 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    PixTV 网页版
    PixTV 网页版
    Windows

    PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。