【重难点】【JUC 05】线程池核心设计与实现、线程池使用了什么设计模式、要你设计的话,如何实现一个线程池

【重难点】【JUC 05】线程池核心设计与实现、线程池使用了什么设计模式、要你设计的话,如何实现一个线程池

一、线程池核心设计与实现

线程池核心实现类是 ThreadPool,下图为 ThreadPoolExcutor 的 UML 类图

在这里插入图片描述

ThreadPoolExcutor 实现的顶层接口是 Executor,顶层接口 Excutor 提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程、如何调度线程来执行任务,用户只需要提供 Runnable 对象,将任务的运行逻辑提交到执行器(Executor)中,由 Executor 框架完成线程的调配和任务的执行部分

ExecutorService 接口增加了一些能力:

  1. 扩充执行任务的能力,补充可以为一个或一批异步任务生成 Future 的方法
  2. 提供了管控线程池的方法,比如停止线程池的运行

AbstractExecutorService 则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可

最下层的实现类 ThreadPoolExecutor 实现最复杂的运行部分,ThreadPoolExecutor 将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好地结合从何执行并行任务

ThreadPoolExecutor 的运行机制如下图所示:

在这里插入图片描述

线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好地缓冲任务,复用线程。线程池的运行主要分为两部分:任务管理、县城管理。任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流传:

  1. 直接申请线程执行该任务
  2. 缓冲到队列中等待线程执行
  3. 拒绝该任务

线程管理部分是消费者,它们被同一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收

二、线程池使用了什么设计模式

数据库连接池、线程池本质上都是连接池技术,连接池技术的核心是让创建的资源复用,通过减少创建和销毁来提升性能

这正是享元模式的理念,享元模式的目的是实现对象的共享,当系统中对象多的时候可以减少内存的开销,通常与工厂模式一起使用

FlyWeightFactory 负责创建和管理享元单元,当一个客户端请求时,工厂需要检查当前对象池中是否有符合条件的对象,如果有,就返回已经存在的对象,如果没有,则创建一个新对象

设计模式详见【设计模式】第四章 工厂模式【设计模式】第七章 享元模式

三、要你设计的话,如何实现一个线程池

1.线程池的关键变量

  • 一个存放所有线程的集合
  • 一个任务分配给线程池的时候,线程池可以分配一个线程处理它,当线程池中没有空闲线程时,我们还需要一个队列来存储提交给线程池的任务
  • 初始化一个线程时,要指定这个线程池的大小
  • 我们还需要一个变量来保存已经运行的线程数目
//存放线程的集合
private ArrayList<MyThread> threads;
//任务队列
private ArrayBlockingQueue<Runnable> taskQueue;
//线程池初始限定大小
private int threadNum
//已经工作的线程数目
private int workThreadNum;

2.线程池的核心方法

执行任务

向线程池提交一个任务时,如果已经运行的线程 < 线程池大小,则创建一个线程运行任务,并把这个线程放入线程池,否则将任务放入缓冲队列中

public void execute(Runnable runnable){
	try{
		mainLock.lock();
		//线程池未满,每加入一个任务则开启一个线程
		if(workThreadNum < threadNum){
			MyThread myThread = new MyThread(runnable);
			myThread.start();
			threads.add(myThread);
			workThreadNum++;
		}
		//线程池已满,放入任务队列,等待有空闲线程时执行
		else{
			//队列已满,无法添加时,拒绝任务
			if(!taskQueue.offer(runnable)){
				rejectTask();
		}
	}finally{
		mainLock.unlock();
	}
}

从任务队列中取出任务,分配给线程池中 “空闲” 的线程完成

很容易想到的思路是额外开启一个线程,时刻监控线程池的线程空余情况,一旦有线程空余,则马上从任务队列取出任务,交付给空余线程完成

这种思路理解起来很容易,但仔细思考,实现起来非常麻烦。第一,如何检测到线程池中的空闲线程是一个问题。第二,如何将任务交付给一个 .start() 运行状态中的空闲线程。想要实现这两点都不轻松

所以我们转换一下思维,线程池中的所有线程一直都是运行状态的,线程的空闲只是代表此刻它没有在执行任务而已。一旦运行中的线程没有执行任务,就自己去队列中取任务执行

为了达到这种效果,我们要重写 run 方法,所以要自定义一个 MyThread 类,把线程都放在这个自定义线程类

class MyThread extends Thread{
	private Runnable task;

	public MyThread(Runnable runnable){
		this.task = runnable;
	}
	@Override
	public void run(){
		//该线程一直启动着,不断地从任务队列取出任务执行
		while(true){
			//如果初始化任务不为空,则执行初始化任务
			if(task != null){
				task.run();
				task = null;
			}
			//否则去任务队列取任务并执行
			else{
				Runnable queueTask = taskQueue.poll();
				if(queueTask != null)
					queueTask.run();
			}
		}
	}
}

完整代码

public class MyThreadPool{

	public static void main(String[] args){
		MyThreadPool myThreadPool = new MyThreadPool(5);
		Runnable task = new Runnable(){
			@Override
			public void run(){
				System.out.println(Thread.currentThread().getName()+"执行中");
			}
		};

		for(int i = 0; i < 20; i++){
			myThreadPool.execute(task);
		}
	}

	//存放线程的集合
	private ArrayList<MyThread> threads;
	//任务队列
	private ArrayBlockingQueue<Runnable> taskQueue;
	//线程池初始限定大小
	private int threadNum;
	//已经工作的线程数目
	private int workThreadNum;

	private final ReentrantLock mainLock = new ReentrantLock();

	public MyThreadPool(int initPoolNum){
		threadNum = initPoolNum;
		threads = new ArrayList<>(initPoolNum);
		//任务队列初始化为线程池线程数的四倍
		taskQueue = new ArrayBlockingQueue<>(initPoolNum*4);

		threadNum = initPoolNum;
		workThreadNum = 0;
	}

	public void execute(Runnable runnable){
		try{
			mainLock.lock();
			//线程池未满,每加入一个任务则开启一个线程
			if(workThreadNum < threadNum){
				MyThread myThread = new MyThread(runnalbe);
				myThread.start();
				thread.start();
				threads.add(myThread);
				workThreadNum++;
			}
			//线程池已满,放入任务队列,等待有空闲线程时执行
			else{
				//队列已满,无法添加时,拒绝任务
				if(!taskQueue.offer(runnable)){
					rejectTask();
				}
			}
		}finally{
			mainLock.unlock();
		}
	}

	private void rejectTask(){
		System.out.println("任务队列已满,无法继续添加,请扩大您的初始化线程池");
	}

	class MyThread extends Thread{
		private Runnable task;

		public MyThread(Runnable runnable){
			this.task = runnable;
		}
		
		@Override
		public void run(){
			//该线程一直启动着,不断地从任务队列取出任务执行
			while(true){
				//如果初始化任务不为空,则执行初始化任务
				if(task != null){
					task.run();
					task = null;
				}
				//否则去任务队列取任务并执行
				else{
					Runnable queueTask = taskQueue.poll();
					if(queueTask != null)
						queueTask.run();
				}
			}
		}
	}
}	

总结一下自定义线程池的工作流程:

  1. 初始化线程池,指定线程池大小
  2. 向线程池中放入任务执行
  3. 如果线程池中创建的线程数未满,则创建我们自定义的线程类放入线程池集合,并执行任务。任务完成后该线程会一直监听队列
  4. 如果线程池中创建的线程数已满,则将任务放入缓冲任务队列
  5. 线程池中所有创建的线程都会一直从缓存任务队列中取任务,取到任务后立即执行
### 回答1: Java 并发编程中有许多难点,其中一些常见的包括: 1. 线程安全问题:多线程环境下,如果没有足够的同步机制,可能会导致数据不一致的情况。 2. 线程同步问题:多线程环境下,如何使用同步机制来协调线程之间的协作。 3. 线程通信问题:多线程环境下,如何在线程之间传递信息。 4. 线程死锁问题:多线程环境下,当线程之间存在相互等待的情况,可能会导致线程死锁。 5. 线程优先级问题:Java 中的线程有优先级的概念,如何调整线程优先级以提高系统性能。 6. 线程池问题:Java 中可以使用线程池来管理线程,但是如何配置线程池以及如何使用线程池可能是一个难点。 ### 回答2: Java并发编程的难点主要有以下几个方面: 1. 线程安全:在多线程环境中,多个线程同时访问共享数据可能引发竞态条件,如数据不一致、死锁等问题。为了保证线程安全,需要正确使用synchronized关键字或者并发容器(如ConcurrentHashMap),并处理好线程间的通信和同步。 2. 死锁:死锁是指两个或多个进程在无法继续执行的状态下互相等待对方释放资源,从而导致系统无法继续运行。在Java并发编程中,死锁可能发生在多个线程之间,解决死锁的难点在于正确地设计锁的获取顺序,并避免出现循环等待的情况。 3. 并发控制:在并发编程中,需要根据实际需求选择合适的并发控制方式,如互斥锁、信号量、条件变量等。选择合适的并发控制方式并使用正确的方法是一个难点。 4. 线程间通信:多个线程之间需要进行信息交换和同步,而线程间通信存在着争用条件、死锁等问题。在Java中,可以使用wait和notify方法、Condition对象等实现线程间的通信,但正确使用这些方法也是一个难点。 5. 性能优化:在并发编程中,为了提高程序的性能,需要考虑线程的创建和销毁、锁的粒度、线程池使用等问题。合理地优化并发程序的性能也是一个难点。 总之,Java并发编程的难点包括线程安全、死锁、并发控制、线程间通信和性能优化等方面,需要对并发编程的原理和相关API有深入的理解,并结合实际需求选择合适的并发控制方式,才能开发出稳定高效的并发程序。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

313YPHU3

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值