首页 > 编程语言 >Java并发编程-线程池

Java并发编程-线程池

时间:2024-10-11 19:51:19浏览次数:10  
标签:Java 队列 编程 任务 线程 执行 CPU ThreadPoolExecutor

ThreadLocal

应用场景:两个线程争执一个资源。

解决问题:实现每个线程绑定自己的专属本地变量,可以将ThreadLocal类理解成存放数据的盒子,盒子中存放每个线程的私有数据。

线程池的用途选择
  1. 快速响应用户请求:比如说用户查询商品详情页,会涉及查询商品关联的一系列信息如价格、优惠、库存、基础信息等,站在用户体验的角度,希望商详页的响应时间越短越好,此时可以考虑使用线程池并发地查询价格、优惠、库存等信息,再聚合结果返回,降低接口总rt。这种线程池用途追求的是最快响应速度,所以可以考虑不设置队列去缓冲并发任务,而是尽可能设置更大的corePoolSize和maxPoolSize;
  2. 快速处理批量任务:比如说项目中在对接渠道同步商品供给时,需要查询大量的商品数据并同步给渠道,此时可以考虑使用线程池快速处理批量任务。这种线程池用途关注的是如何使用有限的机器资源,尽可能地在单位时间内处理更多的任务,提升系统吞吐量,所以需要设置阻塞队列缓冲任务,并根据任务场景调整合适的corePoolSize;
如何使用ThreadLocal
import java.text.SimpleDateFormat;
import java.util.Random;

public class ThreadLocalExample implements Runnable{

     // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
    private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalExample obj = new ThreadLocalExample();
        for(int i=0 ; i<10; i++){
            Thread t = new Thread(obj, ""+i);
            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }
    }

    @Override
    public void run() {
        System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //formatter pattern is changed here by thread, but it won't reflect to other threads
        formatter.set(new SimpleDateFormat());

        System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
    }

}

ThreadLocal内存泄漏问题

ThreadLocalMap中使用的key为ThreadLocal的弱引用(类似可有可无的物品,与垃圾回收器线程有关),而value是强引用,所以ThreadLocalMap可能出现key为null的Entry,如果不做任何措施,value永远无法不会被GC回收,这个时候就会导致OOM。

解决:调用set()、get()、remove()方法的时候,清理掉key为null的记录,使用完ThreadLocalMap方法最好手动调用remove()方法。

线程池

当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。

为什么要用线程池?

池化思想:减少每次获取资源的消耗,提高对资源的利用率

线程池好处:
  • 降低资源消耗:降低线程创建和销毁造成的消耗。
  • 提高响应速度:任务无需等待线程创建,直接从线程池中拿出线程即可立即执行。
  • 提高线程的管理性:对线程进行统一分配,调优和监控。
如何创建线程池?

通过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.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

ThreadPoolExecutor 3 个最重要的参数:

  • corePoolSize : 任务队列未达到队列容量时,最大可以同时运行的线程数量。
  • maximumPoolSize : 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  • workQueue: 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

ThreadPoolExecutor其他常见参数 :

  • keepAliveTime:当线程池中的线程数量大于 corePoolSize ,即有非核心线程(线程池中核心线程以外的线程)时,这些非核心线程空闲后不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁。
  • unitkeepAliveTime 参数的时间单位。
  • threadFactory :executor 创建新线程的时候会用到。
  • handler :拒绝策略(后面会单独详细介绍一下)

线程池的核心线程可以被回收吗?

ThreadPoolExecutor默认不回收核心线程,但是提供了allowCoreThreadTimeOut(boolean value)方法,当参数为true时,可以在达到线程空闲时间后,回收核心线程,在业务代码中,如果线程池是周期性的使用,可以考虑将该参数设置为true;

 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(4, 6, 6, TimeUnit.SECONDS, new SynchronousQueue<>());
        threadPoolExecutor.allowCoreThreadTimeOut(true);

线程池的拒绝策略:

  • ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果你的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求。

不指定拒绝策略,默认采用AbortPolicy,在这种拒绝策略下,如果队列满了,线程池将抛出异常来拒绝新任,同时丢失这个任务的处理,如果不想丢弃任务,可以使用其他策略。

如果不允许丢弃任务,应该选择哪个拒绝策略

选用CallerRunsPolicy

public static class CallerRunsPolicy implements RejectedExecutionHandler {
        public CallerRunsPolicy() { }
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            //只要当前程序没有关闭,就用执行execute方法的线程执行该任务
            if (!e.isShutdown()) {
                r.run();
            }
        }
    }

CallerRunsPolicy拒绝策略有什么风险?如何解决?

问题:导致耗时的任务用了主线程执行,导致线程池阻塞,进而导致后续任务无法及时执行,严重的情况下很可能导致 OOM。

解决:(解决OOM基本思路

  1. 在内存允许情况下,可以增加阻塞队列的大小并调整堆内存。(调整堆内存)
  2. 为了充分利用CPU,可以调整线程池maximumPoolSize(最大线程数),提高任务处理速度,避免阻塞队列任务过多导致内存用完。(调整方法参数)
  3. 服务器资源达到可利用的极限 —> 调整涉及策略(任务持久化思路)
    • 设计一张任务表,将任务存储到Mysql数据库中。(数据库持久化)
    • Redis缓存任务。(缓存)
    • 将任务提交到消息队列中。(消息队列)

线程池常用的阻塞队列有哪些?

  • 容量为 Integer.MAX_VALUELinkedBlockingQueue(有界阻塞队列):FixedThreadPoolSingleThreadExecutorFixedThreadPool最多只能创建核心线程数的线程(核心线程数和最大线程数相等),SingleThreadExecutor只能创建一个线程(核心线程数和最大线程数都是 1),二者的任务队列永远不会被放满。

  • SynchronousQueue(同步队列):CachedThreadPoolSynchronousQueue 没有容量,不存储元素,目的是保证对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务。也就是说,CachedThreadPool 的最大线程数是 Integer.MAX_VALUE ,可以理解为线程数是可以无限扩展的,可能会创建大量线程,从而导致 OOM。

  • DelayedWorkQueue(延迟队列):ScheduledThreadPoolSingleThreadScheduledExecutorDelayedWorkQueue 的内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构,可以保证每次出队的任务都是当前队列中执行时间最靠前的。DelayedWorkQueue 添加元素满了之后会自动扩容,增加原来容量的 50%,即永远不会阻塞,最大扩容可达 Integer.MAX_VALUE,所以最多只能创建核心线程数的线程。

  • ArrayBlockingQueue(有界阻塞队列):底层由数组实现,容量一旦创建,就不能修改。

线程池处理任务的流程(核心线程 --> 阻塞队列 --> 最大线程)

  1. 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。
  2. 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。
  3. 如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。
  4. 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,拒绝策略会调用RejectedExecutionHandler.rejectedExecution()方法。

线程池在提交任务前,可以提前创建线程吗?

可以的!ThreadPoolExecutor 提供了两个方法帮助我们在提交任务之前,完成核心线程的创建,从而实现线程池预热的效果:

  • prestartCoreThread():启动一个线程,等待任务,如果已达到核心线程数,这个方法返回 false,否则返回 true;
  • prestartAllCoreThreads():启动所有的核心线程,并返回启动成功的核心线程数。

线程池中线程异常后,销毁还是复用?(京东技术分享)

  • 使用execute()提交任务:当任务通过execute()提交到线程池并在执行过程中抛出异常时,如果这个异常没有在任务内被捕获,那么该异常会导致当前线程终止,并且异常会被打印到控制台或日志文件中。线程池会检测到这种线程终止,并创建一个新线程来替换它,从而保持配置的线程数不变。
  • 使用submit()提交任务:对于通过submit()提交的任务,如果在任务执行中发生异常,这个异常不会直接打印出来。相反,异常会被封装在由submit()返回的Future对象中。当调用Future.get()方法时,可以捕获到一个ExecutionException。在这种情况下,线程不会因为异常而终止,它会继续存在于线程池中,准备执行后续的任务。

使用execute()时,未捕获异常导致线程终止,线程池创建新线程替代;使用submit()时,异常被封装在Future中,线程继续复用。

这种设计允许submit()提供更灵活的错误处理机制,因为它允许调用者决定如何处理异常,而execute()则适用于那些不需要关注执行结果的场景。

线程池如何命名

初始化线程池名称前缀 --> 定位问题。

1、利用 guava 的 ThreadFactoryBuilder

ThreadFactory threadFactory = new ThreadFactoryBuilder()
                        .setNameFormat(threadNamePrefix + "-%d")
                        .setDaemon(true).build();
ExecutorService threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory);

2、重实现ThreadFactory

import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 线程工厂,它设置线程名称,有利于我们定位问题。
 */
public final class NamingThreadFactory implements ThreadFactory {

    private final AtomicInteger threadNum = new AtomicInteger();
    private final String name;

    /**
     * 创建一个带名字的线程池生产工厂
     */
    public NamingThreadFactory(String name) {
        this.name = name;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        t.setName(name + " [#" + threadNum.incrementAndGet() + "]");
        return t;
    }
}

线程池大小如何设置

  • 线程池的数量不是越多越好,就拿生活中举个例子,并不是人多就能把事情做好,沟通成本就会增加,一个事需要3个人做,硬是拉来6个人,做事效率会有提升吗?我想并不会,线程数量过多也是一个道理,多线程的场景下,大量线程可能会同时在争取 CPU 资源,增加了上下文的切换成本,从而增加线程的执行时间,影响整体执行效率。

上下文切换:多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换

  • 线程池数量设置太小也不行,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求,在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。

线程数池数量计算公式

  • CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1。比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
  • I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

如何判断是 CPU 密集任务还是 IO 密集任务?

CPU 密集型简单理解就是利用 CPU 计算能力的任务,比如你在内存中对大量数据进行排序

但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。

线程数更严谨的计算的方法应该是:最佳线程数 = N(CPU 核心数)∗(1+WT(线程等待时间)/ST(线程计算时间)),其中 WT(线程等待时间)=线程运行总时间 - ST(线程计算时间)

线程等待时间所占比例越高,需要越多线程。线程计算时间所占比例越高,需要越少线程。

我们可以通过 JDK 自带的工具 VisualVM 来查看 WT/ST 比例。

CPU 密集型任务的 WT/ST 接近或者等于 0,因此, 线程数可以设置为 N(CPU 核心数)∗(1+0)= N,和我们上面说的 N(CPU 核心数)+1 差不多。

IO 密集型任务下,几乎全是线程等待时间,从理论上来说,你就可以将线程数设置为 2N(按道理来说,WT/ST 的结果应该比较大,这里选择 2N 的原因应该是为了避免创建过多线程吧)。

动态修改线程池的参数

三个核心参数:corePoolSizemaximumPoolSizeworkQueue(https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html)

开源框架:Hippo4j 异步线程池Dynamic TP 轻量级动态线程池(为什么应用、如何应用)

如何设计一个能够根据任务的优先级来执行的线程池

PriorityBlockingQueue优先阻塞队列

存在问题:

1、该队列是无界的,存在堆积请求 --> OOM

2、饥饿问题,即优先级低的任务长时间得不到执行

3、需要对队列中的元素进行排序以及线程安全(创建PriorityBlockingQueue传入comparator对象,指定排序规则)

Future (异步)

解决问题:将耗时任务转交给子线程执行(异步),Future获取子线程执行的结果(可查看任务执行、状态、取消任务),返回结果向主线程。

4个功能:

1、取消任务

2、判断任务是否被取消

3、判断任务是否已经执行完成

4、获取任务执行结果

标签:Java,队列,编程,任务,线程,执行,CPU,ThreadPoolExecutor
From: https://blog.csdn.net/m0_74119287/article/details/142767735

相关文章

  • Java String.valueOf 和 toString的区别
    String.valueOf()和toString()都是Java中用于获取字符串表示的方法,但它们的使用场景和实现方式有所不同。以下是它们之间的主要区别:1.方法来源String.valueOf(Objectobj):是String类的静态方法,接受一个对象作为参数。如果传入的对象为null,它会返回字符串"null"。......
  • JavaScript的内置对象有哪些?
    一、内置对象1、概念​JavaScript中的对象共分为3种:自定义对象、浏览器对象和内置对象。之前我们自己创建的对象都属于自定义对象,而内置对象又称为API,是指JavaScript语言自己封装的一些对象,用来提供一些常用的基本功能,来帮助我们提高开发速度,例如:数学-Math、日期-Date......
  • Java中反射的机制
    反射目录反射反射的概念反射的作用反射的原理直接使用类使用反射总结什么情况下使用反射反射的优缺点反射是否真的会让你的程序性能降低?反射的概念反射(Reflection)是Java的一种特性,它可以让程序在运行时获取自身的信息,并且动态地操作类或对象的属性、方法和构造器等。通过反......
  • JVM系列1:深入分析Java虚拟机堆和栈及OutOfMemory异常产生原因
    JVM系列1:深入分析Java虚拟机堆和栈及OutOfMemory异常产生原因前言JVM系列文章如无特殊说明,一些特性均是基于HotSpot虚拟机和JDK1.8版本讲述。下面这张图我想对于每个学习Java的人来说再熟悉不过了,这就是整个JDK的关系图: 从上图我们可以看到,JavaVirtualMachine位于最底......
  • [Java原创精品]基于Springboot+Vue的仿小红书博客论坛系统,社交媒体平台,含DFA敏感词过
    项目提供:完整源码+数据库sql文件+数据库表对应Excel文件项目获取看主......
  • java算法OJ(2)链表
    目录1.前言2.正文2.1合并俩个有序链表2.1.1题目描述2.1.2示例2.1.3代码2.2俩数相加2.2.1题目描述2.2.2示例2.2.3代码2.3分割链表2.3.1题目描述2.3.2示例2.3.3代码3.小结1.前言哈喽大家好吖,今天来对先前学习的链表进行巩固,做几道算法题,如果大家有更加优良的......
  • Java中的专有名词——JVM、JRE、JDK到底是什么关系
            相信刚开始学习Java的同学一定见过“JVM、JDK、JRE”这三个专有名词,那他们到底代表的是什么,三者之间又有何种关系呢?        下面我们先来介绍一下三者:1.JVM    JVM:Java虚拟机(JavaVirtualMachine,JVM)是运行Java字节码的虚拟机。JVM有......
  • 实验一,现代C++编程初体验
    一、实验目的 体验C++的标准库,算法库用法。数据表示,分支循环,函数和标准库等,编程解决简单基础问题。二、实验准备 第二章C++语言简单设计第三章函数第九章函数模板 三、实验内容 1.实验任务1代码:1#include<iostream>2#include<string>3#include<vector>......
  • Vector线程安全问题
    背景在韩顺平的Java课程中,有一个坦克大战练习项目,其中有这样一个功能需求:敌人坦克自动发射多个子弹,检测子弹是否击中我方坦克。视频中使用的是Vector存储这个子弹队列。代码实现对于这一部分,我的实现代码是://MyPanel.java的run()方法while(true){try{Thr......
  • 找到你的编程“秘密武器”:提升工作效率的工具分享
    在如今竞争激烈、任务繁忙的工作环境中,开发者们始终在寻找能够提高效率的编程工具。无论是智能的代码编辑器,强大的版本控制工具,还是帮助自动化日常工作流程的脚本,正确的工具能让开发工作变得更加轻松,并大幅提升生产力。在这篇文章中,我们将分享几款广受好评的编程工具,帮助你在开......