java多线程[9]:线程池(ExecutorService)

java多线程已经写了好几篇了,博客中使用所有demo,线程都是通过Thread类的start()方法启动的,这样的话每次都会创建一个新的线程,创建线程时会耗费一些操作系统的资源,当线程比较多时对性能有较大的影响。为了解决这个问题,java提供了好几种类型的线程池来提高运行时效率。

ExecutorService接口

绝大部分(如果不是全部的话)的线程池都实现了ExecutorService接口,ExecutorService又实现了Executor接口,总之,ExecutorService接口包含以下常用的方法

void execute(Runnable command);

上面这个方法会启动一个线程。第一,它有可能会创建一个新的线程来执行,也有可能复用线程池中已有的线程来执行。第二,该方法返回时并不保证该线程马上回被执行,有可能会排队。

Future<?> submit(Runnable task);
<T> Future<T> submit(Runnable task, T result);
<T> Future<T> submit(Callable<T> task);

上面这三个方法和上面的execute()方法类似,只是上面这三个方法返回一个Future对象,它的get()方法会挂起调用线程,直到Runable线程或Callable线程运行结束。下一篇博客会重点讨论FutureCallable

上面第一个方法,如果返回的Future对象的get()方法为null,则表示线程执行成功。
上面第二个方法,如果返回的Future对象的get()方法得到的是result对象,则表示线程执行成功。
上面第三个方法,当线程成功执行后,通过返回的Future对象的get()方法可以获取Callable线程的运行结果。

void shutdown();

该方法关闭线程池。如果没有调用它的话,进程将不会结束。

除了ExecutorService接口之外,java还提供了ScheduledExecutorService接口,该接口继承自ExecutorService,并加入了几个版本的schedule方法,例如

public ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);

它可以延迟启动一个线程,其他方面和ExecutorServicesubmit方法一样。

多种类型的线程池

java提供了好几种类型的线程池,通过java.util.concurrent.Executors类的静态方法来创建。例如:

CachedThreadPool

Executors.newCachedThreadPool()

上面的方法创建了一个具有缓存功能的线程池。当它需要启动一个线程时,如果当前线程池内有可用的线程,则会复用,否则,会创建新的线程。闲置时间超过1分钟的线程将会被自动销毁并移除当前线程池。

FixedThreadPool

Executors.newFixedThreadPool(int nThreads);

上面的方法创建了一个具有固定数量(nThreads个)线程的线程池,在任何时间,最多有nThreads个线程可以同时运行。当新的任务被提交到线程池时,如果有闲置的线程,就立刻复用该线程来运行该任务,如果没有闲置的线程,则该任务会在队列中等候闲置的线程来运行自己。顾名思义,FixedThreadPool拥有固定数量线程,不会动态创建新线程,也不会销毁闲置的线程。

SingleThreadExecutor

Executors.newSingleThreadExecutor()

上面的方法创建了一个只包括一个工作者线程的线程池,类似Executors.newFixedThreadPool(1)

ScheduledExecutor

Executors.newScheduledThreadPool(int corePoolSize)

上面的方法具有corePoolSize个常驻线程的线程池,它可以用来延迟启动进程,或周期性地运行线程。

效率对比

为了说明线程池的性能优势,下面来一个例子对比一下。有一个工作者线程,计算从1到10000000的和,并且一次启动100000个工作者线程。分别使用三种方式启动线程
- Thread.start()
- CachedThreadPool
- FixedThreadPool

先来看工作者线程

import java.util.concurrent.CountDownLatch;

public class MyWorker implements Runnable {
    String name;
    CountDownLatch countDownLatch;

    public MyWorker(String name, CountDownLatch countDownLatch) {
        this.name = name;
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        long sum = 0;
        for (int i = 1; i <= 10000000; i++) {
            sum += i;
        }
        countDownLatch.countDown();
    }
}

下面以直接启动线程的方式来一次启动100000个工作者线程

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadStartDemo {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newCachedThreadPool();
        final int threadCount = 100000;
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        long from = System.currentTimeMillis();
        for (int i = 0; i < threadCount; i++) {
            String threadName = "t" + i;
            Runnable runnable = new MyWorker(threadName, countDownLatch);
            new Thread(runnable).start();
        }
        countDownLatch.await();
        long to = System.currentTimeMillis();
        long period = to - from;
        System.out.println("use " + period + " milliseconds");
        service.shutdown();
    }
}

运行上面的代码运行完平均需要8000毫秒。

下面使用CachedThreadPool来启动线程

mport java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CachedThreadPoolDemo {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newCachedThreadPool();
        final int threadCount = 100000;
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        long from = System.currentTimeMillis();
        for (int i = 0; i < threadCount; i++) {
            String threadName = "t" + i;
            service.execute(new MyWorker(threadName, countDownLatch));
        }
        countDownLatch.await();
        long to = System.currentTimeMillis();
        long period = to - from;
        System.out.println("use " + period + " milliseconds");
        service.shutdown();
    }
}

上面的代码运行完大概只需要800毫秒,正好差一个数量级。下面再来看一下通过FixedThreadPool启动线程的例子

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class FixedThreadPoolDemo {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(4);
        final int threadCount = 100000;
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        long from = System.currentTimeMillis();
        for (int i = 0; i < threadCount; i++) {
            String threadName = "t" + i;
            service.execute(new MyWorker(threadName, countDownLatch));
        }
        countDownLatch.await();
        long to = System.currentTimeMillis();
        long period = to - from;
        System.out.println("use " + period + " milliseconds");
        service.shutdown();
    }
}

我通过Executors.newFixedThreadPool(4)创建了一个包含4个线程的线程池,用这种方式运行的话,平均只需要170毫秒。

由此可见:
1. 线程池的引入,大大减少了直接创建线程带来的性能开销
2. 求和属于CPU密集型操作,这种情况并不是创建的线程越多越好,因为线程太多的话(超过了CPU的核心数),线程间会频繁的切换,也会消耗一定的性能。Executors.newFixedThreadPool(4)的情况要明显好于Executors.newCachedThreadPool()的情况。我运行上面代码的电脑是四核的处理器,所以创建一个包含四个线程的线程池,性能是最佳的。
3. 如果工作者线程包含大量的IO操作的话,例如读写磁盘文件、读写数据库、请求网络接口等,newCachedThreadPool的效率有可能会优于newFixedThreadPool(4),因为做IO操作时,CPU会闲置。这种情况下,更多的线程更有利于压榨CPU。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值