线程池的总结

线程池是经经常使用的,所以今天特地将其原理、优点、jdk的实现方式整理出来,以供以后复习之用。

问题:server应用程序中经常出现的情况是:单个任务处理的时间非常短而请求的数目却是巨大的。

假设每一个请求相应一个线程(thread-per-request)方法的不足之中的一个是:为每一个请求创建一个新线程的开销非常大。为每一个请求创建新线程的server在创建和销毁线程上花费的时间和消耗的系统资源要比花在处理实际的用户请求的时间和资源很多其他。


其二是:除了创建和销毁线程的开销之外,活动的线程也消耗系统资源。

在一个 JVM 里创建太多的线程可能会导致系统由于过度消耗内存而用完内存或“切换过度”。

处理方案:使用线程池

理由:线程池为线程生命周期开销问题和资源不足问题提供了解决方式。通过对多个任务重用线程。线程创建的开销被分摊到了多个任务上。

其优点是,由于在请求到达时线程已经存在,所以无意中也消除了线程创建所带来的延迟。这样,就能够马上为请求服务,使应用程序响应更快。并且,通过适当地调整线程池中的线程数目,也就是当请求的数目超过某个阈值时。就强制其他不论什么新到的请求一直等待,直到获得一个线程来处理为止。从而能够防止资源不足。(简单概括就是:复用,降低开销。防止资源不足)。

这是李刚的疯狂讲义对线程池的叙述:



可是又引出了新的问题:比如1、在池为空时,会发生什么呢?试图向池线程传递任务的调用者都会发现池为空。在调用者等待一个可用的池线程时。它的线程将堵塞 2、若无工作进行处理时,线程池里的线程会如何?

设计:一组固定的工作线程相结合的工作队列,它使用 wait() 和 notify() 来通知等待线程新的工作已经到达了。该工作队列通常被实现成具有相关监视器对象的某种链表。

详细线程池的实现代码,例如以下:

public class WorkQueue
{
    private final int nThreads;
    private final PoolWorker[] threads;
    private final LinkedList queue;
    public WorkQueue(int nThreads)
    {
        this.nThreads = nThreads;
        queue = new LinkedList();
        threads = new PoolWorker[nThreads];
        for (int i=0; i<nThreads; i++) {
            threads[i] = new PoolWorker();
            threads[i].start();
        }
    }
    public void execute(Runnable r) {
        synchronized(queue) {
            queue.addLast(r);
            queue.notify();
        }
    }
    private class PoolWorker extends Thread {
        public void run() {
            Runnable r;
            while (true) {
                synchronized(queue) {
                    while (queue.isEmpty()) {
                        try
                        {
                            queue.wait();
                        }
                        catch (InterruptedException ignored)
                        {
                        }
                    }
                    r = (Runnable) queue.removeFirst();
                }
                // If we don't catch RuntimeException, 
                // the pool could leak threads
                try {
                    r.run();
                }
                catch (RuntimeException e) {
                    // You might want to log something here
                }
            }
        }
    }
}

使用线程池的风险:

1、死锁
2、资源不足
以下重点说一下:
3、并发错误:
线程池和其他排队机制依靠使用 wait() 和 notify() 方法,这两个方法都难于使用。假设编码不对。那么可能丢失通知,导致线程保持空暇状态,虽然队列中有工作要处理。
4、线程泄漏:
各种类型的线程池中一个严重的风险是线程泄漏,当从池中除去一个线程以运行一项任务,而在任务完毕后该线程却没有返回池时。会发生这样的情况。发生线程泄漏的一种情形出如今任务抛出一个 RuntimeException 或一个 Error 时。

假设池类没有捕捉到它们,那么线程仅仅会退出而线程池的大小将会永久降低一个。当这样的情况发生的次数足够多时。线程池终于就为空。并且系统将停止。由于没有可用的线程来处理任务。


理解了线程池的原理,以下介绍几种经常使用的线程池:

这三个方法返回一个ExecutorService对象,该对象代表一个线程池。它能够运行Runnable对象或Callable对象所代表的线程。
使用线程池运行任务的过程例如以下:
a、调用Executors类的静态工厂方法创建一个ExecutorService对象。该对象代表一个线程池
b、创建Runnable实现类。作为要运行的任务
c、调用ExecutorService对象的submit方法来提交b的实例
d、调用ExecutorService对象的shutdown方法关闭线程池

代码实现:

写到这里我又想到以前自己写过的代码,那时我是直接调用的Execute函数。那么submit和execute又有什么差别呢???我以前写的代码:

以下说一下二者的稍有不同之处是,execute是Executor接口的方法,而这个接口仅仅有一个方法,返回值是void:

 然后ExecutorService接口继承了Executor接口。并声明了自己的一些方法:submit、invokeAll、invokeAny以及shutDown等;
当中submit()方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了详细的实现,在ThreadPoolExecutor中并没有对其进行重写,这种方法也是用来向线程池提交任务的。可是它和execute()方法不同。它能够返回任务运行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法。仅仅只是它利用了Future来获取任务运行结果。

简单列一下结构关系:















写到这里,发现线程池的东西越写越复杂,同一时候也參考了网友的一些理解。以及李刚讲义的一些内容,欢迎大家一起交流指正。

posted @ 2018-02-28 13:55  zhchoutai  阅读(220)  评论(0编辑  收藏  举报