【多線程】父子線程共享ThreadLocal數據


1.ThreadLocal

在分析問題之前我們先來看一下ThreadLocal的內部獲取數據的方法:

可以看到160行代碼,獲取了當前線程。並且通過getMap方法傳入了當前線程,並返回了ThreadLocalMap。然后轉為Entry類型,再取出相應的值。

而getMap方法實現如下:

看到getMap方法的實現我們可以知道,其實每個線程都是維護了一個ThreadLocalMap,而ThreadLocalMap是ThreadLocal的一個內部類。

筆者在閱讀Thread類源碼的時候發現Thread本身並沒有對ThreadLocal.ThreadLocalMap進行初始化。而是在使用ThreadLocal類Set方法、Get方法的時候完成初始化的。

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

由於是線程級的變量,所以在線程之間肯定是無法共享的,如下例代碼:

package thread.pdd;

/**
 * @author:liyangpeng
 * @date:2020/5/25 11:38
 */
public class ThreadLocalTest {

    public static void main(String[] args) {
        ThreadLocal<String> threadLocal=new ThreadLocal<>();
        threadLocal.set("1111");
        Thread thread=new Thread(()->{
            System.out.println(threadLocal.get());
        });
        thread.start();
    }
}

輸出結果:null

相信認真閱讀本文的讀者肯定注意到了圖3 對Thread類的代碼截圖。我們只講了命名為 threadLocals 的ThreadLocal.ThreadLocalMap,他還有一個命名為 inheritableThreadLocals 的 ThreadLocal.ThreadLocalMap。沒錯,就是他了。
Thread類並沒有對 threadLocals 變量進行初始化操作。但是我們可以繼續閱讀源碼:Thread 的默認構造調用了 init 方法。init方法內部對inheritableThreadLocals 變量進行了初始化。

 private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
      if (name == null) {
	throw new NullPointerException("name cannot be null");
      }
      this.name = name;
      Thread parent = currentThread();
      SecurityManager security = System.getSecurityManager();
      //線程組加入邏輯
      if (g == null) {
	//獲取到安全管理器則使用安全管理器
	if (security != null) {
		g = security.getThreadGroup();
	}
	//未獲取到安全管理器則加入父線程線程組
	if (g == null) {
	      g = parent.getThreadGroup();
	}
      }
      g.checkAccess();
      if (security != null) {
	if (isCCLOverridden(getClass())) {
	      security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
	}
      }
      g.addUnstarted();
      this.group = g;
      //是否守護線程
      this.daemon = parent.isDaemon();
      //線程權重
      this.priority = parent.getPriority();
      if (security == null || isCCLOverridden(parent.getClass()))
	this.contextClassLoader = parent.getContextClassLoader();
      else
	this.contextClassLoader = parent.contextClassLoader;
      this.inheritedAccessControlContext =acc != null ? acc : AccessController.getContext();
      this.target = target;
      setPriority(priority);

      //【注意此處代碼實現】如果父線程inheritableThreadLocals 不為null,則初始化自身inheritThreadLocals。
      // ThreadLocal.createInheritedMap方法傳入了父線程的inheritableThreadLocals
      // 返回的是 new ThreadLocalMap(parentMap),注意此處是引用傳遞。理論上也就是說子線程修改數據對父線程是可見的。
      if (inheritThreadLocals && parent.inheritableThreadLocals != null)
	this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

      //分配線程內存
      this.stackSize = stackSize;

      //設置線程id
      tid = nextThreadID();
}

當然了 ThreadLocal 內部是沒有對線程的 inheritableThreadLocals進行讀寫操作的。好了,不跟你多BB,馬上進入我們的第二塊內容。

2.InheritableThreadLocal

大家點開源碼可以看到InheritableThreadLocal是繼承自ThreadLocal的。主要是覆蓋了ThreadLocal的以下幾個方法:

大家看到上面ThreadLocal的 get、set方法可以知道,如果是要讀寫數據的時候首先要調用的則是getMap方法,如果當前沒有實例化ThreadLocalMap則先實例化,覆蓋createMap方法直接為線程的inheritableThreadLocals屬性完成初始化。

InheritableThreadLocal類的getMap方法返回的則是當前線程的inheritableThreadLocals變量。

包括上面的實例化大家也能知道子線程初始化的時候inheritableThreadLocals變量是直接從父線程獲取的。由此便可以完成父子線程ThreadLocal數據共享啦。下面請查看代碼案例:

package thread.pdd;

import java.util.stream.IntStream;

/**
 * @author:liyangpeng
 * @date:2020/5/25 11:38
 */
public class ThreadLocalTest {

    public static void main(String[] args) throws InterruptedException {

        InheritableThreadLocal<String> threadLocal=new InheritableThreadLocal<>();
        threadLocal.set("1111");

        Thread thread=new Thread(()->{
            System.out.println(threadLocal.get());
        });
        thread.start();
        Thread.sleep(500);
        System.out.println("-------------------華麗的分割線-----------------");
        //流操作|內部實現也是多線程
        IntStream.range(0,10).parallel().forEach(id->{
            System.out.println(id+"_~_~_"+threadLocal.get());
        });
    }
}

輸出結果:
1111
-------------------華麗的分割線-----------------
6_~_~_1111
5_~_~_1111
1_~_~_1111
0_~_~_1111
4_~_~_1111
8_~_~_1111
3_~_~_1111
9_~_~_1111
7_~_~_1111
2_~_~_1111

於是乎,父子線程ThreadLocal數據共享我們便完成了,你以為這樣就結束了嗎?不。遠遠沒有你想的這么簡單。我們將以上的代碼稍作修改:

package thread.pdd;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;

/**
 * @author:liyangpeng
 * @date:2020/5/25 11:38
 */
public class ThreadLocalTest {

    public static void main(String[] args){
        
        InheritableThreadLocal<String> threadLocal=new InheritableThreadLocal<>();
        threadLocal.set("--->哈哈哈");
        ExecutorService executorService = Executors.newFixedThreadPool(1);

        executorService.execute(()->{
            Thread thread=Thread.currentThread();
            System.out.println(thread.getName()+"__"+threadLocal.get());
        });
        //重新修改InheritableThreadLocal內的數據
        threadLocal.set("--->呵呵呵");

        executorService.execute(()->{
            Thread thread=Thread.currentThread();
            System.out.println(thread.getName()+"__"+threadLocal.get());
        });

        executorService.shutdown();
    }
}
//猜猜以上結果是啥?是 哈哈哈 然后  呵呵呵?

//不不不,結果出人意料的是:

//pool-1-thread-1__--->哈哈哈

//pool-1-thread-1__--->哈哈哈

是以上代碼對 threadLocal 的修改沒有成功嗎?不,其實修改是成功了,但是由於線程池為了減少線程創建的開支,對使用完畢的線程並沒有立即銷毀。而是繼續返還到了線程池中,所以我們下次使用線程的時候。並不會重新創建一個新的線程,也就是不會執行線程初始化的init方法。也有同學會問。之前初始化InheritableThreadLocal的時候不是引用傳遞嗎?為什么修改不了?因為你沒有詳細看ThreadLocalMap的創建過程源碼。ThreadLocalMap只是復制了一份新的數據,並沒有直接使用父線程的InheritableThreadLocal。

出自程序猿本性,出了問題一定要去解決問題。那我們怎么去解決這個問題呢?不用緊張,已經有人幫我們解決了這個問題。

阿里的 transmittable-thread-local 已經幫我們解決了這個問題。

使用方式以及實現原理想知道的同學可以自己去研究。

熱愛編程的朋友可以加入QQ技術交流群:1026659175


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM