欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java面试题必备知识之ThreadLocal

程序员文章站 2023-10-16 20:21:28
老套路,先列举下关于ThreadLocal常见的疑问,希望可以通过这篇学习笔记来解决这几个问题: 1. ThreadLocal是用来解决什么问题的? 2. 如何使用ThreadLocal? 3. ThreadLocal的实现原理是什么? 4. 可否举几个实际项目中使用ThreadLocal的案例? ......

Java面试题必备知识之ThreadLocal

老套路,先列举下关于threadlocal常见的疑问,希望可以通过这篇学习笔记来解决这几个问题:

  1. threadlocal是用来解决什么问题的?
  2. 如何使用threadlocal?
  3. threadlocal的实现原理是什么?
  4. 可否举几个实际项目中使用threadlocal的案例?

基础知识

threadlocal是线程局部变量,和普通变量的不同在于:每个线程持有这个变量的一个副本,可以独立修改(set方法)和访问(get方法)这个变量,并且线程之间不会发生冲突。

类中定义的threadlocal实例一般会被private static修饰,这样可以让threadlocal实例的状态和thread绑定在一起,业务上,一般用threadlocal包装一些业务id(user id或事务id)——不同的线程使用的id是不相同的。

如何使用

case1

从某个角度来看,threadlocal为java并发编程提供了额外的思路——避免并发,如果某个对象本身是非线程安全的,但是你想实现多线程同步访问的效果,例如simpledateformat,你可以使用threadlocal变量。

public class foo
{
    // simpledateformat is not thread-safe, so give one to each thread
    private static final threadlocal<simpledateformat> formatter = new threadlocal<simpledateformat>(){
        @override
        protected simpledateformat initialvalue()
        {
            return new simpledateformat("yyyymmdd hhmm");
        }
    };

    public string formatit(date date)
    {
        return formatter.get().format(date);
    }
}

注意,这里针对每个线程只需要初始化一次simpledateformat对象,其实跟在自定义线程中定义一个simpledateformat成员变量,并在线程初始化的时候new这个对象,效果是一样的,只是这样看起来代码更规整。

case2

之前在yunos做酷盘项目的数据迁移时,我们需要按照用户维度去加锁,每个线程在处理迁移之前,都需要先获取当前用户的锁,每个锁的key是带着用户信息的,因此也可以使用threadlocal变量实现:Java面试题必备知识之ThreadLocal

case3

下面这个例子,我们定义了一个myrunnable对象,这个myrunnable对象会被线程1和线程2使用,但是通过内部的threadlocal变量,每个线程访问到的整数都是自己单独的一份。

package org.java.learn.concurrent.threadlocal;

/**
 * @author duqi
 * @createtime 2018-12-29 23:25
 **/
public class threadlocalexample {
    public static class myrunnable implements runnable {

        private threadlocal<integer> threadlocal =
                new threadlocal<integer>();

        @override
        public void run() {
            threadlocal.set((int) (math.random() * 100d));

            try {
                thread.sleep(2000);
            } catch (interruptedexception e) {
            }

            system.out.println(threadlocal.get());
        }
    }


    public static void main(string[] args) throws interruptedexception {
        myrunnable sharedrunnableinstance = new myrunnable();

        thread thread1 = new thread(sharedrunnableinstance);
        thread thread2 = new thread(sharedrunnableinstance);

        thread1.start();
        thread2.start();

        thread1.join(); //wait for thread 1 to terminate
        thread2.join(); //wait for thread 2 to terminate
    }
}

threadlocal关键知识点

源码分析

threadlocal是如何被线程使用的?原理如下图所示:thread引用和threadlocal引用都在栈上,thread引用会引用一个threadlocalmap对象,这个map中的key是threadlocal对象(使用weakreference包装),value是业务上变量的值。Java面试题必备知识之ThreadLocal

首先看java.lang.thread中的代码:

public
class thread implements runnable {
    //......其他源码
    /* threadlocal values pertaining to this thread. this map is maintained by the threadlocal class. */
    threadlocal.threadlocalmap threadlocals = null;

    /*
     * inheritablethreadlocal values pertaining to this thread. this map is maintained by the inheritablethreadlocal class.
     */
    threadlocal.threadlocalmap inheritablethreadlocals = null;
    //......其他源码

thread中的threadlocals变量指向的是一个map,这个map就是threadlocal.threadlocalmap,里面存放的是跟当前线程绑定的threadlocal变量;inheritablethreadlocals的作用相同,里面也是存放的threadlocal变量,但是存放的是从当前线程的父线程继承过来的threadlocal变量。

在看java.lang.threadlocal类,主要的成员和接口如下:Java面试题必备知识之ThreadLocal

  1. withinitial方法,java 8以后用于初始化threadlocal的一种方法,在外部调用get()方法的时候,会通过supplier确定变量的初始值;

    public static <s> threadlocal<s> withinitial(supplier<? extends s> supplier) {
        return new suppliedthreadlocal<>(supplier);
    }
  2. get方法,获取当前线程的变量副本,如果当前线程还没有创建该变量的副本,则需要通过调用initialvalue方法来设置初始值;get方法的源代码如下,首先通过当前线程获取当前线程对应的map,如果map不为空,则从map中取出对应的entry,然后取出对应的值;如果map为空,则调用setinitialvalue设置初始值;如果map不为空,当前threadlocal实例对应的entry为空,则也需要设置初始值。

    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;
            }
        }
        return setinitialvalue();
    }
  3. set方法,跟get方法一样,先获取当前线程对应的map,如果map为空,则调用createmap创建map,否则将变量的值放入map——key为当前这个threadlocal对象,value为变量的值。

    public void set(t value) {
        thread t = thread.currentthread();
        threadlocalmap map = getmap(t);
        if (map != null)
            map.set(this, value);
        else
            createmap(t, value);
    }
  4. remove方法,删除当前线程绑定的这个副本

         public void remove() {
             threadlocalmap m = getmap(thread.currentthread());
             if (m != null)
                 m.remove(this);
         }
  5. 数字0x61c88647,这个值是hash_increment的值,普通的hashmap是使用链表来处理冲突的,但是threadlocalmap是使用线性探测法来处理冲突的,hash_increment就是每次增加的步长,根据参考资料1所说,选择这个数字是为了让冲突概率最小。

    /**
     * the difference between successively generated hash codes - turns
     * implicit sequential thread-local ids into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int hash_increment = 0x61c88647;

父子进程数据共享

inheritablethreadlocal主要用于子线程创建时,需要自动继承父线程的threadlocal变量,实现子线程访问父线程的threadlocal变量。inheritablethreadlocal继承了threadlocal,并重写了childvalue、getmap、createmap三个方法。

public class inheritablethreadlocal<t> extends threadlocal<t> {
    /**
     * 创建线程的时候,如果需要继承且父线程中thread-local变量,则需要将父线程中的threadlocal变量一次拷贝过来。
     */
    protected t childvalue(t parentvalue) {
        return parentvalue;
    }

    /**
    * 由于重写了getmap,所以在操作inheritablethreadlocal变量的时候,将只操作thread类中的inheritablethreadlocals变量,与threadlocals变量没有关系
    **/
    threadlocalmap getmap(thread t) {
       return t.inheritablethreadlocals;
    }

    /**
     * 跟getmap类似,set或getinheritablethreadlocal变量的时候,将只操作thread类中的inheritablethreadlocals变量
     */
    void createmap(thread t, t firstvalue) {
        t.inheritablethreadlocals = new threadlocalmap(this, firstvalue);
    }
}

关于childvalue多说两句,拷贝是如何发生的?
首先看thread.init方法,

    private void init(threadgroup g, runnable target, string name, long stacksize, accesscontrolcontext acc, boolean inheritthreadlocals) {
        //其他源码
        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方法,最终会调用到newthreadlocalmap方法,这里inheritablethreadlocal对childvalue做了重写,可以看出,这里确实是将父线程关联的threadlocalmap中的内容依次拷贝到子线程的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);
                        entry c = new entry(key, value);
                        int h = key.threadlocalhashcode & (len - 1);
                        while (table[h] != null)
                            h = nextindex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

threadlocal对象何时被回收?

threadlocalmap中的key是threadlocal对象,然后threadlocal对象时被weakreference包装的,这样当没有强引用指向该threadlocal对象之后,或者说map中的threadlocal对象被判定为弱引用可达时,就会在垃圾收集中被回收掉。看下entry的定义:

 static class entry extends weakreference<threadlocal<?>> {
     /** the value associated with this threadlocal. */
     object value;

     entry(threadlocal<?> k, object v) {
         super(k);
         value = v;
     }
}

threadlocal和线程池一起使用?

threadlocal对象的生命周期跟线程的生命周期一样长,那么如果将threadlocal对象和线程池一起使用,就可能会遇到这种情况:一个线程的threadlocal对象会和其他线程的threadlocal对象串掉,一般不建议将两者一起使用。

案例学习

dubbo中对threadlocal的使用

我从dubbo中找到了threadlocal的例子,它主要是用在请求缓存的场景,具体代码如下:

@activate(group = {constants.consumer, constants.provider}, value = constants.cache_key)
public class cachefilter implements filter {

    private cachefactory cachefactory;

    public void setcachefactory(cachefactory cachefactory) {
        this.cachefactory = cachefactory;
    }

    @override
    public result invoke(invoker<?> invoker, invocation invocation) throws rpcexception {
        if (cachefactory != null && configutils.isnotempty(invoker.geturl().getmethodparameter(invocation.getmethodname(), constants.cache_key))) {
            cache cache = cachefactory.getcache(invoker.geturl(), invocation);
            if (cache != null) {
                string key = stringutils.toargumentstring(invocation.getarguments());
                object value = cache.get(key);
                if (value != null) {
                    if (value instanceof valuewrapper) {
                        return new rpcresult(((valuewrapper)value).get());
                    } else {
                        return new rpcresult(value);
                    }
                }
                result result = invoker.invoke(invocation);
                if (!result.hasexception()) {
                    cache.put(key, new valuewrapper(result.getvalue()));
                }
                return result;
            }
        }
        return invoker.invoke(invocation);
    }

可以看出,在rpc调用(invoke)的链路上,会先使用请求参数判断当前线程是否刚刚发起过同样参数的调用——这个调用会使用threadlocalcache保存起来。具体的看,threadlocalcache的实现如下:

package org.apache.dubbo.cache.support.threadlocal;

import org.apache.dubbo.cache.cache;
import org.apache.dubbo.common.url;

import java.util.hashmap;
import java.util.map;

/**
 * threadlocalcache
 */
public class threadlocalcache implements cache {

    //threadlocal里存放的是参数到结果的映射
    private final threadlocal<map<object, object>> store;

    public threadlocalcache(url url) {
        this.store = new threadlocal<map<object, object>>() {
            @override
            protected map<object, object> initialvalue() {
                return new hashmap<object, object>();
            }
        };
    }

    @override
    public void put(object key, object value) {
        store.get().put(key, value);
    }

    @override
    public object get(object key) {
        return store.get().get(key);
    }

}

rocketmq

在rocketmq中,我也找到了threadlocal的身影,它是用在消息发送的场景,mqclientapiimpl是rmq中负责将消息发送到服务端的实现,其中有一个步骤需要选择一个具体的队列,选择具体的队列的时候,不同的线程有自己负责的index值,这里使用了threadlocal的机制,可以看下threadlocalindex的实现:

package org.apache.rocketmq.client.common;

import java.util.random;

public class threadlocalindex {
    private final threadlocal<integer> threadlocalindex = new threadlocal<integer>();
    private final random random = new random();

    public int getandincrement() {
        integer index = this.threadlocalindex.get();
        if (null == index) {
            index = math.abs(random.nextint());
            if (index < 0)
                index = 0;
            this.threadlocalindex.set(index);
        }

        index = math.abs(index + 1);
        if (index < 0)
            index = 0;

        this.threadlocalindex.set(index);
        return index;
    }

    @override
    public string tostring() {
        return "threadlocalindex{" +
            "threadlocalindex=" + threadlocalindex.get() +
            '}';
    }
}

总结

这篇文章主要是解决了关于threadlocal的几个问题:(1)具体的概念是啥?(2)在java开发中的什么场景下使用?(3)threadlocal的实现原理是怎样的?(4)开源项目中有哪些案例可以参考?不知道你是否对这几个问题有了一定的了解呢?如果还有疑问,欢迎交流。

参考资料

  1. why 0x61c88647?
  2. java threadlocal
  3. when and how should i use a threadlocal variable?
  4. 技术小黑屋:理解java中的threadlocal
  5. 深入分析threadlocal的内存泄漏问题
  6. 《java并发编程实战》
  7. inheritablethreadlocal详解
  8. threadlocal详解
  9. threadlocal的使用场景
  10. 数据结构:哈希表

本号专注于后端技术、jvm问题排查和优化、java面试题、个人成长和自我管理等主题,为读者提供一线开发者的工作和成长经验,期待你能在这里有所收获。
Java面试题必备知识之ThreadLocal