文章506
标签266
分类65

Java并发编程总结-1: ThreadLocal

有关Java中ThreadLocal相关内容的总结!

ThreadLocal

多个线程访问同一个共享变量时特别容易出现并发问题, 特别是在多个线程同时需要对一个共享变量进行写入时! 为了保证线程安全, 一般使用者在访问共享变量时需要进行适当的同步.

同步的一般措施为加锁. 但是加锁机制较为复杂, 有没有一种方法可以做到:当创建一个变量之后, 每一个线程进行访问时, 访问的是自己线程的变量. 此方法即为ThreadLocal变量!

ThreadLocal由JDK包提供, 提供了线程本地变量, 即:

当你创建了一个ThreadLocal变量, 那么访问这个变量的每一个线程都会有这个变量的一个副本. 当多个线程操作这个变量的时候, 每一个线程操作的都是自己本地内存中的变量. 避免了线程安全问题! 创建一个ThreadLocal后, 每一个线程都会复制一个变量到自己的本地内存.


1. ThreadLocal使用实例

package club.jasonkayzk666.chapter1.lesson11.threadlocal;

public class ThreadLocalTest {

    public static ThreadLocal<String> localVariable = new ThreadLocal<>();

    public static void print(String str) {
        // 1. 打印当前线程本地内存中的localVariable变量的值
        System.out.println(str + ": " + localVariable.get());

        // 2. 清除当前线程本地内存中的localVariable变量
        localVariable.remove();
    }

    public static void main(String[] args) {
        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                localVariable.set("thread-one local variable");
                print("thread-one");

                System.out.println("thread-one remove after: " + localVariable.get());
            }
        });

        Thread threadTwo = new Thread(new Runnable() {
            @Override
            public void run() {
                localVariable.set("thread-two local variable");
                print("thread-two");

                System.out.println("thread-two remove after: " + localVariable.get());
            }
        });

        threadOne.start();
        threadTwo.start();
    }


}

代码中, 线程One通过set方法设置了localVariable的值, *这其实是线程One本地内存中的一个副本! 这个副本线程Two是无法访问的! *

所以输出为:

thread-two: thread-two local variable
thread-two remove after: null
thread-one: thread-one local variable
thread-one remove after: null

2. ThreadLocal的实现原理

下图为ThreadLocal相关类的类图结构.

ThreadLocal相关类的类图结构

在图中可知:

  • Thread类中有一个threadLocals和一个inheritableThreadLocals, 都是ThreadLocalMap类型的变量, 而ThreadLocalMap是一个定制化的HashMap.
  • 默认情况下, 每个线程中的两个变量都为null, 只有当前线程第一次调用ThreadLocalset或者get方法时才会创建他们.
  • 每个线程的本地变量实际上并不是存放在ThreadLocal实例中! 而是存放在调用线程的threadLocals里面, 并存放起来. 当调用线程调用它的get方法时, 再从当前线程的threadLocals变量中将其拿出来使用!
  • 如果调用线程一直不终止, 那么这个变量会一直存放在调用线程的threadLocals变量里面, 占用内存!
  • 所以当不再使用本地变量时, 可以通过ThreadLocal变量的remove方法, 从当前线程的threadLocals里面删除该本地变量!
  • ThreadLocal中的threadLocals被设计为map的原因: 每个线程可以关联多个ThreadVariable变量!

1): void set(T value)

public void set(T value) {
    // 1. 获取当前线程
    Thread t = Thread.currentThread();
    // 2. 将当前线程作为key, 查找对应的线程变量
    ThreadLocalMap map = getMap(t);

    // 3.  如果找到该线程则设置
    if (map != null) {
        map.set(this, value);
    } else {
        // 4. 未找到则说明是第一次, 就创建对应Map
        createMap(t, value);
    }
}

下面再看看createMap(t, value)

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

即创建了一个当前线程的threadLocals变量!

2): T get()

public T get() {
    // 1. 获取当前线程
    Thread t = Thread.currentThread();
    // 2. 获取当前线程的ThreadLocal变量
    ThreadLocalMap map = getMap(t);

    // 3. 如果找到, 则返回对应变量的值
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 当ThreadLocal变量为null, 则初始化当前ThreadLocal变量
    return setInitiaVaule();
}

对于setInitialValue()的代码:

private T setInitialValue() {
    // 1. 初始化为null 
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);

    // 2. 当前的线程ThreadLocal变量不为空, 设置
    if (map != null) {
        map.set(this, value);
    } else {
        // 3. 当前ThreadLocal变量为空
        createMap(t, value);
    }
    return value;
}

protected T initialValue() {
    return null;
}

当前ThreadLocal变量不为空则设置为null, 否则调用createMap方法创建线程变量.

3): void remove()

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null) {
        m.remove(this);
    } 
}

如果当前线程的ThreadLocal变量不为空, 则删除.

4): 总结:

  • 每个线程内部都有一个名为threadLocals的成员变量, 变量类型为HashMap, 其中key为定义的ThreadLocalthis引用, value为使用set设置的值!
  • 每个线程的本地变量存放在线程自己的内存变量threadLocals变量中, 若线程一直不消亡, 本地变量一直存在, 可能造成内存溢出. *所以使用完毕要调用ThreadLocal的remove方法删除本地变量!

3. ThreadLocal不支持继承性

例:

package club.jasonkayzk666.chapter1.lesson11.threadlocal;

public class ThreadLocalNotInherible {

    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {

        threadLocal.set("hello world");

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread: " + threadLocal.get());
            }
        });

        thread.start();

        System.out.println("main: " + threadLocal.get());
    }

}

输出为:

main: hello world
thread: null

本例中创建了一个ThreadLocal变量并在父线程中设置了值hello world, 此值在子线程中无法获取! 这是对的!

但是有时想要子线程访问父线程中的值, 此时可以使用InheritableThreadLocal类


4. InheritableThreadLocal类

InheritableThreadLocal继承自ThreadLocal, 提供了一个特性即: 让子线程可以访问父线程中设置的本地变

1): InheritableThreadLocal的源码:

public class  InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        return parentValue;
    }

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

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

}

上述代码可以看出, InheritableThreadLocal继承了ThreadLocal, 并重写了三个方法;

  • InheritableThreadLocal重写了createMap方法:

    当首次调用set方法时, 创建当前线程的inheritableThreadLocals变量实例, 而不再是threadLocals.

  • 同时调用get方法时, 获取的也是inheritableThreadLocals而不是threadLocals.

下面看下如何让子线程可以访问到父线程的本地变量, 从创建Thread的代码:

public Thread(Runnable target) {
    init(null ,target, "Thread-" + nextThreadNum(), 0);
}

private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc) {
    .....
    // 获取当前线程
    Thread parent = currentThread();
    ...
    // 如果父进程 InheritableThreadLocal变量不为空, 
    if (parent. inheritableThreadLocals != null) {
        this. inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        this.stackSize = stackSize;
        tid = nextThreadID();
    }
}

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

Thread创建的构造函数的源码如上: 在创建线程时, 构造函数会调用init方法. 可以看到, 在createInheritedMap内部使用父进程的inheritableThreadLocals变量, 然后赋予了子线程!

ThreadLocalMap的构造函数将父线程的InheritableThreadLocal变量复制到新的LocalThreadMap对象中!

2): 总结:

InheritableThreadLocal类通过重新方法, 将本地变量保存到了inheritableThreadLocals里面. 线程在通过InheritableThreadLocal类实例的get或者set方法设置变量时会创建当前线程的inheritableThreadLocals变量. 当父线程创建子线程时, 构造函数会把父线程中的inheritableThreadLocals变量里面的本地变量复制一份给子线程.

*例: *

package club.jasonkayzk666.chapter1.lesson11.threadlocal;

public class InheritableThreadLocalDemo {

    private static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        inheritableThreadLocal.set("hello world");

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread: " + inheritableThreadLocal.get());
            }
        });

        thread.start();

        System.out.println("main: " + inheritableThreadLocal.get());
    }
}

输出为:

main: hello world
thread: hello world

3):InheritableThreadLocal使用场景

  • 子线程需要使用存放在threadLocal变量中的用户登录信息.
  • 一些中间件把统一的id追踪的整个调用链路记录下来等

本文作者:Jasonkay
本文链接:https://jasonkayzk.github.io/2019/09/10/Java并发编程总结-1-ThreadLocal/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可