Java线程池的几种实现方法及常见问题解答

工作中,经常会涉及到线程。比如有些任务,经常会交与线程去异步执行。抑或服务端程序为每个请求单独建立一个线程处理任务。线程之外的,比如我们用的数据库连接。这些创建销毁或者打开关闭的操作,非常影响系统性能。所以,“池”的用处就凸显出来了。

1. 为什么要使用线程池

在3.6.1节介绍的实现方式中,对每个客户都分配一个新的工作线程。当工作线程与客户通信结束,这个线程就被销毁。这种实现方式有以下不足之处:

•服务器创建和销毁工作的开销( 包括所花费的时间和系统资源 )很大。这一项不用解释,可以去查下"线程创建过程"。除了机器本身所做的工作,我们还要实例化,启动,这些都需要占用堆栈资源。

•除了创建和销毁线程的开销之外,活动的线程也消耗系统资源。 这个应该是对堆栈资源的消耗,猜测数据库连接数设置一个合理的值,也有这个考虑。

•如果线程数目固定,并且每个线程都有很长的声明周期,那么线程切换也是相对固定的。不同的操作系统有不同的切换周期,一般20ms左右。这里说的切换是在jvm以及底层操作系统的调度下,线程之间转让cpu的使用权。如果频繁创建和销毁线程,那么就将频繁的切换线程,因为一个线程销毁后,必然要让出使用权给已经就绪的线程,使该线程获得运行机会。在这种情况下,线程之间的切换就不在遵循系统的固定切换周期,切换线程的开销甚至比创建和销毁的开销还要大。

相对来说,使用线程池,会预创建一些线程,它们不断的从工作队列中取出任务,然后执行该任务。当工作线程执行完一个任务后,就会继续执行工作队列中的另一个任务。优点如下:

•减少了创建和销毁的次数,每个工作线程都可以一直被重用,能执行多个任务。

•可以根据系统的承载能力,方便的调整线程池中线程的数目,防止因为消耗过量的系统资源而导致系统崩溃。

2. 线程池的简单实现

下面是自己写的一个简单的线程池,也是从Java网络编程这本书上直接照着敲出来的

package thread;

import java.util.LinkedList;

/**
 * 线程池的实现,根据常规线程池的长度,最大长度,队列长度,我们可以增加数目限制实现
 * @author Han
 */
public class MyThreadPool extends ThreadGroup{
  //cpu 数量 ---Runtime.getRuntime().availableProcessors();
  //是否关闭
  private boolean isClosed = false;
  //队列
  private LinkedList<Runnable> workQueue;
  //线程池id
  private static int threadPoolID;
  private int threadID;
  public MyThreadPool(int poolSize){
    super("MyThreadPool."+threadPoolID);
    threadPoolID++;
    setDaemon(true);
    workQueue = new LinkedList<Runnable>();
    for(int i = 0;i<poolSize;i++){
      new WorkThread().start();
    }
  }
  //这里可以换成ConcurrentLinkedQueue,就可以避免使用synchronized的效率问题
  public synchronized void execute(Runnable task){
    if(isClosed){
      throw new IllegalStateException("连接池已经关闭...");
    }else{
      workQueue.add(task);
      notify();
    }
  }

  protected synchronized Runnable getTask() throws InterruptedException {
    while(workQueue.size() == 0){
      if(isClosed){
        return null;
      }
      wait();
    }
    return workQueue.removeFirst();
  }

  public synchronized void close(){
    if(!isClosed){
      isClosed = true;
      workQueue.clear();
      interrupt();
    }
  }

  public void join(){
    synchronized (this) {
      isClosed = true;
      notifyAll();
    }
    Thread[] threads = new Thread[activeCount()];
    int count = enumerate(threads);
    for(int i = 0;i<count;i++){
      try {
        threads[i].join();
      } catch (Exception e) {
      }
    }
  }

  class WorkThread extends Thread{
    public WorkThread(){
      super(MyThreadPool.this,"workThread"+(threadID++));
      System.out.println("create...");
    }
    @Override
    public void run() {
      while(!isInterrupted()){
        System.out.println("run..");
        Runnable task = null;
        try {
          //这是一个阻塞方法
          task = getTask();

        } catch (Exception e) {

        }
        if(task != null){
          task.run();
        }else{
          break;
        }
      }
    }
  }
}

该线程池主要定义了一个工作队列和一些预创建的线程。只要调用execute方法,就可以向线程提交任务。

后面线程在没有任务的时候,会阻塞在getTask(),直到有新任务进来被唤醒。

join和close都可以用来关闭线程池。不同的是,join会把队列中的任务执行完,而close则立刻清空队列,并且中断所有的工作线程。close()中的interrupt()相当于调用了ThreadGroup中包含子线程的各自的interrupt(),所以有线程处于wait或者sleep时,都会抛出InterruptException

测试类如下:

public class TestMyThreadPool {
  public static void main(String[] args) throws InterruptedException {
    MyThreadPool pool = new MyThreadPool(3);
    for(int i = 0;i<10;i++){
      pool.execute(new Runnable() {
        @Override
        public void run() {
          try {
            Thread.sleep(1000);
          } catch (InterruptedException e) {
          }
          System.out.println("working...");
        }
      });
    }
    pool.join();
    //pool.close();
  }
}

3. jdk类库提供的线程池

java提供了很好的线程池实现,比我们自己的实现要更加健壮以及高效,同时功能也更加强大。

类图如下:

关于这类线程池,前辈们已经有很好的讲解。任意百度下java线程池,都有写的非常详细的例子和教程,这里就不再赘述。

4. spring注入线程池

在使用spring框架的时候,如果我们用java提供的方法来创建线程池,在多线程应用中非常不方便管理,而且不符合我们使用spring的思想。(虽然spring可以通过静态方法注入)

其实,Spring本身也提供了很好的线程池的实现。这个类叫做ThreadPoolTaskExecutor。

在spring中的配置如下:

<bean id="executorService" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
    <property name="corePoolSize" value="${threadpool.corePoolSize}" />
    <!-- 线程池维护线程的最少数量 -->
    <property name="keepAliveSeconds" value="${threadpool.keepAliveSeconds}" />
    <!-- 线程池维护线程所允许的空闲时间 -->
    <property name="maxPoolSize" value="${threadpool.maxPoolSize}" />
    <!-- 线程池维护线程的最大数量 -->
    <property name="queueCapacity" value="${threadpool.queueCapacity}" />
    <!-- 线程池所使用的缓冲队列 -->
  </bean>

5. 使用线程池的注意事项

•死锁

任何多线程程序都有死锁的风险,最简单的情形是两个线程AB,A持有锁1,请求锁2,B持有锁2,请求锁1。(这种情况在mysql的排他锁也会出现,不会数据库会直接报错提示)。线程池中还有另一种死锁:假设线程池中的所有工作线程都在执行各自任务时被阻塞,它们在等待某个任务A的执行结果。而任务A却处于队列中,由于没有空闲线程,一直无法得以执行。这样线程池的所有资源将一直阻塞下去,死锁也就产生了。

•系统资源不足

如果线程池中的线程数目非常多,这些线程会消耗包括内存和其他系统资源在内的大量资源,从而严重影响系统性能。

•并发错误

线程池的工作队列依靠wait()和notify()方法来使工作线程及时取得任务,但这两个方法难以使用。如果代码错误,可能会丢失通知,导致工作线程一直保持空闲的状态,无视工作队列中需要处理的任务。因为最好使用一些比较成熟的线程池。

•线程泄漏

使用线程池的一个严重风险是线程泄漏。对于工作线程数目固定的线程池,如果工作线程在执行任务时抛出RuntimeException或Error,并且这些异常或错误没有被捕获,那么这个工作线程就异常终止,使线程池永久丢失了一个线程。(这一点太有意思)

另一种情况是,工作线程在执行一个任务时被阻塞,如果等待用户的输入数据,但是用户一直不输入数据,导致这个线程一直被阻塞。这样的工作线程名存实亡,它实际上不执行任何任务了。如果线程池中的所有线程都处于这样的状态,那么线程池就无法加入新的任务了。

•任务过载

当工作线程队列中有大量排队等待执行的任务时,这些任务本身可能会消耗太多的系统资源和引起资源缺乏。

综上所述,使用线程池时,要遵循以下原则:

1. 如果任务A在执行过程中需要同步等待任务B的执行结果,那么任务A不适合加入到线程池的工作队列中。如果把像任务A一样的需要等待其他任务执行结果的加入到队列中,可能造成死锁

2. 如果执行某个任务时可能会阻塞,并且是长时间的阻塞,则应该设定超时时间,避免工作线程永久的阻塞下去而导致线程泄漏。在服务器才程序中,当线程等待客户连接,或者等待客户发送的数据时,都可能造成阻塞,可以通过以下方式设置时间:

调用ServerSocket的setSotimeout方法,设定等待客户连接的超时时间。

对于每个与客户连接的socket,调用该socket的setSoTImeout方法,设定等待客户发送数据的超时时间。

3. 了解任务的特点,分析任务是执行经常会阻塞io操作,还是执行一直不会阻塞的运算操作。前者时断时续的占用cpu,而后者具有更高的利用率。预计完成任务大概需要多长时间,是短时间任务还是长时间任务,然后根据任务的特点,对任务进行分类,然后把不同类型的任务加入到不同的线程池的工作队列中,这样就可以根据任务的特点,分配调整每个线程池

4. 调整线程池的大小。线程池的最佳大小主要取决于系统的可用cpu的数目,以及工作队列中任务的特点。假如一个具有N个cpu的系统上只有一个工作队列,并且其中全部是运算性质(不会阻塞)的任务,那么当线程池拥有N或N+1个工作线程时,一般会获得最大的cpu使用率。

如果工作队列中包含会执行IO操作并经常阻塞的任务,则要让线程池的大小超过可用 cpu的数量,因为并不是所有的工作线程都一直在工作。选择一个典型的任务,然后估计在执行这个任务的工程中,等待时间与实际占用cpu进行运算的时间的比例WT/ST。对于一个具有N个cpu的系统,需要设置大约N*(1+WT/ST)个线程来保证cpu得到充分利用。

当然,cpu利用率不是调整线程池过程中唯一要考虑的事项,随着线程池工作数目的增长,还会碰到内存或者其他资源的限制,如套接字,打开的文件句柄或数据库连接数目等。要保证多线程消耗的系统资源在系统承受的范围之内。

5. 避免任务过载。服务器应根据系统的承载能力,限制客户并发连接的数目。当客户的连接超过了限制值,服务器可以拒绝连接,并进行友好提示,或者限制队列长度。

以上这篇Java线程池的几种实现方法及常见问题解答就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • 详谈Java几种线程池类型介绍及使用方法

    一.线程池使用场景 •单个任务处理时间短 •将需处理的任务数量大 二.使用Java线程池好处 1.使用new Thread()创建线程的弊端: •每次通过new Thread()创建对象性能不佳. •线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom. •缺乏更多功能,如定时执行.定期执行.线程中断. 2.使用Java线程池的好处: •重用存在的线程,减少对象创建.消亡的开销,提升性能. •可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞

  • Java编程中线程池的基本概念和使用

    1 引入线程池的原因 由于线程的生命周期中包括创建.就绪.运行.阻塞.销毁阶段,当我们待处理的任务数目较小时,我们可以自己创建几个线程来处理相应的任务,但当有大量的任务时,由于创建.销毁线程需要很大的开销,运用线程池这些问题就大大的缓解了. 2 线程池的使用 我们只需要运用Executors类给我们提供的静态方法,就可以创建相应的线程池: public static ExecutorSevice newSingleThreadExecutor() public static ExecutorSe

  • Java线程池的几种实现方法和区别介绍

    Java线程池的几种实现方法和区别介绍 import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.E

  • Java 线程池详解及实例代码

    线程池的技术背景 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源.在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收. 所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁.如何利用已有对象来服务就是一个需要解决的关键问题,其实这就是一些"池化资源"技术产生的原因. 例如Android中常见到的很多通用组件一般都离不开"池"的概念,如各种图片

  • Java四种线程池的使用详解

    Java通过Executors提供四种线程池,分别为: newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程. newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待. newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行. newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,

  • 深入java线程池的使用详解

    在Java 5.0之前启动一个任务是通过调用Thread类的start()方法来实现的,任务的提于交和执行是同时进行的,如果你想对任务的执行进行调度或是控制 同时执行的线程数量就需要额外编写代码来完成.5.0里提供了一个新的任务执行架构使你可以轻松地调度和控制任务的执行,并且可以建立一个类似数据库连接 池的线程池来执行任务.这个架构主要有三个接口和其相应的具体类组成.这三个接口是Executor, ExecutorService.ScheduledExecutorService,让我们先用一个图

  • 四种Java线程池用法解析

    本文为大家分析四种Java线程池用法,供大家参考,具体内容如下 1.new Thread的弊端 执行一个异步任务你还只是如下new Thread吗? new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub } } ).start(); 那你就out太多了,new Thread的弊端如下: a. 每次new Thread新建对象性能差. b. 线程缺乏统一管理,可能无限

  • java中通用的线程池实例代码

    复制代码 代码如下: package com.smart.frame.task.autoTask; import java.util.Collection;import java.util.Vector; /** * 任务分发器 */public class TaskManage extends Thread{    protected Vector<Runnable> tasks = new Vector<Runnable>();    protected boolean run

  • java基于线程池和反射机制实现定时任务完整实例

    本文实例讲述了java基于线程池和反射机制实现定时任务的方法.分享给大家供大家参考,具体如下: 主要包括如下实现类: 1. Main类: 任务执行的入口: 调用main方法,开始加载任务配置并执行任务 package com.yanek.task; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import ja

  • Java线程池的几种实现方法及常见问题解答

    工作中,经常会涉及到线程.比如有些任务,经常会交与线程去异步执行.抑或服务端程序为每个请求单独建立一个线程处理任务.线程之外的,比如我们用的数据库连接.这些创建销毁或者打开关闭的操作,非常影响系统性能.所以,"池"的用处就凸显出来了. 1. 为什么要使用线程池 在3.6.1节介绍的实现方式中,对每个客户都分配一个新的工作线程.当工作线程与客户通信结束,这个线程就被销毁.这种实现方式有以下不足之处: •服务器创建和销毁工作的开销( 包括所花费的时间和系统资源 )很大.这一项不用解释,可以

  • Java线程池的几种实现方法和区别介绍实例详解

    下面通过实例代码为大家介绍Java线程池的几种实现方法和区别: import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Random; import java.util.concurrent.Callable; import java.util.

  • Java数据库连接池的几种配置方法(以MySQL数据库为例)

    一.Tomcat配置数据源: 前提:需要将连接MySQL数据库驱动jar包放进Tomcat安装目录中common文件夹下的lib目录中 1.方法一:在WebRoot下面建文件夹META-INF,里面建一个文件context.xml,如下: <?xml version="1.0" encoding="UTF-8"?> <Context> <Resource name="jdbc/chaoshi" auth="

  • Java线程池的四种拒绝策略详解

    目录 预先配置 配置线程池. 创建线程任务 拒绝策略一:AbortPolicy 拒绝策略二:CallerRunsPolicy 拒绝策略三:DiscardPolicy 拒绝策略四:DiscardOldestPolicy 总结 dk1.5版本新增了 JUC 并发包,其中一个包含线程池. 四种拒绝策略:   拒绝策略类型 说明 1 ThreadPoolExecutor.AbortPolicy 默认拒绝策略,拒绝任务并抛出任务 2 ThreadPoolExecutor.CallerRunsPolicy

  • java线程池的四种创建方式详细分析

    目录 前言 1. 线程池 2. 创建方式 前言 在讲述线程池的前提 先补充一下连接池的定义 连接池是创建和管理一个连接的缓冲池的技术,这些连接准备好被任何需要它们的线程使用 可以看到其连接池的作用如下: 1. 线程池 线程池(英语:thread pool):一种线程使用模式.线程过多会带来调度开销,进而影响缓存局部性和整体性能.而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务.这避免了在处理短时间任务时创建与销毁线程的代价.线程池不仅能够保证内核的充分利用,还能防止过分调度 特点:

  • 一种类似JAVA线程池的C++线程池实现方法

    什么是线程池 线程池(thread pool)是一种线程使用模式.线程过多或者频繁创建和销毁线程会带来调度开销,进而影响缓存局部性和整体性能.而线程池维护着多个线程,等待着管理器分配可并发执行的任务.这避免了在处理短时间任务时创建与销毁线程的代价,以及保证了线程的可复用性.线程池不仅能够保证内核的充分利用,还能防止过分调度. 线程池的实现 线程池在JAVA平台上已经有成熟的实现方式,本文介绍参考JAVA线程池实现方式实现的C++线程池类库. 该类库代码已上传至github仓库中,下载地址为:ht

  • java线程池:获取运行线程数并控制线程启动速度的方法

    在java里, 我们可以使用Executors.newFixedThreadPool 来创建线程池, 然后就可以不停的创建新任务,并用线程池来执行了. 在提交任务时,如果线程池已经被占满,任务会进到一个队列里等待执行. 这种机制在一些特定情况下会有些问题.今天我就遇到一种情况:创建线程比线程执行的速度要快的多,而且单个线程占用的内存又多,所以很快内存就爆了. 想了一个办法,就是在提交任务之前,先检查目前正在执行的线程数目,只有没把线程池占满的时候在去提交任务. 代码很简单: int thread

随机推荐