爲什麼面試要求看過源碼——案例:由於設計不當導致線程池execute方法拋異常

問題描述

我寫了一個線程池和延遲任務工具類,正常使用中,突然一天同事告訴我:你的代碼阻塞啓動了,快去看看!

問題背景

線程池是使用的內部框架中提供的,使用者的注入寫法也是框架給的,其實本質上是JDK的 ThreadPoolExcute,我能決定的幾乎只是對線程的命名。

之前一直是可以運行的,發生問題的某一天我的電腦上可以正常啓動,而其他同事的電腦上則無法啓動web容器(IllegalStateException)。

我寫的延遲任務是依賴線程池的,有一個搬運工線程,負責從delayQueue中拿任務放入線程池中。

搬運工線程

@Override
    public void run() {
        while (true) {
            try {
                // 從延時隊列中獲取任務
                DelayTask<?> delay = delayQueue.take();
                Runnable task = delay.getTask();
                if (task != null) {
                    threadPool.execute(task);
                }
            } catch (Exception e) {
	               ...
            }
        }
    }

這個線程是在一個bean的構造方法中執行的:在啓動時把這麼一個runnable放入了線程池

	@Autowired
    public SdmcDelayTaskPorter(XxxxThreadPool threadPool) {
        this.threadPool = threadPool;
        this.threadPool.execute(this);// 這裏報錯!!!
    }

代碼中註釋位置報錯
IllegalStateException(“can not find application context.”);
線程池雖然是框架組提供的,但是excute方法是JDK的,怎麼會拋出這個異常呢?

問題定位

由於我電腦上沒問題,而同事電腦上有問題,因此排查時在同事電腦上進行。

1. JDK源碼部分

在idea裏進入excute具體調用的方法,確實是jdk的代碼,這點我們不用懷疑,繼續跟蹤就是了。
jdk ThreadPoolExcute#execute方法所有代碼如下:

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))//這裏!!!!!!!!!!!!!!!
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

代碼怎麼讀呢?一眼望去,都是jdk寫的,大概率不會是jdk問題,但這個方法裏調用了其他方法,不保證其他方法裏沒有給我們留擴展,看方法名稱發現addWorker(command, true)這個地方最可疑(已標註)
進入addWorker方法後,代碼很長,看名字也沒調用什麼方法,唯一值得關注的是new Worker(firstTask);
進入這個方法看,發現果然有更多的方法調用

	Worker(Runnable firstTask) {
            setState(-1);
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);// 這裏!!!
        }

其中 getThreadFactory() 只是反回了線程池的一個內部變量線程工廠,是由初始化的時候決定的,newThread方法是線程工廠的,看到這裏,我們就知道該關注什麼了!

暫時猜測:框架代碼出問題了,因爲同事執行了maven reimport,導致依賴框架代碼(snapshot版本)更新了,然而我沒有執行,因此纔會出現別人有問題,而我這裏正常的現象。
2. 框架源碼部分

立馬拋開jdk源碼,去找創建線程池的代碼,看線程工廠是什麼,在創建新線程時候做了什麼事情。找到具體的線程工廠(框架代碼)後,發現以下:

public Thread newThread(Runnable r) {
        TraceRunnable traceRunnable = new TraceRunnable((Tracing)ApplicationContextExt.getBean(Tracing.class), r.toString(), r);
        ...
    }
發現框架想幫我們做到追蹤線程的運行狀態,和監控線程的運行信息,於是第一行做了一個封裝。

果然,第一行代碼里居然有個從容器裏getBean的操作
這個類實現了 ApplicationContextAware 接口,同時實現也很簡單正常的保存了一下
這裏放出ApplicationContextExt的 getBean代碼:

getBean代碼

	public static <T> T getBean(Class<T> cls) {
        if (context == null) {
            throw new IllegalStateException("can not find application context.");// 這裏拋出了異常!!!
        } else {
            try {
                return context.getBean(cls);
            } catch (BeansException var2) {
                return null;
            }
        }
    }

終於來到出錯的地點!發現因爲context爲null,而context唯一設置值的時刻爲接口方法 setApplicationContext,實現如下

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }

經過以上分析,看過spring源碼的應該知道,setApplicationContext 是由spring調用的,調用點是在bean初始化完畢後執行的。
其實看到這裏熟悉spring源碼的同學應該知道該怎麼解決了,爲了照顧不熟悉源碼的同學,我們繼續跟蹤。


3. spring源碼部分

翻過spring源碼的我們知道,ApplicationContextAware的注入時機是:
bean後處理器其中有一個叫 ApplicationContextAwareProcessor,其中的處理方法調用setApplicationContext方法
具體源碼調用位置爲

  • 入口:AbstractApplicationContext#refresh,調用了 prepareBeanFactory
  • 進入 prepareBeanFactory方法,有一行代碼如下
    • beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
    • 該方法即爲調用

知道 setApplicationContext 調用時間點之後,我們就知道爲什麼會出這個問題了,因爲構造方法注入在spring的聲明週期中是早於 ApplicationContextAwareProcessor 的,在bean注入階段,我們是無法通過ApplicationContextAware拿到 spring 容器上下文的,因此纔出現這個錯誤。

問題解決

既然知道是因爲調用點太早,所以解決的時候只需將調用時機推遲即可。

但是!!!
思考過框架設計的同學肯定知道發現這很顯然是框架的設計問題,憑什麼讓我們開發來解決?

沒錯,這確實是一個框架組的設計缺陷,在設計一個框架時,不應該過分地限制使用者的使用,設計的線程池竟然依賴了spring容器的生命週期,難道我用個線程池還要關心spring容器的啓動順序?在spring容器啓動前我們不能調用?顯然這不合理,作爲一個框架的設計者在設計時就應該依賴分明。


回顧

博主校招入職未滿3個月,這就是面試時爲什麼還要問一個校招生,你看過spring源碼嗎,知道spring運行順序嗎,用過線程池嗎,看過線程池源碼嗎。

如果你看過這些源碼,在解決以上問題時,2分鐘不到就定位清楚,並把缺陷提給框架組,也知道自己的代碼裏要如何避開這個坑,而若這些都沒看過,在解決這個問題時可能就要花費一段時間了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章