目录
为什么需要线程池?
在日常开发中,我们容易遇到以下几方面的问题:
1、资源消耗过大
线程池可以重复利用已创建的线程降低线程创建和销毁造成的消耗。
2、响应速度过慢
当任务到达时,线程池任务可以不需要等到线程创建就能立即执行。
3、提高线程的可管理性
线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控
4、内存溢出、CPU耗尽
合理配置内存和使用CPU,线程池可以防止服务器过载。
需求
在日常开发中,我们常常会遇到多客户端同时访问服务端的情况。
当访问的客户端数量增加时,我们既要保证与客户端的通讯,又要保证在可支持成本下服务器的正常运行。
线程池设计
1、线程数
CPU密集型:
即计算型任务,如搜索、排序,占用CPU资源较多,应配置尽可能少的线程,效率越低。线程数建议配置N +1 ,N为CPU的核数。
IO密集型:
即网络请求,读写内存的任务,如WEB应用,占用CPU资源较少(因为大部分的时间,CPU都在等待IO操作的完成),应配置尽可能多的线程。线程数建议配置2×N,N指的是CPU的核数。
此处为IO密集型,因此线程数设置为2×N。
2、keepAliveTime
当客户端退出时,线程立即退出,线程池在第一时间获取队列中的任务,因此keepAliveTime设置为0L。
3、任务排队策略为LinkedBlockingQuene
阻塞队列 | 特点 |
ArrayBlockingQueue | 基于数组结构的有界阻塞队列,按FIFO排序任务 |
LinkedBlockingQuene | 基于链表结构的无界阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene |
SynchronousQuene | 直接提交。一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,在这种任务提交方式下,这种方式没有任务缓冲区 |
priorityBlockingQuene | 具有优先级的无界阻塞队列 |
我们的任务没有优先级,只有先来后到。由于访问量大,需要尽可能地接受更多任务,必要时可以存入缓冲区。因此任务排队策略选择LinkedBlockingQuene。
4、拒绝策略
拒绝策略 | 特点 |
AbortPolicy | 默认策略,处理程序遭到拒绝将抛出运行时异常。 |
CallerRunsPolicy
| 线程用调用者所在的线程来执行任务,提供简单的反馈控制机制,会减缓新任务的提交速度。 |
DiscardOldestPolicy
| 丢弃阻塞队列中靠最前的任务,并执行当前任务,即如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程) |
DiscardPolicy | 不能执行的任务将被直接丢弃任务,不抛出异常。 |
在队列中的任务先进先执行,对不能执行的任务,我们需要跑出异常。因此拒绝策略选择默认的AbortPolicy。
至此,线程池的参数设计完毕。
常用线程池 | 特点 | 适应场景 |
newSingleThreadExecutor | 单线程的线程池 | 用于需要保证顺序执行的场景,并且只有一个线程在执行 |
newFixedThreadPool | 固定大小的线程池 | 用于已知并发压力的情况下,对线程数做限制。 |
newCachedThreadPool | 可以无限扩大的线程池 | 比较适合处理执行时间比较小的任务。 |
newScheduledThreadPool | 可以延时启动,定时启动的线适 | 用于需要多个后台线程执行周期任务的场景。 |
newWorkStealingPool | 拥有多个任务队列的线程池 | 可以减少连接数,创建当前可用cpu数量的线程来并行执行。 |
在高并发状态下,将超出的任务存在队列中等候,等有空闲线程出现时,立即取出队列中的任务执行,保证了任务的执行的同时不会使内存溢出。
综上所述,线程池选择newFixedThreadPool类型。
源码分析
Server.java
package hptestthreadpool;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Logger;
/**
* @author hp
*/
public class Server {
private Logger logger = Logger.getLogger("Server.class");
private static final int THREADPOOL_COEFFICIENT = 2;
private ServerSocketChannel serverSocketChannel = null;
private ExecutorService executorService;
private int PORT = 23;
public static void main(String args[]) throws IOException {
new Server().service();
}
public Server() throws IOException {
executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * THREADPOOL_COEFFICIENT);
//executorService = Executors.newFixedThreadPool(1);
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().setReuseAddress(true);
serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
logger.info("端口:" + PORT);
}
public void service() {
while (true) {
SocketChannel socketChannel = null;
try {
socketChannel = serverSocketChannel.accept();
executorService.execute(new Process(socketChannel));
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
1、首先将线程数系数设为2,即线程数为2xN。
2、使用Executors工具类创建newFixedThreadPool类型的线程池,负责与客户端的通讯。
Executors是一个工具类,提供了创建常用配置线程池的方法,便捷地创建ThreadPoolExecutor对象,底层调用的是ThreadPoolExecutor的构造方法:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler);
}
3、打开socket并监听客户端的连接,当有客户端连上时,便新开一条线程去处理与该客户端的通讯任务。
线程池执行任务主要通过ThreadPoolExecutor类的execute方法,它将我们的任务(即Process类)变成Runnable类型的命令。线程池先判断能不能把该任务加入核心线程中,如果不能再看能不能加入阻塞工作队列中,若队列已满,则在非核心线程中创建新的线程来处理任务,往maxPoolSize发展。
Process.java
具体任务
package hptestthreadpool;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.nio.channels.SocketChannel;
import java.util.Date;
import java.util.logging.Logger;
/**
* @author hp
*/
public class Process implements Runnable {
private Logger logger = Logger.getLogger("Process.class");
private SocketChannel socketChannel;
public Process(SocketChannel socketChannel) {
this.socketChannel = socketChannel;
}
@Override
public void run() {
try {
Socket socket = socketChannel.socket();
logger.info("客户端已连上: " + socket.getInetAddress() + ":" + socket.getPort());
InputStream socketIn = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(
socketIn));
OutputStream socketOut = socket.getOutputStream();
PrintWriter printWriter = new PrintWriter(socketOut, true);
String msg = null;
while ((msg = br.readLine()) != null) {
logger.info("接收到:" + socket.getInetAddress() + ":" + socket.getPort() + " 内容:" + msg);
printWriter.println(new Date());
if (msg.equals("guanbi")) {
logger.info(socket.getInetAddress() + ":" + socket.getPort() + " 已关闭");
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (socketChannel != null) {
socketChannel.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Client1.java
package hptestthreadpool;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.util.logging.Logger;
/**
* @author hp
*/
public class Client1 {
private Logger logger = Logger.getLogger("Client1.class");
private SocketChannel socketChannel;
private String HOST = "localhost";
private int PORT = 23;
public static void main(String[] args) throws IOException {
new Client1().talk();
}
public Client1() throws IOException {
socketChannel = SocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(HOST, PORT);
socketChannel.connect(inetSocketAddress);
}
public void talk() throws IOException {
try {
InputStream inputStream = socketChannel.socket().getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
OutputStream socketOutputStream = socketChannel.socket().getOutputStream();
PrintWriter printWriter = new PrintWriter(socketOutputStream, true);
BufferedReader localReader = new BufferedReader(new InputStreamReader(System.in));
String msg = null;
while ((msg = localReader.readLine()) != null) {
printWriter.println(msg);
logger.info(bufferedReader.readLine());
if (msg.equals("guanbi")) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Client2.java同理
客户端向服务端发送信息,服务端收到消息后,执行任务,即将收到的消息输出。客户端使用“关闭”作为断开与服务端连接的指令。此处非本文重点,只做简单的逻辑展示,有兴趣的朋友可以具体展开。
结果分析
首先打开服务端,再打开客户端1并发送消息“11”,再打开客户端2并发送消息“22”。
客户端1:
11
2月 05, 2020 9:35:35 上午 hptestthreadpool.Client1 talk
信息: Wed Feb 05 09:35:35 CST 2020
guanbi
2月 05, 2020 9:35:50 上午 hptestthreadpool.Client1 talk
信息: Wed Feb 05 09:35:50 CST 2020
客户端2:
22
2月 05, 2020 9:35:37 上午 hptestthreadpool.Client2 talk
信息: Wed Feb 05 09:35:37 CST 2020
guanbi
2月 05, 2020 9:35:53 上午 hptestthreadpool.Client2 talk
信息: Wed Feb 05 09:35:53 CST 2020
服务端:
2月 05, 2020 9:35:16 上午 hptestthreadpool.Server <init>
信息: 端口:23
2月 05, 2020 9:35:24 上午 hptestthreadpool.Process run
信息: 客户端已连上: /127.0.0.1:49965
2月 05, 2020 9:35:30 上午 hptestthreadpool.Process run
信息: 客户端已连上: /127.0.0.1:49972
2月 05, 2020 9:35:35 上午 hptestthreadpool.Process run
信息: 接收到:/127.0.0.1:49965 内容:11
2月 05, 2020 9:35:37 上午 hptestthreadpool.Process run
信息: 接收到:/127.0.0.1:49972 内容:22
2月 05, 2020 9:35:50 上午 hptestthreadpool.Process run
信息: 接收到:/127.0.0.1:49965 内容:guanbi
2月 05, 2020 9:35:50 上午 hptestthreadpool.Process run
信息: /127.0.0.1:49965 已关闭
2月 05, 2020 9:35:53 上午 hptestthreadpool.Process run
信息: 接收到:/127.0.0.1:49972 内容:guanbi
2月 05, 2020 9:35:53 上午 hptestthreadpool.Process run
信息: /127.0.0.1:49972 已关闭
若将线程数设为1,则只有一个客户端能与服务端通讯,运行结果如下:
开启服务端:
2月 05, 2020 9:52:44 上午 hptestthreadpool.Server <init>
信息: 端口:23
运行client1并发送消息11,运行client2并发送消息22:
2月 05, 2020 9:53:18 上午 hptestthreadpool.Process run
信息: 客户端已连上: /127.0.0.1:50108
2月 05, 2020 9:53:25 上午 hptestthreadpool.Process run
信息: 接收到:/127.0.0.1:50108 内容:11
此时,服务端并没有执行客户端2的任务,因为线程数设置为1,只执行了客户端1的任务。
关闭client1:
2月 05, 2020 9:54:00 上午 hptestthreadpool.Process run
信息: 接收到:/127.0.0.1:50108 内容:guanbi
2月 05, 2020 9:54:00 上午 hptestthreadpool.Process run
信息: /127.0.0.1:50108 已关闭
2月 05, 2020 9:54:00 上午 hptestthreadpool.Process run
信息: 客户端已连上: /127.0.0.1:50112
2月 05, 2020 9:54:00 上午 hptestthreadpool.Process run
信息: 接收到:/127.0.0.1:50112 内容:22
当客户端1与服务端断开连接后,线程释放,此时被占用的线程数为0,有了空闲线程后,之前队列里等候着的客户端2的任务被取出执行。
关闭client2:
2月 05, 2020 9:54:07 上午 hptestthreadpool.Process run
信息: 接收到:/127.0.0.1:50112 内容:guanbi
2月 05, 2020 9:54:07 上午 hptestthreadpool.Process run
信息: /127.0.0.1:50112 已关闭
附
1、可以自己调用ThreadPoolExecutor来创建线程池
2、可以根据应用场景实现RejectedExecutionHandler接口,自定义拒绝策略,如记录日志或持久化存储不能处理的任务。