关于java线程池的问题

1.为什么要使用线程池?

线程的创建需要OS和JVM大量操作配合完成,java高并发的情况下频繁创建和销毁线程是非常低效的,引用线程池可以降低线程的创建成本。

并且线程池可以提升性能,在执行大量异步任务时,最大限度的对已经创建的线程进行复用,提高性能;并且线程池能够对线程进行统计管理。

 

2.线程池有哪些核心参数?/线程池的执行原理是什么?

  • corePoolSize:核心线程数
  • maximumPoolSize:最大线程数 = 核心线程数 + 救急线程的最大数目
  • keepAliveTime:生存时间(救急线程的生存时间,生存时间内没有新任务则会释放该线程)
  • unit:救急线程生存的时间单位
  • workQueue:阻塞队列当没有空闲核心线程时,新的任务会投入此队列,队列满了之后会把创建救急线程执行任务
  • threadFactory:线程工厂,可以定制线程对象的创建
  • handler:拒绝策略,所有线程都繁忙并且workQueue也满了的时候会触发拒绝策略

拒绝策略有以下四种:

  1. 直接抛出异常,这是默认策略AbortPolicy
  2. 用调用者所在的线程执行任务(一般就是用主线程来执行任务)CallRunsPolicy
  3. 抛弃当前任务DiscardPolicy
  4. 抛弃阻塞队列最靠前的任务DiscardOldestPolicy

线程池执行原理

 

 

3.线程池中有哪些常见的阻塞队列

linkedBlockQueue:

基于链表实现的阻塞队列,可以不初始化大小(默认大小是Integer.MAX_VALUE,因此是推荐要初始化大小的),有头尾指针对应的两把锁,因此效率上讲是比arrayBlockQueue高的。更推荐使用这个阻塞队列。

ArrayBlockQueue:

基于数组实现的阻塞队列,必须初始化大小,有一把锁锁住整个数组保证线程安全

 

4.如何确认线程池的核心线程数

分为两种情况(N即CPU的核数,可在任务管理器查看,也可通过java代码查看)

  • 高并发,且任务执行时间短。这种直接N+1
  • 并发不高,但是任务执行时间较长
    • IO密集型,线程数为2N+1(这个才是java程序常见的)例如DB读写,文件读写,网络请求
    • CPU密集型,线程数为N+1(这个好像在大模型计算里比较常见)例如计算型代码,Bitmap转换,Gson转换
       

5.线程池的种类有哪些

固定线程数线程池newFixedThreadPool,这个创建没有空闲线程,阻塞队列最大容量为Integer.MAX_VALUE,用的是LinkedBlockingQueue

单线程化线程池newSingleThreadPool,这个创建只有一个线程,阻塞队列最大容量也是Integer.MAX_VALUE,用的是LinkedBlockingQueue

可缓存线程池newCachedThreadPool,没有核心线程数,最大线程数为Integer.MAX_VALUE,临时线程存活60s,用的是SynchronousQueue,适用于任务数密集,执行时间短的情况

延迟+周期执行的线程池ScheduledThreadPoolExecutor

 

6.为什么不建议使用executor创建线程池

executor返回的线程池有上述种类的前三种,队列/线程大量堆积都可能会产生OOM。推荐使用ThreadPoolExecutor

 

7.线程池的使用场景

①CountDownLatch(闭锁/倒计时锁)用来进行线程同步协议,需等待所有线程完成后才能执行接下来的操作:通过构造函数确定等待计数值(和线程数挂钩);通过await()方法等待计数值到0;利用countDown()方法让计数减一。该方法保证当前线程能在指定个数线程完成后再进行,确保顺序。

使用CountDownLatch防止ES导入的OOM问题:

  • 一次性大量导入ES的索引可能会发生崩溃,因此使用了多线程
  • 多线程无限制的开辟可能会导致内存资源迅速耗尽,因此引入了线程池控制线程数量来分批次导入;
  • 在线程池批量导入后无法确保主线程在所有子线程完成任务后才继续执行,过早的释放资源或者进行下一步操作,因此引入了CountDownLatch,确保资源的正确释放与执行顺序。

首先在MySQL中分页查询到每页信息,按页数设置线程数。自定义合适的线程池,每个线程都会执行ES的add方法,再最后统一将数据batch到ES中,该线程执行完成后执行countDown,所有线程都载入完数据后才可以进行后续操作(释放资源、执行其他指令)

②使用future实现微服务模块的多数据源读取(数据汇总)问题

通过将普通的同步串行方法改为线程池+future来分线程读取数据,future.get()支持在线程执行后返回具体内容,这样优化了返回时间;但是与同步串行方法相比需要使用三个线程

③异步调用

一些一致性要求不高的操作除了用mq以外也可以用线程池来执行异步调用减少用户等待时间

需要在对应方法上添加@Async("指定的线程池的beanname")注解,在启动类上添加@EnableAsync

 

8.如何控制某个方法并发访问线程的数量

使用Semaphore(信号量),是JUC包下的一个类,底层是AQS,可以用来限制数量。

在构造时需要确认容量大小,然后每次线程准备使用时semaphore.acquire(),在线程结束时释放semaphore.release()

 

9.谈谈你对threadlocal的理解

threadlocal是多线程解决线程安全的一个操作类,为每个线程都分配了一个独立的线程副本解决了并发访问量冲突的问题。实现了线程之间的资源共享

通过set,get,remove三个方法对资源进行保存

源码分析:

set方法:根据当前线程对象,获取对应的ThreadLocalMap,判断map是否存在,不存在需要创建

复制代码
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }
复制代码

this是当前线程,firstValue就是上述的value。创建一个新的threadlocalmap

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

容量和hashmap一样默认是16,把他赋给了数组,根据threadlocal的hash值为当前threadlocal在table里找了一个下标位置,把数据挂载入了数组。此时K即threadlocal,v是传入的value

    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }    

 

get方法/remove方法

复制代码
public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
复制代码

整体来说类似于hashmap的查找:根据hash定位数组的位置,返回他的KV

        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.refersTo(key))
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

在threadlocalmap中key是弱引用,会被GC释放内存;而值是强引用,不会被释放,因此在使用时需要主动remove释放key,value

posted @   天启A  阅读(10)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
历史上的今天:
2023-10-21 读书笔记06《智能与数据重构世界》读后感
2023-10-21 软考上午03程序语言
点击右上角即可分享
微信分享提示