JAVA线程如何通过ThreadLocal共享数据?

5 4 月, 2022 176点热度 0人点赞 0条评论

在JAVA开发过程中, 我们经常会使用到ThreadLocal类,该类主要用于存储于线程相关的数据,并且数据只能够通过线程获取。其他线程是无法拿到数据的。但是有这么一个场景,父线程创建了一个子线程,希望子线程能够共享父线程ThreadLocal中的变量数据,这应该怎么做呢?

InheritableThreadLocal

在JAVA中,有InheritableThreadLocal这个类,该类根据名称就可以知道,其实就是可继承的ThreadLocal. 下面我们通过实例的方式查看该类应该怎么使用。

public class InheritThreadLocalTest {

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

        // 父线程插入数据
        Thread parent = new Thread("父线程") {
            @Override
            public void run() {
                local.set("父线程内容");
                inheritableThreadLocal.set("可继承的变量信息...");
                Thread child = new Thread("子线程") {
                    @Override
                    public void run() {
                        String r = local.get();
                        System.out.println("子线程读取父线程内容: " + r);
                        System.out.println("子线程读取可继承变量: " + inheritableThreadLocal.get());

                        inheritableThreadLocal.set("子线程内容");
                    }
                };

                child.start();
                try {
                    child.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ": " + inheritableThreadLocal.get());
            }
        };
        parent.start();
    }
}

在上面的程序中,主要是在Thread内部又创建了一个Thread,一次来形成了一个父子关系。我们将上面的程序跑起来, 看看结果会是什么样子:

子线程读取父线程内容: null
子线程读取可继承变量: 可继承的变量信息...
父线程: 可继承的变量信息...

以上程序主要做了三个事情:

  • 在父线程中往ThreadLocal与InheriableThreadLocal中设置绑定值
  • 在子线程中分别从ThreadLocal与InheriableThreadLocal中获取父线程绑定的值
  • 在子线程中向InheriableThreadLocal设置值,并由父线程获取值

通过结果可以看出, ThreadLocal对变量是隔离的,线程间的数据是不能共享的。而InheriableThreadLocal却可以在父子线程这种模式能够共享数据。下面我们查看在创建Thread的时候,代码里面做了什么样的事情。

Thread源码分析

public Thread(String name) {
        init(null, null, name, 0);
    }

// 执行线程初始化
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) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();

        /*
         * Do we have the required permissions?
         */
        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);
        // 判断是否集成父线程的threadlocal并且父线程的inheriablethreadlocal不为空, 则将父线程的threadlocal中的值
        // 拷贝到子线程内部。
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

ThreadLocal.createInheritedMap

该方法主要是实现了将InheriableThreadLocal中的数据拷贝到新的InheriableThreadLocal中,因此我们看看这段代码的而实现:

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

private ThreadLocalMap(ThreadLocalMap parentMap) {
            // 获取父线程map中的所有entry列表
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            // 创建新的entry列表
            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);
                        // 重新生成entry
                        Entry c = new Entry(key, value);
                        // 计算hash值
                        int h = key.threadLocalHashCode & (len - 1);
                        // 当存在有hash值的时候, 则重新生成hash,知道没有冲突位置
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        // 存入entry
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

通过上面的代码分析可以得知, 在创建子线程的时候,其实会从父线程中拷贝inheriableThreadLocal中的数据。但是这里有个点需要注意,这个拷贝只是做了浅拷贝,并不能保证数据的一致性和原子性。因此当我们ThreadLocal中存入的是一个可变对象的时候,很可能会造成数据不一致。

数据不一致的例子?

public class InheritThreadLocalTest2 {

    public static void main(String[] args) {
        ThreadLocal<String> local = new ThreadLocal();
        InheritableThreadLocal<BindValue> inheritableThreadLocal = new InheritableThreadLocal<>();

        // 父线程插入数据
        Thread parent = new Thread("父线程") {
            @Override
            public void run() {
                local.set("父线程内容");
                BindValue bindValue = new BindValue();
                bindValue.setName("父线程内容");
                bindValue.setAge(23);
                inheritableThreadLocal.set(bindValue);

                Thread child = new Thread("子线程") {
                    @Override
                    public void run() {
                        String r = local.get();
                        System.out.println("子线程读取父线程内容: " + r);
                        System.out.println("子线程读取可继承变量: " + inheritableThreadLocal.get());

                        BindValue childVal = inheritableThreadLocal.get();
                        childVal.setName("子线程内容");
                        inheritableThreadLocal.set(childVal);
                    }
                };

                child.start();
                try {
                    child.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ": " + inheritableThreadLocal.get());
            }
        };
        parent.start();
    }

    @Data
    static class BindValue {
        private String name;
        private Integer age;
    }
}

我们将上面的代码再次跑一次, 查看输出的结果:

子线程读取父线程内容: null
子线程读取可继承变量: InheritThreadLocalTest2.BindValue(name=父线程内容, age=23)
父线程: InheritThreadLocalTest2.BindValue(name=子线程内容, age=23)

我们可以发现, 父线程读取的内容与子线程读取的内容是不一致的,因为子线程在执行的过程中修改了ThreadLocal中存储的值, 因为InheriableThreadLocal是属于浅拷贝,因此子线程的修改数据会对父线程的内容产生影响

怎么解决?

那以上的问题怎么解决内,我觉得至少有两种方式可以解决:

  • 如果父子线程在没有产生数据共享的时候,可以采用ThreadLocal替代InheriableThreadLocal
  • 如果父子线程需要通过ThreadLocal进行共享数据,而父线程不希望子线程修改父线程的数据,此时我们可以将BindValue设计成为不可变类。此时如果子线程需要修改ThreadLocal中的值时,就需要重新创建新的BindValue, 一次来避免互相影响。

 

 

专注着

一个奋斗在编程路上的小伙

文章评论