线程池+CountDownLatch优化代码,提高程序执行效率

在前几篇博客中,我使用Redis来缓存热点数据,提高首页的访问速度 (参考SSM整合Redis

发现,相比于每次从数据库中查询数据,程序执行速度提高了很多。

今天,我决定对这个需求继续优化。采用多线程的方式来实现。

先来看一下优化之前的代码:

long startTime = System.currentTimeMillis();
model.addAttribute("types", typeService.findHottestType(6));
model.addAttribute("tags", tagService.findHottestTag(6));
model.addAttribute("blogs", blogService.findHotestBlogs());
model.addAttribute("newBlogs", blogService.findNewestBlogs(8));
System.out.println("操作耗时\t" + (System.currentTimeMillis() - startTime) + " ms");
return INDEX;//返回首页

依次查询,我们假设每行代码需要执行10秒,那么一共就是40秒。这可太慢了。

我们看看执行结果:

从redis中获取type
从redis获取tag
从redis中获取Blog数据
从redis中获取Blog数据
操作耗时    424 ms

从redis中获取type
从redis获取tag
从redis中获取Blog数据
从redis中获取Blog数据
操作耗时    407 ms

从redis中获取type
从redis获取tag
从redis中获取Blog数据
从redis中获取Blog数据
操作耗时    470 ms

我执行了三次,发现每次耗时几乎都在400ms以上

于是我首先决定手动new 出四个线程,来分别执行他们,并使用CountDownLatch来实现并发访问控制,用于主线程等待其他子线程执行完毕后进行相关操作,这里用于等待四个查询的子线程查询完毕,返回首页。

关于CountDownLatch的内容,可以参考Java并发之CountDownLatch、CyclicBarrier、Semaphore使用实例我的另一篇博客

代码如下:

        long startTime = System.currentTimeMillis();
        CountDownLatch countDownLatch = new CountDownLatch(4);
        new Thread(() -> {
            model.addAttribute("types", typeService.findHottestType(6));
            System.out.println(Thread.currentThread().getName() + "线程执行完毕");
            countDownLatch.countDown();
        }, "Type线程").start();
        new Thread(() -> {
            model.addAttribute("tags", tagService.findHottestTag(6));
            System.out.println(Thread.currentThread().getName() + "线程执行完毕");
            countDownLatch.countDown();
        }, "Tag线程").start();
        new Thread(() -> {
            model.addAttribute("blogs", blogService.findHotestBlogs());
            System.out.println(Thread.currentThread().getName() + "线程执行完毕");
            countDownLatch.countDown();
        }, "Hot线程").start();
        new Thread(() -> {
            model.addAttribute("newBlogs", blogService.findNewestBlogs(8));
            System.out.println(Thread.currentThread().getName() + "线程执行完毕");
            countDownLatch.countDown();
        }, "New线程").start();

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("操作耗时\t" + (System.currentTimeMillis() - startTime) + " ms");
        return INDEX;

我们继续执行3次,执行结果:

从redis获取tag
Tag线程线程执行完毕
从redis中获取type
Type线程线程执行完毕
从redis中获取Blog数据
New线程线程执行完毕
从redis中获取Blog数据
Hot线程线程执行完毕
操作耗时    194 ms

从redis获取tag
Tag线程线程执行完毕
从redis中获取type
Type线程线程执行完毕
从redis中获取Blog数据
New线程线程执行完毕
从redis中获取Blog数据
Hot线程线程执行完毕
操作耗时    193 ms

从redis获取tag
Tag线程线程执行完毕
从redis中获取type
Type线程线程执行完毕
从redis中获取Blog数据
从redis中获取Blog数据
New线程线程执行完毕
Hot线程线程执行完毕
操作耗时    183 ms

效果很明显,一下子就从400ms+,优化到200ms一下。

程序执行速度是优化了,但是代码看起来貌似挺繁琐的,需要自己创建,线程。

我想到了线程池,Java线程池主要用于管理线程组及其运行状态,以便Java虚拟机更好的利用CPU资源。

线程池的主要作用的线程复用,线程资源管理,控制操作系统的最大并发数量,以保证高校且安全的运行。

我们常用的线程池有以下三种,关于线程池方面的只是可以访问我的另一篇博客

一文看懂Java中创建线程的所有方式(继承Thread,实现Runnable,实现Callable,线程池)

1、Executors.newSingleThreadExecutor()

创建一个单线程话的线程池,他只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行

这里不适合我们的序需求

2、Executors.newFixedThreadPool(4)

执行长期的任务,性能好很多

创建一个定长线程池,可控制线程最大并发数,超出的线程回在队列中等待。

代码如下:

        long startTime = System.currentTimeMillis();
        CountDownLatch countDownLatch = new CountDownLatch(4);


        ExecutorService threadPool = Executors.newFixedThreadPool(4);
        threadPool.execute(() -> {
            model.addAttribute("tags", tagService.findHottestTag(6));
            System.out.println(Thread.currentThread().getName() + "线程执行完毕");
            countDownLatch.countDown();
        });
        threadPool.execute(() -> {
            model.addAttribute("blogs", blogService.findHotestBlogs());
            System.out.println(Thread.currentThread().getName() + "线程执行完毕");
            countDownLatch.countDown();
        });
        threadPool.execute(() -> {
            model.addAttribute("newBlogs", blogService.findNewestBlogs(8));
            System.out.println(Thread.currentThread().getName() + "线程执行完毕");
            countDownLatch.countDown();
        });


        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("操作耗时\t" + (System.currentTimeMillis() - startTime) + " ms");
        return INDEX;
我们访问两次首页,执行效果:

从redis获取tag
pool-8-thread-2线程执行完毕
从redis中获取type
pool-8-thread-1线程执行完毕
从redis中获取Blog数据
pool-8-thread-4线程执行完毕
从redis中获取Blog数据
pool-8-thread-3线程执行完毕
操作耗时    182 ms


从redis获取tag
pool-9-thread-2线程执行完毕
从redis中获取type
pool-9-thread-1线程执行完毕
从redis中获取Blog数据
pool-9-thread-4线程执行完毕
从redis中获取Blog数据
pool-9-thread-3线程执行完毕
操作耗时    189 ms

 

嗯,看起来跟手动创建线程差不多,但还是有一个问题,当我疯狂刷新首页的时候,出现了以下情况

操作耗时    832 ms

操作耗时    697 ms

操作耗时    937 ms

为什么呢,其实和newFixedThreadPool(4)有关,这是一个固定数量的线程池,核心线程数和最大线程数都是4,

如果请求大于4,则将新提交的任务放在阻塞队列中等待,注意,是等待!直到有可用的线程资源。

我们看看源码:

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

//阻塞队列
 public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

    /**
     * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity.
     *
     * @param capacity the capacity of this queue
     * @throws IllegalArgumentException if {@code capacity} is not greater
     *         than zero
     */
public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }

从源码中可以看出,阻塞队列的最大容量是Integer.MAX_VALUE,可能会创建大量线程,导致OOM 。

3、Executors.CachedThreadPool()

同上,阻塞队列的最大容量是Integer.MAX_VALUE,可能会创建大量线程,导致OOM 。不用!

 

那么我们如何做呢?自定义线程池!

      ExecutorService threadPool =
                new ThreadPoolExecutor(
                        10,
                        100,
                        1,
                        TimeUnit.SECONDS,
                        new LinkedBlockingDeque<Runnable>(4),
                        Executors.defaultThreadFactory(),
                        new ThreadPoolExecutor.AbortPolicy());

核心线程数设置为:10

最大线程容量:100

空闲线程存活时间:1s

阻塞队列长度:4

这里我测试使用,所以参数设置的比较随意,(我的博客网站访问人数我还是有自知之明的哈哈 )

合理配置线程池参数请参考:合理配置线程池参数

如何合理的配置线程池的参数?

  1. CPU密集型

    CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行

    CPU密集任务只有在真正多核CPU上才可能得到加速(通过多线程)

    而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些

    CPU密集型任务配置尽可能少的线程数量:

    ==一般公式:CPU核数+1个线程的线程池==

  2. IO密集型

    • 由于IO密集型任务线程并不是一直在执行任务,则应配置经可能多的线程,如CPU核数 * 2

    • IO密集型,即该任务需要大量的IO,即大量的阻塞。

      在单线程上运行IO密集型的任务会导致浪费大量的 CPU运算能力浪费在等待。

      所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。

      IO密集型时,大部分线程都阻塞,故需要多配置线程数:

      参考公式:==CPU核数/(1-阻塞系数) 阻塞系数在0.8~0.9之间==

      八核CPU:8/(1-0,9)=80

 

 

 

 

 

 

 

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值