1 什么是Thread-Per-Message模式
Thread-Per-Message的意思是为每一个消息的处理开辟一个线程使得消息能够以并发的方式进行处理,从而提高系统整体的吞吐能力。这就好比电话接线员一样,收到的每一个电话投诉或者业务处理请求,都会提交对应的工单,然后交由对应的工作人员来处理
2 每个任务一个线程
- 定义一个请求:
public class Request {
/**
* 请求的内容
*/
private final String content;
public Request(String content) {
this.content = content;
}
@Override
public String toString() {
return content;
}
}
- 定义一个请求处理器实现Runnable接口,实现run方法,内部就是对Request的处理
public class RequestHandler implements Runnable {
private final Request request;
public RequestHandler(Request request) {
this.request = request;
}
/**
* 实现run方法,处理每个请求
* 这里就简单模拟
*/
@Override
public void run() {
System.out.println("Begin handle " + request);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("End handle " + request);
}
}
- 模式每一个请求都是一个新的线程去处理
public class Operator {
/**
* 模拟接受内容,包装成Request交给RequestHandler去处理
* 每次都交给一个线程去处理
*
* @param content
*/
public void call(String content) {
Request request = new Request(content);
new Thread(new RequestHandler(request)).start();
}
}
public static void main(String[] args) {
List<String> strings = Arrays.asList("请求内容1", "请求内容2", "请求内容3", "请求内容4", "请求内容5", "请求内容6", "请求内容7");
Operator operator = new Operator();
strings.forEach(operator::call);
}
这就是一个简单的一个任务一个线程去处理,当然还是有几个问题的:
- 每一个JVM中可创建的线程数量是有限的,针对每一个任务都创建一个新的线程,假如每一个线程执行的时间比较长,那么在某个时刻JVM会由于无法再创建新的线程而导致栈内存的溢出;再假如每一个任务的执行时间都比较短,频繁地创建销毁线程对系统性能的开销也一个不小的影响。
- 创建新线程的方式交给线程池去处理,这样可以避免线程频繁创建和销毁带来的系统开销,还能将线程数量控制在一个可控的范围之内
3 多用户的网络聊天
Thread-Per-Message模式在网络通信中的使用也是非常广泛的,比如在本节中介绍的网络聊天程序,在服务端每一个连接到服务端的连接都将创建一个独立的线程进行处理,当客户端的连接数超过了服务端的最大受理能力时,客户端将被存放至排队队列中。
3.1 服务端
public class ChatServer {
/**
* 服务端端口号
*/
private final int port;
private ServerSocket serverSocket;
private ExecutorService executorService;
public ChatServer(int port) {
this.port = port;
}
/**
* 启动服务
*/
public void startServer() throws IOException {
// 创建线程池
executorService = Executors.newSingleThreadExecutor();
this.serverSocket = new ServerSocket(this.port);
this.serverSocket.setReuseAddress(true);
System.out.println("Chat Server is started and listen at port: " + port);
this.listen();
}
private void listen() throws IOException {
while (true) {
// 接受客户端链接,该方法是个阻塞方法,当没有客户端链接的时候该方法会阻塞
// 返回的 Socket对象就是代表与客户端连接套接字对象
Socket client = this.serverSocket.accept();
// 将客户端对象作为一个Request交ClientHandler处理
// 并开启一个线程去处理
executorService.submit(new ClientHandler(client));
}
}
}
ClientHandler实现:和上面的RequestHandler基本一样
public class ClientHandler implements Runnable {
private final Socket socket;
private final String clientIdentify;
public ClientHandler(final Socket socket) {
this.socket = socket;
this.clientIdentify = socket.getInetAddress().getHostAddress() + ":" + socket.getPort();
}
@Override
public void run() {
try {
this.chat();
} catch (IOException e) {
e.printStackTrace();
}
}
private void chat() throws IOException {
BufferedReader bufferedReader = wrap2Reader(this.socket.getInputStream());
PrintStream printStream = wrap2Print(this.socket.getOutputStream());
String received;
while ((received = bufferedReader.readLine()) != null) {
// 将客户端发送的消息输出到控制台
System.out.printf("client:%s-message:%s\n", clientIdentify, received);
if (received.equals("quit")) {
// 如果收到的是quit,就退出
write2Client(printStream, "client will close");
socket.close();
break;
}
// 响应结果给客户端
write2Client(printStream, "Server:" + received);
}
}
/**
* 将输入字节流封装成BufferedReader缓冲字符流
* @param inputStream
* @return
*/
private BufferedReader wrap2Reader(InputStream inputStream) {
return new BufferedReader(new InputStreamReader(inputStream));
}
/**
* 将输出字节流封装成PrintStream
* @param outputStream
* @return
*/
private PrintStream wrap2Print(OutputStream outputStream) {
return new PrintStream(outputStream);
}
private void write2Client(PrintStream print, String message) {
print.println(message);
print.flush();
}
}
上面的通信方式是一种典型的一问一答的聊天方式,客户端连接后发送消息给服务端,服务端回复消息给客户端,每一个线程负责处理一个来自客户端的连接。
3.2 测试
public static void main(String[] args) throws IOException {
ChatServer chatServer = new ChatServer(10011);
chatServer.startServer();
}
启动服务之后,可以使用telnet命令测试
telnet localhost 10011