伪代码
class Server { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPollExecutor(100); serverSocket.bind(9090); //主线程等待新连接的到来 while (!Thread.currentThread.isInturrupted()) { Socket socket = serverSocket.accpet(); //将新的连接交给线程池处理 executor.submit(new ConnectIOHandler(socket)); } }}class ConnectIOHandler implements Runnable { private Socket socket; public ConnectIOHandler(Socket socket) { this.socket = socket; } @Override public void run() { while (!Thread.currentThread.isInturruted()) { //读取数据 String data = socket.read().... if (data != null) { //处理数据 dosomething(); //写数据 socket.write()... } } }}
上述模型的一些特性:
1. 每连接每线程。socket.accept()、socket.read()、socket.write()这三个函数均是同步阻塞,当一个连接在处理I/O时,系统是阻塞的,如果是单线程的话必然会挂死。开启多线程,会将CPU释放出来,可以处理更多的事情。
2. 使用线程池,降低线程创建和回收的成本。
缺点就是严重依赖线程:
1. 线程的创建和销毁的成本很高,在Linux中,线程本质就是一个进程(科普:Linux里面的线程其实是通过fork函数来fork一个子进程的方式来实现的,并且是通过关闭COW(Copy on Write)特性来实现线程间独立但是又共享父进程内存的效果。总而言之,在Linux世界里,线程其实就是进程,进程是Linux内核调度的实体。),创建和销毁都是重量级的系统函数。
2. 线程本身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间(不显式设置-Xss或-XX:ThreadStackSize时,在Linux x64上ThreadStackSize的默认值就是1024KB,给Java线程创建栈会用这个参数指定的大小。),并发高时,JVM的内存会吃紧。
3. 线程的切换成本高,操作系统线程切换时,需要保留线程的上下文,然后进行系统调用。高并发时,可能会导致线程切换的时间大于线程执行的时间,带来系统load偏高,CPU sy使用率特别高,导致系统陷入几乎不可用状态。
4. 容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大。