• 记低版本okhttp超时会导致死锁


    一、问题起源

    在处理一次生产环境cpu拉满问题时,把日志拉下来看发现很多http请求调用出错,项目使用的是okhttp 3.8.1版本。

    二、问题描述

    问题出在okhttp3.Dispatcher.finished(Dispatcher.java:201)代码如下:

    1. void finished(AsyncCall call) {
    2. finished(runningAsyncCalls, call, true);
    3. }
    4. void finished(RealCall call) {
    5. finished(runningSyncCalls, call, false);
    6. }
    7. private void finished(Deque calls, T call, boolean promoteCalls) {
    8. int runningCallsCount;
    9. Runnable idleCallback;
    10. synchronized (this) { //201行
    11. if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
    12. if (promoteCalls) promoteCalls();
    13. runningCallsCount = runningCallsCount();
    14. idleCallback = this.idleCallback;
    15. }
    16. if (runningCallsCount == 0 && idleCallback != null) {
    17. idleCallback.run();
    18. }
    19. }
    20. private void promoteCalls() {
    21. if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
    22. if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.
    23. for (Iterator i = readyAsyncCalls.iterator(); i.hasNext(); ) {
    24. AsyncCall call = i.next();
    25. if (runningCallsForHost(call) < maxRequestsPerHost) {
    26. i.remove();
    27. runningAsyncCalls.add(call);
    28. executorService().execute(call);
    29. }
    30. if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
    31. }
    32. }

    三、分析代码

    OkHttpClientfinal Dispatcher dispatcher; 作为成员对象,而我们代码中OkHttpClient作为连接池是单例的,这里是对dispatcher做synchronized。

    追踪代码发现,在finished的调用方法中,我们方法中使用的是异步AsyncCall,而这里synchronized方法中的promoteCalls被置为true。所以会调用promoteCalls()方法, 而promoteCalls()方法中会继续调用executorService().execute(call);,就是这里,问题大了,synchronized中执行http请求,那上面代码中的超时不就长时间占用锁了?怪不得进程blocked了。

    关于线程的BLOCKED,需要知道:

    • java.lang.Thread.State: BLOCKED:等待监视器锁而被阻塞的线程的线程状态,当进入 synchronized 块/方法或者在调用 wait()被唤醒/超时之后重新进入 synchronized 块/方法, 但是锁被其它线程占有,这个时候被操作系统挂起,状态为阻塞状态。若是有线程长时间处于 BLOCKED 状态,要考虑是否发生了死锁(deadlock)的情况。
    • blocked的线程不会消耗cpu,但频繁的频繁切换线程上下文会导致cpu过高。线程被频繁唤醒,而又由于抢占锁失败频繁地被挂起. 因此也会带来大量的上下文切换, 消耗系统的cpu资源。

    四、解决方案

    okttp关于这个问题已经有过解答:

    Dispatcher no longer has quadratic behaviour by iamdanfox · Pull Request #4581 · square/okhttp · GitHub

    [improvement] okhttp 3.12.0 -> 3.13.1, to pick up perf improvements to okhttp3.Dispatcher by iamdanfox · Pull Request #940 · palantir/conjure-java-runtime · GitHub

    解决方案就简单多了:升级okhttp到3.14.9,虽然目前最新稳定版本为4.9.3,但是OkHttp 4发布,从Java切换到Kotlin。谨慎一点,还是小版本升级吧。

    在3.14.9中,这部分代码被优化为:

    1. private boolean promoteAndExecute() {
    2. assert (!Thread.holdsLock(this));
    3. List executableCalls = new ArrayList<>();
    4. boolean isRunning;
    5. synchronized (this) {
    6. for (Iterator i = readyAsyncCalls.iterator(); i.hasNext(); ) {
    7. AsyncCall asyncCall = i.next();
    8. if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
    9. if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity.
    10. i.remove();
    11. asyncCall.callsPerHost().incrementAndGet();
    12. executableCalls.add(asyncCall);
    13. runningAsyncCalls.add(asyncCall);
    14. }
    15. isRunning = runningCallsCount() > 0;
    16. }
    17. for (int i = 0, size = executableCalls.size(); i < size; i++) {
    18. AsyncCall asyncCall = executableCalls.get(i);
    19. asyncCall.executeOn(executorService());
    20. }
    21. return isRunning;
    22. }

    执行HTTP请求被移出了synchronized方法了。

  • 相关阅读:
    C++ Primer学习笔记-----第十三章:拷贝控制
    四万字!多线程50问!
    Java学习:动态代理
    Redis集群(Cluster)
    Linux 根文件系统的移植(从入门到精通)
    Linux高级IO
    大数据Flink(九十八):SQL函数的归类和引用方式
    优化编译速度&包优化
    1668. 最大重复子字符串
    【建议收藏】ヾ(^▽^*)))全网最全输入输出格式符整理
  • 原文地址:https://blog.csdn.net/u012758488/article/details/133993395