Go最新线程池的经典应用场景_线程池使用场景(2),含答案解析

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

    emailTaskPool.submit(() -> {
        try {
            System.out.printf("sending email .... emailDto is %s \n", emailDTO);
            Thread.sleep(1000);
            System.out.println("sended success");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

}


邮件的发送逻辑通过一个简单的线程睡眠来模拟发送过程中的耗时操作。


然后是线程池方面的配置:



@Configuration
public class ThreadPoolConfig {
@Bean
public ExecutorService emailTaskPool() {
return new ThreadPoolExecutor(2, 4,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue(), new SysThreadFactory(“email-task”));
}
}


**controller模块的触发**



@RestController
@RequestMapping(value = “/test”)
public class TestController {
@Resource
private SendEmailService sendEmailService;
@GetMapping(value = “/send-email”)
public boolean sendEmail() {
EmailDTO emailDTO = new EmailDTO();
emailDTO.setContent(“测试文案”);
emailDTO.setReceiver(“idea”);
emailDTO.setTitle(“邮件标题”);
sendEmailService.sendEmail(emailDTO);
return true;
}
}


这是一个非常简单的案例,通过一个http请求,然后触发一个邮件的发送操作。


### 心跳请求场景


这类应用场景一般会在一些基础组件中使用到,例如一些具有心跳探活机制类型功能的中间件,如nacos。下边来看看对应的代码实践:  
 首先是心跳模块代码:



public class HeartBeatInfo {
private String info;
private long nextSendTimeDelay;
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}

public long getNextSendTimeDelay() {
    return nextSendTimeDelay;
}
public void setNextSendTimeDelay(long nextSendTimeDelay) {
    this.nextSendTimeDelay = nextSendTimeDelay;
}
@Override
public String toString() {
    return "HeartBeatInfo{" +
            "info='" + info + '\'' +
            ", nextSendTimeDelay=" + nextSendTimeDelay +
            '}';
}

}


然后是模拟一个心跳包的发送服务接口定义:



public interface HeartBeatTaskService {
void sendBeatInfo();
}


接下来是心跳任务的发送核心部分实现:



@Service
public class HeartBeatTaskServiceImpl implements HeartBeatTaskService {
@Resource
private ScheduledThreadPoolExecutor scheduledThreadPoolExecutor;
@Override
public void sendBeatInfo() {
HeartBeatInfo heartBeatInfo = new HeartBeatInfo();
heartBeatInfo.setInfo(“test-info”);
heartBeatInfo.setNextSendTimeDelay(1000);
scheduledThreadPoolExecutor.schedule(new HeartBeatTask(heartBeatInfo),
heartBeatInfo.getNextSendTimeDelay(), TimeUnit.MILLISECONDS);
}
class HeartBeatTask implements Runnable {
private HeartBeatInfo heartBeatInfo;
public HeartBeatTask(HeartBeatInfo heartBeatInfo) {
this.heartBeatInfo = heartBeatInfo;
}
@Override
public void run() {
System.out.println(“发送心跳数据包:” + heartBeatInfo.getInfo());
HeartBeatInfo heartBeatInfo = new HeartBeatInfo();
heartBeatInfo.setInfo(“test-info”);
heartBeatInfo.setNextSendTimeDelay(1000);
scheduledThreadPoolExecutor.schedule(new HeartBeatTask(heartBeatInfo),
heartBeatInfo.getNextSendTimeDelay(), TimeUnit.MILLISECONDS);
}
}
}


在核心实现的内部有一个延时线程池ScheduledThreadPoolExecutor,ScheduledThreadPoolExecutor会在放入线程任务的一段指定的时间之后才触发任务的执行:



@Configuration
public class ThreadPoolConfig {

@Bean
public ScheduledThreadPoolExecutor  scheduledThreadPoolExecutor(){
    return new ScheduledThreadPoolExecutor(2, new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            thread.setDaemon(true);
            thread.setName("org.idea.threadpool.beat.sender");
            return thread;
        }
    });
}

}


### JDK内部线程池的设计


看了上边两个简单的案例之后,不知道你是否会有好奇:  
 到底线程池的内部运行机制会是怎样的呢?


#### 简单手写一个单消费者任务处理模型


这里我们可以通过一段简单的代码来学习这部分的内容:  
 首先,我们将需要处理的任务封装在一个对象内部,暂时定义如下所示:



public class AsyncHandlerData {

private String dataInfo;

public String getDataInfo() {
    return dataInfo;
}

public void setDataInfo(String dataInfo) {
    this.dataInfo = dataInfo;
}

@Override
public String toString() {
    return "AsyncHandlerData{" +
            "dataInfo='" + dataInfo + '\'' +
            '}';
}

}


然后会有一个专门消费这些个任务的service:



public interface AsyncHandlerService {
/**
* 任务放入队列中
*
* @param asyncHandlerData
*/
void putTask(AsyncHandlerData asyncHandlerData);
}


最后根据提前定义好的接口编写一个实现类,此时将相关的任务处理逻辑规整到了一个对象当中:



@Service
public class AsyncHandlerServiceImpl implements AsyncHandlerService, CommandLineRunner {

private volatile TaskQueueHandler taskQueueHandler = new TaskQueueHandler();

@Override
public void putTask(AsyncHandlerData asyncHandlerData) {
    taskQueueHandler.addTask(asyncHandlerData);
}

@Override
public void run(String... args) throws Exception {
    Thread thread = new Thread(taskQueueHandler);
    thread.setDaemon(true);
    thread.start();
}


public class TaskQueueHandler implements Runnable {

    private BlockingQueue<AsyncHandlerData> tasks = new ArrayBlockingQueue<>(1024 \* 1024);

    public void addTask(AsyncHandlerData asyncHandlerData) {
        tasks.offer(asyncHandlerData);
    }


    @Override
    public void run() {
        for (; ; ) {
            try {
                AsyncHandlerData asyncHandlerData = tasks.take();
                System.out.println("异步处理任务数据:" + asyncHandlerData.getDataInfo());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

}


整个代码的思路逻辑比较简单,大致可以归整成下图所示:  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/5206c38873e54052a9fc8596347e2017.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBARGFubnlfaWRlYQ==,size_18,color_FFFFFF,t_70,g_se,x_16)  
 整体的设计模式就是一端放入,由单个消费者取出。但是存在一个不足点,一旦消费者能力较弱,或者出现任务堵塞的话,就会导致任务队列出现堆积,然后越堆积越难处理地过来。


但是这样的设计还是一个过于简单的模型,下边我们来看看jdk内部线程池的设计模式:


### 线程池内部的源代码分析


我们在项目里使用线程池的时候,通常都会先创建一个具体实现Bean来定义线程池,例如:



@Bean
public ExecutorService emailTaskPool() {
return new ThreadPoolExecutor(2, 4,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue(), new SysThreadFactory(“email-task”));
}


ThreadPoolExecutor的父类是AbstractExecutorService,然后AbstractExecutorService的顶层接口是:ExecutorService。  
 就例如发送邮件接口而言,当线程池触发了submit函数的时候,实际上会调用到父类AbstractExecutorService对象的java.util.concurrent.AbstractExecutorService#submit(java.lang.Runnable)方法,然后进入到ThreadPoolExecutor#execute部分。



@Override
public void sendEmail(EmailDTO emailDTO) {
emailTaskPool.submit(() -> {
try {
System.out.printf(“sending email … emailDto is %s \n”, emailDTO);
Thread.sleep(1000);
System.out.println(“sended success”);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}


java.util.concurrent.AbstractExecutorService#submit(java.lang.Runnable) 源代码位置:



/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}


这里面你会看到返回的是一个future对象供调用方判断线程池内部的函数到底是否有完全执行成功。因此如果有时候如果需要判断线程池执行任务的结果话,可以这样操作:



Future future = emailTaskPool.submit(() -> {
try {
System.out.printf(“sending email … emailDto is %s \n”, emailDTO);
Thread.sleep(1000);
System.out.println(“sended success”);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//todo something
future.get();
}


在jdk8源代码中,提交任务的执行逻辑部分如下所示:  
 新增线程任务的时候代码:



public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn’t, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
//工作线程数小于核心线程的时候,可以填写worker线程
if (workerCountOf© < corePoolSize) {
//新增工作线程的时候会加锁
if (addWorker(command, true))
return;
c = ctl.get();
}
//如果线程池的状态正常,切任务放入就绪队列正常
if (isRunning© && 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);
}


通过深入阅读工作线程主要存放在了一个hashset集合当中,  
 添加工作线程部分的逻辑代码如下所示:



private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;😉 {
int c = ctl.get();
int rs = runStateOf©;
//确保当前线程池没有进入到一个销毁状态中
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;

    for (;;) {
        int wc = workerCountOf(c);
        if (wc >= CAPACITY ||
          // 如果传入的core属性是false,则这里需要比对maximumPoolSize参数
            wc >= (core ? corePoolSize : maximumPoolSize))
            return false;
            //通过cas操作去增加线程池的工作线程数亩
        if (compareAndIncrementWorkerCount(c))
            break retry;
        c = ctl.get();  // Re-read ctl
        if (runStateOf(c) != rs)
            continue retry;
        // else CAS failed due to workerCount change; retry inner loop
    }
}

boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
   //真正需要指定的任务是firstTask,它会被注入到worker对象当中
    w = new Worker(firstTask);
    final Thread t = w.thread;
    if (t != null) {
    //加入了锁
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            // Recheck while holding lock.
            // Back out on ThreadFactory failure or if
            // shut down before lock acquired.
            int rs = runStateOf(ctl.get());

            if (rs < SHUTDOWN ||
                (rs == SHUTDOWN && firstTask == null)) {
                if (t.isAlive()) // precheck that t is startable
                    throw new IllegalThreadStateException();
                //workers是一个hashset集合,会往里面新增工作线程 
                workers.add(w);
                int s = workers.size();
                if (s > largestPoolSize)
                    largestPoolSize = s;
                workerAdded = true;
            }
        } finally {
            mainLock.unlock();
        }
        if (workerAdded) {
            //worker本身是一个线程,但是worker对象内部还有一个线程的参数,
            //这个t才是真正的任务内容
            t.start();
            workerStarted = true;
        }
    }
} finally {
    //如果worker线程创建好了,但是内部的真正任务还没有启动,此时突然整个
    //线程池的状态被关闭了,那么这时候workerStarted就会为false,然后将
    //工作线程的数目做自减调整。
    if (! workerStarted)
        addWorkerFailed(w);
}
return workerStarted;

}


进过理解之后,整体执行的逻辑以及先后顺序如下图所示:


![在这里插入图片描述](https://img-blog.csdnimg.cn/a63f04f3bbc5495489c5f5a008104211.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBARGFubnlfaWRlYQ==,size_18,color_FFFFFF,t_70,g_se,x_16)


首先判断线程池内部的现场是否都有任务需要执行。如果不是,则使用一个空闲的工作线程用于任务执行。否则会判断当前的工作队列是否已经满了,如果没有满则往队列里面投递一个任务,等待线程去处理。如果工作队列已经满了,此时会根据饱和策略去判断,是否需要创建新的线程还是果断抛出异常等方式来进行处理。


#### 线程池常用参数介绍


**corePoolSize**  
 核心线程数,当往线程池内部提交任务的时候,线程池会创建一个线程来执行任务。即使此时有空闲的工作线程能够处理当前任务,只要总的工作线程数小于corePoolSize,也会创建新的工作线程。


**maximumPoolSize**  
 当任务的堵塞队列满了之后,如果还有新的任务提交到线程池内部,此时倘若工作线程数小于maximumPoolSize,则会创建新的工作线程。


**keepAliveTime**  
 上边我们说到了工作线程Worker(java.util.concurrent.ThreadPoolExecutor.Worker),当工作线程处于空闲状态中,如果超过了keepAliveTime依然没有任务,那么就会销毁当前工作线程。  
 如果工作线程需要一直处于执行任务,每个任务的连续间隔都比较短,那么这个keepAliveTime  
 属性可以适当地调整大一些。


**unit**  
 keepAliveTime对应的时间单位


**workQueue**  
 工作队列,当工作线程数达到了核心线程数,那么此时新来的线程就会被放入到工作队列中。  
 线程池内部的工作队列全部都是继承自阻塞队列的接口,对于常用的阻塞队列类型为:




![img](https://img-blog.csdnimg.cn/img_convert/2bfc2c23c4b6e74ba1c69648e7ef2832.png)
![img](https://img-blog.csdnimg.cn/img_convert/37c759aec8b1fcac7612c92b502c2322.png)
![img](https://img-blog.csdnimg.cn/img_convert/819a4a169c8b9fa4909de62a136cb911.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618658159)**

于执行任务,每个任务的连续间隔都比较短,那么这个keepAliveTime  
 属性可以适当地调整大一些。


**unit**  
 keepAliveTime对应的时间单位


**workQueue**  
 工作队列,当工作线程数达到了核心线程数,那么此时新来的线程就会被放入到工作队列中。  
 线程池内部的工作队列全部都是继承自阻塞队列的接口,对于常用的阻塞队列类型为:




[外链图片转存中...(img-TPE104Hf-1715899529149)]
[外链图片转存中...(img-uQuxUrtE-1715899529149)]
[外链图片转存中...(img-kTH2Lmes-1715899529150)]

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618658159)**

  • 20
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值