线程池原理详解与Java代码示例

来源:互联网 发布:林忆莲歌词知乎 编辑:程序博客网 时间:2024/05/21 13:22

为什么使用线程池

对于服务端的程序,经常面对的是客户端传入的短小(执行时间短、工作内容较为单一)任务,需要服务端快速处理并返回结果。如果服务端每次接受到一个任务,创建一个线程,然后进行执行,这在原型阶段是个不错的选择,但是面对成千上万的任务递交进服务器时,如果还是采用一个任务一个线程的方式,那么将会创建数以万记的线程,这不是一个好的选择。因为这会使操作系统频繁的进行线程上下文切换,无故增加系统的负载,而线程的创建和消亡都是需要耗费系统资源的,也无疑浪费了系统资源。线程池就很好的解决了这个问题。

线程池的工作原理

线程池是系统启动时预先创建一定数量的线程。线程池在没有任务请求的时候,创建一定数量的线程放到空闲队列中。这些线程都是出于睡眠状态,都是启动的,不消耗CPU,只是占用较小空间的内存。当请求到来之后,缓冲池给这次请求分配一个空闲的线程,把请求传入该线程中运行,进行处理。当预先创建的线程都处于运行状态,即预制线程不够,线程池可以自有创建一定数量的新线程,用于处理更多的请求。当系统比较空间的时候,也可以通过移除一部分处于停用的线程。
这样做的好处是,一方面,消除了频繁创建和消亡线程的系统资源开销,另一方面,面对过量任务的提交能够平缓的劣化。

代码示例

public interface ThreadPool<Job extends Runnable> {    // 执行一个Job,这个Job需要实现Runnable    void execute(Job job);    // 关闭线程池    void shutdown();    // 增加工作者线程    void addWorkers(int num);    // 减少工作者线程    void removeWorker(int num);    // 得到正在等待执行的任务数量    int getJobSize();}

客户端可以通过execute(Job)方法将Job提交入线程池执行,而客户端自身不用等待Job的执行完成。除了execute(Job)方法以外,线程池接口提供了增大/减少工作者线程以及关闭线程池的方法。这里工作者线程代表着一个重复执行Job的线程,而每个由客户端提交的Job都将进入到一个工作队列中等待工作者线程的处理。

package com.thread;import java.util.ArrayList;import java.util.Collections;import java.util.LinkedList;import java.util.List;import java.util.concurrent.atomic.AtomicLong;public class DefaultThreadPool<Job extends Runnable> implements ThreadPool<Job>{    // 线程池最大限制数    private static final int MAX_WORKER_NUMBERS = 10;    // 线程池默认的数量    private static final int DEFAULT_WORKER_NUMBERS = 5;    // 线程池最小的数量    private static final int MIN_WORKER_NUMBERS = 1;    // 这是一个工作列表,将会向里面插入工作    private final LinkedList<Job> jobs = new LinkedList<Job>();    // 工作者列表    private final List<Worker> workers = Collections.synchronizedList(new ArrayList<Worker>());    // 工作者线程的数量    private int workerNum = DEFAULT_WORKER_NUMBERS;    // 线程编号生成    private AtomicLong threadNum = new AtomicLong();    public DefaultThreadPool(){        initializeWokers(DEFAULT_WORKER_NUMBERS);    }    public DefaultThreadPool(int num){        workerNum = num > MAX_WORKER_NUMBERS ? MAX_WORKER_NUMBERS:num<MIN_WORKER_NUMBERS?MIN_WORKER_NUMBERS:num;        initializeWokers(workerNum);    }    /**     * 初始化一定数量的线程     * @param defaultWorkerNumbers     */    private void initializeWokers(int defaultWorkerNumbers) {        for(int i=0;i<defaultWorkerNumbers;i++){            Worker worker = new Worker();            workers.add(worker);            Thread thread = new Thread(worker,"ThreadPool-Worker-"+threadNum.incrementAndGet());            thread.start();        }    }    @Override    public void execute(Job job) {        if(job != null){            jobs.addLast(job);            jobs.notify();        }    }    @Override    public void shutdown() {        for(Worker worker:workers){            worker.shutdown();        }    }    @Override    public void addWorkers(int num) {        synchronized (jobs) {            // 限制新增的Worker数量不能超过最大值            if(num + this.workerNum > MAX_WORKER_NUMBERS){                num = MAX_WORKER_NUMBERS - this.workerNum;            }            initializeWokers(num);            this.workerNum+=num;        }    }    @Override    public void removeWorker(int num) {        synchronized (jobs) {            if(num > this.workerNum){                throw new IllegalArgumentException("beyond workNum");            }            int count = 0;            while(count < num){                Worker worker = workers.get(count);                if(workers.remove(worker)){                    worker.shutdown();                    count++;                }            }            this.workerNum -= count;        }    }    @Override    public int getJobSize() {        return jobs == null?0:jobs.size();    }    class Worker implements Runnable{        // 是否在工作        private volatile boolean running = true;        @Override        public void run() {            while(running){                Job job = null;                synchronized (jobs) {                    // 如果工作者列表是空,就wait                    while(jobs.isEmpty()){                        try {                            jobs.wait();                        } catch (InterruptedException e) {                            // 感知到外部对WorkerThread的中断,返回                            Thread.currentThread().interrupt();                            return;                        }                    }                    // 取出一个job                    job = jobs.removeFirst();                }                if(job != null){                    try {                        job.run();                    } catch (Exception e) {                        e.printStackTrace();                    }                }            }        }        public void shutdown() {            running = false;        }    }}

从线程池的实现可以看到,当客户端调用execute(Job)方法时,会不断地向任务列表jobs中添加Job,而每个工作者线程会不断地从jobs上取出一个Job进行执行,当jobs为空时,工作者线
程进入等待状态。添加一个Job后,对工作队列jobs调用了其notify()方法,而不是notifyAll()方法,因为能够确定有工作者线程被唤醒,这时使用notify()方法将会比notifyAll()方法获得更小的开销(避免将等待队列中的线程全部移动到阻塞队列中)。可以看到,线程池的本质就是使用了一个线程安全的工作队列连接工作者线程和客户端线程,客户端线程将任务放入工作队列后便返回,而工作者线程则不断地从工作队列上取出工作并执行。当工作队列为空时,所有的工作者线程均等待在工作队列上,当有客户端提交了一个任务之后会通知任意一个工作者线程,随着大量的任务被提交,更多的工作者线程会被唤醒。

线程池使用的风险

用线程池构建的应用程序容易遭受任何其它多线程应用程序容易遭受的所有并发风险,诸如同步错误和死锁,它还容易遭受特定于线程池的少数其它风险,诸如与池有关的死锁、资源不足和线程泄漏。

死锁

任何多线程应用程序都有死锁风险。当一组进程或线程中的每一个都在等待一个只有该组中另一个进程才能引起的事件时,我们就说这组进程或线程 死锁了。死锁的最简单情形是:线程 A 持有对象 X 的独占锁,并且在等待对象 Y 的锁,而线程 B 持有对象 Y 的独占锁,却在等待对象 X 的锁。除非有某种方法来打破对锁的等待(Java 锁定不支持这种方法),否则死锁的线程将永远等下去。虽然任何多线程程序中都有死锁的风险,但线程池却引入了另一种死锁可能,在那种情况下,所有池线程都在执行已阻塞的等待队列中另一任务的执行结果的任务,但这一任务却因为没有未被占用的线程而不能运行。当线程池被用来实现涉及许多交互对象的模拟,被模拟的对象可以相互发送查询,这些查询接下来作为排队的任务执行,查询对象又同步等待着响应时,会发生这种情况。

资源不足

线程池的一个优点在于:相对于其它替代调度机制(有些我们已经讨论过)而言,它们通常执行得很好。但只有恰当地调整了线程池大小时才是这样的。线程消耗包括内存和其它系统资源在内的大量资源。除了Thread对象所需的内存之外,每个线程都需要两个可能很大的执行调用堆栈。除此以外,JVM 可能会为每个 Java 线程创建一个本机线程,这些本机线程将消耗额外的系统资源。最后,虽然线程之间切换的调度开销很小,但如果有很多线程,环境切换也可能严重地影响程序的性能。
如果线程池太大,那么被那些线程消耗的资源可能严重地影响系统性能。在线程之间进行切换将会浪费时间,而且使用超出比您实际需要的线程可能会引起资源匮乏问题,因为池线程正在消耗一些资源,而这些资源可能会被其它任务更有效地利用。除了线程自身所使用的资源以外,服务请求时所做的工作可能需要其它资源,例如 JDBC 连接、套接字或文件。这些也都是有限资源,有太多的并发请求也可能引起失效,例如不能分配 JDBC 连接。

并发错误

线程池和其它排队机制依靠使用 wait() 和 notify() 方法,这两个方法都难于使用。如果编码不正确,那么可能丢失通知,导致线程保持空闲状态,尽管队列中有工作要处理。使用这些方法时,必须格外小心;即便是专家也可能在它们上面出错。而最好使用现有的、已经知道能工作的实现,例如util.concurrent 包。

线程泄露

各种类型的线程池中一个严重的风险是线程泄漏,当从池中除去一个线程以执行一项任务,而在任务完成后该线程却没有返回池时,会发生这种情况。发生线程泄漏的一种情形出现在任务抛出一个 RuntimeException 或一个 Error 时。如果池类没有捕捉到它们,那么线程只会退出而线程池的大小将会永久减少一个。当这种情况发生的次数足够多时,线程池最终就为空,而且系统将停止,因为没有可用的线程来处理任务。
有些任务可能会永远等待某些资源或来自用户的输入,而这些资源又不能保证变得可用,用户可能也已经回家了,诸如此类的任务会永久停止,而这些停止的任务也会引起和线程泄漏同样的问题。如果某个线程被这样一个任务永久地消耗着,那么它实际上就被从池除去了。对于这样的任务,应该要么只给予它们自己的线程,要么只让它们等待有限的时间。

请求过载

仅仅是请求就压垮了服务器,这种情况是可能的。在这种情形下,我们可能不想将每个到来的请求都排队到我们的工作队列,因为排在队列中等待执行的任务可能会消耗太多的系统资源并引起资源缺乏。在这种情形下决定如何做取决于您自己;在某些情况下,您可以简单地抛弃请求,依靠更高级别的协议稍后重试请求,您也可以用一个指出服务器暂时很忙的响应来拒绝请求。

线程池使用准则

1、不要对那些同步等待其它任务结果的任务排队。这可能会导致上面所描述的那种形式的死锁,在那种死锁中,所有线程都被一些任务所占用,这些任务依次等待排队任务的结果,而这些任务又无法执行,因为所有的线程都很忙。
2、在时间可能很长的操作使用合用的线程时要小心。如果程序必须等待诸如 I/O 完成这样的某个资源,那么请指定最长的等待时间,以及随后是失效还是将任务重新排队以便稍后执行。这样做保证了:通过将某个线程释放给某个可能成功完成的任务,从而将最终取得某些进展
3、理解任务。要有效地调整线程池大小,您需要理解正在排队的任务以及它们正在做什么。它们是 CPU 限制的(CPU-bound)吗?它们是 I/O 限制的(I/O-bound)吗?您的答案将影响您如何调整应用程序。如果您有不同的任务类,这些类有着截然不同的特征,那么为不同任务类设置多个工作队列可能会有意义,这样可以相应地调整每个池。

线程池的使用场景

1、多个定时任务(任务之间没有顺序依赖关系)
2、并发测试
3、记录日志(前提是SSD,如果HDD只有一个磁头,写磁盘是性能瓶颈)
后面的文章我会针对几种常见的线程池逐一说明

原创粉丝点击