socket链接---多线程 线程池---TCP/IP半包、粘包、分包

 PS:  socket是TCP/IP的封装,URL类(java中)是对Socket 的封装。

 一  ServerSocket工做原理:

                     客户端socket请求链接Serversocket的请求链接,按照请求顺序进入客户端链接请求队列(队列的容量是由操做系统完成的),ServerSocket的构造函数中的backlog就是用来指定请求队列的长度。 这个值会失效的三种状况:大于操做系统默认值|小于等于0|没有设置。 (见下面)html

                 serversocket中的accept方法,会从客户端链接请求队列(先进先出)中取出一个请求链接的请求,生成一个用于通讯的socket。只有当serversocket的accept方法成功返回时,才代表客户端与服务端创建了链接。java


  socket链接是Java中进行通讯的基本方式,也是效率最高的方式,虽然他有http等让是进行http请求,可是若是是进行tcp、下载等通讯,仍是使用socket更好。Java中封装了很是完美的socket机制,使用也很是简单。主要包括socket和serversocket。算法

  socket的使用很是简单,主要包括的构造方法有:socket(),socket(string host,string port),socket(Inetaddress address,int port)等,很是明白了,经过传入host和port进行socket的请求,当在建立相应的套接字实例的时候,会自动去对相应的ip和port进行链接,只有当链接成功,才表示相应的套接字创建成功,才能够进行相应的I/O操做。经过getOutputStream和getInputStream获取相应的输入输出流,进行相应的I/O操做,可是有一个是比较特别的,getChannel用来获取SocketChannel,他之因此特别是由于他属于java.nio.channels下面的类,其继承于java.nio.channels.SelectableChannel,就是说在进行nio非阻塞式的请求链接时,他很是有用,具体参见http://www.cnblogs.com/likwo/archive/2010/06/29/1767814.html。可能有些人会问,对于能够经过设置服务器链接的timeout来防止过多的阻塞,可是若是对于超过timeout,socket通常是抛出超时异常,这样就算对异常进行了处理,也将会重新创建socket链接,浪费消费 重建的资源。例如QQ聊天,当你打开一个聊天面板,好久不说话的时候,并不会自动为你断开socket链接,而是一直处于阻塞状态,直到你发送了新的信息,再进行处理,所以nio的阻塞方式会更好些。shell

  对于serversocket也比较简单,经常使用的只有四个构造函数:数据库

l  ServerSocket()throws IOException 
l  ServerSocket(int port) throws IOException 
ServerSocket(int port, int backlog) throws IOException 
l  ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException  编程

分别对这几个构造进行简单的解释:windows

第一个无参构造,只是建立一个serversocket实例,可是不进行任何端口的监听,你还必须经过bind方法进行端口的绑定,好处就是在绑定以前能够进行相应属性的设置,例如so_reuseaddress等;设计模式

第二个构造函数须要一个端口(1024-65535),通常不使用0-1023之间的,这个属于系统占用的预留端口,可是若是你传入的端口号为0,则会默认使用匿名端口,就是系统随机分配一个端口,进行暂时通讯,这个匿名方式,通常状况下不使用;浏览器

第三个构造函数须要一个端口,和一个backlog(监听对列大小),serversocket进行某端口的监听,当有多个链接请求是,每一个请求默认都会放入一个请求队列里,若是你没有设置这个值,则默认为操做系统的值,根据不一样的系统有所不一样,例如40等,有几种状况,这个值将会失效:大于操做系统默认值|小于等于0|没有设置。若是设置了,而没有及时对队列进行处理,则会报ConnectException异常;缓存

第四个构造函数除了具备端口、队列大小外,还具备一个参数是ip地址,就是进行相应ip地址的绑定。固然,这个进行的是服务器ip地址的绑定,不会限制客户端的ip访问。当一台服务器存在多个网卡的时候,就须要经过这个参数来设置客户端访问的ip。

服务器socket的关闭,经过使用close进行关闭,使用isclosed进行判断,还能够进行端口的绑定判断。

对于服务器close方法,须要有一点进行说明:调用close方法以后,操做系统并不会当即进行端口的释放,依旧会对旧端口占用一段时间,以防止客户端发送的数据有延迟现象。所以有时候,就算你进行了close方法的调用,进行了端口的释放,可是若是你当即进行同一个端口的链接时,依旧会包端口占用异常,这个是能够理解的。

serversocket经过使用accept方法进行客户端请求的处理,每当请求队列里有客户端请求时,serversocket就会从队列顶端取一个socket请求进行处理,生成一个socket来负责与客户端通讯。若是一个时间只能处理一个socket,当有多个客户端请求时,则必需要排队处理,等待全部前面的socket处理完,这是个极其痛苦,而且不合理的过程。所以,引入了线程的概念。

 二  socket与多线程 

            为了实现客户端请求的快速相应和快速处理,据是高并发,则必须使用多线程机制。主题思想是:serversocket经过accept创建一个socket,而后起一个线程,把这个socket扔给新建的线程进行处理,而serversocket所在的主线程,则继续去监听端口,以此实现多线程通讯。通常有三种方式:

  一、每个socket请求就创建一个线程。这个是最简单的方式,大体代码以下:

  • public void service() {   
  •   while (true) {   
  •     Socket socket=null;   
  •     try {   
  •       socket = serverSocket.accept();                                            //接收客户链接   
  •       Thread workThread=new Thread(new Handler(socket));            //建立一个工做线程   
  •       workThread.start();                                                            //启动工做线程   
  •     }catch (IOException e) {   
  •        e.printStackTrace();   
  •     }   
  •   }   

  上面的方式很是简单,可以处理基本的多线程问题,当数据量不大时,应该没有什么问题,可是若是数据量过大时,就会出现严重的性能,甚至是宕机问题。其缺点主要有以下几个:

  a:每一个socket请求,创建一个链接,当每一个都是进行简短的通讯时,则异常的耗费系统创建、销毁线程资源。

  b:若是创建线程太多,每一个线程都会占用必定的系统内存,这样将致使内存溢出。

  c:频繁地对线程进行创建 销毁,会致使操做系统进行频繁的cpu切换线程切换,这样也会很是耗费系统资源。

二、本身实现线程池。

  本身写线程池,可以对线程池的工做原理以及工做状况,更加的了解和控制,可是因为线程池必然涉及到多线程问题,所以为了防止出现死锁、线程泄漏、并发错误、任务过载等问题,须要性能很是好的机制,通常不推荐我的现实。若是非要实现,能够经过使用linkedlist<runnable>的数据结构来实现一个多线程队列。下面,仍是主要推荐jdk已经帮你实现的线程池。

三、使用jdk自带的线程池。jar包是:java.util.concurrent

  这个jar包都是一些并发编程会常用到的工具类,主要有阻塞队列,原子操做的map以及线程池等。其基本包括Executor、ExecutorService接口和Executors类,两个接口定义了执行线程的方法,而Executors则定义了管理线程池的方法,主要能够建立的经常使用线程池有:

newSingleThreadScheduledExecutor() 建立一个能够延迟执行和定时执行的单线程线程池

newSingleThreadExecutor() 建立一个运行单线程的线程池               new LinkedBlockingQueue<Runnable>()) --任务队列

newScheduledThreadPool(int corePoolSize)  建立一个能够延迟执行和定时执行的线程池,设定线程数,经常使用来代替Timer(定时器)

newFixedThreadPool(int nThreads)  建立一个固定线程数的线程池    new LinkedBlockingQueue<Runnable>()) --任务队列

newCachedThreadPool() 建立一个带有缓冲区的线程池                             new SynchronousQueue<Runnable>()

而后经过生成的线程池的execute方法进行线程的执行,具体能够百度下哈哈

使用线程池有如下几点风险:

一、死锁。任何多线程都不可避免的问题。可是对于线程池可能会存在另外一种死锁:就是线程池中的线程都在等待一个资源,而这个资源须要执行A后获得,而因为线程池没有可用的线程,致使A没法执行,故而也会发生死锁。

二、系统资源不足。多线程一定须要大量的内存资源,可能出现内存泄漏问题。

三、并发错误。

四、线程泄漏。就是全部的线程池中的线程都在等待输入资源,或者都抛出了异常而没有捕获,则会致使线程池中全部的线程假死。

五、线程过载。运行线程过多,致使过载,这个能够经过设置线程池的大小来进行必定成功的避免。

至于如何避免,主要是要在使用多线程时要当心,同时不要使用destroy despause 等操做,尽可能使用sleep notify wait等操做,在这里不详细说明了。

二 补充:JDK1.5中线程池的简单使用(java.util.concurrent.ThreadPoolExecutor)    

在多线程大师Doug Lea的贡献下,在JDK1.5中加入了许多对并发特性的支持,例如:线程池。这里介绍的就是1.5种的线程池的简单使用方法。

1、简介 

线程池类为 java.util.concurrent.ThreadPoolExecutor,经常使用构造方法为:

[java]  view plain copy
  1. ThreadPoolExecutor(int corePoolSize,   
  2.                    int maximumPoolSize,   
  3.                    long keepAliveTime, TimeUnit unit,   
  4.                    BlockingQueue<Runnable> workQueue,   
  5.                    RejectedExecutionHandler handler)   

  • corePoolSize
    线程池维护线程的最少数量
  • maximumPoolSiz
    线程池维护线程的最大数量
  • keepAliveTime
    线程池维护线程所容许的空闲时间
  • unit
    线程池维护线程所容许的空闲时间的单位
  • workQueue
    线程池所使用的缓冲队列
  • handler
    线程池对拒绝任务的处理策略

一个任务经过 execute(Runnable)方法被添加到线程池,任务就是一个 Runnable类型的对象,任务的执行方法就是 Runnable类型对象的run()方法。 

当一个任务经过execute(Runnable)方法欲添加到线程池时:

  • 若是此时线程池中的数量小于corePoolSize,即便线程池中的线程都处于空闲状态,也要建立新的线程来处理被添加的任务。
  • 若是此时线程池中的数量等于 corePoolSize,可是缓冲队列 workQueue未满,那么任务被放入缓冲队列。
  • 若是此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,而且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
  • 若是此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,而且线程池中的数量等于maximumPoolSize,那么经过 handler所指定的策略来处理此任务。

也就是:处理任务的优先级为: 
核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,若是三者都满了,使用handler处理被拒绝
的任务。 

当线程池中的线程数量大于 corePoolSize时,若是某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池能够动态的调整池中的线程数。 

unit可选的参数为java.util.concurrent.TimeUnit中的几个静态属性: 
NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS。 

workQueue我经常使用的是:java.util.concurrent.ArrayBlockingQueue 

handler有四个选择:

  • ThreadPoolExecutor.AbortPolicy() 
    抛出java.util.concurrent.RejectedExecutionException异常
  • ThreadPoolExecutor.CallerRunsPolicy() 
    由调用者执行这个任务
  • ThreadPoolExecutor.DiscardOldestPolicy() 
    抛弃旧的任务
  • ThreadPoolExecutor.DiscardPolicy() 
    抛弃当前的任务

 

2、通常用法举例

[java]  view plain copy
  1. package cn.simplelife.exercise;  
  2. import java.util.concurrent.ArrayBlockingQueue;  
  3. import java.util.concurrent.ThreadPoolExecutor;  
  4. import java.util.concurrent.TimeUnit;  
  5. public class TestThreadPool {  
  6.     private static int produceTaskSleepTime = 2;  
  7.     public static void main(String[] args) {  
  8.         //构造一个线程池  
  9.         ThreadPoolExecutor producerPool = new ThreadPoolExecutor(240, TimeUnit.SECONDS, new ArrayBlockingQueue(3), new ThreadPoolExecutor.DiscardOldestPolicy());  
  10.         //每隔produceTaskSleepTime的时间向线程池派送一个任务。  
  11.         int i=1;  
  12.         while(true){  
  13.             try {  
  14.                 Thread.sleep(produceTaskSleepTime);  
  15.                 String task = "task@ " + i;  
  16.                 System.out.println("put " + task);  
  17.                 producerPool.execute(new ThreadPoolTask(task));  
  18.                 i++;  
  19.             } catch (Exception e) {  
  20.                 e.printStackTrace();  
  21.             }  
  22.         }  
  23.     }  
  24. }     

[java]  view plain copy
  1. package cn.simplelife.exercise;  
  2. import java.io.Serializable;  
  3. /** 
  4.  * 线程池执行的任务 
  5.  * @author hdpan 
  6.  */  
  7. public class ThreadPoolTask implements Runnable,Serializable{  
  8.     //JDK1.5中,每一个实现Serializable接口的类都推荐声明这样的一个ID  
  9.     private static final long serialVersionUID = 0;  
  10.     private static int consumeTaskSleepTime = 2000;  
  11.     private Object threadPoolTaskData;  
  12.     ThreadPoolTask(Object tasks){  
  13.         this.threadPoolTaskData = tasks;  
  14.     }  
  15.     //每一个任务的执行过程,如今是什么都没作,除了print和sleep,:)  
  16.     public void run(){  
  17.         System.out.println("start .."+threadPoolTaskData);  
  18.         try {  
  19.             //便于观察现象,等待一段时间  
  20.             Thread.sleep(consumeTaskSleepTime);  
  21.         } catch (Exception e) {  
  22.             e.printStackTrace();  
  23.         }  
  24.         threadPoolTaskData = null;  
  25.     }  
  26. }           

 

 

 对这两段程序的说明:

  1. 在这段程序中,一个任务就是一个Runnable类型的对象,也就是一个ThreadPoolTask类型的对象。
  2. 通常来讲任务除了处理方式外,还须要处理的数据,处理的数据经过构造方法传给任务。
  3. 在这段程序中,main()方法至关于一个残忍的领导,他派发出许多任务,丢给一个叫 threadPool的不辞辛苦的小组来作。
    • 这个小组里面队员至少有两个,若是他们两个忙不过来, 任务就被放到任务列表里面。
    • 若是积压的任务过多,多到任务列表都装不下(超过3个)的时候,就雇佣新的队员来帮忙。可是基于成本的考虑,不能雇佣太多的队员, 至多只能雇佣 4个。
    • 若是四个队员都在忙时,再有新的任务, 这个小组就处理不了了,任务就会被经过一种策略来处理,咱们的处理方式是不停的派发, 直到接受这个任务为止(更残忍!呵呵)。
    • 由于队员工做是须要成本的,若是工做很闲,闲到 3SECONDS都没有新的任务了,那么有的队员就会被解雇了,可是,为了小组的正常运转,即便工做再闲,小组的队员也不能少于两个。
  4. 经过调整 produceTaskSleepTime和 consumeTaskSleepTime的大小来实现对派发任务和处理任务的速度的控制, 改变这两个值就能够观察不一样速率下程序的工做状况。
  5. 经过调整4中所指的数据,再加上调整任务丢弃策略, 换上其余三种策略,就能够看出不一样策略下的不一样处理方式。
  6. 对于其余的使用方法,参看jdk的帮助,很容易理解和使用。

TCP/IP,http,socket,长链接,短链接

TCP/IP是个协议组,可分为三个层次:网络层、传输层和应用层。 

   在网络层有: IP协议、ICMP协议、ARP协议、RARP协议和BOOTP协议。 
   在传输层中有:TCP协议与UDP协议。 
   在应用层有:   TCP包括FTP、HTTP、TELNET、SMTP等协议 
                      UDP包括DNS(域名系统协议)、SNMP(简单网络管理协议)、TFTP(小型文件传输协议/简单文件传输协议)等协议 

发送接收方式 

一、异步  
报文发送和接收是分开的,相互独立的,互不影响。这种方式又分两种状况: 
(1)异步双工:接收和发送在同一个程序中,由两个不一样的子进程分别负责发送和接收 
(2)异步单工:接收和发送是用两个不一样的程序来完成。 
二、同步  
报文发送和接收是同步进行,既报文发送后等待接收返回报文。 同步方式通常须要考虑超时问题,即报文发出去后不能无限等待,须要设定超时时间,超过该时间发送方再也不等待读返回报文,直接通知超时返回。   

Socket是什么 

     Socket是应用层与TCP/IP协议族通讯的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来讲,一组简单的接口就是所有,让Socket去组织数据,以符合指定的协议。

Socket 通讯示例


主机 A 的应用程序要能和主机 B 的应用程序通讯,必须经过 Socket 创建链接,而创建 Socket 链接必须须要底层 TCP/IP 协议来创建 TCP 链接。创建 TCP 链接须要底层 IP 协议来寻址网络中的主机。咱们知道网络层使用的 IP 协议能够帮助咱们根据 IP 地址来找到目标主机,可是一台主机上可能运行着多个应用程序,如何才能与指定的应用程序通讯就要经过 TCP 或 UPD 的地址也就是端口号来指定。这样就能够经过一个 Socket 实例惟一表明一个主机上的一个应用程序的通讯链路了。

socket的半包,粘包与分包的问题

      首先看两个概念:  短链接--长链接

短链接:  

链接->传输数据->关闭链接  
HTTP是无状态的,浏览器和服务器每进行一次HTTP操做,就创建一次链接,但任务结束就中断链接。  
也能够这样说:短链接是指SOCKET链接后发送后接收完数据后立刻断开链接。  

长链接:  

链接->传输数据->保持链接 -> 传输数据-> 。。。 ->关闭链接。  
长链接指创建SOCKET链接后无论是否使用都保持链接,但安全性较差。  

http的长链接 

HTTP也能够创建长链接的,使用Connection:keep-alive,HTTP 1.1默认进行持久链接(长链接)。HTTP1.1和HTTP1.0相比较而言,最大的区别就是增长了持久链接支持(貌似最新的 http1.0 能够显示的指定 keep-alive),但仍是无状态的,或者说是不能够信任的。 
之因此出现粘包和半包现象,是由于TCP当中,只有流的概念,没有包的概念.  
  
    在长链接中通常是没有条件可以判断读写何时结束,因此必需要加长度报文头。读函数先是读取报文头的长度,再根据这个长度去读相应长度的报文。

何时用长链接,短链接? 

     长链接多用于操做频繁,点对点的通信,并且链接数不能太多状况,。每一个TCP链接都须要三步握手,这须要时间,若是每一个操做都是先链接,再操做的话那么处理速度会下降不少,因此每一个操做完后都不断开,次处理时直接发送数据包就OK了,不用创建TCP链接。例如:数据库的链接用长链接, 若是用短链接频繁的通讯会形成socket错误,并且频繁的socket 建立也是对资源的浪费。 
  
      而像WEB网站的http服务通常都用短连接,由于长链接对于服务端来讲会耗费必定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的链接用短链接会更省一些资源,若是用长链接,并且同时有成千上万的用户,若是每一个用户都占用一个链接的话,那可想而知吧。因此并发量大,但每一个用户无需频繁操做状况下需用短连好。 
  
总之,长链接和短链接的选择要视状况而定。 

半包  

指接受方没有接受到一个完整的包,只接受了部分,这种状况主要是因为TCP为提升传输效率,将一个包分配的足够大,致使接受方并不能一次接受完。( 在长链接和短链接中都会出现)。  

粘包与分包  

指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。出现粘包现象的缘由是多方面的,它既可能由发送方形成,也可能由接收方形成。发送方引发的粘包是由TCP协议自己形成的,TCP为提升传输效率,发送方每每要收集到足够多的数据后才发送一包数据。若连续几回发送的数据都不多,一般TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。接收方引发的粘包是因为接收方用户进程不及时接收数据,从而致使粘包现象。这是由于接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据还没有被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据以后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。分包是指在出现粘包的时候咱们的接收方要进行分包处理。(在长链接中都会出现)  

何时须要考虑半包的状况?  

从备注中咱们了解到Socket内部默认的收发缓冲区大小大概是8K,可是咱们在实际中每每须要考虑效率问题,从新配置了这个值,来达到系统的最佳状态。  
一个实际中的例子:用mina做为服务器端,使用的缓存大小为10k,这里使用的是短链接,全部不用考虑粘包的问题。  
问题描述:在并发量比较大的状况下,就会出现一次接受并不能完整的获取全部的数据。  
处理方式:  
1.经过包头+包长+包体的协议形式,当服务器端获取到指定的包长时才说明获取完整。  
2.指定包的结束标识,这样当咱们获取到指定的标识时,说明包获取完整。  

何时须要考虑粘包的状况?  
1.当时短链接的状况下,不用考虑粘包的状况  
2.若是发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包  
3.若是双方创建链接,须要在链接后一段时间内发送不一样结构数据  
处理方式:  
接收方建立一预处理线程,对接收到的数据包进行预处理,将粘连的包分开  
注:粘包状况有两种,一种是粘在一块儿的包都是完整的数据包,另外一种状况是粘在一块儿的包有不完整的包  

备注:  
一个包没有固定长度,以太网限制在46-1500字节,1500就是以太网的MTU,超过这个量,TCP会为IP数据报设置偏移量进行分片传输,如今通常可容许应用层设置8k(NTFS系)的缓冲区,8k的数据由底层分片,而应用看来只是一次发送。windows的缓冲区经验值是4k,Socket自己分为两种,流(TCP)和数据报(UDP),你的问题针对这两种不一样使用而结论不同。甚至还和你是用阻塞、仍是非阻塞Socket来编程有关。  
一、通讯长度,这个是你本身决定的,没有系统强迫你要发多大的包,实际应该根据需求和网络情况来决定。对于TCP,这个长度能够大点,但要知道,Socket内部默认的收发缓冲区大小大概是8K,你能够用SetSockOpt来改变。但对于UDP,就不要太大,通常在1024至10K。注意一点,你不管发多大的包,IP层和链路层都会把你的包进行分片发送,通常局域网就是1500左右,广域网就只有几十字节。分片后的包将通过不一样的路由到达接收方,对于UDP而言,要是其中一个分片丢失,那么接收方的IP层将把整个发送包丢弃,这就造成丢包。显然,要是一个UDP发包佷大,它被分片后,链路层丢失分片的概率就佷大,你这个UDP包,就佷容易丢失,可是过小又影响效率。最好能够配置这个值,以根据不一样的环境来调整到最佳状态。  

send()函数返回了实际发送的长度,在网络不断的状况下,它毫不会返回(发送失败的)错误,最多就是返回0。对于TCP你能够字节写一个循环发送。当send函数返回SOCKET_ERROR时,才标志着有错误。但对于UDP,你不要写循环发送,不然将给你的接收带来极大的麻烦。因此UDP须要用SetSockOpt来改变Socket内部Buffer的大小,以能容纳你的发包。明确一点,TCP做为流,发包是不会整包到达的,而是源源不断的到,那接收方就必须组包。而UDP做为消息或数据报,它必定是整包到达接收方。 

二、关于接收,通常的发包都有包边界,首要的就是你这个包的长度要让接收方知道,因而就有个包头信息,对于TCP,接收方先收这个包头信息,而后再收包数据。一次收齐整个包也能够,可要对结果是否收齐进行验证。这也就完成了组包过程。UDP,那你只能整包接收了。要是你提供的接收Buffer太小,TCP将返回实际接收的长度,余下的还能够收,而UDP不一样的是,余下的数据被丢弃并返回WSAEMSGSIZE错误。注意TCP,要是你提供的Buffer佷大,那么可能收到的就是多个发包,你必须分离它们,还有就是当Buffer过小,而一次收不完Socket内部的数据,那么Socket接收事件(OnReceive),可能不会再触发,使用事件方式进行接收时,密切注意这点。这些特性就是体现了流和数据包的区别。 




参考:http://www.cnblogs.com/canghaitianyuan/archive/2012/11/16/2772987.html

             http://my.oschina.net/ksfzhaohui/blog/95451

           《 java并发编程实践》

          《java网络编程精解》

        http://dalezhu.javaeye.com/blog/186895