Java多线程(一)-了解ThreadLocal

一、什么是ThreadLocal

  ThreadLocal被大多数人叫线程本地变量,ThreadLocal为变量在每个线程中都创建了一个副本,每个线程可以访问自己内部的副本变量。

二、使用ThreadLocal与ThreadLocal是如何实现的

  下面看一段代码例子。

public class ThreadLocalDemo {

    ThreadLocal<String> stringThreadLocal = new ThreadLocal<String>();

    public String get() {
        return stringThreadLocal.get();
    }

    public void set() {
        stringThreadLocal.set("线程id:"+Thread.currentThread().getId()+",线程名:"+Thread.currentThread().getName());
    }

    public static void main(String[] args) throws InterruptedException {
        final ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();

        //为threadLocal赋值
        threadLocalDemo.set();

        //输出
        System.out.println(threadLocalDemo.get());

        Thread thread = new Thread(){
            @Override
            public void run() {
                //在新的线程中为threadLocal赋值
                threadLocalDemo.set();
                //输出
                System.out.println(threadLocalDemo.get());
            }
        };
        thread.start();
        thread.join();

        //输出
        System.out.println(threadLocalDemo.get());


    }
}

如果stringThreadLocal就是正常的String变量的话在第一次输出的时候应该输出

 线程id:1,线程名:main

第二三次输出的时候应该输出的都是新创建线程的线程id与线程名
那么我们来看一下实际结果


线程id:1,线程名:main
线程id:12,线程名:Thread-0
线程id:1,线程名:main


执行后我们发现第三次输出的还是主线程的id与线程名,也就是说ThreadLocal起到了作用,他在新的线程中创建了一个变量副本,而没有去修改主线程中的变量。
那么ThreadLocal是如何做到的呢,首先我们先看一下ThreadLocal提供的方法。

//get()是用来获取ThreadLocal在当前线程中保存的副本
public T get() { }
//set() 是用来设置当前线程中的变量副本
public void set(T value) { }
//remove() 用来清除当前线程中的变量副本
public void remove() { }
// initialValue() 这个方法是需要使用的时候实现,具体的作用会在讲解get方法时说到
protected T initialValue() { }

首先我们先来看看get()方法的实现。

/**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    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();
    }

这段代码首先获取了当前线程,随后调用了一个叫做getMap()的方法,获取类一个类型为ThreadLocalMap的Map,随后通过传入this,获取到键值对,如果成功获取到键值对则返回value,如果没有获取到map或键值对e都会去调用setInitialValue()方法。

在概括的说明了get方法之后,我们从上到下看一下get方法中所调用的方法都干了些什么。

首先是getMap(),我们去看一下ThreadLocalMap是从哪里获得的。

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

令人震惊的是他直接返回了当前线程的一个成员变量threadLocals,那我们再去看一下Thread类中的成员变量。

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

在查看了成员变量threadLocals后我们发现Thread中的成员变量threadLocals就是一个ThreadLocalMap,而这个类型是一个ThreadLocal的内部类。

    static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

ThreadLocalMap的Entry继承了WeakReference,并使用了ThreadLocal作为键值。

那么看完了getMap(),我们继续来看setInitialValue()

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

这里就用到了之前说过的需要自己进行实现的方法initialValue(),咱们先不管他只需要知道他给我们返回了一个泛型T就可以了,之后的步骤就很容易了解了,与之前说的一样先去获得当前线程然后getMap(),判断map是否为null,如果为null创建并存入一个一个键值对,如果不为null,直接向map中存入一套键值对。

看完整个setInitialValue()方法,大家会发现initialValue()返回的value实际上就是存入键值对时的value,那我们去看一下没有重写的情况下initialValue()返回的是什么。

    protected T initialValue() {
        return null;
    }

emmmmm,就是孤零零的一个null。

按照我们刚才的理解如果不事先进行set(),直接get()的话应该会返回给我们一个null,如果我们自己重写了initialValue()方法那么就会按照我们重写的方法返回给我们对应的对象,那现在我们来试一试。

首先我们去掉所有的set()执行一遍试试,按照预期的想法他应该会输出null。

public class ThreadLocalDemo {

    ThreadLocal<String> stringThreadLocal = new ThreadLocal<String>();

    public String get() {
        return stringThreadLocal.get();
    }

    public void set() {
        stringThreadLocal.set("线程id:"+Thread.currentThread().getId()+",线程名:"+Thread.currentThread().getName());
    }

    public static void main(String[] args) throws InterruptedException {
        final ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();

        //为threadLocal赋值
        //threadLocalDemo.set();

        //输出
        System.out.println(threadLocalDemo.get());

        Thread thread = new Thread(){
            @Override
            public void run() {
                //在新的线程中为threadLocal赋值
               // threadLocalDemo.set();
                //输出
                System.out.println(threadLocalDemo.get());
            }
        };
        thread.start();
        thread.join();

        //输出
        System.out.println(threadLocalDemo.get());


    }
}

下面看一下执行结果:

null
null
null

嗯,和预期的结果一样都是null,那下面我们来重写initialValue()方法试一下。

    ThreadLocal<String> stringThreadLocal = new ThreadLocal<String>(){
        @Override
        protected String initialValue() {
            return "线程id:"+Thread.currentThread().getId()+",线程名:"+Thread.currentThread().getName();
        }
    };

看一下执行结果:

线程id:1,线程名:main
线程id:12,线程名:Thread-0
线程id:1,线程名:main

和分析的结果一样,重写了initialValue()方法后,不需要我们去set()直接调用get()方法也能获取到我们想要的内容。

三、withInitial(Supplier<? extends S> supplier)

  如果你不愿意去重写initialValue()方法,java8也为你提供了新的方案,通过函数式编程的方式来生成一个ThreadLocal,多说无益,直接修改一下我们的例子。

    ThreadLocal<String> stringThreadLocal = ThreadLocal.
            withInitial(()->"线程id:"+Thread.currentThread().getId()+",线程名:"+Thread.currentThread().getName());

修改后执行结果:

线程id:1,线程名:main
线程id:12,线程名:Thread-0
线程id:1,线程名:main

与之前的代码一致。

四、总结

  1. 通过ThreadLocal创建的副本是保存在每个Thread中自己的成员变量threadLocals中的。
  2. ThreadLocalMap的键使用ThreadLocal对象是因为每个线程中可能有许多ThreadLocal变量。
  3. 如果不希望调用set()方法请重写initialValue()方法,或通过withInitial()创建ThreadLocal对象,否则直接调用get()方法返回为null。
  4. 如果想在get()之前不需要调用set()的话,必须重写initialValue()方法,或通过withInitial()创建ThreadLocal对象。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值