线程池

什么是线程池?

​ 线程池就是创建若干个可执行的线程放入一个池(容器)中,有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,而是仍然在线程池中等待下一个任务。

为什么要使用线程池?

​ 因为 Java 中创建一个线程,需要调用操作系统内核的 API,操作系统要为线程分配一系列的资源,成本很高,所以线程是一个重量级的对象,应该避免频繁创建和销毁。使用线程池就能很好地避免频繁创建和销毁。

​ java线程池的代码:

public class ThreadPool {
	
	//阻塞队列实现生产者-消费者
	BlockingQueue<Runnable> taskQueue;
	
	//工作线程集合
	List<Thread> threads = new ArrayList<Thread>();
	
	//线程池的构造方法
	ThreadPool(int poolSize, BlockingQueue<Runnable> taskQueue) {
		this.taskQueue = taskQueue;
		
		//启动线程池对应 size 的工作线程
		for (int i = 0; i <poolSize; i++) {
			Thread t = new Thread(() -> {
				while (true) {
					Runnable task;
					try {
						task = taskQueue.take();//获取任务队列中的下一个任务
						task.run();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			});
			t.start();
			threads.add(t);
		}
	}
	
	//提交执行任务
	void execute(Runnable task) {
		try {
			//把任务方法放到任务队列
			taskQueue.put(task);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

}

线程池的使用测试

public class TestThreadPool {

	public static void main(String[] args) {
		// 创建有界阻塞任务队列
		BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<>(10);
		// 创建 3个 工作线程的线程池
		ThreadPool tp = new ThreadPool(3, taskQueue);
		
		//提交 10 个任务
		for (int i = 1; i <= 10; i++) {
			final int j = i;
			tp.execute(() -> {
				System.out.println("执行任务" + j);
			});
		}
	}
	
}

结果:

执行任务1
执行任务2
执行任务3
执行任务6
执行任务5
执行任务4
执行任务8
执行任务7
执行任务10
执行任务9

这个线程池的代码中

  • poolSize 是线程池工作线程的个数
  • BlockingQueue taskQueue 是用有界阻塞队列存储 Runnable 任务
  • execute(Runnable task) 提交任务
  • 线程池对象被创建,就自动启动 poolSize 个工作线程
  • 工作线程一直从任务队列 taskQueue 中取任务

线程池的原理就是这么简单,但是 JDK 中的线程池的功能,要远比这个强大的多。

JDK 中线程池的使用

JDK 中提供的最核心的线程池工具类 ThreadPoolExecutor,在 JDK 1.8 中这个类最复杂的构造方法有 7 个参数。

ThreadPoolExecutor(
    int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler)
  • corePoolSize:线程池保有的最小线程数。
  • maximumPoolSize:线程池创建的最大线程数。
  • keepAliveTime:上面提到项目根据忙闲来增减人员,那在编程世界里,如何定义忙和闲呢?很简单,一个线程如果在一段时间内,都没有执行任务,说明很闲,keepAliveTime 和 unit 就是用来定义这个“一段时间”的参数。也就是说,如果一个线程空闲了keepAliveTime & unit这么久,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收了。
  • unit:keepAliveTime 的时间单位
  • workQueue:任务队列
  • threadFactory:线程工厂对象,可以自定义如何创建线程,如给线程指定name。
  • handler:自定义任务的拒绝策略。线程池中所有线程都在忙碌,且任务队列已满,线程池就会拒绝接收再提交的任务。handler 就是拒绝策略,包括 4 种(即RejectedExecutionHandler 接口的 4个实现类)。
    • AbortPolicy:默认的拒绝策略,throws RejectedExecutionException
    • CallerRunsPolicy:提交任务的线程自己去执行该任务
    • DiscardPolicy:直接丢弃任务,不抛出任何异常
    • DiscardOldestPolicy:丢弃最老的任务,加入新的任务

JDK 的并发工具包里还有一个静态线程池工厂类 Executors,可以方便地创建线程池,但是由于 Executors 创建的线程池内部很多地方用到了无界任务队列,在高并发场景下,无界任务队列会接收过多的任务对象,导致 JVM 抛出OutOfMemoryError,整个 JVM 服务崩溃,影响严重。所以很多公司已经不建议使用 Executors 去创建线程。

Executors 的简介

虽然不建议使用,作为对 JDK 的学习,还是简单介绍一下.

  1. newFixedThreadPool:创建定长线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程数量不再变化,当线程发生错误结束时,线程池会补充一个新的线程
  2. newCachedThreadPool:创建可缓存的线程池,如果线程池的容量超过了任务数,自动回收空闲线程,任务增加时可以自动添加新线程,线程池的容量不限制
  3. newScheduledThreadPool:创建定长线程池,可执行周期性的任务
  4. newSingleThreadExecutor:创建单线程的线程池,线程异常结束,会创建一个新的线程,能确保任务按提交顺序执行
  5. newSingleThreadScheduledExecutor:创建单线程可执行周期性任务的线程池
  6. newWorkStealingPool:任务可窃取线程池,不保证执行顺序,当有空闲线程时会从其他任务队列窃取任务执行,适合任务耗时差异较大。

线程池包含哪些状态?

线程池的5种状态:RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED。

1. RUNNING:线程池一旦被创建,就处于 RUNNING 状态,任务数为 0,能够接收新任务,对已排队的任务进行处理。

  1. SHUTDOWN:不接收新任务,但能处理已排队的任务。调用线程池的 shutdown() 方法,线程池由 RUNNING 转变为 SHUTDOWN 状态。

  2. STOP:不接收新任务,不处理已排队的任务,并且会中断正在处理的任务。调用线程池的 shutdownNow() 方法,线程池由(RUNNING 或 SHUTDOWN ) 转变为 STOP 状态。

  3. TIDYING

  • SHUTDOWN 状态下,任务数为 0, 其他所有任务已终止,线程池会变为 TIDYING 状态,会执行 terminated() 方法。线程池中的 terminated() 方法是空实现,可以重写该方法进行相应的处理。
  • 线程池在 SHUTDOWN 状态,任务队列为空且执行中任务为空,线程池就会由 SHUTDOWN 转变为 TIDYING 状态。
  • 线程池在 STOP 状态,线程池中执行中任务为空时,就会由 STOP 转变为 TIDYING 状态。
  1. TERMINATED:线程池彻底终止。线程池在 TIDYING 状态执行完 terminated() 方法就会由 TIDYING 转变为 TERMINATED 状态。

    转换状态如图:

    img

    JDK 源码中的解释如下

    状态:

    The runState provides the main lifecyle control, taking on values:
    
      RUNNING:  Accept new tasks and process queued tasks
      SHUTDOWN: Don't accept new tasks, but process queued tasks
      STOP:     Don't accept new tasks, don't process queued tasks,
                and interrupt in-progress tasks
      TIDYING:  All tasks have terminated, workerCount is zero,
                the thread transitioning to state TIDYING
                will run the terminated() hook method
      TERMINATED: terminated() has completed
    

    状态间的变化

    RUNNING -> SHUTDOWN
       On invocation of shutdown(), perhaps implicitly in finalize()
    (RUNNING or SHUTDOWN) -> STOP
       On invocation of shutdownNow()
    SHUTDOWN -> TIDYING
       When both queue and pool are empty
    STOP -> TIDYING
       When pool is empty
    TIDYING -> TERMINATED
       When the terminated() hook method has completed
    
    Threads waiting in awaitTermination() will return when the
    state reaches TERMINATED.
    

    如何停止一个线程池?

    Java 并发工具包中 java.util.concurrent.ExecutorService 接口定义了线程池任务提交、获取线程池状态、线程池停止的方法等。

    JDK 1.8 中,线程池的停止一般使用 shutdown()、shutdownNow()、shutdown() + awaitTermination(long timeout, TimeUnit unit) 方法。

    1.shutdown() 方法源码中解释

         * Initiates an orderly shutdown in which previously submitted
         * tasks are executed, but no new tasks will be accepted.
         * Invocation has no additional effect if already shut down.
    
    • 有序关闭,已提交任务继续执行
    • 不接受新任务

    2、shutdownNow() 方法源码中解释

         * Attempts to stop all actively executing tasks, halts the
         * processing of waiting tasks, and returns a list of the tasks
         * that were awaiting execution.
    
    • 尝试停止所有正在执行的任务
    • 停止等待执行的任务,并返回等待执行的任务列表

3、awaitTermination(long timeout, TimeUnit unit) 方法源码中解释

     * Blocks until all tasks have completed execution after a shutdown
     * request, or the timeout occurs, or the current thread is
     * interrupted, whichever happens first.
     *
     * @param timeout the maximum time to wait
     * @param unit the time unit of the timeout argument
     * @return {@code true} if this executor terminated and
     *         {@code false} if the timeout elapsed before termination
     * @throws InterruptedException if interrupted while waiting
  • 收到关闭请求后,所有任务执行完成、超时、线程被打断,阻塞直到三种情况任意一种发生
  • 参数可以设置超时时间与超时单位
  • 线程池关闭返回 true;超过设置时间未关闭,返回 false

实践:

1、使用 Executors.newFixedThreadPool(int nThreads) 创建固定大小线程池,测试 shutdown() 方法

package constxiong.concurrency.a013;

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

/**
 * 测试固定数量线程池 shutdown() 方法
 * @author ConstXiong
 */
public class TestFixedThreadPoolShutdown {
	
	public static void main(String[] args) {
		//创建固定 3 个线程的线程池
		ExecutorService threadPool = Executors.newFixedThreadPool(3);
		
		//向线程池提交 10 个任务
		for (int i = 1; i <= 10; i++) {
			final int index = i;
			threadPool.submit(() -> {
				System.out.println("正在执行任务 " + index);
				//休眠 3 秒
				try {
					Thread.sleep(3000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			});
		}
		
		//休眠 4 秒
		try {
			Thread.sleep(4000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		//关闭线程池
		threadPool.shutdown();
	}

}

打印结果如下,可以看出,主线程向线程池提交了 10 个任务,休眠 4 秒后关闭线程池,线程池把 10 个任务都执行完成后关闭了。

正在执行任务 1
正在执行任务 3
正在执行任务 2
正在执行任务 4
正在执行任务 6
正在执行任务 5
正在执行任务 8
正在执行任务 9
正在执行任务 7
正在执行任务 10

2、使用 Executors.newFixedThreadPool(int nThreads) 创建固定大小线程池,测试 shutdownNow() 方法

package constxiong.concurrency.a013;

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

/**
 * 测试固定数量线程池 shutdownNow() 方法
 * @author ConstXiong
 */
public class TestFixedThreadPoolShutdownNow {
	
	public static void main(String[] args) {
		//创建固定 3 个线程的线程池
		ExecutorService threadPool = Executors.newFixedThreadPool(3);
		
		//向线程池提交 10 个任务
		for (int i = 1; i <= 10; i++) {
			final int index = i;
			threadPool.submit(() -> {
				System.out.println("正在执行任务 " + index);
				//休眠 3 秒
				try {
					Thread.sleep(3000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			});
		}
		
		//休眠 4 秒
		try {
			Thread.sleep(4000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		//关闭线程池
		List<Runnable> tasks = threadPool.shutdownNow();
		System.out.println("剩余 " + tasks.size() + " 个任务未执行");
	}

}

打印结果如下,可以看出,主线程向线程池提交了 10 个任务,休眠 4 秒后关闭线程池,线程池执行了 6 个任务,抛出异常,打印返回的剩余未执行的任务个数。

正在执行任务 1
正在执行任务 2
正在执行任务 3
正在执行任务 4
正在执行任务 6
正在执行任务 5
剩余 4 个任务未执行
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at constxiong.concurrency.a013.TestFixedThreadPoolShutdownNow.lambda$0(TestFixedThreadPoolShutdownNow.java:24)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at constxiong.concurrency.a013.TestFixedThreadPoolShutdownNow.lambda$0(TestFixedThreadPoolShutdownNow.java:24)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at constxiong.concurrency.a013.TestFixedThreadPoolShutdownNow.lambda$0(TestFixedThreadPoolShutdownNow.java:24)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)

3、Executors.newFixedThreadPool(int nThreads) 创建固定大小线程池,测试 awaitTermination(long timeout, TimeUnit unit) 方法

package constxiong.concurrency.a013;

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

/**
 * 测试固定数量线程池 shutdownNow() 方法
 * @author ConstXiong
 */
public class TestFixedThreadPoolAwaitTermination {
	
	public static void main(String[] args) {
		//创建固定 3 个线程的线程池
		ExecutorService threadPool = Executors.newFixedThreadPool(3);
		
		//向线程池提交 10 个任务
		for (int i = 1; i <= 10; i++) {
			final int index = i;
			threadPool.submit(() -> {
				System.out.println("正在执行任务 " + index);
				//休眠 3 秒
				try {
					Thread.sleep(3000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			});
		}
		
		//关闭线程池,设置等待超时时间 3 秒
		System.out.println("设置线程池关闭,等待 3 秒...");
		threadPool.shutdown();
		try {
			boolean isTermination = threadPool.awaitTermination(3, TimeUnit.SECONDS);
			System.out.println(isTermination ? "线程池已停止" : "线程池未停止");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		//再等待超时时间 20 秒
		System.out.println("再等待 20 秒...");
		try {
			boolean isTermination = threadPool.awaitTermination(20, TimeUnit.SECONDS);
			System.out.println(isTermination ? "线程池已停止" : "线程池未停止");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

}

打印结果如下,可以看出,主线程向线程池提交了 10 个任务,申请关闭线程池 3 秒超时,3 秒后线程池并未成功关闭;再获取线程池关闭状态 20 秒超时,线程池成功关闭。

正在执行任务 1
正在执行任务 3
正在执行任务 2
设置线程池关闭,等待 3...
线程池未停止
正在执行任务 4
正在执行任务 6
再等待 20...
正在执行任务 5
正在执行任务 7
正在执行任务 9
正在执行任务 8
正在执行任务 10
线程池已停止

总结:

  1. 调用 shutdown() 和 shutdownNow() 方法关闭线程池,线程池都无法接收新的任务
  2. shutdown() 方法会继续执行正在执行未完成的任务;shutdownNow() 方法会尝试停止所有正在执行的任务
  3. shutdown() 方法没有返回值;shutdownNow() 方法返回等待执行的任务列表
  4. awaitTermination(long timeout, TimeUnit unit) 方法可以获取线程池是否已经关闭,需要配合 shutdown() 使用
  5. shutdownNow() 不一定能够立马结束线程池,该方法会尝试停止所有正在执行的任务,通过调用 Thread.interrupt() 方法来实现的,如果线程中没有 sleep() 、wait()、Condition、定时锁等应用, interrupt() 方法是无法中断当前的线程的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值