面试官:线程池提交一个任务占多大内存?

743145c42f2b33a5fdb252689461f346.jpeg来源:juejin.cn/post/7284158980113350691

👉 欢迎加入小哈的星球 ,你将获得: 专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡 /  每月赠书

新项目:仿小红书(微服务架构)正在更新中... 。全栈前后端分离博客项目 2.0 版本完结啦, 演示链接http://116.62.199.48/ 。全程手摸手,后端 + 前端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,直到项目上线。目前已更新了287小节,累计45w+字,讲解图:2008张,还在持续爆肝中.. 后续还会上新更多项目,目标是将Java领域典型的项目都整一波,如秒杀系统, 在线商城, IM即时通讯,Spring Cloud Alibaba 等等,戳我加入学习,已有1600+小伙伴加入(早鸟价超低)

f5818def5619621143160d3b110d1c3d.gif

  • execute

  • submit

  • lambda中没有使用上下文中的其他变量时

  • 总结


我们知道提交任务到线程池有两种方法,一种是execute,一种是submit

这两种提交方式占用的内存是一样大的吗?一个空任务究竟占多少内存?

通过源码分析一下

execute

public static void main(String[] args) {
    ThreadPoolExecutor threadPoolExecutor =
            new ThreadPoolExecutor(8, 8, 15, TimeUnit.MINUTES, new LinkedBlockingQueue<>(), new ThreadPoolExecutor.CallerRunsPolicy());
    for (int i = 0; i < (int) 2e5; i++) {
        int finalI = i;
        threadPoolExecutor.execute(() -> {
            // 乱写...
            int p = finalI;
            LockSupport.park();
        });
    }
    LockSupport.park();
}

这是一段典型的使用execute往线程池中提交任务的代码,这里是提交了20w个任务

execute的源码如下:

// ThreadPoolExecutor.execute
public void execute(Runnable command) {
    // 判空
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    // 当前线程数没达到核心线程数
    if (workerCountOf(c) < corePoolSize) {
        // 创建核心线程
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 尝试往阻塞队列中提交任务
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 二次检查,防止进入execute之后,线程池shutdown了
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 阻塞队列放不下,创建非核心线程
    else if (!addWorker(command, false))
        reject(command);
}

execute体现的就是线程池的工作原理,addWorker中有更复杂的逻辑来保证worker的原子性地插入,这个逻辑以后有机会可以聊聊

那么使用execute提交一个任务,这个任务究竟多大呢?

我们使用得最多的就是使用lambda表达式来提交任务

threadPoolExecutor.execute(() -> {
 // ...
});

那么这个lambda实例占用多少个字节呢?

16字节;在开了指针压缩的情况下,对象头占12个字节,4个字节用于填充补齐到8的整数倍,由于这个lambda实例中没有其他成员变量了,所以它就是占据16个字节

除此之外,如果使用的是LinkedBlockingQueue阻塞队列来存放任务,那么还涉及到LinkedBlockingQueue中的NodeLinkedBlockingQueue会使用这个Node来封装任务

static class Node<E> {
    E item;

    /**
     * One of:
     * - the real successor Node
     * - this Node, meaning the successor is head.next
     * - null, meaning there is no successor (this is the last node)
     */
    Node<E> next;

    Node(E x) { item = x; }
}

这个Node占多少字节呢?

24个字节;同样对象头占12字节,item是一个4字节的引用,next也是一个4字节的引用,一共20字节,4个字节用于填充对齐,所以一个node对象是24字节

所以在使用execute且阻塞队列是LinkedBlockingQueue时一个任务占用40个字节

如果execute 20w个任务,会占用800w个字节,约7.6MB内存

堆快照如下:

6c2c0e6fdd06627e165e94ed4b4893f7.jpeg

图片

如果是使用ArrayBlockingQueue的话,只有lambda实例这一个开销,所以只会使用320w个字节,约3.05MB内存,比起LinkedBlockingQueue少了一倍不止

8a1b9a39d5d9503d0a2abad48c8177cc.jpeg

图片

submit

接下来分析一下submit

// AbstractExecutorService.submit
public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}

submit中调用了newTaskFor方法来返回一个ftask对象,然后execute这个ftask对象,newTaskFor代码如下:

// AbstractExecutorService.newTaskFor
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
    return new FutureTask<T>(runnable, value);
}

newTaskFor又调用我们熟悉的FutureTask的有参构造器来创建一个futureTask实例,代码如下:

// FutureTask有参构造器
public FutureTask(Runnable runnable, V result) {
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       // ensure visibility of callable
}

这个有参构造器中又调用了Executors的静态方法callable创建一个callable实例来赋值给futureTask的callable属性,代码如下:

// Executors.callable
public static <T> Callable<T> callable(Runnable task, T result) {
    if (task == null)
        throw new NullPointerException();
    return new RunnableAdapter<T>(task, result);
}

最后还是使用了RunnableAdapter来包装这个task,代码如下:

// Executors.RunnableAdapter类
static final class RunnableAdapter<T> implements Callable<T> {
    final Runnable task;
    final T result;
    RunnableAdapter(Runnable task, T result) {
        this.task = task;
        this.result = result;
    }
    public T call() {
        task.run();
        return result;
    }
}

梳理一下整个流程,run和call的关系的伪代码如下

// submit
run(){
    // RunnableAdapter.call
 call(){
        // task.run
  run(){
   // 实际的任务
  }
 }
}

为什么要这么麻烦封装一层又一层呢?

可能是为了适配。submit的返回值是futureTask,但是我们传给submit的是个runnable,然后submit会把这个runnable继续传给futureTaskfutureTask的结果值是null,但是又由于futureTask的run方法已经被重写成执行call方法了,所以只能在call方法里面跑我们真正的run方法了

所以最后用了submit方法之后,会多出两类对象,一个是FutureTask,一个是RunnableAdapter

FutureTask的成员变量如下:

/** The underlying callable; nulled out after running */
private Callable<V> callable;
/** The result to return or exception to throw from get() */
private Object outcome; // non-volatile, protected by state reads/writes
/** The thread running the callable; CASed during run() */
private volatile Thread runner;
/** Treiber stack of waiting threads */
private volatile WaitNode waiters;

一个FutureTask对象占用的字节数是12+4+4+4+4=28个字节,还需要4个字节做填充,所以一共是32个字节

RunnableTask的成员变量如下:

final Runnable task;
final T result;

一个RunnableTask对象占用的字节数是12+4+4=20个字节,同样需要4个字节做填充,所以一共是24个字节

所以在使用submit且阻塞队列是LinkedBlockingQueue时一个任务占用96个字节

如果submit 20w个任务,会占用1920w个字节,约18.31MB内存

b5946a4ff6a2422e593bd2165a68aa27.jpeg

图片

如果使用的是ArrayBlockingQueue会省去Node的占用的内存

lambda中没有使用上下文中的其他变量时

public static void main(String[] args) {
    ThreadPoolExecutor threadPoolExecutor =
            new ThreadPoolExecutor(8, 8, 15, TimeUnit.MINUTES, new LinkedBlockingQueue<>(), new ThreadPoolExecutor.CallerRunsPolicy());
    for (int i = 0; i < (int) 2e5; i++) {
        // int finalI = i;
        threadPoolExecutor.submit(() -> {
            // 乱写...
            // int p = finalI;
            LockSupport.park();
        });
    }
    LockSupport.park();
}

如果在lambda中没有使用到上下文的其他变量时,是不会重复创建lambda实例的,只会创建一个

c9aa98dd495db4781bbab5bdc05f9069.jpeg

图片

只会创建一个lambda实例

60d5e1ccad6fbf550146ee7d6469d5c9.jpeg

图片

如果配合上ArrayBlockingQueue以及execute,提交20w个任务的空间复杂度可以降至O(1)

因为20w个任务的实例都是同一个

e36b2305f29b382ae4656033b0dd8ca1.jpeg

图片

总结

如果是lambda中没有上下文变量,使用的队列是ArrayBlockingQueue,提交方式是execute,那么空间复杂度可以达到O(1);如果lambda中有上下文变量,每次提交任务都会创建一个新的lambda实例;

如果使用的队列是LinkedBlockingQueue,那么还要算上LinkedBlockingQueue的Node实例的开销;如果提交的方式是submit,那么还要算上FutureTaskRunnableAdapter的开销

当然这里只是浅层地讨论了一下创建一个空任务所占用的内存大小,如果是更加复杂的任务,任务内的内存开销需要算上

如果错误,请斧正

👉 欢迎加入小哈的星球 ,你将获得: 专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡 /  每月赠书

新项目:仿小红书(微服务架构)正在更新中... 。全栈前后端分离博客项目 2.0 版本完结啦, 演示链接http://116.62.199.48/ 。全程手摸手,后端 + 前端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,直到项目上线。目前已更新了287小节,累计45w+字,讲解图:2008张,还在持续爆肝中.. 后续还会上新更多项目,目标是将Java领域典型的项目都整一波,如秒杀系统, 在线商城, IM即时通讯,Spring Cloud Alibaba 等等,戳我加入学习,已有1600+小伙伴加入(早鸟价超低)

f14b3e725207ee0e4fb07c8fb2d17dce.gif

93d4e1a5c827464cbcc3ccc98449b63b.jpeg

 
 

debed9b04d622c1d10fff267f3bf61d8.gif

 
 
 
 
1. 我的私密学习小圈子~
2. 离谱!CPU狂飙900%,这怎么处理?
3. 几种优雅实现在线人数统计的方案
4. Redis 官方可视化工具,功能真心强大!
 
 
最近面试BAT,整理一份面试资料《Java面试BATJ通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。
获取方式:点“在看”,关注公众号并回复 Java 领取,更多内容陆续奉上。
PS:因公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。
点“在看”支持小哈呀,谢谢啦
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值