专项攻克——线程池

文章目录

  • 0、学习资料与git
  • 1、线程池背景
  • 2、五种线程池的介绍和使用场景
  • 2.2 但是我们为什么不用Executors来创建线程池呢?
  • 3、线程池的继承关系
  • 4、线程池的具体实现类ThreadPoolExecutor的重要参数
  • 5、==线程池的几步主要工作流程思路分析==
  • 6、线程池都有哪几种工作队列
  • 7、==自定义线程池工厂==
  • 8、扩展线程池
  • 9、手动创建线程池有几个注意点

0、学习资料与git

  • 参考文献:多线程面试问题

  • 参考文献:xxx博客

  • 另外:非常建议这个文章,一定要看

  • 参考我的gitHub——手写线程池:ThreadPool Git(感兴趣的可以自己手写一个,我是参考别人的来写的)

1、线程池背景

1.1. 不用线程池,会有什么问题?
多线程的引入,可以大大增强系统的并发能力,但是创建一个线程的开销是很大的,频繁的创建和销毁线程反而使得我们的系统在高并发时性能急剧下降。
1.2. 线程池是怎么解决这个问题的?
线程池通过“线程复用”实现:如果线程用完了,先不着急销毁,有下个任务来了,再重复利用。即:当一个新任务到来的时候,线程池会找到一个空闲的线程来执行任务。如果线程池里的线程都用完了,那么线程池会将任务加入到一个队列中去等待。当队列也满了,线程池会根据要求创建一个新的线程来执行任务,或者执行某种拒绝策略。

2、五种线程池的介绍和使用场景

  • 一个使用线程池管理线程的例子
    //创建一个可重用固定线程数量的线程池  
    ExecutorService pool = Executors.newFixedThreadPool(2); 
    //创建Thread类的子类的线程实例 
    Thread t1 = new MyThread();
    //将线程放入池中开始执行,请求cpu时间片
    pool.execute(t1);
    //关闭线程池
    pool.shutdown();  
    
  1. newSingleThreadExecutor:一个单线程的线程池,可以用于需要保证顺序执行的场景,并且只有一个线程在执行。

    Executors.newSingleThreadPool() 创建单任务线程池
    
  2. newFixedThreadPool:一个固定大小的线程池,可以用于已知并发压力的情况下,对线程数做限制。

    Executors.newFixedThreadPool(int number) 创建固定大小的线程池
    
  3. newCachedThreadPool:一个可以无限扩大的线程池,比较适合处理执行时间比较小的任务。

    Executors.newCachedThreadPool() 创建可变大小的线程池
    
  4. newScheduledThreadPool:可以延时启动,定时启动的线程池,适用于需要多个后台线程执行周期任务的场景。

    Executors.newScheduledThreadPool(int number) 创建延迟线程池
    
  5. newWorkStealingPool:一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu数量的线程来并行执行。

2.2 但是我们为什么不用Executors来创建线程池呢?

因为通过Executors创建线程池不容易让开发员理解底层实现,而且Executors的默认成员不一定适合你的场景。

3、线程池的继承关系

专项攻克——线程池_第1张图片
Executor: 所有线程池的接口,只有一个方法。
ExecutorService: 增加Executor的行为,是Executor实现类的最直接接口。

Executors: 提供一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService 接口。
ThreadPoolExecutor:线程池的具体实现类,一般用的各种线程池都是基于这个类实现的。

4、线程池的具体实现类ThreadPoolExecutor的重要参数

  • 构造方法源码,注意理解每一个参数的含义
public class ThreadPoolExecutor extends AbstractExecutorService {
    //四个空构造器,要理解每一个参数的含义
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue);

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
        BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
    ...
}

1、第一个参数:int corePoolSIze,
核心池大小(其实这个就相当于是球队的主力队员,一般情况先都是这几个主力队员上场,但是如果遇到主力队员人数不够或者受伤之后不足以满足比赛才会启用maximumPoolSize这个参数),也就是线程池中会维持不被释放的线程数量。我们可以看到FixedThreadPool中这个参数值就是设定的线程数量,而SingleThreadExcutor中就是1,newCachedThreadPool中就是0,不会维持,只会缓存60L。但需要注意的是,在线程池刚创建时,里面并没有建好的线程,只有当有任务来的时候才会创建(除非调用方法prestartAllCoreThreads()与prestartCoreThread()方法),在corePoolSize数量范围的线程在完成任务后不会被回收。
2、第二个参数:int maximumPoolSize
(可以把这个参数当成是球队后背球员,当主力不足时才会让后备队员上场救急)线程池的最大线程数,代表着线程池中能创建多少线程池。超出corePoolSize,小于maximumPoolSize的线程会在执行任务结束后被释放。此配置在CatchedThreadPool中有效。
3、第三个参数:long keepAliveTime
,刚刚说到的会被释放的线程缓存的时间。我们可以看到,正如我们所说的,在CachedThreadPool()构造过程中,会被设置缓存时间为60s(时间单位由第四个参数控制)。
4、第四个参数:TimeUnit unit
设置第三个参数keepAliveTime的时间单位。

5、第五个参数:(就是四种阻塞的队列,也就是当线程池满了之后,再进来的任务都会放到这个阻塞队列中等待)
存储等待执行任务的阻塞队列,有多种选择,分别介绍:

SynchronousQueue——直接提交策略,适用于CachedThreadPool。它将任务直接提交给线程而不保持它们。如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求最大的 maximumPoolSize 以避免拒绝新提交的任务(正如CachedThreadPool这个参数的值为Integer.MAX_VALUE)。当任务以超过队列所能处理的量、连续到达时,此策略允许线程具有增长的可能性。吞吐量较高。

LinkedBlockingQueue——无界队列,适用于FixedThreadPool与SingleThreadExcutor。基于链表的阻塞队列,创建的线程数不会超过corePoolSizes(maximumPoolSize值与其一致),当线程正忙时,任务进入队列等待。按照FIFO原则对元素进行排序,吞吐量高于ArrayBlockingQueue。

ArrayListBlockingQueue——有界队列,有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。

6、第六个参数:threadFactory
这个参数就是一个线程工厂,主要的功能就是用来创建线程的

7、第七个参数:RejectedExecutionHandler handler
这个参数是当任务到队列中之后缓存中队列阻塞的也已经满了的时候,会去启动备用后备队员去进行补充球队,但是如果此时后备队员也不够的话(),这个参数就会起到他的作用,会启用无法执行任务的策略:

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

5、线程池的几步主要工作流程思路分析

专项攻克——线程池_第2张图片
线程池的执行流程又是怎样的呢?
由图我们可以看出,任务进来时,首先执行判断,判断核心线程是否已满,如果不是,核心线程就先执行任务,如果核心线程已满,则判断任务队列是否有地方存放该任务,若果有,就将任务保存在任务队列中,等待执行,如果任务队列满了,再判断线程池最大可容纳的线程数,如果没有超出这个数量,就开创非核心线程执行任务,如果线程池最大可容纳的线程数超出了,就调用handler实现拒绝策略。

handler的拒绝策略有四种:
第一种AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满
第二种DisCardPolicy:不执行新任务,也不抛出异常
第三种DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行
第四种CallerRunsPolicy:直接调用execute来执行当前任务

整个线程池的大体流程的代码,如下:

        //有任务提交过来的话,会执行这个方法
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
         //这个是先做第一个判断当前线程是不是大于等于核心线程池(说明满了),如果大于会继续执行第二步把提交过来的任务添加到任务队列中去
        if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
        //如果当前线程池处于RUNNING状态,则将任务放入任务缓存队列;

            if (runState == RUNNING && workQueue.offer(command)) {
              //如果当前线程池不处于RUNNING状态或者任务放入缓存队列失败,
              //则说明需要启用备用球员来上场(maximumPoolSize可以把这个看成是紧急预备队),来去处理这个提交的任务
                if (runState != RUNNING || poolSize == 0)
                //然后去处理任务
                    ensureQueuedTaskHandled(command);
            }
            else if (!addIfUnderMaximumPoolSize(command))
                reject(command); // is shutdown or saturated
        }
    }

6、线程池都有哪几种工作队列

1、ArrayBlockingQueue
是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。

2、LinkedBlockingQueue
一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列

3、SynchronousQueue
一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。

4、PriorityBlockingQueue
一个具有优先级的无限阻塞队列。

7、自定义线程池工厂

Executors的线程池如果不指定线程工厂会使用Executors中的DefaultThreadFactory,默认线程池工厂创建的线程都是非守护线程。

使用自定义的线程工厂可以做很多事情,比如可以跟踪线程池在何时创建了多少线程,也可以自定义线程名称和优先级。如果将

新建的线程都设置成守护线程,当主线程退出后,将会强制销毁线程池。

下面这个例子,记录了线程的创建,并将所有的线程设置成守护线程。

public class ThreadFactoryDemo {

	//实现了Runnable的内部类
    public static class MyTask1 implements Runnable{

        @Override
        public void run() {
            System.out.println(System.currentTimeMillis()+"Thrad ID:"+Thread.currentThread().getId());
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args){
        MyTask1 task = new MyTask1();
        //创建线程池:
        //线程池的核心池大小=5,最大能创建多线程数=5。
        //当线程池中的线程数大于corePoolSize,并且一个线程空闲时间达到了0秒,就shutdown。
        //单位秒
        //通过workQueue,线程池实现了阻塞功能
        //new ThreadFactory() :线程工厂,通过重写newTread()方法来创建线程。
        ExecutorService es = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MICROSECONDS, new SynchronousQueue<Runnable>(), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setDaemon(true);
                System.out.println("创建线程"+t);
                return  t;
            }
        });
        for (int i = 0;i<=4;i++){
           es.submit(task);
        }
    }
}

8、扩展线程池

ThreadPoolExecutor是可以拓展的,它提供了几个可以在子类中改写的方法:beforeExecute,afterExecute和terimated。

在执行任务的线程中将调用beforeExecute和afterExecute,这些方法中还可以添加日志,计时,监视或统计收集的功能,

还可以用来输出有用的调试信息,帮助系统诊断故障。下面是一个扩展线程池的例子:

public class ThreadFactoryDemo {
    public static class MyTask1 implements Runnable{

        @Override
        public void run() {
            System.out.println(System.currentTimeMillis()+"Thrad ID:"+Thread.currentThread().getId());
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args){
          MyTask1 task = new MyTask1();
        ExecutorService es = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MICROSECONDS, new SynchronousQueue<Runnable>(), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setDaemon(true);
                System.out.println("创建线程"+t);
                return  t;
            }
        });
        for (int i = 0;i<=4;i++){
           es.submit(task);
        }
    }
} 

线程池的正确使用

以下阿里编码规范里面说的一段话:

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor:
  主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
2)newCachedThreadPool和newScheduledThreadPool:
  主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

9、手动创建线程池有几个注意点

1.任务独立。如何任务依赖于其他任务,那么可能产生死锁。例如某个任务等待另一个任务的返回值或执行结果,那么除非线程池足够大,否则将发生线程饥饿死锁。

2.合理配置阻塞时间过长的任务。如果任务阻塞时间过长,那么即使不出现死锁,线程池的性能也会变得很糟糕。在Java并发包里可阻塞方法都同时定义了限时方式和不限时方式。例如

Thread.join,BlockingQueue.put,CountDownLatch.await等,如果任务超时,则标识任务失败,然后中止任务或者将任务放回队列以便随后执行,这样,无论任务的最终结果是否成功,这种办法都能够保证任务总能继续执行下去。

3.设置合理的线程池大小。只需要避免过大或者过小的情况即可,上文的公式线程池大小=NCPU *UCPU(1+W/C)。

4.选择合适的阻塞队列。newFixedThreadPool和newSingleThreadExecutor都使用了无界的阻塞队列,无界阻塞队列会有消耗很大的内存,如果使用了有界阻塞队列,它会规避内存占用过大的问题,但是当任务填满有界阻塞队列,新的任务该怎么办?在使用有界队列是,需要选择合适的拒绝策略,队列的大小和线程池的大小必须一起调节。对于非常大的或者无界的线程池,可以使用SynchronousQueue来避免任务排队,以直接将任务从生产者提交到工作者线程。

下面是Thrift框架处理socket任务所使用的一个线程池,可以看一下FaceBook的工程师是如何自定义线程池的。

 private static ExecutorService createDefaultExecutorService(Args args) {
        SynchronousQueue executorQueue = new SynchronousQueue();

        return new ThreadPoolExecutor(args.minWorkerThreads, args.maxWorkerThreads, 60L, TimeUnit.SECONDS,
                executorQueue);
    }

你可能感兴趣的:(高并发编程,java,jvm,面试)