最近有朋友在公众号留言问熔断的问题:使用hystrix进行httpclient超时熔断报错,我是顺序操作(无并发),发现hystrix会超时断开连接,但是会导致hystrix线程池不断增加,直到因为后面线程池hold不住而被reject?此问题与线程中断、超时和降级有关,因此本文将详细介绍此问题背后的原因。当我们在线程中执行用户请求等任务时,比如HTTP处理线程,我们最担心的是什么?线程数不断增加;线程执行时间长;线程不能被中断。对于线程数的最大增长,我们可以使用线程池来控制线程数,控制线程是不是最大增长。对于执行时间较长的线程,我们应该设置合理的超时时间,保证线程执行时间可控。发生超时时,要么向用户返回错误页面,要么返回降级页面。对于不可中断的线程,我们应该想办法把线程设计成可中断的,这样线程遇到问题就可以中断降级。下一节将重点介绍线程中断。线程中断是通过Thread.interrupt()方法完成的,通常在线程A中中断线程B。首先我们看一下这个方法的一些Javadoc描述:1.如果线程被Object的wait(),wait(long),wait(long,int)或者Thread的join(),join(long),join(long,int),sleep(long),sleep(long,int)方法被阻塞,执行线程被中断,抛出InterruptedException,但中断状态被清除并重置,即Thread.isInterrupted()返回false;2.如果线程被java中断了。nio.channels.InterruptibleChannel的I/O操作被阻塞,执行线程中断,InterruptibleChannel会被关闭,抛出java.nio.channels.ClosedByInterruptException,并设置线程中断状态,即,Thread.isInterrupted()返回真;3、如果线程被java.nio.channels.Selector阻塞,执行线程中断,Selector#select()方法会立即返回,相当于调用java.nio.channels.Selector#wakeup()没有抛出异常,但会设置中断状态,即Thread.isInterrupted()返回true;4.如果不满足以上条件,那么执行线程中断不会抛出异常,只会设置中断状态,即Thread.isInterrupted()返回true。也就是说,我们的代码需要根据状态来决定下一步做什么。从上面的描述可以看出,如果方法异常描述抛出InterruptedException、ClosedByInterruptException,说明该方法可以被中断,比如“publicfinalnativevoidwait(longtimeout)throwsInterruptedException”,但是中断状态会被重置请参阅其Javadoc说明。其他情况基本不用中断运行就设置中断状态。BIO(阻塞I/O)操作不能被中断。例如,java.net.Socket在读写网络I/O时被阻塞。除了设置超时时间外,还应该考虑使其可中断或尽快中断。可以参考《你的Java代码可中断吗》。另外mysql-connector-java、HttpClient等JDBC驱动大多使用BIO,同样是不可中断的。NIO(NewI/O)操作可中断NIO涉及两个部分:java.nio.channels.Selector和java.nio.channels.InterruptibleChannel,它们是可中断的。例如,java.nio.channels#SocketChannel实现了InterruptibleChannel。以下方法都是可中断的,会抛出ClosedByInterruptException:通过BIO实现做一个实验,如下代码所示:9090/ajax";//HttpClient为BIO,不可中断//虽然threadA线程中断在threadB中执行//但是只是将中断状态设置为true//线程A的执行没有被中断,线程仍然正常执行完成System.out.println("threadAisinterrupted:"+Thread.currentThread().isInterrupted());}catch(异常){e.printStackTrace();}});ThreadthreadB=newThread(()->{try{Thread.sleep(2000L);//休眠2s后,中断线程AthreadA.interrupt();}catch(Exceptione){}});线A.start();threadB.start();Thread.sleep(15000L);}}上面代码的输出是:httpstatuscode:200threadAisinterrupted:true上面代码的执行流程如下:ThreadAimplementsHttpClientremote通过BIO调用http://localhost:9090/ajax获取数据,服务5s响应;线程B在线程A执行2s后中断处理,但是线程A调用的HttpClient是阻塞不可中断操作,只是设置了线程A的中断状态为true,所以一直等待网络I/O完成;当线程A从远程获取结果后继续执行时,Thread.currentThread().isInterrupted()会输出true,说明线程A设置了中断状态,所以需要注意的是设置中断状态是不一样的作为中断执行。所以对于BIO的使用,需要设置连接和读写的超时时间。另外,可中断设计可以参考《你的Java代码可中断吗》。线程池、Future和中断我们提交一个HttpClient任务到线程池,通过Future等待执行结果,如下代码所示:{FuturefutureA=executorService.submit((Callable)()->{//url会阻塞for5sStringurl="http://localhost:9090/ajax";//HttpClient是BIO,不能中断HttpResponseresponse=HttpClientUtils.getHttpClient().execute(newHttpGet(url));Integerresult=response.getStatusLine().getStatusCode();System.out.println("threadaresult:"+result);returnresponse.getStatusLine().getStatusCode();});FuturefutureB=executorService.submit((Callable)()->{//url会阻塞5sStringurl="http://localhost:9090/ajax";//HttpClient是BIO,不能被中断HttpResponseresponse=HttpClientUtils.getHttpClient().execute(newHttpGet(url));Integerresult=response.getStatusLine().getStatusCode();System.out.println("threadbresult:"+result);returnresult;});try{IntegerresultA=futureA.get(100,TimeUnit.MILLISECONDS);}catch(TimeoutExceptione){System.out.println("futureatimeout");}try{IntegerresultB=futureB.get(100,TimeUnit.MILLISECONDS);}catch(TimeoutExceptione)){System.out.println("futurebtimeout");}executorService.awaitTermination(10000L,TimeUnit.MILLISECONDS);}}上面代码的输出结果为:futureatimeoutfuturebtimeoutthreadaresult:200threadbresult:200上面代码的执行过程如下:mainthread两个HttpClient阻塞调用任务提交到线程池,任务响应时间5s;主线程阻塞在两个超时等待的Future上,Future在等待线程池任务执行结束,Future的超时时间设置为100ms,所以很快超时返回,主线程继续执行。在《亿级流量》中,我们大量使用了这种并发获取数据并进行降级或融合处理的方法;线程池中的两个任务实际上并没有被中断,而是仍然占用着线程池中的线程在后台继续执行,直到完成。从上面可以看出,在使用Future时,只是解除了主线程的阻塞,并没有取消线程池任务。它仍然占用线程并阻塞执行。之前有同学在公众号后台留言咨询:我用hystrix做httpclient超时熔断错误。我是顺序操作的(没有并发),发现hystrix会超时断开连接,但是会导致hystrix线程池不断增加,直到后面因为线程失效。无法适应游泳池并拒绝?看完上面的例子,读者的疑惑应该就解开了。虽然熔断了,但线程中的操作并没有真正中断,但还是占用了线程资源。接下来我们可以简单看一下Future的实现之一FutureTask:超时等待方法get(longtimeout,TimeUnitunit)伪代码:while(true){if(Thread.interrupted()){//如果当前线程被中断,处理场景,抛出中断异常//somecodethrownewInterruptedException();}//判断剩余睡眠时间nanos=deadline-System.nanoTime();if(nanos<=0L){//如果没有睡眠时间,处理线程,并终止执行//somecodereturnstate;}//休眠一段时间,内部实现是UNSAFE.park(false,nanos)LockSupport.parkNanos(this,nanos);}取消方法cancel(booleanmayInterruptIfRunning)伪代码:if(mayInterruptIfRunning){//中断当前线程Threadt=runner;if(t!=null)t.interrupt();}//执行UNSAFE.unpark(thread)唤醒休眠的当前线程LockSupport.unpark(吨);也就是说,当我们调用Future#cancel时,是通过唤醒Future所在的线程来实现的。当然,实际上比这更复杂。回填结果方法伪代码set(Vv)://修改Future的状态完成//保持v的值,使得Future#get可以获取//通过LockSupport.unpark(t)唤醒休眠线程时在线程池中线程执行完成后,通过Future#set将该值重新设置回Future,从而唤醒休眠线程,即阻塞Future#get中的等待,进而获取结果。锁和中断synchronized和ReentrantLock#lock()在获取锁的过程中是不可中断的。如果出现死锁,它将保持僵局并且无法终止块。但是我们可以使用可中断的ReentrantLock#lockInterruptibly()方法或者ReentrantLock#tryLock(longtimeout,TimeUnitunit)来实现可中断。总结在设计高可用系统时,尽量使用线程池,而不是为每个请求创建一个线程,利用线程池的拒绝策略优雅地拒绝无法处理的请求。检查整个请求链路并设置合理的超时时间,与调用方协商合理的SLA,降级限流方案。超时时间越长意味着出现问题时堆积的请求越多,发生雪崩的可能性就越大。清楚地知道您的服务是否可以中断。如果不能中断,就应该使用线程池和Future来实现伪中断。通过Future配置合理的超时时间,超时时执行相应的降级策略。也很清楚Future只是一个假中断,线程池中的任务仍然在后台执行。当Future超时重试时,会向被调用的服务发出更多的请求,这将导致一种DDos。注意相应的处理策略。poolsize、timeoutperiod、interrupt没有完美的配置策略。他们应该根据自己的场景动态调整。当系统遇到高并发或者异常的时候,我们要保护什么和放弃什么之间要有一个平衡点。【本文为专栏作者“张凯涛”原创文章,作者微信公众号:凯涛博客(kaitao-1234567)】点此阅读更多本作者好文