Java并发编程之线程池的使用

概述

为什么需要使用池
  • 我们平常使用线程的时候,都会通过new Thread()去创建一个新的线程,这样比较方便,但在实际开发时大多使用线程池。
  • 线程的开启和销毁需要耗费系统资源,而频繁的创建销毁线程就会大大降低程序的效率。
  • 程序的运行需要系统资源,而我们就需要去优化资源的使用,其中一种优化策略就是池化技术。
  • 说到池大家一定多不陌生,线程池、JDBC连接池、对象池等都是经常使用的。池化技术通俗的讲就是实现准备好一些资源,有人要用的时候就来拿,用完就还,这样方便管理资源。
线程池的好处
  1. 降低资源的消耗。使用线程池可以避免重复开启线程,节省系统资源。
  2. 提高响应速度。由于事先开启了一定数量的线程,需要线程时可以直接从池中取来使用而不需要创建。
  3. 方便管理线程。创建线程池时我们可以指定最大线程数、等待队列等参数,避免因创建过多线程而造成内存溢出。

线程池的使用

如何创建线程池?

《阿里巴巴Java开发手册》中强制线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,避免资源好耗尽的风险。

Executors 返回线程池对象的弊端如下:

  • FixedThreadPoolSingleThreadExecutor : 允许请求的队列长度为
    Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OOM。
  • CachedThreadPoolScheduledThreadPool : 允许创建的线程数量为
    Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
1、通过ThreadPoolExecutor的构造方法实现

在这里插入图片描述
具体参数后面再讲。

2、通过Executors工具类创建线程池

Executors中大多方法都是用了static修饰,因此它是个工具类,通过Executors我们可以有三种方式创建线程池:

(1) Executors.newSingleThreadExecutor()

在这里插入图片描述
方法返回⼀个只有⼀个线程的线程池。若多余⼀个任务被提交到该线程池,任务会被保存在⼀个任务队列中,待线程空闲,按先⼊先出的顺序执⾏队列中的任务。
程序测试:

public class ThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
//        ExecutorService executor = Executors.newFixedThreadPool(10);
//        ExecutorService executor = Executors.newCachedThreadPool();

        try {
            for (int i = 0; i < 100; i++) {
                executor.submit(() -> {
                    System.out.println(Thread.currentThread().getName());
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            executor.shutdown();  // 关闭线程池
        }
    }
}

执行结果:

pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
...
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1

该方式创建的线程池只有一个线程。

(2) Executors.newFixedThreadPool()

在这里插入图片描述
该⽅法返回⼀个固定线程数量的线程池。该线程池中的线程数量始终不变。当有⼀个新的任务提交时,线程池中若有空闲线程,则⽴即执⾏。若没有,则新的任务会被暂存在⼀个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
程序测试:

public class ThreadPoolDemo {
    public static void main(String[] args) {
//        ExecutorService executor = Executors.newSingleThreadExecutor();
        ExecutorService executor = Executors.newFixedThreadPool(10);
//        ExecutorService executor = Executors.newCachedThreadPool();

        try {
            for (int i = 0; i < 100; i++) {
                executor.submit(() -> {
                    System.out.println(Thread.currentThread().getName());
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            executor.shutdown();  // 关闭线程池
        }
    }
}

执行结果:

...
pool-1-thread-5
pool-1-thread-3
pool-1-thread-5
pool-1-thread-10
pool-1-thread-8
pool-1-thread-7
...

该方式创建固定数量的线程池。

(3) Executors.newCachedThreadPool()

在这里插入图片描述
该⽅法返回⼀个可根据实际情况调整线程数量的线程池。
程序测试:

public class ThreadPoolDemo {
    public static void main(String[] args) {
//        ExecutorService executor = Executors.newSingleThreadExecutor();
//        ExecutorService executor = Executors.newFixedThreadPool(10);
        ExecutorService executor = Executors.newCachedThreadPool();

        try {
            for (int i = 0; i < 100; i++) {
                executor.submit(() -> {
                    System.out.println(Thread.currentThread().getName());
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            executor.shutdown();  // 关闭线程池
        }
    }
}

执行结果:

...
pool-1-thread-52
pool-1-thread-59
pool-1-thread-35
pool-1-thread-36
pool-1-thread-13
...

可以看出该方法创建的线程池中线程数量是随着任务量动态变化的,但当任务量过大的时候,创建过多的线程会导致系统效率低下,甚至内存溢出。

从上面三幅图中我们可以看到Executors工具类创建线程池时还是使用了ThreadPoolExecutor这个类去构造,相当于只是帮我们写好了参数,但在开发时我们应根据自己实际情况而配置参数,因此不建议使用方式2而是使用方式1手动指定参数。

ThreadPoolExecutor 构造参数和执行过程

ThreadPoolExecutor 类一共提供了四个构造⽅法,我们只需要看参数最多的那个,其余三个都是在这个构造⽅法的基础上产⽣,不多说。

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

该构造方法一共有七个参数:

  1. corePoolSize:核心线程数目
  2. maximumPoolSize:最大线程数目
  3. keepAliveTime:超时等待时间,时间到了自动释放
  4. unit:超时单位
  5. workQueue:工作队列
  6. threadFactory:线程工厂
  7. handler:拒绝策略

这里根据狂神的JUC视频(7大参数及自定义线程池)给出的例子来讲解7个参数。

假设现在有家银行,它总共开设了5个窗口,平常只有2个窗口是在运行的,另外3个是备用的,另外还设置一个等候区,该区域有3个座位。
在这里插入图片描述
当银行来人时首先就会安排开放的2个窗口来服务。
在这里插入图片描述
这时如果陆续来人了,但是备用窗口是没人客服服务的,因此它们只能在等候区等候。在这里插入图片描述
这时候开放窗口和等候区都已经满了,因此银行只能要求备用窗口也开放用于处理更多业务。
在这里插入图片描述
但这时如果继续涌入人,银行怎么也无法再提供服务了,这时候就会拒绝服务,不再让更多的人涌入银行。

上面银行的业务处理和线程池的处理策略是一致的。
以下面创建的线程池对象为例:

public class ThreadPoolDemo {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2,
                5,
                4,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        
    }
}

这里使用ThreadPoolExecutor创建了一个线程池对象。

  • 2:核心线程数目,一开始只开启两个线程用于处理任务。
  • 5:最大线程数目为5,因此除了两个核心线程外,还留有3个备用线程。
  • 4:超时时间,当备用线程开启且处理完任务后,超过4秒没有新任务可执行,备用线程就会关闭。
  • TimeUnit.SECONDS:超时单位为秒。
  • new LinkedBlockingDeque<>(3):创建了一个链式阻塞队列,长度为3。
  • Executors.defaultThreadFactory():创建线程的工厂,一般均采用此形式。
  • new ThreadPoolExecutor.AbortPolicy()):忽略并弹出异常的拒绝策略。

该线程池的线程处理流程如下:

  1. 若任务数n为n<=2,则使用这两个核心线程处理。
  2. 若任务数n增长到2<n<=5,则用核心线程执行任务,并且另外几个任务停留在阻塞队列中。
  3. 若任务数n继续增长到5<n<8,则开启另外的3个备用线程来处理任务(备用线程=最大线程数-核心线程数)
  4. 若备用线程处理任务后,发现没有新的任务可供其执行(阻塞队列为空),则等待超时时间4秒,假设4秒内仍然没有新任务,则它会自动关闭。
  5. 若任务数n增长到n>8,则线程池此时所有线程以全部使用,且阻塞队列已满,启动拒绝策略,忽略新任务且抛出异常。

程序演示:
(1)任务数 < 核心线程数+阻塞队列大小

public class ThreadPoolDemo {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2,
                5,
                4,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        try {
			for (int i = 0; i < 5; i++) {       // 任务数为5,<= 2(核心线程数) + 3(阻塞队列大小)
            	threadPool.submit(()->{
                	System.out.println(Thread.currentThread().getName());
           	});
        }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}

执行结果:

pool-1-thread-1
pool-1-thread-2
pool-1-thread-1
pool-1-thread-2
pool-1-thread-1

可以看到线程池只使用了2个核心线程执行任务。

(2)任务数 > (核心线程数+阻塞队列大小) < (最大线程数 + 阻塞队列数)

public class ThreadPoolDemo {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2,
                5,
                4,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        try {
            for (int i = 0; i < 7; i++) {       // 任务数为7,大于5(核心线程数 + 阻塞队列大小) 且 小于8(最大线程数 + 阻塞队列大小)
                threadPool.submit(()->{
                    System.out.println(Thread.currentThread().getName());
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}

执行结果:

pool-1-thread-2
pool-1-thread-3
pool-1-thread-1
pool-1-thread-1
pool-1-thread-4
pool-1-thread-1
pool-1-thread-3

此时只开启了4个线程,因为任务数为7,有三个在阻塞队列中等待执行。

(3)任务数 > (最大线程数 + 阻塞队列数)

public class ThreadPoolDemo {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2,
                5,
                1,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        try {
            for (int i = 0; i < 9; i++) {       // 任务数为9,大于(最大线程数 + 阻塞队列大小)
                threadPool.submit(() -> {
                    System.out.println(Thread.currentThread().getName());
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}

执行结果:

pool-1-thread-1
pool-1-thread-5
pool-1-thread-4
pool-1-thread-3
pool-1-thread-2
pool-1-thread-5
pool-1-thread-4
pool-1-thread-1
java.util.concurrent.RejectedExecutionException:...

在线程池总任务数达到8时,线程数达到最大数目5,且阻塞队列已满,此时若继续添加任务则会拒绝服务并抛出异常:RejectedExecutionException

从源码角度简单分析线程池工作原理

public void execute(Runnable command) {
		// 如果任务为null,则抛出NullPointerException异常
        if (command == null)
            throw new NullPointerException();

		// 获取当前线程池的状态+线程个数变量的组合值
        int c = ctl.get();
		
		// (1)当前线程池线程个数是否小于核心线程数,小于则开启新线程运行
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
		// (2)如果线程池处于RUNNING状态,则添加任务到阻塞队列
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        // (3)如果队列满了,则新增线程,新增失败则执行拒绝策略
        else if (!addWorker(command, false))
            reject(command);
    }

这里跳过具体的细节,大概分析一下这段代码:

  1. 判断当前线程数是否小于核心线程数,如果小于则通过addWorker(command, true)新建⼀个线程,并将任务(command)添加到该线程中,然后启动该线程从而执⾏任务;否则调到步骤2
if (workerCountOf(c) < corePoolSize){
    if (addWorker(command, true))
                return;
    ...
}
  1. 如果线程池仍在运行,则将任务添加到工作队列中,不成功(队列已满)则调到步骤3
if (isRunning(c) && workQueue.offer(command)){
	...
}
  1. 继续添加任务,此时会开启备用线程,若以达到最大限度,则会拒绝任务
else if (!addWorker(command, false))
            reject(command);

这里从源码角度大概分析了一下线程池的工作原理,想要更加细致的源码分析可以参考这篇博客:
线程池之ThreadPoolExecutor线程池源码分析笔记,博主讲的十分细致。

拒绝策略

当线程池已经达到了所能承受的最大容量时,对于新来的任务必须采用一定的拒绝策略。
ThreadPoolTaskExecutor 定义⼀些策略:
(1)ThreadPoolExecutor.AbortPolicy()
如果线程池达到最大限度,不处理新增的任务且抛出RejectedExecutionException异常。具体情况如上面线程池执行过程中的拒绝过程。

(2)ThreadPoolExecutor.CallerRunsPolicy()
谁产生的任务就由谁去执行,由于任务是main线程产生的,因此线程池将该任务交由mian线程去执行。

public class ThreadPoolDemo {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2,
                5,
                1,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy());        // CallerRunsPolicy拒绝策略
        try {
            for (int i = 0; i < 9; i++) {       // 任务数为9,大于(最大线程数 + 阻塞队列大小)
                threadPool.submit(() -> {
                    System.out.println(Thread.currentThread().getName());
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}

执行结果:

pool-1-thread-2
pool-1-thread-4
main
pool-1-thread-3
pool-1-thread-1
pool-1-thread-4
pool-1-thread-2
pool-1-thread-5
pool-1-thread-3

可以看到有个任务被main线程执行。

(3)ThreadPoolExecutor.DiscardPolicy()
对于新任务直接忽略,不抛出异常。

public class ThreadPoolDemo {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2,
                5,
                1,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardPolicy());        // DiscardPolicy拒绝策略
        try {
            for (int i = 0; i < 9; i++) {       // 任务数为9,大于(最大线程数 + 阻塞队列大小)
                threadPool.submit(() -> {
                    System.out.println(Thread.currentThread().getName());
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}

执行结果:

pool-1-thread-1
pool-1-thread-2
pool-1-thread-2
pool-1-thread-2
pool-1-thread-2
pool-1-thread-3
pool-1-thread-4
pool-1-thread-5

(4)ThreadPoolExecutor.DiscardOldestPolicy()
尝试去和最早的进程竞争,不会抛出异常。
由于程序结果很难看出抢占的线程,这里不加演示。

如何设置线程池的大小

由于线程数量很影响系统的运行效率,因此设置一个合适的线程数量是十分有必要的。
这里以CPU密集型和IO密集型为分类设置线程池大小。

CPU密集型

电脑的核数是N核就选择N+1,设置maximunPoolSize参数。
线程并行(不是并发)执行时程序是很高的,因此将最大线程数设置为CPU核数可以取得较高的性能。+1使得当计算密集型的线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能确保CPU的时钟周期不会被浪费。
查看CPU核数:
在这里插入图片描述
可以通过任务管理器查看CPU核数,注意是看逻辑处理器数量。
在java程序查看CPU核数:

System.out.println(Runtime.getRuntime().availableProcessors());

结果:

8

创建线程池对象时我们可以这样创建:

int max = Runtime.getRuntime().availableProcessors();       // 获取CPU核数
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2,
                max+1,    // 最大线程数设置为CPU核数
                1,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy());
IO密集型

由于IO型任务十分耗费系统资源,因此我们至少要为其分配线程,一般来说设置为2倍的CPU核数就可以了。

总结

线程池的使用归根到底可以用狂神老师的一句话概括:
三大方法、七大参数、四种拒绝策略。

  • 三大方法
    • newSingleThreadExecutor(); //单个线程
    • newFixedThreadPool(5); //创建一个固定的线程池的大小
    • newCachedThreadPool(); //可伸缩的线程池
  • 七大参数
    • corePoolSize:核心线程数目
    • maximumPoolSize:最大线程数目
    • keepAliveTime:超时等待时间,时间到了自动释放
    • unit:超时单位
    • workQueue:工作队列
    • threadFactory:线程工厂
    • handler:拒绝策略
  • 四种拒绝策略
    • ThreadPoolExecutor.AbortPolicy():忽略任务,抛出异常
    • ThreadPoolExecutor.CallerRunsPolicy():哪来的去哪里执行
    • ThreadPoolExecutor.DiscardPolicy():忽略任务,不抛出异常
    • ThreadPoolExecutor.DiscardOldestPolicy():尝试去和最早的线程竞争,不抛出异常

参考

狂神JUC笔记线程池
Java并发编程:线程池的使用
线程池之ThreadPoolExecutor线程池源码分析笔记

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值