`
chenzehe
  • 浏览: 538994 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

记一次线程池的使用

 
阅读更多

本文介绍多线程在使用时由直接new Thread()到Executors.newCachedThreadPool(),再到直接new ThreadPoolExecutor()的过程。

 

背景:

项目需要在Servlet前加个Filter做数据的转发,只是数据的简单透传,为了不影响用户的体验,而采用多线程来实现。

 

最简单的实现

直接在filter中new Thread来实现,如下:

 但是访问量一大,这种方法会把系统搞死。

 

线程池实现

后面采用线程池的方式实现,虽然之前也有研究过线程池,但是对线程池的记忆最深的就是线程池是对线程的缓存,重复利用,避免线程的重复创建和销毁带来的性能损耗,还有就是ExecutorService类提供的几种线程池方式:signle线程数的线程、线程数固定的线程、线程数不固定的线程。当时也没多考虑,担心影响系统的并发数,就直接使用Executors.newCachedThreadPool()缓存的线程池,如下:

 但是上线一段时间后,运维反馈系统不太稳定,但也没有直接原因表示是该线程池导致,接着研究。

 

自己的设想

后面觉得这里就是数据的转发,其实也就是一个典型的生产者和消费者模型,可以使用一个阻塞队列来把请求数据缓冲起来,再使用一个线程池把数据消费掉,想到之前研究过的并发大师Doug Lea写的几个并发队列,不错,再比较下这几个队列使用的是显示锁还是CAS就能确定使用哪个队列了,有搞头...

 

线程池的原理

古人云:没文化,真可怕。正当我开开心心的写我的生产者消费者,使用线程池来消费队列里的任务时悲剧的发现,线程池也有队列来缓存任务,我上面的设计就是线程池的另一个作用,线程池不只是对线程的管理,避免线程的重复创建和销毁带来的性能损耗,而且还对任务的缓冲,提高系统的响应速度。

 

比较靠谱的实现

通过直接研究线程池的实现类ThreadPoolExecutor,而不是之前只关注线程池的铺助类和接口,使用线程数量固定、LinkedBlockingQueue为缓存任务的队列,如果任务队列满了就做丢弃处理,如下:

 

ThreadPoolExecutor类的构造函数为:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

 

 创建一个线程池需要输入几个参数:

  • corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
  • maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
  • keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
  • TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
  • workQueue(任务队列):用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列。
    • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
    • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
    • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
    • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
  • threadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
  • RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。
    • AbortPolicy:直接抛出异常。
    • CallerRunsPolicy:只用调用者所在线程来运行任务。
    • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
    • DiscardPolicy:不处理,丢弃掉。
    • 当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。

 明白了上面线程池的构造参数定义后,再回头看前面使用Executors.newCachedThreadPool()方法的实现就是个坑:

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

 

线程池的合理配置

1、线程池的线程数量:一般建议2*CPU(Runtime.getRuntime().availableProcessors()),如果是I/O密集型的任务,可以再大点。

2、队列的使用: 建议使用有界队列,并且经过测试确定任务数在队列里占用内存大小,这里我测试了5W个任务时大约占用20M内存。

 

Java的线程实现

并发的实现可以通过多种方式来实现,例如:单进程-单线程模型,通过在一台服务器上启多个进程实现多任务的并行处理。但是在JAVA语言中,通过是通过单进程-多线程的模型进行多任务的并发处理。每个java.lang.Thread类的实例就代表一个线程,但是Thread类的很多接口都被声明为Native,直接调用本地方法实现,所以在new的Thread实例过多时,不但占用了JVM的堆内存,还要考虑占用系统的本地内存。

public synchronized void start() {
    if (threadStatus != 0 || this != me)
        throw new IllegalThreadStateException();
    group.add(this);
    start0();
    if (stopBeforeStart) {
	stop0(throwableFromStop);
    }
}

private native void start0();

 

 主流的操作系统都提供了线程实现,目前实现线程的方式主要有三种,分别是:

  1. 内核线程(KLT)实现,这种线程由内核来完成线程切换,内核通过线程调度器对线程进行调度,并负责将线程任务映射到不同的处理器上;
  2. 用户线程实现(UT),通常情况下,用户线程指的是完全建立在用户空间线程库上的线程,用户线程的创建、启动、运行、销毁和切换完全在用户态中完成,不需要内核的帮助,因此执行性能更高;
  3. 混合实现:将内核线程和用户线程混合在一起使用的方式。

由于虚拟机规范并没有强制规定JAVA的线程必须使用哪种方式实现,因此,不同的操作系统实现的方式也可能存在差异。对于SUN的JDK,在Windows和Linux操作系统上采用了内核线程的实现方式,在Solaris版本的JDK中,提供了一些专有的虚拟机线程参数,用于设置使用哪种线程模型。

 

JAVA内存模型

Java内存模型规定所有的变量都存储在主内存中(JVM内存的一部分),每个线程有自己独立的工作内存,它保存了被该线程使用的变量的主内存拷贝,线程对这些变量的操作都在自己的工作内存中进行,不能直接操作主内存和其它工作内存中存储的变量或者变量副本,线程间的变量访问需通过主内存来完成,三者的关系如下图所示:

 

JAVA内存模型定义了八种操作来完成主内存和工作内存的变量访问,具体如下:

  1. lock:主内存变量,把一个变量标识为某个线程独占的状态;
  2. unlock:主内存变量,把一个处于锁定状态变量释放出来,被释放后的变量才可以被其它线程锁定;
  3. read:主内存变量,把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用;
  4. load:工作内存变量,把read读取到的主内存中的变量值放入工作内存的变量拷贝中;
  5. use:工作内存变量,把工作内存中变量的值传递给java虚拟机执行引擎,每当虚拟机遇到一个需要使用到变量值的字节码指令时将会执行该操作;
  6. assign:工作内存变量,把从执行引擎接收到的变量的值赋值给工作变量,每当虚拟机遇到一个给变量赋值的字节码时将会执行该操作;
  7. store:工作内存变量,把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用;
  8. write:主内存变量,把store操作从工作内存中得到的变量值放入主内存的变量中。

 

总结

线程池的作用不只是线程复用,还能缓冲任务进行流控,new Thread()不但占用JVM内存,还占用本地内存。

 

 

 

分享到:
评论

相关推荐

    java线程池概念.txt

    这里线程池线程大小还需要判断一次;前面的判断过程中并没有加锁,因此可能在execute方法判断的时候poolSize小于corePoolSize,而判断完之后,在其他线程中又向线程池提交了任务,就可能导致poolSize不小于...

    Java实现的线程池、消息队列功能

    线程池是一种线程使用模式,它预先创建了一组可重用的线程,当有任务需要执行时,不再直接创建新的线程,而是从线程池中取出一个空闲线程来执行任务,完成任务后线程返回到线程池而不是直接销毁。这样可以避免频繁地...

    Spring基于线程池的定时任务线挰异常实践

    我们可以通过@Scheduled注解来定义一个方法为定时任务,例如每5秒执行一次,或者在特定时间点执行。 当涉及线程池和定时任务时,异常处理是一个不可忽视的话题。线程池中的任务可能会抛出异常,如果不进行妥善处理...

    SpringBootz整合mybatis、线程池、定时任务等

    通过定义JobDetail和Trigger,我们可以设定任务的执行时间,例如每小时执行一次或者每天特定时间执行。Spring的TaskScheduler或SchedulerFactoryBean也可以用来调度任务,但Quartz提供了更高级和灵活的调度功能。 ...

    Java ExecutorService四种线程池使用详解

    - **关闭线程池**:线程池不是一次性资源,使用完毕后需要调用`shutdown()`或`shutdownNow()`方法进行关闭,防止内存泄漏。 了解并合理使用线程池,不仅可以提高系统的并发性能,还能有效地管理和控制线程,提升...

    VC写的一个简单的线程池

    当任务完成后,线程并不立即销毁,而是返回到线程池中等待下一次的任务分配。这种方式避免了频繁地创建和销毁线程所带来的开销,提高了系统性能。 描述中提到“可以作为参考”,暗示这个线程池实现可能是为了教学或...

    thr_pool.rar_线程池 vc++

    6. **TemporarilyThread.cpp**:可能处理临时任务或一次性任务的线程类实现。 7. **IThreadExcute.cpp**:可能定义了一个接口类,规定了线程执行的任务的接口,使得线程池可以执行不同类型的任务。 8. **Sdate.cpp...

    基于配置中心的轻量级动态线程池,内置监控告警功能,集成常用中间件线程池管理,可通过SPI自定义扩展实现

    线程池是一种多线程处理形式,预先创建一定数量的线程,存放在池中,当需要执行任务时,无需每次都创建新线程,而是从池中取出一个线程来执行任务,任务完成后,线程返回线程池,等待下一次的任务分配。这种设计可以...

    .Net 高性能线程池组件 smartthreadpool

    通过深入研究SmartThreadPool的源码,开发者不仅可以学习到线程池的设计与实现,还可以根据自身的需求进行二次开发,打造出更加符合业务场景的线程池解决方案。 总之,SmartThreadPool作为一款开源的高性能线程池...

    四种Java线程池用法解析

    可以用来安排在给定延迟后执行一次任务,或者周期性地执行任务。这对于需要定期维护或者清理的任务非常有用。例如,可以使用`scheduleAtFixedRate`或`scheduleWithFixedDelay`方法来实现定时任务。 4. `...

    基于C++17实现的简易线程池源码(含超详细注释+知识说明文档).zip

    【项目介绍】基于C++17的简易线程池任务描述- 实现多线程安全的任务队列,线程池使用异步操作,提交(submit)使用与thread相同。- 内部利用完美转发获取可调用对象的函数签名,lambda与function包装任务,使用RAII...

    java编写的智能电表采集系统,使用线程池进行采集,采集频率为5S,实现电表协议解析存入数据库.zip

    其次,系统设定的采集频率为每5秒一次,这是根据实际应用场景和电表数据刷新频率综合考虑的结果。频繁的采集可能会增加网络负担,而采集间隔过长则可能导致数据丢失或延迟。通过定时任务(如...

    记一次 .NET 某工控数据采集平台 线程数 爆高分析.doc

    - **优化线程池使用**:限制线程池最大线程数,确保任务能及时完成并释放线程。 - **审查同步代码**:检查并修复可能导致死锁或阻塞的代码段。 - **检查异步编程**:确保正确使用异步模型,避免不必要的同步等待...

    实现端口转发功能,采用线程池模型,是我在别人的基础上该的,然后再共享出来,实现其良性循环

    `TemporarilyThread.cpp`可能表示一种临时线程的实现,这可能是为了处理一次性或者非持久的任务,例如初始化操作或者异常处理。 `ThreadManage.aps`和`ThreadManage.clw`这两个文件可能是工程文件,用于Visual ...

    java多线程分页查询

    分页查询是指在查询数据时,将数据分成多个页面展示,而不是一次性返回所有数据。这种方式能够有效地减少单次查询的数据量,从而提高查询速度和用户体验。通常分页查询涉及到的关键参数包括:当前页码、每页显示记录...

    java对大数据的处理.pdf

    数据入库时,通常会使用数据库的批处理功能,如文中调用的`stencDao.insertBatch(listBean)`,这允许一次提交多条记录,减少了与数据库交互的次数,从而提高性能。 在数据处理阶段,使用多线程技术来并发处理数据,...

    MySQL新增线程池插件说明 mysql 数据库优化、性能压测、详细测试方,

    当线程完成任务后,不会立即销毁,而是返回线程池等待下一次的任务分配,这样可以有效地减少线程创建和销毁的时间成本,提高系统响应速度。 ### 2. 线程池插件的安装与配置 安装线程池插件通常需要在MySQL源码编译...

    threadpool-0_2_5-src

    5. 任务执行完毕后,线程返回到等待状态,等待下一次任务分配。 在Boost线程池库中,`thread_pool`类还提供了`stop()`方法来停止线程池,`join()`方法等待所有线程结束。此外,`thread_pool`类内部使用`condition_...

Global site tag (gtag.js) - Google Analytics