Java并发编程05:线程池

线程池顶层接口

在这里插入图片描述

线程池顶级接口Executor

Executor接口为线程池的顶级接口,其executor()方法接收一个Runnable实现类对象,定义了在使用线程池时,如何调用线程中的业务逻辑.

class DirectExecutor implements Executor {
	public void execute(Runnable r) {
		r.run();
	}
}

class ThreadPerTaskExecutor implements Executor {
	public void execute(Runnable r) {
		new Thread(r).start();
	}
}

线程池接口ExecutorService

ExecutorService是下面所有线程池的父接口,继承自Executor.

  • submit()方法接收一个CallableRunnable对象,用于指定线程的行为.返回一个Future对象,用来取消任务或取得Callable对象call()方法的返回值.

  • shutdown()方法和shutdownNow()方法都可以关闭线程池,调用此方法后,线程池不能再调用submit()

    • shutdown()方法会等待当前线程池内所有任务全部完成再关闭线程池.
    • shutdownNow()方法会尝试终止池内正在运行的线程且放弃正在等待的任务,马上关闭线程池.
    • awaitTermination()方法可以使当前线程阻塞一段时间等待线程池被关闭完.返回一个boolean指示该线程池是否被关闭完.
    void shutdownAndAwaitTermination(ExecutorService pool) {
       pool.shutdown(); // Disable new tasks from being submitted   
        try {
        // Wait a while for existing tasks to terminate
        if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
           pool.shutdownNow(); // Cancel currently executing tasks
           // Wait a while for tasks to respond to being cancelled
           if (!pool.awaitTermination(60, TimeUnit.SECONDS))
           	System.err.println("Pool did not terminate");
       }
    } catch (InterruptedException ie) {
        // (Re-)Cancel if current thread also interrupted
    	pool.shutdownNow();
        // Preserve interrupt status
    	Thread.currentThread().interrupt();
    }
    

一个线程池的状态有以下三种:

  1. Running: 活动状态,线程池中的线程正在运行且可以通过调用submit()方法接收新任务.
  2. ShuttingDown: 线程池正在关闭过程中,线程池中有线程在执行任务,但不会接收新任务.
  3. Terminated: 线程池已经关闭,此案城池中没有线程在运行,且不接受新任务.

与线程池相关的类

Executors-线程池的工厂类和工具类

Executors为线程池的工厂类和工具类,我们使用其newXXXPool()方法创建各种封装好的线程池.

在这里插入图片描述

Callable- 用于定义任务及其返回值

Callable类用于创建一个任务,类似于Runnable接口,其call()方法定义任务具体执行的行为.与Runnalbe类的不同之处在于Callablecall()方法可以有返回值且支持泛型.

要注意CallableRunnable定义的都是任务而不是线程,要将其传入一个线程或线程池后才可以执行.

Future-用于获取任务返回值

每将一个任务(CallableRunnable对象)被剑入线程池后,会返回一个Future对象.其主要方法和属性如下:
在这里插入图片描述

  • cancel()方法可以取消该任务.
  • get()方法可以阻塞当前线程直到该任务执行完毕并获取其返回值.
  • done属性指示任务是否执行完毕,cancelled属性指示任务是否被取消.
public static void main(String[] args) throws ExecutionException, InterruptedException {

    // 将任务提交给线程池后会返回一个Future,其泛型代表任务方法返回值类型
    Future<String> result = Executors.newFixedThreadPool(5).submit(() -> {
        TimeUnit.MILLISECONDS.sleep(5000);
        return "returnValue";
    });
        
    System.out.println(result.isDone());	// result对应的线程没有执行完毕,返回false
    System.out.println(result.get()); 		// 阻塞主线程直到 result对应的线程执行完毕
    System.out.println(result.isDone()); 	//result对应的线程没有执行完毕,返回true
}

运行程序输出如下

false
# 线程会在这里阻塞五秒钟
returnValue
true

Java线程池的具体实现

在这里插入图片描述

ThreadPoolExecutor实现的线程池

ThreadPoolExecutor为最常见的线程池类,其构造函数如下:

public ThreadPoolExecutor(
    int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    RejectedExecutionHandler handler
);  

各参数代表的意义如下:

  • corePoolSize: 核心线程数量.
  • maximumPoolSize: 最大线程数量.
  • keepAliveTime: 线程的最大存活时间.一个线程超过keepAliveTime时间不工作则会被销毁.(除非当前池中线程个数小于corePoolSize)
  • unit: 枚举类型,表示keepAliveTime的单位.
  • workQueue: 存放任务的队列.
  • handler: 拒绝策略(添加任务失败后如何处理该任务)

线程池的运行策略如下:

  1. 线程池刚创建时,里面没有任何线程.
  2. 当调用execute()方法添加一个任务时,会根据当前线程池中运行线程个数做出不同行为
    • 当前线程池中运行线程数小于corePoolSize,则在线程池中添加一个新线程执行该任务,即使当前线程池中有空闲线程.
    • 当前线程池中运行线程数大于等于corePoolSize,则尝试将这个任务存入任务队列workQueue.
      1. 若任务队列满了且当前线程池中运行线程数小于等于maximumPoolSize,则在线程池中添加一个新线程执行该任务.
      2. 若任务队列满了且当前线程池中运行线程数大于maximumPoolSize,则任务将被拒绝并执行handler中的拒绝策略.
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行.
  4. 当一个线程超过keepAliveTime时间未执行任务时,线程池根据当前线程池中运行线程个数判断是否销毁该线程.若当前线程池中运行线程数大于corePoolSize,就会销毁该线程,直到线程池收缩到corePoolSize大小.

FixedThreadPool:固定容量的线程池

FixedThreadPool线程池内的最大线程个数是固定的,是一种最常见的线程池实现.通过Executors类的ExecutorService newFixedThreadPool(int nThreads)方法创建,其中nThreads参数指定最大线程数.

在JDK源码中可以查到其创建方法如下:

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

下面程序通过一个容量为4的FixedThreadPool并行寻找质数:

public static void main(String[] args) {
    ExecutorService pool = Executors.newFixedThreadPool(3);

    // 向线程池中添加四个任务,分别对四个区间进行查找
    Future<List<Integer>> future1 = pool.submit(new ComputeTask(1, 8_0000));
    Future<List<Integer>> future2 = pool.submit(new ComputeTask(8_0001, 13_0000));
    Future<List<Integer>> future3 = pool.submit(new ComputeTask(13_0001, 17_0000));
    Future<List<Integer>> future4 = pool.submit(new ComputeTask(17_0001, 20_0000));
 
 	// 主程序阻塞等待四个任务执行完成并获取结果
	List<Integer> primes = new LinkedList<>();
    primes.addAll(future1.get());
    primes.addAll(future2.get());
    primes.addAll(future3.get());
    primes.addAll(future4.get());
    
    // 关闭线程池
    pool.shutdown();
}

// 定义计算任务
static class ComputeTask implements Callable<List<Integer>> {

    private int start, end;

    ComputeTask(int start, int end) {this.start = start; this.end = end; }

    @Override
    public List<Integer> call()  {
        System.out.println(Thread.currentThread().getName() + "start");
        List<Integer> returnValue = getPrime(start, end);
        System.out.println(Thread.currentThread().getName() + "end");
        return returnValue;
    }
}

// 寻找[start, end]范围内的质数集合,细节省略
static List<Integer> getPrime(int start, int end) {
    // ...
}

我们向容量为3的线程池中加入4个任务,则同一时刻只有3个任务并行执行.程序输出如下,我们发现线程pool-1-thread-3执行了两个任务.

pool-1-thread-1start
pool-1-thread-2start
pool-1-thread-3start
pool-1-thread-3end
pool-1-thread-1end
pool-1-thread-2end
pool-1-thread-3start
pool-1-thread-3end

CachedThreadPool:容量自动调整的线程池

CachedThreadPool线程池中存活的线程数可以根据实际情况自动调整

  • 向线程池添加新任务时,优先使用线程池中存活的可用线程;若线程池当前没有可用线程,则向线程池中添加一个新线程
  • 若线程池中的线程超过60秒未使用,则回收该线程.

通过Executors类的ExecutorService newCachedThreadPool()方法创建.在JDK源码中可以查到其创建方法如下:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

SingleThreadExecutor:线程数为1的线程池

SingleThreadExecutor中的线程个数为1,可以用来保证任务同步执行.通过Executors类的ExecutorService newSingleThreadExecutor()方法创建.在JDK源码中可以查到其创建方法如下:

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

示例程序如下:

public static void main(String[] args) {
    ExecutorService service = Executors.newSingleThreadExecutor();
    for (int i = 0; i < 5; i++) {
        int j = i;
        service.execute(() -> {
            for (int k = 0; k < 3; k++) {
                System.out.println("task" + j + "." + k + " running in " + Thread.currentThread().getName());
            }
        });
    }
}

程序输出如下,我们发现所有任务都被同一个线程同步执行

task0.0 running in pool-1-thread-1
task0.1 running in pool-1-thread-1
task0.2 running in pool-1-thread-1
task1.0 running in pool-1-thread-1
task1.1 running in pool-1-thread-1
task1.2 running in pool-1-thread-1
task2.0 running in pool-1-thread-1
task2.1 running in pool-1-thread-1
task2.2 running in pool-1-thread-1
task3.0 running in pool-1-thread-1
task3.1 running in pool-1-thread-1
task3.2 running in pool-1-thread-1

ScheduledThreadPoolExecutor实现的线程池

ScheduledThreadPool:定时执行任务的线程池

ScheduledThreadPool线程池可以定时执行任务.通过Executors类的ExecutorService newSingleThreadExecutor(int corePoolSize)方法创建,其corePoolSize参数指定线程池的核心线程数.在JDK源码中可以查到其创建方法如下:

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

其主要方法有schedule(),scheduleAtFixedRate(),scheduleWithFixedDelay(),可以设定任务的执行计划.可以根据 当前任务的到期情况 自动调整线程池中的线程数.示例程序如下:

public static void main(String[] args) {
    ScheduledExecutorService service = Executors.newScheduledThreadPool(4);
    // scheduleAtFixedRate()使用固定的频率执行某个任务,四个参数:
    // 	command指定执行的任务
    // 	initialDelay指定延时多久第一次执行该任务
    // 	period-指定执行任务的间隔周期
    // 	unit-时间单位
    service.scheduleAtFixedRate(() -> {
        try {
            TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
    }, 0, 500, TimeUnit.MILLISECONDS);  
    // 我们每隔500毫秒执行一次该任务,然而任务需要1000毫秒才能执行完成
    // 因此最终核心线程会满并且会不断向线程池添加新线程
}

ForkJoinPool实现的线程池

ForkJoinPool:执行递归任务的线程池

ForkJoinPool线程池的submit()方法接收ForkJoinTask类的任务,该类支持fork(),join()成员方法:

  • fork()方法会将该子任务加入线程池异步执行.
  • join()方法会阻塞当前线程直到子任务执行完成并获取其返回值

在这里插入图片描述
ForkJoinTask有两个子类: RecursiveActionRecursiveTask,其中RecursiveAction任务没有返回值,而RecursiveTask任务有返回值.它们的任务执行逻辑均写在其compute()方法中.

在这里插入图片描述

ForkJoinPool是一个较底层的线程池,因而Executor中没有与其对应的构造方法,需要我们显示调用其构造函数获得该类型的线程池.

public class T {

    final static int[] numbers = new int[1000000];
    final static int MAX_SIZE = 50000;
    final static Random r = new Random();

    static {
        for (int i = 0; i < numbers.length; i++) {numbers[i] = r.nextInt(1000); }
    }

    // 累加计算[begin, end]区间和任务
    static class AddTask extends RecursiveTask<Long> {

        int begin, end;    // 计算区间

        public AddTask(int begin, int end) {this.begin = begin; this.end = end; }

        // 累加计算[begin, end]区间和
        protected Long compute() {
            if ((end - begin) < MAX_SIZE) {
                long sum = 0L;
                for (int i = begin; i < end; i++) {sum += numbers[i]; }
                return sum;
            } else {
                int middle = begin + (end - begin) / 2;
                // 创建两个子任务并加入线程池
                AddTask task1 = new AddTask(begin, middle);
                AddTask task2 = new AddTask(middle, end);
                task1.fork();
                task2.fork();
                // 阻塞当前线程直到两个子任务执行完毕
                return task1.join() + task2.join();
            }
        }
    }

    public static void main(String[] args) throws Exception {        
        ForkJoinPool pool = new ForkJoinPool();
        Future<Long> future = pool.submit(new AddTask(0, numbers.length));
        System.out.println(future.get());
    }
}

WorkStealingPool:工作窃取线程池

WorkStealingPool线程池通过Executors类的ExecutorService newWorkStealingPool()方法创建,其核心线程数为机器的核心数.

在JDK源码中可以查到其创建方法如下:

public static ExecutorService newWorkStealingPool() {
    return new ForkJoinPool(Runtime.getRuntime().availableProcessors(),
                            ForkJoinPool.defaultForkJoinWorkerThreadFactory,
                            null, true);
}

WorkStealingPool线程池采用工作窃取模式,相比于一般的线程池实现,工作窃取模式的优势体现在对递归任务的处理方式上.

  • 在一般的线程池中,若一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态.
  • 而在工作窃取模式中,若某个子问题由于等待另外一个子问题的完成而无法继续运行,则处理该子问题的线程会主动寻找其他尚未运行的子问题(窃取过来)来执行.这种方式减少了线程的等待时间,提高了性能.
  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值