ThreadLocal讲解

简介

一般情况下,我们创建的变量是可以被任意一个线程访问和修改的,但是有时候我们需要每个线程中都含有一个属于自己的变量,每个线程中的get和set方法不会影响到其他线程,这个时候就需要用到JDK提供的ThreadLocal类,ThreadLocal类主要解决的就是让每个线程都绑定一个自己的值,防止线程对可变的单实例变量或全局变量进行共享,实现了线程封闭

ThreadLocal实例一

该例子出自《Java并发编程实战》,例举JDBC的Connection对象。
在典型的服务器应用程序中,线程从连接池中获得一个获得一个Connection对象,并且用该对象处理请求,使用完后再将对象返还给连接池。如果是在单线程应用中,为了避免每一次调用方法都要传递一个Connection对象,通常会在程序启动时就初始化这个连接对象。由于JDBC的连接对象不一定是线程安全的,因此,当多线程应用程序在没有协同的情况下使用这个全局变量时,就不是线程安全的。

通过将JDBC的连接保存在ThreadLocal对象中,每个线程都会拥有属于自己的连接,代码如下。

	private static ThreadLocal<Connection> connectionHolder = 
            new ThreadLocal<Connection>() {
                public Connection initialValue(){
                    return DriverManager.getConnection(DB_URL);
                }
            };
    
    public static Connection getConnection(){
        return connectionHolder.get();
    }

当某个线程初次调用getConnection()时,就会调用ThreadLocal的initialValue()来获得初始值,你可以大致将ThreadLocal< T>视为包含了Map< Thread,T>对象,其中保存了特定于该线程的值,当线程终止后,这些值会作为垃圾回收,后面分析源码再详细讲解。

ThreadLocal实例二

相信经过上面的例子,大家应该都知道ThreadLocal类是个什么东西了,下面再来一个例子,
首先,我们要知道SimpleDateFormat不是线程安全的,至于为什么有兴趣的童鞋可以去查一查,下面上代码

  • 正常情况下使用SimpleDateFormat
public class ThreadLocalTest2 implements Runnable{
    private static SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd HHmm");

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalTest2 obj = new ThreadLocalTest2();
        for (int i = 0;i < 10;i++){
            Thread t = new Thread(obj, "" + i);
            Thread.sleep(new Random().nextInt(1200));
            t.start();
        }
    }

    @Override
    public void run() {
        System.out.println("Thread Name=" + Thread.currentThread().getName() + " default Formatter " + formatter.toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //在该线程中改名formatter的pattern
        formatter = new SimpleDateFormat();
        System.out.println("Thread Name=" + Thread.currentThread().getName() + " changed Formatter " + formatter.toPattern());
    }
}

在这里插入图片描述
通过创建10个线程去改变全局变量SimpleDateFormat,可以看到,当第一个线程改变了SimpleDateFormat的pattern后,其他线程的pattarn也被改变了。这显然不是我们希望看到的。

  • 通过ThreadLocal创建线程安全的SimpleDateFormat
public class ThreadLocalTest implements Runnable{
    private static final ThreadLocal<SimpleDateFormat> formatter
            = new ThreadLocal<SimpleDateFormat>(){
        public SimpleDateFormat initialValue(){
            return new SimpleDateFormat("yyyyMMdd HHmm");
        }
    };

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalTest obj = new ThreadLocalTest();
        for (int i = 0;i < 10;i++){
            Thread t = new Thread(obj, "" + i);
            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }
    }

    @Override
    public void run() {
        System.out.println("Thread Name=" + Thread.currentThread().getName() + " default Formatter " + formatter.get().toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //在该线程中改名formatter的pattern
        formatter.set(new SimpleDateFormat());
        System.out.println("Thread Name=" + Thread.currentThread().getName() + " changed Formatter " + formatter.get().toPattern());
    }
}

在这里插入图片描述
同样是创建10个线程去改变共享变量SimpleDateFormat的值,可以看到,虽然0号线程改变了pattern,但其他线程并没有改变。

源码分析

Thread源代码入手

public class Thread implements Runnable {
		//与此线程有关的ThreadLocal值。由ThreadLocal类维护
		ThreadLocal.ThreadLocalMap threadLocals = null;
		//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
		ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

从上面的代码中,我们可以看到,Thread类维护了两个ThreadLocal的内部类ThreadLocalMap,且都是默认为null。那么这个ThreadLocalMap到底是什么东西?我们可以把它理解为是一个为ThreaLocal类定制化的hashMap(不在本篇讲解),其中的key就是ThreadLocal,而value是一个Object

static class ThreadLocalMap {
		....
		//可以看到,它的构造器参数一个是ThreadLocal,一个是Object
		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);
        }
}

到现在为止,我们理解了每个线程都维护着一个ThreadLocalMap,这个Map可以理解为是一个存放以key为ThreadLocal,value为ObjecthashMap

那么什么时候对这个map进行存取呢?我们来看一下ThreadLocal的几个主要方法。

	//保存当前线程的副本变量
	public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        //判断当前线程的ThreadLocalMap初始化没有
        if (map != null)
        	//在map中再插入一个副本变量
            map.set(this, value);
        else
        	//延迟初始化,但要set时才创建ThreadLocalMap
            createMap(t, value);
    }
    
    //返回当前线程的ThreadLocalMap
	ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
	void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
	public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        	//从当前线程中获得现在这个ThreadLocal的副本变量
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    
    //创建一个value为null的entry
    private T setInitialValue() {
        T value = initialValue();//初始化的value为null
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

结论

通过上面那些内容,我们足以通过得出结论:

  • 每个线程的副本变量是存放在当前线程的ThreadLocalMap中,并不是存在ThreadLoca中的,ThreadLocal可以理解为只是一个Thread和ThreadLocalMap的关联量,作为map的key值。
  • ThreadLocal类中通过Thread.currentThread()获取到当前线程对象,再通过当前线程调用getMap(Thread T)访问到当前线程中的ThreadLocalMap对象,该对象中存放这共享数据的副本变量。
  • 同一个线程可以声明多个ThreadLocal对象,map的key的就是ThreadLocal对象,value就是该对象调用set方法设置的值。

ThreadLocal内存泄漏问题

ThreadLocalMap中使用的key为ThreadLocal的弱引用,而value是强引用,如果ThreadLocal在没有外部强引用的情况下,在垃圾回收时,key就会被清理掉,而value不会被清理掉。这样一来,ThreadLocalMap中就会出现key为null的Entry。假如我们不做任何措施的话,value永远无法被GC回收,这个时候就可能会产生内存泄漏。ThreadLocalMap实现中已经考虑了这种情况,再调用remove方法的时候,会清理key为null的记录。所以使用完ThreadLocal方法后,最好手动调用remove方法。

		static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值