线程池原理手写实现

线程池原理手写实现

一、使用线程池的好处

  • 降低资源消耗:通过重复利用线程降低线程创建和销毁的资源消耗。
  • 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性:线程是稀缺资源,使用线程池可以统一对其进行分配、调优和监控。

二、线程池实现原理

线程池原理手写实现_第1张图片

当一个任务被提交至线程池,线程池处理任务的流程是:

  • 首先线程池会判断核心线程池里的线程是否都在执行任务。如果不是,就创建一个新的线程来执行任务。如果是,则进入下个流程。
  • 线程池判断工作队列中的线程是否已满。如果未满,则将新提交的任务存储在这里,如果满了,进行下个流程。
  • 线程池判断整个线程池的线程是否都处于工作状态,如果没有,则创立新的线程执行此任务,如果满了,则交给饱和策略来处理。

之所以这样处理,是想要尽可能避免获取全局锁,上面的步骤二无需获得全局锁。线程池在创建线程时,会将线程封装成工作线程worker,worker在执行完任务后,还会从工作队列中获取新的任务来执行。

三、线程池的使用

我们先看一下ThreadPoolExecutor这个线程池的老祖宗。看看它的构造器:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

创建一个线程池所需要的核心参数如下:

  • corePoolSize(线程池的基本大小):当一个任务被提交到线程池时,线程池会创建一个新的线程来处理这个任务,当线程数大于或等于corePoolSize时便不再创建。
  • runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列,通常有ArrayBlockingQueue(有界阻塞队列)、LinkedBlockingQueue(无界阻塞队列)、SynchoronousQueue(一个不存储元素的阻塞队列)、PriorityBlockingQueue(优先级阻塞队列)。
  • maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果使用无界队列,则这个参数便没有意义,因为无界队列可以无限制地接受任务。
  • ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程一个有意义的名字。
  • RejectExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态。此时要对进来的任务采取一种处理策略,通常有如下四种策略
    • AbortPolicy:直接抛出异常
    • CallerRunsPolicy:只用调用者所在的线程来运行任务
    • DiscardOldestPolicy:丢弃掉任务队列里最近的一个任务,并执行当前任务。
    • DiscardPolicy:不处理,直接丢掉。
    • 自定义策略,比如持久化存储和记录日志等。
  • keepAliveTime(线程活动保持时间):线程池工作线程空闲后,需要保持存活的时间,为了提高线程的复用性,如果每个任务执行时间都很短,则可以调大此值,提高效率。
  • TimeUnit:线程活动时间保持单位。

四、向线程池提交任务的两种方式

  • excute()方法用于提交无需返回值的任务,需要传入的是一个Runnable实例。
  • submit()用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过future对象可以判断任务是否返回成功。可以通过得到的future对象的get()方法得到处理结果,get()方法会阻塞当前线程直到任务完成。

五、关闭线程池

线程池通过遍历线程池中的工作线程,逐个调用线程的interrupt方法中断线程。

shutdown方法会将线程池的状态设置为SHUTDOWN,然后中断没有正在执行任务的线程。是一种委婉的关闭。

shutdownNow会先将线程池状态设置为STOP,然后尝试停止所有正在执行或者暂停任务的线程。是一种强制关闭的策略。

六、合理配置线程池

可以从以下角度分析:任务的性质是I/O密集型还是CPU密集型?任务的优先级有区分吗?任务执行时间的长短?任务的依赖性?是否依赖于其他资源(比如数据库连接)?

  • CPU密集型任务应该配置尽可能少的线程数,比如N+1。
  • I/O密集型可以配置尽肯能多的线程数,比如2*N。
  • 混合型任务可以拆分为上述两种类型,再分别设置。
  • 优先级不同的任务可以将工作队列配置为优先级队列。
  • 如果CPU空闲时间越长,则可以设置越多的线程数。
  • 建议使用有界队列,增加系统稳定性和预警能力。

七、Excutor框架——ThreadPoolExcutor

FixedThreadPool

FixedThreadPool是固定线程数的线程池。有时我们为了满足资源管理的需求,需要限制当前线程数量,比如用于负载较重的服务器。

特点:

它的核心线程出和固定线程数都被设置成了一样的大小,且不为0。但是它的线程存活时间被设置成了0,且使用的是无界阻塞队列(LinkedBlockingQueue)。

这意味着多余的空闲线程会被立即终止,无法复用。且线程池中的线程数不会超过核心线程数,因为无法立即执行的任务都跑到无界队列中去了,当核心线程数小于初始值时,再去队列中poll()任务就可以了。

当然,由于是无界队列,其也不会拒绝任务。

SingleThreadExcutor

SingleThreadExcutor是使用单个worker线程的Executor。适用于保证顺序地执行各个任务,在任意时间点,不会有多个线程是活动的应用场景。

特点:其核心线程数和最大线程数都被设置为1。也是使用的无界队列(LinkedBlockingQueue)。它的运行机制与特性与FixedThreadPool相似,最大的不同就是此线程池中永远只有一个正在执行任务的线程。

CachedThreadPool

它是大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者负载较轻的服务器。

特点:它的核心线程数量被设置为0,最大线程数量被设置为最大值。而线程存活时间为60s,意味着此线程池中空闲线程等待任务的时间为60s。

它使用无容量的SynchoronousQueue作为线程池的工作队列。如此设置,意味着,如果主线程提交任务的速度高于线程池处理任务的速度,则线程池会不断地创建新的线程,极端情况下会耗尽CPU和内存资源。

SynchoronousQueue没有容量,在这里起到什么作用呢?其实如果不设置60s的存活时间,那么SynchoronousQueue意义就不大,效率也不会高,我们在小程序较多的场景中,追求的就是效率,SynchoronousQueue起到一个传递者的作用,如果使用其他的有界或者无界队列,虽然可以达到资源的控制目的,但是在此场景下非常耗费时间,所以我们需要一个控制者,而不是一个容器。试想一下,如果一个小任务很快被执行(poll->excute)完了,还不到60s,此时刚刚所用的线程可以继续复用,而中间的步骤资源开销很少。

八、手写一个线程池

参数

  1. 设置默认的线程数,如果用户没有设置,就用默认的。

    private static int WORK_NUM = 5;

  2. 设置默认的任务个数(这里指的是默认的工作队列的大小,使用有界队列)。

    private static int TASK_COUNT = 100;

  3. 设置存放正在工作线程的容器,这里我们用一个工作线程类型的数组来实现,这个数组主要用于在开启或者关闭线程时,可以遍历到设置的线程。

    private WorkThread[] workThreads;

  4. 设置尚未工作的线程存放队列容器。这里我们会使用ArrayBlockingQueue。

    private final BlockingQueue taskQueue;

  5. 构造线程数。用户自定义的线程数。

    private final int worker_num

编写工作线程类

通过继承Thread类的方法,重写run()。

run()主要做的是:如果没有被shutdown()中断,那么会一直轮询,从工作队列中取出任务来执行。执行完之后需要释放掉引用,以免内存溢出。

设置停止线程的方法stopWorker()。直接使用interrupt()即可。

 private class WorkThread extends Thread{  	
    	@Override
    	public void run(){
    		Runnable r = null;
    		try {
				while (!isInterrupted()) {
					r = taskQueue.take();
					if(r!=null) {
						System.out.println(getId()+" ready exec :"+r);
						r.run();
					}
					r = null;//help gc;
				} 
			} catch (Exception e) {
					e.printStackTrace();
			}
    	}
    	
    	public void stopWorker() {
    		interrupt();
    	}
    	
    }

编写初始化方法

public MyThreadPool2(int worker_num,int taskCount) {
      	//用户传入的工作线程数如果小于0,就用默认的
    	if (worker_num<=0) worker_num = WORK_NUM;
    	//用户传入任务数如果小于0,就用默认的
    	if(taskCount<=0) taskCount = TASK_COUNT;
        this.worker_num = worker_num;
    	//初始化任务队列容器
        taskQueue = new ArrayBlockingQueue<>(taskCount);
    	//初始化工作线程容器
        workThreads = new WorkThread[worker_num];
    	//创建并开启工作线程容器中的全部线程
        for(int i=0;i<worker_num;i++) {
            workThreads[i] = new WorkThread();
            workThreads[i].start();
        }
    }

执行任务

当用户执行execute()的时候,只管往里扔就行,无需显示地调用start()方法。因为线程池会通过轮询自动从工作队列中取出任务去执行。

 public void execute(Runnable task) {
    	try {
			taskQueue.put(task);//扔到队列中即可
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
    }

销毁线程池

public void destroy() {
        // 工作线程停止工作,且置为null
        System.out.println("ready close pool.....");
        for(int i=0;i<worker_num;i++) {
        	workThreads[i].stopWorker();
        	workThreads[i] = null;//help gc
        }
        taskQueue.clear();// 清空任务队列
    }

全部代码

package com.xiangxue.ch6.mypool;

import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;

public class MyThreadPool2 {
    // 线程池中默认线程的个数为5
    private static int WORK_NUM = 5;
    // 队列默认任务个数为100
    private static int TASK_COUNT = 100;  
    
    // 工作线程组
    private WorkThread[] workThreads;

    // 任务队列,作为一个缓冲
    private final BlockingQueue<Runnable> taskQueue;
    private final int worker_num;//用户在构造这个池,希望的启动的线程数

    // 创建具有默认线程个数的线程池
    public MyThreadPool2() {
        this(WORK_NUM,TASK_COUNT);
    }

    // 创建线程池,worker_num为线程池中工作线程的个数
    public MyThreadPool2(int worker_num,int taskCount) {
    	if (worker_num<=0) worker_num = WORK_NUM;
    	if(taskCount<=0) taskCount = TASK_COUNT;
        this.worker_num = worker_num;
        taskQueue = new ArrayBlockingQueue<>(taskCount);
        workThreads = new WorkThread[worker_num];
        for(int i=0;i<worker_num;i++) {
            workThreads[i] = new WorkThread();
            workThreads[i].start();
        }
    }


    // 执行任务,其实只是把任务加入任务队列,什么时候执行有线程池管理器决定
    public void execute(Runnable task) {
    	try {
			taskQueue.put(task);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

    }


    // 销毁线程池,该方法保证在所有任务都完成的情况下才销毁所有线程,否则等待任务完成才销毁
    public void destroy() {
        // 工作线程停止工作,且置为null
        System.out.println("ready close pool.....");
        for(int i=0;i<worker_num;i++) {
        	workThreads[i].stopWorker();
        	workThreads[i] = null;//help gc
        }
        taskQueue.clear();// 清空任务队列
    }

    // 覆盖toString方法,返回线程池信息:工作线程个数和已完成任务个数
    @Override
    public String toString() {
        return "WorkThread number:" + worker_num
                + "  wait task number:" + taskQueue.size();
    }

    /**
     * 内部类,工作线程
     */
    private class WorkThread extends Thread{
    	
    	@Override
    	public void run(){
    		Runnable r = null;
    		try {
				while (!isInterrupted()) {
					r = taskQueue.take();
					if(r!=null) {
						System.out.println(getId()+" ready exec :"+r);
						r.run();
					}
					r = null;//help gc;
				} 
			} catch (Exception e) {
				e.printStackTrace();
			}
    	}
    	
    	public void stopWorker() {
    		interrupt();
    	}
    	
    }
}

缺点

  • 可执行线程数量写死了,不能动态调整。
  • 当线程池中任务数量到达临界值时(工作队列中满了,线程也用光了),没有合适的拒绝策略。

可以使用JDK中的Executors工具类获取已经实现的线程池,也可以继承ThreadPoolExecutor类自定义线程池。

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