精华内容
下载资源
问答
  • 今天看到一篇hao文章,...简单点说,线程池就是有一堆已经创建好了的线程,初始它们都处于空闲等待状态,当有新的任务需要处理的时候,就从这个池子里面取一个空闲等 待的线程来处理该任务,当处理完成了就再次把该

    http://blog.chinaunix.net/uid-26983585-id-3336491.html

    今天看到一篇hao文章,现在跟大家分享一下,但是必须得自己去学会实践!


    什么是线程池?

    简单点说,线程池就是有一堆已经创建好了的线程,初始它们都处于空闲等待状态,当有新的任务需要处理的时候,就从这个池子里面取一个空闲等 待的线程来处理该任务,当处理完成了就再次把该线程放回池中,以供后面的任务使用。当池子里的线程全都处理忙碌状态时,线程池中没有可用的空闲等待线程, 此时,根据需要选择创建一个新的线程并置入池中,或者通知任务线程池忙,稍后再试。
    为什么要用线程池?

    为什么要用线程池?

             我们说,线程的创建和销毁比之进程的创建和销毁是轻量级的,但是当我们的任务需要大量进行大量线程的创建和销毁操作时,这个消耗就会变成的相当大。比如, 当你设计一个压力性能测试框架的时候,需要连续产生大量的并发操作,这个是时候,线程池就可以很好的帮上你的忙。线程池的好处就在于线程复用,一个任务处理完成后,当前线程可以直接处理下一个任务,而不是销毁后再创建,非常适用于连续产生大量并发任务的场合。


    线程池工作原理

    线程池中每一个线程的工作过程如下:


    图 1: 线程的工作流程

    线程池的任务就在于负责这些线程的创建,销毁和任务处理参数

    传递、唤醒和等待。

    1.      创建若干线程,置入线程池

    2.      任务达到时,从线程池取空闲线程

    3.      取得了空闲线程,立即进行任务处理

    4.      否则新建一个线程,并置入线程池,执行3

    5.      如果创建失败或者线程池已满,根据设计策略选择返回错误或将任务置入处理队列,等待处理

    6.      销毁线程池






    线程池设计



    数据结构设计


    任务设计
    [cpp] view plaincopy
    typedef struct tp_work_desc_s TpWorkDesc;  
    typedef void (*process_job)(TpWorkDesc*job);  
    struct tp_work_desc_s {  
             void *ret; //call in, that is arguments  
             void *arg; //call out, that is return value  
    };  

    其中,TpWorkDesc是任务参数描述,arg是传递给任务的参数,ret则是任务处理完成后的返回值;
    process_job函数是任务处理函数原型,每个任务处理函数都应该这样定义,然后将它作为参数传给线程池处理,线程池将会选择一个空闲线程通过调用该函数来进行任务处理;

     

    线程设计
    [cpp] view plaincopy
    typedef struct tp_thread_info_s TpThreadInfo;  
    struct tp_thread_info_s {  
             pthread_t thread_id; //thread id num  
             TPBOOL is_busy; //thread status:true-busy;flase-idle  
             pthread_cond_t thread_cond;  
             pthread_mutex_t thread_lock;  
             process_job proc_fun;  
             TpWorkDesc* th_job;  
             TpThreadPool* tp_pool;  
    };  

    TpThreadInfo是对一个线程的描述。
    thread_id是该线程的ID;

    is_busy用于标识该线程是否正处理忙碌状态;

    thread_cond用于任务处理时的唤醒和等待;

    thread_lock,用于任务加锁,用于条件变量等待加锁;

    proc_fun是当前任务的回调函数地址;

    th_job是任务的参数信息;

    tp_pool是所在线程池的指针;

     

    线程池设计
    [cpp] view plaincopy
    typedef struct tp_thread_pool_s TpThreadPool;  
    struct tp_thread_pool_s {  
             unsigned min_th_num; //min thread number in the pool  
             unsigned cur_th_num; //current thread number in the pool  
             unsigned max_th_num; //max thread number in the pool  
             pthread_mutex_t tp_lock;  
             pthread_t manage_thread_id; //manage thread id num  
             TpThreadInfo* thread_info;  
             Queue idle_q;  
             TPBOOL stop_flag;  
    };  

    TpThreadPool是对线程池的描述。
    min_th_num是线程池中至少存在的线程数,线程池初始化的过程中会创建min_th_num数量的线程;

    cur_th_num是线程池当前存在的线程数量;

    max_th_num则是线程池最多可以存在的线程数量;

    tp_lock用于线程池管理时的互斥;

    manage_thread_id是线程池的管理线程ID;

    thread_info则是指向线程池数据,这里使用一个数组来存储线程池中线程的信息,该数组的大小为max_th_num;

    idle_q是存储线程池空闲线程指针的队列,用于从线程池快速取得空闲线程;

    stop_flag用于线程池的销毁,当stop_flag为FALSE时,表明当前线程池需要销毁,所有忙碌线程在处理完当前任务后会退出;


    算法设计


    线程池的创建和初始化
    线程创建

    创建伊始,线程池线程容量大小上限为max_th_num,初始容量为min_th_num;

    [cpp] view plaincopy
    TpThreadPool *tp_create(unsigned min_num, unsigned max_num) {  
        TpThreadPool *pTp;  
        pTp = (TpThreadPool*) malloc(sizeof(TpThreadPool));  
      
        memset(pTp, 0, sizeof(TpThreadPool));  
      
        //init member var  
        pTp->min_th_num = min_num;  
        pTp->cur_th_num = min_num;  
        pTp->max_th_num = max_num;  
        pthread_mutex_init(&pTp->tp_lock, NULL);  
      
        //malloc mem for num thread info struct  
        if (NULL != pTp->thread_info)  
            free(pTp->thread_info);  
        pTp->thread_info = (TpThreadInfo*) malloc(sizeof(TpThreadInfo) * pTp->max_th_num);  
        memset(pTp->thread_info, 0, sizeof(TpThreadInfo) * pTp->max_th_num);  
      
        return pTp;  
    }  

    线程初始化

    [cpp] view plaincopy
    TPBOOL tp_init(TpThreadPool *pTp) {  
        int i;  
        int err;  
        TpThreadInfo *pThi;  
      
        initQueue(&pTp->idle_q);  
        pTp->stop_flag = FALSE;  
      
        //create work thread and init work thread info  
        for (i = 0; i < pTp->min_th_num; i++) {  
            pThi = pTp->thread_info +i;  
            pThi->tp_pool = pTp;  
            pThi->is_busy = FALSE;  
            pthread_cond_init(&pThi->thread_cond, NULL);  
            pthread_mutex_init(&pThi->thread_lock, NULL);  
            pThi->proc_fun = def_proc_fun;  
            pThi->th_job = NULL;  
            enQueue(&pTp->idle_q, pThi);  
      
            err = pthread_create(&pThi->thread_id, NULL, tp_work_thread, pThi);  
            if (0 != err) {  
                perror("tp_init: create work thread failed.");  
                clearQueue(&pTp->idle_q);  
                return FALSE;  
            }  
        }  
      
        //create manage thread  
        err = pthread_create(&pTp->manage_thread_id, NULL, tp_manage_thread, pTp);  
        if (0 != err) {  
            clearQueue(&pTp->idle_q);  
            printf("tp_init: creat manage thread failed\n");  
            return FALSE;  
        }  
      
        return TRUE;  
    }  


    初始线程池中线程数量为min_th_num,对这些线程一一进行初始化;
    将这些初始化的空闲线程一一置入空闲队列;

    创建管理线程,用于监控线程池的状态,并适当回收多余的线程资源;

     

    线程池的关闭和销毁
    [cpp] view plaincopy
    void tp_close(TpThreadPool *pTp, TPBOOL wait) {  
        unsigned i;  
      
        pTp->stop_flag = TRUE;  
        if (wait) {  
            for (i = 0; i < pTp->cur_th_num; i++) {  
                pthread_cond_signal(&pTp->thread_info[i].thread_cond);  
            }  
            for (i = 0; i < pTp->cur_th_num; i++) {  
                pthread_join(pTp->thread_info[i].thread_id, NULL);  
                pthread_mutex_destroy(&pTp->thread_info[i].thread_lock);  
                pthread_cond_destroy(&pTp->thread_info[i].thread_cond);  
            }  
        } else {  
            //close work thread  
            for (i = 0; i < pTp->cur_th_num; i++) {  
                kill((pid_t)pTp->thread_info[i].thread_id, SIGKILL);  
                pthread_mutex_destroy(&pTp->thread_info[i].thread_lock);  
                pthread_cond_destroy(&pTp->thread_info[i].thread_cond);  
            }  
        }  
        //close manage thread  
        kill((pid_t)pTp->manage_thread_id, SIGKILL);  
        pthread_mutex_destroy(&pTp->tp_lock);  
      
        //free thread struct  
        free(pTp->thread_info);  
        pTp->thread_info = NULL;  
    }  

    线程池关闭的过程中,可以选择是否对正在处理的任务进行等待,如果是,则会唤醒所有任务,然后等待所有任务执行完成,然后返回;如果不是,则将立即杀死所有线程,然后返回,注意:这可能会导致任务的处理中断而产生错误!
     

    任务处理
    [cpp] view plaincopy
    TPBOOL tp_process_job(TpThreadPool *pTp, process_job proc_fun, TpWorkDesc *job) {  
        TpThreadInfo *pThi ;  
        //fill pTp->thread_info's relative work key  
        pthread_mutex_lock(&pTp->tp_lock);  
        pThi = (TpThreadInfo *) deQueue(&pTp->idle_q);  
        pthread_mutex_unlock(&pTp->tp_lock);  
        if(pThi){  
            pThi->is_busy =TRUE;  
            pThi->proc_fun = proc_fun;  
            pThi->th_job = job;  
            pthread_cond_signal(&pThi->thread_cond);  
            DEBUG("Fetch a thread from pool.\n");  
            return TRUE;  
        }  
        //if all current thread are busy, new thread is created here  
        pthread_mutex_lock(&pTp->tp_lock);  
        pThi = tp_add_thread(pTp);  
        pthread_mutex_unlock(&pTp->tp_lock);  
      
        if(!pThi){  
            DEBUG("The thread pool is full, no more thread available.\n");  
            return FALSE;  
        }  
        DEBUG("No more idle thread, created a new one.\n");  
        pThi->proc_fun = proc_fun;  
        pThi->th_job = job;  
      
        //send cond to work thread  
        pthread_cond_signal(&pThi->thread_cond);  
        return TRUE;  
    }  

    当一个新任务到达是,线程池首先会检查是否有可用的空闲线程,如果是,则采用才空闲线程进行任务处理并返回TRUE,如果不是,则尝试新建一个线程,并使用该线程对任务进行处理,如果失败则返回FALSE,说明线程池忙碌或者出错。
    [cpp] view plaincopy
    static void *tp_work_thread(void *arg) {  
        pthread_t curid;//current thread id  
        TpThreadInfo *pTinfo = (TpThreadInfo *) arg;  
      
        //wait cond for processing real job.  
        while (!(pTinfo->tp_pool->stop_flag)) {  
            pthread_mutex_lock(&pTinfo->thread_lock);  
            pthread_cond_wait(&pTinfo->thread_cond, &pTinfo->thread_lock);  
            pthread_mutex_unlock(&pTinfo->thread_lock);  
      
            //process  
            pTinfo->proc_fun(pTinfo->th_job);  
      
            //thread state be set idle after work  
            //pthread_mutex_lock(&pTinfo->thread_lock);  
            pTinfo->is_busy = FALSE;  
            enQueue(&pTinfo->tp_pool->idle_q, pTinfo);  
            //pthread_mutex_unlock(&pTinfo->thread_lock);  
            DEBUG("Job done, I am idle now.\n");  
        }  
    }  

    上面这个函数是任务处理函数,该函数将始终处理等待唤醒状态,直到新任务到达或者线程销毁时被唤醒,然后调用任务处理回调函数对任务进行处理;当任务处理完成时,则将自己置入空闲队列中,以供下一个任务处理。
    [cpp] view plaincopy
    TpThreadInfo *tp_add_thread(TpThreadPool *pTp) {  
        int err;  
        TpThreadInfo *new_thread;  
      
        if (pTp->max_th_num <= pTp->cur_th_num)  
            return NULL;  
      
        //malloc new thread info struct  
        new_thread = pTp->thread_info + pTp->cur_th_num;   
      
        new_thread->tp_pool = pTp;  
        //init new thread's cond & mutex  
        pthread_cond_init(&new_thread->thread_cond, NULL);  
        pthread_mutex_init(&new_thread->thread_lock, NULL);  
      
        //init status is busy, only new process job will call this function  
        new_thread->is_busy = TRUE;  
        err = pthread_create(&new_thread->thread_id, NULL, tp_work_thread, new_thread);  
        if (0 != err) {  
            free(new_thread);  
            return NULL;  
        }  
        //add current thread number in the pool.  
        pTp->cur_th_num++;  
      
        return new_thread;  
    }  

    上面这个函数用于向线程池中添加新的线程,该函数将会在当线程池没有空闲线程可用时被调用。
    函数将会新建一个线程,并设置自己的状态为busy(立即就要被用于执行任务)。

    线程池管理
    线程池的管理主要是监控线程池的整体忙碌状态,当线程池大部分线程处于空闲状态时,管理线程将适当的销毁一定数量的空闲线程,以便减少线程池对系统资源的消耗。

     

    这里设计认为,当空闲线程的数量超过线程池线程数量的1/2时,线程池总体处理空闲状态,可以适当销毁部分线程池的线程,以减少线程池对系统资源的开销。

     

    线程池状态计算

    这里的BUSY_THRESHOLD的值是0.5,也即是当空闲线程数量超过一半时,返回0,说明线程池整体状态为闲,否则返回1,说明为忙。

    [cpp] view plaincopy
    int tp_get_tp_status(TpThreadPool *pTp) {  
        float busy_num = 0.0;  
        int i;  
      
        //get busy thread number  
        busy_num = pTp->cur_th_num - pTp->idle_q.count;     
      
        DEBUG("Current thread pool status, current num: %u, busy num: %u, idle num: %u\n", pTp->cur_th_num, (unsigned)busy_num, pTp->idle_q.count);  
        //0.2? or other num?  
        if (busy_num / (pTp->cur_th_num) < BUSY_THRESHOLD)  
            return 0;//idle status  
        else  
            return 1;//busy or normal status      
    }  

    线程的销毁算法
    1.      从空闲队列中dequeue一个空闲线程指针,该指针指向线程信息数组的某项,例如这里是p;

    2.      销毁该线程

    3.      把线程信息数组的最后一项拷贝至位置p

    4.      线程池数量减少一,即cur_th_num--

    [cpp] view plaincopy
    TPBOOL tp_delete_thread(TpThreadPool *pTp) {  
        unsigned idx;  
        TpThreadInfo *pThi;  
        TpThreadInfo tT;  
      
        //current thread num can't < min thread num  
        if (pTp->cur_th_num <= pTp->min_th_num)  
            return FALSE;  
        //pthread_mutex_lock(&pTp->tp_lock);  
        pThi = deQueue(&pTp->idle_q);  
        //pthread_mutex_unlock(&pTp->tp_lock);  
        if(!pThi)  
          return FALSE;  
          
        //after deleting idle thread, current thread num -1  
        pTp->cur_th_num--;  
        memcpy(&tT, pThi, sizeof(TpThreadInfo));  
        memcpy(pThi, pTp->thread_info + pTp->cur_th_num, sizeof(TpThreadInfo));  
      
        //kill the idle thread and free info struct  
        kill((pid_t)tT.thread_id, SIGKILL);  
        pthread_mutex_destroy(&tT.thread_lock);  
        pthread_cond_destroy(&tT.thread_cond);  
      
        return TRUE;  
    }  

    线程池监控
    线程池通过一个管理线程来进行监控,管理线程将会每隔一段时间对线程池的状态进行计算,根据线程池的状态适当的销毁部分线程,减少对系统资源的消耗。

     

    [cpp] view plaincopy
    static void *tp_manage_thread(void *arg) {  
        TpThreadPool *pTp = (TpThreadPool*) arg;//main thread pool struct instance  
      
        //1?  
        sleep(MANAGE_INTERVAL);  
      
        do {  
            if (tp_get_tp_status(pTp) == 0) {  
                do {  
                    if (!tp_delete_thread(pTp))  
                        break;  
                } while (TRUE);  
            }//end for if  
      
            //1?  
            sleep(MANAGE_INTERVAL);  
        } while (!pTp->stop_flag);  
        return NULL;  
    }  

    程序测试

    至此,我们的设计需要使用一个测试程序来进行验证。于是,我们写下这样一段代码。

    [cpp] view plaincopy
    #include  
    #include  
    #include "thread_pool.h"  
      
    #define THD_NUM 10   
    void proc_fun(TpWorkDesc *job){  
        int i;  
        int idx=*(int *)job->arg;  
        printf("Begin: thread %d\n", idx);  
        sleep(3);  
        printf("End:   thread %d\n", idx);  
    }  
      
    int main(int argc, char **argv){  
        TpThreadPool *pTp= tp_create(5,10);  
        TpWorkDesc pWd[THD_NUM];  
        int i, *idx;  
      
        tp_init(pTp);  
        for(i=0; i < THD_NUM; i++){  
            idx=(int *) malloc(sizeof(int));  
            *idx=i;  
            pWd[i].arg=idx;  
            tp_process_job(pTp, proc_fun, pWd+i);  
            usleep(400000);  
        }  
        //sleep(1);  
        tp_close(pTp, TRUE);  
        free(pTp);  
        printf("All jobs done!\n");  
        return 0;  
    }  

    展开全文
  • 线程池

    2019-07-15 21:40:32
    1. 设计线程池遵循的规则 我们应该设计通用的线程池,那么该怎么设计呢,其实就是通过回调函数,将线程函数和参数都用void*来表示,这样用户可以定义自己的回调函数,而参数的话可以放在结构体里面,这样每个客户都...

    1. 设计线程池遵循的规则

    我们应该设计通用的线程池,那么该怎么设计呢,其实就是通过回调函数,将线程函数和参数都用void*来表示,这样用户可以定义自己的回调函数,而参数的话可以放在结构体里面,这样每个客户都可以使用该线程池来调用自己的函数,并且可以传递多个入参。

    2. 什么是线程池

    顾名思义,就是多个线程事先已经建立好了,放在一个池子里,当有需要的时候拿来用,不需要的时候还到池子里去。

    3. 线程池的作用

    第一,可以实现代码的重用,不必每次都去调用线程api来实现;
    第二,减少资源的损耗,避免了频繁的创建和销毁线程所带来的资源损耗;
    第三,方便管理,我们可以根据机器情况来决定启多少个线程,并且可以知道哪个线程正在使用,哪个没有被使用;

    4. 线程池的实现

    假定一个线程就是一个任务的话,线程池就是多个任务放入一个队列,需要的时候我们启动这个任务,不需要的时候,停止这个任务,并且会有一个标志来表示任务是启动还是停止状态。
    例如:
    空间和线程事先已经初始化好,需要执行任务时,修改执行标志,传入函数指针和参数指针,执行,执行完毕后又放入未使用队列;
    在此过程中需用到互斥锁和条件变量,每个子线程在执行完任务后,会阻塞在条件变量那里,一旦主线程有任务的话,就通过条件变量通知到子线程,子线程就会重新启动,开始执行;

    一个简单线程池结构设计如下:

    typedef struct ts_queue_item TSQItem;
    struct ts_queue_item{
     void *data;
     struct ts_queue_item *next;
    };
    
    typedef struct ts_queue TSQueue;
    
    //一个链表队列,记录链表头、尾、元素个数以及预先申请的空白链表
    struct ts_queue{
     TSQItem *head;
     TSQItem *tail;
     pthread_mutex_t lock;
        
     TSQItem *cqi_freelist; //预先申请的空白链表
     pthread_mutex_t cqi_freelist_lock;
        
     unsigned count;
    };
    

    5. 线程池设计步骤

    线程池设计:

    • 限制最多能起多少个线程
    • 初始化起指定数量的线程,让他们处于等待状态,当有任务来时将任务传递给线程进行处理
    • 当启动线程数量超过最大值时再有任务来要么拒绝要么放入队列等待
    • 使用单例模式,一个程序中只会有一个线程池,同时写一组接口管理线程池实例
    • 当有任务时,队列中空闲线程数量变少
    • 当任务完成时,释放相应的线程


     

    展开全文
  • 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,一个被代码耽误的文学创作者,不是大佬,但是喜欢分享,是一个又暖又有料的四川好男人。

    还有,欢迎关注我呀。

    展开全文
  • 先把这个面试题拿出来一下:1000 多个并发线程,10 台机器,每台机器 4 核,设计线程池大小。这题给的信息非常的简陋,但是简陋的好处就是想象空间足够大。第一眼看到这题的时候,我直观的感受到了两...

    一道面试题

    822026e858636cdf18bd2c8a71badd69.png

    兄弟们,怎么说?

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

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

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

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

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

    d846e2c528383e68004c8685bd3989a4.png

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

    1. 线程池设计。
    2. 负载均衡策略。

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

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

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

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

    线程池设计

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

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

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

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

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

    1. 自定义线程池的 7 个参数。
    2. JDK 线程池的执行流程。

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

    java.util.concurrent.ThreadPoolExecutor#ThreadPoolExecutor

    b3eaac3770ee38a335f6b0bf5989bbc7.png

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

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

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

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

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

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

    5929e002a2041b6ee196cd7e554a6d23.png

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

    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 线程池的执行流程。

    一图胜千言:

    78df1a82025a1762c64b4c2ea48a00b9.png

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

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

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

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

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

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

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

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

    64950b69cfa2c82649ad0bd26661b3af.png

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

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

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

    c91f6fe9958fbfffdfaf97bef8f08972.png

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

    前面我们说了,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 模式):

    1b9d338dd69bf38078aec9bccb252e90.png

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

    edee1b74f4647365f11af7455eff0ffa.png

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

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

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

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

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

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

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

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

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

    0986cdd93638a6697fe3598573cde07e.png

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

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

    7ffad5a129944dbced16471a0d084f56.png

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

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

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

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

    那么怎么办?

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

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

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

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

    3611e215f4fd09a7687a6a237698d4df.png

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

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

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

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

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

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

    负载均衡策略

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

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

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

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

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

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

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

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

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

    e214f0dfb26dd891e6f5a764bd7b7cd6.png

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

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

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

    不扯远了,说回来。

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

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

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

    8ab5cccf7b706a46623061792f03c5dc.png

    关于负载均衡策略,我的《吐血输出: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 次请求,处理流程如下图有所示:

    0c63aebac245738354bc6e5ba3213901.png

    一次应用操作数据库增删改查(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 ,此时会出现高并发的情况,高并发分为高并发读操作和高并发写操作。

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

    作者:why技术

    1000个并发线程,10台机器,每台机器4核,设计线程池大小 - why技术 - 博客园

    展开全文
  • 基于token的多平台身份认证架构设计3. select count(*)底层究竟做了什么?4. Springboot启动原理解析我们在工作中或多或少都使用过线程池,但是为什么要使用线程池呢?从他的名字中我们就应该知道,线程池使用了一...
  • 线程池的自我修养 最近重构行情服务端的框架,其中有一部分就是重写mysql线程池线程池是一个很独立的东西,今天就拿出来给大家分享, 怎样设计一个线程池, 以及我是怎么做的.为什么要使用线程池 常见的线程池使用...
  • 背景Java线程池的写法和参数是面试中出现频率很高的基础题。越是基础的东西,特别是对高阶职位的面试者,需要回答的符合自己面试的职位等级。这里也不能说是一个多么好的...回答线程池设计目标Java的线程主流实现都...
  • 线程池面试必考

    千次阅读 2021-02-23 10:24:17
    目录 为什么要用线程池? 你说下线程池核心参数? execute任务添加流程?...线程的重复使用是线程池设计的重点,如果需要开启1000个线程执行程序,系统会创建1000个线程,如果用线程池来执行1000个任务.
  • 随着计算机行业的飞速发展,摩尔定律逐渐失效,多核CPU成为主流。...本文开篇简述线程池概念和用途,接着结合线程池的源码,帮助读者领略线程池设计思路,最后回归实践,通过案例讲述使用线程池遇...
  • 线程池和内存池

    2018-11-25 15:40:24
    文章目录一、线程池1、线程池的概念2、线程池的组成部分3、线程池的流程4、线程池的Demo5、线程池的应用二、线程池的惊群效应1、惊群效应的概念2、惊群效应存在的问题3、线程池的惊群效应4、怎么判断发生了惊群5、...
  • 线程池的自我修养

    2019-04-06 10:39:32
      最近重构行情服务端的框架,其中有一部分就是重写mysql线程池线程池是一个很独立的东西,今天就拿出来给大家分享, 怎样设计一个线程池, 以及我是怎么做的. 为什么要使用线程池   常见的线程池使用场景分为两...
  • 线程池原理(面试)

    2019-10-16 16:19:03
    2.怎么设计一个线程池 (1) 新建一个数组,创建一堆线程存放进去; (2) 线程池中的线程来处理任务,处理完成后回收线程而不是销毁线程; (3) 设计等待队列来存放来不及处理的任务; (4) 拒绝策略 4.JDK中的...
  • 线程池面试的10连问

    2020-10-29 17:19:35
    文章目录1问题描述1 工作流程2 线程池有几种工作 队列2.1 怎么理解有界队列和无界队列3拒绝策略有何用途?4如何创建,停止线程池?为什么不建议使用executors构建线程池?4.1线程池终止4.2线程池的构建4.3为什么不...
  • 作为公司总负责人,我以后还要管理技术部门呢,怎么能不会技术呢 CEO,CTO,CFO于一身的CXO (技术部完了)。。。。。。。 菜菜 赶紧看看线上那个线程特别多的程序,给你2个小时优化一下 CEO,CTO,CFO于一身的CXO ...
  • 【Java】线程池原理

    2020-12-15 14:13:05
    3、线程池怎么处理线程共用参数? 3、从线程池设计中,我们能学到什么? 源码 1、ThreadPoolExecutor 核心实现类是ThreadPoolExecutor,先看这个类的第一个成员变量ctl,AtomicInteger这个类可以通过CAS达到无锁...
  • 面试官:如果让你设计线程池,你会怎么设计? 小贱:… 发生肾么事情了,面试官你不讲码德。 面试官:出门右拐,坐三轮车走成华大道到二仙桥。 对于线程池,有经验的程序员一定不会陌生,在Java中用Executor框架,啪...
  • 首先设计一张表,记录任务状态,执行时间,已执行,未执行等等 其次任务来的时候写入表中,任务标识未执行 如果此时有1000个任务都过来了,那么表中也有1000行记录 接着JVM内存溢出OOM,程序挂了,未执行的任务就知道是哪些...
  • 如何计算tomcat线程池大小?... 接下来,我将介绍本人是怎么设计以及计算的。 目标  确定tomcat服务器线程池大小 具体方法  众所周知,tomcat接受一个request后处理过程中,会涉及到cpu和I...
  • 菜菜呀,我最近研究技术呢,发现线上一个任务程序线程数有点多呀CEO,CTO,CFO于一身的CXOx总,你学编程呢?菜菜作为公司总负责人,我以后还要管理技术部门呢,怎么能不...
  • 接下来,我将介绍本人是怎么设计以及计算的。 具体方法 众所周知,tomcat接受一个request后处理过程中,会涉及到cpu和IO时间。其中IO等待时间,cpu被动放弃执行,其他线程就可以利用这段时间片进行操作。...
  • 是什么(使用线程池的原因,线程池的定义,好处,线程池原理) 怎么用(常见的使用方式,以及各个参数的作用) 为什么(源码分析,设计模式分析) 关于原理在android中的部分应用,部分注意事项 ...
  • java线程与线程池

    2019-09-17 18:02:07
    为了提升程序的处理能力的一种线程设计,叫多线程;第一为了迎合当代多核心cpu 的发展,多线程能提升cpu 的利用率,第二提升系统的吞吐量,处理速度,从而提升软件的用户体验, 第三更好的编程,已经制定了一套多...
  • 推荐阅读:阿里P8架构师谈:工作1-5年的Java工程师,怎样提高核心竞争力 阿里架构师直言:“没有实战都是纸上谈兵”!...之前在分析扩展线程池实现可回调的Future时候曾经提到并发大师Doug Lea在设计...
  • java线程池的实现原理(netty)

    千次阅读 2013-08-02 18:13:52
    博客已经好久都没有写了,感觉自己变慵懒了。。。这本来也是应该早就应该要写的。。。...当看完了它是怎么实现的之后觉得设计还是挺漂亮的。。。 要自己实现java的线程池,那么有两个接口是需要熟悉
  • 多线程的软件设计方法确实可以最大限度的发挥现代多核处理器的计算能力,提高生产系统的吞吐量和性能。 但是,如果一个系统同时创建大量线程,线程间频繁的切换上下文导致的系统开销将会拖慢整个系统。严重的甚至...

空空如也

空空如也

1 2 3 4 5 ... 12
收藏数 233
精华内容 93
关键字:

怎么设计线程池