并发编程系列(1)- 线程池原理

一、线程池基本原理分析

1、什么是线程池

线程池就是一种池化技术,核心思想就是事先创建多个线程,将线程资源放到池子中,这样任务到达时可以 不需要等到线程创建就能立刻去执行。
创建线程池的好处:

  • 降低资源消耗。线程池可避免大量线程的创建于销毁造成的消耗。
  • 提高响应速度。任务到达时,线程池中线程可立即去执行,无需等待线程的创建。
  • 提高线程的可管理性。利用线程池可以进行统一分配、调优和监控。

2、线程池原理

当向线程池提交一个任务的时候,工作原理如下:
(1)首先判断核心线程池中(corePool)线程是否都在执行任务。如果不是,则在核心线程池中创建一个线程来执行任务。如果核心线程池的线程都在执行任务,则进入第2步;
(2)判断工作队列(BlockingQueue)是否已经满。如果工作队列没有满,则将新提交的任务存储在工作队列中(此时核心线程池中的某个线程执行完任务后,就会执行工作队列中存储的任务)。如果工作队列已经满了,则进入第3步;
(3)判断线程池(maximumPool)所有线程是否都处在工作状态,如果不是,则创建一个线程来执行任务(注:非核心线程池中的线程);如果是,则交给饱和策略来处理这个任务。

线程池工作原理示意图如下图所示:
并发编程系列(1)- 线程池原理_第1张图片

3、线程池ThreadPoolExecutor源码分析

线程池的核心实现类是ThreadPoolExecutor,所以首先看看ThreadPoolExecutor中最重要的方法:execute()。
注:下面源码来自JDK 1.8.

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
    
        int c = ctl.get();
        // 1、如果当前线程数小于核心线程数,就在核心线程池中创建线程执行任务
        if (workerCountOf(c) < corePoolSize) {
            //true-代表在corePool中创建线程,添加成功返回
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        
        // 2、若线程数大于等于corePool线程数量,就将任务放入队列workQueue
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            // 再次确认线程池是否正在运行(可能崩溃了),如果不是就将刚刚添加的任务移除
            if (! isRunning(recheck) && remove(command))
                reject(command);
            // 3、检查正在工作的线程数,如果为0表示corePool都执行完成并停止,呢么需要在maximumPool中添加线程,执行任务
            else if (workerCountOf(recheck) == 0)
                // false-代表在maximum中添加线程
                addWorker(null, false);
        }
         // 4、如果超过maximumPool最大线程数量,抛异常
        else if (!addWorker(command, false))
            reject(command);
    }

二、线程池的使用

1、线程池的创建

可以利用ThreadPoolExecutor来创建一个线程池。
ThreadPoolExecutor是线程池中最核心的一个类。
下面是其中常用的一个构造方法。

	public ThreadPoolExecutor(
		int corePoolSize,
		int maximumPoolSize,
		long keepAliveTime,
		TimeUnit unit,
		BlockingQueue workQueue,
		ThreadFactory threadFactory,
		RejectedExecutionHandler handler
	);

1)corePoolSize:线程池的大小(也叫核心线程池)。默认情况下,核心线程池是没有创建线程的,只有任务到达时,才会创建线程去执行。
2)maximumPoolSize:线程池允许创建的最大线程数。如果核心线程池满了,且队列满了,就会在线程池中再创建线程去执行任务。
3)keepAliveTime:线程保持存活的时间。当线程执行完任务后,若超过这个keepAliveTime仍然没有任务到达,就销毁。
4)unit:参数keepAliveTime的时间单位,有7种取值:

  • 天(TimeUnit.DAYS)
  • 小时(TimeUnit.HOURS)
  • 分钟(TimeUnit.MINUTES)
  • 毫秒(TimeUnit.MILLISECONDS)
  • 微妙(TimeUnit.MICROSECONDS)
  • 纳秒(TimeUnit.NANOSECONDS)

5)workQueue:任务队列,用于存放等待执行的任务的阻塞队列。有以下几种阻塞队列:

  • ArrayBlockingQueue:基于数组的有界阻塞队列,FIFO顺序。
  • LinkedBlockingQueue:基于链表的无界阻塞队列,FIFO顺序。
  • SynchronousQueue:不存储元素的阻塞队列。往此队列插入任务时需等待之前的任务移除后才能执行,否则会被阻塞。
  • PriorityBlockingQueue:具有优先级的无界阻塞队列。

6)ThreadFactory:线程工厂,主要用来创建线程;
7)RejectedExecutionHandler :饱和策略。当线程池和队列都满时需要采取的一种措施。
有以下4种:

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy: 也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy: 丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy: 由调用线程处理该任务。

也可以自己实现RejectedExecutionHandler 。

2、 向线程池提交任务

可以通过execute() 或 submit() 向线程池提交任务。
区别:

  • execute() 用于提交不需要返回值的任务;
threadPool.execute(
	new Runnable(){
		@Override
		public void run(){
			......
		}
	}
);
  • submit() 用于提交需要返回一个Future类型值的任务,可以通过future.get()查看;
Future future = executor.submit(task);
Object value = future.get();
 
  

3、 关闭线程池

调用ThreadPoolExecutor的shutDown() 和 shutDownNow() 来关闭线程池。区别在于:
shutDown() :中断没有执行任务的线程,将线程池状态设为SHUTDOWN ,此时线程池不能接受新任务,等待所有任务执行结束;
shutDownNow() :此时线程池不能接受新任务且尝试停止所有运行或暂停的线程,将线程池状态设为STOP。

4、 线程池状态

ThreadPoolExecutor中定义了一个volatile变量runState代表线程池状态:
volatile int runState;
下面是可能的几个取值:

static final int RUNNING = 0;
static final int SHUTDOWN = 1;
static final int STOP = 2;
static final int TERMINATED = 3;

1)创建线程池,初始时,线程池处于RUNNING 状态;
2)调用shutDown()后,变为 SHUTDOWN 状态;
3)调用shutDownNow()后,变为 STOP 状态;
4)当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。

5、 合理配置线程池

1)当任务为CPU密集型任务的时候,应配置较小线程如Ncpu+1个线程;
2)当任务时IO密集型的任务时候,则应配置尽可能多的任务,如2*Ncpu.
3)任务执行有优先级的时候,可用优先级队列PriorityBlockingQueue来处理,让优先级高的先执行;
4)当任务执行时间不同时,也可用优先级队列PriorityBlockingQueue让执行时间短的线程先执行。
5)建议使用有界队列,能增加系统的稳定性和预警能力。
如用无界队列,当一个任务执行过程中阻塞,会导致任务在队列中不断积压而得不到执行,最终导致系统崩溃。

具体设置参数参考
假设每秒的任务数为 tasks: 100~500; 每个任务处理的时间为costTime:0.1s;
系统的响应超时时间为responseTime:1s

  • corePoolSize = tasks / (1/costTime) = tasks * costTime = 10 ~ 50
    假设系统中80%的任务每秒任务数为200, 那么corePoolSize = 200 * 0.1 = 20
  • queueCapacity = (corePoolSize/costTime)responseTime = 20/0.11 = 200
  • maxPoolSize = (最大任务数-队列容量)/每个线程每秒处理能力 = (tasks-queueCapacity)/(1/costTime) =(500-200)/10=30

三、 Java手撸一个简单的线程池

package com.wgs.多线程;

import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

interface ThreadPool{
	//执行一个Job任务,需要实现Runnable
	void execute(Task task);
	//关闭线程池
	void shutDown();
	//增加工作者线程
	void addWorkers(int num);
	//移除工作者线程
	void removeWorkers(int num);
	//获取等待队列中的任务数量
	int getWorkingQueueSize();
}

class DefaultThreadPool implements ThreadPool{

	private static final int MAX_WORKER_NUMBERS = 10;
	private static final int DEFAULT_WORKER_NUMBERS = 5;
	private static final int MIN_WORKER_NUMBERS = 1;
	
	//工作队列
	private final LinkedList bolockingQueue = new LinkedList<>();
	//线程池中处理任务的线程列表
	private final LinkedList workers = new LinkedList<>();
	private int workerNum = DEFAULT_WORKER_NUMBERS;
	
	private AtomicLong threadNum = new AtomicLong();
	
	//创建默认大小的线程池
	public DefaultThreadPool() {
		initializeWorkers(DEFAULT_WORKER_NUMBERS);
	}
	//创建指定大小的线程池
	public DefaultThreadPool(int num) {
		workerNum = num > MAX_WORKER_NUMBERS ? MAX_WORKER_NUMBERS 
				: num < MIN_WORKER_NUMBERS ? MIN_WORKER_NUMBERS : num;
		initializeWorkers(workerNum);
	}
	//初始化线程池中的线程,即事先创建多个线程放入线程池中
	private void initializeWorkers(int num) {
		for(int i = 0; i < num; i++){
			Worker workerThread = new Worker();
			workers.add(workerThread);
			
			Thread thread = new Thread(workerThread, "ThreadPool-Worker-" + threadNum.incrementAndGet());
			thread.start();
			//workerThread.setName("ThreadPool-Worker-" + threadNum.incrementAndGet());
			//workerThread.start();
		}
	}

	//放入工作队列中,执行任务
	@Override
	public void execute(Task task) {
		if(task != null){
			synchronized (bolockingQueue) {
				bolockingQueue.addLast(task);
				bolockingQueue.notify();
			}
		}
		
	}

	@Override
	public void shutDown() {
		for(Worker worker : workers){
			worker.shutDown() ;
		}
		
	}

	//添加工作线程
	@Override
	public void addWorkers(int num) {
		synchronized (bolockingQueue) {
			if(num + this.workerNum > MAX_WORKER_NUMBERS){
				num = MAX_WORKER_NUMBERS - this.workerNum;
			}
			initializeWorkers(num);
			this.workerNum += num;
		}
		
	}

	//获取工作队列大小
	@Override
	public int getWorkingQueueSize() {
		return bolockingQueue.size();
	}
	
	//停止工作线程
	@Override
	public void removeWorkers(int num) {
		synchronized (bolockingQueue) {
			if(num >= this.workerNum){
				throw new IllegalArgumentException("参数错误");
			}
			int count = 0;
			while(count < num){
				Worker worker = workers.get(count);
				if(workers.remove(worker)){
					worker.shutDown();
					count++;
				}
			}
			this.workerNum -= count;
		}
		
	}
	
	
	//工作线程,负责处理任务
	class Worker implements Runnable{

		private volatile boolean running = true;
		@Override
		public void run() {
			while(running){
				Task task;
				synchronized (bolockingQueue) {
					while(bolockingQueue.isEmpty()){
						try {
							bolockingQueue.wait();
						} catch (InterruptedException e) {
							Thread.currentThread().interrupt();
							return;
						}
					}
					//取出工作队列的头任务执行
					task = bolockingQueue.removeFirst();
				}
				if(task != null){
					task.run();
				}
			}
		}
		
		public void shutDown(){
			running = false;
		}
		
	}
	
	
}

public class ThreadPoolDemo {
	
	
}

客户端调用execute(Task),不断向工作队列WorkingQueue中添加任务Task;添加一个任务,就会唤醒一个工作队列线程进行处理。
而线程池中的工作线程会不断从WorkingQueue中取出任务进行执行。当WorkingQueue为空的时候,线程进入等待状态。

可以看出:线程池的本质就是使用了一个线程安全的工作队列连接工作者线程和客户端线程,客户端线程将任务放入工作队列后返回;而工作线程不断从工作队列上取出任务并返回。
当工作队列为空时,所有工作者线程均等待工作队列上。当有客户端提交一个任务后会通知任意一个工作线程。

你可能感兴趣的:(多线程/并发)