切底明白ThreadLocal的原理与使用
阅读原文时间:2021年04月20日阅读:1

ThreadLocal

最近在做Spark项目的时候,使用Scala语言,遇到了多线程的问题,想到之前理解的ThreadLocal的数据隔离的意义,在使用后,以为解决了,但是还是遇到了多线程问题;这是我很困惑,后来在做了几个Demo后,才彻底明白ThreadLocal的意义。

写在前面

以前,在看很多博客的时候,总是有很多作者在描述ThreadLocal的作用是数据隔离,并且是每一个线程复制了一份,每个线程的访问的数据都是不受其他线程影响的。其实,这句话前半句是对的,ThreadLocal的确是数据的隔离,但是并非数据的复制,而是在每一个线程中创建一个新的数据对象,然后每一个线程使用的是不一样的。

ThreadLocal详解

我们首先来看一段代码,读者可以猜一猜这个运行结果是什么。

package cn.edu.hust.constant;

import java.util.Random;

public class Person {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public static void main(String[] args) throws InterruptedException {
        final Person p=new Person();
        //这样做其实就是在操作同一个对象,如果需要实现多线程应该像下下面的注释一样,这样就针对于每一个线程创建一个独立的Person对象
          final ThreadLocal<Person> t=new ThreadLocal<Person>(){
            public Person initialValue() {
            //return new Person();
                return p;
            }
        };

        p.setName("小明");
        for(int i=0;i<3;i++)
        {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    t.set(p);
                    t.get().setName(new Random().nextInt(100)+"");
                    System.out.println(t.get().getName()+"=="+t.get());
                }
            }).start();
        }
        Thread.sleep(1000);
        System.out.println(p.getName());
    }
}


这里给出运行的答案,其实这个程序每一次运行的结果都是不一样的。为什么会这样的?
我们知道,ThreadLocal是数据隔离,如果按照以前的理解,应该是最后的输出一定是不会改变的,也就是“小明”;其实,这里不一样,是因为每一个线程保存的Person对象的地址都是一样的,这里的运行结果可以看出。
如果是这样就是不能实现多线程的共享。下面,我们再来看一份代码:

package cn.edu.hust.constant;

public class TestNum {
    // ①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
    private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {
        public Integer initialValue() {
            return 0;
        }
    };

    // ②获取下一个序列值
    public int getNextNum() {
        seqNum.set(seqNum.get() + 1);
        //System.out.println();
        return seqNum.get();
    }


    public  ThreadLocal<Integer> getThreadLocal()
    {
        return seqNum;
    }

    public static void main(String[] args) {
        TestNum sn = new TestNum();
        // ③ 3个线程共享sn,各自产生序列号
        TestClient t1 = new TestClient(sn);
        TestClient t2 = new TestClient(sn);
        TestClient t3 = new TestClient(sn);
        t1.start();
        t2.start();
        t3.start();
    }

    private static class TestClient extends Thread {
        private TestNum sn;

        public TestClient(TestNum sn) {
            this.sn = sn;
        }

        public void run() {
            for (int i = 0; i < 3; i++) {
                // ④每个线程打出3个序列值
                System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn["
                        + sn.getNextNum() + "]"+sn.getThreadLocal().get().hashCode());
            }
        }
    }
}


这里可以看出,在每一个线程的确是没有互相影响,但是,我们知道,Integer是一个final修饰的类,在每次操作后,都会创建一个新的Integer,每次线程都是会在初始值的基础上创建,也就是每一次的地址都是不一样,然后进行操作。

ThreadLocal数据结构

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;
            }
        }

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

ThreadLocal内部有一个ThreadLocalMap,可以从名字看出,它是一个HashMap,我们在使用的键值对多少保存在这个Map里面。让我们来看一下,ThreadLocal的set()方法:

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

从上面的代码可以看出,这里的方法是以当前线程为Key,程序员自定义的value为值,然后放置在ThreadLocal的内部ThreadLocalMap中。

ThreadLocal在框架中的例子

在著名的框架Hiberante中,我们来看一下数据库连接的代码:

private static final ThreadLocal threadSession = new ThreadLocal();  

public static Session getSession() throws InfrastructureException {  
    Session s = (Session) threadSession.get();  
    try {  
        if (s == null) {  
            s = getSessionFactory().openSession();  
            threadSession.set(s);  
        }  
    } catch (HibernateException ex) {  
        throw new InfrastructureException(ex);  
    }  
    return s;  
}  

在建立每一个数据库会话,都是通过一个Session工厂创建,然后返回给当前线程;这里对于ThreadLocal的使用时,通过SessionFactory创建一个Session的对象,然后设置在ThreadLocal的内部类中ThreadLocalMap中,并发所说的数据复制。

总结

在理解ThreadLocal后,给我的最大感觉是,在看完博客后,一定要自己动手实现,不能听之任之,要真正弄懂,对于ThreadLocal,并非对象的复制,而是针对于每一个线程创建一份新的对象设置在其中。