《Java并发编程的艺术》——线程池

一 使用线程池的好处

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗
  2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,不能一直创建。线程池还可以进行调优和监控。

二 如何创建线程池

通过ThreadPoolExecutor 来创建线程池

	    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  1. corePoolSize,核心线程池的大小(也是线程的基本大小,先吃池维护的线程数)。当一个任务被提交到线程池的时候,线程池会创建一个线程来执行任务。即使当前有线程是空闲状态也会创建线程。直到线程数等于corePoolSize的时候 才不会在创建。如果调用了线程池的 prestarAllCoreThreads()方法,线程池会提前创建并启动所有的核心线程。需要理解的是,线程池中并没有区分核心线程池和一般线程池,corePoolSize只是一个计数的功能。
  2. maximumPoolSize,线程池中的最大线程数。线程池允许创建的最大线程数。如果队列满了,并且已经创建的先吃小于最大线程数,则线程池会再创建新的线程去执行任务。如果队列是无界的,则此参数无意义
  3. keepAliveTime,线程池的工作线程空闲后,保持存活的时间。所有如果任务很多,并且执行时间比较短,可以调大时间,提高线程的利用率。
  4. TimeUnit ,keepAliveTime的单位。可以是天、小时、分钟、秒、毫秒、微秒,纳秒等
  5. workQueue;任务队列,用于保持等待执行的任务的阻塞队列。可以选择的队列如下:
    • ArrayBlockingQueue 一个由数组结构组成的有界阻塞队列,按照FIFO原则对元素进行排序
    • LinkedBlockingQueue 一个由链表结构组成的有界阻塞队列,按照FIFO原则对元素进行排序。吞吐量通常高于 ArrayBlockingQueue。Executors.newFixedThreadPool 使用的这个队列。
    • PriorityBlockingQueue 一个支持优先级排序的无界阻塞队列。
    • SynchronousQueue 一个不储存元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常高于 LinkedBlockingQueue。Executors.newCahcedThreadPool 使用的这个队列。
  6. threadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。默认才用 Executors.defaultThreadFactory() 来创建的线程工厂。可以通过开源框架guava提供的ThreadFactoryBuilder来创建有意义的名字,代码如下:
 new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build();
  1. RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态了,那么必须采取一种策略处理提交的新任务。默认才用的是new AbortPolicy(),表示无法处理的时候直接抛出异常:
    • AbortPolicy:直接抛出异常
    • DiscardPolicy:不处理,并且只丢弃
    • DiscardOldestPolicy,丢弃队列里最近的一个任务,并执行当前任务
    • CallerRunsPolicy 只用调用者所在线程来运行任务

三 使用线程池的实现原理

结合代码 ThreadPoolExecutor 的源代码 分析代码原理。
在这里插入图片描述
如上图所示:

  • 当执行了 execute()之后的代码流程如下:
public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        //判断线程数量小于 corePoolSize,则创建一个新的核心线程数,并执行当前command任务,详细见下面的 addWorker方法解析
        if (workerCountOf(c) < corePoolSize) {
        	//true表示核心线程数 创建成功之后直接返回,否则执行下面的
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        //如果线程数量大于 corePoolSize,则想队列里面添加一个任务,offer返回true表示入队成功,否则入队失败
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //如果入队失败,说明队列已经满了,则创建普通的线程
        else if (!addWorker(command, false))
            reject(command);
    }
  • addWorker方法是如何添加工作线程并启动的呢, firstTask为execute()启动传入的任务。core表示当前worker是不是核心线程类型
private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            //... 省略部分代码
            for (;;) {
                int wc = workerCountOf(c);
                //通过core的类型 来判断线程数量为多少的时候 返回false
                if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                //通过case循环操作 来实现原子自增工作线程的数量。 c是一个原子类,记录工作线程的数量
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                //... 省略部分代码
            }
        }
        boolean workerStarted = false; boolean workerAdded = false;
        Worker w = null;
        try {
        	//通过提交的任务 创建当前的工作线程
            w = new Worker(firstTask);
            //获取当前更新线程的线程类型,通过Worker的线程可以得知,t.start 会执行 Worker.run方法
            final Thread t = w.thread;
            if (t != null) {
            	//创建线程 创建新的线程是需要获取全局锁的
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    int rs = runStateOf(ctl.get());
                    if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                //创建成功并加入到工作线程集合之后,启动工作线程。t.start()即使执行 Worker.run方法
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
        	//线程启动失败的话,,需要从工作线程集合中移除调,并把线程数减去1 这样不影响其他任务的提交
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }
  • Worker线程是怎么启动,并且怎么获取任务的呢?
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
	//定义的当前线程
	final Thread thread;
	//需要执行的任务
	Runnable firstTask;
	
	Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
    }
    //... 省略部分代码
}

通过查询代码可以得知,Worker继承类AQS同步器,所以他本身现实了锁的功能,并且实现了 Runnable接口,并通过线程工厂把自己封装到了私有方法 thread中,这就是 thread.start的时候,会启动Worker的run方法的原因。

public void run() {
            runWorker(this);
}
//启动工作线程
final void runWorker(Worker w) {
		//获取当前的先吃
        Thread wt = Thread.currentThread();
        //获取当前Worker中需要执行的任务
        Runnable task = w.firstTask;
        //并把当前worker的工作任务设置为null
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
        	//如果当前取出来的task不为空,则进入循环提,否则从任务队列中取出一个任务并赋值给task
            while (task != null || (task = getTask()) != null) {
            	//获取当前工作线程的线程锁,一个工作线程,同一个时刻只能执行一个任务
                w.lock();
                //判断当前主线程是否中断???
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                	//线程启动之前的操作,wt为当前主线程,task为当前需要执行的任务
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                    	//执行之后的操作
                    	//如果有异常情况 会提前结束循环 并跳出循环体
                        afterExecute(task, thrown);
                    }
                } finally {
                	//当task执行完成之后,把该任务赋值为null,然后继续循环的时候 从任务队列中继续读取任务
                    task = null;
                    //记录当前线程完成的任务数量 
                    w.completedTasks++;
                    //释放工作线程锁
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
        	//	当推出循环体,说明当前工作队列已经为空了。那把把改工作线程从 工作线程集合中 移除
        	//并且会保留 corePoolSize个线程 ??是如何保留的呢?
            processWorkerExit(w, completedAbruptly);
        }
    }

四 线程池的原理总结

  1. 线程池是什么时候创建线程的?
    在提交任务的时候,创建的线程。并且创建的线程会先执行当前任务,然后在从队列中读取

  2. 任务runnable task是先放到core到maxThread之间的线程,还是先放到队列?
    由上面的流程图可得知,当达到了 corePoolSize的时候,会先放到队列中,等队列满了,才会去创建 corePoolSize到 maximumPoolSize个线程,并把任务放到新创建的线程里面来。

  3. 队列中的任务是什么时候取出来的?
    从 runWorker()源码分析来看,当线程执行了 创建的任务之后,,循环从队列中读取,直到队列里面为空。

  4. 什么时候会触发reject策略?
    从上面的流程图可以得知,当队列满了,并且工作线程数达到了 maximumPoolSize之后 会触发reject策略。

  5. core到maxThread之间的线程什么时候会die?
    没有任务的时候,或者 在执行任务有异常的时候。

  6. task抛出异常,线程池中这个work thread还能运行其他任务吗?
    不能了,如果task抛出异常,当前工作线程会死掉,工作线程数也会减1,后面有新的任务进来,通过策略可能又回重新创建一个线程。

  7. 如何理解 corePoolSize?
    corePoolSize只是一个数字,并不是真的有核心线程池和 普通线程池的区别,只是一个在有任务的时候,线程池里面最小维护的线程数。

五 合理的配置线程池

想要合理的配置线程池,需要对任务进行分析

  • 任务的类型:CPU密集型任务、IO密集型任务和混合型任务
  • 任务的优先级:高中低
  • 任务的执行时间:长中短
  • 任务的依赖性:是否依赖其他系统资源

什么是CPU密集任务、什么是IO密集任务?
具体参考

  • CPU密集任务:任务需要进行大量的计算,消耗CPU资源。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。

  • IO密集:涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成。 对于IO密集型任务,任务越多,CPU效率越高,同时进行的线程数可以设置在CPU的核心数的2倍。

  • 混合型任务:可以拆分成一个CPU密集和一个IO密集的自任务,然后在执行。

  • 优先级的任务可以使用优先级队列来处理买。从优先级高的先执行

  • 对于执行时间的任务,可以通过优先级队列 ,让执行时间短的任务先执行。

  • 依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则cpu空闲时间就越长,那么线程数应该设置的越大,这样才能充分利用CPU

  • 另外在设置队列的时候 尽量使用有界队列。

六 线程池的监控

  • taskCount:线程池需要执行的任务数
  • completedTaskCount:线程池在运行过程中已完成的任务数量。
  • largestPoolSize:线程池里面曾经创建过的最大线程数量,可以通过这个参数来判断线程池曾经是否满过
  • getPoolSize:线程池的线程数量
  • getActiveSize:获取活跃的线程数

七 手写一个线程池

demo地址
https://gitee.com/luffyu/manually/tree/master/manually_pool

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值