线程池的Java实战及剖析

1 篇文章 0 订阅
1 篇文章 0 订阅

目录

为什么需要线程池?

需求

线程池设计

1、线程数

2、keepAliveTime

3、任务排队策略为LinkedBlockingQuene

4、拒绝策略

源码分析

结果分析


为什么需要线程池?

在日常开发中,我们容易遇到以下几方面的问题:

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接口,自定义拒绝策略,如记录日志或持久化存储不能处理的任务。

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值