并发编程之手写一个简单的线程池

并发编程之手写一个简单的线程池

前言:

有些人可能对线程池比较陌生,并且更不熟悉线程池的工作原理。所以他们在使用多线程的时候,往往都是通过直接new Thread来实现多线程。但是往往良好多线程的设计大多都是使用线程池去实现,今天主要是跟大家分享如何自己实现一个简单的线程池,帮助理解线程池的工作的原理,以及手动实现的这个线程池存在哪些缺点不足,最后分析JDK源码中的线程池是如何设计来解决这些缺点和不足。

一,为什么要使用线程池

(1)降低资源的消耗。降低线程创建和销毁的资源消耗;

(2)提高响应速度:线程的创建时间为T1,执行时间T2,销毁时间T3,免去T1和T3的时间

(3)提高线程的可管理性。

二,手写一个线程池

1、在手写一个线程池之前,我们应该简单的考虑一下,这个线程的数据结构

(1)线程池中运行线程的个数

(2)线程池中如何保存未处理的任务

(3)线程池中的执行任务和清除任务的方法

2、直接上代码吧

MySelfThreadPool .java

package com.concurrent.pool;

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class MySelfThreadPool {
	//默认线程池中的线程的数量
	private static final int WORK_NUM = 5;
	//默认处理任务的数量
	private static final int TASK_NUM = 100;
	
	private int workNum;//线程数量
	
	private int taskNum;//任务数量
	
	private final Set<WorkThread> workThreads;//保存线程的集合
	
	private final BlockingQueue<Runnable> taskQueue;//阻塞有序队列存放任务
	
	
	public MySelfThreadPool() {
		this(WORK_NUM, TASK_NUM);
	}


	public MySelfThreadPool(int workNum, int taskNum) {
		if (workNum <= 0) workNum = WORK_NUM;
		if (taskNum <= 0) taskNum = TASK_NUM;
		taskQueue = new ArrayBlockingQueue<>(taskNum);
		this.workNum = workNum;
		this.taskNum = taskNum;
		workThreads = new HashSet<>();
		//启动一定数量的线程数,从队列中获取任务处理
		for (int i=0;i<workNum;i++) {
			WorkThread workThread = new WorkThread("thead_"+i);
			workThread.start();
			workThreads.add(workThread);
		}
	}
	
	/**
	 * 线程池执行任务的方法,其实就是往BlockingQueue中添加元素
	 * @param task
	 */
	public void execute(Runnable task) {
		try {
			taskQueue.put(task);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public void destroy() {
		System.out.println("ready close thread pool...");
		if (workThreads == null || workThreads.isEmpty()) return ;
		for (WorkThread workThread : workThreads) {
			workThread.stopWork();
			workThread = null;//help gc
		}
		workThreads.clear();
	}
	
	/**
	 * 线程池中的工作线程,直接从BlockingQueue中获取任务
	 * 然后执行任务而已
	 * blockQueue为阻塞队列
	 *
	 */
	private class WorkThread extends Thread{
		public WorkThread(String name) {
			super();
			setName(name);
		}
		
		@Override
		public void run() {
			while (!interrupted()) {
				try {
					Runnable runnable = taskQueue.take();//获取任务
					if (runnable !=null) {
						System.out.println(getName()+" ready execute:"+runnable.toString());
						runnable.run();//执行任务
					}
					runnable = null;//help gc
				} catch (Exception e) {
					interrupt();
					e.printStackTrace();
				}
			}
		}
		
		
		public void stopWork() {
			interrupt();
		}
	}
	
	
}

到上面,我们就已经实现了一个简单的线程池了,当然功能比较简单,但是对我们理解线程池的工作原理还是很有帮助,下面我们来看看测试程序

TestMySelfThreadPool .java

package com.concurrent.pool;

public class TestMySelfThreadPool {
	
	private static final int TASK_NUM = 50;//任务的个数

	public static void main(String[] args) {
		MySelfThreadPool myPool = new MySelfThreadPool(3,50);
		for (int i=0;i<TASK_NUM;i++) {
			myPool.execute(new MyTask("task_"+i));
		}
		
	}
	
	static class MyTask implements Runnable{
		
		private String name;
		public MyTask(String name) {
			this.name = name;
		}
		
		public String getName() {
			return name;
		}

		public void setName(String name) {
			this.name = name;
		}


		@Override
		public void run() {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println("task :"+name+" end...");
			
		}
		
		@Override
		public String toString() {
			// TODO Auto-generated method stub
			return "name = "+name;
		}
	}
}

3, 程序运行的部分截图:

并发编程之手写一个简单的线程池

上面实现的线程池其实非常简单,就是客户端往线程池中的队列中添加任务,然后线程池中的线程一直从队列中去拿任务,拿到就执行,拿不到就一直阻塞。测试程序中就是创建多个任务,然后往线程池中扔。

虽然上面实现的简单的线程池能够实现基本功能,但是还是存在很多的不足

(1)线程池中的线程的个数,不能随着任务进行自动调整,这可能导致线程的浪费,或者导致提交的任务一直无法执行,导致应用崩溃

(2)当无法处理任务的时候,没有很好的方式一直去处理,而是让它一直阻塞

针对上面的问题,我们来看看jdk源码中的线程池是如何去处理的

三,ThreadPoolExecutor原理分析

先看下ThraedPoolExecutor的构造方法(参数最多的那个)

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {}

1,参数解析:

int corePoolSize:线程池中核心线程数,< corePoolSize,就会创建新线程,= corePoolSize,这个任务就会保存到BlockingQueue,如果调用prestartAllCoreThreads()方法就会一次性的启动corePoolSize个数的线程。

int maximumPoolSize:允许的最大线程数,BlockingQueue也满了,< maximumPoolSize时候就会再次创建新的线程

long keepAliveTime:线程空闲下来后,存活的时间,这个参数只在> corePoolSize才有用

TimeUnit unit:存活时间的单位值(秒、毫秒…)

BlockingQueue<Runnable> workQueue:保存任务的阻塞队列

ThreadFactory threadFactory:创建线程的工厂,给新建的线程赋予名字

RejectedExecutionHandler handler:饱和策略

AbortPolicy:直接抛出异常,默认;

CallerRunsPolicy:用调用者所在的线程来执行任务

DiscardOldestPolicy:丢弃阻塞队列里最老的任务,队列里最靠前的任务

DiscardPolicy:当前任务直接丢弃

实现自己的饱和策略,实现RejectedExecutionHandler接口即可

2,图解

看了上面的参数解析,其实基本上已经了解了它的处理的逻辑了,下面用一个图来解释一下

并发编程之手写一个简单的线程池

(1)当线程池中的线程个数<corePool,每次进来新的任务,就启动一个线程去处理这个任务

(2)当线程池中的线程个数>=corePool,这时每次进来的新的任务,就会加入到队列中。

(3)当线程池中的线程个数>=corePool但是<maxiMumPool,并且队列也满了(有界队列),这个时候再来新的任务,就会继续创建新的线程去处理。这个时候创建的线程,当线程空闲下来的时候到keepAliveTime的时间就会销毁(线程资源宝贵啊)

(4)当线程池中的线程数>=maximumPool时,这个时候再来新的任务,就可以选择拒绝的机制

3、代码解析

接下来看看代码验证一下上面的描述把,看下面这一段就行了

 并发编程之手写一个简单的线程池

好了,就分享到这里把,欢迎指正!

 

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/111189.html原文链接:https://javaforall.net

(0)
上一篇 2021年8月4日 下午3:00
下一篇 2021年8月4日 下午4:00


相关推荐

  • 聊聊Dotnetty

    聊聊Dotnetty以前 我曾经写过一个 C S 的期货交易系统 C S 就绕不开通信 我大概的了解了 net 的通信机制后 选择用 TCP 长连接实现了通信 客户端可以调用服务器端 服务器端可以主动推送消息到客户端 实现是实现了 但是代码笨重而且低效 近来又要解决一个 C S 的通信问题 感觉到自己以前写的通信继续复用会触犯自己的洁癖 同时深感自己的精力大不如以前 重新写一个优雅而且高效的通信的中间件没有任何信心 于是就在网络上

    2026年3月19日
    3
  • 什么是Git?Gitlab和Github区别

    什么是Git?Gitlab和Github区别一 什么是 Gitgit 是一个分布式版本控制软件 最初由林纳斯 托瓦兹创作 于 2005 年以 GPL 发布 最初目的是为更好地管理 Linux 内核开发而设计 应注意的是 这与 GNUInteracti 有所不同 git 最初的开发动力来自于 BitKeeper 和 Monotone git 最初的开发动力来自于 BitKeeper 和 Monotone git 最初只

    2026年3月19日
    2
  • 修复受损的系统的两个小妙招

    修复受损的系统的两个小妙招

    2021年7月28日
    73
  • 讯飞星火怎么设置密码

    讯飞星火怎么设置密码

    2026年3月14日
    1
  • 批处理for命令的用法_批处理主要解决

    批处理for命令的用法_批处理主要解决1.前言for是批处理中最复杂,也最强大的关键字。熟练掌握for的用法,才可能理解批处理的强大之处。2.基本用法2.1.概念for是对一组文件中的每一个文件执行某个特定命令。FOR%variableIN(set)DOcommand[command-parameters]%variable,指定一个单一字母可替换的参数。(set),指定一个或一组文…

    2022年8月31日
    7
  • 如何用决策树模型做数据分析报告_决策树实例

    如何用决策树模型做数据分析报告_决策树实例什么是决策树?决策树模型本质是一颗由多个判断节点组成的树。在树的每个节点做参数判断,进而在树的最末枝(叶结点)能够对所关心变量的取值作出最佳判断。通常,一棵决策树包含一个根结点,若干内…

    2022年8月30日
    4

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号