【多线程】父子线程共享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 已经帮我们解决了这个问题。

使用方式以及实现原理想知道的同学可以自己去研究。

猜你喜欢

转载自www.cnblogs.com/lyp-make/p/12964517.html