精华内容
下载资源
问答
  • 1.高并发、任务执行时间段 的业务使用什么样的线程池 2.并发不高、任务执行时间长的的业务怎样使用线程池 3.并发搞、业务执行时间长的业务怎样使用线程池 线程池的本质是生产者和消费者模型,包括三要素: 往线程...

    1.高并发、任务执行时间段 的业务使用什么样的线程池
    2.并发不高、任务执行时间长的的业务怎样使用线程池
    3.并发搞、业务执行时间长的业务怎样使用线程池

    线程池的本质是生产者和消费者模型,包括三要素:

    • 往线程队列中投递任务的生产者
    • 任务池队列
    • 从任务池队列取出任务执行的worker线程(消费者)

    想要合理配置线程池的大小,就要分析线程池任务的特性,可以从以下几个方面进行分析:
    根据任务性质分:

    • cpu密集型
    • IO密集型
    • 混合型任务

    根据任务的优先级:

    根据任务的执行时间:

    不同性质的任务可以交给不同配置的线程池执行:
    cpu密集型任务配置尽可能小的线程,如配置cpu个数+1个线程;IO密集型任务应配置尽可能多的线程,因为IO操作不占用cpu,不要让cpu闲下来,应加大线程的数量,如配置两倍cpu数量+1个线程;对于混合型的任务,如果可以拆分,拆换成IO密集型和cpu密集型分别处理,如果两种处理方式时间差距很大则没必要拆分。
    如果任务执行时间长,在worker线程数量有限的情况下,worker很快就被任务占用,导致后续任务不能及时被处理,此时应增加worker线程数量;反过来,如果任务执行时间短,那么worker线程数量不用太多,太多的worker线程会导致过多的时间浪费在线程上下文切换上。

    展开全文
  • 自己设计线程池

    千次阅读 2017-08-20 17:24:06
    一,线程池的基本要素 线程池一般需要一个线程管理类: ThreadPoolManager,其作用有:  1)提供创建一定数量的线程的方法。主线程调用该方法,从而创建线程。创建的线程执行自己的例程,线程的例程阻塞在任务抓取...

    一,线程池的基本要素

    线程池一般需要一个线程管理类: ThreadPoolManager,其作用有:

      1)提供创建一定数量的线程的方法。主线程调用该方法,从而创建线程。创建的线程执行自己的例程,线程的例程阻塞在任务抓取上。

      2)提供对任务队列的操作的方法。主线程调用初始化任务队列的方法,然后在有任务的时候,调用提供的任务添加方法,将任务添入等待队列。当主线程调用任务的添加方法时,会触发等待的线程,从而使得阻塞的线程被唤醒,其抓取任务,并执行任务。


    线程池需要一个任务队列: List<Task>,其作用有:

      提供任务的增删方法。而且该任务队列需要进行排他处理,防止多个工作线程对该任务队列进行同时的抓取操作或者主线程的加入与工作线程的抓取的并发操作。


    线程池需要一个类似信号量的通知机制:wait -notify:

      工作线程调用wait阻塞在任务抓取上。主线程添加任务后,调用notify触发阻塞的线程。

     

    线程池需要一个线程类:WorkThread,其作用有:

      提供线程的例程。创建线程WorkThread后,需要抓取任务,并执行任务。这是线程的例程。


    线程池需要一个任务类:Task,其作用有:

      提供线程抓取并执行的任务目标。

      

    二,基础知识和策略选择


      1,线程同步和通知机制

      Object类的wait()方法、notify()方法、notifyAll()方法。

      Object类的对象都有两个池: 监视池(monitor,或者称锁池)和等待池。

      监视池: 如果一个线程想要调用一个对象的synchronized的方法,或者对对象的操作被synchronized块所包含,则线程必须获得该对象的对象锁才能执行这个方法或者代码块。如果获取不到,则被加入到该对象的监视池中,等待锁资源。

      等待锁:如果一个线程调用对象的wait()方法,则线程会放弃持有的该对象的锁资源(因为wait()必须在获取到对象锁之后才能执行),并进入该对象的等待池。如果另外一个线程调用了相同对象的notify()/notifyAll()方法,就会唤醒在该对象的等待池中的一个或者所有线程。一旦被唤醒,线程就进入该对象的锁池,进行锁资源竞争,注意不是立即执行,也不能保证唤醒的是哪个线程。

      另外由于wait()方法可能发生中断和虚假唤醒,以及时间timeout(带时间的wait方法),因此,应该在while循环中使用。

    //在同步块或者synchronized方法中
    synchronized (obj) {
    //在循环中
    while (<condition does not hold>)
    obj.wait();
    ... // Perform action appropriate to condition
         }
    

      注: 我个人理解,这里的“虚假唤醒”的具体含义,应该包含如下这种情况: 一个被唤醒的线程发现其需要获取的条件已经被别的被唤醒的线程导致的不再成立。 比如在线程池的实现中,任务队列的任务: 当主线程向其中添加两个任务的时候,会唤醒第一个线程去抓取任务执行,而这个线程不会仅仅抓取一个,它执行完第一个立马抓取第二个。这个时候,第二个被唤醒的线程将无法抓取到,那么它不应该在被唤醒并获取锁后,对任务队列进行任务的获取并删除操作,即应该在获取锁后检查队列是否为空。由于获取锁后是继续wait()后的代码执行,因此必须使用while进行判断,而不能是if()判断,因为获取锁后,if的条件判断不会被再执行,而while语句则必须再进行循环判断看是否跳出循环。详细参见代码。

      synchronized用来修饰一个非静态方法,表示执行这个方法,必须获取该方法所属对象的锁;  synchronized用来修饰静态方法,表示要执行该方法必须获取该类的类锁;synchronized修饰代码块synchronized(obj) { //code.... }表示执行该代码块必须获取obj这个对象的对象锁。这样做的目的是减小锁的粒度,保证当不同块所需的锁不冲突时不用对整个对象加锁。利用零长度的byte数组对象做obj非常经济。

      atomic action(原子操作):

      在Java中,以下两点操作是原子操作。

      1),对引用变量和除了long和double之外的原始数据类型变量进行读写。

      2),对所有声明为volatile的变量(包括long和double)的读写。
      另外:在java.util.concurrent和java.util.concurrent.atomic包中提供了一些不依赖于同步机制的线程安全的类和方法。


      2,线程和任务

      线程:

      线程可以赋予名字、优先级、标识是否是守护线程等。

      在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)。

      守护线程和用户线程唯一的区别就是守护线程会随着用户线程的(被守护的)结束而结束。

      使用方法:

      setDaemon(true);

      这里有几点需要注意: 
        (1) setDaemon(true)必须在start()之前设置,否则会抛出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。 
        (2) 在Daemon线程中产生的新线程也是Daemon的。  
        (3) 不要认为所有的应用都可以分配给Daemon来进行服务,比如读写操作或者计算逻辑。有可能在守护线程跑了一半的时候主线程跑完了,然后守护线程就也会停止了。


      Java中的线程可以通过两种方式进行创建:继承Thread类,或者实现Runnable接口。继承Thread类应该重写其run()方法。实现Runnable类应该实现run()方法,并且将其作为Thread(Runnable target)的参数,即Thread的执行目标,创建线程,并开启其例程。例如:

      1)继承Thread:

     class PrimeThread extends Thread {
             long minPrime;
             PrimeThread(long minPrime) {
                 this.minPrime = minPrime;
             }
     
             public void run() {
                 // compute primes larger than minPrime
                  . . .
             }
         }
      PrimeThread p = new PrimeThread(143);
      //开启线程的例程
      p.start();

      2)实现Runnable:

         class PrimeRun implements Runnable {
             long minPrime;
             PrimeRun(long minPrime) {
                 this.minPrime = minPrime;
             }
     
             public void run() {
                 // compute primes larger than minPrime
                  . . .
             }
         } 
       PrimeRun p = new PrimeRun(143);
        //作为Thread的参数,并start例程 
        new Thread(p).start();
     
      任务类:

      任务类应该实现Runnable接口并提供run()方法。因为其是打算通过线程执行其实例的。Runnable为希望在活动时(线程被开启)执行代码的对象提供了一个公共协议。其为非Thread类的子类的类提供了一种激活方式,即无需继承Thread类便可以进行激活。即通过实例化某个Thread类的子类,并通过将此实现Runnable接口的类的实例作为线程的目标传递给线程,就可运行Runnable类而无需继承Thread。这在大多数情况下,如果只想重写run() 方法,而不重写其他Thread 方法,就应使用 Runnable 接口。因为除非程序员打算修改或增强类的基本行为,否则不应为该类创建子类。

      因此任务类应该是实现Runnable接口的抽象类并提供run的基本实现。

     3,任务队列的数据结构选择

      ArrayList和LinkedList :

      ArrayList和LinkedList是两个集合类,可以用于存储一系列的对象引用。ArrayList是基于动态数组的实现,而LinkedList是基于链表的。

      对于随机访问操作(获取第n个元素的值,或者查找指定值的某个元素),ArrayList比LinkedList占优势,因为LinkedList需要指针的移动。

      对于ArrayList,进行随机方法,其每次访问都是一个常数,而对于ArrayList则是于ArrayList的长度(要访问元素所在索引)成比例的。

      对于增删操作,由于ArrayList需要数据的移动,因而LinkedList占优势。对于ArrayList与插入的索引成比例,而LinkedList则是固定的。

       因而,对于需要开始、中间插入删除的并且是顺序访问,而不是随机访问的时候,可以采用LinkedList;不会进行开始中间插入删除,

    而是末尾操作,并且多是随机访问则应该使用ArrayList。整体来说,ArrayList查询效率高,增删操作效率低;LinkedList查询效率低,增删操作效率较高。


    综上所述,我们设计的线程池要考虑线程和任务类,以及对于任务队列访问的排他处理和wait-notify机制的细节,同时要考虑任务队列的存储数据结构的选择。


    三,实现


      1)线程池管理类

      

    package poolmanager;

     /**
     * ThreadPoolManager 线程池管理类
     * @version 1.0.0
     * @see 线程池定义式样书
     * @date    2011/09/30 IterZebra
     */
    //import java.util.Collections;
    import java.util.LinkedList;
    import java.util.List;

    import systemconfig.SystemConfig;

    public class ThreadPoolManager extends ThreadGroup {

        /**线程池是否开启标识*/
        int flagThreadPoolValid = 0;

        /**线程池中线程的数量,从系统配置类中获取*/
        int threadSize = SystemConfig.getThreadDefaultCount();

        /**任务队列*/
         /**
         Java 2 SE Documentation.
         Returns a synchronized (thread-safe) list backed by the specified list.
         In order to guarantee serial access, it is critical that all access to
         the backing list is accomplished through the returned list.
         根据指定的参数list,返回一个线程安全的同步的list。
         为了保证有序访问,所有的对于参数list的访问都应该通过返回的这个list去进行。
         It is imperative that the user manually synchronize
         on the returned list when iterating over it.
         如果进行迭代访问的时候,对于返回的队列应用程序本身进行同步处理是必要的。
         注:即使用迭代器访问应该使用synchronized(){}修饰代码块,使得其对list的访问进行同步;
         而如果是采用返回的list的add等方法,则应用程序本身不需要进行同步处理。

         另外由于本类中对ThreadPoolManager的访问方法都进行了同步操作,
         因此对本List的同步不是必要的。
         当然,由于加锁顺序一致性,使用对List的同步,也不会导致线程死锁。
        List<Task> TaskList= Collections.synchronizedList(new LinkedList<Task>());

        */
        List<Task> TaskList= new LinkedList<Task>();

        /**线程池管理类构造方法*/
        public ThreadPoolManager(String threadpoolname) {

             //ThreadGroup的名称
            super(threadpoolname);

            //继承自父类的方法,设置是否是守护线程
            setDaemon(true);

        }

        /**
         * @brief 开启线程池
         * @param null
         * @return void
         */
        public synchronized void threadPoolStart(){

            if(threadSize ==0 || flagThreadPoolValid !=0){

                try {
                    throw new Exception();
                } catch (Exception e) {
                    e.printStackTrace();
                }

                return ;
            }

            if( TaskList ==null ){

                try {
                    throw new Exception();
                } catch (Exception e) {
                    e.printStackTrace();
                }

                return;
            }

            // 创建并开启线程例程
            for (int i = 0;i<threadSize;i++){

                 new WorkThread(i).start();

            }

            flagThreadPoolValid = 1;

        }

        /**
         * @brief 关闭线程池
         * @param null
         * @return void
         */
        public synchronized void threadPoolEnd(){

            if(threadSize ==0 || flagThreadPoolValid !=1){

                try {
                    throw new Exception();
                } catch (Exception e) {
                    e.printStackTrace();
                }

                return;

            }
            if(TaskList ==null ){

                try {
                    throw new Exception();
                } catch (Exception e) {
                    e.printStackTrace();
                }

                return;
            }

            TaskList.clear();
            threadSize = 0;
            flagThreadPoolValid = 0;

            // 继承自父类,中断ThreadGroup中添加的所有线程
            interrupt();

        }

        /**
         * @brief 向线程池管理类的任务队列中添加任务
         * @param newTask Task任务类
         * @return void
         */
        public synchronized void addTask(Task newTask){

            if(TaskList == null ){

                try {
                    throw new Exception();
                } catch (Exception e) {
                    e.printStackTrace();
                }

                return;
            }

            TaskList.add(newTask);

            //唤醒一个正在getTask()方法中等待任务的工作线程
            notify();

        }

        /**
         * @brief 获取线程池中任务队列中的任务
         * @param null
         * @return Task类
         */
        public synchronized Task getTask(){

            if(TaskList ==null ){

                try {
                    throw new Exception();
                } catch (Exception e) {
                    e.printStackTrace();
                }

                return null;
            }
            /**
             java 2 SE Documentation.
              As in the one argument version,
              interrupts and spurious wakeups are possible,
              and this method should always be used in a loop.
              正如具有一个参数的函数版本,
              由于wait()函数调用过程中可能发生中断、虚假唤醒,
              因此这个方法必须在检测循环中使用。
              另外,在这里,如果不使用while循环,存在如下一种问题:
              抛出地址越界异常。
              例如:
              主线程添加了一个任务,这样会唤醒一个线程。在这个线程被唤醒执行。
              此时主线程添加另外一个任务,又唤醒一个线程。
              第一个线程其抓取任务后,继续抓取下一个任务执行,也就是抓取添加的第二个任务。
              这样队列中已经不存在任务。第二个被唤醒的线程必然在执行remove(0)的时候抛出上述异常。

              而且也不能仅仅是if判断,因为唤醒获取锁后,
              会直接执行wait()后的代码,只有while可以达到进行判断的目的。
            */
            while(TaskList.size() == 0 ){

                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }

            return  TaskList.remove(0);

        }

        /**
         * @brief 内部类,工作线程类
         */
        private class WorkThread extends Thread{

            public WorkThread(int threadID) {

                //父类构造方法,将线程加入到ThreadGroup中
                super(ThreadPoolManager.this,""+threadID);
            }

            /**
              * @brief 重写父类Thread的run方法
              * @param null
              * @return void
              */
            public void run(){

                //isInterrupted()方法继承自Thread类,判断线程是否被中断
                while(! isInterrupted()){

                    Task runTask = getTask();

                    //getTask()返回null或者线程执行getTask()时被中断
                    if(runTask == null)    break ;

                    runTask.run();

                }

            }

        }
    }


      2)任务类


    package poolmanager;

    public abstract class Task implements Runnable {

        public void run() {
        }

    }

    //系统配置类


    package systemconfig;

    public class SystemConfig {
        
        static final int THREAD_DEFAULT_COUNT = 10;
        
        public static int getThreadDefaultCount() {
            return THREAD_DEFAULT_COUNT;
        }
        

    }

      3)测试  

    package poolmanager.test;

    import poolmanager.Task;

    public class TestTask extends Task {

        private   int i ;

        public TestTask(int i) {
            this.i = i;

        }

        public void run() {
            System.out.println("Hello world");
            System.out.println(i+"");
        }

    }

    package poolmanager.test;

    import poolmanager.Task;
    import poolmanager.ThreadPoolManager;

    public class PoolManagerTest {

        /**
         * @param args
         */
        public static void main(String[] args) {
            // TODO Auto-generated method stub

            ThreadPoolManager threadpoolmanager = new ThreadPoolManager("SimpleThreadPoll");
            threadpoolmanager.threadPoolStart();
            Task newTask = new TestTask(1);
            threadpoolmanager.addTask(newTask);
             newTask = new TestTask(2);
            threadpoolmanager.addTask(newTask);
             newTask = new TestTask(3);
            threadpoolmanager.addTask(newTask);
             newTask = new TestTask(4);
            threadpoolmanager.addTask(newTask);
             newTask = new TestTask(5);
            threadpoolmanager.addTask(newTask);
             newTask = new TestTask(6);
            threadpoolmanager.addTask(newTask);
            //threadpoolmanager.threadPoolEnd();

        }
    }


    转载自:http://blog.csdn.net/iterzebra/article/details/6758481

    展开全文
  • NULL 博文链接:https://ycde2009.iteye.com/blog/2032605
  • 如果你是 JDK 设计者,如何设计线程池?我跟面试官大战了三十个回合 是基友朋友最近去面滴滴遇到的这两面试题。 今天就借着这两面试真题来深入一波线程池吧,这篇文章力求把线程池核心点和常问的面试点一网打尽,...

    如果你是 JDK 设计者,如何设计线程池?我跟面试官大战了三十个回合

    是基友朋友最近去面滴滴遇到的这两面试题。

    img

    今天就借着这两面试真题来深入一波线程池吧,这篇文章力求把线程池核心点和常问的面试点一网打尽,当然个人能力有限,可能会有遗漏,欢迎留言补充!

    先把 大部分 问题列出来,如果你都答得出来,那没必要看下去:

    • 为什么会有线程池?
    • 简单手写一个线程池?
    • 为什么要把任务先放在任务队列里面,而不是把线程先拉满到最大线程数?
    • 线程池如何动态修改核心线程数和最大线程数?
    • 如果你是 JDK 设计者,如何设计?
    • 如果要让你设计一个线程池,你要怎么设计?
    • 你是如何理解核心线程的?
    • 你是怎么理解 KeepAliveTime 的?
    • 那 workQueue 有什么用?
    • 你是如何理解拒绝策略的?
    • 你说你看过源码,那你肯定知道线程池里的 ctl 是干嘛的咯?
    • 你知道线程池有几种状态吗?
    • 你知道线程池的状态是如何变迁的吗?
    • 如何修改原生线程池,使得可以先拉满线程数再入任务队列排队?
    • Tomcat 中的定制化线程池实现 如果线程池中的线程在执行任务的时候,抛异常了,会怎么样?
    • 原生线程池的核心线程一定伴随着任务慢慢创建的吗?
    • 线程池的核心线程在空闲的时候一定不会被回收吗?

    接得住吗?话不多说,发车!

    为什么会有线程池?

    想要深入理解线程池的原理得先知道为什么需要线程池。

    首先你要明白, 线程是一个重资源 ,JVM 中的线程与操作系统的线程是一对一的关系,所以在 JVM 中每创建一个线程就需要调用操作系统提供的 API 创建线程,赋予资源,并且销毁线程同样也需要系统调用。

    而系统调用就意味着上下文切换等开销,并且线程也是需要占用内存的,而内存也是珍贵的资源。

    因此线程的创建和销毁是一个 重操作 ,并且线程本身也占用资源。

    然后你还需要知道, 线程数并不是越多越好

    我们都知道线程是 CPU 调度的最小单位,在单核时代,如果是纯运算的操作是不需要多线程的,一个线程一直执行运算即可。但如果这个线程正在等待 I/O 操作,此时 CPU 就处于空闲状态,这就浪费了 CPU 的算力,因此有了多线程,在某线程等待 I/O 等操作的时候,另一个线程顶上,充分利用 CPU,提高处理效率。

    img

    此时的多线程主要是为了提高 CPU 的利用率而提出。

    而随着 CPU 的发展,核心数越来越多,能同时运行的线程数也提升了,此时的多线程不仅是为了提高单核 CPU 的利用率,也是为了充分利用多个核心。

    至此想必应该明白了为什么会有多线程,无非就是为了充分利用 CPU 空闲的时间,一刻也不想让他停下来。

    但 CPU 的核心数有限,同时能运行的线程数有限,所以需要根据调度算法切换执行的线程,而线程的切换需要开销,比如替换寄存器的内容、高速缓存的失效等等。

    如果线程数太多,切换的频率就变高,可能使得多线程带来的好处抵不过线程切换带来的开销,得不偿失。

    因此线程的数量需要得以控制,结合上述的描述可知,线程的数量与 CPU 核心数和 I/O 等待时长息息相关。

    小结一下:

    • Java中线程与操作系统线程是一比一的关系。
    • 线程的创建和销毁是一个“较重”的操作。
    • 多线程的主要是为了提高 CPU 的利用率。
    • 线程的切换有开销,线程数的多少需要结合 CPU核心数与 I/O 等待占比。

    综上我们知道了线程的这些特性,所以说它不是一个可以“随意拿捏”的东西,我们需要重视它,好好规划和管理它,充分利用硬件的能力,从而提升程序执行效率,所以线程池应运而生。

    什么是线程池?

    那我们要如何管理好线程呢?

    因为线程数太少无法充分利用 CPU ,太多的话由于上下文切换的消耗又得不偿失,所以我们需要评估系统所要承载的并发量和所执行任务的特性,得出大致需要多少个线程数才能充分利用 CPU,因此需要 控制线程数量

    又因为线程的创建和销毁是一个“重”操作,所以我们需要避免线程频繁地创建与销毁,因此我们需要 缓存一批线程 ,让它们时刻准备着执行任务。

    目标已经很清晰了,弄一个池子,里面存放约定数量的线程,这就是线程池,一种池化技术。

    熟悉对象池、连接池的朋友肯定对池化技术不陌生, 一般池化技术的使用方式是从池子里拿出资源 ,然后使用,用完了之后归还。

    但是线程池的实现不太一样,不是说我们从线程池里面拿一个线程来执行任务,等任务执行完了之后再归还线程,你可以想一下这样做是否合理。

    线程池的常见实现更像是一个黑盒存在,我们设置好线程池的大小之后,直接往线程池里面丢任务,然后就不管了。

    img

    剥开来看, 线程池其实是一个典型的生产者-消费者模式

    线程池内部会有一个队列来存储我们提交的任务,而内部线程不断地从队列中索取任务来执行,这就是线程池最原始的执行机制。

    img

    按照这个思路,我们可以很容易的实现一个简单版线程池,想必看了下面这个代码实现,对线程池的核心原理就会了然于心。

    首先线程池内需要定义两个成员变量,分别是阻塞队列和线程列表,然后自定义线程使它的任务就是不断的从阻塞队列中拿任务然后执行。

    @Slf4j
    public class YesThreadPool {
    
    	BlockingQueue<Runnable> taskQueue;  //存放任务的阻塞队列
    	List<YesThread> threads; //线程列表
    
    	YesThreadPool(BlockingQueue<Runnable> taskQueue, int threadSize) {
    		this.taskQueue = taskQueue;
    		threads = new ArrayList<>(threadSize);
            // 初始化线程,并定义名称
    		IntStream.rangeClosed(1, threadSize).forEach((i)-> {
    			YesThread thread = new YesThread("yes-task-thread-" + i);
    			thread.start();
    			threads.add(thread);
    		});
    	}
        //提交任务只是往任务队列里面塞任务
    	public void execute(Runnable task) throws InterruptedException {
    		taskQueue.put(task);
    	}
    
    	class YesThread extends Thread { //自定义一个线程
    		public YesThread(String name) {
    			super(name);
    		}
    		@Override
    		public void run() {
    			while (true) { //死循环
    				Runnable task = null;
    				try {
    					task = taskQueue.take(); //不断从任务队列获取任务
    				} catch (InterruptedException e) {
    					logger.error("记录点东西.....", e);
    				}
    				task.run(); //执行
    			}
    		}
    	}
    }
    复制代码
    

    一个简单版线程池就完成了,简单吧!

    再写个 main 方法用一用,丝滑,非常丝滑。

    public static void main(String[] args) {
    		YesThreadPool pool = new YesThreadPool(new LinkedBlockingQueue<>(10), 3);
    		IntStream.rangeClosed(1, 5).forEach((i)-> {
    			try {
    				pool.execute(()-> {
    					System.out.println(Thread.currentThread().getName() + " 公众号:yes的练级攻略");
    				});
    			} catch (InterruptedException e) {
    				logger.error("记录点东西.....", e);
    			}
    		});
    	}
    复制代码
    

    运行结果如下:

    img

    下次面试官让你手写线程池,直接上这个简单版,然后他会开始让你优化,比如什么线程一开始都 start 了不好,想懒加载,然后xxxx…最终其实就是想往李老爷实现的 ThreadPoolExecutor 上面靠。

    那就来嘛。

    ThreadPoolExecutor 剖析

    这玩意就是常被问的线程池的实现类了,先来看下构造函数:

    img

    核心原理其实和咱们上面实现的差不多,只是生产级别的那肯定是要考虑的更多,接下来我们就来看看此线程池的工作原理。

    先来一张图:

    img

    简单来说线程池把任务的提交和任务的执行剥离开来,当一个任务被提交到线程池之后:

    • 如果此时线程数 小于 核心线程数,那么就会新起一个线程来执行当前的任务。
    • 如果此时线程数 大于 核心线程数,那么就会将任务塞入阻塞队列中,等待被执行。
    • 如果阻塞队列满了,并且此时线程数 小于 最大线程数,那么会创建新线程来执行当前任务。
    • 如果阻塞队列满了,并且此时线程数 大于 最大线程数,那么会采取拒绝策略。

    以上就是任务提交给线程池后各种状况汇总,一个很容易出现理解错误的地方就是当线程数达到核心数的时候,任务是先入队,而不是先创建最大线程数。

    从上述可知,线程池里的线程不是一开始就直接拉满的,是根据任务量开始慢慢增多的,这就算一种懒加载,到用的时候再创建线程,节省资源。

    来先吃我几问。

    此时线程数小于核心线程数,并且线程都处于空闲状态,现提交一个任务,是新起一个线程还是给之前创建的线程?

    李老是这样说的: If fewer than corePoolSize threads are running, try to start a new thread with the given command as its first task.

    我觉得把 threads are running 去了,更合理一些,此时线程池会新起一个线程来执行这个新任务,不管老线程是否空闲。

    你是如何理解核心线程的 ?

    从上一个问题可以看出,线程池虽说 默认 是懒创建线程,但是它实际是想要快速拥有核心线程数的线程。核心线程指的是线程池承载日常任务的中坚力量,也就是说本质上线程池是需要这么些数量的线程来处理任务的,所以在懒中又急着创建它。

    而最大线程数其实是为了应付突发状况。

    举个装修的例子,正常情况下施工队只要 5 个人去干活,这 5 人其实就是核心线程,但是由于工头接的活太多了,导致 5 个人在约定工期内干不完,所以工头又去找了 2 个人来一起干,所以 5 是核心线程数,7 是最大线程数。

    平时就是 5 个人干活,特别忙的时候就找 7 个,等闲下来就会把多余的 2 个辞了。

    看到这里 你可能会觉得核心线程在线程池里面会有特殊标记

    并没有,不论是核心还是非核心线程,在线程池里面都是一视同仁,当淘汰的时候不会管是哪些线程,反正留下核心线程数个线程即可,下文会作详解。

    你是怎么理解 KeepAliveTime 的?

    这就是上面提到的,线程池其实想要的只是核心线程数个线程,但是又预留了一些数量来预防突发状况,当突发状况过去之后,线程池希望只维持核心线程数的线程,所以就弄了个 KeepAliveTime,当线程数大于核心数之后,如果线程空闲了一段时间(KeepAliveTime),就回收线程,直到数量与核心数持平。

    那 workQueue 有什么用?

    缓存任务供线程获取,这里要注意限制工作队列的大小。队列长了,堆积的任务就多,堆积的任务多,后面任务等待的时长就长。

    想想你点击一个按钮是一直转圈等半天没反应舒服,还是直接报错舒服,所以有时心是好的,想尽量完成提交的任务,但是用户体验不如直接拒绝。 更有可能由于允许囤积的任务过多,导致资源耗尽而系统崩溃 。

    所以工作队列起到一个缓冲作用,具体队列长度需要结合线程数,任务的执行时长,能承受的等待时间等。

    你是如何理解拒绝策略的?

    线程数总有拉满的一天,工作队列也是一样,如果两者都满了,此时的提交任务就需要拒绝,默认实现是 AbortPolicy 直接抛出异常。

    img

    剩下的拒绝策略有直接丢弃任务一声不吭的、让提交任务的线程自己运行的、淘汰老的未执行的任务而空出位置的,具体用哪个策略,根据场景选择。当然也可以自定义拒绝策略,实现 RejectedExecutionHandler 这个接口即可。

    所以线程池尽可能只维护核心数量的线程,提供任务队列暂存任务,并提供拒绝策略来应对过载的任务。

    这里还有个细节,如果线程数已经达到核心线程数,那么新增加的任务只会往任务队列里面塞,不会直接给予某个线程,如果任务队列也满了, 新增最大线程数的线程时,任务是可以直接给予新建的线程执行的,而不是入队 。

    感觉已经会了?那再来看几道面试题:

    你说你看过源码,那你肯定知道线程池里的 ctl 是干嘛的咯?

    img

    其实看下注释就很清楚了,ctl 是一个涵盖了两个概念的原子整数类,它将工作线程数和线程池状态结合在一起维护,低 29 位存放 workerCount,高 3 位存放 runState。

    img

    其实并发包中有很多实现都是一个字段存多个值的,比如读写锁的高 16 位存放读锁,低 16 位存放写锁,这种一个字段存放多个值可以更容易的维护多个值之间的一致性,也算是极简主义。

    你知道线程池有几种状态吗?

    img

    注解说的很明白,我再翻译一下:

    • RUNNING:能接受新任务,并处理阻塞队列中的任务
    • SHUTDOWN:不接受新任务,但是可以处理阻塞队列中的任务
    • STOP:不接受新任务,并且不处理阻塞队列中的任务,并且还打断正在运行任务的线程,就是直接撂担子不干了!
    • TIDYING:所有任务都终止,并且工作线程也为0,处于关闭之前的状态
    • TERMINATED:已关闭。

    你知道线程池的状态是如何变迁的吗?

    img

    注释里面也写的很清楚,我再画个图

    img

    为什么要把任务先放在任务队列里面,而不是把线程先拉满到最大线程数?

    我说下我的个人理解。

    其实经过上面的分析可以得知,线程池本意只是让核心数量的线程工作着,不论是 core 的取名,还是 keepalive 的设定,所以你可以直接把 core 的数量设为你想要线程池工作的线程数,而任务队列起到一个缓冲的作用。最大线程数这个参数更像是无奈之举,在最坏的情况下做最后的努力,去新建线程去帮助消化任务。

    所以我个人觉得没有为什么,就是这样设计的,并且这样的设定挺合理。

    当然如果你想要扯一扯 CPU 密集和 I/O 密集,那可以扯一扯。

    原生版线程池的实现可以认为是偏向 CPU 密集的,也就是当任务过多的时候不是先去创建更多的线程,而是先缓存任务,让核心线程去消化,从上面的分析我们可以知道,当处理 CPU 密集型任务的时,线程太多反而会由于线程频繁切换的开销而得不偿失,所以优先堆积任务而不是创建新的线程。

    而像 Tomcat 这种业务场景,大部分情况下是需要大量 I/O 处理的情况就做了一些定制,修改了原生线程池的实现,使得在队列没满的时候,可以创建线程至最大线程数。

    如何修改原生线程池,使得可以先拉满线程数再入任务队列排队?

    如果了解线程池的原理,很轻松的就知道关键点在哪,就是队列的 offer 方法。

    img

    execute 方法想必大家都不陌生,就是给线程池提交任务的方法。在这个方法中可以看到只要在 offer 方法内部判断此时线程数还小于最大线程数的时候返回 false,即可走下面 else ifaddWorker (新增线程)的逻辑,如果数量已经达到最大线程数,直接入队即可。

    详细的我们可以看看 Tomcat 中是如何定制线程的。

    Tomcat 中的定制化线程池实现

    public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor {}
    复制代码
    

    可以看到先继承了 JUC 的线程池,然后我们重点关注一下 execute 这个方法

    img

    这里可以看到,Tomcat 维护了一个 submittedCount 变量,这个变量的含义是统计已经提交的但是还未完成的任务数量(记住这个变量,很关键),所以只要提交一个任务,这个数就加一,并且捕获了拒绝异常,再次尝试将任务入队,这个操作其实是为了尽可能的挽救回一些任务,因为这么点时间差可能已经执行完很多任务,队列腾出了空位,这样就不需要丢弃任务。

    然后我们再来看下代码里出现的 TaskQueue,这个就是上面提到的定制关键点了。

    public class TaskQueue extends LinkedBlockingQueue<Runnable> {
        private transient volatile ThreadPoolExecutor parent = null;
        ........
    }
    复制代码
    

    可以看到这个任务队列继承了 LinkedBlockingQueue,并且有个 ThreadPoolExecutor 类型的成员变量 parent ,我们再来看下 offer 方法的实现,这里就是修改原来线程池任务提交与线程创建逻辑的核心了。

    img

    从上面的逻辑可以看出是有机会在队列还未满的时候,先创建线程至最大线程数的!

    再补充一下,如果对直接返回 false 就能创建线程感到疑惑的话,往上翻一翻,上面贴了原生线程池 execute 的逻辑。

    然后上面的代码其实只看到 submittedCount 的增加,正常的减少在 afterExecute 里实现了。

    img

    而这个 afterExecute 在任务执行完毕之后就会调用,与之对应的还有个 beforeExecute ,在任务执行之前调用。

    img

    至此,想必 Tomcat 中的定制化线程池的逻辑已经明白了。

    如果线程池中的线程在执行任务的时候,抛异常了,会怎么样?

    嘿嘿,细心的同学想必已经瞄到了上面的代码,task.run() 被 try catch finally 包裹,异常被扔到了 afterExecute 中,并且也继续被抛了出来。

    而这一层外面,还有个 try finally ,所以异常的抛出打破了 while 循环,最终会执行 `processWorkerExit方法

    img

    我们来看下这个方法,其实逻辑很简单, 把这个线程废了,然后新建一个线程替换之

    img

    移除了引用等于销毁了,这事儿 GC 会做的。

    所以如果一个任务执行一半就抛出异常,并且你没有自行处理这个异常,那么这个任务就这样戛然而止了,后面也不会有线程继续执行剩下的逻辑,所以要自行捕获和处理业务异常。

    addWorker 的逻辑就不分析了,就是新建一个线程,然后塞到 workers 里面,然后调用 start() 让它跑起来。

    原生线程池的核心线程一定伴随着任务慢慢创建的吗?

    并不是,线程池提供了两个方法:

    • prestartCoreThread:启动一个核心线程
    • prestartAllCoreThreads :启动所有核心线程

    不要小看这个预创建方法,预热很重要,不然刚重启的一些服务有时是顶不住瞬时请求的,就立马崩了,所以有预热线程、缓存等等操作。

    线程池的核心线程在空闲的时候一定不会被回收吗?

    有个 allowCoreThreadTimeOut 方法,把它设置为 true ,则所有线程都会超时,不会有核心数那条线的存在。

    具体是会调用 interruptIdleWorkers 这个方法。

    img

    这里需要讲一下的是 w.tryLock() 这个方法,有些人可能会奇怪,Worker 怎么还能 lock。

    Worker 是属于工作线程的封装类,它不仅实现了 Runnable 接口,还继承了 AQS。

    img

    之所以要继承 AQS 就是为了用上 lock 的状态, 执行任务的时候上锁,任务执行完了之后解锁 ,这样执行关闭线程池等操作的时候可以通过 tryLock 来判断此时线程是否在干活,如果 tryLock 成功说明此时线程是空闲的,可以安全的回收。

    interruptIdleWorkers 对应的还有一个 interruptWorkers 方法,从名字就能看出差别,不空闲的 worker 也直接给打断了。

    根据这两个方法,又可以扯到 shutdown 和 shutdownNow,就是关闭线程池的方法,一个是安全的关闭线程池,会等待任务都执行完毕,一个是粗暴的直接咔嚓了所有线程,管你在不在运行,两个方法分别调用的就是 interruptIdleWorkers() 和 interruptWorkers() 来中断线程。

    img

    这又可以引申出一个问题, shutdownNow 了之后还在任务队列中的任务咋办 ?眼尖的小伙伴应该已经看到了,线程池还算负责,把未执行的任务拖拽到了一个列表中然后返回,至于怎么处理,就交给调用者了!

    详细就是上面的 drainQueue 方法。

    img

    这里可能又会有同学有疑问,都 drainTo 了,为什么还要判断一下队列是否为空,然后进行循环?

    那是因为如果队列是 DelayQueue 或任何其他类型的队列,其中 poll 或 drainTo 可能无法删除某些元素,所以需要遍历,逐个删除它们。

    回到最开始的面试题

    线程池如何动态修改核心线程数和最大线程数?

    其实之所以会有这样的需求是因为线程数是真的不好配置。

    你可能会在网上或者书上看到很多配置公式,比如:

    • CPU 密集型的话,核心线程数设置为 CPU核数+1
    • I/O 密集型的话,核心线程数设置为 2*CPU核数

    比如:

    线程数=CPU核数 *(1+线程等待时间 / 线程时间运行时间)

    这个比上面的更贴合与业务,还有一些理想的公式就不列了。就这个公式而言,这个线程等待时间就很难测,拿 Tomcat 线程池为例,每个请求的等待时间能知道?不同的请求不同的业务,就算相同的业务,不同的用户数据量也不同,等待时间也不同。

    所以说线程数真的很难通过一个公式一劳永逸, 线程数的设定是一个迭代的过程,需要压测适时调整 ,以上的公式做个初始值开始调试是 ok 的。

    再者,流量的突发性也是无法判断的,举个例子 1 秒内一共有 1000 个请求量,但是如果这 1000 个请求量都是在第一毫秒内瞬时进来的呢?

    这就很需要线程池的动态性,也是这个上面这个面试题的需求来源。

    原生的线程池核心我们大致都过了一遍,不过这几个方法一直没提到,先来看看这几个方法:

    img

    我就不一一翻译了,大致可以看出线程池其实已经给予方法暴露出内部的一些状态,例如正在执行的线程数、已完成的任务数、队列中的任务数等等。

    当然你可以想要更多的数据监控都简单的,像 Tomcat 那种继承线程池之后自己加呗,动态调整的第一步监控就这样搞定了!定时拉取这些数据,然后搞个看板,再结合邮件、短信、钉钉等报警方式,我们可以很容易的监控线程池的状态!

    接着就是动态修改线程池配置了。

    img

    可以看到线程池已经提供了诸多修改方法来更改线程池的配置,所以李老都已经考虑到啦!

    同样,也可以继承线程池增加一些方法来修改,看具体的业务场景了。同样搞个页面,然后给予负责人员配置修改即可。

    所以原生线程池已经提供修改配置的方法,也对外暴露出线程池内部执行情况,所以只要我们实时监控情况,调用对应的 set 方法,即可动态修改线程池对应配置。

    回答面试题的时候一定要提监控,显得你是有的放矢的。

    如果你是 JDK 设计者,如何设计?

    其实我觉得这个是紧接着上一题问的,应该算是同一个问题。

    而且 JDK 设计者已经设计好了呀?所以怎么说我也不清楚,不过我们可以说一说具体的实现逻辑呗。

    先来看下设置核心线程数的方法:

    img

    随着注释看下来应该没什么问题,就是那个 k 值我说一下,李老觉得核心线程数是配置了,但是此时线程池内部是否需要这么多线程是不确定的,那么就按工作队列里面的任务数来,直接按任务数立刻新增线程,当任务队列为空了之后就终止新增。

    这其实和李老设计的默认核心线程数增加策略是一致的,都是懒创建线程。

    再看看设置最大线程数的方法:

    img

    没啥花头,调用的 interruptIdleWorkers 之前都分析过了。

    我再贴一下之前写过的线程池设计面试题吧。

    如果要让你设计一个线程池,你要怎么设计?

    这种设计类问题还是一样,先说下理解,表明你是知道这个东西的用处和原理的,然后开始 BB。基本上就是按照现有的设计来说,再添加一些个人见解。

    线程池讲白了就是存储线程的一个容器,池内保存之前建立过的线程来重复执行任务,减少创建和销毁线程的开销,提高任务的响应速度,并便于线程的管理。

    我个人觉得如果要设计一个线程池的话得考虑池内工作线程的管理、任务编排执行、线程池超负荷处理方案、监控。

    初始化线程数、核心线程数、最大线程池都暴露出来可配置,包括超过核心线程数的线程空闲消亡配置。

    任务的存储结构可配置,可以是无界队列也可以是有界队列,也可以根据配置分多个队列来分配不同优先级的任务,也可以采用 stealing 的机制来提高线程的利用率。

    再提供配置来表明此线程池是 IO 密集还是 CPU 密集型来改变任务的执行策略。

    超负荷的方案可以有多种,包括丢弃任务、拒绝任务并抛出异常、丢弃最旧的任务或自定义等等。

    线程池埋好点暴露出用于监控的接口,如已处理任务数、待处理任务数、正在运行的线程数、拒绝的任务数等等信息。

    我觉得基本上这样答就差不多了,等着面试官的追问就好。

    注意不需要跟面试官解释什么叫核心线程数之类的,都懂的没必要。

    当然这种开放型问题还是仁者见仁智者见智,我这个不是标准答案,仅供参考。

    关于线程池的一点碎碎念

    线程池的好处我们都知道了,但是不是任何时刻都上线程池的,我看过一些奇怪的代码,就是为了用线程池而用线程池…

    还有需要根据不同的业务划分不同的线程池,不然会存在一些耗时的业务影响了另一个业务导致这个业务崩了,然后都崩了的情况,所以要做好线程池隔离。

    最后:

    最近我整理了整套**《JAVA核心知识点总结》**,说实话 ,作为一名Java程序员,不论你需不需要面试都应该好好看下这份资料。拿到手总是不亏的~我的不少粉丝也因此拿到腾讯字节快手等公司的Offer

    进[Java架构资源交流群] ,找管理员获取哦-!

    还有需要根据不同的业务划分不同的线程池,不然会存在一些耗时的业务影响了另一个业务导致这个业务崩了,然后都崩了的情况,所以要做好线程池隔离。

    最后:

    最近我整理了整套**《JAVA核心知识点总结》**,说实话 ,作为一名Java程序员,不论你需不需要面试都应该好好看下这份资料。拿到手总是不亏的~我的不少粉丝也因此拿到腾讯字节快手等公司的Offer

    进[Java架构资源交流群] ,找管理员获取哦-!

    展开全文
  • 1000 多个并发线程,10 台机器,每台机器 4 核,设计线程池大小。 这题给的信息非常的简陋,但是简陋的好处就是想象空间足够大。 第一眼看到这题的时候,我直观的感受到了两个考点: 线程池设计。 负载均衡策略。 ...

    这是why哥的第 71 篇原创文章

    一道面试题

    兄弟们,怎么说?

    我觉得如果你工作了两年左右的时间,或者是突击准备了面试,这题回答个八成上来,应该是手到擒来的事情。这题中规中矩,考点清晰,可以说的东西不是很多。

    但是这都上血书了,那不得分析一波?

    先把这个面试题拿出来一下:

    1000 多个并发线程,10 台机器,每台机器 4 核,设计线程池大小。

    这题给的信息非常的简陋,但是简陋的好处就是想象空间足够大。

    第一眼看到这题的时候,我直观的感受到了两个考点:

    1. 线程池设计。

    2. 负载均衡策略。

    我就开门见山的给你说了,这两个考点,刚好都在我之前的文章的射程范围之内:

    《如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答》

    《吐血输出:2万字长文带你细细盘点五种负载均衡策略》

    下面我会针对我感受到的这两个考点去进行分析。

    线程池设计

    我们先想简单一点:1000 个并发线程交给 10 台机器去处理,那么 1 台机器就是承担 100 个并发请求。

    100 个并发请求而已,确实不多。

    而且他也没有说是每 1 秒都有 1000 个并发线程过来,还是偶尔会有一次 1000 个并发线程过来。

    先从线程池设计的角度去回答这个题。

    要回答好这个题目,你必须有两个最基本的知识贮备:

    1. 自定义线程池的 7 个参数。

    2. JDK 线程池的执行流程。

    先说第一个,自定义线程池的 7 个参数。

    java.util.concurrent.ThreadPoolExecutor#ThreadPoolExecutor

    害,这 7 个参数我真的都不想说了,你去翻翻历史文章,我都写过多少次了。你要是再说不出个头头是道的,你都对不起我写的这些文章。

    而且这个类上的 javadoc 已经写的非常的明白了。这个 javadoc 是 Doug Lea 老爷子亲自写的,你都不拜读拜读?

    为了防止你偷懒,我把老爷子写的粘下来,我们一句句的看。

    关于这几个参数,我通过这篇文章再说最后一次。

    如果以后的文章我要是再讲这几个参数,我就不叫 why 哥,以后你们就叫我小王吧。

    写着写着,怎么还有一种生气的感觉呢。似乎突然明白了当年在讲台上越讲越生气的数学老师说的:这题我都讲了多少遍了!还有人错?

    好了,不生气了,说参数:

    1. corePoolSize:the number of threads to keep in the pool, even if they are idle, unless {@code allowCoreThreadTimeOut} is set (核心线程数大小:不管它们创建以后是不是空闲的。线程池需要保持 corePoolSize 数量的线程,除非设置了 allowCoreThreadTimeOut。)

    2. maximumPoolSize:the maximum number of threads to allow in the pool。 (最大线程数:线程池中最多允许创建 maximumPoolSize 个线程。)

    3. keepAliveTime:when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating。 (存活时间:如果经过 keepAliveTime 时间后,超过核心线程数的线程还没有接受到新的任务,那就回收。)

    4. unit:the time unit for the {@code keepAliveTime} argument (keepAliveTime 的时间单位。)

    5. workQueue:the queue to use for holding tasks before they are executed. This queue will hold only the {@code Runnable} tasks submitted by the {@code execute} method。 (存放待执行任务的队列:当提交的任务数超过核心线程数大小后,再提交的任务就存放在这里。它仅仅用来存放被 execute 方法提交的 Runnable 任务。所以这里就不要翻译为工作队列了,好吗?不要自己给自己挖坑。)

    6. threadFactory:the factory to use when the executor creates a new thread。 (线程工程:用来创建线程工厂。比如这里面可以自定义线程名称,当进行虚拟机栈分析时,看着名字就知道这个线程是哪里来的,不会懵逼。)

    7. handler :the handler to use when execution is blocked because the thread bounds and queue capacities are reached。 (拒绝策略:当队列里面放满了任务、最大线程数的线程都在工作时,这时继续提交的任务线程池就处理不了,应该执行怎么样的拒绝策略。)

    第一个知识贮备就讲完了,你先别开始背,这玩意你背下来有啥用,你得结合着执行流程去理解。

    接下来我们看第二个:JDK 线程池的执行流程。

    一图胜千言:

    关于 JDK 线程池的 7 个参数和执行流程。

    虽然我很久没有参加面试了,但是我觉得这题属于必考题吧。

    所以如果你真的还不会,麻烦你写个 Demo ,换几个参数调试一下。把它给掌握了。

    而且还得多注意由这些知识点引申出来的面试题。

    比如从图片也可以看出来,JDK 线程池中如果核心线程数已经满了的话,那么后面再来的请求都是放到阻塞队列里面去,阻塞队列再满了,才会启用最大线程数。

    但是你得知道,假如我们是 web 服务,请求是通过 Tomcat 进来的话,那么 Tomcat 线程池的执行流程可不是这样的。

    Tomcat 里面的线程池的运行过程是:如果核心线程数用完了,接着用最大线程数,最后才提交任务到队列里面去的。这样是为了保证响应时间优先。

    所以,Tomcat 的执行流程是这样的:

    其技术细节就是自己重写了队列的 offer 方法。在这篇文章里面说的很清楚了,大家可以看看:

    《每天都在用,但你知道 Tomcat 的线程池有多努力吗?》

    好的,前面两个知识点铺垫完成了。

    这个题,从线程池设计的角度,我会这样去回答:

    前面我们说了,10 个机器,1000 个请求并发,平均每个服务承担 100 个请求。服务器是 4 核的配置。

    那么如果是 CPU 密集型的任务,我们应该尽量的减少上下文切换,所以核心线程数可以设置为 5,队列的长度可以设置为 100,最大线程数保持和核心线程数一致。

    如果是 IO 密集型的任务,我们可以适当的多分配一点核心线程数,更好的利用 CPU,所以核心线程数可以设置为 8,队列长度还是 100,最大线程池设置为 10。

    当然,上面都是理论上的值。

    我们也可以从核心线程数等于 5 开始进行系统压测,通过压测结果的对比,从而确定最合适的设置。

    同时,我觉得线程池的参数应该是随着系统流量的变化而变化的。

    所以,对于核心服务中的线程池,我们应该是通过线程池监控,做到提前预警。同时可以通过手段对线程池响应参数,比如核心线程数、队列长度进行动态修改。

    上面的回答总结起来就是四点:

    1. CPU密集型的情况。
    2. IO密集型的情况。
    3. 通过压测得到合理的参数配置。
    4. 线程池动态调整。

    前两个是教科书上的回答,记下来就行,面试官想听到这两个答案。

    后两个是更具有实际意义的回答,让面试官眼前一亮。

    基于这道面试题有限的信息,设计出来的线程池队列长度其实只要大于 100 就可以。

    甚至还可以设置的极限一点,比如核心线程数和最大线程数都是 4,队列长度为 96,刚好可以承担这 100 个请求,多一个都不行了。

    所以这题我觉得从这个角度来说,并不是要让你给出一个完美的解决方案,而是考察你对于线程池参数的理解和技术的运用。

    面试的时候我觉得这个题答到这里就差不多了。

    接下来,我们再发散一下。

    比如面试官问:如果我们的系统里面没有运用线程池,那么会是怎么样的呢?

    首先假设我们开发的系统是一个运行在 Tomcat 容器里面的,对外提供 http 接口的 web 服务。

    系统中没有运用线程池相关技术。那么我们可以直接抗住这 100 个并发请求吗?

    答案是可以的。

    Tomcat 里面有一个线程池。其 maxThreads 默认值是 200(假定 BIO 模式):

    maxThreads 用完了之后,进队列。队列长度(acceptCount)默认是 100:

    在 BIO 的模式下,Tomcat 的默认配置,最多可以接受到 300 (200+100)个请求。再多就是连接拒绝,connection refused。

    所以,你要说处理这 100 个并发请求,那不是绰绰有余吗?

    但是,如果是每秒 100 个并发请求,源源不断的过来,那就肯定是吃不消了。

    这里就涉及到两个层面的修改:

    1. Tomcat 参数配置的调优。
    2. 系统代码的优化。

    针对 Tomcat 参数配置的调优,我们可以适当调大其 maxThreads 等参数的值。

    针对系统代码的优化,我们就可以引入线程池技术,或者引入消息队列。总之其目的是增加系统吞吐量。

    同理,假设我们是一个 Dubbo 服务,对外提供的是 RPC 接口。

    默认情况下,服务端使用的是 fixed 线程池,核心线程池数和最大线程数都是 200。队列长度默认为 0:

    那么处理这个 100 个并发请求也是绰绰有余的。

    同样,如果是每秒 100 个并发请求源源不断的过来,那么很快就会抛出线程池满的异常:

    解决套路其实是和 Tomcat 的情况差不多的,调参数,改系统,加异步。

    这个情况下的并发,大多数系统还是抗住的。

    面试官还可以接着追问:如果这时由于搞促销活动,系统流量翻了好倍,那你说这种情况下最先出现性能瓶颈的地方是什么?

    最先出问题的地方肯定是数据库嘛,对吧。

    那么怎么办?

    分散压力。分库分表、读写分离这些东西往上套就完事了。

    然后在系统入口的地方削峰填谷,引入缓存,如果可以,把绝大部分流量拦截在入口处。

    对于拦不住的大批流量,关键服务节点还需要支持服务熔断、服务降级。

    实在不行,加钱,堆机器。没有问题是不能通过堆机器解决的,如果有,那么就是你堆的机器不够多。

    面试反正也就是这样的套路。看似一个发散性的题目,其实都是有套路可寻的。

    好了,第一个角度我觉得我能想到的就是这么多了。

    首先正面回答了面试官线程池设计的问题。

    然后分情况聊了一下如果我们项目中没有用线程池,能不能直接抗住这 1000 的并发。

    最后简单讲了一下突发流量的情况。

    接下来,我们聊聊负载均衡。

    负载均衡策略

    我觉得这个考点虽然稍微隐藏了一下,但还是很容易就挖掘到的。

    毕竟题目中已经说了:10 台机器。

    而且我们也假设了平均 1 台处理 100 个情况。

    这个假设的背后其实就是一个负载均衡策略:轮询负载均衡。

    如果负载均衡策略不是轮询的话,那么我们前面的线程池队列长度设计也是有可能不成立的。

    还是前面的场景,如果我们是运行在 Tomcat 容器中,假设前面是 nginx,那么 nginx 的负载均衡策略有如下几种:

    1. (加权)轮询负载均衡
    2. 随机负载均衡
    3. 最少连接数负载均衡
    4. 最小响应时间负载均衡
    5. ip_hash负载均衡
    6. url_hash负载均衡

    如果是 RPC 服务,以 Dubbo 为例,有下面几种负载均衡策略:

    1. (加权)轮询负载均衡
    2. 随机负载均衡
    3. 最少活跃数负载均衡
    4. 最小响应时间负载均衡
    5. 一致性哈希负载均衡

    哦,对了。记得之前还有一个小伙伴问我,在 Dubbo + zookeeper 的场景下,负载均衡是 Dubbo 做的还是 zk 做的?

    肯定是 Dubbo 啊,朋友。源码都写在 Dubbo 里面的,zk 只是一个注册中心,关心的是自己管理着几个服务,和这几个服务的上下线。

    你要用的时候,我把所有能用的都给你,至于你到底要用那个服务,也就是所谓的负载均衡策略,这不是 zk 关心的事情。

    不扯远了,说回来。

    假设我们用的是随机负载均衡,我们就不能保证每台机器各自承担 100 个请求了。

    这时候我们前面给出的线程池设置就是不合理的。

    常见的负载均衡策略对应的优缺点、适用场景可以看这个表格:

    关于负载均衡策略,我的《吐血输出:2万字长文带你细细盘点五种负载均衡策略》这篇文章,写了 2 万多字,算是写的很清楚了,这里就不赘述了。

    说起负载均衡,我还想起了之前阿里举办的一个程序设计大赛。赛题是《自适应负载均衡的设计实现》。

    赛题的背景是这样的:

    负载均衡是大规模计算机系统中的一个基础问题。灵活的负载均衡算法可以将请求合理地分配到负载较少的服务器上。

    理想状态下,一个负载均衡算法应该能够最小化服务响应时间(RTT),使系统吞吐量最高,保持高性能服务能力。

    自适应负载均衡是指无论处在空闲、稳定还是繁忙状态,负载均衡算法都会自动评估系统的服务能力,更好的进行流量分配,使整个系统始终保持较好的性能,不产生饥饿或者过载、宕机。

    具体题目和获奖团队答辩可以看这里:

    题目:https://tianchi.aliyun.com/competition/entrance/231714/information?spm=a2c22.12849246.1359729.1.6b0d372cO8oYGK
    
    答辩:https://tianchi.aliyun.com/course/video?spm=5176.12586971.1001.1.32de8188ivjLZj&liveId=41090
    

    推荐大家有兴趣的去看一下,还是很有意思的,可以学到很多的东西。

    扩展阅读

    这一小节,我截取自《分布式系统架构》这本书里面,我觉得这个示例写的还不错,分享给大家:

    这是一个购物商场的例子:

    系统部署在一台 4C/8G 的应用服务器上、数据在一台 8C/16G 的数据库上,都是虚拟机。

    假设系统总用户量是 20 万,日均活跃用户根据不同系统场景稍有区别,此处取 20%,就是 4 万。

    按照系统划分二八法则,系统每天高峰算 4 小时,高峰期活跃用户占比 80%,高峰 4 小时内有 3.2 万活跃用户。

    每个用户对系统发送请求,如每个用户发送 30 次,高峰期间 3.2 万用户发起的请求是 96 万次,QPS=960 000/(4x60x60)≈67 次请求,每秒处理 67 次请求,处理流程如下图有所示:

    一次应用操作数据库增删改查(CRUD)次数平均是操作应用的三倍,具体频率根据系统的操作算平均值即可。一台应用、数据库能处理多少请求呢?

    具体分析如下。

    1. 首先应用、数据库都分别部署在服务器,所以和服务器的性能有直接关系,如 CPU、内存、磁盘存储等。

    2. 应用需要部署在容器里面,如 Tomcat、Jetty、JBoss 等,所以和容器有关系,容器的系统参数、配置能增加或减少处理请求的数目。

    3. Tomcat 部署应用。Tomcat 里面需要分配内存,服务器共 8GB 内存,服务器主要用来部署应用,无其他用途,所以设计 Tomcat 的可用内存为8/2=4GB (物理内存的1/2),同时设置一个线程需要 128KB 的内存。由于应用服务器默认的最大线程数是 1000(可以参考系统配置文件),考虑到系统自身处理能力,调整 Tomcat 的默认线程数至 600,达到系统的最大处理线程能力。到此一台应用最大可以处理 1000 次请求,当超过 1000 次请求时,暂存到队列中,等待线程完成后进行处理。

    4. 数据库用 MySQL。MySQL 中有连接数这个概念,默认是 100 个,1 个请求连接一次数据库就占用 1 个连接,如果 100 个请求同时连接数据库,数据库的连接数将被占满,后续的连接需要等待,等待之前的连接释放掉。根据数据库的配置及性能,可适当调整默认的连接数,本次调整到 500,即可以处理 500 次请求。

    显然当前的用户数以及请求量达不到高并发的条件,如果活跃用户从 3.2 万扩大到 32 万,每秒处理 670 次请求,已经超过默认最大的 600 ,此时会出现高并发的情况,高并发分为高并发读操作和高并发写操作。

    好了,书上分享的案例就是这样的。

    荒腔走板

    上周五晚上去看了《金刚川》。

    据说拍摄周期只有 2 个月,但是电影整体来说还是挺好看的。前半段比较的平缓,但是后半段高潮迭起。对我而言,是有好几个泪点的。

    第一个泪点是“喀秋莎”火箭炮出来的时候,像烟花一样,战士们说:这就是我们的喀秋莎吧?

    第二个泪点是张译扮演的张飞,一个人对抗美军侦察机,在高炮上高呼:“千古流芳莽撞人”的时候。

    用老张的话说:不要以为这是神剧套路,历史现场比这还要惨烈。

    张译的演技,没的说。一个人撑起了这个片段的一半。影帝预定一个。

    第三个泪点就是燃烧弹落到江面,然后随之响起的《我的祖国》 BGM 了:

    一条大河波浪宽

    风吹稻花香两岸

    我家就在岸上住

    听惯了艄公的号子

    看惯了船上的白帆

    这是美丽的祖国

    是我生长的地方

    配合着整个修桥的故事,简直就是泪点暴击。

    实话实话,《金刚川》这个电影不是特别的完美。但是我还是力荐大家去电影院支持这部电影。

    因为这部电影,拍在抗美援朝 70 周年纪念的这个特殊节点。能让有幸生活在和平时代的我们知道,现在的和平长安,国富民强不是从天上掉下来的,是 70 年前,那一群只有十七八岁的“最可爱的人”用生命打下来的。

    之前看《人民日报》里面的一个短视频,一位老兵说的话特别的感动,他说:

    什么是祖国?当我们跨过鸭绿江,看到战火的时候,我身后就是祖国。

    和平来之不易,吾辈自当珍惜。

    向最可爱的人致敬。

    最后说一句(求关注)

    好了,看到了这里安排个 “一键三连”吧,周更很累的,不要白嫖我,需要一点正反馈。

    才疏学浅,难免会有纰漏,如果你发现了错误的地方,可以在留言区提出来,我对其加以修改。 感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。

    我是 why,一个被代码耽误的文学创作者,不是大佬,但是喜欢分享,是一个又暖又有料的四川好男人。

    还有,欢迎关注我呀。

    展开全文
  • 线程池设计思路 线程池是什么 我们先来打个比方,线程池就好像一个工具箱,我们每次需要拧螺丝的时候都要从工具箱里面取出一个螺丝刀来,有时候需要取出一个来拧,有时候螺丝多的时候需要多个人取出多个来拧,拧完...
  • 理解Java并发工具包线程池设计

    千次阅读 2018-09-12 21:25:58
    为什么需要线程池? 答:主要原因是因为创建一个线程开销太大,尤其是对大量的小任务需要执行这种场景。 在Java里面创建一个线程,需要包含的东西: (1)它为一个线程堆栈分配内存,该堆栈为每个线程方法调用...
  • 20.简单设计实现线程池

    千次阅读 2018-03-29 16:54:49
    上代码之前,要先补充一下线程池构造的核心几个点1. 线程池里的核心线程数与最大线程数2. 线程池里真正工作的线程类worker3. 线程池里用来存取任务的队列4. 线程中的任务类task 另外一个比较简单的方法,推荐...
  • 碰到一个很有意思的问题 : 如何设计一个线程池
  • 线程池 一、为什么要采用线程池? 线程虽然是一种轻量级的工具,但是创建与关闭线程依然需要花费时间,如果为每一个小的任务都创建一个线程,则有可能创建和销毁线程所占用的时间大于线程真实工作所消耗的时间。 ...
  • 设计实现一个线程池(120分钟) 需求描述: 线程池这种技术及时为了解决减少线程的创建时间和销毁时间,在客户端的任务请求来到之前,服务器已经创建好了多个线程并把它放在线程池里以供客户端的任务使用,从而提高...
  • 线程池设计,一篇就够

    千次阅读 2018-12-20 17:03:30
    文章目录为什么需要线程池条件变量结合互斥锁 + 任务队列eventfd + epolleventfd + epoll + 多队列的设计Lock-free 为什么需要线程池 在那些情况下我们会使用到多线程: 阻塞调用(阻塞IO调用、等待资源) 耗时...
  • 系统编程之线程池 在进程中,创建多个线程,每当有任务执行时,直接调用已创建好的线程执行,不需要额外创建线程。 实际情况是线程的数量受限于计算机内存不能创建太多,那么就可以利用线程池设置线程数量上限 .....
  • 线程池参数设计技巧

    2018-07-20 09:18:28
    ThreadPoolExecutor线程池参数设置技巧 一、ThreadPoolExecutor的重要参数 corePoolSize:核心线程数 核心线程会一直存活,及时没有任务需要执行 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新...
  • 线程池设计与实现解析 概述 前言: 主要分享基于ThreadPoolExecutor实现的线程池设计和实现. 编写目的 介绍线程相关的基础知识; 介绍线程池的原理,及如何合理使用线程池; 适用人群 有Java基础,了解...
  • 当然,如果你仅把它当做面试题,那就太遗憾了, 这是一个非常好的问题,能反映出开发者对线程池的理解深入程度以及对高性能服务结构的设计能力。 线程池本质上是生产者和消费者模型,包括三要素: 往线程池队列...
  • 线程池就是创建若干个可执行的线程放入一个池(容器)中,有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,而是仍然在线程池中等待下一个任务。 一个线程池包括以下四个基本组成部分: ...
  • Java线程池设计与实现

    千次阅读 2017-01-17 14:38:36
    权声明:本文为博主原创文章,未经博主允许不得转载。 ...1 设计思路 ...多线程技术主要是针对CPU解决高效执行任务...如果在服务器上创建线程和销毁线程的时间之和大于线程的执行时间,那么就可以考虑采用线程池。 
  • cpu密集型的任务 一般设置 线程数 = 核心数N + 1 io密集型的任务 一般设置 线程数 = 核心数...如果都存在,则分开两个线程池 实际应用中 线程数 = ((线程CPU时间+线程等待时间)/ 线程CPU时间 ) * 核心数N ...
  • 如何合理设置线程池大小

    千次阅读 2019-07-11 15:59:20
    要想合理的配置线程池的大小,首先得分析任务的特性,可以从以下几个角度分析: 任务的性质:CPU密集型任务、IO密集型任务、混合型任务。 任务的优先级:高、中、低。 任务的执行时间:长、中、短。 任务的依赖...
  • 线程池设计详解

    2021-03-19 18:32:15
    J.U.C提供的线程池:ThreadPoolExecutor类,帮助开发人员管理线程并方便地执行并行任务。了解并合理使用线程池,是一个开发人员必修的基本功。 文章目录线程池是什么 线程池是什么 线程池(Thread Pool)是一种基于...
  • Thread Pool(线程池
  • 线程池线程池:在设计线程池上的存在的问题以及解决:线程池的实现框架:基于IO密集型和CPU密集型的线程池如何设计对于CPU密集型程序的线程池设计:对于IO密集型程序的线程池设计:线程池的实现: 线程池: 概念: ...
  • 1、cpu密集型,多是运算型应用,io操作较少,主要消耗cpu资源,此时若开启多个线程,容易造成频繁的cpu上线文切换,...网上对其线程池核心线程数大小有两种说法,第一种,线程数=cpu核数/(1-阻塞系数),阻塞系数大小范
  • 简单的线程池实现。网上有好多demo,至于为什么需要线程池,由于每次系统调用都会创建一个线程的话,系统的开销比较大 ,如果用一个线程池来复用线程的话,可以有效避免系统的开销 JDK内置线程池 线程池的...
  • 线程池设计和实现

    2019-06-01 16:40:40
    线程池这个概念在之前的文章中曾经多次谈及过,但每次都是浅尝辄止,从来没有实现过。因为感觉这个东西很高深莫测,觉得自己很难实现。但是通过这几天的研究,决定在这篇文章中尝试实现一下线程池,如果大家没有看到...
  • 设计模式-线程池模式

    千次阅读 2019-04-03 08:27:01
    当没有请求工作的时候,所有的工人线程都会等待新的请求过来,一旦有工作到达,就马上从线程池中唤醒某个线程来执行任务,执行完毕后继续在线程池中等待任务池的工作请求的到达。 任务池:主要是存储接受请求的集合...
  • 线程池主要属性分析

    千次阅读 2018-08-04 10:12:36
    Java线程池使用说明 一简介 线程的使用在java中占有极其重要的地位,在jdk1.4极其之前的jdk版本中,关于线程池的使用是极其简陋的。在jdk1.5之后这一情况有了很大的改观。Jdk1.5之后加入了java.util.concurrent包...
  • J.U.C提供的线程池:ThreadPoolExecutor类,帮助开发人员管理线程并方便地执行并行任务。了解并合理使用线程池,是一个开发人员必修的基本功。线程池参数配置方案显得十分重要。 一、参数设置的传统方案 1. 线程池...
  • 线程池设计模式

    2019-12-27 22:20:21
    线程池 线程池:        一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个 线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 143,859
精华内容 57,543
关键字:

怎么设计线程池