Java学习笔记 线程池使用及详解

有点笨,参考了好几篇大佬们写的文章才整理出来的笔记....

字面意思上解释,线程池就是装有线程的池,我们可以把要执行的多线程交给线程池来处理,和连接池的概念一样,通过维护一定数量的线程池来达到多个线程的复用。

好处

多线程产生的问题

一般我们使用到多线程的编程的时候,需要通过new Thread(xxRunnable).start()创建并开启线程,我们可以使用多线程来达到最优效率(如多线程下载)。

但是,线程不是越多就越好,线程过多,创建和销毁就会消耗系统的资源,也不方便管理。

除此之外,多线程还会造成并发问题,线程并发数量过多,抢占系统资源从而导致阻塞。

线程池优点

我们将线程放入线程池,由线程池对线程进行管理,可以对线程池中缓冲的线程进行复用,这样,就不会经常去创建和销毁线程了,从而省下了系统的资源。

线程池能够有效的控制线程并发的数量,能够解决多线程造成的并发问题。

除此之外,线程池还能够对线程进行一定的管理,如延时执行、定时循环执行的策略等

线程池实现

线程池的实现,主要是通过这个类ThreadPoolExecutor,其的构造参数非常长,我们先大概了解,之后再进行详细的介绍。

public ThreadPoolExecutor(int corePoolSize,
	int maximumPoolSize,long keepAliveTime,
	TimeUnit unit,BlockingQueue workQueue,
	RejectedExecutionHandler handler)
  • corePoolSize:线程池核心线程数量
  • maximumPoolSize:线程池最大线程数量
  • keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间
  • unit:存活时间的单位
  • workQueue:存放线程的工作队列
  • handler:超出线程范围和队列容量的任务的处理程序(拒绝策略)

这里大概简单说明一下线程池的运行流程:

当线程被添加到线程池中,如果线程池中的当前的线程数量等于线程池定义的最大核心线程数量(corePoolSize)了,此线程就会别放入线程的工作队列(workQueue)中,等待线程池的调用。

Java提供了一个工具类Excutors,方便我们快速创建线程池,其底层也是调用了ThreadPoolExecutor

不过阿里巴巴Java规范中强制要求我们应该通过ThreadPoolExecutor来创建自己的线程池,使用Excutors容易造成OOM问题。

所以,我们先从Excutors开始学习,之后在对ThreadPoolExecutor进行详细的讲解

Excutors

由于Excutors是工具类,所以下面的介绍的都是其的静态方法,如果是比较线程数目比较少的小项目,可以使用此工具类来创建线程池

PS:把线程提交给线程池中,有两种方法,一种是submit,另外一种则是execute

两者的区别:

  1. execute没有返回值,如果不需要知道线程的结果就使用execute方法,性能会好很多。
  2. submit返回一个Future对象,如果想知道线程结果就使用submit提交,而且它能在主线程中通过Future的get方法捕获线程中的异常

线程池可以接收两种的参数,一个为Runnable对象,另外则是Callable对象

Callable是JDK1.5时加入的接口,作为Runnable的一种补充,允许有返回值,允许抛出异常。

主要的几个静态方法:

方法 说明
newFixedThreadPool(int nThreads) 创建固定大小的线程池
newSingleThreadExecutor() 创建只有一个线程的线程池
newCachedThreadPool() 创建一个不限线程数上限的线程池,任何提交的任务都将立即执行
newScheduledThreadPool(int nThreads) 创建一个支持定时、周期性或延时任务的限定线程数目的线程池
newSingleThreadScheduledExecutor() 创建一个支持定时、周期性或延时任务的单个线程的线程池

1.newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行,我们可以使用它来达到控制线程顺序执行。

控制进程顺序执行:

Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("这是线程1");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
Thread thread2 = new Thread(new Runnable() {
	@Override
	public void run() {
		try {
			System.out.println("这是线程2");
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
});
Thread thread3 = new Thread(new Runnable() {
	@Override
	public void run() {
		try {
			System.out.println("这是线程3");
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
});
//创建线程池对象
ExecutorService executorService = Executors.newSingleThreadExecutor();
//把线程添加到线程池中
executorService.submit(thread1);
executorService.submit(thread2);
executorService.submit(thread3);

之后出现的结果就是按照顺序输出

2.newFixedThreadPool

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()

3.newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程,线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

代码:

//创建了一个自定义的线程
public class MyThread extends Thread {
    private int index;

    public MyThread(int index) {
        this.index = index;
    }

    @Override
    public void run() {
        System.out.println(index+" 当前线程"+Thread.currentThread().getName());
    }
}

//创建缓存线程池
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
	executorService.execute(new MyThread(i));
	try {
		//这里模拟等待时间,等待线程池复用回收线程
		Thread.sleep(1000);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
}

可以看到结果都是使用的同一个线程

4.newScheduledThreadPool

创建一个定长线程池,支持定时、周期性或延时任务执行

延迟1s后启动线程:

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
scheduledExecutorService.schedule(new MyThread(1),1, TimeUnit.SECONDS);

ThreadPoolExecutor

构造方法

上面提到的那个构造方法其实只是ThreadPoolExecutor类中的一个,ThreadPoolExecutor类中存在有四种不同的构造方法,主要区别就是参数不同。

//五个参数的构造函数
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue)

//六个参数的构造函数-1
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory)

//六个参数的构造函数-2
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler)

//七个参数的构造函数
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

首先,有个概念需要明白,线程池的最大线程数(线程总数,maximumPoolSize)= 核心线程数(corePoolSize)+非核心线程数

  • corePoolSize:线程池核心线程数量
  • maximumPoolSize:线程池最大线程数量
  • keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间
  • unit:存活时间的单位
  • workQueue:存放线程的工作队列
  • handler:超出线程范围和队列容量的任务的处理程序(拒绝策略)

核心线程和非核心线程有什么区别呢?

核心线程是永远不会被线程池丢弃回收(即使核心线程没有工作),非核心线程则是超过一定时间(keepAliverTime)则就会被丢弃

workQueue

当所有的核心线程都在工作时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务

1.SynchronousQueue:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,如果所有线程都在工作怎么办?那就新建一个线程来处理这个任务!所以为了保证不出现线程数达到了maximumPoolSize而不能新建线程的错误,使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大

2.LinkedBlockingQueue:这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize

3.ArrayBlockingQueue:可以限定队列的长度,接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误

4.DelayQueue:队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务

拒绝策略:

拒绝策略 拒绝行为
AbortPolicy 抛出RejectedExecutionException异常(默认)
DiscardPolicy 不处理,丢弃掉
DiscardOldestPolicy 丢弃执行队列中等待最久的一个任务,尝试为新来的任务腾出位置
CallerRunsPolicy 直接由提交任务者执行这个任务

两种方法设置拒绝策略:

//ThreadPoolExecutor对象的setRejectedExecutionHandler方法设置
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 5, 60, TimeUnit.SECONDS, queue);
threadPool.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
//构造方法进行设置,省略

线程池默认的拒绝行为是AbortPolicy,也就是抛出RejectedExecutionHandler异常,该异常是非受检异常,很容易忘记捕获。

如果不关心任务被拒绝的事件,可以将拒绝策略设置成DiscardPolicy,这样多余的任务会悄悄的被忽略。

ThreadFactory

一个接口类,用来对线程进行设置,需要实现newThread(Runnable r)方法

官方的文档说明:

newThread此方法一般来初始化线程的优先级(priority),名字(name),守护进程(daemon)或线程组(ThreadGroup)

简单的例子(让某个类实现ThreadFactory接口):

@Override
public Thread newThread(Runnable r) {
	Thread thread = new Thread(r);
	thread.setDaemon(true);
	return thread;
}

线程池获取执行结果

PS:把线程提交给线程池中,有两种方法,一种是submit,另外一种则是execute

两者的区别:

  1. execute没有返回值,如果不需要知道线程的结果就使用execute方法,性能会好很多。
  2. submit返回一个Future对象,如果想知道线程结果就使用submit提交,而且它能在主线程中通过Future的get方法捕获线程中的异常

线程池可以接收两种的参数,一个为Runnable对象,另外则是Callable对象

Callable是JDK1.5时加入的接口,作为Runnable的一种补充,允许有返回值,允许抛出异常。

线程池的处理结果、以及处理过程中的异常都被包装到Future中,并在调用Future.get()方法时获取,执行过程中的异常会被包装成ExecutionException,submit()方法本身不会传递结果和任务执行过程中的异常。

获取执行结果的代码可以这样写:

ExecutorService executorService = Executors.newFixedThreadPool(4);
Future<Object> future = executorService.submit(new Callable<Object>() {
        @Override
        public Object call() throws Exception {
			//该异常会在调用Future.get()时传递给调用者
            throw new RuntimeException("exception in call~");
        }
    });
    
try {
	//获得返回结果
	Object result = future.get();
	
	
} catch (InterruptedException e) {
  // interrupt
} catch (ExecutionException e) {
  // exception in Callable.call()
  e.printStackTrace();
}

线程池运行流程

一个形象的比喻说明线程池的流程:

规定:

  1. 线程池比作成一家公司
  2. 公司的最大员工数为maximumPoolSize
  3. 最大正式员工数为coolPoolSize(核心线程的总数)
  4. 最大员工数(maximumPoolSize) = 最大正式员工(coolPoolSize)和临时工(非核心线程)
  5. 单子(任务)可看做为线程
  6. 队列使用的是ArrayBlockingQueue
  7. 一个员工只能干一个任务

最开始的时候,公司是没有一名员工。之后,公司接到了单子(任务),这个时候,公司才去找员工(创建核心线程并让线程开始执行),这个时候找到的员工就是正式员工了。

公司的声誉越来越好,于是来了更多的单子,公司继续招人,直到正式员工数量达到最大的正式员工的数量(核心线程数量已达到最大)

于是,多出来的单子就暂时地存放在了队列中,都在排队,等待正式员工们把手头的工作做完之后,就从队列中依次取出单子继续工作。

某天,来了一个新单子,但是这个时候队列已经满了,公司为了自己的信誉和声誉着想,不得已只能去找临时工(创建非核心线程)来帮忙开始进行工作(负责新单子)

在此之后,又来了新单子,公司继续去招临时工为新来的单子工作,直到正式工和临时工的数量已经达到了公司最大员工数。

这个时候,公司没有办法了,只能拒绝新来的单子了(拒绝策略)

此时,正式工和临时工都是在加班加点去从队列中取出任务来工作,终于某一天,队列的已经没有单子了,市场发展不好,单子越来越少,临时工很久都不工作了(非核心线程超过了最大存活时间keepAliveTime),公司就把这些临时工解雇了,直到剩下只有正式员工。

PS:如果也想要解雇正式员工(销毁核心线程),可以设置ThreadPoolExecutor对象的的allowCoreThreadTimeOut这个属性为true

个人理解,可能不是很正确,仅供参考!

线程池关闭

方法 说明
shutdown() 不再接受新的任务,之前提交的任务等执行结束再关闭线程池
shutdownNow() 不再接受新的任务,试图停止池中的任务再关闭线程池,返回所有未处理的线程list列表。

总结

如果是小的Java程序,可以使用Excutors,如果是服务器程序,则使用ThreadPoolExecutor进行自定义线程池的创建

参考链接:
java中常用线程池的:newCachedThreadPool
Java线程池详解
Java 线程池的认识和使用
Java 线程池全面解析
线程池,这一篇或许就够了
Java线程池的运行原理以及使用详解

posted @ 2019-11-27 22:54  Stars-one  阅读(1272)  评论(0编辑  收藏  举报