• Java 线程池的原理与实现


     

    JAVA线程池原理以及几种线程池类型介绍

    文章分类:Java编程


        在什么情况下使用线程池?

        1.单个任务处理的时间比较短
        2.将需处理的任务的数量大

        使用线程池的好处:

        1.减少在创建和销毁线程上所花的时间以及系统资源的开销
        2.如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存以及”过度切换”。

    线程池工作原理:http://www.ibm.com/developerworks/cn/java/j-jtp0730/
    该文章里有个例子,简单的描述了线程池的内部实现,建议根据里面的例子来了解JAVA 线程池的原理。同时,里面还详细描述了使用线程池存在的优点和弊端,大家可以研究下,我觉得是篇非常好的文章。

        JDK自带线程池总类介绍介绍:

        1、newFixedThreadPool创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。

        2、newCachedThreadPool创建一个可缓存的线程池。这种类型的线程池特点是:
        1).工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
        2).如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。

        3、newSingleThreadExecutor创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,如果这个线程异常结束,会有另一个取代它,保证顺序执行(我觉得这点是它的特色)。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的 。

        4、newScheduleThreadPool创建一个定长的线程池,而且支持定时的以及周期性的任务执行,类似于Timer。(这种线程池原理暂还没完全了解透彻)

        总结: 一.FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。

            二.CachedThreadPool的特点就是在线程池空闲时,即线程池中没有可运行任务时,它会释放工作线程,从而释放工作线程所占用的资源。但是,但当出现新任务时,又要创建一新的工作线程,又要一定的系统开销。并且,在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。

            三.就是向各位请教一下,请问各位使用过SingleThreadExecutor吗?它一般使用在哪些地方?

        刚研究了一下线程池,以上为个人学习过程以及观点,请勿见笑!下章,将分析一下JDK自带线程池(ThreadPoolExecutor.java)源码,如果感兴趣,可看看。顺便帮忙踩踩,呵呵!

    java线程池原理及简单实例

    建议:在阅读本文前,先理一理同步的知识,特别是syncronized同步关键字的用法。
    关于我对同步的认识,要缘于大三年的一本书,书名好像是 Java 实战,这本书写得实在太妙了,真正的从理论到实践,从截图分析到.class字节码分析。哇,我想市场上很难买到这么精致的书了。作为一个Java爱好者,我觉得绝对值得一读。
    我对此书印象最深之一的就是:equal()方法,由浅入深,经典!
    还有就是同步了,其中提到了我的几个编程误区,以前如何使用同步提高性能等等,通过学习,使我对同步的认识进一步加深。


    简单介绍

        创建线程有两种方式:继承Thread或实现Runnable。Thread实现了Runnable接口,提供了一个空的run()方法,所以不论是继承Thread还是实现Runnable,都要有自己的run()方法。
        一个线程创建后就存在,调用start()方法就开始运行(执行run()方法),调用wait进入等待或调用sleep进入休眠期,顺利运行完毕或休眠被中断或运行过程中出现异常而退出。

    wait和sleep比较:
          sleep方法有:sleep(long millis),sleep(long millis, long nanos),调用sleep方法后,当前线程进入休眠期,暂停执行,但该线程继续拥有监视资源的所有权。到达休眠时间后线程将继续执行,直到完成。若在休眠期另一线程中断该线程,则该线程退出。
          wait方法有:wait(),wait(long timeout),wait(long timeout, long nanos),调用wait方法后,该线程放弃监视资源的所有权进入等待状态;
          wait():等待有其它的线程调用notify()或notifyAll()进入调度状态,与其它线程共同争夺监视。wait()相当于wait(0),wait(0, 0)。
          wait(long timeout):当其它线程调用notify()或notifyAll(),或时间到达timeout亳秒,或有其它某线程中断该线程,则该线程进入调度状态。
          wait(long timeout, long nanos):相当于wait(1000000*timeout + nanos),只不过时间单位为纳秒。



    线程池:
        多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。
        
        假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。
        
        如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。
                    一个线程池包括以下四个基本组成部分:
                    1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
                    2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
                    3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
                    4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。
                    
        线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。它把T1,T3分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有T1,T3的开销了。

        线程池不仅调整T1,T3产生的时间段,而且它还显著减少了创建线程的数目,看一个例子:

        假设一个服务器一天要处理50000个请求,并且每个请求需要一个单独的线程完成。在线程池中,线程数一般是固定的,所以产生线程总数不会超过线程池中线程的数目,而如果服务器不利用线程池来处理这些请求则线程总数为50000。一般线程池大小是远小于50000。所以利用线程池的服务器程序不会为了创建50000而在处理请求时浪费时间,从而提高效率。

    在现代的操作系统中,有一个很重要的概念――线程,几乎所有目前流行的操作系统都支持线程,线程来源于操作系统中进程的概念,进程有自己的虚拟地址空间以及正文段、数据段及堆栈,而且各自占有不同的系统资源(例如文件、环境变量等等)。与此不同,线程不能单独存在,它依附于进程,只能由进程派生。如果一个进程派生出了两个线程,那这两个线程共享此进程的全局变量和代码段,但每个线程各拥有各自的堆栈,因此它们拥有各自的局部变量,线程在UNIX系统中还被进一步分为用户级线程(由进程自已来管理)和系统级线程(由操作系统的调度程序来管理)。

      既然有了进程,为什么还要提出线程的概念呢?因为与创建一个新的进程相比,创建一个线程将会耗费小得多的系统资源,对于一些小型的应用,可能感觉不到这点,但对于那些并发进程数特别多的应用,使用线程会比使用进程获得更好的性能,从而降低操作系统的负担。另外,线程共享创建它的进程的全局变量,因此线程间的通讯编程会更将简单,完全可以抛弃传统的进程间通讯的IPC编程,而采用共享全局变量来进行线程间通讯。

      有了上面这个概念,我们下面就进入正题,来看一下线程池究竟是怎么一回事?其实线程池的原理很简单,类似于操作系统中的缓冲区的概念,它的流程如下:先启动若干数量的线程,并让这些线程都处于睡眠状态,当客户端有一个新请求时,就会唤醒线程池中的某一个睡眠线程,让它来处理客户端的这个请求,当处理完这个请求后,线程又处于睡眠状态。可能你也许会问:为什么要搞得这么麻烦,如果每当客户端有新的请求时,我就创建一个新的线程不就完了?这也许是个不错的方法,因为它能使得你编写代码相对容易一些,但你却忽略了一个重要的问题――性能!就拿我所在的单位来说,我的单位是一个省级数据大集中的银行网络中心,高峰期每秒的客户端请求并发数超过100,如果为每个客户端请求创建一个新线程的话,那耗费的CPU时间和内存将是惊人的,如果采用一个拥有 200个线程的线程池,那将会节约大量的的系统资源,使得更多的CPU时间和内存用来处理实际的商业应用,而不是频繁的线程创建与销毁。

      既然一切都明白了,那我们就开始着手实现一个真正的线程池吧,线程编程可以有多种语言来实现,例如C、C++、java等等,但不同的操作系统提供不同的线程API接口,为了让你能更明白线程池的原理而避免陷入烦琐的API调用之中,我采用了JAVA语言来实现它,由于JAVA语言是一种跨平台的语言,因此你不必为使用不同的操作系统而无法编译运行本程序而苦恼,只要你安装了JDK1.2以上的版本,都能正确地编译运行本程序。另外JAVA语言本身就内置了线程对象,而且JAVA语言是完全面像对象的,因此能够让你更清晰地了解线程池的原理,如果你注意看一下本文的标题,你会发现整个示例程序的代码只有大约100行。

      本示例程序由三个类构成,第一个是TestThreadPool类,它是一个测试程序,用来模拟客户端的请求,当你运行它时,系统首先会显示线程池的初始化信息,然后提示你从键盘上输入字符串,并按下回车键,这时你会发现屏幕上显示信息,告诉你某个线程正在处理你的请求,如果你快速地输入一行行字符串,那么你会发现线程池中不断有线程被唤醒,来处理你的请求,在本例中,我创建了一个拥有10个线程的线程池,如果线程池中没有可用线程了,系统会提示你相应的警告信息,但如果你稍等片刻,那你会发现屏幕上会陆陆续续提示有线程进入了睡眠状态,这时你又可以发送新的请求了。

      第二个类是ThreadPoolManager类,顾名思义,它是一个用于管理线程池的类,它的主要职责是初始化线程池,并为客户端的请求分配不同的线程来进行处理,如果线程池满了,它会对你发出警告信息。

      最后一个类是SimpleThread类,它是Thread类的一个子类,它才真正对客户端的请求进行处理,SimpleThread在示例程序初始化时都处于睡眠状态,但如果它接受到了ThreadPoolManager类发过来的调度信息,则会将自己唤醒,并对请求进行处理。
        首先我们来看一下TestThreadPool类的源码:
     

    package com.thread.simple;

    import java.io.*;

    public class TestThreadPool {

        /**

         * @param args

         */

        public static void main(String[] args) {
            try
            {
                BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
                String s;
                ThreadPoolManager manager = new ThreadPoolManager(10);          

                while((s = br.readLine()) != null)
                {
                  manager.process(s);
                }
            }
            catch(IOException e){
            }
        }
    }

    由于此测试程序用到了输入输入类,因此第1行导入了JAVA的基本IO处理包,在第11行中,我们创建了一个名为manager的类,它给 ThreadPoolManager类的构造函数传递了一个值为10的参数,告诉ThreadPoolManager类:我要一个有10个线程的池,给我创建一个吧!第12行至15行是一个无限循环,它用来等待用户的键入,并将键入的字符串保存在s变量中,并调用ThreadPoolManager类的 process方法来将这个请求进行处理。

      下面我们再进一步跟踪到ThreadPoolManager类中去,以下是它的源代码:
     

    package com.thread.simple;
     
    import java.util.*;
     
    public class ThreadPoolManager {

        private int maxThread;

        public Vector vector;
      
        public void setMaxThread(int threadCount)
        {
            this.maxThread = threadCount;      
        }
      
        public ThreadPoolManager(int threadCount)
        {
            this.setMaxThread(threadCount);
          
            System.out.println("Starting thread pool...");
          
            vector = new Vector();

            for(int i=1;i<=10;i++)
            {
                SimpleThread thread = new SimpleThread(i);

                vector.addElement(thread);

                thread.start();
            }
        }
      
        public void process(String argument)
        {
            int i;
            for(i = 0;i<vector.size();i++)
            {
                SimpleThread currentThread = (SimpleThread)vector.elementAt(i);          

                if(!currentThread.isRunning())
                {
                    System.out.println("Thread "+(i+1)+" is processing:"+argument);
                    currentThread.setArgument(argument);
                    currentThread.setRunning(true);
                    return;
                }
            }      

            if(i == vector.size())
            {
                System.out.println("pool is full,try in another time.");
            }       
        }
    }

    我们先关注一下这个类的构造函数,然后再看它的process()方法。第16-24行是它的构造函数,首先它给ThreadPoolManager类的成员变量maxThread赋值,maxThread表示用于控制线程池中最大线程的数量。第18行初始化一个数组vector,它用来存放所有的 SimpleThread类,这时候就充分体现了JAVA语言的优越性与艺术性:如果你用C语言的话,至少要写100行以上的代码来完成vector的功能,而且C语言数组只能容纳类型统一的基本数据类型,无法容纳对象。好了,闲话少说,第19-24行的循环完成这样一个功能:先创建一个新的 SimpleThread类,然后将它放入vector中去,最后用thread.start()来启动这个线程,为什么要用start()方法来启动线程呢?因为这是JAVA语言中所规定的,如果你不用的话,那这些线程将永远得不到激活,从而导致本示例程序根本无法运行。

      下面我们再来看一下process()方法,第30-40行的循环依次从vector数组中选取SimpleThread线程,并检查它是否处于激活状态(所谓激活状态是指此线程是否正在处理客户端的请求),如果处于激活状态的话,那继续查找vector数组的下一项,如果vector数组中所有的线程都处于激活状态的话,那它会打印出一条信息,提示用户稍候再试。相反如果找到了一个睡眠线程的话,那第35-38行会对此进行处理,它先告诉客户端是哪一个线程来处理这个请求,然后将客户端的请求,即字符串argument转发给SimpleThread类的setArgument()方法进行处理,并调用SimpleThread类的setRunning()方法来唤醒当前线程,来对客户端请求进行处理。

      可能你还对setRunning()方法是怎样唤醒线程的有些不明白,那我们现在就进入最后一个类:SimpleThread类,它的源代码如下:

    package com.thread.simple;

    public class SimpleThread extends Thread {
        private boolean runningFlag;
        private String argument;
       
        public boolean isRunning()
        {
            return runningFlag;
        }
       
        public synchronized void setRunning(boolean flag)
        {
            runningFlag = flag;
           
            if(flag)
                this.notify();
        }
       
        public String getArgument()
        {
            return this.argument;
        }
       
        public void setArgument(String argument)
        {
            this.argument = argument;
        }
       
        public SimpleThread(int threadNumber)
        {
            runningFlag = false;
            System.out.println("Thread "+threadNumber+" started.");
        }
       
        public synchronized void run()
        {
            try
            {
                while(true)
                {
                    if(!runningFlag)
                    {
                        this.wait();
                    }
                    else
                    {
                        System.out.println("processing "+getArgument()+"...done");
                        sleep(5000);
                        System.out.println("Thread is sleeping...");
                        setRunning(false);
                    }
                }
            }
            catch(InterruptedException e)
            {
                System.out.println("Interrupt");
            }
        }//end of run()
       
    }//end of class SimpleThread

      如果你对JAVA的线程编程有些不太明白的话,那我先在这里简单地讲解一下,JAVA有一个名为Thread的类,如果你要创建一个线程,则必须要从 Thread类中继承,并且还要实现Thread类的run()接口,要激活一个线程,必须调用它的start()方法,start()方法会自动调用 run()接口,因此用户必须在run()接口中写入自己的应用处理逻辑。那么我们怎么来控制线程的睡眠与唤醒呢?其实很简单,JAVA语言为所有的对象都内置了wait()和notify()方法,当一个线程调用wait()方法时,则线程进入睡眠状态,就像停在了当前代码上了,也不会继续执行它以下的代码了,当调用notify()方法时,则会从调用wait()方法的那行代码继续执行以下的代码,这个过程有点像编译器中的断点调试的概念。以本程序为例,第38行调用了wait()方法,则这个线程就像凝固了一样停在了38行上了,如果我们在第13行进行一个notify()调用的话,那线程会从第 38行上唤醒,继续从第39行开始执行以下的代码了。

      通过以上的讲述,我们现在就不难理解SimpleThread类了,第9-14行通过设置一个标志runningFlag激活当前线程,第 25-29行是SimpleThread类的构造函数,它用来告诉客户端启动的是第几号进程。第31-50行则是我实现的run()接口,它实际上是一个无限循环,在循环中首先判断一下标志runningFlag,如果没有runningFlag为false的话,那线程处理睡眠状态,否则第42-45行会进行真正的处理:先打印用户键入的字符串,然后睡眠5秒钟,为什么要睡眠5秒钟呢?如果你不加上这句代码的话,由于计算机处理速度远远超过你的键盘输入速度,因此你看到的总是第1号线程来处理你的请求,从而达不到演示效果。最后第45行调用setRunning()方法又将线程置于睡眠状态,等待新请求的到来。

      最后还有一点要注意的是,如果你在一个方法中调用了wait()和notify()函数,那你一定要将此方法置为同步的,即 synchronized,否则在编译时会报错,并得到一个莫名其妙的消息:“current thread not owner”(当前线程不是拥有者)。

      至此为止,我们完整地实现了一个线程池,当然,这个线程池只是简单地将客户端输入的字符串打印到了屏幕上,而没有做任何处理,对于一个真正的企业级运用,本例还是远远不够的,例如错误处理、线程的动态调整、性能优化、临界区的处理、客户端报文的定义等等都是值得考虑的问题,但本文的目的仅仅只是让你了解线程池的概念以及它的简单实现,如果你想成为这方面的高手,本文是远远不够的,你应该参考一些更多的资料来深入地了解它。

    创建Java程序中线程池详解

    线程是Java的一大特性,它可以是给定的指令序列、给定的方法中定义的变量或者一些共享数据(类一级的变量)。在Java中每个线程有自己的堆栈和程序计数器(PC),其中堆栈是用来跟踪线程的上下文(上下文是当线程执行到某处时,当前的局部变量的值),而程序计数器则用来跟踪当前线程正在执行的指令。

    通常情况,一个线程不能访问另外一个线程的堆栈变量,而且这个线程必须处于如下状态之一:

    1.排队状态(Ready),在用户创建了一个线程以后,这个线程不会立即运行。当线程中的方法start()被调用时,这个线程就会进行排队状态,等待调度程序将它转入运行状态(Running)。当一个进程被执行后它也可以进行排队状态。如果调度程序允许的话,通过调用方法yield()就可以将进程放入排队状态。

    2.运行状态(Running),当调度程序将CPU的运行时间分配给一个线程,这个线程就进入了运行状态开始运行。

    3.等待状态(Waiting),很多原因都可以导致线程处于等待状态,例如线程执行过程中被暂停,或者是等待I/O请求的完成而进入等待状态。

    在Java中不同的线程具有不同的优先级,高优先级的线程可以安排在低优先级线程之前完成。如果多个线程具有相同的优先级,Java会在不同的线程之间切换运行。一个应用程序可以通过使用线程中的方法setPriority()来设置线程的优先级,使用方法getPriority()来获得一个线程的优先级。

    线程的生命周期

    一个线程的的生命周期可以分成两阶段:生存(Alive)周期和死亡(Dead)周期,其中生存周期又包括运行状态(Running)和等待状态(Waiting)。当创建一个新线程后,这个线程就进入了排队状态(Ready),当线程中的方法start()被调用时,线程就进入生存周期,这时它的方法isAlive()始终返回真值,直至线程进入死亡状态。

    线程的实现

    有两种方法可以实现线程,一种是扩展java.lang.Thread类,另一种是通过java.lang.Runnable接口。世界经理人电子商务网讯

    Thread类封装了线程的行为。要创建一个线程,必须创建一个从Thread类扩展出的新类。由于在Thread类中方法run()没有提供任何的操作,因此,在创建线程时用户必须覆盖方法run()来完成有用的工作。当线程中的方法start()被调用时,方法run()再被调用。下面的代码就是通过扩展Thread类来实现线程:


    import java.awt.*;
    class Sample1{
    public static void main(String[] args){
    Mythread test1=new Mythread(1);
    Mythread test2=new Mythread(2);
    test1.start();
    test2.start();
    }
    }
    class Mythread extends Thread {
    int id;
    Mythread(int i)
    { id=i;}
    public void run() {
    int i=0;
    while(id+i==1){
    try {sleep(1000);
    } catch(InterruptedException e) {}
    }
    System.out.println(“The id is ”+id);
    }
     

    通常当用户希望一个类能运行在自己的线程中,同时也扩展其它某些类的特性时,就需要借助运行Runnable接口来实现。Runnable接口只有一个方法run()。不论什么时候创建了一个使用Runnable接口的类,都必须在类中编写run()方法来覆盖接口中的run()方法。例如下面的代码就是通过Runnable接口实现的线程:世界经理人电子商务网讯


    import java.awt.*;
    import java.applet.Applet;
    public class Bounce extends Applet implements Runnable{
    static int r=30;
    static int x=100;
    static int y=30;
    Thread t;
    public void init()
    {
    t = new Thread(this);
    t.start();
    }
    public void run()
    {
    int y1=+1;
    int i=1;
    int sleeptime=10;
    while(true)
    {
    y+=(i*y);
    if(y-rgetSize().height)
    y1*=-1;
    try{
    t.sleep(sleeptime);
    }catch(InterruptedException e){ }
    }}
    }
     

    为什么要使用线程池

    在Java中,如果每当一个请求到达就创建一个新线程,开销是相当大的。在实际使用中,每个请求创建新线程的服务器在创建和销毁线程上花费的时间和消耗的系统资源,甚至可能要比花在处理实际的用户请求的时间和资源要多得多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个JVM里创建太多的线程,可能会导致系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,服务器应用程序需要一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务,这就是“池化资源”技术产生的原因。

    线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快。另外,通过适当地调整线程池中的线程数目可以防止出现资源不足的情况。 世界经理人电子商务网讯

    创建一个线程池

    一个比较简单的线程池至少应包含线程池管理器、工作线程、任务队列、任务接口等部分。其中线程池管理器(ThreadPool Manager)的作用是创建、销毁并管理线程池,将工作线程放入线程池中;工作线程是一个可以循环执行任务的线程,在没有任务时进行等待;任务队列的作用是提供一种缓冲机制,将没有处理的任务放在任务队列中;任务接口是每个任务必须实现的接口,主要用来规定任务的入口、任务执行完后的收尾工作、任务的执行状态等,工作线程通过该接口调度任务的执行。下面的代码实现了创建一个线程池,以及从线程池中取出线程的操作:



    //create threads
    synchronized(workThreadVector)
    {
    for(int j = 0; j < i; j )
    {
    threadNum ;
    WorkThread workThread = new WorkThread(taskVector, threadNum);
    workThreadVector.addElement(workThread);
    }}

    线程池适合应用的场合

    当一个Web服务器接受到大量短小线程的请求时,使用线程池技术是非常合适的,它可以大大减少线程的创建和销毁次数,提高服务器的工作效率。但如果线程要求的运行时间比较长,此时线程的运行时间比创建时间要长得多,单靠减少创建时间对系统效率的提高不明显,此时就不适合应用线程池技术,需要借助其它的技术来提高服务器的服务效率。

  • 相关阅读:
    cookie操作和代理
    发起post请求
    scrapy核心组件
    爬取多个url页面数据--手动实现
    scrapy之持久化存储
    selenium + phantomJs
    scrapy框架简介和基础使用
    校验验证码 实现登录验证
    beautifulsoup解析
    xpath
  • 原文地址:https://www.cnblogs.com/rosepotato/p/3424407.html
Copyright © 2020-2023  润新知