基于Nacos实现动态线程池

542 阅读5分钟

image.png

实现原理

属性值更新

nacos配置值变更后可以自动修改上下文中使用到的该配置值,在 spring-cloud-starter-alibaba-nacos-config中主要借助 RefreshEventListenerContextRefresher 实现。

image.png

上面的属性刷新调用时序图,我们可以比较好介入的点就是 RefreshListenerContextRefresher中的处理流程,一种解耦的方式就是利用spring的事件机制,在刷新属性方法执行结束后发布一个属性更新完成事件,并把本次更新的key嵌在事件中一起发布,方便外部感知。而这恰好在 ContextRefreshe.refreshEnvironment已经有相关逻辑实现了,源码如下:

public synchronized Set<String> refreshEnvironment() {
		Map<String, Object> before = extract(
				this.context.getEnvironment().getPropertySources());
		addConfigFilesToEnvironment();
		Set<String> keys = changes(before,
				extract(this.context.getEnvironment().getPropertySources())).keySet();
    	//发布了环境变更事件
		this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
		return keys;
}

但是很重要得一点是,销毁bean然后重建bean以及赋值属性是在 RefreshScope.refreshAll()中进行的,所以监听这个EnvironmentChangeEvent事件行不通,那监听 RefreshScope中发布的RefreshScopeRefreshedEvent 呢?还是不太现实,因为他发布事件时没有发布被更新的key我们无法利用这些被更新的key做文章。那要实现发布属性,而且能访问被更新后的值只能在RefreshListenerContextRefresher上做文章。

为什么要选择ContextRefresher呢?

@Configuration
@ConditionalOnClass(RefreshScope.class)
@ConditionalOnProperty(name = RefreshAutoConfiguration.REFRESH_SCOPE_ENABLED, matchIfMissing = true)
@AutoConfigureBefore(HibernateJpaAutoConfiguration.class)
public class RefreshAutoConfiguration {

	......
	
    @Bean
	@ConditionalOnMissingBean
	public ContextRefresher contextRefresher(ConfigurableApplicationContext context,
			RefreshScope scope) {
		return new ContextRefresher(context, scope);
	}

	@Bean
	public RefreshEventListener refreshEventListener(ContextRefresher contextRefresher) {
		return new RefreshEventListener(contextRefresher);
	}

    ......
}

RefreshAutoConfiguration这个配置类中做了一些Bean托管定义,ContextRefresher上有@ConditionalOnMissingBean修饰,告诉我们可以自己自定义ContextRefresher来代替框架中定义好的(当然没有标注@ConditionalOnMissingBean我们也可以强制替换,即定义相同Bean 加上@Primary即可 )。实现如下:

public class ContextRefresherDecorator extends ContextRefresher {

    private ApplicationContext context;

    public ContextRefresherDecorator(ConfigurableApplicationContext context,
                                     RefreshScope scope) {
        super(context, scope);
        this.context = context;
    }

    @Override
    public synchronized Set<String> refresh() {
        Set<String> refresh = super.refresh();
        context.publishEvent(new RefreshCompleteEvent(refresh));
        return refresh;
    }
}

public class RefreshCompleteEvent extends ApplicationEvent {

    /**
     * 被刷新的key集合
     */
    private final Set<String> refreshedKeySet;

    /**
     * Create a new {@code ApplicationEvent}.
     *
     * @param source the object on which the event initially occurred or with
     *               which the event is associated (never {@code null})
     */
    public RefreshCompleteEvent(Object source) {
        super(source);
        this.refreshedKeySet = (Set<String>) source;
    }

    public Set<String> getRefreshedKeySet() {
        return refreshedKeySet;
    }
}

线程池参数更新

涉及到线程,任何临界区操作必须考虑的问题是数据竞争和线程安全。线程池的配置参数是典型的共享变量,线程池中的线程都会在各自的执行流程上使用到,下面是ThreadPoolExecutor关键参数在代码中的定义:

public class ThreadPoolExecutor extends AbstractExecutorService {

    private volatile ThreadFactory threadFactory;

    /**
     * Handler called when saturated or shutdown in execute.
     */
    private volatile RejectedExecutionHandler handler;

    /**
     * Timeout in nanoseconds for idle threads waiting for work.
     * Threads use this timeout when there are more than corePoolSize
     * present or if allowCoreThreadTimeOut. Otherwise they wait
     * forever for new work.
     */
    private volatile long keepAliveTime;

    /**
     * Core pool size is the minimum number of workers to keep alive
     * (and not allow to time out etc) unless allowCoreThreadTimeOut
     * is set, in which case the minimum is zero.
     */
    private volatile int corePoolSize;

    /**
     * Maximum pool size. Note that the actual maximum is internally
     * bounded by CAPACITY.
     */
    private volatile int maximumPoolSize;
}

上面是线程池核心参数的定义,我们发现核心参数都是由volatile修饰,保证线程间的修改的可见性。而我们使用的场景也是

nacos配置修改->一个线程轮训到变化值修改线程池核心参数->线程池中的线程感知到线程池核心参数变化

这是典型的一写多度场景,所以对于corePoolSize maximumPoolSize RejectedExecutionHandler keepAliveTime我们直接调用线程池的属性set方法修改值,就能实现加锁修改的效果。

工作队列调整

工作队列的修改就是调整队列大大小,扩缩容问题。以LinkedBlockingQueue为例讲解,它定义的容量capacity是使用final修饰的,直接是无法修改的,当然可以使用反射去修改,但是缺乏可见性保证,存在竞态条件不可取。

Java中提供了并发量控制工具Semaphore,它完美适配容量控制。

public class ResizableSemaphore extends Semaphore {
    public ResizableSemaphore(int permits, boolean fair) {
        super(permits, fair);
    }

    public ResizableSemaphore(int permits) {
        super(permits);
    }

    @Override
    protected void reducePermits(int reduction) {
        super.reducePermits(reduction);
    }

    public void increasePermits(int delta) {
        //Aqs中这里会 将 当前值+delta 设置未新的permit
        this.release(delta);
    }
}

新的可resize的队列实现

public class ResizeableLinkedBlockingQueue<E> extends LinkedBlockingQueue<E> {
    private int queueSize;
    private final ResizableSemaphore availablePlaces;

    public ResizeableLinkedBlockingQueue() {
        super();
        this.queueSize = Integer.MAX_VALUE;
        availablePlaces = new ResizableSemaphore(queueSize, true);
    }

    public ResizeableLinkedBlockingQueue(Collection<? extends E> collection) {
        super(collection);
        this.queueSize = Integer.MAX_VALUE;
        availablePlaces = new ResizableSemaphore(queueSize, true);
    }

    public ResizeableLinkedBlockingQueue(int initialCapacity) {
        super();
        this.queueSize = initialCapacity;
        availablePlaces = new ResizableSemaphore(initialCapacity, true);
    }

    @Override
    public int size() {
        return queueSize;
    }

    /**
     * 调整队列大小
     *
     * @param newSize
     * @throws IllegalArgumentException
     */
    public synchronized void resizeQueue(int newSize) throws IllegalArgumentException {
        if (newSize < 0) {
            throw new IllegalArgumentException("Cannot set queue size to a value below zero.");
        }

        int difference;

        if (newSize < queueSize) {
            difference = queueSize - newSize;
            availablePlaces.reducePermits(difference);
        } else if (newSize > queueSize) {
            difference = newSize - queueSize;
            availablePlaces.increasePermits(difference);
        }
        queueSize = newSize;
        //Nothing to do if newSize == queueSize.
    }

    @Override
    public boolean offer(E e) {
        if (Objects.isNull(e)) {
            throw new IllegalArgumentException("Element to offer cannot be null.");
        }

        boolean returnValue;

        if (availablePlaces.tryAcquire()) {
            if (super.offer(e)) {
                returnValue = true;
            } else {
                returnValue = false;
                //If the queue.offer(e) fails, the availablePlaces.tryAcquire() will still have occurred, so we'll have
                //to release the acquired permit.
                availablePlaces.release();
            }
        } else {
            returnValue = false;
        }

        return returnValue;
    }

    @Override
    public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException {
        if (Objects.isNull(e)) {
            throw new IllegalArgumentException("Element to offer cannot be null.");
        }

        boolean returnValue;

        if (availablePlaces.tryAcquire(timeout, unit)) {
            if (super.offer(e)) {
                returnValue = true;
            } else {
                returnValue = false;
                //If the queue.offer(e) fails, the availablePlaces.tryAcquire(timeout, unit) will still have occurred,
                //so we'll have to release the acquired permit.
                availablePlaces.release();
            }
        } else {
            returnValue = false;
        }

        return returnValue;
    }

    @Override
    public E poll() {
        E returnValue = super.poll();
        if (returnValue != null) {
            availablePlaces.release();
        }
        return returnValue;
    }

    @Override
    public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        E resultValue = super.poll(timeout, unit);

        if (resultValue != null) {
            availablePlaces.release();
        }
        return resultValue;
    }

    @Override
    public void put(E e) throws InterruptedException {
        availablePlaces.acquire();
        super.put(e);
    }

    @Override
    public int remainingCapacity() {
        return availablePlaces.availablePermits();
    }

    @Override
    public boolean remove(Object o) {
        if (super.remove(o)) {
            availablePlaces.release();
            return true;
        }

        return false;
    }

    @Override
    public E take() throws InterruptedException {
        E result = super.take();
        availablePlaces.release();
        return result;
    }

}

代码实现

代码结构

关键实现

可调整参数的线程池

@Slf4j
public class ResizeableThreadPoolExecutor extends ThreadPoolExecutor implements Refreshable {
    private IPoolProperty iPoolProperty;
    private ResizeableLinkedBlockingQueue<Runnable> queue;

    public ResizeableThreadPoolExecutor(IPoolProperty properties, ResizeableLinkedBlockingQueue<Runnable> queue) {
        super(properties.getCorePoolSize(), properties.getMaxPoolSize(),
                properties.getKeepLiveTime(),
                TimeUnit.SECONDS,
                queue);
        this.queue = queue;
        this.iPoolProperty = properties;
        log.info("created {}", pintExecutorInfo());
    }

    @Override
    public void refresh() {
        if (iPoolProperty.getCorePoolSize() > iPoolProperty.getMaxPoolSize()) {
            log.error("illegal config for thread pool:{}", iPoolProperty);
        }
        log.info("before update:{}", pintExecutorInfo());
        this.setCorePoolSize(iPoolProperty.getCorePoolSize());
        this.setMaximumPoolSize(iPoolProperty.getMaxPoolSize());
        this.setKeepAliveTime(iPoolProperty.getKeepLiveTime(), TimeUnit.SECONDS);
        queue.resizeQueue(iPoolProperty.getQueueSize());
        log.info("after update:{}", pintExecutorInfo());
    }

    /**
     * 打印线程池的信息
     *
     * @return 线程池描述信息字符串
     */
    private String pintExecutorInfo() {
        FormattingTuple formattingTuple = MessageFormatter.arrayFormat("Thread executor:core_size={}," +
                "thread_cur_size={},max_size={}," +
                "queue_cur_size={},total_task_count={}", new Object[]{this.getCorePoolSize(),
                this.getActiveCount(),
                this.getMaximumPoolSize(),
                this.getQueue().size(),
                this.getTaskCount()});
        return formattingTuple.getMessage();
    }
}

线程池和配置绑定注册类

@Slf4j
public class DynamicThreadPoolRegistryImpl implements DynamicThreadPoolRegistry {
    private ConcurrentHashMap<String, Refreshable> containerStorage = new ConcurrentHashMap<>(8);
    private final IPoolProperty property;

    public DynamicThreadPoolRegistryImpl(IPoolProperty property) {
        this.property = property;
    }

    /**
     * 注册容器
     *
     * @param propertyClass 属性类 属性类必须被 {@link org.springframework.boot.context.properties.ConfigurationProperties} 修饰
     * @param refreshable   可更新容器
     */
    @Override
    public void register(Class<? extends IPoolProperty> propertyClass, Refreshable refreshable) {
        ConfigurationProperties annotation = AnnotationUtils.getAnnotation(propertyClass, ConfigurationProperties.class);
        if (Objects.isNull(annotation)) {
            throw new IllegalArgumentException("propertyClass must be annotated by @ConfigurationProperties");
        }
        if (Objects.isNull(refreshable)) {
            throw new IllegalArgumentException("refreshable container must not be null");
        }
        String prefix = annotation.prefix();
        containerStorage.put(prefix, refreshable);
    }


    @Override
    public void trigger(RefreshCompleteEvent event) {
        Set<String> refreshedKeySet = event.getRefreshedKeySet();
        log.info("property value of context:{} have been updated", refreshedKeySet);
        Set<String> keyPrefixSet = refreshedKeySet.stream().map(key -> {
            String[] keySegmentArray = StringUtils.split(key, ".");
            return String.join(".", Arrays.copyOf(keySegmentArray, keySegmentArray.length - 1));
        }).filter(StringUtils::isNotBlank).collect(Collectors.toSet());
        keyPrefixSet.stream().map(containerStorage::get).filter(Objects::nonNull).forEach(Refreshable::refresh);
    }

配置类

@Configuration
public class DynamicThreadPoolConfigure {
    private static final String THREAD_CONFIG_FILE_NAME = "dynamic-thread-pool";


    @Bean
    public DynamicThreadPoolRegistry dynamicThreadPoolRegistry(@Qualifier("RESIZEABLE_EXECUTOR") Executor resizableExecutor, IPoolProperty property) {
        DynamicThreadPoolRegistry dynamicThreadPoolRegistry = new DynamicThreadPoolRegistryImpl(property);
        Refreshable refreshable = (Refreshable) resizableExecutor;
        dynamicThreadPoolRegistry.register(ApiExecuteThreadPoolProperty.class, refreshable);
        return dynamicThreadPoolRegistry;
    }

    @Bean("RESIZEABLE_EXECUTOR")
    public Executor resizableExecutor(IPoolProperty properties) {
        ResizeableLinkedBlockingQueue<Runnable> queue = new ResizeableLinkedBlockingQueue<>(properties.getQueueSize());
        ResizeableThreadPoolExecutor executor = new ResizeableThreadPoolExecutor(properties, queue);
        return executor;
    }

    @Bean
    public ContextRefresher contextRefresherDecorator(ConfigurableApplicationContext context, RefreshScope scope) {
        return new ContextRefresherDecorator(context, scope);
    }

备注

代码地址:gitee.com/jitaop9_adm…

资料来源:如何实现可调整的线程池