线程池原理解析与简单实现

线程池概念

线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。 例如,线程数一般取cpu数量+2比较合适,线程数过多会导致额外的线程切换开销。

以上是百度百科对概念的解释,下面从多线程开始来理解线程池,首先看看多线程的一些可能的面试问题

关于多线程的简单面试问题简单解析

问题1、用多线程的目的是什么?
答:充分利用cpu资源,并发做多件事
问题2、单核cpu机器上适不适合用多线程?
答: 适合,如果是单线程,线程中需要等待IO时,此时cpu就空闲出来了,利用多线程可以充分利用cpu资源。
问题3、线程什么时候会让出cpu?
答:1、阻塞时候 ,wait 、await 、等待IO
    2、Sleep、yield、线程结束。
问题4、线程是什么?
答:1、一条代码执行流,完成一组代码的执行。
   2、这是一组代码,往往称为一个任务。   
问题5、cpu做的是什么工作?
	答:1、执行代码。
	任务(code)——>线程(code 线程)——> cpu(执行代码)。
问题6、线程是不是越多越好?
  答:不是,
      1、创建线程需要时间,用完线程需要销毁,创建和销毁都需要消耗时间。线程在java中是一个对象,每个java线程都需要操作系统线程支持。如果 线程创建时间+销毁时间 大于 执行任务时间,就失去意义了。
      2、java对象占用堆内存,操作系统线程占用系统内存,根据jvm 规范,一个线程默认最大栈大小1M,这个栈空间需要从系统内存中分配,线程过多,会消耗内存。
      3、操作系统需要频繁切换线程上下文,影响性能。

针对这个问题可以用一个图解更加形象表示:
在这里插入图片描述

问题7、该如何正确使用多线程?
1、多线程目的:充分利用cpu并发做事(多做任务)
2、线程的本质:将代码送给cpu执行
3、用合适数量的线程 不断运送代码即可
4、合适数量的线程就构成了一个池
问题8、如何确定合适数量的线程?
1、如果是计算型任务:就取cpu数量的1-2倍。
2、如果是IO型任务:则需要多一些线程,需根据具体的IO阻塞时长进行考量决定。tomcat中默认的最大线程数为200
也可考虑根据需要在一个最小和最大数量间自动增减线程数。

线程池工作原理

1、接收任务,放入任务仓库中
2、 工作线程从仓库取出任务,并执行取出的任务。
3、当没有任务时候,线程阻塞,当有任务时候唤醒线程执行。
这里也先用一张图解来形象表示:
在这里插入图片描述
图中的卡车表示线程池中工作的线程,任务仓库中存着需要执行的任务。下面再用提问讨论方式说明。

1、 那么工作中的线程(卡车)用什么表示?

1、Runnable 2、Callable
通过实现Runnable接口、Callable可以创建多线程,其中使用Callable有返回值,两种方式具体区别不细说。下面用代码写一下如何实现这个两个接口来建立线程。
Ruanable:

public class RunnableDemo2 {
//通过类实现接口
public static class TestRunnableDemo implements Runnable{
	@Override
	public void run() {
		System.out.println("类实现:"+Thread.currentThread().getName()+"在运行");
	}
	
}
public static void main(String[] args) {
	//直接声明
	Runnable runnable = new Runnable() {
		@Override
		public void run() {
			System.out.println("直接声明:"+Thread.currentThread().getName()+"在运行");
		}
	};
	//启动线程
	new  Thread(runnable).start();
	new Thread(new TestRunnableDemo()).start();
}
}

运行结果:

类实现:Thread-1在运行
直接声明:Thread-0在运行

Callable:

public class CallableDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
	Callable<String> callable = new Callable<String>() {
		@Override
		public String call() throws Exception {
			Random random = new Random();
			return "返回随机结果:"+random.nextInt(200);
		}
	}; 
	FutureTask<String> futureTask = new FutureTask<>(callable);
	
	new Thread(futureTask).start();
	String result = futureTask.get();
	System.out.println(result);
}
}

Callable代码看起来比runnable要复杂一点,但是可以自己拿到执行的返回值,并且可以抛出已检查的异常。

2、 任务仓库用什么?
用 BlockingQueue  阻塞队列,就是线程安全的队列    

BlockingQueue核心方法以四种形式出现:
在这里插入图片描述
抛出异常

  • 当阻塞队列满时,再往队列里add插入元素会抛出 java.lang.IllegalStateException: Queue full;
  • 当阻塞队列空时,再从队列里remove移除元素会抛出 java.util.NoSuchElementException

特殊值

  • 插入方法,成功true失败false
  • 移除方法,成功返回出队列的元素,队列里面没有就返回null。

一直阻塞

  • 当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产线程知道put数据或者响应中断退出
  • 当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程知道队列可用。

超时退出

  • 当阻塞队列满时,队列会阻塞生产者现场一定时间,超过限时后生产者线程会退出。

我们的任务会在仓库中进进出出,当仓库为空时,获取(take)操作是阻塞的;当仓库为满时,添加(put)操作是阻塞的。

这里我们把上述操作进行分类

插入方法:

add(E e) :

  • 添加成功返回true,失败抛IllegalStateException异常

offer(E e) :

  • 成功返回 true,如果此队列已满,则返回 false。

put(E e) :

  • 将元素插入此队列的尾部,如果该队列已满,则一直阻塞

删除方法:

remove(Object o) :

  • 移除指定元素,成功返回true,失败返回false

poll() :

  • 获取并移除此队列的头元素,若队列为空,则返回 null

take():

  • 获取并移除此队列头元素,若没有元素则一直阻塞。

检查方法

element() :

  • 获取但不移除此队列的头元素,没有元素则抛异常

peek() :

  • 获取但不移除此队列的头;若队列为空,则返回 null。
3、简单实现自己的线程池

前面说了讨论了线程池的组成,现在来把前面的部分整合起来实现自己的线程池。
怎么写?
思路:

1、首先要有自己任务仓库
2、要有多个线程(线程集合)
3、每个线程要去做的事--->从任务仓库(队列)中拿到任务
4、初始化线程池
5、将任务放入任务仓库,任务本身也就是线程
6、线程池的关闭
6.1 关闭的时候,总共分几步?
  a.仓库停止接受任务
  b.仓库中剩下的任务要执行完
  c.去仓库中拿任务时候就不应该阻塞了
  d.若有线程被阻塞了,就中断线程

代码实现:

public class FixedSizeThreadPool {
//1.仓库(实际就是一个任务队列)
private BlockingQueue<Runnable> blockingQueue;
//2.线程集合
private List<Thread> workers;
//3.每一个线程要去干的事--->队列中拿任务
public static class Worker extends Thread{
	private FixedSizeThreadPool pool;
	
	public Worker(FixedSizeThreadPool pool) {
		this.pool = pool;
	}
	@Override
	public void run() {
		//到队列中拿任务并执行
		while (this.pool.blockingQueue.size()>0||this.pool.isWorking){
			Runnable task = null;
			try {
				if(this.pool.isWorking) {
					//阻塞的方式拿任务
					task = this.pool.blockingQueue.take();
				}else {
					//不阻塞的方式拿
					task = this.pool.blockingQueue.poll();
				}
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			if(task!=null) {
				task.run();
				System.out.println("线程:"+Thread.currentThread().getName()+"执行完毕!");
			}
		}
	}
}
//4.初始化线程池
public FixedSizeThreadPool(int poolSize,int taskSize) {
	if(poolSize<=0||taskSize<=0) {
		throw new IllegalArgumentException("非法参数!");
	}
	this.blockingQueue = new LinkedBlockingDeque<>(taskSize);
	//使得 ArrayList 变的安全
	this.workers = Collections.synchronizedList(new ArrayList<Thread>());
	for(int i=0;i<poolSize;i++) {
		Worker worker = new Worker(this);
		worker.start();
		workers.add(worker);
	} 
}
//5.将任务放入队列,这里相当与提交的任务,提交的任务也是线程
public boolean submit(Runnable task) {
	if(this.isWorking) {
		return this.blockingQueue.offer(task);
	}else {
		return false;
	}
}

//6.线程池的关闭
//a.仓库停止接受任务
//b.仓库中剩下的任务要执行完
//c.去仓库中拿任务时候就不应该阻塞了
//d.若有线程被阻塞了,就中断线程
private volatile boolean isWorking = true;
public void shutDown(){
	this.isWorking = false;
	
	for (Thread thread : workers) {
		if(Thread.State.BLOCKED.equals(thread.getState())) {
			//中断线程
			thread.interrupt();
		}
	}
}
//调用线程池
public static void main(String[] args){
	//实例化线程池
	FixedSizeThreadPool pool=new FixedSizeThreadPool(3, 6);
	int task= 6;
	//放入6个任务
	for(int i=0;i<task;i++) {
		pool.submit(new Runnable() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				System.out.println("任务执行");
				try {
					TimeUnit.SECONDS.sleep(2);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		});
	}
	//关闭线程池
	pool.shutDown();
}
}

运行结果:

任务执行
任务执行
任务执行
线程:Thread-2执行完毕!
任务执行
线程:Thread-0执行完毕!
任务执行
线程:Thread-1执行完毕!
任务执行
线程:Thread-2执行完毕!
线程:Thread-0执行完毕!
线程:Thread-1执行完毕!

从打印顺序可以看出,3个线程执行6个任务,while循环了两次,第一次三个线程各自拿到了一个任务,执行任务后阻塞了2秒,进行第二次执行。这样就完成了一个简单的线程池的实现。

后面简单说一说下常用Java线程池

4、Java线程池API

Java并发包中提供了丰富的线程池实现!其中常用的有两类 ThreadPoolExecutor、Executors。后面直接用图片展示。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
线程池实现方式:
在这里插入图片描述

ThreadPoolExecutor 的使用,代码示例:

针对 runnable

public class RunnableDemo {
//线程池
static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(50, 100, 1000, TimeUnit.MICROSECONDS,new LinkedBlockingQueue<Runnable>());
//任务1
public static class TestRunnableDemo implements Runnable{
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName()+"在运行");
	}
	
}

public static void main(String[] args) {
	//任务2
	Runnable runnable = new Runnable() {
		@Override
		public void run() {
			System.out.println(Thread.currentThread().getName()+"main中在运行");
		}
	};
	//线程池执行任务
	threadPoolExecutor.execute(runnable);
	threadPoolExecutor.execute(runnable);
	threadPoolExecutor.execute(runnable);
	threadPoolExecutor.execute(runnable);
	threadPoolExecutor.execute(runnable);
	threadPoolExecutor.execute(new TestRunnableDemo());
	//关闭线程池
	threadPoolExecutor.shutdown();
}
}

运行结果:

pool-1-thread-2main中在运行
pool-1-thread-3main中在运行
pool-1-thread-4main中在运行
pool-1-thread-5main中在运行
pool-1-thread-1main中在运行
pool-1-thread-6在运行

针对Callable

public class CallableDemo2 {
//线程池
static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(50, 100, 1000, TimeUnit.MICROSECONDS,new LinkedBlockingQueue<Runnable>());

public static void main(String[] args) throws InterruptedException, ExecutionException {
	//任务
	Callable<String> callable = new Callable<String>() {
		@Override
		public String call() throws Exception {
			Random random = new Random();
			System.out.println(Thread.currentThread().getName()+"执行任务");
			return "返回随机结果:"+random.nextInt(200);
		}
	}; 
	//提交任务并执行
	threadPoolExecutor.submit(callable);
	
	Future<String> future = threadPoolExecutor.submit(callable);
	
	String result = future.get();
	System.out.println(result);
	threadPoolExecutor.shutdown();
}
}

运行结果:

pool-1-thread-1执行任务
pool-1-thread-2执行任务
返回随机结果:21

以上两个针对callable 和runnable 使用线程池不同的是一提交任务方法不同,Submit()返回一个方法Future结果,Execute()无返回值。

注:本篇笔记是自己在学习公开知识内容过程中整理的,有不同见解的朋友可以指点指点。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值