只用一个线程还要使用线程池么?

518 阅读4分钟

1.previously

周五在给使用Jfinal的项目中加上了数据库的缓存,Jfinal现在进步了呀!虽然文档不多,好歹也是有了,看了看文档中关于cache的介绍,发现也不难,所以就打算自己实现一个简单键值对的缓存,不用那个现成的ehcache了。

2.currently

自制的缓存只有实现ICache接口即可,代码略过,现在键值对映射是这样的

ConcurrentHashMap<String, ConcurrentHashMap<Object, Object>>

外层映射中的key是cacheName,内层的具体的值名和值。 写完之后发现一个问题,如果没有定时清理缓存的功能,那么之后缓存无限膨胀的话,不是出大事了么?,不行还要有一个定时缓存的功能,同时也要有个时间戳来来作为清除功能的依据,所有缓存映射变成这样

ConcurrentHashMap<Tuple2<String, LocalDateTime>, ConcurrentHashMap<Object, Object>>

使用一个LocalDateTime对象作为创建的缓存时的记录,之后每隔一定时间就清理一次过期的缓存,那么清理的代码如下

       Runnable r = (() -> {

            while (true) {
                try {
                    Thread.sleep(Duration.ofDays(3).toMillis());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                log.info("run");

                this.cache.keySet().forEach(t -> {
                    if (LocalDateTime.now().minusDays(3).isAfter(t.f1())) {
                        removeAll(t.f0());
                    }
                });
            }
        });
        new Thread(r).start();
    }

这里使用java8中的stream类型来遍历映射中的过期的缓存,好既然处理逻辑写完了。那么就在这个类启动的时候启动一个线程后台执行这个功能,这个线程每次清理完sleep一段间隔,再执行下次的清理工作,这个功能貌似完成了,so easy!

且慢上面中的runnable有一下几个问题,

  • while(true)首先就会引发某种异常的问题,就是进程停止的时候,线程无法自行停止虽然,在上面的这个例子中,问题好像是不大,不过并不是一个想要变优秀的程序员做的事,另外这个while(true), 没有退出声明,这个代码有很大的问题呀,
  • 线程每次睡眠的时候都要显示的处理可能出现的异常,这个看起来实在无法跟美观挂钩,

终于引题了,这种方式是比较古老的办法,现在java8在出现好久了,找找看有没有更好的方式,这一找发现一个看起来很好用的类

Executors.newScheduledThreadPool

不对,记得阿里的规范里不能使用Executors,这个大佬写的非常好了。 再找,发现一个

ScheduledThreadPoolExecutor

嗯这个类看起来不错,看一下他的方法声明

/**
     * Creates a new {@code ScheduledThreadPoolExecutor} with the
     * given initial parameters.
     *
     * @param corePoolSize the number of threads to keep in the pool, even
     *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
     * @param threadFactory the factory to use when the executor
     *        creates a new thread
     * @throws IllegalArgumentException if {@code corePoolSize < 0}
     * @throws NullPointerException if {@code threadFactory} is null
     */
    public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue(), threadFactory);
    }

第一个是pool的size,第二个是创建线程的工厂方法,好说,看看创建完应该使用的方法

    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit) {
        if (command == null || unit == null)
            throw new NullPointerException();
        if (period <= 0)
            throw new IllegalArgumentException();
        ScheduledFutureTask<Void> sft =
            new ScheduledFutureTask<Void>(command,
                                          null,
                                          triggerTime(initialDelay, unit),
                                          unit.toNanos(period));
        RunnableScheduledFuture<Void> t = decorateTask(command, sft);
        sft.outerTask = t;
        delayedExecute(t);
        return t;
    }

我自己选的固定频率的,其实还有一个方法,是固定间隔的

    /**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     * @throws IllegalArgumentException   {@inheritDoc}
     */
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit) {
        if (command == null || unit == null)
            throw new NullPointerException();
        if (delay <= 0)
            throw new IllegalArgumentException();
        ScheduledFutureTask<Void> sft =
            new ScheduledFutureTask<Void>(command,
                                          null,
                                          triggerTime(initialDelay, unit),
                                          unit.toNanos(-delay));
        RunnableScheduledFuture<Void> t = decorateTask(command, sft);
        sft.outerTask = t;
        delayedExecute(t);
        return t;
    }

注意看方法中的第三个参数的名字,一个是period,一个是delay, 最后的使用ScheduledThreadPoolExecutor的代码如下

  final Runnable command = (() -> {
            log.info("Skr the clean thread to the custom cache at {}", LocalDateTime.now());
            this.cache.keySet().forEach(t -> {
                if (LocalDateTime.now().minusDays(this.CLEAN_QUEUE_PERIOD).isBefore(t.f1())) {
                    removeAll(t.f0());
                }

                // 这个方法不能使用parallel 执行,因为removeAll方法有锁,没什么意义
                // 每次删除的数量也不太大,避免卡着太长时间
                if (this.cache.size() > this.maxSizeForCacheQueue) {
                    this.cache.keySet().stream()
                            .sorted((x, y) -> y.f1().compareTo(x.f1()))
                            .limit(this.maxSizeForCacheQueue >> 3)
                            .forEach(e -> removeAll(e.f0()));
                }
            });
        });


        new ScheduledThreadPoolExecutor(1, r -> {
            final Thread t = new Thread(r);
            t.setDaemon(true);
            return t;
        }).scheduleAtFixedRate(command, 3, 5, TimeUnit.DAYS);

3.finally

对比最开始的代码

  • while已经被schedue代替了
  • 难看的try-catch也已经不见了
  • 这里ThreadFactory方法中的设置每个线程都是守护线程,这个也是一个优点