Java网络处理模型-阻塞I/O+线程池

作者:聂勇 欢迎转载,请保留作者信息并说明文章来源!

阻塞I/O+线程池网络模型是阻塞I/O+多线程网络模型的改进版,人们意识到每次有新连接创建和销毁线程的代价太大,就用线程池来解决这个问题:每次接收到新连接后从池中取一个空闲线程进行处理,处理完成后再放回池中,重用线程避免了频率地创建和销毁线程带来的开销。
阻塞I/O+线程池模型

和”阻塞I/O+多线程”相比,在大量短连接的场景中性能会有提升,因为不用每次都创建和销毁线程,而是重用连接池中的线程。但在大量长连接的场景中,因为线程被连接长期占用,不需要频繁地创建和销毁线程,因而没有什么优势。

代码示例

ThreadPoolEchoServer源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package cn.aofeng.demo.io;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* 线程池网络echo服务。每接收到一个新连接都由线程池中的空闲线程处理,连接关闭后释放线程(不会销毁线程,仍在线程池中)。
*
* @author <a href="mailto:aofengblog@163.com">NieYong</a>
*/
public class ThreadPoolEchoServer {
private final static Logger logger = Logger.getLogger(MultiThreadEchoServer.class.getName());
/**
* @param args [0]-监听端口
*/
public static void main(String[] args) {
if (args.length != 1) {
System.err.println("无效的参数。使用示例:");
System.err.println(" java cn.aofeng.demo.io.ThreadPoolEchoServer 9090");
System.exit(-1);
}
int port = Integer.parseInt(args[0]);
ExecutorService threadpool = Executors.newFixedThreadPool(5);
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(port));
if (logger.isLoggable(Level.INFO)) {
logger.info("线程池网络echo服务启动完毕,监听端口:" +port);
}
while (true) {
// 接收新的客户端连接
Socket socket = serverSocket.accept();
if (logger.isLoggable(Level.INFO)) {
logger.info("收到一个新的连接,客户端IP:"+socket.getInetAddress().getHostAddress()+",客户端Port:"+socket.getPort());
}
// 将Socket连接交给线程池处理
threadpool.submit(new Worker(socket));
}
} catch (IOException e) {
logger.log(Level.SEVERE, "处理网络连接出错", e);
}
}
}

Worker源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package cn.aofeng.demo.io;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* 处理客户端Socket连接的工作线程。
*
* @author NieYong <aofengblog@163.com>
*/
public class Worker implements Runnable {
private final static Logger logger = Logger.getLogger(MultiThreadEchoServer.class.getName());
// 字符集编码
private final static String CHAR_SET = "utf8";
// 行结束符
private final static String CRLF = "\r\n";
private Socket socket = null;
public Worker(Socket socket) {
this.socket = socket;
}
public void setSocket(Socket socket) {
this.socket = socket;
}
public void close(Closeable c) {
if (null != c) {
try {
c.close();
} catch (IOException e) {
// ingore
}
}
}
@Override
public void run() {
if (null == socket || socket.isClosed()) {
logger.warning("无效的Socket连接:" + socket);
return;
}
String lineEnd = CRLF;
try {
BufferedReader reader = new BufferedReader(
new InputStreamReader(
socket.getInputStream()));
OutputStream outs = socket.getOutputStream();
String line = null;
while ( null != (line = reader.readLine()) ) {
// 客户端退出
if ("quit".equalsIgnoreCase(line) || "exit".equalsIgnoreCase(line)) {
break;
}
outs.write(line.getBytes(CHAR_SET));
outs.write(lineEnd.getBytes(CHAR_SET));
}
close(reader);
close(outs);
} catch (IOException e) {
logger.log(Level.SEVERE, "读取网络连接数据出错", e);
}
}
}

验证

1、启动服务。

1
java cn.aofeng.demo.io.ThreadPoolEchoServer 9090

执行上面的命令,启动服务,输出信息:

2013-10-24 21:06:43 cn.aofeng.demo.io.ThreadPoolEchoServer main
信息: 线程池网络echo服务启动完毕,监听端口:9090

2、打开三个终端窗口,执行命令:

1
telnet 192.168.56.102 9090

服务输出如下信息:

2013-10-24 21:11:05 cn.aofeng.demo.io.ThreadPoolEchoServer main
信息: 收到一个新的连接,客户端IP:192.168.56.101,客户端Port:1167
2013-10-24 21:11:10 cn.aofeng.demo.io.ThreadPoolEchoServer main
信息: 收到一个新的连接,客户端IP:192.168.56.101,客户端Port:1170
2013-10-24 21:11:15 cn.aofeng.demo.io.ThreadPoolEchoServer main
信息: 收到一个新的连接,客户端IP:192.168.56.101,客户端Port:1171

注:服务所在机器的IP地址是192.168.56.102。

3、连接一段时间后,从终端输入exit或quit指令,服务端关闭连接,对应的线程也随之销毁,如下图所示:
阻塞I/O+线程池模型运行时线程

可以看到连接池创建了三个线程(pool-1-thread,pool-2-thread,pool-3-thread),当连接关闭后,线程并没有销毁,只是从运行状态变成空闲状态。