线程安全类的设计——合理使用ThreadLocal

浅步调 2019-10-14 19:10:12
原文地址:https://segmentfault.com/a/1190000020589380

一、ThreadLocal类定义
ThreadLocal,意指线程局部变量,它可以为每一个线程提供一个实例变量的副本,每个线程独立访问和更改自己的副本,从而保证线程之间不会发生变量冲突,是一种通过将共享变量进行线程隔离而实现线程安全的方式。主要方法有以下三个:

T get():返回此线程局部变量中当前线程副本中的值。
void remove():删除此线程局部变量中当前线程的值。
void set(T value):设置此线程局部变量中当前线程副本中的值。

我们可以通过一个简单的实验,来验证ThreadLocal如何保证线程安全:

public class ThreadLocalTest implements Runnable {

    //重写ThreadLocal类的初始化方法
    private ThreadLocal<Integer> i = new ThreadLocal<Integer>(){
        public Integer initialValue(){
            return 0;
        }
    };

    public static void main(String[] args){
        ThreadLocalTest threadLocalTest = new ThreadLocalTest();

        new Thread(threadLocalTest,"st-0").start();
        new Thread(threadLocalTest,"st-1").start();
        new Thread(threadLocalTest,"st-2").start();
    }

    @Override
    public void run() {
        for (;i.get()<100;){
            i.set(i.get() + 1);
            System.out.println(Thread.currentThread().getName() + ": " + i.get());
        }
    }
}

运行以上代码,截取部分实验结果,可以看到每个线程都是从1开始计数,每个线程更改后的变量副本并不会影响到其他线程。

st-0: 1
st-1: 1
st-0: 2
st-1: 2
st-2: 1
st-0: 3
st-2: 2
st-1: 3

二、ThreadLocal类的实现原理
我们可以从java.lang.ThreadLocal类的源码出发,来了解ThreadLocal类的实现原理。

1、ThreadLocal提供了一个ThreadLocalMap类,类的定义如下,不同的ThreadLocal变量在ThreadLocalMap中拥有自己独立的一个键值对,key值为ThreadLocal实例。

    ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }

2、ThreadLocal定义了ThreadLocalMap,但是ThreadLocalMap的引用变量保存在Thread实例中,与每个线程共存亡,一个ThreadLocalMap可以存放多个ThreadLocal变量。

public class Thread implements Runnable {

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

3、再来看get和set方法,可以看到,每个线程拥有自己的ThreadLocalMap,Map中保存自己的ThreadLocal变量,并只对自己的ThreadLocal变量做读和写操作。

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T)e.value;
    }
    return setInitialValue();
}

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

三、线程池模式下ThreadLocal的表现
每个线程占用的资源在运行结束后均会被JVM回收,但是在线程池模式下,线程结束后并不会立即死亡,而是归还到线程池成为空闲线程,等待下一次调用。如果在线程池模式下使用ThreadLocal,由于线程结束后,ThreadLocalMap变量并没有被回收,所以线程池在下次处理同个ThreadLocal变量时,有可能会有冲突。

1、不使用线程池时,ThreadLocal的表现:

    public class ThreadLocalTest2 implements Runnable {

    //重写ThreadLocal类的初始化方法
    private ThreadLocal<Integer> i = new ThreadLocal<Integer>(){
        public Integer initialValue(){
            return 0;
        }
    };

    public static void main(String[] args){
        ThreadLocalTest2 threadLocalTest = new ThreadLocalTest2();

        Thread t1 = new Thread(threadLocalTest,"st-1");
        Thread t2 = new Thread(threadLocalTest,"st-2");

        t1.start();

        while (t1.isAlive());

        if (!t1.isAlive()) {
            System.out.println("t1 is dead");
            t2.start();
        }
    }

    @Override
    public void run() {
        for (;i.get()<100;){
            i.set(i.get() + 1);
            System.out.println(Thread.currentThread().getName() + ": " + i.get());
        }

        i.set(50);
    }
}

运行结果:

st-1: 96
st-1: 97
st-1: 98
st-1: 99
st-1: 100
t1 is dead
st-2: 1
st-2: 2
st-2: 3
st-2: 4

t2线程在t1线程结束后开始启动,可以发现,i变量仍从0开始,t1线程的运行不会对t2线程造成影响。

2、使用线程池时,ThreadLocal的表现:

public class ThreadPoolTest implements Runnable{

    //重写ThreadLocal类的初始化方法
    private ThreadLocal<Integer> i = new ThreadLocal<Integer>(){
        public Integer initialValue(){
            return 0;
        }
    };

    public static void main(String[] args) throws Exception{
        ExecutorService pool = Executors.newFixedThreadPool(1);
        ThreadPoolTest threadPoolTest = new ThreadPoolTest();

        Thread t1 = new Thread(threadPoolTest,"st-1");
        Thread t2 = new Thread(threadPoolTest,"st-2");

        Future<Integer> result = pool.submit(t1,1);
        if (result.get() ==  1){
            System.out.println("t1 runs off");
            pool.submit(t2);
        }
    }

    @Override
    public void run() {
        for (;i.get()<100;){
            i.set(i.get() + 1);
            System.out.println(Thread.currentThread().getName() + ": " + i.get());
        }

        i.set(50);
    }
}

运行结果:

pool-1-thread-1: 96
pool-1-thread-1: 97
pool-1-thread-1: 98
pool-1-thread-1: 99
pool-1-thread-1: 100
t1 runs off
pool-1-thread-1: 51
pool-1-thread-1: 52
pool-1-thread-1: 53
pool-1-thread-1: 54

我们发现,t1线程运行结束之后,t2线程的i变量从50开始,也就是说t1线程对ThreadLocal变量的更改,影响到了t2线程的读取。此时ThreadLocal并不能保证数据的隔离性和安全性,所以在线程池模式下,需慎重考虑用ThreadLocal实现线程安全的方式,可以在每次线程结束后,调用remove方法将key释放。

四、总结
ThreadLocal可以有效隔离多个线程访问共享变量的冲突,但不适用于多个线程通过共享数据进行通信的场景。在线程池模式下,ThreadLocal不仅会造成数据的冲突,而且有可能在线程池长时间运行时,ThreadLocal变量长期存活在内存中,导致大量的内存消耗,故需慎重考虑两者并存的场景。

声明:该文章系转载,转载该文章的目的在于更广泛的传递信息,并不代表本网站赞同其观点,文章内容仅供参考。

本站是一个个人学习和交流平台,网站上部分文章为网站管理员和网友从相关媒体转载而来,并不用于任何商业目的,内容为作者个人观点, 并不代表本网站赞同其观点和对其真实性负责。

我们已经尽可能的对作者和来源进行了通告,但是可能由于能力有限或疏忽,导致作者和来源有误,亦可能您并不期望您的作品在我们的网站上发布。我们为这些问题向您致歉,如果您在我站上发现此类问题,请及时联系我们,我们将根据您的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。