子執行緒使用父執行緒RequestScope作用域Bean問題的探究

加多發表於2018-06-04

一、前言

最近我們組在做專案分層模組化專案調研,同組通元童鞋在調研ajdk8的多租戶方案需要對每一個請求開啟一個執行緒進行處理,然後就產生一個問題如何在開啟的執行緒中不破壞使用習慣情況下使用請求執行緒裡面的RequestScope作用域的bean,感覺這個問題比較有意思就研究並整理下一下,以便備忘,下面從最基礎知識將起,一步步引入問題和解決方法

二、ThreadLocal原理

眾所周知如果一個變數定義為了threadlocal變數,那麼訪問這個變數的每個執行緒都獨有一個屬於自己的變數,這變數值只有當前執行緒才能訪問使用,各個執行緒直接相互不干擾,那原理究竟如何那?

2.1 ThreadLocal類

通常程式碼裡面經常使用threadlocal的set和get方法,下面就講解下這兩個方法,首先set方法:

    public void set(T value) {
        //獲取當前執行緒
        Thread t = Thread.currentThread();
        //當前執行緒作為key,去查詢對應的執行緒變數,找到則設定
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
        //第一次呼叫則建立當前執行緒對應的map
            createMap(t, value);
    }

getMap(t)所做的就是獲取執行緒自己的變數threadLocals,可知threadlocal變數是繫結到了執行緒的成員變數裡面,那麼threadLocals是什麼結構那?其實是ThreadLocalMap型別,不關心裡面細節的話我們可以認為他是個map.

 ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

createMap(t, value)裡面做了啥那?其實就是執行緒第一次設定threalocal.set時候建立執行緒的成員變數threadLocals,並把值初始化到map.

     void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

下面看下get方法:

    public T get() {
        //獲取當前執行緒的成員變數值為map,如果map不為空則獲取當前執行緒對應的threadlocal變數的值,並返回
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //map為空則給例項化當前執行緒的t.threadLocals成員變數,並且初始化threadlocal值為null
        return setInitialValue();
    }

總結:
(1)、 每個執行緒都有一個名字為threadLocals的成員變數,該變數型別為ThreadLocalMap,不深究的話可以簡單認為是一個特殊的map,其中key為我們定義的ThreadLocal變數的this引用,value則為我們set時候的值
(2)、 對於下面定義ThreadLocal threadLocal = new ThreadLocal();
當執行緒A首次呼叫threadLocal.set(new Integer(666));時候會建立執行緒A的成員變數threadLocals,並且把threadLocal做為key,new Integer(666)作為value放入threadLocals中。然後當執行緒A呼叫threadLocal.get()時候那麼首先獲取到成員變數threadLocals,然後以key等於threadLocal去threadLocals中獲取對應的值為new Integer(666)。

2.2 ThreadLocal不支援繼承特性

執行一下程式碼:

public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();
    public static void main(String args[]) {

        threadLocal.set(new Integer(666));

        Thread thread = new MyThread();
        thread.start();

        System.out.println("main = " + threadLocal.get());
    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("MyThread = " + threadLocal.get());
        }
    }

結果為:
main = 666
MyThread = null
也就是說ThreadLocal不支援在子執行緒中獲取父執行緒中的設定的值,這個根據程式碼來看很正常,因為子執行緒get時候當前執行緒為thread,而設定執行緒變數是在main執行緒,兩者是不同的執行緒

三、InheritableThreadLocal原理

為了解決2.2的問題InheritableThreadLocal應運而生。

3.1 InheritableThreadLocal類

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    
    protected T childValue(T parentValue) {
        return parentValue;
    }


    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }


    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

可知InheritableThreadLocal繼承了ThreadLocal,並重寫了三個方法,好下面在重新走下set和get方法
如下程式碼第一次呼叫set的時候,map為空,所以呼叫createMap,

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

而ThreadLocal的createMap例項化了當前執行緒的threadLocals成員變數,InheritableThreadLocal重寫的createMap則例項化了inheritableThreadLocals變數,這是其一。
如下程式碼呼叫get方法時候與set方法對應InheritableThreadLocal重寫了getMap方法,目的是獲取當前執行緒的inheritableThreadLocals成員變數,而不是threadLocals。

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //map為空則給例項化當前執行緒的t.threadLocals成員變數,並且初始化threadlocal值為null
        return setInitialValue();
    }

到了這裡我們知道了inheritableThreadLocal改變在於set和get操作的都是當前執行緒的inheritableThreadLocals成員變數,替代了ThreadLocals操作threadLocals,這樣看來似乎兩者沒啥區別,但是還有一個重寫的函式我們沒有分析到那就是childValue。

開啟Thread.java類的建構函式:

    public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {
       .....
        if (parent.inheritableThreadLocals != null)
          //拷貝父執行緒的inheritableThreadLocals噹噹前執行緒的inheritableThreadLocals
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
       .....
    }

建立執行緒時候在建構函式裡面會呼叫init方法,前面講到了inheritableThreadLocal類get,set的是inheritableThreadLocals,所以這裡parent.inheritableThreadLocals != null,呼叫了createInheritedMap個方法。

    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

下面函式所做的事情就是把父執行緒的inheritableThreadLocals成員變數的值複製到ThreadLocalMap物件
private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);//返回e.value
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

所以把上節程式碼修改為

    public static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<Integer>();
    public static void main(String args[]) {

        threadLocal.set(new Integer(666));

        Thread thread = new MyThread();
        thread.start();

        System.out.println("main = " + threadLocal.get());
    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("MyThread = " + threadLocal.get());
        }
    }

結果為:
MyThread = 666
main = 666
現在可以從子執行緒中正常的獲取到執行緒變數值了,但是我們使用場景並不是那麼簡單我們使用的是requestscope的bean,所以下節將下RequestContextListener的原理以及遇到的問題。

四、RequestContextListener原理

spring中配置bean的作用域時候我們一般配置的都是Singleton,但是有些業務場景則需要三個web作用域,分別為request、session和global session,如果你想讓你Spring容器裡的某個bean擁有web的某種作用域,則除了需要bean級上配置相應的scope屬性,還必須在web.xml裡面配置如下:

<listener>
        <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>

主要看RequestContextListener的兩個方法public void requestInitialized(ServletRequestEvent
requestEvent)和public void requestDestroyed(ServletRequestEvent requestEvent)。

當web請求過來時候:

    public void requestInitialized(ServletRequestEvent requestEvent) {
                .......
        HttpServletRequest request = (HttpServletRequest) requestEvent.getServletRequest();
        ServletRequestAttributes attributes = new ServletRequestAttributes(request);
        request.setAttribute(REQUEST_ATTRIBUTES_ATTRIBUTE, attributes);
        LocaleContextHolder.setLocale(request.getLocale());
                //設定屬性到threadlocal變數
        RequestContextHolder.setRequestAttributes(attributes);
    }

    public static void setRequestAttributes(RequestAttributes attributes) {
  
        setRequestAttributes(attributes, false);
    }
    public static void setRequestAttributes(RequestAttributes attributes, boolean inheritable) {
        if (attributes == null) {
            resetRequestAttributes();
        }
        else {
                        //預設inheritable=false
            if (inheritable) {
                inheritableRequestAttributesHolder.set(attributes);
                requestAttributesHolder.remove();
            }
            else {
                requestAttributesHolder.set(attributes);
                inheritableRequestAttributesHolder.remove();
            }
        }
    }

所以知道預設inheritable為FALSE,我們的屬性值都放到了requestAttributesHolder裡面,而他是:

    private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
            new NamedThreadLocal<RequestAttributes>("Request attributes");

    private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
            new NamedInheritableThreadLocal<RequestAttributes>("Request context");

NamedThreadLocal extends ThreadLocal所以不具有繼承性
NamedInheritableThreadLocal extends InheritableThreadLocal 所以具有繼承性,所以預設放入到RequestContextHolder裡面的屬性值在子執行緒中獲取不到。

當請求結束時候呼叫:

    public void requestDestroyed(ServletRequestEvent requestEvent) {
        ServletRequestAttributes attributes =
                (ServletRequestAttributes) requestEvent.getServletRequest().getAttribute(REQUEST_ATTRIBUTES_ATTRIBUTE);
        ServletRequestAttributes threadAttributes =
                (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (threadAttributes != null) {
            // We`re assumably within the original request thread...
            if (attributes == null) {
                attributes = threadAttributes;
            }
                       //請求結束則清除當前執行緒的執行緒變數。
            LocaleContextHolder.resetLocaleContext();
            RequestContextHolder.resetRequestAttributes();
        }
        if (attributes != null) {
            attributes.requestCompleted();
        }
    }

這個listener呼叫邏輯如何那,如下:
screenshot.png
也就是說每次發起一個web請求在tomcat中context(具體應用)處理前,host匹配後都會去設定下RequestContextHolder屬性,讓requestAttributesHolder不為空,在請求結束時候會清除。

總結:預設情況下放入RequestContextHolder裡面的屬性子執行緒訪問不到。spring的request作用域的bean是使用threadlocal實現的。

五、根據需求對RequestContextListener進行改造

需求模擬,配置如下bean

    <bean id="lavaPvgInfo" class="com.alibaba.lava.privilege.PrivilegeInfo"
        scope="request">
        <property name="aesKey" value="666" />
        <aop:scoped-proxy />
    </bean>

然後在Rpc裡面autowired該bean,然後在rpc方法裡面訪問lavaPvgInfo,然後方法裡面在開啟一個執行緒去獲取lavaPvgInfo裡面設定的值。

@WebResource("/testService")
public class TestRpc {

    @Autowired
    private PrivilegeInfo pvgInfo;

    @ResourceMapping("test")
    public ActionResult test(ErrorContext context) {
        ActionResult result = new ActionResult();

        String aseKey = pvgInfo.getAesKey();
        pvgInfo.setAesKey("888");
        System.out.println("aseKey---" + aseKey);

        Thread myThread = new Thread(new Runnable() { 
            public void run() {
                System.out.println("hellobegin");
                System.out.println(pvgInfo.getAesKey());
                System.out.println("helloend");

            }
        });   

        myThread.start();
        try {
           //防止主執行緒結束後清除執行緒變數
            myThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
       
        return result;
    }
}

直接呼叫Rpc返回結果為:
screenshot.png
明顯子執行緒獲取父執行緒執行緒變數時候拋異常了。

下面分析下呼叫這個rpc方法時候時序圖為:
screenshot.png

下面看下test方法內發生了啥:

screenshot.png
下面重點看RequestScope的get方法:

    public Object get(String name, ObjectFactory objectFactory) {
                //獲取RequestContextListener.requestInitialized設定的屬性值
        RequestAttributes attributes = RequestContextHolder.currentRequestAttributes();(1)
        Object scopedObject = attributes.getAttribute(name, getScope());
        if (scopedObject == null) {
            scopedObject = objectFactory.getObject();(2)
            attributes.setAttribute(name, scopedObject, getScope());(3)
        }
        return scopedObject;
    }

    public static RequestAttributes currentRequestAttributes() throws IllegalStateException {
                
               //獲取RequestContextListener.requestInitialized設定的屬性值,為null則丟擲異常,所以如果在非web專案中普通執行緒中呼叫會拋異常,這是因為他沒有在RequestContextListener.requestInitialized設定過。
        RequestAttributes attributes = getRequestAttributes();
        if (attributes == null) {
            if (jsfPresent) {
                attributes = FacesRequestAttributesFactory.getFacesRequestAttributes();
            }
            if (attributes == null) { (4)
                throw new IllegalStateException("No thread-bound request found: " +
                        "Are you referring to request attributes outside of an actual web request, " +
                        "or processing a request outside of the originally receiving thread? " +
                        "If you are actually operating within a web request and still receive this message, " +
                        "your code is probably running outside of DispatcherServlet/DispatcherPortlet: " +
                        "In this case, use RequestContextListener or RequestContextFilter to expose the current request.");
            }
        }
        return attributes;
    }

      //獲取RequestContextListener.requestInitialized設定的屬性值
    public static RequestAttributes getRequestAttributes() {
        RequestAttributes attributes = requestAttributesHolder.get();(5)
        if (attributes == null) {
            attributes = inheritableRequestAttributesHolder.get();
        }
        return attributes;
    }

可知時序圖訪問rpc方法時候在RequestContextListener.requestInitialized裡面呼叫RequestContextHolder.setRequestAttributess設定了設定了requestAttributesHolder,所以在test方法內第一次呼叫getAesKey()方法時候,RequestScope.get()方法裡面第一步時候獲取到了attributes,但是屬性裡面卻沒有pvginfo物件,所以會建立個,然後放入attributes.然後返回,然後在cgib代理裡面呼叫pvginfo的getAesKey方法。

呼叫setAesKey時候RequestScope.get()則是直接從attributes裡面獲取返回,然後在cglib代理裡面呼叫pvginfo的setAesKey方法設定。

在子執行緒中呼叫getAesKey方法時候,RequestScope.get()方法裡面第一步時候獲取attributes時候,由於(5)是threadlocal,所以根據第二節講的threadlocal原理知道返回的attributesnull.所以(4)出丟擲了異常。

從第三節講的如果是inheritthreadlocal,則子執行緒克繼承父執行緒pvginfo資訊,而前面正好介紹了
RequestContextHolder裡面:

private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
        new NamedThreadLocal<RequestAttributes>("Request attributes");

private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
        new NamedInheritableThreadLocal<RequestAttributes>("Request context");

有一個inheritableRequestAttributesHolder是繼承了inheritthreadlocal。
在回頭看RequestContextListener.requestInitialized裡面呼叫RequestContextHolder.setRequestAttributess設定了設定了requestAttributesHolder,而setRequestAttributess還有個 public static void setRequestAttributes(RequestAttributes attributes, boolean inheritable) 用來指定使用這兩個變數的哪一個,那如果我們重寫RequestContextListener的requestInitialized方法為:

public class InheritableRequestContextListener extends RequestContextListener {
    private static final String REQUEST_ATTRIBUTES_ATTRIBUTE =
        InheritableRequestContextListener.class.getName() + ".REQUEST_ATTRIBUTES";

    @Override
    public void requestInitialized(ServletRequestEvent requestEvent) {
        if (!(requestEvent.getServletRequest() instanceof HttpServletRequest)) {
            throw new IllegalArgumentException(
                    "Request is not an HttpServletRequest: " + requestEvent.getServletRequest());
        }
        HttpServletRequest request = (HttpServletRequest) requestEvent.getServletRequest();
        ServletRequestAttributes attributes = new ServletRequestAttributes(request);
        request.setAttribute(REQUEST_ATTRIBUTES_ATTRIBUTE, attributes);
        LocaleContextHolder.setLocale(request.getLocale());
       //傳入true標示使用inheritableRequestAttributesHolder而不是requestAttributesHolder
        RequestContextHolder.setRequestAttributes(attributes, true);
    }
}

根據第三屆原理知道這樣應該是可以行的,其實如果使用的不是webx框架而是springmvc確實是可行的,但是我們使用的是webx框架發現還是丟擲了和上面一樣的異常,為啥那?請看第六屆`

六、Webx的改造

對程式碼進行debug發現RequestContextHolder.setRequestAttributess被呼叫了兩次,其中一次是在RequestContextListener.requestInitialized裡面,這個正常,並且inheritable=true


    public static void setRequestAttributes(RequestAttributes attributes) {
  
        setRequestAttributes(attributes, false);
    }
    public static void setRequestAttributes(RequestAttributes attributes, boolean inheritable) {
        if (attributes == null) {
            resetRequestAttributes();
        }
        else {
                        //inheritable=true
            if (inheritable) {
                inheritableRequestAttributesHolder.set(attributes);
                requestAttributesHolder.remove();
            }
            else {
                requestAttributesHolder.set(attributes);
                inheritableRequestAttributesHolder.remove();
            }
        }
    }

然後發現第二次webx竟然也呼叫這個方法但是傳遞的inheritable=FALSE,艾瑪,我們前面的工作白做了。
screenshot.png
具體看下是RequestContextChainingServiceImpl的setupSpringWebEnvironment
screenshot.png
那麼這個值哪裡來的那?
諮詢千臂後查詢了如下:
screenshot.png
預設為FALSE,那麼如何進行設定那,從這xsd知道webx.xml裡面應該有標籤設定。
然後看下webx.xml裡面有個標籤,那下面看下這個標籤如何解析的吧。
根據經驗搜尋類RequestContextChainingService*,找到了RequestContextChainingServiceDefinitionParser。


    protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
        parseBeanDefinitionAttributes(element, parserContext, builder);
        //sort為RequestContextChainingService的成員變數
        builder.addPropertyValue("sort", defaultIfNull(trimToNull(element.getAttribute("sort")), "true"));

        List<Object> factoryList = createManagedList(element, parserContext);

        for (Element subElement : subElements(element)) {
            factoryList.add(parseConfigurationPointBean(subElement, requestContextsConfigurationPoint, parserContext,
                                                        builder));
        }
        //factories為RequestContextChainingService的成員變數
        builder.addPropertyValue("factories", factoryList);
    }

但是唯獨沒有在bean定義中填入threadContextInheritable的,諮詢千臂後確實目前沒法配置這個變數,建議debug時候新增threadContextInheritable到builder測試下,然後在debug 下display中執行
builder.addPropertyValue(“threadContextInheritable”,true)後,上面Rpc程式碼輸出為:
aseKey–666
hellobegin
888
helloend

至此達到了想要的結果,然後千臂幫搞了個snapshot版本的webx包,其實就是在parse裡面新增一行:
貼上圖片.png
然後在webx.xml配置如下:
貼上圖片.png

七、總結

其實子執行緒中使用父執行緒中threadlocal方法有很多方式,比如建立執行緒時候傳入執行緒變數的拷貝到執行緒中,或者在父執行緒中構造個map作為引數傳遞給子執行緒,但是這些都改變了我們的使用習慣,所以研究了本文方法。
想獲取更多技術乾貨,請關注微信公眾號:‘技術原始積累’

image.png


相關文章