Java 线程池

1:线程池优势

(1)降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
(2)方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的,需要保持当前执行线程的现场,并恢复要执行线程的现场)。

如何保证线程池是线程安全的?

在线程池类中通过一个互斥锁(pthread_mutex_t类型变量)来实现线程池的线程安全性。每次往任务队列中添加任务、或者从任务队列中取任务前都要使用pthread_mutex_lock函数加一把锁,然后对任务队列进行操作。操作完后再使用pthread_mutex_unlock释放这个锁,从而实现对任务队列的互斥访问。也就是说每次想要对任务队列进行操作都需要:

      pthread_mutex_lock(&mutex);

      增加任务到任务队列或者从任务队列取任务;

      pthread_mutex_unlock(&mutex);

2:线程池ThreadPoolExecutor参数

在这里插入图片描述

3:线程池执行流程

在这里插入图片描述

4:参数详解

代码如下:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

corePoolSize 核心线程池大小
线程池的基本大小,当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,则会创建一个新的线程来执行该任务。当工作队列满了,则会创建超过corePoolSize大小的线程。注意:刚创建ThreadPoolExecutor的时候,线程不会立即启动,而是要等到有任务提交时才会启动。除非调用了prestartCoreThread/prestartAllCoreThreads事先启动核心线程。另一方面,keepAliveTime 和 allowCoreThreadTimeOut 超时参数的影响,所以没有任务需要执行的时候,线程池的大小不一定是 corePoolSize。

maximumPoolSize 最大线程池大小
线程池所允许的最大线程个数当工作队列满了且已经创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。注意largestPoolSize,代表线程池在整个生命周期中曾经出现的最大的线程个数。

poolSize 当前线程池大小
线程池中当前线程的数量,当该值为0的时候,意味着没有任何线程,线程池会终止;同一时刻,poolSize 不会超过 maximumPoolSize。

keepAliveTime 线程最大空闲时间
是线程池中空闲线程等待工作的超时时间。单位是纳秒,1秒等于10亿纳秒。当线程池中线程数量大于corePoolSize或设置了allowCoreThreadTimeOut时,线程会根据keepAliveTime的值进行活性检查,一旦超时,便销毁线程。否则,线程会永远等待新的工作。

unit 参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性

TimeUnit.DAYS;               //天
TimeUnit.HOURS;             //小时
TimeUnit.MINUTES;           //分钟
TimeUnit.SECONDS;           //秒
TimeUnit.MILLISECONDS;      //毫秒
TimeUnit.MICROSECONDS;      //微妙
TimeUnit.NANOSECONDS;       //纳秒

workQueue 任务队列
用于传输和保存等待执行任务阻塞队列,包括以下四个阻塞队列:
(1)ArrayBlockingQueue:基于数组结构的有界阻塞队列,此队列按照FIFO原则对元素进行排序。此队列创建时必须指定大小
(2)LinkedBlockingQueue:基于链表结构的阻塞队列,按照FIFO(先进先出)原则排序,吞吐量高于ArrayBlockingQueue。如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE。静态工厂方法Executors.newFixedThreadPool()使用了该队列。
(3)SynchronousQueue:不存储元素的阻塞队列。每个插入操作必须等待另一个线程调用移除操作,否则插入操作一直处理阻塞状态。吞吐量高于LinkedBlockingQueue。静态工厂方法Executors.newCachedThreadPool()使用了此队列。
(4)PriorityBlockingQueue:具有优先级的无限阻塞队列。

为什么此处的线程池要用阻塞队列呢?
我们知道队列是先进先出的。当放入一个元素的时候,会放在队列的末尾,取出元素的时候,会从队头取。那么,当队列为空或者队列满的时候怎么办呢。
这时,阻塞队列,会自动帮我们处理这种情况。
当阻塞队列为空的时候,从队列中取元素的操作就会被阻塞。当阻塞队列满的时候,往队列中放入元素的操作就会被阻塞。
而后,一旦空队列有数据了,或者满队列有空余位置时,被阻塞的线程就会被自动唤醒。
这就是阻塞队列的好处,你不需要关心线程何时被阻塞,也不需要关心线程何时被唤醒,一切都由阻塞队列自动帮我们完成。我们只需要关注具体的业务逻辑就可以了。
而这种阻塞队列经常用在生产者消费者模式中。

threadFactory 线程工厂
如果这里没有传参的话,会使用 Executors 中的默认线程工厂类 DefaultThreadFactory。

/**
 * The default thread factory
 */
static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                              Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                      poolNumber.getAndIncrement() +
                     "-thread-";
    }

    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。

handler 拒绝策略
拒绝任务是指当线程池数量达到maxmumPoolSize且workQueue队列已满的情况下拒绝被尝试添加进来的任务。而handler提供了四种拒绝策略:
(1)CallerRunsPolicy:这个策略重试添加当前的任务,会自动重复调用 execute() 方法,直到成功。
(2)AbortPolicy :对拒绝任务抛弃处理,并且抛出异常。
(3)DiscardPolicy :对拒绝任务直接无声抛弃,没有异常信息。
(4)DiscardOldestPolicy :对拒绝任务不抛弃,而是抛弃队列里面等待最久的一个线程,然后把拒绝任务加到队列。

举个例子

corePoolSize:1
mamximumPoolSize:3
keepAliveTime:60s
workQueue:ArrayBlockingQueue,有界阻塞队列,队列大小是4
handler:默认的策略,抛出来一个ThreadPoolRejectException

1.一开始有一个线程变量poolSize维护当前线程数量,此时poolSize=0
2.此时来了一个任务,需要创建线程.poolSize(0) < corePoolSize(1),那么直接创建线程
3.此时来了一个任务,需要创建线程.poolSize(1) >= corePoolSize(1),此时队列没满,那么就丢到队列中去
4.如果队列也满了,但是poolSize < mamximumPoolSize,那么继续创建线程
5.如果poolSize == maximumPoolSize,那么此时再提交一个一个任务,就要执行handler,默认就是抛出异常
6.此时线程池有3个线程(poolSize == maximumPoolSize(3))。假如都处于空闲状态,但是corePoolSize=1,那么就有(3-1 =2)。那么这超出的2个空闲线程,空闲超过60s,就会给回收掉。

5:execute和submit方法区别

(1)execute(),执行一个任务,没有返回值。
(2)submit(),提交一个线程任务,有返回值

submit(Callable task)能获取到它的返回值,通过future.get()获取(阻塞直到任务执行完)。一般使用FutureTask+Callable配合使用

submit(Runnable task, T result)能通过传入的载体result间接获得线程的返回值。
submit(Runnable task)则是没有返回值的,就算获取它的返回值也是null。

Future.get方法会使取结果的线程进入阻塞状态,知道线程执行完成之后,唤醒取结果的线程,然后返回结果。可以参考我的另一篇博客(Java获取多线程返回值方式

6:如何配置线程池

CPU密集型任务
尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。

IO密集型任务
可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。

如果一个项目中调用RPC方式(比如Dubbo)较多,则核心线程数要设置为2*CPU核数,因为RPC属于IO密集型任务,需要大量用到网络IO传输

我们公司BPM系统,2台应用服务器,每台核数16,内存64G,则如果IO密级型,则核心线程池数设置为32。

7:常见线程池

7.1 newSingleThreadExecutor
单个线程的线程池,即线程池中每次只有一个线程工作
具体执行步骤:
(1)线程池中没有线程时,新建一个线程执行任务;
(2)有一个线程以后,将任务加入阻塞队列,不停加加加;
(3)唯一的这一个线程不停地去队列里取任务执行。
单线程串行执行任务。核心线程数和最大线程数大小一样且都是1,keepAliveTime为0,阻塞队列是LinkedBlockingQueue。
SingleThreadExecutor适用于串行执行任务的场景,每个任务必须按顺序执行,不需要并发执行。

7.2 newFixedThreadPool(n)
固定数量的线程池,keepAliveTime为0,阻塞队列是LinkedBlockingQueue,每提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成,才去队列中取任务执行。
FixedThreadPool 适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程即可。一般n*cpu+1。

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

为什么FixedThreadExecutor的corePoolSize和mamximumPoolSize要设计成一样的?
因为线程池是先判断corePoolSize,再判断workQueue,最后判断mamximumPoolSize,然而LinkedBlockingQueue是无界队列,所以他是达不到判断mamximumPoolSize这一步的,所以mamximumPoolSize成多少,并没有多大所谓!

7.3 newCachedThreadPool(推荐使用)
可缓存线程池,当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。
核心线程数为0,且最大线程数为Integer.MAX_VALUE。阻塞队列是SynchronousQueue
SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue
具体执行步骤:
(1)当没有核心线程,直接向SynchronousQueue中提交任务;
(2)如果有空闲线程,就去取出任务执行;
(3)如果没有空闲线程,就新建一个执行完任务的线程有60秒生存时间,如果在这个时间内可以接到新任务,就可以继续活下去,否则就GG。
newCachedThreadPool适用于并发执行大量短期的小任务。

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

为什么CachedThreadExecutor的mamximumPoolSize要设计成接近无限大的?
这个SynchronousQueue队列的容量是很小的,如果mamximumPoolSize不设计得很大,那么就很容易抛出异常

7.4 newScheduleThreadExecutor
大小无限制的线程池,支持定时和周期性的执行线程。
最大线程数为Integer.MAX_VALUE,阻塞队列是DelayedWorkQueue。
ScheduledThreadPoolExecutor 添加任务提供了另外两个方法:

scheduleAtFixedRate() :按某种速率周期执行
scheduleWithFixedDelay():在某个延迟后执行

具体执行步骤:
(1)线程从 DelayQueue 中获取 time 大于等于当前时间的 ScheduledFutureTask
DelayQueue.take()
(2)执行完后修改这个 task 的 time 为下次被执行的时间
(3)然后再把这个 task 放回队列中
DelayQueue.add()
ScheduledThreadPoolExecutor用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景。

8:使用示例

案例解析 LinkedBlockingQueue

(1)LinkedBlockingQueue,当超过核心线程数时,会将新的线程放入到linked队列里面。复用核心线程(可以看出线程名称相同!比如3个核心线程数,则超过3个后会复用这3个线程)

package com.test;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.apache.tomcat.util.collections.SynchronizedQueue;

public class ThreadPoolLinkedBlockingQueueTest {

	 public static void main(String[] args) throws Exception {   
	        
		    Runnable runnable = new Runnable() {
				@Override
				public void run() {
						try {
							Thread.sleep(2000);
						} catch (InterruptedException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
						System.out.println(Thread.currentThread().getName()+" run  ");
				}
			};
			/*
	          LinkedBlockingQueue当线程不超过核心线程数之时,会复用核心线程数(可以看出线程名称相同!)
	         */
	    	ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 4, 5, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<>());  
	    	executor.execute(runnable);
	    	executor.execute(runnable);
	    	executor.execute(runnable);
	    	System.out.println("先开3个线程-----------");
	    	System.out.println("线程池核心线程数:"+executor.getCorePoolSize());
	    	System.out.println("线程池中线程数:"+executor.getPoolSize());
	    	System.out.println("线程池中队列任务数:"+executor.getQueue().size());
	    	executor.execute(runnable);
	    	executor.execute(runnable);
	    	executor.execute(runnable);
	    	System.out.println("再开3个线程-----------");
	    	System.out.println("线程池核心线程数:"+executor.getCorePoolSize());
	    	System.out.println("线程池中线程数:"+executor.getPoolSize());
	    	System.out.println("线程池中队列任务数:"+executor.getQueue().size());
	    	executor.shutdown();
	    }

}

在这里插入图片描述
(2)从下图可以看出,LinkedBlockingQueue不受最大线程数影响,但是当其queue有大小限制则会受影响!
在这里插入图片描述
(3) 当Queue有大小限制时,则LinkedBlockingQueue会受到限制,超过队列限定值,会报错 !
在这里插入图片描述

案例解析 SynchronousQueue

package com.test;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.apache.tomcat.util.collections.SynchronizedQueue;

public class ThreadPoolWorkQueueTest {

	 public static void main(String[] args) throws Exception {   
	        
		    Runnable runnable = new Runnable() {
				@Override
				public void run() {
						try {
							Thread.sleep(2000);
						} catch (InterruptedException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
						System.out.println(Thread.currentThread().getName()+" run  ");
				}
			};
	
	    	ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 4, 5, TimeUnit.MILLISECONDS, new SynchronousQueue());  
	    	executor.execute(runnable);
	    	executor.execute(runnable);
	    	executor.execute(runnable);
	    	System.out.println("开3个线程-----------");
	    	System.out.println("线程池核心线程数:"+executor.getCorePoolSize());
	    	System.out.println("线程池中线程数:"+executor.getPoolSize());
	    	System.out.println("线程池中队列任务数:"+executor.getQueue().size());
	    	executor.execute(runnable);
	    	System.out.println("再开1个线程-----------");
	    	System.out.println("线程池核心线程数:"+executor.getCorePoolSize());
	    	System.out.println("线程池中线程数:"+executor.getPoolSize());
	    	System.out.println("线程池中队列任务数:"+executor.getQueue().size());
	    	/*
	    	executor.execute(runnable);   
	    	System.out.println("再开1个线程-----------");
	    	System.out.println("线程池核心线程数:"+executor.getCorePoolSize());
	    	System.out.println("线程池中线程数:"+executor.getPoolSize());
	    	System.out.println("线程池中队列任务数:"+executor.getQueue().size());
	    	*/
	    	Thread.sleep(8000);
	    	System.out.println("8秒之后的线程-----------");
	    	System.out.println("线程池核心线程数:"+executor.getCorePoolSize());
	    	System.out.println("线程池中线程数:"+executor.getPoolSize());
	    	System.out.println("线程池中队列任务数:"+executor.getQueue().size());
	    	
	    	executor.shutdown();
	    }

}

在这里插入图片描述
在这里插入图片描述

package com.test.thread;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class TestThreadPool1 {
    public static void main(String[] args) {   
        /*核心线程池数量大小为5
         *最大线程池数量大小为10 
         *线程最大空闲时间200毫秒
         *基于数组结构的有界阻塞队列
         * 当线程池中线程的数目大于5时,便将任务放入任务缓存队列里面,当任务缓存队列满了之后,便创建新的线程。
         * 如果将for循环中改成执行20个任务,就会抛出任务拒绝异常了。
         */
    	ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,new ArrayBlockingQueue<Runnable>(5));  
        //for(int i=0;i<20;i++){
    	for(int i=0;i<15;i++){
            MyTask myTask = new MyTask(i);
            executor.execute(myTask);//执行一个任务,没有返回值。
            System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中等待执行的任务数目:"+
            executor.getQueue().size()+",已执行完别的任务数目:"+executor.getCompletedTaskCount());
        }
        executor.shutdown();

    }
}
   class MyTask implements Runnable {
       private int taskNum;
       public MyTask(int num) {
       this.taskNum = num;
   }
  
   @Override
   public void run() {
       System.out.println("正在执行task "+taskNum);
       try {
           Thread.currentThread().sleep(4000);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
       System.out.println("task "+taskNum+"执行完毕");
   }
}

for循环中为15时,执行结果:
在这里插入图片描述

for循环中为20时,执行结果:
在这里插入图片描述

参考文章:
Java 线程池
Java线程池详解
线程池原理
java多线程9:线程池(ExecutorService)
Java-线程池专题 (美团)
关于线程池的面试题

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值